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,20 +1,21 @@
1
1
  module Hecks
2
2
  module QuerySpecification
3
- # ONE reading of a dotted query-field path, shared by every place that
4
- # used to invent its own. The reference interpreter did a bare
5
- # `record[field]` lookup (a dotted path silently matched nothing), the
6
- # SQL side split dots but judged numericness from the FIRST nested
7
- # segment only (a two-level path quietly compared text), and the build
8
- # seal walked the declaration graph a third way. Three
9
- # implementations of "what does pizza.price_cents.cents mean" is two
10
- # more than a language gets to have.
3
+ # One reading of a dotted query-field path, shared by every place that
4
+ # would otherwise invent its own. Left to themselves they disagree: a
5
+ # reference interpreter doing a bare `record[field]` lookup silently
6
+ # matches nothing for a dotted path, an SQL side that splits dots but
7
+ # judges numericness from the first nested segment only quietly
8
+ # compares a two-level path as text, and a build seal walks the
9
+ # declaration graph a third way. Three implementations of "what does
10
+ # pizza.price_cents.cents mean" is two more than a language gets to
11
+ # have.
11
12
  #
12
13
  # Two sides, deliberately in one file so they cannot drift:
13
14
  #
14
- # dig(holder, field) — VALUE side: walk a record's held state,
15
+ # dig(holder, field) — value side: walk a record's held state,
15
16
  # segment by segment, through Value
16
17
  # objects and plain hashes alike.
17
- # leaf_attribute / numeric? — DECLARATION side: walk the declared
18
+ # leaf_attribute / numeric? — declaration side: walk the declared
18
19
  # shape to the attribute a path lands on.
19
20
  # Callers supply value-object lookup as a
20
21
  # block, because an Aggregate and a
@@ -26,22 +27,39 @@ module Hecks
26
27
  NUMERIC_PRIMITIVES = %w[Integer Float].freeze
27
28
  SCALAR_PRIMITIVES = %w[String Integer Float TrueClass FalseClass].freeze
28
29
 
30
+ # Reads the value a dotted field path names out of a record's held state.
31
+ #
29
32
  # The held value a dotted field names, or nil — never a raise. The
30
33
  # first segment reads off the record (an Instance, a Value, or a
31
34
  # plain row hash); the rest read through whatever each step holds. A
32
35
  # stored nested value object is a plain hash by the time it is read
33
36
  # back, keyed by symbol in memory and by string off a wire decode,
34
37
  # so both spellings are tried — `key?` first, never `||`, because
35
- # `||` falls through a genuinely-stored `false` to the OTHER
38
+ # `||` falls through a genuinely-stored `false` to the other
36
39
  # spelling (usually absent) and returns `nil` instead. The seal
37
40
  # admits boolean leaves (`SCALAR_PRIMITIVES` below), so a `false`
38
41
  # here is a real, held answer, not a missing one.
42
+ #
43
+ # @param holder [Runtime::Instance, Runtime::Value, Hash, nil] the record, value
44
+ # object or row Hash the first segment is read from
45
+ # @param field [String, Symbol, nil] the path, segments separated by `.`, such as
46
+ # `"price.cents"`; a bare field name is a one-segment path
47
+ # @return [Object, nil] the value held at the end of the path; `nil` when `field` is
48
+ # `nil`, a segment is absent, or a step lands on `nil` or an Array
39
49
  def dig(holder, field)
40
50
  return nil if field.nil?
41
51
 
42
52
  field.to_s.split(".").reduce(holder) { |current, segment| read(current, segment) }
43
53
  end
44
54
 
55
+ # Reads one path segment off whatever the previous step held — a single
56
+ # step of `dig`.
57
+ #
58
+ # @param current [Runtime::Instance, Runtime::Value, Hash, Array, nil] the value the
59
+ # walk has reached; a Hash is tried by Symbol key, then by String key
60
+ # @param segment [String] one segment of the dotted path
61
+ # @return [Object, nil] the member named `segment`; `nil` when `current` is `nil` or
62
+ # an Array, or holds no such member
45
63
  def read(current, segment)
46
64
  return nil if current.nil?
47
65
 
@@ -54,7 +72,7 @@ module Hecks
54
72
  # an Array broke it: `Array#[]` demands an Integer index, so
55
73
  # `current[segment]` (a String) raised `TypeError` straight
