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
@@ -5,7 +5,7 @@ require_relative "../../rendering"
5
5
  module Hecks
6
6
  module Runtime
7
7
  class Value
8
- # HOW A `list_of` ATTRIBUTE'S OWN ELEMENTS GET HYDRATED — entity-typed
8
+ # How a `list_of` attribute's own elements GET hydrated — entity-typed
9
9
  # and value-object-typed alike — split out of `Coercion` (this file's
10
10
  # sibling, extended into `Value` alongside it exactly the way
11
11
  # `Admission` already is) once `Coercion` itself grew past
@@ -15,22 +15,28 @@ module Hecks
15
15
  # own header, and this repo's `.rubocop_todo.yml` for the merge that
16
16
  # made the split necessary). Cross-calls into `Coercion`'s own
17
17
  # `for_attribute`/`build`/`fields_for`/`value_object_for`/
18
- # `trusting_stored_state?` work unqualified here exactly as they did
19
- # before the split, because both modules land on the SAME `Value`
20
- # singleton class once extended — `self` never has to know which file
21
- # a sibling method actually lives in.
18
+ # `trusting_stored_state?` work unqualified here, exactly as they do
19
+ # within `Coercion` itself, because both modules land on the same
20
+ # `Value` singleton class once extended — `self` never has to know
21
+ # which file a sibling method actually lives in.
22
22
  module EntityListCoercion
23
- # S17, ADR 0026 — SEARCHES THE WHOLE ENTITY TREE, not only the
23
+ # S17, ADR 0026 — searches the whole entity tree, not only the
24
24
  # root's own direct children. `aggregate` here is always the
25
- # ROOT aggregate — `for_attribute`'s own `aggregate` argument is
25
+ # root aggregate — `for_attribute`'s own `aggregate` argument is
26
26
  # never reassigned as hydration recurses into a nested element,
27
27
  # because coercion has to resolve value objects, and only the
28
28
  # root answers `.value_object` at all (Entity's own header
29
- # comment: an entity must NOT answer to it, or `Value.
29
+ # comment: an entity must not answer to it, or `Value.
30
30
  # for_attribute` could no longer tell a piece from a head). So
31
- # a NESTED entity — Dispatch, inside Handler — is not a direct
31
+ # a nested entity — Dispatch, inside Handler — is not a direct
32
32
  # child of the root the way Handler itself is, and a plain
33
33
  # `aggregate.entities.find` stops one level short of it.
34
+ #
35
+ # @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct to
36
+ # search, and every entity nested under it
37
+ # @param name [String] the entity's declared `hecks_name` to find
38
+ # @return [Bluebook::Entity, nil] the matching entity anywhere in the tree, or
39
+ # nil if none matches
34
40
  def find_entity(construct, name)
35
41
  construct.entities.each do |candidate|
36
42
  return candidate if candidate.hecks_name == name
@@ -41,39 +47,54 @@ module Hecks
41
47
  nil
42
48
  end
43
49
 
50
+ # Hydrates a `list_of` attribute's offered value — each element of a
51
+ # whole-list offering, or a single remove-target value, depending on
52
+ # `attribute.type` and `value`'s own shape.
53
+ #
44
54
  # Frozen through: a list read back out of the store is an answer,
45
55
  # not a handle on what is stored.
46
56
  #
47
- # ADR 0047 — this used to bail (`return value unless entity`) the
48
- # moment `attribute.type` named a value object rather than an
49
- # entity, handing back the raw, un-hydrated argument untouched.
50
- # A `sets :field` mutation sourced from a whole-array argument (as
51
- # opposed to element-by-element `append:`) went straight through
57
+ # ADR 0047 — bailing (`return value unless entity`) the moment
58
+ # `attribute.type` names a value object rather than an entity would
59
+ # hand back the raw, un-hydrated argument untouched. A `sets :field`
60
+ # mutation sourced from a whole-array argument (as opposed to
61
+ # element-by-element `append:`) goes straight through
52
62
  # `for_attribute`'s `:list` branch, so `Banking::CardPayment.
53
- # Authorize`'s own `sets :tags` (`list_of(Tag)`) stored plain
63
+ # Authorize`'s own `sets :tags` (`list_of(Tag)`) would store plain
54
64
  # Ruby Hashes as its `tags` elements forever — never a real
55
65
  # `Value`, never through `Tag`'s own `pattern:`/`invariant`
56
66
  # checks. `remove:`'s `==` comparison (a real `Value` against a
