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
data/rust/host/src/ir.rs CHANGED
@@ -17,7 +17,10 @@ pub fn ir() -> Option<&'static Value> {
17
17
  /// Aggregates bound to a lineage-capable adapter, as `(qualified_name,
18
18
  /// storage_name)` — read from `ir.json`'s own `lineage` key.
19
19
  pub fn lineage_capable_aggregates(domain_ir: &Value) -> Vec<(String, String)> {
20
- let domain_name = domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or_default();
20
+ let domain_name = domain_ir
21
+ .get("name")
22
+ .and_then(|v| v.as_str())
23
+ .unwrap_or_default();
21
24
  domain_ir
22
25
  .get("lineage")
23
26
  .and_then(|l| l.get("capable_aggregates"))
@@ -56,12 +59,66 @@ pub fn persistence_adapters(domain_ir: &Value) -> Vec<(String, String)> {
56
59
  }
57
60
 
58
61
  /// The only persistence adapters `rust/host` has a real backend for — see
59
- /// `refuse_unsupported_persistence_adapters` below.
62
+ /// `refuse_unsupported_persistence_adapters` below. Must list the same names
63
+ /// as [`BACKENDS`] (a test holds them together).
60
64
  pub const SUPPORTED_PERSISTENCE_ADAPTERS: &[&str] = &["Postgres", "PostgresEra"];
61
65
 
66
+ /// What serves a bound adapter inside this host.
67
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
68
+ pub enum Backend {
69
+ /// The one `tokio_postgres` engine (journal tables, era heads).
70
+ TokioPostgres,
71
+ }
72
+
73
+ /// The host's implemented backends, adapter name -> backend. A future backend
74
+ /// is one more row here (and one in [`SUPPORTED_PERSISTENCE_ADAPTERS`]).
75
+ pub const BACKENDS: &[(&str, Backend)] = &[
76
+ ("Postgres", Backend::TokioPostgres),
77
+ ("PostgresEra", Backend::TokioPostgres),
78
+ ];
79
+
80
+ /// The backend that serves `adapter`, if the host has one.
81
+ pub fn backend_for(adapter: &str) -> Option<Backend> {
82
+ BACKENDS
83
+ .iter()
84
+ .find(|(name, _)| *name == adapter)
85
+ .map(|(_, backend)| *backend)
86
+ }
87
+
88
+ /// The port every persistence binding must be declared against.
89
+ const PERSISTENCE_PORT: &str = "persistence";
90
+
91
+ /// `ir.json`'s own `adapters` key, as `name -> declaration`. `None` when the
92
+ /// key is absent (an older IR); `Some(Err(..))` when present but malformed
93
+ /// (not an array, or an entry without a string `name`).
94
+ pub fn adapter_declarations(
95
+ domain_ir: &Value,
96
+ ) -> Option<Result<std::collections::BTreeMap<String, &Value>, String>> {
97
+ let raw = domain_ir.get("adapters")?;
98
+ let Some(entries) = raw.as_array() else {
99
+ return Some(Err(
100
+ "the IR's \"adapters\" key is not an array of adapter declarations".to_string(),
101
+ ));
102
+ };
103
+ let mut declared = std::collections::BTreeMap::new();
104
+ for (index, entry) in entries.iter().enumerate() {
105
+ match entry.get("name").and_then(|n| n.as_str()) {
106
+ Some(name) => {
107
+ declared.insert(name.to_string(), entry);
108
+ }
109
+ None => {
110
+ return Some(Err(format!(
111
+ "the IR's \"adapters\" entry {index} has no string \"name\""
112
+ )))
113
+ }
114
+ }
115
+ }
116
+ Some(Ok(declared))
117
+ }
118
+
62
119
  /// The chapter this domain's role checks resolve against, read from
63
120
  /// `ir.json`'s own `authorization` key. `None` when nothing provides it.
64
- #[derive(Debug, Clone, PartialEq)]
121
+ #[derive(Debug, Clone, PartialEq, Eq)]
65
122
  pub struct AuthorizationProvider {
66
123
  /// Qualified grant command, e.g. `Governance::RoleAssignment.Assign`.
67
124
  pub grant: String,
@@ -80,7 +137,7 @@ pub fn authorization_provider(domain_ir: &Value) -> Option<AuthorizationProvider
80
137
 
81
138
  /// The chapter that answers "who may sign in", read from `ir.json`'s own
82
139
  /// `membership` key. `None` when nothing provides `"membership"`.
83
- #[derive(Debug, Clone, PartialEq)]
140
+ #[derive(Debug, Clone, PartialEq, Eq)]
84
141
  pub struct MembershipProvider {
85
142
  /// Chapter name, e.g. `Membership`.
86
143
  pub provider: String,
@@ -91,8 +148,10 @@ pub struct MembershipProvider {
91
148
  /// Aggregates whose mutations mirror into their era head: the lineage-capable
92
149
  /// ones plus the membership aggregate, which is not itself lineage-capable.
93
150
  pub fn mirrored_aggregates(domain_ir: &Value) -> std::collections::BTreeSet<String> {
94
- let mut mirrored: std::collections::BTreeSet<String> =
95
- lineage_capable_aggregates(domain_ir).into_iter().map(|(qualified, _)| qualified).collect();
151
+ let mut mirrored: std::collections::BTreeSet<String> = lineage_capable_aggregates(domain_ir)
152
+ .into_iter()
153
+ .map(|(qualified, _)| qualified)
154
+ .collect();
96
155
  if let Some(membership) = membership_provider(domain_ir) {
97
156
  mirrored.insert(membership.aggregate);
98
157
  }
@@ -110,7 +169,7 @@ pub fn membership_provider(domain_ir: &Value) -> Option<MembershipProvider> {
110
169
  /// The chapter that answers "who is this authenticated pair", read from
111
170
  /// `ir.json`'s own `identity` key. `None` when nothing provides `"identity"`.
112
171
  /// Link's reference field must stay `identity` — never `identity_id`, never `to:`.
113
- #[derive(Debug, Clone, PartialEq)]
172
+ #[derive(Debug, Clone, PartialEq, Eq)]
114
173
  pub struct IdentityProvider {
115
174
  /// Chapter name, e.g. `Identity`.
116
175
  pub provider: String,
@@ -132,12 +191,117 @@ pub fn identity_provider(domain_ir: &Value) -> Option<IdentityProvider> {
132
191
  })
133
192
  }
134
193
 
194
+ /// Settings a world binding carries that are not adapter fields.
195
+ const WORLD_EXEMPT_SETTINGS: &[&str] = &["adapter", "role"];
196
+
197
+ fn world_error(what: &str) -> String {
198
+ format!("the IR's \"world\" {what}")
199
+ }
200
+
201
+ fn declared_names(declaration: &Value) -> std::collections::BTreeSet<&str> {
202
+ ["fields", "secrets"]
203
+ .iter()
204
+ .filter_map(|key| declaration.get(key)?.as_array())
205
+ .flatten()
206
+ .filter_map(|name| name.as_str())
207
+ .collect()
208
+ }
209
+
210
+ fn check_world_binding(
211
+ binding: &Value,
212
+ declared: &std::collections::BTreeMap<String, &Value>,
213
+ ) -> Result<(), String> {
214
+ let adapter = binding
215
+ .get("adapter")
216
+ .and_then(|a| a.as_str())
217
+ .ok_or_else(|| world_error("persistence entry has no string \"adapter\""))?;
218
+ let settings = binding
219
+ .get("settings")
220
+ .and_then(|s| s.as_array())
221
+ .ok_or_else(|| world_error("persistence entry's \"settings\" is not an array"))?;
222
+ let Some(declaration) = declared.get(adapter) else {
223
+ return Ok(());
224
+ };
225
+ let known = declared_names(declaration);
226
+ for setting in settings {
227
+ let name = setting
228
+ .as_str()
229
+ .ok_or_else(|| world_error("persistence entry has a setting that is not a string"))?;
230
+ if !WORLD_EXEMPT_SETTINGS.contains(&name) && !known.contains(name) {
231
+ return Err(format!(
232
+ "{adapter} does not declare {name:?} — the world sets it, but the adapter declares \
233
+ {known:?}. Add the field to the adapter, or remove it from the world."
234
+ ));
235
+ }
236
+ }
237
+ Ok(())
238
+ }
239
+
240
+ /// Refuses a world that sets, on a declared persistence adapter, a setting the adapter does not
241
+ /// declare as a field or secret (`adapter` and `role` are exempt). Entries for adapters the IR's
242
+ /// `adapters` key does not declare are skipped, and an IR without a `world` key is not checked.
243
+ pub fn refuse_undeclared_world_settings(domain_ir: &Value) -> Result<(), String> {
244
+ let Some(world) = domain_ir.get("world") else {
245
+ return Ok(());
246
+ };
247
+ if !world.is_object() {
248
+ return Err(world_error("key is not an object"));
249
+ }
250
+ let entries = world
251
+ .get("persistence")
252
+ .and_then(|p| p.as_array())
253
+ .ok_or_else(|| world_error("\"persistence\" is not an array"))?;
254
+ let declared = match adapter_declarations(domain_ir) {
255
+ Some(declared) => declared?,
256
+ None => return Ok(()),
257
+ };
258
+ entries
259
+ .iter()
260
+ .try_for_each(|binding| check_world_binding(binding, &declared))
261
+ }
262
+
135
263
  /// Refuses at boot rather than silently building a second, disjoint history
136
264
  /// nothing but this runtime reads while the real state lives elsewhere.
265
+ ///
266
+ /// Each aggregate's bound adapter is looked up in the IR, then in the host's
267
+ /// [`BACKENDS`] table. Rules, in order:
268
+ /// 1. When the IR carries an `adapters` key (even empty), the bound adapter
269
+ /// must be declared there.
270
+ /// 2. That declaration's `port`, when non-null, must be `"persistence"`.
271
+ /// 3. The adapter must have a host backend (the long-standing refusal).
272
+ ///
273
+ /// Compatibility: deployed IR files predate the `adapters` key. When it is
274
+ /// absent, rules 1 and 2 are skipped and only rule 3 applies, exactly as
275
+ /// before. A present-but-malformed `adapters` value is refused with a
276
+ /// message naming the problem (it never panics).
137
277
  pub fn refuse_unsupported_persistence_adapters(domain_ir: &Value) -> Result<(), String> {
138
- let unsupported: Vec<String> = persistence_adapters(domain_ir)
278
+ refuse_undeclared_world_settings(domain_ir)?;
279
+ let bindings = persistence_adapters(domain_ir);
280
+ if let Some(declared) = adapter_declarations(domain_ir) {
281
+ let declared = declared?;
282
+ for (aggregate, adapter) in &bindings {
283
+ let Some(declaration) = declared.get(adapter) else {
284
+ return Err(format!(
285
+ "{aggregate} binds {adapter}, which the domain's IR does not declare (no \
286
+ .adapter file declares it) — refusing instead of guessing what it means"
287
+ ));
288
+ };
289
+ match declaration.get("port") {
290
+ None | Some(Value::Null) => {}
291
+ Some(Value::String(port)) if port == PERSISTENCE_PORT => {}
292
+ Some(port) => {
293
+ return Err(format!(
294
+ "{aggregate} binds {adapter}, which the domain's IR declares for port \
295
+ {port} rather than {PERSISTENCE_PORT:?} — refusing a persistence \
296
+ binding to a non-persistence adapter"
297
+ ))
298
+ }
299
+ }
300
+ }
301
+ }
302
+ let unsupported: Vec<String> = bindings
139
303
  .into_iter()
140
- .filter(|(_, adapter)| !SUPPORTED_PERSISTENCE_ADAPTERS.contains(&adapter.as_str()))
304
+ .filter(|(_, adapter)| backend_for(adapter).is_none())
141
305
  .map(|(name, adapter)| format!("{name} (persisted_by {adapter:?})"))
142
306
  .collect();
143
307
  if unsupported.is_empty() {
@@ -175,8 +339,10 @@ mod tests {
175
339
  "lineage": { "capable_aggregates": [{ "name": "Order", "storage_name": "order" }] },
176
340
  "membership": { "provider": "Membership", "aggregate": "Membership::Person" }
177
341
  });
178
- let expected: std::collections::BTreeSet<String> =
179
- ["Shop::Order", "Membership::Person"].into_iter().map(String::from).collect();
342
+ let expected: std::collections::BTreeSet<String> = ["Shop::Order", "Membership::Person"]
343
+ .into_iter()
344
+ .map(String::from)
345
+ .collect();
180
346
  assert_eq!(mirrored_aggregates(&ir), expected);
181
347
  }
182
348
 
@@ -186,7 +352,8 @@ mod tests {
186
352
  "name": "Shop",
187
353
  "lineage": { "capable_aggregates": [{ "name": "Order", "storage_name": "order" }] }
188
354
  });
189
- let expected: std::collections::BTreeSet<String> = ["Shop::Order"].into_iter().map(String::from).collect();
355
+ let expected: std::collections::BTreeSet<String> =
356
+ std::iter::once("Shop::Order").map(String::from).collect();
190
357
  assert_eq!(mirrored_aggregates(&ir), expected);
191
358
  assert!(mirrored_aggregates(&serde_json::json!({ "name": "Banking" })).is_empty());
192
359
  }
@@ -194,7 +361,10 @@ mod tests {
194
361
  #[test]
195
362
  fn lineage_capable_aggregates_is_empty_for_a_domain_with_no_lineage_key() {
196
363
  let ir = serde_json::json!({ "name": "Banking" });
197
- assert_eq!(lineage_capable_aggregates(&ir), Vec::<(String, String)>::new());
364
+ assert_eq!(
365
+ lineage_capable_aggregates(&ir),
366
+ Vec::<(String, String)>::new()
367
+ );
198
368
  }
199
369
 
200
370
  #[test]
@@ -241,8 +411,63 @@ mod tests {
241
411
  ] }
242
412
  });
