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
@@ -4,7 +4,7 @@ use super::evaluator::Evaluator;
4
4
  use super::{find_operator, Operator};
5
5
 
6
6
  /// Which of `all?`, `any?` or `none?` a block predicate uses.
7
- #[derive(Debug, Clone, Copy, PartialEq)]
7
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
8
8
  pub enum BlockMode {
9
9
  All,
10
10
  Any,
@@ -12,20 +12,12 @@ pub enum BlockMode {
12
12
  }
13
13
 
14
14
  impl BlockMode {
15
- pub fn rust_name(&self) -> &'static str {
16
- match self {
17
- BlockMode::All => "All",
18
- BlockMode::Any => "Any",
19
- BlockMode::None => "None",
20
- }
21
- }
22
-
23
15
  /// The lowercase spelling written into the AST JSON.
24
- pub fn json_name(&self) -> &'static str {
16
+ pub const fn json_name(self) -> &'static str {
25
17
  match self {
26
- BlockMode::All => "all",
27
- BlockMode::Any => "any",
28
- BlockMode::None => "none",
18
+ Self::All => "all",
19
+ Self::Any => "any",
20
+ Self::None => "none",
29
21
  }
30
22
  }
31
23
  }
@@ -38,41 +30,79 @@ pub enum Resolver {
38
30
  StringLiteral(String),
39
31
  BoolLiteral(bool),
40
32
  NilLiteral,
41
- Addition(Box<Resolver>, Box<Resolver>),
42
- Subtraction(Box<Resolver>, Box<Resolver>),
43
- Multiplication(Box<Resolver>, Box<Resolver>),
33
+ Addition(Box<Self>, Box<Self>),
34
+ Subtraction(Box<Self>, Box<Self>),
35
+ Multiplication(Box<Self>, Box<Self>),
44
36
  /// Integer operands floor toward negative infinity; a zero divisor is an evaluation fault.
45
- Division(Box<Resolver>, Box<Resolver>),
46
- SignTest { operator: Operator, receiver: Box<Resolver> },
47
- Empty(Box<Resolver>),
48
- ToS(Box<Resolver>),
49
- Modulo { receiver: Box<Resolver>, divisor: Box<Resolver> },
50
- Size(Box<Resolver>),
37
+ Division(Box<Self>, Box<Self>),
38
+ SignTest {
39
+ operator: Operator,
40
+ receiver: Box<Self>,
41
+ },
42
+ Empty(Box<Self>),
43
+ ToS(Box<Self>),
44
+ Modulo {
45
+ receiver: Box<Self>,
46
+ divisor: Box<Self>,
47
+ },
48
+ Size(Box<Self>),
51
49
  Lookup(String),
52
50
  /// `["active", "suspended"]`: a literal set, the haystack of an `.include?`.
53
- ArrayLiteral(Vec<Resolver>),
51
+ ArrayLiteral(Vec<Self>),
54
52
  /// `receiver.any? { |param| predicate }` and siblings; the body is parsed as a predicate.
55
- BlockPredicate { mode: BlockMode, receiver: Box<Resolver>, param: String, predicate: Box<Evaluator> },
53
+ BlockPredicate {
54
+ mode: BlockMode,
55
+ receiver: Box<Self>,
56
+ param: String,
57
+ predicate: Box<Evaluator>,
58
+ },
56
59
  /// `receiver.match?(/pattern/flags)`: the pattern text is kept verbatim.
57
- MatchesRegex { receiver: Box<Resolver>, pattern: String, flags: String },
60
+ MatchesRegex {
61
+ receiver: Box<Self>,
62
+ pattern: String,
63
+ flags: String,
64
+ },
58
65
  /// `receiver.present?` (`negated: false`) or `receiver.blank?` (`negated: true`).
59
- Presence { receiver: Box<Resolver>, negated: bool },
66
+ Presence {
67
+ receiver: Box<Self>,
68
+ negated: bool,
69
+ },
60
70
  /// `receiver.set?` (`negated: false`) or `receiver.unset?` (`negated: true`): only `!nil?`.
61
- Assignment { receiver: Box<Resolver>, negated: bool },
71
+ Assignment {
72
+ receiver: Box<Self>,
73
+ negated: bool,
74
+ },
62
75
  /// `receiver.split("sep")`: the separator is sliced verbatim between the quotes.
63
- Split { receiver: Box<Resolver>, separator: String },
76
+ Split {
77
+ receiver: Box<Self>,
78
+ separator: String,
79
+ },
64
80
  /// `receiver.strip` / `.lstrip` / `.rstrip`; `side` is `both`, `left` or `right`.
65
- Strip { receiver: Box<Resolver>, side: &'static str },
81
+ Strip {
82
+ receiver: Box<Self>,
83
+ side: &'static str,
84
+ },
66
85
  /// `receiver.first`.
67
- First(Box<Resolver>),
86
+ First(Box<Self>),
68
87
  /// `receiver.last`.
69
- Last(Box<Resolver>),
88
+ Last(Box<Self>),
70
89
  /// `receiver.start_with?("substring")`.
71
- StartsWith { receiver: Box<Resolver>, substring: String },
90
+ StartsWith {
91
+ receiver: Box<Self>,
92
+ substring: String,
93
+ },
72
94
  /// `receiver.end_with?("substring")`.
73
- EndsWith { receiver: Box<Resolver>, substring: String },
95
+ EndsWith {
96
+ receiver: Box<Self>,
97
+ substring: String,
98
+ },
74
99
  /// `receiver.find { |param| predicate }.a.b`.
75
- Find { receiver: Box<Resolver>, param: String, predicate: Box<Evaluator>, path: Vec<String> },
100
+ Find {
101
+ receiver: Box<Self>,
102
+ param: String,
103
+ predicate: Box<Evaluator>,
104
+ path: Vec<String>,
105
+ },
76
106
  }
77
107
 
78
108
  const SIGN_TESTS: [(&str, &str); 3] = [("positive?", ">"), ("negative?", "<"), ("zero?", "==")];
@@ -85,10 +115,20 @@ pub fn parse(expr: &str) -> Resolver {
85
115
  }
86
116
 
87
117
  if is_integer_literal(expr) {
88
- return Resolver::IntegerLiteral(expr.parse::<i64>().unwrap_or_else(|_| panic!("bad integer literal {expr:?}")));
118
+ // reason: `parse` has no error channel; a digits-only literal fails only on i64 overflow.
119
+ #[allow(clippy::panic)]
120
+ let value = expr
121
+ .parse::<i64>()
122
+ .unwrap_or_else(|_| panic!("bad integer literal {expr:?}"));
123
+ return Resolver::IntegerLiteral(value);
89
124
  }
90
125
  if is_float_literal(expr) {
91
- return Resolver::FloatLiteral(expr.parse::<f64>().unwrap_or_else(|_| panic!("bad float literal {expr:?}")));
126
+ // reason: `parse` has no error channel; `is_float_literal` vetted the text above.
127
+ #[allow(clippy::panic)]
128
+ let value = expr
129
+ .parse::<f64>()
130
+ .unwrap_or_else(|_| panic!("bad float literal {expr:?}"));
131
+ return Resolver::FloatLiteral(value);
92
132
  }
93
133
  if let Some(s) = quoted(expr) {
94
134
  return Resolver::StringLiteral(s.to_string());
@@ -108,19 +148,36 @@ pub fn parse(expr: &str) -> Resolver {
108
148
 
109
149
  if let Some((operator, left, right)) = split_last_binary(expr, b"+-") {
110
150
  let (left, right) = (Box::new(parse(left)), Box::new(parse(right)));
111
- return if operator == b'+' { Resolver::Addition(left, right) } else { Resolver::Subtraction(left, right) };
151
+ return if operator == b'+' {
152
+ Resolver::Addition(left, right)
153
+ } else {
154
+ Resolver::Subtraction(left, right)
155
+ };
112
156
  }
113
157
  if let Some((operator, left, right)) = split_last_binary(expr, b"*/") {
114
158
  let (left, right) = (Box::new(parse(left)), Box::new(parse(right)));
115
- return if operator == b'*' { Resolver::Multiplication(left, right) } else { Resolver::Division(left, right) };
159
+ return if operator == b'*' {
160
+ Resolver::Multiplication(left, right)
161
+ } else {
162
+ Resolver::Division(left, right)
163
+ };
116
164
  }
117
165
  if grouped(expr) {
118
166
  return parse(&expr[1..expr.len() - 1]);
119
167
  }
120
168
 
121
- if let Some((receiver, test)) = match_suffix(expr, &SIGN_TESTS.map(|(s, _)| s)) {
122
- let symbol = SIGN_TESTS.iter().find(|(s, _)| *s == test).unwrap().1;
123
- return Resolver::SignTest { operator: find_operator(symbol), receiver: Box::new(parse(receiver)) };
169
+ if let Some((receiver, symbol)) =
170
+ match_suffix(expr, &SIGN_TESTS.map(|(s, _)| s)).and_then(|(receiver, test)| {
171
+ SIGN_TESTS
172
+ .iter()
173
+ .find(|(s, _)| *s == test)
174
+ .map(|&(_, symbol)| (receiver, symbol))
175
+ })
176
+ {
177
+ return Resolver::SignTest {
178
+ operator: find_operator(symbol),
179
+ receiver: Box::new(parse(receiver)),
180
+ };
124
181
  }
125
182
 
126
183
  if let Some(inner) = strip_suffix_dotted(expr, "empty?") {
@@ -131,7 +188,10 @@ pub fn parse(expr: &str) -> Resolver {
131
188
  }
132
189
 
133
190
  if let Some((receiver, divisor)) = match_call(expr, ".modulo(") {
134
- return Resolver::Modulo { receiver: Box::new(parse(receiver)), divisor: Box::new(parse(divisor)) };
191
+ return Resolver::Modulo {
192
+ receiver: Box::new(parse(receiver)),
193
+ divisor: Box::new(parse(divisor)),
194
+ };
135
195
  }
136
196
 
137
197
  if let Some(inner) = strip_suffix_dotted(expr, "size") {
@@ -139,28 +199,44 @@ pub fn parse(expr: &str) -> Resolver {
139
199
  }
140
200
 
141
201
  if let Some((receiver, pattern, flags)) = match_regex(expr) {
142
- return Resolver::MatchesRegex { receiver: Box::new(parse(receiver)), pattern: pattern.to_string(), flags: flags.to_string() };
202
+ return Resolver::MatchesRegex {
203
+ receiver: Box::new(parse(receiver)),
204
+ pattern: pattern.to_string(),
205
+ flags: flags.to_string(),
206
+ };
143
207
  }
144
208
 
145
209
  for (suffix, negated) in [("present?", false), ("blank?", true)] {
146
210
  if let Some(inner) = strip_suffix_dotted(expr, suffix) {
147
- return Resolver::Presence { receiver: Box::new(parse(inner)), negated };
211
+ return Resolver::Presence {
212
+ receiver: Box::new(parse(inner)),
213
+ negated,
214
+ };
148
215
  }
149
216
  }
150
217
  for (suffix, negated) in [("set?", false), ("unset?", true)] {
151
218
  if let Some(inner) = strip_suffix_dotted(expr, suffix) {
152
- return Resolver::Assignment { receiver: Box::new(parse(inner)), negated };
219
+ return Resolver::Assignment {
220
+ receiver: Box::new(parse(inner)),
221
+ negated,
222
+ };
153
223
  }
154
224
  }
155
225
 
156
226
  for (suffix, side) in [("strip", "both"), ("lstrip", "left"), ("rstrip", "right")] {
157
227
  if let Some(inner) = strip_suffix_dotted(expr, suffix) {
158
- return Resolver::Strip { receiver: Box::new(parse(inner)), side };
228
+ return Resolver::Strip {
229
+ receiver: Box::new(parse(inner)),
230
+ side,
231
+ };
159
232
  }
160
233
  }
161
234
 
162
235
  if let Some((receiver, separator)) = match_string_call(expr, "split") {
163
- return Resolver::Split { receiver: Box::new(parse(receiver)), separator: separator.to_string() };
236
+ return Resolver::Split {
237
+ receiver: Box::new(parse(receiver)),
238
+ separator: separator.to_string(),
239
+ };
164
240
  }
165
241
 
166
242
  if let Some(inner) = strip_suffix_dotted(expr, "first") {
@@ -171,10 +247,16 @@ pub fn parse(expr: &str) -> Resolver {
171
247
  }
172
248
 
173
249
  if let Some((receiver, substring)) = match_string_call(expr, "start_with?") {
174
- return Resolver::StartsWith { receiver: Box::new(parse(receiver)), substring: substring.to_string() };
250
+ return Resolver::StartsWith {
251
+ receiver: Box::new(parse(receiver)),
252
+ substring: substring.to_string(),
253
+ };
175
254
  }
176
255
  if let Some((receiver, substring)) = match_string_call(expr, "end_with?") {
177
- return Resolver::EndsWith { receiver: Box::new(parse(receiver)), substring: substring.to_string() };
256
+ return Resolver::EndsWith {
257
+ receiver: Box::new(parse(receiver)),
258
+ substring: substring.to_string(),
259
+ };
178
260
  }
179
261
 
180
262
  // Last before the `Lookup` catch-all, matching `resolver.rb`.
@@ -186,8 +268,12 @@ pub fn parse(expr: &str) -> Resolver {
186
268
  }
187
269
 
188
270
  /// Block-opener suffixes, in the order Ruby's pattern alternates them.
189
- const BLOCK_OPENERS: [(&str, Option<BlockMode>); 4] =
190
- [("all?", Some(BlockMode::All)), ("any?", Some(BlockMode::Any)), ("none?", Some(BlockMode::None)), ("find", None)];
271
+ const BLOCK_OPENERS: [(&str, Option<BlockMode>); 4] = [
272
+ ("all?", Some(BlockMode::All)),
273
+ ("any?", Some(BlockMode::Any)),
274
+ ("none?", Some(BlockMode::None)),
275
+ ("find", None),
276
+ ];
191
277
 
192
278
  /// Hand-matched `/\A(.+?)\.(all?|any?|none?|find)\s*\{\s*\|(\w+)\|\s*/m`: the earliest
193
279
  /// `.suffix { |param|`, then a brace-balanced body. Only `find` may trail a dotted path.
@@ -248,20 +334,34 @@ fn parse_block_opener(expr: &str) -> Option<Resolver> {
248
334
  if !(trailing.is_empty() || trailing.starts_with('.')) {
249
335
  return None;
250
336
  }
251
- let path = if trailing.is_empty() { Vec::new() } else { trailing[1..].split('.').map(str::to_string).collect() };
252
- Some(Resolver::Find { receiver, param, predicate, path })
337
+ let path = if trailing.is_empty() {
338
+ Vec::new()
339
+ } else {
340
+ trailing[1..].split('.').map(str::to_string).collect()
341
+ };
342
+ Some(Resolver::Find {
343
+ receiver,
344
+ param,
345
+ predicate,
346
+ path,
347
+ })
253
348
  }
254
349
  Some(mode) => {
255
350
  if !trailing.is_empty() {
256
351
  return None;
257
352
  }
258
- Some(Resolver::BlockPredicate { mode, receiver, param, predicate })
353
+ Some(Resolver::BlockPredicate {
354
+ mode,
355
+ receiver,
356
+ param,
357
+ predicate,
358
+ })
259
359
  }
260
360
  }
261
361
  }
262
362
 
263
363
  /// Index of the `}` closing the block whose body starts at `start`, honouring quotes.
264
- fn matching_brace(expr: &str, start: usize) -> Option<usize> {
364
+ const fn matching_brace(expr: &str, start: usize) -> Option<usize> {
265
365
  let bytes = expr.as_bytes();
266
366
  let mut depth = 1;
267
367
  let mut quote: Option<u8> = None;
@@ -310,7 +410,9 @@ fn is_float_literal(expr: &str) -> bool {
310
410
  let Some(dot) = s.find('.') else { return false };
311
411
  let (int_part, rest) = s.split_at(dot);
312
412
  let frac_part = &rest[1..];
313
- int_part.bytes().all(|b| b.is_ascii_digit()) && !frac_part.is_empty() && frac_part.bytes().all(|b| b.is_ascii_digit())
413
+ int_part.bytes().all(|b| b.is_ascii_digit())
414
+ && !frac_part.is_empty()
415
+ && frac_part.bytes().all(|b| b.is_ascii_digit())
314
416
  }
315
417
 
316
418
  fn quoted(expr: &str) -> Option<&str> {
@@ -368,7 +470,12 @@ fn array_elements(expr: &str) -> Option<Vec<String>> {
368
470
  }
369
471
  elements.push(current.trim().to_string());
370
472
 
371
- Some(elements.into_iter().filter(|element| !element.is_empty()).collect())
473
+ Some(
474
+ elements
475
+ .into_iter()
476
+ .filter(|element| !element.is_empty())
477
+ .collect(),
478
+ )
372
479
  }
373
480
 
374
481
  /// The canonical text of an arithmetic expression over names and numbers, as the Ruby DSL prints
@@ -377,7 +484,10 @@ fn array_elements(expr: &str) -> Option<Vec<String>> {
377
484
  pub fn arithmetic_text(expr: &str) -> Option<String> {
378
485
  let node = parse(expr);
379
486
  match node {
380
- Resolver::Addition(..) | Resolver::Subtraction(..) | Resolver::Multiplication(..) | Resolver::Division(..) => render_arithmetic(&node),
487
+ Resolver::Addition(..)
488
+ | Resolver::Subtraction(..)
489
+ | Resolver::Multiplication(..)
490
+ | Resolver::Division(..) => render_arithmetic(&node),
381
491
  _ => None,
382
492
  }
383
493
  }
@@ -395,11 +505,15 @@ fn render_arithmetic(node: &Resolver) -> Option<String> {
395
505
  }
396
506
  }
397
507
 
398
- fn binding_strength(operator: char) -> u8 {
399
- if matches!(operator, '*' | '/') { 2 } else { 1 }
508
+ const fn binding_strength(operator: char) -> u8 {
509
+ if matches!(operator, '*' | '/') {
510
+ 2
511
+ } else {
512
+ 1
513
+ }
400
514
  }
401
515
 
402
- fn node_strength(node: &Resolver) -> u8 {
516
+ const fn node_strength(node: &Resolver) -> u8 {
403
517
  match node {
404
518
  Resolver::Addition(..) | Resolver::Subtraction(..) => 1,
405
519
  Resolver::Multiplication(..) | Resolver::Division(..) => 2,
@@ -414,9 +528,19 @@ fn render_binary(left: &Resolver, operator: char, right: &Resolver) -> Option<St
414
528
  let side = |node: &Resolver, on_right: bool| -> Option<String> {
415
529
  let text = render_arithmetic(node)?;
416
530
  let held = node_strength(node);
417
- Some(if held != 0 && (held < strength || (on_right && held == strength)) { format!("({text})") } else { text })
531
+ Some(
532
+ if held != 0 && (held < strength || (on_right && held == strength)) {
533
+ format!("({text})")
534
+ } else {
535
+ text
536
+ },
537
+ )
418
538
  };
419
- Some(format!("{} {operator} {}", side(left, false)?, side(right, true)?))
539
+ Some(format!(
540
+ "{} {operator} {}",
541
+ side(left, false)?,
542
+ side(right, true)?
543
+ ))
420
544
  }
421
545
 
422
546
  /// The operator and the texts either side of the LAST top-level binary operator in `operators`, so
@@ -439,7 +563,9 @@ fn split_last_binary<'a>(expr: &'a str, operators: &[u8]) -> Option<(u8, &'a str
439
563
  b'"' | b'\'' => quote = Some(byte),
440
564
  b'(' | b'{' | b'[' => depth += 1,
441
565
  b')' | b'}' | b']' => depth -= 1,
442
- _ if depth == 0 && operators.contains(&byte) && binary_position(bytes, index) => found = Some(index),
566
+ _ if depth == 0 && operators.contains(&byte) && binary_position(bytes, index) => {
567
+ found = Some(index);
568
+ }
443
569
  _ => {}
444
570
  }
445
571
  }
@@ -451,7 +577,10 @@ fn binary_position(bytes: &[u8], index: usize) -> bool {
451
577
  if bytes[index] != b'-' {
452
578
  return true;
453
579
  }
454
- let before = bytes[..index].iter().rev().find(|b| !b.is_ascii_whitespace());
580
+ let before = bytes[..index]
581
+ .iter()
582
+ .rev()
583
+ .find(|b| !b.is_ascii_whitespace());
455
584
  matches!(before, Some(b) if b.is_ascii_alphanumeric() || matches!(b, b'_' | b')' | b']' | b'"' | b'\''))
456
585
  }
457
586
 
@@ -505,7 +634,10 @@ fn match_regex(expr: &str) -> Option<(&str, &str, &str)> {
505
634
  if !flags.bytes().all(|b| b.is_ascii_lowercase()) {
506
635
  return None;
507
636
  }
508
- let index = body[..close + 1].rmatch_indices(MARKER).map(|(i, _)| i).find(|i| *i > 0 && i + MARKER.len() <= close)?;
637
+ let index = body[..=close]
638
+ .rmatch_indices(MARKER)
639
+ .map(|(i, _)| i)
640
+ .find(|i| *i > 0 && i + MARKER.len() <= close)?;
509
641
  Some((&expr[..index], &body[index + MARKER.len()..close], flags))
510
642
  }
511
643
 
@@ -536,7 +668,11 @@ mod tests {
536
668
 
537
669
  fn regex_parts(node: Resolver) -> (Resolver, String, String) {
538
670
  match node {
539
- Resolver::MatchesRegex { receiver, pattern, flags } => (*receiver, pattern, flags),
671
+ Resolver::MatchesRegex {
672
+ receiver,
673
+ pattern,
674
+ flags,
675
+ } => (*receiver, pattern, flags),
540
676
  other => panic!("expected MatchesRegex, got {other:?}"),
541
677
  }
542
678
  }
@@ -566,41 +702,74 @@ mod tests {
566
702
 
567
703
  /// The emitted JSON with its pretty-printing whitespace removed (no test string has spaces).
568
704
  fn compact(text: &str) -> String {
569
- crate::emit::write(&crate::expr::ast_json::emit_predicate(text)).split_whitespace().collect()
705
+ crate::emit::write(&crate::expr::ast_json::emit_predicate(text))
706
+ .split_whitespace()
707
+ .collect()
570
708
  }
571
709
 
572
710
  #[test]
573
711
  fn present_and_blank_are_one_node_told_apart_by_negated() {
574
- assert!(matches!(parse("name.present?"), Resolver::Presence { negated: false, .. }));
575
- assert!(matches!(parse("name.blank?"), Resolver::Presence { negated: true, .. }));
712
+ assert!(matches!(
713
+ parse("name.present?"),
714
+ Resolver::Presence { negated: false, .. }
715
+ ));
716
+ assert!(matches!(
717
+ parse("name.blank?"),
718
+ Resolver::Presence { negated: true, .. }
719
+ ));
576
720
  }
577
721
 
578
722
  #[test]
579
723
  fn set_and_unset_are_one_node_told_apart_by_negated() {
580
- assert!(matches!(parse("note.set?"), Resolver::Assignment { negated: false, .. }));
581
- assert!(matches!(parse("note.unset?"), Resolver::Assignment { negated: true, .. }));
724
+ assert!(matches!(
725
+ parse("note.set?"),
726
+ Resolver::Assignment { negated: false, .. }
727
+ ));
728
+ assert!(matches!(
729
+ parse("note.unset?"),
730
+ Resolver::Assignment { negated: true, .. }
731
+ ));
582
732
  }
583
733
 
584
734
  #[test]
585
735
  fn split_keeps_its_separator_verbatim_and_chains_into_first_and_last() {
586
- let Resolver::First(inner) = parse("path.split(\"/\").first") else { panic!("expected First") };
587
- let Resolver::Split { receiver, separator } = *inner else { panic!("expected Split") };
736
+ let Resolver::First(inner) = parse("path.split(\"/\").first") else {
737
+ panic!("expected First")
738
+ };
739
+ let Resolver::Split {
740
+ receiver,
741
+ separator,
742
+ } = *inner
743
+ else {
744
+ panic!("expected Split")
745
+ };
588
746
  assert_eq!(separator, "/");
589
747
  assert!(matches!(*receiver, Resolver::Lookup(_)));
590
- assert!(matches!(parse("path.split(\"::\").last"), Resolver::Last(_)));
748
+ assert!(matches!(
749
+ parse("path.split(\"::\").last"),
750
+ Resolver::Last(_)
751
+ ));
591
752
  }
592
753
 
593
754
  #[test]
594
755
  fn start_and_end_with_take_a_quoted_argument() {
595
- let Resolver::StartsWith { substring, .. } = parse("branch.value.start_with?(\"qa/\")") else { panic!() };
756
+ let Resolver::StartsWith { substring, .. } = parse("branch.value.start_with?(\"qa/\")")
757
+ else {
758
+ panic!()
759
+ };
596
760
  assert_eq!(substring, "qa/");
597
- let Resolver::EndsWith { substring, .. } = parse("name.end_with?(\"x, y\")") else { panic!() };
761
+ let Resolver::EndsWith { substring, .. } = parse("name.end_with?(\"x, y\")") else {
762
+ panic!()
763
+ };
598
764
  assert_eq!(substring, "x, y");
599
765
  }
600
766
 
601
767
  #[test]
602
768
  fn a_call_without_a_quoted_argument_stays_a_lookup() {
603
- assert!(matches!(parse("name.start_with?(prefix)"), Resolver::Lookup(_)));
769
+ assert!(matches!(
770
+ parse("name.start_with?(prefix)"),
771
+ Resolver::Lookup(_)
772
+ ));
604
773
  assert!(matches!(parse("name.split(sep)"), Resolver::Lookup(_)));
605
774
  assert!(matches!(parse("name.split(\"a\"b\")"), Resolver::Lookup(_)));
606
775
  }
@@ -609,18 +778,48 @@ mod tests {
609
778
  fn emits_ruby_key_order_for_each_new_node() {
610
779
  let a = r#"{"op":"lookup","path":["a"]}"#;
611
780
  let cases = [
612
- ("a.present?", format!(r#"{{"op":"presence","receiver":{a},"negated":false}}"#)),
613
- ("a.blank?", format!(r#"{{"op":"presence","receiver":{a},"negated":true}}"#)),
614
- ("a.set?", format!(r#"{{"op":"assignment","receiver":{a},"negated":false}}"#)),
615
- ("a.unset?", format!(r#"{{"op":"assignment","receiver":{a},"negated":true}}"#)),
616
- ("a.split(\"/\")", format!(r#"{{"op":"split","receiver":{a},"separator":"/"}}"#)),
617
- ("a.strip", format!(r#"{{"op":"strip","receiver":{a},"side":"both"}}"#)),
618
- ("a.lstrip", format!(r#"{{"op":"strip","receiver":{a},"side":"left"}}"#)),
619
- ("a.rstrip", format!(r#"{{"op":"strip","receiver":{a},"side":"right"}}"#)),
781
+ (
782
+ "a.present?",
783
+ format!(r#"{{"op":"presence","receiver":{a},"negated":false}}"#),
784
+ ),
785
+ (
786
+ "a.blank?",
787
+ format!(r#"{{"op":"presence","receiver":{a},"negated":true}}"#),
788
+ ),
789
+ (
790
+ "a.set?",
791
+ format!(r#"{{"op":"assignment","receiver":{a},"negated":false}}"#),
792
+ ),
793
+ (
794
+ "a.unset?",
795
+ format!(r#"{{"op":"assignment","receiver":{a},"negated":true}}"#),
796
+ ),
797
+ (
798
+ "a.split(\"/\")",
799
+ format!(r#"{{"op":"split","receiver":{a},"separator":"/"}}"#),
800
+ ),
801
+ (
802
+ "a.strip",
803
+ format!(r#"{{"op":"strip","receiver":{a},"side":"both"}}"#),
804
+ ),
805
+ (
806
+ "a.lstrip",
807
+ format!(r#"{{"op":"strip","receiver":{a},"side":"left"}}"#),
808
+ ),
809
+ (
810
+ "a.rstrip",
811
+ format!(r#"{{"op":"strip","receiver":{a},"side":"right"}}"#),
812
+ ),
620
813
  ("a.first", format!(r#"{{"op":"first","receiver":{a}}}"#)),
621
814
  ("a.last", format!(r#"{{"op":"last","receiver":{a}}}"#)),
622
- ("a.start_with?(\"x\")", format!(r#"{{"op":"starts_with","receiver":{a},"substring":"x"}}"#)),
623
- ("a.end_with?(\"y\")", format!(r#"{{"op":"ends_with","receiver":{a},"substring":"y"}}"#)),
815
+ (
816
+ "a.start_with?(\"x\")",
817
+ format!(r#"{{"op":"starts_with","receiver":{a},"substring":"x"}}"#),
818
+ ),
819
+ (
820
+ "a.end_with?(\"y\")",
821
+ format!(r#"{{"op":"ends_with","receiver":{a},"substring":"y"}}"#),
822
+ ),
624
823
  ];
625
824
  for (text, expected) in cases {
626
825
  assert_eq!(compact(text), expected, "{text}");
@@ -630,19 +829,27 @@ mod tests {
630
829
  #[test]
631
830
  fn splits_arithmetic_by_level_and_left_to_right() {
632
831
  assert!(matches!(parse("a + b * c"), Resolver::Addition(..)));
633
- assert!(matches!(parse("a - b - c"), Resolver::Subtraction(left, _) if matches!(*left, Resolver::Subtraction(..))));
634
- assert!(matches!(parse("a * b / c"), Resolver::Division(left, _) if matches!(*left, Resolver::Multiplication(..))));
832
+ assert!(
833
+ matches!(parse("a - b - c"), Resolver::Subtraction(left, _) if matches!(*left, Resolver::Subtraction(..)))
834
+ );
835
+ assert!(
836
+ matches!(parse("a * b / c"), Resolver::Division(left, _) if matches!(*left, Resolver::Multiplication(..)))
837
+ );
635
838
  }
636
839
 
637
840
  #[test]
638
841
  fn a_minus_is_binary_only_after_an_operand() {
639
- assert!(matches!(parse("a - -5"), Resolver::Subtraction(_, right) if matches!(*right, Resolver::IntegerLiteral(-5))));
842
+ assert!(
843
+ matches!(parse("a - -5"), Resolver::Subtraction(_, right) if matches!(*right, Resolver::IntegerLiteral(-5)))
844
+ );
640
845
  assert!(matches!(parse("-5"), Resolver::IntegerLiteral(-5)));
641
846
  }
642
847
 
643
848
  #[test]
644
849
  fn a_parenthesised_group_reads_as_what_it_wraps() {
645
- assert!(matches!(parse("(a + b) * c"), Resolver::Multiplication(left, _) if matches!(*left, Resolver::Addition(..))));
850
+ assert!(
851
+ matches!(parse("(a + b) * c"), Resolver::Multiplication(left, _) if matches!(*left, Resolver::Addition(..)))
852
+ );
646
853
  }
647
854
 
648
855
  #[test]