hecks 1.3.0 → 1.4.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 (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -10,103 +10,96 @@ module Hecks
10
10
  # increment/decrement land on an Integer or a one-numeric-field value
11
11
  # object.
12
12
  module Arithmetic
13
- # The ops Runtime::CommandInterpreter applies. Declared the same way in
14
- # Vocabulary::MutationOp (language/bluebook/vocabulary.bluebook)
15
- # spec/vocabulary_conformance_spec holds the two tables equal, so
16
- # increment/decrement's sign cannot drift from what the language says
17
- # each op means. set/append carry no sign — they do no arithmetic.
13
+ # The ops Runtime::CommandInterpreter applies Vocabulary::MutationOp
14
+ # (language/bluebook/vocabulary.bluebook), read off the generated
15
+ # table, so increment/decrement's sign cannot drift from what the
16
+ # language says each op means. Every other op (set, append, multiply,
17
+ # clamp, remove, delegate, corrects) declares an empty sign — it does
18
+ # no add-or-subtract arithmetic — which reads here as nil.
18
19
  MutationOp = Struct.new(:name, :sign, keyword_init: true)
19
20
 
20
- MUTATION_OPS = [
21
- MutationOp.new(name: "set", sign: nil),
22
- MutationOp.new(name: "append", sign: nil),
23
- MutationOp.new(name: "increment", sign: 1),
24
- MutationOp.new(name: "decrement", sign: -1),
25
- # Vendored addition, not (yet) upstream hecks (migration
26
- # plan task 4, i106): multiply/clamp carry no sign -- like
27
- # set/append, they do no add-or-subtract arithmetic (multiply
28
- # scales, clamp bounds). See #multiply/#clamp below.
29
- MutationOp.new(name: "multiply", sign: nil),
30
- MutationOp.new(name: "clamp", sign: nil),
31
- # Vendored addition, not (yet) upstream hecks (migration
32
- # plan task 4): remove -- carries no sign, like set/append; it
33
- # matches a list element by value rather than doing arithmetic.
34
- # Declared here so this table stays exactly what
35
- # Vocabulary::MutationOp declares (spec/vocabulary_conformance_spec
36
- # holds the two equal) -- MutationApplier's own `when :remove`
37
- # branch (mutation_applier.rb) never calls #sign_of, so this was
38
- # a declared-vocabulary gap, not a behaviour gap.
39
- MutationOp.new(name: "remove", sign: nil),
40
- # Vendored addition, not (yet) upstream hecks —
41
- # CommandBuilder#delegates_to's own comment gives the full
42
- # reasoning; carries no sign, like set/append/remove — it does
43
- # no arithmetic, only a synchronous handoff into one nested
44
- # entity command. Declared here so this table stays exactly
45
- # what Vocabulary::MutationOp declares — MutationApplier's own
46
- # `when :delegate` branch (mutation_applier.rb) never calls
47
- # #sign_of either, same as `remove`'s own note above.
48
- MutationOp.new(name: "delegate", sign: nil),
49
- # CommandBuilder#corrects_impl's own comment gives the full
50
- # reasoning — a command amending a past event rather than
51
- # acting fresh. Carries no sign, like delegate: it does no
52
- # arithmetic of its own; the record's actual change, if any,
53
- # is an ordinary `sets` declared alongside it.
54
- MutationOp.new(name: "corrects", sign: nil)
55
- ].freeze
21
+ MUTATION_OPS = Hecks::Vocabulary.rows("MutationOp").map do |row|
22
+ MutationOp.new(name: row["name"], sign: row["sign"].empty? ? nil : Integer(row["sign"]))
23
+ end.freeze
56
24
 
57
- # A mutation's source is either the NAME OF AN ARGUMENT or a LITERAL, and
25
+ # Reads what a mutation's source means: an argument's value for a Symbol, the literal
26
+ # itself for anything else.
27
+ #
28
+ # A mutation's source is either the name of an argument or a literal, and
58
29
  # the two are told apart by type : a Symbol is always a name, a String or a
59
30
  # number is always a value. Checked across all eight chapters — `to: :name`
60
31
  # and `to: "sold"`, never a Symbol meant as a value.
61
32
  #
62
- # `&& args.key?(source)` used to guard the lookup, and that guard is what
63
- # made an ABSENT argument fall through to `source` and return THE SYMBOL
64
- # ITSELF as the value. `Customer.Register` without its `name` set name to
65
- # the literal `:name`, coercion met a Symbol where a PersonName belonged,
66
- # and the refusal read "name is a PersonName — pass its fields as an
33
+ # The lookup is unconditional, with no `&& args.key?(source)` guard: such a
34
+ # guard makes an absent argument fall through to `source` and return the symbol
35
+ # itself as the value. `Customer.Register` without its `name` would set name to
36
+ # the literal `:name`, coercion would meet a Symbol where a PersonName belongs,
37
+ # and the refusal would read "name is a PersonName — pass its fields as an
67
38
  # object, not :name" — a message describing a mistake the caller had not
68
- # made. The real mistake, an absent argument, was never the one refused,
39
+ # made. The real mistake, an absent argument, would never be the one refused,
69
40
  # which is what fuzz surfaced.
70
41
  #
71
- # STALE (as of the equivalence-gap plan's own audit): this used to
72
- # say "the language cannot yet say which arguments are optional"
73
- # it already can, and always could once `attribute ..., optional:
74
- # true` existed (`CommandBuilder#attribute_impl`,
75
- # `attribute_collector.rb`): `sets` already sources correctly from
42
+ # An absent argument resolving to nil is correct, not pending work. The
43
+ # language can say which arguments are optional (`attribute ...,
44
+ # optional: true` `CommandBuilder#attribute_impl`,
45
+ # `attribute_collector.rb`): `sets` sources correctly from
76
46
  # an optional attribute, resolving absent to nil exactly as this
77
- # method does, and REFUSING it here would be wrong, not merely
47
+ # method does, and refusing it here would be wrong, not merely
78
48
  # undone work — `TillRoom::Till.TakeIn`'s own `note` (spec/
79
49
  # fixtures/till.bluebook) and Banking's `CardPayment.Authorize`'s
80
50
  # `tags` (payment_cards.bluebook) are real, live commands whose
81
51
  # `sets` mutation is deliberately sourced from an optional
82
52
  # attribute the caller may omit — `spec/runtime/command_rules_spec
83
- # .rb`'s own "says an absent OPTIONAL argument is nil, not the
53
+ # .rb`'s own "says an absent optional argument is nil, not the
84
54
  # name of the argument" pins exactly this as correct, not pending.
85
55
  # The meta-domain's own self-hosted commands (Command.Declare's
86
56
  # `role`/`goal`/`provenance`/`from`/`position`, and ~35 more sites
87
57
  # across the language) all lean on the identical pattern — nil is
88
- # the RIGHT answer for a `sets` sourced from a declared-optional
58
+ # the right answer for a `sets` sourced from a declared-optional
89
59
  # attribute the caller left out, every time.
90
60
  #
91
- # The one thing that WAS still a real, narrow gap — a `sets`
92
- # source Symbol naming NOTHING the command declares at all (a
93
- # typo, not an optional argument) silently resolved to nil
94
- # forever the same way, indistinguishable at either build or run
95
- # time from a legitimate optional absence. Closed at BUILD time
61
+ # The one narrow gap that leaves — a `sets` source Symbol naming
62
+ # nothing the command declares at all (a typo, not an optional
63
+ # argument), which would resolve to nil here, indistinguishable
64
+ # from a legitimate optional absence — is closed at build time
96
65
  # instead of here: `CommandBuilder#refuse_unknown_argument_sources!`
97
66
  # refuses it the moment the `.bluebook` file loads, mirroring
98
67
  # `AggregateBuilder#seal_query_argument`'s identical check for a
99
- # query's own where-clause argument. This function stays exactly
100
- # what it always was a pure, unconditional lookup — because by
101
- # the time ANY mutation reaches it, the source has already been
102
- # proven to name either a real, possibly-optional argument, or a
103
- # StateRef/literal; there is nothing left here to refuse.
68
+ # query's own where-clause argument. This function stays a pure,
69
+ # unconditional lookup because by the time any mutation reaches
70
+ # it, the source has already been proven to name either a real,
71
+ # possibly-optional argument, or a StateRef/literal; there is
72
+ # nothing left here to refuse.
73
+ #
74
+ # @param source [Symbol, Object] a mutation's source: a Symbol names a command argument,
75
+ # anything else (String, Numeric, Array, Hash, `StateRef`) is returned as is
76
+ # @param args [Hash{Symbol => Object}] the normalized command arguments
77
+ # @return [Object, nil] the named argument's value, nil when the caller left that
78
+ # argument out; otherwise `source` itself
104
79
  def resolve_source(source, args)
105
80
  return args[source] if source.is_a?(Symbol)
106
81
 
107
82
  source
108
83
  end
109
84
 
85
+ # Adds or subtracts `amount` from an attribute's current value, on a bare number or on
86
+ # the one numeric field two value objects share.
87
+ #
88
+ # @param current [Numeric, Runtime::Value, nil] the attribute's pre-dispatch value; nil
89
+ # (never set) counts as 0
90
+ # @param amount [Numeric, Runtime::Value] how much to move by; a value object is combined
91
+ # field by field with a value-object `current`, and otherwise unwrapped to its single
92
+ # numeric field
93
+ # @param target [Symbol, String] name of the attribute, used only to word a refusal
94
+ # @param sign [Integer] 1 to increment, -1 to decrement, as `sign_of` answers
95
+ # @return [Numeric, Runtime::Value] the new value: a `Runtime::Value` when both sides
96
+ # are value objects, otherwise a bare number the caller re-wraps
97
+ # @raise [Runtime::TypeMismatch] if `amount` or `current` is not numeric, or two value
98
+ # objects do not share exactly one numeric field
99
+ # @raise [Runtime::InvariantViolation] if a value-object result breaks one of its own
100
+ # invariants
101
+ # @raise [Bluebook::Expression::EvaluationError] if the result does not fit a signed
102
+ # 64-bit Integer, or is a non-finite Float
110
103
  def arithmetic(current, amount, target, sign)
111
104
  op = sign.positive? ? "increment" : "decrement"
112
105
  current ||= 0
@@ -117,7 +110,7 @@ module Hecks
117
110
  # `amount` arrives VO-wrapped — a real command argument typed the
118
111
  # same as the attribute, but with nothing to combine field-by-
119
112
  # field against yet (that is what `arithmetic_value_object`,
120
- # above, is for once BOTH sides carry real fields). Before this,
113
+ # above, is for once both sides carry real fields). Before this,
121
114
  # falling straight to `unless amount.is_a?(Numeric)` below
122
115
  # refused with "increment needs an Integer, got 500" — true of
123
116
  # nothing: 500 is exactly the Integer it asked for, just still
@@ -135,12 +128,12 @@ module Hecks
135
128
  # counters before this corpus existed. Integer stays the common
136
129
  # case; Float is now accepted the same way.
137
130
  unless amount.is_a?(Numeric)
138
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
139
- op: op, target: target, offered: Rendering.describe(amount))
131
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "arithmetic_amount",
132
+ op: op, target: target, offered: Rendering.describe(amount))
140
133
  end
