hecks 3.14.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -5,16 +5,23 @@
5
5
  enum Node {
6
6
  Literal(char),
7
7
  Any,
8
- Class { negate: bool, ranges: Vec<(char, char)> },
8
+ Class {
9
+ negate: bool,
10
+ ranges: Vec<(char, char)>,
11
+ },
9
12
  // `^`/`$` match at line boundaries, as in Ruby.
10
13
  LineStart,
11
14
  LineEnd,
12
15
  // `\A`/`\z`/`\Z` match only at the whole-string boundaries.
13
16
  StringStart,
14
17
  StringEnd,
15
- Group(Vec<Node>),
16
- Alt(Vec<Vec<Node>>),
17
- Repeat { node: Box<Node>, min: usize, max: Option<usize> },
18
+ Group(Vec<Self>),
19
+ Alt(Vec<Vec<Self>>),
20
+ Repeat {
21
+ node: Box<Self>,
22
+ min: usize,
23
+ max: Option<usize>,
24
+ },
18
25
  }
19
26
 
20
27
  /// `true` if `text` contains a match for `pattern` read as dispatch reads it: `^` and `$` are
@@ -66,7 +73,9 @@ fn whole_string(pattern: &str) -> String {
66
73
 
67
74
  /// `true` if `text` contains a match for `pattern`; a pattern that fails to parse is `false`.
68
75
  pub fn matches(pattern: &str, text: &str) -> bool {
69
- let Ok(nodes) = parse(pattern) else { return false };
76
+ let Ok(nodes) = parse(pattern) else {
77
+ return false;
78
+ };
70
79
  let chars: Vec<char> = text.chars().collect();
71
80
  // Only `\A` may skip to position 0; `^` can still match after a later `\n`.
72
81
  let anchored_start = matches!(nodes.first(), Some(Node::StringStart));
@@ -76,24 +85,48 @@ pub fn matches(pattern: &str, text: &str) -> bool {
76
85
  (0..=chars.len()).any(|start| match_from(&nodes, 0, &chars, start, &|_| true))
77
86
  }
78
87
 
79
- fn match_from(nodes: &[Node], idx: usize, text: &[char], ti: usize, cont: &dyn Fn(usize) -> bool) -> bool {
88
+ fn match_from(
89
+ nodes: &[Node],
90
+ idx: usize,
91
+ text: &[char],
92
+ ti: usize,
93
+ cont: &dyn Fn(usize) -> bool,
94
+ ) -> bool {
80
95
  if idx == nodes.len() {
81
96
  return cont(ti);
82
97
  }
83
98
  match &nodes[idx] {
84
- Node::Literal(c) => ti < text.len() && text[ti] == *c && match_from(nodes, idx + 1, text, ti + 1, cont),
99
+ Node::Literal(c) => {
100
+ ti < text.len() && text[ti] == *c && match_from(nodes, idx + 1, text, ti + 1, cont)
101
+ }
85
102
  // Like Ruby without `/m`, `.` does not match `\n`.
86
- Node::Any => ti < text.len() && text[ti] != '\n' && match_from(nodes, idx + 1, text, ti + 1, cont),
103
+ Node::Any => {
104
+ ti < text.len() && text[ti] != '\n' && match_from(nodes, idx + 1, text, ti + 1, cont)
105
+ }
87
106
  Node::Class { negate, ranges } => {
88
- ti < text.len() && class_matches(*negate, ranges, text[ti]) && match_from(nodes, idx + 1, text, ti + 1, cont)
107
+ ti < text.len()
108
+ && class_matches(*negate, ranges, text[ti])
109
+ && match_from(nodes, idx + 1, text, ti + 1, cont)
110
+ }
111
+ Node::LineStart => {
112
+ (ti == 0 || text[ti - 1] == '\n') && match_from(nodes, idx + 1, text, ti, cont)
113
+ }
114
+ Node::LineEnd => {
115
+ (ti == text.len() || text[ti] == '\n') && match_from(nodes, idx + 1, text, ti, cont)
89
116
  }
90
- Node::LineStart => (ti == 0 || text[ti - 1] == '\n') && match_from(nodes, idx + 1, text, ti, cont),
91
- Node::LineEnd => (ti == text.len() || text[ti] == '\n') && match_from(nodes, idx + 1, text, ti, cont),
92
117
  Node::StringStart => ti == 0 && match_from(nodes, idx + 1, text, ti, cont),
93
118
  Node::StringEnd => ti == text.len() && match_from(nodes, idx + 1, text, ti, cont),
94
- Node::Group(inner) => match_from(inner, 0, text, ti, &|ti2| match_from(nodes, idx + 1, text, ti2, cont)),
95
- Node::Alt(branches) => branches.iter().any(|b| match_from(b, 0, text, ti, &|ti2| match_from(nodes, idx + 1, text, ti2, cont))),
96
- Node::Repeat { node, min, max } => match_repeat(node, 0, *min, *max, nodes, idx + 1, text, ti, cont),
119
+ Node::Group(inner) => match_from(inner, 0, text, ti, &|ti2| {
120
+ match_from(nodes, idx + 1, text, ti2, cont)
121
+ }),
122
+ Node::Alt(branches) => branches.iter().any(|b| {
123
+ match_from(b, 0, text, ti, &|ti2| {
124
+ match_from(nodes, idx + 1, text, ti2, cont)
125
+ })
126
+ }),
127
+ Node::Repeat { node, min, max } => {
128
+ match_repeat(node, 0, *min, *max, nodes, idx + 1, text, ti, cont)
129
+ }
97
130
  }
98
131
  }
99
132
 
@@ -133,7 +166,7 @@ fn class_matches(negate: bool, ranges: &[(char, char)], c: char) -> bool {
133
166
  }
134
167
 
135
168
  // `\t`, `\n` and `\r` become control characters; any other escape is the literal itself.
136
- fn escape_char(c: char) -> char {
169
+ const fn escape_char(c: char) -> char {
137
170
  match c {
138
171
  't' => '\t',
139
172
  'n' => '\n',
@@ -145,7 +178,10 @@ fn escape_char(c: char) -> char {
145
178
  // Recursive descent: `alternation > concat > repeat > atom`.
146
179
  fn parse(pattern: &str) -> Result<Vec<Node>, ()> {
147
180
  let chars: Vec<char> = pattern.chars().collect();
148
- let mut p = Parser { chars: &chars, pos: 0 };
181
+ let mut p = Parser {
182
+ chars: &chars,
183
+ pos: 0,
184
+ };
149
185
  let node = p.parse_alt()?;
150
186
  if p.pos != p.chars.len() {
151
187
  return Err(());
@@ -158,7 +194,7 @@ struct Parser<'a> {
158
194
  pos: usize,
159
195
  }
160
196
 
161
- impl<'a> Parser<'a> {
197
+ impl Parser<'_> {
162
198
  fn peek(&self) -> Option<char> {
163
199
  self.chars.get(self.pos).copied()
164
200
  }
@@ -203,26 +239,41 @@ impl<'a> Parser<'a> {
203
239
  match self.peek() {
204
240
  Some('*') => {
205
241
  self.bump();
206
- Ok(Node::Repeat { node: Box::new(atom), min: 0, max: None })
242
+ Ok(Node::Repeat {
243
+ node: Box::new(atom),
244
+ min: 0,
245
+ max: None,
246
+ })
207
247
  }
208
248
  Some('+') => {
209
249
  self.bump();
210
- Ok(Node::Repeat { node: Box::new(atom), min: 1, max: None })
250
+ Ok(Node::Repeat {
251
+ node: Box::new(atom),
252
+ min: 1,
253
+ max: None,
254
+ })
211
255
  }
212
256
  Some('?') => {
213
257
  self.bump();
214
- Ok(Node::Repeat { node: Box::new(atom), min: 0, max: Some(1) })
258
+ Ok(Node::Repeat {
259
+ node: Box::new(atom),
260
+ min: 0,
261
+ max: Some(1),
262
+ })
215
263
  }
216
264
  Some('{') => {
217
265
  let save = self.pos;
218
266
  self.bump();
219
- match self.parse_bounded_repeat() {
220
- Some((min, max)) => Ok(Node::Repeat { node: Box::new(atom), min, max }),
221
- None => {
222
- // Not a well-formed `{n,m}`: read the brace as a literal.
223
- self.pos = save;
224
- Ok(atom)
225
- }
267
+ if let Some((min, max)) = self.parse_bounded_repeat() {
268
+ Ok(Node::Repeat {
269
+ node: Box::new(atom),
270
+ min,
271
+ max,
272
+ })
273
+ } else {
274
+ // Not a well-formed `{n,m}`: read the brace as a literal.
275
+ self.pos = save;
276
+ Ok(atom)
226
277
  }
227
278
  }
228
279
  _ => Ok(atom),
@@ -264,7 +315,11 @@ impl<'a> Parser<'a> {
264
315
  if self.pos == start {
265
316
  return None;
266
317
  }
267
- self.chars[start..self.pos].iter().collect::<String>().parse().ok()
318
+ self.chars[start..self.pos]
319
+ .iter()
320
+ .collect::<String>()
321
+ .parse()
322
+ .ok()
268
323
  }
269
324
 
270
325
  fn parse_atom(&mut self) -> Result<Node, ()> {
@@ -308,11 +363,21 @@ impl<'a> Parser<'a> {
308
363
  }
309
364
  Some(c) => {
310
365
  self.bump();
311
- let lo = if c == '\\' { escape_char(self.bump().ok_or(())?) } else { c };
312
- if self.peek() == Some('-') && self.chars.get(self.pos + 1).is_some_and(|c| *c != ']') {
366
+ let lo = if c == '\\' {
367
+ escape_char(self.bump().ok_or(())?)
368
+ } else {
369
+ c
370
+ };
371
+ if self.peek() == Some('-')
372
+ && self.chars.get(self.pos + 1).is_some_and(|c| *c != ']')
373
+ {
313
374
  self.bump();
314
375
  let hi_raw = self.bump().ok_or(())?;
315
- let hi = if hi_raw == '\\' { escape_char(self.bump().ok_or(())?) } else { hi_raw };
376
+ let hi = if hi_raw == '\\' {
377
+ escape_char(self.bump().ok_or(())?)
378
+ } else {
379
+ hi_raw
380
+ };
316
381
  ranges.push((lo, hi));
317
382
  } else {
318
383
  ranges.push((lo, lo));
@@ -336,12 +401,20 @@ mod tests {
336
401
  fn matches_every_row_of_the_recorded_pattern_contract() {
337
402
  let raw = include_str!("../../../spec/corpus/fixtures/patterns.json");
338
403
  let parsed = Json::parse(raw).expect("patterns.json must parse");
339
- let rows = parsed.as_array().expect("patterns.json must be a JSON array");
404
+ let rows = parsed
405
+ .as_array()
406
+ .expect("patterns.json must be a JSON array");
340
407
  assert!(!rows.is_empty(), "the recorded contract must not be empty");
341
408
 
342
409
  for row in rows {
343
- let pattern = row.get("pattern").and_then(Json::as_str).expect("row must carry a pattern");
344
- let input = row.get("input").and_then(Json::as_str).expect("row must carry an input");
410
+ let pattern = row
411
+ .get("pattern")
412
+ .and_then(Json::as_str)
413
+ .expect("row must carry a pattern");
414
+ let input = row
415
+ .get("input")
416
+ .and_then(Json::as_str)
417
+ .expect("row must carry an input");
345
418
  let expected = matches!(row.get("matches"), Some(Json::Bool(true)));
346
419
 
347
420
  assert_eq!(
@@ -8,31 +8,37 @@ pub use super::vocab::QueryComparator;
8
8
  impl QueryComparator {
9
9
  /// The wire's `op` string, or `None` if unrecognized (callers refuse, never default to `eq`).
10
10
  pub fn parse(op: &str) -> Option<Self> {
11
- QueryComparator::from_name(op)
11
+ Self::from_name(op)
12
12
  }
13
13
 
14
14
  /// `Comparison#holds?`; `held`/`want` arrive already reduced through `comparable`.
15
15
  pub fn matches(self, held: &Json, want: &Json) -> bool {
16
16
  match self {
17
- QueryComparator::Eq => held == want,
18
- QueryComparator::Ne => held != want,
19
- QueryComparator::Gt => ordered(held, want) && as_f64(held) > as_f64(want),
20
- QueryComparator::Gte => ordered(held, want) && as_f64(held) >= as_f64(want),
21
- QueryComparator::Lt => ordered(held, want) && as_f64(held) < as_f64(want),
22
- QueryComparator::Lte => ordered(held, want) && as_f64(held) <= as_f64(want),
23
- QueryComparator::In => members(want).iter().any(|member| member == &to_s(held)),
24
- QueryComparator::Contains => contains(held, want),
17
+ Self::Eq => held == want,
18
+ Self::Ne => held != want,
19
+ Self::Gt => ordered(held, want) && as_f64(held) > as_f64(want),
20
+ Self::Gte => ordered(held, want) && as_f64(held) >= as_f64(want),
21
+ Self::Lt => ordered(held, want) && as_f64(held) < as_f64(want),
22
+ Self::Lte => ordered(held, want) && as_f64(held) <= as_f64(want),
23
+ Self::In => members(want).iter().any(|member| member == &to_s(held)),
24
+ Self::Contains => contains(held, want),
25
25
  // Needs a repository, so `filter_entries_cross_domain` calls `none_in_state_matches`
26
26
  // first; this arm keeps the match exhaustive with the same "not excluded" default.
27
- QueryComparator::NoneInState => true,
27
+ Self::NoneInState => true,
28
28
  }
29
29
  }
30
30
  }
31
31
 
32
32
  /// `Comparison#none_in_state?`: `want` is `"Aggregate:state"`, `held` the referring foreign key.
33
33
  /// `cross_domain` is searched in order, first match wins; an empty slice answers `true`.
34
- pub fn none_in_state_matches(cross_domain: &[(&str, &dyn super::AggregateScan)], held: &Json, want: &Json) -> bool {
35
- let Some((aggregate_name, state)) = to_s(want).split_once(':').map(|(a, s)| (a.to_string(), s.to_string()))
34
+ pub fn none_in_state_matches(
35
+ cross_domain: &[(&str, &dyn super::AggregateScan)],
36
+ held: &Json,
37
+ want: &Json,
38
+ ) -> bool {
39
+ let Some((aggregate_name, state)) = to_s(want)
40
+ .split_once(':')
41
+ .map(|(a, s)| (a.to_string(), s.to_string()))
36
42
  else {
37
43
  return true;
38
44
  };
@@ -40,9 +46,13 @@ pub fn none_in_state_matches(cross_domain: &[(&str, &dyn super::AggregateScan)],
40
46
 
41
47
  for (domain_name, store) in cross_domain {
42
48
  let qualified = format!("{domain_name}::{aggregate_name}");
43
- let Some(entries) = store.scan(&qualified) else { continue };
49
+ let Some(entries) = store.scan(&qualified) else {
50
+ continue;
51
+ };
44
52
 
45
- let Some((_, record)) = entries.iter().find(|(id, _)| *id == held_id) else { return true };
53
+ let Some((_, record)) = entries.iter().find(|(id, _)| *id == held_id) else {
54
+ return true;
55
+ };
46
56
  let record_state = comparable(&record.dig("state").cloned().unwrap_or(Json::Null));
47
57
  return to_s(&record_state) != state;
48
58
  }
@@ -52,12 +62,13 @@ pub fn none_in_state_matches(cross_domain: &[(&str, &dyn super::AggregateScan)],
52
62
  }
53
63
 
54
64
  /// gt/gte/lt/lte are numeric-only; anything else is silently false, never a refusal.
55
- fn ordered(held: &Json, want: &Json) -> bool {
56
- matches!(held, Json::Num(_, _) | Json::Float(_)) && matches!(want, Json::Num(_, _) | Json::Float(_))
65
+ const fn ordered(held: &Json, want: &Json) -> bool {
66
+ matches!(held, Json::Num(_, _) | Json::Float(_))
67
+ && matches!(want, Json::Num(_, _) | Json::Float(_))
57
68
  }
58
69
 
59
70
  /// Only called after `ordered` has gated the comparison, so the NaN fallback is unreachable.
60
- pub(crate) fn as_f64(value: &Json) -> f64 {
71
+ pub(crate) const fn as_f64(value: &Json) -> f64 {
61
72
  match value {
62
73
  Json::Num(n, _) | Json::Float(n) => *n,
63
74
  _ => f64::NAN,
@@ -97,9 +108,14 @@ pub fn comparable(value: &Json) -> Json {
97
108
 
98
109
  /// `comparable` without the copy: the reduced value is always `value` itself or a member of it.
99
110
  pub fn comparable_ref(value: &Json) -> &Json {
100
- let Json::Object(fields) = value else { return value };
111
+ let Json::Object(fields) = value else {
112
+ return value;
113
+ };
101
114
 
102
- if let Some((_, numeric)) = fields.iter().find(|(_, v)| matches!(v, Json::Num(_, _) | Json::Float(_))) {
115
+ if let Some((_, numeric)) = fields
116
+ .iter()
117
+ .find(|(_, v)| matches!(v, Json::Num(_, _) | Json::Float(_)))
118
+ {
103
119
  return numeric;
104
120
  }
105
121
  if fields.len() == 1 {
@@ -113,7 +129,10 @@ fn members(value: &Json) -> Vec<String> {
113
129
  if let Json::Array(items) = value {
114
130
  return items.iter().map(|item| to_s(&comparable(item))).collect();
115
131
  }
116
- to_s(value).split(',').map(|piece| piece.trim().to_string()).collect()
132
+ to_s(value)
133
+ .split(',')
134
+ .map(|piece| piece.trim().to_string())
135
+ .collect()
117
136
  }
118
137
 
119
138
  /// Element membership for a `list_of` field, substring match for anything else.
@@ -139,68 +158,126 @@ mod none_in_state_tests {
139
158
  impl AggregateScan for FakeStore {
140
159
  fn scan(&self, aggregate: &str) -> Option<Vec<(String, Json)>> {
141
160
  let bare = aggregate.strip_prefix(&format!("{}::", self.domain))?;
142
- self.aggregates.iter().find(|(name, _)| *name == bare).map(|(_, entries)| entries.clone())
161
+ self.aggregates
162
+ .iter()
163
+ .find(|(name, _)| *name == bare)
164
+ .map(|(_, entries)| entries.clone())
143
165
  }
144
166
  }
145
167
 
146
168
  /// A record whose `state` is a single-field value object, as in the real corpus fixture.
147
169
  fn record_in_state(state: &str) -> Json {
148
- Json::obj(vec![("state", Json::obj(vec![("value", Json::str(state))]))])
170
+ Json::obj(vec![(
171
+ "state",
172
+ Json::obj(vec![("value", Json::str(state))]),
173
+ )])
149
174
  }
150
175
 
151
176
  #[test]
152
177
  fn with_no_domains_to_search_answers_vacuously_true() {
153
178
  // Ruby's `return true unless registry`.
154
- assert!(none_in_state_matches(&[], &Json::str("c1"), &Json::str("Claim:held")));
179
+ assert!(none_in_state_matches(
180
+ &[],
181
+ &Json::str("c1"),
182
+ &Json::str("Claim:held")
183
+ ));
155
184
  }
156
185
 
157
186
  #[test]
158
187
  fn excludes_a_row_whose_target_is_currently_in_the_named_state() {
159
- let store = FakeStore { domain: "AntiJoinGrowth", aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])] };
188
+ let store = FakeStore {
189
+ domain: "AntiJoinGrowth",
190
+ aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])],
191
+ };
160
192
  let stores: Vec<(&str, &dyn AggregateScan)> = vec![("AntiJoinGrowth", &store)];
161
- assert!(!none_in_state_matches(&stores, &Json::str("c1"), &Json::str("Claim:held")));
193
+ assert!(!none_in_state_matches(
194
+ &stores,
195
+ &Json::str("c1"),
196
+ &Json::str("Claim:held")
197
+ ));
162
198
  }
163
199
 
164
200
  #[test]
165
201
  fn keeps_a_row_whose_target_has_moved_out_of_the_named_state() {
166
- let store = FakeStore { domain: "AntiJoinGrowth", aggregates: vec![("Claim", vec![("c2".into(), record_in_state("released"))])] };
202
+ let store = FakeStore {
203
+ domain: "AntiJoinGrowth",
204
+ aggregates: vec![("Claim", vec![("c2".into(), record_in_state("released"))])],
205
+ };
167
206
  let stores: Vec<(&str, &dyn AggregateScan)> = vec![("AntiJoinGrowth", &store)];
168
- assert!(none_in_state_matches(&stores, &Json::str("c2"), &Json::str("Claim:held")));
207
+ assert!(none_in_state_matches(
208
+ &stores,
209
+ &Json::str("c2"),
210
+ &Json::str("Claim:held")
211
+ ));
169
212
  }
170
213
 
171
214
  #[test]
172
215
  fn keeps_a_row_whose_target_was_never_filed_at_all() {
173
216
  // No record reads the same as a record in another state.
174
- let store = FakeStore { domain: "AntiJoinGrowth", aggregates: vec![("Claim", vec![])] };
217
+ let store = FakeStore {
218
+ domain: "AntiJoinGrowth",
219
+ aggregates: vec![("Claim", vec![])],
220
+ };
175
221
  let stores: Vec<(&str, &dyn AggregateScan)> = vec![("AntiJoinGrowth", &store)];
176
- assert!(none_in_state_matches(&stores, &Json::str("nonexistent"), &Json::str("Claim:held")));
222
+ assert!(none_in_state_matches(
223
+ &stores,
224
+ &Json::str("nonexistent"),
225
+ &Json::str("Claim:held")
226
+ ));
177
227
  }
178
228
 
179
229
  #[test]
180
230
  fn keeps_every_row_when_no_domain_declares_the_named_aggregate_at_all() {
181
- let store = FakeStore { domain: "AntiJoinGrowth", aggregates: vec![] };
231
+ let store = FakeStore {
232
+ domain: "AntiJoinGrowth",
233
+ aggregates: vec![],
234
+ };
182
235
  let stores: Vec<(&str, &dyn AggregateScan)> = vec![("AntiJoinGrowth", &store)];
183
- assert!(none_in_state_matches(&stores, &Json::str("c1"), &Json::str("NoSuchAggregate:held")));
236
+ assert!(none_in_state_matches(
237
+ &stores,
238
+ &Json::str("c1"),
239
+ &Json::str("NoSuchAggregate:held")
240
+ ));
184
241
  }
185
242
 
186
243
  #[test]
187
244
  fn searches_a_second_domain_when_the_first_does_not_declare_the_aggregate() {
188
245
  // Cross-domain search; no real deployment has this shape.
189
- let domain_a = FakeStore { domain: "DomainA", aggregates: vec![("Widget", vec![])] };
190
- let domain_b =
191
- FakeStore { domain: "DomainB", aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])] };
192
- let stores: Vec<(&str, &dyn AggregateScan)> = vec![("DomainA", &domain_a), ("DomainB", &domain_b)];
193
- assert!(!none_in_state_matches(&stores, &Json::str("c1"), &Json::str("Claim:held")));
246
+ let domain_a = FakeStore {
247
+ domain: "DomainA",
248
+ aggregates: vec![("Widget", vec![])],
249
+ };
250
+ let domain_b = FakeStore {
251
+ domain: "DomainB",
252
+ aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])],
253
+ };
254
+ let stores: Vec<(&str, &dyn AggregateScan)> =
255
+ vec![("DomainA", &domain_a), ("DomainB", &domain_b)];
256
+ assert!(!none_in_state_matches(
257
+ &stores,
258
+ &Json::str("c1"),
259
+ &Json::str("Claim:held")
260
+ ));
194
261
  }
195
262
 
196
263
  #[test]
197
264
  fn first_match_wins_when_two_domains_declare_the_same_bare_aggregate_name() {
198
265
  // Pins first-match-wins (Ruby's `find_aggregate_by_name`): DomainA's "held" must win
199
266
  // over DomainB's "released".
200
- let first = FakeStore { domain: "DomainA", aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])] };
201
- let second =
202
- FakeStore { domain: "DomainB", aggregates: vec![("Claim", vec![("c1".into(), record_in_state("released"))])] };
203
- let stores: Vec<(&str, &dyn AggregateScan)> = vec![("DomainA", &first), ("DomainB", &second)];
204
- assert!(!none_in_state_matches(&stores, &Json::str("c1"), &Json::str("Claim:held")));
267
+ let first = FakeStore {
268
+ domain: "DomainA",
269
+ aggregates: vec![("Claim", vec![("c1".into(), record_in_state("held"))])],
270
+ };
271
+ let second = FakeStore {
272
+ domain: "DomainB",
273
+ aggregates: vec![("Claim", vec![("c1".into(), record_in_state("released"))])],
274
+ };
275
+ let stores: Vec<(&str, &dyn AggregateScan)> =
276
+ vec![("DomainA", &first), ("DomainB", &second)];
277
+ assert!(!none_in_state_matches(
278
+ &stores,
279
+ &Json::str("c1"),
280
+ &Json::str("Claim:held")
281
+ ));
205
282
  }
206
283
  }
@@ -57,12 +57,18 @@ pub fn apply(
57
57
  rows
58
58
  }
59
59
 
60
+ /// Records by id, in the order a read model's rows are held.
61
+ type Rows = Vec<(String, Json)>;
62
+
60
63
  // `sort_by` is stable, so the id order from `apply` survives ties without an index tie-break.
61
- fn apply_declared_order(rows: Vec<(String, Json)>, order_by: &OrderBy) -> Vec<(String, Json)> {
62
- let (mut null_rows, mut valued_rows): (Vec<(String, Json)>, Vec<(String, Json)>) =
63
- rows.into_iter().partition(|(_, record)| order_key(record, order_by.field) == Json::Null);
64
+ fn apply_declared_order(rows: Rows, order_by: &OrderBy) -> Rows {
65
+ let (mut null_rows, mut valued_rows): (Rows, Rows) = rows
66
+ .into_iter()
67
+ .partition(|(_, record)| order_key(record, order_by.field) == Json::Null);
64
68
 
65
- valued_rows.sort_by(|(_, a), (_, b)| compare_comparable(&order_key(a, order_by.field), &order_key(b, order_by.field)));
69
+ valued_rows.sort_by(|(_, a), (_, b)| {
70
+ compare_comparable(&order_key(a, order_by.field), &order_key(b, order_by.field))
71
+ });
66
72
 
67
73
  if order_by.descending {
68
74
  valued_rows.reverse();
@@ -72,7 +78,9 @@ fn apply_declared_order(rows: Vec<(String, Json)>, order_by: &OrderBy) -> Vec<(S
72
78
  match order_by.nulls {
73
79
  NullsMode::First => null_rows.into_iter().chain(valued_rows).collect(),
74
80
  NullsMode::Last => valued_rows.into_iter().chain(null_rows).collect(),
75
- NullsMode::Native if !order_by.descending => null_rows.into_iter().chain(valued_rows).collect(),
81
+ NullsMode::Native if !order_by.descending => {
82
+ null_rows.into_iter().chain(valued_rows).collect()
83
+ }
76
84
  NullsMode::Native => valued_rows.into_iter().chain(null_rows).collect(),
77
85
  }
78
86
  }