hecks 2.9.0 → 3.0.1

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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +13 -4
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +15 -8
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,690 @@
1
+ require_relative "skip_reason"
2
+
3
+ # Compiles declared read models into the static Rust tables
4
+ # `kernel/read_model.rs` interprets at runtime.
5
+ module RustProjection
6
+ module Projector
7
+ module_function
8
+
9
+ # `group_by` is now always present on the wire (`[]` when undeclared), so
10
+ # eligibility below must check its value, not the key's mere presence.
11
+ READ_MODEL_BARE_KEYS = %i[name description reference_name reference_target query_name aggregate_heads
12
+ wheres order_by offset limit freshness index_hints group_by null_semantics
13
+ authorization count median_field sum_field avg_field min_field max_field
14
+ percentile_field percentile_at any_field all_field].freeze
15
+
16
+ # Returns nil if the read model can be generated as-is, otherwise a
17
+ # short reason string naming what's unsupported.
18
+ def read_model_skip_reason(read_model, aggregates_by_name, unsupported_names)
19
+ extra = read_model.keys.map(&:to_sym) - READ_MODEL_BARE_KEYS
20
+ return read_model_options_skip_reason(extra) if extra.any?
21
+
22
+ # Checked before the root lookup below: a rootless, group_by'd read
23
+ # model has no reference_target, and reporting that as "no matching
24
+ # aggregate head" would be misleading instead of the real reason.
25
+ return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names) if Array(read_model[:group_by]).any?
26
+
27
+ heads = read_model[:aggregate_heads]
28
+ root = heads.find { |head| head[:aggregate].to_s == read_model[:reference_target].to_s }
29
+ # A rootless read model (every head reads its own whole table) has no
30
+ # reference_target at all; only a named target with no matching head,
31
+ # which leaves nothing for the root fetch to key off, is refused.
32
+ if !root && !read_model[:reference_target].to_s.empty?
33
+ return skip("missing_root_head", "declares reference_to #{read_model[:reference_target]}, but includes no matching aggregate head — " \
34
+ "nothing for this generator's own root fetch to key off")
35
+ end
36
+
37
+ if root && !aggregates_by_name[root[:aggregate]] && nested_entity_names(aggregates_by_name).include?(root[:aggregate].to_s)
38
+ return entity_head_skip_reason(root[:aggregate], "root", "fetch by id")
39
+ end
40
+
41
+ heads.each do |head|
42
+ reason = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
43
+ return reason if reason
44
+ end
45
+
46
+ reason = read_model_options_content_skip_reason(read_model, aggregates_by_name)
47
+ return reason if reason
48
+
49
+ aggregation_skip_reason(read_model, aggregates_by_name)
50
+ end
51
+
52
+ def read_model_options_skip_reason(extra)
53
+ skip(extra.map(&:to_s).min, "declares #{extra.map(&:to_s).sort.join(', ')} — out of scope for this generator: cursor/" \
54
+ "consistency/inspection are real " \
55
+ "capabilities Ports::Query::InMemory/Ports::Query::Ordering/TenantScope implement that this generator " \
56
+ "does not port (this file's own header has the full argument, the same boundary queries.rb already " \
57
+ "draws for a declared AGGREGATE query); freshness/use_index are never disqualifying on their own — " \
58
+ "neither is read by the in-memory interpreter path this kernel matches")
59
+ end
60
+
61
+ # The many-side heads a where/order_by/offset/limit's own `on:` (or, for an
62
+ # untargeted option, the sole many-side head) could possibly name — every
63
+ # many-side head this read model declares.
64
+ def read_model_many_heads(read_model)
65
+ read_model[:aggregate_heads].select { |head| head[:many] }
66
+ end
67
+
68
+ # Whether one declared option (its own `:target`, or nil when untargeted)
69
+ # applies to `head`. `target` names the included aggregate by TYPE (ADR
70
+ # 0055), not by its `as:` alias; nil only ever means "the sole many-side
71
+ # head" — `ReadModelBuilder#seal_query_options` refuses an untargeted
72
+ # option whenever a read model declares more than one, so an untargeted
73
+ # option reaching codegen at all is proof there's exactly one to mean.
74
+ def option_target_matches_head?(target, head, many_heads)
75
+ return many_heads.size == 1 && many_heads.first[:as].to_s == head[:as].to_s if target.nil?
76
+
77
+ target.to_s == head[:aggregate].to_s
78
+ end
79
+
80
+ # Whether `head` has at least one declared where/order_by/offset/limit
81
+ # (or, for the sole many-side head only, a declared `authorize ...,
82
+ # tenant:` — `on:` doesn't extend to `tenant:`, a deliberate ADR 0055
83
+ # scope limit) targeting it.
84
+ def read_model_option_targets_head?(read_model, head, many_heads)
85
+ Array(read_model[:wheres]).any? { |w| option_target_matches_head?(w[:target], head, many_heads) } ||
86
+ (read_model[:order_by] && option_target_matches_head?(read_model[:order_by][:target], head, many_heads)) ||
87
+ (read_model[:limit] && option_target_matches_head?(read_model[:limit][:target], head, many_heads)) ||
88
+ (read_model[:offset] && option_target_matches_head?(read_model[:offset][:target], head, many_heads)) ||
89
+ (read_model.dig(:authorization, :tenant) && many_heads.size == 1 && many_heads.first[:as].to_s == head[:as].to_s)
90
+ end
91
+
92
+ # Every many-side head this read model declares options for, in
93
+ # aggregate_heads' own declared order (stable regardless of where in the
94
+ # block a `where`/`order_by`/`on:` appears) — one entry when there's a
95
+ # single many-side head (the pre-ADR-0055 shape), more than one only when
96
+ # `on:` targets several.
97
+ def read_model_filtered_heads(read_model)
98
+ many_heads = read_model_many_heads(read_model)
99
+ many_heads.select { |head| read_model_option_targets_head?(read_model, head, many_heads) }
100
+ end
101
+
102
+ # One targeted head's own where/order_by/offset/limit, pulled out of the
103
+ # read model's full declared set. `authorization` carries `read_model`'s
104
+ # own declared authorization only for the sole many-side head (`on:`
105
+ # doesn't extend to `tenant:`), nil for every other targeted head.
106
+ def read_model_head_query(read_model, head, many_heads)
107
+ {
108
+ wheres: Array(read_model[:wheres]).select { |w| option_target_matches_head?(w[:target], head, many_heads) },
109
+ order_by: read_model[:order_by] && option_target_matches_head?(read_model[:order_by][:target], head, many_heads) ? read_model[:order_by] : nil,
110
+ limit: read_model[:limit] && option_target_matches_head?(read_model[:limit][:target], head, many_heads) ? read_model[:limit] : nil,
111
+ offset: read_model[:offset] && option_target_matches_head?(read_model[:offset][:target], head, many_heads) ? read_model[:offset] : nil,
112
+ authorization: many_heads.size == 1 && many_heads.first[:as].to_s == head[:as].to_s ? read_model[:authorization] : nil,
113
+ }
114
+ end
115
+
116
+ # Checks every targeted head's own where/order_by/limit for generability
117
+ # against ITS aggregate (not the read model's root); nil means clean.
118
+ def read_model_options_content_skip_reason(read_model, aggregates_by_name)
119
+ many_heads = read_model_many_heads(read_model)
120
+
121
+ read_model_filtered_heads(read_model).each do |head|
122
+ aggregate = aggregates_by_name[head[:aggregate]]
123
+ return entity_head_skip_reason(head[:aggregate], "filtered head", "filter, order or authorize") unless aggregate
124
+
125
+ value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name], vo] }
126
+ head_query = read_model_head_query(read_model, head, many_heads)
127
+ head_label = "head #{head[:aggregate]} (as #{head[:as]})"
128
+
129
+ head_query[:wheres].each do |where|
130
+ field = where[:field].to_s
131
+ hop = query_hop_plan(aggregate, field, aggregates_by_name)
132
+ if hop.nil? && field.include?("/")
133
+ return skip("reference_hop_where", "#{head_label}'s own where clause on #{field.inspect} hops through a " \
134
+ "reference this generator can't resolve yet (more than one hop, the head isn't a real " \
135
+ "reference attribute, or the target aggregate isn't declared in this domain) — not generated yet")
136
+ end
137
+
138
+ if hop
139
+ target_value_objects_by_name = hop[:target][:value_objects].to_h { |vo| [vo[:name], vo] }
140
+ reason = query_where_skip_reason(where.merge(field: hop[:inner_field]), hop[:target], target_value_objects_by_name)
141
+ return reskip(reason, "#{head_label}'s own hop through #{hop[:via_field]} to " \
142
+ "#{hop[:target_aggregate]}'s own #{reason}") if reason
143
+ next
144
+ end
145
+
146
+ reason = query_where_skip_reason(where, aggregate, value_objects_by_name)
147
+ return reskip(reason, "#{head_label}'s own #{reason}") if reason
148
+ end
149
+
150
+ auth_reason = declared_authorization_skip_reason(head_query[:authorization], aggregate, value_objects_by_name)
151
+ return auth_reason if auth_reason
152
+
153
+ order_reason = declared_order_by_skip_reason(head_query[:order_by], aggregate, value_objects_by_name)
154
+ return order_reason if order_reason
155
+
156
+ offset_reason = declared_offset_skip_reason(head_query[:offset])
157
+ return offset_reason if offset_reason
158
+
159
+ limit_reason = declared_limit_skip_reason(head_query[:limit])
160
+ return limit_reason if limit_reason
161
+ end
162
+
163
+ nil
164
+ end
165
+
166
+ # Every reduction beyond `count` (which needs no field), each mapped to whether it admits
167
+ # only Integer (not Float) or a boolean field — mirrors
168
+ # Runtime::ReadModelInterpreter::INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS (ADR 0078).
169
+ AGGREGATION_FIELD_KEYS = %i[median_field sum_field avg_field min_field max_field percentile_field
170
+ any_field all_field].freeze
171
+ INTEGER_ONLY_AGGREGATION_FIELDS = %i[sum_field avg_field].freeze
172
+ BOOLEAN_AGGREGATION_FIELDS = %i[any_field all_field].freeze
173
+
174
+ # A reduction field's own eligibility, checked after where/order_by/limit since a
175
+ # reduction only reduces the already-filtered row set.
176
+ def aggregation_skip_reason(read_model, aggregates_by_name)
177
+ field_key = AGGREGATION_FIELD_KEYS.find { |key| read_model[key] }
178
+ return nil unless field_key
179
+
180
+ target = read_model[:aggregate_heads].find { |head| head[:many] }
181
+ aggregate = aggregates_by_name[target[:aggregate]]
182
+ word = field_key.to_s.delete_suffix("_field")
183
+ return entity_head_skip_reason(target[:aggregate], "#{word} target", "reduce") unless aggregate
184
+
185
+ value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name], vo] }
186
+ field = read_model[field_key].to_s
187
+ kind = query_field_kind(aggregate, field, value_objects_by_name)
188
+ return skip(field_key.to_s, "#{word} names #{field.inspect}, but #{target[:aggregate]} declares no such attribute — not generated yet") if kind == :unknown
189
+
190
+ if BOOLEAN_AGGREGATION_FIELDS.include?(field_key)
191
+ return nil if query_field_boolean?(aggregate, field, value_objects_by_name)
192
+
193
+ return skip(field_key.to_s, "#{word} names #{field.inspect} on #{target[:aggregate]}, which is not boolean — #{word} needs a " \
194
+ "true/false field — not generated yet")
195
+ end
196
+
197
+ return skip(field_key.to_s, "#{word} names #{field.inspect} on #{target[:aggregate]}, which is not numeric — #{word} needs a numeric " \
198
+ "field (a bare number, or a value object carrying one) — not generated yet") unless kind == :number
199
+
200
+ if INTEGER_ONLY_AGGREGATION_FIELDS.include?(field_key)
201
+ return nil if query_field_numeric_type(aggregate, field, value_objects_by_name) == "Integer"
202
+
203
+ return skip(field_key.to_s, "#{word} names #{field.inspect} on #{target[:aggregate]}, which is a Float field — #{word} admits " \
204
+ "only an Integer field (summing/averaging Floats cannot be made to agree, byte for byte, between " \
205
+ "Ruby and Rust) — not generated yet")
206
+ end
207
+
208
+ nil
209
+ end
210
+
211
+ # Whether a field (or dotted path) reduces specifically to a `TrueClass`/`FalseClass` —
212
+ # `any`/`all` need this, distinct from `query_field_kind`'s coarser `:other` bucket.
213
+ def query_field_boolean?(aggregate, field, value_objects_by_name)
214
+ segments = field.to_s.split(".")
215
+ attr = aggregate[:attributes].find { |a| a[:name].to_s == segments.first }
216
+ return false unless attr && !attr[:list]
217
+
218
+ query_type_boolean?(attr[:type].to_s, segments[1..] || [], value_objects_by_name)
219
+ end
220
+
221
+ def query_type_boolean?(type_name, segments, value_objects_by_name)
222
+ if segments.empty?
223
+ return true if %w[TrueClass FalseClass].include?(type_name)
224
+ return false if reference_type?(type_name)
225
+
226
+ vo = value_objects_by_name[type_name]
227
+ return false unless vo
228
+ return query_type_boolean?(vo[:attributes].first[:type].to_s, [], value_objects_by_name) if vo[:attributes].size == 1
229
+
230
+ false
231
+ else
232
+ vo = value_objects_by_name[type_name]
233
+ return false unless vo
234
+
235
+ member = vo[:attributes].find { |a| a[:name].to_s == segments.first }
236
+ return false if member.nil? || member[:list]
237
+
238
+ query_type_boolean?(member[:type].to_s, segments[1..] || [], value_objects_by_name)
239
+ end
240
+ end
241
+
242
+ # The winning member's own type name for a numeric field ("the sole numeric member wins",
243
+ # mirroring `query_vo_collapse_kind`) — "Integer" or "Float" distinctly, where
244
+ # `query_field_kind`'s own `:number` collapses both; `nil` for anything else. `sum`/`avg`
245
+ # need the distinction (ADR 0078).
246
+ def query_field_numeric_type(aggregate, field, value_objects_by_name)
247
+ segments = field.to_s.split(".")
248
+ attr = aggregate[:attributes].find { |a| a[:name].to_s == segments.first }
249
+ return nil unless attr && !attr[:list]
250
+
251
+ query_type_numeric_type(attr[:type].to_s, segments[1..] || [], value_objects_by_name)
252
+ end
253
+
254
+ def query_type_numeric_type(type_name, segments, value_objects_by_name)
255
+ if segments.empty?
256
+ return type_name if %w[Integer Float].include?(type_name)
257
+ return nil if reference_type?(type_name)
258
+
259
+ vo = value_objects_by_name[type_name]
260
+ return nil unless vo
261
+
262
+ numeric_member = vo[:attributes].find { |a| %w[Integer Float].include?(a[:type].to_s) }
263
+ return numeric_member[:type].to_s if numeric_member
264
+ return query_type_numeric_type(vo[:attributes].first[:type].to_s, [], value_objects_by_name) if vo[:attributes].size == 1
265
+
266
+ nil
267
+ else
268
+ vo = value_objects_by_name[type_name]
269
+ return nil unless vo
270
+
271
+ member = vo[:attributes].find { |a| a[:name].to_s == segments.first }
272
+ return nil if member.nil? || member[:list]
273
+
274
+ query_type_numeric_type(member[:type].to_s, segments[1..] || [], value_objects_by_name)
275
+ end
276
+ end
277
+
278
+ # Nested entity names across every aggregate (recursively) — an
279
+ # `include` naming one of these is generated as an empty head, since
280
+ # entities have no rows of their own to project.
281
+ def nested_entity_names(aggregates_by_name)
282
+ collect = lambda do |owners|
283
+ owners.flat_map { |owner| Array(owner[:entities]).flat_map { |entity| [entity[:name].to_s, *collect.call([entity])] } }
284
+ end
285
+ collect.call(aggregates_by_name.values)
286
+ end
287
+
288
+ def entity_head_skip_reason(aggregate_name, role, purpose)
289
+ skip("include_entity_head", "includes #{aggregate_name}, a nested entity, as the #{role} — an entity has no rows of its own " \
290
+ "(ReadModelInterpreter#records reads it as empty), so there is nothing to #{purpose} — not generated yet")
291
+ end
292
+
293
+ def read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
294
+ target = aggregates_by_name[head[:aggregate]]
295
+ unless target
296
+ return nil if nested_entity_names(aggregates_by_name).include?(head[:aggregate].to_s)
297
+
298
+ return skip("include_undeclared_aggregate", "includes #{head[:aggregate]}, which this domain never declares")
299
+ end
300
+ return skip("include_unsupported_aggregate", "includes #{head[:aggregate]}, which this generator couldn't itself generate " \
301
+ "(unsupported attribute type — see this domain's own aggregate-level manifest entry)") if unsupported_names.include?(head[:aggregate])
302
+
303
+ nil
304
+ end
305
+
306
+ # `group_by` eligibility, narrowed to the one shape the corpus declares:
307
+ # a single rootless head, group_by alone (build time already guarantees
308
+ # at most one many-side head whenever group_by is declared at all).
309
+ def group_by_skip_reason(read_model, aggregates_by_name, unsupported_names)
310
+ heads = read_model[:aggregate_heads]
311
+ return skip("group_by", "declares group_by across #{heads.size} aggregate heads — not generated yet (only a single, rootless head is)") if heads.size != 1
312
+ return skip("group_by", "declares group_by on a NON-rootless read model (reference_to #{read_model[:reference_target]}) — not generated yet") unless read_model[:reference_target].nil?
313
+ if read_model[:count] || AGGREGATION_FIELD_KEYS.any? { |key| read_model[key] }
314
+ return skip("group_by", "declares group_by alongside a reduction — not generated yet")
315
+ end
316
+ return skip("group_by", "declares group_by alongside where/order_by/limit/offset — not generated yet") if Array(read_model[:wheres]).any? || read_model[:order_by] || read_model[:limit] || read_model[:offset]
317
+ return skip("group_by", "declares group_by with an authorize policy — not generated yet") if read_model[:authorization]
318
+
319
+ head = heads.first
320
+ reason = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
321
+ return reason if reason
322
+
323
+ aggregate = aggregates_by_name[head[:aggregate]]
324
+ return entity_head_skip_reason(head[:aggregate], "group_by head", "group") unless aggregate
325
+ lifecycle_field = aggregate[:lifecycle] && aggregate[:lifecycle][:field].to_s
326
+ Array(read_model[:group_by]).each do |row|
327
+ field_s = row[:field].to_s
328
+ next if aggregate[:attributes].any? { |a| a[:name].to_s == field_s }
329
+ next if field_s == lifecycle_field
330
+
331
+ return skip("group_by", "group_by names #{field_s.inspect}, but #{aggregate[:name]} declares no such attribute — not generated yet")
332
+ end
333
+
334
+ nil
335
+ end
336
+
337
+ # Compiles already-confirmed-generable hop wheres into the wire shape
338
+ # ReferenceHopCondition expects.
339
+ def read_model_hop_conditions(domain_name, hop_wheres, aggregate, aggregates_by_name)
340
+ hop_wheres.map do |where|
341
+ hop = query_hop_plan(aggregate, where[:field].to_s, aggregates_by_name)
342
+ raw_value = where[:value].to_s
343
+ symbol = raw_value.start_with?(":")
344
+ {
345
+ via_field: hop[:via_field],
346
+ target_aggregate: "#{domain_name}::#{hop[:target_aggregate]}",
347
+ through: hop[:through].map { |step| { via_field: step[:via_field], target_aggregate: "#{domain_name}::#{step[:target_aggregate]}" } },
348
+ inner_field: hop[:inner_field],
349
+ op: where[:op].to_s,
350
+ arg: symbol ? raw_value.delete_prefix(":") : nil,
351
+ literal: symbol ? nil : Hecks::Literal.read(raw_value),
352
+ }
353
+ end
354
+ end
355
+
356
+ def emit_reference_hop_condition(hop)
357
+ comparator_expr = "crate::kernel::query_comparators::QueryComparator::#{query_comparator_variant(hop[:op])}"
358
+ through = Array(hop[:through]).map do |step|
359
+ "crate::kernel::read_model::HopStep { via_field: #{step[:via_field].inspect}, target_aggregate: #{step[:target_aggregate].inspect} }"
360
+ end
361
+ "crate::kernel::read_model::ReferenceHopCondition { via_field: #{hop[:via_field].inspect}, " \
362
+ "target_aggregate: #{hop[:target_aggregate].inspect}, through: &[#{through.join(', ')}], " \
363
+ "inner_field: #{hop[:inner_field].inspect}, " \
364
+ "inner_comparator: #{comparator_expr}, inner_value: #{emit_query_condition_value(hop)} },"
365
+ end
366
+
367
+ # Baked in once at codegen time — the compiled kernel has no runtime
368
+ # attribute-type reflection. Includes every reference field regardless
369
+ # of whether its target is a head here; the matcher simply won't match.
370
+ def read_model_reference_fields(head_aggregate)
371
+ head_aggregate[:attributes].filter_map do |attr|
372
+ target = reference_target(attr[:type])
373
+ next unless target
374
+
375
+ { target: target, field: attr[:name] }
376
+ end
377
+ end
378
+
379
+ def emit_reference_field(domain_name, reference_field)
380
+ qualified_target = "#{domain_name}::#{reference_field[:target]}"
381
+ "crate::kernel::read_model::ReferenceField { target_aggregate: #{qualified_target.inspect}, " \
382
+ "field: #{reference_field[:field].to_s.inspect} }"
383
+ end
384
+
385
+ # Compiles one declared `include`. `is_root` is precomputed here rather
386
+ # than re-compared at runtime; the root has no reference_fields since
387
+ # it's fetched directly by id, never matched by reference.
388
+ def emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
389
+ head_aggregate = aggregates_by_name[head[:aggregate]]
390
+ reference_fields = is_root || head_aggregate.nil? ? [] : read_model_reference_fields(head_aggregate)
391
+ reference_fields_expr = reference_fields.map { |rf| emit_reference_field(domain_name, rf) }.join(", ")
392
+ qualified_aggregate = "#{domain_name}::#{head[:aggregate]}"
393
+
394
+ "crate::kernel::read_model::ReadModelHead { aggregate: #{qualified_aggregate.inspect}, " \
395
+ "as_name: #{head[:as].to_s.inspect}, many: #{head[:many] ? 'true' : 'false'}, " \
396
+ "is_root: #{is_root ? 'true' : 'false'}, reference_fields: &[#{reference_fields_expr}] }"
397
+ end
398
+
399
+ # Compiles a declared order_by into Rust's ReadModelOrderBy; `descending`
400
+ # collapses the desc/asc string check once, at codegen time.
401
+ def emit_read_model_order_by(order_by, null_semantics = nil)
402
+ descending = order_by[:direction].to_s == "desc" ? "true" : "false"
403
+ "crate::kernel::read_model::ReadModelOrderBy { field: #{order_by[:field].to_s.inspect}, descending: #{descending}, " \
404
+ "nulls: #{null_semantics_variant(null_semantics)} }"
405
+ end
406
+
407
+ # Compiles a declared limit into a Literal or an Arg reference; a
408
+ # non-Arg value is already confirmed to be a real integer literal.
409
+ def emit_read_model_limit(limit)
410
+ raw = limit[:value].to_s
411
+ return "crate::kernel::read_model::ReadModelLimit::Arg(#{raw.delete_prefix(':').inspect})" if raw.start_with?(":")
412
+
413
+ "crate::kernel::read_model::ReadModelLimit::Literal(#{raw.to_i})"
414
+ end
415
+
416
+ # ReadModelOffset is a type alias of ReadModelLimit, so this reuses
417
+ # emit_read_model_limit and swaps only the spelled type name.
418
+ def emit_read_model_offset(offset) = emit_read_model_limit(offset).sub("read_model::ReadModelLimit::", "read_model::ReadModelOffset::")
419
+
420
+ # One targeted head's own where/order_by/offset/limit, compiled to plain
421
+ # data for `emit_filtered_head`. Hop wheres need a different wire shape
422
+ # (ReferenceHopCondition) than local ones, so they're split out here
423
+ # rather than reused wholesale; every clause has already been confirmed
424
+ # resolvable or generable by read_model_options_content_skip_reason.
425
+ def filtered_head_def(domain_name, read_model, head, many_heads, aggregates_by_name)
426
+ aggregate = aggregates_by_name[head[:aggregate]]
427
+ head_query = read_model_head_query(read_model, head, many_heads)
428
+ local_wheres, hop_wheres = head_query[:wheres].partition { |w| query_hop_plan(aggregate, w[:field].to_s, aggregates_by_name).nil? }
429
+ conditions_source = head_query.merge(wheres: local_wheres)
430
+
431
+ {
432
+ as_name: head[:as].to_s,
433
+ conditions: head_query[:authorization] ? query_conditions_with_authorization(conditions_source) : query_conditions(conditions_source),
434
+ reference_hop_conditions: read_model_hop_conditions(domain_name, hop_wheres, aggregate, aggregates_by_name),
435
+ order_by: head_query[:order_by] ? emit_read_model_order_by(head_query[:order_by], read_model[:null_semantics]) : nil,
436
+ offset: head_query[:offset] ? emit_read_model_offset(head_query[:offset]) : nil,
437
+ limit: head_query[:limit] ? emit_read_model_limit(head_query[:limit]) : nil,
438
+ }
439
+ end
440
+
441
+ # Compiles a whole declared read model to plain data for
442
+ # `emit_read_model_def`. `verb` uses the read model's declared name
443
+ # ("Domain.Name"), the same convention queries.rb picks for a named
444
+ # query — `kernel/cli.rs` tells the two shapes apart by the "::" that
445
+ # only a Domain::Aggregate.Name query verb has before the first ".".
446
+ #
447
+ # `filtered_heads` holds one entry per head `read_model_filtered_heads`
448
+ # names; skip_reason above already refused anything whose content wasn't
449
+ # generable. `authorization` (the `ReadModelDef`-level tenant-arg check)
450
+ # only ever applies to a single many-side head — `on:` doesn't extend to
451
+ # `tenant:` (ADR 0055's own scope limit).
452
+ def read_model_def(domain_name, read_model, aggregates_by_name)
453
+ heads = read_model[:aggregate_heads].map do |head|
454
+ is_root = head[:aggregate].to_s == read_model[:reference_target].to_s
455
+ emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
456
+ end
457
+
458
+ many_heads = read_model_many_heads(read_model)
459
+ filtered_heads = read_model_filtered_heads(read_model).map { |head| filtered_head_def(domain_name, read_model, head, many_heads, aggregates_by_name) }
460
+ sole_head_as = many_heads.size == 1 ? many_heads.first[:as] : nil
461
+ group_by_fn_name, group_by_fn_body = read_model_group_by_fn(read_model, aggregates_by_name)
462
+
463
+ {
464
+ verb: "#{domain_name}.#{read_model[:name]}",
465
+ reference_name: read_model[:reference_name] ? read_model[:reference_name].to_s : nil,
466
+ heads: heads,
467
+ filtered_heads: filtered_heads,
468
+ authorization: sole_head_as ? emit_query_authorization(read_model[:name], read_model[:authorization]) : nil,
469
+ group_by_fn: group_by_fn_name,
470
+ group_by_fn_body: group_by_fn_body,
471
+ count: !!read_model[:count],
472
+ median_field: read_model[:median_field] ? read_model[:median_field].to_s : nil,
473
+ sum_field: read_model[:sum_field] ? read_model[:sum_field].to_s : nil,
474
+ avg_field: read_model[:avg_field] ? read_model[:avg_field].to_s : nil,
475
+ min_field: read_model[:min_field] ? read_model[:min_field].to_s : nil,
476
+ max_field: read_model[:max_field] ? read_model[:max_field].to_s : nil,
477
+ percentile_field: read_model[:percentile_field] ? read_model[:percentile_field].to_s : nil,
478
+ percentile_at: read_model[:percentile_field] ? read_model[:percentile_at] : nil,
479
+ any_field: read_model[:any_field] ? read_model[:any_field].to_s : nil,
480
+ all_field: read_model[:all_field] ? read_model[:all_field].to_s : nil,
481
+ }
482
+ end
483
+
484
+ # The generated group_by transform fn's name and body, or `[nil, nil]` when this read model
485
+ # declares no `group_by` at all. Split out of `read_model_def` to keep its own complexity down.
486
+ def read_model_group_by_fn(read_model, aggregates_by_name)
487
+ group_by_fields = Array(read_model[:group_by]).map { |row| row[:field].to_s }
488
+ return [nil, nil] if group_by_fields.empty?
489
+
490
+ fn_name = "group_by_#{read_model[:name].to_s.downcase}"
491
+ root_aggregate = aggregates_by_name[read_model[:aggregate_heads].first[:aggregate]]
492
+ [fn_name, emit_group_by_transform(fn_name, read_model[:name].to_s, root_aggregate, group_by_fields)]
493
+ end
494
+
495
+ # Generates the group_by transform fn by name, since the kernel's own
496
+ # generic `run` only ever holds already-serialized Json — which value
497
+ # object unwraps to a bare scalar is a type-level fact only codegen has.
498
+ #
499
+ # It re-attaches `id`, keeps only real declared attributes plus any
500
+ # `projects` field while excluding Rust-only synthetic fields (ADR 0049),
501
+ # and recursively unwraps single-attribute value objects. Actual grouping
502
+ # is `kernel::read_model::nest`, which refuses a second row per leaf
503
+ # unless `group_by_leaf_check` found the key path covers the identity.
504
+ def emit_group_by_transform(fn_name, read_model_name, aggregate, group_by_fields)
505
+ value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name].to_s, vo] }
506
+ lifecycle_field = aggregate[:lifecycle] && aggregate[:lifecycle][:field].to_s
507
+ fields = aggregate[:attributes] + Projector.projected_field_pseudo_attributes(aggregate)
508
+ kept_keys = fields.map { |a| a[:name].to_s } + ["id"] + (lifecycle_field ? [lifecycle_field] : [])
509
+ keep_cond = kept_keys.map { |k| "k == #{k.inspect}" }.join(" || ")
510
+
511
+ arms = fields.map do |a|
512
+ unwrapped = unwrap_json_expr("v", a[:type].to_s, a[:list], aggregate, value_objects_by_name)
513
+ "#{a[:name].to_s.inspect} => #{unwrapped},"
514
+ end.join("\n ")
515
+
516
+ fields_literal = "&[#{group_by_fields.map(&:inspect).join(', ')}]"
517
+ leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields)
518
+
519
+ <<~RUST.rstrip
520
+ pub fn #{fn_name}(rows: Vec<(String, crate::kernel::Json)>) -> Result<crate::kernel::Json, crate::kernel::Refusal> {
521
+ let unwrapped: Vec<crate::kernel::Json> = rows
522
+ .into_iter()
523
+ .map(|(id, record)| {
524
+ let wrapped = crate::kernel::repository::row_json(id, record);
525
+ match wrapped {
526
+ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(
527
+ fields
528
+ .into_iter()
529
+ .filter(|(k, _)| #{keep_cond})
530
+ .map(|(k, v)| {
531
+ let new_v = match k.as_str() {
532
+ #{arms}
533
+ _ => v,
534
+ };
535
+ (k, new_v)
536
+ })
537
+ .collect(),
538
+ ),
539
+ other => other,
540
+ }
541
+ })
542
+ .collect();
543
+ crate::kernel::read_model::nest(unwrapped, #{fields_literal}, #{leaf_check})
544
+ }
545
+ RUST
546
+ end
547
+
548
+ # A key path naming every identity field of the grouped aggregate can't
549
+ # collide, so its leaves go unchecked; any other refuses a second row
550
+ # per leaf (ADR 0061). `identified_by` in the IR holds identity paths.
551
+ def group_by_leaf_check(read_model_name, aggregate, group_by_fields)
552
+ identity = Array(aggregate[:identified_by]).map { |path| path.to_s.split(".").first }.uniq
553
+ return "crate::kernel::read_model::LeafCheck::IdentityCovered" if identity.any? && (identity - group_by_fields).empty?
554
+
555
+ "crate::kernel::read_model::LeafCheck::RefuseCollision(#{read_model_name.inspect})"
556
+ end
557
+
558
+ # Ports Value.materialize_unwrapped: a single-attribute value object
559
+ # unwraps to its bare field; a multi-attribute one or an entity keeps
560
+ # its object shape but recurses into each field; scalars and lists
561
+ # (mapped element-wise) pass through the same logic unchanged.
562
+ def unwrap_json_expr(expr, type_name, list, aggregate, value_objects_by_name)
563
+ if list
564
+ inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name)
565
+ return "match #{expr} { crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| #{inner}).collect()), other => other }"
566
+ end
567
+
568
+ vo = value_objects_by_name[type_name]
569
+ entity = (aggregate[:entities] || []).find { |e| e[:name].to_s == type_name.to_s }
570
+ fields_meta = vo ? vo[:attributes] : (entity ? entity[:attributes] : nil)
571
+ return expr unless fields_meta
572
+
573
+ if vo && fields_meta.size == 1
574
+ field = fields_meta.first
575
+ unwrapped = unwrap_json_expr("field_value", field[:type].to_s, field[:list], aggregate, value_objects_by_name)
576
+ return "match #{expr} { crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == #{field[:name].to_s.inspect}).map(|(_, field_value)| #{unwrapped}).unwrap_or(crate::kernel::Json::Null), other => other }"
577
+ end
578
+
579
+ inner_arms = fields_meta.map do |f|
580
+ unwrapped = unwrap_json_expr("v", f[:type].to_s, f[:list], aggregate, value_objects_by_name)
581
+ "#{f[:name].to_s.inspect} => #{unwrapped},"
582
+ end.join(" ")
583
+ "match #{expr} { crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| { let new_v = match k.as_str() { #{inner_arms} _ => v }; (k, new_v) }).collect()), other => other }"
584
+ end
585
+
586
+ # One `FilteredHead` entry, always emitted as a single line — matching
587
+ # the codebase's own established idiom for every OTHER leaf struct
588
+ # (ReadModelHead, ReferenceField, ReferenceHopCondition, QueryCondition
589
+ # are all single-line too; only the top-level ReadModelDef/QueryDef get
590
+ # pretty multi-line formatting with their arrays broken across lines).
591
+ def emit_filtered_head(filtered_head_def)
592
+ conditions = filtered_head_def[:conditions].map { |c| emit_query_condition(c) }.join(" ")
593
+ reference_hop_conditions = filtered_head_def[:reference_hop_conditions].map { |h| emit_reference_hop_condition(h) }.join(" ")
594
+ order_by = filtered_head_def[:order_by] ? "Some(#{filtered_head_def[:order_by]})" : "None"
595
+ offset = filtered_head_def[:offset] ? "Some(#{filtered_head_def[:offset]})" : "None"
596
+ limit = filtered_head_def[:limit] ? "Some(#{filtered_head_def[:limit]})" : "None"
597
+
598
+ "crate::kernel::read_model::FilteredHead { as_name: #{filtered_head_def[:as_name].inspect}, " \
599
+ "conditions: &[#{conditions}], reference_hop_conditions: &[#{reference_hop_conditions}], " \
600
+ "order_by: #{order_by}, offset: #{offset}, limit: #{limit} },"
601
+ end
602
+
603
+ def emit_read_model_def(read_model_def)
604
+ heads = read_model_def[:heads].map { |head| " #{head}," }.join("\n")
605
+ filtered_heads = read_model_def[:filtered_heads].map { |fh| " #{emit_filtered_head(fh)}" }.join("\n")
606
+ authorization = read_model_def[:authorization] ? "Some(#{read_model_def[:authorization]})" : "None"
607
+ reference_name = read_model_def[:reference_name] ? "Some(#{read_model_def[:reference_name].inspect})" : "None"
608
+ group_by = read_model_def[:group_by_fn] ? "Some(#{read_model_def[:group_by_fn]})" : "None"
609
+ count = read_model_def[:count] ? "true" : "false"
610
+ median_field = read_model_def[:median_field] ? "Some(#{read_model_def[:median_field].inspect})" : "None"
611
+ sum_field = read_model_def[:sum_field] ? "Some(#{read_model_def[:sum_field].inspect})" : "None"
612
+ avg_field = read_model_def[:avg_field] ? "Some(#{read_model_def[:avg_field].inspect})" : "None"
613
+ min_field = read_model_def[:min_field] ? "Some(#{read_model_def[:min_field].inspect})" : "None"
614
+ max_field = read_model_def[:max_field] ? "Some(#{read_model_def[:max_field].inspect})" : "None"
615
+ percentile_field = read_model_def[:percentile_field] ? "Some(#{read_model_def[:percentile_field].inspect})" : "None"
616
+ percentile_at = read_model_def[:percentile_at] ? "Some(#{Float(read_model_def[:percentile_at])}_f64)" : "None"
617
+ any_field = read_model_def[:any_field] ? "Some(#{read_model_def[:any_field].inspect})" : "None"
618
+ all_field = read_model_def[:all_field] ? "Some(#{read_model_def[:all_field].inspect})" : "None"
619
+
620
+ <<~RUST.rstrip
621
+ crate::kernel::read_model::ReadModelDef {
622
+ verb: #{read_model_def[:verb].inspect},
623
+ reference_name: #{reference_name},
624
+ heads: &[
625
+ #{heads}
626
+ ],
627
+ filtered_heads: &[
628
+ #{filtered_heads}
629
+ ],
630
+ authorization: #{authorization},
631
+ group_by: #{group_by},
632
+ count: #{count},
633
+ median_field: #{median_field},
634
+ sum_field: #{sum_field},
635
+ avg_field: #{avg_field},
636
+ min_field: #{min_field},
637
+ max_field: #{max_field},
638
+ percentile_field: #{percentile_field},
639
+ percentile_at: #{percentile_at},
640
+ any_field: #{any_field},
641
+ all_field: #{all_field},
642
+ },
643
+ RUST
644
+ end
645
+
646
+ # Must dedent-match `rust/src/exemplar/read_models.rs`'s own
647
+ # TMPL:read_model_table placeholder row exactly — Exemplar.render
648
+ # substitutes by literal substring.
649
+ READ_MODEL_TABLE_ROW_PLACEHOLDER = <<~RUST.rstrip
650
+ crate::kernel::read_model::ReadModelDef {
651
+ verb: "tmpl_verb",
652
+ reference_name: Some("tmpl_reference_name"),
653
+ heads: &[
654
+ crate::kernel::read_model::ReadModelHead {
655
+ aggregate: "tmpl_aggregate",
656
+ as_name: "tmpl_as_name",
657
+ many: true,
658
+ is_root: false,
659
+ reference_fields: &[
660
+ crate::kernel::read_model::ReferenceField { target_aggregate: "tmpl_target_aggregate", field: "tmpl_field" },
661
+ ],
662
+ },
663
+ ],
664
+ filtered_heads: &[
665
+ crate::kernel::read_model::FilteredHead { as_name: "tmpl_as_name", conditions: &[crate::kernel::QueryCondition { field: "tmpl_field", comparator: crate::kernel::query_comparators::QueryComparator::Eq, value: crate::kernel::QueryConditionValue::Literal("tmpl_literal") },], reference_hop_conditions: &[crate::kernel::read_model::ReferenceHopCondition { via_field: "tmpl_via_field", target_aggregate: "tmpl_target_aggregate", through: &[crate::kernel::read_model::HopStep { via_field: "tmpl_via_field", target_aggregate: "tmpl_target_aggregate" }], inner_field: "tmpl_inner_field", inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq, inner_value: crate::kernel::QueryConditionValue::Literal("tmpl_literal") },], order_by: Some(crate::kernel::read_model::ReadModelOrderBy { field: "tmpl_order_field", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }), offset: Some(crate::kernel::read_model::ReadModelOffset::Literal(1)), limit: Some(crate::kernel::read_model::ReadModelLimit::Literal(5)) },
666
+ ],
667
+ authorization: Some(crate::kernel::named_query::TenantAuth { query_name: "tmpl_query_name", tenant_field: "tmpl_tenant_field", policy: "tmpl_policy" }),
668
+ group_by: None,
669
+ count: false,
670
+ median_field: None,
671
+ sum_field: None,
672
+ avg_field: None,
673
+ min_field: None,
674
+ max_field: None,
675
+ percentile_field: None,
676
+ percentile_at: None,
677
+ any_field: None,
678
+ all_field: None,
679
+ },
680
+ RUST
681
+
682
+ # The read model table `kernel::read_model::run` walks at runtime — one
683
+ # row per read model that `read_model_skip_reason` lets through; a
684
+ # skipped read model simply has no row here.
685
+ def emit_read_model_table(read_model_defs)
686
+ rows = read_model_defs.map { |rmd| emit_read_model_def(rmd) }
687
+ Exemplar.render("read_model_table", READ_MODEL_TABLE_ROW_PLACEHOLDER => rows.join("\n"))
688
+ end
689
+ end
690
+ end