57
- # raw `Hash`) then always failed, since `Hash#==` refuses anything
58
- # that isn't itself a compatible Hash — the bug ADR 0047 traces in
59
- # full. Delegating to `hydrate_value_object_list` below closes
60
- # that gap the same way the ENTITY branch already worked: build a
61
- # real, validated `Value` per element, reusing `for_attribute`'s
62
- # own composite-construction path rather than inventing a second
63
- # one.
64
- # BUG#32 (QualityControl ledger) — `remove:`'s own single-target
65
- # value used to fall straight into `Array(value).map { ... }`
66
- # below UNGUARDED, unlike this method's own delegated
67
- # `hydrate_value_object_list` sibling, whose `value.is_a?(Array)`
68
- # check exists for exactly this shape (see that method's own
69
- # comment). For an ENTITY-typed list, `remove:`'s scalar target
70
- # (`Ledger.Void`'s `sequence: EntrySequence`) is never a Hash, so
71
- # `Array(2)` merely wrapped it as `[2]` rather than shredding it —
72
- # but the wrapping itself was still wrong: `MutationApplier#
73
- # removed`'s `element == value` then compared a stored `Entry`
74
- # Hash against a one-element Array, which can never be `==` a
75
- # Hash, so nothing was ever removed and nothing ever refused
76
- # either. Routed to `hydrate_entity_identity` instead, below.
67
+ # raw `Hash`) would then always fail, since `Hash#==` refuses
68
+ # anything that isn't itself a compatible Hash — the bug ADR 0047
69
+ # traces in full. Delegating to `hydrate_value_object_list` below
70
+ # closes that gap the same way the entity branch already works:
71
+ # build a real, validated `Value` per element, reusing
72
+ # `for_attribute`'s own composite-construction path rather than
73
+ # inventing a second one.
74
+ # BUG#32 (QualityControl ledger) — without this, `remove:`'s own
75
+ # single-target value would fall straight into
76
+ # `Array(value).map { ... }` below unguarded, unlike this method's
77
+ # own delegated `hydrate_value_object_list` sibling, whose
78
+ # `value.is_a?(Array)` check exists for exactly this shape (see
79
+ # that method's own comment). For an entity-typed list, `remove:`'s
80
+ # scalar target (`Ledger.Void`'s `sequence: EntrySequence`) is
81
+ # never a Hash, so `Array(2)` would merely wrap it as `[2]` rather
82
+ # than shredding it — but the wrapping itself would still be wrong:
83
+ # `MutationApplier#removed`'s `element == value` would then compare
84
+ # a stored `Entry` Hash against a one-element Array, which can
85
+ # never be `==` a Hash, so nothing would ever be removed and
86
+ # nothing would ever refuse either. Routed to `hydrate_entity_identity`
87
+ # instead, below.
88
+ #
89
+ # @param aggregate [Bluebook::Aggregate] the root aggregate `attribute` is
90
+ # declared on
91
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
92
+ # @param value [Array<Hash, Object>, Object] the offered value: a whole-list
93
+ # Array (a `sets`/hydrate load) or a single remove-target value
94
+ # @return [Object] a frozen Array of hydrated elements when `value` is a
95
+ # whole-list Array and `attribute.type` names an entity; otherwise whatever
96
+ # `hydrate_entity_identity` (a non-Array value naming an entity) or
97
+ # `hydrate_value_object_list` (`attribute.type` naming no entity) returns
77
98
  def hydrate_entity_list(aggregate, attribute, value)
78
99
  entity = find_entity(aggregate, attribute.type.to_s)
79
100
  return hydrate_value_object_list(aggregate, attribute, value) unless entity
@@ -89,7 +110,7 @@ module Hecks
89
110
  acc[key] = field ? for_attribute(aggregate, field, field_value) : field_value
90
111
  end
91
112
  end
92
- # BUG#33 — only a genuine WHOLE-LIST offering (`value.is_a?(Array)`)
113
+ # BUG#33 — only a genuine whole-list offering (`value.is_a?(Array)`)
93
114
  # is a caller naming every element's own identity at once; the
94
115
  # single-target shape this same method also hydrates (a `remove:`
95
116
  # target, wrapped one level up by `Array()`) never reaches this
@@ -101,16 +122,16 @@ module Hecks
101
122
 
102
123
  # `MutationApplier#check_entity_collision`'s own guard
103
124
  # (mutation_applier.rb), extended from a single caller-supplied
104
- # APPEND (BUG#13) to a whole-list REPLACE — `sets :entries` bare
125
+ # append (BUG#13) to a whole-list replace — `sets :entries` bare
105
126
  # (`Ledger.ReplaceEntries`, the corpus's first `list_of(ENTITY)`
106
127
  # command argument/mutation, qa/stress_domains/corrections). The
107
128
  # array branch above rebuilds each offered element's own declared
