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
@@ -16,17 +16,33 @@ module Hecks
16
16
  # model is simple enough for one; otherwise runs the whole join
17
17
  # in-process against loaded records.
18
18
  class ReadModelInterpreter
19
+ # @param registry [Runtime::Registry] the booted registry whose repositories
20
+ # this interpreter reads
19
21
  def initialize(registry) = @registry = registry
20
22
 
23
+ # Runs one declared read model and returns its projected rows.
24
+ #
25
+ # @param domain [String, Symbol] the domain the read model is declared in
26
+ # @param model [Bluebook::ReadModel] the read model to run
27
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
28
+ # @return [Array<Hash>] a one-element Array holding a Hash of head name to
29
+ # projected rows (or a single row, for a non-`:many` head)
30
+ # @raise [Runtime::TypeMismatch] if the reference argument is offered as a whole
31
+ # object rather than a plain identity, or a `median` field is not numeric
32
+ # @raise [Runtime::NotFound] if the reference argument names no record
33
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
34
+ # @raise [ArgumentError] if `group_by` or `median` names a field its target
35
+ # aggregate does not declare
36
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
21
37
  def call(domain, model, args)
22
38
  project(domain, model, args)
23
39
  end
24
40
 
25
41
  private
26
42
 
27
- # ROOT-FIRST, THEN THE SQLITE ESCAPE HATCH, THEN THE JOIN LOOP —
28
- # each step's own comment names a real, previously-shipped bug the
29
- # current ORDER fixes (the reference/TenantScope refusal ordering
43
+ # Root-first, then the SQLite escape hatch, then the join loop —
44
+ # each step's own comment names a real, already-shipped bug the
45
+ # current order fixes (the reference/TenantScope refusal ordering
30
46
  # above, the root-first head processing below). Splitting this
31
47
  # into smaller methods would scatter that ordering across method
32
48
  # boundaries where a future editor could silently break it, and
@@ -40,7 +56,7 @@ module Hecks
40
56
  def project(domain, model, args)
41
57
  bluebook = @registry.bluebook(domain)
42
58
  rootless = model.reference_target.nil?
43
- # BEFORE the adapter early-return below, so the SQLite path inherits it.
59
+ # Before the adapter early-return below, so the SQLite path inherits it.
44
60
  # Without this a stale caller passing a wrapped reference gets a
45
61
  # path-dependent answer — an adapter could quietly open the wrapped
46
62
  # reference while the in-process path reads it whole and finds nothing.
@@ -49,16 +65,16 @@ module Hecks
49
65
  # wrong at all.
50
66
  refuse_object_reference(model, args) unless rootless
51
67
  reference_id = reference(args.fetch(model.reference_name)) unless rootless
52
- # Computed off the ORIGINAL model, before TenantScope wraps it — the
68
+ # Computed off the original model, before TenantScope wraps it — the
53
69
  # "which head(s) do options apply to" question is about what the
54
70
  # bluebook author declared, not about the synthetic tenant clause
55
71
  # the wrapper adds underneath. Plural (ADR 0055) — `on:` lets more
56
72
  # than one many-side head be eligible at once.
57
73
  eligible = model.filtered_head_names
58
74
  model = TenantScope.apply(model, args)
59
- # A ROOTLESS, `group_by`-declared, or `count`/`median`-declared
75
+ # A rootless, `group_by`-declared, or `count`/`median`-declared
60
76
  # model skips the SQLite native escape hatch entirely (there is
61
- # no root aggregate to look up a repository FOR when rootless,
77
+ # no root aggregate to look up a repository for when rootless,
62
78
  # and `query_read_model` knows nothing about grouping or
63
79
  # reducing) — always runs the in-process loop below instead.
64
80
  # Correct everywhere ; not SQL-pushed-down for a SQLite-backed
@@ -73,17 +89,17 @@ module Hecks
73
89
  end
74
90
  end
75
91
 
76
- # ROOT FIRST, ALWAYS — regardless of `include` order in the
92
+ # **Root first, always** — regardless of `include` order in the
77
93
  # bluebook. `read_model_builder.rb`'s own `include` is
78
94
  # documented "Order-independent" (the `:many` flag is resolved
79
95
  # at build time, once `@reference_target` is known), but that
