hecks 2.8.0 → 3.0.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 (886) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +50 -7
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +32 -100
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +10 -32
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  25. data/lib/hecks/adapters/driven/postgres.rb +149 -136
  26. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  27. data/lib/hecks/adapters/driven/prism.rb +48 -38
  28. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  29. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  30. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  31. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  32. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  33. data/lib/hecks/adapters/driven/tenant_provisioner.rb +76 -62
  34. data/lib/hecks/adapters/driven.rb +5 -36
  35. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  36. data/lib/hecks/behaviors/dsl.rb +7 -25
  37. data/lib/hecks/behaviors/expectations.rb +32 -142
  38. data/lib/hecks/behaviors/ir.rb +7 -20
  39. data/lib/hecks/behaviors/rspec.rb +6 -17
  40. data/lib/hecks/behaviors/runner.rb +10 -34
  41. data/lib/hecks/behaviors.rb +4 -8
  42. data/lib/hecks/bench/cli.rb +62 -0
  43. data/lib/hecks/bench/environment.rb +71 -0
  44. data/lib/hecks/bench/postgres_probe.rb +30 -0
  45. data/lib/hecks/bench/report.rb +76 -0
  46. data/lib/hecks/bench/ruby_runner.rb +93 -0
  47. data/lib/hecks/bench/run.rb +55 -0
  48. data/lib/hecks/bench/rust_runner.rb +101 -0
  49. data/lib/hecks/bench/stats.rb +46 -0
  50. data/lib/hecks/bench/suite.rb +98 -0
  51. data/lib/hecks/bench/workload.rb +89 -0
  52. data/lib/hecks/bench.rb +17 -0
  53. data/lib/hecks/bluebook/aggregate.rb +9 -75
  54. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  55. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  56. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  57. data/lib/hecks/bluebook/assembly/contracts.rb +56 -174
  58. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  59. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  60. data/lib/hecks/bluebook/assembly.rb +6 -45
  61. data/lib/hecks/bluebook/attribute.rb +12 -61
  62. data/lib/hecks/bluebook/behaviour/aggregate.rb +39 -39
  63. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  64. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  65. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  66. data/lib/hecks/bluebook/behaviour/domain_port.rb +11 -20
  67. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  68. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  69. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  70. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  71. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  73. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  75. data/lib/hecks/bluebook/capabilities.rb +17 -49
  76. data/lib/hecks/bluebook/chapter.rb +16 -50
  77. data/lib/hecks/bluebook/command.rb +23 -81
  78. data/lib/hecks/bluebook/domain_port.rb +49 -67
  79. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  80. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  81. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  82. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  83. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +96 -708
  84. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +63 -169
  85. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +3 -1
  86. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  87. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  88. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +42 -89
  89. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  90. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  91. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +148 -197
  92. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  93. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  94. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  95. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  96. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  97. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  98. data/lib/hecks/bluebook/dsl/query_builder.rb +38 -43
  99. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  100. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  101. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  102. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  103. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  104. data/lib/hecks/bluebook/dsl/world_builder.rb +46 -111
  105. data/lib/hecks/bluebook/entity.rb +13 -71
  106. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  107. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  108. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  109. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  110. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  111. data/lib/hecks/bluebook/expression/resolver.rb +39 -661
  112. data/lib/hecks/bluebook/hexagon.rb +18 -33
  113. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  114. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  115. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  116. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  117. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  118. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  119. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -176
  120. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  121. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -306
  122. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  123. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  124. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  125. data/lib/hecks/bluebook/meta_validator.rb +87 -352
  126. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  127. data/lib/hecks/bluebook/model_check.rb +122 -596
  128. data/lib/hecks/bluebook/pattern_subset.rb +49 -69
  129. data/lib/hecks/bluebook/process_manager.rb +13 -83
  130. data/lib/hecks/bluebook/project_loader.rb +3 -39
  131. data/lib/hecks/bluebook/project_register.rb +12 -60
  132. data/lib/hecks/bluebook/query.rb +27 -51
  133. data/lib/hecks/bluebook/read_model.rb +57 -81
  134. data/lib/hecks/bluebook/reference.rb +7 -41
  135. data/lib/hecks/bluebook/smoke_test.rb +9 -99
  136. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  137. data/lib/hecks/bluebook/translation.rb +26 -62
  138. data/lib/hecks/bluebook/value_object.rb +11 -36
  139. data/lib/hecks/bluebook.rb +5 -22
  140. data/lib/hecks/cache_dir.rb +4 -23
  141. data/lib/hecks/canonical_json.rb +30 -0
  142. data/lib/hecks/chapters.rb +102 -0
  143. data/lib/hecks/cli/behaviors.rb +61 -0
  144. data/lib/hecks/cli/canonicalise.rb +25 -0
  145. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  146. data/lib/hecks/cli/check_era.rb +88 -0
  147. data/lib/hecks/cli/console.rb +66 -0
  148. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  149. data/lib/hecks/cli/doc_coverage.rb +32 -0
  150. data/lib/hecks/cli/document.rb +5 -23
  151. data/lib/hecks/cli/expression_projection.rb +41 -0
  152. data/lib/hecks/cli/follow.rb +88 -0
  153. data/lib/hecks/cli/history.rb +44 -0
  154. data/lib/hecks/cli/ir.rb +4 -6
  155. data/lib/hecks/cli/mcp.rb +5 -6
  156. data/lib/hecks/cli/model_check.rb +24 -88
  157. data/lib/hecks/cli/pattern_cases.rb +47 -0
  158. data/lib/hecks/cli/present.rb +39 -0
  159. data/lib/hecks/cli/project_cli.rb +223 -64
  160. data/lib/hecks/cli/project_diagrams.rb +4 -10
  161. data/lib/hecks/cli/project_glossary.rb +44 -0
  162. data/lib/hecks/cli/reference.rb +46 -0
  163. data/lib/hecks/cli/refresh_projections.rb +44 -0
  164. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  165. data/lib/hecks/cli/release.rb +70 -0
  166. data/lib/hecks/cli/release_gem.rb +59 -0
  167. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  168. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  169. data/lib/hecks/cli/run.rb +15 -61
  170. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  171. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  172. data/lib/hecks/cli/shape.rb +70 -0
  173. data/lib/hecks/cli/smoke_http.rb +230 -0
  174. data/lib/hecks/cli/smoke_test.rb +7 -19
  175. data/lib/hecks/cli/spec_example.rb +27 -0
  176. data/lib/hecks/cli/statements.rb +32 -0
  177. data/lib/hecks/cli/stores.rb +4 -6
  178. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  179. data/lib/hecks/cli.rb +216 -73
  180. data/lib/hecks/codemod.rb +324 -0
  181. data/lib/hecks/construct.rb +8 -34
  182. data/lib/hecks/corpus.rb +423 -0
  183. data/lib/hecks/deploy/bluebook/deploy.bluebook +617 -158
  184. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -11
  185. data/lib/hecks/deploy/oidc.json +82 -2
  186. data/lib/hecks/doc/reference.rb +351 -0
  187. data/lib/hecks/doors/cli_door.rb +196 -0
  188. data/lib/hecks/doors/cli_runner.rb +297 -0
  189. data/lib/hecks/{facade → doors}/command_request.rb +14 -34
  190. data/lib/hecks/doors/handle.rb +206 -0
  191. data/lib/hecks/doors/json_door.rb +132 -0
  192. data/lib/hecks/doors/launcher_options.rb +129 -0
  193. data/lib/hecks/{cli → doors}/mcp_door.rb +25 -120
  194. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  195. data/lib/hecks/doors/ruby_door/aggregate_door.rb +162 -0
  196. data/lib/hecks/doors/ruby_door/chapter.rb +72 -0
  197. data/lib/hecks/doors/ruby_door.rb +77 -0
  198. data/lib/hecks/doors.rb +61 -0
  199. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  200. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  201. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  202. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  203. data/lib/hecks/embryonaut_bluebook.rb +5 -72
  204. data/lib/hecks/engine_agreement.rb +156 -0
  205. data/lib/hecks/facade.rb +23 -51
  206. data/lib/hecks/forms/app.rb +21 -76
  207. data/lib/hecks/forms/banking_presentation.rb +50 -0
  208. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  209. data/lib/hecks/forms/field_renderer.rb +17 -156
  210. data/lib/hecks/forms/field_shape.rb +31 -222
  211. data/lib/hecks/forms/html.rb +12 -47
  212. data/lib/hecks/forms/index_renderer.rb +1 -4
  213. data/lib/hecks/forms/page.rb +3 -9
  214. data/lib/hecks/forms/params.rb +14 -60
  215. data/lib/hecks/forms/port_argument.rb +2 -19
  216. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  217. data/lib/hecks/forms/record_renderer.rb +8 -75
  218. data/lib/hecks/forms/record_table.rb +7 -50
  219. data/lib/hecks/forms/reference_options.rb +3 -30
  220. data/lib/hecks/forms/value_object_shape.rb +8 -35
  221. data/lib/hecks/forms.rb +5 -33
  222. data/lib/hecks/fqn.rb +5 -12
  223. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  224. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  225. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  226. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  227. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  228. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  229. data/lib/hecks/framework.rb +11 -80
  230. data/lib/hecks/freezer.rb +5 -39
  231. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  232. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  233. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  234. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  235. data/lib/hecks/fuzzing/differential.rb +156 -0
  236. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  237. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  238. data/lib/hecks/fuzzing/form_census.rb +136 -0
  239. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  240. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  241. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  242. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  243. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  244. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  245. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  246. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  247. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  248. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  249. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  250. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  251. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  252. data/lib/hecks/fuzzing/properties.rb +162 -0
  253. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  254. data/lib/hecks/fuzzing/replay.rb +505 -0
  255. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  256. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  257. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  258. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  259. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  260. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  261. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  262. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  263. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  264. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  265. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  266. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  267. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  268. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  269. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  270. data/lib/hecks/fuzzing.rb +23 -0
  271. data/lib/hecks/grammar/evolve.rb +336 -0
  272. data/lib/hecks/grammar/expression.bluebook +15 -40
  273. data/lib/hecks/grammar/translation.bluebook +3 -6
  274. data/lib/hecks/grammar.rb +16 -41
  275. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  276. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  277. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  278. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  279. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  280. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  281. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  282. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  283. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  284. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  285. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  286. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  287. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  288. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  289. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  290. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  291. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  292. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  293. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  294. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  295. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  296. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  297. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  298. data/lib/hecks/hecks/adapters/git.rb +102 -0
  299. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  300. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  301. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  302. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  303. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  304. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  305. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  306. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  307. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  308. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  309. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  310. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  311. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  312. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  313. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  314. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  315. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  316. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  317. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  318. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  319. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  320. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  321. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  322. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  323. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  324. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  325. data/lib/hecks/hecks/codebase.behaviors +172 -0
  326. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  327. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  328. data/lib/hecks/hecks/custodian.behaviors +179 -0
  329. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  330. data/lib/hecks/hecks/deploy.behaviors +74 -0
  331. data/lib/hecks/hecks/deploy.world +12 -0
  332. data/lib/hecks/hecks/environments/memory.world +10 -0
  333. data/lib/hecks/hecks/governance.world +7 -0
  334. data/lib/hecks/hecks/hecks.behaviors +62 -0
  335. data/lib/hecks/hecks/hecks.bluebook +124 -0
  336. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  337. data/lib/hecks/hecks/hecks.world +42 -0
  338. data/lib/hecks/ir.rb +14 -83
  339. data/lib/hecks/kernel_capabilities.rb +194 -0
  340. data/lib/hecks/language/adapter.bluebook +2 -7
  341. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  342. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  343. data/lib/hecks/language/bluebook/bluebook.bluebook +26 -83
  344. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  345. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  346. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  347. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  348. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  349. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  350. data/lib/hecks/language/bluebook/query.bluebook +19 -43
  351. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  352. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  353. data/lib/hecks/language/bluebook/vocabulary.bluebook +83 -198
  354. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  355. data/lib/hecks/language/hecksagon/domain_port.bluebook +14 -19
  356. data/lib/hecks/language/hecksagon/hecksagon.bluebook +26 -25
  357. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  358. data/lib/hecks/language/oidc.json +35 -0
  359. data/lib/hecks/language/port.bluebook +4 -17
  360. data/lib/hecks/language/translation/translation.bluebook +3 -9
  361. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  362. data/lib/hecks/language/world/wiring.bluebook +1 -3
  363. data/lib/hecks/language/world/world.bluebook +5 -7
  364. data/lib/hecks/literal.rb +8 -50
  365. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  366. data/lib/hecks/naming.rb +29 -137
  367. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  368. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  369. data/lib/hecks/ports/access_control.rb +7 -23
  370. data/lib/hecks/ports/agent/answers.rb +5 -23
  371. data/lib/hecks/ports/agent.rb +47 -172
  372. data/lib/hecks/ports/authentication.rb +12 -39
  373. data/lib/hecks/ports/authorization.rb +16 -43
  374. data/lib/hecks/ports/clock.rb +5 -45
  375. data/lib/hecks/ports/extraction.rb +7 -12
  376. data/lib/hecks/ports/identity_assignment.rb +8 -29
  377. data/lib/hecks/ports/identity_generation.rb +6 -29
  378. data/lib/hecks/ports/identity_resolution.rb +2 -10
  379. data/lib/hecks/ports/key_vault.rb +9 -45
  380. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  381. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  382. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  383. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  384. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  385. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +44 -144
  386. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  387. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +20 -85
  388. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  389. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +18 -24
  390. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  391. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  392. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  393. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  394. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  395. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  396. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  397. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  398. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +19 -76
  399. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  400. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  401. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  402. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  403. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  404. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +43 -127
  405. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  406. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +131 -555
  407. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  408. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  409. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  410. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  411. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  412. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  413. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  414. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +50 -47
  415. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  416. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  417. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  418. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  419. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  420. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  421. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  422. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  423. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  424. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  425. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  426. data/lib/hecks/ports/persistence/state_codec.rb +47 -108
  427. data/lib/hecks/ports/persistence.rb +7 -14
  428. data/lib/hecks/ports/projection.rb +42 -84
  429. data/lib/hecks/ports/query/in_memory.rb +10 -58
  430. data/lib/hecks/ports/query/ordering.rb +11 -32
  431. data/lib/hecks/ports/query.rb +14 -37
  432. data/lib/hecks/ports.rb +2 -8
  433. data/lib/hecks/projection_files.rb +285 -0
  434. data/lib/hecks/projections/bootstrap_table.rb +12 -44
  435. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  436. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  437. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  438. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  439. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  440. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  441. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  442. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  443. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  444. data/lib/hecks/projections/deploy/fargate.rb +40 -207
  445. data/lib/hecks/projections/deploy/lambda.rb +191 -708
  446. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  447. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  448. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  449. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  450. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  451. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  452. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  453. data/lib/hecks/projections/deploy/preview/template.rb +3 -59
  454. data/lib/hecks/projections/deploy/preview.rb +2 -67
  455. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  456. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  457. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  458. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  459. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  460. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  461. data/lib/hecks/projections/deploy/shared.rb +48 -147
  462. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  463. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  464. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  465. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  466. data/lib/hecks/projections/diagrams.rb +22 -584
  467. data/lib/hecks/projections/glossary/html.rb +14 -130
  468. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  469. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  470. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  471. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  472. data/lib/hecks/projections/glossary.rb +30 -222
  473. data/lib/hecks/projections/ir.rb +3 -10
  474. data/lib/hecks/projections/model/deviations.rb +16 -43
  475. data/lib/hecks/projections/model.rb +12 -51
  476. data/lib/hecks/projections/oidc.rb +12 -56
  477. data/lib/hecks/projections/parser_table.rb +13 -32
  478. data/lib/hecks/projections/reference.rb +4 -18
  479. data/lib/hecks/projections/rust_vocabulary.rb +13 -251
  480. data/lib/hecks/projections/shape.rb +3 -29
  481. data/lib/hecks/projections/statements.rb +7 -86
  482. data/lib/hecks/projections/vocabulary.rb +6 -22
  483. data/lib/hecks/projections.rb +2 -16
  484. data/lib/hecks/projector/cli_projector.rb +154 -400
  485. data/lib/hecks/projector/docs_projector.rb +10 -201
  486. data/lib/hecks/projector/exporter.rb +42 -134
  487. data/lib/hecks/projector/ir_projector.rb +2 -9
  488. data/lib/hecks/projector/narrate_projector.rb +15 -189
  489. data/lib/hecks/projector/target.rb +13 -76
  490. data/lib/hecks/projector.rb +30 -121
  491. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  492. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  493. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  494. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  495. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  496. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  497. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  498. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  499. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  500. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  501. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  502. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  503. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  504. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  505. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  506. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  507. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  508. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  509. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  510. data/lib/hecks/quality_control/cli/child.rb +45 -0
  511. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  512. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  513. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  514. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  515. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  516. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  517. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  518. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  519. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  520. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  521. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  522. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  523. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  524. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  525. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  526. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  527. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  528. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  529. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  530. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  531. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  532. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  533. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  534. data/lib/hecks/query_ir.rb +346 -0
  535. data/lib/hecks/query_ir_mcp.rb +198 -0
  536. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  537. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  538. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  539. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  540. data/lib/hecks/query_specification/common/options.rb +2 -6
  541. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  542. data/lib/hecks/query_specification/field_path.rb +37 -77
  543. data/lib/hecks/query_specification/hop_path.rb +12 -76
  544. data/lib/hecks/release/gem_pin.rb +2 -26
  545. data/lib/hecks/release/runner/ci_publisher.rb +5 -8
  546. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  547. data/lib/hecks/release/runner/commands.rb +2 -4
  548. data/lib/hecks/release/runner/console.rb +6 -0
  549. data/lib/hecks/release/runner/gem_publisher.rb +15 -9
  550. data/lib/hecks/release/runner/npm_publisher.rb +9 -53
  551. data/lib/hecks/release/runner/preflight.rb +29 -20
  552. data/lib/hecks/release/runner/published.rb +8 -26
  553. data/lib/hecks/release/runner/tagger.rb +31 -23
  554. data/lib/hecks/release/runner.rb +60 -38
  555. data/lib/hecks/rendering.rb +4 -26
  556. data/lib/hecks/router/namespace_installer.rb +15 -39
  557. data/lib/hecks/router.rb +4 -8
  558. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  559. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  560. data/lib/hecks/runtime/boot_gates.rb +3 -18
  561. data/lib/hecks/runtime/caller.rb +19 -59
  562. data/lib/hecks/runtime/capability_graph.rb +5 -21
  563. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  564. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +27 -160
  565. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  566. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  567. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  568. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  569. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  570. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  571. data/lib/hecks/runtime/dependency_planning.rb +50 -104
  572. data/lib/hecks/runtime/dispatcher.rb +134 -243
  573. data/lib/hecks/runtime/entity_element.rb +40 -462
  574. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  575. data/lib/hecks/runtime/errors.rb +32 -115
  576. data/lib/hecks/runtime/event.rb +3 -25
  577. data/lib/hecks/runtime/identity.rb +18 -154
  578. data/lib/hecks/runtime/instance.rb +18 -150
  579. data/lib/hecks/runtime/interpreting.rb +14 -70
  580. data/lib/hecks/runtime/invocation.rb +28 -164
  581. data/lib/hecks/runtime/loader.rb +109 -194
  582. data/lib/hecks/runtime/outbox.rb +39 -262
  583. data/lib/hecks/runtime/policy_interpreter.rb +43 -202
  584. data/lib/hecks/runtime/port_operation_interpreter.rb +28 -97
  585. data/lib/hecks/runtime/query_interpreter.rb +122 -187
  586. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  587. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  588. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  589. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  590. data/lib/hecks/runtime/reference_hop.rb +8 -81
  591. data/lib/hecks/runtime/refusal_wording.rb +28 -87
  592. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  593. data/lib/hecks/runtime/registry/verification.rb +213 -227
  594. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  595. data/lib/hecks/runtime/registry.rb +156 -247
  596. data/lib/hecks/runtime/remote_dispatcher.rb +68 -154
  597. data/lib/hecks/runtime/routing.rb +4 -15
  598. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  599. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  600. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  601. data/lib/hecks/runtime/tenant_check.rb +6 -68
  602. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  603. data/lib/hecks/runtime/value/admission.rb +16 -67
  604. data/lib/hecks/runtime/value/coercion.rb +162 -760
  605. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  606. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  607. data/lib/hecks/runtime/value.rb +15 -84
  608. data/lib/hecks/runtime.rb +35 -69
  609. data/lib/hecks/rust_build/conformance.rb +158 -0
  610. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  611. data/lib/hecks/rust_build/coverage.rb +218 -0
  612. data/lib/hecks/rust_build/native_build.rb +124 -0
  613. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  614. data/lib/hecks/rust_build/project_rust.rb +332 -0
  615. data/lib/hecks/rust_build/wasm.rb +105 -0
  616. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  617. data/lib/hecks/rust_build.rb +144 -0
  618. data/lib/hecks/storehouse.rb +155 -582
  619. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +10 -34
  620. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  621. data/lib/hecks/three_zero/forms.yml +127 -0
  622. data/lib/hecks/three_zero.rb +12 -0
  623. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  624. data/lib/hecks/tools/comment_style.rb +1070 -0
  625. data/lib/hecks/tools/corpus_report.rb +87 -0
  626. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  627. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  628. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  629. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  630. data/lib/hecks/tools/era_reattest.rb +146 -0
  631. data/lib/hecks/tools/evolve_run.rb +189 -0
  632. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  633. data/lib/hecks/tools/heki_compaction.rb +100 -0
  634. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  635. data/lib/hecks/tools/journal_compaction.rb +89 -0
  636. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  637. data/lib/hecks/tools/query_ir_run.rb +81 -0
  638. data/lib/hecks/tools/regeneration_run.rb +166 -0
  639. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  640. data/lib/hecks/tools/sequence_script.rb +62 -0
  641. data/lib/hecks/tools/tail_merge.rb +117 -0
  642. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  643. data/lib/hecks/tools/translation_audit.rb +208 -0
  644. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  645. data/lib/hecks/tools.rb +86 -0
  646. data/lib/hecks/vendoring/git_environment.rb +2 -8
  647. data/lib/hecks/vendoring/git_source.rb +4 -10
  648. data/lib/hecks/vendoring.rb +7 -46
  649. data/lib/hecks/version.rb +3 -14
  650. data/lib/hecks.rb +52 -90
  651. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  652. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  653. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  654. data/qa/settings.yml +66 -0
  655. data/rust/.cargo/config.toml +6 -0
  656. data/rust/.gitignore +2 -0
  657. data/rust/Cargo.lock +54 -0
  658. data/rust/Cargo.toml +61 -0
  659. data/rust/build/.cargo/config.toml +3 -0
  660. data/rust/build/.gitignore +1 -0
  661. data/rust/build/Cargo.lock +7 -0
  662. data/rust/build/Cargo.toml +19 -0
  663. data/rust/build/src/build_artifact.rs +47 -0
  664. data/rust/build/src/cargo_sync.rs +248 -0
  665. data/rust/build/src/json.rs +421 -0
  666. data/rust/build/src/lineage_pass.rs +285 -0
  667. data/rust/build/src/main.rs +51 -0
  668. data/rust/build/src/optional_pass.rs +150 -0
  669. data/rust/build/src/pipeline.rs +264 -0
  670. data/rust/build/src/reserved_names.rs +76 -0
  671. data/rust/build/src/resolve.rs +124 -0
  672. data/rust/build/src/root.rs +25 -0
  673. data/rust/build/src/sidecars.rs +73 -0
  674. data/rust/build/src/subprocess.rs +51 -0
  675. data/rust/build/src/tmp.rs +31 -0
  676. data/rust/codegen/.cargo/config.toml +3 -0
  677. data/rust/codegen/.gitignore +1 -0
  678. data/rust/codegen/Cargo.lock +7 -0
  679. data/rust/codegen/Cargo.toml +20 -0
  680. data/rust/codegen/src/attr.rs +32 -0
  681. data/rust/codegen/src/bridging.rs +481 -0
  682. data/rust/codegen/src/commands.rs +1226 -0
  683. data/rust/codegen/src/constraints.rs +109 -0
  684. data/rust/codegen/src/dependency_planning.rs +209 -0
  685. data/rust/codegen/src/domain_generator.rs +1288 -0
  686. data/rust/codegen/src/exemplar.rs +230 -0
  687. data/rust/codegen/src/expr_emitter.rs +87 -0
  688. data/rust/codegen/src/fielded.rs +192 -0
  689. data/rust/codegen/src/hecks_naming.rs +33 -0
  690. data/rust/codegen/src/json.rs +335 -0
  691. data/rust/codegen/src/json_codec.rs +697 -0
  692. data/rust/codegen/src/literal.rs +207 -0
  693. data/rust/codegen/src/main.rs +290 -0
  694. data/rust/codegen/src/manifest.rs +227 -0
  695. data/rust/codegen/src/mutations.rs +784 -0
  696. data/rust/codegen/src/naming.rs +286 -0
  697. data/rust/codegen/src/ports.rs +208 -0
  698. data/rust/codegen/src/prelude.rs +116 -0
  699. data/rust/codegen/src/queries.rs +786 -0
  700. data/rust/codegen/src/reactions.rs +532 -0
  701. data/rust/codegen/src/read_models.rs +697 -0
  702. data/rust/codegen/src/reference_specs.rs +40 -0
  703. data/rust/codegen/src/registry.rs +966 -0
  704. data/rust/codegen/src/reserved_names.rs +76 -0
  705. data/rust/codegen/src/shared.rs +93 -0
  706. data/rust/codegen/src/skip_reason.rs +25 -0
  707. data/rust/codegen/src/types.rs +362 -0
  708. data/rust/host/.cargo/config.toml +3 -0
  709. data/rust/host/.gitignore +1 -0
  710. data/rust/host/Cargo.lock +4297 -0
  711. data/rust/host/Cargo.toml +61 -0
  712. data/rust/host/HECKS_RELEASE +1 -0
  713. data/rust/host/rds-ca-bundle.pem +2736 -0
  714. data/rust/host/src/api.rs +1596 -0
  715. data/rust/host/src/approval.rs +652 -0
  716. data/rust/host/src/auth.rs +1287 -0
  717. data/rust/host/src/bin/expr_harness.rs +46 -0
  718. data/rust/host/src/bin/lineage_harness.rs +119 -0
  719. data/rust/host/src/bin/mint_harness.rs +135 -0
  720. data/rust/host/src/checkout.rs +265 -0
  721. data/rust/host/src/dispatch.rs +1443 -0
  722. data/rust/host/src/expr_json.rs +1117 -0
  723. data/rust/host/src/field_hints.rs +52 -0
  724. data/rust/host/src/ir.rs +733 -0
  725. data/rust/host/src/journal.rs +874 -0
  726. data/rust/host/src/lambda_client.rs +378 -0
  727. data/rust/host/src/log.rs +147 -0
  728. data/rust/host/src/main.rs +474 -0
  729. data/rust/host/src/mint.rs +1865 -0
  730. data/rust/host/src/payments/keystore.rs +330 -0
  731. data/rust/host/src/payments/tests.rs +1400 -0
  732. data/rust/host/src/payments.rs +767 -0
  733. data/rust/host/src/presentation.rs +698 -0
  734. data/rust/host/src/presentation_write.rs +1431 -0
  735. data/rust/host/src/query_step.rs +149 -0
  736. data/rust/host/src/rate_limit.rs +904 -0
  737. data/rust/host/src/reference_transform.rs +241 -0
  738. data/rust/host/src/reference_validate.rs +338 -0
  739. data/rust/host/src/resend.rs +296 -0
  740. data/rust/host/src/secrets.rs +106 -0
  741. data/rust/host/src/server.rs +663 -0
  742. data/rust/host/src/storage_shape.rs +175 -0
  743. data/rust/host/src/test_pg.rs +40 -0
  744. data/rust/host/src/ui_schema.rs +1351 -0
  745. data/rust/host/src/wasm_runner.rs +191 -0
  746. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  747. data/rust/host/src/web/newsletter/tests.rs +104 -0
  748. data/rust/host/src/web/newsletter.rs +310 -0
  749. data/rust/host/src/web/newsletter_send.rs +357 -0
  750. data/rust/host/src/web/registrations.rs +1505 -0
  751. data/rust/host/src/web.rs +2937 -0
  752. data/rust/lsp/.gitignore +1 -0
  753. data/rust/lsp/Cargo.lock +7 -0
  754. data/rust/lsp/Cargo.toml +16 -0
  755. data/rust/lsp/README.md +133 -0
  756. data/rust/lsp/src/diagnostics.rs +165 -0
  757. data/rust/lsp/src/json.rs +385 -0
  758. data/rust/lsp/src/main.rs +399 -0
  759. data/rust/lsp/src/outline.rs +177 -0
  760. data/rust/lsp/src/rpc.rs +67 -0
  761. data/rust/parser/.cargo/config.toml +5 -0
  762. data/rust/parser/.gitignore +1 -0
  763. data/rust/parser/Cargo.lock +7 -0
  764. data/rust/parser/Cargo.toml +21 -0
  765. data/rust/parser/src/build/closed_sets.rs +27 -0
  766. data/rust/parser/src/build/identity.rs +276 -0
  767. data/rust/parser/src/build/mod.rs +12 -0
  768. data/rust/parser/src/build/naming.rs +141 -0
  769. data/rust/parser/src/build/pattern_subset.rs +161 -0
  770. data/rust/parser/src/build/query_derive.rs +72 -0
  771. data/rust/parser/src/build/query_inference.rs +269 -0
  772. data/rust/parser/src/build/query_options.rs +100 -0
  773. data/rust/parser/src/build/read_model.rs +94 -0
  774. data/rust/parser/src/build/references.rs +32 -0
  775. data/rust/parser/src/canonical.rs +114 -0
  776. data/rust/parser/src/diag.rs +55 -0
  777. data/rust/parser/src/emit.rs +953 -0
  778. data/rust/parser/src/expr/ast_json.rs +147 -0
  779. data/rust/parser/src/expr/evaluator.rs +81 -0
  780. data/rust/parser/src/expr/mod.rs +84 -0
  781. data/rust/parser/src/expr/resolver.rs +497 -0
  782. data/rust/parser/src/ir.rs +387 -0
  783. data/rust/parser/src/keywords.rs +585 -0
  784. data/rust/parser/src/lex.rs +662 -0
  785. data/rust/parser/src/main.rs +252 -0
  786. data/rust/parser/src/parse/aggregate.rs +612 -0
  787. data/rust/parser/src/parse/chapter.rs +517 -0
  788. data/rust/parser/src/parse/command.rs +637 -0
  789. data/rust/parser/src/parse/domain_port.rs +127 -0
  790. data/rust/parser/src/parse/entity.rs +527 -0
  791. data/rust/parser/src/parse/file.rs +67 -0
  792. data/rust/parser/src/parse/hecksagon.rs +231 -0
  793. data/rust/parser/src/parse/lifecycle.rs +162 -0
  794. data/rust/parser/src/parse/mod.rs +1262 -0
  795. data/rust/parser/src/parse/policy.rs +87 -0
  796. data/rust/parser/src/parse/process_manager.rs +305 -0
  797. data/rust/parser/src/parse/query.rs +100 -0
  798. data/rust/parser/src/parse/read_model.rs +214 -0
  799. data/rust/parser/src/parse/value_object.rs +250 -0
  800. data/rust/parser/src/ruby_value.rs +422 -0
  801. data/rust/project/bridging.rb +257 -0
  802. data/rust/project/commands.rb +877 -0
  803. data/rust/project/constraints.rb +86 -0
  804. data/rust/project/dependency_planning.rb +178 -0
  805. data/rust/project/domain_generator.rb +980 -0
  806. data/rust/project/exemplar.rb +148 -0
  807. data/rust/project/expr_emitter.rb +72 -0
  808. data/rust/project/fielded.rb +141 -0
  809. data/rust/project/json_codec.rb +641 -0
  810. data/rust/project/mutations.rb +721 -0
  811. data/rust/project/naming.rb +205 -0
  812. data/rust/project/ports.rb +65 -0
  813. data/rust/project/queries.rb +493 -0
  814. data/rust/project/reactions.rb +317 -0
  815. data/rust/project/read_models.rb +690 -0
  816. data/rust/project/reference_specs.rb +27 -0
  817. data/rust/project/registry.rb +396 -0
  818. data/rust/project/skip_reason.rb +29 -0
  819. data/rust/project/types.rb +225 -0
  820. data/rust/project/write_if_changed.rb +83 -0
  821. data/rust/project.rb +30 -0
  822. data/rust/project_rust_pipeline.rb +383 -0
  823. data/rust/src/exemplar/commands.rs +279 -0
  824. data/rust/src/exemplar/constraints.rs +18 -0
  825. data/rust/src/exemplar/fielded.rs +280 -0
  826. data/rust/src/exemplar/json.rs +277 -0
  827. data/rust/src/exemplar/mod.rs +17 -0
  828. data/rust/src/exemplar/mutations.rs +80 -0
  829. data/rust/src/exemplar/queries.rs +33 -0
  830. data/rust/src/exemplar/reactions.rs +88 -0
  831. data/rust/src/exemplar/read_models.rs +48 -0
  832. data/rust/src/exemplar/registry.rs +166 -0
  833. data/rust/src/exemplar/types.rs +44 -0
  834. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  835. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  836. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  837. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  838. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  839. data/rust/src/kernel/cli.rs +585 -0
  840. data/rust/src/kernel/dispatch.rs +1090 -0
  841. data/rust/src/kernel/expr.rs +458 -0
  842. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  843. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  844. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  845. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  846. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  847. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  848. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  849. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  850. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  851. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  852. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  853. data/rust/src/kernel/expression_operators/text.rs +79 -0
  854. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  855. data/rust/src/kernel/json.rs +779 -0
  856. data/rust/src/kernel/mod.rs +133 -0
  857. data/rust/src/kernel/named_query.rs +219 -0
  858. data/rust/src/kernel/orchestrate.rs +1810 -0
  859. data/rust/src/kernel/pattern.rs +307 -0
  860. data/rust/src/kernel/query_comparators.rs +206 -0
  861. data/rust/src/kernel/query_ordering.rs +103 -0
  862. data/rust/src/kernel/read_model.rs +914 -0
  863. data/rust/src/kernel/reference_lookup.rs +149 -0
  864. data/rust/src/kernel/repository.rs +468 -0
  865. data/rust/src/kernel/routing.rs +612 -0
  866. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  867. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  868. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  869. data/rust/src/kernel/vocab/mod.rs +20 -0
  870. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  871. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  872. data/rust/src/lib.rs +8 -0
  873. data/rust/src/main.rs +16 -0
  874. data/rust/web/.gitignore +1 -0
  875. data/rust/web/Cargo.lock +119 -0
  876. data/rust/web/Cargo.toml +18 -0
  877. data/rust/web/src/lib.rs +8 -0
  878. metadata +478 -12
  879. data/lib/hecks/facade/cli_door.rb +0 -178
  880. data/lib/hecks/facade/cli_runner.rb +0 -271
  881. data/lib/hecks/facade/handle.rb +0 -293
  882. data/lib/hecks/facade/json_door.rb +0 -247
  883. data/lib/hecks/facade/surface/aggregate_door.rb +0 -222
  884. data/lib/hecks/facade/surface/chapter.rb +0 -116
  885. data/lib/hecks/facade/surface.rb +0 -61
  886. data/lib/hecks/mcp_door_scope.rb +0 -176
