hecks 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -2
@@ -17,15 +17,27 @@ module Hecks
17
17
  # ReadModelBuilder so the two specification kinds parse the same
18
18
  # clauses identically rather than each carrying its own copy.
19
19
  module DSL
20
- # RENAMED FROM `where`/`order_by`/`limit`/`offset`/`authorize`
21
- # (all below) item #13's full metaprogrammed dispatch (slice
22
- # 4c). A SHARED mixin, same shape `attribute_impl` proved in
23
- # slice 3: ONE renamed method each, both Query and ReadModel
24
- # Keyword rows name it in `calls:`. `where`/`order_by` are
25
- # bootstrap-reachable (every core chapter's own `read_model`
26
- # filters its roster with them), so both are in
27
- # BOOTSTRAP_CALLS_FALLBACK for the ReadModel context; `limit`/
28
- # `offset`/`authorize` are not (checked directly).
20
+ # Records one `WhereClause` per `field => value` pair — what the
21
+ # `where` DSL word forwards to.
22
+ #
23
+ # `where`/`order_by`/`limit`/`offset`/`authorize` (all below) carry
24
+ # an `_impl` name because the words themselves are dispatched by
25
+ # the grammar table rather than defined as methods: both the Query
26
+ # and the ReadModel Keyword rows name the `_impl` method in
27
+ # `calls:`, the same shape `attribute_impl` has, and a shared mixin
28
+ # means one method each. `where`/`order_by` are bootstrap-reachable
29
+ # (every core chapter's own `read_model` filters its roster with
30
+ # them), so they must resolve through
31
+ # `GenericDispatch::BOOTSTRAP_CALLS_FALLBACK` while the grammar
32
+ # table is still being built.
33
+ #
34
+ # @param clauses [Hash{Symbol => Object}] field name (a dotted or slashed path is
35
+ # one Symbol) to either a literal, a Symbol naming a query argument, or a
36
+ # one-pair Hash `{ comparator => operand }` such as `{ gte: :minimum }`; a bare
37
+ # value means `eq`
38
+ # @return [void]
39
+ # @raise [ArgumentError] if a Hash value does not have exactly one pair, or names a
40
+ # comparator outside `COMPARATORS`
29
41
  def where_impl(clauses)
30
42
  @wheres ||= []
31
43
  clauses.each do |field, value|
@@ -34,15 +46,59 @@ module Hecks
34
46
  end
35
47
  end
36
48
 
49
+ # Records the query's single ordering, replacing any declared earlier.
50
+ #
51
+ # @param field [Symbol, String] the field to order by; a dotted path such as
52
+ # `:"order.value"` reaches a value object's member
53
+ # @param direction [Symbol, String] `:asc` or `:desc`
54
+ # @return [OrderBy] the ordering just recorded
37
55
  def order_by_impl(field, direction = :asc)
38
56
  @order_by = OrderBy.new(field: field, direction: direction)
39
57
  end
40
58
 
59
+ # Records the most rows the query returns.
60
+ #
61
+ # @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
62
+ # argument that supplies it
63
+ # @return [LimitSpec] the limit just recorded
41
64
  def limit_impl(value) = @limit = LimitSpec.new(value: value)
65
+
66
+ # Records how many matched rows the query skips before the limit applies.
67
+ #
68
+ # @param value [Integer, Symbol] a literal row count, or a Symbol naming the query
69
+ # argument that supplies it
70
+ # @return [OffsetSpec] the offset just recorded
42
71
  def offset_impl(value) = @offset = OffsetSpec.new(value: value)
72
+
73
+ # Records a cursor declaration. The word parses and round-trips, but both builders
74
+ # refuse it at `build` (`seal_cursor`) because no interpreter applies it.
75
+ #
76
+ # @param value [Symbol, Object] the query argument carrying the cursor, such as
77
+ # `:after`, or a literal
78
+ # @return [CursorSpec] the cursor just recorded
43
79
  def cursor(value) = @cursor = CursorSpec.new(value: value)
80
+
81
+ # Records the authorization the query declares, and the argument that scopes it
82
+ # to one tenant.
83
+ #
84
+ # @param policy [Symbol, String] name of the access policy, such as `:vault_access`
85
+ # @param tenant [Symbol, String, nil] the field every ask must supply a value for
86
+ # and is filtered by (`Runtime::TenantScope`); `nil` declares no tenant scoping
87
+ # @return [AuthorizationSpec] the authorization just recorded
44
88
  def authorize_impl(policy, tenant: nil) = @authorization = AuthorizationSpec.new(policy: policy, tenant: tenant)
