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,1226 @@
1
+ //! Port of `rust/project/commands.rb`: emits the per-command dispatch functions.
2
+
3
+ use crate::exemplar::Exemplar;
4
+ use crate::json::Json;
5
+ use crate::literal::Literal;
6
+ use crate::mutations;
7
+ use crate::naming;
8
+ use crate::skip_reason::{skip, SkipReason};
9
+ use std::collections::HashMap;
10
+
11
+ /// The aggregate's invariant set, generated once per aggregate file and passed to every dispatch.
12
+ pub fn invariants_fn_name(aggregate: &Json) -> String {
13
+ format!("{}_invariants", naming::rust_ident_field(aggregate.get("name").and_then(Json::as_str).unwrap_or("")).to_lowercase())
14
+ }
15
+
16
+ pub fn emit_invariants_fn(aggregate: &Json) -> String {
17
+ let aggregate_vec = invariant_specs_vec(aggregate.get("invariants").map(Json::each).unwrap_or(&[]), 8);
18
+ let entities_vec = entity_invariants_vec(aggregate, 8);
19
+ // `Expr` is imported only when an invariant literal was emitted; otherwise it is unused.
20
+ let needs_expr = aggregate_vec.contains("Expr::") || entities_vec.contains("Expr::");
21
+ [
22
+ Some(format!("fn {}() -> crate::kernel::InvariantSet {{", invariants_fn_name(aggregate))),
23
+ needs_expr.then(|| " use crate::kernel::Expr;".to_string()),
24
+ Some(" crate::kernel::InvariantSet {".to_string()),
25
+ Some(format!(" aggregate: {aggregate_vec},")),
26
+ Some(format!(" entities: {entities_vec},")),
27
+ Some(" }".to_string()),
28
+ Some("}".to_string()),
29
+ ]
30
+ .into_iter()
31
+ .flatten()
32
+ .collect::<Vec<_>>()
33
+ .join("\n")
34
+ }
35
+
36
+ fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
37
+ if rules.is_empty() {
38
+ return "vec![]".to_string();
39
+ }
40
+ let pad = " ".repeat(indent + 4);
41
+ let rows: Vec<String> = rules
42
+ .iter()
43
+ .map(|rule| {
44
+ let description = rule.get("description").and_then(Json::as_str).unwrap_or("");
45
+ let ast = rule.get("ast").unwrap_or_else(|| panic!("invariant row has no ast: {rule:?}"));
46
+ format!(
47
+ "{pad}crate::kernel::InvariantSpec {{ description: {}, expr: {} }},",
48
+ naming::ruby_inspect_string(description),
49
+ crate::expr_emitter::emit_ast(ast)
50
+ )
51
+ })
52
+ .collect();
53
+ format!("vec![\n{}\n{}]", rows.join("\n"), " ".repeat(indent))
54
+ }
55
+
56
+ fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
57
+ let entities = owner.get("entities").map(Json::each).unwrap_or(&[]);
58
+ let attributes = owner.get("attributes").map(Json::each).unwrap_or(&[]);
59
+ let pieces: Vec<(&Json, String)> = entities
60
+ .iter()
61
+ .filter_map(|entity| {
62
+ if entity.get("invariants").map(Json::each).unwrap_or(&[]).is_empty() {
63
+ return None;
64
+ }
65
+ let name = entity.get("name").map(Json::to_s).unwrap_or_default();
66
+ let list = attributes
67
+ .iter()
68
+ .find(|a| a.get("list").map(Json::as_bool).unwrap_or(false) && a.get("type").map(Json::to_s).unwrap_or_default() == name)?;
69
+ Some((entity, list.get("name").map(Json::to_s).unwrap_or_default()))
70
+ })
71
+ .collect();
72
+ if pieces.is_empty() {
73
+ return "vec![]".to_string();
74
+ }
75
+ let pad = " ".repeat(indent + 4);
76
+ let rows: Vec<String> = pieces
77
+ .iter()
78
+ .map(|(entity, list_field)| {
79
+ format!(
80
+ "{pad}crate::kernel::EntityInvariants {{ name: {}, list_field: {}, specs: {}, nested: {} }},",
81
+ naming::ruby_inspect_string(&entity.get("name").map(Json::to_s).unwrap_or_default()),
82
+ naming::ruby_inspect_string(list_field),
83
+ invariant_specs_vec(entity.get("invariants").map(Json::each).unwrap_or(&[]), indent + 4),
84
+ entity_invariants_vec(entity, indent + 4)
85
+ )
86
+ })
87
+ .collect();
88
+ format!("vec![\n{}\n{}]", rows.join("\n"), " ".repeat(indent))
89
+ }
90
+
91
+ /// Extra parameters of every generated `dispatch_*`/`dispatch_entity_*`, resolved by the router.
92
+ fn deref_params() -> [&'static str; 2] {
93
+ ["owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>", "command_deref: Vec<(&'static str, crate::kernel::DerefNode)>"]
94
+ }
95
+
96
+ /// Write-side tenant boundary result, threaded through aggregate commands only.
97
+ /// The router always passes `Ok(())`: no domain generated from `ir.json` declares a boundary.
98
+ const TENANT_BOUNDARY_PARAM: &str = "tenant_boundary_check: Result<(), crate::kernel::Refusal>";
99
+
100
+ fn with_references_binding() -> String {
101
+ "let with_references = crate::kernel::WithReferences { command_deref: &command_deref, args: &args, owner_deref: &owner_deref };".to_string()
102
+ }
103
+
104
+ /// Binds the synchronous half of `projects` (ADR 0025).
105
+ fn seed_projections_binding(aggregate: &Json) -> String {
106
+ let table = naming::screaming_snake(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
107
+ format!("let seed_projections = crate::kernel::seeded_projections(&with_references, {table}_PROJECTED_FIELDS);")
108
+ }
109
+
110
+ fn target_type_for<'a>(target: &str, aggregate: &'a Json, lifecycle_field: Option<&str>) -> Option<&'a str> {
111
+ if lifecycle_field == Some(target) {
112
+ return Some("String");
113
+ }
114
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
115
+ attrs.iter().find(|a| crate::attr::name(a) == target).map(crate::attr::type_name)
116
+ }
117
+
118
+ // The lifecycle field is never a list; `false` when the target is not found.
119
+ fn target_list_for(target: &str, aggregate: &Json, lifecycle_field: Option<&str>) -> bool {
120
+ if lifecycle_field == Some(target) {
121
+ return false;
122
+ }
123
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
124
+ attrs.iter().find(|a| crate::attr::name(a) == target).map(crate::attr::list).unwrap_or(false)
125
+ }
126
+
127
+ pub fn command_skip_reason(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
128
+ command_skip_reason_with(command, aggregate, value_objects_by_name, true)
129
+ }
130
+
131
+ /// `creating_possible` is false for an entity command: its optional identity argument is not an
132
+ /// identity source.
133
+ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, creating_possible: bool) -> Option<SkipReason> {
134
+ let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
135
+
136
+ let mut unsupported_ops: Vec<String> = Vec::new();
137
+ for m in mutations_list {
138
+ let op = m.get("op").map(Json::to_s).unwrap_or_default();
139
+ // `remove` passes here; `mutations::remove_field_problems` narrows it to entity lists.
140
+ if !["append", "set", "increment", "decrement", "multiply", "clamp", "remove", "delegate", "corrects"].contains(&op.as_str()) && !unsupported_ops.contains(&op) {
141
+ unsupported_ops.push(op);
142
+ }
143
+ }
144
+ if !unsupported_ops.is_empty() {
145
+ return Some(skip("mutation_op", format!("sets op(s) {} not generated yet (only append/set/increment/decrement/multiply/clamp/remove/delegate/corrects are)", unsupported_ops.join(", "))));
146
+ }
147
+
148
+ if let Some(corrects) = crate::bridging::corrects_of(command) {
149
+ // The IR is already derived, so only a command carrying nothing but its `corrects` is
150
+ // refused.
151
+ if crate::bridging::corrects_reverses(corrects) && mutations_list.iter().all(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects") {
152
+ let event = corrects.get("target").map(Json::to_s).unwrap_or_default();
153
+ return Some(skip("corrects_reverses", format!("corrects {event}, 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")));
154
+ }
155
+ }
156
+
157
+ if let Some(problem) = delegate_skip_reason(command, aggregate, value_objects_by_name) {
158
+ return Some(skip("delegate", problem));
159
+ }
160
+
161
+ let append_problems = mutations::append_field_problems(command, aggregate, value_objects_by_name);
162
+ if !append_problems.is_empty() {
163
+ return Some(skip("append_field", format!("sets append field(s): {}", append_problems.join("; "))));
164
+ }
165
+
166
+ let state_problems = mutations::state_source_problems(command, aggregate, value_objects_by_name);
167
+ if !state_problems.is_empty() {
168
+ return Some(skip("state_source", format!("sets state source(s): {}", state_problems.join("; "))));
169
+ }
170
+
171
+ let remove_problems = mutations::remove_field_problems(command, aggregate, value_objects_by_name);
172
+ if !remove_problems.is_empty() {
173
+ return Some(skip("remove_field", format!("sets remove field(s): {}", remove_problems.join("; "))));
174
+ }
175
+
176
+ let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
177
+
178
+ let literal_set_targets: Vec<String> = mutations_list
179
+ .iter()
180
+ .filter(|m| {
181
+ if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
182
+ return false;
183
+ }
184
+ let Some(source) = m.get("source") else { return false };
185
+ if source.get("kind").map(Json::to_s).unwrap_or_default() != "literal" {
186
+ return false;
187
+ }
188
+ let value = source.get("value").unwrap_or(&Json::Null);
189
+ let target = m.get("target").map(Json::to_s).unwrap_or_default();
190
+ let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
191
+ !crate::bridging::literal_set_bridgeable(&Literal::from_json(value), target_type, value_objects_by_name)
192
+ })
193
+ .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
194
+ .collect();
195
+ if !literal_set_targets.is_empty() {
196
+ return Some(skip("set_literal", format!("sets to: a literal that doesn't bridge to the target's type ({}) — not generated yet", literal_set_targets.join(", "))));
197
+ }
198
+
199
+ let mismatched_sets: Vec<String> = mutations_list
200
+ .iter()
201
+ .filter(|m| {
202
+ if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
203
+ return false;
204
+ }
205
+ let Some(source) = m.get("source") else { return false };
206
+ if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
207
+ return false;
208
+ }
209
+ let target = m.get("target").map(Json::to_s).unwrap_or_default();
210
+ let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
211
+ let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
212
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
213
+ let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name);
214
+ let source_type = source_attr.map(crate::attr::type_name);
215
+ // List-ness must match too: element-type bridging alone would collapse a `Vec` to a
216
+ // scalar.
217
+ match (target_type, source_type, source_attr) {
218
+ (Some(t), Some(s), Some(source_attr)) => {
219
+ target_list_for(&target, aggregate, lifecycle_field.as_deref()) != crate::attr::list(source_attr)
220
+ || !crate::bridging::bridgeable_value_types(s, t, value_objects_by_name)
221
+ }
222
+ _ => false,
223
+ }
224
+ })
225
+ .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
226
+ .collect();
227
+ if !mismatched_sets.is_empty() {
228
+ return Some(skip("set_argument_bridge", format!("sets :{} sources an argument no single-field rewrap can bridge to the target's type — not generated yet", mismatched_sets.join(", "))));
229
+ }
230
+
231
+ let arithmetic_targets: Vec<&Json> = mutations_list.iter().filter(|m| ["increment", "decrement", "multiply"].contains(&m.get("op").map(Json::to_s).unwrap_or_default().as_str())).collect();
232
+ let unsupported_arithmetic: Vec<String> = arithmetic_targets
233
+ .iter()
234
+ .filter(|m| {
235
+ let target = crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
236
+ match target {
237
+ Some((_, field)) => crate::bridging::arithmetic_amount_expr(m.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &field).is_none(),
238
+ None => true,
239
+ }
240
+ })
241
+ .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
242
+ .collect();
243
+ if !unsupported_arithmetic.is_empty() {
244
+ return Some(skip("arithmetic", format!("sets :{} increment/decrement/multiply amount or target field isn't bridgeable — not generated yet", unsupported_arithmetic.join(", "))));
245
+ }
246
+
247
+ // `:clamp`: target as for increment/decrement/multiply; bounds via `clamp_bounds_ints`.
248
+ let clamp_targets: Vec<&Json> = mutations_list.iter().filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "clamp").collect();
249
+ let unsupported_clamp: Vec<String> = clamp_targets
250
+ .iter()
251
+ .filter(|m| {
252
+ let target = crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
253
+ let bounds = m.get("source").and_then(crate::bridging::clamp_bounds_ints);
254
+ target.is_none() || bounds.is_none()
255
+ })
256
+ .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
257
+ .collect();
258
+ if !unsupported_clamp.is_empty() {
259
+ return Some(skip("clamp", format!("sets :{} clamp target field or bounds isn't bridgeable — not generated yet", unsupported_clamp.join(", "))));
260
+ }
261
+
262
+ let optional_problems = optional_source_mismatches_with(command, aggregate, value_objects_by_name, creating_possible);
263
+ if !optional_problems.is_empty() {
264
+ return Some(skip("optional_source", format!("optional argument feeds a non-optional target: {} — not generated yet", optional_problems.join("; "))));
265
+ }
266
+
267
+ None
268
+ }
269
+
270
+ pub fn optional_source_mismatches(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
271
+ optional_source_mismatches_with(command, aggregate, value_objects_by_name, true)
272
+ }
273
+
274
+ fn optional_source_mismatches_with(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, creating_possible: bool) -> Vec<String> {
275
+ let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
276
+ let mut problems: Vec<String> = Vec::new();
277
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
278
+
279
+ if creating_possible && crate::shared::creates_owner(aggregate, command, value_objects_by_name) {
280
+ let identified_by = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]);
281
+ for path in identified_by {
282
+ let path = path.as_str().unwrap_or("");
283
+ let head = path.split('.').next().unwrap_or("");
284
+ if let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == head) {
285
+ if crate::attr::optional(source_attr) {
286
+ problems.push(format!("identified_by :{path} sources optional argument {}", crate::attr::name(source_attr)));
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
293
+ for m in mutations_list {
294
+ let op = m.get("op").map(Json::to_s).unwrap_or_default();
295
+ let target = m.get("target").map(Json::to_s).unwrap_or_default();
296
+ match op.as_str() {
297
+ "set" => {
298
+ let Some(source) = m.get("source") else { continue };
299
+ if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
300
+ continue;
301
+ }
302
+ let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
303
+ let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name) else { continue };
304
+ if !crate::attr::optional(source_attr) {
305
+ continue;
306
+ }
307
+
308
+ if lifecycle_field.as_deref() == Some(target.as_str()) {
309
+ problems.push(format!("sets :{target} sources optional argument {} into the lifecycle field", crate::attr::name(source_attr)));
310
+ continue;
311
+ }
312
+
313
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
314
+ let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target);
315
+ if target_attr.map(crate::attr::list).unwrap_or(false) {
316
+ continue;
317
+ }
318
+ // An aggregate command's record fields are all `Option<T>`.
319
+ if creating_possible {
320
+ continue;
321
+ }
322
+ if !target_attr.map(crate::attr::optional).unwrap_or(false) {
323
+ problems.push(format!("sets :{target} sources optional argument {}", crate::attr::name(source_attr)));
324
+ }
325
+ }
326
+ "append" => {
327
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
328
+ let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target) else { continue };
329
+ let Some(element) = mutations::append_element(aggregate, crate::attr::type_name(target_attr), value_objects_by_name) else { continue };
330
+
331
+ let fields = match m.get("fields") {
332
+ Some(Json::Object(pairs)) => pairs.clone(),
333
+ _ => Vec::new(),
334
+ };
335
+ let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
336
+ for (field_name, source) in &fields {
337
+ let parsed = mutations::append_field_source(&source.to_s());
338
+ let Literal::Symbol(arg_name) = &parsed else { continue };
339
+ let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()) else { continue };
340
+ if !crate::attr::optional(source_attr) {
341
+ continue;
342
+ }
343
+ let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str());
344
+ if !field_attr.map(crate::attr::optional).unwrap_or(false) {
345
+ problems.push(format!("sets append {target}.{field_name} sources optional argument {}", crate::attr::name(source_attr)));
346
+ }
347
+ }
348
+ }
349
+ _ => {}
350
+ }
351
+ }
352
+
353
+ problems
354
+ }
355
+
356
+ /// One attribute's `admits:` check and value-object invariant check against `value_expr`.
357
+ /// `pattern:` is not checked: Ruby never enforces it on a bare command attribute (ADR 0051).
358
+ /// List attributes get no `admits:` check either, matching Ruby.
359
+ pub fn argument_check_lines(exemplar: &Exemplar, attr: &Json, value_expr: &str, aggregates_by_name: &HashMap<String, &Json>, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
360
+ let mut lines: Vec<String> = Vec::new();
361
+
362
+ if !crate::attr::list(attr) {
363
+ // Raw field expression: `constraints.rs` handles optionals itself.
364
+ if let Some(c) = crate::constraints::emit_admits_check(exemplar, value_expr, attr, aggregates_by_name, value_objects_by_name) {
365
+ lines.push(format!(" {c}"));
366
+ }
367
+ }
368
+
369
+ let vo = value_objects_by_name.get(crate::attr::type_name(attr));
370
+ if let Some(vo) = vo {
371
+ if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
372
+ let line = if crate::attr::optional(attr) {
373
+ if crate::attr::list(attr) {
374
+ format!(" if let Some(items) = &{value_expr} {{ for item in items {{ item.check_invariants()?; }} }}")
375
+ } else {
376
+ format!(" if let Some(v) = &{value_expr} {{ v.check_invariants()?; }}")
377
+ }
378
+ } else if crate::attr::list(attr) {
379
+ format!(" for item in &{value_expr} {{ item.check_invariants()?; }}")
380
+ } else {
381
+ format!(" {value_expr}.check_invariants()?;")
382
+ };
383
+ lines.push(line);
384
+ }
385
+ }
386
+
387
+ lines
388
+ }
389
+
390
+ /// Invariant checks against the built `args.{field}`, redundant with `emit_from_json_flat`'s copy.
391
+ pub fn invariant_checks_for(exemplar: &Exemplar, command: &Json, aggregates_by_name: &HashMap<String, &Json>, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
392
+ let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
393
+ let mut lines: Vec<String> = Vec::new();
394
+
395
+ for attr in attrs {
396
+ let field = naming::rust_ident_field(crate::attr::name(attr));
397
+ let value_expr = format!("args.{field}");
398
+ lines.extend(argument_check_lines(exemplar, attr, &value_expr, aggregates_by_name, value_objects_by_name));
399
+ }
400
+
401
+ lines
402
+ }
403
+
404
+ /// One emitter for every command shape `kernel::dispatch` can run.
405
+ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domain_name: &str, value_objects_by_name: &HashMap<String, &Json>, aggregates_by_name: &HashMap<String, &Json>) -> String {
406
+ let record = naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
407
+ let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
408
+ let creates = crate::shared::creates_owner(aggregate, command, value_objects_by_name);
409
+ let identity = mutations::identity_components(aggregate, command);
410
+ let identity_extra_params: Vec<String> = identity.iter().filter_map(|c| c.param.clone()).collect();
411
+
412
+ let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
413
+ let identity_reading = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
414
+
415
+ let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
416
+ let mut args_struct = vec![format!("pub struct {cmd}Args {{")];
417
+ for attr in attrs {
418
+ let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
419
+ if crate::attr::optional(attr) {
420
+ ty = format!("Option<{ty}>");
421
+ }
422
+ args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
423
+ }
424
+ args_struct.push("}".to_string());
425
+
426
+ let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
427
+
428
+ let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
429
+ let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
430
+ given_specs.extend(givens.iter().map(|g| {
431
+ let description = g.get("description").and_then(Json::as_str).unwrap_or("");
432
+ let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
433
+ format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
434
+ }));
435
+
436
+ let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
437
+ let ensures_specs: Vec<String> = ensures
438
+ .iter()
439
+ .map(|e| {
440
+ let description = e.get("description").and_then(Json::as_str).unwrap_or("");
441
+ let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
442
+ format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
443
+ })
444
+ .collect();
445
+
446
+ let transition = mutations::lifecycle_transition_for(command, aggregate);
447
+ let transition_arg = match &transition {
448
+ Some(t) => format!(
449
+ "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
450
+ naming::ruby_inspect_string(&t.field),
451
+ t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
452
+ ),
453
+ None => "None".to_string(),
454
+ };
455
+
456
+ let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
457
+ // A `delegate` mutation is rendered by `delegation_of`, which overwrites `mutation_lines`;
458
+ // `mutations::emit_mutation_line` panics on the op, so it must be excluded here.
459
+ let mut mutation_lines: Vec<String> = mutations_list
460
+ .iter()
461
+ .filter(|m| {
462
+ let op = m.get("op").map(Json::to_s).unwrap_or_default();
463
+ op != "corrects" && op != "delegate"
464
+ })
465
+ .map(|m| mutations::emit_mutation_line(exemplar, m, aggregate, command, value_objects_by_name, true))
466
+ .collect();
467
+ if mutations::reads_pre_state(mutations_list) {
468
+ mutation_lines.insert(0, mutations::pre_state_line());
469
+ }
470
+ // Stamp the fact forward for every command whose `emits` names an event a `corrects` targets.
471
+ let correctable = crate::bridging::correctable_event_names(aggregate);
472
+ let mut emitted_names: Vec<String> = Vec::new();
473
+ for name in command.get("emits").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s) {
474
+ // `.uniq`: first-occurrence order, not sorted.
475
+ if !emitted_names.contains(&name) {
476
+ emitted_names.push(name);
477
+ }
478
+ }
479
+ for name in &emitted_names {
480
+ if correctable.contains(name) {
481
+ mutation_lines.push(format!(" record.{} = true;", crate::bridging::corrects_flag_field(name)));
482
+ }
483
+ }
484
+ if let Some(t) = &transition {
485
+ if !t.to_state.is_empty() {
486
+ mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
487
+ }
488
+ }
489
+ if mutation_lines.is_empty() {
490
+ mutation_lines = vec![" let _ = record;".to_string()];
491
+ }
492
+ let delegation = delegation_of(exemplar, command, aggregate, value_objects_by_name, domain_name);
493
+ if let Some(d) = &delegation {
494
+ assert!(!creates, "{cmd}: a creating command cannot delegate — nothing exists to delegate to");
495
+ // `d.element` runs first, inside the closure `dispatch` calls after a successful hydrate.
496
+ mutation_lines = vec![d.element.clone(), d.apply.clone()];
497
+ }
498
+ let prelude = delegation.as_ref().map(|d| d.prelude.clone()).unwrap_or_default();
499
+ let payload = if delegation.is_some() { "delegate_facts.clone()," } else { "args.to_json()," }.to_string();
500
+
501
+ let (hydrate, fn_signature);
502
+ // A `projects` field is never a command argument; `seed_projections` populates it before save.
503
+ // Owned because the pseudo-attributes are freshly built.
504
+ let mut record_agg_attrs_owned: Vec<Json> = aggregate.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
505
+ record_agg_attrs_owned.extend(crate::types::projected_field_pseudo_attributes(aggregate));
506
+ let record_agg_attrs = record_agg_attrs_owned.as_slice();
507
+
508
+ if creates {
509
+ let record_fields: Vec<String> = record_agg_attrs
510
+ .iter()
511
+ .map(|attr| {
512
+ let matched = attrs.iter().find(|a| crate::attr::name(a) == crate::attr::name(attr));
513
+ let field = naming::rust_ident_field(crate::attr::name(attr));
514
+ if let Some(matched) = matched {
515
+ let matched_type = crate::attr::type_name(matched);
516
+ let attr_type = crate::attr::type_name(attr);
517
+ if crate::attr::optional(matched) {
518
+ if crate::attr::list(attr) && !crate::shared::list_attr_creation_optional(aggregate, crate::attr::name(attr), value_objects_by_name) {
519
+ format!(" {field}: args.{field}.clone().unwrap_or_default(),")
520
+ } else if crate::attr::list(attr) || matched_type == attr_type {
521
+ format!(" {field}: args.{field}.clone(),")
522
+ } else {
523
+ // A cross-aggregate argument named like the owner's field but of
524
+ // another type needs a bridge.
525
+ format!(" {field}: {},", mutations::optional_value_rhs(&format!("args.{field}"), matched_type, attr_type, value_objects_by_name))
526
+ }
527
+ } else if crate::attr::list(attr) || matched_type == attr_type {
528
+ if crate::attr::list(attr) {
529
+ format!(" {field}: args.{field}.clone(),")
530
+ } else {
531
+ format!(" {field}: Some(args.{field}.clone()),")
532
+ }
533
+ } else {
534
+ format!(" {field}: Some({}),", crate::bridging::value_rhs(&format!("args.{field}"), matched_type, attr_type, value_objects_by_name))
535
+ }
536
+ } else if crate::attr::list(attr) {
537
+ format!(" {field}: vec![],")
538
+ } else {
539
+ match crate::bridging::creation_default_rhs(attr, value_objects_by_name) {
540
+ Some(rhs) => format!(" {field}: Some({rhs}),"),
541
+ None => format!(" {field}: None,"),
542
+ }
543
+ }
544
+ })
545
+ .collect();
546
+ let mut record_fields = record_fields;
547
+ if let Some(lifecycle) = aggregate.get("lifecycle") {
548
+ let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("");
549
+ let default = lifecycle.get("default").map(Json::to_s).unwrap_or_default();
550
+ record_fields.push(format!(" {}: {}.to_string(),", naming::rust_ident_field(field), naming::ruby_inspect_string(&default)));
551
+ }
552
+ for ev in crate::bridging::correctable_event_names(aggregate) {
553
+ record_fields.push(format!(" {}: false,", crate::bridging::corrects_flag_field(&ev)));
554
+ }
555
+
556
+ // Mirrors `CommandInterpreter#step_hydrate`: `complete_state? && state_independent?`.
557
+ let state_independent = crate::dependency_planning::state_independent_creation(aggregate, command, value_objects_by_name);
558
+ let create_block = format!(
559
+ "crate::kernel::Hydrate::Create {{\n id: __hydrate_id,\n build: Box::new(|| {record} {{\n{}\n }}),\n state_independent: {state_independent},\n }}",
560
+ record_fields.join("\n")
561
+ );
562
+
563
+ // A route on a creating command is checked against its derived identity (`TypeMismatch`)
564
+ // only when `complete_state_creation` holds; otherwise it forces find-or-`NotFound`.
565
+ let complete_state = crate::dependency_planning::complete_state_creation(aggregate, command, value_objects_by_name);
566
+ let derived_id_expr = mutations::build_identity_expr(&identity);
567
+ let route_mismatch_message = format!(
568
+ "{} routes to {{:?}}, but its identity facts name {{:?}}",
569
+ command.get("name").and_then(Json::as_str).unwrap_or("")
570
+ );
571
+ let route_arm = if complete_state {
572
+ format!(
573
+ "Some(__route) => {{\n __route.require_depth(0)?;\n let __hydrate_id: String = {derived_id_expr};\n if __route.aggregate() != __hydrate_id.as_str() {{\n return Err(crate::kernel::Refusal::TypeMismatch(format!({}, __route.aggregate(), __hydrate_id)));\n }}\n {create_block}\n }}",
574
+ naming::ruby_inspect_string(&route_mismatch_message)
575
+ )
576
+ } else {
577
+ "Some(__route) => {\n __route.require_depth(0)?;\n crate::kernel::Hydrate::Act { id: __route.aggregate().to_string() }\n }".to_string()
578
+ };
579
+ hydrate = format!(
580
+ "match route {{\n {route_arm}\n None => {{ let __hydrate_id: String = {derived_id_expr}; {create_block} }}\n }}"
581
+ );
582
+ let mut sig_parts = vec![
583
+ format!("repo: &mut impl crate::kernel::Repository<{record}>"),
584
+ "route: Option<&crate::kernel::RoutingEnvelope>".to_string(),
585
+ ];
586
+ sig_parts.extend(identity_extra_params.iter().cloned());
587
+ sig_parts.push(format!("args: {cmd}Args"));
588
+ sig_parts.push("mutations: &mut Vec<crate::kernel::MutationRecord>".to_string());
589
+ sig_parts.extend(deref_params().iter().map(|s| s.to_string()));
590
+ sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
591
+ fn_signature = sig_parts.join(", ");
592
+ } else {
593
+ let mut sig_parts = vec![
594
+ format!("repo: &mut impl crate::kernel::Repository<{record}>"),
595
+ "id: &str".to_string(),
596
+ format!("args: {cmd}Args"),
597
+ "mutations: &mut Vec<crate::kernel::MutationRecord>".to_string(),
598
+ ];
599
+ sig_parts.extend(deref_params().iter().map(|s| s.to_string()));
600
+ sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
601
+ hydrate = "crate::kernel::Hydrate::Act { id: id.to_string() }".to_string();
602
+ fn_signature = sig_parts.join(", ");
603
+ }
604
+
605
+ let emits_expr = match &delegation {
606
+ Some(d) => d.emits.iter().map(|e| naming::ruby_inspect_string(e)).collect::<Vec<_>>().join(", "),
607
+ None => command.get("emits").map(Json::each).unwrap_or(&[]).iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", "),
608
+ };
609
+
610
+ let dispatch_fn = exemplar.render(
611
+ "dispatch_fn",
612
+ &[
613
+ (
614
+ "repo: &mut impl crate::kernel::Repository<TmplRecord>, id: &str, args: TmplArgs, mutations: &mut Vec<crate::kernel::MutationRecord>, tenant_boundary_check: Result<(), crate::kernel::Refusal>",
615
+ fn_signature,
616
+ ),
617
+ ("dispatch_tmpl", format!("dispatch_{}", naming::dispatch_fn_name(&cmd))),
618
+ ("TmplRecord", record),
619
+ ("tmpl_invariant_check_placeholder()?;", invariant_checks.join("\n")),
620
+ ("let tmpl_eval_fielded = tmpl_with_references_placeholder();", with_references_binding()),
621
+ ("&tmpl_eval_fielded,", "&with_references,".to_string()),
622
+ ("let tmpl_seed_projections = tmpl_seed_projections_placeholder();", seed_projections_binding(aggregate)),
623
+ ("tmpl_seed_projections,", "seed_projections,".to_string()),
624
+ ("tmpl_hydrate_placeholder()", hydrate),
625
+ ("tmpl_prelude_placeholder();", prelude),
626
+ ("\"TmplCmdName\"", naming::ruby_inspect_string(&cmd)),
627
+ ("\"TmplQualifiedName\"", naming::ruby_inspect_string(&format!("{domain_name}::{}", aggregate.get("name").and_then(Json::as_str).unwrap_or("")))),
628
+ ("\"TmplAggregateName\"", naming::ruby_inspect_string(&aggregate_name)),
629
+ ("\"TmplIdentityReading\"", naming::ruby_inspect_string(&identity_reading)),
630
+ ("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
631
+ ("tmpl_transition_placeholder()", transition_arg),
632
+ ("tmpl_mutation_lines_placeholder(record);", mutation_lines.join("\n")),
633
+ ("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
634
+ ("tmpl_invariants_placeholder()", format!("{}()", invariants_fn_name(aggregate))),
635
+ ("tmpl_emit_placeholder()", emits_expr),
636
+ ("args.to_json(),", payload),
637
+ ],
638
+ );
639
+
640
+ format!("{}\n\n#[derive(Debug, Clone)]\n{}\n\n{dispatch_fn}", crate::fielded::emit_fielded_flat(exemplar, &format!("{cmd}Args"), attrs, value_objects_by_name, &[]), args_struct.join("\n"))
641
+ }
642
+
643
+ /// The `:delegate` mutation (`delegates_to`).
644
+ fn delegate_of(command: &Json) -> Option<&Json> {
645
+ command.get("mutations").map(Json::each).unwrap_or(&[]).iter().find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "delegate")
646
+ }
647
+
648
+ fn delegate_mapping(delegation: &Json) -> Vec<(String, String)> {
649
+ match delegation.get("fields") {
650
+ Some(Json::Object(pairs)) => pairs.iter().map(|(k, v)| (k.clone(), v.to_s().trim_start_matches(':').to_string())).collect(),
651
+ _ => Vec::new(),
652
+ }
653
+ }
654
+
655
+ fn delegate_target<'a>(delegation: &Json, aggregate: &'a Json) -> (Option<&'a Json>, Option<&'a Json>) {
656
+ let target = delegation.get("target").map(Json::to_s).unwrap_or_default();
657
+ let (entity_name, command_name) = match target.rfind('.') {
658
+ Some(dot) => (&target[..dot], &target[dot + 1..]),
659
+ None => ("", target.as_str()),
660
+ };
661
+ let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == entity_name);
662
+ let command = entity.and_then(|e| e.get("commands").map(Json::each).unwrap_or(&[]).iter().find(|c| c.get("name").map(Json::to_s).unwrap_or_default() == command_name));
663
+ (entity, command)
664
+ }
665
+
666
+ pub fn delegate_skip_reason(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
667
+ let delegation = delegate_of(command)?;
668
+ let label = format!("delegates_to {}", delegation.get("target").map(Json::to_s).unwrap_or_default());
669
+ if command.get("mutations").map(Json::each).unwrap_or(&[]).len() > 1 {
670
+ return Some(format!("{label} alongside other sets — not generated yet"));
671
+ }
672
+ let (entity, target) = delegate_target(delegation, aggregate);
673
+ let Some(entity) = entity else {
674
+ return Some(format!("{label}: {} has no such entity", aggregate.get("name").map(Json::to_s).unwrap_or_default()));
675
+ };
676
+ let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
677
+ let Some(target) = target else {
678
+ return Some(format!("{label}: {entity_name} declares no such command"));
679
+ };
680
+ if !crate::json_codec::extract_id_supported(entity) {
681
+ return Some(format!("{label}: {entity_name} cannot be addressed by identity"));
682
+ }
683
+ if let Some(problem) = entity_command_skip_reason(target, entity, value_objects_by_name) {
684
+ return Some(format!("{label}: {problem}"));
685
+ }
686
+ let mapping = delegate_mapping(delegation);
687
+ let source_name_for = |name: &str| mapping.iter().find(|(t, _)| t == name).map(|(_, s)| s.clone()).unwrap_or_else(|| name.to_string());
688
+ let door_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
689
+ for attr in target.get("attributes").map(Json::each).unwrap_or(&[]) {
690
+ let name = crate::attr::name(attr);
691
+ let source_name = source_name_for(name);
692
+ let source = door_attrs.iter().find(|a| crate::attr::name(a) == source_name);
693
+ let Some(source) = source else {
694
+ if crate::attr::optional(attr) {
695
+ continue;
696
+ }
697
+ return Some(format!("{label}: target argument {name} has no source on the door"));
698
+ };
699
+ if crate::attr::list(source) != crate::attr::list(attr) {
700
+ return Some(format!("{label}: door argument {source_name} is a list, target wants a scalar (or vice versa) — not generated yet"));
701
+ }
702
+ // Bridgeable, not exact type-name equality: the alias-then-deserialize delegate never
703
+ // compares declared type names (docs/decisions/0045).
704
+ if !crate::bridging::bridgeable_value_types(crate::attr::type_name(source), crate::attr::type_name(attr), value_objects_by_name) {
705
+ return Some(format!("{label}: door argument {source_name} is {}, target wants {} — not generated yet", crate::attr::type_name(source), crate::attr::type_name(attr)));
706
+ }
707
+ if crate::attr::optional(source) && !crate::attr::optional(attr) {
708
+ return Some(format!("{label}: optional door argument {source_name} feeds required {name}"));
709
+ }
710
+ }
711
+ for path in entity.get("identified_by").map(Json::each).unwrap_or(&[]) {
712
+ let path = path.to_s();
713
+ let head = path.split('.').next().unwrap_or("").to_string();
714
+ let source_name = source_name_for(&head);
715
+ if door_attrs.iter().any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a)) {
716
+ continue;
717
+ }
718
+ return Some(format!("{label}: the element's identity {head} has no source on the door"));
719
+ }
720
+ None
721
+ }
722
+
723
+ struct Delegation {
724
+ prelude: String,
725
+ // Split from `prelude` so the element identity is extracted after `dispatch`'s hydrate.
726
+ element: String,
727
+ apply: String,
728
+ emits: Vec<String>,
729
+ }
730
+
731
+ fn indent_block(text: &str, indent: &str) -> String {
732
+ text.lines().map(|l| format!("{indent}{l}")).collect::<Vec<_>>().join("\n").trim_end().to_string()
733
+ }
734
+
735
+ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, domain_name: &str) -> Option<Delegation> {
736
+ let delegation = delegate_of(command)?;
737
+ let (entity, target) = delegate_target(delegation, aggregate);
738
+ let (entity, target) = (entity.expect("delegate_skip_reason admitted the entity"), target.expect("delegate_skip_reason admitted the target"));
739
+ let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
740
+ let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
741
+ let list_attr = aggregate
742
+ .get("attributes")
743
+ .map(Json::each)
744
+ .unwrap_or(&[])
745
+ .iter()
746
+ .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity_name)
747
+ .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it"));
748
+
749
+ let element_record = naming::rust_ident(&entity_name);
750
+ let target_args_name = format!("{element_record}{}EntityArgs", naming::rust_ident(&target.get("name").map(Json::to_s).unwrap_or_default()));
751
+ let aliases: Vec<String> = delegate_mapping(delegation).iter().map(|(t, s)| format!("({}, {})", naming::ruby_inspect_string(t), naming::ruby_inspect_string(s))).collect();
752
+ let given_specs: Vec<String> = target
753
+ .get("givens")
754
+ .map(Json::each)
755
+ .unwrap_or(&[])
756
+ .iter()
757
+ .map(|g| {
758
+ let description = g.get("description").and_then(Json::as_str).unwrap_or("");
759
+ let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
760
+ format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
761
+ })
762
+ .collect();
763
+ let ensures_specs: Vec<String> = target
764
+ .get("ensures")
765
+ .map(Json::each)
766
+ .unwrap_or(&[])
767
+ .iter()
768
+ .map(|e| {
769
+ let description = e.get("description").and_then(Json::as_str).unwrap_or("");
770
+ let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
771
+ format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
772
+ })
773
+ .collect();
774
+ let transition = mutations::lifecycle_transition_for(target, entity);
775
+ let transition_arg = match &transition {
776
+ Some(t) => format!(
777
+ "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
778
+ naming::ruby_inspect_string(&t.field),
779
+ t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
780
+ ),
781
+ None => "None".to_string(),
782
+ };
783
+ let mut mutation_lines: Vec<String> = target
784
+ .get("mutations")
785
+ .map(Json::each)
786
+ .unwrap_or(&[])
787
+ .iter()
788
+ .map(|m| mutations::emit_mutation_line(exemplar, m, entity, target, value_objects_by_name, false))
789
+ .collect();
790
+ if mutations::reads_pre_state(target.get("mutations").map(Json::each).unwrap_or(&[])) {
791
+ mutation_lines.insert(0, mutations::pre_state_line());
792
+ }
793
+ if let Some(t) = &transition {
794
+ if !t.to_state.is_empty() {
795
+ mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
796
+ }
797
+ }
798
+ if mutation_lines.is_empty() {
799
+ mutation_lines = vec![" let _ = record;".to_string()];
800
+ }
801
+ let identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
802
+
803
+ let prelude = exemplar.render("delegate_prelude", &[("tmpl_aliases_placeholder()", aliases.join(", ")), ("TmplTargetArgs", target_args_name)]);
804
+ let element = exemplar.render("delegate_element", &[("TmplElement", element_record.clone())]);
805
+ let apply = exemplar.render(
806
+ "delegate_apply",
807
+ &[
808
+ ("TmplRecord", naming::rust_ident(&aggregate_name)),
809
+ ("tmpl_list_field", naming::rust_ident_field(crate::attr::name(list_attr))),
810
+ ("TmplElement", element_record),
811
+ // Bare, not entity-qualified: it feeds refusal-message text, as in Ruby.
812
+ ("\"TmplQualifiedCommandName\"", naming::ruby_inspect_string(&target.get("name").map(Json::to_s).unwrap_or_default())),
813
+ // Lets `apply_entity_command` (kernel/dispatch.rs) judge `corrects` admissibility.
814
+ ("\"TmplQualifiedName\"", naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}"))),
815
+ ("\"TmplAggregateName\"", naming::ruby_inspect_string(&aggregate_name)),
816
+ ("\"TmplEntityName\"", naming::ruby_inspect_string(&entity_name)),
817
+ ("\"TmplEntityIdentityReading\"", naming::ruby_inspect_string(&identity_reading)),
818
+ ("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
819
+ ("tmpl_transition_placeholder()", transition_arg),
820
+ ("tmpl_entity_mutation_lines_placeholder(record);", mutation_lines.join("\n")),
821
+ ("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
822
+ ],
823
+ );
824
+ Some(Delegation {
825
+ prelude: indent_block(&prelude, " "),
826
+ element: indent_block(&element, " "),
827
+ apply: indent_block(&apply, " "),
828
+ emits: target.get("emits").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect(),
829
+ })
830
+ }
831
+
832
+ /// `command_skip_reason` for an entity command; an entity has the same IR shape as an aggregate.
833
+ pub fn entity_command_skip_reason(command: &Json, entity: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
834
+ command_skip_reason_with(command, entity, value_objects_by_name, false)
835
+ }
836
+
837
+ /// Emits the dispatch function for an entity command.
838
+ pub fn emit_entity_command(
839
+ exemplar: &Exemplar,
840
+ command: &Json,
841
+ entity: &Json,
842
+ parent_aggregate: &Json,
843
+ domain_name: &str,
844
+ value_objects_by_name: &HashMap<String, &Json>,
845
+ aggregates_by_name: &HashMap<String, &Json>,
846
+ process_managers: &[Json],
847
+ ) -> String {
848
+ let parent_record = naming::rust_ident(parent_aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
849
+ let element_record = naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
850
+ let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
851
+
852
+ let aggregate_name = parent_aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
853
+ let parent_identity_reading = parent_aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
854
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("").to_string();
855
+ let entity_identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
856
+
857
+ let parent_attrs = parent_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
858
+ let list_attr = parent_attrs
859
+ .iter()
860
+ .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
861
+ .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
862
+ let list_field = naming::rust_ident_field(crate::attr::name(list_attr));
863
+ let args_struct_name = format!("{element_record}{cmd}EntityArgs");
864
+
865
+ let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
866
+ let mut args_struct = vec![format!("pub struct {args_struct_name} {{")];
867
+ for attr in attrs {
868
+ let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
869
+ if crate::attr::optional(attr) {
870
+ ty = format!("Option<{ty}>");
871
+ }
872
+ args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
873
+ }
874
+ args_struct.push("}".to_string());
875
+
876
+ let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
877
+
878
+ // `corrects_given_specs` is prepended as in `emit_command`; `apply_entity_command` evaluates
879
+ // it against the parent record, never the entity's element.
880
+ let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
881
+ let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
882
+ given_specs.extend(givens.iter().map(|g| {
883
+ let description = g.get("description").and_then(Json::as_str).unwrap_or("");
884
+ let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
885
+ format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
886
+ }));
887
+
888
+ let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
889
+ let ensures_specs: Vec<String> = ensures
890
+ .iter()
891
+ .map(|e| {
892
+ let description = e.get("description").and_then(Json::as_str).unwrap_or("");
893
+ let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
894
+ format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
895
+ })
896
+ .collect();
897
+
898
+ let transition = mutations::lifecycle_transition_for(command, entity);
899
+ let transition_arg = match &transition {
900
+ Some(t) => format!(
901
+ "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
902
+ naming::ruby_inspect_string(&t.field),
903
+ t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
904
+ ),
905
+ None => "None".to_string(),
906
+ };
907
+
908
+ let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
909
+ let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, entity, command, value_objects_by_name, false)).collect();
910
+ if mutations::reads_pre_state(mutations_list) {
911
+ mutation_lines.insert(0, mutations::pre_state_line());
912
+ }
913
+ if let Some(t) = &transition {
914
+ if !t.to_state.is_empty() {
915
+ mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
916
+ }
917
+ }
918
+ if mutation_lines.is_empty() {
919
+ mutation_lines = vec![" let _ = record;".to_string()];
920
+ }
921
+
922
+ // Bare, not entity-qualified, as in `delegate_apply`.
923
+ let qualified_command_name = command.get("name").and_then(Json::as_str).unwrap_or("").to_string();
924
+ let emits = command.get("emits").map(Json::each).unwrap_or(&[]);
925
+ let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
926
+
927
+ let entity_dispatch_fn = exemplar.render(
928
+ "entity_dispatch_fn",
929
+ &[
930
+ ("dispatch_entity_tmpl", format!("dispatch_entity_{}_{}", entity.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(), naming::dispatch_fn_name(&cmd))),
931
+ ("TmplRecord", parent_record),
932
+ ("TmplArgs", args_struct_name.clone()),
933
+ ("tmpl_deref_params_placeholder: ()", deref_params().join(", ")),
934
+ ("tmpl_invariant_check_placeholder()?;", invariant_checks.join("\n")),
935
+ ("let tmpl_eval_fielded = tmpl_with_references_placeholder();", with_references_binding()),
936
+ ("&tmpl_eval_fielded,", "&with_references,".to_string()),
937
+ ("let tmpl_seed_projections = tmpl_seed_projections_placeholder();", seed_projections_binding(parent_aggregate)),
938
+ ("tmpl_seed_projections,", "seed_projections,".to_string()),
939
+ ("tmpl_list_field", list_field),
940
+ ("TmplElement", element_record),
941
+ ("\"TmplQualifiedCommandName\"", naming::ruby_inspect_string(&qualified_command_name)),
942
+ ("\"TmplQualifiedName\"", naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}"))),
943
+ ("\"TmplAggregateName\"", naming::ruby_inspect_string(&aggregate_name)),
944
+ ("\"TmplParentIdentityReading\"", naming::ruby_inspect_string(&parent_identity_reading)),
945
+ ("\"TmplEntityName\"", naming::ruby_inspect_string(&entity_name)),
946
+ ("\"TmplEntityIdentityReading\"", naming::ruby_inspect_string(&entity_identity_reading)),
947
+ ("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
948
+ ("tmpl_transition_placeholder()", transition_arg),
949
+ ("tmpl_entity_mutation_lines_placeholder(record);", mutation_lines.join("\n")),
950
+ ("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
951
+ ("tmpl_invariants_placeholder()", format!("{}()", invariants_fn_name(parent_aggregate))),
952
+ ("tmpl_emit_placeholder()", emits_expr),
953
+ ],
954
+ );
955
+
956
+ [
957
+ crate::fielded::emit_fielded_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, &[]),
958
+ format!("#[derive(Debug, Clone)]\n{}", args_struct.join("\n")),
959
+ crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs, value_objects_by_name),
960
+ {
961
+ // `unknown_argument_allowlist:` as for an aggregate command, plus the entity's
962
+ // identity head (`extra_identity_heads:` in Ruby's `ArgumentGate`).
963
+ let entity_identity_heads: Vec<String> = entity
964
+ .get("identified_by")
965
+ .map(Json::each)
966
+ .unwrap_or(&[])
967
+ .iter()
968
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
969
+ .collect();
970
+ let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &entity_identity_heads);
971
+ crate::json_codec::emit_from_json_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, Some(&allowlist), Some(&qualified_command_name), true, true, Some(aggregates_by_name))
972
+ },
973
+ // Argument gates, run by `kernel::decode_entity_arguments` in `EntityStep::ORDER`.
974
+ {
975
+ let entity_identity_heads: Vec<String> = entity
976
+ .get("identified_by")
977
+ .map(Json::each)
978
+ .unwrap_or(&[])
979
+ .iter()
980
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
981
+ .collect();
982
+ let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &entity_identity_heads);
983
+ crate::json_codec::emit_argument_gates(&args_struct_name, &qualified_command_name, attrs, Some(&allowlist))
984
+ },
985
+ entity_dispatch_fn,
986
+ ]
987
+ .join("\n\n")
988
+ }
989
+
990
+ /// Emits the dispatch function for a command on an entity nested two levels deep.
991
+ /// Takes plain `&str` ids per hop, so routed and flat-args callers can both use it.
992
+ pub fn emit_nested_entity_command(
993
+ exemplar: &Exemplar,
994
+ command: &Json,
995
+ nested: &Json,
996
+ entity: &Json,
997
+ parent_aggregate: &Json,
998
+ domain_name: &str,
999
+ value_objects_by_name: &HashMap<String, &Json>,
1000
+ aggregates_by_name: &HashMap<String, &Json>,
1001
+ process_managers: &[Json],
1002
+ ) -> String {
1003
+ let parent_record = naming::rust_ident(parent_aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
1004
+ let entity_record = naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
1005
+ let nested_record = naming::rust_ident(nested.get("name").and_then(Json::as_str).unwrap_or(""));
1006
+ let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
1007
+
1008
+ let aggregate_name = parent_aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
1009
+ let parent_identity_reading = parent_aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1010
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("").to_string();
1011
+ let entity_identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1012
+ let nested_name = nested.get("name").and_then(Json::as_str).unwrap_or("").to_string();
1013
+ let nested_identity_reading = nested.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1014
+
1015
+ let parent_attrs = parent_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
1016
+ let list_attr1 = parent_attrs
1017
+ .iter()
1018
+ .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
1019
+ .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
1020
+ let list_field1 = naming::rust_ident_field(crate::attr::name(list_attr1));
1021
+
1022
+ let entity_attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
1023
+ let list_attr2 = entity_attrs
1024
+ .iter()
1025
+ .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == nested.get("name").and_then(Json::as_str).unwrap_or(""))
1026
+ .unwrap_or_else(|| panic!("{nested_name}: no list attribute on {entity_name} holds it — unsupported_attribute_types should have caught this"));
1027
+ let list_field2 = naming::rust_ident_field(crate::attr::name(list_attr2));
1028
+
1029
+ let args_struct_name = format!("{nested_record}{cmd}NestedEntityArgs");
1030
+
1031
+ let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
1032
+ let mut args_struct = vec![format!("pub struct {args_struct_name} {{")];
1033
+ for attr in attrs {
1034
+ let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
1035
+ if crate::attr::optional(attr) {
1036
+ ty = format!("Option<{ty}>");
1037
+ }
1038
+ args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
1039
+ }
1040
+ args_struct.push("}".to_string());
1041
+
1042
+ let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
1043
+
1044
+ let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
1045
+ let given_specs: Vec<String> = givens
1046
+ .iter()
1047
+ .map(|g| {
1048
+ let description = g.get("description").and_then(Json::as_str).unwrap_or("");
1049
+ let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
1050
+ format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
1051
+ })
1052
+ .collect();
1053
+
1054
+ let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
1055
+ let ensures_specs: Vec<String> = ensures
1056
+ .iter()
1057
+ .map(|e| {
1058
+ let description = e.get("description").and_then(Json::as_str).unwrap_or("");
1059
+ let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
1060
+ format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
1061
+ })
1062
+ .collect();
1063
+
1064
+ // The nested entity's lifecycle, as in `emit_entity_command`.
1065
+ let transition = mutations::lifecycle_transition_for(command, nested);
1066
+ let transition_arg = match &transition {
1067
+ Some(t) => format!(
1068
+ "Some(crate::kernel::TransitionCheck {{ field: {}, from_states: &[{}] }})",
1069
+ naming::ruby_inspect_string(&t.field),
1070
+ t.from_states.iter().map(|s| naming::ruby_inspect_string(s)).collect::<Vec<_>>().join(", ")
1071
+ ),
1072
+ None => "None".to_string(),
1073
+ };
1074
+
1075
+ let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
1076
+ let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, nested, command, value_objects_by_name, false)).collect();
1077
+ if mutations::reads_pre_state(mutations_list) {
1078
+ mutation_lines.insert(0, mutations::pre_state_line());
1079
+ }
1080
+ if let Some(t) = &transition {
1081
+ if !t.to_state.is_empty() {
1082
+ mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
1083
+ }
1084
+ }
1085
+ if mutation_lines.is_empty() {
1086
+ mutation_lines = vec![" let _ = record;".to_string()];
1087
+ }
1088
+
1089
+ // Bare, as in `emit_entity_command`.
1090
+ let qualified_command_name = command.get("name").and_then(Json::as_str).unwrap_or("").to_string();
1091
+ // The hop-2 `apply_entity_command` gets the root aggregate's qualified name only to satisfy
1092
+ // the signature: its `record` is the hop-1 entity, so `corrects` admissibility is not checked
1093
+ // there. Its trailing `parent_in_args` literal is `true`, byte for byte as Ruby emits.
1094
+ let emits = command.get("emits").map(Json::each).unwrap_or(&[]);
1095
+ let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
1096
+ let fn_name = format!(
1097
+ "dispatch_entity_{}_{}_{}",
1098
+ entity.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(),
1099
+ nested.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(),
1100
+ naming::dispatch_fn_name(&cmd)
1101
+ );
1102
+
1103
+ let nested_dispatch_fn = format!(
1104
+ "pub fn {fn_name}(\n \
1105
+ repo: &mut impl crate::kernel::Repository<{parent_record}>, parent_id: &str, hop1_id: &str, hop1_wants: &str,\n \
1106
+ hop2_id: &str, hop2_wants: &str, args: {args_struct_name}, mutations: &mut Vec<crate::kernel::MutationRecord>,\n \
1107
+ owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>, command_deref: Vec<(&'static str, crate::kernel::DerefNode)>,\n\
1108
+ ) -> crate::kernel::DispatchResult<{parent_record}> {{\n\
1109
+ {invariant_checks}\n \
1110
+ {with_references}\n \
1111
+ {seed_projections}\n\n \
1112
+ crate::kernel::dispatch_entity(\n \
1113
+ repo,\n \
1114
+ parent_id,\n \
1115
+ |r: &{parent_record}| &r.{list_field1},\n \
1116
+ |r: &mut {parent_record}| &mut r.{list_field1},\n \
1117
+ |el: &{entity_record}| el.identity() == hop1_id,\n \
1118
+ {qualified_command_name_inspect},\n \
1119
+ {qualified_name_inspect},\n \
1120
+ {aggregate_name_inspect},\n \
1121
+ {parent_identity_reading_inspect},\n \
1122
+ {entity_name_inspect},\n \
1123
+ {entity_identity_reading_inspect},\n \
1124
+ hop1_wants,\n \
1125
+ &with_references,\n \
1126
+ &[],\n \
1127
+ None,\n \
1128
+ |nested_owner: &mut {entity_record}| {{\n \
1129
+ crate::kernel::apply_entity_command(\n \
1130
+ nested_owner,\n \
1131
+ hop1_id,\n \
1132
+ |r: &{entity_record}| &r.{list_field2},\n \
1133
+ |r: &mut {entity_record}| &mut r.{list_field2},\n \
1134
+ |el: &{nested_record}| el.identity() == hop2_id,\n \
1135
+ {qualified_command_name_inspect},\n \
1136
+ {qualified_name_inspect},\n \
1137
+ {aggregate_name_inspect},\n \
1138
+ {nested_name_inspect},\n \
1139
+ {nested_identity_reading_inspect},\n \
1140
+ hop2_wants,\n \
1141
+ &with_references,\n \
1142
+ &[\n{given_specs}\n \
1143
+ ],\n \
1144
+ {transition_arg},\n \
1145
+ |record| {{\n{mutation_lines}\n \
1146
+ Ok(())\n \
1147
+ }},\n \
1148
+ &[\n{ensures_specs}\n \
1149
+ ],\n \
1150
+ true,\n \
1151
+ )\n \
1152
+ }},\n \
1153
+ &[],\n \
1154
+ &{invariants_fn}(),\n \
1155
+ &[{emits_expr}],\n \
1156
+ args.to_json(),\n \
1157
+ mutations,\n \
1158
+ seed_projections,\n \
1159
+ )\n\
1160
+ }}\n",
1161
+ invariant_checks = invariant_checks.join("\n"),
1162
+ with_references = with_references_binding(),
1163
+ seed_projections = seed_projections_binding(parent_aggregate),
1164
+ qualified_command_name_inspect = naming::ruby_inspect_string(&qualified_command_name),
1165
+ qualified_name_inspect = naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}")),
1166
+ aggregate_name_inspect = naming::ruby_inspect_string(&aggregate_name),
1167
+ parent_identity_reading_inspect = naming::ruby_inspect_string(&parent_identity_reading),
1168
+ entity_name_inspect = naming::ruby_inspect_string(&entity_name),
1169
+ entity_identity_reading_inspect = naming::ruby_inspect_string(&entity_identity_reading),
1170
+ nested_name_inspect = naming::ruby_inspect_string(&nested_name),
1171
+ nested_identity_reading_inspect = naming::ruby_inspect_string(&nested_identity_reading),
1172
+ given_specs = given_specs.join("\n"),
1173
+ ensures_specs = ensures_specs.join("\n"),
1174
+ mutation_lines = mutation_lines.join("\n"),
1175
+ invariants_fn = invariants_fn_name(parent_aggregate),
1176
+ emits_expr = emits_expr,
1177
+ );
1178
+
1179
+ [
1180
+ crate::fielded::emit_fielded_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, &[]),
1181
+ format!("#[derive(Debug, Clone)]\n{}", args_struct.join("\n")),
1182
+ crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs, value_objects_by_name),
1183
+ {
1184
+ // Both hops' identity heads, as `ctx.chain.flat_map(&:identity_heads)` in Ruby.
1185
+ let mut identity_heads: Vec<String> = entity
1186
+ .get("identified_by")
1187
+ .map(Json::each)
1188
+ .unwrap_or(&[])
1189
+ .iter()
1190
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
1191
+ .collect();
1192
+ identity_heads.extend(
1193
+ nested
1194
+ .get("identified_by")
1195
+ .map(Json::each)
1196
+ .unwrap_or(&[])
1197
+ .iter()
1198
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
1199
+ );
1200
+ let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &identity_heads);
1201
+ crate::json_codec::emit_from_json_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, Some(&allowlist), Some(&qualified_command_name), true, true, Some(aggregates_by_name))
1202
+ },
1203
+ // Argument gates, as in the one-hop entity command.
1204
+ {
1205
+ let mut identity_heads: Vec<String> = entity
1206
+ .get("identified_by")
1207
+ .map(Json::each)
1208
+ .unwrap_or(&[])
1209
+ .iter()
1210
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
1211
+ .collect();
1212
+ identity_heads.extend(
1213
+ nested
1214
+ .get("identified_by")
1215
+ .map(Json::each)
1216
+ .unwrap_or(&[])
1217
+ .iter()
1218
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
1219
+ );
1220
+ let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &identity_heads);
1221
+ crate::json_codec::emit_argument_gates(&args_struct_name, &qualified_command_name, attrs, Some(&allowlist))
1222
+ },
1223
+ nested_dispatch_fn,
1224
+ ]
1225
+ .join("\n\n")
1226
+ }