hecks 3.13.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -1,18 +1,23 @@
1
- // A Rust port of `web/ui_schema.rb`: turns a domain IR plus config into
2
- // the exact JSON `GET /api/ui-schema` serves (nav, columns, forms).
1
+ // Turns a domain IR plus config into the exact JSON `GET /api/ui-schema` serves (nav, columns,
2
+ // forms). There is no Ruby ui_schema: the Ruby HTML renderer (lib/hecks/forms/field_shape.rb and
3
+ // value_object_shape.rb) shares its field-shape rules, but this module is the only builder.
3
4
 
4
5
  use serde_json::{json, Map, Value};
5
6
  use std::collections::HashMap;
6
- use std::sync::LazyLock;
7
7
 
8
- use regex::Regex;
8
+ use crate::hecks_naming::{plural, snake};
9
9
 
10
10
  /// `Hecks::Bluebook::IR::Attribute::PRIMITIVES`.
11
11
  const PRIMITIVES: &[&str] = &["String", "Integer", "Float", "TrueClass", "FalseClass"];
12
12
 
13
13
  /// `UiSchema::DISPLAY_BY_SHAPE` — a detail field's default placement,
14
14
  /// taken from its own shape; `display:` config overrides it.
15
- const DISPLAY_BY_SHAPE: &[(&str, &str)] = &[("text", "prose"), ("lines", "bullet"), ("rows", "rows"), ("compound", "mono")];
15
+ const DISPLAY_BY_SHAPE: &[(&str, &str)] = &[
16
+ ("text", "prose"),
17
+ ("lines", "bullet"),
18
+ ("rows", "rows"),
19
+ ("compound", "mono"),
20
+ ];
16
21
 
17
22
  /// Shapes a table column can't carry, so they're left out of the
18
23
  /// default (unconfigured) column list.
