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
@@ -3,41 +3,41 @@ require_relative "../saga_pending_dispatch"
3
3
  module Hecks
4
4
  module Runtime
5
5
  class Registry
6
- # SAGA PERSISTENCE — no new DSL verb, no new world setting.
6
+ # **Saga persistence** — no new DSL verb, no new world setting.
7
7
  # Whatever adapter a domain's own aggregates already use for
8
8
  # durability, its sagas use the same one, automatically: three
9
- # OPTIONAL adapter methods (`save_saga`/`delete_saga`/`each_saga`),
9
+ # optional adapter methods (`save_saga`/`delete_saga`/`each_saga`),
10
10
  # `respond_to?`-checked the same way `Ports::Persistence::AppendOnly`
11
11
  # already treats an adapter's own optional methods
12
12
  # (`find`/`all`/`count`/`reset!`/`events`/`record_event`).
13
13
  module SagaPersistence
14
- # Resolved and memoized per domain — the FIRST real aggregate
14
+ # Resolved and memoized per domain — the first real aggregate
15
15
  # declared in the domain's own bluebook is the resolution
16
16
  # anchor. `Ports::Persistence::BindingPolicy.resolve` for that
17
17
  # aggregate already falls back to a domain-level default bind
18
18
  # (§0's `Hecksagon#bind_for`) before falling back to Memory, so
19
19
  # this needs no separate "was a default declared" branch of its
20
- # own — whatever adapter that anchor resolves to already IS the
20
+ # own — whatever adapter that anchor resolves to already is the
21
21
  # domain's own default in the normal case (every aggregate
22
22
  # shares one adapter), and is the honest, documented fallback
23
23
  # for the rarer domain genuinely split across more than one
24
24
  # local adapter with no default declared.
25
25
  #
26
- # GENUINELY LAZY, GENUINELY POST-BOOT — unlike `rehydrate_sagas!`
26
+ # Genuinely lazy, genuinely POST-boot — unlike `rehydrate_sagas!`
27
27
  # below (boot-only), this is called from live dispatch
28
28
  # (`SagaInterpreter#checkpoint`/`#end_saga`, on every saga
29
- # transition), so the FIRST call for a given domain can come from
29
+ # transition), so the first call for a given domain can come from
30
30
  # any dispatching thread, not just the boot thread. `@saga_persistence`
31
31
  # itself is a plain Hash stood up once in `Registry#initialize` (no
32
32
  # race on the container), but `resolve_saga_persistence` is real work
33
33
  # (a `BindingPolicy.resolve` plus a lazy `repository` build) whose
34
- # RESULT — the actual adapter instance a domain's sagas persist
35
- # through — must be the SAME object for every caller: two threads
34
+ # result — the actual adapter instance a domain's sagas persist
35
+ # through — must be the same object for every caller: two threads
36
36
  # racing the first lookup and each building their own adapter would
37
37
  # silently split one domain's saga writes across two adapter
38
38
  # instances (worse than `Dispatcher#reaction_depth`'s M20 — that bug
39
- # corrupted a counter; this one can corrupt WHICH STORE a saga's
40
- # state lands in). Double-checked locking against a DEDICATED mutex
39
+ # corrupted a counter; this one can corrupt which store a saga's
40
+ # state lands in). Double-checked locking against a dedicated mutex
41
41
  # — never `@saga_mutex` — see `Registry#initialize`'s own comment for
42
42
  # why reusing that one would deadlock.
43
43
  def saga_persistence(domain)
@@ -47,23 +47,23 @@ module Hecks
47
47
  end
48
48
  end
49
49
 
50
- # WALKS EVERY LOADED DOMAIN, repopulating `saga_instances` from
50
+ # Walks every loaded domain, repopulating `saga_instances` from
51
51
  # whatever `saga_persistence(domain)` resolves to — a real store
52
52
  # for a domain whose adapter answers the capability, `each_saga`
53
53
  # yielding real rows; `NULL_SAGA_STORE`'s own `each_saga` for
54
54
  # everything else, which never yields at all, so this needs no
55
- # `respond_to?` branch of its own — the SAME reason every other
55
+ # `respond_to?` branch of its own — the same reason every other
56
56
  # call site in this capability never needs one. Called once at
57
57
  # boot (`Loader.boot`, between `verify!` and dispatcher
58
58
  # construction) — a process that's been running has no reason to
59
59
  # re-walk its own already-current `saga_instances`.
60
60
  #
61
- # BOOT-TIME-ONLY (OR ITS TEST-RUNNER EQUIVALENT) — this method's
61
+ # Boot-time-only (or its test-runner equivalent) — this method's
62
62
  # only callers are `Loader.run_boot_gates!` (single-threaded, before
