hecks 2.9.0 → 3.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 (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,733 @@
1
+ // The IR sidecar, loaded once from `HECKS_IR_PATH`: this crate has no path
2
+ // dependency on the kernel crate that embeds the same JSON as `IR_JSON`.
3
+
4
+ use serde_json::Value;
5
+ use std::sync::OnceLock;
6
+
7
+ pub fn ir() -> Option<&'static Value> {
8
+ static IR: OnceLock<Option<Value>> = OnceLock::new();
9
+ IR.get_or_init(|| {
10
+ let path = std::env::var("HECKS_IR_PATH").ok()?;
11
+ let text = std::fs::read_to_string(path).ok()?;
12
+ serde_json::from_str(&text).ok()
13
+ })
14
+ .as_ref()
15
+ }
16
+
17
+ /// Aggregates bound to a lineage-capable adapter, as `(qualified_name,
18
+ /// storage_name)` — read from `ir.json`'s own `lineage` key.
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();
21
+ domain_ir
22
+ .get("lineage")
23
+ .and_then(|l| l.get("capable_aggregates"))
24
+ .and_then(|v| v.as_array())
25
+ .map(|entries| {
26
+ entries
27
+ .iter()
28
+ .filter_map(|entry| {
29
+ let name = entry.get("name")?.as_str()?;
30
+ let storage_name = entry.get("storage_name")?.as_str()?;
31
+ Some((format!("{domain_name}::{name}"), storage_name.to_string()))
32
+ })
33
+ .collect()
34
+ })
35
+ .unwrap_or_default()
36
+ }
37
+
38
+ /// Every aggregate's declared persistence adapter, as `(name, adapter)` —
39
+ /// read from `ir.json`'s own `persistence` key.
40
+ pub fn persistence_adapters(domain_ir: &Value) -> Vec<(String, String)> {
41
+ domain_ir
42
+ .get("persistence")
43
+ .and_then(|p| p.get("aggregates"))
44
+ .and_then(|v| v.as_array())
45
+ .map(|entries| {
46
+ entries
47
+ .iter()
48
+ .filter_map(|entry| {
49
+ let name = entry.get("name")?.as_str()?;
50
+ let adapter = entry.get("adapter")?.as_str()?;
51
+ Some((name.to_string(), adapter.to_string()))
52
+ })
53
+ .collect()
54
+ })
55
+ .unwrap_or_default()
56
+ }
57
+
58
+ /// The only persistence adapters `rust/host` has a real backend for — see
59
+ /// `refuse_unsupported_persistence_adapters` below.
60
+ pub const SUPPORTED_PERSISTENCE_ADAPTERS: &[&str] = &["Postgres", "PostgresEra"];
61
+
62
+ /// The chapter this domain's role checks resolve against, read from
63
+ /// `ir.json`'s own `authorization` key. `None` when nothing provides it.
64
+ #[derive(Debug, Clone, PartialEq)]
65
+ pub struct AuthorizationProvider {
66
+ /// Qualified grant command, e.g. `Governance::RoleAssignment.Assign`.
67
+ pub grant: String,
68
+ /// Qualified aggregate holding the assignments, e.g.
69
+ /// `Governance::RoleAssignment` — instance keys start `<this>#`.
70
+ pub assignment_aggregate: String,
71
+ }
72
+
73
+ pub fn authorization_provider(domain_ir: &Value) -> Option<AuthorizationProvider> {
74
+ let fact = domain_ir.get("authorization")?;
75
+ Some(AuthorizationProvider {
76
+ grant: fact.get("grant")?.as_str()?.to_string(),
77
+ assignment_aggregate: fact.get("assignment_aggregate")?.as_str()?.to_string(),
78
+ })
79
+ }
80
+
81
+ /// The chapter that answers "who may sign in", read from `ir.json`'s own
82
+ /// `membership` key. `None` when nothing provides `"membership"`.
83
+ #[derive(Debug, Clone, PartialEq)]
84
+ pub struct MembershipProvider {
85
+ /// Chapter name, e.g. `Membership`.
86
+ pub provider: String,
87
+ /// Qualified aggregate, e.g. `Membership::Person`.
88
+ pub aggregate: String,
89
+ }
90
+
91
+ /// Aggregates whose mutations mirror into their era head: the lineage-capable
92
+ /// ones plus the membership aggregate, which is not itself lineage-capable.
93
+ 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();
96
+ if let Some(membership) = membership_provider(domain_ir) {
97
+ mirrored.insert(membership.aggregate);
98
+ }
99
+ mirrored
100
+ }
101
+
102
+ pub fn membership_provider(domain_ir: &Value) -> Option<MembershipProvider> {
103
+ let fact = domain_ir.get("membership")?;
104
+ Some(MembershipProvider {
105
+ provider: fact.get("provider")?.as_str()?.to_string(),
106
+ aggregate: fact.get("aggregate")?.as_str()?.to_string(),
107
+ })
108
+ }
109
+
110
+ /// The chapter that answers "who is this authenticated pair", read from
111
+ /// `ir.json`'s own `identity` key. `None` when nothing provides `"identity"`.
112
+ /// Link's reference field must stay `identity` — never `identity_id`, never `to:`.
113
+ #[derive(Debug, Clone, PartialEq)]
114
+ pub struct IdentityProvider {
115
+ /// Chapter name, e.g. `Identity`.
116
+ pub provider: String,
117
+ /// Qualified register command, e.g. `Identity::Identity.Register`.
118
+ pub register: String,
119
+ /// Qualified link command, e.g. `Identity::ExternalIdentifier.Link`.
120
+ pub link: String,
121
+ /// Qualified resolve query, e.g. `Identity::ExternalIdentifier.ResolvedBy`.
122
+ pub resolve: String,
123
+ }
124
+
125
+ pub fn identity_provider(domain_ir: &Value) -> Option<IdentityProvider> {
126
+ let fact = domain_ir.get("identity")?;
127
+ Some(IdentityProvider {
128
+ provider: fact.get("provider")?.as_str()?.to_string(),
129
+ register: fact.get("register")?.as_str()?.to_string(),
130
+ link: fact.get("link")?.as_str()?.to_string(),
131
+ resolve: fact.get("resolve")?.as_str()?.to_string(),
132
+ })
133
+ }
134
+
135
+ /// The chapter that answers the guest newsletter signup, read from
136
+ /// `ir.json`'s own `newsletter` key. `None` when nothing provides it.
137
+ #[derive(Debug, Clone, PartialEq)]
138
+ pub struct NewsletterProvider {
139
+ /// Chapter name, e.g. `Newsletter`.
140
+ pub provider: String,
141
+ /// Qualified subscribe command, e.g. `Newsletter::Subscriber.Subscribe`.
142
+ pub subscribe: String,
143
+ /// Qualified add-name command, e.g. `Newsletter::Subscriber.AddName`.
144
+ pub add_name: String,
145
+ /// Qualified confirm command, e.g. `Newsletter::Subscriber.Confirm`.
146
+ pub confirm: String,
147
+ /// Qualified unsubscribe command, e.g. `Newsletter::Subscriber.Unsubscribe`.
148
+ pub unsubscribe: String,
149
+ /// Qualified subscribing aggregate, e.g. `Newsletter::Subscriber`.
150
+ pub aggregate: String,
151
+ }
152
+
153
+ impl NewsletterProvider {
154
+ /// The prefix every subscriber's key in a `dispatch::read` `instances`
155
+ /// map starts with, e.g. `Newsletter::Subscriber#`.
156
+ pub fn instance_prefix(&self) -> String {
157
+ format!("{}#", self.aggregate)
158
+ }
159
+ }
160
+
161
+ /// Whether the domain's own `aggregate`'s `command` declares an argument
162
+ /// named `field` — so a route only sends an argument the command declares.
163
+ pub fn command_declares(domain_ir: &Value, aggregate: &str, command: &str, field: &str) -> bool {
164
+ let named = |value: &Value, name: &str| value.get("name").and_then(|n| n.as_str()) == Some(name);
165
+ let Some(aggregates) = domain_ir.get("aggregates").and_then(|a| a.as_array()) else {
166
+ return false;
167
+ };
168
+ aggregates
169
+ .iter()
170
+ .filter(|a| named(a, aggregate))
171
+ .filter_map(|a| a.get("commands").and_then(|c| c.as_array()))
172
+ .flatten()
173
+ .filter(|c| named(c, command))
174
+ .filter_map(|c| c.get("attributes").and_then(|a| a.as_array()))
175
+ .flatten()
176
+ .any(|attribute| named(attribute, field))
177
+ }
178
+
179
+ pub fn newsletter_provider(domain_ir: &Value) -> Option<NewsletterProvider> {
180
+ let fact = domain_ir.get("newsletter")?;
181
+ Some(NewsletterProvider {
182
+ provider: fact.get("provider")?.as_str()?.to_string(),
183
+ subscribe: fact.get("subscribe")?.as_str()?.to_string(),
184
+ add_name: fact.get("add_name")?.as_str()?.to_string(),
185
+ confirm: fact.get("confirm")?.as_str()?.to_string(),
186
+ unsubscribe: fact.get("unsubscribe")?.as_str()?.to_string(),
187
+ aggregate: fact.get("aggregate")?.as_str()?.to_string(),
188
+ })
189
+ }
190
+
191
+ /// The chapter that declares `provides "newsletter_issues"`, read from
192
+ /// `ir.json`'s own `newsletter_issues` key. Omitted when nothing declares it.
193
+ #[derive(Debug, Clone, PartialEq)]
194
+ pub struct NewsletterIssuesProvider {
195
+ /// Qualified send command, e.g. `Newsletter::Issue.Send`.
196
+ pub send_issue: String,
197
+ /// Qualified delivery-record command, e.g. `Newsletter::Delivery.Record`.
198
+ pub record_delivery: String,
199
+ /// Qualified issue aggregate, e.g. `Newsletter::Issue`.
200
+ pub issue_aggregate: String,
201
+ }
202
+
203
+ impl NewsletterIssuesProvider {
204
+ /// The prefix every issue's key in a `dispatch::read` `instances` map
205
+ /// starts with, e.g. `Newsletter::Issue#`.
206
+ pub fn issue_prefix(&self) -> String {
207
+ format!("{}#", self.issue_aggregate)
208
+ }
209
+ }
210
+
211
+ /// The declared issue-sending verbs, or `None` when the IR has no
212
+ /// `newsletter_issues` key.
213
+ pub fn newsletter_issues_provider(domain_ir: &Value) -> Option<NewsletterIssuesProvider> {
214
+ let fact = domain_ir.get("newsletter_issues")?;
215
+ Some(NewsletterIssuesProvider {
216
+ send_issue: fact.get("send_issue")?.as_str()?.to_string(),
217
+ record_delivery: fact.get("record_delivery")?.as_str()?.to_string(),
218
+ issue_aggregate: fact.get("issue_aggregate")?.as_str()?.to_string(),
219
+ })
220
+ }
221
+
222
+
223
+ /// The chapter that takes payments, read from `ir.json`'s own `payments`
224
+ /// key. `None` when nothing provides it.
225
+ #[derive(Debug, Clone, PartialEq)]
226
+ pub struct PaymentsProvider {
227
+ /// Chapter name, e.g. `Payments`.
228
+ pub provider: String,
229
+ /// Qualified initiate command, e.g. `Payments::Payment.Initiate`.
230
+ pub initiate: String,
231
+ /// Qualified port operation the processor's success report dispatches,
232
+ /// e.g. `Payments::Payment.PaymentGateway.Succeeded`.
233
+ pub succeeded: String,
234
+ /// Qualified port operation the processor's failure report dispatches,
235
+ /// e.g. `Payments::Payment.PaymentGateway.Failed`.
236
+ pub failed: String,
237
+ /// Qualified paying aggregate, e.g. `Payments::Payment`.
238
+ pub aggregate: String,
239
+ }
240
+
241
+ impl PaymentsProvider {
242
+ /// The prefix every payment's key in a `dispatch::read` `instances` map
243
+ /// starts with, e.g. `Payments::Payment#`.
244
+ pub fn instance_prefix(&self) -> String {
245
+ format!("{}#", self.aggregate)
246
+ }
247
+ }
248
+
249
+ pub fn payments_provider(domain_ir: &Value) -> Option<PaymentsProvider> {
250
+ let fact = domain_ir.get("payments")?;
251
+ Some(PaymentsProvider {
252
+ provider: fact.get("provider")?.as_str()?.to_string(),
253
+ initiate: fact.get("initiate")?.as_str()?.to_string(),
254
+ succeeded: fact.get("succeeded")?.as_str()?.to_string(),
255
+ failed: fact.get("failed")?.as_str()?.to_string(),
256
+ aggregate: fact.get("aggregate")?.as_str()?.to_string(),
257
+ })
258
+ }
259
+
260
+ /// The chapter that answers scheduling sessions and taking registrations,
261
+ /// read from `ir.json`'s own `registrations` key. `None` when nothing provides it.
262
+ #[derive(Debug, Clone, PartialEq)]
263
+ pub struct RegistrationsProvider {
264
+ /// Chapter name, e.g. `Studio`.
265
+ pub provider: String,
266
+ /// Qualified schedule command, e.g. `Studio::Event.Schedule`.
267
+ pub schedule: String,
268
+ /// Qualified request command, e.g. `Studio::Registration.Request`.
269
+ pub request: String,
270
+ /// Qualified event aggregate, e.g. `Studio::Event`.
271
+ pub event_aggregate: String,
272
+ /// Qualified registration aggregate, e.g. `Studio::Registration`.
273
+ pub registration_aggregate: String,
274
+ }
275
+
276
+ impl RegistrationsProvider {
277
+ /// The conventional names for a domain that declares no
278
+ /// `provides "registrations"` — `Event`/`Registration` under the domain itself.
279
+ pub fn conventional(domain: &str) -> Self {
280
+ Self {
281
+ provider: domain.to_string(),
282
+ schedule: format!("{domain}::Event.Schedule"),
283
+ request: format!("{domain}::Registration.Request"),
284
+ event_aggregate: format!("{domain}::Event"),
285
+ registration_aggregate: format!("{domain}::Registration"),
286
+ }
287
+ }
288
+
289
+ /// The prefix every event's key in a `dispatch::read` `instances` map
290
+ /// starts with, e.g. `Studio::Event#`.
291
+ pub fn event_prefix(&self) -> String {
292
+ format!("{}#", self.event_aggregate)
293
+ }
294
+
295
+ /// The prefix every registration's key starts with, e.g.
296
+ /// `Studio::Registration#`.
297
+ pub fn registration_prefix(&self) -> String {
298
+ format!("{}#", self.registration_aggregate)
299
+ }
300
+
301
+ /// The request command's own aggregate and command name without the
302
+ /// chapter qualifier, e.g. `("Registration", "Request")`, as `command_declares` expects.
303
+ pub fn request_target(&self) -> Option<(&str, &str)> {
304
+ self.request.rsplit("::").next()?.split_once('.')
305
+ }
306
+ }
307
+
308
+ pub fn registrations_provider(domain_ir: &Value) -> Option<RegistrationsProvider> {
309
+ let fact = domain_ir.get("registrations")?;
310
+ Some(RegistrationsProvider {
311
+ provider: fact.get("provider")?.as_str()?.to_string(),
312
+ schedule: fact.get("schedule")?.as_str()?.to_string(),
313
+ request: fact.get("request")?.as_str()?.to_string(),
314
+ event_aggregate: fact.get("event_aggregate")?.as_str()?.to_string(),
315
+ registration_aggregate: fact.get("registration_aggregate")?.as_str()?.to_string(),
316
+ })
317
+ }
318
+
319
+ /// The registrations binding of the IR this host loaded, or the
320
+ /// conventional names for `domain` when the IR declares none.
321
+ pub fn registrations_binding(domain: &str) -> RegistrationsProvider {
322
+ ir().and_then(registrations_provider).unwrap_or_else(|| RegistrationsProvider::conventional(domain))
323
+ }
324
+
325
+ /// One command of the payment-processor connection, by the role it plays —
326
+ /// resolved through `PaymentConnectionProvider::verb`.
327
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
328
+ pub enum ConnectionVerb {
329
+ Connect,
330
+ Reconnect,
331
+ Disconnect,
332
+ Suspend,
333
+ Resume,
334
+ Enable,
335
+ Disable,
336
+ }
337
+
338
+ /// The chapter that owns the business's payment-processor connection, read
339
+ /// from `ir.json`'s own `payment_connection` key. `None` when nothing provides it.
340
+ #[derive(Debug, Clone, PartialEq)]
341
+ pub struct PaymentConnectionProvider {
342
+ /// Chapter name, e.g. `Studio`.
343
+ pub provider: String,
344
+ pub connect: String,
345
+ pub reconnect: String,
346
+ pub disconnect: String,
347
+ pub suspend: String,
348
+ pub resume: String,
349
+ pub enable: String,
350
+ pub disable: String,
351
+ /// Qualified connection aggregate, e.g. `Studio::PaymentConnection`.
352
+ pub aggregate: String,
353
+ }
354
+
355
+ impl PaymentConnectionProvider {
356
+ /// The conventional names for a domain that declares no
357
+ /// `provides "payment_connection"` — `PaymentConnection` under the domain itself.
358
+ pub fn conventional(domain: &str) -> Self {
359
+ let qualified = |command: &str| format!("{domain}::PaymentConnection.{command}");
360
+ Self {
361
+ provider: domain.to_string(),
362
+ connect: qualified("Connect"),
363
+ reconnect: qualified("Reconnect"),
364
+ disconnect: qualified("Disconnect"),
365
+ suspend: qualified("Suspend"),
366
+ resume: qualified("Resume"),
367
+ enable: qualified("EnablePayments"),
368
+ disable: qualified("DisablePayments"),
369
+ aggregate: format!("{domain}::PaymentConnection"),
370
+ }
371
+ }
372
+
373
+ /// The prefix every connection's key in a `dispatch::read` `instances`
374
+ /// map starts with, e.g. `Studio::PaymentConnection#`.
375
+ pub fn instance_prefix(&self) -> String {
376
+ format!("{}#", self.aggregate)
377
+ }
378
+
379
+ /// The qualified command that plays `which`.
380
+ pub fn verb(&self, which: ConnectionVerb) -> &str {
381
+ match which {
382
+ ConnectionVerb::Connect => &self.connect,
383
+ ConnectionVerb::Reconnect => &self.reconnect,
384
+ ConnectionVerb::Disconnect => &self.disconnect,
385
+ ConnectionVerb::Suspend => &self.suspend,
386
+ ConnectionVerb::Resume => &self.resume,
387
+ ConnectionVerb::Enable => &self.enable,
388
+ ConnectionVerb::Disable => &self.disable,
389
+ }
390
+ }
391
+ }
392
+
393
+ pub fn payment_connection_provider(domain_ir: &Value) -> Option<PaymentConnectionProvider> {
394
+ let fact = domain_ir.get("payment_connection")?;
395
+ let text = |key: &str| Some(fact.get(key)?.as_str()?.to_string());
396
+ Some(PaymentConnectionProvider {
397
+ provider: text("provider")?,
398
+ connect: text("connect")?,
399
+ reconnect: text("reconnect")?,
400
+ disconnect: text("disconnect")?,
401
+ suspend: text("suspend")?,
402
+ resume: text("resume")?,
403
+ enable: text("enable")?,
404
+ disable: text("disable")?,
405
+ aggregate: text("aggregate")?,
406
+ })
407
+ }
408
+
409
+ /// The payment-connection binding of the IR this host loaded, or the
410
+ /// conventional names for `domain` when the IR declares none.
411
+ pub fn payment_connection_binding(domain: &str) -> PaymentConnectionProvider {
412
+ ir().and_then(payment_connection_provider).unwrap_or_else(|| PaymentConnectionProvider::conventional(domain))
413
+ }
414
+
415
+ /// The checkout fixture's own payments binding, read from its committed
416
+ /// `ir.json` — what the checkout routes' tests dispatch against.
417
+ #[cfg(test)]
418
+ pub fn fixture_ir() -> Value {
419
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/generated/checkout_fixture/ir.json");
420
+ serde_json::from_str(&std::fs::read_to_string(path).expect("checkout_fixture ir.json")).expect("valid json")
421
+ }
422
+
423
+ /// The checkout fixture's own payments binding.
424
+ #[cfg(test)]
425
+ pub fn fixture_payments() -> PaymentsProvider {
426
+ payments_provider(&fixture_ir()).expect("the checkout fixture provides payments")
427
+ }
428
+
429
+ /// Refuses at boot rather than silently building a second, disjoint history
430
+ /// nothing but this runtime reads while the real state lives elsewhere.
431
+ pub fn refuse_unsupported_persistence_adapters(domain_ir: &Value) -> Result<(), String> {
432
+ let unsupported: Vec<String> = persistence_adapters(domain_ir)
433
+ .into_iter()
434
+ .filter(|(_, adapter)| !SUPPORTED_PERSISTENCE_ADAPTERS.contains(&adapter.as_str()))
435
+ .map(|(name, adapter)| format!("{name} (persisted_by {adapter:?})"))
436
+ .collect();
437
+ if unsupported.is_empty() {
438
+ return Ok(());
439
+ }
440
+ Err(format!(
441
+ "rust/host only has a backend for {SUPPORTED_PERSISTENCE_ADAPTERS:?} today; this domain \
442
+ also binds: {}. Dispatching against it here would silently build a second, disjoint \
443
+ history nothing but this runtime ever reads, while the real state stays wherever its own \
444
+ adapter actually wrote it — refusing instead of risking that.",
445
+ unsupported.join(", ")
446
+ ))
447
+ }
448
+
449
+ #[cfg(test)]
450
+ mod tests {
451
+ use super::*;
452
+
453
+ #[test]
454
+ fn lineage_capable_aggregates_qualifies_names_with_the_domain() {
455
+ let ir = serde_json::json!({
456
+ "name": "Pizzas",
457
+ "lineage": { "capable_aggregates": [{ "name": "Order", "storage_name": "order" }] }
458
+ });
459
+ assert_eq!(
460
+ lineage_capable_aggregates(&ir),
461
+ vec![("Pizzas::Order".to_string(), "order".to_string())]
462
+ );
463
+ }
464
+
465
+ #[test]
466
+ fn mirrored_aggregates_adds_a_vendored_membership_aggregate_to_the_capable_ones() {
467
+ let ir = serde_json::json!({
468
+ "name": "Shop",
469
+ "lineage": { "capable_aggregates": [{ "name": "Order", "storage_name": "order" }] },
470
+ "membership": { "provider": "Membership", "aggregate": "Membership::Person" }
471
+ });
472
+ let expected: std::collections::BTreeSet<String> =
473
+ ["Shop::Order", "Membership::Person"].into_iter().map(String::from).collect();
474
+ assert_eq!(mirrored_aggregates(&ir), expected);
475
+ }
476
+
477
+ #[test]
478
+ fn mirrored_aggregates_is_just_the_capable_ones_when_no_chapter_provides_membership() {
479
+ let ir = serde_json::json!({
480
+ "name": "Shop",
481
+ "lineage": { "capable_aggregates": [{ "name": "Order", "storage_name": "order" }] }
482
+ });
483
+ let expected: std::collections::BTreeSet<String> = ["Shop::Order"].into_iter().map(String::from).collect();
484
+ assert_eq!(mirrored_aggregates(&ir), expected);
485
+ assert!(mirrored_aggregates(&serde_json::json!({ "name": "Banking" })).is_empty());
486
+ }
487
+
488
+ #[test]
489
+ fn lineage_capable_aggregates_is_empty_for_a_domain_with_no_lineage_key() {
490
+ let ir = serde_json::json!({ "name": "Banking" });
491
+ assert_eq!(lineage_capable_aggregates(&ir), Vec::<(String, String)>::new());
492
+ }
493
+
494
+ #[test]
495
+ fn persistence_adapters_reads_every_declared_aggregate() {
496
+ let ir = serde_json::json!({
497
+ "name": "Banking",
498
+ "persistence": { "aggregates": [
499
+ { "name": "Account", "storage_name": "account", "adapter": "Heki" },
500
+ { "name": "Transfer", "storage_name": "transfer", "adapter": "Heki" }
501
+ ] }
502
+ });
503
+ assert_eq!(
504
+ persistence_adapters(&ir),
505
+ vec![
506
+ ("Account".to_string(), "Heki".to_string()),
507
+ ("Transfer".to_string(), "Heki".to_string())
508
+ ]
509
+ );
510
+ }
511
+
512
+ #[test]
513
+ fn persistence_adapters_is_empty_for_a_domain_with_no_persistence_key() {
514
+ let ir = serde_json::json!({ "name": "Pizzas" });
515
+ assert_eq!(persistence_adapters(&ir), Vec::<(String, String)>::new());
516
+ }
517
+
518
+ #[test]
519
+ fn refuse_unsupported_persistence_adapters_passes_postgres_and_postgres_era() {
520
+ let ir = serde_json::json!({
521
+ "name": "Pizzas",
522
+ "persistence": { "aggregates": [
523
+ { "name": "Order", "storage_name": "order", "adapter": "PostgresEra" }
524
+ ] }
525
+ });
526
+ assert!(refuse_unsupported_persistence_adapters(&ir).is_ok());
527
+ }
528
+
529
+ #[test]
530
+ fn refuse_unsupported_persistence_adapters_refuses_heki_by_name() {
531
+ let ir = serde_json::json!({
532
+ "name": "Banking",
533
+ "persistence": { "aggregates": [
534
+ { "name": "Account", "storage_name": "account", "adapter": "Heki" }
535
+ ] }
536
+ });
537
+ let err = refuse_unsupported_persistence_adapters(&ir).unwrap_err();
538
+ assert!(err.contains("Account"), "error should name the offending aggregate: {err}");
539
+ assert!(err.contains("Heki"), "error should name the unsupported adapter: {err}");
540
+ }
541
+
542
+ #[test]
543
+ fn membership_provider_reads_the_declared_capability() {
544
+ let ir = serde_json::json!({
545
+ "name": "Studio",
546
+ "membership": { "provider": "Membership", "aggregate": "Membership::Person" }
547
+ });
548
+ let provider = membership_provider(&ir).expect("should find membership");
549
+ assert_eq!(provider.provider, "Membership");
550
+ assert_eq!(provider.aggregate, "Membership::Person");
551
+ assert!(membership_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
552
+ }
553
+
554
+ #[test]
555
+ fn identity_provider_reads_the_declared_capability() {
556
+ let ir = serde_json::json!({
557
+ "name": "Studio",
558
+ "identity": {
559
+ "provider": "Identity",
560
+ "register": "Identity::Identity.Register",
561
+ "link": "Identity::ExternalIdentifier.Link",
562
+ "resolve": "Identity::ExternalIdentifier.ResolvedBy"
563
+ }
564
+ });
565
+ let provider = identity_provider(&ir).expect("should find identity");
566
+ assert_eq!(provider.provider, "Identity");
567
+ assert_eq!(provider.register, "Identity::Identity.Register");
568
+ assert_eq!(provider.link, "Identity::ExternalIdentifier.Link");
569
+ assert_eq!(provider.resolve, "Identity::ExternalIdentifier.ResolvedBy");
570
+ assert!(identity_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
571
+ }
572
+
573
+ #[test]
574
+ fn newsletter_provider_reads_the_declared_capability() {
575
+ let ir = serde_json::json!({
576
+ "name": "Studio",
577
+ "newsletter": {
578
+ "provider": "Newsletter",
579
+ "subscribe": "Newsletter::Subscriber.Subscribe",
580
+ "add_name": "Newsletter::Subscriber.AddName",
581
+ "confirm": "Newsletter::Subscriber.Confirm",
582
+ "unsubscribe": "Newsletter::Subscriber.Unsubscribe",
583
+ "aggregate": "Newsletter::Subscriber"
584
+ }
585
+ });
586
+ let provider = newsletter_provider(&ir).expect("should find newsletter");
587
+ assert_eq!(provider.provider, "Newsletter");
588
+ assert_eq!(provider.subscribe, "Newsletter::Subscriber.Subscribe");
589
+ assert_eq!(provider.add_name, "Newsletter::Subscriber.AddName");
590
+ assert_eq!(provider.confirm, "Newsletter::Subscriber.Confirm");
591
+ assert_eq!(provider.unsubscribe, "Newsletter::Subscriber.Unsubscribe");
592
+ assert_eq!(provider.instance_prefix(), "Newsletter::Subscriber#");
593
+ assert!(newsletter_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
594
+ }
595
+
596
+ #[test]
597
+ fn newsletter_issues_provider_reads_the_declared_capability() {
598
+ let ir = serde_json::json!({
599
+ "name": "Studio",
600
+ "newsletter_issues": {
601
+ "provider": "Newsletter",
602
+ "send_issue": "Newsletter::Issue.Send",
603
+ "record_delivery": "Newsletter::Delivery.Record",
604
+ "issue_aggregate": "Newsletter::Issue",
605
+ "delivery_aggregate": "Newsletter::Delivery"
606
+ }
607
+ });
608
+ let provider = newsletter_issues_provider(&ir).expect("should find newsletter_issues");
609
+ assert_eq!(provider.send_issue, "Newsletter::Issue.Send");
610
+ assert_eq!(provider.record_delivery, "Newsletter::Delivery.Record");
611
+ assert_eq!(provider.issue_prefix(), "Newsletter::Issue#");
612
+ assert!(newsletter_issues_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
613
+ }
614
+
615
+ #[test]
616
+ fn payments_provider_reads_the_declared_capability() {
617
+ let ir = serde_json::json!({
618
+ "name": "Studio",
619
+ "payments": {
620
+ "provider": "Payments",
621
+ "initiate": "Payments::Payment.Initiate",
622
+ "succeeded": "Payments::Payment.PaymentGateway.Succeeded",
623
+ "failed": "Payments::Payment.PaymentGateway.Failed",
624
+ "aggregate": "Payments::Payment"
625
+ }
626
+ });
627
+ let provider = payments_provider(&ir).expect("should find payments");
628
+ assert_eq!(provider.provider, "Payments");
629
+ assert_eq!(provider.initiate, "Payments::Payment.Initiate");
630
+ assert_eq!(provider.succeeded, "Payments::Payment.PaymentGateway.Succeeded");
631
+ assert_eq!(provider.failed, "Payments::Payment.PaymentGateway.Failed");
632
+ assert_eq!(provider.instance_prefix(), "Payments::Payment#");
633
+ assert!(payments_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
634
+ }
635
+
636
+ #[test]
637
+ fn registrations_provider_reads_the_declared_capability() {
638
+ let provider = registrations_provider(&fixture_ir()).expect("the checkout fixture provides registrations");
639
+ assert_eq!(provider.schedule, "CheckoutFixture::Event.Schedule");
640
+ assert_eq!(provider.request, "CheckoutFixture::Registration.Request");
641
+ assert_eq!(provider.event_prefix(), "CheckoutFixture::Event#");
642
+ assert_eq!(provider.registration_prefix(), "CheckoutFixture::Registration#");
643
+ assert_eq!(provider.request_target(), Some(("Registration", "Request")));
644
+ }
645
+
646
+ #[test]
647
+ fn the_conventional_registrations_names_match_what_the_fixture_declares() {
648
+ assert_eq!(registrations_provider(&fixture_ir()), Some(RegistrationsProvider::conventional("CheckoutFixture")));
649
+ }
650
+
651
+ #[test]
652
+ fn a_declared_registrations_binding_names_whatever_the_chapter_calls_things() {
653
+ let ir = serde_json::json!({"registrations": {
654
+ "provider": "Bookings",
655
+ "schedule": "Bookings::Session.Plan",
656
+ "request": "Bookings::Seat.Claim",
657
+ "event_aggregate": "Bookings::Session",
658
+ "registration_aggregate": "Bookings::Seat"
659
+ }});
660
+ let provider = registrations_provider(&ir).expect("declared");
661
+ assert_eq!(provider.event_prefix(), "Bookings::Session#");
662
+ assert_eq!(provider.registration_prefix(), "Bookings::Seat#");
663
+ assert_eq!(provider.request_target(), Some(("Seat", "Claim")));
664
+ }
665
+
666
+ #[test]
667
+ fn registrations_provider_is_none_without_the_key() {
668
+ assert_eq!(registrations_provider(&serde_json::json!({"name": "Pizzas"})), None);
669
+ }
670
+
671
+ #[test]
672
+ fn registrations_binding_falls_back_to_the_conventional_names_when_no_ir_is_loaded() {
673
+ assert_eq!(registrations_binding("Shop"), RegistrationsProvider::conventional("Shop"));
674
+ }
675
+
676
+ #[test]
677
+ fn payment_connection_provider_reads_the_declared_capability() {
678
+ let provider = payment_connection_provider(&fixture_ir()).expect("the checkout fixture provides payment_connection");
679
+ assert_eq!(provider.verb(ConnectionVerb::Connect), "CheckoutFixture::PaymentConnection.Connect");
680
+ assert_eq!(provider.verb(ConnectionVerb::Enable), "CheckoutFixture::PaymentConnection.EnablePayments");
681
+ assert_eq!(provider.verb(ConnectionVerb::Disable), "CheckoutFixture::PaymentConnection.DisablePayments");
682
+ assert_eq!(provider.instance_prefix(), "CheckoutFixture::PaymentConnection#");
683
+ }
684
+
685
+ #[test]
686
+ fn the_conventional_payment_connection_names_match_what_the_fixture_declares() {
687
+ assert_eq!(payment_connection_provider(&fixture_ir()), Some(PaymentConnectionProvider::conventional("CheckoutFixture")));
688
+ }
689
+
690
+ #[test]
691
+ fn a_declared_payment_connection_binding_names_whatever_the_chapter_calls_things() {
692
+ let ir = serde_json::json!({"payment_connection": {
693
+ "provider": "Billing",
694
+ "connect": "Billing::Link.Open", "reconnect": "Billing::Link.Reopen",
695
+ "disconnect": "Billing::Link.Close", "suspend": "Billing::Link.Pause",
696
+ "resume": "Billing::Link.Unpause", "enable": "Billing::Link.TurnOn",
697
+ "disable": "Billing::Link.TurnOff", "aggregate": "Billing::Link"
698
+ }});
699
+ let provider = payment_connection_provider(&ir).expect("declared");
700
+ assert_eq!(provider.verb(ConnectionVerb::Suspend), "Billing::Link.Pause");
701
+ assert_eq!(provider.verb(ConnectionVerb::Resume), "Billing::Link.Unpause");
702
+ assert_eq!(provider.instance_prefix(), "Billing::Link#");
703
+ }
704
+
705
+ #[test]
706
+ fn payment_connection_binding_falls_back_to_the_conventional_names_when_no_ir_is_loaded() {
707
+ assert_eq!(payment_connection_binding("Shop"), PaymentConnectionProvider::conventional("Shop"));
708
+ }
709
+
710
+ #[test]
711
+ fn command_declares_finds_an_argument_on_the_named_command_only() {
712
+ let ir = serde_json::json!({
713
+ "aggregates": [
714
+ {"name": "Registration", "commands": [
715
+ {"name": "Request", "attributes": [{"name": "email"}, {"name": "news_signup"}]},
716
+ {"name": "Cancel", "attributes": [{"name": "reason"}]}
717
+ ]},
718
+ {"name": "Event", "commands": [{"name": "Request", "attributes": [{"name": "slug"}]}]}
719
+ ]
720
+ });
721
+ assert!(command_declares(&ir, "Registration", "Request", "news_signup"));
722
+ assert!(!command_declares(&ir, "Registration", "Cancel", "news_signup"));
723
+ assert!(!command_declares(&ir, "Event", "Request", "news_signup"));
724
+ assert!(!command_declares(&ir, "Registration", "Request", "phone"));
725
+ assert!(!command_declares(&serde_json::json!({"name": "Pizzas"}), "Registration", "Request", "news_signup"));
726
+ }
727
+
728
+ #[test]
729
+ fn refuse_unsupported_persistence_adapters_passes_a_domain_with_no_persistence_key() {
730
+ let ir = serde_json::json!({ "name": "Pizzas" });
731
+ assert!(refuse_unsupported_persistence_adapters(&ir).is_ok());
732
+ }
733
+ }