56
74
  # through `dig` instead of answering nil. A dotted path stepping
57
- # INTO a list_of attribute (`where "tags.name" == "x"` against a
75
+ # into a list_of attribute (`where "tags.name" == "x"` against a
58
76
  # bare list, rather than each element) has no single member a
59
77
  # bare index would name anyway — nil is the honest answer, the
60
78
  # same one a dangling reference or a missing key already gets.
@@ -63,11 +81,23 @@ module Hecks
63
81
  current[segment]
64
82
  end
65
83
 
84
+ # Walks the declared shape to the attribute a dotted path lands on.
85
+ #
66
86
  # The declared attribute a path lands on, or nil. `segments` is the
67
87
  # dotted tail — for a bare field it is empty and the root attribute
68
88
  # is its own leaf. The walk stops dead at a reference (an id is a
69
89
  # scalar; nothing nests under it) and at any member the declared
70
90
  # value object does not carry.
91
+ #
92
+ # @param attribute [Bluebook::Attribute, nil] the root attribute the path's first
93
+ # segment names; `nil` (no such attribute) answers `nil`
94
+ # @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
95
+ # @yield looks a value object up by name in the caller's own declarations
96
+ # @yieldparam type [String] the type name of the attribute being stepped into
97
+ # @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil` when
98
+ # the type is not a value object the caller knows
99
+ # @return [Bluebook::Attribute, nil] the attribute at the end of the path; `nil` when
100
+ # a step crosses a reference, an undeclared type or a member that does not exist
71
101
  def leaf_attribute(attribute, segments)
72
102
  current = attribute
73
103
  segments.each do |segment|
@@ -79,12 +109,24 @@ module Hecks
79
109
  current
80
110
  end
81
111
 
112
+ # Decides from the declared shape whether a field path holds a number.
113
+ #
82
114
  # May an ordered comparator (lt/lte/gt/gte) land here, and may an
83
- # ORDER BY cast here numerically? A bare field keeps the one-level
115
+ # `ORDER BY` cast here numerically? A bare field keeps the one-level
84
116
  # convention every adapter already implements: a numeric primitive,
85
117
  # or a value object carrying at least one numeric member. A dotted
86
118
  # path must land on a numeric primitive itself — the convention does
87
- # not reach through a named path, it IS the absence of one.
119
+ # not reach through a named path, it is the absence of one.
120
+ #
121
+ # @param attribute [Bluebook::Attribute, nil] the root attribute the path starts at;
122
+ # `nil` answers `false`
123
+ # @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
124
+ # @yield looks a value object up by name, as for `leaf_attribute`
125
+ # @yieldparam type [String] the type name to look up
126
+ # @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil`
127
+ # @return [Boolean] `true` when the leaf is an `Integer` or `Float`, or a bare field
128
+ # naming a value object with at least one such member; `false` for a list, a
129
+ # reference, or a path that lands nowhere
88
130
  def numeric?(attribute, segments, &)
89
131
  leaf = leaf_attribute(attribute, segments, &)
90
132
  return false if leaf.nil? || leaf.list? || leaf.reference?
@@ -95,9 +137,21 @@ module Hecks
95
137
  !shape.nil? && shape.attributes.any? { |member| NUMERIC_PRIMITIVES.include?(member.type.to_s) }
96
138
  end
97
139
 
98
- # A dotted path must end on a SCALAR member — landing on a value
140
+ # Decides whether a path ends on a scalar primitive that every engine
141
+ # compares the same way.
142
+ #
143
+ # A dotted path must end on a scalar member — landing on a value
99
144
  # object would hand SQL a JSON object where the reference
100
145
  # interpreter unwraps a hash, and the two would answer differently.
146
+ #
147
+ # @param attribute [Bluebook::Attribute, nil] the root attribute the path starts at;
148
+ # `nil` answers `false`
149
+ # @param segments [Array<String>] the path's remaining segments, `[]` for a bare field
150
+ # @yield looks a value object up by name, as for `leaf_attribute`
151
+ # @yieldparam type [String] the type name to look up
152
+ # @yieldreturn [Class<Bluebook::ValueObject>, nil] the declared shape, or `nil`
153
+ # @return [Boolean] `true` when the leaf is a non-list, non-reference attribute typed
154
+ # `String`, `Integer`, `Float`, `TrueClass` or `FalseClass`
101
155
  def scalar_leaf?(attribute, segments, &)