243
413
  let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
244
- assert!(err.contains("Account"), "error should name the offending aggregate: {err}");
245
- assert!(err.contains("Heki"), "error should name the unsupported adapter: {err}");
414
+ assert!(
415
+ err.contains("Account"),
416
+ "error should name the offending aggregate: {err}"
417
+ );
418
+ assert!(
419
+ err.contains("Heki"),
420
+ "error should name the unsupported adapter: {err}"
421
+ );
422
+ }
423
+
424
+ fn world_ir(settings: &Value) -> Value {
425
+ serde_json::json!({
426
+ "adapters": [{"name": "PostgresEra", "port": "persistence",
427
+ "fields": ["url"], "secrets": ["password"], "defaults": {}}],
428
+ "world": {"default_adapter": null, "default_database": false,
429
+ "persistence": [{"adapter": "PostgresEra", "settings": settings}]}
430
+ })
431
+ }
432
+
433
+ #[test]
434
+ fn a_world_setting_the_adapter_does_not_declare_is_refused() {
435
+ let err =
436
+ refuse_undeclared_world_settings(&world_ir(&serde_json::json!(["bogus"]))).unwrap_err();
437
+ assert!(
438
+ err.contains("PostgresEra") && err.contains("bogus"),
439
+ "{err}"
440
+ );
441
+ }
442
+
443
+ #[test]
444
+ fn declared_fields_secrets_role_and_adapter_pass() {
445
+ let ir = world_ir(&serde_json::json!(["url", "password", "role", "adapter"]));
446
+ assert!(refuse_undeclared_world_settings(&ir).is_ok());
447
+ }
448
+
449
+ #[test]
450
+ fn an_undeclared_adapter_in_the_world_is_skipped() {
451
+ let mut ir = world_ir(&serde_json::json!(["bogus"]));
452
+ ir["adapters"] = serde_json::json!([]);
453
+ assert!(refuse_undeclared_world_settings(&ir).is_ok());
454
+ }
455
+
456
+ #[test]
457
+ fn an_ir_without_a_world_key_is_not_checked() {
458
+ assert!(refuse_undeclared_world_settings(&serde_json::json!({"name": "X"})).is_ok());
459
+ }
460
+
461
+ #[test]
462
+ fn a_malformed_world_is_refused() {
463
+ let mut not_object = world_ir(&serde_json::json!([]));
464
+ not_object["world"] = serde_json::json!("x");
465
+ let mut not_array = world_ir(&serde_json::json!([]));
466
+ not_array["world"]["persistence"] = serde_json::json!({});
467
+ let bad_setting = world_ir(&serde_json::json!([3]));
468
+ for ir in [not_object, not_array, bad_setting] {
469
+ assert!(refuse_undeclared_world_settings(&ir).is_err());
470
+ }
246
471
  }
