hecks 1.2.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 (358) 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 +60 -2
  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/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -13,28 +13,28 @@ module Hecks
13
13
  # working, golden-tested example of the shape every future projector
14
14
  # (`:rust`, `:ul`, `:openid`, ...) is meant to follow.
15
15
  #
16
- # The unit is ONE bluebook's IR, not a whole booted registry — matching
16
+ # The unit is one bluebook's IR, not a whole booted registry — matching
17
17
  # every real projection target (Rust/UL/OIDC all project one domain at
18
18
  # a time), and deliberately narrower than `Exporter.call`'s own
19
19
  # multi-domain shape.
20
- # THREE KINDS OF "PROJECT", TOLD APART BY WHAT THEY NEED AS INPUT.
20
+ # Three kinds of "PROJECT", told apart by what they need as input.
21
21
  # Only the first belongs in this registry.
22
22
  #
23
- # A PROJECTION takes a chapter's DECLARATION and answers something
24
- # that DESCRIBES the domain: its IR, its storage shape, an OIDC scope
23
+ # a projection takes a chapter's declaration and answers something
24
+ # that describes the domain: its IR, its storage shape, an OIDC scope
25
25
  # manifest, the parser's keyword table, the reference pages. Inert,
26
26
  # derived, and runnable against any chapter that carries what it
27
27
  # declares it needs. These are what `register` holds.
28
28
  #
29
- # AN EXPORT takes a declaration AND its BINDINGS and answers
30
- # something that IS the domain, running elsewhere — rust/project.rb's
29
+ # an export takes a declaration and its bindings and answers
30
+ # something that is the domain, running elsewhere — rust/project.rb's
31
31
  # generated crate, the WASM artifact, the SAM template
32
32
  # bin/project_deploy renders. It needs the `.world`/`.hecksagon` a
33
33
  # projection never looks at, because a running system has to know how
34
34
  # it is wired. That is the whole reason bin/project_deploy cannot use
35
35
  # this protocol: `call(bluebook:, options:)` has no channel for it.
36
36
  #
37
- # A STATE PROJECTION takes RECORDS — a domain after dispatch — and is
37
+ # a state projection takes records — a domain after dispatch — and is
38
38
  # a read-model question wearing the same word.
39
39
  # `bin/expression_projection` is the one of these: its operators are
40
40
  # not declared anywhere, they are what exists after
@@ -42,7 +42,7 @@ module Hecks
42
42
  # dispatches. Converting it into this registry would be a category
43
43
  # error, however much its name suggests otherwise.
44
44
  #
45
- # ONE WORD, THREE OTHER MEANINGS — worth naming too, because grepping
45
+ # **One word, three other meanings** — worth naming too, because grepping
46
46
  # "projection" turns all of these up and none is the above:
47
47
  #
48
48
  # Ports::Projection read-model catch-up, events folded into state
@@ -68,7 +68,7 @@ module Hecks
68
68
  # `bluebook:` is kept as the keyword because it is the shipped
69
69
  # spelling and every existing caller uses it — but what it accepts is
70
70
  # any construct that emits IR, and `admits!` is what decides whether
71
- # THIS target can actually take the one handed over.
71
+ # this target can actually take the one handed over.
72
72
  def call(name, bluebook:, options: {})
73
73
  projector = registry.fetch(name.to_sym) do
74
74
  raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
@@ -77,12 +77,12 @@ module Hecks
77
77
  projector.call(bluebook: bluebook, options: options)
78
78
  end
79
79
 
80
- # A projection names the CAPABILITIES it needs; this refuses a
80
+ # A projection names the capabilities it needs; this refuses a
81
81
  # construct that lacks one, before the projector runs.
82
82
  #
83
- # ONE CHECK COVERS BOTH SHAPES. An ordinary construct INCLUDES its
83
+ # One check covers both shapes. An ordinary construct includes its
84
84
  # capabilities and a class-shaped one — Command, Entity, ValueObject
85
- # — EXTENDS them, and `is_a?` consults the singleton chain, so it
85
+ # — extends them, and `is_a?` consults the singleton chain, so it
86
86
  # answers for an extended module as readily as an included one. This
87
87
  # started as two checks on the assumption it would not; a spec
88
88
  # asserting the assumption failed, which is the only reason the
@@ -107,7 +107,7 @@ module Hecks
107
107
 
108
108
  def capable?(construct, capability) = construct.is_a?(capability)
109
109
 
110
- # What KIND of artifact a registered target emits — asked of the
110
+ # What kind of artifact a registered target emits — asked of the
111
111
  # projection rather than inferred from what it returned.
112
112
  def emits_for(name)
113
113
  projector = registry.fetch(name.to_sym) { return :artifact }
@@ -132,7 +132,7 @@ module Hecks
132
132
  target
133
133
  end
134
134
 
135
- # WRITING IS THE CALLER'S CHOICE, NOT THE PROJECTOR'S. A projector
135
+ # Writing is the caller's choice, not the projector's. A projector
136
136
  # returns an artifact and never touches disk, which is what lets
