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
@@ -6,7 +6,7 @@ require_relative "identity"
6
6
  require_relative "instance"
7
7
  require_relative "value"
8
8
  require_relative "refusal_wording"
9
- require_relative "routing"
9
+ require_relative "invocation"
10
10
  require_relative "dependency_planning"
11
11
  require_relative "../ports/persistence/execution"
12
12
  require_relative "entity_element"
@@ -23,9 +23,9 @@ module Hecks
23
23
  # entity-owned commands.
24
24
  class EntityInterpreter
25
25
  include Interpreting
26
- # THE SAME PAYLOAD GATE aggregate commands and port operations already
27
- # run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): this
28
- # class used to run NEITHER refuse_unknown_arguments NOR
26
+ # The same payload gate aggregate commands and port operations already
27
+ # run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): without
28
+ # this, this class runs neither refuse_unknown_arguments nor
29
29
  # refuse_absent_arguments, on a comment claiming "an entity inherits
30
30
  # its aggregate's own gate." Nothing on the entity dispatch path ever
31
31
  # ran one — confirmed live, `LedgerEntry.Reverse` accepted an
@@ -39,11 +39,9 @@ module Hecks
39
39
 
40
40
  attr_reader :registry
41
41
 
42
- # THE DECLARED ORDER, HAND-TYPED — mirrors Vocabulary::EntityDispatchOrder
43
- # (language/bluebook/vocabulary.bluebook:217-232), held equal to it by
44
- # spec/vocabulary_conformance_spec.rb the same way CommandInterpreter's
45
- # own DISPATCH_ORDER is; see that constant's doc comment for why this is
46
- # hand-typed rather than read live off the meta-domain at every dispatch.
42
+ # **The declared order** — Vocabulary::EntityDispatchOrder
43
+ # (language/bluebook/vocabulary.bluebook), read off the generated table
44
+ # the same way CommandInterpreter's own DISPATCH_ORDER is.
47
45
  # `refuse_unknown_arguments`/`refuse_absent_arguments` now lead it, same
48
46
  # position `AggregateDispatchOrder` holds them at (H1, above) — the only
49
47
  # remaining difference from the aggregate order is no
@@ -55,7 +53,7 @@ module Hecks
55
53
  # see that constant's own comment.
56
54
  MAX_STALE_WRITE_RETRIES = 5
57
55
 
58
- # `instance` is the PARENT aggregate record (what gets saved and
56
+ # `instance` is the parent aggregate record (what gets saved and
59
57
  # returned) ; `element`/`view` are the entity piece itself — `view`
60
58
  # wraps `element` as it stood at `locate_element`, pre-mutation, and
61
59
  # `enforce_ensures` builds its own settled wrapper off `element` as it
@@ -63,7 +61,7 @@ module Hecks
63
61
  #
64
62
  # `chain` — S17, ADR 0026 — every entity the dotted verb passes
65
63
  # through, root-first (`[Handler, Dispatch]` for `Handler.Dispatch.
66
- # Bind`) ; `entity`/`entity_name` stay the CHAIN'S OWN LAST entry,
64
+ # Bind`) ; `entity`/`entity_name` stay the chain's own last entry,
67
65
  # the one a command actually belongs to and a mutation actually
68
66
  # targets, so every step written before this ADR (enforce_givens,
69
67
  # apply_mutations, advance_lifecycle, element_identity, ...) reads
@@ -76,8 +74,66 @@ module Hecks
76
74
  Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
77
75
  :args, :repository, :instance, :chain, :element, :view, :transition,
78
76
  :old_element, :result, :route, :plan, :persistence_outcome, :dry_run, :outbox_rows,
79
- :correction_bindings)
77
+ :correction_bindings, :invocation)
80
78
 