141
134
  unless current.is_a?(Numeric)
142
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
143
- op: op, target: target, offered: Rendering.describe(current))
135
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "arithmetic_current",
136
+ op: op, target: target, offered: Rendering.describe(current))
144
137
  end
145
138
 
146
139
  bounded(current + (sign * amount), op, current, sign * amount, sign.positive? ? "+" : "-")
@@ -148,11 +141,23 @@ module Hecks
148
141
 
149
142
  # C3.3/C3.4 — an effect's arithmetic is held to the same value
150
143
  # model an expression's is: Integer is signed 64-bit, Float is
151
- # finite. A result outside that is an evaluation FAULT (never a
144
+ # finite. A result outside that is an evaluation fault (never a
152
145
  # refusal, C8.3), worded as the Rust kernel's own generated
153
146
  # `checked_add`/`checked_sub`/`checked_mul` word it.
154
147
  INT64_RANGE = (-(2**63))..((2**63) - 1)
155
148
 
149
+ # Passes an arithmetic result through only if it fits the value model: a signed 64-bit
150
+ # Integer or a finite Float.
151
+ #
152
+ # @param result [Numeric] the computed value to check
153
+ # @param oper [String] the op's name (`"increment"`, `"decrement"`, `"multiply"`),
154
+ # the first word of the fault message
155
+ # @param lhs [Numeric] the left operand, quoted in the fault message
156
+ # @param rhs [Numeric] the right operand; its absolute value is quoted
157
+ # @param symbol [String] the operator to print between the operands: `"+"`, `"-"`, `"*"`
158
+ # @return [Numeric] `result`, unchanged
159
+ # @raise [Bluebook::Expression::EvaluationError] if an Integer result is outside
160
+ # `INT64_RANGE`, or a Float result is NaN or infinite
156
161
  def bounded(result, oper, lhs, rhs, symbol)
