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
@@ -16,6 +16,29 @@ module Hecks
16
16
  module CommandRequest
17
17
  module_function
18
18
 
19
+ # Splits one external request into routing (`to:`) and facts (`with:`), accepting
20
+ # either the explicit envelope or a flat Hash, and refuses a receiver that does
21
+ # not fit the kind of command being called.
22
+ #
23
+ # Keys are symbolized at every depth first, so a parsed JSON body and a Ruby Hash
24
+ # are treated alike. In a flat Hash everything but `to` (or the legacy receiver
25
+ # keys) is a fact.
26
+ #
27
+ # @param input [Hash, Object] the request, with String or Symbol keys; anything
28
+ # that is not a Hash is refused
29
+ # @param receiver [Symbol, nil] the kind of receiver the command takes:
30
+ # `:aggregate` (an identity), `:entity` (a Hash of `aggregate:` and `entity:`
31
+ # identities), or `nil` for a command that takes none, such as a creating command
32
+ # @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] where
33
+ # a flat request without `to` may carry its receiver instead: one key name (such
34
+ # as `:id`) for an `:aggregate` receiver, or `{ aggregate: key, entity: key }` for
35
+ # an `:entity` receiver; `nil` accepts no legacy spelling
36
+ # @return [Hash{Symbol => Object}] `{ with: facts }`, plus `to:` holding the route
37
+ # whenever `receiver` is not `nil`; ready to pass to `Dispatcher#dispatch_flat`
38
+ # @raise [Runtime::TypeMismatch] if `input` or its `with:` is not a Hash, if an
39
+ # explicit envelope carries keys other than `to:` and `with:`, or if the route
40
+ # is missing, blank, malformed, or given to a command that takes no receiver
41
+ # @raise [ArgumentError] if `receiver` is not `nil`, `:aggregate` or `:entity`
19
42
  def normalize(input, receiver:, legacy_receiver: nil)
20
43
  request = symbolize(input)
21
44
  raise Runtime::TypeMismatch, "a command request must be a hash" unless request.is_a?(Hash)
@@ -2,22 +2,29 @@ require_relative "../naming"
2
2
 
3
3
  module Hecks
4
4
  module Facade
5
- # ONE record in hand — the object `Pizza.create_pizza(...)` and
5
+ # One record in hand — the object `Pizza.create_pizza!(...)` and
6
6
  # `Pizza.find(id)` give back.
7
7
  #
8
- # ONE SHARED CLASS, not one minted per aggregate. The old door subclassed
9
- # `Hecks::Aggregate` per head and defined a reader per field ; this
10
- # wraps the same `Runtime::Instance` state hash and answers readers and
11
- # verbs through `method_missing`, closing over the dispatcher and the
12
- # aggregate's IR — so a boot mints no classes at all, and two boots in
13
- # one process each hand out handles bound to their own dispatcher.
8
+ # One shared class, not one minted per aggregate. Rather than subclassing
9
+ # per head and defining a reader per field, this wraps a
10
+ # `Runtime::Instance` state hash, answers readers through
11
+ # `method_missing` and verbs through per-handle singleton methods, and
12
+ # closes over the dispatcher and the aggregate's IR — so a boot mints no
13
+ # classes at all, and two boots in one process each hand out handles bound
14
+ # to their own dispatcher.
14
15
  #
15
16
  # A non-creating verb is a method returning self, so commands chain :
16
17
  #
17
- # Pizza.create_pizza(...).add_topping(...).purchase(...)
18
+ # Pizza.create_pizza!(...).add_topping!(...).purchase!(...)
18
19
  class Handle
19
20
  attr_reader :id
20
21
 
22
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
23
+ # dispatcher this record's verbs, `events` and `reload` go through
24
+ # @param domain [String] the owning chapter's name, the first half of `fqn`
25
+ # @param aggregate [Bluebook::Aggregate] the IR of the aggregate this record is one of
26
+ # @param instance [Runtime::Instance] the stored record; its `id` and `state` are
27
+ # read once here and the instance itself is not retained
21
28
  def initialize(dispatcher:, domain:, aggregate:, instance:)
22
29
  @dispatcher = dispatcher
23
30
  @domain = domain
@@ -28,37 +35,67 @@ module Hecks
28
35
  define_verb_methods
29
36
  end
30
37
 
38
+ # Reads one field's raw stored value, without hydrating a reference the way the
39
+ # reference accessor of the same name does.
40
+ #
41
+ # @param key [Symbol, String] the attribute name
42
+ # @return [Object, nil] the value held in state (a scalar, a value object, a list,
43
+ # or a referenced record's id); `nil` when the field is unset or not in state
31
44
  def [](key) = @state[key.to_sym]
32
45
 