63
63
  # `dispatcher_for` ever exists) and `Registry#reset_runtime_state!`
64
64
  # (single-threaded test runner — see that method's own comment).
65
65
  # Never called from live dispatch, so `@saga_instances` mutation
66
- # here has no concurrent caller to race, unlike its OTHER two write
66
+ # here has no concurrent caller to race, unlike its other two write
67
67
  # points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
68
68
  # rb`), which genuinely do and are guarded accordingly.
69
69
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation
@@ -81,28 +81,28 @@ module Hecks
81
81
 
82
82
  private
83
83
 
84
- # THE OTHER HALF OF THE OUTBOX-SHAPED FIX (see saga_pending_
84
+ # The other half of the outbox-shaped fix (see saga_pending_
85
85
  # dispatch.rb) — `SAGA_PENDING_DISPATCH_KEY`, if the crashed
86
86
  # process left it standing, means the row's own `state` was
87
87
  # checkpointed but the dispatch cascade that justifies it may
88
88
  # never have run. Stripped out of `memory` before it becomes
89
- # this instance's LIVE `:memory` (so nothing downstream — a
89
+ # this instance's live `:memory` (so nothing downstream — a
90
90
  # `given`, a `with:` mapping, the fuzzer — ever sees it), and
91
- # surfaced loudly instead: a WARNING plus a `saga_log` entry,
91
+ # surfaced loudly instead: a warning plus a `saga_log` entry,
92
92
  # never an automatic redrive (see saga_pending_dispatch.rb for
93
93
  # why redriving without idempotent delivery would be worse than
94
- # the stall). This is real, durable crash-recovery VISIBILITY
94
+ # the stall). This is real, durable crash-recovery visibility
95
95
  # still not the reconciliation itself, which stays a human's
96
96
  # call until hecks has idempotent redelivery to make it safe.
97
97
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation -- same
98
98
  # justification as `rehydrate_sagas!`'s own disable comment
99
- # above: this method's ONLY caller is that boot-time-only walk,
99
+ # above: this method's only caller is that boot-time-only walk,
100
100
  # never live dispatch, so `@saga_log` has no concurrent writer
101
101
  # to race here.
102
102
  def warn_stalled_saga(domain, process_manager, correlation, state, pending)
103
103
  # `.transform_keys(&:to_sym)` — Heki's own `each_saga` only
104
- # symbolizes `memory`'s TOP-level keys (`SagaStore#each_saga`'s
105
- # own `transform_keys`, one level deep); a value NESTED under
104
+ # symbolizes `memory`'s top-level keys (`SagaStore#each_saga`'s
105
+ # own `transform_keys`, one level deep); a value nested under
106
106
  # one of those keys, like this marker, comes back with plain
107
107
  # string keys from Heki specifically, symbol keys already from
108
108
  # Postgres/SQLite/D1's own `symbolize_names: true` parse. Normalizing
@@ -21,11 +21,11 @@ module Hecks
21
21
  # A domain-level default (§0) — `persisted_by "Heki"` bare,
22
22
  # applying to whichever aggregates don't override it — names
23
23
  # no aggregate of its own, so there's nothing to look up in
24
- # the bluebook for THIS row specifically. Still validate its
24
+ # the bluebook for this row specifically. Still validate its
25
25
  # own adapter/verb shape (the same reason
26
26
  # `verify_default_adapter!` checks the framework-wide
27
27
  # default the same way, aggregate-less). Coverage of real
28
- # aggregates that only resolve THROUGH this default comes
28
+ # aggregates that only resolve through this default comes
29
29
  # from their own dispatch-time `BindingPolicy.resolve` —
30
30
  # deliberately not required to be exhaustive here, the same
31
31
  # leniency this method already extended to any aggregate
@@ -78,8 +78,8 @@ module Hecks
78
78
  "and cannot satisfy #{bind.verb}"
79
79
  end
80
80
 
81
- # THE METHOD CONTRACT A `.port` FILE'S `verb`/`signal` NEVER
82
- # CARRIED — an adapter can name the right port, satisfy the right
81
+ # The method contract a `.port` file's `verb`/`signal` never
82
+ # carried — an adapter can name the right port, satisfy the right
83
83
  # verb, and admit every `.world` setting `check_settings` checks,
84
84
  # and still be missing the one method a live dispatch will
85
85
  # actually call. `answers` is optional per port (an empty list is
@@ -99,7 +99,7 @@ module Hecks
99
99
  "#{answers.map(&:inspect).join(', ')}"
100
100
  end
101
101
 
102
- # THE NINE SINGLETON PORTS' OWN GAP — `persistence`, `projection`
102
+ # **The nine singleton ports' own gap** — `persistence`, `projection`
103
103
  # and `loading` are per-aggregate bindings, checked above through