79
+ # A dotted entity verb resolved against its aggregate. `Resolution.of`
80
+ # lives here rather than as a second public verb on the interpreter
81
+ # (spec/runtime/command_rules_spec.rb holds each interpreter to one),
82
+ # so `Dispatcher` can resolve it at the point `Invocation.from_call`
83
+ # reads the declaring command — after `to:` is parsed, before the
84
+ # facts are.
85
+ Resolution = Data.define(:entity_names, :chain, :command_name, :command) do
86
+ # Resolves a dotted entity verb against `aggregate` into the entity
87
+ # chain it walks and the command it names.
88
+ #
89
+ # Refuses UnknownVerb for an unknown entity or command.
90
+ #
91
+ # @param aggregate [Bluebook::Aggregate] the root aggregate the dotted verb
92
+ # is resolved against
93
+ # @param dotted [String, Symbol] the entity verb, with the leading
94
+ # "Domain::Aggregate." already stripped (e.g. `"Handler.Dispatch.Bind"`)
95
+ # @return [EntityInterpreter::Resolution] the resolved entity names, chain,
96
+ # command name and command class
97
+ # @raise [Runtime::UnknownVerb] if `dotted` names no entity, or the located
98
+ # entity declares no such command
99
+ def self.of(aggregate, dotted)
100
+ *entity_names, command_name = dotted.to_s.split(".")
101
+ if entity_names.empty?
102
+ raise UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
103
+ aggregate: aggregate.hecks_name, entity: dotted.to_s)
104
+ end
105
+
106
+ chain = walk(aggregate, entity_names)
107
+ command = chain.last.command(command_name) ||
108
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_no_command",
109
+ entity: chain.last.hecks_name, command: command_name))
110
+ new(entity_names: entity_names, chain: chain, command_name: command_name, command: command)
111
+ end
112
+
113
+ # **One hop per dotted segment** — `ProcessManager.Handler.Dispatch.Bind`
114
+ # (once the dispatcher has already stripped "Domain::Aggregate.")
115
+ # walks Handler off the aggregate, then Dispatch off Handler, each
116
+ # step reading `.entities` exactly the way the single-level case
117
+ # always did — a nested entity is "structurally interchangeable
118
+ # with an aggregate" (Entity's own header) for precisely this
119
+ # reason. Two levels is what Handler/Dispatch need today ; nothing
120
+ # here assumes it stops at two.
121
+ def self.walk(aggregate, entity_names)
122
+ owner = aggregate
123
+ entity_names.map do |name|
124
+ found = owner.entities.find { |piece| piece.hecks_name == name } ||
125
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
126
+ aggregate: owner.hecks_name, entity: name))
127
+ owner = found
128
+ found
129
+ end
130
+ end
131
+ private_class_method :walk
132
+ end
133
+
134
+ # @param registry [Runtime::Registry] the booted registry this interpreter reads
135
+ # @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
136
+ # references, arithmetic, authorization, emission) dispatch runs through
81
137
  def initialize(registry, rules:)
82
138
  @registry = registry
83
139
  @rules = rules
@@ -87,39 +143,57 @@ module Hecks
87
143
  # comment for the shared reasoning (Dispatcher#dry_run?'s own entry
88
144
  # point). `step_save`/`step_emit` are the only two steps here that
89
145
  # read it either.
90
- # RETRIES THE WHOLE METHOD BODY on `StaleWrite` — same reasoning as
146
+ # Retries the whole method body on `StaleWrite` — same reasoning as
91
147
  # `CommandInterpreter#call`'s own retry: a fresh `ctx`, a fresh
92
148
  # `step_hydrate_parent`/`step_locate_element` re-reading current
93
149
  # state.
94
- def call(domain, aggregate, dotted, legacy_args, route: nil, with: nil, dry_run: false)
95
- *entity_names, command_name = dotted.to_s.split(".")
96
- if entity_names.empty?
97
- raise UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
98
- aggregate: aggregate.hecks_name, entity: dotted.to_s.inspect)
99
- end
100
-
101
- chain = walk_entity_chain(aggregate, entity_names)
102
- entity = chain.last
103
- command = entity.command(command_name) ||
104
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_no_command",
105
- entity: entity.hecks_name, command: command_name.inspect))
106
-
107
- args = Routing.payload(command, with: with, legacy: legacy_args)
150
+ #
151
+ # `resolution` is #resolve's answer; `invocation` the
152
+ # `Runtime::Invocation` `Dispatcher` built — `ctx.args` is its
153
+ # `to_args`, `ctx.route` its `target`.
154
+ #
155
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
156
+ # @param aggregate [Bluebook::Aggregate] the root aggregate owning the entity
157
+ # chain the command targets
158
+ # @param resolution [EntityInterpreter::Resolution] the resolved entity chain
159
+ # and command, as `Resolution.of` builds it
160
+ # @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
161
+ # this call
162
+ # @param dry_run [Boolean] whether to run every step through validation without
163
+ # saving, emitting or enqueueing
164
+ # @return [Array(Runtime::Instance, Array<Runtime::Event>,
165
+ # Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
166
+ # Array<Runtime::Outbox::Row>)] the settled parent aggregate instance, emitted
167
+ # events, execution plan, persistence outcome and outbox rows — the last
168
+ # three nil on a dry run, which skips save/emit/outbox
169
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
170
+ # given/ensures/invariant/authorization/admissibility rule refuses
171
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
172
+ # retry (`MAX_STALE_WRITE_RETRIES`)
173
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
174
+ def call(domain, aggregate, resolution, invocation, dry_run: false)
175
+ chain = resolution.chain
176
+ entity = chain.last
177
+ command = resolution.command
178
+ command_name = resolution.command_name
179
+ route = invocation.target
180
+ args = invocation.to_args
108
181
  attempt = 0
