hecks 3.13.0 → 4.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 (440) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -4,38 +4,92 @@
4
4
  use crate::exemplar::Exemplar;
5
5
  use crate::json::Json;
6
6
  use crate::queries;
7
+ use crate::shared::ruby_to_i;
7
8
  use crate::skip_reason::{reskip, skip, SkipReason};
8
9
  use std::collections::HashMap;
9
10
 
10
11
  const READ_MODEL_BARE_KEYS: &[&str] = &[
11
- "name", "description", "reference_name", "reference_target", "query_name", "aggregate_heads", "wheres", "order_by", "offset", "limit", "freshness", "index_hints", "group_by", "null_semantics", "authorization", "count", "median_field",
12
- "sum_field", "avg_field", "min_field", "max_field", "percentile_field", "percentile_at", "any_field", "all_field",
12
+ "name",
13
+ "description",
14
+ "reference_name",
15
+ "reference_target",
16
+ "query_name",
17
+ "aggregate_heads",
18
+ "wheres",
19
+ "order_by",
20
+ "offset",
21
+ "limit",
22
+ "freshness",
23
+ "index_hints",
24
+ "group_by",
25
+ "null_semantics",
26
+ "authorization",
27
+ "count",
28
+ "median_field",
29
+ "sum_field",
30
+ "avg_field",
31
+ "min_field",
32
+ "max_field",
33
+ "percentile_field",
34
+ "percentile_at",
35
+ "any_field",
36
+ "all_field",
13
37
  ];
14
38
 
15
39
  // Every reduction beyond `count` (which needs no field) — mirrors
16
40
  // Runtime::ReadModelInterpreter::REDUCTION_WORD/INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS
17
41
  // (ADR 0078).
18
- const AGGREGATION_FIELD_KEYS: &[&str] = &["median_field", "sum_field", "avg_field", "min_field", "max_field", "percentile_field", "any_field", "all_field"];
42
+ const AGGREGATION_FIELD_KEYS: &[&str] = &[
43
+ "median_field",
44
+ "sum_field",
45
+ "avg_field",
46
+ "min_field",
47
+ "max_field",
48
+ "percentile_field",
49
+ "any_field",
50
+ "all_field",
51
+ ];
19
52
  const INTEGER_ONLY_AGGREGATION_FIELDS: &[&str] = &["sum_field", "avg_field"];
20
53
  const BOOLEAN_AGGREGATION_FIELDS: &[&str] = &["any_field", "all_field"];
21
54
 
22
- pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
55
+ pub fn read_model_skip_reason(
56
+ read_model: &Json,
57
+ aggregates_by_name: &HashMap<String, &Json>,
58
+ unsupported_names: &[String],
59
+ ) -> Option<SkipReason> {
23
60
  let keys: Vec<&str> = match read_model {
24
- Json::Object(pairs) => pairs.iter().filter(|(_, v)| !matches!(v, Json::Null)).map(|(k, _)| k.as_str()).collect(),
61
+ Json::Object(pairs) => pairs
62
+ .iter()
63
+ .filter(|(_, v)| !matches!(v, Json::Null))
64
+ .map(|(k, _)| k.as_str())
65
+ .collect(),
25
66
  _ => Vec::new(),
26
67
  };
27
- let extra: Vec<&str> = keys.into_iter().filter(|k| !READ_MODEL_BARE_KEYS.contains(k)).collect();
68
+ let extra: Vec<&str> = keys
69
+ .into_iter()
70
+ .filter(|k| !READ_MODEL_BARE_KEYS.contains(k))
71
+ .collect();
28
72
  if !extra.is_empty() {
29
73
  return Some(read_model_options_skip_reason(&extra));
30
74
  }
31
75
 
32
- if read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
76
+ if read_model
77
+ .get("group_by")
78
+ .map_or_default(Json::each)
79
+ .iter()
80
+ .any(|_| true)
81
+ {
33
82
  return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names);
34
83
  }
35
84
 
36
- let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
37
- let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
38
- let root = heads.iter().find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
85
+ let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
86
+ let reference_target = read_model
87
+ .get("reference_target")
88
+ .map(Json::to_s)
89
+ .unwrap_or_default();
90
+ let root = heads
91
+ .iter()
92
+ .find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
39
93
  // Rootless (no `reference_to`) is generated — `read_models.rb`'s own
40
94
  // comment on this check.
41
95
  if root.is_none() && !reference_target.is_empty() {
@@ -45,13 +99,20 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
45
99
  ));
46
100
  }
47
101
 
48
- let root_name = root.and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
49
- if !aggregates_by_name.contains_key(&root_name) && nested_entity_names(aggregates_by_name).contains(&root_name) {
102
+ let root_name = root
103
+ .and_then(|h| h.get("aggregate"))
104
+ .map(Json::to_s)
105
+ .unwrap_or_default();
106
+ if !aggregates_by_name.contains_key(&root_name)
107
+ && nested_entity_names(aggregates_by_name).contains(&root_name)
108
+ {
50
109
  return Some(entity_head_skip_reason(&root_name, "root", "fetch by id"));
51
110
  }
52
111
 
53
112
  for head in heads {
54
- if let Some(reason) = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names) {
113
+ if let Some(reason) =
114
+ read_model_head_skip_reason(head, aggregates_by_name, unsupported_names)
115
+ {
55
116
  return Some(reason);
56
117
  }
57
118
  }
@@ -65,7 +126,7 @@ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<St
65
126
 
66
127
  fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
67
128
  let mut sorted: Vec<&str> = extra.to_vec();
68
- sorted.sort();
129
+ sorted.sort_unstable();
69
130
  skip(sorted[0], format!(
70
131
  "declares {} — out of scope for this generator: cursor/consistency/inspection are real capabilities Ports::Query::InMemory/Ports::Query::Ordering/TenantScope implement that this generator does not port (this file's own header has the full argument, the same boundary queries.rb already draws for a declared AGGREGATE query); freshness/use_index are never disqualifying on their own — neither is read by the in-memory interpreter path this kernel matches",
71
132
  sorted.join(", ")
@@ -74,7 +135,12 @@ fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
74
135
 
75
136
  /// Every many-side head this read model declares.
76
137
  fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
77
- read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]).iter().filter(|h| h.get("many").map(Json::as_bool).unwrap_or(false)).collect()
138
+ read_model
139
+ .get("aggregate_heads")
140
+ .map_or_default(Json::each)
141
+ .iter()
142
+ .filter(|h| h.get("many").is_some_and(Json::as_bool))
143
+ .collect()
78
144
  }
79
145
 
80
146
  /// Whether one declared option (its own `:target`, or `None` when untargeted) applies to
@@ -84,10 +150,13 @@ fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
84
150
  /// reaching codegen at all is proof there's exactly one to mean.
