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
@@ -12,8 +12,17 @@ use std::collections::HashMap;
12
12
  ///
13
13
  /// Local entities are checked first: value objects are merged domain-wide, so a same-named
14
14
  /// value object (Command's `Argument`) must not shadow Syntax's own `Argument` entity.
15
- pub fn append_element<'a>(aggregate: &'a Json, target_type: &str, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<&'a Json> {
16
- if let Some(local) = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(target_type)) {
15
+ pub fn append_element<'a>(
16
+ aggregate: &'a Json,
17
+ target_type: &str,
18
+ value_objects_by_name: &HashMap<String, &'a Json>,
19
+ ) -> Option<&'a Json> {
20
+ if let Some(local) = aggregate
21
+ .get("entities")
22
+ .map_or_default(Json::each)
23
+ .iter()
24
+ .find(|e| e.get("name").and_then(Json::as_str) == Some(target_type))
25
+ {
17
26
  return Some(local);
18
27
  }
19
28
  value_objects_by_name.get(target_type).copied()
@@ -21,8 +30,11 @@ pub fn append_element<'a>(aggregate: &'a Json, target_type: &str, value_objects_
21
30
 
22
31
  /// Returns an entity's identity attribute and its single-field value object, when it can be
23
32
  /// auto-minted at append time.
24
- pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<(&'a Json, &'a Json)> {
25
- let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
33
+ pub fn entity_identity_mint<'a>(
34
+ entity: &'a Json,
35
+ value_objects_by_name: &HashMap<String, &'a Json>,
36
+ ) -> Option<(&'a Json, &'a Json)> {
37
+ let identified_by = entity.get("identified_by").map_or_default(Json::each);
26
38
  let id_path = identified_by.first()?.as_str()?;
27
39
  let mut parts = id_path.split('.');
28
40
  let head = parts.next()?;
@@ -31,13 +43,15 @@ pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMa
31
43
  return None;
32
44
  }
33
45
 
34
- let attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
46
+ let attrs = entity.get("attributes").map_or_default(Json::each);
35
47
  let attr = attrs.iter().find(|a| crate::attr::name(a) == head)?;
36
- let vo = value_objects_by_name.get(crate::attr::type_name(attr)).copied()?;
37
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
48
+ let vo = value_objects_by_name
49
+ .get(crate::attr::type_name(attr))
50
+ .copied()?;
51
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
38
52
  return None;
39
53
  }
40
- let vo_attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
54
+ let vo_attrs = vo.get("attributes").map_or_default(Json::each);
41
55
  if vo_attrs.len() != 1 || crate::attr::name(&vo_attrs[0]) != rest[0] {
42
56
  return None;
43
57
  }
@@ -50,26 +64,39 @@ pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMa
50
64
  /// Every `identified_by` head is compared at once (not just the first), so a composite identity
51
65
  /// is guarded the same as a single-field one — see `check_entity_collision` in
52
66
  /// `entity_element.rb` (Ruby's `heads.all?`).
53
- pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, target_field: &str, rhs: &str, value_objects_by_name: &HashMap<String, &'a Json>) -> (String, String) {
67
+ pub fn entity_list_replace_guard(
68
+ aggregate: &Json,
69
+ target_attr: &Json,
70
+ target_field: &str,
71
+ rhs: &str,
72
+ value_objects_by_name: &HashMap<String, &Json>,
73
+ ) -> (String, String) {
54
74
  let target_type = crate::attr::type_name(target_attr);
55
- let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(target_type));
56
- let entity = match entity {
57
- Some(e) => e,
58
- None => return (String::new(), rhs.to_string()),
75
+ let entity = aggregate
76
+ .get("entities")
77
+ .map_or_default(Json::each)
78
+ .iter()
79
+ .find(|e| e.get("name").and_then(Json::as_str) == Some(target_type));
80
+ let Some(entity) = entity else {
81
+ return (String::new(), rhs.to_string());
59
82
  };
60
- let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
83
+ let identified_by = entity.get("identified_by").map_or_default(Json::each);
61
84
  if identified_by.is_empty() {
62
85
  return (String::new(), rhs.to_string());
63
86
  }
64
87
 
65
- let attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
88
+ let attrs = entity.get("attributes").map_or_default(Json::each);
66
89
  let mut checks = Vec::new();
67
90
  let mut offered_exprs = Vec::new();
68
91
  for path in identified_by {
69
- let id_head = path.to_s().split('.').next().unwrap_or_default().to_string();
70
- let id_attr = match attrs.iter().find(|a| crate::attr::name(a) == id_head) {
71
- Some(a) => a,
72
- None => return (String::new(), rhs.to_string()),
92
+ let id_head = path
93
+ .to_s()
94
+ .split('.')
95
+ .next()
96
+ .unwrap_or_default()
97
+ .to_string();
98
+ let Some(id_attr) = attrs.iter().find(|a| crate::attr::name(a) == id_head) else {
99
+ return (String::new(), rhs.to_string());
73
100
  };
74
101
  let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
75
102
  checks.push(format!("prior.{id_field} == e.{id_field}"));
@@ -77,10 +104,17 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
77
104
  // Unwrap a single-field identity so `offered` prints the bare scalar, as
78
105
  // `Rendering.describe` does; Debug-printing the value object would give
79
106
  // `EntrySequence { value: 1 }`.
80
- let id_vo = value_objects_by_name.get(crate::attr::type_name(id_attr)).copied();
107
+ let id_vo = value_objects_by_name
108
+ .get(crate::attr::type_name(id_attr))
109
+ .copied();
81
110
  let offered_expr = match id_vo {
82
- Some(vo) if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) && vo.get("attributes").map(Json::each).unwrap_or(&[]).len() == 1 => {
83
- let inner = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
111
+ Some(vo)
112
+ if !vo.get("closed_set").is_some_and(Json::as_bool)
113
+ && vo.get("attributes").map_or_default(Json::each).len() == 1 =>
114
+ {
115
+ let inner = naming::rust_ident_field(crate::attr::name(
116
+ &vo.get("attributes").map_or_default(Json::each)[0],
117
+ ));
84
118
  format!("e.{id_field}.{inner}")
85
119
  }
86
120
  _ => format!("e.{id_field}"),
@@ -89,15 +123,31 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
89
123
  }
90
124
 
91
125
  let local_var = format!("replaced_{target_field}");
92
- let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
93
- let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
94
- let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
126
+ let entity_name = entity
127
+ .get("name")
128
+ .and_then(Json::as_str)
129
+ .unwrap_or_default();
130
+ let aggregate_name = aggregate
131
+ .get("name")
132
+ .and_then(Json::as_str)
133
+ .unwrap_or_default();
134
+ let identity_reading = identified_by
135
+ .iter()
136
+ .map(Json::to_s)
137
+ .collect::<Vec<_>>()
138
+ .join(", ");
95
139
  let entity_lit = naming::ruby_inspect_string(entity_name);
96
140
  let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
97
141
  let identity_lit = naming::ruby_inspect_string(&identity_reading);
98
142
  let condition = checks.join(" && ");
99
- let offered_lets: Vec<String> = offered_exprs.iter().enumerate().map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});")).collect();
100
- let offered_refs: Vec<String> = (0..offered_exprs.len()).map(|i| format!("offered_{i}.as_str()")).collect();
143
+ let offered_lets: Vec<String> = offered_exprs
144
+ .iter()
145
+ .enumerate()
146
+ .map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});"))
147
+ .collect();
148
+ let offered_refs: Vec<String> = (0..offered_exprs.len())
149
+ .map(|i| format!("offered_{i}.as_str()"))
150
+ .collect();
101
151
  let guard = format!(
102
152
  "let {local_var} = {rhs};\n for (i, e) in {local_var}.iter().enumerate() {{ if {local_var}[..i].iter().any(|prior| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }} }}\n ",
103
153
  offered_lets.join(" "), offered_refs.join(", ")
@@ -111,6 +161,10 @@ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, targe
111
161
  /// `EntityElement.check_entity_collision`'s `heads.all?` on the Ruby side. Called only once every
112
162
  /// head is confirmed present in the append's own `fields` map (composite identities never mint).
113
163
  #[allow(clippy::too_many_arguments)]
164
+ #[allow(
165
+ clippy::expect_used,
166
+ reason = "the message names an invariant an earlier check in this generator established"
167
+ )]
114
168
  fn composite_append_collision_guard(
115
169
  heads: &[String],
116
170
  fields: &[(String, String)],
@@ -140,10 +194,17 @@ fn composite_append_collision_guard(
140
194
  // Unwrap a single-field identity component so `offered` prints the bare scalar, as
141
195
  // `Rendering.describe` does; Debug-printing the value object would give
142
196
  // `LineSequence { value: 654 }`.
143
- let vo = value_objects_by_name.get(crate::attr::type_name(field_attr)).copied();
197
+ let vo = value_objects_by_name
198
+ .get(crate::attr::type_name(field_attr))
199
+ .copied();
144
200
  let offered_expr = match vo {
145
- Some(vo) if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) && vo.get("attributes").map(Json::each).unwrap_or(&[]).len() == 1 => {
146
- let inner = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
201
+ Some(vo)
202
+ if !vo.get("closed_set").is_some_and(Json::as_bool)
203
+ && vo.get("attributes").map_or_default(Json::each).len() == 1 =>
204
+ {
205
+ let inner = naming::rust_ident_field(crate::attr::name(
206
+ &vo.get("attributes").map_or_default(Json::each)[0],
207
+ ));
147
208
  format!("{rhs}.{inner}")
148
209
  }
149
210
  _ => rhs,
@@ -151,9 +212,19 @@ fn composite_append_collision_guard(
151
212
  offered_exprs.push(offered_expr);
152
213
  }
153
214
 
154
- let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
155
- let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
156
- let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
215
+ let entity_name = entity
216
+ .get("name")
217
+ .and_then(Json::as_str)
218
+ .unwrap_or_default();
219
+ let aggregate_name = aggregate
220
+ .get("name")
221
+ .and_then(Json::as_str)
222
+ .unwrap_or_default();
223
+ let identity_reading = identified_by
224
+ .iter()
225
+ .map(Json::to_s)
226
+ .collect::<Vec<_>>()
227
+ .join(", ");
157
228
  let entity_lit = naming::ruby_inspect_string(entity_name);
158
229
  let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
159
230
  let identity_lit = naming::ruby_inspect_string(&identity_reading);
@@ -163,7 +234,9 @@ fn composite_append_collision_guard(
163
234
  .enumerate()
164
235
  .map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});"))
165
236
  .collect();
166
- let offered_refs: Vec<String> = (0..offered_exprs.len()).map(|i| format!("offered_{i}.as_str()")).collect();
237
+ let offered_refs: Vec<String> = (0..offered_exprs.len())
238
+ .map(|i| format!("offered_{i}.as_str()"))
239
+ .collect();
167
240
 
168
241
  format!(
169
242
  "if record.{target_field}.iter().any(|e| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }}\n ",
@@ -179,21 +252,97 @@ pub fn append_field_source(source: &str) -> Literal {
179
252
  /// Whether any effect reads the record's own state (a `state(:field)` source, or an expression that
180
253
  /// may name a field), so the closure must bind `let pre`.
181
254
  pub fn reads_pre_state(mutations: &[Json]) -> bool {
182
- mutations.iter().any(|m| match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
183
- "set" => matches!(m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default().as_str(), "state" | "expression"),
184
- "append" => match m.get("fields") {
185
- Some(Json::Object(pairs)) => pairs.iter().any(|(_, source)| source.to_s().starts_with("state(:")),
255
+ mutations.iter().any(
256
+ |m| match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
257
+ "set" => matches!(
258
+ m.get("source")
259
+ .and_then(|s| s.get("kind"))
260
+ .map(Json::to_s)
261
+ .unwrap_or_default()
262
+ .as_str(),
263
+ "state" | "expression"
264
+ ),
265
+ "append" => match m.get("fields") {
266
+ Some(Json::Object(pairs)) => pairs
267
+ .iter()
268
+ .any(|(_, source)| source.to_s().starts_with("state(:")),
269
+ _ => false,
270
+ },
186
271
  _ => false,
187
272
  },
188
- _ => false,
189
- })
273
+ )
190
274
  }
191
275
 
192
276
  pub fn pre_state_line() -> String {
193
277
  " let pre = record.clone();".to_string()
194
278
  }
195
279
 
280
+ /// The value an arithmetic op (`increment`/`decrement`/`multiply`/`clamp`) reads from its target.
281
+ ///
282
+ /// An absent optional target reads as 0 (C4.4), the integer field zeroed and every other field at
283
+ /// its declared default. A value object whose other fields lack defaults has no sensible zero, so
284
+ /// that case keeps the unwrap.
285
+ fn numeric_current(
286
+ optional: bool,
287
+ target_field: &str,
288
+ target_attr: &Json,
289
+ numeric_field: &str,
290
+ kind: crate::bridging::NumericKind,
291
+ value_objects_by_name: &HashMap<String, &Json>,
292
+ ) -> String {
293
+ if !optional {
294
+ return format!("record.{target_field}.clone()");
295
+ }
296
+ let vo_type = crate::attr::type_name(target_attr);
297
+ let attrs = value_objects_by_name
298
+ .get(vo_type)
299
+ .and_then(|vo| vo.get("attributes"))
300
+ .map_or_default(Json::each);
301
+ let zero_fields: Option<Vec<String>> = attrs
302
+ .iter()
303
+ .map(|f| {
304
+ let name = crate::attr::name(f);
305
+ let rhs = if name == numeric_field {
306
+ Some(
307
+ match kind {
308
+ crate::bridging::NumericKind::Integer => "0",
309
+ crate::bridging::NumericKind::Float => "0.0",
310
+ }
311
+ .to_string(),
312
+ )
313
+ } else {
314
+ crate::attr::default(f).map(naming::literal_rhs)
315
+ };
316
+ rhs.map(|rhs| format!("{}: {rhs}", naming::rust_ident_field(name)))
317
+ })
318
+ .collect();
319
+ zero_fields.map_or_else(
320
+ || format!("record.{target_field}.clone().unwrap()"),
321
+ |fields| {
322
+ format!(
323
+ "record.{target_field}.clone().unwrap_or({} {{ {} }})",
324
+ naming::rust_ident(vo_type),
325
+ fields.join(", ")
326
+ )
327
+ },
328
+ )
329
+ }
330
+
331
+ /// The mutation for a text operand: refuses `TypeMismatch` as Ruby's rewrap of the amount does.
332
+ ///
333
+ /// The `?` on an `Err` keeps the closure's `Ok(())` reachable, and the offered text renders
334
+ /// through `Json::describe`, as every refusal quotes a value.
335
+ fn text_operand_refusal(target_type: &str, integer_field: &str, text_expr: &str) -> String {
336
+ format!(
337
+ "Err::<(), _>(crate::kernel::Refusal::TypeMismatch(format!(\"{target_type}.{integer_field} expects Integer, got {{}}\", crate::kernel::Json::Str({text_expr}.clone()).describe())))?;"
338
+ )
339
+ }
340
+
196
341
  // Integer arithmetic that leaves signed 64 bits is a `Refusal::Fault`, never a wrap or a panic.
342
+ #[allow(
343
+ clippy::panic,
344
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
345
+ )]
197
346
  fn checked_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &str) -> String {
198
347
  let checked = match symbol {
199
348
  "+" => "checked_add",
@@ -206,19 +355,61 @@ fn checked_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &s
206
355
  )
207
356
  }