109
182
  begin
110
- ctx = Context.new(domain, aggregate, entity, entity_names.join("."), command, command_name, args)
183
+ ctx = Context.new(domain, aggregate, entity, resolution.entity_names.join("."), command, command_name, args)
184
+ ctx.invocation = invocation
111
185
  ctx.chain = chain
112
186
  ctx.route = route
113
187
  ctx.dry_run = dry_run
114
188
  # `root_aggregate:` — `entity` is the immediate owner (what
115
189
  # `owner_fields` inside the Analyzer means), but a `parent.X`
116
- # read inside this command's own given/ensures means the ROOT
117
- # aggregate's own field, not the entity's — `aggregate` here IS
190
+ # read inside this command's own given/ensures means the root
191
+ # aggregate's own field, not the entity's — `aggregate` here is
118
192
  # that root (this method's own first parameter, never the
119
193
  # entity). See DependencyPlanning::Analyzer.call's own header
120
194
  # for the bug this closes.
121
195
  ctx.plan = DependencyPlanning::Analyzer.call(aggregate: entity, command: command, root_aggregate: aggregate)
122
- # RESOLVED HERE, ONCE — see CommandInterpreter#call's own comment;
196
+ # **Resolved here, once** — see CommandInterpreter#call's own comment;
123
197
  # `step_hydrate_parent` reads `ctx.repository` without re-fetching.
124
198
  ctx.repository = @registry.repository(domain, aggregate)
125
199
  lock_id = Identity.best_effort(aggregate, args, route)
@@ -134,28 +208,13 @@ module Hecks
134
208
 
135
209
  private
136
210
 
137
- # ONE HOP PER DOTTED SEGMENT — `ProcessManager.Handler.Dispatch.Bind`
138
- # (once the dispatcher has already stripped "Domain::Aggregate.")
139
- # walks Handler off the aggregate, then Dispatch off Handler, each
140
- # step reading `.entities` exactly the way the single-level case
141
- # always did — a nested entity is "structurally interchangeable
142
- # with an aggregate" (Entity's own header) for precisely this
143
- # reason. Two levels is what Handler/Dispatch need today ; nothing
144
- # here assumes it stops at two.
145
- def walk_entity_chain(aggregate, entity_names)
146
- owner = aggregate
147
- entity_names.map do |name|
148
- found = owner.entities.find { |piece| piece.hecks_name == name } ||
149
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
150
- aggregate: owner.hecks_name, entity: name.inspect))
151
- owner = found
152
- found
153
- end
154
- end
211
+ # **A no-op, and untraced** — Vocabulary::EntityDispatchOrder's
212
+ # decode_arguments. See CommandInterpreter#step_decode_arguments.
213
+ def step_decode_arguments(_ctx); end
155
214
 
156
215
  # `extra_identity_heads:` — every entity `ctx.chain` walks through, not
157
216
  # just the root aggregate `ArgumentGate` already knows about. A
158
- # two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by BOTH
217
+ # two-hop dispatch (`Handler.Dispatch.Bind`) is addressed by both
159
218
  # hops' own identity, each read straight out of `args` by
160
219
  # `EntityElement#element_of` — refusing those as unknown would refuse
161
220
  # every legitimate nested-entity dispatch there is, the same reasoning
@@ -202,30 +261,30 @@ module Hecks
202
261
  ctx.element = step(:locate_element) do
203
262
  EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
204
263
  end
205
- # `view` was hydrated ONCE, here, into its OWN state hash
264
+ # `view` was hydrated once, here, into its own state hash
206
265
  # (Value.hydrate builds a fresh Hash — never aliased with `element`)
207
266
  # — exactly right for enforce_givens, which must read pre-mutation.
208
267
  ctx.view = Instance.new(aggregate: ctx.entity, id: EntityElement.element_identity(ctx.entity, ctx.element).to_s,
209
268
  state: ctx.element)
210
269
  end