33
- # `id: @id` LAST, not first an aggregate is free to declare its own
46
+ # Answers the record's state as a plain Hash with the bare identity under `:id`.
47
+ #
48
+ # `id: @id` last, not first — an aggregate is free to declare its own
34
49
  # attribute literally named `id` (BurningManPrep's `Item`, `attribute
35
50
  # :id, ItemId`, is real corpus now: `identified_by :id` reads
36
- # THAT attribute for identity). When it does, `@state[:id]` holds the
51
+ # that attribute for identity). When it does, `@state[:id]` holds the
37
52
  # full wrapped value object, not the bare identity string — merging
38
- # `@state` on top of `{ id: @id }` let that wrapped VO silently
53
+ # `@state` on top of `{ id: @id }` would let that wrapped VO silently
39
54
  # clobber the correct bare `@id`, so every caller of `to_h` (the JSON
40
- # door's own `/api/:coll` listing, in particular) got an object where
41
- # a plain identity string belonged. `@id` merged LAST always wins,
55
+ # door's own `/api/:coll` listing, in particular) would get an object
56
+ # where a plain identity string belongs. `@id` merged last always wins,
42
57
  # so `to_h[:id]` is always the true bare identity, regardless of
43
58
  # whether the aggregate also happens to declare a same-named field.
59
+ #
60
+ # @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
61
+ # name, plus `:id` holding the identity String
44
62
  def to_h = @state.merge(id: @id)
45
63
 
64
+ # Names the aggregate this record belongs to, in the form every dispatch verb and
65
+ # event is addressed by.
66
+ #
67
+ # @return [String] the fully qualified aggregate name, such as `"Pizzas::Pizza"`
46
68
  def fqn = "#{@domain}::#{@ir.hecks_name}"
47
69
 
70
+ # Lists the events this one record has emitted, filtered out of the dispatcher's
71
+ # whole event log on each call.
72
+ #
73
+ # @return [Array<Runtime::Event>] this record's events in the order the log holds
74
+ # them; `[]` when it has emitted none
48
75
  def events
49
76
  @dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
50
77
  end
51
78
 
79
+ # Refreshes this handle's state from the repository, picking up writes made through
80
+ # another handle or door. Keeps the current state when the record is not found.
81
+ #
82
+ # @return [Facade::Handle] this handle, so the call chains
83
+ # @raise [Runtime::WiringError] if the aggregate's persistence bind cannot be
84
+ # resolved into a repository
52
85
  def reload
53
86
  stored = repository.find(@id)
54
87
  @state = stored.state if stored
55
88
  self
56
89
  end
57
90
 
58
- # Equality is (WHICH AGGREGATE, WHICH ID) — two handles to the same record
91
+ # Equality is (which aggregate, which ID) — two handles to the same record
59
92
  # are the same record, and a Pizza never equals an Account that happens to
60
- # share an id. The old door said this with `other.is_a?(self.class)`,
61
- # leaning on one class per aggregate ; the fqn says it in data.
93
+ # share an id. With one shared class for every aggregate,
94
+ # `other.is_a?(self.class)` cannot tell them apart ; the fqn says it in data.
95
+ #
96
+ # @param other [Object] anything; only another `Handle` can be equal
97
+ # @return [Boolean] true when `other` is a `Handle` with the same `fqn` and `id`,
98
+ # whatever state either one holds
62
99
  def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
63
100
  alias eql? ==
64
101
  def hash = [Handle, fqn, @id].hash
@@ -69,9 +106,19 @@ module Hecks
69
106
  end
70
107
  alias to_s inspect
71
108
 
109
+ # Answers a field reader: `pizza.name` reads `name` out of state.
110
+ #
72
111
  # A declared field not yet written arrives here too (nil, the way a
73
- # defined reader answered). Verbs are NOT handled here — see
112
+ # defined reader answers). Verbs are not handled here — see
74
113
  # `define_verb_methods` for why.
114
+ #
115
+ # @param name [Symbol] the method called, read as an attribute or lifecycle field name
116
+ # @param args [Array<Object>] ignored by a reader; passed on to `super` otherwise
117
+ # @param kwargs [Hash{Symbol => Object}] ignored by a reader; passed on to `super`
118
+ # otherwise
119
+ # @return [Object, nil] the field's value; `nil` for a declared field with nothing
120
+ # written yet
121
+ # @raise [NoMethodError] if `name` is neither a key in state nor a declared field
75
122
  def method_missing(name, *args, **kwargs, &)
76
123
  return @state[name] if @state.key?(name) || reader?(name)
77
124
 
@@ -90,16 +137,16 @@ module Hecks
90
137
  !@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
91
138
  end
92
139
 
93
- # NON-CREATING VERBS ARE DEFINED, NOT DISPATCHED THROUGH method_missing.
140
+ # Non-creating verbs are defined, not dispatched through method_missing.
94
141
  #
