hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -1,23 +1,32 @@
1
1
  require_relative "../naming"
2
+ require_relative "../runtime/caller"
3
+ require_relative "../ports/authorization"
2
4
 
3
5
  module Hecks
4
6
  module Facade
5
- # ONE record in hand — the object `Pizza.create_pizza(...)` and
7
+ # One record in hand — the object `Pizza.create_pizza!(...)` and
6
8
  # `Pizza.find(id)` give back.
7
9
  #
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.
10
+ # One shared class, not one minted per aggregate. Rather than subclassing
11
+ # per head and defining a reader per field, this wraps a
12
+ # `Runtime::Instance` state hash, answers readers through
13
+ # `method_missing` and verbs through per-handle singleton methods, and
14
+ # closes over the dispatcher and the aggregate's IR — so a boot mints no
15
+ # classes at all, and two boots in one process each hand out handles bound
16
+ # to their own dispatcher.
14
17
  #
15
18
  # A non-creating verb is a method returning self, so commands chain :
16
19
  #
17
- # Pizza.create_pizza(...).add_topping(...).purchase(...)
20
+ # Pizza.create_pizza!(...).add_topping!(...).purchase!(...)
18
21
  class Handle
19
22
  attr_reader :id
20
23
 
24
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
25
+ # dispatcher this record's verbs, `events` and `reload` go through
26
+ # @param domain [String] the owning chapter's name, the first half of `fqn`
27
+ # @param aggregate [Bluebook::Aggregate] the IR of the aggregate this record is one of
28
+ # @param instance [Runtime::Instance] the stored record; its `id` and `state` are
29
+ # read once here and the instance itself is not retained
21
30
  def initialize(dispatcher:, domain:, aggregate:, instance:)
22
31
  @dispatcher = dispatcher
23
32
  @domain = domain
@@ -28,37 +37,67 @@ module Hecks
28
37
  define_verb_methods
29
38
  end
30
39
 
31
- def [](key) = @state[key.to_sym]
40
+ # Reads one field's raw stored value, without hydrating a reference the way the
41
+ # reference accessor of the same name does.
42
+ #
43
+ # @param key [Symbol, String] the attribute name
44
+ # @return [Object, nil] the value held in state (a scalar, a value object, a list,
45
+ # or a referenced record's id); `nil` when the field is unset or not in state
46
+ def [](key) = redacted(key.to_sym)
32
47
 
33
- # `id: @id` LAST, not first — an aggregate is free to declare its own
48
+ # Answers the record's state as a plain Hash with the bare identity under `:id`.
49
+ #
50
+ # `id: @id` last, not first — an aggregate is free to declare its own
34
51
  # attribute literally named `id` (BurningManPrep's `Item`, `attribute
35
52
  # :id, ItemId`, is real corpus now: `identified_by :id` reads
36
- # THAT attribute for identity). When it does, `@state[:id]` holds the
53
+ # that attribute for identity). When it does, `@state[:id]` holds the
37
54
  # full wrapped value object, not the bare identity string — merging
38
- # `@state` on top of `{ id: @id }` let that wrapped VO silently
55
+ # `@state` on top of `{ id: @id }` would let that wrapped VO silently
39
56
  # 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,
57
+ # door's own `/api/:coll` listing, in particular) would get an object
58
+ # where a plain identity string belongs. `@id` merged last always wins,
42
59
  # so `to_h[:id]` is always the true bare identity, regardless of
43
60
  # whether the aggregate also happens to declare a same-named field.
44
- def to_h = @state.merge(id: @id)
61
+ #
62
+ # @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
63
+ # name, plus `:id` holding the identity String
64
+ def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
45
65
 
66
+ # Names the aggregate this record belongs to, in the form every dispatch verb and
67
+ # event is addressed by.
68
+ #
69
+ # @return [String] the fully qualified aggregate name, such as `"Pizzas::Pizza"`
46
70
  def fqn = "#{@domain}::#{@ir.hecks_name}"
47
71
 
72
+ # Lists the events this one record has emitted, filtered out of the dispatcher's
73
+ # whole event log on each call.
74
+ #
75
+ # @return [Array<Runtime::Event>] this record's events in the order the log holds
76
+ # them; `[]` when it has emitted none
48
77
  def events
49
78
  @dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
50
79
  end
51
80
 
81
+ # Refreshes this handle's state from the repository, picking up writes made through
82
+ # another handle or door. Keeps the current state when the record is not found.
83
+ #
84
+ # @return [Facade::Handle] this handle, so the call chains
85
+ # @raise [Runtime::WiringError] if the aggregate's persistence bind cannot be
86
+ # resolved into a repository
52
87
  def reload
