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
@@ -24,9 +24,9 @@ module Hecks
24
24
 
25
25
  private
26
26
 
27
- # ROOT-FIRST, THEN THE SQLITE ESCAPE HATCH, THEN THE JOIN LOOP
27
+ # **Root-first, then the SQLite escape hatch, then the join loop**
28
28
  # each step's own comment names a real, previously-shipped bug the
29
- # current ORDER fixes (the reference/TenantScope refusal ordering
29
+ # current order fixes (the reference/TenantScope refusal ordering
30
30
  # above, the root-first head processing below). Splitting this
31
31
  # into smaller methods would scatter that ordering across method
32
32
  # boundaries where a future editor could silently break it, and
@@ -40,7 +40,7 @@ module Hecks
40
40
  def project(domain, model, args)
41
41
  bluebook = @registry.bluebook(domain)
42
42
  rootless = model.reference_target.nil?
43
- # BEFORE the adapter early-return below, so the SQLite path inherits it.
43
+ # Before the adapter early-return below, so the SQLite path inherits it.
44
44
  # Without this a stale caller passing a wrapped reference gets a
45
45
  # path-dependent answer — an adapter could quietly open the wrapped
46
46
  # reference while the in-process path reads it whole and finds nothing.
@@ -49,16 +49,16 @@ module Hecks
49
49
  # wrong at all.
50
50
  refuse_object_reference(model, args) unless rootless
51
51
  reference_id = reference(args.fetch(model.reference_name)) unless rootless
52
- # Computed off the ORIGINAL model, before TenantScope wraps it — the
52
+ # Computed off the original model, before TenantScope wraps it — the
53
53
  # "which head(s) do options apply to" question is about what the
54
54
  # bluebook author declared, not about the synthetic tenant clause
55
55
  # the wrapper adds underneath. Plural (ADR 0055) — `on:` lets more
56
56
  # than one many-side head be eligible at once.
57
57
  eligible = model.filtered_head_names
58
58
  model = TenantScope.apply(model, args)
59
- # A ROOTLESS, `group_by`-declared, or `count`/`median`-declared
59
+ # A rootless, `group_by`-declared, or `count`/`median`-declared
60
60
  # model skips the SQLite native escape hatch entirely (there is
61
- # no root aggregate to look up a repository FOR when rootless,
61
+ # no root aggregate to look up a repository for when rootless,
62
62
  # and `query_read_model` knows nothing about grouping or
63
63
  # reducing) — always runs the in-process loop below instead.
64
64
  # Correct everywhere ; not SQL-pushed-down for a SQLite-backed
@@ -73,17 +73,17 @@ module Hecks
73
73
  end
74
74
  end
75
75
 
76
- # ROOT FIRST, ALWAYS — regardless of `include` order in the
76
+ # **Root first, always** — regardless of `include` order in the
77
77
  # bluebook. `read_model_builder.rb`'s own `include` is
78
78
  # documented "Order-independent" (the `:many` flag is resolved
79
79
  # at build time, once `@reference_target` is known), but that
80
- # promise was never kept HERE: this loop used to run heads in
80
+ # promise was never kept here: this loop used to run heads in
81
81
  # their literal declared order and match each "many" head
82
- # against whatever was ALREADY in `projected` — empty, the
82
+ # against whatever was already in `projected` — empty, the
83
83
  # very first time through, if a many-side head happened to be
84
84
  # declared before the root. A real, live bug (not a guess):
85
85
  # `include Promotion` before `include Item` on a read model
86
- # whose root IS Item silently returned an empty array for
86
+ # whose root is Item silently returned an empty array for
87
87
  # Promotion — no error, just a wrong, too-small answer — while
88
88
  # the reverse order worked purely by accident. `partition`,
89
89
  # not `sort_by`: Ruby's `sort_by` is not guaranteed stable,
