hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -11,7 +11,7 @@ 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
14
+ # Wraps `subject` so a guard can read a declared-but-storage-absent
15
15
  # optional attribute as nil, not "cannot resolve." `Instance
16
16
  # #hydrate_with_defaults` deliberately leaves one absent rather
17
17
  # than nil-filled — "an attribute with no default stays absent,
@@ -25,10 +25,10 @@ module Hecks
25
25
  # exact behavior, zero risk to a path this bug was never measured
26
26
  # against.
27
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
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
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,
31
+ # non-optional field predating a record read nil the same way,
32
32
  # which is the `ne:`/array-`in:` bug class applied to rule
33
33
  # evaluation: a predicate silently answers against a value nobody
34
34
  # ever wrote (ADR 0025, "Added attributes and absence"). `[]`
@@ -37,17 +37,19 @@ module Hecks
37
37
  # identifying the field, so a rule that reads it fails loud
38
38
  # instead of quietly wrong.
39
39
  class GuardState
40
+ # @param instance [Runtime::Instance] the record a `given`/`ensures`/
41
+ # `invariant` rule reads state from
40
42
  def initialize(instance)
41
43
  @instance = instance
42
44
  @declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
43
- # S12, ADR 0025 — a SEPARATE index, the same reason
45
+ # S12, ADR 0025 — a separate index, the same reason
44
46
  # `Aggregate#projected_fields` is a separate IR collection
45
47
  # rather than folded into `attributes` (see that field's
46
48
  # own comment): a projected field's absence means
47
49
  # something different from an ordinary attribute's, so it
48
50
  # needs its own refusal below, not `AttributeAbsent`'s.
49
- # `projects` is AGGREGATE-scoped only — an entity's own
50
- # `instance.aggregate` answers the ENTITY construct here
51
+ # `projects` is aggregate-scoped only — an entity's own
52
+ # `instance.aggregate` answers the entity construct here
51
53
  # (EntityInterpreter's own subject), which declares no
52
54
  # `projected_fields` of its own, hence the extra guard
53
55
  # `@declared` above does not need.
@@ -55,8 +57,23 @@ module Hecks
55
57
  @projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
56
58
  end
57
59
 
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
58
65
  def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
59
66
 
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
60
77
  def [](name)
61
78
  return @instance[name] if @instance.key?(name)
62
79
 
@@ -67,17 +84,17 @@ module Hecks
67
84
  return nil if attribute.nil? || attribute.optional?
68
85
 
69
86
  raise AttributeAbsent,
70
- RefusalWording.render("AttributeAbsent", "absent_read",
71
- aggregate: @instance.aggregate.hecks_name, field: name)
87
+ RefusalWording.render_site("AttributeAbsent", "absent_read",
88
+ aggregate: @instance.aggregate.hecks_name, field: name)
72
89
  end
73
90
 
74
91
  private
75
92
 
76
93
  def raise_projection_absent(projected)
77
94
  raise ProjectionAbsent,
78
- RefusalWording.render("ProjectionAbsent", "absent_read",
79
- aggregate: @instance.aggregate.hecks_name, field: projected.name,
80
- reference: projected.reference, remote_field: projected.remote_field)
95
+ RefusalWording.render_site("ProjectionAbsent", "absent_read",
96
+ aggregate: @instance.aggregate.hecks_name, field: projected.name,
97
+ reference: projected.reference, remote_field: projected.remote_field)
81
98
  end
82
99
  end
83
100
  private_constant :GuardState
@@ -89,54 +106,74 @@ module Hecks
89
106
  # `Instance`); a `subject` with no `.aggregate` just hydrates
90
107
  # nothing from state, same as GuardState degrades above.
91
108
  #
92
- # MERGE ORDER MATTERS, and it is NOT "args always win": an
109
+ # Merge order matters, and it is not "args always win": an
93
110
  # unaliased command-level reference dereferences under a
94
111
  # different name than the argument holds (`account_id` the arg,
95
112
  # `account` the hydrated key — no collision, order is moot). An
96
- # ALIASED one (`reference_to Customer, as: :customer`) hydrates
97
- # under the SAME name the argument itself holds — `customer` is
113
+ # aliased one (`reference_to Customer, as: :customer`) hydrates
114
+ # under the same name the argument itself holds — `customer` is
98
115
  # both the raw id an arg puts there and the key `customer.status`
99
116
  # expects to dig into. If `args` merged last, the raw id (a
100
117
  # String) would win and `.status` on a String is where a fuzzer
101
118
  # found this — TypeError, not a refusal. Command-level
