hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -19,7 +19,9 @@ pub fn reference_type(type_name: &str) -> bool {
19
19
  /// `X` out of `Reference<X>` — `None` for anything that isn't a reference
20
20
  /// type at all.
21
21
  pub fn reference_target(type_name: &str) -> Option<&str> {
22
- type_name.strip_prefix("Reference<").and_then(|rest| rest.strip_suffix('>'))
22
+ type_name
23
+ .strip_prefix("Reference<")
24
+ .and_then(|rest| rest.strip_suffix('>'))
23
25
  }
24
26
 
25
27
  pub fn effective_scalar_type(type_name: &str) -> Option<&'static str> {
@@ -36,12 +38,16 @@ pub fn effective_scalar_type(type_name: &str) -> Option<&'static str> {
36
38
  }
37
39
  }
38
40
 
41
+ #[allow(
42
+ clippy::unwrap_used,
43
+ reason = "the value was checked just before; None would mean invalid IR"
44
+ )]
39
45
  pub fn rust_type(type_name: &str, list: bool) -> String {
40
46
  let scalar = effective_scalar_type(type_name);
41
- let inner = match scalar {
42
- Some(s) => scalar_rust_type(s).unwrap().to_string(),
43
- None => rust_ident(type_name),
44
- };
47
+ let inner = scalar.map_or_else(
48
+ || rust_ident(type_name),
49
+ |s| scalar_rust_type(s).unwrap().to_string(),
50
+ );
45
51
  if list {
46
52
  format!("Vec<{inner}>")
47
53
  } else {
@@ -50,7 +56,7 @@ pub fn rust_type(type_name: &str, list: bool) -> String {
50
56
  }
51
57
 
52
58
  pub fn rust_ident(name: &str) -> String {
53
- name.chars().filter(|c| c.is_ascii_alphanumeric()).collect()
59
+ name.chars().filter(char::is_ascii_alphanumeric).collect()
54
60
  }
55
61
 
56
62
  /// `cmd.gsub(/(?<=.)([A-Z])/, '_\1').downcase` — insert `_` before every
@@ -103,12 +109,18 @@ pub fn legal_aggregate_mod_identifier(name: &str) -> bool {
103
109
  /// Mirrors `valid_domain_mod_name?` — shape, and neither a Rust keyword nor
104
110
  /// a reserved Cargo.toml key.
105
111
  pub fn valid_domain_mod_name(name: &str) -> bool {
106
- plain_lower_identifier(name) && !RUST_KEYWORDS.contains(&name) && !CARGO_RESERVED_DOMAIN_NAMES.contains(&name)
112
+ plain_lower_identifier(name)
113
+ && !RUST_KEYWORDS.contains(&name)
114
+ && !CARGO_RESERVED_DOMAIN_NAMES.contains(&name)
107
115
  }
108
116
 
109
117
  /// `None` when every aggregate name and the domain's module name are usable, else the
110
118
  /// refusal text the Ruby generator raises, byte for byte. Aggregates are checked first.
111
- pub fn reserved_name_refusal(source_label: &str, mod_name: &str, aggregate_names: &[&str]) -> Option<String> {
119
+ pub fn reserved_name_refusal(
120
+ source_label: &str,
121
+ mod_name: &str,
122
+ aggregate_names: &[&str],
123
+ ) -> Option<String> {
112
124
  let refused: Vec<&str> = aggregate_names
113
125
  .iter()
114
126
  .copied()
@@ -118,7 +130,11 @@ pub fn reserved_name_refusal(source_label: &str, mod_name: &str, aggregate_names
118
130
  })
119
131
  .collect();
120
132
  if let Some(first) = refused.first() {
121
- let names = refused.iter().map(|name| format!("{name:?}")).collect::<Vec<_>>().join(", ");
133
+ let names = refused
134
+ .iter()
135
+ .map(|name| format!("{name:?}"))
136
+ .collect::<Vec<_>>()
137
+ .join(", ");
122
138
  return Some(format!(
123
139
  "{source_label}: aggregate name(s) {names} can't be used as-is — downcased, each becomes a bare Rust \
124
140
  module identifier (`pub mod {};`) and a generated file name, and at least one is not a plain identifier \
@@ -180,7 +196,11 @@ pub fn unsafe_name_refusal(source_label: &str, ir: &Json) -> Option<String> {
180
196
  if refused.is_empty() {
181
197
  return None;
182
198
  }
183
- let list = refused.iter().map(|name| ruby_inspect_string(name)).collect::<Vec<_>>().join(", ");
199
+ let list = refused
200
+ .iter()
201
+ .map(|name| ruby_inspect_string(name))
202
+ .collect::<Vec<_>>()
203
+ .join(", ");
184
204
  Some(format!(
185
205
  "{source_label}: declared name(s) {list} can't be used as-is — each is written into the generated Rust as \
186
206
  an identifier, and at least one is not a plain identifier (letters, digits and underscores, not starting \
@@ -194,7 +214,10 @@ pub fn rust_field(name: &str) -> String {
194
214
 
195
215
  pub fn rust_ident_field(name: &str) -> String {
196
216
  let field = rust_field(name);
197
- if RUST_KEYWORDS.contains(&field.as_str()) {
217
+ if matches!(field.as_str(), "crate" | "self" | "Self" | "super") {
218
+ // Rust refuses these as raw identifiers, so they take a trailing underscore.
219
+ format!("{field}_")
220
+ } else if RUST_KEYWORDS.contains(&field.as_str()) {
198
221
  format!("r#{field}")
199
222
  } else {
200
223
  field
@@ -208,45 +231,22 @@ pub fn closed_set_variant(value: &str) -> String {
208
231
  .split(|c: char| !c.is_ascii_alphanumeric())
209
232
  .filter(|s| !s.is_empty())
210
233
  .map(capitalize)
211
- .collect::<Vec<_>>()
212
- .join("");
234
+ .collect::<String>();
213
235
  if variant != "Self" {
214
236
  return variant;
215
237
  }
216
- if value.starts_with('S') { "SelfType".to_string() } else { "SelfValue".to_string() }
238
+ if value.starts_with('S') {
239
+ "SelfType".to_string()
240
+ } else {
241
+ "SelfValue".to_string()
242
+ }
217
243
  }
218
244
 
219
245
  fn capitalize(word: &str) -> String {
220
246
  let mut chars = word.chars();
221
- match chars.next() {
222
- None => String::new(),
223
- Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
224
- }
225
- }
226
-
227
- /// Port of `Hecks::Naming.snake` —
228
- /// `gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase`.
229
- pub fn snake(text: &str) -> String {
230
- let chars: Vec<char> = text.chars().collect();
231
- let mut first = String::new();
232
- for (i, &c) in chars.iter().enumerate() {
233
- let next_upper = chars.get(i + 1).is_some_and(|n| n.is_ascii_uppercase());
234
- let then_lower = chars.get(i + 2).is_some_and(|n| n.is_ascii_lowercase());
235
- first.push(c);
236
- if c.is_ascii_uppercase() && next_upper && then_lower {
237
- first.push('_');
238
- }
239
- }
240
- let chars: Vec<char> = first.chars().collect();
241
- let mut second = String::new();
242
- for (i, &c) in chars.iter().enumerate() {
243
- second.push(c);
244
- let next_upper = chars.get(i + 1).is_some_and(|n| n.is_ascii_uppercase());
245
- if (c.is_ascii_lowercase() || c.is_ascii_digit()) && next_upper {
246
- second.push('_');
247
- }
248
- }
249
- second.to_lowercase()
247
+ chars.next().map_or_else(String::new, |first| {
248
+ first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
249
+ })
250
250
  }
251
251
 
252
252
  /// `name.to_s.gsub(/([a-z0-9])([A-Z])/, '\1_\2').upcase`
@@ -289,18 +289,36 @@ pub fn scalar_to_value(type_name: &str, rust_expr: &str) -> Option<String> {
289
289
  /// single-field closed-set enums implement `Fielded`; multi-field closed-set
290
290
  /// tables do not.
291
291
  pub fn fielded_capable_nested(vo: &crate::json::Json) -> bool {
292
- !vo.get("closed_set").map(crate::json::Json::as_bool).unwrap_or(false)
293
- || vo.get("attributes").map(crate::json::Json::each).unwrap_or(&[]).len() == 1
292
+ !vo.get("closed_set").is_some_and(crate::json::Json::as_bool)
293
+ || vo
294
+ .get("attributes")
295
+ .map_or_default(crate::json::Json::each)
296
+ .len()
297
+ == 1
294
298
  }
295
299
 
296
300
  /// The `Fielded` impl emitted beside a single-field closed-set enum, exposing `"value"`.
297
301
  pub fn emit_closed_set_fielded_impl(vo: &crate::json::Json) -> String {
298
- let name = rust_ident(vo.get("name").and_then(crate::json::Json::as_str).unwrap_or(""));
299
- let arms = vo.get("members").map(crate::json::Json::each).unwrap_or(&[]).iter().filter_map(|row| {
300
- let pair = row.as_array()?.first()?.as_array()?;
301
- let raw = pair.get(1)?.to_s();
302
- Some(format!("{name}::{} => {}.to_string(),", closed_set_variant(&raw), ruby_inspect_string(&raw)))
303
- }).collect::<Vec<_>>().join(" ");
302
+ let name = rust_ident(
303
+ vo.get("name")
304
+ .and_then(crate::json::Json::as_str)
305
+ .unwrap_or(""),
306
+ );
307
+ let arms = vo
308
+ .get("members")
309
+ .map_or_default(crate::json::Json::each)
310
+ .iter()
311
+ .filter_map(|row| {
312
+ let pair = row.as_array()?.first()?.as_array()?;
313
+ let raw = pair.get(1)?.to_s();
314
+ Some(format!(
315
+ "Self::{} => {}.to_string(),",
316
+ closed_set_variant(&raw),
317
+ ruby_inspect_string(&raw)
318
+ ))
319
+ })
320
+ .collect::<Vec<_>>()
321
+ .join(" ");
304
322
 
305
323
  format!(
306
324
  "impl crate::kernel::Fielded for {name} {{\n fn field(&self, name: &str) -> Option<crate::kernel::Field<'_>> {{\n use crate::kernel::{{Field, Value}};\n match name {{\n \"value\" => Some(Field::Value(Value::Str(match self {{ {arms} }}))),\n _ => None,\n }}\n }}\n fn as_scalar(&self) -> Option<crate::kernel::Value> {{\n match self.field(\"value\") {{ Some(crate::kernel::Field::Value(v)) => Some(v), _ => None }}\n }}\n}}"
@@ -308,6 +326,10 @@ pub fn emit_closed_set_fielded_impl(vo: &crate::json::Json) -> String {
308
326
  }
309
327
 
310
328
  /// A literal mutation-source RHS; `literal` is a String/Integer/Float/Bool read from `ir.json`.
329
+ #[allow(
330
+ clippy::panic,
331
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
332
+ )]
311
333
  pub fn literal_rhs(literal: &crate::json::Json) -> String {
312
334
  match literal {
313
335
  crate::json::Json::String(s) => format!("{}.to_string()", ruby_inspect_string(s)),
@@ -346,11 +368,23 @@ mod tests {
346
368
  #[test]
347
369
  fn a_scalar_reached_through_a_reference_is_dereferenced_when_it_is_a_copy_type() {
348
370
  // `self.level.as_ref().map(|v| Field::Value(Value::Int(v)))` does not compile: `v` is `&i64`.
349
- assert_eq!(scalar_to_value("Integer", &deref_scalar("Integer", "v")).as_deref(), Some("Value::Int(*v)"));
350
- assert_eq!(scalar_to_value("Float", &deref_scalar("Float", "v")).as_deref(), Some("Value::Float(*v)"));
351
- assert_eq!(scalar_to_value("TrueClass", &deref_scalar("TrueClass", "v")).as_deref(), Some("Value::Bool(*v)"));
371
+ assert_eq!(
372
+ scalar_to_value("Integer", &deref_scalar("Integer", "v")).as_deref(),
373
+ Some("Value::Int(*v)")
374
+ );
375
+ assert_eq!(
376
+ scalar_to_value("Float", &deref_scalar("Float", "v")).as_deref(),
377
+ Some("Value::Float(*v)")
378
+ );
379
+ assert_eq!(
380
+ scalar_to_value("TrueClass", &deref_scalar("TrueClass", "v")).as_deref(),
381
+ Some("Value::Bool(*v)")
382
+ );
352
383
  // A String is cloned, so the reference is left alone.
353
- assert_eq!(scalar_to_value("String", &deref_scalar("String", "v")).as_deref(), Some("Value::Str(v.clone())"));
384
+ assert_eq!(
385
+ scalar_to_value("String", &deref_scalar("String", "v")).as_deref(),
386
+ Some("Value::Str(v.clone())")
387
+ );
354
388
  }
355
389
 
356
390
  #[test]
@@ -368,8 +402,12 @@ mod tests {
368
402
 
369
403
  #[test]
370
404
  fn refuses_an_aggregate_whose_module_name_is_a_rust_keyword() {
371
- let refusal = reserved_name_refusal("shop", "shop", &["Pizza", "Match", "Type"]).expect("refused");
372
- assert!(refusal.starts_with("shop: aggregate name(s) \"Match\", \"Type\" can't be used as-is"), "{refusal}");
405
+ let refusal =
406
+ reserved_name_refusal("shop", "shop", &["Pizza", "Match", "Type"]).expect("refused");
407
+ assert!(
408
+ refusal.starts_with("shop: aggregate name(s) \"Match\", \"Type\" can't be used as-is"),
409
+ "{refusal}"
410
+ );
373
411
  assert!(refusal.contains("`pub mod match;`"), "{refusal}");
374
412
  assert!(refusal.contains("Rust keyword"), "{refusal}");
375
413
  }
@@ -381,7 +419,12 @@ mod tests {
381
419
  )
382
420
  .unwrap();
383
421
  let refusal = unsafe_name_refusal("shop", &ir).expect("refused");
384
- assert!(refusal.starts_with("shop: declared name(s) \"a: String, pub evil: u8\" can't be used as-is"), "{refusal}");
422
+ assert!(
423
+ refusal.starts_with(
424
+ "shop: declared name(s) \"a: String, pub evil: u8\" can't be used as-is"
425
+ ),
426
+ "{refusal}"
427
+ );
385
428
  }
386
429
 
387
430
  #[test]
@@ -412,12 +455,20 @@ mod tests {
412
455
  fn refuses_a_domain_module_name_that_is_a_keyword_or_a_cargo_key() {
413
456
  for name in ["crate", "type", "package", "default", "version"] {
414
457
  let refusal = reserved_name_refusal("label", name, &["Widget"]).expect("refused");
415
- assert!(refusal.starts_with(&format!("label: domain module name {name:?} can't be used as-is")), "{refusal}");
458
+ assert!(
459
+ refusal.starts_with(&format!(
460
+ "label: domain module name {name:?} can't be used as-is"
461
+ )),
462
+ "{refusal}"
463
+ );
416
464
  }
417
465
  }
418
466
 
419
467
  #[test]
420
468
  fn accepts_ordinary_names_and_cargo_keys_as_aggregate_names() {
421
- assert_eq!(reserved_name_refusal("shop", "shop", &["Pizza", "Version", "Default"]), None);
469
+ assert_eq!(
470
+ reserved_name_refusal("shop", "shop", &["Pizza", "Version", "Default"]),
471
+ None
472
+ );
422
473
  }
423
474
  }
@@ -27,7 +27,7 @@ pub fn run(ir: &mut Json) {
27
27
  }
28
28
 
29
29
  fn list<'a>(node: &'a Json, key: &str) -> &'a [Json] {
30
- node.get(key).map(Json::each).unwrap_or(&[])
30
+ node.get(key).map_or_default(Json::each)
31
31
  }
32
32
 
33
33
  fn name_of(node: &Json) -> Option<&str> {
@@ -47,15 +47,28 @@ fn domain_wide_value_objects(aggregates: &[Json]) -> HashMap<&str, (usize, usize
47
47
  by_name
48
48
  }
49
49
 
50
- fn find_element(aggregates: &[Json], a: usize, domain_wide: &HashMap<&str, (usize, usize)>, type_name: &str) -> Option<Element> {
50
+ fn find_element(
51
+ aggregates: &[Json],
52
+ a: usize,
53
+ domain_wide: &HashMap<&str, (usize, usize)>,
54
+ type_name: &str,
55
+ ) -> Option<Element> {
51
56
  let aggregate = &aggregates[a];
52
- if let Some(e) = list(aggregate, "entities").iter().position(|entity| name_of(entity) == Some(type_name)) {
57
+ if let Some(e) = list(aggregate, "entities")
58
+ .iter()
59
+ .position(|entity| name_of(entity) == Some(type_name))
60
+ {
53
61
  return Some(Element::Entity(a, e));
54
62
  }
55
- if let Some(v) = list(aggregate, "value_objects").iter().rposition(|value_object| name_of(value_object) == Some(type_name)) {
63
+ if let Some(v) = list(aggregate, "value_objects")
64
+ .iter()
65
+ .rposition(|value_object| name_of(value_object) == Some(type_name))
66
+ {
56
67
  return Some(Element::ValueObject(a, v));
57
68
  }
58
- domain_wide.get(type_name).map(|&(owner, v)| Element::ValueObject(owner, v))
69
+ domain_wide
70
+ .get(type_name)
71
+ .map(|&(owner, v)| Element::ValueObject(owner, v))
59
72
  }
60
73
 
61
74
  // Read first, write after: the commands are read while the elements they mark are mutated.
@@ -66,8 +79,15 @@ fn collect_marks(ir: &Json) -> Vec<Mark> {
66
79
 
67
80
  for (a, aggregate) in aggregates.iter().enumerate() {
68
81
  for attribute in list(aggregate, "attributes") {
69
- let (Some(attr_name), Some(type_name)) = (name_of(attribute), attribute.get("type").and_then(Json::as_str)) else { continue };
70
- let Some(element) = find_element(aggregates, a, &domain_wide, type_name) else { continue };
82
+ let (Some(attr_name), Some(type_name)) = (
83
+ name_of(attribute),
84
+ attribute.get("type").and_then(Json::as_str),
85
+ ) else {
86
+ continue;
87
+ };
88
+ let Some(element) = find_element(aggregates, a, &domain_wide, type_name) else {
89
+ continue;
90
+ };
71
91
 
72
92
  for field in collect_fields_to_mark(aggregate, attr_name) {
73
93
  marks.push(Mark { element, field });
@@ -90,15 +110,21 @@ fn collect_fields_to_mark(aggregate: &Json, target_attr_name: &str) -> Vec<Strin
90
110
  continue;
91
111
  }
92
112
 
93
- let Some(fields) = mutation.get("fields").and_then(Json::as_object) else { continue };
113
+ let Some(fields) = mutation.get("fields").and_then(Json::as_object) else {
114
+ continue;
115
+ };
94
116
  for (field_name, source) in fields {
95
117
  // Only a Symbol (an argument name) renders with a leading `:` in `Literal.render`.
96
- let Some(source_text) = source.as_str() else { continue };
97
- let Some(arg_name) = source_text.strip_prefix(':') else { continue };
98
-
99
- let source_is_optional = command_attrs
100
- .iter()
101
- .any(|a| name_of(a) == Some(arg_name) && a.get("optional").map(Json::as_bool).unwrap_or(false));
118
+ let Some(source_text) = source.as_str() else {
119
+ continue;
120
+ };
121
+ let Some(arg_name) = source_text.strip_prefix(':') else {
122
+ continue;
123
+ };
124
+
125
+ let source_is_optional = command_attrs.iter().any(|a| {
126
+ name_of(a) == Some(arg_name) && a.get("optional").is_some_and(Json::as_bool)
127
+ });
102
128
  if source_is_optional {
103
129
  out.push(field_name.clone());
104
130
  }
@@ -110,17 +136,31 @@ fn collect_fields_to_mark(aggregate: &Json, target_attr_name: &str) -> Vec<Strin
110
136
  }
111
137
 
112
138
  fn apply_marks(ir: &mut Json, marks: &[Mark]) {
113
- let Some(aggregates) = ir.get_mut("aggregates").and_then(Json::as_array_mut) else { return };
139
+ let Some(aggregates) = ir.get_mut("aggregates").and_then(Json::as_array_mut) else {
140
+ return;
141
+ };
114
142
 
115
143
  for mark in marks {
116
144
  let (a, key, index) = match mark.element {
117
145
  Element::ValueObject(a, v) => (a, "value_objects", v),
118
146
  Element::Entity(a, e) => (a, "entities", e),
119
147
  };
120
- let element = aggregates.get_mut(a).and_then(|aggregate| aggregate.get_mut(key)).and_then(Json::as_array_mut).and_then(|elements| elements.get_mut(index));
121
- let Some(attrs) = element.and_then(|element| element.get_mut("attributes")).and_then(Json::as_array_mut) else { continue };
148
+ let element = aggregates
149
+ .get_mut(a)
150
+ .and_then(|aggregate| aggregate.get_mut(key))
151
+ .and_then(Json::as_array_mut)
152
+ .and_then(|elements| elements.get_mut(index));
153
+ let Some(attrs) = element
154
+ .and_then(|element| element.get_mut("attributes"))
155
+ .and_then(Json::as_array_mut)
156
+ else {
157
+ continue;
158
+ };
122
159
 
123
- if let Some(field_attr) = attrs.iter_mut().find(|a| name_of(a) == Some(mark.field.as_str())) {
160
+ if let Some(field_attr) = attrs
161
+ .iter_mut()
162
+ .find(|a| name_of(a) == Some(mark.field.as_str()))
163
+ {
124
164
  field_attr.set_bool("optional", true);
125
165
  }
126
166
  }
@@ -163,12 +203,16 @@ mod tests {
163
203
  }
164
204
 
165
205
  fn domain(aggregates: &[String]) -> Json {
166
- Json::parse(&format!(r#"{{"aggregates": [{}]}}"#, aggregates.join(","))).expect("fixture parses")
206
+ Json::parse(&format!(r#"{{"aggregates": [{}]}}"#, aggregates.join(",")))
207
+ .expect("fixture parses")
167
208
  }
168
209
 
169
210
  fn amount_optional(ir: &Json, aggregate: usize, key: &str) -> bool {
170
211
  let element = &list(&list(ir, "aggregates")[aggregate], key)[0];
171
- let amount = list(element, "attributes").iter().find(|a| name_of(a) == Some("amount")).unwrap();
212
+ let amount = list(element, "attributes")
213
+ .iter()
214
+ .find(|a| name_of(a) == Some("amount"))
215
+ .unwrap();
172
216
  amount.get("optional").unwrap().as_bool()
173
217
  }
174
218
 
@@ -176,8 +220,12 @@ mod tests {
176
220
  // recorded from it. Read at test time, not compiled in: an installed gem has no `spec/`.
177
221
  #[test]
178
222
  fn marks_what_the_ruby_pass_marks_on_the_shared_fixture() {
179
- let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../spec/fixtures/append_optionals");
180
- let read = |name: &str| Json::parse(&std::fs::read_to_string(dir.join(name)).expect("reads the fixture")).expect("parses");
223
+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
224
+ .join("../../spec/fixtures/append_optionals");
225
+ let read = |name: &str| {
226
+ Json::parse(&std::fs::read_to_string(dir.join(name)).expect("reads the fixture"))
227
+ .expect("parses")
228
+ };
181
229
 
182
230
  let mut ir = read("input.json");
183
231
  run(&mut ir);
@@ -197,28 +245,42 @@ mod tests {
197
245
 
198
246
  #[test]
199
247
  fn marks_a_value_object_another_aggregate_declares() {
200
- let owner = r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#.replace("LEDGER", &element(false));
248
+ let owner =
249
+ r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#
250
+ .replace("LEDGER", &element(false));
201
251
  let appender = aggregate_with("", "", "LedgerEntry");
202
252
  let mut ir = domain(&[appender, owner]);
203
253
  run(&mut ir);
204
254
 
205
- assert!(amount_optional(&ir, 1, "value_objects"), "the value object lives in the second aggregate");
255
+ assert!(
256
+ amount_optional(&ir, 1, "value_objects"),
257
+ "the value object lives in the second aggregate"
258
+ );
206
259
  }
207
260
 
208
261
  #[test]
209
262
  fn prefers_a_local_entity_over_a_value_object_of_the_same_name() {
210
263
  let local = aggregate_with(&element(false), "", "LedgerEntry");
211
- let owner = r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#.replace("LEDGER", &element(false));
264
+ let owner =
265
+ r#"{"attributes": [], "value_objects": [LEDGER], "entities": [], "commands": []}"#
266
+ .replace("LEDGER", &element(false));
212
267
  let mut ir = domain(&[local, owner]);
213
268
  run(&mut ir);
214
269
 
215
- assert!(amount_optional(&ir, 0, "entities"), "the local entity is the one marked");
216
- assert!(!amount_optional(&ir, 1, "value_objects"), "the domain-wide value object is left alone");
270
+ assert!(
271
+ amount_optional(&ir, 0, "entities"),
272
+ "the local entity is the one marked"
273
+ );
274
+ assert!(
275
+ !amount_optional(&ir, 1, "value_objects"),
276
+ "the domain-wide value object is left alone"
277
+ );
217
278
  }
218
279
 
219
280
  #[test]
220
281
  fn leaves_a_field_alone_when_its_argument_is_required() {
221
- let mut ir = domain(&[aggregate_with(&element(false), "", "LedgerEntry").replace("\"optional\": true", "\"optional\": false")]);
282
+ let mut ir = domain(&[aggregate_with(&element(false), "", "LedgerEntry")
283
+ .replace("\"optional\": true", "\"optional\": false")]);
222
284
  run(&mut ir);
223
285
 
224
286
  assert!(!amount_optional(&ir, 0, "entities"));
@@ -17,7 +17,7 @@ pub fn port_operation_skip_reason(
17
17
  _owner_name: &str,
18
18
  value_objects_by_name: &HashMap<String, &Json>,
19
19
  ) -> Option<SkipReason> {
20
- let attrs = operation.get("attributes").map(Json::each).unwrap_or(&[]);
20
+ let attrs = operation.get("attributes").map_or_default(Json::each);
21
21
  let unresolved: Vec<&Json> = attrs
22
22
  .iter()
23
23
  .filter(|attr| {
@@ -63,7 +63,7 @@ pub fn emit_port_operation(
63
63
  naming::rust_ident(operation.get("name").and_then(Json::as_str).unwrap_or(""))
64
64
  );
65
65
  let qualified = format!("{domain_name}::{owner_name}");
66
- let attrs = operation.get("attributes").map(Json::each).unwrap_or(&[]);
66
+ let attrs = operation.get("attributes").map_or_default(Json::each);
67
67
  // A self-reference on a port operation is a migration-era spelling of
68
68
  // its receiver, never an external fact. Keep accepting old IR while
69
69
  // excluding that field from the typed args and event payload.
@@ -109,7 +109,7 @@ pub fn emit_port_operation(
109
109
  ))
110
110
  );
111
111
 
112
- let emits = operation.get("emits").map(Json::each).unwrap_or(&[]);
112
+ let emits = operation.get("emits").map_or_default(Json::each);
113
113
  let events: Vec<String> = emits
114
114
  .iter()
115
115
  .map(|event_name| {
@@ -130,7 +130,7 @@ pub fn emit_port_operation(
130
130
  let operation_name = operation.get("name").and_then(Json::as_str).unwrap_or("");
131
131
  [
132
132
  format!("#[derive(Debug, Clone)]\n{}", struct_lines.join("\n")),
133
- crate::json_codec::emit_to_json_flat(exemplar, &args_struct, &fact_attrs, value_objects_by_name, false, &[], None),
133
+ crate::json_codec::emit_to_json_flat(exemplar, &args_struct, &fact_attrs, false, &[], None),
134
134
  crate::json_codec::emit_from_json_flat(
135
135
  exemplar,
136
136
  &args_struct,
@@ -141,6 +141,7 @@ pub fn emit_port_operation(
141
141
  false,
142
142
  true,
143
143
  Some(aggregates_by_name),
144
+ None,
144
145
  ),
145
146
  dispatch_fn,
146
147
  ]