80
- # promise was never kept HERE: this loop used to run heads in
81
- # their literal declared order and match each "many" head
82
- # against whatever was ALREADY in `projected` — empty, the
83
- # very first time through, if a many-side head happened to be
84
- # declared before the root. A real, live bug (not a guess):
96
+ # promise is not kept without this: running heads in
97
+ # their literal declared order and matching each "many" head
98
+ # against whatever was already in `projected` would leave it
99
+ # empty, the very first time through, if a many-side head happened
100
+ # to be declared before the root. A real, live bug (not a guess):
85
101
  # `include Promotion` before `include Item` on a read model
86
- # whose root IS Item silently returned an empty array for
102
+ # whose root is Item silently returned an empty array for
87
103
  # Promotion — no error, just a wrong, too-small answer — while
88
104
  # the reverse order worked purely by accident. `partition`,
89
105
  # not `sort_by`: Ruby's `sort_by` is not guaranteed stable,
@@ -97,14 +113,14 @@ module Hecks
97
113
  [fetch(bluebook, domain, head[:aggregate], reference_id)]
98
114
  elsif rootless
99
115
  # No root to FK-match against — a rootless model reads
100
- # each of its own heads WHOLE, independently. Multiple
101
- # heads on one rootless model are NEVER cross-joined
116
+ # each of its own heads whole, independently. Multiple
117
+ # heads on one rootless model are never cross-joined
102
118
  # against each other, and there is no DSL to declare
103
119
  # one if you wanted to — `ReadModelBuilder#include_impl`
104
120
  # takes only `type`/`as:` (checked directly, not
105
121
  # assumed), and `group_by` groups this bulk read's own
106
122
  # output, it names no predicate between two heads.
107
- # Building a cross-join here would mean CHOOSING a join
123
+ # Building a cross-join here would mean choosing a join
108
124
  # semantics (equality on which fields?) nobody has
109
125
  # declared — a real, deliberate scope limit pending a
110
126
  # future `include ..., joins: ...`-shaped grammar
@@ -124,7 +140,7 @@ module Hecks
124
140
  projected << { aggregate: head[:aggregate], rows: rows }
125
141
  rows_by_as[head[:as]] = head[:many] ? rows : rows.first
126
142
  end
127
- # Declared order preserved in the OUTPUT — only the
143
+ # Declared order preserved in the output — only the
128
144
  # computation above needed reordering, not what a caller sees
129
145
  # back.
130
146
  heads = model.aggregate_heads.to_h { |head| [head[:as], rows_by_as[head[:as]]] }
@@ -145,9 +161,9 @@ module Hecks
145
161
  end]
146
162
  end
147
163
 
148
- # THE ROOT-FIRST FIX'S OWN FIX — root-first alone only reaches one
164
+ # **The root-first fix's own fix** — root-first alone only reaches one
149
165
  # level: it guarantees the root is in `projected` before any other
150
- # head is matched, but a CHAIN of non-root heads (a head that
166
+ # head is matched, but a chain of non-root heads (a head that
151
167
  # references another non-root head, not the root) is still
152
168
  # matched against whatever declaration order happened to put in
153
169
  # `projected` so far. `include Coupon` before `include Promotion`
@@ -160,9 +176,9 @@ module Hecks
160
176
  # to declare `include` in dependency order (the same promise
161
177
  # `read_model_builder.rb` already makes and this file is the one
162
178
  # place obligated to keep), but by topologically sorting the
163
- # non-root heads on their OWN declared reference fields before
179
+ # non-root heads on their own declared reference fields before
164
180
  # this method's runtime matching ever runs — Kahn's algorithm,
165
- # picking ready heads in DECLARED order at each step so declaring
181
+ # picking ready heads in declared order at each step so declaring
166
182
  # order still governs whenever there is no dependency to break a
167
183
  # tie. This generalizes root-first (a chain of length 1) to a
168
184
  # chain of any depth, and to a head depending on more than one
@@ -192,13 +208,13 @@ module Hecks
192
208
  ordered
193
209
  end
194
210
 
195
- # Which OTHER declared (non-root) heads a head's own aggregate
211
+ # Which other declared (non-root) heads a head's own aggregate
196
212
  # holds a reference field toward — the same relationship this
197
213
  # file's runtime matching checks record-by-record, asked here
198
214
  # statically, once, to order heads before any record is read.
199
215
  #
200
216
  # `head[:aggregate]` names whatever `include` was given — and