157
162
  if result.is_a?(Integer)
158
163
  return result if INT64_RANGE.cover?(result)
@@ -166,6 +171,22 @@ module Hecks
166
171
  "#{oper} overflowed: #{lhs} #{symbol} #{rhs.abs} is not a finite number"
167
172
  end
168
173
 
174
+ # Adds or subtracts on the one numeric field two value objects share, answering a new
175
+ # value object of `current`'s type.
176
+ #
177
+ # @param current [Runtime::Value] the attribute's pre-dispatch value
178
+ # @param amount [Runtime::Value] how much to move by, coerced to the attribute's type
179
+ # @param target [Symbol, String] name of the attribute, used only to word a refusal
180
+ # @param sign [Integer] 1 to add, -1 to subtract
181
+ # @param oper [String] `"increment"` or `"decrement"`, for refusal and fault wording
182
+ # @return [Runtime::Value] a copy of `current` with the shared field replaced, rebuilt
183
+ # and re-validated through `Value#with`
184
+ # @raise [Runtime::TypeMismatch] if the two do not share exactly one numeric field, or
185
+ # the rebuilt value object refuses the new field value
186
+ # @raise [Runtime::InvariantViolation] if the new field value breaks one of the value
187
+ # object's own invariants
188
+ # @raise [Bluebook::Expression::EvaluationError] if the result does not fit a signed
189
+ # 64-bit Integer, or is a non-finite Float
169
190
  def arithmetic_value_object(current, amount, target, sign, oper)