102
156
  leaf = leaf_attribute(attribute, segments, &)
103
157
  !leaf.nil? && !leaf.list? && !leaf.reference? && SCALAR_PRIMITIVES.include?(leaf.type.to_s)
@@ -2,25 +2,25 @@ require_relative "../naming"
2
2
 
3
3
  module Hecks
4
4
  module QuerySpecification
5
- # ONE reading of a dotted query-field path that hops THROUGH a
5
+ # One reading of a dotted query-field path that hops through a
6
6
  # reference into another aggregate's own shape — FieldPath's
7
- # sibling, not its member. FieldPath walks a SHAPE, which cannot
7
+ # sibling, not its member. FieldPath walks a shape, which cannot
8
8
  # loop, and answers nil, never raising, because there is nothing
9
- # left to say beyond "not found." HopPath walks the REFERENCE
10
- # GRAPH instead — `BluebookBuilder#validate_no_bidirectional_references!`
11
- # refuses any reference CYCLE at declaration time (ADR 0025,
9
+ # left to say beyond "not found." HopPath walks the reference
10
+ # graph instead — `BluebookBuilder#validate_no_bidirectional_references!`
11
+ # refuses any reference cycle at declaration time (ADR 0025,
12
12
  # "References" — widened from a direct pair to any ring, DFS over
13
13
  # the chapter's own reference graph), so what's left to guard here
14
14
  # is depth, not cycles: MAX_HOPS below, and this module's own
15
15
  # `refusal` states for a target the chapter doesn't resolve. A walk
16
- # here still needs to say WHY it stopped, not just that it did.
16
+ # here still needs to say why it stopped, not just that it did.
17
17
  #
18
- # Every method below takes an ATTRIBUTE ARRAY, never a "shape"
18
+ # Every method below takes an attribute array, never a "shape"
19
19
  # object — deliberately, because the two real callers hold their
20
20
  # attributes differently. `AggregateBuilder` (tier-1 seal, mid-
21
21
  # build) exposes `attributes` as a plain reader, but its own
22
22
  # `attribute(name, type = String, ...)` is the DSL's attribute-
23
- # DECLARING method — calling it as a finder would silently mint a
23
+ # declaring method — calling it as a finder would silently mint a
24
24
  # new String attribute instead of looking one up. A sealed
25
25
  # `Aggregate` (tier-2 seal, and every runtime target) has a
26
26
  # real `attribute(name)` finder, but taking the array either way
@@ -37,37 +37,51 @@ module Hecks
37
37
  Plan = Struct.new(:hops, :tail, :refusal, keyword_init: true)
38
38
 
39
39
  # A hop chain long enough to matter is long enough to be a
40
- # mistake — NOT a guard against a walk that cannot terminate.
40
+ # mistake — not a guard against a walk that cannot terminate.
41
41
  # Nothing here loops forever regardless of how the reference
42
42
  # graph is shaped: a hop chain is a literal dotted string, fixed
43
43
  # at declaration time, and every step consumes exactly one of
44
- # its own segments — the walk is bounded by what was TYPED, not
44
+ # its own segments — the walk is bounded by what was typed, not
45
45
  # by the graph. A self-referential aggregate hopping through
46
46
  # itself more than once (`"parent.parent.name"`, a grandparent
47
47
  # query) is real, common, and perfectly safe; this exists only
48
48
  # to refuse a chain nobody meant to write this long.
49
49
  MAX_HOPS = 8
50
50
 
51
+ # Names the path segment a reference attribute answers to.
52
+ #
51
53
  # The segment name a Reference answers to in a hop path — its own
52
54
  # declared attribute name, unchanged (ADR 0025, "References":
53
- # `reference_to` mints that bare name now, no `_id`, so there is
55
+ # `reference_to` mints that bare name, no `_id`, so there is
54
56
  # no derivation left to apply). `proposal/client` (a query hop)
55
57
  # and `proposal.client` (the Ruby accessor,
56
58
  # `Facade::Handle#define_reference_accessors`) name the same
57
59
  # concept the same way.
60
+ #
61
+ # @param attribute [Bluebook::Attribute] a reference-typed attribute
62
+ # @return [String] the attribute's declared name
58
63
  def hop_name(attribute) = attribute.name.to_s