108
- # fields but never checked the OFFERED LIST ITSELF for either way
129
+ # fields but never checked the offered list itself for either way
109
130
  # it can misname its own entities:
110
131
  #
111
132
  # - two elements sharing one identity — the same silent-duplicate
112
133
  # hazard BUG#13's own comment describes: `EntityInterpreter#
113
- # element_of`'s own `find_index` always matches the FIRST
134
+ # element_of`'s own `find_index` always matches the first
114
135
  # match, so the second becomes permanently unaddressable by any
115
136
  # later command.
116
137
  # - an element missing its identity altogether — there is no
@@ -120,17 +141,30 @@ module Hecks
120
141
  # whole state, offered at once), so an absent identity is
121
142
  # refused rather than guessed.
122
143
  #
123
- # NON-COMPOSITE identities only, exactly BUG#13's own scope
144
+ # Non-composite identities only, exactly BUG#13's own scope
124
145
  # (`entity.identified_by` answers a single Symbol only when
125
146
  # `identity_heads.size == 1`, `Behaviour::Traits#derive_identity`) —
126
147
  # a composite identity's own duplicate/missing question is the
127
148
  # same pre-existing, narrower gap BUG#13 documented and left open,
128
149
  # not widened here.
129
150
  #
130
- # SKIPPED under `trusting_stored_state?`, the same guard `validate!`
151
+ # Refuses a whole-list offering that misnames its own entities' identities —
152
+ # a duplicate identity across two elements, or an element missing one.
153
+ #
154
+ # Skipped under `trusting_stored_state?`, the same guard `validate!`
131
155
  # already gives every value object (C6.3): a record already
132
156
  # written is trusted as it was, so tightening this check can never
133
157
  # make an old, already-persisted record unreadable.
158
+ #
159
+ # @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to,
160
+ # named in a refusal
161
+ # @param entity [Bluebook::Entity] the entity type `elements` are shaped as
162
+ # @param elements [Array<Hash, Object>] the hydrated whole-list offering; a
163
+ # non-Hash element is skipped, not checked
164
+ # @return [void]
165
+ # @raise [Runtime::TypeMismatch] if `entity` declares a single-field identity
166
+ # and an element offers no value for it
167
+ # @raise [Runtime::AlreadyExists] if two elements offer the same identity value
134
168
  def check_entity_list_identities(aggregate, entity, elements)
135
169
  identity = entity.identified_by
136
170
  return unless identity
@@ -144,32 +178,32 @@ module Hecks
144
178
  offered = fields[identity]
145
179
  if offered.nil?
146
180
  raise TypeMismatch,
147
- RefusalWording.render("TypeMismatch", "numeric_field",
148
- type: entity.hecks_name, field: identity,
149
- expected: field&.type, offered: "nil")
181
+ RefusalWording.render_site("TypeMismatch", "numeric_field",
182
+ type: entity.hecks_name, field: identity,
183
+ expected: field&.type, offered: "nil")
150
184
  end
151
185
 
152
186
  if seen.include?(offered)
153
187
  raise AlreadyExists,
154
- RefusalWording.render("AlreadyExists", "entity_duplicate",
155
- entity: entity.hecks_name, aggregate: aggregate.hecks_name,
156
- identity: entity.identity_paths.join(", "),
157
- offered: Rendering.describe(offered))
188
+ RefusalWording.render_site("AlreadyExists", "entity_duplicate",
189
+ entity: entity.hecks_name, aggregate: aggregate.hecks_name,
190
+ identity: entity.identity_paths.join(", "),
191
+ offered: [Rendering.describe(offered)])
158
192
  end
159
193
  seen << offered
160
194
  end
161
195
  end
162
196
 
163
197
  # BUG#32 — `remove:`'s own single-target value against an
164
- # ENTITY-typed list. An entity is never offered to `remove:`
165
- # WHOLE the way a value object is (`hydrate_value_object_list`'s
198
+ # entity-typed list. An entity is never offered to `remove:`
199
+ # whole the way a value object is (`hydrate_value_object_list`'s
166
200
  # own `element == value` full-value-equality shape) — an entity
167
201
  # must never answer `.value_object` at all (`Entity`'s own header
168
202
  # comment), so there is no whole-value shape here to rebuild in
169
- # the first place, only the ONE field a caller would otherwise
170
- # have to NAME to address that element any other way
203
+ # the first place, only the one field a caller would otherwise
204
+ # have to name to address that element any other way
171
205
  # (`EntityElement#element_of`'s own `wants`). So this coerces the
172
- # offered scalar against the entity's OWN identity field's
206
+ # offered scalar against the entity's own identity field's
173
207
  # declared type — not the entity's full shape — and hands back a