211
270
 
212
- # BUG#30 — THE ENTITY-LEVEL HALF OF `CommandInterpreter#step_enforce_
271
+ # BUG#30 — the entity-level half of `CommandInterpreter#step_enforce_
213
272
  # givens`'s own structural-before-declared ordering (see that
214
273
  # method's comment for the shared reasoning): "does the fact this
215
274
  # command's `corrects` names even exist" is checked here too, once,
216
275
  # before the entity's own `given`s.
217
276
  #
218
- # ADMISSIBILITY IS CHECKED AGAINST THE PARENT/ROOT, NOT THE ENTITY —
219
- # deliberately `ctx.instance`/`ctx.aggregate` (the PARENT aggregate
220
- # record and the ROOT aggregate construct), never `ctx.view`/
277
+ # Admissibility is checked against the parent/root, not the entity —
278
+ # deliberately `ctx.instance`/`ctx.aggregate` (the parent aggregate
279
+ # record and the root aggregate construct), never `ctx.view`/
221
280
  # `ctx.entity` (the entity's own pre-mutation view/construct). This
222
- # is not a simplification; it is the ONLY choice that lines up with
281
+ # is not a simplification; it is the only choice that lines up with
223
282
  # how the event being corrected was actually recorded: an entity has
224
283
  # no event stream of its own — `CommandRules::Emission#emit` (called
225
- # from THIS class's own `step_emit`, and from `CommandInterpreter`'s
284
+ # from this class's own `step_emit`, and from `CommandInterpreter`'s
226
285
  # `step_emit` for an aggregate-level command alike) always stamps an
227
- # emitted event with the ROOT aggregate's own qualified name
228
- # (`"#{domain}::#{aggregate.hecks_name}"`) and the PARENT record's
286
+ # emitted event with the root aggregate's own qualified name
287
+ # (`"#{domain}::#{aggregate.hecks_name}"`) and the parent record's
229
288
  # own id (`ctx.instance.id`), regardless of which level dispatched
230
289
  # it. `enforce_correction_target` (CommandRules::Admissibility)
231
290
  # looks a correction target up by exactly those two fields plus the
@@ -234,31 +293,31 @@ module Hecks
234
293
  # carry, and every entity-level correction would refuse
235
294
  # (NothingToCorrect) even against a real, already-emitted event.
236
295
  # `qa/stress_domains/corrections`' own `Entry.Amend` (corrects
237
- # "EntryRecorded", which `Ledger.Record` — an AGGREGATE-level
296
+ # "EntryRecorded", which `Ledger.Record` — an aggregate-level
238
297
  # command — actually emits) is exactly this shape: the corrected
239
- # event's `aggregate`/`id` are the LEDGER's, never the Entry's own
298
+ # event's `aggregate`/`id` are the ledger's, never the Entry's own
240
299
  # (an Entry has no id an event could be filed under in the first
241
300
  # place). `Fuzzing::Properties::Corrections#corrections_reference_
242
301
  # an_emitted_event` independently encodes the identical rule
243
- # (`aggregate_key` built off the OUTER aggregate for both the
302
+ # (`aggregate_key` built off the outer aggregate for both the
244
303
  # `corrects` target and the `emits` produced event, regardless of
245
304
  # entity nesting depth) — this is that property's dispatch-time
246
305
  # enforcement counterpart, not a new invention.
247
306
  #
248
307
  # One structural consequence, worth being explicit about for a
249
- # Rust port: because the lookup is scoped to the PARENT record
308
+ # Rust port: because the lookup is scoped to the parent record
250
309
  # (not to any one entity element within it), an entity-level
251
310
  # `corrects` only proves "this parent record has emitted the named
252
- # event at some point" — it does NOT, and cannot, further narrow
311
+ # event at some point" — it does not, and cannot, further narrow
253
312
  # to "...specifically for THIS entity element" (a Ledger with three
254
313
  # Entries all satisfy the same `EntryRecorded`-was-emitted check).
255
- # That is not a gap this fix introduces: it is the SAME granularity
314
+ # That is not a gap this fix introduces: it is the same granularity
256
315
  # the aggregate-level check already has (one record, one event
257
316
  # history), just observed from one level down. A command wanting a
258
317
  # tighter, element-specific correlation has to encode it itself, in
259
318
  # its own `given`s, off `correction`-bound payload fields.
260
319
  #
261
- # `correction:` bindings computed here are threaded through to BOTH
320
+ # `correction:` bindings computed here are threaded through to both
262
321
  # halves of the same command's admissibility, same as the