95
- # method_missing only runs once Ruby finds no REAL method already
142
+ # method_missing only runs once Ruby finds no real method already
96
143
  # answering the name — and every object already answers `freeze` and
97
144
  # `send` (Kernel/Object), among others. A verb whose snake-cased name
98
145
  # collided with one of those — `Account::Freeze` -> `freeze`,
99
146
  # `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
100
- # used to silently run the Kernel method instead of dispatching: no
101
- # error, no refusal, the call just did the wrong thing. Defining a
102
- # real singleton method per verb closed that; the `!` suffix (every
147
+ # would silently run the Kernel method instead of dispatching: no
148
+ # error, no refusal, the call just does the wrong thing. Defining a
149
+ # real singleton method per verb closes that; the `!` suffix (every
103
150
  # command, door and Handle alike) closes it a second, permanent way —
104
151
  # `freeze!`/`send!` name nothing Kernel/Object already answers to,
105
152
  # so this exact class of collision cannot recur no matter what a
@@ -112,7 +159,7 @@ module Hecks
112
159
  end
113
160
  end
114
161
 
115
- # ONE HEAD ADDRESSES THE SAME WAY AS SEVERAL. `@ir.identified_by` is only
162
+ # One head addresses the same way as several. `@ir.identified_by` is only
116
163
  # the single-head shorthand — nil the moment an identity is composite
117
164
  # (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
118
165
  # identity payload from `identity_heads` instead reads every head, one
@@ -122,7 +169,7 @@ module Hecks
122
169
  self
123
170
  end
124
171
 
125
- # THE OTHER HALF OF A CROSS-REFERENCE. `transfer.source` already reads
172
+ # **The other half of a cross-reference**. `transfer.source` already reads
126
173
  # the raw value — a plain reader, same as any other attribute, still
127
174
  # needed by a `given`. This is the hydrated hop docs/rails-integration.md
128
175
  # designed and marked "nothing built": `transfer.source_account`
@@ -134,25 +181,25 @@ module Hecks
134
181
  # option — that shape was considered and rejected in the same design
135
182
  # note for hiding how many lookups actually happened behind one call.
136
183
  #
137
- # Defined BEFORE verb methods, not after — on the vanishing chance a
184
+ # Defined before verb methods, not after — on the vanishing chance a
138
185
  # reference's own accessor name collided with a command's, the verb
139
186
  # should win; `initialize` calls this first so `define_verb_methods`
140
187
  # defines second and last.
141
- # NO DERIVATION LEFT (ADR 0025, "References"): `reference_to`
188
+ # No derivation left (ADR 0025, "References"): `reference_to`
142
189
  # itself mints the bare attribute name now — `:account`, never
143
190
  # `:account_id` — so the accessor is spelled exactly like the
144
191
  # attribute it reads, with no `_id`-strip or `as:`-suffix rule to
145
- # apply first. `piece.account` (a METHOD, defined here) and
192
+ # apply first. `piece.account` (a method, defined here) and
146
193
  # `piece[:account]` (`Handle#[]`, bracket access reading the raw
147
194
  # id straight off `@instance`) never collide despite sharing a
148
- # name — Ruby dispatches the two completely differently — which is
149
- # what makes the OLD "studio_studio" double-suffix workaround
150
- # (this method used to force a DIFFERENT name specifically to dodge
151
- # that non-collision) unnecessary rather than merely simplified.
195
+ # name — Ruby dispatches the two completely differently — so no
196
+ # renamed accessor (a "studio_studio"-style double suffix) is needed
197
+ # to keep them apart.
152
198
  def define_reference_accessors
153
199
  @ir.attributes.select(&:reference?).each do |attribute|
154
200
  target = attribute.type.resolve
155
- next unless target # cross-domain, or otherwise unresolvable — no accessor rather than a guess
201
+ # Cross-domain, or otherwise unresolvable — no accessor rather than a guess.
202
+ next unless target
156
203
 
157
204
  domain = @domain
158
205
  field = attribute.name
@@ -7,22 +7,23 @@ require_relative "../runtime/value"
7
7
 
8
8
  module Hecks
9
9
  module Facade
10
- # THE JSON DOORWHERE Facade MEETS BODY-IN/BODY-OUT CALLERS.
10
+ # **The JSON door**where Facade meets body-in/body-out callers.
11
11
  #
12
12
  # `Handle`/`Surface` are Ruby sugar over the dispatcher for a Ruby caller
13
13
  # holding real objects — a symbol verb name, a `**kwargs` payload, a
14
- # `Handle` back in hand. A REST-ish JSON API is a caller holding STRINGS
14
+ # `Handle` back in hand. A REST-ish JSON API is a caller holding strings
15
15
  # instead: a URL segment naming a collection, a URL segment naming a