@@ -97,14 +97,14 @@ module Hecks
97
97
  [fetch(bluebook, domain, head[:aggregate], reference_id)]
98
98
  elsif rootless
99
99
  # No root to FK-match against — a rootless model reads
100
- # each of its own heads WHOLE, independently. Multiple
101
- # heads on one rootless model are NEVER cross-joined
100
+ # each of its own heads whole, independently. Multiple
101
+ # heads on one rootless model are never cross-joined
102
102
  # against each other, and there is no DSL to declare
103
103
  # one if you wanted to — `ReadModelBuilder#include_impl`
104
104
  # takes only `type`/`as:` (checked directly, not
105
105
  # assumed), and `group_by` groups this bulk read's own
106
106
  # output, it names no predicate between two heads.
107
- # Building a cross-join here would mean CHOOSING a join
107
+ # Building a cross-join here would mean choosing a join
108
108
  # semantics (equality on which fields?) nobody has
109
109
  # declared — a real, deliberate scope limit pending a
110
110
  # future `include ..., joins: ...`-shaped grammar
@@ -124,7 +124,7 @@ module Hecks
124
124
  projected << { aggregate: head[:aggregate], rows: rows }
125
125
  rows_by_as[head[:as]] = head[:many] ? rows : rows.first
126
126
  end
127
- # Declared order preserved in the OUTPUT — only the
127
+ # Declared order preserved in the output — only the
128
128
  # computation above needed reordering, not what a caller sees
129
129
  # back.
130
130
  heads = model.aggregate_heads.to_h { |head| [head[:as], rows_by_as[head[:as]]] }
@@ -145,9 +145,9 @@ module Hecks
145
145
  end]
146
146
  end
147
147
 
148
- # THE ROOT-FIRST FIX'S OWN FIX — root-first alone only reaches one
148
+ # **The root-first fix's own fix** — root-first alone only reaches one
149
149
  # level: it guarantees the root is in `projected` before any other
150
- # head is matched, but a CHAIN of non-root heads (a head that
150
+ # head is matched, but a chain of non-root heads (a head that
151
151
  # references another non-root head, not the root) is still
152
152
  # matched against whatever declaration order happened to put in
153
153
  # `projected` so far. `include Coupon` before `include Promotion`
@@ -160,9 +160,9 @@ module Hecks
160
160
  # to declare `include` in dependency order (the same promise
161
161
  # `read_model_builder.rb` already makes and this file is the one
162
162
  # place obligated to keep), but by topologically sorting the
163
- # non-root heads on their OWN declared reference fields before
163
+ # non-root heads on their own declared reference fields before
164
164
  # this method's runtime matching ever runs — Kahn's algorithm,
165
- # picking ready heads in DECLARED order at each step so declaring
165
+ # picking ready heads in declared order at each step so declaring
166
166
  # order still governs whenever there is no dependency to break a
167
167
  # tie. This generalizes root-first (a chain of length 1) to a
168
168
  # chain of any depth, and to a head depending on more than one
@@ -192,13 +192,13 @@ module Hecks
192
192
  ordered
193
193
  end
194
194
 
195
- # Which OTHER declared (non-root) heads a head's own aggregate
195
+ # Which other declared (non-root) heads a head's own aggregate
196
196
  # holds a reference field toward — the same relationship this
197
197
  # file's runtime matching checks record-by-record, asked here
198
198
  # statically, once, to order heads before any record is read.
199
199
  #
200
200
  # `head[:aggregate]` names whatever `include` was given — and
201
- # `include` accepts a nested ENTITY (Member, nested under
201
+ # `include` accepts a nested entity (Member, nested under
202
202
  # ValueObject ; Handler and Dispatch, nested under ProcessManager
203
203
  # — bluebook.bluebook's own `WholeBluebook` read model includes
204
204
  # all three) just as readily as a top-level aggregate.
@@ -220,7 +220,7 @@ module Hecks
220
220
  .map { |other| other[:aggregate] }