170
191
  current_fields = current.to_h
171
192
  amount_fields = amount.to_h
@@ -180,7 +201,7 @@ module Hecks
180
201
  end
181
202
  unless shared_numeric.size == 1
182
203
  raise TypeMismatch,
183
- RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
204
+ RefusalWording.render_site("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
184
205
  end
185
206
 
186
207
  field = shared_numeric.first
@@ -188,10 +209,12 @@ module Hecks
188
209
  current[field], amount[field], sign.positive? ? "+" : "-"))
189
210
  end
190
211
 
191
- # Not a bare `.find(...)&.sign || -1` that silently answered
192
- # DECREMENT'S sign for BOTH an op this table has never heard of
193
- # AND a declared, real op that simply carries no sign at all
194
- # (set/append/multiply/clamp/remove see MUTATION_OPS above).
212
+ # Looks up whether a mutation op adds or subtracts, from the generated `MUTATION_OPS` table.
213
+ #
214
+ # Not a bare `.find(...)&.sign || -1` that would silently answer
215
+ # decrement's sign for both an op this table has never heard of
216
+ # and a declared, real op that simply carries no sign at all
217
+ # (set/append/multiply/clamp/remove — see `MUTATION_OPS` above).
195
218
  # Callers today only ever reach this for :increment/:decrement
196
219
  # (both MutationApplier#apply and EntityInterpreter#
197
220
  # apply_to_element gate every other op through their own `case`
@@ -199,11 +222,19 @@ module Hecks
199
222
  # raise is not a real runtime path yet — it is the same
200
223
  # backstop one level down, in case a future caller reaches
201
224
  # #sign_of directly for an op that was never meant to have one.
225
+ #
226
+ # @param oper [Symbol, String] the mutation op's name, such as `:increment`
227
+ # @return [Integer] 1 for increment, -1 for decrement
228
+ # @raise [Runtime::WiringError] if the op is unknown, or is declared with no sign
229
+ # (set, append, multiply, clamp, remove, delegate, corrects)
202
230
  def sign_of(oper)
203
231
  MUTATION_OPS.find { |candidate| candidate.name == oper.to_s }&.sign ||
204
232
  raise(WiringError, "no sign declared for mutation op #{oper.inspect} — add one before calling #sign_of")
205
233
  end
206
234
 
