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