137
137
  # spec/projector_spec.rb compare `:ir`'s output against a golden
138
138
  # fixture without a tmpdir. `out:` is the only thing that writes.
@@ -178,7 +178,7 @@ Hecks::Projector.register(:docs, Hecks::Projector::DocsProjector)
178
178
  Hecks::Projector.register(:narrate, Hecks::Projector::NarrateProjector)
179
179
  Hecks::Projector.register(:cli, Hecks::Projector::CliProjector)
180
180
 
181
- # The TARGETS are required from lib/hecks.rb, immediately after this
181
+ # The targets are required from lib/hecks.rb, immediately after this
182
182
  # file — deliberately not from here. A target requires this file (it
183
183
  # needs `Target` and the registry), so requiring them back from here
184
184
  # would close a genuine `circular require considered harmful` loop. Ruby
@@ -4,7 +4,7 @@ require_relative "projections/model"
4
4
  require_relative "fuzzing/properties"
5
5
 
6
6
  module Hecks
7
- # THE SHARED CORE behind `bin/query_ir` (a text CLI) and
7
+ # The shared core behind `bin/query_ir` (a text CLI) and
8
8
  # `bin/hecks_query_ir_mcp` (an MCP server exposing the same two
9
9
  # queries as tools) — one implementation, two front ends, the same
10
10
  # reason `Hecks::Codemod` exists once rather than per-script.
@@ -14,7 +14,7 @@ module Hecks
14
14
  Codemod = Hecks::Codemod
15
15
  Deviations = Hecks::Projections::Model::Deviations
16
16
 
17
- # THE SAME MAPPING spec/model_shape_conformance_spec.rb's own
17
+ # The same mapping spec/model_shape_conformance_spec.rb's own
18
18
  # MODEL_CONSTRUCTS holds — kept here rather than shared from the
19
19
  # spec (a spec file is not a library other code should require),
20
20
  # matching Deviations' own doc comment: "the generator and the gate
@@ -44,7 +44,7 @@ module Hecks
44
44
 
45
45
  # The real structural diff between what a Ruby IR class emits
46
46
  # (Class.ir_spec.keys) and what the self-hosted meta-domain declares
47
- # for it — the SAME comparison spec/model_shape_conformance_spec.rb
47
+ # for it — the same comparison spec/model_shape_conformance_spec.rb
48
48
  # makes, reusing its own Deviations data so this can never silently
49
49
  # drift from what that gate actually checks.
50
50
  def construct_diff(name)
@@ -54,8 +54,8 @@ module Hecks
54
54
  declared = meta_declared(name)
55
55
  emitted = klass.ir_spec.keys
56
56
 
57
- accounted = declared.reject { |field| Deviations::PARENT_REF.call(field) } -
58
- Deviations::JUDGE_ONLY -
57
+ accounted = declared.reject { |field| Deviations.parent_ref?(field) } -
58
+ Deviations.judge_only(name) -
59
59
  Deviations.folded(name).values.flatten -
60
60
  Deviations.off_the_wire(name) -
61
61
  Deviations.dynamic_tail(name) -
@@ -77,22 +77,22 @@ module Hecks
77
77
  targets.map { |name| construct_diff(name) }
78
78
  end
79
79
 
80
- # Every given/ensures/invariant DECLARATION reachable from a booted
80
+ # Every given/ensures/invariant declaration reachable from a booted
81
81
  # registry, walked recursively — every owner's own `.preconditions`/
82
82
  # `.invariants` (block-declared rules), every value object's own
83
83
  # `.invariants`, and every command's own `.givens`/`.ensures`
84
- # (`.givens` too, not just `.ensures` — a command's own LOCAL
84
+ # (`.givens` too, not just `.ensures` — a command's own local
85
85
  # `given("x") { block }` not yet hoisted to its owner, round 4's own
86
86
  # starting shape, would otherwise be invisible).
87
87
  #
88
- # NOT keyed by object identity (a real, hard-won correction — see
88
+ # Not keyed by object identity (a real, hard-won correction — see
89
89
  # the comment on `duplicates`' own dedup below for why: a bare
90
90
  # `given("x")` reference and its owner's own block declaration are
91
- # the SAME Ruby object at DSL build time, but `MetaValidator.call`
92
- # (S14 — every bluebook is judged by dispatching its OWN IR into the
91
+ # the same Ruby object at DSL build time, but `MetaValidator.call`
92
+ # (S14 — every bluebook is judged by dispatching its own IR into the
93
93
  # self-hosted grammar, then reconstructed via `Assembly.call` from
94
94
  # flat rows) rebuilds the whole graph fresh from there. By the time
95
- # any caller reads `chapter.aggregates`, EVERY given/invariant is
95
+ # any caller reads `chapter.aggregates`, every given/invariant is
96
96
  # already a distinct object, whether it was block-declared or
97
97
  # bare-referenced — object identity carries no signal past that