221
221
  end
222
222
 
223
- # `group_by`'s own declared fields, checked against the ONE
223
+ # `group_by`'s own declared fields, checked against the one
224
224
  # many-side head they apply to (`seal_group_by` already refuses
225
225
  # zero or several) — resolved here, once, rather than re-derived
226
226
  # per row. Raises loudly on a typo'd field name rather than
@@ -232,15 +232,15 @@ module Hecks
232
232
  aggregate = bluebook.aggregate(target[:aggregate])
233
233
  model.group_by_fields.each do |field|
234
234
  next if aggregate.attribute(field)
235
- # THE LIFECYCLE FIELD IS A FIELD, and refusing it here was a drift
235
+ # The lifecycle field is a field, and refusing it here was a drift
236
236
  # between two halves of the same language: `where(status: "logged")`
237
237
  # has always been legal on the same aggregate, because a lifecycle
238
238
  # state is stored on the record like anything else — it is simply
239
239
  # declared by `lifecycle :status` rather than by `attribute`.
240
240
  #
241
241
  # It is also the grouping anybody actually wants. "How are we doing"
242
- # over a bug ledger IS the count per status, and a report that could
243
- # group by every field EXCEPT that one could not answer the question
242
+ # over a bug ledger is the count per status, and a report that could
243
+ # group by every field except that one could not answer the question
244
244
  # reports exist for.
245
245
  next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
246
246
 
@@ -253,7 +253,7 @@ module Hecks
253
253
 
254
254
  # One level of nesting per field, in `group_by`'s own declared
255
255
  # order — the leaf is the row with every grouped field removed
256
- # (already spent, as the keys that reached it). ASSUMES the full
256
+ # (already spent, as the keys that reached it). Assumes the full
257
257
  # `group_by` path uniquely identifies one row (true for grouping by
258
258
  # an aggregate's own full identity, ConsoleSettings' own real use)
259
259
  # — `leaves.first` silently keeps only the first row when several
@@ -263,9 +263,9 @@ module Hecks
263
263
  def nest(rows, fields)
264
264
  field, *rest = fields
265
265
  rows.group_by { |row| row[field] }.transform_values do |group|
266
- # Strip ONLY the field just grouped by, not the whole remaining
266
+ # Strip only the field just grouped by, not the whole remaining
267
267
  # list — `rest`'s own fields have to survive into the recursive
268
- # call below, or the NEXT level groups by a key that's already
268
+ # call below, or the next level groups by a key that's already
269
269
  # gone (found by trying it: a two-field group_by's own second
270
270
  # level came back keyed `nil` for every group, every time).
271
271
  stripped = group.map { |row| row.reject { |key, _| key == field } }
@@ -273,7 +273,7 @@ module Hecks
273
273
  end
274
274
  end
275
275
 
276
- # `count`/`median`'s own declared target — the SAME single
276
+ # `count`/`median`'s own declared target — the same single
277
277
  # many-side head `group_by_target` resolves, for the same reason
278
278
  # (`seal_aggregation` already refuses zero or several many-side
279
279
  # heads, and refuses count/median declared alongside group_by, so
@@ -303,12 +303,12 @@ module Hecks
303
303
  target
304
304
  end
305
305
 
306
- # THE STANDARD DEFINITION. An ODD count's median is its one true
307
- # middle value, sorted ; an EVEN count's median is the AVERAGE of
306
+ # **The standard definition**. An odd count's median is its one true
307
+ # middle value, sorted ; an even count's median is the average of
308
308
  # its two middle values — the common convention (as opposed to,
309
309
  # say, always taking the lower of the two), and the one this
310
310
  # session's own task named explicitly as the deliberate choice.
311
- # An EMPTY collection has no median: nil, not zero, so a caller
311
+ # An empty collection has no median: nil, not zero, so a caller
312
312
  # cannot mistake "nothing to average" for "the values averaged to