104
104
  # every real `bind` a hexagon declares; a singleton port
105
105
  # (`clock`, `authorization`, …) is never bound to an aggregate at
@@ -108,9 +108,9 @@ module Hecks
108
108
  # already refuses zero or multiple implementations, live, at
109
109
  # first dispatch — that stays exactly as-is here (0 or 2+ is
110
110
  # ambiguity, not a method-contract question, and asserting every
111
- # declared port MUST have exactly one adapter would wrongly
111
+ # declared port must have exactly one adapter would wrongly
112
112
  # refuse a boot that simply never wires a port it doesn't use).
113
- # This only ever tightens the ONE case those checks don't cover:
113
+ # This only ever tightens the one case those checks don't cover:
114
114
  # exactly one adapter, wired, missing a method `answers` names.
115
115
  PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
116
116
 
@@ -158,13 +158,13 @@ module Hecks
158
158
 
159
159
  private
160
160
 
161
- # `role` IS REAL ACCESS CONTROL ONLY WHEN GOVERNANCE CAN CHECK IT
162
- # AGAINST SOMETHING — a command that declares a role but whose
161
+ # `role` is real access control only when governance can check it
162
+ # against something — a command that declares a role but whose
163
163
  # domain never attaches Governance would leave that role forever
164
164
  # unchecked, exactly the defect ADR 0025 §9 names ("role gates
165
165
  # access control by exact string equality ... Governance ...
166
- # connected to none of it"). Checked here, at `verify!` — RECOVERED
167
- # and MOVED, not new: this used to run per-block, at HECKSAGON
166
+ # connected to none of it"). Checked here, at `verify!` — recovered
167
+ # and moved, not new: this used to run per-block, at hecksagon
168
168
  # build time (Bluebook::DSL::HecksagonBuilder#build), which broke
169
169
  # the moment a domain could be split across multiple hecksagon
170
170
  # blocks (base + an `environments/<name>.hecksagon` overlay,
@@ -172,20 +172,21 @@ module Hecks
172
172
  # the recovery provenance): every block but the one declaring
173
173
  # `uses_framework "Governance"` would be refused there, even
174
174
  # though `Registry#add_hecksagon` merges every block for a domain
175
- # into ONE Hecksagon before anything ever dispatches against it.
176
- # Checking the MERGED result once, here, after every file for
175
+ # into one Hecksagon before anything ever dispatches against it.
176
+ # Checking the merged result once, here, after every file for
177
177
  # this domain has loaded, is both more permissive (no need to
178
178
  # repeat `uses_framework` in every file) and strictly more
179
179
  # correct (a check against an incomplete, not-yet-merged
180
180
  # hecksagon can never see the real final shape).
181
181
  #
182
- # GOVERNANCE ITSELF IS EXEMPT it cannot `uses_framework` its own
183
- # aggregates, and it IS the source of truth a role check runs
184
- # against, the same self-reference `CommandRules::Authorization
185
- # #governance_attached?` grants it at dispatch time.
182
+ # A provider is RECOGNISED by its declaration, not its name —
183
+ # `authorization_provider_for` answers for the domain's own
184
+ # chapter too, so Governance (which declares `provides
185
+ # "authorization"`) passes here because of what it declares, and
186
+ # the same rule `CommandRules::Authorization#governance_attached?`
187
+ # applies at dispatch time.
186
188
  def refuse_ungoverned_roles!(hexagon)
187
- return if hexagon.domain == "Governance"
188
- return if hexagon.framework_members.include?("Governance")
189
+ return if authorization_provider_for(hexagon.domain)
189
190
 
190
191
  bluebook_ir = bluebook(hexagon.domain)
191
192
  return unless bluebook_ir
@@ -195,12 +196,21 @@ module Hecks
195
196
 
196
197
  raise WiringError,
197
198
  "#{offender.hecks_fqn} declares role #{offender.role.inspect}, but " \
198
- "#{hexagon.domain}'s hecksagon never uses_framework \"Governance\" — role is only " \
199
- "real access control once Governance is attached to check it against; without that " \
200
- "it is silent decoration, the exact defect this refusal exists to catch"
199
+ "#{hexagon.domain}'s hecksagon never #{authorization_attachment_hint} — role is only " \
200
+ "real access control once an authorization provider is attached to check it against; " \
201
+ "without that it is silent decoration, the exact defect this refusal exists to catch"
201
202
  end
202
203
 