98
98
  # point, for any construct, not just this one).
@@ -114,7 +114,7 @@ module Hecks
114
114
  rules
115
115
  end
116
116
 
117
- # THE RECURSIVE WALK `collect_rules` drives — pulled out of that method
117
+ # The recursive walk `collect_rules` drives — pulled out of that method
118
118
  # (pure extraction, identical traversal and Rule shapes) as its own
119
119
  # named, self-recursive method rather than a lambda closing over the
120
120
  # same locals. `rules` is the one piece of state every call shares —
@@ -147,14 +147,14 @@ module Hecks
147
147
  end
148
148
  private_class_method :walk_construct_rules
149
149
 
150
- # A rule's OWNER — the construct path a "(declared)" location names
150
+ # A rule's owner — the construct path a "(declared)" location names
151
151
  # directly, or (for a command-level `.givens`/`.ensures` entry) the
152
152
  # path with its trailing `.CommandName` segment stripped. Two rules
153
- # sharing an owner are the SAME declaration read twice (an owner's
153
+ # sharing an owner are the same declaration read twice (an owner's
154
154
  # own precondition, and a command under it referencing that
155
155
  # precondition by name) — not two independent ones.
156
156
  #
157
- # PUBLIC, not a `duplicates`-only internal — `bin/codemod_hoist_
157
+ # Public, not a `duplicates`-only internal — `bin/codemod_hoist_
158
158
  # local_givens` reads it directly to group `collect_rules`' own
159
159
  # output by owner itself, the same reading `duplicates`' own
160
160
  # `declaration_count` makes.
@@ -167,23 +167,23 @@ module Hecks
167
167
  # Grouped by (kind, description, canonical), not canonical text
168
168
  # alone — a generic one-liner like `!value.to_s.empty?` legitimately
169
169
  # recurs dozens of times for unrelated fields; the real signal is
170
- # the SAME RULE (same description, same predicate), which is also
170
+ # the same rule (same description, same predicate), which is also
171
171
  # exactly what the given/invariant reference mechanism itself
172
172
  # resolves on.
173
173
  #
174
- # DEDUPED BY OWNER, not object identity (`collect_rules`' own
174
+ # Deduped by owner, not object identity (`collect_rules`' own
175
175
  # comment has the full story — identity is gone by the time this
176
176
  # reads the registry). Within a group, every command-level rule
177
- # whose OWNER already has its own "(declared)" entry in the same
177
+ # whose owner already has its own "(declared)" entry in the same
178
178
  # group is just that declaration read again through a reference —
179
179
  # `Account.Open`/`Account.Credit`/etc. all naming `Account`'s own
180
- # `given("customer is active")` count as Account's ONE declaration,
180
+ # `given("customer is active")` count as Account's one declaration,
181
181
  # not nine. A command-level rule with no matching owner declaration
182
- # (a LOCAL, not-yet-hoisted `given("x") { block }`) counts as its
182
+ # (a local, not-yet-hoisted `given("x") { block }`) counts as its
183
183
  # own standalone declaration — two different commands independently
184
- # writing the identical local predicate IS two declarations, a real
184
+ # writing the identical local predicate is two declarations, a real
185
185
  # hoisting opportunity. A group is reported only when it adds up to
186
- # MORE than one real declaration this way.
186
+ # more than one real declaration this way.
187
187
  #
188
188
  # `domains: []` means "the self-hosted meta-domain only" — pass real
189
189
  # domain directories explicitly to include them, or `nil` (the
@@ -210,48 +210,48 @@ module Hecks
210
210
  end
211
211
  end
212
212
 
213
- # `given` ONLY (not `invariant`/`ensures`) also covers CROSS-ENTITY
213
+ # `given` only (not `invariant`/`ensures`) also covers cross-entity
214
214
  # coverage — one piece's own entity-level declaration, shared with
215
- # any OTHER piece nested under the SAME root aggregate (real corpus
215
+ # any other piece nested under the same root aggregate (real corpus
216
216
  # this closes: SafeDepositBox's own `Visit`/`KeyIssuance`, two
217
217
  # different pieces on one head). A rule owned by a nested entity
218
218
  # (its own owner path has more than one segment) is covered when
219
- # SOME "(declared)" entry exists ANYWHERE under that SAME root
220
- # aggregate — not just under its OWN exact owner — matching the
219
+ # some "(declared)" entry exists anywhere under that same root
220
+ # aggregate — not just under its own exact owner — matching the
221
221
  # DSL's own pool, threaded unchanged through an aggregate's whole
222
222
  # entity tree (`AggregateBuilder#entity`'s own comment).
223
223
  #
224
- # A KNOWN, ACCEPTED GAP this does NOT (and structurally cannot)
225
- # close: CHAPTER-WIDE given sharing (`AggregateBuilder#given`'s own
224
+ # A known, accepted gap this does not (and structurally cannot)
225
+ # close: chapter-wide given sharing (`AggregateBuilder#given`'s own
226
226
  # bare form, `docs/implemented/resolution-rules/chapter-given.md`) — `Account`,