16
16
  # record or a verb, a parsed request body whose every key arrived as a
17
17
  # String because that is all JSON ever gives. Every app that wants to put
18
18
  # a JSON API in front of a booted domain has to do that translation —
19
- # name to class, string to symbol, nested `Runtime::Value` back to plain
20
- # data and one sibling app had already hand-written it once, bespoke,
21
- # against its own routes, before this existed. This is that translation
22
- # pulled out, generic, reading the SAME IR the rest of the facade already
23
- # reads rather than re-deriving "how do I find an aggregate by name".
19
+ # name to door module, string to symbol, nested `Runtime::Value` back to
20
+ # plain data. This is that translation written once, generic, reading the
21
+ # same IR the rest of the facade already reads rather than each app
22
+ # re-deriving "how do I find an aggregate by name" against its own routes.
24
23
  #
25
- # NO HTTP LIVES HERE. Same discipline `Router` and `Surface` already
24
+ # ## No HTTP lives here
25
+ #
26
+ # Same discipline `Router` and `Surface` already
26
27
  # hold: this module never sees a request object, never picks a status
27
28
  # code, never calls `halt`. Every method here takes plain Ruby values in
28
29
  # — a raw JSON String is the one exception, see `.parse` below, every
@@ -31,10 +32,12 @@ module Hecks
31
32
  # turning a raised exception into an HTTP status, both stay the calling
32
33
  # app's job, exactly the way they already are for `Router#dispatch`.
33
34
  #
34
- # EVERY "THAT DOESN'T EXIST" CASE RAISES `Runtime::NotFound` — THE SAME
35
- # CLASS EVERY TIME, NOT A NEW ONE PER CALLER. `Runtime::NotFound` already
35
+ # ## One refusal class for every miss
36
+ #
37
+ # Every "that doesn't exist" case raises `Runtime::NotFound` — the same
38
+ # class every time, not a new one per caller. `Runtime::NotFound` already
36
39
  # sits in `Runtime::DOMAIN_REFUSALS` (runtime/errors.rb) — the family a
37
- # booted app already has, or trivially can have, ONE generic `error`
40
+ # booted app already has, or trivially can have, one generic `error`
38
41
  # handler for, mapping the whole family to a status code without a
39
42
  # special case per refusal. A bespoke `JsonDoor::CollectionNotFound` (or
40
43
  # three of those, one per flavor of "not found") would just force every
@@ -45,12 +48,15 @@ module Hecks
45
48
  module JsonDoor
46
49
  module_function
47
50
 
48
- # "Banking", "Customer" -> the `Banking::Customer` class `.find` /
49
- # `.create_...` / etc already answer for — the same class
51
+ # Resolves a domain name and an aggregate name, as two URL segments carry them, to
52
+ # that aggregate's door.
53
+ #
54
+ # "Banking", "Customer" -> the `Banking::Customer` door module `.find` /
55
+ # `.create_...!` / etc already answer for — the same module
50
56
  # `Facade::Handle`'s own reference accessors reach with
51
57
  # `Object.const_get` (see handle.rb's `define_reference_accessors`).
52
58
  #
53
- # Checked against the CURRENT boot's IR first, not against Ruby's
59
+ # Checked against the current boot's IR first, not against Ruby's
54
60
  # constant table directly — a name that names nothing in this
55
61
  # registry should refuse before ever asking Ruby whether some
56
62
  # same-named constant happens to exist (possibly a stale one, left
@@ -58,6 +64,16 @@ module Hecks
58
64
  # `AggregateDoor#port`'s own comment describes at length). Only once
59
65
  # the IR confirms the aggregate is real does this read the constant
60
66
  # the current boot's `Surface.install` actually minted for it.
67
+ #
68
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
69
+ # dispatcher whose registry decides whether the aggregate exists
70
+ # @param domain [String, Symbol] the chapter name, such as `"Banking"`
71
+ # @param name [String, Symbol] the aggregate's declared name, such as `"Customer"`
72
+ # @return [Module] the aggregate door installed at `domain::name`
73
+ # @raise [Runtime::NotFound] if the registry holds no such chapter, or the chapter
74
+ # declares no aggregate of that name
75
+ # @raise [NameError] if the IR has the aggregate but no facade constant is
76
+ # installed for it, as after a boot with `install_facade: false`
61
77
  def aggregate(dispatcher, domain, name)
62
78
  ir = dispatcher.registry.bluebook(domain)&.aggregate(name)
63
79
  raise Runtime::NotFound, "#{domain} declares no aggregate named #{name.inspect}" unless ir
@@ -65,14 +81,22 @@ module Hecks
65
81
  Object.const_get("#{domain}::#{ir.hecks_name}")
66
82
  end
67
83
 