174
208
  # real, correctly-typed `Value` ready to compare against each
175
209
  # stored element's own identity field
@@ -177,15 +211,26 @@ module Hecks
177
211
  # `MutationApplier#removed` and `EntityElement#
178
212
  # removed_from_element` match against).
179
213
  #
180
- # A COMPOSITE identity (more than one head) has no single field a
214
+ # A composite identity (more than one head) has no single field a
181
215
  # bare `remove:` target could mean, so this passes the value
182
- # through UNCOERCED rather than guessing which head — the same
216
+ # through uncoerced rather than guessing which head — the same
183
217
  # "nothing in this corpus needs it yet" boundary
184
218
  # `MutationApplier#check_entity_collision`'s own header already
185
219
  # draws for entity identity elsewhere in this runtime.
220
+ # Coerces a `remove:` target against an entity's own single-field
221
+ # identity type, rather than the entity's full shape.
222
+ #
186
223
  # `list_element_match?` treats an uncoerced value the same way
187
224
  # it always treated the pre-fix wrapped Array: never a match, a
188
225
  # documented no-op rather than a crash.
226
+ #
227
+ # @param aggregate [Bluebook::Aggregate] the aggregate `entity` belongs to
228
+ # @param entity [Bluebook::Entity] the entity type `value` addresses
229
+ # @param value [Object] the offered remove-target value
230
+ # @return [Runtime::Value, Object] `value` already coerced (returned unchanged);
231
+ # the value coerced against the identity field's declared type when `entity`
232
+ # has a single identity head and a matching attribute; `value` unchanged
233
+ # otherwise (no identity head, a composite identity, or no matching attribute)
189
234
  def hydrate_entity_identity(aggregate, entity, value)
190
235
  return value if value.is_a?(self)
191
236
 
@@ -198,9 +243,12 @@ module Hecks
198
243
  for_attribute(aggregate, field, value)
199
244
  end
200
245
 
246
+ # Hydrates a `list_of` attribute's offered value when `attribute.type`
247
+ # names a value object rather than an entity.
248
+ #
201
249
  # The value-object sibling of the entity branch above: an element
202
250
  # already shaped like the target `Value` (or a `Hash`/scalar that
203
- # `fields_for` can still open) is rebuilt through the SAME `build`
251
+ # `fields_for` can still open) is rebuilt through the same `build`
204
252
  # a scalar composite attribute already uses (`for_attribute`'s own
205
253
  # `coerced = ... build(value_object, fields_for(...), aggregate)`
206
254
  # line) — same defaults, same `pattern:`/`admits:`/invariant
@@ -210,21 +258,32 @@ module Hecks
210
258
  # element passes through unchanged, exactly as the entity branch's
211
259
  # own non-Hash elements do.
212
260
  #
213
- # NOT `Array(value).map` (unlike the entity branch above) — `value`
261
+ # Not `Array(value).map` (unlike the entity branch above) — `value`
214
262
  # here is not always genuinely list-shaped. `MutationApplier#
215
263
  # removed`'s own `Value.for_attribute(aggregate, attribute, value)`
216
- # call (`attribute` = the LIST attribute, `mutation.target`; `value`
217
- # = the single REMOVE-target argument, already a real `Value` by
264
+ # call (`attribute` = the list attribute, `mutation.target`; `value`
265
+ # = the single remove-target argument, already a real `Value` by
218
266
  # the time it gets here) reuses this exact branch — for `remove:`,
219
267
  # not for a whole-list `sets`. `Array(a_real_Value)` alone would be
220
268
  # harmless (`Value` defines neither `to_a` nor `to_ary`, so Kernel
221
- # wraps it `[value]`), but `Array(a_Hash)` is NOT harmless: Ruby's
269
+ # wraps it `[value]`), but `Array(a_Hash)` is not harmless: Ruby's
222
270
  # `Array()` opens a bare Hash into its own `[[k, v], ...]` pairs,
223
271
  # not `[hash]` — silently shredding a single-element Hash-shaped
224
272
  # target into garbage instead of hydrating it. Branching on
225
273
  # `value.is_a?(Array)` up front (true only for a genuine whole-list
226
274
  # `sets`/hydrate load) keeps the single-target shape a single
227
275
  # target, hydrated the same way, never listified.
276
+ #
277
+ # @param aggregate [Bluebook::Aggregate] the aggregate `attribute` is declared
278
+ # on, or a construct that does not respond to `value_object` (an entity), in
279
+ # which case `value` passes through unchanged
280
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute being hydrated
281
+ # @param value [Array<Hash, Object>, Object] the offered value: a whole-list
282
+ # Array, or a single remove-target value
283
+ # @return [Object] `value` unchanged when `aggregate` does not respond to
284
+ # `value_object`, or `attribute.type` names no value object; otherwise a
285
+ # frozen Array of built `Value`s for a whole-list Array, or a single built
286
+ # `Value` (via `hydrate_value_object_element`) otherwise
228
287
  def hydrate_value_object_list(aggregate, attribute, value)