263
322
  # aggregate-level path: `ctx.correction_bindings` is read again by
264
323
  # `step_enforce_ensures`, below, so an `as:`-named binding is
@@ -292,7 +351,7 @@ module Hecks
292
351
  step(:advance_lifecycle) { ctx.element[ctx.entity.lifecycle.field] = ctx.transition.target }
293
352
  end
294
353
 
295
- # An ensures reads the SETTLED record, so it needs a view hydrated from
354
+ # An ensures reads the settled record, so it needs a view hydrated from
296
355
  # `element` as it stands now, mutations included — unlike `view` above,
297
356
  # built once and read pre-mutation by enforce_givens.
298
357
  def step_enforce_ensures(ctx)
@@ -309,9 +368,9 @@ module Hecks
309
368
  end
310
369
  end
311
370
 
312
- # THE PARENT AGGREGATE's own invariants — `ctx.instance` is the
371
+ # The parent aggregate's own invariants — `ctx.instance` is the
313
372
  # parent record an entity mutation writes into (this file's own
314
- # `Context` comment), the SAME boundary an aggregate-level
373
+ # `Context` comment), the same boundary an aggregate-level
315
374
  # invariant guards regardless of which interpreter changed it. No
316
375
  # separate "entity invariant" exists (S10, ADR 0025 scopes
317
376
  # `invariant` to the aggregate only) — see `Admissibility#
@@ -320,21 +379,23 @@ module Hecks
320
379
  step(:enforce_invariants) { @rules.enforce_invariants(ctx.instance, ctx.aggregate, domain: ctx.domain) }
321
380
  end
322
381
 
323
- # `dry_run:` skips this — see CommandInterpreter#step_save's own
324
- # comment, same reasoning and the same precedent
325
- # (`step_assign_creation_attributes`'s own conditional-skip).
382
+ # `dry_run:` skips the write half of this step — see
383
+ # CommandInterpreter#step_save's own comment (BUG#127): the
384
+ # reference-existence check stays unconditional, only the actual
385
+ # persist is behind the early return.
326
386
  def step_save(ctx)
387
+ step(:save) { @rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state) }
388
+
327
389
  return if ctx.dry_run
328
390
 
329
391
  step(:save) do
330
- @rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
331
392
  # `expected_version:` — see CommandInterpreter#step_save's own
332
393
  # comment: nil for a repository that isn't CAS-capable, or an
333
394
  # instance never read from storage, either of which falls
334
395
  # through to a plain save inside `AppendOnly#save`.
335
396
  ctx.persistence_outcome = ctx.repository.save(ctx.instance, expected_version: ctx.instance.version)
336
397
  if ctx.persistence_outcome.status == :stale
337
- # NOT a `RefusalWording.render` call — see
398
+ # Not a `RefusalWording.render` call — see
338
399
  # `CommandInterpreter#step_save`'s identical branch and
339
400
  # `Runtime::StaleWrite`'s own comment.
340
401
  raise(StaleWrite,
@@ -353,7 +414,7 @@ module Hecks
353
414
  ctx.result = step(:emit) { @rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository) }
354
415
  end
355
416
 
356
- # THE PARENT AGGREGATE, addressed exactly as `CommandInterpreter#hydrate`
417
+ # The parent aggregate, addressed exactly as `CommandInterpreter#hydrate`
357
418
  # addresses one acting on itself — derive from the declared identity first
358
419
  # (`Identity.of`), and let a bare `id:` name an already-derived record when
359
420
  # the identity itself is not what the caller is holding.
@@ -361,24 +422,24 @@ module Hecks
361
422
  parent_id = route&.aggregate ||
362
423
  Identity.of(aggregate, args) ||
363
424
  Identity.from(aggregate, args, :id) ||
364
- raise(NotFound, RefusalWording.render("NotFound", "entity_parent_no_identity",
365
- command: command_name, aggregate: aggregate.hecks_name,
366
- entity: entity_name, identity: Identity.reading(aggregate)))
425
+ raise(NotFound, RefusalWording.render_site("NotFound", "entity_parent_no_identity",
426
+ command: command_name, aggregate: aggregate.hecks_name,
427
+ entity: entity_name, identity: Identity.reading(aggregate)))
367
428
  found = repository.find(parent_id) ||