53
88
  stored = repository.find(@id)
54
89
  @state = stored.state if stored
55
90
  self
56
91
  end
57
92
 
58
- # Equality is (WHICH AGGREGATE, WHICH ID) — two handles to the same record
93
+ # Equality is (which aggregate, which ID) — two handles to the same record
59
94
  # 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.
95
+ # share an id. With one shared class for every aggregate,
96
+ # `other.is_a?(self.class)` cannot tell them apart ; the fqn says it in data.
97
+ #
98
+ # @param other [Object] anything; only another `Handle` can be equal
99
+ # @return [Boolean] true when `other` is a `Handle` with the same `fqn` and `id`,
100
+ # whatever state either one holds
62
101
  def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
63
102
  alias eql? ==
64
103
  def hash = [Handle, fqn, @id].hash
@@ -69,11 +108,21 @@ module Hecks
69
108
  end
70
109
  alias to_s inspect
71
110
 
111
+ # Answers a field reader: `pizza.name` reads `name` out of state.
112
+ #
72
113
  # A declared field not yet written arrives here too (nil, the way a
73
- # defined reader answered). Verbs are NOT handled here — see
114
+ # defined reader answers). Verbs are not handled here — see
74
115
  # `define_verb_methods` for why.
116
+ #
117
+ # @param name [Symbol] the method called, read as an attribute or lifecycle field name
118
+ # @param args [Array<Object>] ignored by a reader; passed on to `super` otherwise
119
+ # @param kwargs [Hash{Symbol => Object}] ignored by a reader; passed on to `super`
120
+ # otherwise
121
+ # @return [Object, nil] the field's value; `nil` for a declared field with nothing
122
+ # written yet
123
+ # @raise [NoMethodError] if `name` is neither a key in state nor a declared field
75
124
  def method_missing(name, *args, **kwargs, &)
76
- return @state[name] if @state.key?(name) || reader?(name)
125
+ return redacted(name) if @state.key?(name) || reader?(name)
77
126
 
78
127
  super
79
128
  end
@@ -86,20 +135,91 @@ module Hecks
86
135
 
87
136
  def repository = @dispatcher.registry.repository(@domain, @ir)
88
137
 
138
+ # One field's value, with any Privacy::Marking-flagged leaf masked
139
+ # out unless the ambient caller holds a live Governance grant of
140
+ # the marking's own `readable_by` — the read-side half of the
141
+ # Privacy framework member (lib/hecks/framework/bluebook/
142
+ # privacy.bluebook): a marking's presence is what makes a read
143
+ # redacted, not a separate flag this class carries itself.
144
+ #
145
+ # ALWAYS THE STRONG CHECK, never the weak string-only fallback
146
+ # `CommandRules::Authorization#refuse_role_mismatch` allows an
147
+ # unidentified caller — a read gate gone wrong is a leak, not a
148
+ # refused command, so an ambient caller with no `actor_id` (or no
149
+ # caller at all) is masked here, full stop, rather than waved
150
+ # through the way a self-asserted `role` string is for a command.
151
+ #
152
+ # ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
153
+ # the returned `Runtime::Value` via its own `#with`; a marking two
154
+ # levels deep is not supported and is left unmasked rather than
155
+ # silently mishandled, since nothing in this corpus needs it yet.
156
+ #
157
+ # @param field [Symbol] the state key being read
158
+ # @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
159
+ # the literal String `"[redacted]"` when the caller is not authorized to see it
160
+ def redacted(field)
161
+ raw = @state[field]
162
+ rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
163
+ return raw if rows.empty?
164
+
165
+ rows.each do |row|
166
+ path = row[:attribute_path][:value].to_s
167
+ next if authorized_for?(row[:readable_by][:value].to_s)
168
+
169
+ segments = path.split(".", 2)
170
+ if segments.size == 1
171
+ raw = "[redacted]"
172
+ elsif raw.is_a?(Runtime::Value)
173
+ raw = raw.with(segments[1], "[redacted]")
174
+ end
175
+ end
176
+
177
+ raw
178
+ end
179
+
180
+ # Every Privacy::Marking declared for this record's own aggregate — `[]` when the
181
+ # Privacy framework member is not attached in this boot at all, checked once and
182
+ # cheaply rather than paying for a query dispatch every read on a domain that never
183
+ # attached Privacy.
184
+ #
185
+ # @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
186
+ def marked_paths
187
+ return @marked_paths if defined?(@marked_paths)
188
+ return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
189
+
190
+ @marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
191
+ end
192
+
193
+ # Whether the ambient caller holds a live Governance grant of `role`, over this
194
+ # record's own domain — `false` outright for an unidentified caller or a domain
195
+ # with no authorization provider attached, never the weak fallback a command's own
196
+ # role check allows (see `redacted`'s own header for why).
197
+ #
198
+ # @param role [String] the marking's own `readable_by`
199
+ # @return [Boolean] true only for an identified caller holding a live grant
200
+ def authorized_for?(role)
201
+ caller = Runtime::Caller.current
202
+ return false unless caller&.actor_id
203
+ return false unless @dispatcher.registry.authorization_provider_for(@domain)
204
+
205
+ Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
206
+ as_of: caller.as_of, scope: caller.scope)
207
+ end
208
+
89
209
  def reader?(name)