59
64
 
60
- # Does this path's HEAD cross into another record via `/`? THE
61
- # OPERATOR IS THE ANSWER NOW, not a name collision to arbitrate —
65
+ # Decides whether a field path starts by hopping through one of the given
66
+ # references, without resolving the reference's target.
67
+ #
68
+ # Does this path's head cross into another record via `/`? The
69
+ # operator is the answer now, not a name collision to arbitrate —
62
70
  # `.` walks fields inside this record, `/` crosses into another
63
71
  # one, so a path with no `/` is never a hop, full stop, and the
64
- # OLD "a real local attribute wins first" rule (needed only
72
+ # old "a real local attribute wins first" rule (needed only
65
73
  # because `.` was overloaded for both meanings, and `client_id`
66
74
  # vs `client` was how the two were told apart) has nothing left
67
75
  # to arbitrate. Answerable from `attributes` alone — a Reference
68
76
  # knows its own `target_name` at declaration, before it can
69
- # `resolve` it — which is what lets the AGGREGATE seal recognise
77
+ # `resolve` it — which is what lets the aggregate seal recognise
70
78
  # a hop it cannot yet check.
79
+ #
80
+ # @param field [String, Symbol] the query field path, such as `:"client/status"`
81
+ # @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
82
+ # starts from
83
+ # @return [Boolean] `true` when the path has a `/` and the segment before the first
84
+ # one names a reference attribute
71
85
  def hop_head?(field, attributes)
72
86
  head, rest = field.to_s.split("/", 2)
73
87
  return false unless rest
@@ -75,16 +89,27 @@ module Hecks
75
89
  attributes.any? { |candidate| candidate.reference? && hop_name(candidate) == head }
76
90
  end
77
91
 
78
- # ONE STEP: does `field`'s head hop through one of `attributes`'
92
+ # Resolves the first hop of a field path and hands back what is left to walk.
93
+ #
94
+ # **One step**: does `field`'s head hop through one of `attributes`'
79
95
  # own references? Answers the resolved `Hop` plus the string
80
96
  # still left to walk (itself possibly another `/`-hop, against
81
- # the TARGET's own attributes, or a plain `.`-dotted field walk
97
+ # the target's own attributes, or a plain `.`-dotted field walk
82
98
  # once the hops run out) — or nil, when the head names nothing or
83
99
  # the path has no `/` at all. This is the one primitive
84
100
  # Runtime::ReferenceHop needs: it recurses hop by hop through its
85
101
  # own `apply`, one ordinary same-aggregate query at a time, and
86
102
  # never needs the whole chain resolved up front the way a seal
87
103
  # does.
104
+ #
105
+ # @param field [String, Symbol] the query field path, such as `"client/region/name"`
106
+ # @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
107
+ # starts from
108
+ # @return [Array(Hop, String), nil] the hop and the rest of the path after the first
109
+ # `/`; the hop's `target` is `nil` when the referenced aggregate is not in the
110
+ # declaring chapter. `nil` when the path has no `/` or its head names no reference
111
+ # @raise [Bluebook::DSL::Malformed] if the reference has no `declared_in` aggregate
112
+ # to resolve its target through
88
113
  def next_hop(field, attributes)
89
114
  head, rest = field.to_s.split("/", 2)
90
115
  return nil unless rest
@@ -96,10 +121,22 @@ module Hecks
96
121
  [hop, rest]
97
122
  end
98
123
 
99
- # The WHOLE chain, resolved — every hop's target found, in
124
+ # Resolves every hop of a field path in order, stopping with a reason at
125
+ # the first one it cannot follow.
126
+ #
127
+ # The whole chain, resolved — every hop's target found, in
100
128
  # order — for the one caller that needs it all at once:
101
- # BluebookBuilder#validate_query_hops!, checking a hop chain
129
+ # `BluebookBuilder#validate_query_hops!`, checking a hop chain
102
130
  # before anything ever dispatches it.
131
+ #
132
+ # @param field [String, Symbol] the query field path, such as `"client/region/name"`
133
+ # @param attributes [Array<Bluebook::Attribute>] the attributes of the shape the path
134
+ # starts from
135
+ # @return [Plan] `hops` walked so far; `tail` the remaining `.`-dotted field (`nil`
136
+ # on refusal); `refusal` `nil` when clean, `:unresolvable` when the last hop's
137
+ # target is not found, `:too_deep` when the chain exceeds `MAX_HOPS`
138
+ # @raise [Bluebook::DSL::Malformed] if a reference on the path has no `declared_in`
139
+ # aggregate to resolve its target through
103
140
  def plan(field, attributes)