84
+ # Names the door method that creates a record of this aggregate.
85
+ #
68
86
  # The one command a POST to a bare collection URL means — "make one of
69
- # these". `AggregateDoor` already enforces exactly one creating
70
- # command per aggregate (`Command#creates?`, true exactly when the
71
- # command declares no `references`); this just names it the same
87
+ # these". A creating command is one that declares no `references`
88
+ # (`Command#creates?`), and the first one the aggregate declares is the
89
+ # one named here; this just names it the same
72
90
  # snake_case-plus-bang a Ruby caller would already ask the door for
73
91
  # (`Naming.snake`, the identical call `AggregateDoor` itself makes
74
92
  # when it defines that singleton method in the first place — `!`
75
- # because every command does now, door and Handle alike).
93
+ # because every command carries it, door and Handle alike).
94
+ #
95
+ # @param klass [Module] an aggregate door, as `aggregate` returns; anything
96
+ # answering `ir` with a `Bluebook::Aggregate` works
97
+ # @return [String] the method name to `public_send` to the door, such as
98
+ # `"create_pizza!"`
99
+ # @raise [Runtime::NotFound] if the aggregate declares no creating command
76
100
  def creating_command(klass)
77
101
  creating = klass.ir.commands.find(&:creates?)
78
102
  raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no creating command" unless creating
@@ -80,21 +104,31 @@ module Hecks
80
104
  "#{Naming.snake(creating.hecks_name)}!"
81
105
  end
82
106
 
107
+ # Confirms that a command name arriving as text is one a `Handle` of this aggregate
108
+ # answers, before a caller `public_send`s it.
109
+ #
83
110
  # A URL segment or a JSON body's "command" field, checked against what
84
- # a `Handle` can actually dispatch — NOT `klass.commands`, which is
111
+ # a `Handle` can actually dispatch — not `klass.commands`, which is
85
112
  # `AggregateDoor`'s own door-level list and includes the one creating
86
113
  # command too (`aggregate_door.rb`'s `commands` singleton method maps
87
114
  # every `ir.commands`, full stop). A `Handle` only ever defines
88
- # singleton methods for the NON-creating ones
115
+ # singleton methods for the non-creating ones
89
116
  # (`Handle#define_verb_methods`, `@ir.commands.reject(&:creates?)`) —
90
117
  # the creating command lives on the aggregate class itself, dispatched
91
118
  # through `.creating_command` above, not through a `Handle` in hand.
92
- # Accepting a creating-command name here let it past this gate clean,
93
- # only to blow up as a raw `NoMethodError` the moment a caller tried
94
- # `handle.public_send(name, **args)`, instead of the 404 this method
95
- # promises. Filtering `reject(&:creates?)` here, the same filter
119
+ # Accepting a creating-command name here would let it past this gate
120
+ # clean, only to blow up as a raw `NoMethodError` the moment a caller
121
+ # tries `handle.public_send(name, **args)`, instead of the 404 this
122
+ # method promises. Filtering `reject(&:creates?)` here, the same filter
96
123
  # `Handle` itself applies, is what keeps "accepted here" and
97
124
  # "dispatchable there" the same set.
125
+ #
126
+ # @param klass [Module] an aggregate door, as `aggregate` returns
127
+ # @param name [String, Symbol] the wanted method name with its bang, such as
128
+ # `"add_topping!"`
129
+ # @return [String] `name` as a String, unchanged, when a `Handle` answers it
130
+ # @raise [Runtime::NotFound] if no non-creating command has that method name,
131
+ # including when `name` is the creating command's or lacks the `!`
98
132
  def validate_command!(klass, name)
99
133
  wanted = name.to_s
100
134
  dispatchable = klass.ir.commands.reject(&:creates?).map { |command| "#{Naming.snake(command.hecks_name)}!" }
@@ -103,20 +137,34 @@ module Hecks
103
137
  raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no command named #{wanted.inspect}"
104
138
  end
105
139
 
140
+ # Fetches one record by id, refusing a miss instead of answering nil.
141
+ #
106
142
  # `klass.find` already answers nil-on-miss — the right shape for a
107
143
  # Ruby caller that means to check for itself. A JSON caller asking for
108
- # one record by id off a URL means to HAVE it, or answer 404 — this is
144
+ # one record by id off a URL means to have it, or answer 404 — this is
109
145
  # that stricter wrapper, raising the same `Runtime::NotFound` the rest
110
146
  # of this door raises rather than handing back nil for the caller to
111
147
  # remember to check.
148
+ #
149
+ # @param klass [Module] an aggregate door, as `aggregate` returns
150
+ # @param id [String] the record's identity, as the URL carried it
151
+ # @return [Facade::Handle] the record in hand
152
+ # @raise [Runtime::NotFound] if the repository holds no record with that id
112
153
  def find!(klass, id)