229
288
  return value unless aggregate.respond_to?(:value_object)
230
289
 
@@ -237,6 +296,19 @@ module Hecks
237
296
  Freezer.deep(hydrated)
238
297
  end
239
298
 
299
+ # Rebuilds one `list_of` element into a real, validated `Value`, unless it
300
+ # already is one of `value_object`'s own type.
301
+ #
302
+ # @param aggregate [Bluebook::Aggregate] the aggregate `value_object` is
303
+ # resolved against
304
+ # @param attribute [Bluebook::Attribute] the `list_of` attribute `element`
305
+ # belongs to
306
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass `element`
307
+ # is built as
308
+ # @param element [Object] the raw element to build: a Hash of fields, a scalar
309
+ # `fields_for` can still open, or an already-built `Value`
310
+ # @return [Runtime::Value] `element` unchanged if it is already a `Value` of
311
+ # `value_object`'s own type; otherwise a freshly built one
240
312
  def hydrate_value_object_element(aggregate, attribute, value_object, element)
241
313
  return element if element.is_a?(self) && element.type_name == value_object.hecks_name
242
314
 
@@ -8,7 +8,7 @@ require_relative "value/admission"
8
8
  module Hecks
9
9
  module Runtime
10
10
  # A typed value object in hand: frozen fields, read by name. How one is
11
- # MADE — coerced from a raw argument, checked against its declared
11
+ # made — coerced from a raw argument, checked against its declared
12
12
  # numeric types, patterns and closed sets — is the class-side engine in
13
13
  # value/coercion.rb, value/entity_list_coercion.rb (a `list_of`
14
14
  # attribute's own elements — split out once growing Coercion tripped
@@ -22,10 +22,10 @@ module Hecks
22
22
 
23
23
  attr_reader :value_object
24
24
 
25
- # FROZEN THROUGH, not just on top.
25
+ # Frozen through, not just on top.
26
26
  #
27
27
  # `@fields.freeze` alone stops a key being added or removed and
28
- # nothing else: the String, Array or Hash a field HOLDS stays
28
+ # nothing else: the String, Array or Hash a field holds stays
29
29
  # mutable, so `vo[:value] << "!"` edits a value object in place —
30
30
  # demonstrated on a real dispatch before this was written, not
31
31
  # supposed. Same shape as the three freezing bugs already fixed
@@ -33,29 +33,67 @@ module Hecks
33
33
  # was frozen and the contents were not.
34
34
  #
35
35
  # A value object is the one thing in the domain that has no
36
- # identity to change over — `with` already answers a NEW one rather
36
+ # identity to change over — `with` already answers a new one rather
37
37
  # than mutating — so freezing it through is what it always claimed
38
38
  # to be.
39
+ #
40
+ # @param value_object [Bluebook::ValueObject] the declared type this instance is one of
41
+ # @param fields [Hash] the type's fields, keyed by name (String or Symbol); deep-frozen
42
+ # and stored with Symbol keys
39
43
  def initialize(value_object, fields)
40
44
  @value_object = value_object
41
45
  @fields = Freezer.deep(fields.transform_keys(&:to_sym))
42
46
  freeze
43
47
  end
44
48
 
49
+ # Reads the declared type's own name.
50
+ #
51
+ # @return [String] the value object's `hecks_name`
45
52
  def type_name = @value_object.hecks_name
53
+
54
+ # Reads one field.
55
+ #
56
+ # @param field [String, Symbol] the field name; `:value` reads the sole field of a
57
+ # single-attribute value object, whatever it is actually named
58
+ # @return [Object, nil] the field's coerced value; nil if the field is not held
46
59
  def [](field) = @fields[resolve_field(field)]
60
+
61
+ # Answers whether this value object holds the named field.
62
+ #
63
+ # @param field [String, Symbol] the field name; `:value` resolves the same way `[]` does
64
+ # @return [Boolean] true when the field is held
47
65
  def key?(field) = @fields.key?(resolve_field(field))
48
66
  def to_h = @fields.transform_values { |value| self.class.materialize(value) }
67
+
68
+ # Renders this value object as JSON, through the same shape `to_h` builds.
69
+ #
70
+ # @return [String] a JSON object of the materialized fields
49
71
  def to_json(*) = JSON.generate(to_h)
50
72
 
51
73
  def ==(other)