313
313
  # zero". Reuses `Ports::Query::InMemory`'s own field reading
314
314
  # (`FieldPath.dig` + `comparable`) — the same unwrap `where`/
@@ -326,8 +326,8 @@ module Hecks
326
326
 
327
327
  def fetch(bluebook, domain, aggregate_name, id)
328
328
  @registry.read_repository(domain, bluebook.aggregate(aggregate_name)).find(id) ||
329
- raise(NotFound, RefusalWording.render("NotFound", "read_model_reference_missing",
330
- aggregate: aggregate_name, offered: Rendering.describe(id)))
329
+ raise(NotFound, RefusalWording.render_site("NotFound", "read_model_reference_missing",
330
+ aggregate: aggregate_name, offered: Rendering.describe(id)))
331
331
  end
332
332
 
333
333
  def records(bluebook, domain, aggregate_name)
@@ -335,27 +335,27 @@ module Hecks
335
335
  aggregate ? @registry.read_repository(domain, aggregate).all : []
336
336
  end
337
337
 
338
- # A stored reference holds the target's id inside the REFERENCE
339
- # ATTRIBUTE's own declared shape, so reading it is reading that shape —
338
+ # A stored reference holds the target's id inside the reference
339
+ # attribute's own declared shape, so reading it is reading that shape —
340
340
  # a different thing from the identity unwrap that was removed. An
341
- # IDENTITY is declared as a path and followed (Runtime::Identity) ; a
341
+ # identity is declared as a path and followed (Runtime::Identity) ; a
342
342
  # reference has no path of its own, and `Value.scalar` refuses a
343
343
  # composite rather than guessing which field was meant.
344
344
  #
345
345
  # Storing the scalar itself would remove this reading altogether. That
346
- # is a change to how references are STORED, not to how identities are
346
+ # is a change to how references are stored, not to how identities are
347
347
  # declared, so it is not made here.
348
348
  # An ask names itself where a command would name itself, and says the
349
349
  # same thing about the same shape. `Value.refuse_object_reference` is
350
- # not reused because it speaks of a COMMAND and its attribute ; a read
350
+ # not reused because it speaks of a command and its attribute ; a read
351
351
  # model has a query name and one declared reference.
352
352
  def refuse_object_reference(model, args)
353
353
  offered = args.fetch(model.reference_name, nil)
354
354
  return unless offered.is_a?(Hash) || offered.is_a?(Value)
355
355
 
356
356
  raise TypeMismatch,
357
- RefusalWording.render("TypeMismatch", "read_model_object_reference",
358
- query: model.query_name, field: model.reference_name)
357
+ RefusalWording.render_site("TypeMismatch", "read_model_object_reference",
358
+ query: model.query_name, field: model.reference_name)
359
359
  end
360
360
 
361
361
  # A reference is the id, in the argument and in the stored row alike.
@@ -1,15 +1,15 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # THE OUT-OF-BAND HALF OF `projects` (S12, ADR 0025 — "Consistency
3
+ # The out-of-band half of `projects` (S12, ADR 0025 — "Consistency
4
4
  # across aggregate boundaries"). A projected field is never written
5
5
  # by the command that reads it — nothing at dispatch time takes a
6
6
  # live cross-aggregate read the way `CommandRules::References
7
7
  # #dereference` still does — so this is the one place a projected
8
8
  # field's value actually gets copied over: walk every record of the
9
- # OWNING aggregate, resolve each of its own `projected_fields`
9
+ # owning aggregate, resolve each of its own `projected_fields`
10
10
  # through the reference it names, and `save` the local copy.
11
11
  #
12
- # EXPLICIT AND CALLABLE, NOT AUTOMATIC — no on-boot detection of a
12
+ # **Explicit and callable, not automatic** — no on-boot detection of a
13
13
  # freshly-declared `projects` with no held-era precedent, no
14
14
  # generated `Policy#for_each` reaction keeping it live in real