368
- raise(NotFound, RefusalWording.render("NotFound", "record_missing",
369
- aggregate: aggregate.hecks_name,
370
- identity: Identity.reading(aggregate),
371
- offered: Rendering.describe(parent_id)))
429
+ raise(NotFound, RefusalWording.render_site("NotFound", "record_missing",
430
+ aggregate: aggregate.hecks_name,
431
+ identity: Identity.reading(aggregate),
432
+ offered: Rendering.describe(parent_id)))
372
433
  found.dup
373
434
  end
374
435
 
375
436
  # `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
376
- # their own helpers used to live here — moved to `Runtime::EntityElement`
377
- # (see that file's own header) so `CommandInterpreter`'s own
378
- # `delegate_to_entity` step can locate and mutate the same element the
379
- # same way, against an aggregate record already held in memory. `call`,
380
- # above, and every `step_*` method reach them through that module now;
381
- # nothing about the STEPS themselves changed.
437
+ # their own helpers live in `Runtime::EntityElement`
438
+ # (see that file's own header), shared rather than kept here, so
439
+ # `CommandInterpreter`'s own `delegate_to_entity` step can locate and
440
+ # mutate the same element the same way, against an aggregate record
441
+ # already held in memory. `call`, above, and every `step_*` method
442
+ # reach them through that module.
382
443
  end
383
444
  end
384
445
  end
@@ -2,17 +2,21 @@ require_relative "value/invariant_violation"
2
2
  require_relative "../vocabulary"
3
3
 
4
4
  module Hecks
5
+ # What runs a booted domain: dispatch, the command/entity/query/policy/saga
6
+ # interpreters, the registry a boot assembles, and the errors below —
7
+ # everything downstream of a `.bluebook`/`.hecksagon`/`.world` declaration.
8
+ # See `lib/hecks/runtime.rb` for the module's own facade and boot entry points.
5
9
  module Runtime
6
10
  class UnknownVerb < StandardError; end
7
11
  class EnsuresNotMet < StandardError; end
8
12
 
9
13
  # `detail` — the failing comparison's own resolved operands, "left: X,
10
- # right: Y" — set only when the given's TOP-LEVEL shape is a bare
14
+ # right: Y" — set only when the given's top-level shape is a bare
11
15
  # comparison (`Evaluator.comparison_detail`'s own comment has the full
12
- # scoping); nil otherwise. Deliberately NOT folded into `#message`:
16
+ # scoping); nil otherwise. Deliberately not folded into `#message`:
13
17
  # that string is pinned byte-for-byte across this corpus's own specs
14
18
  # (`raise_error(GivenNotMet, "...")`, command_rules_spec.rb and every
15
- # domain that vendors this gem) as CONTRACT, so changing its shape by
19
+ # domain that vendors this gem) as contract, so changing its shape by
16
20
  # default would be a breaking change for every one of them. Riding on
17
21
  # `#detailed_message` instead (Ruby 3.2+, what irb/a Rails console's
18
22
  # own unhandled-exception banner already calls to show more than
@@ -21,11 +25,21 @@ module Hecks
21
25
  class GivenNotMet < StandardError
22
26
  attr_reader :detail
23
27
 
28
+ # @param message [String, nil] the refusal text, pinned byte-for-byte across specs
29
+ # @param detail [String, nil] the failing comparison's resolved operands ("left: X,
30
+ # right: Y"), or nil when the given's top-level shape is not a bare comparison
24
31
  def initialize(message = nil, detail: nil)
25
32
  super(message)
26
33
  @detail = detail
27
34
  end
28
35
 
36
+ # Renders `message` with `detail` appended, for a human reading an unhandled refusal.
37
+ #
38
+ # @param highlight [Boolean] unused; accepted for compatibility with `Exception#
39
+ # detailed_message`'s own signature
40
+ # @param opts [Hash] unused; accepted for compatibility with `Exception#
41
+ # detailed_message`'s own signature
42
+ # @return [String] `message`, with `" (#{detail})"` appended when `detail` is present
29
43
  def detailed_message(highlight: false, **opts)
30
44
  base = super
31
45
  detail ? "#{base} (#{detail})" : base
@@ -39,7 +53,7 @@ module Hecks
39
53
  # is the right name carrying the wrong thing, this one is a name the command
40
54
  # never had. Both are the payload gate refusing before any rule runs.
41
55
  class UnknownArgument < StandardError; end
42
- # The third of the trio, and the one that was missing : a name the command DOES
56
+ # The third of the trio, and the one that was missing : a name the command does
43
57
  # declare, absent. TypeMismatch is the right name carrying the wrong thing,