104
141
  hops = []
105
142
  remaining = field.to_s
@@ -113,7 +150,7 @@ module Hecks
113
150
  return Plan.new(hops: hops, tail: nil, refusal: :too_deep) if hops.size >= MAX_HOPS
114
151
 
115
152
  # Pushed even unresolved — a caller reporting :unresolvable
116
- # needs THIS hop's own target_name (real, known at
153
+ # needs this hop's own target_name (real, known at
117
154
  # declaration, regardless of whether resolve succeeded), not
118
155
  # whatever hop came before it. `.target` is nil on this one
119
156
  # entry; every caller checking `hops.last.target` already has
@@ -9,6 +9,10 @@ module Hecks
9
9
  class Specification < Common::Options
10
10
  attr_reader :joins
11
11
 
12
+ # @param joins [Array<Hash{Symbol => Object}>] one row per projected aggregate head,
13
+ # keyed `:aggregate` (String target name), `:as` (Symbol output name) and `:many`
14
+ # (Boolean), as `ReadModelBuilder` builds them
15
+ # @param ** [Hash] the shared query options, passed to `Common::Options#initialize`
12
16
  def initialize(joins: [], **)
13
17
  super(**)
14
18
  @joins = joins
@@ -1,8 +1,8 @@
1
1
  require "json"
2
2
 
3
3
  module Hecks
4
- # HOW A VALUE READS INSIDE A REFUSAL, in one place, because a refusal is an
5
- # ANSWER and its wording is contract — pinned byte-for-byte by the corpus.
4
+ # How a value reads inside a refusal, in one place, because a refusal is an
5
+ # answer and its wording is contract — pinned byte-for-byte by the corpus.
6
6
  #
7
7
  # `inspect` and JSON agree on scalars and disagree on everything composite :
8
8
  # a hash is `{"cents"=>100}` against `{"cents":100}`, an array is `["a", "b"]`
@@ -17,6 +17,12 @@ module Hecks
17
17
  module Rendering
18
18
  module_function
19
19
 
20
+ # Renders a value the way it should read inside a refusal message.
21
+ #
22
+ # @param value [Object] the value to render
23
+ # @return [String] `"nil"` for nil, JSON for a Hash/Array or a duck-typed value
24
+ # object (unwrapped to its bare scalar when it has exactly one field), or
25
+ # `value.inspect` for anything else
20
26
  def describe(value)
21
27
  case value
22
28
  when nil then "nil"
@@ -29,7 +35,7 @@ module Hecks
29
35
  # ("#<Hecks::Runtime::Value:0x...>") into an otherwise
30
36
  # correct domain refusal. Duck-typed on `respond_to?(:to_h)`
31
37
  # rather than naming `Runtime::Value` directly — `Runtime::Value`
32
- # itself requires THIS file (`runtime/value.rb`'s own
38
+ # itself requires this file (`runtime/value.rb`'s own
33
39
  # `require_relative "../rendering"`), so naming it here would be
34
40
  # circular. A single-field wrapper (the overwhelming common case
35
41
  # — an amount, an id, a lifecycle field) unwraps to its bare
@@ -12,6 +12,13 @@ module Hecks
12
12
  # to the router, pinned to this specific FQN version rather than
13
13
  # the router's default resolution.
14
14
  class OptionsProxy
15
+ # @param router [Router] router to dispatch resolved calls through
16
+ # @param realm [String, nil] realm segment of the FQN this proxy pins
17
+ # @param domain [String] domain segment of the FQN this proxy pins
18
+ # @param aggregate [String, nil] aggregate segment, or nil for a domain-level query
19
+ # @param options [Hash{Symbol => Object}] the `.options(...)` call's keywords; only
20
+ # `:version` is accepted
21
+ # @raise [ArgumentError] if `options` has a key other than `:version`
15
22
  def initialize(router:, realm:, domain:, aggregate:, options:)
16
23
  unknown = options.keys - [:version]
17
24
  raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