208
357
 
209
- pub fn literal_problem(mutation: &Json, field_name: &str, lit: &Literal, field_attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
210
- if crate::bridging::literal_set_bridgeable(lit, Some(crate::attr::type_name(field_attr)), value_objects_by_name) {
358
+ // A Float result must stay finite (C3.4); a NaN or infinity is a `Refusal::Fault`, worded as Ruby words it.
359
+ fn float_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &str) -> String {
360
+ format!(
361
+ "{{ let amount = {amount_expr}; crate::kernel::ruby_float::finite_or_fault(\"{op}\", \"{symbol}\", current.{field_ident}, amount, current.{field_ident} {symbol} amount)? }}"
362
+ )
363
+ }
364
+
365
+ // The new numeric field value: `checked_*` for an Integer target, `finite_or_fault` for a Float one.
366
+ fn arithmetic_field_value(
367
+ kind: crate::bridging::NumericKind,
368
+ op: &str,
369
+ field_ident: &str,
370
+ symbol: &str,
371
+ amount_expr: &str,
372
+ ) -> String {
373
+ match kind {
374
+ crate::bridging::NumericKind::Integer => {
375
+ checked_arithmetic(op, field_ident, symbol, amount_expr)
376
+ }
377
+ crate::bridging::NumericKind::Float => {
378
+ float_arithmetic(op, field_ident, symbol, amount_expr)
379
+ }
380
+ }
381
+ }
382
+
383
+ pub fn literal_problem(
384
+ mutation: &Json,
385
+ field_name: &str,
386
+ lit: &Literal,
387
+ field_attr: &Json,
388
+ value_objects_by_name: &HashMap<String, &Json>,
389
+ ) -> Option<String> {
390
+ if crate::bridging::literal_set_bridgeable(
391
+ lit,
392
+ Some(crate::attr::type_name(field_attr)),
393
+ value_objects_by_name,
394
+ ) {
211
395
  return None;
212
396
  }
213
397
  let target = mutation.get("target").map(Json::to_s).unwrap_or_default();
214
- Some(format!("{target}.{field_name}: literal doesn't bridge to {}", crate::attr::type_name(field_attr)))
398
+ Some(format!(
399
+ "{target}.{field_name}: literal doesn't bridge to {}",
400
+ crate::attr::type_name(field_attr)
401
+ ))
215
402
  }
216
403
 
217
404
  /// Problems with `set`/`append` fields sourced from `state(:field)`: an undeclared state field,
218
405
  /// or a type or cardinality that differs from the target's.
219
- pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
220
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
221
- let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
406
+ pub fn state_source_problems(
407
+ command: &Json,
408
+ aggregate: &Json,
409
+ value_objects_by_name: &HashMap<String, &Json>,
410
+ ) -> Vec<String> {
411
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
412
+ let mutations = command.get("mutations").map_or_default(Json::each);
222
413
  mutations
223
414
  .iter()
224
415
  .flat_map(|m| {
@@ -226,23 +417,56 @@ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_
226
417
  match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
227
418
  "set" => {
228
419
  let source = m.get("source");
229
- if source.and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default() != "state" {
420
+ if source
421
+ .and_then(|s| s.get("kind"))
422
+ .map(Json::to_s)
423
+ .unwrap_or_default()
424
+ != "state"
425
+ {
230
426
  return Vec::new();
231
427
  }
232
- let state_name = source.and_then(|s| s.get("name")).map(Json::to_s).unwrap_or_default();
233
- state_source_problem(&target, &state_name, aggregate, attrs.iter().find(|a| crate::attr::name(a) == target)).into_iter().collect()
428
+ let state_name = source
429
+ .and_then(|s| s.get("name"))
430
+ .map(Json::to_s)
431
+ .unwrap_or_default();
432
+ state_source_problem(
433
+ &target,
434
+ &state_name,
435
+ aggregate,
436
+ attrs.iter().find(|a| crate::attr::name(a) == target),
437
+ )
438
+ .into_iter()
439
+ .collect()
234
440
  }
235
441
  "append" => {
236
- let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target) else { return Vec::new() };
237
- let Some(element) = append_element(aggregate, crate::attr::type_name(target_attr), value_objects_by_name) else { return Vec::new() };
238
- let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
239
- let Some(Json::Object(fields)) = m.get("fields") else { return Vec::new() };
442
+ let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
443
+ else {
444
+ return Vec::new();
445
+ };
446
+ let Some(element) = append_element(
447
+ aggregate,
448
+ crate::attr::type_name(target_attr),
449
+ value_objects_by_name,
450
+ ) else {
451
+ return Vec::new();
452
+ };
453
+ let element_attrs = element.get("attributes").map_or_default(Json::each);
454
+ let Some(Json::Object(fields)) = m.get("fields") else {
455
+ return Vec::new();
456
+ };
240
457
  fields
241
458
  .iter()
242
459
  .filter_map(|(field_name, source)| {
243
460
  let text = source.to_s();
244
461
  let state_name = text.strip_prefix("state(:")?.strip_suffix(')')?;
245
- state_source_problem(&format!("{target}.{field_name}"), state_name, aggregate, element_attrs.iter().find(|a| crate::attr::name(a) == field_name))
462
+ state_source_problem(
463
+ &format!("{target}.{field_name}"),
464
+ state_name,
465
+ aggregate,
466
+ element_attrs
467
+ .iter()
468
+ .find(|a| crate::attr::name(a) == field_name),
469
+ )
246
470
  })
247
471
  .collect()
248
472
  }
@@ -255,12 +479,15 @@ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_
255
479
  /// Problems with `set` mutations sourced from arithmetic: the target must be a scalar
256
480
  /// (non-list) attribute typed by a single-field Integer or Float value object, the one shape the
257
481
  /// generated code can rebuild a computed number into.
258
- pub fn expression_source_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
259
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
482
+ pub fn expression_source_problems(
483
+ command: &Json,
484
+ aggregate: &Json,
485
+ value_objects_by_name: &HashMap<String, &Json>,
486
+ ) -> Vec<String> {
487
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
260
488
  command
261
489
  .get("mutations")
262
- .map(Json::each)
263
- .unwrap_or(&[])
490
+ .map_or_default(Json::each)
264
491
  .iter()
265
492
  .filter(|m| m.get("op").map(Json::to_s).as_deref() == Some("set"))
266
493
  .filter(|m| m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("expression"))
@@ -276,13 +503,21 @@ pub fn expression_source_problems(command: &Json, aggregate: &Json, value_object
276
503
  }
277
504
 
278
505
  // The scalar type (`Integer` or `Float`) of the one field of a non-list, single-field value object.
279
- fn computed_field_type<'a>(attr: &Json, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<(&'a Json, &'static str)> {
506
+ fn computed_field_type<'a>(
507
+ attr: &Json,
508
+ value_objects_by_name: &HashMap<String, &'a Json>,
509
+ ) -> Option<(&'a Json, &'static str)> {
280
510
  if crate::attr::list(attr) {
281
511
  return None;
282
512
  }
283
- let vo = value_objects_by_name.get(crate::attr::type_name(attr)).copied()?;
284
- let fields = vo.get("attributes").map(Json::each).unwrap_or(&[]);
285
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) || fields.len() != 1 || crate::attr::list(&fields[0]) {
513
+ let vo = value_objects_by_name
514
+ .get(crate::attr::type_name(attr))
515
+ .copied()?;
516
+ let fields = vo.get("attributes").map_or_default(Json::each);
517
+ if vo.get("closed_set").is_some_and(Json::as_bool)
518
+ || fields.len() != 1
519
+ || crate::attr::list(&fields[0])
520
+ {
286
521
  return None;
287
522
  }
288
523
  match crate::attr::type_name(&fields[0]) {
@@ -295,10 +530,23 @@ fn computed_field_type<'a>(attr: &Json, value_objects_by_name: &HashMap<String,
295
530
  /// Right-hand side for a `set` sourced from arithmetic: evaluates the expression through the
296
531
  /// kernel over the command's arguments and then the pre-dispatch record, and rebuilds the number
297
532
  /// into the target's single-field value object. A fault returns from the mutation closure.
298
- pub fn expression_set_rhs(source: &Json, target_attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
299
- let (vo, scalar) = computed_field_type(target_attr, value_objects_by_name).expect("expression_source_problems already confirmed the target");
300
- let ast = source.get("ast").expect("an expression source carries its ast");
301
- let field = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
533
+ #[allow(
534
+ clippy::expect_used,
535
+ reason = "the message names an invariant an earlier check in this generator established"
536
+ )]
537
+ pub fn expression_set_rhs(
538
+ source: &Json,
539
+ target_attr: &Json,
540
+ value_objects_by_name: &HashMap<String, &Json>,
541
+ ) -> String {
542
+ let (vo, scalar) = computed_field_type(target_attr, value_objects_by_name)
543
+ .expect("expression_source_problems already confirmed the target");
544
+ let ast = source
545
+ .get("ast")
546
+ .expect("an expression source carries its ast");
547
+ let field = naming::rust_ident_field(crate::attr::name(
548
+ &vo.get("attributes").map_or_default(Json::each)[0],
549
+ ));
302
550
  let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
303
551
  let arms = if scalar == "Integer" {
304
552
  "crate::kernel::Value::Int(number) => number,".to_string()
@@ -312,20 +560,34 @@ pub fn expression_set_rhs(source: &Json, target_attr: &Json, value_objects_by_na
312
560
  )
313
561
  }
314
562
 
315
- fn state_source_problem(label: &str, state_name: &str, aggregate: &Json, target_attr: Option<&Json>) -> Option<String> {
316
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
563
+ fn state_source_problem(
564
+ label: &str,
565
+ state_name: &str,
566
+ aggregate: &Json,
567
+ target_attr: Option<&Json>,
568
+ ) -> Option<String> {
569
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
317
570
  let Some(state_attr) = attrs.iter().find(|a| crate::attr::name(a) == state_name) else {
318
571
  let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
319
- return Some(format!("{label}: sources state(:{state_name}), which {aggregate_name} does not declare"));
572
+ return Some(format!(
573
+ "{label}: sources state(:{state_name}), which {aggregate_name} does not declare"
574
+ ));
320
575
  };
321
576
  let Some(target_attr) = target_attr else {
322
577
  return Some(format!("{label}: no such target field"));
323
578
  };
324
- let same = crate::attr::type_name(state_attr) == crate::attr::type_name(target_attr) && crate::attr::list(state_attr) == crate::attr::list(target_attr);
579
+ let same = crate::attr::type_name(state_attr) == crate::attr::type_name(target_attr)
580
+ && crate::attr::list(state_attr) == crate::attr::list(target_attr);
325
581
  if same {
326
582
  return None;
327
583
  }
328
- let list_of = |a: &Json| if crate::attr::list(a) { "a list of " } else { "" };
584
+ let list_of = |a: &Json| {
585
+ if crate::attr::list(a) {
586
+ "a list of "
587
+ } else {
588
+ ""
589
+ }
590
+ };
329
591
  Some(format!(
330
592
  "{label}: state(:{state_name}) is {}{}, the target wants {}{} — not generated yet",
331
593
  list_of(state_attr),
@@ -336,55 +598,109 @@ fn state_source_problem(label: &str, state_name: &str, aggregate: &Json, target_
336
598
  }
337
599
 
338
600
  /// Problems with each `append` mutation's fields, checked against the element they build.
339
- pub fn append_field_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
340
- let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
601
+ #[allow(
602
+ clippy::unwrap_used,
603
+ reason = "the value was checked just before; None would mean invalid IR"
604
+ )]
605
+ pub fn append_field_problems(
606
+ command: &Json,
607
+ aggregate: &Json,
608
+ value_objects_by_name: &HashMap<String, &Json>,
609
+ ) -> Vec<String> {
610
+ let mutations = command.get("mutations").map_or_default(Json::each);
341
611
  mutations
342
612
  .iter()
343
613
  .filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "append")
344
614
  .flat_map(|m| {
345
615
  let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
346
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
616
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
347
617
  let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name);
348
- let element = target_attr.and_then(|ta| append_element(aggregate, crate::attr::type_name(ta), value_objects_by_name));
618
+ let element = target_attr.and_then(|ta| {
619
+ append_element(aggregate, crate::attr::type_name(ta), value_objects_by_name)
620
+ });
349
621
  let Some(element) = element else {
350
- let type_desc = target_attr.map(crate::attr::type_name).unwrap_or("");
351
- return vec![format!("{target_name}: element type {} not resolvable", naming::ruby_inspect_string(type_desc))];
622
+ let type_desc = target_attr.map_or("", crate::attr::type_name);
623
+ return vec![format!(
624
+ "{target_name}: element type {} not resolvable",
625
+ naming::ruby_inspect_string(type_desc)
626
+ )];
352
627
  };
353
628
  let target_attr = target_attr.unwrap();
354
629
 
355
630
  let fields = m.get("fields").map(fields_pairs).unwrap_or_default();
356
- let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
631
+ let element_attrs = element.get("attributes").map_or_default(Json::each);
357
632
  let mut problems: Vec<String> = fields
358
633
  .iter()
359
634
  .filter_map(|(field_name, source)| {
360
- let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str());
635
+ let field_attr = element_attrs
636
+ .iter()
637
+ .find(|a| crate::attr::name(a) == field_name.as_str());
361
638
  let Some(field_attr) = field_attr else {
362
639
  return Some(format!("{target_name}.{field_name}: not a declared field"));
363
640
  };
364
641
 
365
642
  let parsed = append_field_source(source);
366
643
  let Literal::Symbol(arg_name) = &parsed else {
367
- return literal_problem(m, field_name, &parsed, field_attr, value_objects_by_name);
644
+ return literal_problem(
645
+ m,
646
+ field_name,
647
+ &parsed,
648
+ field_attr,
649
+ value_objects_by_name,
650
+ );
368
651
  };
369
652
 
370
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
371
- match cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()) {
372
- None => Some(format!("{target_name}.{field_name}: sources undeclared argument {arg_name}")),
373
- Some(arg_attr) if !crate::bridging::bridgeable_value_types(crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name) => {
374
- Some(format!("{target_name}.{field_name}: {} doesn't bridge to {}", crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr)))
653
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
654
+ match cmd_attrs
655
+ .iter()
656
+ .find(|a| crate::attr::name(a) == arg_name.as_str())
657
+ {
658
+ None => Some(format!(
659
+ "{target_name}.{field_name}: sources undeclared argument {arg_name}"
660
+ )),
661
+ Some(arg_attr)
662
+ if !crate::bridging::bridgeable_value_types(
663
+ crate::attr::type_name(arg_attr),
664
+ crate::attr::type_name(field_attr),
665
+ value_objects_by_name,
666
+ ) =>
667
+ {
668
+ Some(format!(
669
+ "{target_name}.{field_name}: {} doesn't bridge to {}",
670
+ crate::attr::type_name(arg_attr),
671
+ crate::attr::type_name(field_attr)
672
+ ))
375
673
  }
376
674
  _ => None,
377
675
  }
378
676
  })
379
677
  .collect();
380
678
 
381
- let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr)));
679
+ let entity = aggregate
680
+ .get("entities")
681
+ .map_or_default(Json::each)
682
+ .iter()
683
+ .find(|e| {
684
+ e.get("name").and_then(Json::as_str)
685
+ == Some(crate::attr::type_name(target_attr))
686
+ });
382
687
  if let Some(entity) = entity {
383
- let present: Vec<String> = fields.iter().map(|(k, _)| k.clone()).collect();
384
- let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
385
- let id_head = identified_by.first().and_then(Json::as_str).unwrap_or("").split('.').next().unwrap_or("").to_string();
386
- if !present.contains(&id_head) && entity_identity_mint(entity, value_objects_by_name).is_none() {
387
- problems.push(format!("{target_name}: {}'s identity doesn't auto-mint", entity.get("name").and_then(Json::as_str).unwrap_or("")));
688
+ let identified_by = entity.get("identified_by").map_or_default(Json::each);
689
+ let id_head = identified_by
690
+ .first()
691
+ .and_then(Json::as_str)
692
+ .unwrap_or("")
693
+ .split('.')
694
+ .next()
695
+ .unwrap_or("")
696
+ .to_string();
697
+ if !fields.iter().map(|(k, _)| k.clone()).any(|x| x == id_head)
698
+ && entity_identity_mint(entity, value_objects_by_name).is_none()
699
+ {
700
+ problems.push(format!(
701
+ "{target_name}: {}'s identity doesn't auto-mint",
702
+ entity.get("name").and_then(Json::as_str).unwrap_or("")
703
+ ));
388
704
  }
389
705
  }
390
706
  problems
@@ -392,27 +708,75 @@ pub fn append_field_problems(command: &Json, aggregate: &Json, value_objects_by_
392
708
  .collect()
393
709
  }
394
710
 
395
- /// Problems with `remove:` mutations, which are generatable only against an entity list whose
396
- /// single-head identity mints, sourced from a declared argument that bridges to its type.
397
- pub fn remove_field_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
398
- let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
711
+ /// Why a `remove:` on a list of value objects can't be generated, or `None` when it can.
712
+ ///
713
+ /// The argument must be a declared command attribute of the element's own value-object type, so
714
+ /// the generated `retain` compares whole values and nothing needs bridging.
715
+ fn value_object_remove_problem(
716
+ mutation: &Json,
717
+ command: &Json,
718
+ target_attr: &Json,
719
+ value_objects_by_name: &HashMap<String, &Json>,
720
+ ) -> Option<String> {
721
+ let target_name = mutation.get("target").map(Json::to_s).unwrap_or_default();
722
+ let element_type = crate::attr::type_name(target_attr);
723
+ if !value_objects_by_name.contains_key(element_type) {
724
+ return Some(format!(
725
+ "{target_name}: remove on a list of {element_type}, which is neither an entity nor a value object, is not generated yet"
726
+ ));
727
+ }
728
+ let source = mutation.get("source").filter(|s| {
729
+ matches!(s, Json::Object(_))
730
+ && s.get("kind").map(Json::to_s).unwrap_or_default() == "argument"
731
+ });
732
+ let Some(source) = source else {
733
+ return Some(format!(
734
+ "{target_name}: remove sources a literal or record state, not an argument — not generated yet"
735
+ ));
736
+ };
737
+ let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
738
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
739
+ let Some(source_attr) = cmd_attrs
740
+ .iter()
741
+ .find(|a| crate::attr::name(a) == source_name)
742
+ else {
743
+ return Some(format!(
744
+ "{target_name}: remove sources undeclared argument {source_name}"
745
+ ));
746
+ };
747
+ if crate::attr::list(source_attr) || crate::attr::type_name(source_attr) != element_type {
748
+ return Some(format!(
749
+ "{target_name}: remove on a list of {element_type} needs a single {element_type} argument, not {source_name}"
750
+ ));
751
+ }
752
+ None
753
+ }
754
+
755
+ /// Problems with `remove:` mutations: an entity list whose single-head identity mints, or a
756
+ /// value-object list, each sourced from a declared argument of a matching type.
757
+ pub fn remove_field_problems(
758
+ command: &Json,
759
+ aggregate: &Json,
760
+ value_objects_by_name: &HashMap<String, &Json>,
761
+ ) -> Vec<String> {
762
+ let mutations = command.get("mutations").map_or_default(Json::each);
399
763
  mutations
400
764
  .iter()
401
765
  .filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "remove")
402
766
  .filter_map(|m| {
403
767
  let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
404
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
768
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
405
769
  let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target_name).filter(|a| crate::attr::list(a)) else {
406
770
  return Some(format!("{target_name}: not a declared list attribute"));
407
771
  };
408
772
 
409
- let entities = aggregate.get("entities").map(Json::each).unwrap_or(&[]);
773
+ let entities = aggregate.get("entities").map_or_default(Json::each);
410
774
  let Some(entity) = entities.iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))) else {
411
- return Some(format!("{target_name}: remove on a value-object-typed list is not generated yet"));
775
+ return value_object_remove_problem(m, command, target_attr, value_objects_by_name);
412
776
  };
413
777
  let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("");
414
778
 
415
- if entity.get("identified_by").map(Json::each).unwrap_or(&[]).len() != 1 {
779
+ if entity.get("identified_by").map_or_default(Json::each).len() != 1 {
416
780
  return Some(format!("{target_name}: {entity_name}'s identity is composite — remove not generated yet"));
417
781
  }
418
782
 
@@ -426,7 +790,7 @@ pub fn remove_field_problems(command: &Json, aggregate: &Json, value_objects_by_
426
790
  };
427
791
 
428
792
  let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
429
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
793
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
430
794
  let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name) else {
431
795
  return Some(format!("{target_name}: remove sources undeclared argument {source_name}"));
432
796
  };
@@ -466,7 +830,11 @@ pub fn transition_check_arg(transition: Option<&Transition>) -> String {
466
830
  Some(t) if !t.unconstrained => format!(
467
831
  "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
468
832
  naming::ruby_inspect_string(&t.field),
469
- t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
833
+ t.from_states
834
+ .iter()
835
+ .map(|s| naming::ruby_inspect_string(s))
836
+ .collect::<Vec<_>>()
837
+ .join(", ")
470
838
  ),
471
839
  _ => "None".to_string(),
472
840
  }
@@ -476,8 +844,11 @@ pub fn transition_check_arg(transition: Option<&Transition>) -> String {
476
844
  pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Transition> {
477
845
  let lifecycle = aggregate.get("lifecycle")?;
478
846
  let command_name = command.get("name").map(Json::to_s).unwrap_or_default();
479
- let transitions = lifecycle.get("transitions").map(Json::each).unwrap_or(&[]);
480
- let rows: Vec<&Json> = transitions.iter().filter(|t| t.get("command").map(Json::to_s).unwrap_or_default() == command_name).collect();
847
+ let transitions = lifecycle.get("transitions").map_or_default(Json::each);
848
+ let rows: Vec<&Json> = transitions
849
+ .iter()
850
+ .filter(|t| t.get("command").map(Json::to_s).unwrap_or_default() == command_name)
851
+ .collect();
481
852
  if rows.is_empty() {
482
853
  // `from:` without a transition guards the current state and moves nothing.
483
854
  let froms: Vec<String> = match command.get("from") {
@@ -494,11 +865,24 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
494
865
  from_states.push(from);
495
866
  }
496
867
  }
497
- let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
498
- return Some(Transition { field, to_state: String::new(), from_states, unconstrained: false });
868
+ let field = lifecycle
869
+ .get("field")
870
+ .and_then(Json::as_str)
871
+ .unwrap_or("")
872
+ .to_string();
873
+ return Some(Transition {
874
+ field,
875
+ to_state: String::new(),
876
+ from_states,
877
+ unconstrained: false,
878
+ });
499
879
  }
500
880
 
501
- let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
881
+ let field = lifecycle
882
+ .get("field")
883
+ .and_then(Json::as_str)
884
+ .unwrap_or("")
885
+ .to_string();
502
886
  let to_state = rows[0].get("to_state").map(Json::to_s).unwrap_or_default();
503
887
  let mut from_states: Vec<String> = Vec::new();
504
888
  let mut unconstrained = false;
@@ -513,26 +897,52 @@ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Tran
513
897
  }
514
898
  }
515
899
  }
516
- Some(Transition { field, to_state, from_states, unconstrained })
900
+ Some(Transition {
901
+ field,
902
+ to_state,
903
+ from_states,
904
+ unconstrained,
905
+ })
517
906
  }
518
907
 
519
908
  /// Whether a `state(:field)` source reads an `Option` out of the pre-dispatch record: the field
520
909
  /// is declared optional, or the record holds every field as an `Option` (`record_optional`).
521
910
  /// Such a source already has the target's wrapping, so it must not be wrapped in `Some` again.
522
911
  fn state_source_is_option(source: Option<&Json>, aggregate: &Json, record_optional: bool) -> bool {
523
- let Some(source) = source.filter(|s| s.get("kind").map(Json::to_s).unwrap_or_default() == "state") else { return false };
912
+ let Some(source) =
913
+ source.filter(|s| s.get("kind").map(Json::to_s).unwrap_or_default() == "state")
914
+ else {
915
+ return false;
916
+ };
524
917
  let name = source.get("name").map(Json::to_s).unwrap_or_default();
525
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
526
- record_optional || attrs.iter().any(|a| crate::attr::name(a) == name && crate::attr::optional(a))
918
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
919
+ record_optional
920
+ || attrs
921
+ .iter()
922
+ .any(|a| crate::attr::name(a) == name && crate::attr::optional(a))
527
923
  }
528
924
 
529
925
  /// Right-hand side for a `set`, coercing the source into the target attribute's declared type.
530
926
  ///
531
927
  /// `target_list` routes list-to-list sets through `list_value_rhs`; `value_rhs` is scalar-only.
532
- pub fn mutation_set_rhs(source: &Json, target_type: &str, command: &Json, value_objects_by_name: &HashMap<String, &Json>, target_list: bool) -> String {
928
+ #[allow(
929
+ clippy::expect_used,
930
+ reason = "the message names an invariant an earlier check in this generator established"
931
+ )]
932
+ pub fn mutation_set_rhs(
933
+ source: &Json,
934
+ target_type: &str,
935
+ command: &Json,
936
+ value_objects_by_name: &HashMap<String, &Json>,
937
+ target_list: bool,
938
+ ) -> String {
533
939
  if source.get("kind").map(Json::to_s).unwrap_or_default() == "literal" {
534
940
  let value = source.get("value").unwrap_or(&Json::Null);
535
- return crate::bridging::literal_rhs_for(&Literal::from_json(value), Some(target_type), value_objects_by_name);
941
+ return crate::bridging::literal_rhs_for(
942
+ &Literal::from_json(value),
943
+ Some(target_type),
944
+ value_objects_by_name,
945
+ );
536
946
  }
537
947
 
538
948
  let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
@@ -540,13 +950,32 @@ pub fn mutation_set_rhs(source: &Json, target_type: &str, command: &Json, value_
540
950
  if source.get("kind").map(Json::to_s).unwrap_or_default() == "state" {
541
951
  return format!("pre.{}.clone()", naming::rust_ident_field(&source_name));
542
952
  }
543
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
544
- let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name).expect("mutation source argument must be a declared command attribute");
953
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
954
+ let source_attr = cmd_attrs
955
+ .iter()
956
+ .find(|a| crate::attr::name(a) == source_name)
957
+ .expect("mutation source argument must be a declared command attribute");
545
958
  let source_expr = format!("args.{}", naming::rust_ident_field(&source_name));
546
- if target_list && crate::attr::list(source_attr) && crate::bridging::list_bridge_requires_element_mapping(crate::attr::type_name(source_attr), target_type) {
547
- return crate::bridging::list_value_rhs(&source_expr, crate::attr::type_name(source_attr), target_type, value_objects_by_name);
959
+ if target_list
960
+ && crate::attr::list(source_attr)
961
+ && crate::bridging::list_bridge_requires_element_mapping(
962
+ crate::attr::type_name(source_attr),
963
+ target_type,
964
+ )
965
+ {
966
+ return crate::bridging::list_value_rhs(
967
+ &source_expr,
968
+ crate::attr::type_name(source_attr),
969
+ target_type,
970
+ value_objects_by_name,
971
+ );
548
972
  }
549
- crate::bridging::value_rhs(&source_expr, crate::attr::type_name(source_attr), target_type, value_objects_by_name)
973
+ crate::bridging::value_rhs(
974
+ &source_expr,
975
+ crate::attr::type_name(source_attr),
976
+ target_type,
977
+ value_objects_by_name,
978
+ )
550
979
  }
551
980
 
552
981
  pub struct IdentityComponent {
@@ -558,13 +987,12 @@ pub struct IdentityComponent {
558
987
  /// Builds one component per identity part: a `set` target or same-named command argument reads
559
988
  /// `args.<head>`, and anything else becomes a parameter.
560
989
  pub fn identity_components(aggregate: &Json, command: &Json) -> Vec<IdentityComponent> {
561
- let identified_by = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]);
562
- let cmd_mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
990
+ let identified_by = aggregate.get("identified_by").map_or_default(Json::each);
991
+ let cmd_mutations = command.get("mutations").map_or_default(Json::each);
563
992
  let append_claimed = crate::shared::append_claimed_names(command);
564
993
  let declared_names: std::collections::HashSet<String> = command
565
994
  .get("attributes")
566
- .map(Json::each)
567
- .unwrap_or(&[])
995
+ .map_or_default(Json::each)
568
996
  .iter()
569
997
  .map(|a| crate::attr::name(a).to_string())
570
998
  .filter(|name| !append_claimed.contains(name))
@@ -579,19 +1007,40 @@ pub fn identity_components(aggregate: &Json, command: &Json) -> Vec<IdentityComp
579
1007
  let rest: Vec<&str> = parts.collect();
580
1008
 
581
1009
  let set_target = cmd_mutations.iter().any(|m| {
582
- m.get("op").map(Json::to_s).as_deref() == Some("set") && m.get("target").map(Json::to_s).as_deref() == Some(head)
1010
+ m.get("op").map(Json::to_s).as_deref() == Some("set")
1011
+ && m.get("target").map(Json::to_s).as_deref() == Some(head)
583
1012
  });
584
1013
 
585
1014
  if set_target || declared_names.contains(head) {
586
- if !rest.is_empty() {
587
- let rest_path = rest.iter().map(|seg| naming::rust_ident_field(seg)).collect::<Vec<_>>().join(".");
588
- IdentityComponent { expr: format!("args.{}.{}.to_string()", naming::rust_ident_field(head), rest_path), param: None, head: None }
1015
+ if rest.is_empty() {
1016
+ IdentityComponent {
1017
+ expr: format!("args.{}.to_string()", naming::rust_ident_field(head)),
1018
+ param: None,
1019
+ head: None,
1020
+ }
589
1021
  } else {
590
- IdentityComponent { expr: format!("args.{}.to_string()", naming::rust_ident_field(head)), param: None, head: None }
1022
+ let rest_path = rest
1023
+ .iter()
1024
+ .map(|seg| naming::rust_ident_field(seg))
1025
+ .collect::<Vec<_>>()
1026
+ .join(".");
1027
+ IdentityComponent {
1028
+ expr: format!(
1029
+ "args.{}.{}.to_string()",
1030
+ naming::rust_ident_field(head),
1031
+ rest_path
1032
+ ),
1033
+ param: None,
1034
+ head: None,
1035
+ }
591
1036
  }
592
1037
  } else {
593
1038
  let param = naming::rust_ident_field(head);
594
- IdentityComponent { expr: format!("{param}.to_string()"), param: Some(format!("{param}: &str")), head: Some(head.to_string()) }
1039
+ IdentityComponent {
1040
+ expr: format!("{param}.to_string()"),
1041
+ param: Some(format!("{param}: &str")),
1042
+ head: Some(head.to_string()),
1043
+ }
595
1044
  }
596
1045
  })
597
1046
  .collect()
@@ -601,34 +1050,80 @@ pub fn build_identity_expr(components: &[IdentityComponent]) -> String {
601
1050
  if components.len() == 1 {
602
1051
  return components[0].expr.clone();
603
1052
  }
604
- let placeholders = components.iter().map(|_| "{}").collect::<Vec<_>>().join(":");
605
- format!("format!({}, {})", naming::ruby_inspect_string(&placeholders), components.iter().map(|c| c.expr.as_str()).collect::<Vec<_>>().join(", "))
1053
+ let placeholders = components
1054
+ .iter()
1055
+ .map(|_| "{}")
1056
+ .collect::<Vec<_>>()
1057
+ .join(":");
1058
+ format!(
1059
+ "format!({}, {})",
1060
+ naming::ruby_inspect_string(&placeholders),
1061
+ components
1062
+ .iter()
1063
+ // A format argument is displayed already, so the `.to_string()` an id part carries is dropped.
1064
+ .map(|c| c.expr.strip_suffix(".to_string()").unwrap_or(&c.expr))
1065
+ .collect::<Vec<_>>()
1066
+ .join(", ")
1067
+ )
606
1068
  }
607
1069
 
608
1070
  /// The value expression for one `append` field.
609
- pub fn append_field_rhs(source: &str, field_attr: &Json, command: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
1071
+ #[allow(
1072
+ clippy::expect_used,
1073
+ reason = "the message names an invariant an earlier check in this generator established"
1074
+ )]
1075
+ pub fn append_field_rhs(
1076
+ source: &str,
1077
+ field_attr: &Json,
1078
+ command: &Json,
1079
+ value_objects_by_name: &HashMap<String, &Json>,
1080
+ ) -> String {
610
1081
  let parsed = append_field_source(source);
611
1082
  let Literal::Symbol(arg_name) = &parsed else {
612
- return crate::bridging::literal_rhs_for(&parsed, Some(crate::attr::type_name(field_attr)), value_objects_by_name);
1083
+ return crate::bridging::literal_rhs_for(
1084
+ &parsed,
1085
+ Some(crate::attr::type_name(field_attr)),
1086
+ value_objects_by_name,
1087
+ );
613
1088
  };
614
1089
 
615
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
616
- let arg_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()).expect("append field argument must be a declared command attribute");
617
- let arg_expr = format!("args.{}", naming::rust_ident_field(crate::attr::name(arg_attr)));
1090
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
1091
+ let arg_attr = cmd_attrs
1092
+ .iter()
1093
+ .find(|a| crate::attr::name(a) == arg_name.as_str())
1094
+ .expect("append field argument must be a declared command attribute");
1095
+ let arg_expr = format!(
1096
+ "args.{}",
1097
+ naming::rust_ident_field(crate::attr::name(arg_attr))
1098
+ );
618
1099
 