89
+
90
+ # Records where a null sorts, overriding the `native` default.
91
+ #
92
+ # @param mode [Symbol, String] `:first` or `:last`; anything else orders as
93
+ # `:native` does (see `NullPolicy.order`)
94
+ # @return [NullSemantics] the null policy just recorded
45
95
  def nulls(mode) = @null_semantics = NullSemantics.new(mode: mode)
96
+
97
+ # Records that the query asks its adapter to expose the query it generates.
98
+ #
99
+ # @param mode [Symbol, String] what to expose; `:sql` is the only mode
100
+ # `Ports::Query.validate!` lets an adapter without `inspect_query` serve
101
+ # @return [InspectionSpec] the inspection request just recorded
46
102
  def inspect_query(mode = :sql) = @inspection = InspectionSpec.new(mode: mode)
47
103
 
48
104
  private
@@ -10,13 +10,27 @@ module Hecks
10
10
  module NullPolicy
11
11
  module_function
12
12
 
13
- # STABLE on purpose : rows arrive already in identity order from
14
- # Ports::Query::Ordering, and that base is what makes a tie deterministic
13
+ # Sorts records in memory by one key, placing the null-keyed ones where
14
+ # the policy says and keeping ties in their incoming order.
15
+ #
16
+ # Stable on purpose : rows arrive already in identity order from
17
+ # `Ports::Query::Ordering`, and that base is what makes a tie deterministic
15
18
  # rather than store-dependent. A plain sort_by is not stable in Ruby, so
16
19
  # equal keys would shuffle and the identity tier would be lost exactly
17
20
  # where it is needed. Descending reverses both partitions, so a tie reads
18
21
  # identity-descending too — the same total order sql_order renders as
19
22
  # `field DESC, id DESC`.
23
+ #
24
+ # @param records [Array<Object>] the rows to order, already in identity order
25
+ # @param direction [Symbol, String] `desc` sorts descending; anything else ascending
26
+ # @param policy [NullSemantics, nil] where nulls go: mode `first` or `last`; `nil` or
27
+ # any other mode (`native`) puts nulls first ascending and last descending
28
+ # @yield reads the sort key out of one record
29
+ # @yieldparam record [Object] one element of `records`
30
+ # @yieldreturn [Comparable, nil] the key, mutually comparable across records; `nil`
31
+ # marks the record as null-keyed
32
+ # @return [Array<Object>] a new Array holding every record in the requested order
33
+ # @raise [ArgumentError] if two non-nil keys cannot be compared with each other
20
34
  def order(records, direction:, policy: nil, &key)
21
35
  null_rows, valued_rows = records.partition { |record| key.call(record).nil? }
22
36
  sorted = valued_rows.each_with_index.sort_by { |record, index| [key.call(record), index] }.map(&:first)
@@ -31,18 +45,30 @@ module Hecks
31
45
  end
32
46
  end
33
47
 
34
- # M3 an UNDECLARED (`native`) null policy used to render no
35
- # `NULLS ...` clause at all here, leaving each dialect's own
36
- # default to decide: Postgres puts nulls LAST on ASC (and FIRST
48
+ # Renders the `ORDER BY` terms for one expression, with an explicit
49
+ # `NULLS FIRST`/`NULLS LAST` and an `id` tiebreak in the same direction.
50
+ #
51
+ # M3 — an undeclared (`native`) null policy still renders a
52
+ # `NULLS ...` clause. Rendering none leaves each dialect's own
53
+ # default to decide: Postgres puts nulls last on ASC (and first
37
54
  # on DESC), while `#order` above — this same "native" default,
38
- # for Memory — puts nulls FIRST on ASC (and LAST on DESC), the
55
+ # for Memory — puts nulls first on ASC (and last on DESC), the
39
56
  # SQLite convention. Same query, same data, different row order
40
57
  # depending only on which adapter ran it. Rendered explicitly
41
- # here instead, so an undeclared policy means the SAME total
58
+ # here instead, so an undeclared policy means the same total
42
59
  # order everywhere rather than "whatever this store already does"
43
60
  # — matching `#order`'s own default rather than the other way
44
61
  # round, since that default is unconditional (Memory/Heki have no
45
62
  # dialect to defer to) and SQLite already agrees with it natively.
