hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -1,15 +1,13 @@
1
1
  use super::{instances_for, respond};
2
- use crate::dispatch;
2
+ use crate::clock::unix_now;
3
3
  use crate::commerce_ir::newsletter_issues_provider;
4
+ use crate::crypto::constant_time_eq;
5
+ use crate::dispatch;
4
6
  use crate::extension::loaded_ir;
5
- use crate::journal::LineageConfig;
6
- use crate::lambda_client::LambdaInvoker;
7
+ use crate::host::Host;
7
8
  use hmac::{Hmac, Mac};
8
9
  use serde_json::{json, Value};
9
10
  use sha2::Sha256;
10
- use std::path::Path;
11
- use tokio::sync::Mutex;
12
- use tokio_postgres::Client;
13
11
 
14
12
  type HmacSha256 = Hmac<Sha256>;
15
13
 
@@ -32,34 +30,48 @@ enum Tracked {
32
30
  }
33
31
 
34
32
  impl Tracked {
35
- fn command_suffix(&self) -> &'static str {
33
+ const fn command_suffix(&self) -> &'static str {
36
34
  match self {
37
- Tracked::Open => "RecordOpen",
38
- Tracked::Click => "RecordClick",
35
+ Self::Open => "RecordOpen",
36
+ Self::Click => "RecordClick",
39
37
  }
40
38
  }
41
39
  }