90
210
  !@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
91
211
  end
92
212
 
93
- # NON-CREATING VERBS ARE DEFINED, NOT DISPATCHED THROUGH method_missing.
213
+ # Non-creating verbs are defined, not dispatched through method_missing.
94
214
  #
95
- # method_missing only runs once Ruby finds no REAL method already
215
+ # method_missing only runs once Ruby finds no real method already
96
216
  # answering the name — and every object already answers `freeze` and
97
217
  # `send` (Kernel/Object), among others. A verb whose snake-cased name
98
218
  # collided with one of those — `Account::Freeze` -> `freeze`,
99
219
  # `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
220
+ # would silently run the Kernel method instead of dispatching: no
221
+ # error, no refusal, the call just does the wrong thing. Defining a
222
+ # real singleton method per verb closes that; the `!` suffix (every
103
223
  # command, door and Handle alike) closes it a second, permanent way —
104
224
  # `freeze!`/`send!` name nothing Kernel/Object already answers to,
105
225
  # so this exact class of collision cannot recur no matter what a
@@ -112,7 +232,7 @@ module Hecks
112
232
  end
113
233
  end
114
234
 
115
- # ONE HEAD ADDRESSES THE SAME WAY AS SEVERAL. `@ir.identified_by` is only
235
+ # One head addresses the same way as several. `@ir.identified_by` is only
116
236
  # the single-head shorthand — nil the moment an identity is composite
117
237
  # (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
118
238
  # identity payload from `identity_heads` instead reads every head, one
@@ -122,7 +242,7 @@ module Hecks
122
242
  self
123
243
  end
124
244
 
125
- # THE OTHER HALF OF A CROSS-REFERENCE. `transfer.source` already reads
245
+ # **The other half of a cross-reference**. `transfer.source` already reads
126
246
  # the raw value — a plain reader, same as any other attribute, still
127
247
  # needed by a `given`. This is the hydrated hop docs/rails-integration.md
128
248
  # designed and marked "nothing built": `transfer.source_account`
@@ -134,25 +254,25 @@ module Hecks
134
254
  # option — that shape was considered and rejected in the same design
135
255
  # note for hiding how many lookups actually happened behind one call.
136
256
  #
137
- # Defined BEFORE verb methods, not after — on the vanishing chance a
257
+ # Defined before verb methods, not after — on the vanishing chance a
138
258
  # reference's own accessor name collided with a command's, the verb
139
259
  # should win; `initialize` calls this first so `define_verb_methods`
140
260
  # defines second and last.
141
- # NO DERIVATION LEFT (ADR 0025, "References"): `reference_to`
261
+ # No derivation left (ADR 0025, "References"): `reference_to`
142
262
  # itself mints the bare attribute name now — `:account`, never
143
263
  # `:account_id` — so the accessor is spelled exactly like the
144
264
  # attribute it reads, with no `_id`-strip or `as:`-suffix rule to
145
- # apply first. `piece.account` (a METHOD, defined here) and
265
+ # apply first. `piece.account` (a method, defined here) and
146
266
  # `piece[:account]` (`Handle#[]`, bracket access reading the raw
147
267
  # 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.
268
+ # name — Ruby dispatches the two completely differently — so no
269
+ # renamed accessor (a "studio_studio"-style double suffix) is needed
270
+ # to keep them apart.
152
271
  def define_reference_accessors
153
272
  @ir.attributes.select(&:reference?).each do |attribute|
154
273
  target = attribute.type.resolve
155
- next unless target # cross-domain, or otherwise unresolvable — no accessor rather than a guess
274
+ # Cross-domain, or otherwise unresolvable — no accessor rather than a guess.
275
+ next unless target
156
276
 
157
277
  domain = @domain
158
278
  field = attribute.name
@@ -7,22 +7,23 @@ require_relative "../runtime/value"
7
7
 
8
8
  module Hecks
9
9
  module Facade
10
- # THE JSON DOOR — WHERE 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