247
472
 
248
473
  #[test]
@@ -276,12 +501,97 @@ mod tests {
276
501
  assert!(identity_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
277
502
  }
278
503
 
279
-
280
504
  #[test]
281
505
  fn refuse_unsupported_persistence_adapters_passes_a_domain_with_no_persistence_key() {
282
506
  let ir = serde_json::json!({ "name": "Pizzas" });
283
507
  assert!(refuse_unsupported_persistence_adapters(&ir).is_ok());
284
508
  }
509
+
510
+ fn bound(adapters: &[(&str, &str)]) -> Value {
511
+ let aggregates: Vec<Value> = adapters
512
+ .iter()
513
+ .map(|(n, a)| serde_json::json!({"name": n, "adapter": a}))
514
+ .collect();
515
+ serde_json::json!({"persistence": {"aggregates": aggregates}})
516
+ }
517
+
518
+ fn with_adapters(mut ir: Value, adapters: Value) -> Value {
519
+ ir["adapters"] = adapters;
520
+ ir
521
+ }
522
+
523
+ #[test]
524
+ fn backend_table_matches_the_supported_names() {
525
+ let names: Vec<&str> = BACKENDS.iter().map(|(n, _)| *n).collect();
526
+ assert_eq!(names, SUPPORTED_PERSISTENCE_ADAPTERS);
527
+ }
528
+
529
+ #[test]
530
+ fn absent_adapters_key_keeps_the_old_behaviour() {
531
+ assert!(refuse_unsupported_persistence_adapters(&bound(&[("A", "Postgres")])).is_ok());
532
+ let err = refuse_unsupported_persistence_adapters(&bound(&[("A", "Heki")])).unwrap_err();
533
+ assert!(err.starts_with("rust/host only has a backend"), "{err}");
534
+ }
535
+
536
+ #[test]
537
+ fn declared_and_supported_boots_with_null_or_persistence_port() {
538
+ let ir = with_adapters(
539
+ bound(&[("A", "Postgres"), ("B", "PostgresEra")]),
540
+ serde_json::json!([
541
+ {"name": "Postgres", "port": "persistence"},
542
+ {"name": "PostgresEra", "port": null}
543
+ ]),
544
+ );
545
+ assert!(refuse_unsupported_persistence_adapters(&ir).is_ok());
546
+ }
547
+
548
+ #[test]
549
+ fn empty_adapters_key_refuses_every_binding_as_undeclared() {
550
+ let ir = with_adapters(bound(&[("A", "Postgres")]), serde_json::json!([]));
551
+ let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
552
+ assert!(
553
+ err.contains("A binds Postgres, which the domain's IR does not declare"),
554
+ "{err}"
555
+ );
556
+ }
557
+
558
+ #[test]
559
+ fn declared_but_unsupported_hits_the_backend_rule() {
560
+ let ir = with_adapters(
561
+ bound(&[("A", "Heki")]),
562
+ serde_json::json!([{"name": "Heki", "port": "persistence"}]),
563
+ );
564
+ let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
565
+ assert!(err.starts_with("rust/host only has a backend"), "{err}");
566
+ assert!(err.contains("A (persisted_by \"Heki\")"), "{err}");
567
+ }
568
+
569
+ #[test]
570
+ fn wrong_port_is_refused_before_the_backend_rule() {
571
+ let ir = with_adapters(
572
+ bound(&[("A", "Postgres")]),
573
+ serde_json::json!([{"name": "Postgres", "port": "mail"}]),
574
+ );
575
+ let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
576
+ assert!(
577
+ err.contains("A binds Postgres") && err.contains("\"mail\""),
578
+ "{err}"
579
+ );
580
+ }
581
+
582
+ #[test]
583
+ fn malformed_adapters_are_refused_not_panicked() {
584
+ for bad in [
585
+ serde_json::json!("x"),
586
+ serde_json::json!({"name": "Postgres"}),
587
+ serde_json::json!([{"port": "persistence"}]),
588
+ serde_json::json!([null, 3]),
589
+ ] {
590
+ let ir = with_adapters(bound(&[("A", "Postgres")]), bad);
591
+ let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
592
+ assert!(err.contains("\"adapters\""), "{err}");
593
+ }
594
+ }
285
595
  }
286
596
 
287
597
  #[cfg(test)]