201
- # `include` accepts a nested ENTITY (Member, nested under
217
+ # `include` accepts a nested entity (Member, nested under
202
218
  # ValueObject ; Handler and Dispatch, nested under ProcessManager
203
219
  # — bluebook.bluebook's own `WholeBluebook` read model includes
204
220
  # all three) just as readily as a top-level aggregate.
@@ -220,7 +236,7 @@ module Hecks
220
236
  .map { |other| other[:aggregate] }
221
237
  end
222
238
 
223
- # `group_by`'s own declared fields, checked against the ONE
239
+ # `group_by`'s own declared fields, checked against the one
224
240
  # many-side head they apply to (`seal_group_by` already refuses
225
241
  # zero or several) — resolved here, once, rather than re-derived
226
242
  # per row. Raises loudly on a typo'd field name rather than
@@ -232,15 +248,15 @@ module Hecks
232
248
  aggregate = bluebook.aggregate(target[:aggregate])
233
249
  model.group_by_fields.each do |field|
234
250
  next if aggregate.attribute(field)
235
- # THE LIFECYCLE FIELD IS A FIELD, and refusing it here was a drift
251
+ # The lifecycle field is a field, and refusing it here was a drift
236
252
  # between two halves of the same language: `where(status: "logged")`
237
253
  # has always been legal on the same aggregate, because a lifecycle
238
254
  # state is stored on the record like anything else — it is simply
239
255
  # declared by `lifecycle :status` rather than by `attribute`.
240
256
  #
241
257
  # It is also the grouping anybody actually wants. "How are we doing"
242
- # over a bug ledger IS the count per status, and a report that could
243
- # group by every field EXCEPT that one could not answer the question
258
+ # over a bug ledger is the count per status, and a report that could
259
+ # group by every field except that one could not answer the question
244
260
  # reports exist for.
245
261
  next if aggregate.lifecycle && aggregate.lifecycle.field.to_sym == field.to_sym
246
262
 
@@ -253,7 +269,7 @@ module Hecks
253
269
 
254
270
  # One level of nesting per field, in `group_by`'s own declared
255
271
  # order — the leaf is the row with every grouped field removed
256
- # (already spent, as the keys that reached it). ASSUMES the full
272
+ # (already spent, as the keys that reached it). Assumes the full
257
273
  # `group_by` path uniquely identifies one row (true for grouping by
258
274
  # an aggregate's own full identity, ConsoleSettings' own real use)
259
275
  # — `leaves.first` silently keeps only the first row when several
@@ -263,9 +279,9 @@ module Hecks
263
279
  def nest(rows, fields)
264
280
  field, *rest = fields
265
281
  rows.group_by { |row| row[field] }.transform_values do |group|
266
- # Strip ONLY the field just grouped by, not the whole remaining
282
+ # Strip only the field just grouped by, not the whole remaining
267
283
  # list — `rest`'s own fields have to survive into the recursive
268
- # call below, or the NEXT level groups by a key that's already
284
+ # call below, or the next level groups by a key that's already
269
285
  # gone (found by trying it: a two-field group_by's own second
270
286
  # level came back keyed `nil` for every group, every time).
271
287
  stripped = group.map { |row| row.reject { |key, _| key == field } }
@@ -273,7 +289,7 @@ module Hecks
273
289
  end
274
290
  end
275
291
 
276
- # `count`/`median`'s own declared target — the SAME single
292
+ # `count`/`median`'s own declared target — the same single
277
293
  # many-side head `group_by_target` resolves, for the same reason
278
294
  # (`seal_aggregation` already refuses zero or several many-side
279
295
  # heads, and refuses count/median declared alongside group_by, so
@@ -303,12 +319,12 @@ module Hecks
303
319
  target
304
320
  end
305
321
 
306
- # THE STANDARD DEFINITION. An ODD count's median is its one true
307
- # middle value, sorted ; an EVEN count's median is the AVERAGE of
322
+ # **The standard definition**. An odd count's median is its one true
323
+ # middle value, sorted ; an even count's median is the average of
308
324
  # its two middle values — the common convention (as opposed to,
309
325
  # say, always taking the lower of the two), and the one this
310
326
  # session's own task named explicitly as the deliberate choice.
311
- # An EMPTY collection has no median: nil, not zero, so a caller
327
+ # An empty collection has no median: nil, not zero, so a caller
312
328
  # cannot mistake "nothing to average" for "the values averaged to