@@ -45,10 +52,16 @@ module Hecks
45
52
  end
46
53
  end
47
54
 
55
+ # @param router [Router] router whose current routes get Ruby constants and methods
48
56
  def initialize(router)
49
57
  @router = router
50
58
  end
51
59
 
60
+ # Installs every current-version route as a namespace constant/method,
61
+ # plus the aggregate `find`/`all`/`count`/`events`/`repository` door and
62
+ # short `Aggregate.verb` shortcuts.
63
+ #
64
+ # @return [self]
52
65
  def install!
53
66
  current_entries.each { |entry| install_namespace_entry(entry) }
54
67
  install_shortcuts!
@@ -77,16 +90,16 @@ module Hecks
77
90
  end
78
91
  end
79
92
 
80
- # `.find`/`.all`/`.count`/`.events`/`.repository` — the SAME read/CRUD
93
+ # `.find`/`.all`/`.count`/`.events`/`.repository` — the same read/CRUD
81
94
  # surface `Facade::Surface::AggregateDoor` gives a plain `Hecks
82
95
  # .boot`, missing here until now: `install_namespace_entry` above
83
- # installs ONE method per declared VERB, so an aggregate with no
96
+ # installs one method per declared verb, so an aggregate with no
84
97
  # commands or queries of its own shape (or simply never asked for
85
98
  # a `find`-shaped query) had no way to look up one record by id
86
99
  # through the router surface at all — real gap, hit live building
87
100
  # a `List` aggregate meant to be read this way. Grouped by
88
101
  # (realm, domain, aggregate) rather than installed per-verb,
89
- # because unlike a command or query this is the SAME five methods
102
+ # because unlike a command or query this is the same five methods
90
103
  # regardless of which verb happened to trigger this aggregate's
91
104
  # own namespace module into existing.
92
105
  def install_aggregate_doors!
data/lib/hecks/router.rb CHANGED
@@ -9,31 +9,60 @@ module Hecks
9
9
 
10
10
  attr_reader :register
11
11
 
12
+ # Loads a project's bluebooks and builds a router over them, without
13
+ # installing any Ruby namespace shortcuts.
14
+ #
15
+ # @param root [String] project root to discover bluebooks under
16
+ # @return [Router] the built router
12
17
  def self.load(root) = new(Bluebook::ProjectLoader.load(root))
13
18
 
14
19
  # Install one project router for ordinary application calls. The explicit
15
20
  # `Router.load` API remains useful for tests and embedded hosts.
21
+ #
22
+ # @param root [String] project root to discover bluebooks under
23
+ # @return [Router] the booted router, also stored as `.default`
16
24
  def self.boot(root)
17
25
  router = load(root)
18
26
  router.install_namespace!
19
27
  @default = router
20
28
  end
21
29
 
30
+ # The process-wide router `.boot` installed.
31
+ #
32
+ # @return [Router] the router `.boot` installed
33
+ # @raise [NotBooted] if `.boot` has not been called yet
22
34
  def self.default
23
35
  @default || raise(NotBooted, "no project router is booted — call Hecks::Router.boot(root) first")
24
36
  end
25
37
 
38
+ # (see #dispatch)
26
39
  def self.dispatch(address, **args) = default.dispatch(address, **args)
40
+ # (see #query)
27
41
  def self.query(address, **args) = default.query(address, **args)
28
42
 
43
+ # @param register [Bluebook::ProjectRegister, Bluebook::ProjectLoader] the FQN
44
+ # catalogue to resolve addresses against
29
45
  def initialize(register)
30
46
  @register = register
31
47
  end
32
48
 
49
+ # Lists every routed FQN entry.
50
+ #
51
+ # @return [Array<Bluebook::ProjectRegister::Entry>] every routed FQN entry
33
52
  def available = register.entries.values
34
53
 
54
+ # Installs Ruby namespace constants and shortcut methods for every
55
+ # current-version route.
56
+ #
57
+ # @return [NamespaceInstaller] the installer that performed the install
35
58
  def install_namespace! = NamespaceInstaller.new(self).install!
36
59
 
60
+ # Resolves an address to its routed FQN entry.
61
+ #
62
+ # @param address [String] a fully-qualified command or query address, realm included
63
+ # @return [Bluebook::ProjectRegister::Entry] the routed entry
64
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
65
+ # @raise [Fqn::Invalid] if `address` is not a well-formed FQN
37
66
  def resolve(address)