52
74
  other.is_a?(self.class) && other.type_name == type_name && other.to_h == to_h
53
75
  end
54
76
 
77
+ # Builds a new value object of the same type with one field replaced, re-validated.
78
+ #
79
+ # @param field [String, Symbol] the field to replace; `:value` resolves the same way
80
+ # `[]` does
81
+ # @param value [Object] the field's new, uncoerced value
82
+ # @return [Runtime::Value] a new instance of the same type, with `field` replaced
83
+ # @raise [Runtime::TypeMismatch] if the new fields do not satisfy the type's declared
84
+ # shape (an unknown field, a missing required one, a wrong numeric type, …)
85
+ # @raise [Runtime::InvariantViolation] if the new fields violate one of the type's own
86
+ # invariants, or are not a member of its closed set
55
87
  def with(field, value)
56
88
  self.class.build(@value_object, @fields.merge(resolve_field(field) => value))
57
89
  end
58
90
 
91
+ # Recursively converts a `Runtime::Value` (and any nested inside a Hash or Array) to
92
+ # plain data.
93
+ #
94
+ # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
95
+ # Array or Hash passes through unchanged
96
+ # @return [Object] `value` with every nested `Runtime::Value` replaced by its own `to_h`
59
97
  def self.materialize(value)
60
98
  case value
61
99
  when self then value.to_h
@@ -68,27 +106,33 @@ module Hecks
68
106
  # `materialize`, but a single-attribute value object (`sole_attribute`
69
107
  # — [[feedback_name_the_scalar_field]]) recurses into its own bare
70
108
  # field instead of building `{field: ...}` — a Board's own `label`
71
- # unwraps to `"Kanban"`, not `{value: "Kanban"}`. NOT a replacement
109
+ # unwraps to `"Kanban"`, not `{value: "Kanban"}`. Not a replacement
72
110
  # for `materialize` itself: every existing report/query/command
73
111
  # caller keeps the wrapped shape it already depends on (Banking's
74
112
  # own `CustomerPortfolio` reads `payment[:amount][:cents]`, and
75
113
  # changing that out from under it would be a real breaking change,
76
114
  # not a bug fix). This is read_model_interpreter.rb's own opt-in,
77
- # used ONLY for a `group_by`-declared head's own rows — grouping
115
+ # used only for a `group_by`-declared head's own rows — grouping
78
116
  # needs a real scalar to key by regardless, so a report already
79
117
  # asking for that gets the unwrap for free.
118
+ #
119
+ # @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
120
+ # Array or Hash passes through unchanged
121
+ # @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
122
+ # field's value (recursively unwrapped), or by its own `Hash` of fields when it has
123
+ # more than one
80
124
  def self.materialize_unwrapped(value)
81
125
  case value
82
126
  when self
83
127
  sole = value.value_object.sole_attribute
84
128
  return materialize_unwrapped(value[sole.name]) if sole
85
129
 
86
- # NOT `value.to_h` — `Value#to_h` materializes each field through
130
+ # Not `value.to_h` — `Value#to_h` materializes each field through
87
131
  # plain `materialize`, so a VO nested inside a multi-attribute VO
88
132
  # would already be a plain Hash by the time this method ever saw
89
133
  # it, and never reach the `when self` branch above. Read each
90
134
  # field straight off `value` instead, so recursion actually
91
- # happens through THIS method the whole way down.
135
+ # happens through this method the whole way down.
92
136
  value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
93
137
  when Array then value.map { |item| materialize_unwrapped(item) }
94
138
  when Hash then value.transform_values { |item| materialize_unwrapped(item) }
@@ -96,22 +140,28 @@ module Hecks
96
140
  end
97
141
  end
98
142
 
99
- # REDUCES AN APPEND-ONLY SUB-LOG TO ITS CURRENT STATE — the same
143
+ # Reduces an append-only sub-log to its current state — the same
100
144
  # "a later fact supersedes an earlier one" reduction this runtime
101
- # already performs replaying an AGGREGATE's own command history
145
+ # already performs replaying an aggregate's own command history
102
146
  # into its current attributes, applied here to a single `list_of`
103
- # FIELD acting as its own miniature append-only log (a placement
147
+ # field acting as its own miniature append-only log (a placement
104
148
  # history, a tombstone-style soft-delete list, a versioned
105
149
  # setting). `rows` is what a `list_of` attribute hands back — an
106
150
  # Array of `Value`, in append order — and `key` names the field
107
151
  # that identifies "the same logical thing" across entries.
108
152
  #
109
- # GROUPING ONLY, NEVER INTERPRETATION. What counts as "removed,"
153
+ # **Grouping only, never interpretation**. What counts as "removed,"
110
154
  # how to order what survives — that meaning belongs to whichever