619
1100
  if crate::attr::optional(arg_attr) {
620
- let same_representation = crate::attr::type_name(arg_attr) == crate::attr::type_name(field_attr)
621
- || match (naming::effective_scalar_type(crate::attr::type_name(arg_attr)), naming::effective_scalar_type(crate::attr::type_name(field_attr))) {
1101
+ let same_representation = crate::attr::type_name(arg_attr)
1102
+ == crate::attr::type_name(field_attr)
1103
+ || match (
1104
+ naming::effective_scalar_type(crate::attr::type_name(arg_attr)),
1105
+ naming::effective_scalar_type(crate::attr::type_name(field_attr)),
1106
+ ) {
622
1107
  (Some(a), Some(b)) => a == b,
623
1108
  _ => false,
624
1109
  };
625
1110
  if same_representation {
626
1111
  return format!("{arg_expr}.clone()");
627
1112
  }
628
- return optional_value_rhs(&arg_expr, crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name);
1113
+ return optional_value_rhs(
1114
+ &arg_expr,
1115
+ crate::attr::type_name(arg_attr),
1116
+ crate::attr::type_name(field_attr),
1117
+ value_objects_by_name,
1118
+ );
629
1119
  }
630
1120
 
631
- let rhs = crate::bridging::value_rhs(&arg_expr, crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name);
1121
+ let rhs = crate::bridging::value_rhs(
1122
+ &arg_expr,
1123
+ crate::attr::type_name(arg_attr),
1124
+ crate::attr::type_name(field_attr),
1125
+ value_objects_by_name,
1126
+ );
632
1127
  if crate::attr::optional(field_attr) {
633
1128
  format!("Some({rhs})")
634
1129
  } else {
@@ -637,17 +1132,55 @@ pub fn append_field_rhs(source: &str, field_attr: &Json, command: &Json, value_o
637
1132
  }
638
1133
 
639
1134
  /// The optional-source variant of `value_rhs`.
640
- pub fn optional_value_rhs(source_expr: &str, source_type: &str, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
641
- format!("{source_expr}.clone().map(|v| {})", crate::bridging::value_rhs("v", source_type, target_type, value_objects_by_name))
1135
+ pub fn optional_value_rhs(
1136
+ source_expr: &str,
1137
+ source_type: &str,
1138
+ target_type: &str,
1139
+ value_objects_by_name: &HashMap<String, &Json>,
1140
+ ) -> String {
1141
+ format!(
1142
+ "{source_expr}.clone().map(|v| {})",
1143
+ crate::bridging::value_rhs("v", source_type, target_type, value_objects_by_name)
1144
+ )
642
1145
  }
643
1146
 
644
1147
  /// Emits the closure line for one mutation.
645
- pub fn emit_mutation_line(exemplar: &Exemplar, mutation: &Json, aggregate: &Json, command: &Json, value_objects_by_name: &HashMap<String, &Json>, optional: bool) -> String {
646
- let target_field = naming::rust_ident_field(&mutation.get("target").map(Json::to_s).unwrap_or_default());
647
- let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
648
- format!(" {}", emit_mutation_line_body(exemplar, mutation, aggregate, command, value_objects_by_name, &target_field, lifecycle_field.as_deref(), optional))
1148
+ pub fn emit_mutation_line(
1149
+ exemplar: &Exemplar,
1150
+ mutation: &Json,
1151
+ aggregate: &Json,
1152
+ command: &Json,
1153
+ value_objects_by_name: &HashMap<String, &Json>,
1154
+ optional: bool,
1155
+ ) -> String {
1156
+ let target_field =
1157
+ naming::rust_ident_field(&mutation.get("target").map(Json::to_s).unwrap_or_default());
1158
+ let lifecycle_field = aggregate
1159
+ .get("lifecycle")
1160
+ .and_then(|l| l.get("field"))
1161
+ .map(Json::to_s);
1162
+ format!(
1163
+ " {}",
1164
+ emit_mutation_line_body(
1165
+ exemplar,
1166
+ mutation,
1167
+ aggregate,
1168
+ command,
1169
+ value_objects_by_name,
1170
+ &target_field,
1171
+ lifecycle_field.as_deref(),
1172
+ optional
1173
+ )
1174
+ )
649
1175
  }
650
1176
 
1177
+ #[allow(clippy::too_many_arguments)] // established emitter signature
1178
+ #[allow(
1179
+ clippy::expect_used,
1180
+ clippy::panic,
1181
+ clippy::unwrap_used,
1182
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose; the message names an invariant an earlier check in this generator established; the value was checked just before; None would mean invalid IR"
1183
+ )]
651
1184
  fn emit_mutation_line_body(
652
1185
  exemplar: &Exemplar,
653
1186
  mutation: &Json,
@@ -663,26 +1196,49 @@ fn emit_mutation_line_body(
663
1196
 
664
1197
  match op.as_str() {
665
1198
  "append" => {
666
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
667
- let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("append target must be a declared aggregate attribute");
1199
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
1200
+ let target_attr = attrs
1201
+ .iter()
1202
+ .find(|a| crate::attr::name(a) == target_name)
1203
+ .expect("append target must be a declared aggregate attribute");
668
1204
  let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
669
- let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr)));
670
- let element = entity.or_else(|| value_objects_by_name.get(crate::attr::type_name(target_attr)).copied()).expect("append target's element type must resolve");
1205
+ let entity = aggregate
1206
+ .get("entities")
1207
+ .map_or_default(Json::each)
1208
+ .iter()
1209
+ .find(|e| {
1210
+ e.get("name").and_then(Json::as_str)
1211
+ == Some(crate::attr::type_name(target_attr))
1212
+ });
1213
+ let element = entity
1214
+ .or_else(|| {
1215
+ value_objects_by_name
1216
+ .get(crate::attr::type_name(target_attr))
1217
+ .copied()
1218
+ })
1219
+ .expect("append target's element type must resolve");
671
1220
 
672
1221
  let fields = mutation.get("fields").map(fields_pairs).unwrap_or_default();
673
- let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
1222
+ let element_attrs = element.get("attributes").map_or_default(Json::each);
674
1223
  let mut fields_assignment: Vec<String> = fields
675
1224
  .iter()
676
1225
  .map(|(field_name, source)| {
677
- let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str()).expect("append field must be a declared element attribute");
678
- format!("{}: {}", naming::rust_ident_field(field_name), append_field_rhs(source, field_attr, command, value_objects_by_name))
1226
+ let field_attr = element_attrs
1227
+ .iter()
1228
+ .find(|a| crate::attr::name(a) == field_name.as_str())
1229
+ .expect("append field must be a declared element attribute");
1230
+ format!(
1231
+ "{}: {}",
1232
+ naming::rust_ident_field(field_name),
1233
+ append_field_rhs(source, field_attr, command, value_objects_by_name)
1234
+ )
679
1235
  })
680
1236
  .collect();
681
1237
 
682
1238
  let mut collision_guard = String::new();
683
1239
  if let Some(entity) = entity {
684
1240
  let mut present: Vec<String> = fields.iter().map(|(k, _)| k.clone()).collect();
685
- let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
1241
+ let identified_by = entity.get("identified_by").map_or_default(Json::each);
686
1242
  if identified_by.len() > 1 {
687
1243
  // Composite identity: minting a partial key makes no sense, so every head must
688
1244
  // already be caller-supplied (as `Venue.AddLine`'s `append: { batch: :batch,
@@ -691,24 +1247,28 @@ fn emit_mutation_line_body(
691
1247
  // used to be skipped outright for any composite identity (its comment read
692
1248
  // "Composite identities are excluded"), so two elements sharing every identity
693
1249
  // component went through unrefused. Root cause of this bug.
694
- let heads: Vec<String> = identified_by.iter().map(|p| p.to_s().split('.').next().unwrap_or_default().to_string()).collect();
1250
+ let heads: Vec<String> = identified_by
1251
+ .iter()
1252
+ .map(|p| p.to_s().split('.').next().unwrap_or_default().to_string())
1253
+ .collect();
695
1254
  if heads.iter().all(|h| present.iter().any(|p| p == h)) {
696
- collision_guard = composite_append_collision_guard(&heads, &fields, element_attrs, command, value_objects_by_name, target_field, entity, aggregate, identified_by);
1255
+ collision_guard = composite_append_collision_guard(
1256
+ &heads,
1257
+ &fields,
1258
+ element_attrs,
1259
+ command,
1260
+ value_objects_by_name,
1261
+ target_field,
1262
+ entity,
1263
+ aggregate,
1264
+ identified_by,
1265
+ );
697
1266
  }
698
- } else if let Some((id_attr, id_vo)) = entity_identity_mint(entity, value_objects_by_name) {
1267
+ } else if let Some((id_attr, id_vo)) =
1268
+ entity_identity_mint(entity, value_objects_by_name)
1269
+ {
699
1270
  let id_name = crate::attr::name(id_attr);
700
- if !present.iter().any(|p| p == id_name) {
701
- let id_vo_attrs = id_vo.get("attributes").map(Json::each).unwrap_or(&[]);
702
- // Mint one past the highest identity held.
703
- let id_field = naming::rust_ident_field(id_name);
704
- let vo_field = naming::rust_ident_field(crate::attr::name(&id_vo_attrs[0]));
705
- let mint = format!(
706
- "{} {{ {vo_field}: record.{target_field}.iter().map(|e| e.{id_field}.{vo_field}).max().unwrap_or(0) + 1 }}",
707
- naming::rust_ident(crate::attr::type_name(id_attr)),
708
- );
709
- fields_assignment.push(format!("{}: {mint}", naming::rust_ident_field(id_name)));
710
- present.push(id_name.to_string());
711
- } else {
1271
+ if present.iter().any(|p| p == id_name) {
712
1272
  // Caller-supplied single-field identity: refuse a duplicate, which would
713
1273
  // be unaddressable later.
714
1274
  let id_field = naming::rust_ident_field(id_name);
@@ -723,26 +1283,60 @@ fn emit_mutation_line_body(
723
1283
  append_field_rhs(source, field_attr, command, value_objects_by_name)
724
1284
  })
725
1285
  .expect("caller-supplied identity field must be in the append's own field map");
726
- let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
727
- let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
728
- let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1286
+ let entity_name = entity
1287
+ .get("name")
1288
+ .and_then(Json::as_str)
1289
+ .unwrap_or_default();
1290
+ let aggregate_name = aggregate
1291
+ .get("name")
1292
+ .and_then(Json::as_str)
1293
+ .unwrap_or_default();
1294
+ let identity_reading = identified_by
1295
+ .iter()
1296
+ .map(Json::to_s)
1297
+ .collect::<Vec<_>>()
1298
+ .join(", ");
729
1299
  let entity_lit = naming::ruby_inspect_string(entity_name);
730
1300
  let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
731
1301
  let identity_lit = naming::ruby_inspect_string(&identity_reading);
732
1302
  // Unwrap to the bare scalar, as `entity_list_replace_guard` does; the mint
733
1303
  // branch above guarantees exactly one attribute.
734
- let offered_field = naming::rust_ident_field(crate::attr::name(&id_vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
1304
+ let offered_field = naming::rust_ident_field(crate::attr::name(
1305
+ &id_vo.get("attributes").map_or_default(Json::each)[0],
1306
+ ));
735
1307
  let offered_expr = format!("{id_rhs}.{offered_field}");
736
1308
  collision_guard = format!(
737
1309
  "if record.{target_field}.iter().any(|e| e.{id_field} == {id_rhs}) {{ let offered = format!(\"{{:?}}\", {offered_expr}); return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &[offered.as_str()] }}.render_args())); }}\n "
738
1310
  );
1311
+ } else {
1312
+ let id_vo_attrs = id_vo.get("attributes").map_or_default(Json::each);
1313
+ // Mint one past the highest identity held.
1314
+ let id_field = naming::rust_ident_field(id_name);
1315
+ let vo_field = naming::rust_ident_field(crate::attr::name(&id_vo_attrs[0]));
1316
+ let mint = format!(
1317
+ "{} {{ {vo_field}: record.{target_field}.iter().map(|e| e.{id_field}.{vo_field}).max().unwrap_or(0) + 1 }}",
1318
+ naming::rust_ident(crate::attr::type_name(id_attr)),
1319
+ );
1320
+ fields_assignment
1321
+ .push(format!("{}: {mint}", naming::rust_ident_field(id_name)));
1322
+ present.push(id_name.to_string());
739
1323
  }
740
1324
  }
741
1325
  if let Some(entity_lifecycle) = entity.get("lifecycle") {
742
- let lc_field = entity_lifecycle.get("field").map(Json::to_s).unwrap_or_default();
1326
+ let lc_field = entity_lifecycle
1327
+ .get("field")
1328
+ .map(Json::to_s)
1329
+ .unwrap_or_default();
743
1330
  if !present.iter().any(|p| p == &lc_field) {
744
- let default = entity_lifecycle.get("default").map(Json::to_s).unwrap_or_default();
745
- fields_assignment.push(format!("{}: {}.to_string()", naming::rust_ident_field(&lc_field), naming::ruby_inspect_string(&default)));
1331
+ let default = entity_lifecycle
1332
+ .get("default")
1333
+ .map(Json::to_s)
1334
+ .unwrap_or_default();
1335
+ fields_assignment.push(format!(
1336
+ "{}: {}.to_string()",
1337
+ naming::rust_ident_field(&lc_field),
1338
+ naming::ruby_inspect_string(&default)
1339
+ ));
746
1340
  present.push(lc_field);
747
1341
  }
748
1342
  }
@@ -756,7 +1350,10 @@ fn emit_mutation_line_body(
756
1350
  if present.iter().any(|p| p == attr_name) {
757
1351
  continue;
758
1352
  }
759
- fields_assignment.push(format!("{}: Vec::new()", naming::rust_ident_field(attr_name)));
1353
+ fields_assignment.push(format!(
1354
+ "{}: Vec::new()",
1355
+ naming::rust_ident_field(attr_name)
1356
+ ));
760
1357
  present.push(attr_name.to_string());
761
1358
  }
762
1359
 
@@ -769,116 +1366,372 @@ fn emit_mutation_line_body(
769
1366
  if present.iter().any(|p| p == attr_name) {
770
1367
  continue;
771
1368
  }
772
- fields_assignment.push(format!("{}: None", naming::rust_ident_field(attr_name)));
1369
+ fields_assignment
1370
+ .push(format!("{}: None", naming::rust_ident_field(attr_name)));
773
1371
  present.push(attr_name.to_string());
774
1372
  }
775
1373
  }
776
1374
 
777
- format!("{collision_guard}{}", exemplar.render("mutation_append", &[("tmpl_field", target_field.to_string()), ("tmpl_fields_placeholder()", format!("{vo_type} {{ {} }}", fields_assignment.join(", ")))]))
1375
+ format!(
1376
+ "{collision_guard}{}",
1377
+ exemplar.render(
1378
+ "mutation_append",
1379
+ &[
1380
+ ("tmpl_field", target_field.to_string()),
1381
+ (
1382
+ "tmpl_fields_placeholder()",
1383
+ format!("{vo_type} {{ {} }}", fields_assignment.join(", "))
1384
+ )
1385
+ ]
1386
+ )
1387
+ )
778
1388
  }
779
1389
  "set" => {
780
- if lifecycle_field.map(|f| f == target_name).unwrap_or(false) {
781
- let rhs = mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), "String", command, value_objects_by_name, false);
782
- exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
1390
+ if lifecycle_field.is_some_and(|f| f == target_name) {
1391
+ let rhs = mutation_set_rhs(
1392
+ mutation.get("source").unwrap_or(&Json::Null),
1393
+ "String",
1394
+ command,
1395
+ value_objects_by_name,
1396
+ false,
1397
+ );
1398
+ exemplar.render(
1399
+ "mutation_set_plain",
1400
+ &[
1401
+ ("tmpl_field", target_field.to_string()),
1402
+ ("tmpl_rhs_placeholder2()", rhs),
1403
+ ],
1404
+ )
783
1405
  } else {
784
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
785
- let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("set target must be a declared aggregate attribute");
1406
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
1407
+ let target_attr = attrs
1408
+ .iter()
1409
+ .find(|a| crate::attr::name(a) == target_name)
1410
+ .expect("set target must be a declared aggregate attribute");
786
1411
  let source = mutation.get("source");
787
- let rhs = if source.and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("expression") {
1412
+ let rhs = if source
1413
+ .and_then(|s| s.get("kind"))
1414
+ .map(Json::to_s)
1415
+ .as_deref()
1416
+ == Some("expression")
1417
+ {
788
1418
  expression_set_rhs(source.unwrap(), target_attr, value_objects_by_name)
789
1419
  } else {
790
- mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), crate::attr::type_name(target_attr), command, value_objects_by_name, crate::attr::list(target_attr))
1420
+ mutation_set_rhs(
1421
+ mutation.get("source").unwrap_or(&Json::Null),
1422
+ crate::attr::type_name(target_attr),
1423
+ command,
1424
+ value_objects_by_name,
1425
+ crate::attr::list(target_attr),
1426
+ )
791
1427
  };
792
- let source_attr = if source.map(|s| s.get("kind").map(Json::to_s).unwrap_or_default()) == Some("argument".to_string()) {
793
- let name = source.unwrap().get("name").map(Json::to_s).unwrap_or_default();
794
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
1428
+ let source_attr = if source
1429
+ .map(|s| s.get("kind").map(Json::to_s).unwrap_or_default())
1430
+ == Some("argument".to_string())
1431
+ {
1432
+ let name = source
1433
+ .unwrap()
1434
+ .get("name")
1435
+ .map(Json::to_s)
1436
+ .unwrap_or_default();
1437
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
795
1438
  cmd_attrs.iter().find(|a| crate::attr::name(a) == name)
796
1439
  } else {
797
1440
  None
798
1441
  };
799
1442
 
800
- if crate::attr::list(target_attr) && optional && crate::shared::list_attr_creation_optional(aggregate, crate::attr::name(target_attr), value_objects_by_name) {
801
- exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
802
- } else if crate::attr::list(target_attr) && source_attr.map(crate::attr::optional).unwrap_or(false) {
803
- exemplar.render("mutation_set_unwrap_or_default", &[("tmpl_field", target_field.to_string()), ("tmpl_optional_rhs_placeholder()", rhs)])
1443
+ if crate::attr::list(target_attr)
1444
+ && optional
1445
+ && crate::shared::list_attr_creation_optional(
1446
+ aggregate,
1447
+ crate::attr::name(target_attr),
1448
+ )
1449
+ {
1450
+ exemplar.render(
1451
+ "mutation_set_plain",
1452
+ &[
1453
+ ("tmpl_field", target_field.to_string()),
1454
+ ("tmpl_rhs_placeholder2()", rhs),
1455
+ ],
1456
+ )
1457
+ } else if crate::attr::list(target_attr)
1458
+ && source_attr.is_some_and(crate::attr::optional)
1459
+ {
1460
+ exemplar.render(
1461
+ "mutation_set_unwrap_or_default",
1462
+ &[
1463
+ ("tmpl_field", target_field.to_string()),
1464
+ ("tmpl_optional_rhs_placeholder()", rhs),
1465
+ ],
1466
+ )
804
1467
  } else if crate::attr::list(target_attr) {
805
1468
  // Only whole-list replaces carry the duplicate-identity guard.
806
- let (guard, effective_rhs) = entity_list_replace_guard(aggregate, target_attr, &target_field, &rhs, value_objects_by_name);
807
- format!("{guard}{}", exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", effective_rhs)]))
1469
+ let (guard, effective_rhs) = entity_list_replace_guard(
1470
+ aggregate,
1471
+ target_attr,
1472
+ target_field,
1473
+ &rhs,
1474
+ value_objects_by_name,
1475
+ );
1476
+ format!(
1477
+ "{guard}{}",
1478
+ exemplar.render(
1479
+ "mutation_set_plain",
1480
+ &[
1481
+ ("tmpl_field", target_field.to_string()),
1482
+ ("tmpl_rhs_placeholder2()", effective_rhs)
1483
+ ]
1484
+ )
1485
+ )
808
1486
  } else {
809
- let source_is_option = source_attr.map(crate::attr::optional).unwrap_or(false) || state_source_is_option(source, aggregate, optional);
810
- let wrap = (optional || crate::attr::optional(target_attr)) && !source_is_option;
1487
+ let source_is_option = source_attr.is_some_and(crate::attr::optional)
1488
+ || state_source_is_option(source, aggregate, optional);
1489
+ let wrap =
1490
+ (optional || crate::attr::optional(target_attr)) && !source_is_option;
811
1491
  if wrap {
812
- exemplar.render("mutation_set_wrapped", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
1492
+ exemplar.render(
1493
+ "mutation_set_wrapped",
1494
+ &[
1495
+ ("tmpl_field", target_field.to_string()),
1496
+ ("tmpl_rhs_placeholder2()", rhs),
1497
+ ],
1498
+ )
813
1499
  } else {
814
- exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
1500
+ exemplar.render(
1501
+ "mutation_set_plain",
1502
+ &[
1503
+ ("tmpl_field", target_field.to_string()),
1504
+ ("tmpl_rhs_placeholder2()", rhs),
1505
+ ],
1506
+ )
815
1507
  }
816
1508
  }
817
1509
  }
818
1510
  }
819
1511
  "increment" | "decrement" => {
820
- let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("arithmetic target must resolve");
1512
+ let (target_attr, integer_field, kind) = crate::bridging::arithmetic_target_field(
1513
+ mutation,
1514
+ aggregate,
1515
+ value_objects_by_name,
1516
+ )
1517
+ .expect("arithmetic target must resolve");
821
1518
  let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
822
1519
  let field_ident = naming::rust_ident_field(&integer_field);
823
- let amount_expr = crate::bridging::arithmetic_amount_expr(mutation.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &integer_field).expect("arithmetic amount must resolve");
1520
+ if let Some(text) = crate::bridging::text_operand_expr(
1521
+ mutation,
1522
+ aggregate,
1523
+ command,
1524
+ value_objects_by_name,
1525
+ &integer_field,
1526
+ ) {
1527
+ return text_operand_refusal(
1528
+ crate::attr::type_name(target_attr),
1529
+ &integer_field,
1530
+ &text,
1531
+ );
1532
+ }
1533
+ let amount_expr = crate::bridging::arithmetic_amount_expr(
1534
+ mutation.get("source").unwrap_or(&Json::Null),
1535
+ command,
1536
+ value_objects_by_name,
1537
+ &integer_field,
1538
+ kind,
1539
+ )
1540
+ .expect("arithmetic amount must resolve");
824
1541
  // Use the IR's `sign`, not the op name.
825
- let sign = if mutation.get("sign").map(Json::to_s).unwrap_or_default() == "1" { "+" } else { "-" };
826
- let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
1542
+ let sign = if mutation.get("sign").map(Json::to_s).unwrap_or_default() == "1" {
1543
+ "+"
1544
+ } else {
1545
+ "-"
1546
+ };
1547
+ let current = numeric_current(
1548
+ optional,
1549
+ target_field,
1550
+ target_attr,
1551
+ &integer_field,
1552
+ kind,
1553
+ value_objects_by_name,
1554
+ );
827
1555
  let op = mutation.get("op").map(Json::to_s).unwrap_or_default();
828
- let updated = format!("{vo_type} {{ {field_ident}: {}, ..current }}", checked_arithmetic(&op, &field_ident, sign, &amount_expr));
1556
+ let updated = format!(
1557
+ "{vo_type} {{ {field_ident}: {}, ..current }}",
1558
+ arithmetic_field_value(kind, &op, &field_ident, sign, &amount_expr)
1559
+ );
829
1560
  exemplar.render(
830
1561
  "mutation_arithmetic",
831
- &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
1562
+ &[
1563
+ ("tmpl_field", target_field.to_string()),
1564
+ ("tmpl_current_placeholder()", current),
1565
+ (
1566
+ "tmpl_updated_placeholder()",
1567
+ if optional {
1568
+ format!("Some({updated})")
1569
+ } else {
1570
+ updated
1571
+ },
1572
+ ),
1573
+ ],
832
1574
  )
833
1575
  }
834
1576
  "multiply" => {
835
- // Same Integer-field subset as `increment`/`decrement`, with `*`.
836
- let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("arithmetic target must resolve");
1577
+ // Same numeric-field subset as `increment`/`decrement`, with `*`.
1578
+ let (target_attr, integer_field, kind) = crate::bridging::arithmetic_target_field(
1579
+ mutation,
1580
+ aggregate,
1581
+ value_objects_by_name,
1582
+ )
1583
+ .expect("arithmetic target must resolve");
837
1584
  let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
838
1585
  let field_ident = naming::rust_ident_field(&integer_field);
839
- let amount_expr = crate::bridging::arithmetic_amount_expr(mutation.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &integer_field).expect("arithmetic amount must resolve");
840
- let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
841
- let updated = format!("{vo_type} {{ {field_ident}: {}, ..current }}", checked_arithmetic("multiply", &field_ident, "*", &amount_expr));
1586
+ if let Some(text) = crate::bridging::text_operand_expr(
1587
+ mutation,
1588
+ aggregate,
1589
+ command,
1590
+ value_objects_by_name,
1591
+ &integer_field,
1592
+ ) {
1593
+ return text_operand_refusal(
1594
+ crate::attr::type_name(target_attr),
1595
+ &integer_field,
1596
+ &text,
1597
+ );
1598
+ }
1599
+ let amount_expr = crate::bridging::arithmetic_amount_expr(
1600
+ mutation.get("source").unwrap_or(&Json::Null),
1601
+ command,
1602
+ value_objects_by_name,
1603
+ &integer_field,
1604
+ kind,
1605
+ )
1606
+ .expect("arithmetic amount must resolve");
1607
+ let current = numeric_current(
1608
+ optional,
1609
+ target_field,
1610
+ target_attr,
1611
+ &integer_field,
1612
+ kind,
1613
+ value_objects_by_name,
1614
+ );
1615
+ let updated = format!(
1616
+ "{vo_type} {{ {field_ident}: {}, ..current }}",
1617
+ arithmetic_field_value(kind, "multiply", &field_ident, "*", &amount_expr)
1618
+ );
842
1619
  exemplar.render(
843
1620
  "mutation_arithmetic",
844
- &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
1621
+ &[
1622
+ ("tmpl_field", target_field.to_string()),
1623
+ ("tmpl_current_placeholder()", current),
1624
+ (
1625
+ "tmpl_updated_placeholder()",
1626
+ if optional {
1627
+ format!("Some({updated})")
1628
+ } else {
1629
+ updated
1630
+ },
1631
+ ),
1632
+ ],
845
1633
  )
846
1634
  }
847
1635
  "clamp" => {
848
1636
  // The source is always a literal `[min, max]` pair, so there is no amount to resolve.
849
- let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("clamp target must resolve");
1637
+ let (target_attr, integer_field) =
1638
+ crate::bridging::integer_target_field(mutation, aggregate, value_objects_by_name)
1639
+ .expect("clamp target must resolve");
850
1640
  let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
851
1641
  let field_ident = naming::rust_ident_field(&integer_field);
852
- let (min, max) = mutation.get("source").and_then(crate::bridging::clamp_bounds_ints).expect("clamp bounds must resolve");
853
- let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
1642
+ let (min, max) = mutation
1643
+ .get("source")
1644
+ .and_then(crate::bridging::clamp_bounds_ints)
1645
+ .expect("clamp bounds must resolve");
1646
+ let current = numeric_current(
1647
+ optional,
1648
+ target_field,
1649
+ target_attr,
1650
+ &integer_field,
1651
+ crate::bridging::NumericKind::Integer,
1652
+ value_objects_by_name,
1653
+ );
854
1654
  let updated = format!("{vo_type} {{ {field_ident}: current.{field_ident}.clamp({min}, {max}), ..current }}");
855
1655
  exemplar.render(
856
1656
  "mutation_arithmetic",
857
- &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
1657
+ &[
1658
+ ("tmpl_field", target_field.to_string()),
1659
+ ("tmpl_current_placeholder()", current),
1660
+ (
1661
+ "tmpl_updated_placeholder()",
1662
+ if optional {
1663
+ format!("Some({updated})")
1664
+ } else {
1665
+ updated
1666
+ },
1667
+ ),
1668
+ ],
858
1669
  )
859
1670
  }
860
1671
  "remove" => {
861
1672
  // `remove_field_problems` already confirmed every lookup below resolves.
862
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
863
- let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("remove target must be a declared aggregate attribute");
864
- let entities = aggregate.get("entities").map(Json::each).unwrap_or(&[]);
865
- let entity = entities.iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))).expect("remove target must be an entity-typed list");
866
- let (id_attr, _) = entity_identity_mint(entity, value_objects_by_name).expect("remove target entity's identity must mint");
1673
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
1674
+ let target_attr = attrs
1675
+ .iter()
1676
+ .find(|a| crate::attr::name(a) == target_name)
1677
+ .expect("remove target must be a declared aggregate attribute");
1678
+ let entities = aggregate.get("entities").map_or_default(Json::each);
1679
+ let Some(entity) = entities.iter().find(|e| {
1680
+ e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))
1681
+ }) else {
1682
+ // A value-object list: drop every element equal by value to the argument.
1683
+ let source_name = mutation
1684
+ .get("source")
1685
+ .and_then(|s| s.get("name"))
1686
+ .map(Json::to_s)
1687
+ .unwrap_or_default();
1688
+ return exemplar.render(
1689
+ "mutation_remove_value",
1690
+ &[
1691
+ ("tmpl_field", target_field.to_string()),
1692
+ (
1693
+ "tmpl_remove_value_placeholder()",
1694
+ format!("args.{}", naming::rust_ident_field(&source_name)),
1695
+ ),
1696
+ ],
1697
+ );
1698
+ };
1699
+ let (id_attr, _) = entity_identity_mint(entity, value_objects_by_name)
1700
+ .expect("remove target entity's identity must mint");
867
1701
  let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
868
- let source_name = mutation.get("source").and_then(|s| s.get("name")).map(Json::to_s).unwrap_or_default();
869
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
870
- let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name).expect("remove source must be a declared argument");
1702
+ let source_name = mutation
1703
+ .get("source")
1704
+ .and_then(|s| s.get("name"))
1705
+ .map(Json::to_s)
1706
+ .unwrap_or_default();
1707
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
1708
+ let source_attr = cmd_attrs
1709
+ .iter()
1710
+ .find(|a| crate::attr::name(a) == source_name)
1711
+ .expect("remove source must be a declared argument");
871
1712
  let match_expr = crate::bridging::value_rhs(
872
- &format!("args.{}", naming::rust_ident_field(crate::attr::name(source_attr))),
1713
+ &format!(
1714
+ "args.{}",
1715
+ naming::rust_ident_field(crate::attr::name(source_attr))
1716
+ ),
873
1717
  crate::attr::type_name(source_attr),
874
1718
  crate::attr::type_name(id_attr),
875
1719
  value_objects_by_name,
876
1720
  );
877
- exemplar.render("mutation_remove", &[("tmpl_field", target_field.to_string()), ("tmpl_id_field", id_field), ("tmpl_remove_match_placeholder()", match_expr)])
1721
+ exemplar.render(
1722
+ "mutation_remove",
1723
+ &[
1724
+ ("tmpl_field", target_field.to_string()),
1725
+ ("tmpl_id_field", id_field),
1726
+ ("tmpl_remove_match_placeholder()", match_expr),
1727
+ ],
1728
+ )
878
1729
  }