203
- # Every command this domain declares, an aggregate's own AND every
204
+ # The suggestion, derived from whichever framework members actually
205
+ # declare `provides "authorization"` — never a hardcoded name.
206
+ def authorization_attachment_hint
207
+ providers = Framework.providers_of(Bluebook::Capabilities::AUTHORIZATION)
208
+ return "attaches a chapter that provides \"authorization\" (no framework member declares one)" if providers.empty?
209
+
210
+ providers.map { |name| "uses_framework #{name.inspect}" }.join(" or ")
211
+ end
212
+
213
+ # Every command this domain declares, an aggregate's own and every
204
214
  # entity nested inside one — the same reach `refuse_role_mismatch`
205
215
  # itself needs at dispatch time, just walked ahead of time here.
206
216
  def commands_in(bluebook_ir)
@@ -220,7 +230,7 @@ module Hecks
220
230
  # 0025 named for an unchecked `role`, here applied to saga
221
231
  # durability instead.
222
232
  #
223
- # A WARNING, NOT A REFUSAL — unlike `refuse_ungoverned_roles!`,
233
+ # A warning, not a refusal — unlike `refuse_ungoverned_roles!`,
224
234
  # running sagas on a store with no `save_saga` is legitimate on
225
235
  # purpose in a fast in-memory test/dev boot (this project's own
226
236
  # `saga_durability_spec.rb` boots a process manager on `Memory`
@@ -228,13 +238,13 @@ module Hecks
228
238
  # refusing the boot outright would break a choice an author made
229
239
  # deliberately. What a deploy needs is for the gap to be loud and
230
240
  # undeniable, not for local dev/test to become impossible.
231
- # THE OUTBOX'S TWIN OF `warn_undurable_sagas!` — a domain that
241
+ # The outbox's twin of `warn_undurable_sagas!` — a domain that
232
242
  # declares anything a commit could owe a reaction to (a policy
233
243
  # listening to one of its events, or a process manager) but is
234
244
  # bound to an adapter with no outbox (`AppendOnly#outbox?`) gets
235
245
  # reactions the pre-outbox way: run inline, lost on a crash
236
246
  # between commit and reaction. A warning, not a refusal, for the
237
- # reason `warn_undurable_sagas!` gives — and Memory HAS an outbox
247
+ # reason `warn_undurable_sagas!` gives — and Memory has an outbox
238
248
  # (in-process, like everything else it holds), so a dev/test
239
249
  # boot stays quiet; this speaks up for the file/remote adapters
240
250
  # that persist state durably but hand reactions to nothing.
@@ -29,8 +29,8 @@ module Hecks
29
29
  @event_log = []
30
30
  @reaction_log = []
31
31
  @saga_log = []
32
- # ADDITIVE, RUBY-ONLY — never merged into saga_log/reaction_log.
33
- # rust/src/kernel/orchestrate.rs ports THOSE two arrays' exact
32
+ # **Additive, Ruby-only** — never merged into saga_log/reaction_log.
33
+ # rust/src/kernel/orchestrate.rs ports those two arrays' exact
34
34
  # shape byte-for-byte (spec/rust_conformance_spec.rb's own
35
35
  # equality check) — a landmine found by reading that spec before
36
36
  # touching anything, not by hitting it. These carry the raw
@@ -43,7 +43,7 @@ module Hecks
43
43
  @saga_dispatch_log = []
44
44
  @policy_dispatch_log = []
45
45
  @saga_instances = Hash.new { |h, k| h[k] = {} }
46
- # GUARDS `saga_instances`' OWN mutation+checkpoint sequence
46
+ # Guards `saga_instances`' own mutation+checkpoint sequence
47
47
  # (`SagaInterpreter`'s 4 write points, §7) — the same shape of
48
48
  # hazard this codebase's own prior audit already flagged for
49
49
  # `Dispatcher#reenter`'s reaction-depth counter (M20: a
@@ -51,7 +51,7 @@ module Hecks
51
51
  # `Thread.current`, dispatcher.rb), made meaningfully easier to
52
52
  # hit here once a persistence write sits in the same critical
53
53
  # section. Held across the in-memory
54
- # mutation AND the checkpoint write together, never across a
54
+ # mutation and the checkpoint write together, never across a
55
55
  # saga's own dispatch cascade — see `SagaInterpreter#advance_saga`'s
56
56
  # own comment for why that distinction matters (non-reentrant
57
57
  # Mutex, recursive re-entry is real).
@@ -59,18 +59,18 @@ module Hecks
59
59
  @repositories = {}
60
60
  @projection_repositories = {}
61
61
  @bluebook_builders = {}
62
- # EAGER, NOT LAZY — see `#resolved_eras`'s own comment for why. Built
62
+ # **Eager, not lazy** — see `#resolved_eras`'s own comment for why. Built
63
63
  # here rather than `@resolved_eras ||= {}` on first access so there is
64
64
  # no window, post-boot, where two concurrently dispatching threads
65
65
  # could race creating this Hash (Hecks/ThreadSharedIvarMutation; the
