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,877 @@
1
+ require_relative "skip_reason"
2
+
3
+ module RustProjection
4
+ module Projector
5
+ module_function
6
+
7
+ # The aggregate's own invariant set, passed to every dispatch of its commands.
8
+ # Entity lists are descended into only when the entity declares invariants (C6.2).
9
+ def invariants_fn_name(aggregate) = "#{rust_ident_field(aggregate[:name]).downcase}_invariants"
10
+
11
+ def emit_invariants_fn(aggregate)
12
+ aggregate_vec = invariant_specs_vec(aggregate[:invariants], 8)
13
+ entities_vec = entity_invariants_vec(aggregate, 8)
14
+ # Skip the `Expr` import when neither vec built an `Expr::` literal; it would be unused.
15
+ needs_expr = [aggregate_vec, entities_vec].any? { |vec| vec.include?("Expr::") }
16
+ [
17
+ "fn #{invariants_fn_name(aggregate)}() -> crate::kernel::InvariantSet {",
18
+ (needs_expr ? " use crate::kernel::Expr;" : nil),
19
+ " crate::kernel::InvariantSet {",
20
+ " aggregate: #{aggregate_vec},",
21
+ " entities: #{entities_vec},",
22
+ " }",
23
+ "}"
24
+ ].compact.join("\n")
25
+ end
26
+
27
+ def invariant_specs_vec(rules, indent)
28
+ return "vec![]" if rules.empty?
29
+
30
+ pad = " " * (indent + 4)
31
+ rows = rules.map do |rule|
32
+ "#{pad}crate::kernel::InvariantSpec { description: #{rust_string_literal(rule[:description])}, " \
33
+ "expr: #{ExprEmitter.emit_ast(rule[:ast])} },"
34
+ end
35
+ "vec![\n#{rows.join("\n")}\n#{' ' * indent}]"
36
+ end
37
+
38
+ def entity_invariants_vec(owner, indent)
39
+ pieces = owner[:entities].filter_map do |entity|
40
+ next if entity[:invariants].empty?
41
+
42
+ list = owner[:attributes].find { |a| a[:list] && a[:type].to_s == entity[:name].to_s }
43
+ next unless list
44
+
45
+ [entity, list[:name].to_s]
46
+ end
47
+ return "vec![]" if pieces.empty?
48
+
49
+ pad = " " * (indent + 4)
50
+ rows = pieces.map do |entity, list_field|
51
+ "#{pad}crate::kernel::EntityInvariants { name: #{rust_string_literal(entity[:name])}, " \
52
+ "list_field: #{rust_string_literal(list_field)}, " \
53
+ "specs: #{invariant_specs_vec(entity[:invariants], indent + 4)}, " \
54
+ "nested: #{entity_invariants_vec(entity, indent + 4)} },"
55
+ end
56
+ "vec![\n#{rows.join("\n")}\n#{' ' * indent}]"
57
+ end
58
+
59
+ # `None` covers both "no transition" and an unconstrained one (`from: nil`); an empty
60
+ # `from_states` slice would refuse every state instead of admitting every state.
61
+ def transition_check_arg(transition)
62
+ return "None" if transition.nil? || transition[:unconstrained]
63
+
64
+ "Some(crate::kernel::TransitionCheck { field: #{transition[:field].inspect}, " \
65
+ "from_states: &[#{transition[:from_states].map(&:inspect).join(', ')}] })"
66
+ end
67
+
68
+ # Cross-aggregate dereferences, already resolved by the router into owned data,
69
+ # that every generated dispatch function takes.
70
+ DEREF_PARAMS = ["owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>",
71
+ "command_deref: Vec<(&'static str, crate::kernel::DerefNode)>"].freeze
72
+
73
+ # Deferred tenant-boundary result, aggregate commands only; `dispatch()` checks it where
74
+ # Ruby's `step_save` does.
75
+ TENANT_BOUNDARY_PARAM = "tenant_boundary_check: Result<(), crate::kernel::Refusal>".freeze
76
+
77
+ # Binds `WithReferences`. Precedence, highest first: `command_deref`, typed `args`,
78
+ # `owner_deref`, matching the merge chain in `Admissibility#enforce_givens`.
79
+ def with_references_binding
80
+ "let with_references = crate::kernel::WithReferences { command_deref: &command_deref, args: &args, owner_deref: &owner_deref };"
81
+ end
82
+
83
+ # Seeds `projects` fields from the same `with_references` (ADR 0025); the field table is
84
+ # always emitted, so an empty spec list is a no-op.
85
+ def seed_projections_binding(aggregate)
86
+ table = screaming_snake(aggregate[:name])
87
+ "let seed_projections = crate::kernel::seeded_projections(&with_references, #{table}_PROJECTED_FIELDS);"
88
+ end
89
+
90
+ def command_skip_reason(command, aggregate, value_objects_by_name, creating_possible: true)
91
+ # `remove` is generated only for entity-typed lists matched by identity; value-object
92
+ # lists stay unsupported.
93
+ unsupported_ops = command[:mutations].reject { |m| %w[append set increment decrement multiply clamp remove delegate corrects].include?(m[:op].to_s) }.map { |m| m[:op] }.uniq
94
+ return skip("mutation_op", "sets op(s) #{unsupported_ops.join(', ')} not generated yet (only append/set/increment/decrement/multiply/clamp/remove/delegate/corrects are)") if unsupported_ops.any?
95
+
96
+ corrects = corrects_of(command)
97
+ # Skip only when `derive_reverses_mutations!` left this command untouched: a non-invertible
98
+ # sibling op has no reversal design yet (ADR 0041).
99
+ return skip("corrects_reverses", "corrects #{corrects[:target]}, reverses: true — the derived append/remove-reversal shape is a real, separate gap Ruby's own authors haven't finished designing (AggregateBuilder#seal_correction_targets's own comment) — not generated yet") \
100
+ if corrects && corrects_reverses?(corrects) && command[:mutations].none? { |m| m[:op].to_s != "corrects" }
101
+
102
+ delegate_problem = delegate_skip_reason(command, aggregate, value_objects_by_name)
103
+ return skip("delegate", delegate_problem) if delegate_problem
104
+
105
+ append_problems = append_field_problems(command, aggregate, value_objects_by_name)
106
+ return skip("append_field", "sets append field(s): #{append_problems.join('; ')}") if append_problems.any?
107
+ state_problems = state_source_problems(command, aggregate, value_objects_by_name)
108
+ return skip("state_source", "sets state source(s): #{state_problems.join('; ')}") if state_problems.any?
109
+ remove_problems = remove_field_problems(command, aggregate, value_objects_by_name)
110
+ return skip("remove_field", "sets remove field(s): #{remove_problems.join('; ')}") if remove_problems.any?
111
+
112
+ lifecycle_field = aggregate[:lifecycle] && aggregate[:lifecycle][:field].to_s
113
+ target_type_for = ->(target) { target.to_s == lifecycle_field ? "String" : aggregate[:attributes].find { |a| a[:name].to_s == target.to_s }&.dig(:type) }
114
+ # The lifecycle field is never list-typed.
115
+ target_list_for = ->(target) { target.to_s != lifecycle_field && !!aggregate[:attributes].find { |a| a[:name].to_s == target.to_s }&.dig(:list) }
116
+
117
+ # A literal `:set` source is a scalar or a raw Hash; a Hash must supply every field of the
118
+ # target value object.
119
+ literal_set_targets = command[:mutations].select do |m|
120
+ next false unless m[:op].to_s == "set" && m[:source][:kind] == "literal"
121
+
122
+ !literal_set_bridgeable?(m[:source][:value], target_type_for.call(m[:target]), value_objects_by_name)
123
+ end.map { |m| m[:target] }
124
+ return skip("set_literal", "sets to: a literal that doesn't bridge to the target's type (#{literal_set_targets.join(', ')}) — not generated yet") if literal_set_targets.any?
125
+
126
+ # Ruby coerces a `:set` value into the target's declared type; the generated clone only
127
+ # compiles when source and target agree, and cardinality (list vs scalar) must match.
128
+ mismatched_sets = command[:mutations].select do |m|
129
+ next false unless m[:op].to_s == "set" && m[:source][:kind] == "argument"
130
+
131
+ target_type = target_type_for.call(m[:target])
132
+ source_attr = command[:attributes].find { |a| a[:name].to_s == m[:source][:name] }
133
+ source_type = source_attr&.dig(:type)
134
+ next false unless target_type && source_type
135
+
136
+ target_list_for.call(m[:target]) != !!source_attr[:list] || !bridgeable_value_types?(source_type, target_type, value_objects_by_name)
137
+ end.map { |m| m[:target] }
138
+ return skip("set_argument_bridge", "sets :#{mismatched_sets.join(', ')} sources an argument no single-field rewrap can bridge to the target's type — not generated yet") if mismatched_sets.any?
139
+
140
+ # increment/decrement/multiply share one check: the target names an Integer field and the
141
+ # amount resolves to an integer expression.
142
+ arithmetic_targets = command[:mutations].select { |m| %w[increment decrement multiply].include?(m[:op].to_s) }
143
+ unsupported_arithmetic = arithmetic_targets.reject do |m|
144
+ target = arithmetic_target_field(m, aggregate, value_objects_by_name)
145
+ target && arithmetic_amount_expr(m[:source], command, value_objects_by_name, target[1])
146
+ end.map { |m| m[:target] }
147
+ return skip("arithmetic", "sets :#{unsupported_arithmetic.join(', ')} increment/decrement/multiply amount or target field isn't bridgeable — not generated yet") if unsupported_arithmetic.any?
148
+
149
+ # `clamp` bounds the current value with a literal [min, max] Integer pair; the target
150
+ # field rule is the same as arithmetic.
151
+ clamp_targets = command[:mutations].select { |m| m[:op].to_s == "clamp" }
152
+ unsupported_clamp = clamp_targets.reject do |m|
153
+ arithmetic_target_field(m, aggregate, value_objects_by_name) && clamp_bounds_ints(m[:source])
154
+ end.map { |m| m[:target] }
155
+ return skip("clamp", "sets :#{unsupported_clamp.join(', ')} clamp target field or bounds isn't bridgeable — not generated yet") if unsupported_clamp.any?
156
+
157
+ optional_problems = optional_source_mismatches(command, aggregate, value_objects_by_name, creating_possible: creating_possible)
158
+ return skip("optional_source", "optional argument feeds a non-optional target: #{optional_problems.join('; ')} — not generated yet") if optional_problems.any?
159
+
160
+ nil
161
+ end
162
+
163
+ # An optional argument feeding a field that is not `Option<T>` cannot be generated: the Rust
164
+ # struct shape is fixed at compile time, while Ruby stores `nil`. Skipped, not guessed.
165
+ def optional_source_mismatches(command, aggregate, value_objects_by_name, creating_possible: true)
166
+ lifecycle_field = aggregate[:lifecycle] && aggregate[:lifecycle][:field].to_s
167
+ problems = []
168
+
169
+ # An entity command never creates, so its optional identity argument is not an identity
170
+ # source (`creating_possible: false`).
171
+
172
+ # `identified_by` is never Option-wrapped, so an optional argument feeding it is caught
173
+ # here; the `sets` checks below never see it.
174
+ if creating_possible && creates_owner?(aggregate, command, value_objects_by_name)
175
+ aggregate[:identified_by].each do |path|
176
+ head, = path.split(".")
177
+ source_attr = command[:attributes].find { |a| a[:name].to_s == head }
178
+ next unless source_attr && source_attr[:optional]
179
+
180
+ problems << "identified_by :#{path} sources optional argument #{source_attr[:name]}"
181
+ end
182
+ end
183
+
184
+ command[:mutations].each do |m|
185
+ case m[:op].to_s
186
+ when "set"
187
+ next unless m[:source][:kind] == "argument"
188
+
189
+ source_attr = command[:attributes].find { |a| a[:name].to_s == m[:source][:name].to_s }
190
+ next unless source_attr && source_attr[:optional]
191
+
192
+ if m[:target].to_s == lifecycle_field
193
+ problems << "sets :#{m[:target]} sources optional argument #{source_attr[:name]} into the lifecycle field"
194
+ next
195
+ end
196
+
197
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s }
198
+ # A list target is exempt: both `Vec<T>` and `Option<Vec<T>>` record fields are handled
199
+ # by the `:set` branch.
200
+ next if target_attr && target_attr[:list]
201
+
202
+ # Aggregate-command records hold `Option<T>` fields, so an optional argument assigns
203
+ # straight across; only entity element fields can mismatch.
204
+ next if creating_possible
205
+
206
+ problems << "sets :#{m[:target]} sources optional argument #{source_attr[:name]}" unless target_attr && target_attr[:optional]
207
+ when "append"
208
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s }
209
+ element = target_attr && append_element(aggregate, target_attr[:type], value_objects_by_name)
210
+ next unless element
211
+
212
+ m[:fields].each do |field_name, source|
213
+ parsed = append_field_source(source)
214
+ next unless parsed.is_a?(Symbol) # a literal, not a caller-omittable argument
215
+
216
+ source_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
217
+ next unless source_attr && source_attr[:optional]
218
+
219
+ field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
220
+ problems << "sets append #{m[:target]}.#{field_name} sources optional argument #{source_attr[:name]}" unless field_attr && field_attr[:optional]
221
+ end
222
+ end
223
+ end
224
+
225
+ problems
226
+ end
227
+
228
+ # Per-attribute admits-constraint and nested-value-object invariant checks, against an
229
+ # already-bound value expression.
230
+ # Usage-level `pattern:` is deliberately not checked: Ruby never enforces it on a command
231
+ # argument (ADR 0010). List attributes skip `admits:` too, since `admit_declared_set` is
232
+ # only reached on the scalar branch.
233
+ def argument_check_lines(attr, value_expr, aggregates_by_name, value_objects_by_name)
234
+ lines = []
235
+
236
+ unless attr[:list]
237
+ # Raw field expression: `emit_admits_check` does its own optional handling.
238
+ constraint = emit_admits_check(value_expr, attr, aggregates_by_name, value_objects_by_name)
239
+ lines << " #{constraint}" if constraint
240
+ end
241
+
242
+ if value_objects_by_name.key?(attr[:type]) && !value_objects_by_name[attr[:type]][:closed_set]
243
+ lines << if attr[:optional]
244
+ attr[:list] ? " if let Some(items) = &#{value_expr} { for item in items { item.check_invariants()?; } }" : " if let Some(v) = &#{value_expr} { v.check_invariants()?; }"
245
+ else
246
+ attr[:list] ? " for item in &#{value_expr} { item.check_invariants()?; }" : " #{value_expr}.check_invariants()?;"
247
+ end
248
+ end
249
+
250
+ lines
251
+ end
252
+
253
+ # Post-construction copy against the built Args struct, run at the router and again in the
254
+ # dispatch fn; redundant with the interleaved checks in `from_json`, but kept.
255
+ def invariant_checks_for(command, aggregates_by_name, value_objects_by_name)
256
+ command[:attributes].flat_map do |attr|
257
+ field = rust_ident_field(attr[:name])
258
+ argument_check_lines(attr, "args.#{field}", aggregates_by_name, value_objects_by_name)
259
+ end
260
+ end
261
+
262
+ # One emitter for every command shape `kernel::dispatch` can run, creating or acting.
263
+ def emit_command(command, aggregate, domain_name, value_objects_by_name, aggregates_by_name)
264
+ record = rust_ident(aggregate[:name])
265
+ cmd = rust_ident(command[:name])
266
+ creates = creates_owner?(aggregate, command, value_objects_by_name)
267
+ identity = identity_components(aggregate, command)
268
+ identity_extra_params = identity.filter_map { |c| c[:param] }
269
+
270
+ # `aggregate_name`/`identity_reading` feed the refusal wording of both `creating_duplicate`
271
+ # and `record_missing`, whatever the command shape.
272
+ aggregate_name = aggregate[:name].to_s
273
+ identity_reading = aggregate[:identified_by].join(", ")
274
+
275
+ # Reuses `struct_field` only; the Args derive lacks `PartialEq`, unlike `plain_struct`.
276
+ args_struct = ["pub struct #{cmd}Args {"]
277
+ command[:attributes].each do |attr|
278
+ type = rust_type(attr[:type], list: attr[:list])
279
+ # An optional argument is `Option<T>`; `command_skip_reason` guards the one shape that
280
+ # cannot represent it.
281
+ type = "Option<#{type}>" if attr[:optional]
282
+ args_struct << " #{Exemplar.render('struct_field', 'TmplFieldType' => type, 'tmpl_field' => rust_ident_field(attr[:name]))}"
283
+ end
284
+ args_struct << "}"
285
+
286
+ invariant_checks = invariant_checks_for(command, aggregates_by_name, value_objects_by_name)
287
+
288
+ given_specs = corrects_given_specs(command) + command[:givens].map do |given|
289
+ " crate::kernel::GivenSpec { description: #{rust_string_literal(given[:description])}, expr: #{ExprEmitter.emit_ast(given[:ast])}, corrects_event: None },"
290
+ end
291
+
292
+ ensures_specs = command[:ensures].map do |rule|
293
+ " crate::kernel::EnsuresSpec { description: #{rust_string_literal(rule[:description])}, expr: #{ExprEmitter.emit_ast(rule[:ast])} },"
294
+ end
295
+
296
+ transition = lifecycle_transition_for(command, aggregate)
297
+ transition_arg =
298
+ transition_check_arg(transition)
299
+
300
+ mutation_lines = command[:mutations].reject { |m| m[:op].to_s == "corrects" }
301
+ .map { |m| emit_mutation_line(m, aggregate, command, value_objects_by_name) }
302
+ mutation_lines.unshift(pre_state_line) if reads_pre_state?(command[:mutations])
303
+ mutation_lines.concat(corrects_flag_mutation_lines(command, aggregate))
304
+ # Advance the lifecycle unconditionally once a transition applies, even when no explicit
305
+ # set targets the lifecycle field.
306
+ mutation_lines << " record.#{rust_ident_field(transition[:field])} = #{transition[:to_state].inspect}.to_string();" if transition && transition[:to_state]
307
+ mutation_lines = [" let _ = record;"] if mutation_lines.empty? # nothing to apply — silence the unused-param warning
308
+ delegation = delegation_of(command, aggregate, value_objects_by_name, domain_name)
309
+ if delegation
310
+ raise "#{command[:name]}: a creating command cannot delegate — nothing exists to delegate to" if creates
311
+
312
+ # The element extraction runs first inside the closure, after a successful hydrate.
313
+ mutation_lines = [delegation[:element], delegation[:apply]]
314
+ end
315
+ prelude = delegation ? delegation[:prelude] : ""
316
+ payload = delegation ? "delegate_facts.clone()," : "args.to_json(),"
317
+ emits_out = delegation ? delegation[:emits] : command[:emits]
318
+
319
+ if creates
320
+ # A `projects` field is never a command argument, so it falls to `None`;
321
+ # `seed_projections` fills it before save.
322
+ record_fields = (aggregate[:attributes] + Projector.projected_field_pseudo_attributes(aggregate)).map do |attr|
323
+ matched = command[:attributes].find { |a| a[:name] == attr[:name] }
324
+ field = rust_ident_field(attr[:name])
325
+ if matched && matched[:optional]
326
+ # An optional argument is already `Option<T>`; a scalar record field takes it as is.
327
+ # A list field does too only when `list_attr_creation_optional?` wrapped it, else it is
328
+ # unwrapped with the `[]` fallback `default_for` gives an unmatched list attribute.
329
+ if attr[:list] && !list_attr_creation_optional?(aggregate, attr[:name], value_objects_by_name)
330
+ " #{field}: args.#{field}.clone().unwrap_or_default(),"
331
+ elsif attr[:list] || matched[:type] == attr[:type]
332
+ " #{field}: args.#{field}.clone(),"
333
+ else
334
+ # Same-named cross-aggregate argument of a different type (see `bridging.rb`):
335
+ # bridge it rather than clone.
336
+ " #{field}: #{optional_value_rhs("args.#{field}", matched[:type], attr[:type], value_objects_by_name)},"
337
+ end
338
+ elsif matched
339
+ if attr[:list] || matched[:type] == attr[:type]
340
+ attr[:list] ? " #{field}: args.#{field}.clone()," : " #{field}: Some(args.#{field}.clone()),"
341
+ else
342
+ " #{field}: Some(#{value_rhs("args.#{field}", matched[:type], attr[:type], value_objects_by_name)}),"
343
+ end
344
+ elsif attr[:list]
345
+ " #{field}: vec![],"
346
+ else
347
+ default_rhs = creation_default_rhs(attr, value_objects_by_name)
348
+ default_rhs ? " #{field}: Some(#{default_rhs})," : " #{field}: None,"
349
+ end
350
+ end
351
+ record_fields << " #{rust_ident_field(aggregate[:lifecycle][:field])}: #{aggregate[:lifecycle][:default].inspect}.to_string()," if aggregate[:lifecycle]
352
+ correctable_event_names(aggregate).each { |name| record_fields << " #{corrects_flag_field(name)}: false," }
353
+
354
+ # Mirrors `CommandInterpreter#step_hydrate`'s complete-state and state-independent branch.
355
+ state_independent = state_independent_creation?(aggregate, command, value_objects_by_name)
356
+ create_block = <<~RUST.rstrip
357
+ crate::kernel::Hydrate::Create {
358
+ id: __hydrate_id,
359
+ build: Box::new(|| #{record} {
360
+ #{record_fields.join("\n")}
361
+ }),
362
+ state_independent: #{state_independent},
363
+ }
364
+ RUST
365
+
366
+ # Ruby checks the route before choosing create-vs-find. With `complete_state?` a route is
367
+ # validated against the derived identity (`TypeMismatch` on mismatch), then creates;
368
+ # otherwise a route forces find-or-`NotFound` (`Hydrate::Act`).
369
+ complete_state = complete_state_creation?(aggregate, command, value_objects_by_name)
370
+ route_mismatch_message = "#{command[:name]} routes to {:?}, but its identity facts name {:?}"
371
+ route_arm =
372
+ if complete_state
373
+ <<~RUST.rstrip
374
+ Some(__route) => {
375
+ __route.require_depth(0)?;
376
+ let __hydrate_id: String = #{build_identity_expr(identity)};
377
+ if __route.aggregate() != __hydrate_id.as_str() {
378
+ return Err(crate::kernel::Refusal::TypeMismatch(format!(#{route_mismatch_message.inspect}, __route.aggregate(), __hydrate_id)));
379
+ }
380
+ #{create_block}
381
+ }
382
+ RUST
383
+ else
384
+ <<~RUST.rstrip
385
+ Some(__route) => {
386
+ __route.require_depth(0)?;
387
+ crate::kernel::Hydrate::Act { id: __route.aggregate().to_string() }
388
+ }
389
+ RUST
390
+ end
391
+ hydrate = <<~RUST.rstrip
392
+ match route {
393
+ #{route_arm}
394
+ None => { let __hydrate_id: String = #{build_identity_expr(identity)}; #{create_block} }
395
+ }
396
+ RUST
397
+ fn_signature = (["repo: &mut impl crate::kernel::Repository<#{record}>", "route: Option<&crate::kernel::RoutingEnvelope>"] + identity_extra_params +
398
+ ["args: #{cmd}Args", "mutations: &mut Vec<crate::kernel::MutationRecord>", *DEREF_PARAMS, TENANT_BOUNDARY_PARAM]).join(", ")
399
+ else
400
+ hydrate = %(crate::kernel::Hydrate::Act { id: id.to_string() })
401
+ fn_signature = (["repo: &mut impl crate::kernel::Repository<#{record}>", "id: &str", "args: #{cmd}Args",
402
+ "mutations: &mut Vec<crate::kernel::MutationRecord>", *DEREF_PARAMS, TENANT_BOUNDARY_PARAM]).join(", ")
403
+ end
404
+
405
+ dispatch_fn = Exemplar.render(
406
+ "dispatch_fn",
407
+ "repo: &mut impl crate::kernel::Repository<TmplRecord>, id: &str, args: TmplArgs, mutations: &mut Vec<crate::kernel::MutationRecord>, #{TENANT_BOUNDARY_PARAM}" => fn_signature,
408
+ "dispatch_tmpl" => "dispatch_#{dispatch_fn_name(cmd)}",
409
+ "TmplRecord" => record,
410
+ "tmpl_invariant_check_placeholder()?;" => invariant_checks.join("\n"),
411
+ "let tmpl_eval_fielded = tmpl_with_references_placeholder();" => with_references_binding,
412
+ "&tmpl_eval_fielded," => "&with_references,",
413
+ "let tmpl_seed_projections = tmpl_seed_projections_placeholder();" => seed_projections_binding(aggregate),
414
+ "tmpl_seed_projections," => "seed_projections,",
415
+ "tmpl_hydrate_placeholder()" => hydrate,
416
+ "tmpl_prelude_placeholder();" => prelude,
417
+ '"TmplCmdName"' => cmd.inspect,
418
+ '"TmplQualifiedName"' => "#{domain_name}::#{aggregate[:name]}".inspect,
419
+ '"TmplAggregateName"' => aggregate_name.inspect,
420
+ '"TmplIdentityReading"' => identity_reading.inspect,
421
+ "tmpl_given_spec_placeholder()," => given_specs.join("\n"),
422
+ "tmpl_transition_placeholder()" => transition_arg,
423
+ "tmpl_mutation_lines_placeholder(record);" => mutation_lines.join("\n"),
424
+ "tmpl_ensures_spec_placeholder()," => ensures_specs.join("\n"),
425
+ "tmpl_invariants_placeholder()" => "#{invariants_fn_name(aggregate)}()",
426
+ "tmpl_emit_placeholder()" => emits_out.map(&:inspect).join(", "),
427
+ "args.to_json()," => payload
428
+ )
429
+
430
+ "#{emit_fielded_flat("#{cmd}Args", command[:attributes], value_objects_by_name)}\n\n#[derive(Debug, Clone)]\n#{args_struct.join("\n")}\n\n#{dispatch_fn}"
431
+ end
432
+
433
+ # `corrects "Event"`: a synthetic prepended GivenSpec reads a per-record flag set when a
434
+ # command emitting the event succeeds. `reverses: true` is refused; Ruby has not finished
435
+ # designing it (ADR 0041).
436
+ def corrects_of(command) = command[:mutations].find { |m| m[:op].to_s == "corrects" }
437
+
438
+ def corrects_reverses?(mutation) = !!(mutation[:source].is_a?(Hash) && mutation[:source][:value].is_a?(Hash) && mutation[:source][:value][:reverses])
439
+
440
+ # Synthetic snake_case flag field name, e.g. `emitted_fee_applied` for `FeeApplied`.
441
+ def corrects_flag_field(event_name) = "emitted_#{event_name.to_s.gsub(/([a-z0-9])([A-Z])/, '\1_\2').downcase}"
442
+
443
+ def corrects_given_specs(command)
444
+ corrects = corrects_of(command)
445
+ return [] unless corrects
446
+
447
+ event_name = corrects[:target].to_s
448
+ [" crate::kernel::GivenSpec { description: \"\", " \
449
+ "expr: crate::kernel::Expr::Lookup(#{corrects_flag_field(event_name).inspect}), " \
450
+ "corrects_event: Some(#{event_name.inspect}) },"]
451
+ end
452
+
453
+ # Event names any command or entity command on this aggregate corrects. The flag lives on
454
+ # the parent record whichever level declares `corrects`; entities have no event stream.
455
+ def correctable_event_names(aggregate)
456
+ names = corrects_targets_of(aggregate[:commands])
457
+ (aggregate[:entities] || []).each { |entity| names.concat(entity_correctable_event_names(entity)) }
458
+ names.uniq
459
+ end
460
+
461
+ def entity_correctable_event_names(entity)
462
+ names = corrects_targets_of(entity[:commands])
463
+ (entity[:entities] || []).each { |nested| names.concat(entity_correctable_event_names(nested)) }
464
+ names
465
+ end
466
+
467
+ def corrects_targets_of(commands)
468
+ commands.flat_map { |c| c[:mutations] }
469
+ .select { |m| m[:op].to_s == "corrects" }
470
+ .map { |m| m[:target].to_s }
471
+ end
472
+
473
+ # `extra_fields:` entries so the flag rides the record's ordinary JSON round-trip.
474
+ def corrects_extra_fields(aggregate)
475
+ correctable_event_names(aggregate).map do |ev|
476
+ field = corrects_flag_field(ev)
477
+ deserialize_rhs = "match v.require(#{field.inspect}, #{aggregate[:name].to_s.inspect})? { " \
478
+ "crate::kernel::Json::Bool(b) => *b, " \
479
+ "_ => return Err(#{json_type_error(aggregate[:name].to_s, field, 'a boolean')}) }"
480
+ [field, "crate::kernel::Json::Bool(self.#{field})", deserialize_rhs]
481
+ end
482
+ end
483
+
484
+ # Stamps the flag onto the record when a command emitting a correctable event succeeds;
485
+ # Rust has no ambient event log to consult.
486
+ def corrects_flag_mutation_lines(command, aggregate)
487
+ correctable = correctable_event_names(aggregate)
488
+ Array(command[:emits]).map(&:to_s).uniq.select { |name| correctable.include?(name) }
489
+ .map { |name| " record.#{corrects_flag_field(name)} = true;" }
490
+ end
491
+
492
+ # `delegates_to "Entity.Command"`: the door's only mutation, run on the record inside the
493
+ # dispatch closure.
494
+ def delegate_of(command) = command[:mutations].find { |m| m[:op].to_s == "delegate" }
495
+
496
+ def delegate_mapping(delegation)
497
+ (delegation[:fields] || {}).to_h { |target_key, source_key| [target_key.to_s, source_key.to_s.delete_prefix(":")] }
498
+ end
499
+
500
+ def delegate_target(delegation, aggregate)
501
+ entity_name, _dot, command_name = delegation[:target].to_s.rpartition(".")
502
+ entity = (aggregate[:entities] || []).find { |e| e[:name].to_s == entity_name }
503
+ target = entity && entity[:commands].find { |c| c[:name].to_s == command_name }
504
+ [entity, target]
505
+ end
506
+
507
+ def delegate_skip_reason(command, aggregate, value_objects_by_name)
508
+ delegation = delegate_of(command)
509
+ return nil unless delegation
510
+
511
+ label = "delegates_to #{delegation[:target]}"
512
+ return "#{label} alongside other sets — not generated yet" if command[:mutations].size > 1
513
+
514
+ entity, target = delegate_target(delegation, aggregate)
515
+ return "#{label}: #{aggregate[:name]} has no such entity" unless entity
516
+ return "#{label}: #{entity[:name]} declares no such command" unless target
517
+ return "#{label}: #{entity[:name]} cannot be addressed by identity" unless extract_id_supported?(entity)
518
+
519
+ target_problem = entity_command_skip_reason(target, entity, value_objects_by_name)
520
+ return "#{label}: #{target_problem}" if target_problem
521
+
522
+ mapping = delegate_mapping(delegation)
523
+ target[:attributes].each do |attr|
524
+ source_name = mapping.fetch(attr[:name].to_s, attr[:name].to_s)
525
+ source = command[:attributes].find { |a| a[:name].to_s == source_name }
526
+ next if source.nil? && attr[:optional]
527
+ return "#{label}: target argument #{attr[:name]} has no source on the door" unless source
528
+ return "#{label}: door argument #{source_name} is a list, target wants a scalar (or vice versa) — not generated yet" if !!source[:list] != !!attr[:list]
529
+ # Compare with `bridgeable_value_types?`, not type-name equality: Ruby copies the raw wire
530
+ # value and lets the target's coercion decide (ADR 0045).
531
+ unless bridgeable_value_types?(source[:type].to_s, attr[:type].to_s, value_objects_by_name)
532
+ return "#{label}: door argument #{source_name} is #{source[:type]}, target wants #{attr[:type]} — not generated yet"
533
+ end
534
+ return "#{label}: optional door argument #{source_name} feeds required #{attr[:name]}" if source[:optional] && !attr[:optional]
535
+ end
536
+ entity[:identified_by].each do |path|
537
+ head = path.to_s.split(".").first
538
+ source_name = mapping.fetch(head, head)
539
+ next if command[:attributes].any? { |a| a[:name].to_s == source_name && !a[:optional] }
540
+
541
+ return "#{label}: the element's identity #{head} has no source on the door"
542
+ end
543
+ nil
544
+ end
545
+
546
+ # The prelude (before `dispatch`), the apply block (the closure body), and the target's
547
+ # events, as `step_emit` answers for a delegation.
548
+ def delegation_of(command, aggregate, value_objects_by_name, domain_name)
549
+ delegation = delegate_of(command)
550
+ return nil unless delegation
551
+
552
+ entity, target = delegate_target(delegation, aggregate)
553
+ list_attr = aggregate[:attributes].find { |a| a[:list] && a[:type] == entity[:name] }
554
+ raise "#{entity[:name]}: no list attribute on #{aggregate[:name]} holds it" unless list_attr
555
+
556
+ element_record = rust_ident(entity[:name])
557
+ target_args_name = "#{element_record}#{rust_ident(target[:name])}EntityArgs"
558
+ aliases = delegate_mapping(delegation).map { |target_key, source_key| "(#{target_key.inspect}, #{source_key.inspect})" }
559
+ given_specs = target[:givens].map do |given|
560
+ " crate::kernel::GivenSpec { description: #{rust_string_literal(given[:description])}, expr: #{ExprEmitter.emit_ast(given[:ast])}, corrects_event: None },"
561
+ end
562
+ ensures_specs = target[:ensures].map do |rule|
563
+ " crate::kernel::EnsuresSpec { description: #{rust_string_literal(rule[:description])}, expr: #{ExprEmitter.emit_ast(rule[:ast])} },"
564
+ end
565
+ transition = lifecycle_transition_for(target, entity)
566
+ mutation_lines = target[:mutations].map { |m| emit_mutation_line(m, entity, target, value_objects_by_name, optional: false) }
567
+ mutation_lines.unshift(pre_state_line) if reads_pre_state?(target[:mutations])
568
+ mutation_lines << " record.#{rust_ident_field(transition[:field])} = #{transition[:to_state].inspect}.to_string();" if transition && transition[:to_state]
569
+ mutation_lines = [" let _ = record;"] if mutation_lines.empty?
570
+
571
+ {
572
+ prelude: Exemplar.render(
573
+ "delegate_prelude",
574
+ "tmpl_aliases_placeholder()" => aliases.join(", "),
575
+ "TmplTargetArgs" => target_args_name
576
+ ).lines.map { |l| " #{l}" }.join.rstrip,
577
+ # Rendered separately: the element extraction must run after `dispatch`'s hydrate, as the
578
+ # first closure lines.
579
+ element: Exemplar.render(
580
+ "delegate_element",
581
+ "TmplElement" => element_record
582
+ ).lines.map { |l| " #{l}" }.join.rstrip,
583
+ apply: Exemplar.render(
584
+ "delegate_apply",
585
+ "TmplRecord" => rust_ident(aggregate[:name]),
586
+ "tmpl_list_field" => rust_ident_field(list_attr[:name]),
587
+ "TmplElement" => element_record,
588
+ # Bare command name: it feeds refusal text, and Ruby's `hecks_name` is never
589
+ # entity-qualified.
590
+ '"TmplQualifiedCommandName"' => target[:name].to_s.inspect,
591
+ # `apply_entity_command` needs the qualified name to render `NothingToCorrect` should a
592
+ # corrects-flagged given reach this path.
593
+ '"TmplQualifiedName"' => "#{domain_name}::#{aggregate[:name]}".inspect,
594
+ '"TmplAggregateName"' => aggregate[:name].to_s.inspect,
595
+ '"TmplEntityName"' => entity[:name].to_s.inspect,
596
+ '"TmplEntityIdentityReading"' => entity[:identified_by].join(", ").inspect,
597
+ "tmpl_given_spec_placeholder()," => given_specs.join("\n"),
598
+ "tmpl_transition_placeholder()" => transition_check_arg(transition),
599
+ "tmpl_entity_mutation_lines_placeholder(record);" => mutation_lines.join("\n"),
600
+ "tmpl_ensures_spec_placeholder()," => ensures_specs.join("\n")
601
+ ).lines.map { |l| " #{l}" }.join.rstrip,
602
+ emits: target[:emits]
603
+ }
604
+ end
605
+
606
+ # `command_skip_reason` with `entity` standing in for `aggregate`; both share one IR shape.
607
+ def entity_command_skip_reason(command, entity, value_objects_by_name)
608
+ command_skip_reason(command, entity, value_objects_by_name, creating_possible: false)
609
+ end
610
+
611
+ # An entity command: `kernel::dispatch_entity` ports `EntityInterpreter#call`, with no
612
+ # `Hydrate` branch; the entity is found by `identity()` within the parent.
613
+ def emit_entity_command(command, entity, parent_aggregate, domain_name, value_objects_by_name, aggregates_by_name,
614
+ process_managers: [])
615
+ parent_record = rust_ident(parent_aggregate[:name])
616
+ element_record = rust_ident(entity[:name])
617
+ cmd = rust_ident(command[:name])
618
+
619
+ # The parent lookup and the element lookup each need their own bare name and identity
620
+ # reading.
621
+ aggregate_name = parent_aggregate[:name].to_s
622
+ parent_identity_reading = parent_aggregate[:identified_by].join(", ")
623
+ entity_name = entity[:name].to_s
624
+ entity_identity_reading = entity[:identified_by].join(", ")
625
+
626
+ # The parent's list attribute holding this entity, resolved at generation time.
627
+ list_attr = parent_aggregate[:attributes].find { |a| a[:list] && a[:type] == entity[:name] }
628
+ raise "#{entity[:name]}: no list attribute on #{parent_aggregate[:name]} holds it — unsupported_attribute_types should have caught this" unless list_attr
629
+
630
+ list_field = rust_ident_field(list_attr[:name])
631
+ # `EntityArgs` suffix keeps a door named after the entity command it delegates to from
632
+ # colliding.
633
+ args_struct_name = "#{element_record}#{cmd}EntityArgs"
634
+
635
+ args_struct = ["pub struct #{args_struct_name} {"]
636
+ command[:attributes].each do |attr|
637
+ type = rust_type(attr[:type], list: attr[:list])
638
+ type = "Option<#{type}>" if attr[:optional]
639
+ args_struct << " #{Exemplar.render('struct_field', 'TmplFieldType' => type, 'tmpl_field' => rust_ident_field(attr[:name]))}"
640
+ end
641
+ args_struct << "}"
642
+
643
+ invariant_checks = invariant_checks_for(command, aggregates_by_name, value_objects_by_name)
644
+
645
+ # `corrects_given_specs` is prepended as in `emit_command`; `apply_entity_command`
646
+ # evaluates it against the parent record, never the element.
647
+ given_specs = corrects_given_specs(command) + command[:givens].map do |given|
648
+ " crate::kernel::GivenSpec { description: #{rust_string_literal(given[:description])}, expr: #{ExprEmitter.emit_ast(given[:ast])}, corrects_event: None },"
649
+ end
650
+
651
+ ensures_specs = command[:ensures].map do |rule|
652
+ " crate::kernel::EnsuresSpec { description: #{rust_string_literal(rule[:description])}, expr: #{ExprEmitter.emit_ast(rule[:ast])} },"
653
+ end
654
+
655
+ # The entity's own lifecycle, not the parent's.
656
+ transition = lifecycle_transition_for(command, entity)
657
+ transition_arg =
658
+ transition_check_arg(transition)
659
+
660
+ # A `:corrects` mutation reaches `emit_mutation_line` unfiltered and yields a blank line, a
661
+ # no-op; the check already ran via `corrects_given_specs`.
662
+ # Not handled: an entity command emitting a correctable event would need the flag set on the
663
+ # parent, but `record` in this closure is the element.
664
+ mutation_lines = command[:mutations].map { |m| emit_mutation_line(m, entity, command, value_objects_by_name, optional: false) }
665
+ mutation_lines.unshift(pre_state_line) if reads_pre_state?(command[:mutations])
666
+ mutation_lines << " record.#{rust_ident_field(transition[:field])} = #{transition[:to_state].inspect}.to_string();" if transition && transition[:to_state]
667
+ mutation_lines = [" let _ = record;"] if mutation_lines.empty?
668
+
669
+ # Bare name: it feeds refusal text, which Ruby never qualifies.
670
+ qualified_command_name = command[:name].to_s
671
+
672
+ entity_dispatch_fn = Exemplar.render(
673
+ "entity_dispatch_fn",
674
+ "dispatch_entity_tmpl" => "dispatch_entity_#{entity[:name].downcase}_#{dispatch_fn_name(cmd)}",
675
+ "TmplRecord" => parent_record,
676
+ "TmplArgs" => args_struct_name,
677
+ "tmpl_deref_params_placeholder: ()" => DEREF_PARAMS.join(", "),
678
+ "tmpl_invariant_check_placeholder()?;" => invariant_checks.join("\n"),
679
+ "let tmpl_eval_fielded = tmpl_with_references_placeholder();" => with_references_binding,
680
+ "&tmpl_eval_fielded," => "&with_references,",
681
+ "let tmpl_seed_projections = tmpl_seed_projections_placeholder();" => seed_projections_binding(parent_aggregate),
682
+ "tmpl_seed_projections," => "seed_projections,",
683
+ "tmpl_list_field" => list_field,
684
+ "TmplElement" => element_record,
685
+ '"TmplQualifiedCommandName"' => qualified_command_name.inspect,
686
+ '"TmplQualifiedName"' => "#{domain_name}::#{parent_aggregate[:name]}".inspect,
687
+ '"TmplAggregateName"' => aggregate_name.inspect,
688
+ '"TmplParentIdentityReading"' => parent_identity_reading.inspect,
689
+ '"TmplEntityName"' => entity_name.inspect,
690
+ '"TmplEntityIdentityReading"' => entity_identity_reading.inspect,
691
+ "tmpl_given_spec_placeholder()," => given_specs.join("\n"),
692
+ "tmpl_transition_placeholder()" => transition_arg,
693
+ "tmpl_entity_mutation_lines_placeholder(record);" => mutation_lines.join("\n"),
694
+ "tmpl_ensures_spec_placeholder()," => ensures_specs.join("\n"),
695
+ "tmpl_invariants_placeholder()" => "#{invariants_fn_name(parent_aggregate)}()",
696
+ "tmpl_emit_placeholder()" => command[:emits].map(&:inspect).join(", ")
697
+ )
698
+
699
+ [
700
+ emit_fielded_flat(args_struct_name, command[:attributes], value_objects_by_name),
701
+ "#[derive(Debug, Clone)]\n#{args_struct.join("\n")}",
702
+ emit_to_json_flat(args_struct_name, command[:attributes], value_objects_by_name, sparse: true),
703
+ # Allowlist matches the aggregate-command call site plus the entity's own identity head
704
+ # (`extra_identity_heads:`).
705
+ emit_from_json_flat(args_struct_name, command[:attributes], value_objects_by_name,
706
+ unknown_argument_allowlist: command_argument_allowlist(
707
+ parent_aggregate, command, process_managers,
708
+ extra_identity_heads: entity[:identified_by].map { |path| path.split(".").first }
709
+ ),
710
+ command_name: command[:name].to_s, absent_argument_check: true,
711
+ interleave_checks: true, aggregates_by_name: aggregates_by_name),
712
+ # Argument gates run in `EntityStep::ORDER`; see `emit_argument_gates` (json_codec.rb).
713
+ emit_argument_gates(args_struct_name, command[:name].to_s, command[:attributes],
714
+ command_argument_allowlist(
715
+ parent_aggregate, command, process_managers,
716
+ extra_identity_heads: entity[:identified_by].map { |path| path.split(".").first }
717
+ )),
718
+ entity_dispatch_fn,
719
+ ].join("\n\n")
720
+ end
721
+
722
+ # A command owned by an entity nested two levels deep. The second hop is another
723
+ # `apply_entity_command` inside the first hop's `apply_mutations` closure.
724
+ # The generated function takes plain `&str` ids per hop, so flat-args and routed callers
725
+ # share it. The outer hop has no given/ensures/transition; those belong to the inner call.
726
+ def emit_nested_entity_command(command, nested, entity, parent_aggregate, domain_name, value_objects_by_name, aggregates_by_name,
727
+ process_managers: [])
728
+ parent_record = rust_ident(parent_aggregate[:name])
729
+ entity_record = rust_ident(entity[:name])
730
+ nested_record = rust_ident(nested[:name])
731
+ cmd = rust_ident(command[:name])
732
+
733
+ aggregate_name = parent_aggregate[:name].to_s
734
+ parent_identity_reading = parent_aggregate[:identified_by].join(", ")
735
+ entity_name = entity[:name].to_s
736
+ entity_identity_reading = entity[:identified_by].join(", ")
737
+ nested_name = nested[:name].to_s
738
+ nested_identity_reading = nested[:identified_by].join(", ")
739
+
740
+ # The two list attributes the chain walks: the parent's holding `entity`, and
741
+ # `entity`'s holding `nested`.
742
+ list_attr1 = parent_aggregate[:attributes].find { |a| a[:list] && a[:type] == entity[:name] }
743
+ raise "#{entity[:name]}: no list attribute on #{parent_aggregate[:name]} holds it — unsupported_attribute_types should have caught this" unless list_attr1
744
+
745
+ list_attr2 = entity[:attributes].find { |a| a[:list] && a[:type] == nested[:name] }
746
+ raise "#{nested[:name]}: no list attribute on #{entity[:name]} holds it — unsupported_attribute_types should have caught this" unless list_attr2
747
+
748
+ list_field1 = rust_ident_field(list_attr1[:name])
749
+ list_field2 = rust_ident_field(list_attr2[:name])
750
+
751
+ # `NestedEntityArgs` suffix, distinct from `Args` and `EntityArgs`, for the same reason.
752
+ args_struct_name = "#{nested_record}#{cmd}NestedEntityArgs"
753
+
754
+ args_struct = ["pub struct #{args_struct_name} {"]
755
+ command[:attributes].each do |attr|
756
+ type = rust_type(attr[:type], list: attr[:list])
757
+ type = "Option<#{type}>" if attr[:optional]
758
+ args_struct << " #{Exemplar.render('struct_field', 'TmplFieldType' => type, 'tmpl_field' => rust_ident_field(attr[:name]))}"
759
+ end
760
+ args_struct << "}"
761
+
762
+ invariant_checks = invariant_checks_for(command, aggregates_by_name, value_objects_by_name)
763
+
764
+ given_specs = command[:givens].map do |given|
765
+ " crate::kernel::GivenSpec { description: #{rust_string_literal(given[:description])}, expr: #{ExprEmitter.emit_ast(given[:ast])}, corrects_event: None },"
766
+ end
767
+
768
+ ensures_specs = command[:ensures].map do |rule|
769
+ " crate::kernel::EnsuresSpec { description: #{rust_string_literal(rule[:description])}, expr: #{ExprEmitter.emit_ast(rule[:ast])} },"
770
+ end
771
+
772
+ # The nested entity's own lifecycle.
773
+ transition = lifecycle_transition_for(command, nested)
774
+ transition_arg = transition_check_arg(transition)
775
+
776
+ mutation_lines = command[:mutations].map { |m| emit_mutation_line(m, nested, command, value_objects_by_name, optional: false) }
777
+ mutation_lines.unshift(pre_state_line) if reads_pre_state?(command[:mutations])
778
+ mutation_lines << " record.#{rust_ident_field(transition[:field])} = #{transition[:to_state].inspect}.to_string();" if transition && transition[:to_state]
779
+ mutation_lines = [" let _ = record;"] if mutation_lines.empty?
780
+
781
+ # Bare name, as in `emit_entity_command`.
782
+ qualified_command_name = command[:name].to_s
783
+ fn_name = "dispatch_entity_#{entity[:name].downcase}_#{nested[:name].downcase}_#{dispatch_fn_name(cmd)}"
784
+
785
+ # The hop-2 call gets the root aggregate's qualified name only so it compiles; `corrects`
786
+ # admissibility is not generated at this depth, since the inner `record` is the hop-1 entity,
787
+ # not the root.
788
+ # The trailing `parent_in_args` literal is `true`; `rust/codegen/src/commands.rs` needs the
789
+ # same value (`spec/project_rust_pipeline_spec.rb` compares byte for byte).
790
+ nested_dispatch_fn = <<~RUST
791
+ pub fn #{fn_name}(
792
+ repo: &mut impl crate::kernel::Repository<#{parent_record}>, parent_id: &str, hop1_id: &str, hop1_wants: &str,
793
+ hop2_id: &str, hop2_wants: &str, args: #{args_struct_name}, mutations: &mut Vec<crate::kernel::MutationRecord>,
794
+ owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>, command_deref: Vec<(&'static str, crate::kernel::DerefNode)>,
795
+ ) -> crate::kernel::DispatchResult<#{parent_record}> {
796
+ #{invariant_checks.join("\n")}
797
+ #{with_references_binding}
798
+ #{seed_projections_binding(parent_aggregate)}
799
+
800
+ crate::kernel::dispatch_entity(
801
+ repo,
802
+ parent_id,
803
+ |r: &#{parent_record}| &r.#{list_field1},
804
+ |r: &mut #{parent_record}| &mut r.#{list_field1},
805
+ |el: &#{entity_record}| el.identity() == hop1_id,
806
+ #{qualified_command_name.inspect},
807
+ #{"#{domain_name}::#{parent_aggregate[:name]}".inspect},
808
+ #{aggregate_name.inspect},
809
+ #{parent_identity_reading.inspect},
810
+ #{entity_name.inspect},
811
+ #{entity_identity_reading.inspect},
812
+ hop1_wants,
813
+ &with_references,
814
+ &[],
815
+ None,
816
+ |nested_owner: &mut #{entity_record}| {
817
+ crate::kernel::apply_entity_command(
818
+ nested_owner,
819
+ hop1_id,
820
+ |r: &#{entity_record}| &r.#{list_field2},
821
+ |r: &mut #{entity_record}| &mut r.#{list_field2},
822
+ |el: &#{nested_record}| el.identity() == hop2_id,
823
+ #{qualified_command_name.inspect},
824
+ #{"#{domain_name}::#{aggregate_name}".inspect},
825
+ #{aggregate_name.inspect},
826
+ #{nested_name.inspect},
827
+ #{nested_identity_reading.inspect},
828
+ hop2_wants,
829
+ &with_references,
830
+ &[
831
+ #{given_specs.join("\n")}
832
+ ],
833
+ #{transition_arg},
834
+ |record| {
835
+ #{mutation_lines.join("\n")}
836
+ Ok(())
837
+ },
838
+ &[
839
+ #{ensures_specs.join("\n")}
840
+ ],
841
+ true,
842
+ )
843
+ },
844
+ &[],
845
+ &#{invariants_fn_name(parent_aggregate)}(),
846
+ &[#{command[:emits].map(&:inspect).join(', ')}],
847
+ args.to_json(),
848
+ mutations,
849
+ seed_projections,
850
+ )
851
+ }
852
+ RUST
853
+
854
+ [
855
+ emit_fielded_flat(args_struct_name, command[:attributes], value_objects_by_name),
856
+ "#[derive(Debug, Clone)]\n#{args_struct.join("\n")}",
857
+ emit_to_json_flat(args_struct_name, command[:attributes], value_objects_by_name, sparse: true),
858
+ # Both hops' identity heads are allowed, matching `ctx.chain.flat_map(&:identity_heads)`
859
+ # (entity_interpreter.rb).
860
+ emit_from_json_flat(args_struct_name, command[:attributes], value_objects_by_name,
861
+ unknown_argument_allowlist: command_argument_allowlist(
862
+ parent_aggregate, command, process_managers,
863
+ extra_identity_heads: (entity[:identified_by] + nested[:identified_by]).map { |path| path.split(".").first }
864
+ ),
865
+ command_name: command[:name].to_s, absent_argument_check: true,
866
+ interleave_checks: true, aggregates_by_name: aggregates_by_name),
867
+ # Argument gates, as in the one-hop entity command above.
868
+ emit_argument_gates(args_struct_name, command[:name].to_s, command[:attributes],
869
+ command_argument_allowlist(
870
+ parent_aggregate, command, process_managers,
871
+ extra_identity_heads: (entity[:identified_by] + nested[:identified_by]).map { |path| path.split(".").first }
872
+ )),
873
+ nested_dispatch_fn,
874
+ ].join("\n\n")
875
+ end
876
+ end
877
+ end