44
58
  # UnknownArgument a name that was never declared, AbsentArgument a declared name
45
59
  # that never arrived. Between them they say a command takes exactly the
@@ -56,18 +70,18 @@ module Hecks
56
70
  # `given`/`ensures`/`invariant` that reads the field would otherwise
57
71
  # evaluate against a value nobody wrote, which is the same silent-
58
72
  # wrong-answer class as an unpopulated projection reading "not
59
- # active" (ADR 0025, "Added attributes and absence"). An OPTIONAL
73
+ # active" (ADR 0025, "Added attributes and absence"). An optional
60
74
  # attribute in the same spot reads nil instead — that is what
61
75
  # optional means, and this refusal is deliberately narrower than the
62
76
  # nil-read it sits beside, not a replacement for it.
63
77
  class AttributeAbsent < StandardError; end
64
- # THE SAME SILENT-WRONG-ANSWER CLASS AS ABOVE, one line up — a
78
+ # The same silent-wrong-answer class as above, one line up — a
65
79
  # `projects` field (S12, ADR 0025) this record predates, or that no
66
80
  # rebuild sweep has populated yet, read by a `given`/`ensures`/
67
81
  # `invariant` as though it carried a real value. `GuardState` is
68
82
  # the one place this is raised, the same way AttributeAbsent is —
69
- # a DECLARED field the record does not yet carry, distinguished
70
- # from that one only in WHY: an ordinary attribute is absent
83
+ # a declared field the record does not yet carry, distinguished
84
+ # from that one only in why: an ordinary attribute is absent
71
85
  # because nobody backfilled it, a projected field is absent
72
86
  # because nobody has swept it yet.
73
87
  class ProjectionAbsent < StandardError; end
@@ -79,14 +93,14 @@ module Hecks
79
93
  class Unauthorized < StandardError; end
80
94
  # `corrects` names a past event this record must have already emitted
81
95
  # (CommandBuilder#corrects_impl's own comment) — a fact the expression
82
- # evaluator cannot check (it is not a predicate over the record's OWN
96
+ # evaluator cannot check (it is not a predicate over the record's own
83
97
  # fields, it is "did this exact record ever announce this"), so it is
84
98
  # raised structurally, the same way AlreadyExists/NotFound are, rather
85
99
  # than being expressible as an ordinary `given`. Raised by
86
100
  # `CommandRules::Admissibility#enforce_correction_target`.
87
101
  class NothingToCorrect < StandardError; end
88
102
 
89
- # A RUNTIME FAULT, NOT A DOMAIN REFUSAL — deliberately absent from
103
+ # A runtime fault, not a domain refusal — deliberately absent from
90
104
  # `DOMAIN_REFUSALS` below and from `vocabulary.bluebook`'s own
91
105
  # `DomainRefusal` list. Raised when an optimistic-concurrency CAS write
92
106
  # (`AppendOnly#save`'s `expected_version:`) finds the stored version has
@@ -102,29 +116,29 @@ module Hecks
102
116
 
103
117
  # A Lambda-routed domain's own refusal (rust/host, `Runtime::
104
118
  # RemoteDispatcher`), carrying Rust's own refusal text verbatim —
105
- # NOT yet mapped back to the specific matching class above
119
+ # not yet mapped back to the specific matching class above
106
120
  # (GivenNotMet vs. EnsuresNotMet vs. ...), a real, known,
107
121
  # documented gap: the WASM projector's own event/refusal-wording
108
- # parity work (ADR 0021) makes the TEXT match Ruby's, but nothing
122
+ # parity work (ADR 0021) makes the text match Ruby's, but nothing
109
123
  # yet parses that text back into a typed Ruby exception the way a
110
124
  # local dispatch already raises one directly. Callers that only
111
125
  # need "the domain said no" (not which specific rule) are
112
- # unaffected; callers pattern-matching a SPECIFIC refusal class
126
+ # unaffected; callers pattern-matching a specific refusal class
113
127
  # against a Lambda-routed domain are the ones this gap would bite.
114
128
  class RemoteRefusal < StandardError; end
115
129
 
116
- # The domain saying NO — the errors a reaction may legitimately meet and
130
+ # The domain saying no — the errors a reaction may legitimately meet and
117
131
  # record as an undelivered outcome. A policy whose target refuses is a fact
118
132
  # about the domain ; the originating command still stands.
119
133
  #
120
- # Everything ELSE is a defect : a NoMethodError in an interpreter, a
134
+ # Everything else is a defect : a NoMethodError in an interpreter, a
121
135
  # NameError from a missing constant, a TypeError from a bad assumption. A