15
15
  # time as the target changes. Both are real extensions this same
@@ -19,7 +19,7 @@ module Hecks
19
19
  # needs — proven to work end to end before either automatic
20
20
  # trigger is built on top of it.
21
21
  #
22
- # NEEDS NO NEW ADAPTER CAPABILITY. `find`/`all`/`save` are the same
22
+ # **Needs no new adapter capability**. `find`/`all`/`save` are the same
23
23
  # three primitives every real adapter already answers identically
24
24
  # (`Ports::Persistence::AppendOnly#save` — append, then project,
25
25
  # the same for Memory/Postgres/SQLite/D1/Heki) — confirmed by
@@ -13,12 +13,12 @@ module Hecks
13
13
  # answers a hop for free, with no per-engine code and no way for
14
14
  # one engine to forget it.
15
15
  #
16
- # A hop's own filtering never happens here — RESOLVING one hop
16
+ # A hop's own filtering never happens here — resolving one hop
17
17
  # means running one ordinary, adapter-agnostic query against the
18
- # hop's TARGET aggregate (through the same Ports::Query boundary
18
+ # hop's target aggregate (through the same Ports::Query boundary
19
19
  # any other query goes through), and folding the ids it answers
20
20
  # back in as a local membership check. A multi-hop chain resolves
21
- # from the FAR END inward: `fold` only ever peels off the head hop
21
+ # from the far end inward: `fold` only ever peels off the head hop
22
22
  # (QuerySpecification::HopPath.next_hop, the one-step primitive),
23
23
  # and hands everything still left in the tail to a recursive
24
24
  # `apply` call — so hop 2, hop 3, and so on each get resolved by
@@ -63,12 +63,12 @@ module Hecks
63
63
  QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
64
64
  end
65
65
 
66
- # Every id the inner clause admits on the hop's TARGET — one
66
+ # Every id the inner clause admits on the hop's target — one
67
67
  # whole, ordinary query against the target's own repository,
68
68
  # through the very same Ports::Query boundary the outer ask
69
69
  # uses, so a hop is answered by whatever engine the target
70
70
  # aggregate is actually bound to (which may not be the engine
71
- # the OUTER aggregate is bound to at all) rather than by a
71
+ # the outer aggregate is bound to at all) rather than by a
72
72
  # second reading of the comparators.
73
73
  def matching_ids(domain, target, wheres, args, registry:)
74
74
  spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
@@ -81,7 +81,7 @@ module Hecks
81
81
  end
82
82
 
83
83
  # Never returned to a caller that might call an IR-level method
84
- # (`to_h`, …) whose OWN internal `wheres` read would resolve
84
+ # (`to_h`, …) whose own internal `wheres` read would resolve
85
85
  # against the original object, not this override — the exact
86
86
  # caution TenantScope::Scoped's own comment gives, for the exact
87
87
  # same reason: SimpleDelegator only intercepts calls made
@@ -1,134 +1,111 @@
1
+ require_relative "../vocabulary"
2
+
1
3
  module Hecks
2
4
  module Runtime
3
5
  # Every DomainRefusal wording that is not already data — `given`/
4
6
  # `ensures`/a declared `invariant` already carry their own description,
5
7
  # read at dispatch time off the command or value object that declared
6
- # them. These are different in kind: LANGUAGE-LEVEL refusals, the same
8
+ # them. These are different in kind: language-level refusals, the same
7
9
  # wording for every domain, not authored per-bluebook.
8
10
  #