63
+ #
64
+ # @param expression [String] the SQL expression to order by, already quoted or
65
+ # built by the adapter; interpolated as is
66
+ # @param direction [Symbol, String] `desc` in any letter case renders `DESC`;
67
+ # anything else `ASC`
68
+ # @param policy [NullSemantics, nil] mode `first` or `last` pins the nulls; `nil` or
69
+ # any other mode renders `NULLS FIRST` ascending and `NULLS LAST` descending
70
+ # @return [String] the terms without the `ORDER BY` keyword, such as
71
+ # `"price ASC NULLS FIRST, id ASC"`
46
72
  def sql_order(expression, direction, policy)
47
73
  direction = direction.to_s.downcase == "desc" ? "DESC" : "ASC"
48
74
  nulls = case policy&.mode.to_s
@@ -53,36 +79,54 @@ module Hecks
53
79
  "#{expression} #{direction}#{nulls}, id #{direction}"
54
80
  end
55
81
 
56
- # THE COMPARATORS A NULL CANNOT SATISFY — the other half of
57
- # `sql_predicate` below. That one answers "the value COMPARED TO is
82
+ # **The comparators a NULL cannot satisfy** — the other half of
83
+ # `sql_predicate` below. That one answers "the value compared to is
58
84
  # null" (`eq: nil` -> IS NULL, `ne: nil` -> IS NOT NULL, a real
59
85
  # convention both adapters already shared). This one answers the
60
- # case nothing covered: the ROW'S OWN value is null and the value
86
+ # case nothing covered: the row's own value is null and the value
61
87
  # compared to is not.
62
88
  #
63
89
  # SQL says unknown. `NULL <> 'red'` is NULL, not true, so the row
64
90
  # is not returned. Ruby says `nil != "red"` is true, so it is. The
65
- # two adapters therefore answered the SAME query on the SAME data
91
+ # two adapters therefore answered the same query on the same data
66
92
  # differently — Memory returning a row SQLite omitted — which is
67
93
  # not a difference of opinion a caller can plan around.
68
94
  #
69
95
  # Resolved toward SQL, and not because SQL is the store: an absent
70
- # or null field is UNKNOWN, not a value, and a comparison against
96
+ # or null field is unknown, not a value, and a comparison against
71
97
  # unknown is unknown rather than true. Making SQL match Ruby
72
98
  # instead would mean compiling every `ne:` to
73
99
  # `(col <> $1 OR col IS NULL)` — more to get right in two
74
100
  # dialects, and a reliable way to lose an index — to make a real
75
101
  # query engine agree with an in-memory one.
76
102
  #
77
- # `none_in_state` is deliberately NOT here: it is a 9th, vendored
103
+ # `none_in_state` is deliberately not here: it is a 9th, vendored
78
104
  # comparator whose `held` is a reference id, and a row holding no
79
105
  # reference is genuinely "not in that state" rather than unknown.
80
106
  NULL_UNMATCHABLE = %w[eq ne lt lte gt gte in contains].freeze
81
107
 
108
+ # Decides whether a comparison is lost before it starts because the row's
109
+ # own value is null — the rule `NULL_UNMATCHABLE`'s comment argues for.
110
+ #
111
+ # @param operation [Symbol, String] the comparator name, such as `:ne`
112
+ # @param held [Object, nil] the row's own value for the field
113
+ # @param want [Object, nil] the value compared against
114
+ # @return [Boolean] `true` when `held` is `nil`, `want` is not, and the comparator is
115
+ # one of `NULL_UNMATCHABLE`; `none_in_state` is never unmatchable
82
116
  def unmatchable?(operation, held, want)
83
117
  held.nil? && !want.nil? && NULL_UNMATCHABLE.include?(operation.to_s)
84
118
  end
85
119
 
120
+ # Renders the SQL for a comparison against a null value — `eq: nil` as
121
+ # `IS NULL`, `ne: nil` as `IS NOT NULL` — so an adapter never binds a
122
+ # `NULL` parameter to `=` or `<>`.
123
+ #
124
+ # @param expression [String] the SQL expression for the field, interpolated as is
125
+ # @param operation [Symbol, String] the comparator name
126
+ # @param value [Object, nil] the resolved value compared against
127
+ # @return [Array(String, Array), nil] the predicate text and its (empty) bind
128
+ # parameters; `nil` when `value` is not `nil` or the comparator is neither `eq`
129
+ # nor `ne`, leaving the adapter to render the clause itself
86
130
  def sql_predicate(expression, operation, value)