111
155
  # domain declared the field, never here: a generic reduction that
112
156
  # started guessing domain semantics would need to keep guessing
113
157
  # forever, once per shape of "gone" any caller ever invents. The
114
158
  # caller filters and sorts the result; this only groups it.
159
+ #
160
+ # @param rows [Array<Runtime::Value>] a `list_of` attribute's own elements, in append order
161
+ # @param key [Symbol] the method to call on each row to find "the same logical thing"
162
+ # across entries (typically a field reader)
163
+ # @return [Array<Runtime::Value>] one row per distinct `key` value, each the latest row
164
+ # that had it
115
165
  def self.latest_by(rows, key)
116
166
  rows.to_h { |row| [row.public_send(key), row] }.values
117
167
  end
@@ -119,17 +169,17 @@ module Hecks
119
169
  def method_missing(name, *args)
120
170
  return @fields[name] if @fields.key?(name)
121
171
 
122
- # THE LANGUAGE RULE, not a convenience: ANY value object with
172
+ # The language rule, not a convenience: any value object with
123
173
  # exactly one declared attribute answers `.value`, whatever that
124
174
  # attribute is actually named — a single-attribute value object
125
- # is a NAME for a scalar, not a genuine group
175
+ # is a name for a scalar, not a genuine group
126
176
  # ([[feedback_name_the_scalar_field]], `Behaviour::ValueObject#
127
177
  # sole_attribute`), so `money.value` reads `Money`'s own `amount`
128
178
  # exactly as `label.value` reads a shorthand-declared `value`.
129
- # AFTER the real-field lookup above, on purpose: a field
130
- # literally named `value` is already answered there (and IS the
179
+ # After the real-field lookup above, on purpose: a field
180
+ # literally named `value` is already answered there (and is the
131
181
  # sole attribute whenever the count is one), so this branch only
132
- # ever aliases, never shadows. A MULTI-attribute value object
182
+ # ever aliases, never shadows. A multi-attribute value object
133
183
  # keeps its NoMethodError — `sole_attribute` answers nil for it,
134
184
  # and falling through to `super` is exactly the refusal it
135
185
  # always gave: with two or more fields there is no single value
@@ -151,15 +201,15 @@ module Hecks
151
201
 
152
202
  private
153
203
 
154
- # THE `.value` ALIAS FOR INDEXED ACCESS — the same language rule
204
+ # **The `.value` alias for indexed access** — the same language rule
155
205
  # `method_missing` above enforces for method reads, applied to
156
206
  # `[]`/`key?`/`with`: `:value` names a single-attribute value
157
207
  # object's sole field whatever that field is actually called. A
158
- # REAL key always wins first (a field literally named `value` is
208
+ # real key always wins first (a field literally named `value` is
159
209
  # its own answer, and is the sole attribute anyway whenever the
160
210
  # count is one), so this only ever resolves a `:value` that would
161
- # otherwise MISS — it can never redirect a genuine field read.
162
- # `with(:value, x)` in particular NEEDS this: merging a literal
211
+ # otherwise miss — it can never redirect a genuine field read.
212
+ # `with(:value, x)` in particular needs this: merging a literal
163
213
  # `:value` key beside a sole field named `amount` would build a
164
214
  # two-key hash for a one-field shape and be refused (or worse,
165
215
  # stored) downstream — aliasing at the merge is what keeps the
data/lib/hecks/runtime.rb CHANGED
@@ -6,24 +6,24 @@
6
6
  # declaration is being collected into — sat on the top-level Hecks module
7
7
  # beside the DSL words.
8
8
  #
9
- # What lives here is what RUNS a domain :
9
+ # What lives here is what runs a domain :
10
10
  #
11
11
  # Runtime.boot(path) load a directory and return its Dispatcher
12
12
  # Runtime.with_registry(r) bind the ambient registry for the duration of a load
13
13
  # Runtime.current_registry the registry a declaration is landing in, or nil
14
14
  #
15
- # What does NOT live here is what DECLARES one — `Hecks.bluebook`,
15
+ # What does not live here is what declares one — `Hecks.bluebook`,
16
16
  # `.hecksagon`, `.port`, `.adapter`, `.world` are loading words and stay on the
17
17
  # top-level module, which now reads as a facade over this.
18
18
  #
19
19
  # runtime = Hecks::Runtime.boot("examples/pizzas/bluebook")
20
20
  # runtime.dispatch("Pizzas::Pizza.CreatePizza", name: "Margherita")
21
21
  #
22
- # NOTE, and it is the reason this file exists : `current_registry` is still
22
+ # Note, and it is the reason this file exists : `current_registry` is still
23
23
  # process-global. Each boot builds a fresh Registry, and `Loader.bind_runtime`