113
154
  klass.find(id) or raise Runtime::NotFound, "no #{klass.ir.hecks_name} found for id #{id.inspect}"
114
155
  end
115
156
 
157
+ # Converts every Hash key to a Symbol, at every depth of a parsed JSON body.
158
+ #
116
159
  # JSON only ever hands back String keys. A command's args, and every
117
160
  # nested value-object literal inside them, need symbol keys before
118
161
  # `Handle`/`Dispatcher` will accept them at all — this is that
119
162
  # recursive conversion, blind to how deep a body nests.
163
+ #
164
+ # @param value [Hash, Array, Object] parsed JSON: a Hash or Array is walked, any
165
+ # other value is a leaf
166
+ # @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape
167
+ # with Symbol keys; a leaf is returned as it came
120
168
  def deep_symbolize(value)
121
169
  case value
122
170
  when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
@@ -125,6 +173,9 @@ module Hecks
125
173
  end
126
174
  end
127
175
 
176
+ # Unwraps a record, a query row, or any value holding `Runtime::Value`s into plain
177
+ # Hashes, Arrays and scalars.
178
+ #
128
179
  # The other direction: a `Handle`, or a query row's plain state hash,
129
180
  # carrying a `Runtime::Value` at every level a value object sits at —
130
181
  # down to plain Ruby a JSON encoder can walk without knowing what a
@@ -139,19 +190,44 @@ module Hecks
139
190
  # already, and a `Handle` is none of those three, so the one thing
140
191
  # this adds is `#to_h`'ing a `Handle` first so `materialize`'s own
141
192
  # `Hash` case can take it from there.
193
+ #
194
+ # @param value [Facade::Handle, Runtime::Value, Hash, Array, Object] what to unwrap;
195
+ # a `Handle` is read through its `to_h`, so its `:id` comes along
196
+ # @return [Hash, Array, Object] the same data with every `Runtime::Value` replaced
197
+ # by a Hash of its fields; a value that is none of the listed types is returned
198
+ # as it came
142
199
  def materialize(value)
143
200
  value = value.to_h if value.is_a?(Handle)
144
201
  Runtime::Value.materialize(value)
145
202
  end
146
203
 
204
+ # Turns a command's JSON body, raw or already parsed, into the `to:`/`with:`
205
+ # envelope a dispatcher takes.
206
+ #
147
207
  # Parsed JSON and raw JSON text cross the same receiver/payload boundary
148
208
  # as CLI and forms. The result is ready to splat into Dispatcher#dispatch
149
209
  # and contains no loose routing fields.
210
+ #
211
+ # @param body [String, Hash] raw JSON text, or the Hash it parses to, with String
212
+ # or Symbol keys
213
+ # @param receiver [Symbol, nil] the kind of receiver the command takes: `:aggregate`,
214
+ # `:entity`, or `nil` for none (see `CommandRequest.normalize`)
215
+ # @param legacy_receiver [Symbol, String, Hash{Symbol => Symbol, String}, nil] the
216
+ # flat key, or pair of keys, a body without `to` may name its receiver under;
217
+ # `nil` accepts none
218
+ # @return [Hash{Symbol => Object}] `{ with: facts }`, plus `to:` whenever `receiver`
219
+ # is not `nil`
220
+ # @raise [JSON::ParserError] if `body` is a String that is not valid JSON
221
+ # @raise [Runtime::TypeMismatch] if the body is not a JSON object, or its routing is
222
+ # missing, malformed, or mixed with loose keys beside an explicit `with:`
223
+ # @raise [ArgumentError] if `receiver` is not `nil`, `:aggregate` or `:entity`
150
224
  def command_request(body, receiver:, legacy_receiver: nil)
151
225
  input = body.is_a?(String) ? parse(body) : body
152
226
  CommandRequest.normalize(input, receiver: receiver, legacy_receiver: legacy_receiver)
153
227
  end
154
228
 
229
+ # Parses a raw request body, leaving keys as Strings.
230
+ #
155
231
  # The one place a raw JSON string is legitimate input for this door —
156
232
  # a POST body, still text at the point a generic, HTTP-blind layer can
157
233
  # see it. `JSON::ParserError` already names "this wasn't JSON" exactly
@@ -160,6 +236,11 @@ module Hecks
160
236
  # `JSON.parse` raises it — a calling app catches it the same standard
161
237
  # way it would catch any other malformed-input error, no new class to
162
238
  # learn.
239
+ #
240
+ # @param raw_json [String] JSON text, such as a POST body
241
+ # @return [Hash{String => Object}, Array, String, Numeric, Boolean, nil] whatever
242
+ # the text encodes; a JSON object becomes a Hash with String keys
243
+ # @raise [JSON::ParserError] if the text is not valid JSON
163
244
  def parse(raw_json) = JSON.parse(raw_json)