@@ -28,7 +33,10 @@ pub fn build(domain_ir: &Value, config: &Value) -> Value {
28
33
 
29
34
  let mut collections = Map::new();
30
35
  for aggregate in &aggregates {
31
- let key = key_for.get(agg_name(aggregate)).cloned().unwrap_or_default();
36
+ let key = key_for
37
+ .get(agg_name(aggregate))
38
+ .cloned()
39
+ .unwrap_or_default();
32
40
  let scoped = with_shared_value_objects(aggregate, &aggregates);
33
41
  collections.insert(key, build_collection(&scoped, config, &key_for, &by_name));
34
42
  }
@@ -71,16 +79,18 @@ pub fn schema(domain_ir: &Value, config: &Value) -> Value {
71
79
  /// `UiSchema.collection_key` — config's own `key:` if it names one,
72
80
  /// else the pluralized snake_case of the aggregate's own name.
73
81
  pub fn collection_key(aggregate: &Value, config: &Value) -> String {
74
- match dig(config, &["collections", agg_name(aggregate), "key"]).and_then(|v| v.as_str()) {
75
- Some(key) => key.to_string(),
76
- None => plural(&snake(agg_name(aggregate))),
77
- }
82
+ dig(config, &["collections", agg_name(aggregate), "key"])
83
+ .and_then(|v| v.as_str())
84
+ .map_or_else(|| plural(&snake(agg_name(aggregate))), ToString::to_string)
78
85
  }
79
86
 
80
87
  /// Aggregate name -> collection key, for every aggregate in the
81
88
  /// domain: what a reference field's own `targetKey` resolves through.
82
89
  pub fn key_for(aggregates: &[&Value], config: &Value) -> KeyFor {
83
- aggregates.iter().map(|a| (agg_name(a).to_string(), collection_key(a, config))).collect()
90
+ aggregates
91
+ .iter()
92
+ .map(|a| (agg_name(a).to_string(), collection_key(a, config)))
93
+ .collect()
84
94
  }
85
95
 
86
96
  /// The aggregate with every value object the rest of the domain
@@ -108,35 +118,62 @@ fn with_shared_value_objects(aggregate: &Value, aggregates: &[&Value]) -> Value
108
118
  }
109
119
 
110
120
  pub fn aggregates(domain_ir: &Value) -> Vec<&Value> {
111
- domain_ir.get("aggregates").and_then(|v| v.as_array()).map(|a| a.iter().collect()).unwrap_or_default()
121
+ domain_ir
122
+ .get("aggregates")
123
+ .and_then(|v| v.as_array())
124
+ .map(|a| a.iter().collect())
125
+ .unwrap_or_default()
112
126
  }
113
127
 
114
- fn build_collection(aggregate: &Value, config: &Value, key_for: &KeyFor, by_name: &HashMap<&str, &Value>) -> Value {
128
+ fn build_collection(
129
+ aggregate: &Value,
130
+ config: &Value,
131
+ key_for: &KeyFor,
132
+ by_name: &HashMap<&str, &Value>,
133
+ ) -> Value {
115
134
  let name = agg_name(aggregate);
116
- let agg_cfg = dig(config, &["collections", name]).cloned().unwrap_or_else(|| json!({}));
117
- let state_cfg = dig(config, &["states", name]).cloned().unwrap_or_else(|| json!({}));
135
+ let agg_cfg = dig(config, &["collections", name])
136
+ .cloned()
137
+ .unwrap_or_else(|| json!({}));
138
+ let state_cfg = dig(config, &["states", name])
139
+ .cloned()
140
+ .unwrap_or_else(|| json!({}));
118
141
  let formats = agg_cfg.get("field_formats").cloned().unwrap_or(Value::Null);
119
142
  let identity = agg_cfg.get("identity").filter(|v| !v.is_null());
120
143
  let creating = commands(aggregate).into_iter().find(|c| creates(c));
121
- let others: Vec<&Value> = commands(aggregate).into_iter().filter(|c| !creates(c)).collect();
122
- let noun_sing = match agg_cfg.get("noun_sing").and_then(|v| v.as_str()) {
123
- Some(noun) => noun.to_string(),
124
- None => snake(name).replace('_', " "),
125
- };
144
+ let others: Vec<&Value> = commands(aggregate)
145
+ .into_iter()
146
+ .filter(|c| !creates(c))
147
+ .collect();
148
+ let noun_sing = agg_cfg
149
+ .get("noun_sing")
150
+ .and_then(|v| v.as_str())
151
+ .map_or_else(|| snake(name).replace('_', " "), ToString::to_string);
126
152
 
127
- let fields: Vec<Value> = array(aggregate, "attributes").iter().map(|a| field(a, aggregate, key_for, &formats)).collect();
153
+ let fields: Vec<Value> = array(aggregate, "attributes")
154
+ .iter()
155
+ .map(|a| field(a, aggregate, key_for, &formats))
156
+ .collect();
128
157
 
129
158
  let mut collection = Map::new();
130
159
  collection.insert("aggregate".to_string(), json!(name));
131
- collection.insert("label".to_string(), json!(string_or(&agg_cfg, "label", &humanize_words(name))));
160
+ collection.insert(
161
+ "label".to_string(),
162
+ json!(string_or(&agg_cfg, "label", &humanize_words(name))),
163
+ );
132
164
  collection.insert("nounSing".to_string(), json!(noun_sing));
133
165
  collection.insert("navGroup".to_string(), optional(&agg_cfg, "nav_group"));
134
- collection.insert("primaryField".to_string(), optional(&agg_cfg, "primary_field"));
166
+ collection.insert(
167
+ "primaryField".to_string(),
168
+ optional(&agg_cfg, "primary_field"),
169
+ );
135
170
  collection.insert("listQuery".to_string(), optional(&agg_cfg, "list_query"));
136
- collection.insert("stateField".to_string(), match lifecycle(aggregate) {
137
- Some(l) => l.get("field").cloned().unwrap_or(Value::Null),
138
- None => Value::Null,
139
- });
171
+ collection.insert(
172
+ "stateField".to_string(),
173
+ lifecycle(aggregate).map_or(Value::Null, |l| {
174
+ l.get("field").cloned().unwrap_or(Value::Null)
175
+ }),
176
+ );
140
177
  collection.insert("statusLabels".to_string(), state_facet(&state_cfg, "label"));
141
178
  collection.insert("statusTone".to_string(), state_facet(&state_cfg, "tone"));
142
179
  collection.insert("statusNotes".to_string(), state_facet(&state_cfg, "note"));
@@ -148,16 +185,29 @@ fn build_collection(aggregate: &Value, config: &Value, key_for: &KeyFor, by_name
148
185
  );
149
186
  collection.insert(
150
187
  "detailFields".to_string(),
151
- build_detail_fields(agg_cfg.get("detail_fields"), &fields, aggregate, by_name, key_for),
188
+ build_detail_fields(
189
+ agg_cfg.get("detail_fields"),
190
+ &fields,
191
+ aggregate,
192
+ by_name,
193
+ key_for,
194
+ ),
195
+ );
196
+ collection.insert(
197
+ "transitions".to_string(),
198
+ build_transitions(aggregate, &others, key_for, &formats),
199
+ );
200
+ collection.insert(
201
+ "alwaysAvailable".to_string(),
202
+ build_always_available(aggregate, &others, key_for, &formats),
152
203
  );
153
- collection.insert("transitions".to_string(), build_transitions(aggregate, &others, key_for, &formats));
154
- collection.insert("alwaysAvailable".to_string(), build_always_available(aggregate, &others, key_for, &formats));
155
204
  collection.insert(
156
205
  "create".to_string(),
157
- match creating {
158
- None => Value::Null,
159
- Some(command) => build_create(command, aggregate, &agg_cfg, identity, key_for, &formats, &noun_sing),
160
- },
206
+ creating.map_or(Value::Null, |command| {
207
+ build_create(
208
+ command, aggregate, &agg_cfg, identity, key_for, &formats, &noun_sing,
209
+ )
210
+ }),
161
211
  );
162
212
  Value::Object(collection)
163
213
  }
@@ -175,11 +225,17 @@ fn build_create(
175
225
  // An identity-derived field is computed server-side at dispatch
176
226
  // (`apply_identity!`); asking someone to type "P-004" by hand would
177
227
  // only invite a collision with what the server already computes.
178
- let identity_field = identity.and_then(|i| i.get("field")).and_then(|v| v.as_str());
228
+ let identity_field = identity
229
+ .and_then(|i| i.get("field"))
230
+ .and_then(|v| v.as_str());
179
231
  let fields: Vec<Value> = array(command, "attributes")
180
232
  .iter()
181
- .map(|attribute| field_with_precondition(attribute, aggregate, key_for, preconditions, formats))
182
- .filter(|f| identity_field.is_none_or(|name| f.get("key").and_then(|v| v.as_str()) != Some(name)))
233
+ .map(|attribute| {
234
+ field_with_precondition(attribute, aggregate, key_for, preconditions, formats)
235
+ })
236
+ .filter(|f| {
237
+ identity_field.is_none_or(|name| f.get("key").and_then(|v| v.as_str()) != Some(name))
238
+ })
183
239
  .collect();
184
240
 
185
241
  json!({
@@ -217,44 +273,69 @@ fn status_attention(state_cfg: &Value) -> Value {
217
273
  Value::Array(out)
218
274
  }
219
275
 
220
- fn build_columns(configured: Option<&Value>, fields: &[Value], aggregate: &Value, by_name: &HashMap<&str, &Value>, key_for: &KeyFor) -> Value {
221
- let entries = match configured.filter(|v| !v.is_null()) {
222
- // A configured empty list is a real answer, not "unconfigured"
223
- // — Ruby's `configured || default` only falls through on nil,
224
- // and `[]` is truthy there.
225
- Some(list) => list.as_array().cloned().unwrap_or_default(),
226
- None => {
276
+ fn build_columns(
277
+ configured: Option<&Value>,
278
+ fields: &[Value],
279
+ aggregate: &Value,
280
+ by_name: &HashMap<&str, &Value>,
281
+ key_for: &KeyFor,
282
+ ) -> Value {
283
+ // A configured empty list is a real answer, not "unconfigured"
284
+ // — Ruby's `configured || default` only falls through on nil,
285
+ // and `[]` is truthy there.
286
+ let entries = configured.filter(|v| !v.is_null()).map_or_else(
287
+ || {
227
288
  let mut default: Vec<Value> = fields
228
289
  .iter()
229
- .filter(|f| !UNCOLUMNABLE.contains(&f.get("shape").and_then(|v| v.as_str()).unwrap_or("")))
290
+ .filter(|f| {
291
+ !UNCOLUMNABLE.contains(&f.get("shape").and_then(|v| v.as_str()).unwrap_or(""))
292
+ })
230
293
  .map(|f| f.get("key").cloned().unwrap_or(Value::Null))
231
294
  .collect();
232
295
  if lifecycle(aggregate).is_some() {
233
296
  default.push(json!("__state__"));
234
297
  }
235
298
  default
236
- }
237
- };
238
- Value::Array(entries.iter().map(|entry| column_for(entry, fields, aggregate, by_name, key_for)).collect())
299
+ },
300
+ |list| list.as_array().cloned().unwrap_or_default(),
301
+ );
302
+ Value::Array(
303
+ entries
304
+ .iter()
305
+ .map(|entry| column_for(entry, fields, aggregate, by_name, key_for))
306
+ .collect(),
307
+ )
239
308
  }
240
309
 
241
310
  /// `UiSchema.column_for` — a bare string names an attribute as-is; a
242
311
  /// hash asks for something the shape alone can't say (`as:`/`head:`/`hop:`).
243
- fn column_for(entry: &Value, fields: &[Value], aggregate: &Value, by_name: &HashMap<&str, &Value>, key_for: &KeyFor) -> Value {
312
+ fn column_for(
313
+ entry: &Value,
314
+ fields: &[Value],
315
+ aggregate: &Value,
316
+ by_name: &HashMap<&str, &Value>,
317
+ key_for: &KeyFor,
318
+ ) -> Value {
244
319
  let sort = sort_config(entry);
245
320
 
246
321
  if entry.as_str() == Some("__state__") {
247
322
  // `|| "state"` guards a hand-edited `columns:` naming
248
323
  // `__state__` on an aggregate with no lifecycle at all: a
249
324
  // config mistake degrades to a generic label, never a 500.
250
- let field_name = lifecycle(aggregate).and_then(|l| l.get("field")).and_then(|v| v.as_str()).unwrap_or("state");
251
- return merge(json!({"key": "__state__", "label": humanize_words(field_name), "shape": "pill"}), sort);
325
+ let field_name = lifecycle(aggregate)
326
+ .and_then(|l| l.get("field"))
327
+ .and_then(|v| v.as_str())
328
+ .unwrap_or("state");
329
+ return merge(
330
+ json!({"key": "__state__", "label": humanize_words(field_name), "shape": "pill"}),
331
+ sort,
332
+ );
252
333
  }
253
334
 
254
335
  if let Some(name) = entry.as_str() {
255
- let base = find_field(fields, name)
256
- .cloned()
257
- .unwrap_or_else(|| json!({"key": name, "label": humanize(name), "shape": "text_value"}));
336
+ let base = find_field(fields, name).cloned().unwrap_or_else(
337
+ || json!({"key": name, "label": humanize(name), "shape": "text_value"}),
338
+ );
258
339
  return merge(base, sort);
259
340
  }
260
341
 
@@ -264,15 +345,17 @@ fn column_for(entry: &Value, fields: &[Value], aggregate: &Value, by_name: &Hash
264
345
 
265
346
  let key = entry.get("field").cloned().unwrap_or(Value::Null);
266
347
  let key_name = key.as_str().unwrap_or("");
267
- let base = find_field(fields, key_name).cloned().unwrap_or_else(|| json!({"key": key, "label": humanize(key_name)}));
268
- let shape = match entry.get("as").filter(|v| !v.is_null()) {
269
- Some(as_shape) => as_shape.clone(),
270
- None => base.get("shape").cloned().unwrap_or(Value::Null),
271
- };
272
- let label = match entry.get("head").filter(|v| !v.is_null()) {
273
- Some(head) => head.clone(),
274
- None => base.get("label").cloned().unwrap_or(Value::Null),
275
- };
348
+ let base = find_field(fields, key_name)
349
+ .cloned()
350
+ .unwrap_or_else(|| json!({"key": key, "label": humanize(key_name)}));
351
+ let shape = entry.get("as").filter(|v| !v.is_null()).map_or_else(
352
+ || base.get("shape").cloned().unwrap_or(Value::Null),
353
+ Value::clone,
354
+ );
355
+ let label = entry.get("head").filter(|v| !v.is_null()).map_or_else(
356
+ || base.get("label").cloned().unwrap_or(Value::Null),
357
+ Value::clone,
358
+ );
276
359
  let overridden = merge(base, json!({"key": key, "shape": shape, "label": label}));
277
360
  merge(overridden, sort)
278
361
  }
@@ -287,44 +370,80 @@ fn sort_config(entry: &Value) -> Value {
287
370
  Some(value) if !value.is_null() => value.clone(),
288
371
  _ => json!(true),
289
372
  };
290
- let sort_default = match entry.get("sort_default").filter(|v| !v.is_null()) {
291
- Some(value) => value.clone(),
292
- None => json!("asc"),
293
- };
373
+ let sort_default = entry
374
+ .get("sort_default")
375
+ .filter(|v| !v.is_null())
376
+ .map_or_else(|| json!("asc"), Value::clone);
294
377
  json!({"sortable": sortable, "sortDefault": sort_default})
295
378
  }
296
379
 
297
- fn build_detail_fields(configured: Option<&Value>, fields: &[Value], aggregate: &Value, by_name: &HashMap<&str, &Value>, key_for: &KeyFor) -> Value {
298
- let entries = match configured.filter(|v| !v.is_null()) {
299
- Some(list) => list.as_array().cloned().unwrap_or_default(),
300
- None => fields.iter().map(|f| f.get("key").cloned().unwrap_or(Value::Null)).collect(),
301
- };
302
- Value::Array(entries.iter().map(|entry| detail_field_for(entry, fields, aggregate, by_name, key_for)).collect())
380
+ fn build_detail_fields(
381
+ configured: Option<&Value>,
382
+ fields: &[Value],
383
+ aggregate: &Value,
384
+ by_name: &HashMap<&str, &Value>,
385
+ key_for: &KeyFor,
386
+ ) -> Value {
387
+ let entries = configured.filter(|v| !v.is_null()).map_or_else(
388
+ || {
389
+ fields
390
+ .iter()
391
+ .map(|f| f.get("key").cloned().unwrap_or(Value::Null))
392
+ .collect()
393
+ },
394
+ |list| list.as_array().cloned().unwrap_or_default(),
395
+ );
396
+ Value::Array(
397
+ entries
398
+ .iter()
399
+ .map(|entry| detail_field_for(entry, fields, aggregate, by_name, key_for))
400
+ .collect(),
401
+ )
303
402
  }
304
403
 
305
- fn detail_field_for(entry: &Value, fields: &[Value], aggregate: &Value, by_name: &HashMap<&str, &Value>, key_for: &KeyFor) -> Value {
404
+ fn detail_field_for(
405
+ entry: &Value,
406
+ fields: &[Value],
407
+ aggregate: &Value,
408
+ by_name: &HashMap<&str, &Value>,
409
+ key_for: &KeyFor,
410
+ ) -> Value {
306
411
  let mut base = column_for(entry, fields, aggregate, by_name, key_for);
307
412
  if base.get("shape").and_then(|v| v.as_str()) == Some("rows") {
308
413
  base = apply_row_columns_filter(base, entry);
309
414
  base = apply_row_state_filter(base, entry, aggregate);
310
415
  }
311
416
  let shape = base.get("shape").and_then(|v| v.as_str()).unwrap_or("");
312
- let display = match entry.get("display").filter(|v| !v.is_null()) {
313
- Some(display) => display.clone(),
314
- None => json!(DISPLAY_BY_SHAPE.iter().find(|(s, _)| *s == shape).map(|(_, d)| *d).unwrap_or("simple")),
315
- };
417
+ let display = entry.get("display").filter(|v| !v.is_null()).map_or_else(
418
+ || {
419
+ json!(DISPLAY_BY_SHAPE
420
+ .iter()
421
+ .find(|(s, _)| *s == shape)
422
+ .map_or("simple", |(_, d)| *d))
423
+ },
424
+ Value::clone,
425
+ );
316
426
  merge(base, json!({"display": display}))
317
427
  }
318
428
 
319
429
  /// Which of a rows field's own sub-columns to show, and in what order.
320
430
  fn apply_row_columns_filter(base: Value, entry: &Value) -> Value {
321
- let Some(wanted) = entry.get("columns").and_then(|v| v.as_array()) else { return base };
322
- let existing = base.get("columns").and_then(|v| v.as_array()).cloned().unwrap_or_default();
431
+ let Some(wanted) = entry.get("columns").and_then(|v| v.as_array()) else {
432
+ return base;
433
+ };
434
+ let existing = base
435
+ .get("columns")
436
+ .and_then(|v| v.as_array())
437
+ .cloned()
438
+ .unwrap_or_default();
323
439
  let kept: Vec<Value> = wanted
324
440
  .iter()
325
441
  .filter_map(|want| {
326
442
  let want = want.as_str()?;
327
- existing.iter().find(|c| c.get("key").and_then(|v| v.as_str()) == Some(want)).cloned()
443
+ existing
444
+ .iter()
445
+ .find(|c| c.get("key").and_then(|v| v.as_str()) == Some(want))
446
+ .cloned()
328
447
  })
329
448
  .collect();
330
449
  merge(base, json!({"columns": kept}))
@@ -333,35 +452,63 @@ fn apply_row_columns_filter(base: Value, entry: &Value) -> Value {
333
452
  /// Which rows count as "current" — `list_of` never drops a removed
334
453
  /// element, so the frontend filters using what this stamps per row.
335
454
  fn apply_row_state_filter(base: Value, entry: &Value, aggregate: &Value) -> Value {
336
- let Some(state_filter) = entry.get("state_filter").filter(|v| !v.is_null()) else { return base };
337
- let key = match entry.get("field").and_then(|v| v.as_str()) {
338
- Some(field) => field,
339
- None => entry.as_str().unwrap_or(""),
455
+ let Some(state_filter) = entry.get("state_filter").filter(|v| !v.is_null()) else {
456
+ return base;
457
+ };
458
+ let key = entry
459
+ .get("field")
460
+ .and_then(|v| v.as_str())
461
+ .map_or_else(|| entry.as_str().unwrap_or(""), |field| field);
462
+ let Some(attribute) = find_attribute(aggregate, key) else {
463
+ return base;
464
+ };
465
+ let Some(entity) = find_entity(aggregate, type_of(attribute)) else {
466
+ return base;
467
+ };
468
+ let Some(field_name) = lifecycle(entity)
469
+ .and_then(|l| l.get("field"))
470
+ .filter(|v| !v.is_null())
471
+ else {
472
+ return base;
340
473
  };
341
- let Some(attribute) = find_attribute(aggregate, key) else { return base };
342
- let Some(entity) = find_entity(aggregate, type_of(attribute)) else { return base };
343
- let Some(field_name) = lifecycle(entity).and_then(|l| l.get("field")).filter(|v| !v.is_null()) else { return base };
344
- merge(base, json!({"stateFilter": {"field": field_name, "value": state_filter}}))
474
+ merge(
475
+ base,
476
+ json!({"stateFilter": {"field": field_name, "value": state_filter}}),
477
+ )
345
478
  }
346
479
 
347
480
  /// A second hop, for display only — resolves this reference, then one
348
481
  /// of its references, falling back to the plain column if it can't.
349
- fn hop_column(entry: &Value, fields: &[Value], by_name: &HashMap<&str, &Value>, key_for: &KeyFor) -> Value {
482
+ fn hop_column(
483
+ entry: &Value,
484
+ fields: &[Value],
485
+ by_name: &HashMap<&str, &Value>,
486
+ key_for: &KeyFor,
487
+ ) -> Value {
350
488
  let key = entry.get("field").cloned().unwrap_or(Value::Null);
351
489
  let key_name = key.as_str().unwrap_or("");
352
490
  let hop = entry.get("hop").and_then(|v| v.as_str()).unwrap_or("");
353
491
  let head = entry.get("head").filter(|v| !v.is_null());
354
492
 
355
- let base_field = find_field(fields, key_name).filter(|f| f.get("shape").and_then(|v| v.as_str()) == Some("reference"));
493
+ let base_field = find_field(fields, key_name)
494
+ .filter(|f| f.get("shape").and_then(|v| v.as_str()) == Some("reference"));
356
495
  let Some(base_field) = base_field else {
357
496
  let label = head.cloned().unwrap_or_else(|| json!(humanize(key_name)));
358
497
  return json!({"key": key, "label": label, "shape": "text_value"});
359
498
  };
360
499
 
361
- let target = base_field.get("target").and_then(|v| v.as_str()).unwrap_or("");
362
- let hop_attribute = by_name.get(target).and_then(|agg| find_attribute(agg, hop)).filter(|a| is_reference(a));
500
+ let target = base_field
501
+ .get("target")
502
+ .and_then(|v| v.as_str())
503
+ .unwrap_or("");
504
+ let hop_attribute = by_name
505
+ .get(target)
506
+ .and_then(|agg| find_attribute(agg, hop))
507
+ .filter(|a| is_reference(a));
363
508
  let Some(hop_attribute) = hop_attribute else {
364
- let label = head.cloned().unwrap_or_else(|| base_field.get("label").cloned().unwrap_or(Value::Null));
509
+ let label = head
510
+ .cloned()
511
+ .unwrap_or_else(|| base_field.get("label").cloned().unwrap_or(Value::Null));
365
512
  return merge(base_field.clone(), json!({"label": label}));
366
513
  };
367
514
 
@@ -372,14 +519,16 @@ fn hop_column(entry: &Value, fields: &[Value], by_name: &HashMap<&str, &Value>,
372
519
  "label": head.cloned().unwrap_or_else(|| json!(humanize(hop))),
373
520
  "through": hop,
374
521
  "targetKey": base_field.get("targetKey").cloned().unwrap_or(Value::Null),
375
- "hopTargetKey": key_for.get(&hop_target).map(|k| json!(k)).unwrap_or(Value::Null),
522
+ "hopTargetKey": key_for.get(&hop_target).map_or(Value::Null, |k| json!(k)),
376
523
  })
377
524
  }
378
525
 
379
526
  /// `Lifecycle#states` — the default state, then every state some
380
527
  /// transition targets, deduplicated in that order.
381
528
  pub(crate) fn lifecycle_states(aggregate: &Value) -> Vec<String> {
382
- let Some(lifecycle) = lifecycle(aggregate) else { return Vec::new() };
529
+ let Some(lifecycle) = lifecycle(aggregate) else {
530
+ return Vec::new();
531
+ };
383
532
  let mut states = Vec::new();
384
533
  if let Some(default) = lifecycle.get("default").and_then(|v| v.as_str()) {
385
534
  states.push(default.to_string());
@@ -396,8 +545,15 @@ pub(crate) fn lifecycle_states(aggregate: &Value) -> Vec<String> {
396
545
 
397
546
  /// `from: nil` means no constraint at all — an unconstrained transition
398
547
  /// is offered from every state rather than resolved to just one.
399
- fn build_transitions(aggregate: &Value, others: &[&Value], key_for: &KeyFor, formats: &Value) -> Value {
400
- let Some(lifecycle) = lifecycle(aggregate) else { return json!({}) };
548
+ fn build_transitions(
549
+ aggregate: &Value,
550
+ others: &[&Value],
551
+ key_for: &KeyFor,
552
+ formats: &Value,
553
+ ) -> Value {
554
+ let Some(lifecycle) = lifecycle(aggregate) else {
555
+ return json!({});
556
+ };
401
557
  let transitions = array(lifecycle, "transitions");
402
558
 
403
559
  let mut by_state = Map::new();
@@ -405,8 +561,10 @@ fn build_transitions(aggregate: &Value, others: &[&Value], key_for: &KeyFor, for
405
561
  let mut available = Vec::new();
406
562
  for command in others {
407
563
  let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
408
- let candidates: Vec<&Value> =
409
- transitions.iter().filter(|t| t.get("command").and_then(|v| v.as_str()) == Some(name)).collect();
564
+ let candidates: Vec<&Value> = transitions
565
+ .iter()
566
+ .filter(|t| t.get("command").and_then(|v| v.as_str()) == Some(name))
567
+ .collect();
410
568
  if candidates.is_empty() {
411
569
  // "Always available" — handled separately below.
412
570
  continue;
@@ -430,13 +588,20 @@ fn build_transitions(aggregate: &Value, others: &[&Value], key_for: &KeyFor, for
430
588
 
431
589
  /// A command with no lifecycle transition fires regardless of state
432
590
  /// (Contract's `Revise`); an aggregate with none has every command here.
433
- fn build_always_available(aggregate: &Value, others: &[&Value], key_for: &KeyFor, formats: &Value) -> Value {
434
- let transitions = lifecycle(aggregate).map(|l| array(l, "transitions")).unwrap_or(&[]);
591
+ fn build_always_available(
592
+ aggregate: &Value,
593
+ others: &[&Value],
594
+ key_for: &KeyFor,
595
+ formats: &Value,
596
+ ) -> Value {
597
+ let transitions = lifecycle(aggregate).map_or(&[][..], |l| array(l, "transitions"));
435
598
  let available: Vec<Value> = others
436
599
  .iter()
437
600
  .filter(|command| {
438
601
  let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
439
- !transitions.iter().any(|t| t.get("command").and_then(|v| v.as_str()) == Some(name))
602
+ !transitions
603
+ .iter()
604
+ .any(|t| t.get("command").and_then(|v| v.as_str()) == Some(name))
440
605
  })
441
606
  .map(|command| {
442
607
  let name = command.get("name").and_then(|v| v.as_str()).unwrap_or("");
@@ -451,22 +616,37 @@ fn build_always_available(aggregate: &Value, others: &[&Value], key_for: &KeyFor
451
616
  }
452
617
 
453
618
  fn command_inputs(command: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
454
- Value::Array(array(command, "attributes").iter().map(|a| field(a, aggregate, key_for, formats)).collect())
619
+ Value::Array(
620
+ array(command, "attributes")
621
+ .iter()
622
+ .map(|a| field(a, aggregate, key_for, formats))
623
+ .collect(),
624
+ )
455
625
  }
456
626
 
457
627
  /// Stamps a create-form reference field with the state config says it
458
628
  /// must point at, so the client's picker can filter what the server
459
629
  /// would refuse anyway.
460
- fn field_with_precondition(attribute: &Value, aggregate: &Value, key_for: &KeyFor, preconditions: Option<&Value>, formats: &Value) -> Value {
630
+ fn field_with_precondition(
631
+ attribute: &Value,
632
+ aggregate: &Value,
633
+ key_for: &KeyFor,
634
+ preconditions: Option<&Value>,
635
+ formats: &Value,
636
+ ) -> Value {
461
637
  let descriptor = field(attribute, aggregate, key_for, formats);
462
638
  if descriptor.get("shape").and_then(|v| v.as_str()) != Some("reference") {
463
639
  return descriptor;
464
640
  }
465
641
  let key = descriptor.get("key").and_then(|v| v.as_str()).unwrap_or("");
466
- let rule = preconditions
467
- .and_then(|v| v.as_array())
468
- .and_then(|rules| rules.iter().find(|r| r.get("field").and_then(|v| v.as_str()) == Some(key)));
469
- let Some(state) = rule.and_then(|r| r.get("state")).filter(|v| !v.is_null()) else { return descriptor };
642
+ let rule = preconditions.and_then(|v| v.as_array()).and_then(|rules| {
643
+ rules
644
+ .iter()
645
+ .find(|r| r.get("field").and_then(|v| v.as_str()) == Some(key))
646
+ });
647
+ let Some(state) = rule.and_then(|r| r.get("state")).filter(|v| !v.is_null()) else {
648
+ return descriptor;
649
+ };
470
650
  merge(descriptor, json!({"filterState": state}))
471
651
  }
472
652
 
@@ -485,18 +665,20 @@ fn build_nav(aggregates: &[&Value], config: &Value, key_for: &KeyFor) -> Value {
485
665
 
486
666
  for (index, aggregate) in aggregates.iter().enumerate() {
487
667
  let name = agg_name(aggregate);
488
- let agg_cfg = dig(config, &["collections", name]).cloned().unwrap_or_else(|| json!({}));
668
+ let agg_cfg = dig(config, &["collections", name])
669
+ .cloned()
670
+ .unwrap_or_else(|| json!({}));
489
671
  let nav_group = agg_cfg.get("nav_group").and_then(|v| v.as_str());
490
672
  let collection_key = key_for.get(name).cloned().unwrap_or_default();
491
- let group_key = nav_group.map(String::from).unwrap_or_else(|| collection_key.clone());
492
- let nav_order = agg_cfg.get("nav_order").and_then(|v| v.as_f64());
673
+ let group_key = nav_group.map_or_else(|| collection_key.clone(), String::from);
674
+ let nav_order = agg_cfg.get("nav_order").and_then(serde_json::Value::as_f64);
493
675
 
494
676
  if !groups.contains_key(&group_key) {
495
677
  order.push(group_key.clone());
496
- let label = match nav_group {
497
- Some(group) => humanize_words(group),
498
- None => string_or(&agg_cfg, "label", &humanize_words(name)),
499
- };
678
+ let label = nav_group.map_or_else(
679
+ || string_or(&agg_cfg, "label", &humanize_words(name)),
680
+ humanize_words,
681
+ );
500
682
  groups.insert(
501
683
  group_key.clone(),
502
684
  Group {
@@ -508,17 +690,23 @@ fn build_nav(aggregates: &[&Value], config: &Value, key_for: &KeyFor) -> Value {
508
690
  );
509
691
  }
510
692
 
511
- let group = groups.get_mut(&group_key).expect("just inserted if missing");
512
- group.collections.push(collection_key);
513
- if let Some(nav_order) = nav_order {
514
- if nav_order < group.sort.0 {
515
- group.sort = (nav_order, index);
693
+ if let Some(group) = groups.get_mut(&group_key) {
694
+ group.collections.push(collection_key);
695
+ if let Some(nav_order) = nav_order {
696
+ if nav_order < group.sort.0 {
697
+ group.sort = (nav_order, index);
698
+ }
516
699
  }
517
700
  }
518
701
  }
519
702
 
520
703
  let mut ordered: Vec<&Group> = order.iter().filter_map(|k| groups.get(k)).collect();
521
- ordered.sort_by(|a, b| a.sort.partial_cmp(&b.sort).expect("nav_order is never NaN"));
704
+ // A NaN `nav_order` compares equal, keeping declaration order.
705
+ ordered.sort_by(|a, b| {
706
+ a.sort
707
+ .partial_cmp(&b.sort)
708
+ .unwrap_or(std::cmp::Ordering::Equal)
709
+ });
522
710
  Value::Array(
523
711
  ordered
524
712
  .into_iter()
@@ -558,7 +746,7 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
558
746
  let base = json!({
559
747
  "key": name,
560
748
  "label": humanize(name),
561
- "optional": attribute.get("optional").and_then(|v| v.as_bool()).unwrap_or(false),
749
+ "optional": attribute.get("optional").and_then(serde_json::Value::as_bool).unwrap_or(false),
562
750
  });
563
751
 
564
752
  // `to_h` merges the reduced identity into `id`; an aggregate whose
@@ -574,7 +762,7 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
574
762
  json!({
575
763
  "shape": "reference",
576
764
  "target": target,
577
- "targetKey": key_for.get(&target).map(|k| json!(k)).unwrap_or(Value::Null),
765
+ "targetKey": key_for.get(&target).map_or(Value::Null, |k| json!(k)),
578
766
  }),
579
767
  );
580
768
  }
@@ -593,7 +781,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
593
781
  if is_list(attribute) {
594
782
  return merge(base, list_shape(value_object, aggregate, key_for, formats));
595
783
  }
596
- return merge(base, scalar_vo_shape(value_object, aggregate, key_for, formats, format));
784
+ return merge(
785
+ base,
786
+ scalar_vo_shape(value_object, aggregate, key_for, formats, format),
787
+ );
597
788
  }
598
789
 
599
790
  // An entity, not a value object — a `list_of` of pieces rather than
@@ -601,7 +792,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
601
792
  // this same recursion.
602
793
  if is_list(attribute) {
603
794
  if let Some(entity) = find_entity(aggregate, ty) {
604
- return merge(base, json!({"shape": "rows", "columns": row_columns(entity, aggregate, key_for, formats)}));
795
+ return merge(
796
+ base,
797
+ json!({"shape": "rows", "columns": row_columns(entity, aggregate, key_for, formats)}),
798
+ );
605
799
  }
606
800
  }
607
801
 
@@ -611,7 +805,10 @@ pub fn field(attribute: &Value, aggregate: &Value, key_for: &KeyFor, formats: &V
611
805
 
612
806
  fn primitive_shape(ty: &str, format: Option<Value>) -> Value {
613
807
  let mut shape = Map::new();
614
- shape.insert("shape".to_string(), json!(if numeric(ty) { "number" } else { "text_value" }));
808
+ shape.insert(
809
+ "shape".to_string(),
810
+ json!(if numeric(ty) { "number" } else { "text_value" }),
811
+ );
615
812
  if let Some(format) = format {
616
813
  shape.insert("format".to_string(), format);
617
814
  }
@@ -620,7 +817,7 @@ fn primitive_shape(ty: &str, format: Option<Value>) -> Value {
620
817
 
621
818
  fn enum_shape(value_object: &Value) -> Value {
622
819
  let attributes = array(value_object, "attributes");
623
- let discriminant = attributes.first().map(|a| attr_name(a)).unwrap_or("value");
820
+ let discriminant = attributes.first().map_or("value", attr_name);
624
821
  let options: Vec<Value> = array(value_object, "members")
625
822
  .iter()
626
823
  .filter_map(|member| {
@@ -633,31 +830,45 @@ fn enum_shape(value_object: &Value) -> Value {
633
830
  json!({"shape": "enum", "field": discriminant, "options": options})
634
831
  }
635
832
 
636
- fn scalar_vo_shape(value_object: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value, format: Option<Value>) -> Value {
833
+ fn scalar_vo_shape(
834
+ value_object: &Value,
835
+ aggregate: &Value,
836
+ key_for: &KeyFor,
837
+ formats: &Value,
838
+ format: Option<Value>,
839
+ ) -> Value {
637
840
  if money_shaped(value_object) {
638
841
  return json!({"shape": "money"});
639
842
  }
640
843
  if let Some(sole) = sole_attribute(value_object) {
641
844
  return single_attr_shape(sole, format);
642
845
  }
643
- let fields: Vec<Value> = array(value_object, "attributes").iter().map(|a| field(a, aggregate, key_for, formats)).collect();
846
+ let fields: Vec<Value> = array(value_object, "attributes")
847
+ .iter()
848
+ .map(|a| field(a, aggregate, key_for, formats))
849
+ .collect();
644
850
  json!({"shape": "compound", "fields": fields})
645
851
  }
646
852
 
647
853
  fn list_shape(value_object: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
648
- match sole_attribute(value_object) {
649
- Some(sole) => json!({"shape": "lines", "field": attr_name(sole)}),
650
- None => json!({"shape": "rows", "columns": row_columns(value_object, aggregate, key_for, formats)}),
651
- }
854
+ sole_attribute(value_object).map_or_else(
855
+ || json!({"shape": "rows", "columns": row_columns(value_object, aggregate, key_for, formats)}),
856
+ |sole| json!({"shape": "lines", "field": attr_name(sole)}),
857
+ )
652
858
  }
653
859
 
654
860
  /// A row lays a value object's attributes across a table's width; a
655
861
  /// cents+currency pair collapses into one "money" column, as at top level.
656
862
  fn row_columns(shape: &Value, aggregate: &Value, key_for: &KeyFor, formats: &Value) -> Value {
657
863
  let attributes = array(shape, "attributes");
658
- let names: Vec<&str> = attributes.iter().map(|a| attr_name(a)).collect();
864
+ let names: Vec<&str> = attributes.iter().map(attr_name).collect();
659
865
  if !(names.contains(&"cents") && names.contains(&"currency")) {
660
- return Value::Array(attributes.iter().map(|a| field(a, aggregate, key_for, formats)).collect());
866
+ return Value::Array(
867
+ attributes
868
+ .iter()
869
+ .map(|a| field(a, aggregate, key_for, formats))
870
+ .collect(),
871
+ );
661
872
  }
662
873
  Value::Array(
663
874
  attributes
@@ -709,7 +920,10 @@ pub(crate) fn type_of(attribute: &Value) -> &str {
709
920
  }
710
921
 
711
922
  pub(crate) fn array<'a>(value: &'a Value, field: &str) -> &'a [Value] {
712
- value.get(field).and_then(|v| v.as_array()).map(|a| a.as_slice()).unwrap_or(&[])
923
+ value
924
+ .get(field)
925
+ .and_then(|v| v.as_array())
926
+ .map_or(&[][..], std::vec::Vec::as_slice)
713
927
  }
714
928
 
715
929
  pub fn commands(aggregate: &Value) -> Vec<&Value> {
@@ -719,7 +933,9 @@ pub fn commands(aggregate: &Value) -> Vec<&Value> {
719
933
  /// `IR::Command#creates?` — true exactly when the command declares no
720
934
  /// `references`.
721
935
  pub fn creates(command: &Value) -> bool {
722
- command.get("references").map(|r| r.is_null()).unwrap_or(true)
936
+ command
937
+ .get("references")
938
+ .is_none_or(serde_json::Value::is_null)
723
939
  }
724
940
 
725
941
  fn lifecycle(aggregate: &Value) -> Option<&Value> {
@@ -727,25 +943,35 @@ fn lifecycle(aggregate: &Value) -> Option<&Value> {
727
943
  }
728
944
 
729
945
  pub fn find_attribute<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
730
- array(aggregate, "attributes").iter().find(|a| attr_name(a) == name)
946
+ array(aggregate, "attributes")
947
+ .iter()
948
+ .find(|a| attr_name(a) == name)
731
949
  }
732
950
 
733
951
  pub(crate) fn find_value_object<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
734
- array(aggregate, "value_objects").iter().find(|v| v.get("name").and_then(|n| n.as_str()) == Some(name))
952
+ array(aggregate, "value_objects")
953
+ .iter()
954
+ .find(|v| v.get("name").and_then(|n| n.as_str()) == Some(name))
735
955
  }
736
956
 
737
957
  pub(crate) fn find_entity<'a>(aggregate: &'a Value, name: &str) -> Option<&'a Value> {
738
- array(aggregate, "entities").iter().find(|e| e.get("name").and_then(|n| n.as_str()) == Some(name))
958
+ array(aggregate, "entities")
959
+ .iter()
960
+ .find(|e| e.get("name").and_then(|n| n.as_str()) == Some(name))
739
961
  }
740
962
 
741
963
  fn find_field<'a>(fields: &'a [Value], key: &str) -> Option<&'a Value> {
742
- fields.iter().find(|f| f.get("key").and_then(|v| v.as_str()) == Some(key))
964
+ fields
965
+ .iter()
966
+ .find(|f| f.get("key").and_then(|v| v.as_str()) == Some(key))
743
967
  }
744
968
 
745
969
  /// `Reference<X>` -> `X` — the export's pinned spelling, the same
746
970
  /// prefix convention `web.rs`'s own `reference_target` reads.
747
971
  pub fn reference_target(ty: &str) -> Option<String> {
748
- ty.strip_prefix("Reference<")?.strip_suffix('>').map(String::from)
972
+ ty.strip_prefix("Reference<")?
973
+ .strip_suffix('>')
974
+ .map(String::from)
749
975
  }
750
976
 
751
977
  pub(crate) fn is_reference(attribute: &Value) -> bool {
@@ -753,7 +979,10 @@ pub(crate) fn is_reference(attribute: &Value) -> bool {
753
979
  }
754
980
 
755
981
  pub(crate) fn is_list(attribute: &Value) -> bool {
756
- attribute.get("list").and_then(|v| v.as_bool()).unwrap_or(false)
982
+ attribute
983
+ .get("list")
984
+ .and_then(serde_json::Value::as_bool)
985
+ .unwrap_or(false)
757
986
  }
758
987
 
759
988
  /// `identity_heads` — the first segment of each declared identity
@@ -769,7 +998,10 @@ fn identity_heads(aggregate: &Value) -> Vec<&str> {
769
998
  /// `Presentation::ValueObjectShape.money?` — exactly `{cents,
770
999
  /// currency}`, sorted, and nothing else.
771
1000
  fn money_shaped(value_object: &Value) -> bool {
772
- let mut names: Vec<&str> = array(value_object, "attributes").iter().map(|a| attr_name(a)).collect();
1001
+ let mut names: Vec<&str> = array(value_object, "attributes")
1002
+ .iter()
1003
+ .map(attr_name)
1004
+ .collect();
773
1005
  names.sort_unstable();
774
1006
  names == ["cents", "currency"]
775
1007
  }
@@ -795,7 +1027,7 @@ fn dig<'a>(config: &'a Value, path: &[&str]) -> Option<&'a Value> {
795
1027
  (!node.is_null()).then_some(node)
796
1028
  }
797
1029
 
798
- fn truthy(value: Option<&Value>) -> bool {
1030
+ const fn truthy(value: Option<&Value>) -> bool {
799
1031
  match value {
800
1032
  None | Some(Value::Null) => false,
801
1033
  Some(Value::Bool(b)) => *b,
@@ -808,7 +1040,10 @@ fn optional(config: &Value, key: &str) -> Value {
808
1040
  }
809
1041
 
810
1042
  fn string_or(config: &Value, key: &str, fallback: &str) -> String {
811
- config.get(key).and_then(|v| v.as_str()).map(String::from).unwrap_or_else(|| fallback.to_string())
1043
+ config
1044
+ .get(key)
1045
+ .and_then(|v| v.as_str())
1046
+ .map_or_else(|| fallback.to_string(), String::from)
812
1047
  }
813
1048
 
814
1049
  /// Ruby's `Hash#merge` semantics: an existing key keeps its position,
@@ -826,33 +1061,6 @@ fn merge(base: Value, extra: Value) -> Value {
826
1061
  Value::Object(out)
827
1062
  }
828
1063
 
829
- static SNAKE_ACRONYM: LazyLock<Regex> =
830
- LazyLock::new(|| Regex::new(r"([A-Z]+)([A-Z][a-z])").expect("a literal pattern must compile"));
831
- static SNAKE_BOUNDARY: LazyLock<Regex> =
832
- LazyLock::new(|| Regex::new(r"([a-z\d])([A-Z])").expect("a literal pattern must compile"));
833
- static PLURAL_Y: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^aeiou]y$").expect("a literal pattern must compile"));
834
- static PLURAL_ES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(s|x|z|ch|sh)$").expect("a literal pattern must compile"));
835
-
836
- /// `Naming.snake` — the same two word-boundary substitutions, then
837
- /// downcase.
838
- pub fn snake(text: &str) -> String {
839
- let once = SNAKE_ACRONYM.replace_all(text, "${1}_${2}");
840
- let twice = SNAKE_BOUNDARY.replace_all(&once, "${1}_${2}");
841
- twice.to_lowercase()
842
- }
843
-
844
- /// `Naming.plural` — three suffix rules, exactly as declared; getting
845
- /// a collection name wrong reads as a typo forever.
846
- pub fn plural(word: &str) -> String {
847
- if PLURAL_Y.is_match(word) {
848
- return format!("{}ies", &word[..word.len() - 1]);
849
- }
850
- if PLURAL_ES.is_match(word) {
851
- return format!("{word}es");
852
- }
853
- format!("{word}s")
854
- }
855
-
856
1064
  /// `UiSchema.humanize_words` — snake_case, then each word capitalized.
857
1065
  /// Not `Naming.words`: "ATMCard" reads "Atm Card" here, as in Ruby.
858
1066
  fn humanize_words(text: &str) -> String {
@@ -868,10 +1076,9 @@ fn humanize_words(text: &str) -> String {
868
1076
  .into_iter()
869
1077
  .map(|word| {
870
1078
  let mut chars = word.chars();
871
- match chars.next() {
872
- Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
873
- None => String::new(),
874
- }
1079
+ chars.next().map_or_else(String::new, |first| {
1080
+ first.to_uppercase().collect::<String>() + chars.as_str()
1081
+ })
875
1082
  })
876
1083
  .collect::<Vec<String>>()
877
1084
  .join(" ")
@@ -999,7 +1206,9 @@ mod tests {
999
1206
  }
1000
1207
 
1001
1208
  fn field_named(aggregate: &Value, name: &str) -> Value {
1002
- let attribute = find_attribute(aggregate, name).expect("a declared attribute").clone();
1209
+ let attribute = find_attribute(aggregate, name)
1210
+ .expect("a declared attribute")
1211
+ .clone();
1003
1212
  field(&attribute, aggregate, &keys(), &Value::Null)
1004
1213
  }
1005
1214
 
@@ -1008,7 +1217,10 @@ mod tests {
1008
1217
  assert_eq!(collection_key(&client(), &json!({})), "clients");
1009
1218
  assert_eq!(collection_key(&proposal(), &json!({})), "proposals");
1010
1219
  assert_eq!(
1011
- collection_key(&proposal(), &json!({"collections": {"Proposal": {"key": "pipeline"}}})),
1220
+ collection_key(
1221
+ &proposal(),
1222
+ &json!({"collections": {"Proposal": {"key": "pipeline"}}})
1223
+ ),
1012
1224
  "pipeline"
1013
1225
  );
1014
1226
  }
@@ -1068,7 +1280,10 @@ mod tests {
1068
1280
  assert_eq!(field_named(&client(), "contact_email")["shape"], "address");
1069
1281
  assert_eq!(field_named(&client(), "contact_email")["field"], "address");
1070
1282
  // `notes` is a list of a sole-attribute VO — lines, not rows.
1071
- assert_eq!(field_named(&client(), "notes"), json!({"key": "notes", "label": "Notes", "optional": true, "shape": "lines", "field": "text"}));
1283
+ assert_eq!(
1284
+ field_named(&client(), "notes"),
1285
+ json!({"key": "notes", "label": "Notes", "optional": true, "shape": "lines", "field": "text"})
1286
+ );
1072
1287
  }
1073
1288
 
1074
1289
  #[test]
@@ -1104,7 +1319,9 @@ mod tests {
1104
1319
  #[test]
1105
1320
  fn a_field_format_rides_on_top_of_the_derived_shape_rather_than_replacing_it() {
1106
1321
  let aggregate = client();
1107
- let attribute = find_attribute(&aggregate, "name").expect("declared").clone();
1322
+ let attribute = find_attribute(&aggregate, "name")
1323
+ .expect("declared")
1324
+ .clone();
1108
1325
  let shaped = field(&attribute, &aggregate, &keys(), &json!({"name": "date"}));
1109
1326
 
1110
1327
  assert_eq!(shaped["shape"], "text_value");
@@ -1123,7 +1340,10 @@ mod tests {
1123
1340
  "entities": [], "commands": [], "queries": [], "lifecycle": null
1124
1341
  });
1125
1342
 
1126
- assert_eq!(field_named(&aggregate, "id"), json!({"key": "id", "label": "Id", "optional": false, "shape": "text_value"}));
1343
+ assert_eq!(
1344
+ field_named(&aggregate, "id"),
1345
+ json!({"key": "id", "label": "Id", "optional": false, "shape": "text_value"})
1346
+ );
1127
1347
  // …while an aggregate identified by any other attribute keeps
1128
1348
  // its real wrapped shape, `field: "value"` and all.
1129
1349
  assert_eq!(field_named(&client(), "reference")["field"], "value");
@@ -1138,10 +1358,23 @@ mod tests {
1138
1358
  // column list; `__state__` is appended because Client has a
1139
1359
  // lifecycle.
1140
1360
  assert_eq!(
1141
- columns.as_array().expect("columns").iter().map(|c| c["key"].clone()).collect::<Vec<Value>>(),
1142
- vec![json!("reference"), json!("name"), json!("contact_email"), json!("__state__")]
1361
+ columns
1362
+ .as_array()
1363
+ .expect("columns")
1364
+ .iter()
1365
+ .map(|c| c["key"].clone())
1366
+ .collect::<Vec<Value>>(),
1367
+ vec![
1368
+ json!("reference"),
1369
+ json!("name"),
1370
+ json!("contact_email"),
1371
+ json!("__state__")
1372
+ ]
1373
+ );
1374
+ assert_eq!(
1375
+ columns[3],
1376
+ json!({"key": "__state__", "label": "Status", "shape": "pill", "sortable": true, "sortDefault": "asc"})
1143
1377
  );
1144
- assert_eq!(columns[3], json!({"key": "__state__", "label": "Status", "shape": "pill", "sortable": true, "sortDefault": "asc"}));
1145
1378
  assert_eq!(schema["collections"]["clients"]["stateField"], "status");
1146
1379
  }
1147
1380
 
@@ -1153,7 +1386,11 @@ mod tests {
1153
1386
  assert_eq!(invoices["stateField"], Value::Null);
1154
1387
  assert_eq!(invoices["transitions"], json!({}));
1155
1388
  assert!(
1156
- !invoices["columns"].as_array().expect("columns").iter().any(|c| c["key"] == "__state__"),
1389
+ !invoices["columns"]
1390
+ .as_array()
1391
+ .expect("columns")
1392
+ .iter()
1393
+ .any(|c| c["key"] == "__state__"),
1157
1394
  "{invoices}"
1158
1395
  );
1159
1396
  }
@@ -1201,7 +1438,12 @@ mod tests {
1201
1438
  .as_array()
1202
1439
  .expect("detail fields")
1203
1440
  .iter()
1204
- .map(|f| (f["key"].as_str().unwrap_or("").to_string(), f["display"].clone()))
1441
+ .map(|f| {
1442
+ (
1443
+ f["key"].as_str().unwrap_or("").to_string(),
1444
+ f["display"].clone(),
1445
+ )
1446
+ })
1205
1447
  .collect();
1206
1448
 
1207
1449
  assert_eq!(detail["line_items"], "rows");
@@ -1209,7 +1451,10 @@ mod tests {
1209
1451
 
1210
1452
  let config = json!({"collections": {"Proposal": {"detail_fields": [{"field": "number", "display": "mono"}]}}});
1211
1453
  let configured = build(&domain(), &config);
1212
- assert_eq!(configured["collections"]["proposals"]["detailFields"][0]["display"], "mono");
1454
+ assert_eq!(
1455
+ configured["collections"]["proposals"]["detailFields"][0]["display"],
1456
+ "mono"
1457
+ );
1213
1458
  }
1214
1459
 
1215
1460
  #[test]
@@ -1217,8 +1462,14 @@ mod tests {
1217
1462
  let schema = build(&domain(), &json!({}));
1218
1463
  let transitions = &schema["collections"]["proposals"]["transitions"];
1219
1464
 
1220
- assert_eq!(transitions["drafted"], json!([{"command": "send", "to": "sent", "label": "Send", "inputs": []}]));
1221
- assert_eq!(transitions["sent"], json!([{"command": "accept", "to": "accepted", "label": "Accept", "inputs": []}]));
1465
+ assert_eq!(
1466
+ transitions["drafted"],
1467
+ json!([{"command": "send", "to": "sent", "label": "Send", "inputs": []}])
1468
+ );
1469
+ assert_eq!(
1470
+ transitions["sent"],
1471
+ json!([{"command": "accept", "to": "accepted", "label": "Accept", "inputs": []}])
1472
+ );
1222
1473
  assert_eq!(transitions["accepted"], json!([]));
1223
1474
  }
1224
1475
 
@@ -1246,7 +1497,10 @@ mod tests {
1246
1497
  let schema = build(&domain, &json!({}));
1247
1498
 
1248
1499
  assert_eq!(schema["collections"]["ledgers"]["transitions"], json!({}));
1249
- assert_eq!(schema["collections"]["ledgers"]["alwaysAvailable"][0]["command"], "post");
1500
+ assert_eq!(
1501
+ schema["collections"]["ledgers"]["alwaysAvailable"][0]["command"],
1502
+ "post"
1503
+ );
1250
1504
  }
1251
1505
 
1252
1506
  #[test]
@@ -1261,7 +1515,12 @@ mod tests {
1261
1515
  assert_eq!(create["command"], "register");
1262
1516
  assert_eq!(create["label"], "Add a client");
1263
1517
  assert_eq!(
1264
- create["fields"].as_array().expect("fields").iter().map(|f| f["key"].clone()).collect::<Vec<Value>>(),
1518
+ create["fields"]
1519
+ .as_array()
1520
+ .expect("fields")
1521
+ .iter()
1522
+ .map(|f| f["key"].clone())
1523
+ .collect::<Vec<Value>>(),
1265
1524
  vec![json!("name")]
1266
1525
  );
1267
1526
  }
@@ -1289,14 +1548,25 @@ mod tests {
1289
1548
  fn nav_keeps_declaration_order_until_nav_order_says_otherwise() {
1290
1549
  let schema = build(&domain(), &json!({}));
1291
1550
  assert_eq!(
1292
- schema["nav"].as_array().expect("nav").iter().map(|g| g["key"].clone()).collect::<Vec<Value>>(),
1551
+ schema["nav"]
1552
+ .as_array()
1553
+ .expect("nav")
1554
+ .iter()
1555
+ .map(|g| g["key"].clone())
1556
+ .collect::<Vec<Value>>(),
1293
1557
  vec![json!("clients"), json!("proposals"), json!("invoices")]
1294
1558
  );
1295
1559
 
1296
- let config = json!({"collections": {"Invoice": {"nav_order": 1}, "Client": {"nav_order": 2}}});
1560
+ let config =
1561
+ json!({"collections": {"Invoice": {"nav_order": 1}, "Client": {"nav_order": 2}}});
1297
1562
  let ordered = build(&domain(), &config);
1298
1563
  assert_eq!(
1299
- ordered["nav"].as_array().expect("nav").iter().map(|g| g["key"].clone()).collect::<Vec<Value>>(),
1564
+ ordered["nav"]
1565
+ .as_array()
1566
+ .expect("nav")
1567
+ .iter()
1568
+ .map(|g| g["key"].clone())
1569
+ .collect::<Vec<Value>>(),
1300
1570
  vec![json!("invoices"), json!("clients"), json!("proposals")]
1301
1571
  );
1302
1572
  }
@@ -1311,7 +1581,10 @@ mod tests {
1311
1581
  let nav = schema["nav"].as_array().expect("nav");
1312
1582
 
1313
1583
  assert_eq!(nav.len(), 2);
1314
- assert_eq!(nav[0], json!({"key": "paperwork", "label": "Paperwork", "collections": ["proposals", "invoices"]}));
1584
+ assert_eq!(
1585
+ nav[0],
1586
+ json!({"key": "paperwork", "label": "Paperwork", "collections": ["proposals", "invoices"]})
1587
+ );
1315
1588
  assert_eq!(nav[1]["key"], "clients");
1316
1589
  }
1317
1590
 
@@ -1353,7 +1626,10 @@ mod tests {
1353
1626
  let clients = &schema["collections"]["clients"];
1354
1627
 
1355
1628
  assert_eq!(clients["statusLabels"], json!({"prospect": "Prospect"}));
1356
- assert_eq!(clients["statusTone"], json!({"prospect": "warn", "active": "good"}));
1629
+ assert_eq!(
1630
+ clients["statusTone"],
1631
+ json!({"prospect": "warn", "active": "good"})
1632
+ );
1357
1633
  assert_eq!(clients["statusNotes"], json!({"active": "paying"}));
1358
1634
  assert_eq!(clients["statusAttention"], json!(["active"]));
1359
1635
  }
@@ -1377,7 +1653,7 @@ mod tests {
1377
1653
 
1378
1654
  // Two aggregates: `Site` declares the value objects, `Deployment` types
1379
1655
  // attributes by them without declaring any of its own.
1380
- fn shared_domain(extra_deployment_vos: Value) -> Value {
1656
+ fn shared_domain(extra_deployment_vos: &Value) -> Value {
1381
1657
  let vo = |name: &str, attrs: Value| json!({"name": name, "attributes": attrs, "closed_set": false, "members": []});
1382
1658
  json!({
1383
1659
  "name": "Fleet",
@@ -1419,7 +1695,7 @@ mod tests {
1419
1695
 
1420
1696
  #[test]
1421
1697
  fn a_one_attribute_value_object_another_aggregate_declares_keeps_its_wrapped_field() {
1422
- let schema = build(&shared_domain(json!([])), &json!({}));
1698
+ let schema = build(&shared_domain(&json!([])), &json!({}));
1423
1699
  assert_eq!(
1424
1700
  deployment_field(&schema, "site_key"),
1425
1701
  json!({"key": "site_key", "label": "Site Key", "optional": false, "shape": "text_value", "field": "value"})
@@ -1430,10 +1706,16 @@ mod tests {
1430
1706
  }
1431
1707
 
1432
1708
  #[test]
1433
- fn compound_money_and_enum_value_objects_resolve_the_same_when_another_aggregate_declares_them() {
1434
- let schema = build(&shared_domain(json!([])), &json!({}));
1709
+ fn compound_money_and_enum_value_objects_resolve_the_same_when_another_aggregate_declares_them()
1710
+ {
1711
+ let schema = build(&shared_domain(&json!([])), &json!({}));
1435
1712
  assert_eq!(deployment_field(&schema, "window")["shape"], "compound");
1436
- assert_eq!(deployment_field(&schema, "window")["fields"].as_array().map(Vec::len), Some(2));
1713
+ assert_eq!(
1714
+ deployment_field(&schema, "window")["fields"]
1715
+ .as_array()
1716
+ .map(Vec::len),
1717
+ Some(2)
1718
+ );
1437
1719
  assert_eq!(deployment_field(&schema, "price")["shape"], "money");
1438
1720
  let tier = deployment_field(&schema, "tier");
1439
1721
  assert_eq!(tier["shape"], "enum");
@@ -1453,11 +1735,11 @@ mod tests {
1453
1735
  fn a_name_two_aggregates_declare_resolves_to_the_owner_then_the_first_declared() {
1454
1736
  // The owner's own `SiteKey` (attribute `text`) shadows Site's.
1455
1737
  let own = json!([{"name": "SiteKey", "attributes": [{"name": "text", "type": "String"}], "closed_set": false, "members": []}]);
1456
- let schema = build(&shared_domain(own), &json!({}));
1738
+ let schema = build(&shared_domain(&own), &json!({}));
1457
1739
  assert_eq!(deployment_field(&schema, "site_key")["field"], "text");
1458
1740
 
1459
1741
  // Two other aggregates declaring one name: the earlier one wins.
1460
- let mut domain = shared_domain(json!([]));
1742
+ let mut domain = shared_domain(&json!([]));
1461
1743
  domain["aggregates"].as_array_mut().unwrap().push(json!({
1462
1744
  "name": "Probe", "identified_by": ["id"],
1463
1745
  "attributes": [{"name": "site_key", "type": "SiteKey", "list": false, "optional": false}],
@@ -1471,8 +1753,14 @@ mod tests {
1471
1753
  }));
1472
1754
  let schema = build(&domain, &json!({}));
1473
1755
  // Site precedes Probe in declaration order, so Other reads Site's.
1474
- assert_eq!(schema["collections"]["others"]["fields"][0]["field"], "value");
1756
+ assert_eq!(
1757
+ schema["collections"]["others"]["fields"][0]["field"],
1758
+ "value"
1759
+ );
1475
1760
  // Probe's own declaration still wins for Probe.
1476
- assert_eq!(schema["collections"]["probes"]["fields"][0]["field"], "text");
1761
+ assert_eq!(
1762
+ schema["collections"]["probes"]["fields"][0]["field"],
1763
+ "text"
1764
+ );
1477
1765
  }
1478
1766
  }