66
66
  # same shape of hazard `Dispatcher#reenter`'s `@reaction_depth` was
67
- # fixed for). Every WRITE into it still only ever happens at boot,
67
+ # fixed for). Every write into it still only ever happens at boot,
68
68
  # single-threaded (`EraResolver.check!`, a `:pre_verify` boot gate) —
69
69
  # this only removes the race on standing up the container itself for
70
70
  # a boot with no era-plugin domain at all, whose first touch would
71
71
  # otherwise be a live dispatch's own `RepositoryFactory.build` read.
72
72
  @resolved_eras = {}
73
- # THE SIBLING FACT `EraResolver.check!` records for an OLD checkout:
73
+ # The sibling fact `EraResolver.check!` records for an old checkout:
74
74
  # domain name -> the newest held ordinal that superseded the era this
75
75
  # boot resolved to; absent for every domain booting the current era.
76
76
  # `RepositoryFactory.build` hands it to the adapter as
@@ -79,7 +79,7 @@ module Hecks
79
79
  # half that holds even for a connection row-level security cannot
80
80
  # bite (BUG#24). Eager for exactly the reason `@resolved_eras` is.
81
81
  @superseded_eras = {}
82
- # EAGER, NOT LAZY — see `#capability_graph`'s own comment for why.
82
+ # **Eager, not lazy** — see `#capability_graph`'s own comment for why.
83
83
  # `CapabilityGraph.new` only stores the registry reference; there is
84
84
  # no reason to defer it, and doing so removes the exact same
85
85
  # first-access race `#resolved_eras` above does, while preserving the
@@ -87,10 +87,10 @@ module Hecks
87
87
  # spec.rb` already requires.
88
88
  @capability_graph = CapabilityGraph.new(self)
89
89
  # `@saga_persistence` itself is eager (see `#saga_persistence`'s own
90
- # comment) — only the PER-DOMAIN resolution inside it is genuinely
91
- # expensive and lazy, guarded by this dedicated mutex. NOT the same
90
+ # comment) — only the per-domain resolution inside it is genuinely
91
+ # expensive and lazy, guarded by this dedicated mutex. Not the same
92
92
  # mutex as `@saga_mutex`: `checkpoint` (saga_interpreter.rb) calls
93
- # `saga_persistence(domain)` from INSIDE an `@saga_mutex.synchronize`
93
+ # `saga_persistence(domain)` from inside an `@saga_mutex.synchronize`
94
94
  # block, so reusing `@saga_mutex` here would deadlock the very first
95
95
  # time a saga advanced (a `Mutex` is not reentrant — the exact
96
96
  # warning `@saga_mutex`'s own comment already gives for a different
@@ -100,23 +100,23 @@ module Hecks
100
100
  @outbox = Outbox::Relay.new(self)
101
101
  end
102
102
 
103
- # THE OUTBOX RELAY — one per registry, for its whole life (built
103
+ # **The outbox relay** — one per registry, for its whole life (built
104
104
  # here, never swapped, so no thread ever sees a different one).
105
105
  # It can enqueue from the moment the registry exists; a Dispatcher
106
106
  # attaches the interpreters that let it deliver. See
107
107
  # `Runtime::Outbox`.
108
108
  attr_reader :outbox
109
109
 
110
- # THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter
110
+ # The builder stays open for the life of this registry, keyed by chapter
111
111
  # name — see the comment on `BluebookBuilder.build`. A chapter split across
112
112
  # several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
113
- # needs its declarations to accumulate into ONE builder rather than each
113
+ # needs its declarations to accumulate into one builder rather than each
114
114
  # file minting its own and silently discarding the one before.
115
115
  def bluebook_builder(name)
116
116
  @bluebook_builders[name.to_s] ||= yield
117
117
  end
118
118
 
119
- # BOOT-TIME-ONLY, SINGLE-THREADED — every `add_*` below (through
119
+ # Boot-time-only, single-threaded — every `add_*` below (through
120
120
  # `add_translation`) is called exclusively from `Hecks.collect`
121
121
  # (hecks.rb), which is what `Hecks.bluebook`/`.hecksagon`/`.port`/
122
122
  # `.adapter`/`.world`/`.translation` run inside while a `.bluebook`/
@@ -132,11 +132,11 @@ module Hecks
132
132
  # rubocop:disable Hecks/ThreadSharedIvarMutation
133
133
  def add_bluebook(item) = @bluebooks[item.name] = item
134
134
 
135
- # MERGED, NOT REPLACEDRECOVERED, not new (see Runtime::Loader
135
+ # Merged, not replacedrecovered, not new (see Runtime::Loader
136
136
  # .boot's own comment for the provenance). A domain's hecksagon can