38
67
  fqn = Fqn.parse(address)
39
68
  raise UnknownAddress, "router addresses require a realm: #{address.inspect}" unless fqn.realm
@@ -43,13 +72,39 @@ module Hecks
43
72
  raise UnknownAddress, "no Bluebook route for #{address.inspect}"
44
73
  end
45
74
 
75
+ # Dispatches a command to its resolved aggregate.
76
+ #
77
+ # @param address [String] a fully-qualified command address, realm included
78
+ # @param args [Hash] command facts, plus the dispatcher's optional `:to`, `:with`,
79
+ # and `:saga_correlation` keys
80
+ # @return [Runtime::Dispatcher::Result] the dispatch result
81
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
82
+ # @raise [WrongVerbKind] if `address` names a query
83
+ # @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
84
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
85
+ # the call
86
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
87
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
46
88
  def dispatch(address, **args)
47
89
  entry = resolve(address)
48
90
  raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
49
91
 
50
- entry.dispatcher.dispatch(local_verb(entry), **args)
92
+ entry.dispatcher.dispatch_flat(local_verb(entry), args)
51
93
  end
52
94
 
95
+ # Queries a resolved aggregate, entity, or read model.
96
+ #
97
+ # @param address [String] a fully-qualified query address, realm included
98
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
99
+ # @return [Array<Hash>] one row Hash per match; see `Runtime::Dispatcher#query`
100
+ # for the exact shape per address kind
101
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
102
+ # @raise [WrongVerbKind] if `address` names a command
103
+ # @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
104
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
105
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
106
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
107
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
53
108
  def query(address, **args)
54
109
  entry = resolve(address)
55
110
  raise WrongVerbKind, "#{address.inspect} names a command; use #dispatch" unless entry.query?
@@ -1,40 +1,49 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # A PROCESS-WIDE, STRIPED MUTEX REGISTRY — the concurrency-control
4
- # mechanism for every adapter that does NOT declare
3
+ # **A process-wide, striped mutex registry** — the concurrency-control
4
+ # mechanism for every adapter that does not declare
5
5
  # `:optimistic_concurrency` (Heki, Memory today; see
6
6
  # `CommandInterpreter#call`/`EntityInterpreter#call`, which choose
7
7
  # between this and Postgres's CAS+retry purely off
8
8
  # `repository.capabilities`).
9
9
  #
10
- # WHY A LOCK SUFFICES HERE AND CAS IS NOT NEEDED: both adapters hold
10
+ # Why a lock suffices here and CAS is not needed: both adapters hold
11
11
  # process-local data. `Adapters::Memory.tenant_capable?`'s own comment
12
12
  # states the confirmed fact this relies on — two `Runtime.boot` calls
13
- # get two entirely separate adapter instances; there is never a SECOND
14
- # PROCESS writing the same Heki file or the same Memory Hash, only
15
- # possibly other THREADS within this one process. A `Mutex` held for
13
+ # get two entirely separate adapter instances; there is never a second
14
+ # process writing the same Heki file or the same Memory Hash, only
15
+ # possibly other threads within this one process. A `Mutex` held for
16
16
  # the full hydrate-through-save critical section closes the identical
17
17
  # lost-update gap CAS closes for Postgres, with no schema, no version
18
18
  # column, and no retry loop — the second thread simply doesn't start
19
19
  # its own hydrate until the first thread's save has landed.
20
20
  #
21
- # STRIPED, NOT ONE GLOBAL LOCK: keyed by `[domain, aggregate.hecks_name,
22
- # id]`, so two dispatches against two DIFFERENT records never block
21
+ # **Striped, not one global lock**: keyed by `[domain, aggregate.hecks_name,
22
+ # id]`, so two dispatches against two different records never block
23
23
  # each other. The registry Hash itself is guarded by its own top-level
24
24
  # Mutex only for the moment a new per-key Mutex is created — two
25
- # threads locking DIFFERENT keys for the first time never wait on one
25
+ # threads locking different keys for the first time never wait on one
26
26
  # another beyond that brief creation window.
27
27
  module AggregateLock
28
28
  @registry_lock = Mutex.new
29
29
  @locks = {}
30
30
 
31
31
  class << self
