hecks 3.14.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 (419) 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/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -2,15 +2,11 @@
2
2
  // `PUT /api/presentation`: validates against the live domain IR, then dispatches real commands.
3
3
 
4
4
  use crate::dispatch;
5
- use crate::lambda_client::LambdaInvoker;
6
- use crate::journal::LineageConfig;
5
+ use crate::host::Host;
7
6
  use crate::presentation::{self, KernelRows};
8
7
  use crate::ui_schema;
9
8
  use serde_json::{json, Map, Value};
10
9
  use std::collections::BTreeSet;
11
- use std::path::Path;
12
- use tokio::sync::Mutex;
13
- use tokio_postgres::Client;
14
10
 
15
11
  // Duplicates the chapter's own `one_of` so this refuses before any dispatch starts.
16
12
  const VALID_TONES: &[&str] = &["good", "warn", "danger", "muted", "accent"];
@@ -35,14 +31,11 @@ pub(crate) enum SaveRefusal {
35
31
  pub(crate) async fn save(
36
32
  domain_ir: &Value,
37
33
  submitted: &Value,
38
- client: &Mutex<Client>,
39
- wasm_path: &Path,
40
- lineage: &LineageConfig,
41
- invoker: &dyn LambdaInvoker,
34
+ host: Host<'_>,
42
35
  ) -> Result<Value, SaveRefusal> {
43
36
  // Asked before validated: a host with no such chapter cannot honour this request at all,
44
37
  // regardless of which rule the submitted config would otherwise break.
45
- let existing = match presentation::kernel_rows(client, wasm_path).await {
38
+ let existing = match presentation::kernel_rows(host.client, host.wasm_path).await {
46
39
  Ok(Some(rows)) => rows,
47
40
  Ok(None) => return Err(SaveRefusal::NoKernel),
48
41
  Err(e) => return Err(SaveRefusal::Internal(format!("{e:#}"))),
@@ -50,12 +43,20 @@ pub(crate) async fn save(
50
43
 
51
44
  validate(submitted, domain_ir).map_err(SaveRefusal::Malformed)?;
52
45
 
53
- let mut writer = Writer { client, wasm_path, lineage, invoker };
54
- writer.write_states(section(submitted, "states"), &existing).await?;
55
- writer.write_collections(section(submitted, "collections"), &existing).await?;
56
- writer.write_overview(section(submitted, "overview"), &existing).await?;
57
-
58
- presentation::load(client, wasm_path, lineage).await.map_err(|e| SaveRefusal::Internal(format!("{e:#}")))
46
+ let writer = Writer { host };
47
+ writer
48
+ .write_states(section(submitted, "states"), &existing)
49
+ .await?;
50
+ writer
51
+ .write_collections(section(submitted, "collections"), &existing)
52
+ .await?;
53
+ writer
54
+ .write_overview(section(submitted, "overview"), &existing)
55
+ .await?;
56
+
57
+ presentation::load(host.client, host.wasm_path, host.config)
58
+ .await
59
+ .map_err(|e| SaveRefusal::Internal(format!("{e:#}")))
59
60
  }
60
61
 
61
62
  // A missing, null, or non-object section is the empty one rather than an error: `validate` has
@@ -66,46 +67,47 @@ fn section<'a>(config: &'a Value, key: &str) -> &'a Value {
66
67
  }
67
68
 
68
69
  fn entries(value: &Value) -> Vec<(&String, &Value)> {
69
- value.as_object().map(|map| map.iter().collect()).unwrap_or_default()
70
+ value
71
+ .as_object()
72
+ .map(|map| map.iter().collect())
73
+ .unwrap_or_default()
70
74
  }
71
75
 
72
76
  // A null, a missing key, or anything that isn't a list is the empty list.
73
77
  fn list(value: Option<&Value>) -> &[Value] {
74
- value.and_then(|v| v.as_array()).map(|a| a.as_slice()).unwrap_or(&[])
78
+ value
79
+ .and_then(|v| v.as_array())
80
+ .map_or(&[][..], std::vec::Vec::as_slice)
75
81
  }
76
82
 
77
83
  // Every present field is re-dispatched on every save, even to an unchanged value, so the two
78
84
  // engines produce the same history for the same request. A field once set and later omitted is
79
85
  // never cleared: the chapter declares no `Clear*` command.
80
86
  struct Writer<'a> {
81
- client: &'a Mutex<Client>,
82
- wasm_path: &'a Path,
83
- lineage: &'a LineageConfig,
84
- invoker: &'a dyn LambdaInvoker,
87
+ host: Host<'a>,
85
88
  }
86
89
 
87
90
  impl Writer<'_> {
88
91
  // A creating command — facts only, no identity to route to.
89
- async fn declare(&mut self, verb: &str, facts: Value) -> Result<(), SaveRefusal> {
90
- let outcome = dispatch::handle_facts(self.client, self.wasm_path, verb, facts, None, self.lineage, self.invoker)
92
+ async fn declare(&self, verb: &str, facts: Value) -> Result<(), SaveRefusal> {
93
+ let outcome = dispatch::handle_facts(self.host, verb, facts, None)
91
94
  .await
92
95
  .map_err(|e| SaveRefusal::Internal(format!("{e:#}")))?;
93
- self.accepted(outcome)
96
+ Self::accepted(outcome)
94
97
  }
95
98
 
96
99
  // A command acting on an existing row, routed by that row's own identity: "Agg:state" for
97
100
  // StateStyle, the aggregate name for Collection, "overview" for the singleton.
98
- async fn route(&mut self, verb: &str, id: &str, facts: Value) -> Result<(), SaveRefusal> {
99
- let outcome =
100
- dispatch::handle_routed(self.client, self.wasm_path, verb, json!(id), facts, None, self.lineage, self.invoker)
101
- .await
102
- .map_err(|e| SaveRefusal::Internal(format!("{e:#}")))?;
103
- self.accepted(outcome)
101
+ async fn route(&self, verb: &str, id: &str, facts: Value) -> Result<(), SaveRefusal> {
102
+ let outcome = dispatch::handle_routed(self.host, verb, json!(id), facts, None)
103
+ .await
104
+ .map_err(|e| SaveRefusal::Internal(format!("{e:#}")))?;
105
+ Self::accepted(outcome)
104
106
  }
105
107
 
106
108
  // A refusal stops the whole save where it happened: several independent dispatches, not one
107
109
  // atomic write. `validate` above is what makes this rare rather than routine.
108
- fn accepted(&self, outcome: dispatch::Outcome) -> Result<(), SaveRefusal> {
110
+ fn accepted(outcome: dispatch::Outcome) -> Result<(), SaveRefusal> {
109
111
  if outcome.accepted {
110
112
  Ok(())
111
113
  } else {
@@ -113,7 +115,7 @@ impl Writer<'_> {
113
115
  }
114
116
  }
115
117
 
116
- async fn write_states(&mut self, states: &Value, existing: &KernelRows) -> Result<(), SaveRefusal> {
118
+ async fn write_states(&self, states: &Value, existing: &KernelRows) -> Result<(), SaveRefusal> {
117
119
  let known: BTreeSet<&str> = existing.state_ids();
118
120
  for (agg, per_state) in entries(states) {
119
121
  for (state, entry) in entries(per_state) {
@@ -127,13 +129,26 @@ impl Writer<'_> {
127
129
  }
128
130
 
129
131
  if let Some(tone) = present(entry.get("tone")) {
130
- self.route("ConsoleSettings::StateStyle.SetTone", &id, json!({"tone": {"value": tone}})).await?;
132
+ self.route(
133
+ "ConsoleSettings::StateStyle.SetTone",
134
+ &id,
135
+ json!({"tone": {"value": tone}}),
136
+ )
137
+ .await?;
131
138
  }
132
139
  // Key presence, not truthiness: an explicit `false` is a real answer to store.
133
140
  if let Some(attention) = entry.get("attention") {
134
- let flag = if attention.as_bool() == Some(true) { "true" } else { "false" };
135
- self.route("ConsoleSettings::StateStyle.SetAttention", &id, json!({"attention": {"value": flag}}))
136
- .await?;
141
+ let flag = if attention.as_bool() == Some(true) {
142
+ "true"
143
+ } else {
144
+ "false"
145
+ };
146
+ self.route(
147
+ "ConsoleSettings::StateStyle.SetAttention",
148
+ &id,
149
+ json!({"attention": {"value": flag}}),
150
+ )
151
+ .await?;
137
152
  }
138
153
 
139
154
  let extra = extra_fields(entry, KNOWN_STATE_KEYS);
@@ -150,11 +165,19 @@ impl Writer<'_> {
150
165
  Ok(())
151
166
  }
152
167
 
153
- async fn write_collections(&mut self, collections: &Value, existing: &KernelRows) -> Result<(), SaveRefusal> {
168
+ async fn write_collections(
169
+ &self,
170
+ collections: &Value,
171
+ existing: &KernelRows,
172
+ ) -> Result<(), SaveRefusal> {
154
173
  let known: BTreeSet<&str> = existing.collection_ids();
155
174
  for (agg, entry) in entries(collections) {
156
175
  if !known.contains(agg.as_str()) {
157
- self.declare("ConsoleSettings::Collection.Declare", json!({"agg": {"value": agg}})).await?;
176
+ self.declare(
177
+ "ConsoleSettings::Collection.Declare",
178
+ json!({"agg": {"value": agg}}),
179
+ )
180
+ .await?;
158
181
  }
159
182
 
160
183
  for (field, command) in [
@@ -166,7 +189,8 @@ impl Writer<'_> {
166
189
  ] {
167
190
  if let Some(value) = present(entry.get(field)) {
168
191
  let verb = format!("ConsoleSettings::Collection.{command}");
169
- self.route(&verb, agg, json!({ field: {"value": value} })).await?;
192
+ self.route(&verb, agg, json!({ field: {"value": value} }))
193
+ .await?;
170
194
  }
171
195
  }
172
196
 
@@ -220,10 +244,16 @@ impl Writer<'_> {
220
244
  Ok(())
221
245
  }
222
246
 
223
- async fn write_identity(&mut self, agg: &str, identity: &Value) -> Result<(), SaveRefusal> {
247
+ async fn write_identity(&self, agg: &str, identity: &Value) -> Result<(), SaveRefusal> {
224
248
  let mut facts = Map::new();
225
- facts.insert("identity_field".to_string(), json!({"value": identity.get("field")}));
226
- facts.insert("identity_strategy".to_string(), json!({"value": identity.get("strategy")}));
249
+ facts.insert(
250
+ "identity_field".to_string(),
251
+ json!({"value": identity.get("field")}),
252
+ );
253
+ facts.insert(
254
+ "identity_strategy".to_string(),
255
+ json!({"value": identity.get("strategy")}),
256
+ );
227
257
  if let Some(source) = present(identity.get("source")) {
228
258
  facts.insert("identity_source".to_string(), json!({ "value": source }));
229
259
  }
@@ -232,15 +262,31 @@ impl Writer<'_> {
232
262
  }
233
263
  let extra = extra_fields(identity, KNOWN_IDENTITY_KEYS);
234
264
  if !extra.is_empty() {
235
- facts.insert("identity_extra_json".to_string(), json!({"value": Value::Object(extra).to_string()}));
265
+ facts.insert(
266
+ "identity_extra_json".to_string(),
267
+ json!({"value": Value::Object(extra).to_string()}),
268
+ );
236
269
  }
237
- self.route("ConsoleSettings::Collection.SetIdentity", agg, Value::Object(facts)).await
270
+ self.route(
271
+ "ConsoleSettings::Collection.SetIdentity",
272
+ agg,
273
+ Value::Object(facts),
274
+ )
275
+ .await
238
276
  }
239
277
 
240
278
  // The one row: `Declare`d once, then `ReplaceStats` whole on every save, even an empty list.
241
- async fn write_overview(&mut self, overview: &Value, existing: &KernelRows) -> Result<(), SaveRefusal> {
279
+ async fn write_overview(
280
+ &self,
281
+ overview: &Value,
282
+ existing: &KernelRows,
283
+ ) -> Result<(), SaveRefusal> {
242
284
  if existing.overview.is_empty() {
243
- self.declare("ConsoleSettings::Overview.Declare", json!({"key": {"value": "overview"}})).await?;
285
+ self.declare(
286
+ "ConsoleSettings::Overview.Declare",
287
+ json!({"key": {"value": "overview"}}),
288
+ )
289
+ .await?;
244
290
  }
245
291
  self.route(
246
292
  "ConsoleSettings::Overview.ReplaceStats",
@@ -296,9 +342,14 @@ fn columns_for_dispatch(columns: Option<&Value>) -> Vec<Value> {
296
342
  .map(|column| {
297
343
  // A column is either a bare field name or a descriptor hash — both shapes
298
344
  // `validate_collection!` reads.
299
- let Some(object) = column.as_object() else { return json!({ "field": column }) };
345
+ let Some(object) = column.as_object() else {
346
+ return json!({ "field": column });
347
+ };
300
348
  let mut row = Map::new();
301
- row.insert("field".to_string(), object.get("field").cloned().unwrap_or(Value::Null));
349
+ row.insert(
350
+ "field".to_string(),
351
+ object.get("field").cloned().unwrap_or(Value::Null),
352
+ );
302
353
  // "true"/"false" as strings: `Column#sortable` is a String attribute, not a boolean.
303
354
  if let Some(sortable) = object.get("sortable") {
304
355
  row.insert("sortable".to_string(), json!(ruby_to_s(sortable)));
@@ -308,7 +359,10 @@ fn columns_for_dispatch(columns: Option<&Value>) -> Vec<Value> {
308
359
  }
309
360
  let extra = extra_fields(column, KNOWN_COLUMN_KEYS);
310
361
  if !extra.is_empty() {
311
- row.insert("extra_json".to_string(), json!(Value::Object(extra).to_string()));
362
+ row.insert(
363
+ "extra_json".to_string(),
364
+ json!(Value::Object(extra).to_string()),
365
+ );
312
366
  }
313
367
  Value::Object(row)
314
368
  })
@@ -319,9 +373,14 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
319
373
  list(detail_fields)
320
374
  .iter()
321
375
  .map(|field| {
322
- let Some(object) = field.as_object() else { return json!({ "field": field }) };
376
+ let Some(object) = field.as_object() else {
377
+ return json!({ "field": field });
378
+ };
323
379
  let mut row = Map::new();
324
- row.insert("field".to_string(), object.get("field").cloned().unwrap_or(Value::Null));
380
+ row.insert(
381
+ "field".to_string(),
382
+ object.get("field").cloned().unwrap_or(Value::Null),
383
+ );
325
384
  if let Some(display) = present(object.get("display")) {
326
385
  row.insert("display".to_string(), display.clone());
327
386
  }
@@ -330,7 +389,10 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
330
389
  }
331
390
  let extra = extra_fields(field, KNOWN_DETAIL_FIELD_KEYS);
332
391
  if !extra.is_empty() {
333
- row.insert("extra_json".to_string(), json!(Value::Object(extra).to_string()));
392
+ row.insert(
393
+ "extra_json".to_string(),
394
+ json!(Value::Object(extra).to_string()),
395
+ );
334
396
  }
335
397
  Value::Object(row)
336
398
  })
@@ -341,8 +403,12 @@ fn detail_fields_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
341
403
  fn detail_field_columns_for_dispatch(detail_fields: Option<&Value>) -> Vec<Value> {
342
404
  let mut rows = Vec::new();
343
405
  for field in list(detail_fields) {
344
- let Some(object) = field.as_object() else { continue };
345
- let Some(columns) = object.get("columns") else { continue };
406
+ let Some(object) = field.as_object() else {
407
+ continue;
408
+ };
409
+ let Some(columns) = object.get("columns") else {
410
+ continue;
411
+ };
346
412
  let name = object.get("field").cloned().unwrap_or(Value::Null);
347
413
  for column in list(Some(columns)) {
348
414
  rows.push(json!({ "field": name, "column": column }));
@@ -356,11 +422,20 @@ fn preconditions_for_dispatch(preconditions: Option<&Value>) -> Vec<Value> {
356
422
  .iter()
357
423
  .map(|rule| {
358
424
  let mut row = Map::new();
359
- row.insert("field".to_string(), rule.get("field").cloned().unwrap_or(Value::Null));
360
- row.insert("state".to_string(), rule.get("state").cloned().unwrap_or(Value::Null));
425
+ row.insert(
426
+ "field".to_string(),
427
+ rule.get("field").cloned().unwrap_or(Value::Null),
428
+ );
429
+ row.insert(
430
+ "state".to_string(),
431
+ rule.get("state").cloned().unwrap_or(Value::Null),
432
+ );
361
433
  let extra = extra_fields(rule, KNOWN_PRECONDITION_KEYS);
362
434
  if !extra.is_empty() {
363
- row.insert("extra_json".to_string(), json!(Value::Object(extra).to_string()));
435
+ row.insert(
436
+ "extra_json".to_string(),
437
+ json!(Value::Object(extra).to_string()),
438
+ );
364
439
  }
365
440
  Value::Object(row)
366
441
  })
@@ -372,7 +447,11 @@ fn preconditions_for_dispatch(preconditions: Option<&Value>) -> Vec<Value> {
372
447
  fn field_formats_for_dispatch(field_formats: Option<&Value>) -> Vec<Value> {
373
448
  field_formats
374
449
  .and_then(|v| v.as_object())
375
- .map(|map| map.iter().map(|(field, format)| json!({"field": field, "format": format})).collect())
450
+ .map(|map| {
451
+ map.iter()
452
+ .map(|(field, format)| json!({"field": field, "format": format}))
453
+ .collect()
454
+ })
376
455
  .unwrap_or_default()
377
456
  }
378
457
 
@@ -381,8 +460,14 @@ fn stats_for_dispatch(stats: Option<&Value>) -> Vec<Value> {
381
460
  .iter()
382
461
  .map(|stat| {
383
462
  let mut row = Map::new();
384
- row.insert("label".to_string(), stat.get("label").cloned().unwrap_or(Value::Null));
385
- row.insert("collection".to_string(), stat.get("collection").cloned().unwrap_or(Value::Null));
463
+ row.insert(
464
+ "label".to_string(),
465
+ stat.get("label").cloned().unwrap_or(Value::Null),
466
+ );
467
+ row.insert(
468
+ "collection".to_string(),
469
+ stat.get("collection").cloned().unwrap_or(Value::Null),
470
+ );
386
471
  if let Some(fold) = present(stat.get("as")) {
387
472
  row.insert("as".to_string(), fold.clone());
388
473
  }
@@ -445,11 +530,15 @@ fn refuse_unsupported_yet(collections: &Value) -> Refusal {
445
530
  }
446
531
 
447
532
  fn find_aggregate<'a>(aggregates: &[&'a Value], name: &str) -> Option<&'a Value> {
448
- aggregates.iter().copied().find(|a| ui_schema::agg_name(a) == name)
533
+ aggregates
534
+ .iter()
535
+ .copied()
536
+ .find(|a| ui_schema::agg_name(a) == name)
449
537
  }
450
538
 
451
539
  fn require_aggregate<'a>(aggregates: &[&'a Value], name: &str) -> Result<&'a Value, String> {
452
- find_aggregate(aggregates, name).ok_or_else(|| format!("{name} is not a real aggregate this domain declares"))
540
+ find_aggregate(aggregates, name)
541
+ .ok_or_else(|| format!("{name} is not a real aggregate this domain declares"))
453
542
  }
454
543
 
455
544
  fn validate_states(agg_name: &str, per_state: &Value, aggregates: &[&Value]) -> Refusal {
@@ -478,10 +567,18 @@ fn missing_state_entries(states: &Value, aggregates: &[&Value]) -> Refusal {
478
567
  if missing.is_empty() {
479
568
  return Ok(());
480
569
  }
481
- Err(format!("no presentation entry for {} — every real state needs one, even a bare one", missing.join(", ")))
570
+ Err(format!(
571
+ "no presentation entry for {} — every real state needs one, even a bare one",
572
+ missing.join(", ")
573
+ ))
482
574
  }
483
575
 
484
- fn validate_state_entry(agg_name: &str, state_name: &str, entry: &Value, known: &[String]) -> Refusal {
576
+ fn validate_state_entry(
577
+ agg_name: &str,
578
+ state_name: &str,
579
+ entry: &Value,
580
+ known: &[String],
581
+ ) -> Refusal {
485
582
  if !known.iter().any(|s| s == state_name) {
486
583
  return Err(format!(
487
584
  "{agg_name} has no state {} — it declares {}",
@@ -501,14 +598,20 @@ fn validate_state_entry(agg_name: &str, state_name: &str, entry: &Value, known:
501
598
  }
502
599
 
503
600
  match entry.get("attention") {
504
- None | Some(Value::Null) | Some(Value::Bool(_)) => Ok(()),
505
- Some(other) => {
506
- Err(format!("{agg_name}.{state_name}'s attention must be true or false, not {}", inspect(other)))
507
- }
601
+ None | Some(Value::Null | Value::Bool(_)) => Ok(()),
602
+ Some(other) => Err(format!(
603
+ "{agg_name}.{state_name}'s attention must be true or false, not {}",
604
+ inspect(other)
605
+ )),
508
606
  }
509
607
  }
510
608
 
511
- fn validate_collection(agg_name: &str, entry: &Value, aggregates: &[&Value], domain_ir: &Value) -> Refusal {
609
+ fn validate_collection(
610
+ agg_name: &str,
611
+ entry: &Value,
612
+ aggregates: &[&Value],
613
+ domain_ir: &Value,
614
+ ) -> Refusal {
512
615
  let aggregate = require_aggregate(aggregates, agg_name)?;
513
616
 
514
617
  validate_columns(agg_name, aggregate, entry.get("columns"))?;
@@ -518,7 +621,10 @@ fn validate_collection(agg_name: &str, entry: &Value, aggregates: &[&Value], dom
518
621
  // than a vocabulary.
519
622
  if let Some(nav_order) = entry.get("nav_order") {
520
623
  if !nav_order.is_number() {
521
- return Err(format!("{agg_name}'s nav_order must be a number, not {}", inspect(nav_order)));
624
+ return Err(format!(
625
+ "{agg_name}'s nav_order must be a number, not {}",
626
+ inspect(nav_order)
627
+ ));
522
628
  }
523
629
  }
524
630
 
@@ -543,7 +649,10 @@ fn validate_collection(agg_name: &str, entry: &Value, aggregates: &[&Value], dom
543
649
  // "does this aggregate declare it", not a closed vocabulary.
544
650
  fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>) -> Refusal {
545
651
  for column in list(columns) {
546
- let key = column.get("field").filter(|_| column.is_object()).unwrap_or(column);
652
+ let key = column
653
+ .get("field")
654
+ .filter(|_| column.is_object())
655
+ .unwrap_or(column);
547
656
  let name = ruby_to_s(key);
548
657
  if name != "__state__" && ui_schema::find_attribute(aggregate, &name).is_none() {
549
658
  return Err(format!(
@@ -553,7 +662,9 @@ fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>)
553
662
  ));
554
663
  }
555
664
 
556
- let Some(object) = column.as_object() else { continue };
665
+ let Some(object) = column.as_object() else {
666
+ continue;
667
+ };
557
668
  if let Some(sortable) = object.get("sortable") {
558
669
  if !sortable.is_boolean() {
559
670
  return Err(format!(
@@ -577,29 +688,44 @@ fn validate_columns(agg_name: &str, aggregate: &Value, columns: Option<&Value>)
577
688
  }
578
689
 
579
690
  fn attribute_names(aggregate: &Value) -> String {
580
- ui_schema::array(aggregate, "attributes").iter().map(ui_schema::attr_name).collect::<Vec<_>>().join(", ")
691
+ ui_schema::array(aggregate, "attributes")
692
+ .iter()
693
+ .map(ui_schema::attr_name)
694
+ .collect::<Vec<_>>()
695
+ .join(", ")
581
696
  }
582
697
 
583
698
  // A non-reference field is caught here first, since otherwise the state check below would
584
699
  // simply never fire.
585
- fn validate_precondition(agg_name: &str, aggregate: &Value, rule: &Value, aggregates: &[&Value]) -> Refusal {
700
+ fn validate_precondition(
701
+ agg_name: &str,
702
+ aggregate: &Value,
703
+ rule: &Value,
704
+ aggregates: &[&Value],
705
+ ) -> Refusal {
586
706
  let Some(field) = present(rule.get("field")) else {
587
707
  return Err(format!("{agg_name} has a precondition with no field named"));
588
708
  };
589
709
  let name = ruby_to_s(field);
590
- let attribute = ui_schema::find_attribute(aggregate, &name);
591
- let is_reference = attribute.map(ui_schema::is_reference).unwrap_or(false);
592
- if !is_reference {
593
- return Err(format!("{agg_name}.{name} isn't a reference — a precondition only makes sense against one"));
594
- }
710
+ let Some(attribute) =
711
+ ui_schema::find_attribute(aggregate, &name).filter(|a| ui_schema::is_reference(a))
712
+ else {
713
+ return Err(format!(
714
+ "{agg_name}.{name} isn't a reference — a precondition only makes sense against one"
715
+ ));
716
+ };
595
717
 
596
- let Some(state) = present(rule.get("state")) else { return Ok(()) };
718
+ let Some(state) = present(rule.get("state")) else {
719
+ return Ok(());
720
+ };
597
721
  // A cross-domain or otherwise unresolvable target — nothing more to
598
722
  // check here, exactly as Ruby's own `return unless target` says.
599
- let Some(target_name) = ui_schema::reference_target(ui_schema::type_of(attribute.expect("checked above"))) else {
723
+ let Some(target_name) = ui_schema::reference_target(ui_schema::type_of(attribute)) else {
724
+ return Ok(());
725
+ };
726
+ let Some(target) = find_aggregate(aggregates, &target_name) else {
600
727
  return Ok(());
601
728
  };
602
- let Some(target) = find_aggregate(aggregates, &target_name) else { return Ok(()) };
603
729
 
604
730
  let known = ui_schema::lifecycle_states(target);
605
731
  if known.iter().any(|s| *s == ruby_to_s(state)) {
@@ -615,10 +741,17 @@ fn validate_precondition(agg_name: &str, aggregate: &Value, rule: &Value, aggreg
615
741
  // Keyed by attribute name, not path: a nested compound field is found the same way, since
616
742
  // `ui_schema` threads the same map down through every level of recursion.
617
743
  fn validate_field_formats(agg_name: &str, aggregate: &Value, formats: Option<&Value>) -> Refusal {
618
- let Some(map) = formats.and_then(|v| v.as_object()) else { return Ok(()) };
744
+ let Some(map) = formats.and_then(|v| v.as_object()) else {
745
+ return Ok(());
746
+ };
619
747
  for (field_name, format) in map {
620
- if ui_schema::find_attribute(aggregate, field_name).is_none() && !nested_attribute(aggregate, field_name) {
621
- return Err(format!("{agg_name} has no attribute {} to format", inspect(&json!(field_name))));
748
+ if ui_schema::find_attribute(aggregate, field_name).is_none()
749
+ && !nested_attribute(aggregate, field_name)
750
+ {
751
+ return Err(format!(
752
+ "{agg_name} has no attribute {} to format",
753
+ inspect(&json!(field_name))
754
+ ));
622
755
  }
623
756
  if !VALID_FORMATS.contains(&ruby_to_s(format).as_str()) {
624
757
  return Err(format!(
@@ -632,43 +765,68 @@ fn validate_field_formats(agg_name: &str, aggregate: &Value, formats: Option<&Va
632
765
  }
633
766
 
634
767
  fn nested_attribute(aggregate: &Value, field_name: &str) -> bool {
635
- ui_schema::array(aggregate, "attributes").iter().any(|attribute| {
636
- if ui_schema::is_reference(attribute) {
637
- return false;
638
- }
639
- let Some(value_object) = ui_schema::find_value_object(aggregate, ui_schema::type_of(attribute)) else {
640
- return false;
641
- };
642
- ui_schema::array(value_object, "attributes").iter().any(|sub| ui_schema::attr_name(sub) == field_name)
643
- })
768
+ ui_schema::array(aggregate, "attributes")
769
+ .iter()
770
+ .any(|attribute| {
771
+ if ui_schema::is_reference(attribute) {
772
+ return false;
773
+ }
774
+ let Some(value_object) =
775
+ ui_schema::find_value_object(aggregate, ui_schema::type_of(attribute))
776
+ else {
777
+ return false;
778
+ };
779
+ ui_schema::array(value_object, "attributes")
780
+ .iter()
781
+ .any(|sub| ui_schema::attr_name(sub) == field_name)
782
+ })
644
783
  }
645
784
 
646
785
  // Never a reference: there is nothing to derive from another aggregate's identity.
647
786
  fn validate_identity(agg_name: &str, aggregate: &Value, rule: &Value) -> Refusal {
648
787
  let Some(field) = present(rule.get("field")) else {
649
- return Err(format!("{agg_name} has an identity rule with no field named"));
788
+ return Err(format!(
789
+ "{agg_name} has an identity rule with no field named"
790
+ ));
650
791
  };
651
792
  let name = ruby_to_s(field);
652
793
  let Some(attribute) = ui_schema::find_attribute(aggregate, &name) else {
653
- return Err(format!("{agg_name} has no attribute {} to auto-derive", inspect(field)));
794
+ return Err(format!(
795
+ "{agg_name} has no attribute {} to auto-derive",
796
+ inspect(field)
797
+ ));
654
798
  };
655
799
  if ui_schema::is_reference(attribute) {
656
- return Err(format!("{agg_name}.{name} is a reference — identity can only derive a plain field"));
800
+ return Err(format!(
801
+ "{agg_name}.{name} is a reference — identity can only derive a plain field"
802
+ ));
657
803
  }
658
804
 
659
- match rule.get("strategy").map(ruby_to_s).unwrap_or_default().as_str() {
805
+ match rule
806
+ .get("strategy")
807
+ .map(ruby_to_s)
808
+ .unwrap_or_default()
809
+ .as_str()
810
+ {
660
811
  "slug" => {
661
812
  let Some(source) = present(rule.get("source")) else {
662
- return Err(format!("{agg_name}'s identity slug strategy needs a source field"));
813
+ return Err(format!(
814
+ "{agg_name}'s identity slug strategy needs a source field"
815
+ ));
663
816
  };
664
817
  if ui_schema::find_attribute(aggregate, &ruby_to_s(source)).is_none() {
665
- return Err(format!("{agg_name} has no attribute {} to slug from", inspect(source)));
818
+ return Err(format!(
819
+ "{agg_name} has no attribute {} to slug from",
820
+ inspect(source)
821
+ ));
666
822
  }
667
823
  Ok(())
668
824
  }
669
825
  "sequence" => {
670
826
  if present(rule.get("prefix")).is_none() {
671
- return Err(format!("{agg_name}'s identity sequence strategy needs a prefix"));
827
+ return Err(format!(
828
+ "{agg_name}'s identity sequence strategy needs a prefix"
829
+ ));
672
830
  }
673
831
  Ok(())
674
832
  }
@@ -683,9 +841,16 @@ fn validate_identity(agg_name: &str, aggregate: &Value, rule: &Value) -> Refusal
683
841
  }
684
842
  }
685
843
 
686
- fn validate_detail_fields(agg_name: &str, aggregate: &Value, entries_value: Option<&Value>) -> Refusal {
844
+ fn validate_detail_fields(
845
+ agg_name: &str,
846
+ aggregate: &Value,
847
+ entries_value: Option<&Value>,
848
+ ) -> Refusal {
687
849
  for field in list(entries_value) {
688
- let key = field.get("field").filter(|_| field.is_object()).unwrap_or(field);
850
+ let key = field
851
+ .get("field")
852
+ .filter(|_| field.is_object())
853
+ .unwrap_or(field);
689
854
  let name = ruby_to_s(key);
690
855
  let Some(attribute) = ui_schema::find_attribute(aggregate, &name) else {
691
856
  return Err(format!(
@@ -695,7 +860,9 @@ fn validate_detail_fields(agg_name: &str, aggregate: &Value, entries_value: Opti
695
860
  ));
696
861
  };
697
862
 
698
- let Some(object) = field.as_object() else { continue };
863
+ let Some(object) = field.as_object() else {
864
+ continue;
865
+ };
699
866
 
700
867
  if let Some(display) = present(object.get("display")) {
701
868
  if !VALID_DISPLAYS.contains(&ruby_to_s(display).as_str()) {
@@ -746,7 +913,13 @@ fn validate_row_state_filter(
746
913
 
747
914
  // A `columns:` on a field that is neither a value object nor an entity is a config mistake
748
915
  // this catches by refusing rather than silently doing nothing.
749
- fn validate_row_columns(agg_name: &str, aggregate: &Value, key: &str, attribute: &Value, columns: &Value) -> Refusal {
916
+ fn validate_row_columns(
917
+ agg_name: &str,
918
+ aggregate: &Value,
919
+ key: &str,
920
+ attribute: &Value,
921
+ columns: &Value,
922
+ ) -> Refusal {
750
923
  let type_name = ui_schema::type_of(attribute).to_string();
751
924
  let rows_source = ui_schema::find_value_object(aggregate, &type_name)
752
925
  .or_else(|| ui_schema::find_entity(aggregate, &type_name))
@@ -758,7 +931,10 @@ fn validate_row_columns(agg_name: &str, aggregate: &Value, key: &str, attribute:
758
931
  ));
759
932
  };
760
933
 
761
- let known: Vec<&str> = ui_schema::array(rows_source, "attributes").iter().map(ui_schema::attr_name).collect();
934
+ let known: Vec<&str> = ui_schema::array(rows_source, "attributes")
935
+ .iter()
936
+ .map(ui_schema::attr_name)
937
+ .collect();
762
938
  for column in list(Some(columns)) {
763
939
  if !known.contains(&ruby_to_s(column).as_str()) {
764
940
  return Err(format!(
@@ -777,7 +953,10 @@ fn validate_row_columns(agg_name: &str, aggregate: &Value, key: &str, attribute:
777
953
  fn validate_list_query(agg_name: &str, aggregate: &Value, query_name: &Value) -> Refusal {
778
954
  let name = ruby_to_s(query_name);
779
955
  let Some(query) = find_query(aggregate, &name) else {
780
- return Err(format!("{agg_name} has no query {} to use as its list_query", inspect(query_name)));
956
+ return Err(format!(
957
+ "{agg_name} has no query {} to use as its list_query",
958
+ inspect(query_name)
959
+ ));
781
960
  };
782
961
  let arguments = ui_schema::array(query, "attributes");
783
962
  if arguments.is_empty() {
@@ -791,17 +970,25 @@ fn validate_list_query(agg_name: &str, aggregate: &Value, query_name: &Value) ->
791
970
  }
792
971
 
793
972
  fn find_query<'a>(owner: &'a Value, name: &str) -> Option<&'a Value> {
794
- ui_schema::array(owner, "queries").iter().find(|q| q.get("name").and_then(|v| v.as_str()) == Some(name))
973
+ ui_schema::array(owner, "queries")
974
+ .iter()
975
+ .find(|q| q.get("name").and_then(|v| v.as_str()) == Some(name))
795
976
  }
796
977
 
797
978
  // Cannot check whether a `$field` exists on the source aggregate, since every one of its own
798
979
  // attributes is fair game — a real gap, and the same one Ruby has.
799
- fn validate_after_create(agg_name: &str, after_create: &Value, aggregates: &[&Value], domain_ir: &Value) -> Refusal {
980
+ fn validate_after_create(
981
+ agg_name: &str,
982
+ after_create: &Value,
983
+ aggregates: &[&Value],
984
+ domain_ir: &Value,
985
+ ) -> Refusal {
800
986
  let Some(dispatch_to) = present(after_create.get("dispatch")) else {
801
987
  return Err(format!("{agg_name}'s after_create names no dispatch"));
802
988
  };
803
989
  let dotted = ruby_to_s(dispatch_to);
804
- let (target_agg_name, target_command_name) = dotted.split_once('.').unwrap_or((dotted.as_str(), ""));
990
+ let (target_agg_name, target_command_name) =
991
+ dotted.split_once('.').unwrap_or((dotted.as_str(), ""));
805
992
 
806
993
  let Some(target_agg) = find_aggregate(aggregates, target_agg_name) else {
807
994
  return Err(format!(
@@ -818,7 +1005,9 @@ fn validate_after_create(agg_name: &str, after_create: &Value, aggregates: &[&Va
818
1005
  ));
819
1006
  };
820
1007
 
821
- let Some(args) = after_create.get("args").and_then(|v| v.as_object()) else { return Ok(()) };
1008
+ let Some(args) = after_create.get("args").and_then(|v| v.as_object()) else {
1009
+ return Ok(());
1010
+ };
822
1011
  for (key, spec) in args {
823
1012
  if ui_schema::find_attribute(target_command, key).is_none() {
824
1013
  return Err(format!(
@@ -833,7 +1022,9 @@ fn validate_after_create(agg_name: &str, after_create: &Value, aggregates: &[&Va
833
1022
  }
834
1023
 
835
1024
  fn find_command<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
836
- ui_schema::commands(aggregate).into_iter().find(|c| c.get("name").and_then(|v| v.as_str()) == Some(name))
1025
+ ui_schema::commands(aggregate)
1026
+ .into_iter()
1027
+ .find(|c| c.get("name").and_then(|v| v.as_str()) == Some(name))
837
1028
  }
838
1029
 
839
1030
  fn validate_after_create_arg_spec(
@@ -843,7 +1034,11 @@ fn validate_after_create_arg_spec(
843
1034
  aggregates: &[&Value],
844
1035
  _domain_ir: &Value,
845
1036
  ) -> Refusal {
846
- let Some(count_query) = spec.get("count_query").filter(|_| spec.is_object()).and_then(|v| present(Some(v))) else {
1037
+ let Some(count_query) = spec
1038
+ .get("count_query")
1039
+ .filter(|_| spec.is_object())
1040
+ .and_then(|v| present(Some(v)))
1041
+ else {
847
1042
  return Ok(());
848
1043
  };
849
1044
  let dotted = ruby_to_s(count_query);
@@ -901,8 +1096,10 @@ fn validate_after_create_arg_spec(
901
1096
  // `collection` names a collection key, the one `key:` config can override, since a stat sits
902
1097
  // above any one aggregate the same way a table does.
903
1098
  fn validate_overview(overview: &Value, aggregates: &[&Value], config: &Value) -> Refusal {
904
- let by_key: Vec<(String, &Value)> =
905
- aggregates.iter().map(|a| (ui_schema::collection_key(a, config), *a)).collect();
1099
+ let by_key: Vec<(String, &Value)> = aggregates
1100
+ .iter()
1101
+ .map(|a| (ui_schema::collection_key(a, config), *a))
1102
+ .collect();
906
1103
 
907
1104
  for stat in list(overview.get("stats")) {
908
1105
  validate_stat(stat, &by_key)?;
@@ -916,11 +1113,14 @@ fn validate_stat(stat: &Value, by_key: &[(String, &Value)]) -> Refusal {
916
1113
  };
917
1114
  let key = ruby_to_s(collection);
918
1115
  let Some((_, aggregate)) = by_key.iter().find(|(k, _)| *k == key) else {
919
- return Err(format!("overview stat names collection {}, which isn't a real collection key", inspect(collection)));
1116
+ return Err(format!(
1117
+ "overview stat names collection {}, which isn't a real collection key",
1118
+ inspect(collection)
1119
+ ));
920
1120
  };
921
1121
 
922
1122
  let label = stat.get("label").cloned().unwrap_or(Value::Null);
923
- let fold = present(stat.get("as")).map(ruby_to_s).unwrap_or_else(|| "count".to_string());
1123
+ let fold = present(stat.get("as")).map_or_else(|| "count".to_string(), ruby_to_s);
924
1124
  if !VALID_FOLDS.contains(&fold.as_str()) {
925
1125
  return Err(format!(
926
1126
  "overview stat {}'s as: {} isn't one of {}",
@@ -930,16 +1130,31 @@ fn validate_stat(stat: &Value, by_key: &[(String, &Value)]) -> Refusal {
930
1130
  ));
931
1131
  }
932
1132
  if fold == "money_sum" && present(stat.get("field")).is_none() {
933
- return Err(format!("overview stat {} folds as money_sum but names no field", inspect(&label)));
1133
+ return Err(format!(
1134
+ "overview stat {} folds as money_sum but names no field",
1135
+ inspect(&label)
1136
+ ));
934
1137
  }
935
1138
 
936
- validate_where(stat.get("where").unwrap_or(&Value::Null), aggregate, by_key, &label)
1139
+ validate_where(
1140
+ stat.get("where").unwrap_or(&Value::Null),
1141
+ aggregate,
1142
+ by_key,
1143
+ &label,
1144
+ )
937
1145
  }
938
1146
 
939
1147
  // Recurses through the same shape index.html's `matchesWhere` reads at evaluation time: `not:`
940
1148
  // wraps another whole clause, and `has_related`/`not_has_related` is checked below.
941
- fn validate_where(clause: &Value, aggregate: &Value, by_key: &[(String, &Value)], label: &Value) -> Refusal {
942
- let Some(object) = clause.as_object() else { return Ok(()) };
1149
+ fn validate_where(
1150
+ clause: &Value,
1151
+ aggregate: &Value,
1152
+ by_key: &[(String, &Value)],
1153
+ label: &Value,
1154
+ ) -> Refusal {
1155
+ let Some(object) = clause.as_object() else {
1156
+ return Ok(());
1157
+ };
943
1158
  if let Some(negated) = object.get("not") {
944
1159
  return validate_where(negated, aggregate, by_key, label);
945
1160
  }
@@ -958,7 +1173,11 @@ fn validate_where(clause: &Value, aggregate: &Value, by_key: &[(String, &Value)]
958
1173
  Value::Object(map) => map
959
1174
  .get("eq")
960
1175
  .or_else(|| map.get("ne"))
961
- .or_else(|| map.get("in").and_then(|v| v.as_array()).and_then(|a| a.first()))
1176
+ .or_else(|| {
1177
+ map.get("in")
1178
+ .and_then(|v| v.as_array())
1179
+ .and_then(|a| a.first())
1180
+ })
962
1181
  .cloned()
963
1182
  .unwrap_or(Value::Null),
964
1183
  other => other.clone(),
@@ -978,9 +1197,17 @@ fn validate_where(clause: &Value, aggregate: &Value, by_key: &[(String, &Value)]
978
1197
  }
979
1198
 
980
1199
  // Spelled `field:`, not `on:` — YAML 1.1 reads a bare `on:` as the boolean `true`.
981
- fn validate_has_related(spec: &Value, aggregate: &Value, by_key: &[(String, &Value)], label: &Value) -> Refusal {
1200
+ fn validate_has_related(
1201
+ spec: &Value,
1202
+ aggregate: &Value,
1203
+ by_key: &[(String, &Value)],
1204
+ label: &Value,
1205
+ ) -> Refusal {
982
1206
  let Some(target_key) = present(spec.get("collection")) else {
983
- return Err(format!("overview stat {}'s has_related names no collection", inspect(label)));
1207
+ return Err(format!(
1208
+ "overview stat {}'s has_related names no collection",
1209
+ inspect(label)
1210
+ ));
984
1211
  };
985
1212
  let Some((_, target)) = by_key.iter().find(|(k, _)| *k == ruby_to_s(target_key)) else {
986
1213
  return Err(format!(
@@ -991,7 +1218,10 @@ fn validate_has_related(spec: &Value, aggregate: &Value, by_key: &[(String, &Val
991
1218
  };
992
1219
 
993
1220
  let Some(field) = present(spec.get("field")) else {
994
- return Err(format!("overview stat {}'s has_related names no field:", inspect(label)));
1221
+ return Err(format!(
1222
+ "overview stat {}'s has_related names no field:",
1223
+ inspect(label)
1224
+ ));
995
1225
  };
996
1226
  if ui_schema::find_attribute(target, &ruby_to_s(field)).is_none() {
997
1227
  return Err(format!(
@@ -1002,7 +1232,7 @@ fn validate_has_related(spec: &Value, aggregate: &Value, by_key: &[(String, &Val
1002
1232
  ));
1003
1233
  }
1004
1234
 
1005
- let equals = present(spec.get("equals")).map(ruby_to_s).unwrap_or_else(|| "id".to_string());
1235
+ let equals = present(spec.get("equals")).map_or_else(|| "id".to_string(), ruby_to_s);
1006
1236
  if equals == "id" || ui_schema::find_attribute(aggregate, &equals).is_some() {
1007
1237
  return Ok(());
1008
1238
  }
@@ -1025,10 +1255,16 @@ fn inspect(value: &Value) -> String {
1025
1255
  // same distinction, so the number's own JSON spelling is right.
1026
1256
  Value::Number(number) => number.to_string(),
1027
1257
  Value::String(text) => format!("{text:?}"),
1028
- Value::Array(items) => format!("[{}]", items.iter().map(inspect).collect::<Vec<_>>().join(", ")),
1258
+ Value::Array(items) => format!(
1259
+ "[{}]",
1260
+ items.iter().map(inspect).collect::<Vec<_>>().join(", ")
1261
+ ),
1029
1262
  Value::Object(map) => format!(
1030
1263
  "{{{}}}",
1031
- map.iter().map(|(key, value)| format!("{key:?} => {}", inspect(value))).collect::<Vec<_>>().join(", ")
1264
+ map.iter()
1265
+ .map(|(key, value)| format!("{key:?} => {}", inspect(value)))
1266
+ .collect::<Vec<_>>()
1267
+ .join(", ")
1032
1268
  ),
1033
1269
  }
1034
1270
  }
@@ -1086,8 +1322,8 @@ mod tests {
1086
1322
  json!({"states": {"Event": {"open": {}, "closed": {}}}})
1087
1323
  }
1088
1324
 
1089
- fn refusal(config: Value) -> String {
1090
- validate(&config, &domain()).expect_err("this config should have been refused")
1325
+ fn refusal(config: &Value) -> String {
1326
+ validate(config, &domain()).expect_err("this config should have been refused")
1091
1327
  }
1092
1328
 
1093
1329
  #[test]
@@ -1098,7 +1334,7 @@ mod tests {
1098
1334
  #[test]
1099
1335
  fn every_real_state_needs_an_entry_even_a_bare_one() {
1100
1336
  assert_eq!(
1101
- refusal(json!({"states": {"Event": {"open": {}}}})),
1337
+ refusal(&json!({"states": {"Event": {"open": {}}}})),
1102
1338
  "no presentation entry for Event.closed — every real state needs one, even a bare one"
1103
1339
  );
1104
1340
  }
@@ -1114,7 +1350,7 @@ mod tests {
1114
1350
  #[test]
1115
1351
  fn a_state_section_naming_an_aggregate_this_domain_does_not_declare_refuses() {
1116
1352
  assert_eq!(
1117
- refusal(json!({"states": {"Ghost": {"gone": {}}}})),
1353
+ refusal(&json!({"states": {"Ghost": {"gone": {}}}})),
1118
1354
  "Ghost is not a real aggregate this domain declares"
1119
1355
  );
1120
1356
  }
@@ -1123,7 +1359,10 @@ mod tests {
1123
1359
  fn a_state_this_aggregate_does_not_declare_refuses_and_names_the_ones_it_does() {
1124
1360
  let mut config = complete();
1125
1361
  config["states"]["Event"]["cancelled"] = json!({});
1126
- assert_eq!(refusal(config), "Event has no state \"cancelled\" — it declares open, closed");
1362
+ assert_eq!(
1363
+ refusal(&config),
1364
+ "Event has no state \"cancelled\" — it declares open, closed"
1365
+ );
1127
1366
  }
1128
1367
 
1129
1368
  #[test]
@@ -1131,7 +1370,7 @@ mod tests {
1131
1370
  let mut config = complete();
1132
1371
  config["states"]["Event"]["open"] = json!({"tone": "chartreuse"});
1133
1372
  assert_eq!(
1134
- refusal(config),
1373
+ refusal(&config),
1135
1374
  "Event.open's tone \"chartreuse\" isn't one of good, warn, danger, muted, accent"
1136
1375
  );
1137
1376
  }
@@ -1140,7 +1379,10 @@ mod tests {
1140
1379
  fn attention_is_a_real_boolean_not_a_string() {
1141
1380
  let mut config = complete();
1142
1381
  config["states"]["Event"]["open"] = json!({"attention": "yes"});
1143
- assert_eq!(refusal(config), "Event.open's attention must be true or false, not \"yes\"");
1382
+ assert_eq!(
1383
+ refusal(&config),
1384
+ "Event.open's attention must be true or false, not \"yes\""
1385
+ );
1144
1386
  }
1145
1387
 
1146
1388
  #[test]
@@ -1148,7 +1390,7 @@ mod tests {
1148
1390
  let mut config = complete();
1149
1391
  config["collections"] = json!({"Event": {"columns": [{"field": "nope"}]}});
1150
1392
  assert_eq!(
1151
- refusal(config),
1393
+ refusal(&config),
1152
1394
  "Event has no attribute \"nope\" to put in its columns — it declares slug, name, price, seats, organizer"
1153
1395
  );
1154
1396
  }
@@ -1163,44 +1405,69 @@ mod tests {
1163
1405
  #[test]
1164
1406
  fn a_columns_sortable_is_a_boolean_and_its_sort_default_is_one_of_two_words() {
1165
1407
  let mut config = complete();
1166
- config["collections"] = json!({"Event": {"columns": [{"field": "slug", "sortable": "true"}]}});
1167
- assert_eq!(refusal(config), "Event's column \"slug\" has sortable \"true\", not true or false");
1408
+ config["collections"] =
1409
+ json!({"Event": {"columns": [{"field": "slug", "sortable": "true"}]}});
1410
+ assert_eq!(
1411
+ refusal(&config),
1412
+ "Event's column \"slug\" has sortable \"true\", not true or false"
1413
+ );
1168
1414
 
1169
1415
  let mut config = complete();
1170
- config["collections"] = json!({"Event": {"columns": [{"field": "slug", "sort_default": "sideways"}]}});
1171
- assert_eq!(refusal(config), "Event's column \"slug\" has sort_default \"sideways\", not asc or desc");
1416
+ config["collections"] =
1417
+ json!({"Event": {"columns": [{"field": "slug", "sort_default": "sideways"}]}});
1418
+ assert_eq!(
1419
+ refusal(&config),
1420
+ "Event's column \"slug\" has sort_default \"sideways\", not asc or desc"
1421
+ );
1172
1422
  }
1173
1423
 
1174
1424
  #[test]
1175
1425
  fn nav_order_is_a_number() {
1176
1426
  let mut config = complete();
1177
1427
  config["collections"] = json!({"Event": {"nav_order": "3"}});
1178
- assert_eq!(refusal(config), "Event's nav_order must be a number, not \"3\"");
1428
+ assert_eq!(
1429
+ refusal(&config),
1430
+ "Event's nav_order must be a number, not \"3\""
1431
+ );
1179
1432
  }
1180
1433
 
1181
1434
  #[test]
1182
1435
  fn a_precondition_only_makes_sense_against_a_reference() {
1183
1436
  let mut config = complete();
1184
- config["collections"] = json!({"Event": {"preconditions": [{"field": "slug", "state": "open"}]}});
1185
- assert_eq!(refusal(config), "Event.slug isn't a reference — a precondition only makes sense against one");
1437
+ config["collections"] =
1438
+ json!({"Event": {"preconditions": [{"field": "slug", "state": "open"}]}});
1439
+ assert_eq!(
1440
+ refusal(&config),
1441
+ "Event.slug isn't a reference — a precondition only makes sense against one"
1442
+ );
1186
1443
  }
1187
1444
 
1188
1445
  #[test]
1189
1446
  fn a_precondition_against_a_lifecycle_less_target_names_the_states_it_has() {
1190
1447
  let mut config = complete();
1191
- config["collections"] = json!({"Event": {"preconditions": [{"field": "organizer", "state": "active"}]}});
1192
- assert_eq!(refusal(config), "Event.organizer points at Person, which has no state \"active\" — it declares ");
1448
+ config["collections"] =
1449
+ json!({"Event": {"preconditions": [{"field": "organizer", "state": "active"}]}});
1450
+ assert_eq!(
1451
+ refusal(&config),
1452
+ "Event.organizer points at Person, which has no state \"active\" — it declares "
1453
+ );
1193
1454
  }
1194
1455
 
1195
1456
  #[test]
1196
1457
  fn a_format_names_a_real_attribute_and_one_of_two_formats() {
1197
1458
  let mut config = complete();
1198
1459
  config["collections"] = json!({"Event": {"field_formats": {"nope": "date"}}});
1199
- assert_eq!(refusal(config), "Event has no attribute \"nope\" to format");
1460
+ assert_eq!(
1461
+ refusal(&config),
1462
+ "Event has no attribute \"nope\" to format"
1463
+ );
1200
1464
 
1201
1465
  let mut config = complete();
1202
1466
  config["collections"] = json!({"Event": {"field_formats": {"slug": "shouty"}}});
1203
- assert_eq!(refusal(config), "Event.slug's format \"shouty\" isn't one of date, percent");
1467
+ assert_eq!(
1468
+ refusal(&config),
1469
+ "Event.slug's format \"shouty\" isn't one of date, percent"
1470
+ );
1204
1471
  }
1205
1472
 
1206
1473
  #[test]
@@ -1213,20 +1480,36 @@ mod tests {
1213
1480
  #[test]
1214
1481
  fn an_identity_rule_names_a_plain_field_and_one_of_three_strategies() {
1215
1482
  let mut config = complete();
1216
- config["collections"] = json!({"Event": {"identity": {"field": "organizer", "strategy": "slug"}}});
1217
- assert_eq!(refusal(config), "Event.organizer is a reference — identity can only derive a plain field");
1483
+ config["collections"] =
1484
+ json!({"Event": {"identity": {"field": "organizer", "strategy": "slug"}}});
1485
+ assert_eq!(
1486
+ refusal(&config),
1487
+ "Event.organizer is a reference — identity can only derive a plain field"
1488
+ );
1218
1489
 
1219
1490
  let mut config = complete();
1220
- config["collections"] = json!({"Event": {"identity": {"field": "slug", "strategy": "guesswork"}}});
1221
- assert_eq!(refusal(config), "Event's identity strategy \"guesswork\" isn't 'slug', 'sequence', or 'port'");
1491
+ config["collections"] =
1492
+ json!({"Event": {"identity": {"field": "slug", "strategy": "guesswork"}}});
1493
+ assert_eq!(
1494
+ refusal(&config),
1495
+ "Event's identity strategy \"guesswork\" isn't 'slug', 'sequence', or 'port'"
1496
+ );
1222
1497
 
1223
1498
  let mut config = complete();
1224
- config["collections"] = json!({"Event": {"identity": {"field": "slug", "strategy": "slug"}}});
1225
- assert_eq!(refusal(config), "Event's identity slug strategy needs a source field");
1499
+ config["collections"] =
1500
+ json!({"Event": {"identity": {"field": "slug", "strategy": "slug"}}});
1501
+ assert_eq!(
1502
+ refusal(&config),
1503
+ "Event's identity slug strategy needs a source field"
1504
+ );
1226
1505
 
1227
1506
  let mut config = complete();
1228
- config["collections"] = json!({"Event": {"identity": {"field": "slug", "strategy": "sequence"}}});
1229
- assert_eq!(refusal(config), "Event's identity sequence strategy needs a prefix");
1507
+ config["collections"] =
1508
+ json!({"Event": {"identity": {"field": "slug", "strategy": "sequence"}}});
1509
+ assert_eq!(
1510
+ refusal(&config),
1511
+ "Event's identity sequence strategy needs a prefix"
1512
+ );
1230
1513
 
1231
1514
  let mut config = complete();
1232
1515
  config["collections"] =
@@ -1237,9 +1520,10 @@ mod tests {
1237
1520
  #[test]
1238
1521
  fn a_detail_fields_display_is_one_of_five_placements() {
1239
1522
  let mut config = complete();
1240
- config["collections"] = json!({"Event": {"detail_fields": [{"field": "name", "display": "interpretive"}]}});
1523
+ config["collections"] =
1524
+ json!({"Event": {"detail_fields": [{"field": "name", "display": "interpretive"}]}});
1241
1525
  assert_eq!(
1242
- refusal(config),
1526
+ refusal(&config),
1243
1527
  "Event's detail field \"name\" has display \"interpretive\", not one of simple, prose, bullet, mono, rows"
1244
1528
  );
1245
1529
  }
@@ -1247,24 +1531,34 @@ mod tests {
1247
1531
  #[test]
1248
1532
  fn row_columns_are_checked_against_the_rows_shape_the_field_actually_holds() {
1249
1533
  let mut config = complete();
1250
- config["collections"] = json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["row"]}]}});
1534
+ config["collections"] =
1535
+ json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["row"]}]}});
1251
1536
  assert!(validate(&config, &domain()).is_ok());
1252
1537
 
1253
1538
  let mut config = complete();
1254
- config["collections"] = json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["aisle"]}]}});
1255
- assert_eq!(refusal(config), "Event's detail field \"seats\" has no row column \"aisle\" — it has row, number");
1539
+ config["collections"] =
1540
+ json!({"Event": {"detail_fields": [{"field": "seats", "columns": ["aisle"]}]}});
1541
+ assert_eq!(
1542
+ refusal(&config),
1543
+ "Event's detail field \"seats\" has no row column \"aisle\" — it has row, number"
1544
+ );
1256
1545
 
1257
1546
  let mut config = complete();
1258
- config["collections"] = json!({"Event": {"detail_fields": [{"field": "name", "columns": ["row"]}]}});
1259
- assert_eq!(refusal(config), "Event's detail field \"name\" has columns: but isn't a rows-shaped field");
1547
+ config["collections"] =
1548
+ json!({"Event": {"detail_fields": [{"field": "name", "columns": ["row"]}]}});
1549
+ assert_eq!(
1550
+ refusal(&config),
1551
+ "Event's detail field \"name\" has columns: but isn't a rows-shaped field"
1552
+ );
1260
1553
  }
1261
1554
 
1262
1555
  #[test]
1263
1556
  fn a_state_filter_needs_an_entity_backed_rows_field() {
1264
1557
  let mut config = complete();
1265
- config["collections"] = json!({"Event": {"detail_fields": [{"field": "seats", "state_filter": "taken"}]}});
1558
+ config["collections"] =
1559
+ json!({"Event": {"detail_fields": [{"field": "seats", "state_filter": "taken"}]}});
1266
1560
  assert_eq!(
1267
- refusal(config),
1561
+ refusal(&config),
1268
1562
  "Event's detail field \"seats\" has state_filter: but isn't an entity-backed rows field"
1269
1563
  );
1270
1564
  }
@@ -1277,12 +1571,15 @@ mod tests {
1277
1571
 
1278
1572
  let mut config = complete();
1279
1573
  config["collections"] = json!({"Event": {"list_query": "Nowhere"}});
1280
- assert_eq!(refusal(config), "Event has no query \"Nowhere\" to use as its list_query");
1574
+ assert_eq!(
1575
+ refusal(&config),
1576
+ "Event has no query \"Nowhere\" to use as its list_query"
1577
+ );
1281
1578
 
1282
1579
  let mut config = complete();
1283
1580
  config["collections"] = json!({"Event": {"list_query": "ByOrganizer"}});
1284
1581
  assert_eq!(
1285
- refusal(config),
1582
+ refusal(&config),
1286
1583
  "Event's list_query \"ByOrganizer\" takes its own arguments (organizer) — \
1287
1584
  a generic table view has nowhere to get them from"
1288
1585
  );
@@ -1293,7 +1590,7 @@ mod tests {
1293
1590
  let mut config = complete();
1294
1591
  config["collections"] = json!({"Event": {"after_create": {"dispatch": "Ghost.Add"}}});
1295
1592
  assert_eq!(
1296
- refusal(config),
1593
+ refusal(&config),
1297
1594
  "Event's after_create dispatches \"Ghost.Add\", but \"Ghost\" is not a real aggregate this domain declares"
1298
1595
  );
1299
1596
 
@@ -1302,7 +1599,7 @@ mod tests {
1302
1599
  let mut config = complete();
1303
1600
  config["collections"] = json!({"Event": {"after_create": {"dispatch": "Person.Nope"}}});
1304
1601
  assert_eq!(
1305
- refusal(config),
1602
+ refusal(&config),
1306
1603
  "Event's after_create dispatches \"Person.Nope\", but Person declares no command \"Nope\""
1307
1604
  );
1308
1605
  }
@@ -1310,7 +1607,8 @@ mod tests {
1310
1607
  #[test]
1311
1608
  fn a_storable_after_create_still_refuses_because_nothing_stores_it_yet() {
1312
1609
  let mut ir = domain();
1313
- ir["aggregates"][1]["commands"] = json!([{"name": "Note", "references": "Person", "attributes": []}]);
1610
+ ir["aggregates"][1]["commands"] =
1611
+ json!([{"name": "Note", "references": "Person", "attributes": []}]);
1314
1612
  let mut config = complete();
1315
1613
  config["collections"] = json!({"Event": {"after_create": {"dispatch": "Person.Note"}}});
1316
1614
  assert_eq!(
@@ -1324,38 +1622,46 @@ mod tests {
1324
1622
  fn an_overview_stat_names_a_real_collection_key_and_a_real_fold() {
1325
1623
  let mut config = complete();
1326
1624
  config["overview"] = json!({"stats": [{"label": "Open", "collection": "nowhere"}]});
1327
- assert_eq!(refusal(config), "overview stat names collection \"nowhere\", which isn't a real collection key");
1625
+ assert_eq!(
1626
+ refusal(&config),
1627
+ "overview stat names collection \"nowhere\", which isn't a real collection key"
1628
+ );
1328
1629
 
1329
1630
  let mut config = complete();
1330
- config["overview"] = json!({"stats": [{"label": "Open", "collection": "events", "as": "vibes"}]});
1331
- assert_eq!(refusal(config), "overview stat \"Open\"'s as: \"vibes\" isn't one of count, money_sum");
1631
+ config["overview"] =
1632
+ json!({"stats": [{"label": "Open", "collection": "events", "as": "vibes"}]});
1633
+ assert_eq!(
1634
+ refusal(&config),
1635
+ "overview stat \"Open\"'s as: \"vibes\" isn't one of count, money_sum"
1636
+ );
1332
1637
 
1333
1638
  let mut config = complete();
1334
- config["overview"] = json!({"stats": [{"label": "Take", "collection": "events", "as": "money_sum"}]});
1335
- assert_eq!(refusal(config), "overview stat \"Take\" folds as money_sum but names no field");
1639
+ config["overview"] =
1640
+ json!({"stats": [{"label": "Take", "collection": "events", "as": "money_sum"}]});
1641
+ assert_eq!(
1642
+ refusal(&config),
1643
+ "overview stat \"Take\" folds as money_sum but names no field"
1644
+ );
1336
1645
  }
1337
1646
 
1338
1647
  #[test]
1339
1648
  fn a_stats_where_clause_is_walked_including_through_a_negation() {
1340
1649
  let mut config = complete();
1341
- config["overview"] =
1342
- json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": "ajar"}}]});
1650
+ config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": "ajar"}}]});
1343
1651
  assert_eq!(
1344
- refusal(config),
1652
+ refusal(&config),
1345
1653
  "overview stat \"Live\"'s where: state \"ajar\" isn't one of Event's own states (open, closed)"
1346
1654
  );
1347
1655
 
1348
1656
  let mut config = complete();
1349
- config["overview"] =
1350
- json!({"stats": [{"label": "Live", "collection": "events", "where": {"not": {"state": {"eq": "ajar"}}}}]});
1657
+ config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"not": {"state": {"eq": "ajar"}}}}]});
1351
1658
  assert_eq!(
1352
- refusal(config),
1659
+ refusal(&config),
1353
1660
  "overview stat \"Live\"'s where: state \"ajar\" isn't one of Event's own states (open, closed)"
1354
1661
  );
1355
1662
 
1356
1663
  let mut config = complete();
1357
- config["overview"] =
1358
- json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": {"in": ["open"]}}}]});
1664
+ config["overview"] = json!({"stats": [{"label": "Live", "collection": "events", "where": {"state": {"in": ["open"]}}}]});
1359
1665
  assert!(validate(&config, &domain()).is_ok());
1360
1666
  }
1361
1667
 
@@ -1364,13 +1670,16 @@ mod tests {
1364
1670
  let mut config = complete();
1365
1671
  config["overview"] = json!({"stats": [{"label": "Hosted", "collection": "events",
1366
1672
  "where": {"has_related": {"collection": "persons", "field": "nope"}}}]});
1367
- assert_eq!(refusal(config), "overview stat \"Hosted\"'s has_related.field \"nope\" isn't a real field on Person");
1673
+ assert_eq!(
1674
+ refusal(&config),
1675
+ "overview stat \"Hosted\"'s has_related.field \"nope\" isn't a real field on Person"
1676
+ );
1368
1677
 
1369
1678
  let mut config = complete();
1370
1679
  config["overview"] = json!({"stats": [{"label": "Hosted", "collection": "events",
1371
1680
  "where": {"has_related": {"collection": "persons", "field": "email", "equals": "nope"}}}]});
1372
1681
  assert_eq!(
1373
- refusal(config),
1682
+ refusal(&config),
1374
1683
  "overview stat \"Hosted\"'s has_related.equals \"nope\" isn't a real field on Event"
1375
1684
  );
1376
1685
 
@@ -1383,7 +1692,9 @@ mod tests {
1383
1692
  #[test]
1384
1693
  fn a_bare_column_name_and_a_descriptor_both_dispatch_as_a_row() {
1385
1694
  assert_eq!(
1386
- columns_for_dispatch(Some(&json!(["slug", {"field": "name", "sortable": true, "head": "Title"}]))),
1695
+ columns_for_dispatch(Some(
1696
+ &json!(["slug", {"field": "name", "sortable": true, "head": "Title"}])
1697
+ )),
1387
1698
  vec![
1388
1699
  json!({"field": "slug"}),
1389
1700
  json!({"field": "name", "sortable": "true", "extra_json": "{\"head\":\"Title\"}"})
@@ -1418,7 +1729,11 @@ mod tests {
1418
1729
  let stats = stats_for_dispatch(Some(&json!([{"label": "Live", "collection": "events",
1419
1730
  "where": {"state": "open"}}])));
1420
1731
  assert_eq!(stats[0]["where_json"], json!("{\"state\":\"open\"}"));
1421
- assert!(stats[0].get("as").is_none(), "an absent fold is absent, not null: {}", stats[0]);
1732
+ assert!(
1733
+ stats[0].get("as").is_none(),
1734
+ "an absent fold is absent, not null: {}",
1735
+ stats[0]
1736
+ );
1422
1737
  }
1423
1738
 
1424
1739
  #[test]