879
1730
  // `corrects` targets no field; entity-level commands do not filter it out first.
880
1731
  "corrects" => String::new(),
881
- other => panic!("unsupported mutation op {other:?} — command_skip_reason should have caught this"),
1732
+ other => panic!(
1733
+ "unsupported mutation op {other:?} — command_skip_reason should have caught this"
1734
+ ),
882
1735
  }
883
1736
  }
884
1737
 
@@ -942,17 +1795,29 @@ mod state_source_tests {
942
1795
 
943
1796
  #[test]
944
1797
  fn a_state_source_in_an_all_optional_record_is_already_an_option() {
945
- assert!(state_source_is_option(Some(&state("body")), &aggregate(), true));
1798
+ assert!(state_source_is_option(
1799
+ Some(&state("body")),
1800
+ &aggregate(),
1801
+ true
1802
+ ));
946
1803
  }
947
1804
 
948
1805
  #[test]
949
1806
  fn a_state_source_reads_an_option_where_it_is_declared_optional() {
950
- assert!(state_source_is_option(Some(&state("draft_body")), &aggregate(), false));
1807
+ assert!(state_source_is_option(
1808
+ Some(&state("draft_body")),
1809
+ &aggregate(),
1810
+ false
1811
+ ));
951
1812
  }
952
1813
 
953
1814
  #[test]
954
1815
  fn a_required_state_source_in_a_plain_record_is_not_an_option() {
955
- assert!(!state_source_is_option(Some(&state("body")), &aggregate(), false));
1816
+ assert!(!state_source_is_option(
1817
+ Some(&state("body")),
1818
+ &aggregate(),
1819
+ false
1820
+ ));
956
1821
  }
957
1822
 
958
1823
  #[test]
@@ -967,57 +1832,165 @@ mod expression_source_tests {
967
1832
  use super::*;
968
1833
 
969
1834
  fn aggregate() -> Json {
970
- Json::parse(r#"{"name":"Settlement","attributes":[
1835
+ Json::parse(
1836
+ r#"{"name":"Settlement","attributes":[
971
1837
  {"name":"paid","type":"Cents","list":false},
972
1838
  {"name":"refund","type":"Cents","list":false},
973
- {"name":"note","type":"Note","list":false}]}"#)
1839
+ {"name":"note","type":"Note","list":false}]}"#,
1840
+ )
974
1841
  .unwrap()