9
- # Declared the same way in Vocabulary::RefusalTemplate
10
- # (language/bluebook/vocabulary.bluebook) — spec/refusal_wording_
11
- # conformance_spec holds this table equal to the language, both
12
- # directions. Hand-typed rather than read live off the meta-domain at
13
- # every dispatch, the same reason Expression::Evaluator::
14
- # SIGN_TEST_OPERATORS is hand-typed beside Vocabulary::SignTest.
11
+ # Read off the generated table, not typed a second time. The rows are
12
+ # Vocabulary::RefusalTemplate (language/bluebook/vocabulary.bluebook),
13
+ # projected into lib/hecks/vocabulary.rb by bin/project_vocabulary and
14
+ # into rust/src/kernel/vocab/refusal_template.rs by
15
+ # bin/project_rust_vocabulary; both regenerations are diffed in CI, so
16
+ # there is no hand copy left here to drift. Declared order is kept.
17
+ #
18
+ # **The arguments are data too**. Vocabulary::RefusalSiteArgument names the
19
+ # values each site takes and how each is written (a list's separator,
20
+ # its sort, its empty reading, its quoting). Call sites use
21
+ # `render_site` and hand over raw values; the Rust kernel's typed
22
+ # `render_args` applies the same rows, and the projection that writes it
23
+ # pins every site's output against `format_argument`/`substitute` here.
15
24
  module RefusalWording