85
151
  fn option_target_matches_head(target: Option<&str>, head: &Json, many_heads: &[&Json]) -> bool {
86
152
  let head_as = head.get("as").map(Json::to_s).unwrap_or_default();
87
- match target {
88
- None => many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as,
89
- Some(target) => target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
90
- }
153
+ target.map_or_else(
154
+ || {
155
+ many_heads.len() == 1
156
+ && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as
157
+ },
158
+ |target| target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
159
+ )
91
160
  }
92
161
 
93
162
  fn json_target(option: &Json) -> Option<String> {
@@ -98,8 +167,11 @@ fn json_target(option: &Json) -> Option<String> {
98
167
  /// many-side head only, a declared `authorize ..., tenant:` — `on:` doesn't extend to
99
168
  /// `tenant:`) targeting it.
100
169
  fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[&Json]) -> bool {
101
- let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]);
102
- if wheres.iter().any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)) {
170
+ let wheres = read_model.get("wheres").map_or_default(Json::each);
171
+ if wheres
172
+ .iter()
173
+ .any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
174
+ {
103
175
  return true;
104
176
  }
105
177
  if let Some(order_by) = read_model.get("order_by") {
@@ -117,8 +189,14 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
117
189
  return true;
118
190
  }
119
191
  }
120
- if read_model.get("authorization").and_then(|a| a.get("tenant")).is_some() && many_heads.len() == 1 {
121
- return many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default();
192
+ if read_model
193
+ .get("authorization")
194
+ .and_then(|a| a.get("tenant"))
195
+ .is_some()
196
+ && many_heads.len() == 1
197
+ {
198
+ return many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
199
+ == head.get("as").map(Json::to_s).unwrap_or_default();
122
200
  }
123
201
  false
124
202
  }
@@ -127,7 +205,11 @@ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[
127
205
  /// declared order — one entry when there's a single many-side head (the pre-ADR-0055 shape),
128
206
  /// more than one only when `on:` targets several.
129
207
  fn read_model_filtered_heads<'a>(read_model: &Json, many_heads: &[&'a Json]) -> Vec<&'a Json> {
130
- many_heads.iter().copied().filter(|head| read_model_option_targets_head(read_model, head, many_heads)).collect()
208
+ many_heads
209
+ .iter()
210
+ .copied()
211
+ .filter(|head| read_model_option_targets_head(read_model, head, many_heads))
212
+ .collect()
131
213
  }
132
214
 
133
215
  /// One targeted head's own where/order_by/offset/limit, pulled out of the read model's full
@@ -141,31 +223,73 @@ struct HeadQuery<'a> {
141
223
  authorization: Option<&'a Json>,
142
224
  }
143
225
 
144
- fn read_model_head_query<'a>(read_model: &'a Json, head: &Json, many_heads: &[&Json]) -> HeadQuery<'a> {
145
- let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)).collect();
146
- let order_by = read_model.get("order_by").filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
147
- let limit = read_model.get("limit").filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
148
- let offset = read_model.get("offset").filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
149
- let authorization = if many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default() {
226
+ fn read_model_head_query<'a>(
227
+ read_model: &'a Json,
228
+ head: &Json,
229
+ many_heads: &[&Json],
230
+ ) -> HeadQuery<'a> {
231
+ let wheres = read_model
232
+ .get("wheres")
233
+ .map_or_default(Json::each)
234
+ .iter()
235
+ .filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads))
236
+ .collect();
237
+ let order_by = read_model
238
+ .get("order_by")
239
+ .filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
240
+ let limit = read_model
241
+ .get("limit")
242
+ .filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
243
+ let offset = read_model
244
+ .get("offset")
245
+ .filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
246
+ let authorization = if many_heads.len() == 1
247
+ && many_heads[0].get("as").map(Json::to_s).unwrap_or_default()
248
+ == head.get("as").map(Json::to_s).unwrap_or_default()
249
+ {
150
250
  read_model.get("authorization")
151
251
  } else {
152
252
  None
153
253
  };
154
- HeadQuery { wheres, order_by, limit, offset, authorization }
254
+ HeadQuery {
255
+ wheres,
256
+ order_by,
257
+ limit,
258
+ offset,
259
+ authorization,
260
+ }
155
261
  }
156
262
 
157
263
  /// Checks every targeted head's own where/order_by/limit for generability against ITS
158
264
  /// aggregate (not the read model's root); `None` means clean.