137
137
  # now load in more than one block for the same domain (base file
138
138
  # plus an `environments/<name>.hecksagon` overlay), and the second
139
- # block should ADD to what the first declared, not silently
139
+ # block should add to what the first declared, not silently
140
140
  # discard it.
141
141
  def add_hecksagon(item)
142
142
  existing = @hecksagons[item.domain]
@@ -146,7 +146,7 @@ module Hecks
146
146
  def add_port(item) = @ports[item.name] = item
147
147
  def add_adapter(item) = @adapters[item.name] = item
148
148
 
149
- # MERGED, NOT REPLACED — the same generalization for `World` that
149
+ # Merged, not replaced — the same generalization for `World` that
150
150
  # `add_hecksagon` above recovers for `Hecksagon`: an
151
151
  # `environments/<name>.world` overlay (or a host-owned tenancy
152
152
  # overlay world, same mechanism) can now add or override settings
@@ -165,7 +165,7 @@ module Hecks
165
165
  # rubocop:enable Hecks/ThreadSharedIvarMutation
166
166
 
167
167
  # {domain name => era ordinal} as resolved by the boot-time era
168
- # gate. A lineage adapter writes into ITS OWN era's partition —
168
+ # gate. A lineage adapter writes into its own era's partition —
169
169
  # which, for an old checkout booting a held-but-superseded shape,
170
170
  # is not the newest one. The Hash itself is stood up in `initialize`
171
171
  # (see that comment) — this is a plain reader, not a memoizer;
@@ -182,12 +182,29 @@ module Hecks
182
182
 
183
183
  def verbs = @bluebooks.values.flat_map(&:verbs).sort
184
184
 
185
+ # The chapter that answers a role check for `domain` — the domain's
186
+ # own chapter, or any framework member its hecksagon attaches, that
187
+ # declares `provides "authorization"`. Nil when none does. Replaces
188
+ # every check for the literal name "Governance": Governance is
189
+ # recognised by what it declares, and a chapter that declares the
190
+ # same thing is recognised the same way.
191
+ def authorization_provider_for(domain)
192
+ names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
193
+ names.filter_map { |name| bluebook(name) }
194
+ .find { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
195
+ end
196
+
197
+ # Every loaded chapter declaring `provides "authorization"`.
198
+ def authorization_providers
199
+ @bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
200
+ end
201
+
185
202
  def repository(domain, aggregate)
186
203
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
187
204
  end
188
205
 
189
- # EVERYTHING A DISPATCH WROTE, CLEARED; NOTHING A BOOT DECLARED,
190
- # TOUCHED. Bluebooks, hecksagons, ports, adapters, worlds and the
206
+ # Everything a dispatch wrote, cleared; nothing a boot declared,
207
+ # touched. Bluebooks, hecksagons, ports, adapters, worlds and the
191
208
  # resolved eras are what loading the files produced and stay as
192
209
  # they are; the logs, the saga instances and the repositories are
193
210
  # what running commands against them produced, and go back to
@@ -204,8 +221,8 @@ module Hecks
204
221
  # boot, 76 chess behaviours = two and a half minutes of booting the
205
222
  # same two files — can now boot once and reset between tests.
206
223
  #
207
- # SINGLE-THREADED CALLER, THE SAME REASON THE `add_*` CLUSTER ABOVE
208
- # IS EXEMPT — `Behaviors::Expectations.run_one` is this method's ONLY
224
+ # Single-threaded caller, the same reason the `add_*` cluster above
225
+ # is exempt — `Behaviors::Expectations.run_one` is this method's only
209
226
  # caller (verified by grep before writing this), and it runs one
210
227
  # test at a time: `Runner#run` maps over tests sequentially, and
211
228
  # `Behaviors.rspec`'s generated examples run under RSpec's own
@@ -229,7 +246,7 @@ module Hecks
229
246
 
230
247
  # Built eagerly in `initialize` (see that comment) — this is a plain
231
248
  # reader, not a memoizer; `spec/runtime/capability_graph_spec.rb`
232
- # asserts the SAME instance comes back every call, which this still
249
+ # asserts the same instance comes back every call, which this still
233
250
  # gives, just without a lazy `||=` race on standing it up.
234
251
  attr_reader :capability_graph
235
252
 
@@ -262,9 +279,9 @@ module Hecks
262
279
  false
263
280
  end
264
281
 
265
- # RECOVERED — see `add_hecksagon`'s own comment for provenance.
282
+ # Recovered — see `add_hecksagon`'s own comment for provenance.
266
283
  # Concatenates every list-shaped fact; `binds` in particular is
267
- # additive because an overlay REBINDING an aggregate (a new
284
+ # additive because an overlay rebinding an aggregate (a new
268
285
  # `persisted_by` for the same aggregate/verb) is meant to shadow
269
286
  # the base's own bind at resolution time, not erase it outright —
270
287
  # `Ports::Persistence::BindingPolicy.resolve`'s own "exactly one
@@ -281,11 +298,11 @@ module Hecks
281
298
  )