16
- # `Layout/HashAlignment`'s repo-wide `table` style (.rubocop.yml)
17
- # would force every template string below onto the SAME column —
18
- # matching the widest key — leaving the longer wordings almost no
19
- # room to wrap under Layout/LineLength's own 130-column limit.
20
- # Disabled for exactly this one hash literal (a single statement, so
21
- # disable-next rather than a disable/enable pair).
22
- # rubocop:disable-next Layout/HashAlignment
23
- TEMPLATES = {
24
- %w[NotFound creating_no_identity] =>
25
- "{command} creates a {aggregate} — pass {identity}:",
26
- %w[AlreadyExists creating_duplicate] =>
27
- "{command} creates a {aggregate} that already exists — {identity} {offered}",
28
- %w[AlreadyExists entity_duplicate] =>
29
- "a {entity} already exists on {aggregate} — {identity} {offered}",
30
- %w[NotFound acting_no_identity] =>
31
- "{command} acts on an existing {aggregate} — pass {identity}:",
32
- %w[NotFound record_missing] =>
33
- "no {aggregate} with {identity} {offered}",
34
- %w[NotFound entity_parent_no_identity] =>
35
- "{command} acts on a {aggregate}'s {entity} — pass {identity}:",
36
- %w[UnknownVerb entity_unknown] =>
37
- "{aggregate} has no entity {entity}",
38
- %w[NotFound entity_element_no_identity] =>
39
- "{command} acts on one {entity} — pass {identity}:",
40
- %w[NotFound entity_element_missing] =>
41
- "no {entity} with {identity} {wants} on {aggregate} {parent_id}",
42
- %w[NotFound reference_target_missing] =>
43
- "no {target} with {heads} {key}",
44
- %w[NotFound read_model_reference_missing] =>
45
- "no {aggregate} with reference {offered}",
46
- %w[TypeMismatch read_model_object_reference] =>
47
- "{query} refused — a reference is an id, and {field} arrived as an object",
48
- %w[UnknownVerb no_query] =>
49
- "{aggregate} has no query {query}",
50
- %w[UnknownVerb entity_query_missing] =>
51
- "{entity} has no query {query}",
52
- %w[UnknownVerb entity_holds_no_list] =>
53
- "{aggregate} holds no list of {entity}",
54
- %w[UnknownVerb entity_no_command] =>
55
- "{entity} has no command {command}",
56
- %w[UnknownVerb aggregate_no_command] =>
57
- "{aggregate} has no command {command}",
58
- %w[UnknownVerb port_no_operation] =>
59
- "{port} has no operation {operation}",
60
- %w[UnknownVerb no_domain] =>
61
- "no domain {domain} loaded (verb {verb})",
62
- %w[UnknownVerb no_read_model] =>
63
- "{domain} has no read model {query}",
64
- %w[UnknownVerb not_fully_qualified] =>
65
- "{verb} is not a fully-qualified verb (Domain::Aggregate.Command)",
66
- %w[UnknownVerb no_aggregate] =>
67
- "{domain} has no aggregate {aggregate}",
68
- %w[LifecycleRefused transition_blocked] =>
69
- "{command} refused — {field} is {current}, and {command} moves it only from {allowed}",
70
- %w[TypeMismatch value_object_shape] =>
71
- "{name} is a {type} — pass its fields as an object, not {offered}",
72
- %w[TypeMismatch reference_wrong_shape] =>
73
- "{command} refused — a reference is an id, and {attribute} arrived as {offered}{known_by}",
74
- %w[TypeMismatch multi_field_scalar] =>
75
- "{type} has multiple fields and cannot stand in for a scalar",
76
- %w[TypeMismatch composite_identity] =>
77
- "{type} is a composite identity — an identity must have exactly one field",
78
- %w[TypeMismatch numeric_field] =>
79
- "{type}.{field} expects {expected}, got {offered}",
80
- %w[TypeMismatch non_finite_field] =>
81
- "{type}.{field} must be a finite number, got {offered}",
82
- %w[TypeMismatch integer_range] =>
83
- "{type}.{field} must fit in a 64-bit integer, got {offered}",
84
- %w[TypeMismatch pattern_mismatch] =>
85
- "{type}.{field} must match {pattern}, got {offered}",
86
- %w[TypeMismatch arithmetic_amount] =>
87
- "{op} of {target} needs an Integer, got {offered}",
88
- %w[TypeMismatch arithmetic_current] =>
89
- "{op} of {target} needs an Integer {target}, got {offered}",
90
- %w[TypeMismatch arithmetic_shared_field] =>
91
- "{op} of {target} needs a value object with one shared Integer field",
92
- %w[UnknownArgument unknown_args] =>
93
- "{command} does not declare {unknown} — it takes {declared}",
94
- %w[AbsentArgument absent_args] =>
95
- "{command} was not given {absent} — it takes {declared}",
96
- %w[InvariantViolation closed_set_member] =>
97
- "{type} admits {admitted} — got {offered}",
98
- %w[InvariantViolation value_object_invariant] =>
99
- "{name} invariant violated — {description} (given {offered})",
100
- %w[InvariantViolation admits_declared_set] =>
101
- "{name} admits {admits} — {admitted} — got {offered}",
102
- %w[InvariantViolation undeclared_set] =>
103
- "{name} admits {admits}, which this chapter does not declare — a closed set is named Aggregate::SetName, and it must " \
104
- "be one the bluebook actually holds",
105
- %w[Unauthorized tenant_required] =>
106
- "{query} declares authorize with tenant: {field} — pass {field}: to name which {field} this ask is scoped to",
107
- %w[Unauthorized role_mismatch] =>
108
- "{command} refused — role: {role}, and the caller stated {caller_role}",
109
- %w[Unauthorized cross_tenant_reference] =>
110
- "{aggregate} {field} is {tenant}, but {attribute} names a {target} whose own {target_field} " \
111
- "is {other} — a cross-tenant reference",
112
- %w[AttributeAbsent absent_read] =>
113
- "{aggregate} {field} is absent on this record — declared, not optional, and added since it was written. Backfill it " \
114
- "in a translation (backfill :{field}, default: ...), or declare it optional: true",
115
- %w[ProjectionAbsent absent_read] =>
116
- "{aggregate} {field} is not yet projected on this record — declared via projects :{field}, but no rebuild sweep has " \
117
- "populated it. Run the sweep, or read {reference}.{remote_field} directly if this rule cannot wait"
118
- }.freeze
25
+ TEMPLATES = Hecks::Vocabulary.rows("RefusalTemplate")
26
+ .to_h { |row| [[row["refusal"], row["site"]].freeze, row["template"]] }
27
+ .freeze
28
+
29
+ SHAPES = %w[scalar list].freeze
30
+ QUOTINGS = %w[none inspect].freeze
119
31
 