24
- # installs a fresh facade door whose modules close over THAT boot's dispatcher
24
+ # installs a fresh facade door whose modules close over that boot's dispatcher
25
25
  # — no class-level runtime binding remains, so two boots in one process no
26
- # longer share dispatch state, only the top-level NAME (the last-bound door
26
+ # longer share dispatch state, only the top-level name (the last-bound door
27
27
  # wins the constant, which is what per-boot install means). Owning the state
28
28
  # here was the first move ; the dispatcher reachable per-runtime rather than
29
29
  # through a constant was the second, and it is done — the door is the only
@@ -68,17 +68,40 @@ module Hecks
68
68
 
69
69
  # Load a bluebook directory and return the Dispatcher bound to it.
70
70
  # `install_facade:`, `environment:` — see Loader.boot.
71
+ #
72
+ # @param path [String] path to a domain directory, or a file inside one
73
+ # @param shared [String, nil] a shared-root override; see `Loader.boot`
74
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
75
+ # Ruby facade constants for this boot
76
+ # @param environment [String, nil] the environment name passed through to
77
+ # `Adapters::Folder#load_domain`
78
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
79
+ # to the booted domain
71
80
  def boot(path, shared: nil, install_facade: true, environment: nil)
72
81
  Loader.boot(path, shared: shared, install_facade: install_facade, environment: environment)
73
82
  end
74
83
 
75
84
  # `paths` form — see Loader.boot_files.
85
+ #
86
+ # @param paths [String, Array<String>] one or more file paths within the domain
87
+ # to load, instead of the whole directory
88
+ # @param shared [String, nil] a shared-root override; see `Loader.boot_files`
89
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add`-style
90
+ # Ruby facade constants for this boot
91
+ # @param environment [String, nil] the environment name passed through to the
92
+ # selected-file loader
93
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the dispatcher bound
94
+ # to the booted domain
76
95
  def boot_files(paths, shared: nil, install_facade: true, environment: nil)
77
96
  Loader.boot_files(paths, shared: shared, install_facade: install_facade, environment: environment)
78
97
  end
79
98
 
80
99
  # Bind the ambient registry for the duration of the block, restoring
81
100
  # whatever was there before. Nesting is safe ; a raise still restores.
101
+ #
102
+ # @param registry [Runtime::Registry] the registry to make current for the block
103
+ # @yield the code that should see `registry` as `current_registry`
104
+ # @return [Object] the block's result
82
105
  def with_registry(registry)
83
106
  previous = @current_registry
84
107
  @current_registry = registry
@@ -91,7 +114,7 @@ module Hecks
91
114
  # the block — who a command's declared `role`, if any, is checked
92
115
  # against.
93
116
  #
94
- # `as_of:` is OPTIONAL, same opt-in shape as `actor_id:` — a caller
117
+ # `as_of:` is optional, same opt-in shape as `actor_id:` — a caller
95
118
  # that wants a Governance `RoleAssignment`'s `starts_at` enforced
96
119
  # passes `as_of: Ports::Clock.now(registry)` here, at the door,
97
120
  # exactly where `cli_runner.rb` already merges `Clock.now` into a
@@ -99,11 +122,20 @@ module Hecks
99
122
  # itself — see `Ports::Clock`'s own header for why — so an unbound
100
123
  # `as_of` leaves `starts_at` unchecked, exactly as before.
101
124
  #
102
- # `scope:` is OPTIONAL too — a caller that states which scope it is
125
+ # `scope:` is optional too — a caller that states which scope it is
103
126
  # acting in gets that scope checked against the matching
104
127
  # `RoleAssignment`'s own `scope`, not just its `role_name`. See
105
128
  # `Runtime::Caller::Current`'s own header for why this lives here
106
129
  # rather than as a command-level DSL construct.
130
+ # @param role [String, Symbol] the role to check the caller against
131
+ # @param actor_id [String, nil] who is calling, checked against a real Governance
132
+ # `RoleAssignment` when given; string-equality only against `role` when nil
133
+ # @param as_of [Integer, nil] Unix epoch seconds to check a matching `RoleAssignment`'s
134
+ # own `starts_at` against; unchecked when nil
135
+ # @param scope [String, nil] the scope to check a matching `RoleAssignment`'s own
136
+ # `scope` against; unchecked when nil
137
+ # @yield the code to run with this caller bound
138
+ # @return [Object] the block's result
107
139
  def as_caller(role:, actor_id: nil, as_of: nil, scope: nil, &)
108
140
  Caller.as(role: role, actor_id: actor_id, as_of: as_of, scope: scope, &)
109
141
  end