282
299
  end
283
300
 
284
- # RECOVERED AND GENERALIZED — see `add_world`'s own comment. `realm`/
301
+ # Recovered and generalized — see `add_world`'s own comment. `realm`/
285
302
  # `latest` are scalars, so the overlay's value wins when present,
286
303
  # else the base's survives; `settings` is a shallow merge keyed by
287
304
  # verb (and `"verb:adapter"`) — an overlay entry for a key the base
288
- # also declares REPLACES that key's whole resolved hash (the same
305
+ # also declares replaces that key's whole resolved hash (the same
289
306
  # all-or-nothing shape `WorldBuilder#method_missing` already builds
290
307
  # each entry as), it does not deep-merge field by field within it.
291
308
  def merge_worlds(base, overlay)
@@ -9,19 +9,19 @@ require_relative "../ports/persistence/remote_runtime"
9
9
 
10
10
  module Hecks
11
11
  module Runtime
12
- # THE WRITE-SIDE HALF OF LAMBDA ROUTING — `Runtime::Loader.boot`
13
- # constructs this INSTEAD OF `Dispatcher` when the booted domain's
12
+ # **The write-side half of lambda routing** — `Runtime::Loader.boot`
13
+ # constructs this instead of `Dispatcher` when the booted domain's
14
14
  # own `.world` declares Lambda routing (see loader.rb's own
15
15
  # `dispatcher_class_for`). Matches `Dispatcher`'s public shape
16
- # closely enough that everything built ON TOP of a dispatcher —
16
+ # closely enough that everything built on top of a dispatcher —
17
17
  # `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
18
18
  # at all: `Handle#run`'s entire contract is
19
19
  # `@dispatcher.dispatch("#{fqn}.#{command}", **identity, **args).instance.state`,
20
20
  # and both classes answer that identically.
21
21
  #
22
- # READS DELEGATE, WRITES DON'T. `query`/`reference_query` hand off
23
- # to a REAL `Dispatcher` built over the SAME registry — since that
24
- # registry's own repositories are ALREADY Lambda-backed
22
+ # **Reads delegate, writes don't**. `query`/`reference_query` hand off
23
+ # to a real `Dispatcher` built over the same registry — since that
24
+ # registry's own repositories are already Lambda-backed
25
25
  # (`Adapters::Lambda`, `persisted_by("Lambda")`, Phase 2's other
26
26
  # half), the inherited query machinery (`QueryInterpreter`,
27
27
  # `Ports::Query::InMemory.execute`) works completely unchanged, no
@@ -37,43 +37,58 @@ module Hecks
37
37
 
38
38
  attr_reader :registry
39
39
 
40
- def initialize(registry, region: "us-east-1")
40
+ def initialize(registry, region: "us-east-1", function: nil)
41
41
  @registry = registry
42
42
  # `File.basename(registry.root)`, not `bluebooks.keys.first` —
43
43
  # matches `Adapters::Lambda`'s own function-name resolution
44
44
  # exactly (see its own comment on why: one merged Lambda per
45
45
  # deploy, not one per attached chapter, and `root` is the one
46
46
  # signal every bluebook in this registry shares regardless of
47
- # which one attached it) — INCLUDING that same adapter's own
47
+ # which one attached it) — including that same adapter's own
48
48
  # `ENV["DOMAIN_NAME"]`-first fix: `root` is always `/var/task`
49
49
  # inside a deployed Lambda, giving "task" instead of the real
50
50
  # domain name (a real, live AccessDeniedException on
51
51
  # "hecks-task" caught this).
52
- @client = Adapters::Lambda::Client.new(domain: ENV["DOMAIN_NAME"] || File.basename(registry.root), region: region)
53
- # READ-SIDE DELEGATE ONLY (see class comment) never dispatched
52
+ # `function:` the `.world`'s own `dispatched_by("Lambda")`
53
+ # naming of which function this is, for a deployment whose stack
54
+ # name isn't `hecks-<domain>` (Client's own comment has the real
55
+ # case). Absent, the resolution above is unchanged.
56
+ @client = Adapters::Lambda::Client.new(domain: ENV["DOMAIN_NAME"] || File.basename(registry.root),
57
+ region: region, function: function)
58
+ # Read-side delegate only (see class comment) — never dispatched
54
59
  # through; a real Dispatcher's own `query`/`reference_query`
55
60
  # already resolve generically via `registry.repository(...)`,
56
61
  # so building one here reuses that instead of duplicating it.