102
- # dereferencing is the one thing that is SUPPOSED to override
119
+ # dereferencing is the one thing that is supposed to override
103
120
  # its own source argument for exactly this reason; args still
104
- # wins over stored OWNER state (unchanged from before this fix).
105
- # `parent:` is an entity command's OWN parent aggregate record
106
- # (EntityInterpreter's `ctx.instance` — "the PARENT aggregate
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
107
124
  # record", its own doc comment) — the entity's containment, not a
108
125
  # declared reference attribute, so it doesn't come from
109
126
  # `dereference`'s attribute scan the way `owner`'s do. Hydrated
110
- # the same shape regardless: the parent's own state, MERGED so
127
+ # the same shape regardless: the parent's own state, merged so
111
128
  # the dereferenced hash wins over the raw reference it replaces
112
- # (ADR 0025 dropped the `_id` suffix that used to keep the two
129
+ # (ADR 0025 drops the `_id` suffix that once kept the two
113
130
  # apart by name, so `parent.state.merge(dereference(...))` is
114
- # now load-bearing, not redundant) — plus ITS OWN references
131
+ # load-bearing, not redundant) — plus its own references
115
132
  # dereferenced one level in, so `parent.customer.status` (a
116
- # parent aggregate reaching ITS OWN customer) resolves the same
133
+ # parent aggregate reaching its own customer) resolves the same
117
134
  # way `account.customer.status` does for a command-level reference.
118
135
  # nil for an aggregate command — CommandInterpreter never passes it.
119
136
  # `correction:` — the `{as_name => payload}` bindings
120
137
  # `enforce_correction_target` (above) already located, merged in
121
- # LAST so an `as:` name wins the same way `old:` always wins in
138
+ # last so an `as:` name wins the same way `old:` always wins in
122
139
  # `enforce_ensures`, below — it is a fresh local binding a
123
140
  # `corrects` command introduces, not a real argument/state field
124
141
  # a caller could collide with by accident.
142
+ #
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
157
+ # @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)
125
163
  def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
126
164
  state = GuardState.new(subject)
127
- # A RULE MAY ONLY READ WITHIN ITS OWN AGGREGATE BOUNDARY (S12,
128
- # ADR 0025) — `subject`'s own STORED references are no longer
129
- # dereferenced here at all. What used to be a live query against
130
- # another aggregate's own repository is now just `subject`'s own
131
- # state: a `projects :customer_status, from: :"customer.status"`
132
- # field is a REGULAR stored attribute, already present in
165
+ # A rule may only read within its own aggregate boundary (S12,
166
+ # ADR 0025) — `subject`'s own stored references are no longer
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
133
170
  # `subject`/`state` with no hydration step needed. `dereference`
134
171
  # is still called on `command`/`args`, below — that is a
135
- # DIFFERENT case the ADR explicitly keeps in bounds ("its command
136
- # arguments"): a reference-typed ARGUMENT this dispatch was just
172
+ # different case the ADR explicitly keeps in bounds ("its command
173
+ # arguments"): a reference-typed argument this dispatch was just
137
174
  # handed (`Dispute`'s own `disputed_by`, say) has nothing stored
138
175
  # to project yet, so resolving it here, once, synchronously with
139
- # THIS command's own admission, is not the live-query-against-
176
+ # this command's own admission, is not the live-query-against-
140
177
  # another-aggregate's-stored-state pattern the boundary rule
141
178
  # forbids.
142
179
  attrs = args.merge(dereference(domain, command, args))
@@ -154,42 +191,90 @@ module Hecks
154
191
  enforce_lifecycle_guard(declaring, command, subject) if declaring
155
192
  end
156
193
 
157
- # `corrects` — CommandBuilder#corrects_impl's own comment. NOT
194
+ # `corrects` — CommandBuilder#corrects_impl's own comment. Not
158
195
  # expressible as an ordinary `given`: "has this exact record
159
196
  # already emitted this exact event" is not a predicate over the
160
- # record's OWN fields, it is a fact about the event log, so it is
197
+ # record's own fields, it is a fact about the event log, so it is
161
198
  # raised structurally here, the same way NotFound/AlreadyExists
162
199
  # are, rather than through the expression evaluator. The build-
163
- # time half — does ANYTHING in this aggregate ever emit the named
200
+ # time half — does anything in this aggregate ever emit the named
164
201
  # event at all — is `AggregateBuilder#seal_correction_targets`;
165
- # this is the dispatch-time half — has THIS record actually done
202
+ # this is the dispatch-time half — has this record actually done
166
203
  # so yet.