313
329
  # zero". Reuses `Ports::Query::InMemory`'s own field reading
314
330
  # (`FieldPath.dig` + `comparable`) — the same unwrap `where`/
@@ -326,8 +342,8 @@ module Hecks
326
342
 
327
343
  def fetch(bluebook, domain, aggregate_name, id)
328
344
  @registry.read_repository(domain, bluebook.aggregate(aggregate_name)).find(id) ||
329
- raise(NotFound, RefusalWording.render("NotFound", "read_model_reference_missing",
330
- aggregate: aggregate_name, offered: Rendering.describe(id)))
345
+ raise(NotFound, RefusalWording.render_site("NotFound", "read_model_reference_missing",
346
+ aggregate: aggregate_name, offered: Rendering.describe(id)))
331
347
  end
332
348
 
333
349
  def records(bluebook, domain, aggregate_name)
@@ -335,27 +351,27 @@ module Hecks
335
351
  aggregate ? @registry.read_repository(domain, aggregate).all : []
336
352
  end
337
353
 
338
- # A stored reference holds the target's id inside the REFERENCE
339
- # ATTRIBUTE's own declared shape, so reading it is reading that shape —
354
+ # A stored reference holds the target's id inside the reference
355
+ # attribute's own declared shape, so reading it is reading that shape —
340
356
  # a different thing from the identity unwrap that was removed. An
341
- # IDENTITY is declared as a path and followed (Runtime::Identity) ; a
357
+ # identity is declared as a path and followed (Runtime::Identity) ; a
342
358
  # reference has no path of its own, and `Value.scalar` refuses a
343
359
  # composite rather than guessing which field was meant.
344
360
  #
345
361
  # Storing the scalar itself would remove this reading altogether. That
346
- # is a change to how references are STORED, not to how identities are
362
+ # is a change to how references are stored, not to how identities are
347
363
  # declared, so it is not made here.
348
364
  # An ask names itself where a command would name itself, and says the
349
365
  # same thing about the same shape. `Value.refuse_object_reference` is
350
- # not reused because it speaks of a COMMAND and its attribute ; a read
366
+ # not reused because it speaks of a command and its attribute ; a read
351
367
  # model has a query name and one declared reference.
352
368
  def refuse_object_reference(model, args)
353
369
  offered = args.fetch(model.reference_name, nil)
354
370
  return unless offered.is_a?(Hash) || offered.is_a?(Value)
355
371
 
356
372
  raise TypeMismatch,
357
- RefusalWording.render("TypeMismatch", "read_model_object_reference",
358
- query: model.query_name, field: model.reference_name)
373
+ RefusalWording.render_site("TypeMismatch", "read_model_object_reference",
374
+ query: model.query_name, field: model.reference_name)
359
375
  end
360
376
 
361
377
  # A reference is the id, in the argument and in the stored row alike.
@@ -1,15 +1,15 @@
1
1
  module Hecks
2
2
  module Runtime
3
- # THE OUT-OF-BAND HALF OF `projects` (S12, ADR 0025 — "Consistency
3
+ # The out-of-band half of `projects` (S12, ADR 0025 — "Consistency
4
4
  # across aggregate boundaries"). A projected field is never written
5
5
  # by the command that reads it — nothing at dispatch time takes a
6
6
  # live cross-aggregate read the way `CommandRules::References
7
7
  # #dereference` still does — so this is the one place a projected
8
8
  # field's value actually gets copied over: walk every record of the
9
- # OWNING aggregate, resolve each of its own `projected_fields`
9
+ # owning aggregate, resolve each of its own `projected_fields`
10
10
  # through the reference it names, and `save` the local copy.
11
11
  #
12
- # EXPLICIT AND CALLABLE, NOT AUTOMATIC — no on-boot detection of a
12
+ # **Explicit and callable, not automatic** — no on-boot detection of a
13
13
  # freshly-declared `projects` with no held-era precedent, no
14
14
  # generated `Policy#for_each` reaction keeping it live in real
15
15
  # time as the target changes. Both are real extensions this same
@@ -19,7 +19,7 @@ module Hecks
19
19
  # needs — proven to work end to end before either automatic
20
20
  # trigger is built on top of it.
21
21
  #
22
- # NEEDS NO NEW ADAPTER CAPABILITY. `find`/`all`/`save` are the same
22
+ # **Needs no new adapter capability**. `find`/`all`/`save` are the same
23
23
  # three primitives every real adapter already answers identically