235
+ # Scales an attribute's current value by `amount`, on a bare number or on the one numeric
236
+ # field two value objects share.
237
+ #
207
238
  # Vendored addition, not (yet) upstream hecks (migration plan
208
239
  # task 4, i106): `current * amount` -- the scaling counterpart to
209
240
  # increment/decrement's add/subtract. Same raw-vs-value-object
@@ -211,6 +242,21 @@ module Hecks
211
242
  # rather than duplicated verb-for-verb (a `Proc` picks the actual
212
243
  # arithmetic; everything else -- the Value unwrap/rewrap, the
213
244
  # TypeMismatch refusals -- is identical to the additive pair).
245
+ #
246
+ # @param current [Numeric, Runtime::Value, nil] the attribute's pre-dispatch value; nil
247
+ # (never set) counts as 0
248
+ # @param amount [Numeric, Runtime::Value] the factor; a value object is combined field
249
+ # by field with a value-object `current`, and otherwise unwrapped to its single
250
+ # numeric field
251
+ # @param target [Symbol, String] name of the attribute, used only to word a refusal
252
+ # @return [Numeric, Runtime::Value] the product: a `Runtime::Value` when both sides are
253
+ # value objects, otherwise a bare number the caller re-wraps
254
+ # @raise [Runtime::TypeMismatch] if `amount` or `current` is not numeric, or two value
255
+ # objects do not share exactly one numeric field
256
+ # @raise [Runtime::InvariantViolation] if a value-object result breaks one of its own
257
+ # invariants
258
+ # @raise [Bluebook::Expression::EvaluationError] if the product does not fit a signed
259
+ # 64-bit Integer, or is a non-finite Float
214
260
  def multiply(current, amount, target)
215
261
  current ||= 0
216
262
 
@@ -225,16 +271,16 @@ module Hecks
225
271
  amount = unwrap_single_numeric_field(amount) if amount.is_a?(Value)
226
272
 
227
273
  unless amount.is_a?(Numeric) && current.is_a?(Numeric)
228
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_amount",
229
- op: "multiply", target: target,
230
- offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
274
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "arithmetic_amount",
275
+ op: "multiply", target: target,
276
+ offered: Rendering.describe(current.is_a?(Numeric) ? amount : current))
231
277
  end
232
278
 
233
279
  bounded(current * amount, "multiply", current, amount, "*")
234
280
  end
235
281
 
236
282
  # Vendored addition, not (yet) upstream hecks (migration plan
237
- # task 4, i106): bound the CURRENT value into `[min, max]` -- no
283
+ # task 4, i106): bound the current value into `[min, max]` -- no
238
284
  # "amount" to combine, so it does not go through
239
285
  # #arithmetic/#multiply's shared-numeric-field matching at all;
240
286
  # it clamps whichever single numeric field the wrapping value
@@ -242,12 +288,12 @@ module Hecks
242
288
  # one, per Part 3a's auto-synthesis).
243
289
  def clamp(current, bounds, target)
244
290
  min, max = bounds
245
- # THE SAME `current ||= 0` #arithmetic/#multiply both give a
246
- # PHANTOM (never-set) numeric field, one line up from each —
291
+ # The same `current ||= 0` #arithmetic/#multiply both give a
292
+ # phantom (never-set) numeric field, one line up from each —
247
293
  # this was the one arithmetic op that didn't, so a VO-typed
248
294
  # attribute with no declared `default:` (genuinely absent,
249
295
  # `Instance.defaults`/`#default_for`) hit TypeMismatch on the
250
- # FIRST clamp. (#arithmetic/#multiply's OWN absent-current gap
296
+ # first clamp. (#arithmetic/#multiply's own absent-current gap
251
297
  # was a real, separate bug this comment used to describe wrong —
252
298
  # they did not "silently treat the same absent field as zero";
253
299
  # they raised too, blaming a perfectly valid `amount` for not
@@ -257,14 +303,14 @@ module Hecks
257
303
  if current.is_a?(Value)
258
304
  fields = current.to_h
259
305
  field = fields.keys.find { |f| fields[f].is_a?(Numeric) } or
260
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
261
- op: "clamp", target: target, offered: Rendering.describe(current))
306
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "arithmetic_current",
307
+ op: "clamp", target: target, offered: Rendering.describe(current))
262
308
  return current.with(field, fields[field].clamp(min, max))