167
204
  #
168
- # ALSO LOCATES the matched event now, not just its existence, and
205
+ # Also locates the matched event now, not just its existence, and
169
206
  # returns a `{as_name => payload}` bindings hash — one entry per
170
207
  # `:corrects` mutation that named an `as:` — so `given`/`ensures`
171
- # on a corrects-bearing command can reference the located OLD
208
+ # on a corrects-bearing command can reference the located old
172
209
  # event by that name, the same shape `enforce_ensures`'s own
173
210
  # `old:` binding already has (CommandBuilder#corrects_impl's own
174
211
  # comment: `as:` was stored, from the start, specifically to be
175
212
  # wired into the evaluator once a real runtime consumer existed —
176
- # this is that consumer). `.reverse.find` — the MOST RECENT
213
+ # this is that consumer). `.reverse.find` — the most recent
177
214
  # matching event, if this record has somehow emitted the same
178
215
  # correction target more than once; the prior existence-only
179
216
  # check never had to make this choice, so it's a genuinely new
180
217
  # one, made deliberately: `as:` reads as "the instance being
181
218
  # corrected," which is naturally the latest fact on record, not
182
219
  # an arbitrary one.
220
+ # Reads the durable event history a `corrects` mutation is judged against.
221
+ #
183
222
  # C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
184
- # is judged against the record's DURABLE history: the events the
223
+ # is judged against the record's durable history: the events the
185
224
  # aggregate's own store recorded (`AppendOnly#events`), which
186
225
  # survive a restart the way the Rust kernel's persisted
187
226
  # `emitted_<event>` flag does. The in-process log is the fallback
188
227
  # only for an adapter that records no readable history.
228
+ #
229
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
230
+ # @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
189
234
  def correction_history(domain, aggregate)
190
235
  @registry.repository(domain, aggregate).events || @registry.event_log
191
236
  end
192
237
 
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.
267
+ #
268
+ # @param instance [Runtime::Instance] the record being corrected
269
+ # @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
272
+ # @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
193
278
  def enforce_correction_target(instance, aggregate, command, domain:)
194
279
  bindings = {}
195
280
  command.mutations.each do |mutation|
@@ -213,15 +298,25 @@ module Hecks
213
298
  bindings
214
299
  end
215
300
 
216
- # LIFECYCLE STATE AS A COMMAND GUARD (S10, ADR 0025) — `command
217
- # "Debit", from: "open"` checked here, folded into the SAME
301
+ # Lifecycle state as a command guard (S10, ADR 0025) — `command
302
+ # "Debit", from: "open"` checked here, folded into the same
218
303
  # dispatch step `given` already runs at (both are preconditions,
219
304
  # evaluated before any mutation) rather than earning its own
220
- # DISPATCH_ORDER entry. A GUARD, never a transition: it names no
305
+ # DISPATCH_ORDER entry. A guard, never a transition: it names no
221
306
  # target state and `step_advance_lifecycle` never sees it — see
222
307
  # `admissible_transition`, right below, for the transition this
223
- # is deliberately NOT reusing (its own `StateTransition#target`
308
+ # is deliberately not reusing (its own `StateTransition#target`
224
309
  # is required, and a guard-only command has none to give it).
310
+ #
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
317
+ # @return [void]
318
+ # @raise [Runtime::LifecycleRefused] if `command` declares `from:` and the
319
+ # record's current lifecycle state is not one of them
225
320
  def enforce_lifecycle_guard(declaring, command, subject)
226
321
  return unless command.from
227
322
 
@@ -229,49 +324,68 @@ module Hecks
229
324
  current = Value.scalar(subject[lifecycle.field]).to_s
230
325
  return if Array(command.from).include?(current)
231
326
 
232
- # ROUTED THROUGH RefusalWording's OWN "transition_blocked"
233
- # TEMPLATE — the same one #admissible_transition, right below,
327
+ # Routed through RefusalWording's own "transition_blocked"
328
+ # template — the same one #admissible_transition, right below,
234
329
  # already raises LifecycleRefused through for the same
235
- # refusal class. This used to hand-roll its own wording
330
+ # refusal class, rather than hand-rolling its own wording
236
331
  # inline ("...only runs from..." vs. the template's "...moves
237
- # it only from...") — two shapes for one refusal kind, so
332
+ # it only from..."), two shapes for one refusal kind, so
238
333
  # anything string-matching a LifecycleRefused message (a
239
- # property, a spec, a caller) had to know both existed rather
240
- # than one.
334
+ # property, a spec, a caller) would have to know both existed
335
+ # rather than one.
241
336
  raise LifecycleRefused,