24
24
  # (`Ports::Persistence::AppendOnly#save` — append, then project,
25
25
  # the same for Memory/Postgres/SQLite/D1/Heki) — confirmed by
@@ -32,6 +32,12 @@ module Hecks
32
32
  # `save` only runs when a projected value would differ from what
33
33
  # is already stored, so re-running a sweep with nothing having
34
34
  # moved on the target side touches the append log not at all.
35
+ #
36
+ # @param registry [Runtime::Registry] the booted registry to read repositories from
37
+ # @param domain [String] the domain the aggregate belongs to
38
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose `projected_fields` to
39
+ # refresh
40
+ # @return [Integer] the number of records actually changed and saved
35
41
  def call(registry, domain, aggregate)
36
42
  return 0 if aggregate.projected_fields.empty?
37
43
 
@@ -39,6 +45,16 @@ module Hecks
39
45
  repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
40
46
  end
41
47
 
48
+ # Refreshes one record's own projected fields in place, saving it if any changed.
49
+ #
50
+ # @param registry [Runtime::Registry] the booted registry to read the target's
51
+ # repository from
52
+ # @param domain [String] the domain the aggregate belongs to
53
+ # @param aggregate [Bluebook::Aggregate] the aggregate `record` is an instance of
54
+ # @param record [Runtime::Instance] the record to refresh, mutated in place
55
+ # @param repository [Ports::Persistence::AppendOnly] the repository to save `record`
56
+ # through when it changes
57
+ # @return [Boolean] true if any projected field's value changed and `record` was saved
42
58
  def refresh(registry, domain, aggregate, record, repository)
43
59
  changed = false
44
60
 
@@ -55,10 +71,22 @@ module Hecks
55
71
  changed
56
72
  end
57
73
 
74
+ # Reads the current value of one projected field's remote target field.
75
+ #
58
76
  # `nil` when the reference itself does not resolve in this
59
77
  # chapter (a cross-domain target left "unfollowed" the same way
60
78
  # References#dereference already leaves one) or when the record
61
79
  # names no target at all — an optional reference nobody set.
80
+ #
81
+ # @param registry [Runtime::Registry] the booted registry to read the target's
82
+ # repository from
83
+ # @param domain [String] the domain the aggregate belongs to
84
+ # @param aggregate [Bluebook::Aggregate] the aggregate declaring `field`
85
+ # @param record [Runtime::Instance] the record holding the reference to follow
86
+ # @param field [Bluebook::ProjectedField] the projected field to resolve
87
+ # @return [Object, nil] the target record's own `field.remote_field` value; nil if the
88
+ # reference type does not resolve, the record names no target, or the target record
89
+ # cannot be found
62
90
  def remote_value(registry, domain, aggregate, record, field)
63
91
  target = aggregate.attribute(field.reference)&.type&.resolve
64
92
  return nil unless target
@@ -13,12 +13,12 @@ module Hecks
13
13
  # answers a hop for free, with no per-engine code and no way for
14
14
  # one engine to forget it.
15
15
  #
16
- # A hop's own filtering never happens here — RESOLVING one hop
16
+ # A hop's own filtering never happens here — resolving one hop
17
17
  # means running one ordinary, adapter-agnostic query against the
18
- # hop's TARGET aggregate (through the same Ports::Query boundary
18
+ # hop's target aggregate (through the same Ports::Query boundary
19
19
  # any other query goes through), and folding the ids it answers
20
20
  # back in as a local membership check. A multi-hop chain resolves
21
- # from the FAR END inward: `fold` only ever peels off the head hop
21
+ # from the far end inward: `fold` only ever peels off the head hop
22
22
  # (QuerySpecification::HopPath.next_hop, the one-step primitive),
23
23
  # and hands everything still left in the tail to a recursive
24
24
  # `apply` call — so hop 2, hop 3, and so on each get resolved by
@@ -31,6 +31,21 @@ module Hecks
31
31
  module ReferenceHop
32
32
  module_function
33
33
 