263
309
  end
264
310
 
265
311
  unless current.is_a?(Numeric)
266
- raise TypeMismatch, RefusalWording.render("TypeMismatch", "arithmetic_current",
267
- op: "clamp", target: target, offered: Rendering.describe(current))
312
+ raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "arithmetic_current",
313
+ op: "clamp", target: target, offered: Rendering.describe(current))
268
314
  end
269
315
 
270
316
  current.clamp(min, max)
@@ -275,14 +321,14 @@ module Hecks
275
321
  # `amount` arrives VO-wrapped whenever the command's own declared
276
322
  # attribute type says so (a real `Money`, not a bare Integer) —
277
323
  # true whether or not `current` has ever been set. Only meaningful
278
- # to call once `current` is known NOT to be a Value itself (the
324
+ # to call once `current` is known not to be a Value itself (the
279
325
  # `current.is_a?(Value) && amount.is_a?(Value)` branch, above in
280
326
  # both callers, already owns the case where both sides carry real
281
327
  # fields to combine). Refuses rather than guesses when more than
282
328
  # one field is numeric — genuinely ambiguous which one an absent
283
329
  # `current` should be treated as zero for, the same reasoning
284
330
  # `combine_value_object`'s own `shared_numeric.size == 1` check
285
- # already holds to when both sides ARE present.
331
+ # already holds to when both sides are present.
286
332
  def unwrap_single_numeric_field(value)
287
333
  fields = value.to_h
288
334
  numeric_fields = fields.keys.select { |field| fields[field].is_a?(Numeric) }
@@ -299,7 +345,7 @@ module Hecks
299
345
  end
300
346
  unless shared_numeric.size == 1
301
347
  raise TypeMismatch,