164
245
  end
165
246
  end
@@ -13,16 +13,31 @@ module Hecks
13
13
  # `persisted_by`-style binding collector a `.hecksagon` lands on, and
14
14
  # the aggregate-scoped `port` a `.hecksagon` lands on beside it.
15
15
  module AggregateDoor
16
+ # Builds the anonymous module that is one aggregate's door: a `name!` method per
17
+ # creating command, a bare method per query, `find`/`all`/`count`/`events`,
18
+ # `project`/`docs`/`narrate`, and the `.hecksagon` binding hooks.
19
+ #
20
+ # Warns once for each attribute whose name is in `RESERVED`.
21
+ #
16
22
  # One method building one `door` module's ~20 singleton methods
17
23
  # looks like it splits along each `define_singleton_method` call,
18
24
  # but three of those blocks (`:port`, `:method_missing`,
19
25
  # `:const_missing`) are a single cross-referencing essay on the
20
26
  # stale-facade-across-boots hazard — each one's comment explicitly
21
- # points at "below"/"above" as part of the SAME method. Splitting
27
+ # points at "below"/"above" as part of the same method. Splitting
22
28
  # into helper methods wouldn't break anything at runtime (no
23
29
  # shared mutable state beyond the closed-over args, which just
24
30
  # become parameters), but it would sever that narrative across
25
31
  # method boundaries for no functional gain.
32
+ #
33
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
34
+ # dispatcher the door's methods dispatch, query and read repositories through
35
+ # @param domain [String] the owning chapter's name, the first half of the
36
+ # aggregate's FQN (`"Pizzas"` in `"Pizzas::Pizza"`)
37
+ # @param aggregate [Bluebook::Aggregate] the aggregate to build a door for
38
+ # @return [Module] a fresh, unnamed module carrying the door's singleton methods
39
+ # @raise [Bluebook::DSL::Malformed] never while building; the returned door's
40
+ # `port` raises it when called with no boot in progress
26
41
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
27
42
  def aggregate_module(dispatcher, domain, aggregate)
28
43
  fqn = "#{domain}::#{aggregate.hecks_name}"
@@ -34,13 +49,13 @@ module Hecks
34
49
  warn "[hecks] #{aggregate.hecks_name}##{attribute.name} shadows a built-in — no reader defined"
35
50
  end
36
51
 
37
- # A creating verb is a MODULE method returning the new record in hand ;
52
+ # A creating verb is a module method returning the new record in hand ;
38
53
  # a verb that reaches an existing record lives on the Handle. `!` —
39
- # a command DOES something (mutates, may refuse), Ruby's own
40
- # convention for that ; `Naming.snake` alone used to leave a
54
+ # a command does something (mutates, may refuse), Ruby's own
55
+ # convention for that ; `Naming.snake` alone would leave a
41
56
  # creating command's bare name claiming the exact spelling a
42
- # QUERY of the SAME business name also wants (`Account`'s own
43
- # "Open" — the creating command AND a query listing open
57
+ # query of the same business name also wants (`Account`'s own
58
+ # "Open" — the creating command and a query listing open
44
59
  # accounts, a real same-aggregate collision this corpus already
45
60
  # has) — the suffix is what makes both nameable at all, not
46
61
  # merely a style choice.
@@ -51,7 +66,7 @@ module Hecks
51
66
  end
52
67
  end
53
68
 
54
- # A query is a MODULE method too — same level as a creating
69
+ # A query is a module method too — same level as a creating
55
70
  # command, since neither needs an existing record in hand — but
56
71
  # bare: a query reads and returns, nothing to warn a caller
57
72
  # about the way `!` does for a command. Answers the raw row
@@ -66,7 +81,7 @@ module Hecks
66
81
  door.define_singleton_method(:fqn) { fqn }
67
82
  door.define_singleton_method(:ir) { aggregate }
68
83
 
69
- # THE SAME VERB THE CHAPTER ANSWERS, one level down. Every
84
+ # The same verb the chapter answers, one level down. Every
70
85
  # construct emits its own IR (Hecks::IR), so an aggregate
71
86
  # is a legitimate thing to project — `Pizzas::Order.project(
72
87
  # Projections::IR)` is this aggregate's IR, not the chapter's.
@@ -74,8 +89,8 @@ module Hecks
74
89
  # A chapter-scoped target refuses here rather than inventing an
75
90
  # answer: `Projector.admits!` is what tells `Projections::IR`
76
91
  # (`from: :any`) apart from `Projections::Shape`
77
- # (`from: :chapter`), which used to return a confidently empty
78
- # `{"aggregates" => []}` for an aggregate.
92
+ # (`from: :chapter`), which would otherwise answer a confidently
93
+ # empty `{"aggregates" => []}` for an aggregate.
79
94
  door.define_singleton_method(:project) do |target, out: nil, **options|