87
131
  if value.nil? && operation.to_s == "eq"
88
132
  ["#{expression} IS NULL", []]
@@ -4,6 +4,10 @@ module Hecks
4
4
  NullSemantics = Struct.new(:mode, keyword_init: true) do
5
5
  def to_h = { mode: mode.to_s }
6
6
 
7
+ # Builds the policy a query has when it never writes `nulls`: each
8
+ # engine orders nulls first ascending and last descending (see `NullPolicy`).
9
+ #
10
+ # @return [NullSemantics] a new instance whose mode is `:native`
7
11
  def self.default = new(mode: :native)
8
12
  end
9
13
  end
@@ -13,6 +13,18 @@ module Hecks
13
13
  attr_reader :wheres, :order_by, :limit, :offset, :cursor,
14
14
  :authorization, :null_semantics, :inspection
15
15
 
16
+ # @param wheres [Array<WhereClause>] the filter clauses, all of which must hold
17
+ # @param order_by [OrderBy, nil] the single ordering; `nil` leaves identity order
18
+ # @param limit [LimitSpec, nil] the most rows returned; `nil` is unbounded
19
+ # @param offset [OffsetSpec, nil] rows skipped before the limit; `nil` skips none
20
+ # @param cursor [CursorSpec, nil] the cursor declaration; `nil` when none is declared
21
+ # @param authorization [AuthorizationSpec, nil] the declared policy and tenant field;
22
+ # `nil` when the query declares no `authorize`
23
+ # @param inspection [InspectionSpec, nil] the `inspect_query` request; `nil` when
24
+ # the query asks for none
25
+ # @param null_semantics [NullSemantics, nil] where nulls sort; stored as given, so
26
+ # an explicit `nil` (what `ReadModelBuilder` passes when `nulls` was never
27
+ # written) stays `nil` rather than becoming the `native` default
16
28
  def initialize(wheres: [], order_by: nil, limit: nil, offset: nil, cursor: nil,
17
29
  authorization: nil,
18
30
  inspection: nil, null_semantics: NullSemantics.default)
@@ -26,12 +38,25 @@ module Hecks
26
38
  @inspection = inspection
27
39
  end
28
40
 
41
+ # Serializes every shared option, declared or not, so a subclass's `to_h`
42
+ # has one fixed set of keys to build on.
43
+ #
44
+ # @return [Hash{Symbol => Array<Hash>, Hash, nil}] keys `:wheres` (an Array of clause
45
+ # Hashes, `[]` when none), `:order_by`, `:limit`, `:offset`, `:cursor`,
46
+ # `:authorization`, `:null_semantics` and `:inspection`, each that spec's own
47
+ # `to_h` or `nil` when undeclared
29
48
  def options_to_h
30
49
  { wheres: @wheres.map(&:to_h), order_by: @order_by&.to_h, limit: @limit&.to_h,
31
50
  offset: @offset&.to_h, cursor: @cursor&.to_h, authorization: @authorization&.to_h,
32
51
  null_semantics: @null_semantics&.to_h, inspection: @inspection&.to_h }
33
52
  end
34
53
 
54
+ # Serializes only the declared options beyond the settled three, so a
55
+ # construct that never wrote one keeps its wire shape unchanged.
56
+ #
57
+ # @return [Hash{Symbol => Hash}] the subset of `options_to_h` that is set, without
58
+ # `:wheres`, `:order_by` and `:limit` (which the subclass emits itself) and
59
+ # without a `:null_semantics` of `{ mode: "native" }`; `{}` when nothing is
35
60
  def extra_options_to_h
36
61
  options_to_h.reject do |key, value|
37
62
  value.nil? || value == [] || (key == :null_semantics && value == { mode: "native" })
@@ -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"]`
@@ -29,7 +29,7 @@ module Hecks
29
29
  # ("#<Hecks::Runtime::Value:0x...>") into an otherwise
30
30
  # correct domain refusal. Duck-typed on `respond_to?(:to_h)`
31
31
  # rather than naming `Runtime::Value` directly — `Runtime::Value`
32
- # itself requires THIS file (`runtime/value.rb`'s own
32
+ # itself requires this file (`runtime/value.rb`'s own
33
33
  # `require_relative "../rendering"`), so naming it here would be
34
34
  # circular. A single-field wrapper (the overwhelming common case
35
35
  # — an amount, an id, a lifecycle field) unwraps to its bare