302
- RefusalWording.render("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
348
+ RefusalWording.render_site("TypeMismatch", "arithmetic_shared_field", op: oper, target: target)
303
349
  end
304
350
 
305
351
  field = shared_numeric.first
@@ -10,26 +10,40 @@ module Hecks
10
10
  # the one check that runs before any domain-state work, alongside the
11
11
  # argument gate rather than after it.
12
12
  #
13
- # TWO CHECKS, NOT A REPLACEMENT — ADR 0025 §9's own caution against
13
+ # **Two checks, not a replacement** — ADR 0025 §9's own caution against
14
14
  # "silently downgrading `role` to documentation" cuts both ways: a
15
- # caller who never named WHO they are (every caller before this) is
15
+ # caller who never named who they are (every caller before this) is
16
16
  # checked exactly the way it always has been, string equality
17
- # against the command's own `role`. Only a caller that ALSO binds an
17
+ # against the command's own `role`. Only a caller that also binds an
18
18
  # `actor_id` (`Hecks.as_caller(role:, actor_id:)`) reaches the
19
- # real check — a live Governance::RoleAssignment lookup through
20
- # `Ports::Authorization`, once the command's domain declares
21
- # `uses_framework "Governance"` (`HecksagonBuilder` refuses any
22
- # domain that declares a `role` and does not, at build time — see
23
- # its own `refuse_ungoverned_roles!`). An identified caller is never
19
+ # real check — a live lookup through `Ports::Authorization`, once the
20
+ # command's domain has an authorization provider: its own chapter,
21
+ # or a framework member it attaches, declaring `provides
22
+ # "authorization"` (Governance, via `uses_framework "Governance"`).
23
+ # Boot refuses a domain that declares a `role` and has no provider
24
+ # `Registry::Verification#refuse_ungoverned_roles!`. An identified caller is never
24
25
  # let back through the string fallback: a real identity that holds
25
26
  # no matching grant is refused, not waved through because it also
26
27
  # happens to type the right word.
27
28
  module Authorization
28
- # OPT-IN, on BOTH sides. No caller bound: unchecked, exactly as
29
+ # Refuses the command when the ambient caller (`Caller.current`) does not hold the role
30
+ # the command declares.
31
+ #
32
+ # Opt-in, on both sides. No caller bound: unchecked, exactly as
29
33
  # today. No role declared: unchecked too — `role` is genuinely
30
34
  # optional in this language (roughly a third of banking's own
31
35
  # commands declare none), so a command that never named a role has
32
36
  # nothing to check a caller against.
37
+ #
38
+ # @param command [Bluebook::Command] the command about to run; its `role` is the one
39
+ # required, and a nil or empty role means no check
40
+ # @param domain [String] name of the command's domain, whose authorization provider
41
+ # answers the check
42
+ # @return [nil] when no caller is bound, no role is declared, or the caller is authorized
43
+ # @raise [Runtime::Unauthorized] if an identified caller holds no live grant of the role
44
+ # where a provider is attached, or otherwise if the caller's role string differs
45
+ # @raise [Runtime::WiringError] if an identified caller is checked and zero or more than
46
+ # one adapter implements the authorization port
33
47
  def refuse_role_mismatch(command, domain)
34
48
  caller = Caller.current
35
49
  return unless caller
@@ -45,19 +59,26 @@ module Hecks
45
59
 
46
60
  return if authorized
47
61
 
48
- raise Unauthorized, RefusalWording.render("Unauthorized", "role_mismatch",
49
- command: command.hecks_name, role: command.role,
50
- caller_role: caller.role)
62
+ raise Unauthorized, RefusalWording.render_site("Unauthorized", "role_mismatch",
63
+ command: command.hecks_name, role: command.role,
64
+ caller_role: caller.role)
51
65
  end
52
66
 
53
67
  private
54
68
 
55
- # SELF-EXEMPT, like `HecksagonBuilder#refuse_ungoverned_roles!` —
56
- # Governance IS the thing a `holds_role?` lookup runs against, so
57
- # its own commands are always checked by the string fallback, not
58
- # a lookup against itself.
69
+ # **Declared, not named** — `Registry#authorization_provider_for`.
70
+ #
71
+ # The provider's own commands are looked up too, not waved through
72
+ # the string fallback: `Governance::RoleAssignment.Assign` declares
73
+ # `role "Governance administrator"`, and an identified caller
74
+ # dispatching it is checked against a live assignment of that role
75
+ # like any other gated command (ADR 0025 §9). The Rust kernel's
76
+ # `check_role` (`rust/src/kernel/repository.rs`) does the same. The
77
+ # consequence is deliberate: the first administrator grant has to
78
+ # come from a caller that binds no `actor_id` (the unchecked,
79
+ # string-compared path) — a bootstrap step, not a hole.
59
80
  def governance_attached?(domain)
60
- domain.to_s == "Governance" || registry.hecksagon(domain)&.framework_members&.include?("Governance") || false
81
+ !registry.authorization_provider_for(domain).nil?
61
82
  end
62
83
  end
63
84
  end
@@ -7,12 +7,29 @@ module Hecks
7
7
  # and — where the store can hold one — a recorded event beside the
8
8
  # data it describes.
9
9
  module Emission
10
- # `correlation` arrives HERE rather than being merged onto the
10
+ # Builds one frozen event per name the command `emits`, appends each to the registry's
11
+ # event log, and records it in the store when the repository keeps events.
12
+ #
13
+ # `correlation` arrives here rather than being merged onto the
11
14
  # event afterwards. It is part of the transaction — known from
12
15
  # `dispatch`'s own argument before anything is emitted — and an
13
16
  # event that is still being written to is not yet a record of
14
17
  # what happened. Setting it at construction is what lets the
15
18
  # event be frozen the moment it exists.
19
+ #
20
+ # @param command [Bluebook::Command] the command whose `emits` names the events
21
+ # @param domain [String] name of the emitting domain, the prefix of each event's
22
+ # `aggregate` (`"Domain::Aggregate"`)
23
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
24
+ # @param instance [Runtime::Instance] the settled record; its `id` stamps every event
25
+ # @param args [Hash{Symbol => Object}] the normalized command arguments, used as each
26
+ # event's payload and deep-frozen by this call
27
+ # @param repository [Ports::Persistence::AppendOnly] the aggregate's repository; asked to
28
+ # `record_event` only if it responds to it
29
+ # @param correlation [Hash, nil] saga correlation head => value for a saga-caused
30
+ # dispatch; nil otherwise
31
+ # @return [Array<Runtime::Event>] the emitted events in `emits` order, each frozen;
32
+ # `[]` when the command emits nothing
16
33
  def emit(command, domain, aggregate, instance, args, repository, correlation = nil)
17
34
  command.emits.map do |event_name|
18
35
  event = Event.new(