hecks 3.13.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -21,8 +21,20 @@ pub struct StoredKeys {
21
21
 
22
22
  impl StoredKeys {
23
23
  fn from_json(value: &Value) -> Self {
24
- let text = |field: &str| value.get(field).and_then(|v| v.as_str()).unwrap_or_default().to_string();
25
- Self { secret_key: text("secret_key"), publishable_key: text("publishable_key"), webhook_secret: text("webhook_secret"), webhook_endpoint_id: text("webhook_endpoint_id"), saved_at: text("saved_at") }
24
+ let text = |field: &str| {
25
+ value
26
+ .get(field)
27
+ .and_then(|v| v.as_str())
28
+ .unwrap_or_default()
29
+ .to_string()
30
+ };
31
+ Self {
32
+ secret_key: text("secret_key"),
33
+ publishable_key: text("publishable_key"),
34
+ webhook_secret: text("webhook_secret"),
35
+ webhook_endpoint_id: text("webhook_endpoint_id"),
36
+ saved_at: text("saved_at"),
37
+ }
26
38
  }
27
39
 
28
40
  fn to_json(&self) -> Value {
@@ -45,8 +57,16 @@ pub struct StoredDocument {
45
57
 
46
58
  impl StoredDocument {
47
59
  fn from_json(value: &Value) -> Self {
48
- let entry = |mode: &str| value.get(mode).filter(|v| v.is_object()).map(StoredKeys::from_json);
49
- Self { test: entry("test"), live: entry("live") }
60
+ let entry = |mode: &str| {
61
+ value
62
+ .get(mode)
63
+ .filter(|v| v.is_object())
64
+ .map(StoredKeys::from_json)
65
+ };
66
+ Self {
67
+ test: entry("test"),
68
+ live: entry("live"),
69
+ }
50
70
  }
51
71
 
52
72
  fn to_json(&self) -> Value {
@@ -61,17 +81,29 @@ impl StoredDocument {
61
81
 
62
82
  /// The entry saved for `mode` ("test" or "live").
63
83
  pub fn mode(&self, mode: &str) -> Option<&StoredKeys> {
64
- if mode == "live" { self.live.as_ref() } else { self.test.as_ref() }
84
+ if mode == "live" {
85
+ self.live.as_ref()
86
+ } else {
87
+ self.test.as_ref()
88
+ }
65
89
  }
66
90
 
67
91
  /// Replaces (or with `None` removes) the entry for `mode`.
68
92
  pub fn set(&mut self, mode: &str, keys: Option<StoredKeys>) {
69
- if mode == "live" { self.live = keys } else { self.test = keys }
93
+ if mode == "live" {
94
+ self.live = keys;
95
+ } else {
96
+ self.test = keys;
97
+ }
70
98
  }
71
99
 
72
100
  /// Every webhook signing secret saved, for either mode.
73
101
  pub fn webhook_secrets(&self) -> impl Iterator<Item = &str> {
74
- [self.test.as_ref(), self.live.as_ref()].into_iter().flatten().map(|k| k.webhook_secret.as_str()).filter(|s| !s.is_empty())
102
+ [self.test.as_ref(), self.live.as_ref()]
103
+ .into_iter()
104
+ .flatten()
105
+ .map(|k| k.webhook_secret.as_str())
106
+ .filter(|s| !s.is_empty())
75
107
  }
76
108
  }
77
109
 
@@ -94,7 +126,10 @@ pub struct AwsRawStore {
94
126
  impl AwsRawStore {
95
127
  /// A store over the secret named `secret_id`; nothing is fetched until the first use.
96
128
  pub fn new(secret_id: String) -> Self {
97
- Self { secret_id, fetcher: tokio::sync::OnceCell::new() }
129
+ Self {
130
+ secret_id,
131
+ fetcher: tokio::sync::OnceCell::new(),
132
+ }
98
133
  }
99
134
 
100
135
  async fn fetcher(&self) -> &AwsSecretFetcher {
@@ -105,11 +140,17 @@ impl AwsRawStore {
105
140
  #[async_trait::async_trait]
106
141
  impl RawStore for AwsRawStore {
107
142
  async fn read(&self) -> anyhow::Result<Option<String>> {
108
- self.fetcher().await.fetch_secret_string_if_present(&self.secret_id).await
143
+ self.fetcher()
144
+ .await
145
+ .fetch_secret_string_if_present(&self.secret_id)
146
+ .await
109
147
  }
110
148
 
111
149
  async fn write(&self, document: &str) -> anyhow::Result<()> {
112
- self.fetcher().await.put_secret_string(&self.secret_id, document).await
150
+ self.fetcher()
151
+ .await
152
+ .put_secret_string(&self.secret_id, document)
153
+ .await
113
154
  }
114
155
  }
115
156
 
@@ -123,12 +164,21 @@ pub struct KeyStore {
123
164
  impl KeyStore {
124
165
  /// A cached view over `raw`, starting with an empty cache.
125
166
  pub fn new(raw: Arc<dyn RawStore>) -> Self {
126
- Self { raw, cache: Mutex::new(None) }
167
+ Self {
168
+ raw,
169
+ cache: Mutex::new(None),
170
+ }
127
171
  }
128
172
 
129
173
  fn cached(&self) -> Option<Arc<StoredDocument>> {
130
- let cache = self.cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
131
- cache.as_ref().filter(|(at, _)| at.elapsed() < CACHE_TTL).map(|(_, document)| document.clone())
174
+ let cache = self
175
+ .cache
176
+ .lock()
177
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
178
+ cache
179
+ .as_ref()
180
+ .filter(|(at, _)| at.elapsed() < CACHE_TTL)
181
+ .map(|(_, document)| document.clone())
132
182
  }
133
183
 
134
184
  /// The document, from the cache when it is under a minute old.
@@ -143,11 +193,18 @@ impl KeyStore {
143
193
  pub async fn refresh(&self) -> anyhow::Result<Arc<StoredDocument>> {
144
194
  let text = self.raw.read().await?;
145
195
  let document = match text {
146
- Some(text) => StoredDocument::from_json(&serde_json::from_str::<Value>(&text).map_err(|_| anyhow::anyhow!("the stored payment keys are not valid JSON"))?),
196
+ Some(text) => StoredDocument::from_json(
197
+ &serde_json::from_str::<Value>(&text)
198
+ .map_err(|_| anyhow::anyhow!("the stored payment keys are not valid JSON"))?,
199
+ ),
147
200
  None => StoredDocument::default(),
148
201
  };
149
202
  let document = Arc::new(document);
150
- *self.cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = Some((Instant::now(), document.clone()));
203
+ *self
204
+ .cache
205
+ .lock()
206
+ .unwrap_or_else(std::sync::PoisonError::into_inner) =
207
+ Some((Instant::now(), document.clone()));
151
208
  Ok(document)
152
209
  }
153
210
 
@@ -165,40 +222,59 @@ impl KeyStore {
165
222
 
166
223
  /// Forgets the cached document.
167
224
  pub fn invalidate(&self) {
168
- *self.cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
225
+ *self
226
+ .cache
227
+ .lock()
228
+ .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
169
229
  }
170
230
  }
171
231
 
172
232
  /// The secret's name from a raw `PAYMENTS_ACCOUNT_SECRET_ID` value, or `None`
173
233
  /// when it is unset or blank.
174
234
  fn configured_secret_id(configured: Option<&str>) -> Option<String> {
175
- configured.map(str::trim).filter(|id| !id.is_empty()).map(String::from)
235
+ configured
236
+ .map(str::trim)
237
+ .filter(|id| !id.is_empty())
238
+ .map(String::from)
176
239
  }
177
240
 
178
241
  /// Whether this process runs as a container task or a Lambda function on AWS.
179
242
  fn on_aws() -> bool {
180
- ["ECS_CONTAINER_METADATA_URI_V4", "AWS_LAMBDA_FUNCTION_NAME"].iter().any(|name| std::env::var(name).is_ok_and(|v| !v.is_empty()))
243
+ ["ECS_CONTAINER_METADATA_URI_V4", "AWS_LAMBDA_FUNCTION_NAME"]
244
+ .iter()
245
+ .any(|name| std::env::var(name).is_ok_and(|v| !v.is_empty()))
181
246
  }
182
247
 
183
248
  /// The message that stops the boot when checkout is enabled on AWS without a
184
249
  /// secret to keep the business's keys in; `None` when the boot may go on.
185
- pub fn refusal_at_boot(checkout_enabled: bool, on_aws: bool, configured: Option<&str>) -> Option<String> {
250
+ pub fn refusal_at_boot(
251
+ checkout_enabled: bool,
252
+ on_aws: bool,
253
+ configured: Option<&str>,
254
+ ) -> Option<String> {
186
255
  let missing = checkout_enabled && on_aws && configured_secret_id(configured).is_none();
187
256
  missing.then(|| format!("{SECRET_ID_VARIABLE} is required on AWS when checkout is enabled: name the Secrets Manager secret that holds the business's payment keys"))
188
257
  }
189
258
 
190
259
  /// `refusal_at_boot` for this process's environment.
191
260
  pub fn check_boot(checkout_enabled: bool) -> Result<(), String> {
192
- match refusal_at_boot(checkout_enabled, on_aws(), std::env::var(SECRET_ID_VARIABLE).ok().as_deref()) {
193
- Some(message) => Err(message),
194
- None => Ok(()),
195
- }
261
+ refusal_at_boot(
262
+ checkout_enabled,
263
+ on_aws(),
264
+ std::env::var(SECRET_ID_VARIABLE).ok().as_deref(),
265
+ )
266
+ .map_or(Ok(()), Err)
196
267
  }
197
268
 
198
269
  /// The process-wide store, built once so its cache is shared by every request.
199
270
  pub fn default_store() -> Option<Arc<KeyStore>> {
200
271
  static STORE: OnceLock<Option<Arc<KeyStore>>> = OnceLock::new();
201
- STORE.get_or_init(|| configured_secret_id(std::env::var(SECRET_ID_VARIABLE).ok().as_deref()).map(|id| Arc::new(KeyStore::new(Arc::new(AwsRawStore::new(id)))))).clone()
272
+ STORE
273
+ .get_or_init(|| {
274
+ configured_secret_id(std::env::var(SECRET_ID_VARIABLE).ok().as_deref())
275
+ .map(|id| Arc::new(KeyStore::new(Arc::new(AwsRawStore::new(id)))))
276
+ })
277
+ .clone()
202
278
  }
203
279
 
204
280
  /// `YYYY-MM-DDTHH:MM:SSZ` for a Unix time in seconds (proleptic Gregorian,
@@ -208,18 +284,33 @@ pub fn utc_timestamp(seconds: u64) -> String {
208
284
  let z = days + 719_468;
209
285
  let era = z.div_euclid(146_097);
210
286
  let day_of_era = z.rem_euclid(146_097);
211
- let year_of_era = (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
287
+ let year_of_era =
288
+ (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
212
289
  let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
213
290
  let month_shifted = (5 * day_of_year + 2) / 153;
214
291
  let day = day_of_year - (153 * month_shifted + 2) / 5 + 1;
215
- let month = if month_shifted < 10 { month_shifted + 3 } else { month_shifted - 9 };
292
+ let month = if month_shifted < 10 {
293
+ month_shifted + 3
294
+ } else {
295
+ month_shifted - 9
296
+ };
216
297
  let year = year_of_era + era * 400 + i64::from(month <= 2);
217
- format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", rest / 3_600, rest % 3_600 / 60, rest % 60)
298
+ format!(
299
+ "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z",
300
+ rest / 3_600,
301
+ rest % 3_600 / 60,
302
+ rest % 60
303
+ )
218
304
  }
219
305
 
220
306
  /// The current time as `utc_timestamp` writes it.
221
307
  pub fn now_timestamp() -> String {
222
- utc_timestamp(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs())
308
+ utc_timestamp(
309
+ std::time::SystemTime::now()
310
+ .duration_since(std::time::UNIX_EPOCH)
311
+ .unwrap_or_default()
312
+ .as_secs(),
313
+ )
223
314
  }
224
315
 
225
316
  /// An in-memory stand-in for Secrets Manager that counts its calls, for the
@@ -239,15 +330,23 @@ pub(crate) struct MemoryStore {
239
330
  impl RawStore for MemoryStore {
240
331
  async fn read(&self) -> anyhow::Result<Option<String>> {
241
332
  self.reads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
242
- Ok(self.document.lock().unwrap().clone())
333
+ Ok(self
334
+ .document
335
+ .lock()
336
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
337
+ .clone())
243
338
  }
244
339
 
245
340
  async fn write(&self, document: &str) -> anyhow::Result<()> {
246
- self.writes.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
341
+ self.writes
342
+ .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
247
343
  if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
248
344
  return Err(anyhow::anyhow!("writing the secret failed"));
249
345
  }
250
- *self.document.lock().unwrap() = Some(document.to_string());
346
+ *self
347
+ .document
348
+ .lock()
349
+ .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(document.to_string());
251
350
  Ok(())
252
351
  }
253
352
  }
@@ -258,7 +357,13 @@ mod tests {
258
357
  use std::sync::atomic::Ordering;
259
358
 
260
359
  fn keys(secret: &str) -> StoredKeys {
261
- StoredKeys { secret_key: secret.into(), publishable_key: "pk_test_x".into(), webhook_secret: "whsec_x".into(), webhook_endpoint_id: "we_1".into(), saved_at: "2026-09-26T05:00:00Z".into() }
360
+ StoredKeys {
361
+ secret_key: secret.into(),
362
+ publishable_key: "pk_test_x".into(),
363
+ webhook_secret: "whsec_x".into(),
364
+ webhook_endpoint_id: "we_1".into(),
365
+ saved_at: "2026-09-26T05:00:00Z".into(),
366
+ }
262
367
  }
263
368
 
264
369
  #[tokio::test]
@@ -267,12 +372,23 @@ mod tests {
267
372
  let store = KeyStore::new(raw.clone());
268
373
  assert!(store.document().await.unwrap().test.is_none());
269
374
  assert!(store.document().await.unwrap().test.is_none());
270
- assert_eq!(raw.reads.load(Ordering::SeqCst), 1, "the second read was served from the cache");
271
-
272
- store.update(|doc| doc.set("test", Some(keys("rk_test_one")))).await.unwrap();
375
+ assert_eq!(
376
+ raw.reads.load(Ordering::SeqCst),
377
+ 1,
378
+ "the second read was served from the cache"
379
+ );
380
+
381
+ store
382
+ .update(|doc| doc.set("test", Some(keys("rk_test_one"))))
383
+ .await
384
+ .unwrap();
273
385
  let reads_after_update = raw.reads.load(Ordering::SeqCst);
274
386
  let document = store.document().await.unwrap();
275
- assert_eq!(document.test.as_ref().unwrap().secret_key, "rk_test_one", "the write invalidated the cache");
387
+ assert_eq!(
388
+ document.test.as_ref().unwrap().secret_key,
389
+ "rk_test_one",
390
+ "the write invalidated the cache"
391
+ );
276
392
  assert_eq!(raw.reads.load(Ordering::SeqCst), reads_after_update + 1);
277
393
  assert_eq!(raw.writes.load(Ordering::SeqCst), 1);
278
394
  }
@@ -281,20 +397,38 @@ mod tests {
281
397
  async fn the_stored_json_has_exactly_the_agreed_shape() {
282
398
  let raw = Arc::new(MemoryStore::default());
283
399
  let store = KeyStore::new(raw.clone());
284
- store.update(|doc| doc.set("live", Some(keys("rk_live_one")))).await.unwrap();
285
- let stored: serde_json::Value = serde_json::from_str(raw.document.lock().unwrap().as_ref().unwrap()).unwrap();
400
+ store
401
+ .update(|doc| doc.set("live", Some(keys("rk_live_one"))))
402
+ .await
403
+ .unwrap();
404
+ let stored: serde_json::Value =
405
+ serde_json::from_str(raw.document.lock().unwrap().as_ref().unwrap()).unwrap();
286
406
  assert!(stored.get("test").is_none(), "an unsaved mode has no entry");
287
407
  let live = stored["live"].as_object().unwrap();
288
408
  let mut names: Vec<&str> = live.keys().map(String::as_str).collect();
289
409
  names.sort_unstable();
290
- assert_eq!(names, ["publishable_key", "saved_at", "secret_key", "webhook_endpoint_id", "webhook_secret"]);
410
+ assert_eq!(
411
+ names,
412
+ [
413
+ "publishable_key",
414
+ "saved_at",
415
+ "secret_key",
416
+ "webhook_endpoint_id",
417
+ "webhook_secret"
418
+ ]
419
+ );
291
420
  }
292
421
 
293
422
  #[tokio::test]
294
423
  async fn an_unreadable_document_is_an_error_that_does_not_quote_it() {
295
424
  let raw = Arc::new(MemoryStore::default());
296
425
  *raw.document.lock().unwrap() = Some("rk_live_SECRET not json".to_string());
297
- let error = KeyStore::new(raw).document().await.err().expect("an error").to_string();
426
+ let error = KeyStore::new(raw)
427
+ .document()
428
+ .await
429
+ .err()
430
+ .expect("an error")
431
+ .to_string();
298
432
  assert!(!error.contains("rk_live_SECRET"), "{error}");
299
433
  }
300
434
 
@@ -303,21 +437,39 @@ mod tests {
303
437
  assert_eq!(configured_secret_id(None), None);
304
438
  assert_eq!(configured_secret_id(Some("")), None);
305
439
  assert_eq!(configured_secret_id(Some(" ")), None);
306
- assert_eq!(configured_secret_id(Some(" payments-keys ")), Some("payments-keys".to_string()));
440
+ assert_eq!(
441
+ configured_secret_id(Some(" payments-keys ")),
442
+ Some("payments-keys".to_string())
443
+ );
307
444
  }
308
445
 
309
446
  #[test]
310
447
  fn checkout_on_aws_without_a_secret_id_refuses_the_boot_and_names_the_variable() {
311
448
  let message = refusal_at_boot(true, true, None).expect("refused");
312
449
  assert!(message.contains("PAYMENTS_ACCOUNT_SECRET_ID"), "{message}");
313
- assert!(refusal_at_boot(true, true, Some(" ")).is_some(), "a blank value is unset");
450
+ assert!(
451
+ refusal_at_boot(true, true, Some(" ")).is_some(),
452
+ "a blank value is unset"
453
+ );
314
454
  }
315
455
 
316
456
  #[test]
317
457
  fn every_other_combination_boots() {
318
- assert_eq!(refusal_at_boot(true, true, Some("payments-keys")), None, "set: unchanged");
319
- assert_eq!(refusal_at_boot(true, false, None), None, "off AWS: only environment keys");
320
- assert_eq!(refusal_at_boot(false, true, None), None, "checkout off: payments are not served");
458
+ assert_eq!(
459
+ refusal_at_boot(true, true, Some("payments-keys")),
460
+ None,
461
+ "set: unchanged"
462
+ );
463
+ assert_eq!(
464
+ refusal_at_boot(true, false, None),
465
+ None,
466
+ "off AWS: only environment keys"
467
+ );
468
+ assert_eq!(
469
+ refusal_at_boot(false, true, None),
470
+ None,
471
+ "checkout off: payments are not served"
472
+ );
321
473
  assert_eq!(refusal_at_boot(false, false, None), None);
322
474
  }
323
475
 
@@ -0,0 +1,191 @@
1
+ //! Which signed-in roles may manage a site's payments: a per-site setting, read once at boot.
2
+ //!
3
+ //! `HECKS_PAYMENTS_MANAGER_ROLES` is a comma-separated list of exact role names. Unset means
4
+ //! `Owner`, which is what the host did before the setting existed. A set value must be a
5
+ //! plausible list or the boot is refused; a refusal names the variable and the kind of mistake,
6
+ //! never the value. A caller manages payments when their role is in the list, compared exactly:
7
+ //! no case folding, no hierarchy, no wildcard.
8
+
9
+ use std::sync::OnceLock;
10
+
11
+ /// The variable that names the roles allowed to manage payments.
12
+ pub const VARIABLE: &str = "HECKS_PAYMENTS_MANAGER_ROLES";
13
+ /// The role that manages payments when the variable is unset.
14
+ pub const DEFAULT_ROLE: &str = "Owner";
15
+ /// The most roles one list may name.
16
+ pub const MAX_ENTRIES: usize = 8;
17
+ /// The longest role name, after trimming.
18
+ pub const MAX_ENTRY_LENGTH: usize = 32;
19
+
20
+ /// The roles that manage payments when nothing is configured.
21
+ pub fn default_roles() -> Vec<String> {
22
+ vec![DEFAULT_ROLE.to_string()]
23
+ }
24
+
25
+ /// Parses the variable's value; `None` (unset) gives the default list.
26
+ pub fn parse(raw: Option<&str>) -> Result<Vec<String>, String> {
27
+ let Some(raw) = raw else {
28
+ return Ok(default_roles());
29
+ };
30
+ let refuse = |why: &str| Err(format!("{VARIABLE} is invalid: {why}"));
31
+ let entries: Vec<&str> = raw.split(',').map(str::trim).collect();
32
+ if entries.len() > MAX_ENTRIES {
33
+ return refuse(&format!("it names more than {MAX_ENTRIES} roles"));
34
+ }
35
+ for (index, entry) in entries.iter().enumerate() {
36
+ let position = index + 1;
37
+ if entry.is_empty() {
38
+ return refuse(&format!(
39
+ "entry {position} is empty; name at least one role and no blank entries"
40
+ ));
41
+ }
42
+ if entry.len() > MAX_ENTRY_LENGTH {
43
+ return refuse(&format!(
44
+ "entry {position} is longer than {MAX_ENTRY_LENGTH} characters"
45
+ ));
46
+ }
47
+ let starts_well = entry.starts_with(|c: char| c.is_ascii_alphanumeric());
48
+ if !starts_well
49
+ || !entry
50
+ .chars()
51
+ .all(|c| c.is_ascii_alphanumeric() || c == ' ' || c == '-')
52
+ {
53
+ return refuse(&format!("entry {position} is not a role name: letters, digits, spaces and hyphens only, starting with a letter or digit"));
54
+ }
55
+ }
56
+ Ok(entries.into_iter().map(String::from).collect())
57
+ }
58
+
59
+ /// Reads the setting into `cell` the first time and never again, whatever the environment
60
+ /// does afterwards. `read` stands for the environment lookup.
61
+ pub fn read_once(
62
+ cell: &OnceLock<Result<Vec<String>, String>>,
63
+ read: impl FnOnce() -> Option<String>,
64
+ ) -> &Result<Vec<String>, String> {
65
+ cell.get_or_init(|| parse(read().as_deref()))
66
+ }
67
+
68
+ /// This process's setting, read from the environment on first use (the boot check is the first).
69
+ pub fn configured() -> &'static Result<Vec<String>, String> {
70
+ static CELL: OnceLock<Result<Vec<String>, String>> = OnceLock::new();
71
+ read_once(&CELL, || std::env::var(VARIABLE).ok())
72
+ }
73
+
74
+ /// Refuses the boot when this process's setting is invalid.
75
+ pub fn check_boot() -> Result<(), String> {
76
+ configured().as_ref().map(|_| ()).map_err(Clone::clone)
77
+ }
78
+
79
+ /// The roles for a request. An invalid setting never gets here (the boot was refused); should it
80
+ /// ever, only the default role manages, so a mistake narrows access instead of widening it.
81
+ pub fn manager_roles() -> Vec<String> {
82
+ configured().clone().unwrap_or_else(|_| default_roles())
83
+ }
84
+
85
+ /// Whether `role` is one of `roles`, by exact string match.
86
+ pub fn manages(roles: &[String], role: &str) -> bool {
87
+ roles.iter().any(|allowed| allowed == role)
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+
94
+ fn roles(raw: Option<&str>) -> Vec<String> {
95
+ parse(raw).unwrap()
96
+ }
97
+
98
+ #[test]
99
+ fn unset_means_owner_only() {
100
+ let list = roles(None);
101
+ assert_eq!(list, ["Owner"]);
102
+ assert!(manages(&list, "Owner"));
103
+ assert!(!manages(&list, "Admin"));
104
+ assert!(!manages(&list, "Member"));
105
+ }
106
+
107
+ #[test]
108
+ fn a_configured_list_admits_exactly_its_roles() {
109
+ let list = roles(Some("Owner,Admin"));
110
+ assert!(manages(&list, "Owner") && manages(&list, "Admin"));
111
+ assert!(!manages(&list, "Editor"));
112
+ assert!(!manages(&list, ""));
113
+ }
114
+
115
+ #[test]
116
+ fn whitespace_is_trimmed_and_case_is_exact() {
117
+ let list = roles(Some(" Owner , Account Admin "));
118
+ assert_eq!(list, ["Owner", "Account Admin"]);
119
+ let lower = roles(Some("admin"));
120
+ assert!(manages(&lower, "admin"));
121
+ assert!(!manages(&lower, "Admin"), "no case folding");
122
+ assert!(!manages(&roles(Some("Admin")), "admin"));
123
+ }
124
+
125
+ #[test]
126
+ fn no_wildcard_and_no_hierarchy() {
127
+ assert!(parse(Some("*")).is_err());
128
+ assert!(
129
+ !manages(&roles(Some("Admin")), "Owner"),
130
+ "naming Admin does not admit Owner"
131
+ );
132
+ }
133
+
134
+ #[test]
135
+ fn an_invalid_setting_is_refused_naming_the_variable() {
136
+ let too_many = ["Role"; MAX_ENTRIES + 1].join(",");
137
+ let too_long = "A".repeat(MAX_ENTRY_LENGTH + 1);
138
+ for bad in [
139
+ "",
140
+ " ",
141
+ ",",
142
+ "Owner,",
143
+ ",Owner",
144
+ "Owner,,Admin",
145
+ "Own/er",
146
+ "Owner\nAdmin",
147
+ "Own\u{7}er",
148
+ "Admin;Owner",
149
+ "-Admin",
150
+ "Ad_min",
151
+ "Régisseur",
152
+ too_many.as_str(),
153
+ too_long.as_str(),
154
+ ] {
155
+ let message = parse(Some(bad)).expect_err(bad);
156
+ assert!(message.starts_with(VARIABLE), "{message}");
157
+ }
158
+ assert!(parse(Some(&"A".repeat(MAX_ENTRY_LENGTH))).is_ok());
159
+ assert!(parse(Some(&["Role"; MAX_ENTRIES].join(","))).is_ok());
160
+ }
161
+
162
+ #[test]
163
+ fn a_refusal_never_echoes_the_value() {
164
+ let message = parse(Some("sk_live_SECRET/value")).unwrap_err();
165
+ assert!(!message.contains("sk_live"), "{message}");
166
+ }
167
+
168
+ #[test]
169
+ fn the_setting_is_read_once() {
170
+ let cell = OnceLock::new();
171
+ let reads = std::cell::Cell::new(0);
172
+ let first = read_once(&cell, || {
173
+ reads.set(reads.get() + 1);
174
+ Some("Owner,Admin".to_string())
175
+ });
176
+ assert_eq!(first.as_ref().unwrap(), &["Owner", "Admin"]);
177
+ let second = read_once(&cell, || {
178
+ reads.set(reads.get() + 1);
179
+ Some("Editor".to_string())
180
+ });
181
+ assert_eq!(second.as_ref().unwrap(), &["Owner", "Admin"]);
182
+ assert_eq!(reads.get(), 1);
183
+ }
184
+
185
+ #[test]
186
+ fn an_invalid_value_stays_refused_once_read() {
187
+ let cell = OnceLock::new();
188
+ assert!(read_once(&cell, || Some("a/b".to_string())).is_err());
189
+ assert!(read_once(&cell, || None).is_err());
190
+ }
191
+ }