80
95
  key = Projector.key_for(target)
81
96
  artifact = Projector.call(key, bluebook: aggregate, options: options)
@@ -88,7 +103,7 @@ module Hecks
88
103
  aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort
89
104
  end
90
105
  door.define_singleton_method(:queries) { aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
91
- # ONE AGGREGATE'S USAGE DOCUMENT — the same projection the chapter
106
+ # **One aggregate's usage document** — the same projection the chapter
92
107
  # answers with, narrowed to this head. `commands` above already
93
108
  # answers "what can I call"; this answers "and what does each one
94
109
  # want, refuse, and guarantee", which is the rest of the question.
@@ -96,7 +111,7 @@ module Hecks
96
111
  Projector.call(:docs, bluebook: dispatcher.registry.bluebook(domain),
97
112
  options: options.merge(aggregate: aggregate.hecks_name))
98
113
  end
99
- # ONE AGGREGATE, READ BACK IN ENGLISH — the same narrowing `:docs`
114
+ # **One aggregate, read back in english** — the same narrowing `:docs`
100
115
  # takes, aimed at `:narrate` instead.
101
116
  door.define_singleton_method(:narrate) do |**options|
102
117
  Projector.call(:narrate, bluebook: dispatcher.registry.bluebook(domain),
@@ -116,18 +131,18 @@ module Hecks
116
131
  end
117
132
  end
118
133
 
119
- # THE SAME REASON `method_missing` BELOW EXISTS AT ALL — a facade
120
- # left over from a PREVIOUS boot in this process shadows the fresh
134
+ # The same reason `method_missing` below exists at all — a facade
135
+ # left over from a previous boot in this process shadows the fresh
121
136
  # `BindingProxy` a `.hecksagon` would otherwise reach through
122
137
  # `ConstShim`/`const_missing`, so `Pizzas::Pizza.port(...)` lands
123
- # HERE instead once any boot has run before.
138
+ # here instead once any boot has run before.
124
139
  #
125
- # RE-RESOLVED, NOT THE CLOSED-OVER `aggregate` — this door can be a
126
- # STALE one, built by a boot from earlier in this same process,
140
+ # Re-resolved, not the closed-over `aggregate` — this door can be a
141
+ # stale one, built by a boot from earlier in this same process,
127
142
  # sitting on the `Pizzas`/`Pizza` constants only because nothing
128
- # has re-installed them since. Attaching to this door's OWN
143
+ # has re-installed them since. Attaching to this door's own
129
144
  # `aggregate` would attach the port to a discarded aggregate from
130
- # that old boot, invisible to the CURRENT one actually being
145
+ # that old boot, invisible to the current one actually being
131
146
  # loaded — silently, the exact way `method_missing` below already
132
147
  # has to avoid it for a plain bind, via `HecksagonBuilder.collector`
133
148
  # rather than anything this door closes over. `Hecks.current_registry`
@@ -166,18 +181,18 @@ module Hecks
166
181
  !Bluebook::DSL::HecksagonBuilder.collector.nil? || super(name, include_private)
167
182
  end
168
183
 
169
- # THE SAME STALE-FACADE HAZARD `method_missing`/`port` ABOVE ALREADY
170
- # DOCUMENT, one door lower — `Surface.install` installs an aggregate's
171
- # OWN name as a bare TOP-LEVEL constant too (`Namespace.install(Object,
184
+ # The same stale-facade hazard `method_missing`/`port` above already
185
+ # document, one door lower — `Surface.install` installs an aggregate's
186
+ # own name as a bare top-level constant too (`Namespace.install(Object,
172
187
  # aggregate.hecks_name, ...)`, surface.rb's own `install`), not only
173
- # nested under its chapter. So once ANY domain has booted once in this
174
- # process, `Account::Debit` written while declaring some OTHER
175
- # bluebook — same domain or a different one — reaches THIS door's
188
+ # nested under its chapter. So once any domain has booted once in this
189
+ # process, `Account::Debit` written while declaring some other
190
+ # bluebook — same domain or a different one — reaches this door's
176
191
  # const_missing directly, never `Object.const_missing`/`ConstShim::
177
192
  # Hook` at all: real modules resolve without ever calling that.
178
193
  #
179
- # `aggregate.hecks_name`, NOT the qualified `fqn` — a scoped
180
- # reference has to read the SAME either way, whether or not a stale
194
+ # `aggregate.hecks_name`, not the qualified `fqn` — a scoped
195
+ # reference has to read the same either way, whether or not a stale
181
196
  # door happens to be sitting on this process from an earlier boot;
182
197
  # qualifying it here would make `Account::Debit`'s own meaning
183
198
  # depend on incidental process history, which is the exact