227
227
  # `SafeDepositBox`, and `OnboardingCase` each still show as their
228
- # own "(declared)" owner here even AFTER `SafeDepositBox`/
228
+ # own "(declared)" owner here even after `SafeDepositBox`/
229
229
  # `OnboardingCase` were converted to bare chapter-wide references,
230
- # because a REFERENCED given still write-throughs into its own
231
- # aggregate's `@named_givens` — the SAME reason `collect_rules`'
230
+ # because a referenced given still write-throughs into its own
231
+ # aggregate's `@named_givens` — the same reason `collect_rules`'
232
232
  # own top comment already gives for why object identity carries no
233
- # signal past a bluebook's own build: the EXPORTED IR cannot tell
233
+ # signal past a bluebook's own build: the exported IR cannot tell
234
234
  # "I declared this myself" apart from "I referenced someone else's
235
235
  # declaration," because by the time anything reads `chapter.
236
236
  # aggregates`, both look identical. Closing this would mean reading
237
- # SOURCE TEXT (bare `given(desc)` vs. block `given(desc) { ... }`),
237
+ # source text (bare `given(desc)` vs. block `given(desc) { ... }`),
238
238
  # not the built IR this query is deliberately built on — a
239
239
  # different, source-level tool, not a fix to this one. Treat a
240
240
  # still-flagged group naming multiple aggregates as "verify by
241
- # hand whether this is ALREADY a chapter-wide reference before
241
+ # hand whether this is already a chapter-wide reference before
242
242
  # assuming it's fresh duplication," not as an automatic signal
243
243
  # either way.
244
244
  #
245
- # THE IDENTICAL GAP, ONE LEVEL DOWN: chapter-wide ENTITY-scoped
245
+ # The identical gap, one level down: chapter-wide entity-scoped
246
246
  # sharing (`EntityBuilder#given`'s own bare form,
247
247
  # `docs/implemented/resolution-rules/chapter-entity-given.md`) hits this same wall for
248
248
  # the same structural reason — `SafeDepositBox.Visit` still shows
249
249
  # as its own "(declared)" owner here even after becoming a bare
250
250
  # reference to `Account.LedgerEntry`'s declaration, because a piece
251
251
  # resolving a chapter-wide reference still write-throughs the
252
- # resolved `Given` into its own `@named_givens` (so ITS OWN
252
+ # resolved `Given` into its own `@named_givens` (so its own
253
253
  # commands can read it back locally without a second hop). This is
254
- # not a NEW limitation this feature introduces — it is the exact
254
+ # not a new limitation this feature introduces — it is the exact
255
255
  # same IR-cannot-distinguish-declared-from-referenced fact, one
256
256
  # scope wider. `bin/query_ir duplicates` confirms this directly:
257
257
  # `Account.LedgerEntry (declared)` and `SafeDepositBox.Visit
@@ -278,10 +278,10 @@ module Hecks
278
278
  end
279
279
  private_class_method :declaration_count
280
280
 
281
- # SHARED TEXT FORMATTING — both `bin/query_ir` (a text CLI) and
281
+ # Shared text formatting — both `bin/query_ir` (a text CLI) and
282
282
  # `bin/hecks_query_ir_mcp` (an MCP tool result, itself a text
283
283
  # block) want the identical human-readable rendering; only the
284
- # OUTER framing differs (plain stdout vs. a JSON-RPC content array).
284
+ # outer framing differs (plain stdout vs. a JSON-RPC content array).
285
285
  def format_constructs(diffs)
286
286
  diffs.map do |diff|
287
287
  lines = ["== #{diff[:name]} =="]
@@ -303,26 +303,26 @@ module Hecks
303
303
  end.join("\n\n")
304
304
  end
305
305
 
306
- # ONE HAND-TYPED CONSTRUCT-NAME PER RECONSTRUCTION METHOD — the only
307
- # two `MetaValidator::Reconstruction` methods NOT driven generically
306
+ # **One hand-typed construct-name per reconstruction method** — the only
307
+ # two `MetaValidator::Reconstruction` methods not driven generically
308
308
  # through `Assembly::Contracts`' own table (its own header explains
309
309
  # why: `aggregate(row)`/`entity(row)` predate the table and were
310
310
  # never migrated). `impact_preview`'s own touchpoint 4 is checked
311
- # ONLY for these two — every other construct is read generically, so
311
+ # only for these two — every other construct is read generically, so
312
312
  # asking "does Command's own reconstruction method mention this
313
313
  # field" is a question with no method to check.
314
314
  RECONSTRUCTION_METHODS = { "Aggregate" => :aggregate, "Entity" => :entity }.freeze
315
315
 
316
- # THE SIX TOUCHPOINTS `.claude/skills/bluebook-construct-creator/
316
+ # The six touchpoints `.claude/skills/bluebook-construct-creator/
317
317
  # SKILL.md` walks in prose, checked structurally instead of by hand