57
62
  @local = Dispatcher.new(registry)
58
63
  end
59
64
 
65
+ # Same deprecation as `Dispatcher#dispatch` — loose keyword facts warn;
66
+ # `to:`/`with:` do not.
60
67
  def dispatch(verb, saga_correlation: nil, **args)
68
+ Dispatcher.deprecate_loose_facts(args.except(:to, :with))
69
+ dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
70
+ end
71
+
72
+ # Same flat-facts wire form as `Dispatcher#dispatch_flat`.
73
+ def dispatch_flat(verb, args = {})
74
+ args = args.dup
75
+ saga_correlation = args.delete(:saga_correlation)
61
76
  domain, aggregate_name, = Naming.split_verb(verb) ||
62
77
  raise(UnknownVerb,
63
- RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
78
+ RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
64
79
  aggregate = @registry.bluebook(domain)&.aggregate(aggregate_name) ||
65
80
  raise(UnknownVerb,
66
- RefusalWording.render("UnknownVerb", "no_aggregate", domain: domain, aggregate: aggregate_name.inspect))
81
+ RefusalWording.render_site("UnknownVerb", "no_aggregate", domain: domain, aggregate: aggregate_name))
67
82
 
68
- # NOT EVERY AGGREGATE IN A LAMBDA-ROUTED DOMAIN IS ITSELF
69
- # LAMBDA-BOUND — Member's real name->email rekey carries a
83
+ # Not every aggregate in a lambda-routed domain is itself
84
+ # lambda-bound — Member's real name->email rekey carries a
70
85
  # `compute` rule (era_check.rb's own `check_compute_rules!`),
71
86
  # which can only ever run against Postgres, permanently. Its
72
- # OWN `.hecksagon` bind stays "Postgres" even when
87
+ # own `.hecksagon` bind stays "Postgres" even when
73
88
  # `dispatched_by("Lambda")` is on for everything else — checked
74
- # here by real CAPABILITY (`Ports::Persistence::RemoteRuntime`,
89
+ # here by real capability (`Ports::Persistence::RemoteRuntime`,
75
90
  # §1), not by comparing the adapter's own name to the string
76
- # "Lambda" — a bind resolves to whatever adapter CLASS actually
91
+ # "Lambda" — a bind resolves to whatever adapter class actually
77
92
  # backs it, and only a class shaped like "the real interpreter
78
93
  # lives behind a call boundary" forwards here; anything else
79
94
  # (Postgres, Memory, any future local adapter) falls through to
@@ -82,7 +97,7 @@ module Hecks
82
97
  # all.
83
98
  adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
84
99
  unless @registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
85
- return @local.dispatch(verb, saga_correlation: saga_correlation, **args)
100
+ return @local.dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
86
101
  end
87
102
 
88
103
  response = @client.dispatch(verb, args)
@@ -90,14 +105,14 @@ module Hecks
90
105
  refusal = response.fetch("refusals", []).find { |r| r["verb"] == verb }
91
106
  raise RemoteRefusal, "#{verb} refused: #{refusal['error']}" if refusal
92
107
 
93
- # THIS STEP'S OWN mutations — `mutations` is one entry per
108
+ # This step's own mutations — `mutations` is one entry per
94
109
  # replayed step (rust/host's rehydrate-and-replay design,
95
110
  # Phase 1), so `.last` is exactly the step just dispatched.
96
111
  # Matched by fully-qualified aggregate name, not just "the
97
- # first mutation" — a command whose reaction ALSO mutates a
112
+ # first mutation" — a command whose reaction also mutates a
98
113
  # different aggregate (a policy, a saga leg) puts more than
99
114
  # one mutation in the same step, and the direct effect of
100
- # THIS verb is the one this dispatch's own caller expects
115
+ # this verb is the one this dispatch's own caller expects
101
116
  # `.instance` to be.
102
117
  fqn = "#{domain}::#{aggregate.hecks_name}"
103
118
  mutation = response.fetch("mutations", []).last&.find { |m| m["aggregate"] == fqn } ||
@@ -113,9 +128,9 @@ module Hecks
113
128
  def query(verb, **args) = @local.query(verb, **args)
114
129
  def reference_query(verb, **args) = @local.reference_query(verb, **args)
115
130
 
116
- # THE FULL DOMAIN'S EVENT HISTORY, on every call — `{"read":
131
+ # The full domain's event history, on every call — `{"read":
117
132
  # true}` replays the whole journal (Phase 1's `dispatch::read`),
118
- # so its own `events` array already IS the complete log, the
133
+ # so its own `events` array already is the complete log, the
119
134
  # same thing `@registry.event_log` would answer for a local
120
135
  # dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
121
136
  # are not called in this codebase's own hot paths today: if that