120
32
  module_function
121
33
 
122
34
  # Plain text substitution, never expression syntax — a template is
123
- # read, not evaluated. `render` computes the placeholder VALUES via
124
- # whatever the call site already had (a joined list, a rendered
125
- # identity reading, …) and this only replaces the markers.
35
+ # read, not evaluated. Values arrive already formatted; prefer
36
+ # `render_site`, which formats them off the declared rows.
126
37
  def render(refusal, site, **values)
127
- template = TEMPLATES.fetch([refusal, site]) do
38
+ substitute(template(refusal, site), values)
39
+ end
40
+
41
+ # The one door call sites use: exactly the arguments the site
42
+ # declares (a missing or undeclared one raises ArgumentError before
43
+ # any wording exists), each formatted by its RefusalSiteArgument row,
44
+ # substituted in declared order.
45
+ #
46
+ # RefusalWording.render_site("UnknownArgument", "unknown_args",
47
+ # command: "Close", unknown: [:parcel], declared: [])
48
+ # # => "Close does not declare parcel — it takes none"
49
+ def render_site(refusal, site, **arguments)
50
+ specs = argument_rows(refusal, site)
51
+ declared = specs.map { |spec| spec["argument"].to_sym }
52
+ missing = declared - arguments.keys
53
+ extra = arguments.keys - declared
54
+ if missing.any? || extra.any?
55
+ raise ArgumentError, "#{refusal}/#{site} takes #{declared.join(', ')} — " \
56
+ "missing: #{missing.join(', ')}; undeclared: #{extra.join(', ')}"
57
+ end
58
+
59
+ render_with(template(refusal, site), specs, arguments)
60
+ end
61
+
62
+ # `render_site` without the registry lookups: a template, its
63
+ # argument rows, and raw values. The Rust projection calls this with
64
+ # the chapter's own rows to compute the expected wording it pins.
65
+ def render_with(template, specs, arguments)
66
+ values = specs.to_h do |spec|
67
+ name = spec["argument"].to_sym
68
+ [name, format_argument(spec, arguments.fetch(name))]
69
+ end
70
+ substitute(template, values)
71
+ end
72
+
73
+ # One argument, written the way its row says. A list is sorted first
74
+ # (before quoting), then each item quoted, then joined; an empty list
75
+ # reads `when_empty`. A scalar is quoted or taken as its own text.
76
+ def format_argument(spec, value)
77
+ inspect = spec.fetch("quoting") == "inspect"
78
+ return inspect ? value.inspect : value.to_s unless spec.fetch("shape") == "list"
79
+
80
+ items = Array(value)
81
+ items = items.sort if spec.fetch("sorted") == "true"
82
+ items = items.map(&:inspect) if inspect
83
+ items.empty? ? spec.fetch("when_empty") : items.join(spec.fetch("separator"))
84
+ end
85
+
86
+ def substitute(template, values)
87
+ values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
88
+ end
89
+
90
+ def template(refusal, site)
91
+ TEMPLATES.fetch([refusal, site]) do
128
92
  raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
129
93
  "Vocabulary::RefusalTemplate first"
130
94
  end
131
- values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
95
+ end
96
+
97
+ # Read lazily, not into a constant: bin/project_vocabulary boots
98
+ # `hecks` (and so this file) before it writes the table a newly
99
+ # declared site's rows live in.
100
+ def argument_rows(refusal, site)
101
+ @argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
102
+ .group_by { |row| [row["refusal"], row["site"]] }
103
+ .transform_values(&:freeze)
104
+ .freeze
105
+ @argument_rows.fetch([refusal, site]) do
106
+ raise KeyError, "no refusal arguments for #{refusal}/#{site} — declare them in " \
107
+ "Vocabulary::RefusalSiteArgument first"
108
+ end
132
109
  end
133
110
  end
134
111
  end