242
- RefusalWording.render("LifecycleRefused", "transition_blocked",
243
- command: command.hecks_name, field: lifecycle.field,
244
- current: Rendering.describe(current),
245
- allowed: Array(command.from).map(&:inspect).join(" or "))
337
+ RefusalWording.render_site("LifecycleRefused", "transition_blocked",
338
+ command: command.hecks_name, field: lifecycle.field,
339
+ current: Rendering.describe(current),
340
+ allowed: Array(command.from))
246
341
  end
247
342
 
248
- # The far side of the contract: evaluated against the SETTLED record
343
+ # The far side of the contract: evaluated against the settled record
249
344
  # — after mutations and the lifecycle move, before anything persists
250
345
  # — with `old` carrying the state as the givens saw it. Injected into
251
346
  # the attrs at evaluation time only; the payload gate never sees it.
252
347
  #
253
- # `old` — and every dispatch ARGUMENT — wins over a same-named STATE
348
+ # `old` — and every dispatch argument — wins over a same-named state
254
349
  # field in expression scope (Resolver#fetch checks attrs first). An
255
350
  # ensures naming a field the command also takes as an argument (or,
256
351
  # on an entity, a field that doubles as the addressing argument
257
- # element_of reads) will read the ARGUMENT, not the settled value.
352
+ # element_of reads) will read the argument, not the settled value.
258
353
  # Not new to ensures — `given` lives under the same rule — but an
259
354
  # ensures is more likely to collide, since it typically re-reads a
260
355
  # field the command just took in to mutate it.
356
+ #
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
371
+ # @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)
261
375
  def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
262
376
  state = GuardState.new(subject)
263
377
  # S12, ADR 0025 — same boundary reasoning as enforce_givens
264
378
  # above: `subject`'s own stored references are no longer
265
379
  # dereferenced here; a `projects`-maintained field is already
266
380
  # part of `state`. `command`/`args` still dereferences — a
267
- # fresh reference-typed ARGUMENT stays in bounds.
381
+ # fresh reference-typed argument stays in bounds.
268
382
  # `old` still wins over everything, unchanged. `correction`
269
383
  # (an `as:`-bound corrected event, if this command declares
270
384
  # one) wins right alongside it — a settled-record ensures can
271
385
  # reference the correction target exactly as freely as a
272
386
  # pre-mutation given already can.
273
387
  # C2.3 (docs/semantics/bluebook-semantics.md) — an ensures reads
274
- # the SETTLED STATE first: an argument that shares a field's
388
+ # the settled state first: an argument that shares a field's
275
389
  # name does not shadow the candidate here (it does in a given,
276
390
  # C2.2), so `sets :note` + `ensures { note == ... }` judges what
277
391
  # landed, and `old.<field>` remains the pre-state.
@@ -286,25 +400,37 @@ module Hecks
286
400
  end
287
401
  end
288
402
 
289
- # THE AGGREGATE BOUNDARY, checked after every command, before
403
+ # The aggregate boundary, checked after every command, before
290
404
  # save (S10, ADR 0025 — "Rules") — the same point `enforce_
291
405
  # ensures` already checks at, and for the same reason: an
292
- # invariant is a claim about the SETTLED record, not the
406
+ # invariant is a claim about the settled record, not the
293
407
  # command that produced it, so it reads no `args`/`old` at all,
294
408
  # only the record's own state. `subject` here is
295
- # always the AGGREGATE's own instance — `CommandInterpreter`
409
+ # always the aggregate's own instance — `CommandInterpreter`
296
410
  # passes its own `ctx.instance`, and `EntityInterpreter` passes
297
- # the PARENT record (`ctx.instance`, not the element), since an
298
- # entity mutation changes data inside the SAME aggregate
411
+ # the parent record (`ctx.instance`, not the element), since an
412
+ # entity mutation changes data inside the same aggregate
299
413
  # boundary the invariant guards; there is no separate "entity
300
414
  # invariant" to check the piece's own view against.
301
415
  #
302
- # NO `dereference` (S12, ADR 0025) — an invariant may only read
416
+ # No `dereference` (S12, ADR 0025) — an invariant may only read
303
417
  # `subject`'s own boundary, same rule `enforce_givens`/
304
418
  # `enforce_ensures` now hold to. No invariant in the corpus has