318
- # — for a construct/field pair NOT yet fully propagated (typically
318
+ # — for a construct/field pair not yet fully propagated (typically
319
319
  # mid-round, deciding what's left), or as a sanity check before the
320
320
  # final gate sweep of a round already believed done. Every check
321
- # here is BEST-EFFORT and ADVISORY, not a gate: a `false` does not
321
+ # here is best-effort and advisory, not a gate: a `false` does not
322
322
  # always mean "not yet done" (a field can be legitimately exempt —
323
323
  # `Deviations`' own named categories, `GUARANTEED_BY_CONSTRUCTION`,
324
324
  # or `META_DOMAIN_KNOWN_GAPS`, the last of which lives in
325
- # spec/fuzzing/meta_domain_coverage_spec.rb, a SPEC file this
325
+ # spec/fuzzing/meta_domain_coverage_spec.rb, a spec file this
326
326
  # module deliberately never requires — see `CONSTRUCTS`' own
327
327
  # comment on the same principle). Read the touchpoint's own
328
328
  # existing gate (`model_shape_conformance_spec.rb`,
@@ -365,7 +365,7 @@ module Hecks
365
365
  # the method's own body ends at the next line indented no deeper
366
366
  # than its own `def` — the same boundary Ruby itself uses, read back
367
367
  # textually because there is no live AST here, only a file to grep a
368
- # slice of. `nil` (not `false`) for every OTHER construct — this
368
+ # slice of. `nil` (not `false`) for every other construct — this
369
369
  # touchpoint genuinely does not apply to them (`RECONSTRUCTION_
370
370
  # METHODS` only names the two hand-typed methods), and collapsing
371
371
  # "does not apply" into "not done" would misreport a construct that
@@ -7,10 +7,16 @@ module Hecks
7
7
  # vocabulary — and .render_value, the wire-rendering entry point
8
8
  # shared by every literal-bearing spec struct.
9
9
  module QuerySpecification
10
+ # The vocabulary every query-shaped construct shares — the clause structs
11
+ # (`WhereClause`, `OrderBy`, `LimitSpec`...), the `DSL` mixin that parses
12
+ # them, and the comparison and null rules every engine answers by. It is
13
+ # its own namespace so a plain `Bluebook::Query` and a
14
+ # `ReadModel::Specification` say one thing the same way rather than each
15
+ # carrying a copy.
10
16
  module Common
11
17
  # `none_in_state`, vendored addition not (yet) upstream hecks
12
- # (migration plan task 4): a CROSS-AGGREGATE ANTI-JOIN comparator --
13
- # `where ref: { none_in_state: "Claim:held" }` holds true when NO
18
+ # (migration plan task 4): a cross-aggregate anti-join comparator --
19
+ # `where ref: { none_in_state: "Claim:held" }` holds true when no
14
20
  # record in the named aggregate, keyed by this record's own field
15
21
  # value, is currently in the named state. plan.bluebook's own
16
22
  # description: "a keyed point lookup (HashMap hit), never a scan" --
@@ -20,10 +26,20 @@ module Hecks
20
26
  COMPARATORS = Hecks::Vocabulary.symbols("QueryComparator")
21
27
  end
22
28
 
29
+ # Renders a literal captured in a query clause as its self-describing
30
+ # wire spelling, for a spec struct's `to_h`.
31
+ #
23
32
  # The specification structs' own name for the one wire spelling — see
24
- # Hecks::Literal, which every other `to_h`-bound literal field now
33
+ # `Hecks::Literal`, which every other `to_h`-bound literal field
25
34
  # shares. Kept as a word here because the structs below read better
26
35
  # saying what they are doing than naming the module that does it.
36
+ #
37
+ # @param value [nil, Symbol, String, StateRef, Boolean, Integer, Float, Hash, Array] the
38
+ # literal as the bluebook author wrote it; Hash values and Array elements are
39
+ # rendered recursively
40
+ # @return [String] the wire text: a Symbol keeps its colon, a String its double quotes,
41
+ # and a number, a boolean and `nil` are bare
42
+ # @raise [ArgumentError] if `value`, or a value nested in it, is of any other class
27
43
  def self.render_value(value) = Literal.render(value)
28
44
  end
29
45
  end
@@ -4,7 +4,7 @@ require_relative "../../runtime/value"
4
4
  module Hecks
5
5
  module QuerySpecification
6
6
  module Common
7
- # THE ONE COMPARATOR TABLE. There were two, near-identical copies of
7
+ # **The one comparator table**. There were two, near-identical copies of
8
8
  # this — `Ports::Query::InMemory` (the path a Memory- or Heki-backed
9
9
  # aggregate query actually runs) and `Runtime::QueryInterpreter`
10
10
  # (entity/sub-list queries, and any adapter implementing no `:query`)
@@ -15,23 +15,25 @@ module Hecks
15
15
  # real bugs twice already, both recorded in the comments those files