159
- fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
265
+ fn read_model_options_content_skip_reason(
266
+ read_model: &Json,
267
+ aggregates_by_name: &HashMap<String, &Json>,
268
+ ) -> Option<SkipReason> {
160
269
  let many_heads = read_model_many_heads(read_model);
161
270
 
162
271
  for head in read_model_filtered_heads(read_model, &many_heads) {
163
272
  let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
164
273
  let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
165
- return Some(entity_head_skip_reason(&aggregate_name, "filtered head", "filter, order or authorize"));
274
+ return Some(entity_head_skip_reason(
275
+ &aggregate_name,
276
+ "filtered head",
277
+ "filter, order or authorize",
278
+ ));
166
279
  };
167
- let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
168
- let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
280
+ let vos = aggregate.get("value_objects").map_or_default(Json::each);
281
+ let value_objects_by_name: HashMap<String, &Json> = vos
282
+ .iter()
283
+ .map(|vo| {
284
+ (
285
+ vo.get("name")
286
+ .and_then(Json::as_str)
287
+ .unwrap_or("")
288
+ .to_string(),
289
+ vo,
290
+ )
291
+ })
292
+ .collect();
169
293
  let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
170
294
  let head_label = format!("head {aggregate_name} (as {as_name})");
171
295
  let head_query = read_model_head_query(read_model, head, &many_heads);
@@ -173,7 +297,10 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
173
297
  // A hop through a reference is generated when `query_hop_plan` resolves
174
298
  // it — `read_models.rb`'s own loop, check for check.
175
299
  for where_clause in head_query.wheres.iter().copied() {
176
- let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
300
+ let field = where_clause
301
+ .get("field")
302
+ .map(Json::to_s)
303
+ .unwrap_or_default();
177
304
  let hop = queries::query_hop_plan(aggregate, &field, aggregates_by_name);
178
305
  if hop.is_none() && field.contains('/') {
179
306
  return Some(skip(
@@ -187,22 +314,42 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
187
314
 
188
315
  if let Some(plan) = hop {
189
316
  let target_value_objects_by_name = queries::value_objects_of(plan.target);
190
- if let Some(reason) = queries::query_where_skip_reason(&queries::with_field(where_clause, &plan.inner_field), plan.target, &target_value_objects_by_name) {
191
- return Some(reskip(&reason, format!("{head_label}'s own hop through {} to {}'s own {reason}", plan.via_field, plan.target_aggregate)));
317
+ if let Some(reason) = queries::query_where_skip_reason(
318
+ &queries::with_field(where_clause, &plan.inner_field),
319
+ plan.target,
320
+ &target_value_objects_by_name,
321
+ ) {
322
+ return Some(reskip(
323
+ &reason,
324
+ format!(
325
+ "{head_label}'s own hop through {} to {}'s own {reason}",
326
+ plan.via_field, plan.target_aggregate
327
+ ),
328
+ ));
192
329
  }
193
330
  continue;
194
331
  }
195
332
 
196
- if let Some(reason) = queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name) {
333
+ if let Some(reason) =
334
+ queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name)
335
+ {
197
336
  return Some(reskip(&reason, format!("{head_label}'s own {reason}")));
198
337
  }
199
338
  }
200
339
 
201
- if let Some(reason) = queries::declared_authorization_skip_reason(head_query.authorization, aggregate, &value_objects_by_name) {
340
+ if let Some(reason) = queries::declared_authorization_skip_reason(
341
+ head_query.authorization,
342
+ aggregate,
343
+ &value_objects_by_name,
344
+ ) {
202
345
  return Some(reason);
203
346
  }
204
347
 
205
- if let Some(reason) = queries::declared_order_by_skip_reason(head_query.order_by, aggregate, &value_objects_by_name) {
348
+ if let Some(reason) = queries::declared_order_by_skip_reason(
349
+ head_query.order_by,
350
+ aggregate,
351
+ &value_objects_by_name,
352
+ ) {
206
353
  return Some(reason);
207
354
  }
208
355
 
@@ -221,19 +368,45 @@ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name:
221
368
  // `seal_aggregation` (Ruby, build time) already guarantees exactly one
222
369
  // many-side head and mutual exclusion with `group_by` — only the one declared
223
370
  // reduction's own field needs checking here (ADR 0078).
224
- fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
225
- let field_key = AGGREGATION_FIELD_KEYS.iter().find(|key| read_model.get(**key).is_some())?;
226
- let field = Json::to_s(read_model.get(*field_key).expect("checked by find above"));
371
+ #[allow(
372
+ clippy::expect_used,
373
+ reason = "the message names an invariant an earlier check in this generator established"
374
+ )]
375
+ fn aggregation_skip_reason(
376
+ read_model: &Json,
377
+ aggregates_by_name: &HashMap<String, &Json>,
378
+ ) -> Option<SkipReason> {
379
+ let field_key = AGGREGATION_FIELD_KEYS
380
+ .iter()
381
+ .find(|key| read_model.get(key).is_some())?;
382
+ let field = Json::to_s(read_model.get(field_key).expect("checked by find above"));
227
383
  let word = field_key.strip_suffix("_field").unwrap_or(field_key);
228
384
 
229
- let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
230
- let target = heads.iter().find(|h| h.get("many").map(Json::as_bool).unwrap_or(false))?;
385
+ let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
386
+ let target = heads
387
+ .iter()
388
+ .find(|h| h.get("many").is_some_and(Json::as_bool))?;
231
389
  let aggregate_name = target.get("aggregate").map(Json::to_s).unwrap_or_default();
232
390
  let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
233
- return Some(entity_head_skip_reason(&aggregate_name, &format!("{word} target"), "reduce"));
391
+ return Some(entity_head_skip_reason(
392
+ &aggregate_name,
393
+ &format!("{word} target"),
394
+ "reduce",
395
+ ));
234
396
  };
235
- let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
236
- let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
397
+ let vos = aggregate.get("value_objects").map_or_default(Json::each);
398
+ let value_objects_by_name: HashMap<String, &Json> = vos
399
+ .iter()
400
+ .map(|vo| {
401
+ (
402
+ vo.get("name")
403
+ .and_then(Json::as_str)
404
+ .unwrap_or("")
405
+ .to_string(),
406
+ vo,
407
+ )
408
+ })
409
+ .collect();
237
410
 
238
411
  let kind = queries::query_field_kind(aggregate, &field, &value_objects_by_name);
239
412
  if kind == queries::FieldKind::Unknown {
@@ -255,13 +428,14 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
255
428
  ));
256
429
  }
257
430
 
258
- if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key) {
259
- if queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref() != Some("Integer") {
260
- return Some(skip(
431
+ if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key)
432
+ && queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref()
433
+ != Some("Integer")
434
+ {
435
+ return Some(skip(
261
436
  *field_key,
262
437
  format!("{word} names {field:?} on {aggregate_name}, which is a Float field — {word} admits only an Integer field (summing/averaging Floats cannot be made to agree, byte for byte, between Ruby and Rust) — not generated yet"),
263
438
  ));
264
- }
265
439
  }
266
440
 
267
441
  None
@@ -269,8 +443,12 @@ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<Strin
269
443
 
270
444
  // Mirrors the retired Ruby generator's `read_models.rb`'s `group_by_skip_reason`: the one
271
445
  // shape the corpus declares — a single rootless head, group_by alone.
272
- fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
273
- let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
446
+ fn group_by_skip_reason(
447
+ read_model: &Json,
448
+ aggregates_by_name: &HashMap<String, &Json>,
449
+ unsupported_names: &[String],
450
+ ) -> Option<SkipReason> {
451
+ let heads = read_model.get("aggregate_heads").map_or_default(Json::each);
274
452
  if heads.len() != 1 {
275
453
  return Some(skip("group_by", format!("declares group_by across {} aggregate heads — not generated yet (only a single, rootless head is)", heads.len())));
276
454
  }
@@ -278,15 +456,36 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
278
456
  if reference_target.is_some() && !matches!(reference_target, Some(Json::Null)) {
279
457
  return Some(skip("group_by", format!("declares group_by on a NON-rootless read model (reference_to {}) — not generated yet", reference_target.map(Json::to_s).unwrap_or_default())));
280
458
  }
281
- if read_model.get("count").is_some() || AGGREGATION_FIELD_KEYS.iter().any(|key| read_model.get(*key).is_some()) {
282
- return Some(skip("group_by", "declares group_by alongside a reduction — not generated yet"));
459
+ if read_model.get("count").is_some()
460
+ || AGGREGATION_FIELD_KEYS
461
+ .iter()
462
+ .any(|key| read_model.get(key).is_some())
463
+ {
464
+ return Some(skip(
465
+ "group_by",
466
+ "declares group_by alongside a reduction — not generated yet",
467
+ ));
283
468
  }
284
- let has_wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().any(|_| true);
285
- if has_wheres || read_model.get("order_by").is_some() || read_model.get("limit").is_some() || read_model.get("offset").is_some() {
286
- return Some(skip("group_by", "declares group_by alongside where/order_by/limit/offset — not generated yet"));
469
+ let has_wheres = read_model
470
+ .get("wheres")
471
+ .map_or_default(Json::each)
472
+ .iter()
473
+ .any(|_| true);
474
+ if has_wheres
475
+ || read_model.get("order_by").is_some()
476
+ || read_model.get("limit").is_some()
477
+ || read_model.get("offset").is_some()
478
+ {
479
+ return Some(skip(
480
+ "group_by",
481
+ "declares group_by alongside where/order_by/limit/offset — not generated yet",
482
+ ));
287
483
  }
288
484
  if read_model.get("authorization").is_some() {
289
- return Some(skip("group_by", "declares group_by with an authorize policy — not generated yet"));
485
+ return Some(skip(
486
+ "group_by",
487
+ "declares group_by with an authorize policy — not generated yet",
488
+ ));
290
489
  }
291
490
 
292
491
  let head = &heads[0];
@@ -296,11 +495,18 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
296
495
 
297
496
  let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
298
497
  let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
299
- return Some(entity_head_skip_reason(&aggregate_name, "group_by head", "group"));
498
+ return Some(entity_head_skip_reason(
499
+ &aggregate_name,
500
+ "group_by head",
501
+ "group",
502
+ ));
300
503
  };
301
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
302
- let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
303
- for row in read_model.get("group_by").map(Json::each).unwrap_or(&[]) {
504
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
505
+ let lifecycle_field = aggregate
506
+ .get("lifecycle")
507
+ .and_then(|l| l.get("field"))
508
+ .map(Json::to_s);
509
+ for row in read_model.get("group_by").map_or_default(Json::each) {
304
510
  let field = row.get("field").map(Json::to_s).unwrap_or_default();
305
511
  let is_attr = attrs.iter().any(|a| crate::attr::name(a) == field);
306
512
  let is_lifecycle = lifecycle_field.as_deref() == Some(field.as_str());
@@ -314,7 +520,7 @@ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String,
314
520
  // Every entity name nested at any depth under a declared aggregate.
315
521
  fn nested_entity_names(aggregates_by_name: &HashMap<String, &Json>) -> Vec<String> {
316
522
  fn collect(owner: &Json, out: &mut Vec<String>) {
317
- for entity in owner.get("entities").map(Json::each).unwrap_or(&[]) {
523
+ for entity in owner.get("entities").map_or_default(Json::each) {
318
524
  out.push(entity.get("name").map(Json::to_s).unwrap_or_default());
319
525
  collect(entity, out);
320
526
  }
@@ -334,13 +540,20 @@ fn entity_head_skip_reason(aggregate_name: &str, role: &str, purpose: &str) -> S
334
540
  )
335
541
  }
336
542
 
337
- fn read_model_head_skip_reason(head: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
543
+ fn read_model_head_skip_reason(
544
+ head: &Json,
545
+ aggregates_by_name: &HashMap<String, &Json>,
546
+ unsupported_names: &[String],
547
+ ) -> Option<SkipReason> {
338
548
  let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
339
549
  if !aggregates_by_name.contains_key(&aggregate_name) {
340
550
  if nested_entity_names(aggregates_by_name).contains(&aggregate_name) {
341
551
  return None;
342
552
  }
343
- return Some(skip("include_undeclared_aggregate", format!("includes {aggregate_name}, which this domain never declares")));
553
+ return Some(skip(
554
+ "include_undeclared_aggregate",
555
+ format!("includes {aggregate_name}, which this domain never declares"),
556
+ ));
344
557
  }
345
558
  if unsupported_names.contains(&aggregate_name) {
346
559
  return Some(skip(
@@ -357,28 +570,51 @@ pub struct ReferenceField {
357
570
  }
358
571
 
359
572
  fn read_model_reference_fields(head_aggregate: &Json) -> Vec<ReferenceField> {
360
- let attrs = head_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
573
+ let attrs = head_aggregate.get("attributes").map_or_default(Json::each);
361
574
  attrs
362
575
  .iter()
363
576
  .filter_map(|attr| {
364
577
  let target = crate::naming::reference_target(crate::attr::type_name(attr))?;
365
- Some(ReferenceField { target: target.to_string(), field: crate::attr::name(attr).to_string() })
578
+ Some(ReferenceField {
579
+ target: target.to_string(),
580
+ field: crate::attr::name(attr).to_string(),
581
+ })
366
582
  })
367
583
  .collect()
368
584
  }
369
585
 
370
586
  fn emit_reference_field(domain_name: &str, rf: &ReferenceField) -> String {
371
587
  let qualified_target = format!("{domain_name}::{}", rf.target);
372
- format!("crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}", crate::naming::ruby_inspect_string(&qualified_target), crate::naming::ruby_inspect_string(&rf.field))
588
+ format!(
589
+ "crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}",
590
+ crate::naming::ruby_inspect_string(&qualified_target),
591
+ crate::naming::ruby_inspect_string(&rf.field)
592
+ )
373
593
  }
374
594
 
375
- fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregates_by_name: &HashMap<String, &Json>) -> String {
595
+ fn emit_read_model_head(
596
+ domain_name: &str,
597
+ head: &Json,
598
+ is_root: bool,
599
+ aggregates_by_name: &HashMap<String, &Json>,
600
+ ) -> String {
376
601
  let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
377
- let reference_fields: Vec<ReferenceField> = if is_root { Vec::new() } else { aggregates_by_name.get(&aggregate_name).map(|a| read_model_reference_fields(a)).unwrap_or_default() };
378
- let reference_fields_expr = reference_fields.iter().map(|rf| emit_reference_field(domain_name, rf)).collect::<Vec<_>>().join(", ");
602
+ let reference_fields: Vec<ReferenceField> = if is_root {
603
+ Vec::new()
604
+ } else {
605
+ aggregates_by_name
606
+ .get(&aggregate_name)
607
+ .map(|a| read_model_reference_fields(a))
608
+ .unwrap_or_default()
609
+ };
610
+ let reference_fields_expr = reference_fields
611
+ .iter()
612
+ .map(|rf| emit_reference_field(domain_name, rf))
613
+ .collect::<Vec<_>>()
614
+ .join(", ");
379
615
  let qualified_aggregate = format!("{domain_name}::{aggregate_name}");
380
616
  let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
381
- let many = head.get("many").map(Json::as_bool).unwrap_or(false);
617
+ let many = head.get("many").is_some_and(Json::as_bool);
382
618
 
383
619
  format!(
384
620
  "crate::kernel::read_model::ReadModelHead {{ aggregate: {}, as_name: {}, many: {many}, is_root: {is_root}, reference_fields: &[{reference_fields_expr}] }}",
@@ -388,7 +624,11 @@ fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregate
388
624
  }
389
625
 
390
626
  fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
391
- let descending = order_by.get("direction").map(Json::to_s).unwrap_or_default() == "desc";
627
+ let descending = order_by
628
+ .get("direction")
629
+ .map(Json::to_s)
630
+ .unwrap_or_default()
631
+ == "desc";
392
632
  format!(
393
633
  "crate::kernel::read_model::ReadModelOrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
394
634
  crate::naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
@@ -399,32 +639,24 @@ fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> S
399
639
  fn emit_read_model_limit(limit: &Json) -> String {
400
640
  let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
401
641
  if let Some(arg) = raw.strip_prefix(':') {
402
- return format!("crate::kernel::read_model::ReadModelLimit::Arg({})", crate::naming::ruby_inspect_string(arg));
642
+ return format!(
643
+ "crate::kernel::read_model::ReadModelLimit::Arg({})",
644
+ crate::naming::ruby_inspect_string(arg)
645
+ );
403
646
  }
404
- format!("crate::kernel::read_model::ReadModelLimit::Literal({})", ruby_to_i(&raw))
647
+ format!(
648
+ "crate::kernel::read_model::ReadModelLimit::Literal({})",
649
+ ruby_to_i(&raw)
650
+ )
405
651
  }
406
652
 
407
653
  // `ReadModelOffset` is a type alias for `ReadModelLimit`'s own type, so this
408
654
  // reuses `emit_read_model_limit`'s computation and swaps the type name.
409
655
  fn emit_read_model_offset(offset: &Json) -> String {
410
- emit_read_model_limit(offset).replace("read_model::ReadModelLimit::", "read_model::ReadModelOffset::")
411
- }
412
-
413
- fn ruby_to_i(s: &str) -> i64 {
414
- let trimmed = s.trim_start();
415
- let bytes = trimmed.as_bytes();
416
- let mut end = 0;
417
- if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
418
- end += 1;
419
- }
420
- let digits_start = end;
421
- while end < bytes.len() && bytes[end].is_ascii_digit() {
422
- end += 1;
423
- }
424
- if end == digits_start {
425
- return 0;
426
- }
427
- trimmed[..end].parse().unwrap_or(0)
656
+ emit_read_model_limit(offset).replace(
657
+ "read_model::ReadModelLimit::",
658
+ "read_model::ReadModelOffset::",
659
+ )
428
660
  }
429
661
 
430
662
  /// One targeted many-side head's own where/order_by/offset/limit — the per-head analogue of
@@ -447,64 +679,155 @@ pub struct ReadModelDef {
447
679
  pub authorization: Option<String>,
448
680
  pub group_by_fn: Option<String>,
449
681
  pub group_by_fn_body: Option<String>,
450
- pub count: bool,
451
- pub median_field: Option<String>,
452
- pub sum_field: Option<String>,
453
- pub avg_field: Option<String>,
454
- pub min_field: Option<String>,
455
- pub max_field: Option<String>,
456
- pub percentile_field: Option<String>,
457
- pub percentile_at: Option<String>,
458
- pub any_field: Option<String>,
459
- pub all_field: Option<String>,
682
+ /// The kernel's `Reduction` variant this read model declares, as the expression the
683
+ /// generated table holds (`Sum("total")`); `None` when it declares none.
684
+ pub reduction: Option<String>,
685
+ }
686
+
687
+ /// The reduction a read model's IR declares, in the kernel's own precedence: `count`, then the
688
+ /// `<word> :field` reductions.
689
+ #[allow(
690
+ clippy::panic,
691
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
692
+ )]
693
+ fn reduction_of(read_model: &Json) -> Option<String> {
694
+ if read_model.get("count").is_some() {
695
+ return Some("Count".to_string());
696
+ }
697
+ let field = |key: &str| {
698
+ read_model
699
+ .get(key)
700
+ .map(|v| crate::naming::ruby_inspect_string(&v.to_s()))
701
+ };
702
+ let single = [
703
+ ("median_field", "Median"),
704
+ ("sum_field", "Sum"),
705
+ ("avg_field", "Avg"),
706
+ ("min_field", "Min"),
707
+ ("max_field", "Max"),
708
+ ];
709
+ if let Some(found) = single
710
+ .iter()
711
+ .find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
712
+ {
713
+ return Some(found);
714
+ }
715
+ if let Some(f) = field("percentile_field") {
716
+ let Some(at) = read_model.get("percentile_at").map(Json::to_s) else {
717
+ panic!("percentile_field {f} declares no percentile_at");
718
+ };
719
+ return Some(format!("Percentile {{ field: {f}, at: {at}_f64 }}"));
720
+ }
721
+ [("any_field", "Any"), ("all_field", "All")]
722
+ .iter()
723
+ .find_map(|(key, variant)| field(key).map(|f| format!("{variant}({f})")))
460
724
  }
461
725
 
462
726
  /// `local_wheres, hop_wheres` compiled for one targeted head, mirroring
463
727
  /// `read_models.rb#filtered_head_def`.
464
- fn filtered_head_def(domain_name: &str, read_model: &Json, head: &Json, many_heads: &[&Json], aggregates_by_name: &HashMap<String, &Json>) -> FilteredHeadDef {
728
+ fn filtered_head_def(
729
+ domain_name: &str,
730
+ read_model: &Json,
731
+ head: &Json,
732
+ many_heads: &[&Json],
733
+ aggregates_by_name: &HashMap<String, &Json>,
734
+ ) -> FilteredHeadDef {
465
735
  let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
466
736
  let aggregate = aggregates_by_name[&aggregate_name];
467
737
  let head_query = read_model_head_query(read_model, head, many_heads);
468
738
  let (local_wheres, hop_wheres): (Vec<&Json>, Vec<&Json>) =
469
- head_query.wheres.iter().copied().partition(|w| queries::query_hop_plan(aggregate, &w.get("field").map(Json::to_s).unwrap_or_default(), aggregates_by_name).is_none());
739
+ head_query.wheres.iter().copied().partition(|w| {
740
+ queries::query_hop_plan(
741
+ aggregate,
742
+ &w.get("field").map(Json::to_s).unwrap_or_default(),
743
+ aggregates_by_name,
744
+ )
745
+ .is_none()
746
+ });
470
747
  let synthetic = with_wheres_and_authorization(&local_wheres, head_query.authorization);
471
- let conditions = if head_query.authorization.is_some() { queries::query_conditions_with_authorization(&synthetic) } else { queries::query_conditions(&synthetic) };
748
+ let conditions = if head_query.authorization.is_some() {
749
+ queries::query_conditions_with_authorization(&synthetic)
750
+ } else {
751
+ queries::query_conditions(&synthetic)
752
+ };
472
753
 
473
754
  FilteredHeadDef {
474
755
  as_name: head.get("as").map(Json::to_s).unwrap_or_default(),
475
756
  conditions,
476
- reference_hop_conditions: queries::read_model_hop_conditions(domain_name, &hop_wheres, aggregate, aggregates_by_name),
477
- order_by: head_query.order_by.map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
757
+ reference_hop_conditions: queries::read_model_hop_conditions(
758
+ domain_name,
759
+ &hop_wheres,
760
+ aggregate,
761
+ aggregates_by_name,
762
+ ),
763
+ order_by: head_query
764
+ .order_by
765
+ .map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
478
766
  offset: head_query.offset.map(emit_read_model_offset),
479
767
  limit: head_query.limit.map(emit_read_model_limit),
480
768
  }
481
769
  }
482
770
 
483
- pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> ReadModelDef {
484
- let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
485
- let heads_json = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
771
+ pub fn read_model_def(
772
+ domain_name: &str,
773
+ read_model: &Json,
774
+ aggregates_by_name: &HashMap<String, &Json>,
775
+ ) -> ReadModelDef {
776
+ let reference_target = read_model
777
+ .get("reference_target")
778
+ .map(Json::to_s)
779
+ .unwrap_or_default();
780
+ let heads_json = read_model.get("aggregate_heads").map_or_default(Json::each);
486
781
  let heads: Vec<String> = heads_json
487
782
  .iter()
488
783
  .map(|head| {
489
- let is_root = head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
784
+ let is_root =
785
+ head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
490
786
  emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
491
787
  })
492
788
  .collect();
493
789
 
494
790
  let many_heads = read_model_many_heads(read_model);
495
- let filtered_heads: Vec<FilteredHeadDef> =
496
- read_model_filtered_heads(read_model, &many_heads).into_iter().map(|head| filtered_head_def(domain_name, read_model, head, &many_heads, aggregates_by_name)).collect();
791
+ let filtered_heads: Vec<FilteredHeadDef> = read_model_filtered_heads(read_model, &many_heads)
792
+ .into_iter()
793
+ .map(|head| {
794
+ filtered_head_def(
795
+ domain_name,
796
+ read_model,
797
+ head,
798
+ &many_heads,
799
+ aggregates_by_name,
800
+ )
801
+ })
802
+ .collect();
497
803
  let sole_head_as: bool = many_heads.len() == 1;
498
804
 
499
805
  let read_model_name = read_model.get("name").map(Json::to_s).unwrap_or_default();
500
- let group_by_fields: Vec<String> = read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().map(|row| row.get("field").map(Json::to_s).unwrap_or_default()).collect();
501
- let (group_by_fn, group_by_fn_body) = if !group_by_fields.is_empty() {
806
+ let group_by_fields: Vec<String> = read_model
807
+ .get("group_by")
808
+ .map_or_default(Json::each)
809
+ .iter()
810
+ .map(|row| row.get("field").map(Json::to_s).unwrap_or_default())
811
+ .collect();
812
+ let (group_by_fn, group_by_fn_body) = if group_by_fields.is_empty() {
813
+ (None, None)
814
+ } else {
502
815
  let fn_name = format!("group_by_{}", read_model_name.to_lowercase());
503
- let aggregate_name = heads_json.first().and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
816
+ let aggregate_name = heads_json
817
+ .first()
818
+ .and_then(|h| h.get("aggregate"))
819
+ .map(Json::to_s)
820
+ .unwrap_or_default();
504
821
  let aggregate = aggregates_by_name[&aggregate_name];
505
- (Some(fn_name.clone()), Some(emit_group_by_transform(&fn_name, &read_model_name, aggregate, &group_by_fields)))
506
- } else {
507
- (None, None)
822
+ (
823
+ Some(fn_name.clone()),
824
+ Some(emit_group_by_transform(
825
+ &fn_name,
826
+ &read_model_name,
827
+ aggregate,
828
+ &group_by_fields,
829
+ )),
830
+ )
508
831
  };
509
832
 
510
833
  ReadModelDef {
@@ -512,19 +835,14 @@ pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name:
512
835
  reference_name: read_model.get("reference_name").map(Json::to_s),
513
836
  heads,
514
837
  filtered_heads,
515
- authorization: if sole_head_as { queries::emit_query_authorization(&read_model_name, read_model.get("authorization")) } else { None },
838
+ authorization: if sole_head_as {
839
+ queries::emit_query_authorization(&read_model_name, read_model.get("authorization"))
840
+ } else {
841
+ None
842
+ },
516
843
  group_by_fn,
517
844
  group_by_fn_body,
518
- count: read_model.get("count").is_some(),
519
- median_field: read_model.get("median_field").map(Json::to_s),
520
- sum_field: read_model.get("sum_field").map(Json::to_s),
521
- avg_field: read_model.get("avg_field").map(Json::to_s),
522
- min_field: read_model.get("min_field").map(Json::to_s),
523
- max_field: read_model.get("max_field").map(Json::to_s),
524
- percentile_field: read_model.get("percentile_field").map(Json::to_s),
525
- percentile_at: if read_model.get("percentile_field").is_some() { read_model.get("percentile_at").map(Json::to_s) } else { None },
526
- any_field: read_model.get("any_field").map(Json::to_s),
527
- all_field: read_model.get("all_field").map(Json::to_s),
845
+ reduction: reduction_of(read_model),
528
846
  }
529
847
  }
530
848
 
@@ -541,40 +859,37 @@ fn with_wheres_and_authorization(wheres: &[&Json], authorization: Option<&Json>)
541
859
  }
542
860
 
543
861
  pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
544
- let heads = rmd.heads.iter().map(|h| format!(" {h},")).collect::<Vec<_>>().join("\n");
545
- let filtered_heads = rmd.filtered_heads.iter().map(|fh| format!(" {}", emit_filtered_head(fh))).collect::<Vec<_>>().join("\n");
546
- let authorization = match &rmd.authorization {
547
- Some(a) => format!("Some({a})"),
548
- None => "None".to_string(),
549
- };
550
- let reference_name = match &rmd.reference_name {
551
- Some(r) => format!("Some({})", crate::naming::ruby_inspect_string(r)),
552
- None => "None".to_string(),
553
- };
554
- let group_by = match &rmd.group_by_fn {
555
- Some(f) => format!("Some({f})"),
556
- None => "None".to_string(),
557
- };
558
- let count = if rmd.count { "true" } else { "false" };
559
- let field_some = |f: &Option<String>| match f {
560
- Some(v) => format!("Some({})", crate::naming::ruby_inspect_string(v)),
561
- None => "None".to_string(),
562
- };
563
- let median_field = field_some(&rmd.median_field);
564
- let sum_field = field_some(&rmd.sum_field);
565
- let avg_field = field_some(&rmd.avg_field);
566
- let min_field = field_some(&rmd.min_field);
567
- let max_field = field_some(&rmd.max_field);
568
- let percentile_field = field_some(&rmd.percentile_field);
569
- let percentile_at = match &rmd.percentile_at {
570
- Some(at) => format!("Some({at}_f64)"),
571
- None => "None".to_string(),
572
- };
573
- let any_field = field_some(&rmd.any_field);
574
- let all_field = field_some(&rmd.all_field);
862
+ let heads = rmd
863
+ .heads
864
+ .iter()
865
+ .map(|h| format!(" {h},"))
866
+ .collect::<Vec<_>>()
867
+ .join("\n");
868
+ let filtered_heads = rmd
869
+ .filtered_heads
870
+ .iter()
871
+ .map(|fh| format!(" {}", emit_filtered_head(fh)))
872
+ .collect::<Vec<_>>()
873
+ .join("\n");
874
+ let authorization = rmd
875
+ .authorization
876
+ .as_ref()
877
+ .map_or_else(|| "None".to_string(), |a| format!("Some({a})"));
878
+ let reference_name = rmd.reference_name.as_ref().map_or_else(
879
+ || "None".to_string(),
880
+ |r| format!("Some({})", crate::naming::ruby_inspect_string(r)),
881
+ );
882
+ let group_by = rmd
883
+ .group_by_fn
884
+ .as_ref()
885
+ .map_or_else(|| "None".to_string(), |f| format!("Some({f})"));
886
+ let reduction = rmd.reduction.as_ref().map_or_else(
887
+ || "None".to_string(),
888
+ |r| format!("Some(crate::kernel::read_model::Reduction::{r})"),
889
+ );
575
890
 
576
891
  format!(
577
- "crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n count: {count},\n median_field: {median_field},\n sum_field: {sum_field},\n avg_field: {avg_field},\n min_field: {min_field},\n max_field: {max_field},\n percentile_field: {percentile_field},\n percentile_at: {percentile_at},\n any_field: {any_field},\n all_field: {all_field},\n}},",
892
+ "crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n reduction: {reduction},\n}},",
578
893
  crate::naming::ruby_inspect_string(&rmd.verb)
579
894
  )
580
895
  }
@@ -584,20 +899,30 @@ pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
584
899
  /// `ReferenceHopCondition`, `QueryCondition` are all single-line too; only the top-level
585
900
  /// `ReadModelDef`/`QueryDef` get pretty multi-line formatting with their arrays broken apart).
586
901
  fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
587
- let conditions = fh.conditions.iter().map(queries::emit_query_condition).collect::<Vec<_>>().join(" ");
588
- let reference_hop_conditions = fh.reference_hop_conditions.iter().map(queries::emit_reference_hop_condition).collect::<Vec<_>>().join(" ");
589
- let order_by = match &fh.order_by {
590
- Some(o) => format!("Some({o})"),
591
- None => "None".to_string(),
592
- };
593
- let offset = match &fh.offset {
594
- Some(o) => format!("Some({o})"),
595
- None => "None".to_string(),
596
- };
597
- let limit = match &fh.limit {
598
- Some(l) => format!("Some({l})"),
599
- None => "None".to_string(),
600
- };
902
+ let conditions = fh
903
+ .conditions
904
+ .iter()
905
+ .map(queries::emit_query_condition)
906
+ .collect::<Vec<_>>()
907
+ .join(" ");
908
+ let reference_hop_conditions = fh
909
+ .reference_hop_conditions
910
+ .iter()
911
+ .map(queries::emit_reference_hop_condition)
912
+ .collect::<Vec<_>>()
913
+ .join(" ");
914
+ let order_by = fh
915
+ .order_by
916
+ .as_ref()
917
+ .map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
918
+ let offset = fh
919
+ .offset
920
+ .as_ref()
921
+ .map_or_else(|| "None".to_string(), |o| format!("Some({o})"));
922
+ let limit = fh
923
+ .limit
924
+ .as_ref()
925
+ .map_or_else(|| "None".to_string(), |l| format!("Some({l})"));
601
926
 
602
927
  format!(
603
928
  "crate::kernel::read_model::FilteredHead {{ as_name: {}, conditions: &[{conditions}], reference_hop_conditions: &[{reference_hop_conditions}], order_by: {order_by}, offset: {offset}, limit: {limit} }},",
@@ -608,30 +933,68 @@ fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
608
933
  // Keeps this aggregate's declared attributes plus any `projects` field, id
609
934
  // and lifecycle field, excluding other capabilities' synthetic fields (like
610
935
  // `corrects`'s `emitted_*` flags), and unwraps value objects recursively.
611
- fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
612
- let value_objects: Vec<&Json> = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]).iter().collect();
613
- let value_objects_by_name: HashMap<String, &Json> = value_objects.iter().map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo)).collect();
614
- let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
615
- let mut fields: Vec<Json> = aggregate.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
936
+ fn emit_group_by_transform(
937
+ fn_name: &str,
938
+ read_model_name: &str,
939
+ aggregate: &Json,
940
+ group_by_fields: &[String],
941
+ ) -> String {
942
+ let value_objects: Vec<&Json> = aggregate
943
+ .get("value_objects")
944
+ .map_or_default(Json::each)
945
+ .iter()
946
+ .collect();
947
+ let value_objects_by_name: HashMap<String, &Json> = value_objects
948
+ .iter()
949
+ .map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo))
950
+ .collect();
951
+ let lifecycle_field = aggregate
952
+ .get("lifecycle")
953
+ .and_then(|l| l.get("field"))
954
+ .map(Json::to_s);
955
+ let mut fields: Vec<Json> = aggregate
956
+ .get("attributes")
957
+ .map_or_default(Json::each)
958
+ .to_vec();
616
959
  fields.extend(crate::types::projected_field_pseudo_attributes(aggregate));
617
960
 
618
- let mut kept_keys: Vec<String> = fields.iter().map(|a| crate::attr::name(a).to_string()).collect();
961
+ let mut kept_keys: Vec<String> = fields
962
+ .iter()
963
+ .map(|a| crate::attr::name(a).to_string())
964
+ .collect();
619
965
  kept_keys.push("id".to_string());
620
966
  if let Some(lf) = &lifecycle_field {
621
967
  kept_keys.push(lf.clone());
622
968
  }
623
- let keep_cond = kept_keys.iter().map(|k| format!("k == {k:?}")).collect::<Vec<_>>().join(" || ");
969
+ let keep_cond = kept_keys
970
+ .iter()
971
+ .map(|k| format!("k == {k:?}"))
972
+ .collect::<Vec<_>>()
973
+ .join(" || ");
624
974
 
625
975
  let arms: Vec<String> = fields
626
976
  .iter()
627
977
  .map(|a| {
628
- let unwrapped = unwrap_json_expr("v", crate::attr::type_name(a), crate::attr::list(a), aggregate, &value_objects_by_name);
978
+ let unwrapped = unwrap_json_expr(
979
+ "v",
980
+ crate::attr::type_name(a),
981
+ crate::attr::list(a),
982
+ aggregate,
983
+ &value_objects_by_name,
984
+ );
629
985
  format!("{:?} => {unwrapped},", crate::attr::name(a))
630
986
  })
631
987
  .collect();
632
988
  let arms = arms.join("\n ");
633
989
 
634
- let fields_literal = format!("&[{}]", group_by_fields.iter().map(|f| format!("{f:?}")).collect::<Vec<_>>().join(", "));
990
+ let fields_literal = format!(
991
+ "&[{}]",
992
+ group_by_fields
993
+ .iter()
994
+ .map(|f| format!("{f:?}"))
995
+ .collect::<Vec<_>>()
996
+ .join(", ")
997
+ );
635
998
  let leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields);
636
999
 
637
1000
  format!(
@@ -641,10 +1004,19 @@ fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Jso
641
1004
 
642
1005
  // A key path naming every identity head cannot collide, so its leaves go
643
1006
  // unchecked; any other refuses a second row (ADR 0061).
644
- fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
1007
+ fn group_by_leaf_check(
1008
+ read_model_name: &str,
1009
+ aggregate: &Json,
1010
+ group_by_fields: &[String],
1011
+ ) -> String {
645
1012
  let mut identity: Vec<String> = Vec::new();
646
- for path in aggregate.get("identified_by").map(Json::each).unwrap_or(&[]) {
647
- let head = path.to_s().split('.').next().unwrap_or_default().to_string();
1013
+ for path in aggregate.get("identified_by").map_or_default(Json::each) {
1014
+ let head = path
1015
+ .to_s()
1016
+ .split('.')
1017
+ .next()
1018
+ .unwrap_or_default()
1019
+ .to_string();
648
1020
  if !identity.contains(&head) {
649
1021
  identity.push(head);
650
1022
  }
@@ -652,36 +1024,62 @@ fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields:
652
1024
  if !identity.is_empty() && identity.iter().all(|head| group_by_fields.contains(head)) {
653
1025
  return "crate::kernel::read_model::LeafCheck::IdentityCovered".to_string();
654
1026
  }
655
- format!("crate::kernel::read_model::LeafCheck::RefuseCollision({})", crate::naming::ruby_inspect_string(read_model_name))
1027
+ format!(
1028
+ "crate::kernel::read_model::LeafCheck::RefuseCollision({})",
1029
+ crate::naming::ruby_inspect_string(read_model_name)
1030
+ )
656
1031
  }
657
1032
 
658
1033
  // Port of `Value.materialize_unwrapped` (Ruby).
659
- fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
1034
+ fn unwrap_json_expr(
1035
+ expr: &str,
1036
+ type_name: &str,
1037
+ list: bool,
1038
+ aggregate: &Json,
1039
+ value_objects_by_name: &HashMap<String, &Json>,
1040
+ ) -> String {
660
1041
  if list {
661
1042
  let inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name);
662
1043
  return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
663
1044
  }
664
1045
 
665
1046
  let vo = value_objects_by_name.get(type_name).copied();
666
- let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
667
- let fields_meta: Option<&[Json]> = if let Some(vo) = vo {
668
- vo.get("attributes").map(Json::each)
669
- } else {
670
- entity.and_then(|e| e.get("attributes").map(Json::each))
1047
+ let entity = aggregate
1048
+ .get("entities")
1049
+ .map_or_default(Json::each)
1050
+ .iter()
1051
+ .find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
1052
+ let fields_meta: Option<&[Json]> = vo.map_or_else(
1053
+ || entity.and_then(|e| e.get("attributes").map(Json::each)),
1054
+ |vo| vo.get("attributes").map(Json::each),
1055
+ );
1056
+ let Some(fields_meta) = fields_meta else {
1057
+ return expr.to_string();
671
1058
  };
672
- let Some(fields_meta) = fields_meta else { return expr.to_string() };
673
1059
 
674
1060
  if vo.is_some() && fields_meta.len() == 1 {
675
1061
  let field = &fields_meta[0];
676
- let unwrapped = unwrap_json_expr("field_value", crate::attr::type_name(field), crate::attr::list(field), aggregate, value_objects_by_name);
1062
+ let unwrapped = unwrap_json_expr(
1063
+ "field_value",
1064
+ crate::attr::type_name(field),
1065
+ crate::attr::list(field),
1066
+ aggregate,
1067
+ value_objects_by_name,
1068
+ );
677
1069
  let field_name = crate::attr::name(field);
678
- return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map(|(_, field_value)| {unwrapped}).unwrap_or(crate::kernel::Json::Null), other => other }}");
1070
+ return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map_or(crate::kernel::Json::Null, |(_, field_value)| {unwrapped}), other => other }}");
679
1071
  }
680
1072
 
681
1073
  let inner_arms: Vec<String> = fields_meta
682
1074
  .iter()
683
1075
  .map(|f| {
684
- let unwrapped = unwrap_json_expr("v", crate::attr::type_name(f), crate::attr::list(f), aggregate, value_objects_by_name);
1076
+ let unwrapped = unwrap_json_expr(
1077
+ "v",
1078
+ crate::attr::type_name(f),
1079
+ crate::attr::list(f),
1080
+ aggregate,
1081
+ value_objects_by_name,
1082
+ );
685
1083
  format!("{:?} => {unwrapped},", crate::attr::name(f))
686
1084
  })
687
1085
  .collect();
@@ -689,9 +1087,12 @@ fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, v
689
1087
  format!("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 }}")
690
1088
  }
691
1089
 
692
- const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n 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)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n count: false,\n median_field: None,\n sum_field: None,\n avg_field: None,\n min_field: None,\n max_field: None,\n percentile_field: None,\n percentile_at: None,\n any_field: None,\n all_field: None,\n},";
1090
+ const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n 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)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n reduction: None,\n},";
693
1091
 
694
1092
  pub fn emit_read_model_table(exemplar: &Exemplar, read_model_defs: &[ReadModelDef]) -> String {
695
1093
  let rows: Vec<String> = read_model_defs.iter().map(emit_read_model_def).collect();
696
- exemplar.render("read_model_table", &[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))])
1094
+ exemplar.render(
1095
+ "read_model_table",
1096
+ &[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))],
1097
+ )
697
1098
  }