305
- # ever read across a `reference_to` (verified before this
306
- # change), so this is not a migration, just closing the same
307
- # capability off here that was already unused.
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.
422
+ #
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`
429
+ # @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)
308
434
  def enforce_invariants(subject, aggregate, domain:)
309
435
  state = GuardState.new(subject)
310
436
  attrs = {}
@@ -317,20 +443,20 @@ module Hecks
317
443
  check_entity_invariants(aggregate, subject, domain: domain)
318
444
  end
319
445
 
320
- # A PIECE'S OWN SHAPE RULE, checked against EVERY INSTANCE the
446
+ # A piece's own shape rule, checked against every instance the
321
447
  # aggregate holds — not a separate boundary from the aggregate's
322
448
  # own invariants just above (same two checkpoints: after every
323
- # mutation, before save), just a WIDER one: the aggregate's own
449
+ # mutation, before save), just a wider one: the aggregate's own
324
450
  # consistency includes each of its pieces individually looking
325
451
  # right, the same way `ValueObject#invariants` already checks
326
- # each of ITS OWN instances one construct up. RECURSES into
452
+ # each of its own instances one construct up. Recurses into
327
453
  # nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
328
454
  # same way `check_entity_invariants`'s own caller recurses
329
455
  # nowhere else needs to, since a piece's `entities` are already
330
456
  # exactly as reachable as an aggregate's.
331
457
  #
332
- # `list_attr` reuses the EXACT lookup `EntityInterpreter#
333
- # element_of` already makes to locate a SINGLE addressed
458
+ # `list_attr` reuses the exact lookup `EntityInterpreter#
459
+ # element_of` already makes to locate a single addressed
334
460
  # element by identity — this reads every element instead, but
335
461
  # the "which field on the owner holds this piece's own
336
462
  # instances" question is the identical one. A piece declaring
@@ -338,6 +464,17 @@ module Hecks
338
464
  # matching list attribute) is a static-analysis gap for a
339
465
  # future gate, not a runtime concern here — `next` past it
340
466
  # rather than raising mid-enforcement for an unrelated command.
467
+ #
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
472
+ # @param domain [String, Symbol] the domain `owner_construct` belongs to
473
+ # @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)
341
478
  def check_entity_invariants(owner_construct, owner_instance, domain:)
342
479
  owner_construct.entities.each do |entity|
343
480
  next if entity.invariants.empty?
@@ -348,7 +485,7 @@ module Hecks
348
485
  Array(owner_instance[list_attr.name]).each do |element|
349
486
  wrapped = Instance.new(aggregate: entity, id: nil, state: element)
350
487
  element_state = GuardState.new(wrapped)
351
- # NO `dereference` (S12, ADR 0025) — same boundary rule as
488
+ # No `dereference` (S12, ADR 0025) — same boundary rule as
352
489
  # enforce_invariants above; `parent` (the owner's own
353
490
  # state, projected fields included) stays readable.
354
491
  attrs = { parent: owner_instance.state }
@@ -364,6 +501,20 @@ module Hecks
364
501
  end
365
502
  end
366
503
 
504
+ # Finds the lifecycle transition `command` admits from `subject`'s current
505
+ # state, if `declaring` declares a lifecycle and `command` moves it.
506
+ #
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
367
518
  def admissible_transition(declaring, command, subject)
368
519
  lifecycle = declaring.lifecycle
369
520
  return nil unless lifecycle
@@ -379,7 +530,7 @@ module Hecks
379
530
  # default `Object#to_s` instead of unwrapping the inner
380
531
  # scalar first -- `current` came back as a raw object-pointer
381
532
  # string (`"#<Hecks::Runtime::Value:0x...>"`) that could
382
- # never match any declared `from` state, so EVERY transition
533
+ # never match any declared `from` state, so every transition
383
534
  # on a VO-typed lifecycle field refused unconditionally, and
384
535
  # when it refused the message leaked the pointer too.
385
536
  # Confirmed live via `Plan::Task.Complete` (status defaults to
@@ -399,10 +550,10 @@ module Hecks
399
550
 
400
551
  allowed = candidates.flat_map { |t| Array(t.from) }.uniq
401
552
  raise LifecycleRefused,
402
- RefusalWording.render("LifecycleRefused", "transition_blocked",
403
- command: command.hecks_name, field: lifecycle.field,
404
- current: Rendering.describe(current),
405
- allowed: allowed.map(&:inspect).join(" or "))
553
+ RefusalWording.render_site("LifecycleRefused", "transition_blocked",
554
+ command: command.hecks_name, field: lifecycle.field,
555
+ current: Rendering.describe(current),
556
+ allowed: allowed)
406
557
  end
407
558
  end
408
559
  end