16
16
  # carried. `none_in_state` was added to one copy only, so an ordinary
17
17
  # Memory query's `none_in_state` clause fell to the `else` branch and
18
- # silently excluded every row. And `comparable` DIVERGED without
19
- # anyone noticing: one copy took the FIRST numeric member of a value
20
- # object, the other only unwrapped when there was EXACTLY ONE — so a
18
+ # silently excluded every row. And `comparable` diverged without
19
+ # anyone noticing: one copy took the first numeric member of a value
20
+ # object, the other only unwrapped when there was exactly one — so a
21
21
  # value object with two numeric members compared as a number on one
22
22
  # path and as a whole Hash on the other.
23
23
  #
24
- # The callers still differ in how they REACH a value — one takes a
24
+ # The callers still differ in how they reach a value — one takes a
25
25
  # registry argument, the other closes over `@registry`; one digs the
26
26
  # field through `FieldPath`, the other is handed it — so they keep
27
27
  # their own resolution and share only the comparison itself.
28
28
  module Comparison
29
29
  module_function
30
30
 
31
+ # Unwraps a value object to the one scalar a comparison can mean.
32
+ #
31
33
  # A value object compared as a scalar, when which scalar is meant
32
34
  # is not in doubt. Exactly one numeric member is unambiguous; a
33
35
  # single-member value object is unambiguous whatever its type.
34
- # Anything else is returned UNCHANGED rather than guessed at — two
36
+ # Anything else is returned unchanged rather than guessed at — two
35
37
  # numeric members give no reason to prefer either, and picking the
36
38
  # first silently compares a field the author never named.
37
39
  #
@@ -40,6 +42,11 @@ module Hecks
40
42
  # so this branch is a backstop rather than the primary guard: which
41
43
  # member is meant is knowable at declaration time, and a refusal
42
44
  # naming the candidates is worth more than any runtime reading.
45
+ #
46
+ # @param value [Runtime::Value, Hash, Object, nil] a held or wanted value; a
47
+ # `Runtime::Value` is read through its `to_h`
48
+ # @return [Object, nil] the sole numeric member, or the sole member, of a Hash-shaped
49
+ # value; otherwise `value` unchanged (a `Runtime::Value` comes back as its Hash)
43
50
  def comparable(value)
44
51
  value = value.to_h if value.is_a?(Runtime::Value)
45
52
  return value unless value.is_a?(Hash)
@@ -51,9 +58,15 @@ module Hecks
51
58
  value
52
59
  end
53
60
 
54
- # Which members a value object offers a scalar comparison, for a
61
+ # Lists the members a value object offers a scalar comparison, for a
55
62
  # refusal that can name them. Empty when the value object is
56
63
  # unambiguous — nothing to report.
64
+ #
65
+ # @param value_object [Class<Bluebook::ValueObject>] the declared shape (a subclass
66
+ # minted by `Bluebook::ValueObject.declare`, closed sets included) a query field names
67
+ # @return [Array<Symbol>] every attribute name when no single member can be meant;
68
+ # `[]` when there is exactly one attribute, or exactly one typed `Integer`,
69
+ # `Float` or `Numeric`
57
70
  def ambiguous_members(value_object)
58
71
  numerics = value_object.attributes.select { |a| NUMERIC_TYPES.include?(a.type.to_s) }
59
72
  return [] if numerics.size == 1 || value_object.attributes.size == 1
@@ -63,15 +76,32 @@ module Hecks
63
76
 
64
77
  NUMERIC_TYPES = %w[Integer Float Numeric].freeze
65
78
 
66
- # A `case` over a closed, declared set (Vocabulary::QueryComparator,
79
+ # Decides whether one where-clause comparison holds between the value
80
+ # a record holds and the value the query wants.
81
+ #
82
+ # A `case` over a closed, declared set (`Vocabulary::QueryComparator`,
67
83
  # held equal to this list by spec/vocabulary_table_spec — see the
68
- # `else` branch's own comment) is the whole point of THE ONE
69
- # COMPARATOR TABLE this file's header describes: one place naming
84
+ # `else` branch's own comment) is the whole point of the one
85
+ # comparator table this file's header describes: one place naming
70
86
  # every comparator, not one method per comparator scattered across
71
87
  # a module.
88
+ #
89
+ # @param operation [Symbol, String] the comparator name: `eq`, `ne`, `lt`, `lte`,
90
+ # `gt`, `gte`, `in`, `contains` or `none_in_state`
91
+ # @param held [Object, nil] the record's own value for the field; `nil` against a
92
+ # non-nil `want` satisfies no comparator except `none_in_state` (see
93
+ # `NullPolicy.unmatchable?`)
94
+ # @param want [Object, nil] the value compared against; for `in` an Array or a
95
+ # comma-separated String, for `none_in_state` an `"Aggregate:state"` String
96
+ # @param registry [Runtime::Registry, nil] used only by `none_in_state` to look the
97
+ # target aggregate up; `nil` makes that comparator hold
98
+ # @return [Boolean] whether the comparison holds; an ordered comparator over a
99
+ # non-`Numeric` operand is `false`
100
+ # @raise [Runtime::WiringError] if `operation` names no comparator in this table, or
101
+ # `none_in_state`'s target aggregate has no repository that can be wired
72
102
  # rubocop:disable-next Metrics/CyclomaticComplexity