32
+ # Returns the Mutex striped to one record, creating it on first use.
33
+ #
32
34
  # `AggregateLock.for(domain, aggregate, id).synchronize { ... }`
33
35
  # `id: nil` — identity could not be resolved yet (see
34
- # `Identity.best_effort`) — locks by aggregate TYPE alone, coarser
36
+ # `Identity.best_effort`) — locks by aggregate type alone, coarser
35
37
  # (every record of this aggregate serializes against every other)
36
- # but still correct: it can only ever make dispatch MORE
38
+ # but still correct: it can only ever make dispatch more
37
39
  # conservative than a resolved id would.
40
+ #
41
+ # @param domain [String] the domain the aggregate belongs to
42
+ # @param aggregate [Bluebook::Aggregate] the aggregate being dispatched
43
+ # @param id [String, nil] the resolved record id, or nil to stripe by aggregate type
44
+ # alone
45
+ # @return [Mutex] the mutex for this `[domain, aggregate, id]` key, held for the full
46
+ # hydrate-through-save critical section
38
47
  def for(domain, aggregate, id = nil)
39
48
  key = id.nil? ? [domain.to_s, aggregate.hecks_name] : [domain.to_s, aggregate.hecks_name, id.to_s]
40
49
  @registry_lock.synchronize { @locks[key] ||= Mutex.new }
@@ -1,8 +1,8 @@
1
1
  module Hecks
2
2
  module Runtime
3
3
  # A boot's own small set of phase-tagged, conditionally-present gates —
4
- # ADR 0031. Registration is instance-scoped, ONE PER `Loader.boot`
5
- # CALL, never a module-level singleton: a process that boots more than
4
+ # ADR 0031. Registration is instance-scoped, one per `Loader.boot`
5
+ # call, never a module-level singleton: a process that boots more than
6
6
  # one registry in its lifetime (every spec suite does) must never let
7
7
  # one boot's capability profile leak into the next boot's gate list.
8
8
  #
@@ -13,26 +13,44 @@ module Hecks
13
13
  # order — today exactly one gate per phase, so ordering among
14
14
  # same-phase gates has never been exercised.
15
15
  #
16
- # This is deliberately NOT the same registry `Hecks::Projector` uses
16
+ # This is deliberately not the same registry `Hecks::Projector` uses
17
17
  # (ADR 0027) — that one is a process-wide, static IR-in/artifact-out
18
18
  # registry with no bindings and no live-state mutation; this one is
19
19
  # per-boot and gates real I/O (a Postgres mint, a saga-store read).
20
20
  # Sharing a primitive between them is deferred until a third consumer
21
21
  # actually wants it (0031's own Rejected Alternatives).
22
22
  class BootGates
23
+ # @return [void]
23
24
  def initialize
24
25
  @gates = Hash.new { |h, k| h[k] = [] }
25
26
  end
26
27
 
28
+ # Adds a gate to `phase`, run in registration order alongside any other gate there.
29
+ #
30
+ # @param name [Symbol] the gate's name, checked by `registered?`
31
+ # @param gate [Proc, Method] the gate; called as `gate.call(registry, directory)`
32
+ # @param phase [Symbol] the phase this gate runs under, such as `:pre_verify` or
33
+ # `:post_verify`
34
+ # @return [Hecks::Runtime::BootGates] self, for chaining
27
35
  def register(name, gate, phase:)
28
36
  @gates[phase] << [name, gate]
29
37
  self
30
38
  end
31
39
 
40
+ # Reports whether a gate named `name` has been registered, under any phase.
41
+ #
42
+ # @param name [Symbol] the gate name to look for, across every phase
43
+ # @return [Boolean] true if a gate named `name` was registered under any phase
32
44
  def registered?(name)
33
45
  @gates.values.flatten(1).any? { |registered_name, _gate| registered_name == name }
34
46
  end
35
47
 
48
+ # Runs every gate registered under `phase`, in registration order.
49
+ #
50
+ # @param phase [Symbol] the phase to run, such as `:pre_verify` or `:post_verify`
51
+ # @param registry [Runtime::Registry] the booted registry, passed to each gate
52
+ # @param directory [String] the boot directory, passed to each gate
53
+ # @return [void]
36
54
  def run!(phase, registry, directory)
37
55
  @gates[phase].each { |pair| pair.last.call(registry, directory) }
38
56
  end