975
1842
  }
976
1843
 
977
1844
  fn vos() -> Vec<Json> {
978
1845
  vec![
979
- Json::parse(r#"{"name":"Cents","attributes":[{"name":"value","type":"Integer","list":false}]}"#).unwrap(),
980
- Json::parse(r#"{"name":"Note","attributes":[{"name":"value","type":"String","list":false}]}"#).unwrap(),
1846
+ Json::parse(
1847
+ r#"{"name":"Cents","attributes":[{"name":"value","type":"Integer","list":false}]}"#,
1848
+ )
1849
+ .unwrap(),
1850
+ Json::parse(
1851
+ r#"{"name":"Note","attributes":[{"name":"value","type":"String","list":false}]}"#,
1852
+ )
1853
+ .unwrap(),
981
1854
  ]
982
1855
  }
983
1856
 
984
1857
  fn command(target: &str) -> Json {
985
1858
  let source = r#"{"kind":"expression","text":"paid * 2","ast":{"op":"mul","left":{"op":"lookup","path":["paid"]},"right":{"op":"int","value":2}}}"#;
986
- Json::parse(&format!(r#"{{"mutations":[{{"target":"{target}","op":"set","source":{source}}}]}}"#)).unwrap()
1859
+ Json::parse(&format!(
1860
+ r#"{{"mutations":[{{"target":"{target}","op":"set","source":{source}}}]}}"#
1861
+ ))
1862
+ .unwrap()
987
1863
  }
988
1864
 
989
1865
  fn by_name(vos: &[Json]) -> HashMap<String, &Json> {
990
- vos.iter().map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo)).collect()
1866
+ vos.iter()
1867
+ .map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo))
1868
+ .collect()
991
1869
  }