73
103
  def holds?(operation, held, want, registry: nil)
74
- # A NULL SATISFIES NO COMPARISON — NullPolicy.unmatchable? owns
104
+ # A NULL satisfies no comparison — NullPolicy.unmatchable? owns
75
105
  # the rule and the reasoning, including why `none_in_state` is
76
106
  # exempt from it.
77
107
  return false if NullPolicy.unmatchable?(operation, held, want)
@@ -100,28 +130,51 @@ module Hecks
100
130
  end
101
131
  end
102
132
 
133
+ # Checks that both operands of an ordered comparison are numbers.
134
+ #
103
135
  # gt/gte/lt/lte are numeric-only and silently false otherwise — a
104
136
  # where-clause never raises the way a given does, and that contract
105
137
  # predates the extraction (lt was already exactly this permissive).
138
+ #
139
+ # @param held [Object, nil] the record's own value for the field
140
+ # @param want [Object, nil] the value compared against
141
+ # @return [Boolean] `true` only when both are `Numeric`
106
142
  def ordered?(held, want) = held.is_a?(Numeric) && want.is_a?(Numeric)
107
143
 
144
+ # Reads a comparator's list operand as the Strings membership is tested against.
145
+ #
108
146
  # `in` reads a comma-separated list — a real Array survives
109
147
  # untouched (a bluebook's own in-process value, before any wire
110
148
  # serialisation), each element unwrapped the same way a scalar
111
- # field is. This is `in`'s reading of ITS ARGUMENT (a caller may
149
+ # field is. This is `in`'s reading of its argument (a caller may
112
150
  # legitimately pass "a,b,c" meaning "any of these") — unrelated to
113
- # `contains`, which reads the STORED field. See `contains?`.
151
+ # `contains`, which reads the stored field. See `contains?`.
152
+ #
153
+ # @param value [Array, String, Object, nil] an Array of elements, or anything whose
154
+ # `to_s` is a comma-separated list such as `"a, b,c"`
155
+ # @return [Array<String>] one String per member: Array elements unwrapped through
156
+ # `comparable` then `to_s`; split parts stripped of surrounding whitespace; `[]`
157
+ # for `nil` or an empty String
114
158
  def members(value)
115
159
  return value.map { |element| comparable(element).to_s } if value.is_a?(Array)
116
160
 
117
161
  value.to_s.split(",").map(&:strip)
118
162
  end
119
163
 
164
+ # Answers `in`: whether the held value, or any element of a held Array,
165
+ # occurs in the wanted list.
166
+ #
120
167
  # A folded reference hop asks whether the locally-held identity is
121
168
  # among the matching target identities. A has_many relationship holds
122
169
  # several identities, so the same question becomes an intersection:
123
- # does ANY held identity occur in the wanted set? Scalar `in` retains
170
+ # does any held identity occur in the wanted set? Scalar `in` retains
124
171
  # its existing one-candidate behavior.
172
+ #
173
+ # @param held [Array, Object] the record's own value; an Array contributes each
174
+ # element as a candidate, anything else is the single candidate
175
+ # @param want [Array, String, Object] the wanted set, read through `members`
176
+ # @return [Boolean] whether any candidate, unwrapped through `comparable` and
177
+ # compared as a String, is a wanted member
125
178
  def any_member_in?(held, want)
126
179
  wanted = members(want)
127
180
  candidates = held.is_a?(Array) ? held : [held]
@@ -129,24 +182,35 @@ module Hecks
129
182
  candidates.any? { |candidate| wanted.include?(comparable(candidate).to_s) }
130
183
  end
131
184
 
132
- # `contains` means two different things depending on what is HELD —
133
- # real ELEMENT membership for a `list_of` field (a genuine Array
185
+ # Answers `contains`: element membership for a held Array, substring
186
+ # for anything else.
187
+ #
188
+ # `contains` means two different things depending on what is held —
189
+ # real element membership for a `list_of` field (a genuine Array
134
190
  # arrives already, one element one member, nothing to split), and
135
- # plain SUBSTRING for anything else. It used to fall through to
136
- # `members`' comma-split for the scalar case too, silently reading
137
- # a free-text field's own comma as a separator — which the SQL
138
- # side's `instr`/`position` never did, so the two disagreed the
139
- # moment a scalar's real content held a comma. Matching SQL's
191
+ # plain substring for anything else. The scalar case deliberately
192
+ # stays out of `members`' comma-split: that silently reads a
193
+ # free-text field's own comma as a separator — which the SQL
194
+ # side's `instr`/`position` never does, so the two disagree the
195
+ # moment a scalar's real content holds a comma. Matching SQL's
140
196
  # substring reading here keeps every engine answering `contains`