42
-
43
- #[allow(clippy::too_many_arguments)]
44
40
  pub(super) async fn resend_webhook_route(
45
41
  method: &str,
46
42
  path: &str,
47
43
  raw_body: &str,
48
44
  headers: Option<&Value>,
49
- client: &Mutex<Client>,
50
- wasm_path: &Path,
51
- config: &LineageConfig,
52
- invoker: &dyn LambdaInvoker,
45
+ host: Host<'_>,
53
46
  ) -> Option<Value> {
47
+ let Host {
48
+ client, wasm_path, ..
49
+ } = host;
54
50
  if method != "POST" || path != "/webhooks/resend" {
55
51
  return None;
56
52
  }
57
- let header = |name: &str| headers.and_then(|h| h.get(name)).and_then(|v| v.as_str()).unwrap_or("");
58
- let secret = std::env::var(SECRET_VARIABLE).unwrap_or_default();
53
+ let header = |name: &str| {
54
+ headers
55
+ .and_then(|h| h.get(name))
56
+ .and_then(|v| v.as_str())
57
+ .unwrap_or("")
58
+ };
59
+ let secret = crate::secrets::var(SECRET_VARIABLE).unwrap_or_default();
59
60
  if secret.trim().is_empty() {
60
- return Some(respond(503, "text/plain", &format!("{SECRET_VARIABLE} is not set, so Resend events cannot be verified")));
61
+ return Some(respond(
62
+ 503,
63
+ "text/plain",
64
+ &format!("{SECRET_VARIABLE} is not set, so Resend events cannot be verified"),
65
+ ));
61
66
  }
62
- if let Err(reason) = verify(raw_body, header("svix-id"), header("svix-timestamp"), header("svix-signature"), secret.trim(), unix_now()) {
67
+ if let Err(reason) = verify(
68
+ raw_body,
69
+ header("svix-id"),
70
+ header("svix-timestamp"),
71
+ header("svix-signature"),
72
+ secret.trim(),
73
+ unix_now(),
74
+ ) {
63
75
  return Some(respond(400, "text/plain", &reason));
64
76
  }
65
77
  let event: Value = match serde_json::from_str(raw_body) {
@@ -74,14 +86,21 @@ pub(super) async fn resend_webhook_route(
74
86
  };
75
87
  // `record_delivery` names `<Chapter>::Delivery.Record`; the open and click
76
88
  // commands sit on the same aggregate.
77
- let Some(delivery_aggregate) = provider.record_delivery.rsplit_once('.').map(|(aggregate, _)| aggregate.to_string()) else {
89
+ let Some(delivery_aggregate) = provider
90
+ .record_delivery
91
+ .rsplit_once('.')
92
+ .map(|(aggregate, _)| aggregate.to_string())
93
+ else {
78
94
  return Some(respond(200, "text/plain", ""));
79
95
  };
80
96
  let read = match dispatch::read(client, wasm_path).await {
81
97
  Ok(r) => r,
82
98
  Err(e) => return Some(respond(500, "text/plain", &format!("{e:#}"))),
83
99
  };
84
- let Some(delivery_id) = delivery_for(&instances_for(&read, &format!("{delivery_aggregate}#")), &message_id) else {
100
+ let Some(delivery_id) = delivery_for(
101
+ &instances_for(&read, &format!("{delivery_aggregate}#")),
102
+ &message_id,
103
+ ) else {
85
104
  // A message this site did not send as a newsletter (a receipt, a
86
105
  // confirmation): nothing to record, and a 200 stops Resend's retries.
87
106
  return Some(respond(200, "text/plain", ""));
@@ -90,16 +109,12 @@ pub(super) async fn resend_webhook_route(
90
109
  let verb = format!("{delivery_aggregate}.{}", tracked.command_suffix());
91
110
  // A refusal is a repeat (the first open or click is already recorded):
92
111
  // benign, and 200 tells Resend not to retry.
93
- match dispatch::handle_routed(client, wasm_path, &verb, json!(delivery_id), occurred_at, None, config, invoker).await {
112
+ match dispatch::handle_routed(host, &verb, json!(delivery_id), occurred_at, None).await {
94
113
  Err(e) => Some(respond(500, "text/plain", &format!("{e:#}"))),
95
114
  Ok(_) => Some(respond(200, "text/plain", "")),
96
115
  }
97
116
  }
98
117
 
99
- fn unix_now() -> i64 {
100
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)
101
- }
102
-
103
118
  // What the event is and which sent message it is about; `None` for any event
104
119
  // type nothing here records.
105
120
  fn tracked_event(event: &Value) -> Option<(Tracked, String)> {
@@ -126,9 +141,16 @@ fn parse_rfc3339_seconds(text: &str) -> Option<i64> {
126
141
  let (date, time) = text.split_once('T')?;
127
142
  let mut ymd = date.split('-').map(|p| p.parse::<i64>().ok());
128
143
  let (year, month, day) = (ymd.next()??, ymd.next()??, ymd.next()??);
129
- let clock: String = time.chars().take_while(|c| c.is_ascii_digit() || *c == ':').collect();
144
+ let clock: String = time
145
+ .chars()
146
+ .take_while(|c| c.is_ascii_digit() || *c == ':')
147
+ .collect();
130
148
  let mut hms = clock.split(':').map(|p| p.parse::<i64>().ok());
131
- let (hour, minute, second) = (hms.next()??, hms.next()??, hms.next().flatten().unwrap_or(0));
149
+ let (hour, minute, second) = (
150
+ hms.next()??,
151
+ hms.next()??,
152
+ hms.next().flatten().unwrap_or(0),
153
+ );
132
154
  // Days since 1970-01-01 (Howard Hinnant's civil-from-days, inverted).
133
155
  let year = if month <= 2 { year - 1 } else { year };
134
156
  let era = year.div_euclid(400);
@@ -136,7 +158,7 @@ fn parse_rfc3339_seconds(text: &str) -> Option<i64> {
136
158
  let shifted_month = (month + 9) % 12;
137
159
  let day_of_year = (153 * shifted_month + 2) / 5 + day - 1;
138
160
  let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
139
- let days = era * 146097 + day_of_era - 719468;
161
+ let days = era * 146_097 + day_of_era - 719_468;
140
162
  Some(days * 86400 + hour * 3600 + minute * 60 + second)
141
163
  }
142
164
 
@@ -144,63 +166,57 @@ fn parse_rfc3339_seconds(text: &str) -> Option<i64> {
144
166
  fn delivery_for(deliveries: &[(String, Value)], message_id: &str) -> Option<String> {
145
167
  deliveries
146
168
  .iter()
147
- .find(|(_, state)| state.get("message_id").and_then(|m| m.get("value")).and_then(|v| v.as_str()) == Some(message_id))
169
+ .find(|(_, state)| {
170
+ state
171
+ .get("message_id")
172
+ .and_then(|m| m.get("value"))
173
+ .and_then(|v| v.as_str())
174
+ == Some(message_id)
175
+ })
148
176
  .map(|(id, _)| id.clone())
149
177
  }
150
178
 
151
179
  // Svix signs `<id>.<timestamp>.<raw body>` with the key inside the `whsec_`
152
180
  // secret, base64 (`v1,<signature>`, several space-separated while a secret is
153
181
  // being rotated). `now` is a parameter so a test can hold time fixed.
154
- fn verify(payload: &str, id: &str, timestamp: &str, signatures: &str, secret: &str, now: i64) -> Result<(), String> {
182
+ fn verify(
183
+ payload: &str,
184
+ id: &str,
185
+ timestamp: &str,
186
+ signatures: &str,
187
+ secret: &str,
188
+ now: i64,
189
+ ) -> Result<(), String> {
155
190
  if id.is_empty() || timestamp.is_empty() || signatures.is_empty() {
156
191
  return Err("missing svix-id, svix-timestamp or svix-signature header".to_string());
157
192
  }
158
- let sent: i64 = timestamp.parse().map_err(|_| "svix-timestamp is not a number".to_string())?;
193
+ let sent: i64 = timestamp
194
+ .parse()
195
+ .map_err(|_| "svix-timestamp is not a number".to_string())?;
159
196
  if (now - sent).abs() > TOLERANCE_SECONDS {
160
- return Err(format!("timestamp {sent} is outside the {TOLERANCE_SECONDS}s tolerance (now: {now})"));
197
+ return Err(format!(
198
+ "timestamp {sent} is outside the {TOLERANCE_SECONDS}s tolerance (now: {now})"
199
+ ));
161
200
  }
162
- let key = base64_decode(secret.strip_prefix("whsec_").unwrap_or(secret)).ok_or_else(|| "webhook secret is not valid base64".to_string())?;
163
- let mut mac = HmacSha256::new_from_slice(&key).map_err(|e| format!("webhook secret unusable as an HMAC key: {e}"))?;
201
+ let key = crate::b64::standard_decode(secret.strip_prefix("whsec_").unwrap_or(secret))
202
+ .ok_or_else(|| "webhook secret is not valid base64".to_string())?;
203
+ let mut mac = HmacSha256::new_from_slice(&key)
204
+ .map_err(|e| format!("webhook secret unusable as an HMAC key: {e}"))?;
164
205
  mac.update(format!("{id}.{timestamp}.{payload}").as_bytes());
165
206
  let expected = mac.finalize().into_bytes();
166
207
  let matches = signatures
167
208
  .split(' ')
168
209
  .filter_map(|candidate| candidate.strip_prefix("v1,"))
169
- .filter_map(base64_decode)
210
+ .filter_map(crate::b64::standard_decode)
170
211
  .any(|signature| constant_time_eq(&signature, &expected));
171
212
  if matches {
172
213
  Ok(())
173
214
  } else {
174
- Err("signature does not match — wrong secret, or the payload was tampered with in transit".to_string())
175
- }
176
- }
177
-
178
- fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
179
- a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
180
- }
181
-
182
- // Standard base64, padded or not; `None` on any other character.
183
- fn base64_decode(text: &str) -> Option<Vec<u8>> {
184
- let mut out = Vec::with_capacity(text.len() * 3 / 4);
185
- let (mut bits, mut have) = (0u32, 0u32);
186
- for c in text.trim_end_matches('=').bytes() {
187
- let value = match c {
188
- b'A'..=b'Z' => c - b'A',
189
- b'a'..=b'z' => c - b'a' + 26,
190
- b'0'..=b'9' => c - b'0' + 52,
191
- b'+' => 62,
192
- b'/' => 63,
193
- _ => return None,
194
- } as u32;
195
- bits = (bits << 6) | value;
196
- have += 6;
197
- if have >= 8 {
198
- have -= 8;
199
- out.push((bits >> have) as u8);
200
- bits &= (1 << have) - 1;
201
- }
215
+ Err(
216
+ "signature does not match — wrong secret, or the payload was tampered with in transit"
217
+ .to_string(),
218
+ )
202
219
  }
203
- Some(out)
204
220
  }
205
221
 
206
222
  #[cfg(test)]
@@ -210,10 +226,14 @@ mod tests {
210
226
  const SECRET: &str = "whsec_c2VjcmV0LWtleQ=="; // "secret-key"
211
227
 
212
228
  fn base64_encode(bytes: &[u8]) -> String {
213
- const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
229
+ const TABLE: &[u8; 64] =
230
+ b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
214
231
  let mut out = String::new();
215
232
  for chunk in bytes.chunks(3) {
216
- let n = chunk.iter().enumerate().fold(0u32, |acc, (i, b)| acc | (*b as u32) << (16 - 8 * i));
233
+ let n = chunk
234
+ .iter()
235
+ .enumerate()
236
+ .fold(0u32, |acc, (i, b)| acc | u32::from(*b) << (16 - 8 * i));
217
237
  for i in 0..=chunk.len() {
218
238
  out.push(TABLE[(n >> (18 - 6 * i) & 63) as usize] as char);
219
239
  }
@@ -232,10 +252,13 @@ mod tests {
232
252
  fn base64_round_trips_with_and_without_padding() {
233
253
  for sample in [&b""[..], b"f", b"fo", b"foo", b"foob", b"secret-key"] {
234
254
  let encoded = base64_encode(sample);
235
- assert_eq!(base64_decode(&encoded).unwrap(), sample);
236
- assert_eq!(base64_decode(encoded.trim_end_matches('=')).unwrap(), sample);
255
+ assert_eq!(crate::b64::standard_decode(&encoded).unwrap(), sample);
256
+ assert_eq!(
257
+ crate::b64::standard_decode(encoded.trim_end_matches('=')).unwrap(),
258
+ sample
259
+ );
237
260
  }
238
- assert!(base64_decode("not base64!").is_none());
261
+ assert!(crate::b64::standard_decode("not base64!").is_none());
239
262
  }
240
263
 
241
264
  #[test]
@@ -260,7 +283,15 @@ mod tests {
260
283
  #[test]
261
284
  fn a_stale_or_unsigned_event_is_refused() {
262
285
  let signature = signed("msg_1", "1000", "{}");
263
- assert!(verify("{}", "msg_1", "1000", &signature, SECRET, 1000 + TOLERANCE_SECONDS + 1).is_err());
286
+ assert!(verify(
287
+ "{}",
288
+ "msg_1",
289
+ "1000",
290
+ &signature,
291
+ SECRET,
292
+ 1000 + TOLERANCE_SECONDS + 1
293
+ )
294
+ .is_err());
264
295
  assert!(verify("{}", "", "1000", &signature, SECRET, 1000).is_err());
265
296
  assert!(verify("{}", "msg_1", "soon", &signature, SECRET, 1000).is_err());
266
297
  }
@@ -268,27 +299,54 @@ mod tests {
268
299
  #[test]
269
300
  fn opens_and_clicks_name_their_message_and_everything_else_is_ignored() {
270
301
  let open = json!({ "type": "email.opened", "data": { "email_id": "re_1" } });
271
- assert_eq!(tracked_event(&open), Some((Tracked::Open, "re_1".to_string())));
302
+ assert_eq!(
303
+ tracked_event(&open),
304
+ Some((Tracked::Open, "re_1".to_string()))
305
+ );
272
306
  let click = json!({ "type": "email.clicked", "data": { "email_id": "re_2" } });
273
- assert_eq!(tracked_event(&click), Some((Tracked::Click, "re_2".to_string())));
274
- assert_eq!(tracked_event(&json!({ "type": "email.delivered", "data": { "email_id": "re_3" } })), None);
275
- assert_eq!(tracked_event(&json!({ "type": "email.opened", "data": {} })), None);
307
+ assert_eq!(
308
+ tracked_event(&click),
309
+ Some((Tracked::Click, "re_2".to_string()))
310
+ );
311
+ assert_eq!(
312
+ tracked_event(&json!({ "type": "email.delivered", "data": { "email_id": "re_3" } })),
313
+ None
314
+ );
315
+ assert_eq!(
316
+ tracked_event(&json!({ "type": "email.opened", "data": {} })),
317
+ None
318
+ );
276
319
  }
277
320
 
278
321
  #[test]
279
322
  fn the_delivery_is_found_by_its_stored_message_id() {
280
323
  let deliveries = vec![
281
- ("issue-1:a@b.com".to_string(), json!({ "message_id": { "value": "re_1" } })),
282
- ("issue-1:c@d.com".to_string(), json!({ "message_id": { "value": "re_2" } })),
324
+ (
325
+ "issue-1:a@b.com".to_string(),
326
+ json!({ "message_id": { "value": "re_1" } }),
327
+ ),
328
+ (
329
+ "issue-1:c@d.com".to_string(),
330
+ json!({ "message_id": { "value": "re_2" } }),
331
+ ),
283
332
  ];
284
- assert_eq!(delivery_for(&deliveries, "re_2").as_deref(), Some("issue-1:c@d.com"));
333
+ assert_eq!(
334
+ delivery_for(&deliveries, "re_2").as_deref(),
335
+ Some("issue-1:c@d.com")
336
+ );
285
337
  assert_eq!(delivery_for(&deliveries, "re_9"), None);
286
338
  }
287
339
 
288
340
  #[test]
289
341
  fn the_event_time_is_read_as_unix_seconds() {
290
- assert_eq!(parse_rfc3339_seconds("1970-01-02T00:00:01.500Z"), Some(86401));
291
- assert_eq!(parse_rfc3339_seconds("2026-10-06T21:59:20+00:00"), Some(1_791_323_960));
342
+ assert_eq!(
343
+ parse_rfc3339_seconds("1970-01-02T00:00:01.500Z"),
344
+ Some(86401)
345
+ );
346
+ assert_eq!(
347
+ parse_rfc3339_seconds("2026-10-06T21:59:20+00:00"),
348
+ Some(1_791_323_960)
349
+ );
292
350
  assert_eq!(parse_rfc3339_seconds("yesterday"), None);
293
351
  }
294
352