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,1090 @@
1
+ //! Generic dispatch loops over the declared `AggregateStep`/`EntityStep` order.
2
+ //! Argument gates run in `decode_*_arguments`, before the router resolves identity.
3
+
4
+ use super::expr::{interpret, EvalContext, Expr, Field, Fielded, NoFields, StateFirst, Value, WithOld, WithParent};
5
+ use super::refusal_wording::{
6
+ AlreadyExistsCreatingDuplicateArgs, LifecycleRefusedTransitionBlockedArgs, NotFoundCreatingNoIdentityArgs, NotFoundEntityElementMissingArgs,
7
+ NotFoundRecordMissingArgs,
8
+ };
9
+ use super::vocab::{AggregateStep, EntityStep};
10
+ use super::{Event, Json, MutationRecord, Refusal, Repository, SetProjectedField, ToJson};
11
+
12
+ pub struct GivenSpec {
13
+ pub description: &'static str,
14
+ pub expr: Expr,
15
+ // Carries the event name a `corrects` given checks, so the refusal can interpolate the
16
+ // record's id (the static `description` cannot). `None` for every ordinary given.
17
+ pub corrects_event: Option<&'static str>,
18
+ }
19
+
20
+ /// An `ensures` rule, checked after `apply_mutations`, with `old` bound to the prior state.
21
+ pub struct EnsuresSpec {
22
+ pub description: &'static str,
23
+ pub expr: Expr,
24
+ }
25
+
26
+ /// An invariant rule, checked after `ensures` and before save on the candidate record.
27
+ /// Entity invariants run per list element with `parent` bound to the owner.
28
+ pub struct InvariantSpec {
29
+ pub description: &'static str,
30
+ pub expr: Expr,
31
+ }
32
+
33
+ pub struct EntityInvariants {
34
+ pub name: &'static str,
35
+ pub list_field: &'static str,
36
+ pub specs: Vec<InvariantSpec>,
37
+ pub nested: Vec<EntityInvariants>,
38
+ }
39
+
40
+ pub struct InvariantSet {
41
+ pub aggregate: Vec<InvariantSpec>,
42
+ pub entities: Vec<EntityInvariants>,
43
+ }
44
+
45
+ pub fn enforce_invariants(record: &dyn Fielded, aggregate_name: &str, set: &InvariantSet) -> Result<(), Refusal> {
46
+ for rule in &set.aggregate {
47
+ let ctx = EvalContext { args: &NoFields, instance: record };
48
+ if !interpret(&rule.expr, &ctx)?.truthy() {
49
+ return Err(Refusal::InvariantViolation(format!("{aggregate_name} refused — {}", rule.description)));
50
+ }
51
+ }
52
+ for entity in &set.entities {
53
+ enforce_entity_invariants(record, entity)?;
54
+ }
55
+ Ok(())
56
+ }
57
+
58
+ fn enforce_entity_invariants(owner: &dyn Fielded, entity: &EntityInvariants) -> Result<(), Refusal> {
59
+ let Some(elements) = owner.items(entity.list_field) else { return Ok(()) };
60
+ for element in elements {
61
+ let Field::Nested(element) = element else { continue };
62
+ let with_parent = WithParent { args: &NoFields, parent: owner };
63
+ for rule in &entity.specs {
64
+ let ctx = EvalContext { args: &with_parent, instance: element };
65
+ if !interpret(&rule.expr, &ctx)?.truthy() {
66
+ return Err(Refusal::InvariantViolation(format!("{} refused — {}", entity.name, rule.description)));
67
+ }
68
+ }
69
+ for nested in &entity.nested {
70
+ enforce_entity_invariants(element, nested)?;
71
+ }
72
+ }
73
+ Ok(())
74
+ }
75
+
76
+ /// The check half of a lifecycle transition: the current state must be in `from_states`.
77
+ ///
78
+ /// The write half is folded into the generated `apply_mutations` closure as its last line.
79
+ pub struct TransitionCheck {
80
+ pub field: &'static str,
81
+ pub from_states: &'static [&'static str],
82
+ }
83
+
84
+ /// How a dispatch obtains its starting record: `Create` when the command declares no
85
+ /// `references`, `Act` when it addresses an existing record.
86
+ ///
87
+ /// `'a` lets `build` borrow `args`, which is also borrowed as `&dyn Fielded` in the same call.
88
+ pub enum Hydrate<'a, T> {
89
+ /// A creating command: mint the identity, build a fresh record if nothing answers to it.
90
+ /// `build` assigns creation attributes (implicit, name-matched), not a `sets`.
91
+ ///
92
+ /// `state_independent` defers the `AlreadyExists` check to just before `repo.save`, for
93
+ /// commands whose givens, ensures and mutations never read the aggregate's own state.
94
+ Create { id: String, build: Box<dyn FnOnce() -> T + 'a>, state_independent: bool },
95
+ /// An acting command: the identity already names a record.
96
+ Act { id: String },
97
+ }
98
+
99
+ #[allow(clippy::too_many_arguments)]
100
+ pub fn dispatch<'a, T, R>(
101
+ repo: &mut R,
102
+ hydrate: Hydrate<'a, T>,
103
+ command_name: &'static str,
104
+ aggregate_qualified_name: &'static str,
105
+ // Bare `hecks_name` and the declared identity reading, as refusal wording quotes them;
106
+ // `aggregate_qualified_name` (`{domain}::{aggregate}`) is what events and records use.
107
+ aggregate_name: &'static str,
108
+ identity_reading: &'static str,
109
+ args: &'a dyn Fielded,
110
+ givens: &[GivenSpec],
111
+ transition: Option<TransitionCheck>,
112
+ // Captures the typed args struct by reference (not `move`, like `Hydrate::Create.build`);
113
+ // writing a typed field needs the real type, which `&dyn Fielded` erases.
114
+ apply_mutations: impl FnOnce(&mut T) -> Result<(), Refusal> + 'a,
115
+ ensures: &[EnsuresSpec],
116
+ invariants: &InvariantSet,
117
+ emits: &[&'static str],
118
+ payload: Json,
119
+ mutations: &mut Vec<MutationRecord>,
120
+ // The synchronous half of `projects`, applied right before `repo.save`.
121
+ seed_projections: Vec<(&'static str, Option<String>)>,
122
+ // The write-side tenant boundary. The router computes it (it needs every aggregate's repo)
123
+ // but it is applied here, first in `Save`, before `seed_projections`.
124
+ tenant_boundary_check: Result<(), Refusal>,
125
+ ) -> Result<(T, Vec<Event>), Refusal>
126
+ where
127
+ T: Fielded + Clone + ToJson + SetProjectedField,
128
+ R: Repository<T>,
129
+ {
130
+ // Set by a state-independent `Hydrate::Create`; checked in `Save`, just before `repo.save`.
131
+ let mut defer_existence_check = false;
132
+
133
+ // Each owned or `FnOnce` input is consumed by one arm via `take()`; the const assertions
134
+ // below guarantee `hydrated` is set before any arm reads it.
135
+ let mut hydrate = Some(hydrate);
136
+ let mut apply_mutations = Some(apply_mutations);
137
+ let mut tenant_boundary_check = Some(tenant_boundary_check);
138
+ let mut seed_projections = Some(seed_projections);
139
+ let mut hydrated: Option<(String, T)> = None;
140
+ let mut old_snapshot: Option<T> = None;
141
+ let mut events: Vec<Event> = Vec::new();
142
+
143
+ for step in AggregateStep::ORDER {
144
+ #[deny(clippy::wildcard_enum_match_arm)]
145
+ match step {
146
+ // Already run by `decode_aggregate_arguments`; see `aggregate_step_site`.
147
+ // `aggregate_step_site`.
148
+ AggregateStep::DecodeArguments
149
+ | AggregateStep::RefuseUnknownArguments
150
+ | AggregateStep::RefuseAbsentArguments
151
+ | AggregateStep::NormalizeArgs
152
+ | AggregateStep::RefuseRoleMismatch
153
+ | AggregateStep::ResolveReferences => {}
154
+ AggregateStep::Hydrate => {
155
+ let hydrate = hydrate.take().expect(ONCE);
156
+ hydrated = Some(hydrate_record(
157
+ repo,
158
+ hydrate,
159
+ command_name,
160
+ aggregate_name,
161
+ identity_reading,
162
+ &mut defer_existence_check,
163
+ )?);
164
+ }
165
+ AggregateStep::EnforceGivens => {
166
+ let (id, record) = hydrated.as_ref().expect(HYDRATED);
167
+ enforce_givens(record, args, givens, command_name, aggregate_qualified_name, id)?;
168
+ }
169
+ AggregateStep::AdmissibleTransition => {
170
+ let (_, record) = hydrated.as_ref().expect(HYDRATED);
171
+ admissible_transition(record, transition.as_ref(), command_name)?;
172
+ }
173
+ // Folded into `Hydrate::Create.build`: givens already saw the built record.
174
+ AggregateStep::AssignCreationAttributes => {}
175
+ AggregateStep::ApplyMutations => {
176
+ let (_, record) = hydrated.as_mut().expect(HYDRATED);
177
+ // The state as the givens saw it — `old` inside an `ensures`,
178
+ // `old` for `ensures`: the state before the mutation, cloned only if needed.
179
+ old_snapshot = if ensures.is_empty() { None } else { Some(record.clone()) };
180
+ (apply_mutations.take().expect(ONCE))(record)?;
181
+ }
182
+ // Written by the generated `apply_mutations` closure as its last line.
183
+ AggregateStep::AdvanceLifecycle => {}
184
+ // Performed inside the generated `apply_mutations` closure, which calls
185
+ // `apply_entity_command` on the record in hand.
186
+ AggregateStep::DelegateToEntity => {}
187
+ AggregateStep::EnforceEnsures => {
188
+ let (_, record) = hydrated.as_ref().expect(HYDRATED);
189
+ if let Some(old) = &old_snapshot {
190
+ enforce_ensures(record, old, args, ensures, command_name)?;
191
+ }
192
+ }
193
+ AggregateStep::EnforceInvariants => {
194
+ let (_, record) = hydrated.as_ref().expect(HYDRATED);
195
+ enforce_invariants(record, aggregate_name, invariants)?;
196
+ }
197
+ AggregateStep::Save => {
198
+ let (id, record) = hydrated.as_mut().expect(HYDRATED);
199
+ // Tenant boundary first: it precedes `seed_projections` and the write.
200
+ tenant_boundary_check.take().expect(ONCE)?;
201
+
202
+ for (field, value) in seed_projections.take().expect(ONCE) {
203
+ record.set_projected_field(field, value);
204
+ }
205
+
206
+ // Deferred `AlreadyExists` check for state-independent creates: runs after
207
+ // every other step, immediately before the write.
208
+ if defer_existence_check && repo.find(id.as_str()).is_some() {
209
+ return Err(Refusal::AlreadyExists(
210
+ AlreadyExistsCreatingDuplicateArgs {
211
+ command: command_name,
212
+ aggregate: aggregate_name,
213
+ identity: identity_reading,
214
+ offered: &format!("{id:?}"),
215
+ }
216
+ .render_args(),
217
+ ));
218
+ }
219
+
220
+ persist(repo, id, record, aggregate_qualified_name, mutations);
221
+ }
222
+ AggregateStep::Emit => {
223
+ let (id, _) = hydrated.as_ref().expect(HYDRATED);
224
+ events = emitted(emits, aggregate_qualified_name, id, &payload);
225
+ }
226
+ }
227
+ }
228
+
229
+ let (_, record) = hydrated.expect(HYDRATED);
230
+ Ok((record, events))
231
+ }
232
+
233
+ /// `hydrate` — how this dispatch obtains its starting record (see
234
+ /// `Hydrate`'s own comment for the branch it takes).
235
+ fn hydrate_record<T, R>(
236
+ repo: &R,
237
+ hydrate: Hydrate<'_, T>,
238
+ command_name: &'static str,
239
+ aggregate_name: &'static str,
240
+ identity_reading: &'static str,
241
+ defer_existence_check: &mut bool,
242
+ ) -> Result<(String, T), Refusal>
243
+ where
244
+ T: Clone,
245
+ R: Repository<T>,
246
+ {
247
+ match hydrate {
248
+ Hydrate::Create { id, build, state_independent } => {
249
+ // A blank identity names nothing; refuse before any lookup rather than mint a
250
+ // record keyed by "".
251
+ if id.is_empty() {
252
+ return Err(Refusal::NotFound(
253
+ NotFoundCreatingNoIdentityArgs { command: command_name, aggregate: aggregate_name, identity: identity_reading }
254
+ .render_args(),
255
+ ));
256
+ }
257
+ // State-independent creates defer this check to `Save`.
258
+ if state_independent {
259
+ *defer_existence_check = true;
260
+ } else if repo.find(&id).is_some() {
261
+ return Err(Refusal::AlreadyExists(
262
+ AlreadyExistsCreatingDuplicateArgs {
263
+ command: command_name,
264
+ aggregate: aggregate_name,
265
+ identity: identity_reading,
266
+ offered: &format!("{id:?}"),
267
+ }
268
+ .render_args(),
269
+ ));
270
+ }
271
+ Ok((id, build()))
272
+ }
273
+ Hydrate::Act { id } => {
274
+ // Shared with `dispatch_entity`'s parent lookup: both raise `record_missing`.
275
+ let record = repo.find(&id).ok_or_else(|| {
276
+ Refusal::NotFound(
277
+ NotFoundRecordMissingArgs { aggregate: aggregate_name, identity: identity_reading, offered: &format!("{id:?}") }
278
+ .render_args(),
279
+ )
280
+ })?;
281
+ Ok((id, record))
282
+ }
283
+ }
284
+ }
285
+
286
+ /// `enforce_givens` on an aggregate record.
287
+ fn enforce_givens(
288
+ record: &dyn Fielded,
289
+ args: &dyn Fielded,
290
+ givens: &[GivenSpec],
291
+ command_name: &str,
292
+ aggregate_qualified_name: &str,
293
+ id: &str,
294
+ ) -> Result<(), Refusal> {
295
+ for given in givens {
296
+ let ctx = EvalContext { args, instance: record };
297
+ if !interpret(&given.expr, &ctx)?.truthy() {
298
+ // The `corrects` wording interpolates the record id, so it is built here.
299
+ if let Some(event_name) = given.corrects_event {
300
+ // `NothingToCorrect`, not `GivenNotMet`: the conformance corpus compares kinds.
301
+ return Err(Refusal::NothingToCorrect(format!(
302
+ "{command_name} refused — corrects {event_name}, but {aggregate_qualified_name} #{id} has never emitted it"
303
+ )));
304
+ }
305
+ return Err(Refusal::GivenNotMet(format!("{command_name} refused — {}", given.description)));
306
+ }
307
+ }
308
+ Ok(())
309
+ }
310
+
311
+ /// `admissible_transition` — shared by the aggregate record and an entity
312
+ /// element, which check it identically.
313
+ fn admissible_transition(instance: &dyn Fielded, transition: Option<&TransitionCheck>, command_name: &str) -> Result<(), Refusal> {
314
+ let Some(check) = transition else { return Ok(()) };
315
+ match instance.field(check.field) {
316
+ Some(Field::Value(Value::Str(current))) => {
317
+ if !check.from_states.contains(&current.as_str()) {
318
+ // `allowed` is `from_states`; `render_args` quotes each and joins with " or ".
319
+ return Err(Refusal::LifecycleRefused(
320
+ LifecycleRefusedTransitionBlockedArgs {
321
+ command: command_name,
322
+ field: check.field,
323
+ current: &format!("{current:?}"),
324
+ allowed: check.from_states,
325
+ }
326
+ .render_args(),
327
+ ));
328
+ }
329
+ Ok(())
330
+ }
331
+ // Unreachable for generated records (the lifecycle field is always a string): a
332
+ // codegen bug, surfaced as TypeMismatch with no `RefusalSite` template.
333
+ _ => Err(Refusal::TypeMismatch(format!(
334
+ "{command_name}: lifecycle field {:?} missing or not a string — a codegen bug",
335
+ check.field
336
+ ))),
337
+ }
338
+ }
339
+
340
+ /// `enforce_ensures` against the settled record (or element), `old` merged
341
+ /// into the args it reads.
342
+ fn enforce_ensures(
343
+ settled: &dyn Fielded,
344
+ old: &dyn Fielded,
345
+ args: &dyn Fielded,
346
+ ensures: &[EnsuresSpec],
347
+ command_name: &str,
348
+ ) -> Result<(), Refusal> {
349
+ // C2.3 — the settled state first; see `StateFirst`.
350
+ let state_first = StateFirst { args, settled };
351
+ let with_old = WithOld { args: &state_first, old };
352
+ for rule in ensures {
353
+ let ctx = EvalContext { args: &with_old, instance: settled };
354
+ if !interpret(&rule.expr, &ctx)?.truthy() {
355
+ return Err(Refusal::EnsuresNotMet(format!("{command_name} refused — {}", rule.description)));
356
+ }
357
+ }
358
+ Ok(())
359
+ }
360
+
361
+ /// The write half of `save`: the repository write and its mutation record.
362
+ fn persist<T, R>(repo: &mut R, id: &str, record: &T, aggregate_qualified_name: &str, mutations: &mut Vec<MutationRecord>)
363
+ where
364
+ T: Clone + ToJson,
365
+ R: Repository<T>,
366
+ {
367
+ repo.save(id, record.clone());
368
+ mutations.push(MutationRecord {
369
+ aggregate: aggregate_qualified_name.to_string(),
370
+ id: id.to_string(),
371
+ operation: "save",
372
+ state: record.to_json(),
373
+ });
374
+ }
375
+
376
+ /// `emit` — the declared events, in declared order.
377
+ fn emitted(emits: &[&'static str], aggregate_qualified_name: &str, id: &str, payload: &Json) -> Vec<Event> {
378
+ emits
379
+ .iter()
380
+ .map(|name| Event {
381
+ name: name.to_string(),
382
+ aggregate: aggregate_qualified_name.to_string(),
383
+ id: id.to_string(),
384
+ payload: payload.clone(),
385
+ // Stamped later by `orchestrate`; this constructor has no clock.
386
+ occurred_at: None,
387
+ correlation: None,
388
+ })
389
+ .collect()
390
+ }
391
+
392
+ /// The element half of an entity command on a parent record already in hand, nothing saved:
393
+ /// locate, givens, transition, mutate, ensures.
394
+ ///
395
+ /// Shared by `dispatch_entity` and delegating aggregate commands so a refusal reads the same
396
+ /// through either. `parent_in_args` binds `parent:` (the live owning record) into given and
397
+ /// ensures scope, so an ensures sees the parent after the mutation. Both callers pass `true`:
398
+ /// the routing layer's `parent_deref` snapshot is never refreshed after `apply_mutations`.
399
+ #[allow(clippy::too_many_arguments)]
400
+ pub fn apply_entity_command<'a, T, E>(
401
+ record: &mut T,
402
+ parent_id: &str,
403
+ get_list: impl Fn(&T) -> &Vec<E>,
404
+ get_list_mut: impl FnOnce(&mut T) -> &mut Vec<E>,
405
+ matches: impl Fn(&E) -> bool,
406
+ command_name: &'static str,
407
+ // Renders `NothingToCorrect` wording, which qualifies the aggregate name.
408
+ aggregate_qualified_name: &'static str,
409
+ aggregate_name: &'static str,
410
+ entity_name: &'static str,
411
+ entity_identity_reading: &'static str,
412
+ wants: &str,
413
+ args: &'a dyn Fielded,
414
+ givens: &[GivenSpec],
415
+ transition: Option<TransitionCheck>,
416
+ apply_mutations: impl FnOnce(&mut E) -> Result<(), Refusal> + 'a,
417
+ ensures: &[EnsuresSpec],
418
+ parent_in_args: bool,
419
+ ) -> Result<(), Refusal>
420
+ where
421
+ T: Fielded + Clone,
422
+ E: Fielded + Clone,
423
+ {
424
+ let mut element = ElementHalf {
425
+ parent_id,
426
+ get_list,
427
+ get_list_mut: Some(get_list_mut),
428
+ matches,
429
+ command_name,
430
+ aggregate_qualified_name,
431
+ aggregate_name,
432
+ entity_name,
433
+ entity_identity_reading,
434
+ wants,
435
+ args,
436
+ givens,
437
+ transition,
438
+ apply_mutations: Some(apply_mutations),
439
+ ensures,
440
+ parent_in_args,
441
+ position: None,
442
+ element: None,
443
+ parent_before: None,
444
+ old_snapshot: None,
445
+ };
446
+ for step in EntityStep::ORDER {
447
+ element.run(step, record)?;
448
+ }
449
+ Ok(())
450
+ }
451
+
452
+ /// The element half's state, carried across `EntityStep::ORDER` — what
453
+ /// `apply_entity_command` and `dispatch_entity` both drive, one step at a
454
+ /// time, so a refusal reads identically through either caller.
455
+ struct ElementHalf<'a, 's, T, E, GetList, GetListMut, Matches, Apply> {
456
+ parent_id: &'s str,
457
+ get_list: GetList,
458
+ get_list_mut: Option<GetListMut>,
459
+ matches: Matches,
460
+ command_name: &'static str,
461
+ aggregate_qualified_name: &'static str,
462
+ aggregate_name: &'static str,
463
+ entity_name: &'static str,
464
+ entity_identity_reading: &'static str,
465
+ wants: &'s str,
466
+ args: &'a dyn Fielded,
467
+ givens: &'s [GivenSpec],
468
+ transition: Option<TransitionCheck>,
469
+ apply_mutations: Option<Apply>,
470
+ ensures: &'s [EnsuresSpec],
471
+ parent_in_args: bool,
472
+ position: Option<usize>,
473
+ element: Option<E>,
474
+ parent_before: Option<T>,
475
+ old_snapshot: Option<E>,
476
+ }
477
+
478
+ impl<'a, 's, T, E, GetList, GetListMut, Matches, Apply> ElementHalf<'a, 's, T, E, GetList, GetListMut, Matches, Apply>
479
+ where
480
+ T: Fielded + Clone,
481
+ E: Fielded + Clone,
482
+ GetList: Fn(&T) -> &Vec<E>,
483
+ GetListMut: FnOnce(&mut T) -> &mut Vec<E>,
484
+ Matches: Fn(&E) -> bool,
485
+ Apply: FnOnce(&mut E) -> Result<(), Refusal>,
486
+ {
487
+ /// One declared entity step against the parent `record` already in hand.
488
+ fn run(&mut self, step: EntityStep, record: &mut T) -> Result<(), Refusal> {
489
+ #[deny(clippy::wildcard_enum_match_arm)]
490
+ match step {
491
+ // Already run by `decode_entity_arguments`; see `entity_step_site`.
492
+ EntityStep::DecodeArguments
493
+ | EntityStep::RefuseUnknownArguments
494
+ | EntityStep::RefuseAbsentArguments
495
+ | EntityStep::NormalizeArgs
496
+ | EntityStep::RefuseRoleMismatch
497
+ | EntityStep::ResolveReferences => Ok(()),
498
+ // The parent half: `dispatch_entity`'s own arms, or the delegating aggregate's
499
+ // `dispatch`.
500
+ EntityStep::HydrateParent | EntityStep::EnforceInvariants | EntityStep::Save | EntityStep::Emit => Ok(()),
501
+ EntityStep::LocateElement => self.locate_element(record),
502
+ EntityStep::EnforceGivens => self.enforce_givens(),
503
+ EntityStep::AdmissibleTransition => {
504
+ admissible_transition(self.element.as_ref().expect(LOCATED), self.transition.as_ref(), self.command_name)
505
+ }
506
+ EntityStep::ApplyMutations => self.apply_mutations(record),
507
+ // Written by the generated `apply_mutations` closure (the entity's own lifecycle).
508
+ EntityStep::AdvanceLifecycle => Ok(()),
509
+ EntityStep::EnforceEnsures => self.enforce_ensures(record),
510
+ }
511
+ }
512
+
513
+ fn locate_element(&mut self, record: &T) -> Result<(), Refusal> {
514
+ let position = (self.get_list)(record).iter().position(|el| (self.matches)(el)).ok_or_else(|| {
515
+ Refusal::NotFound(
516
+ NotFoundEntityElementMissingArgs {
517
+ entity: self.entity_name,
518
+ identity: self.entity_identity_reading,
519
+ wants: self.wants,
520
+ aggregate: self.aggregate_name,
521
+ parent_id: &format!("{:?}", self.parent_id),
522
+ }
523
+ .render_args(),
524
+ )
525
+ })?;
526
+ self.element = Some((self.get_list)(record)[position].clone());
527
+ self.position = Some(position);
528
+ self.parent_before = Some(record.clone());
529
+ Ok(())
530
+ }
531
+
532
+ fn enforce_givens(&self) -> Result<(), Refusal> {
533
+ let parent_before = self.parent_before.as_ref().expect(LOCATED);
534
+ let element = self.element.as_ref().expect(LOCATED);
535
+
536
+ // Entity-level `corrects` is asked of the parent record and root aggregate, since an
537
+ // entity has no event stream. It runs after the element lookup, before declared givens,
538
+ // and proves the parent emitted the event, not that this element did.
539
+ for given in self.givens {
540
+ let Some(event_name) = given.corrects_event else { continue };
541
+ let ctx = EvalContext { args: self.args, instance: parent_before };
542
+ if !interpret(&given.expr, &ctx)?.truthy() {
543
+ return Err(Refusal::NothingToCorrect(format!(
544
+ "{} refused — corrects {event_name}, but {} #{} has never emitted it",
545
+ self.command_name, self.aggregate_qualified_name, self.parent_id
546
+ )));
547
+ }
548
+ }
549
+
550
+ let with_parent = WithParent { args: self.args, parent: parent_before };
551
+ let given_args: &dyn Fielded = if self.parent_in_args { &with_parent } else { self.args };
552
+ for given in self.givens {
553
+ // Handled above; the `corrects` flag field lives on the parent, not the element.
554
+ if given.corrects_event.is_some() {
555
+ continue;
556
+ }
557
+ let ctx = EvalContext { args: given_args, instance: element };
558
+ if !interpret(&given.expr, &ctx)?.truthy() {
559
+ return Err(Refusal::GivenNotMet(format!("{} refused — {}", self.command_name, given.description)));
560
+ }
561
+ }
562
+ Ok(())
563
+ }
564
+
565
+ fn apply_mutations(&mut self, record: &mut T) -> Result<(), Refusal> {
566
+ let position = self.position.expect(LOCATED);
567
+ let mut element = self.element.take().expect(LOCATED);
568
+ self.old_snapshot = if self.ensures.is_empty() { None } else { Some(element.clone()) };
569
+ (self.apply_mutations.take().expect(ONCE))(&mut element)?;
570
+ (self.get_list_mut.take().expect(ONCE))(record)[position] = element;
571
+ Ok(())
572
+ }
573
+
574
+ fn enforce_ensures(&self, record: &T) -> Result<(), Refusal> {
575
+ let Some(old) = &self.old_snapshot else { return Ok(()) };
576
+ let position = self.position.expect(LOCATED);
577
+ let parent_after = record.clone();
578
+ let with_parent = WithParent { args: self.args, parent: &parent_after };
579
+ let ensures_args: &dyn Fielded = if self.parent_in_args { &with_parent } else { self.args };
580
+ let settled = &(self.get_list)(record)[position];
581
+ enforce_ensures(settled, old, ensures_args, self.ensures, self.command_name)
582
+ }
583
+ }
584
+
585
+ #[allow(clippy::too_many_arguments)]
586
+ pub fn dispatch_entity<'a, T, E, R>(
587
+ repo: &mut R,
588
+ parent_id: &str,
589
+ get_list: impl Fn(&T) -> &Vec<E>,
590
+ get_list_mut: impl FnOnce(&mut T) -> &mut Vec<E>,
591
+ matches: impl Fn(&E) -> bool,
592
+ command_name: &'static str,
593
+ aggregate_qualified_name: &'static str,
594
+ aggregate_name: &'static str,
595
+ parent_identity_reading: &'static str,
596
+ entity_name: &'static str,
597
+ entity_identity_reading: &'static str,
598
+ wants: &str,
599
+ args: &'a dyn Fielded,
600
+ givens: &[GivenSpec],
601
+ transition: Option<TransitionCheck>,
602
+ apply_mutations: impl FnOnce(&mut E) -> Result<(), Refusal> + 'a,
603
+ ensures: &[EnsuresSpec],
604
+ invariants: &InvariantSet,
605
+ emits: &[&'static str],
606
+ payload: Json,
607
+ mutations: &mut Vec<MutationRecord>,
608
+ // As in `dispatch`: an entity command still ends in a parent aggregate save.
609
+ seed_projections: Vec<(&'static str, Option<String>)>,
610
+ ) -> Result<(T, Vec<Event>), Refusal>
611
+ where
612
+ T: Fielded + Clone + ToJson + SetProjectedField,
613
+ E: Fielded + Clone,
614
+ R: Repository<T>,
615
+ {
616
+ let mut element = ElementHalf {
617
+ parent_id,
618
+ get_list,
619
+ get_list_mut: Some(get_list_mut),
620
+ matches,
621
+ command_name,
622
+ aggregate_qualified_name,
623
+ aggregate_name,
624
+ entity_name,
625
+ entity_identity_reading,
626
+ wants,
627
+ args,
628
+ givens,
629
+ transition,
630
+ apply_mutations: Some(apply_mutations),
631
+ ensures,
632
+ // See `apply_entity_command` on `parent_in_args`.
633
+ parent_in_args: true,
634
+ position: None,
635
+ element: None,
636
+ parent_before: None,
637
+ old_snapshot: None,
638
+ };
639
+ let mut seed_projections = Some(seed_projections);
640
+ let mut hydrated: Option<T> = None;
641
+ let mut events: Vec<Event> = Vec::new();
642
+
643
+ for step in EntityStep::ORDER {
644
+ #[deny(clippy::wildcard_enum_match_arm)]
645
+ match step {
646
+ // Already run by `decode_entity_arguments`; see `entity_step_site`.
647
+ EntityStep::DecodeArguments
648
+ | EntityStep::RefuseUnknownArguments
649
+ | EntityStep::RefuseAbsentArguments
650
+ | EntityStep::NormalizeArgs
651
+ | EntityStep::RefuseRoleMismatch
652
+ | EntityStep::ResolveReferences => {}
653
+ EntityStep::HydrateParent => {
654
+ // Same `record_missing` site as `hydrate_record`.
655
+ hydrated = Some(repo.find(parent_id).ok_or_else(|| {
656
+ Refusal::NotFound(
657
+ NotFoundRecordMissingArgs {
658
+ aggregate: aggregate_name,
659
+ identity: parent_identity_reading,
660
+ offered: &format!("{parent_id:?}"),
661
+ }
662
+ .render_args(),
663
+ )
664
+ })?);
665
+ }
666
+ // The element half: the same per-step body `apply_entity_command` runs.
667
+ EntityStep::LocateElement
668
+ | EntityStep::EnforceGivens
669
+ | EntityStep::AdmissibleTransition
670
+ | EntityStep::ApplyMutations
671
+ | EntityStep::AdvanceLifecycle
672
+ | EntityStep::EnforceEnsures => {
673
+ element.run(step, hydrated.as_mut().expect(HYDRATED))?;
674
+ }
675
+ EntityStep::EnforceInvariants => {
676
+ enforce_invariants(hydrated.as_ref().expect(HYDRATED), aggregate_name, invariants)?;
677
+ }
678
+ EntityStep::Save => {
679
+ let record = hydrated.as_mut().expect(HYDRATED);
680
+ for (field, value) in seed_projections.take().expect(ONCE) {
681
+ record.set_projected_field(field, value);
682
+ }
683
+ persist(repo, parent_id, record, aggregate_qualified_name, mutations);
684
+ }
685
+ EntityStep::Emit => {
686
+ events = emitted(emits, aggregate_qualified_name, parent_id, &payload);
687
+ }
688
+ }
689
+ }
690
+
691
+ Ok((hydrated.expect(HYDRATED), events))
692
+ }
693
+
694
+ const HYDRATED: &str = "the declared order hydrates before any step that reads the record (const-asserted in this file)";
695
+ const LOCATED: &str = "the declared order locates the element before any step that reads it (const-asserted in this file)";
696
+ const ONCE: &str = "each declared step appears exactly once in its ORDER";
697
+ const NORMALIZED: &str = "the declared order normalizes arguments before resolving references (const-asserted in this file)";
698
+
699
+ /// One generated hook per argument-gate step, called in declared order by
700
+ /// `decode_aggregate_arguments` and `decode_entity_arguments`.
701
+ ///
702
+ /// `normalize_args` produces the typed `<Args>` that `resolve_references` reads.
703
+ pub struct ArgumentGates<'g, A> {
704
+ pub decode_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
705
+ pub refuse_unknown_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
706
+ pub refuse_absent_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
707
+ pub normalize_args: &'g dyn Fn(&Json) -> Result<A, Refusal>,
708
+ pub refuse_role_mismatch: &'g dyn Fn() -> Result<(), Refusal>,
709
+ pub resolve_references: &'g dyn Fn(&A) -> Result<(), Refusal>,
710
+ }
711
+
712
+ /// The argument-gate steps the aggregate and entity orders share, so one
713
+ /// body (`ArgumentGates::run`) serves both loops.
714
+ #[derive(Debug, Clone, Copy)]
715
+ enum ArgumentGate {
716
+ DecodeArguments,
717
+ RefuseUnknownArguments,
718
+ RefuseAbsentArguments,
719
+ NormalizeArgs,
720
+ RefuseRoleMismatch,
721
+ ResolveReferences,
722
+ }
723
+
724
+ impl<A> ArgumentGates<'_, A> {
725
+ fn run(&self, gate: ArgumentGate, facts: &Json, args: &mut Option<A>) -> Result<(), Refusal> {
726
+ match gate {
727
+ ArgumentGate::DecodeArguments => (self.decode_arguments)(facts),
728
+ ArgumentGate::RefuseUnknownArguments => (self.refuse_unknown_arguments)(facts),
729
+ ArgumentGate::RefuseAbsentArguments => (self.refuse_absent_arguments)(facts),
730
+ ArgumentGate::NormalizeArgs => {
731
+ *args = Some((self.normalize_args)(facts)?);
732
+ Ok(())
733
+ }
734
+ ArgumentGate::RefuseRoleMismatch => (self.refuse_role_mismatch)(),
735
+ ArgumentGate::ResolveReferences => (self.resolve_references)(args.as_ref().expect(NORMALIZED)),
736
+ }
737
+ }
738
+ }
739
+
740
+ /// `AggregateStep::ORDER`'s argument gates over one aggregate command's facts:
741
+ /// the normalized `<Args>`, or the first refusal in declared order. Every
742
+ /// other step is `dispatch`'s, entered once the router has resolved identity.
743
+ pub fn decode_aggregate_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>) -> Result<A, Refusal> {
744
+ let mut args = None;
745
+ for step in AggregateStep::ORDER {
746
+ #[deny(clippy::wildcard_enum_match_arm)]
747
+ let gate = match step {
748
+ AggregateStep::DecodeArguments => ArgumentGate::DecodeArguments,
749
+ AggregateStep::RefuseUnknownArguments => ArgumentGate::RefuseUnknownArguments,
750
+ AggregateStep::RefuseAbsentArguments => ArgumentGate::RefuseAbsentArguments,
751
+ AggregateStep::NormalizeArgs => ArgumentGate::NormalizeArgs,
752
+ AggregateStep::RefuseRoleMismatch => ArgumentGate::RefuseRoleMismatch,
753
+ AggregateStep::ResolveReferences => ArgumentGate::ResolveReferences,
754
+ AggregateStep::Hydrate
755
+ | AggregateStep::EnforceGivens
756
+ | AggregateStep::AdmissibleTransition
757
+ | AggregateStep::AssignCreationAttributes
758
+ | AggregateStep::ApplyMutations
759
+ | AggregateStep::AdvanceLifecycle
760
+ | AggregateStep::DelegateToEntity
761
+ | AggregateStep::EnforceEnsures
762
+ | AggregateStep::EnforceInvariants
763
+ | AggregateStep::Save
764
+ | AggregateStep::Emit => continue,
765
+ };
766
+ gates.run(gate, facts, &mut args)?;
767
+ }
768
+ Ok(args.expect(NORMALIZED))
769
+ }
770
+
771
+ /// `EntityStep::ORDER`'s argument gates over one entity command's facts — see
772
+ /// `decode_aggregate_arguments`. Every other step is `dispatch_entity`'s.
773
+ pub fn decode_entity_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>) -> Result<A, Refusal> {
774
+ let mut args = None;
775
+ for step in EntityStep::ORDER {
776
+ #[deny(clippy::wildcard_enum_match_arm)]
777
+ let gate = match step {
778
+ EntityStep::DecodeArguments => ArgumentGate::DecodeArguments,
779
+ EntityStep::RefuseUnknownArguments => ArgumentGate::RefuseUnknownArguments,
780
+ EntityStep::RefuseAbsentArguments => ArgumentGate::RefuseAbsentArguments,
781
+ EntityStep::NormalizeArgs => ArgumentGate::NormalizeArgs,
782
+ EntityStep::RefuseRoleMismatch => ArgumentGate::RefuseRoleMismatch,
783
+ EntityStep::ResolveReferences => ArgumentGate::ResolveReferences,
784
+ EntityStep::HydrateParent
785
+ | EntityStep::LocateElement
786
+ | EntityStep::EnforceGivens
787
+ | EntityStep::AdmissibleTransition
788
+ | EntityStep::ApplyMutations
789
+ | EntityStep::AdvanceLifecycle
790
+ | EntityStep::EnforceEnsures
791
+ | EntityStep::EnforceInvariants
792
+ | EntityStep::Save
793
+ | EntityStep::Emit => continue,
794
+ };
795
+ gates.run(gate, facts, &mut args)?;
796
+ }
797
+ Ok(args.expect(NORMALIZED))
798
+ }
799
+
800
+ /// A step's index in `AggregateStep::ORDER`, usable in a const context.
801
+ const fn aggregate_position(step: AggregateStep) -> usize {
802
+ let mut index = 0;
803
+ while index < AggregateStep::ORDER.len() {
804
+ if AggregateStep::ORDER[index] as usize == step as usize {
805
+ return index;
806
+ }
807
+ index += 1;
808
+ }
809
+ panic!("step missing from AggregateStep::ORDER")
810
+ }
811
+
812
+ /// A step's index in `EntityStep::ORDER`, usable in a const context.
813
+ const fn entity_position(step: EntityStep) -> usize {
814
+ let mut index = 0;
815
+ while index < EntityStep::ORDER.len() {
816
+ if EntityStep::ORDER[index] as usize == step as usize {
817
+ return index;
818
+ }
819
+ index += 1;
820
+ }
821
+ panic!("step missing from EntityStep::ORDER")
822
+ }
823
+
824
+ // Pairs where an arm consumes what an earlier arm produced; reordering them in
825
+ // vocabulary.bluebook fails the build instead of panicking at dispatch time.
826
+ const _: () = {
827
+ use AggregateStep as A;
828
+ let hydrate = aggregate_position(A::Hydrate);
829
+ assert!(hydrate < aggregate_position(A::EnforceGivens));
830
+ assert!(hydrate < aggregate_position(A::AdmissibleTransition));
831
+ assert!(hydrate < aggregate_position(A::ApplyMutations));
832
+ assert!(hydrate < aggregate_position(A::EnforceEnsures));
833
+ assert!(hydrate < aggregate_position(A::EnforceInvariants));
834
+ assert!(hydrate < aggregate_position(A::Save));
835
+ assert!(hydrate < aggregate_position(A::Emit));
836
+ // `old_snapshot` is taken in ApplyMutations; an EnforceEnsures before it
837
+ // would see none and skip every ensures.
838
+ assert!(aggregate_position(A::ApplyMutations) < aggregate_position(A::EnforceEnsures));
839
+ // The router resolves identity only after `decode_aggregate_arguments`
840
+ // returns, so every argument gate must be declared ahead of `hydrate`;
841
+ // `resolve_references` reads the `<Args>` `normalize_args` produced.
842
+ assert!(aggregate_position(A::DecodeArguments) < hydrate);
843
+ assert!(aggregate_position(A::RefuseUnknownArguments) < hydrate);
844
+ assert!(aggregate_position(A::RefuseAbsentArguments) < hydrate);
845
+ assert!(aggregate_position(A::NormalizeArgs) < hydrate);
846
+ assert!(aggregate_position(A::RefuseRoleMismatch) < hydrate);
847
+ assert!(aggregate_position(A::ResolveReferences) < hydrate);
848
+ assert!(aggregate_position(A::NormalizeArgs) < aggregate_position(A::ResolveReferences));
849
+
850
+ use EntityStep as E;
851
+ let hydrate_parent = entity_position(E::HydrateParent);
852
+ assert!(hydrate_parent < entity_position(E::LocateElement));
853
+ assert!(hydrate_parent < entity_position(E::EnforceInvariants));
854
+ assert!(hydrate_parent < entity_position(E::Save));
855
+ let locate = entity_position(E::LocateElement);
856
+ assert!(locate < entity_position(E::EnforceGivens));
857
+ assert!(locate < entity_position(E::AdmissibleTransition));
858
+ assert!(locate < entity_position(E::ApplyMutations));
859
+ // ApplyMutations moves the element back into the parent's list.
860
+ assert!(entity_position(E::EnforceGivens) < entity_position(E::ApplyMutations));
861
+ assert!(entity_position(E::AdmissibleTransition) < entity_position(E::ApplyMutations));
862
+ assert!(entity_position(E::ApplyMutations) < entity_position(E::EnforceEnsures));
863
+ // See the aggregate half: identity (parent, then element) is resolved
864
+ // only after `decode_entity_arguments` returns.
865
+ assert!(entity_position(E::DecodeArguments) < hydrate_parent);
866
+ assert!(entity_position(E::RefuseUnknownArguments) < hydrate_parent);
867
+ assert!(entity_position(E::RefuseAbsentArguments) < hydrate_parent);
868
+ assert!(entity_position(E::NormalizeArgs) < hydrate_parent);
869
+ assert!(entity_position(E::RefuseRoleMismatch) < hydrate_parent);
870
+ assert!(entity_position(E::ResolveReferences) < hydrate_parent);
871
+ assert!(entity_position(E::NormalizeArgs) < entity_position(E::ResolveReferences));
872
+ };
873
+
874
+ /// Where a declared dispatch step is actually performed in the Rust port.
875
+ /// The dispatch loops above follow this mapping.
876
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
877
+ pub enum StepSite {
878
+ /// An arm of the kernel's own dispatch loop does the work.
879
+ Kernel,
880
+ /// `decode_aggregate_arguments`/`decode_entity_arguments` call this
881
+ /// step's generated hook (`ArgumentGates`) in its declared position,
882
+ /// before the router resolves identity. `dispatch`'s own arm is a no-op.
883
+ ArgumentGate,
884
+ /// Folded into a generated closure the kernel already calls in another
885
+ /// step's arm (named here). The loop's own arm is a no-op.
886
+ FoldedInto(AggregateStepOrEntityStep),
887
+ }
888
+
889
+ /// The step a `StepSite::FoldedInto` step rides inside.
890
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
891
+ pub enum AggregateStepOrEntityStep {
892
+ Aggregate(AggregateStep),
893
+ Entity(EntityStep),
894
+ }
895
+
896
+ /// See `StepSite`. Exhaustive, no wildcard — a step the vocabulary gains
897
+ /// must be placed here before the crate compiles.
898
+ #[deny(clippy::wildcard_enum_match_arm)]
899
+ pub const fn aggregate_step_site(step: AggregateStep) -> StepSite {
900
+ use AggregateStep as A;
901
+ match step {
902
+ // `<Args>::decode_arguments` (the facts are an object).
903
+ A::DecodeArguments => StepSite::ArgumentGate,
904
+ // `<Args>::refuse_unknown_arguments`/`<Args>::refuse_absent_arguments`.
905
+ A::RefuseUnknownArguments | A::RefuseAbsentArguments => StepSite::ArgumentGate,
906
+ // `<Args>::from_json`'s coercion + the router's invariant checks.
907
+ A::NormalizeArgs => StepSite::ArgumentGate,
908
+ // `kernel::check_role_via`.
909
+ A::RefuseRoleMismatch => StepSite::ArgumentGate,
910
+ // `kernel::check_reference`; the router computes the tenant boundary
911
+ // after it, but the kernel's `Save` arm applies it.
912
+ A::ResolveReferences => StepSite::ArgumentGate,
913
+ A::Hydrate => StepSite::Kernel,
914
+ A::EnforceGivens => StepSite::Kernel,
915
+ A::AdmissibleTransition => StepSite::Kernel,
916
+ // `Hydrate::Create.build`.
917
+ A::AssignCreationAttributes => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::Hydrate)),
918
+ A::ApplyMutations => StepSite::Kernel,
919
+ // The generated `apply_mutations` closure's last line.
920
+ A::AdvanceLifecycle => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations)),
921
+ // The generated `apply_mutations` closure → `apply_entity_command`.
922
+ A::DelegateToEntity => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations)),
923
+ A::EnforceEnsures => StepSite::Kernel,
924
+ A::EnforceInvariants => StepSite::Kernel,
925
+ A::Save => StepSite::Kernel,
926
+ A::Emit => StepSite::Kernel,
927
+ }
928
+ }
929
+
930
+ /// See `StepSite`. Exhaustive, no wildcard.
931
+ #[deny(clippy::wildcard_enum_match_arm)]
932
+ pub const fn entity_step_site(step: EntityStep) -> StepSite {
933
+ use EntityStep as E;
934
+ match step {
935
+ // The `<EntityArgs>` functions and checks named in
936
+ // `aggregate_step_site`, through `decode_entity_arguments`.
937
+ E::DecodeArguments => StepSite::ArgumentGate,
938
+ E::RefuseUnknownArguments | E::RefuseAbsentArguments => StepSite::ArgumentGate,
939
+ E::NormalizeArgs => StepSite::ArgumentGate,
940
+ E::RefuseRoleMismatch => StepSite::ArgumentGate,
941
+ E::ResolveReferences => StepSite::ArgumentGate,
942
+ E::HydrateParent => StepSite::Kernel,
943
+ E::LocateElement => StepSite::Kernel,
944
+ E::EnforceGivens => StepSite::Kernel,
945
+ E::AdmissibleTransition => StepSite::Kernel,
946
+ E::ApplyMutations => StepSite::Kernel,
947
+ E::AdvanceLifecycle => StepSite::FoldedInto(AggregateStepOrEntityStep::Entity(E::ApplyMutations)),
948
+ E::EnforceEnsures => StepSite::Kernel,
949
+ E::EnforceInvariants => StepSite::Kernel,
950
+ E::Save => StepSite::Kernel,
951
+ E::Emit => StepSite::Kernel,
952
+ }
953
+ }
954
+
955
+ // No `match step` above has a wildcard arm, so a step added to the vocabulary fails to
956
+ // compile (E0004) until placed. The tests pin the current kernel/argument-gate mapping.
957
+ #[cfg(test)]
958
+ mod step_order_tests {
959
+ use super::*;
960
+
961
+ #[test]
962
+ fn decode_arguments_leads_both_orders() {
963
+ assert_eq!(AggregateStep::ORDER[0], AggregateStep::DecodeArguments);
964
+ assert_eq!(EntityStep::ORDER[0], EntityStep::DecodeArguments);
965
+ }
966
+
967
+ #[test]
968
+ fn const_positions_agree_with_the_generated_ones() {
969
+ for step in AggregateStep::ORDER {
970
+ assert_eq!(aggregate_position(step), step.position());
971
+ }
972
+ for step in EntityStep::ORDER {
973
+ assert_eq!(entity_position(step), step.position());
974
+ }
975
+ }
976
+
977
+ #[test]
978
+ fn the_kernel_performs_exactly_these_aggregate_steps() {
979
+ let kernel: Vec<&str> =
980
+ AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::Kernel).map(|s| s.step()).collect();
981
+ assert_eq!(
982
+ kernel,
983
+ ["hydrate", "enforce_givens", "admissible_transition", "apply_mutations", "enforce_ensures", "enforce_invariants", "save", "emit"]
984
+ );
985
+ }
986
+
987
+ #[test]
988
+ fn the_kernel_performs_exactly_these_entity_steps() {
989
+ let kernel: Vec<&str> =
990
+ EntityStep::ORDER.iter().filter(|s| entity_step_site(**s) == StepSite::Kernel).map(|s| s.step()).collect();
991
+ assert_eq!(
992
+ kernel,
993
+ [
994
+ "hydrate_parent",
995
+ "locate_element",
996
+ "enforce_givens",
997
+ "admissible_transition",
998
+ "apply_mutations",
999
+ "enforce_ensures",
1000
+ "enforce_invariants",
1001
+ "save",
1002
+ "emit"
1003
+ ]
1004
+ );
1005
+ }
1006
+
1007
+ #[test]
1008
+ fn argument_gates_all_precede_the_first_kernel_step() {
1009
+ // The router resolves identity between the two loops, so every
1010
+ // argument gate sits ahead of the first kernel step.
1011
+ let first_kernel = AggregateStep::ORDER.iter().position(|s| aggregate_step_site(*s) == StepSite::Kernel).unwrap();
1012
+ for step in AggregateStep::ORDER {
1013
+ if aggregate_step_site(step) == StepSite::ArgumentGate {
1014
+ assert!(step.position() < first_kernel, "{} is an argument gate but is declared after hydrate", step.step());
1015
+ }
1016
+ }
1017
+ let first_kernel = EntityStep::ORDER.iter().position(|s| entity_step_site(*s) == StepSite::Kernel).unwrap();
1018
+ for step in EntityStep::ORDER {
1019
+ if entity_step_site(step) == StepSite::ArgumentGate {
1020
+ assert!(step.position() < first_kernel, "{} is an argument gate but is declared after hydrate_parent", step.step());
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ use std::cell::RefCell;
1026
+
1027
+ /// Hooks that record which step ran, refusing at the steps named in `refuse`.
1028
+ fn recorded<R>(refuse: &[&'static str], body: impl FnOnce(&ArgumentGates<'_, i64>) -> R) -> (R, Vec<&'static str>) {
1029
+ let ran = RefCell::new(Vec::new());
1030
+ let hit = |step: &'static str| -> Result<(), Refusal> {
1031
+ ran.borrow_mut().push(step);
1032
+ if refuse.contains(&step) {
1033
+ Err(Refusal::TypeMismatch(step.to_string()))
1034
+ } else {
1035
+ Ok(())
1036
+ }
1037
+ };
1038
+ let decode = |_: &Json| hit("decode_arguments");
1039
+ let unknown = |_: &Json| hit("refuse_unknown_arguments");
1040
+ let absent = |_: &Json| hit("refuse_absent_arguments");
1041
+ let normalize = |_: &Json| hit("normalize_args").map(|()| 7);
1042
+ let role = || hit("refuse_role_mismatch");
1043
+ let references = |args: &i64| {
1044
+ assert_eq!(*args, 7, "resolve_references reads what normalize_args produced");
1045
+ hit("resolve_references")
1046
+ };
1047
+ let gates = ArgumentGates {
1048
+ decode_arguments: &decode,
1049
+ refuse_unknown_arguments: &unknown,
1050
+ refuse_absent_arguments: &absent,
1051
+ normalize_args: &normalize,
1052
+ refuse_role_mismatch: &role,
1053
+ resolve_references: &references,
1054
+ };
1055
+ let result = body(&gates);
1056
+ (result, ran.into_inner())
1057
+ }
1058
+
1059
+ #[test]
1060
+ fn the_argument_gate_loops_call_every_hook_in_declared_order() {
1061
+ let facts = Json::Object(Vec::new());
1062
+ let (result, ran) = recorded(&[], |gates| decode_aggregate_arguments(&facts, gates));
1063
+ assert_eq!(result.unwrap(), 7);
1064
+ let declared: Vec<&str> =
1065
+ AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
1066
+ assert_eq!(ran, declared);
1067
+
1068
+ let (result, ran) = recorded(&[], |gates| decode_entity_arguments(&facts, gates));
1069
+ assert_eq!(result.unwrap(), 7);
1070
+ let declared: Vec<&str> =
1071
+ EntityStep::ORDER.iter().filter(|s| entity_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
1072
+ assert_eq!(ran, declared);
1073
+ }
1074
+
1075
+ #[test]
1076
+ fn the_earliest_declared_violated_gate_wins_and_later_hooks_never_run() {
1077
+ let facts = Json::Object(Vec::new());
1078
+ let gates: Vec<&'static str> =
1079
+ AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
1080
+ for (i, earlier) in gates.iter().enumerate() {
1081
+ for later in &gates[i + 1..] {
1082
+ let (result, ran) = recorded(&[earlier, later], |g| decode_aggregate_arguments(&facts, g));
1083
+ assert_eq!(result.unwrap_err().to_string(), Refusal::TypeMismatch(earlier.to_string()).to_string());
1084
+ assert_eq!(ran.last(), Some(earlier));
1085
+ let (result, _) = recorded(&[earlier, later], |g| decode_entity_arguments(&facts, g));
1086
+ assert_eq!(result.unwrap_err().to_string(), Refusal::TypeMismatch(earlier.to_string()).to_string());
1087
+ }
1088
+ }
1089
+ }
1090
+ }