141
197
  # identically for the same declared field.
198
+ #
199
+ # @param held [Array, Object] the record's own value; anything but an Array is
200
+ # read through `to_s`
201
+ # @param want [Object] the element or substring looked for, compared as its `to_s`
202
+ # @return [Boolean] whether `held` has `want` as a member (Array) or a substring
142
203
  def contains?(held, want)
143
204
  return members(held).include?(want.to_s) if held.is_a?(Array)
144
205
 
145
206
  held.to_s.include?(want.to_s)
146
207
  end
147
208
 
148
- # A CROSS-AGGREGATE ANTI-JOIN `where ref: { none_in_state:
149
- # "Claim:held" }` holds when NO record in the named aggregate,
209
+ # Answers `none_in_state` by looking the held identity up in another
210
+ # aggregate's repository and reading that record's state.
211
+ #
212
+ # **A cross-aggregate anti-join** — `where ref: { none_in_state:
213
+ # "Claim:held" }` holds when no record in the named aggregate,
150
214
  # keyed by this record's own field value, is in the named state.
151
215
  # No registry — no way to look the target up — reads as "not
152
216
  # excluded", the same graceful default a missing record already
@@ -154,6 +218,17 @@ module Hecks
154
218
  # loaded domain; ambiguity (two domains declaring one name) picks
155
219
  # the first match rather than refusing, since a where-clause never
156
220
  # raises (see `ordered?`).
221
+ #
222
+ # @param held [Object, nil] this record's own field value, used as the target
223
+ # record's identity
224
+ # @param want [String, Symbol] `"Aggregate:state"` — the target aggregate's bare
225
+ # name and the excluded state, split on the first colon
226
+ # @param registry [Runtime::Registry, nil] the booted registry to find the target
227
+ # aggregate and its repository in
228
+ # @return [Boolean] `false` only when the target record exists and its lifecycle
229
+ # field (or `:state`, absent a lifecycle) equals the named state; `true` when
230
+ # there is no registry, no such aggregate or no such record
231
+ # @raise [Runtime::WiringError] if the target aggregate's repository cannot be wired
157
232
  def none_in_state?(held, want, registry)
158
233
  return true unless registry
159
234
 
@@ -165,9 +240,42 @@ module Hecks
165
240
  record = registry.repository(target_domain, target_ir).find(held)
166
241
  return true unless record
167
242
 
168
- comparable(record.state[:state]) != state
243
+ # The field a state lives on, read from the target's own
244
+ # declaration — not assumed to be literally named `state`. Every
245
+ # `none_in_state` fixture this comparator originally shipped
246
+ # with (spec/query_none_in_state_*_spec.rb) happens to declare a
247
+ # plain `attribute :state, ...` rather than a real `lifecycle`,
248
+ # which is how the previous hardcoded `record.state[:state]`
249
+ # passed every one of them while being wrong for the shape this
250
+ # whole comparator exists to answer about: a real state machine.
251
+ # `lifecycle :field, default: ... do ... end` stores its state
252
+ # under `field` (`Instance#assign_creation_attributes`'s own
253
+ # `state[aggregate.lifecycle.field.to_sym] = ...`), and this
254
+ # codebase's own convention overwhelmingly names that field
255
+ # `status`, not `state` (`QualityControl::Clearance`'s own
256
+ # `lifecycle :status` among many others) — so the hardcoded key
257
+ # silently read `nil` from every real lifecycle-backed target,
258
+ # comparable(nil) != state was true unconditionally, and
259
+ # `none_in_state` against any lifecycle aggregate answered
260
+ # "not excluded" for every row, always, no matter its actual
261
+ # state. Found chasing `QualityControl::Bug.AwaitingClearance`
262
+ # (qa/bluebook/quality_control.bluebook), which is exactly this
263
+ # shape: `Clearance:green`/`Clearance:red` against a `lifecycle
264
+ # :status` aggregate. Falls back to `:state` when the target
265
+ # declares no lifecycle at all, so every existing fixture (a
266
+ # plain attribute literally named `state`) keeps answering
267
+ # exactly as before.
268
+ field = target_ir.lifecycle&.field || :state
269
+ comparable(record.state[field]) != state
169
270
  end
170
271
 
272
+ # Searches every loaded domain for an aggregate by its bare name, taking
273
+ # the first match in the registry's load order.
274
+ #
275
+ # @param registry [Runtime::Registry] the booted registry whose bluebooks are searched
276
+ # @param name [String, nil] the aggregate's bare `hecks_name`, such as `"Claim"`
277
+ # @return [Array(String, Bluebook::Aggregate), nil] the owning domain's name and the
278
+ # aggregate; `nil` when no loaded domain declares one by that name
171
279
  def find_aggregate_by_name(registry, name)
172
280
  registry.bluebooks.each do |domain, bluebook|
173
281
  aggregate = bluebook.aggregates.find { |a| a.hecks_name == name }