@@ -0,0 +1,914 @@
1
+ //! Interprets a compiled `report` block (`ReadModelDef`) against a domain's generated
2
+ //! `READ_MODELS` table — the hand-written counterpart to `rust/project/read_models.rb`.
3
+
4
+ use super::refusal_wording::{InvariantViolationGroupByCollisionArgs, NotFoundReadModelReferenceMissingArgs, UnauthorizedTenantRequiredArgs};
5
+ use super::{named_query, query_comparators, query_ordering, repository, AggregateScan, Json, QueryCondition, QueryConditionValue, Refusal};
6
+
7
+ /// A `where` clause that crosses one or more references on the eligible head
8
+ /// (`account/status`, or a longer `member/sponsor/standing` chain via `through`).
9
+ #[derive(Debug, Clone, Copy)]
10
+ pub struct ReferenceHopCondition {
11
+ /// The eligible head's own reference field this hop crosses through first.
12
+ pub via_field: &'static str,
13
+ /// The first hop's target aggregate, domain-qualified ("Banking::Account").
14
+ pub target_aggregate: &'static str,
15
+ /// Further chain steps beyond the first hop, in declared order; `&[]` for a single hop.
16
+ pub through: &'static [HopStep],
17
+ /// The remaining where-clause field, resolved against the chain's final target.
18
+ pub inner_field: &'static str,
19
+ pub inner_comparator: query_comparators::QueryComparator,
20
+ pub inner_value: QueryConditionValue,
21
+ }
22
+
23
+ /// One further step in a `ReferenceHopCondition` chain.
24
+ #[derive(Debug, Clone, Copy)]
25
+ pub struct HopStep {
26
+ pub via_field: &'static str,
27
+ pub target_aggregate: &'static str,
28
+ }
29
+
30
+ /// A reference field on a non-root head's own aggregate that may match back to `target_aggregate`.
31
+ #[derive(Debug, Clone, Copy)]
32
+ pub struct ReferenceField {
33
+ pub target_aggregate: &'static str,
34
+ pub field: &'static str,
35
+ }
36
+
37
+ /// One declared `include`, plus `is_root`/`reference_fields` precomputed at codegen time rather
38
+ /// than re-derived per call. Both are empty/false for the root head, which is fetched directly
39
+ /// by the caller's id argument rather than matched by reference.
40
+ #[derive(Debug, Clone, Copy)]
41
+ pub struct ReadModelHead {
42
+ pub aggregate: &'static str,
43
+ pub as_name: &'static str,
44
+ pub many: bool,
45
+ pub is_root: bool,
46
+ pub reference_fields: &'static [ReferenceField],
47
+ }
48
+
49
+ /// A read model's declared `order_by`, applying to `ReadModelDef::filtered_head` alone — an
50
+ /// alias, not a distinct type, since `named_query.rs` needs the identical shape.
51
+ pub type ReadModelOrderBy = query_ordering::OrderBy;
52
+
53
+ /// A read model's declared `limit N`, applying to `ReadModelDef::filtered_head` alone.
54
+ pub type ReadModelLimit = query_ordering::Limit;
55
+
56
+ /// A read model's declared `offset N`, applying to `ReadModelDef::filtered_head` alone.
57
+ pub type ReadModelOffset = query_ordering::Offset;
58
+
59
+ /// One many-side head's own where/order_by/offset/limit. ADR 0055's `on:` lets a read model
60
+ /// with several many-side heads filter/order/page each independently; a read model with exactly
61
+ /// one many-side head and untargeted options still compiles to a single entry here, unchanged
62
+ /// from the shape this carried before ADR 0055 (`filtered_head`/`conditions`/... lived directly
63
+ /// on `ReadModelDef` then, one per whole read model rather than one per head).
64
+ #[derive(Debug, Clone, Copy)]
65
+ pub struct FilteredHead {
66
+ /// The `ReadModelHead::as_name` this entry's options apply to.
67
+ pub as_name: &'static str,
68
+ pub conditions: &'static [QueryCondition],
69
+ /// `where` clauses that hop through a reference, on this same head.
70
+ pub reference_hop_conditions: &'static [ReferenceHopCondition],
71
+ pub order_by: Option<ReadModelOrderBy>,
72
+ pub offset: Option<ReadModelOffset>,
73
+ pub limit: Option<ReadModelLimit>,
74
+ }
75
+
76
+ /// A compiled `report "X" do ... end` block — the read-model analogue of `QueryDef`.
77
+ /// `verb` is the "Domain.Name" wire string a read-model ask is matched against.
78
+ #[derive(Debug, Clone, Copy)]
79
+ pub struct ReadModelDef {
80
+ pub verb: &'static str,
81
+ /// `None` for a rootless read model (no `reference_to` declared) — every head then reads its
82
+ /// own aggregate's whole table instead of being fetched/matched by a caller-supplied id.
83
+ pub reference_name: Option<&'static str>,
84
+ pub heads: &'static [ReadModelHead],
85
+ /// One entry per many-side head this read model declares where/order_by/offset/limit for
86
+ /// (ADR 0055's `on:`) — `&[]` when none do. At most one entry when the read model has a
87
+ /// single many-side head (the pre-ADR-0055 shape); more than one only when `on:` targets
88
+ /// distinct heads, since an untargeted option is only legal with a single many-side head
89
+ /// (`ReadModelBuilder#seal_query_options` refuses any other combination before this table is
90
+ /// ever built).
91
+ pub filtered_heads: &'static [FilteredHead],
92
+ /// `authorize policy, tenant: :field` — reuses `named_query::TenantAuth` directly; the
93
+ /// tenant-argument check runs at the same point in `run` as it does for a declared query.
94
+ /// `on:` doesn't extend to `tenant:` (a deliberate ADR 0055 scope limit — a read model with
95
+ /// several many-side heads and a declared tenant is refused outright, not attached to one),
96
+ /// so this is only ever `Some` alongside a single many-side head, the same as before.
97
+ pub authorization: Option<named_query::TenantAuth>,
98
+ /// `group_by :field, ...`, applied to the one `many`-side head this read model declares.
99
+ /// A per-read-model generated function rather than data here, because unwrapping a
100
+ /// single-attribute value object needs codegen-time type knowledge `run`'s generic body,
101
+ /// holding already-serialized `Json`, doesn't have.
102
+ pub group_by: Option<fn(Vec<(String, Json)>) -> Result<Json, Refusal>>,
103
+ /// `count` on the one `many`-side head — a bare marker; row-count needs no per-read-model
104
+ /// generated function, unlike `group_by`.
105
+ pub count: bool,
106
+ /// `median :field` — the declared numeric field's median across the one `many`-side head.
107
+ pub median_field: Option<&'static str>,
108
+ /// `sum :field` — the declared Integer field's total (ADR 0078).
109
+ pub sum_field: Option<&'static str>,
110
+ /// `avg :field` — the declared Integer field's mean (ADR 0078).
111
+ pub avg_field: Option<&'static str>,
112
+ /// `min :field` — the declared numeric field's smallest value (ADR 0078).
113
+ pub min_field: Option<&'static str>,
114
+ /// `max :field` — the declared numeric field's largest value (ADR 0078).
115
+ pub max_field: Option<&'static str>,
116
+ /// `percentile :field, at:` — the declared numeric field's value at one interpolated
117
+ /// rank; `median` is the fixed `at: 0.5` case (ADR 0078).
118
+ pub percentile_field: Option<&'static str>,
119
+ pub percentile_at: Option<f64>,
120
+ /// `any :field` — whether any row's declared boolean field is `true` (ADR 0078).
121
+ pub any_field: Option<&'static str>,
122
+ /// `all :field` — whether every row's declared boolean field is `true` (ADR 0078).
123
+ pub all_field: Option<&'static str>,
124
+ }
125
+
126
+ /// Whether a `group_by` leaf is checked for a second row — decided once, by the generator, from
127
+ /// the declaration alone (ADR 0061, decision D1).
128
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
129
+ pub enum LeafCheck {
130
+ /// The key path covers the grouped aggregate's whole identity, so no two
131
+ /// rows can reach one leaf and none is checked.
132
+ IdentityCovered,
133
+ /// Any other key path: two rows reaching one leaf refuse, naming this
134
+ /// read model (its bare declared name).
135
+ RefuseCollision(&'static str),
136
+ }
137
+
138
+ /// One level of nesting per declared `group_by` field. Grouping is a Vec-based linear scan, not
139
+ /// a `HashMap`, because `Json` has no `Hash` impl and groups must stay in first-occurrence order
140
+ /// (matching Ruby's `Hash#group_by`) so both runtimes report the same first collision.
141
+ pub fn nest(rows: Vec<Json>, fields: &[&str], check: LeafCheck) -> Result<Json, Refusal> {
142
+ nest_level(rows, fields, fields, check, &[])
143
+ }
144
+
145
+ /// One level of `nest`. `all_fields` is the whole declared `group_by`
146
+ /// (the refusal names it); `reached` is the key path walked so far, one
147
+ /// `(field, key)` per level above this one.
148
+ fn nest_level(rows: Vec<Json>, all_fields: &[&str], fields: &[&str], check: LeafCheck, reached: &[(&str, Json)]) -> Result<Json, Refusal> {
149
+ let (field, rest) = fields.split_first().expect("nest called with empty fields — read_model_skip_reason should refuse a group_by with none");
150
+ let mut groups: Vec<(Json, Vec<Json>)> = Vec::new();
151
+ for row in rows {
152
+ let (key, stripped) = match row {
153
+ Json::Object(pairs) => {
154
+ let key = pairs.iter().find(|(k, _)| k == field).map(|(_, v)| v.clone()).unwrap_or(Json::Null);
155
+ let stripped = Json::Object(pairs.into_iter().filter(|(k, _)| k != field).collect());
156
+ (key, stripped)
157
+ }
158
+ other => (Json::Null, other),
159
+ };
160
+ match groups.iter_mut().find(|(k, _)| *k == key) {
161
+ Some((_, group)) => group.push(stripped),
162
+ None => groups.push((key, vec![stripped])),
163
+ }
164
+ }
165
+ let mut out = Vec::with_capacity(groups.len());
166
+ for (key, group) in groups {
167
+ let mut path = reached.to_vec();
168
+ path.push((field, key.clone()));
169
+ let value = if rest.is_empty() { leaf(group, all_fields, check, &path)? } else { nest_level(group, all_fields, rest, check, &path)? };
170
+ // A non-String key must stringify the same way Ruby's `Hash#[]=` does when used as a
171
+ // JSON object key.
172
+ out.push((super::query_comparators::to_s(&key), value));
173
+ }
174
+ Ok(Json::Object(out))
175
+ }
176
+
177
+ /// The one row a `group_by` key path reached, or the `group_by_collision` refusal when a
178
+ /// checked path was reached by more than one.
179
+ fn leaf(group: Vec<Json>, all_fields: &[&str], check: LeafCheck, path: &[(&str, Json)]) -> Result<Json, Refusal> {
180
+ let read_model = match check {
181
+ LeafCheck::RefuseCollision(read_model) if group.len() > 1 => read_model,
182
+ _ => return Ok(group.into_iter().next().unwrap_or(Json::Null)),
183
+ };
184
+ let ids: Vec<String> = group.iter().map(|row| row.get("id").map(super::query_comparators::to_s).unwrap_or_default()).collect();
185
+ let id_refs: Vec<&str> = ids.iter().map(String::as_str).collect();
186
+ let key = path.iter().map(|(field, value)| format!("{field} = {}", super::query_comparators::to_s(value))).collect::<Vec<_>>().join(", ");
187
+ Err(Refusal::InvariantViolation(
188
+ InvariantViolationGroupByCollisionArgs { read_model, fields: all_fields, ids: &id_refs, key: &key }.render_args(),
189
+ ))
190
+ }
191
+
192
+ /// The declared field's value at one interpolated rank (`at`, `0.0`..`1.0`) across `rows` —
193
+ /// linear interpolation between the two closest ranks, matching SQL's `PERCENTILE_CONT`
194
+ /// (ADR 0078). Landing exactly on a rank returns that value unconverted; interpolating
195
+ /// between two returns a `Json::Float` (matching Ruby's forced float division). An empty
196
+ /// collection is `Json::Null`, not zero, so a caller cannot mistake "nothing to average" for
197
+ /// "averaged to zero".
198
+ fn percentile(rows: &[(String, Json)], field: &'static str, at: f64) -> Json {
199
+ let mut values: Vec<Json> = rows
200
+ .iter()
201
+ .filter_map(|(_, row)| row.get(field))
202
+ .map(query_comparators::comparable)
203
+ .filter(|value| !matches!(value, Json::Null))
204
+ .collect();
205
+ if values.is_empty() {
206
+ return Json::Null;
207
+ }
208
+ values.sort_by(|a, b| query_comparators::as_f64(a).partial_cmp(&query_comparators::as_f64(b)).unwrap_or(std::cmp::Ordering::Equal));
209
+
210
+ let position = at * (values.len() - 1) as f64;
211
+ let lower = position.floor() as usize;
212
+ let fraction = position - lower as f64;
213
+ if fraction == 0.0 {
214
+ values[lower].clone()
215
+ } else {
216
+ let lo = query_comparators::as_f64(&values[lower]);
217
+ let hi = query_comparators::as_f64(&values[lower + 1]);
218
+ Json::Float(lo + fraction * (hi - lo))
219
+ }
220
+ }
221
+
222
+ /// The standard median: the fixed `at: 0.5` case of `percentile` (ADR 0078) — the middle
223
+ /// value when odd, the average of the two middle values when even.
224
+ fn median(rows: &[(String, Json)], field: &'static str) -> Json {
225
+ percentile(rows, field, 0.5)
226
+ }
227
+
228
+ /// The total of the declared Integer field across `rows`; `0`, not `Json::Null`, on no rows —
229
+ /// "no disputes" means zero exposure, where `median`/`min`/`max` have no answer to give
230
+ /// (ADR 0078). Always `Json::Num`, never `Json::Float`: `sum` admits only an Integer field.
231
+ fn sum(rows: &[(String, Json)], field: &'static str) -> Json {
232
+ let total = rows
233
+ .iter()
234
+ .filter_map(|(_, row)| row.get(field))
235
+ .map(query_comparators::comparable)
236
+ .filter(|value| !matches!(value, Json::Null))
237
+ .fold(0.0, |acc, value| acc + query_comparators::as_f64(&value));
238
+ Json::Num(total, None)
239
+ }
240
+
241
+ /// The mean of the declared Integer field across `rows`; `Json::Null` on no rows — a rate of
242
+ /// nothing is undefined, not zero (ADR 0078). `sum / count` as a plain `f64` division: both
243
+ /// operands are exact integers within 2^53 (`sum`'s own overflow refusal), so IEEE 754's
244
+ /// correctly-rounded division agrees bit for bit with Ruby's own `sum.to_f / count.to_f`.
245
+ fn avg(rows: &[(String, Json)], field: &'static str) -> Json {
246
+ let values: Vec<f64> = rows
247
+ .iter()
248
+ .filter_map(|(_, row)| row.get(field))
249
+ .map(query_comparators::comparable)
250
+ .filter(|value| !matches!(value, Json::Null))
251
+ .map(|value| query_comparators::as_f64(&value))
252
+ .collect();
253
+ if values.is_empty() {
254
+ return Json::Null;
255
+ }
256
+ Json::Float(values.iter().sum::<f64>() / values.len() as f64)
257
+ }
258
+
259
+ /// The smallest (`want_max: false`) or largest (`true`) declared field value across `rows`,
260
+ /// its own `Json` variant preserved (an Integer field stays `Num`, a Float field stays
261
+ /// `Float`); `Json::Null` on no rows.
262
+ fn extreme(rows: &[(String, Json)], field: &'static str, want_max: bool) -> Json {
263
+ let mut values: Vec<Json> = rows
264
+ .iter()
265
+ .filter_map(|(_, row)| row.get(field))
266
+ .map(query_comparators::comparable)
267
+ .filter(|value| !matches!(value, Json::Null))
268
+ .collect();
269
+ if values.is_empty() {
270
+ return Json::Null;
271
+ }
272
+ values.sort_by(|a, b| query_comparators::as_f64(a).partial_cmp(&query_comparators::as_f64(b)).unwrap_or(std::cmp::Ordering::Equal));
273
+ if want_max { values.pop().expect("checked non-empty above") } else { values.remove(0) }
274
+ }
275
+
276
+ /// Whether any (`want_all: false`) or every (`true`) row's declared boolean field is `true`
277
+ /// across `rows` — the ordinary OR-identity/AND-identity vacuous-truth reading on no rows
278
+ /// (`any` of nothing is `false`, `all` of nothing is `true`; ADR 0078).
279
+ fn boolean_reduce(rows: &[(String, Json)], field: &'static str, want_all: bool) -> Json {
280
+ let values: Vec<bool> = rows
281
+ .iter()
282
+ .filter_map(|(_, row)| row.get(field))
283
+ .map(|value| query_comparators::comparable(value))
284
+ .filter_map(|value| value.as_bool())
285
+ .collect();
286
+ Json::Bool(if want_all { values.iter().all(|v| *v) } else { values.iter().any(|v| *v) })
287
+ }
288
+
289
+ /// The lookup a read-model ask (a bare "Domain.Name" string, no "::") dispatches through — a
290
+ /// linear scan over a generated domain's `READ_MODELS` table. Accepts either the declared verb
291
+ /// spelling or its snake_case alias (`matches_snake_alias`, below), since a real corpus caller
292
+ /// uses the snake_case form for this read model's own query step.
293
+ pub fn find<'a>(table: &'a [ReadModelDef], verb: &str) -> Option<&'a ReadModelDef> {
294
+ table.iter().find(|def| def.verb == verb || matches_snake_alias(def.verb, verb))
295
+ }
296
+
297
+ /// True when `declared_verb` and `asked` name the same domain and `asked`'s name half equals
298
+ /// `declared_verb`'s name half run through `to_snake`.
299
+ fn matches_snake_alias(declared_verb: &str, asked: &str) -> bool {
300
+ let Some((domain, name)) = declared_verb.split_once('.') else { return false };
301
+ let Some((asked_domain, asked_name)) = asked.split_once('.') else { return false };
302
+
303
+ domain == asked_domain && to_snake(name) == asked_name
304
+ }
305
+
306
+ /// A port of `Hecks::Naming.snake` (lib/hecks/naming.rb). No regex engine in this
307
+ /// dependency-free kernel, so the two `gsub` passes collapse into one left-to-right scan of
308
+ /// adjacent-character pairs; every declared read-model/query name is plain ASCII.
309
+ fn to_snake(name: &str) -> String {
310
+ let chars: Vec<char> = name.chars().collect();
311
+ let mut out = String::with_capacity(name.len() + 4);
312
+
313
+ for (i, &c) in chars.iter().enumerate() {
314
+ if i > 0 {
315
+ let prev = chars[i - 1];
316
+ // "HTTPServer" splits before the "S" of "Server", not before the "H" of "HTTP".
317
+ let rule1 = prev.is_ascii_uppercase() && c.is_ascii_uppercase() && chars.get(i + 1).is_some_and(char::is_ascii_lowercase);
318
+ // "customerPortfolio" splits before "P".
319
+ let rule2 = (prev.is_ascii_lowercase() || prev.is_ascii_digit()) && c.is_ascii_uppercase();
320
+ if rule1 || rule2 {
321
+ out.push('_');
322
+ }
323
+ }
324
+ out.push(c);
325
+ }
326
+
327
+ out.make_ascii_lowercase();
328
+ out
329
+ }
330
+
331
+ #[cfg(test)]
332
+ mod snake_alias_tests {
333
+ use super::*;
334
+
335
+ // Every case here was checked against the real Ruby `Naming.snake`, not just this port.
336
+ #[test]
337
+ fn to_snake_matches_ruby_naming_snake() {
338
+ assert_eq!(to_snake("CustomerPortfolio"), "customer_portfolio");
339
+ assert_eq!(to_snake("ComplianceDashboard"), "compliance_dashboard");
340
+ assert_eq!(to_snake("Active"), "active");
341
+ assert_eq!(to_snake("ByFee"), "by_fee");
342
+ assert_eq!(to_snake("HTTPServer"), "http_server");
343
+ assert_eq!(to_snake("Type2Foo"), "type2_foo");
344
+ }
345
+
346
+ #[test]
347
+ fn find_accepts_both_the_declared_and_the_snake_cased_spelling() {
348
+ let table = [ReadModelDef {
349
+ verb: "Banking.CustomerPortfolio",
350
+ reference_name: Some("reference"),
351
+ heads: &[],
352
+ filtered_heads: &[],
353
+ authorization: None,
354
+ group_by: None,
355
+ count: false,
356
+ median_field: None,
357
+ sum_field: None,
358
+ avg_field: None,
359
+ min_field: None,
360
+ max_field: None,
361
+ percentile_field: None,
362
+ percentile_at: None,
363
+ any_field: None,
364
+ all_field: None,
365
+ }];
366
+
367
+ assert!(find(&table, "Banking.CustomerPortfolio").is_some());
368
+ assert!(find(&table, "Banking.customer_portfolio").is_some());
369
+ assert!(find(&table, "Banking.somethingelse").is_none());
370
+ assert!(find(&table, "Other.customer_portfolio").is_none());
371
+ }
372
+ }
373
+
374
+ /// ADR 0061, decision D1 — a `group_by` leaf holds one row.
375
+ #[cfg(test)]
376
+ mod nest_tests {
377
+ use super::*;
378
+
379
+ fn part(id: &str, bin: &str) -> Json {
380
+ Json::obj(vec![("bin", Json::str(bin)), ("id", Json::str(id))])
381
+ }
382
+
383
+ #[test]
384
+ fn refuses_two_rows_on_one_checked_key_path() {
385
+ let rows = vec![part("p2", "b1"), part("p3", "b2"), part("p1", "b1")];
386
+
387
+ let err = nest(rows, &["bin"], LeafCheck::RefuseCollision("PartsByBin")).expect_err("p1 and p2 share bin b1");
388
+
389
+ assert_eq!(
390
+ err,
391
+ Refusal::InvariantViolation(
392
+ "PartsByBin groups by bin, but rows \"p1\", \"p2\" share bin = b1 — a group_by leaf holds one row; add a field that tells them apart"
393
+ .to_string()
394
+ )
395
+ );
396
+ }
397
+
398
+ #[test]
399
+ fn answers_when_every_key_path_holds_one_row() {
400
+ let rows = vec![part("p1", "b1"), part("p3", "b2")];
401
+
402
+ let nested = nest(rows, &["bin"], LeafCheck::RefuseCollision("PartsByBin")).expect("no two parts share a bin");
403
+
404
+ assert_eq!(nested, Json::obj(vec![("b1", Json::obj(vec![("id", Json::str("p1"))])), ("b2", Json::obj(vec![("id", Json::str("p3"))]))]));
405
+ }
406
+
407
+ #[test]
408
+ fn names_every_level_of_a_multi_field_key_path() {
409
+ let row = |id: &str| Json::obj(vec![("bin", Json::str("b1")), ("shelf", Json::Num(2.0, None)), ("id", Json::str(id))]);
410
+
411
+ let err = nest(vec![row("p1"), row("p2")], &["bin", "shelf"], LeafCheck::RefuseCollision("PartsByShelf")).expect_err("same bin and shelf");
412
+
413
+ assert_eq!(
414
+ err,
415
+ Refusal::InvariantViolation(
416
+ "PartsByShelf groups by bin, shelf, but rows \"p1\", \"p2\" share bin = b1, shelf = 2 — a group_by leaf holds one row; add a field that tells them apart"
417
+ .to_string()
418
+ )
419
+ );
420
+ }
421
+ }
422
+
423
+ /// Exercises `ReferenceHopCondition`'s fold end-to-end through `run`, not just the isolated
424
+ /// `apply_filtered_head_options` helper, so a wiring mistake at either call site fails this the
425
+ /// same way it would fail a real caller.
426
+ #[cfg(test)]
427
+ mod reference_hop_tests {
428
+ use super::*;
429
+ use crate::kernel::AggregateScan;
430
+
431
+ /// `domain` plus `(bare aggregate name, entries)` pairs, searched the same
432
+ /// domain-qualified way a real generated `Store::scan` is.
433
+ struct FakeStore {
434
+ domain: &'static str,
435
+ aggregates: Vec<(&'static str, Vec<(String, Json)>)>,
436
+ }
437
+
438
+ impl AggregateScan for FakeStore {
439
+ fn scan(&self, aggregate: &str) -> Option<Vec<(String, Json)>> {
440
+ let bare = aggregate.strip_prefix(&format!("{}::", self.domain))?;
441
+ self.aggregates.iter().find(|(name, _)| *name == bare).map(|(_, entries)| entries.clone())
442
+ }
443
+ }
444
+
445
+ fn account(status: &str, customer: &str) -> Json {
446
+ Json::obj(vec![("status", Json::str(status)), ("customer", Json::str(customer))])
447
+ }
448
+
449
+ fn customer(status: &str) -> Json {
450
+ Json::obj(vec![("status", Json::str(status))])
451
+ }
452
+
453
+ fn store() -> FakeStore {
454
+ FakeStore {
455
+ domain: "Banking",
456
+ aggregates: vec![
457
+ (
458
+ "Account",
459
+ vec![
460
+ ("acc-suspended-open".to_string(), account("open", "cust-suspended")),
461
+ ("acc-active-open".to_string(), account("open", "cust-active")),
462
+ ("acc-suspended-closed".to_string(), account("closed", "cust-suspended")),
463
+ ],
464
+ ),
465
+ (
466
+ "Customer",
467
+ vec![
468
+ ("cust-suspended".to_string(), customer("suspended")),
469
+ ("cust-active".to_string(), customer("active")),
470
+ ],
471
+ ),
472
+ ],
473
+ }
474
+ }
475
+
476
+ /// A rootless read model with one `many` head carrying both an ordinary local condition
477
+ /// (`status == "open"`) and one hop condition (`customer/status == "suspended"`).
478
+ fn open_for_suspended_customers_def() -> ReadModelDef {
479
+ ReadModelDef {
480
+ verb: "Banking.OpenForSuspendedCustomers",
481
+ reference_name: None,
482
+ heads: &[ReadModelHead { aggregate: "Banking::Account", as_name: "accounts", many: true, is_root: false, reference_fields: &[] }],
483
+ filtered_heads: &[FilteredHead {
484
+ as_name: "accounts",
485
+ conditions: &[QueryCondition { field: "status", comparator: query_comparators::QueryComparator::Eq, value: QueryConditionValue::Literal("open") }],
486
+ reference_hop_conditions: &[ReferenceHopCondition {
487
+ via_field: "customer",
488
+ target_aggregate: "Banking::Customer",
489
+ through: &[],
490
+ inner_field: "status",
491
+ inner_comparator: query_comparators::QueryComparator::Eq,
492
+ inner_value: QueryConditionValue::Literal("suspended"),
493
+ }],
494
+ order_by: None,
495
+ offset: None,
496
+ limit: None,
497
+ }],
498
+ authorization: None,
499
+ group_by: None,
500
+ count: false,
501
+ median_field: None,
502
+ sum_field: None,
503
+ avg_field: None,
504
+ min_field: None,
505
+ max_field: None,
506
+ percentile_field: None,
507
+ percentile_at: None,
508
+ any_field: None,
509
+ all_field: None,
510
+ }
511
+ }
512
+
513
+ #[test]
514
+ fn a_hop_condition_keeps_only_rows_whose_local_and_hopped_conditions_both_hold() {
515
+ let def = open_for_suspended_customers_def();
516
+ let result = run(&store(), &def, &Json::obj(vec![])).expect("a rootless read model with no authorization needs no args");
517
+
518
+ let accounts = result.get("accounts").expect("the one declared head").as_array().expect("a many head answers an array");
519
+ let ids: Vec<&str> = accounts.iter().map(|row| row.get("id").and_then(Json::as_str).expect("row_json always stamps id")).collect();
520
+
521
+ assert_eq!(ids, vec!["acc-suspended-open"]);
522
+ // Named explicitly so a regression that widens either condition fails on the specific
523
+ // wrong row it would admit, not just a count.
524
+ assert!(!ids.contains(&"acc-active-open"), "open but NOT suspended — the hop condition alone should have excluded this");
525
+ assert!(!ids.contains(&"acc-suspended-closed"), "suspended customer but NOT open — the ordinary local condition alone should have excluded this");
526
+ }
527
+
528
+ #[test]
529
+ fn a_hop_against_an_unscannable_target_aggregate_refuses_cleanly() {
530
+ let mut def = open_for_suspended_customers_def();
531
+ def.filtered_heads = &[FilteredHead {
532
+ as_name: "accounts",
533
+ conditions: &[QueryCondition { field: "status", comparator: query_comparators::QueryComparator::Eq, value: QueryConditionValue::Literal("open") }],
534
+ reference_hop_conditions: &[ReferenceHopCondition {
535
+ via_field: "customer",
536
+ target_aggregate: "Banking::NoSuchAggregate",
537
+ through: &[],
538
+ inner_field: "status",
539
+ inner_comparator: query_comparators::QueryComparator::Eq,
540
+ inner_value: QueryConditionValue::Literal("suspended"),
541
+ }],
542
+ order_by: None,
543
+ offset: None,
544
+ limit: None,
545
+ }];
546
+
547
+ let err = run(&store(), &def, &Json::obj(vec![])).expect_err("scanning an aggregate this store doesn't declare must refuse, not silently answer empty");
548
+ assert!(matches!(err, Refusal::TypeMismatch(_)));
549
+ }
550
+
551
+ // An included nested entity has no table of its own; the head answers an empty array and
552
+ // the sibling head's rows are untouched.
553
+ #[test]
554
+ fn a_head_with_no_table_of_its_own_reads_as_empty_rather_than_refusing() {
555
+ let mut def = open_for_suspended_customers_def();
556
+ def.filtered_heads = &[];
557
+ def.heads = &[
558
+ ReadModelHead { aggregate: "Banking::Account", as_name: "accounts", many: true, is_root: false, reference_fields: &[] },
559
+ ReadModelHead { aggregate: "Banking::LedgerEntry", as_name: "ledger_entries", many: true, is_root: false, reference_fields: &[] },
560
+ ];
561
+
562
+ let result = run(&store(), &def, &Json::obj(vec![])).expect("an entity head must not refuse the read model");
563
+
564
+ assert_eq!(result.get("accounts").and_then(|v| v.as_array()).map(|rows| rows.len()), Some(3));
565
+ assert_eq!(result.get("ledger_entries").and_then(|v| v.as_array()).map(|rows| rows.len()), Some(0));
566
+ }
567
+
568
+ // A declared query folds its hop the same way a read model head does.
569
+ #[test]
570
+ fn a_named_query_folds_its_hop_conditions_like_a_read_model_head() {
571
+ let def = crate::kernel::named_query::QueryDef {
572
+ verb: "Banking::Account.OpenForSuspendedCustomers",
573
+ aggregate: "Banking::Account",
574
+ conditions: &[QueryCondition { field: "status", comparator: query_comparators::QueryComparator::Eq, value: QueryConditionValue::Literal("open") }],
575
+ reference_hop_conditions: &[ReferenceHopCondition {
576
+ via_field: "customer",
577
+ target_aggregate: "Banking::Customer",
578
+ through: &[],
579
+ inner_field: "status",
580
+ inner_comparator: query_comparators::QueryComparator::Eq,
581
+ inner_value: QueryConditionValue::Literal("suspended"),
582
+ }],
583
+ order_by: None,
584
+ offset: None,
585
+ limit: None,
586
+ authorization: None,
587
+ };
588
+
589
+ let rows = crate::kernel::named_query::run(&store(), &def, &Json::obj(vec![]), None).expect("no authorization, no args needed");
590
+ let ids: Vec<&str> = rows.iter().map(|(id, _)| id.as_str()).collect();
591
+
592
+ assert_eq!(ids, vec!["acc-suspended-open"]);
593
+ }
594
+
595
+ // An entity query flattens every card's withdrawals, ordered by card then sequence, capped.
596
+ #[test]
597
+ fn an_entity_query_flattens_every_owners_list_ordered_by_parent_then_identity() {
598
+ fn withdrawal(sequence: f64, state: &str) -> Json {
599
+ Json::obj(vec![("sequence", Json::obj(vec![("value", Json::Num(sequence, None))])), ("state", Json::str(state))])
600
+ }
601
+ let cards = FakeStore {
602
+ domain: "Banking",
603
+ aggregates: vec![(
604
+ "ATMCard",
605
+ vec![
606
+ ("card-2".to_string(), Json::obj(vec![("withdrawals", Json::Array(vec![withdrawal(2.0, "taken"), withdrawal(1.0, "taken")]))])),
607
+ ("card-1".to_string(), Json::obj(vec![("withdrawals", Json::Array(vec![withdrawal(1.0, "disputed"), withdrawal(3.0, "taken")]))])),
608
+ ],
609
+ )],
610
+ };
611
+ let def = crate::kernel::named_query::EntityQueryDef {
612
+ verb: "Banking::ATMCard.Withdrawal.Recent",
613
+ aggregate: "Banking::ATMCard",
614
+ list_field: "withdrawals",
615
+ parent_key: "atm_card",
616
+ identity_keys: &["sequence"],
617
+ conditions: &[QueryCondition { field: "state", comparator: query_comparators::QueryComparator::Eq, value: QueryConditionValue::Literal("taken") }],
618
+ order_by: None,
619
+ offset: None,
620
+ limit: Some(query_ordering::Limit::Literal(2)),
621
+ };
622
+
623
+ let rows = crate::kernel::named_query::run_entity(&cards, &def, &Json::obj(vec![])).expect("no args needed");
624
+ let keyed: Vec<(String, String)> = rows
625
+ .iter()
626
+ .map(|row| {
627
+ let parent = row.get("atm_card").and_then(Json::as_str).expect("parent key leads every row").to_string();
628
+ let sequence = format!("{:?}", row.dig("sequence.value"));
629
+ (parent, sequence)
630
+ })
631
+ .collect();
632
+
633
+ assert_eq!(keyed.len(), 2, "limit 2 over three taken withdrawals: {keyed:?}");
634
+ assert_eq!(keyed[0].0, "card-1");
635
+ assert_eq!(keyed[1].0, "card-2");
636
+ assert!(keyed[1].1.contains('1'), "card-2's lowest sequence comes first: {keyed:?}");
637
+ }
638
+
639
+ // A chain: the inner clause picks sponsors, the middle step keeps members sponsored by one,
640
+ // and the head keeps referrals issued by one of those members.
641
+ #[test]
642
+ fn a_hop_chain_folds_inside_out_through_every_step() {
643
+ let chain_store = FakeStore {
644
+ domain: "ReferralChain",
645
+ aggregates: vec![
646
+ (
647
+ "Sponsor",
648
+ vec![
649
+ ("s-good".to_string(), Json::obj(vec![("standing", Json::str("good"))])),
650
+ ("s-poor".to_string(), Json::obj(vec![("standing", Json::str("poor"))])),
651
+ ],
652
+ ),
653
+ (
654
+ "Member",
655
+ vec![
656
+ ("m-good".to_string(), Json::obj(vec![("sponsor", Json::str("s-good"))])),
657
+ ("m-poor".to_string(), Json::obj(vec![("sponsor", Json::str("s-poor"))])),
658
+ ],
659
+ ),
660
+ (
661
+ "Referral",
662
+ vec![
663
+ ("r-good".to_string(), Json::obj(vec![("member", Json::str("m-good"))])),
664
+ ("r-poor".to_string(), Json::obj(vec![("member", Json::str("m-poor"))])),
665
+ ],
666
+ ),
667
+ ],
668
+ };
669
+ let def = crate::kernel::named_query::QueryDef {
670
+ verb: "ReferralChain::Referral.FromGoodSponsors",
671
+ aggregate: "ReferralChain::Referral",
672
+ conditions: &[],
673
+ reference_hop_conditions: &[ReferenceHopCondition {
674
+ via_field: "member",
675
+ target_aggregate: "ReferralChain::Member",
676
+ through: &[HopStep { via_field: "sponsor", target_aggregate: "ReferralChain::Sponsor" }],
677
+ inner_field: "standing",
678
+ inner_comparator: query_comparators::QueryComparator::Eq,
679
+ inner_value: QueryConditionValue::Literal("good"),
680
+ }],
681
+ order_by: None,
682
+ offset: None,
683
+ limit: None,
684
+ authorization: None,
685
+ };
686
+
687
+ let rows = crate::kernel::named_query::run(&chain_store, &def, &Json::obj(vec![]), None).expect("no authorization, no args needed");
688
+ let ids: Vec<&str> = rows.iter().map(|(id, _)| id.as_str()).collect();
689
+
690
+ assert_eq!(ids, vec!["r-good"]);
691
+ }
692
+ }
693
+
694
+ /// The generic read-model interpreter — the single projected row, one entry per declared
695
+ /// head's `as_name`, for exactly the subset this module admits.
696
+ ///
697
+ /// Heads compute root-first regardless of declared `include` order: a non-root head matches
698
+ /// against whatever is already computed, so processing it before its own root would always
699
+ /// compare against nothing.
700
+ pub fn run(store: &impl AggregateScan, def: &ReadModelDef, args: &Json) -> Result<Json, Refusal> {
701
+ let reference_id = match def.reference_name {
702
+ Some(name) => Some(
703
+ args.get(name)
704
+ .and_then(Json::as_str)
705
+ .ok_or_else(|| Refusal::TypeMismatch(format!("{}: missing reference argument {name:?}", def.verb)))?
706
+ .to_string(),
707
+ ),
708
+ // Rootless: no caller-supplied id to require or resolve at all.
709
+ None => None,
710
+ };
711
+
712
+ // Checked right after the reference id resolves, before any head is computed.
713
+ if let Some(auth) = &def.authorization {
714
+ if args.get(auth.tenant_field).is_none() {
715
+ return Err(Refusal::Unauthorized(
716
+ UnauthorizedTenantRequiredArgs { query: auth.query_name, field: auth.tenant_field }.render_args(),
717
+ ));
718
+ }
719
+ }
720
+
721
+ let (root_heads, other_heads): (Vec<&ReadModelHead>, Vec<&ReadModelHead>) = def.heads.iter().partition(|head| head.is_root);
722
+
723
+ // One entry per head already computed, in computation order (root-first): every candidate
724
+ // record is checked against every entry seen so far, not just the immediately-preceding one.
725
+ let mut projected: Vec<(&'static str, Vec<(String, Json)>)> = Vec::new();
726
+ let mut rows_by_as: std::collections::HashMap<&'static str, (bool, Vec<(String, Json)>)> = std::collections::HashMap::new();
727
+ let mut grouped_heads: std::collections::HashSet<&'static str> = std::collections::HashSet::new();
728
+
729
+ for head in root_heads.into_iter().chain(other_heads) {
730
+ let mut rows = if reference_id.is_none() {
731
+ // Rootless: each head reads its own aggregate's whole table independently, with no
732
+ // cross-referencing against `projected`. A head with no table reads as empty.
733
+ store.scan(head.aggregate).unwrap_or_default()
734
+ } else if head.is_root {
735
+ vec![fetch_root(store, head, reference_id.as_deref().unwrap())?]
736
+ } else {
737
+ scan_matching(store, head, &projected)?
738
+ };
739
+
740
+ // Applied before this head's rows go into `projected`, so any later head's own
741
+ // reference-matching sees the filtered rows, not the pre-filter scan.
742
+ if let Some(filtered) = def.filtered_heads.iter().find(|fh| fh.as_name == head.as_name) {
743
+ rows = apply_filtered_head_options(rows, filtered, args, store)?;
744
+ }
745
+
746
+ // Recorded so the output loop below knows not to wrap these rows again — the generated
747
+ // transform already does its own row_json-equivalent wrapping internally.
748
+ if def.group_by.is_some() && head.many {
749
+ grouped_heads.insert(head.as_name);
750
+ }
751
+
752
+ projected.push((head.aggregate, rows.clone()));
753
+ rows_by_as.insert(head.as_name, (head.many, rows));
754
+ }
755
+
756
+ // Declared order, for the output only — `def.heads` itself, not the root-first order
757
+ // `projected`/`rows_by_as` were just built in. `repository::row_json` wraps every record
758
+ // here, at every level of nesting, since this kernel's generated `to_json()` doesn't carry
759
+ // `id` the way a live Ruby record's `to_h` does.
760
+ let mut out = Vec::with_capacity(def.heads.len());
761
+ for head in def.heads {
762
+ let (many, rows) = rows_by_as
763
+ .remove(head.as_name)
764
+ .expect("every head in def.heads was computed in the loop above — as_name is unique per read model (ReadModelBuilder#add_aggregate_head)");
765
+ let value = if grouped_heads.contains(head.as_name) {
766
+ // Already fully formed by the generated transform — must not be wrapped again the
767
+ // way an ordinary head's rows are.
768
+ (def.group_by.expect("grouped_heads is only ever populated when def.group_by is Some"))(rows)?
769
+ } else if many && def.count {
770
+ Json::Num(rows.len() as f64, None)
771
+ } else if many && def.median_field.is_some() {
772
+ median(&rows, def.median_field.expect("checked by the branch guard"))
773
+ } else if many && def.sum_field.is_some() {
774
+ sum(&rows, def.sum_field.expect("checked by the branch guard"))
775
+ } else if many && def.avg_field.is_some() {
776
+ avg(&rows, def.avg_field.expect("checked by the branch guard"))
777
+ } else if many && def.min_field.is_some() {
778
+ extreme(&rows, def.min_field.expect("checked by the branch guard"), false)
779
+ } else if many && def.max_field.is_some() {
780
+ extreme(&rows, def.max_field.expect("checked by the branch guard"), true)
781
+ } else if many && def.percentile_field.is_some() {
782
+ percentile(
783
+ &rows,
784
+ def.percentile_field.expect("checked by the branch guard"),
785
+ def.percentile_at.expect("percentile_field implies percentile_at"),
786
+ )
787
+ } else if many && def.any_field.is_some() {
788
+ boolean_reduce(&rows, def.any_field.expect("checked by the branch guard"), false)
789
+ } else if many && def.all_field.is_some() {
790
+ boolean_reduce(&rows, def.all_field.expect("checked by the branch guard"), true)
791
+ } else if many {
792
+ Json::Array(rows.into_iter().map(|(id, record)| repository::row_json(id, record)).collect())
793
+ } else {
794
+ rows.into_iter().next().map(|(id, record)| repository::row_json(id, record)).unwrap_or(Json::Null)
795
+ };
796
+ out.push((head.as_name.to_string(), value));
797
+ }
798
+
799
+ Ok(Json::Object(out))
800
+ }
801
+
802
+ /// The root head's own single row: find the one instance by id, `NotFound` if it isn't there.
803
+ /// Unlike every other head, the root is looked up directly rather than scanned-and-matched.
804
+ fn fetch_root(store: &impl AggregateScan, head: &ReadModelHead, reference_id: &str) -> Result<(String, Json), Refusal> {
805
+ let entries = store
806
+ .scan(head.aggregate)
807
+ .ok_or_else(|| Refusal::TypeMismatch(format!("unknown aggregate {:?}", head.aggregate)))?;
808
+
809
+ entries
810
+ .into_iter()
811
+ .find(|(id, _)| id == reference_id)
812
+ .ok_or_else(|| {
813
+ // Named by its bare bluebook name; `head.aggregate` is the domain-qualified one.
814
+ let bare = head.aggregate.rsplit("::").next().unwrap_or(head.aggregate);
815
+ Refusal::NotFound(
816
+ NotFoundReadModelReferenceMissingArgs { aggregate: bare, offered: &format!("{reference_id:?}") }
817
+ .render_args(),
818
+ )
819
+ })
820
+ }
821
+
822
+ /// A non-root head's own rows: every instance of this head's aggregate whose reference field(s)
823
+ /// point back at a row already in `projected`, sorted by id ascending.
824
+ fn scan_matching(
825
+ store: &impl AggregateScan,
826
+ head: &ReadModelHead,
827
+ projected: &[(&'static str, Vec<(String, Json)>)],
828
+ ) -> Result<Vec<(String, Json)>, Refusal> {
829
+ // The generator emits a head with no table only when it names a nested entity; that reads
830
+ // as empty here, while the root still refuses in `fetch_root`.
831
+ let Some(entries) = store.scan(head.aggregate) else {
832
+ return Ok(Vec::new());
833
+ };
834
+
835
+ let mut matched: Vec<(String, Json)> = entries.into_iter().filter(|(_, record)| record_matches(record, head, projected)).collect();
836
+ matched.sort_by(|a, b| a.0.cmp(&b.0));
837
+ Ok(matched)
838
+ }
839
+
840
+ /// Whether `record` holds one of its precomputed `reference_fields` pointing at a row already
841
+ /// in `projected`.
842
+ fn record_matches(record: &Json, head: &ReadModelHead, projected: &[(&'static str, Vec<(String, Json)>)]) -> bool {
843
+ head.reference_fields.iter().any(|reference_field| {
844
+ let Some(held_id) = record.get(reference_field.field).and_then(Json::as_str) else { return false };
845
+ projected
846
+ .iter()
847
+ .any(|(aggregate, rows)| *aggregate == reference_field.target_aggregate && rows.iter().any(|(id, _)| id == held_id))
848
+ })
849
+ }
850
+
851
+ /// One targeted head's own where/order_by/offset/limit. Where-filtering chains
852
+ /// `repository::filter_entries` per condition; order/limit reuse `query_ordering::apply`
853
+ /// rather than reimplementing the identity-sort/declared-order/limit logic a declared
854
+ /// aggregate query already needs.
855
+ fn apply_filtered_head_options(
856
+ mut rows: Vec<(String, Json)>,
857
+ filtered: &FilteredHead,
858
+ args: &Json,
859
+ store: &impl AggregateScan,
860
+ ) -> Result<Vec<(String, Json)>, Refusal> {
861
+ for condition in filtered.conditions {
862
+ let want = match condition.value {
863
+ QueryConditionValue::Literal(text) => Json::Str(text.to_string()),
864
+ QueryConditionValue::NumericLiteral(n) => Json::Num(n, None),
865
+ QueryConditionValue::Arg(name) => args.get(name).cloned().unwrap_or(Json::Null),
866
+ };
867
+ rows = repository::filter_entries(rows, condition.field, condition.comparator, &want);
868
+ }
869
+
870
+ rows = apply_reference_hops(rows, filtered.reference_hop_conditions, args, store)?;
871
+
872
+ Ok(query_ordering::apply(rows, filtered.order_by.as_ref(), filtered.offset.as_ref(), filtered.limit.as_ref(), args))
873
+ }
874
+
875
+ /// Folds a `ReferenceHopCondition` chain: one query against the chain's final target picks
876
+ /// matching ids, then each earlier step's `in` filter narrows toward these rows. Shared by a
877
+ /// read model's eligible head and a declared query (`named_query::run_cross_domain`).
878
+ pub(super) fn apply_reference_hops(
879
+ mut rows: Vec<(String, Json)>,
880
+ hops: &[ReferenceHopCondition],
881
+ args: &Json,
882
+ store: &impl AggregateScan,
883
+ ) -> Result<Vec<(String, Json)>, Refusal> {
884
+ for hop in hops {
885
+ let inner_want = match hop.inner_value {
886
+ QueryConditionValue::Literal(text) => Json::Str(text.to_string()),
887
+ QueryConditionValue::NumericLiteral(n) => Json::Num(n, None),
888
+ QueryConditionValue::Arg(name) => args.get(name).cloned().unwrap_or(Json::Null),
889
+ };
890
+ let scan = |aggregate: &str| {
891
+ store.scan(aggregate).ok_or_else(|| Refusal::TypeMismatch(format!("unknown aggregate {aggregate:?}")))
892
+ };
893
+ let ids_of = |entries: Vec<(String, Json)>| -> Vec<Json> { entries.into_iter().map(|(id, _)| Json::Str(id)).collect() };
894
+
895
+ // The chain, first hop included: `(via_field, target_aggregate)` pairs, each via_field a
896
+ // reference on the previous target.
897
+ let chain: Vec<(&str, &str)> = std::iter::once((hop.via_field, hop.target_aggregate))
898
+ .chain(hop.through.iter().map(|step| (step.via_field, step.target_aggregate)))
899
+ .collect();
900
+
901
+ // Inside out: the inner clause picks ids on the last target, and each earlier step keeps
902
+ // the rows of its target whose next via_field points at one of them.
903
+ let (_, last_target) = chain[chain.len() - 1];
904
+ let mut ids = ids_of(repository::filter_entries(scan(last_target)?, hop.inner_field, hop.inner_comparator, &inner_want));
905
+ for step in (1..chain.len()).rev() {
906
+ let (via_field, _) = chain[step];
907
+ let (_, source) = chain[step - 1];
908
+ ids = ids_of(repository::filter_entries(scan(source)?, via_field, query_comparators::QueryComparator::In, &Json::Array(ids)));
909
+ }
910
+
911
+ rows = repository::filter_entries(rows, hop.via_field, query_comparators::QueryComparator::In, &Json::Array(ids));
912
+ }
913
+ Ok(rows)
914
+ }