992
1870
 
993
1871
  #[test]
994
1872
  fn an_expression_source_binds_the_pre_dispatch_record() {
995
- assert!(reads_pre_state(command("refund").get("mutations").map(Json::each).unwrap_or(&[])));
1873
+ assert!(reads_pre_state(
1874
+ command("refund")
1875
+ .get("mutations")
1876
+ .map_or_default(Json::each)
1877
+ ));
996
1878
  }
997
1879
 
998
1880
  #[test]
999
1881
  fn a_target_typed_by_an_integer_value_object_is_generated() {
1000
1882
  let vos = vos();
1001
1883
 
1002
- assert!(expression_source_problems(&command("refund"), &aggregate(), &by_name(&vos)).is_empty());
1884
+ assert!(
1885
+ expression_source_problems(&command("refund"), &aggregate(), &by_name(&vos)).is_empty()
1886
+ );
1003
1887
  }
1004
1888
 
1005
1889
  #[test]
1006
1890
  fn a_target_typed_by_a_text_value_object_is_not_generated_yet() {
1007
1891
  let vos = vos();
1008
1892
 
1009
- assert_eq!(expression_source_problems(&command("note"), &aggregate(), &by_name(&vos)).len(), 1);
1893
+ assert_eq!(
1894
+ expression_source_problems(&command("note"), &aggregate(), &by_name(&vos)).len(),
1895
+ 1
1896
+ );
1010
1897
  }