122
- # blanket `rescue StandardError` used to fold both into one line —
123
- # `delivered: false, reason: "..."` — so a crash in the runtime was
124
- # indistinguishable from a rule doing its job, and read as normal operation
125
- # in the log.
136
+ # blanket `rescue StandardError` would fold both into one line —
137
+ # `delivered: false, reason: "..."` — making a crash in the runtime
138
+ # indistinguishable from a rule doing its job, reading as normal
139
+ # operation in the log.
126
140
  #
127
- # UnknownVerb IS one of these, and deliberately : a cross-domain policy
141
+ # UnknownVerb is one of these, and deliberately : a cross-domain policy
128
142
  # (`across "Notifications"`) fires in deployments where that domain is not
129
143
  # loaded, and recording the undelivered reaction rather than raising is the
130
144
  # design — spec/policy_spec states it in so many words, "records a reaction
@@ -133,12 +147,12 @@ module Hecks
133
147
  # its own rule is the domain saying no as plainly as a given is — but the
134
148
  # class is declared over in value.rb and never made the list, so the policy
135
149
  # and saga interpreters, which rescue exactly these, would let it propagate
136
- # as though the RUNTIME had broken. A reaction whose target violates an
150
+ # as though the runtime had broken. A reaction whose target violates an
137
151
  # invariant is declined, not crashed. Found by spec/domain_refusal_spec on
138
152
  # its first run : every corpus refusal must be a class named here, and 23
139
153
  # of banking's were InvariantViolation.
140
- # THE NAMES COME FROM THE LANGUAGE, the classes from this module.
141
- # `DomainRefusal` declares WHICH refusals are the domain's own —
154
+ # The names come from the language, the classes from this module.
155
+ # `DomainRefusal` declares which refusals are the domain's own —
142
156
  # a rule the caller broke — as against a runtime fault. Resolving
143
157
  # each name here means a refusal declared but never defined fails
144
158
  # at load with a NameError, rather than being quietly absent from
@@ -2,7 +2,7 @@ require "time"
2
2
 
3
3
  module Hecks
4
4
  module Runtime
5
- # `correlation` is NOT on the wire — `to_h` below deliberately omits it,
5
+ # `correlation` is not on the wire — `to_h` below deliberately omits it,
6
6
  # the same as `bin/run`'s own event projection does. It is runtime
7
7
  # bookkeeping stamped by `Dispatcher#dispatch` when a saga leg's own
8
8
  # dispatch causes this event (see `SagaInterpreter#deliver_saga_dispatch`
@@ -11,20 +11,25 @@ module Hecks
11
11
  # one correlating on a different field. Absent for any event no saga
12
12
  # dispatch caused, which is most of them.
13
13
  Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
14
- # AN EMITTED EVENT IS A RECORD OF SOMETHING THAT HAPPENED, and a
15
- # mutable audit trail is not one. The PAYLOAD — the domain fact the
16
- # event carries — is frozen THROUGH on emission: freezing the Hash
14
+ # Freezes the event deep, so nothing about it can change after it happens.
15
+ #
16
+ # An emitted event is a record of something that happened, and a
17
+ # mutable audit trail is not one. The payload — the domain fact the
18
+ # event carries — is frozen through on emission: freezing the Hash
17
19
  # alone would leave every value in it editable in place, which is
18
20
  # the shape all four previous freezing bugs had.
19
21
  #
20
- # THE WHOLE EVENT, not just its payload. Correlation used to be
21
- # merged onto already-emitted events by `Dispatcher#dispatch`, which
22
- # is what kept an event writable after it had happened; it is set at
23
- # construction now, because it is part of the transaction and known
24
- # from `dispatch`'s own argument before anything is emitted.
22
+ # The whole event, not just its payload. Correlation is set at
23
+ # construction rather than merged in here by `Dispatcher#dispatch`
24
+ # after the event already exists, because it is part of the
25
+ # transaction and known from `dispatch`'s own argument before
26
+ # anything is emitted — that is what keeps an event immutable once
27
+ # it exists.
25
28
  #
26
- # The LOG stays appendable: new events are still recorded. It is
29
+ # The log stays appendable: new events are still recorded. It is
27
30
  # each event that stops changing once it exists.
31
+ #
32
+ # @return [void]
28
33
  def emit!
29
34
  Freezer.deep(payload)
30
35
  Freezer.deep(correlation)