34
+ # Folds every hop clause in `declared.wheres` into a synthetic local `in` clause.
35
+ #
36
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
37
+ # QuerySpecification::Common::Options] the declared query specification to fold hop
38
+ # clauses of
39
+ # @param args [Hash] the query's arguments, read when resolving each hop's own query
40
+ # @param registry [Runtime::Registry] the booted registry to resolve each hop's target
41
+ # repository from
42
+ # @param domain [String] the domain `aggregate` belongs to
43
+ # @param aggregate [Bluebook::Aggregate] the aggregate `declared` queries
44
+ # @return [Bluebook::Query, Runtime::TenantScope::Scoped, QuerySpecification::Common::
45
+ # Options, Hecks::Runtime::ReferenceHop::Folded] `declared` unchanged when it has no
46
+ # hop clauses; otherwise a `Folded` wrapper whose `#wheres` replaces each hop clause
47
+ # with its folded `in` clause
48
+ # @raise [Runtime::WiringError] if a hop's target no longer resolves (see `fold`)
34
49
  def apply(declared, args, registry:, domain:, aggregate:)
35
50
  hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
36
51
  return declared if hopped.empty?
@@ -39,6 +54,18 @@ module Hecks
39
54
  Folded.new(declared, local + folded)
40
55
  end
41
56
 
57
+ # Folds one hop clause into a synthetic `in` clause over the hop attribute's own ids.
58
+ #
59
+ # @param clause [QuerySpecification::Common::WhereClause] the hop clause to fold; its
60
+ # `field` names the hop path, dotted past the first segment
61
+ # @param args [Hash] the query's arguments, read when resolving the inner query
62
+ # @param registry [Runtime::Registry] the booted registry to resolve the hop's target
63
+ # repository from
64
+ # @param domain [String] the domain `aggregate` belongs to
65
+ # @param aggregate [Bluebook::Aggregate] the aggregate `clause` is declared against
66
+ # @return [QuerySpecification::Common::WhereClause] a synthetic `in` clause on the hop
67
+ # attribute's name, whose value is every id the inner clause admits on the target
68
+ # @raise [Runtime::WiringError] if the hop's target aggregate no longer resolves
42
69
  def fold(clause, args, registry:, domain:, aggregate:)
43
70
  step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
44
71
  hop, rest = step
@@ -63,13 +90,23 @@ module Hecks
63
90
  QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
64
91
  end
65
92
 
66
- # Every id the inner clause admits on the hop's TARGET — one
93
+ # Every id the inner clause admits on the hop's target — one
67
94
  # whole, ordinary query against the target's own repository,
68
95
  # through the very same Ports::Query boundary the outer ask
69
96
  # uses, so a hop is answered by whatever engine the target
70
97
  # aggregate is actually bound to (which may not be the engine
71
- # the OUTER aggregate is bound to at all) rather than by a
98
+ # the outer aggregate is bound to at all) rather than by a
72
99
  # second reading of the comparators.
100
+ #
101
+ # @param domain [String] the domain `target` belongs to
102
+ # @param target [Bluebook::Aggregate] the hop's target aggregate to query
103
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the inner clause(s) to
104
+ # run against `target`
105
+ # @param args [Hash] the outer query's arguments, read when resolving the inner query
106
+ # @param registry [Runtime::Registry] the booted registry to resolve `target`'s
107
+ # repository from
108
+ # @return [Array<String>] every distinct id the inner clause(s) admit on `target`
109
+ # @raise [Runtime::WiringError] if a hop nested inside `wheres` no longer resolves
73
110
  def matching_ids(domain, target, wheres, args, registry:)
74
111
  spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
75
112
  registry: registry, domain: domain, aggregate: target)
@@ -81,12 +118,17 @@ module Hecks
81
118
  end
82
119
 
83
120
  # Never returned to a caller that might call an IR-level method
84
- # (`to_h`, …) whose OWN internal `wheres` read would resolve
121
+ # (`to_h`, …) whose own internal `wheres` read would resolve
85
122
  # against the original object, not this override — the exact
86
123
  # caution TenantScope::Scoped's own comment gives, for the exact
87
124
  # same reason: SimpleDelegator only intercepts calls made
88
125
  # directly on the wrapper.
89
126
  class Folded < SimpleDelegator
127
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
128
+ # QuerySpecification::Common::Options] the wrapped query specification, delegated to
129
+ # for everything but `#wheres`
130
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the replacement
131
+ # where-clauses, hop clauses folded to synthetic `in` clauses
90
132
  def initialize(declared, wheres)
91
133
  super(declared)
92
134
  @wheres = wheres