1011
1898
 
1012
1899
  #[test]
1013
1900
  fn the_right_hand_side_evaluates_through_the_kernel_and_rebuilds_the_value_object() {
1014
1901
  let vos = vos();
1015
1902
  let aggregate = aggregate();
1016
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
1017
- let source = command("refund").get("mutations").map(Json::each).unwrap_or(&[])[0].get("source").cloned().unwrap();
1903
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
1904
+ let source = command("refund")
1905
+ .get("mutations")
1906
+ .map_or_default(Json::each)[0]
1907
+ .get("source")
1908
+ .cloned()
1909
+ .unwrap();
1018
1910
 
1019
1911
  let rhs = expression_set_rhs(&source, &attrs[1], &by_name(&vos));
1020
1912
 
1021
- assert!(rhs.contains("crate::kernel::interpret(&Expr::Mul(") && rhs.contains("Cents { value: number }"));
1913
+ assert!(
1914
+ rhs.contains("crate::kernel::interpret(&Expr::Mul(")
1915
+ && rhs.contains("Cents { value: number }")
1916
+ );
1917
+ }
1918
+ }
1919
+
1920
+ #[cfg(test)]
1921
+ mod value_object_remove_tests {
1922
+ use super::*;
1923
+
1924
+ fn aggregate() -> Json {
1925
+ Json::parse(r#"{"name":"Tally","attributes":[{"name":"tags","type":"Label","list":true}]}"#)
1926
+ .unwrap()
1927
+ }
1928
+
1929
+ fn label() -> Json {
1930
+ Json::parse(
1931
+ r#"{"name":"Label","attributes":[{"name":"value","type":"String","list":false}]}"#,
1932
+ )
1933
+ .unwrap()
1934
+ }
1935
+
1936
+ fn command(argument_type: &str, source: &str) -> Json {
1937
+ Json::parse(&format!(
1938
+ r#"{{"name":"Untag","attributes":[{{"name":"label","type":"{argument_type}","list":false}}],
1939
+ "mutations":[{{"target":"tags","op":"remove","source":{source}}}]}}"#
1940
+ ))
1941
+ .unwrap()
1942
+ }
1943
+
1944
+ fn problems(command: &Json) -> Vec<String> {
1945
+ let label = label();
1946
+ let by_name: HashMap<String, &Json> = HashMap::from([("Label".to_string(), &label)]);
1947
+ remove_field_problems(command, &aggregate(), &by_name)
1948
+ }
1949
+
1950
+ #[test]
1951
+ fn a_value_object_argument_of_the_element_type_is_generatable() {
1952
+ let command = command("Label", r#"{"kind":"argument","name":"label"}"#);
1953
+ assert!(problems(&command).is_empty());
1954
+ }
1955
+
1956
+ #[test]
1957
+ fn an_argument_of_another_type_is_refused() {
1958
+ let command = command("Other", r#"{"kind":"argument","name":"label"}"#);
1959
+ assert!(problems(&command)[0].contains("needs a single Label argument"));
1960
+ }
1961
+
1962
+ #[test]
1963
+ fn a_literal_source_is_refused() {
1964
+ let command = command("Label", r#"{"kind":"literal","value":"x"}"#);
1965
+ assert!(problems(&command)[0].contains("not an argument"));
1966
+ }
1967
+ }
1968
+
1969
+ #[cfg(test)]
1970
+ mod float_arithmetic_tests {
1971
+ use super::*;
1972
+ use crate::bridging::NumericKind;
1973
+
1974
+ #[test]
1975
+ fn a_float_increment_faults_unless_the_sum_is_finite() {
1976
+ assert_eq!(
1977
+ float_arithmetic("increment", "value", "+", "args.more.value"),
1978
+ "{ let amount = args.more.value; crate::kernel::ruby_float::finite_or_fault(\"increment\", \"+\", current.value, amount, current.value + amount)? }"
1979
+ );
1980
+ }
1981
+
1982
+ #[test]
1983
+ fn a_float_multiply_uses_the_product() {
1984
+ assert!(
1985
+ float_arithmetic("multiply", "value", "*", "2.0").contains("current.value * amount)?")
1986
+ );
1987
+ }
1988
+
1989
+ #[test]
1990
+ fn the_kind_picks_checked_integer_or_finite_float_arithmetic() {
1991
+ let integer = arithmetic_field_value(NumericKind::Integer, "increment", "value", "+", "1");
1992
+ let float = arithmetic_field_value(NumericKind::Float, "increment", "value", "+", "1.0");
1993
+ assert!(integer.contains("checked_add") && !integer.contains("ruby_float"));
1994
+ assert!(float.contains("finite_or_fault") && !float.contains("checked_add"));
1022
1995
  }
1023
1996
  }