hecks 2.9.0 → 3.0.1

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 +13 -4
  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 +15 -8
  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 +15 -5
  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,1288 @@
1
+ //! Port of `rust/project/domain_generator.rb`'s `DomainGenerator.call`: the per-aggregate
2
+ //! `.rs` files plus `registry.rs`. `metadata.rs` and `ir.json` are not emitted here.
3
+
4
+ use crate::exemplar::Exemplar;
5
+ use crate::json::Json;
6
+ use crate::manifest::{ruby_inspect, Manifest};
7
+ use crate::registry::{
8
+ AggregateEntry, CommandEntry, EntityCommandEntry, EntityIdentityEntry, NestedEntityCommandEntry, PortEntry,
9
+ ReferenceCheck, TenantBoundaryCheck,
10
+ };
11
+ use crate::{commands, json_codec, mutations, ports, queries, reactions, read_models, types};
12
+ use std::collections::HashMap;
13
+
14
+ fn puts_str(out: &mut String, s: &str) {
15
+ out.push_str(s);
16
+ if !s.ends_with('\n') {
17
+ out.push('\n');
18
+ }
19
+ }
20
+
21
+ fn puts_blank(out: &mut String) {
22
+ out.push('\n');
23
+ }
24
+
25
+ fn lifecycle_extra_field(node: &Json) -> Vec<(String, String)> {
26
+ match node.get("lifecycle") {
27
+ None => vec![],
28
+ Some(lifecycle) => {
29
+ let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("");
30
+ let ident = crate::naming::rust_ident_field(field);
31
+ vec![(
32
+ crate::naming::rust_field(field),
33
+ format!("crate::kernel::Json::Str(self.{ident}.clone())"),
34
+ )]
35
+ }
36
+ }
37
+ }
38
+
39
+ pub struct GeneratedFile {
40
+ pub name: String,
41
+ pub content: String,
42
+ }
43
+
44
+ pub struct GeneratedDomain {
45
+ /// One entry per not-skipped aggregate: `<name_downcase>.rs`.
46
+ pub aggregate_files: Vec<GeneratedFile>,
47
+ pub registry_rs: String,
48
+ pub mod_rs: String,
49
+ /// Returned so a multi-chapter caller can union aggregates, queries and read models.
50
+ pub registry_aggregates: Vec<AggregateEntry>,
51
+ pub query_defs: Vec<crate::queries::QueryDef>,
52
+ pub read_model_defs: Vec<crate::read_models::ReadModelDef>,
53
+ /// The exact `manifest.json` bytes.
54
+ pub manifest_json: String,
55
+ }
56
+
57
+ fn node_name(node: &Json) -> &str {
58
+ node.get("name").and_then(Json::as_str).unwrap_or("")
59
+ }
60
+
61
+ fn reference_checks(
62
+ command: &Json,
63
+ aggregates_by_name: &HashMap<String, &Json>,
64
+ unsupported_names: &[String],
65
+ ) -> Vec<ReferenceCheck> {
66
+ let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
67
+ attrs
68
+ .iter()
69
+ .filter_map(|attr| {
70
+ let target_name = crate::naming::reference_target(crate::attr::type_name(attr))?;
71
+ let target = aggregates_by_name.get(target_name)?;
72
+ if unsupported_names.iter().any(|n| n == target_name) {
73
+ return None;
74
+ }
75
+ let identified_by = target.get("identified_by").map(Json::each).unwrap_or(&[]);
76
+ let heads = identified_by
77
+ .iter()
78
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
79
+ .collect::<Vec<_>>()
80
+ .join(", ");
81
+ Some(ReferenceCheck {
82
+ field: crate::attr::name(attr).to_string(),
83
+ optional: crate::attr::optional(attr),
84
+ list_item: None,
85
+ target_mod: target
86
+ .get("name")
87
+ .and_then(Json::as_str)
88
+ .unwrap_or("")
89
+ .to_lowercase(),
90
+ target_name: target
91
+ .get("name")
92
+ .and_then(Json::as_str)
93
+ .unwrap_or("")
94
+ .to_string(),
95
+ heads,
96
+ })
97
+ })
98
+ .collect()
99
+ }
100
+
101
+ /// Reference checks for a command that sets an aggregate `Reference<X>` field from a plain
102
+ /// value-object argument (`sets :member`), which `reference_checks` cannot see.
103
+ ///
104
+ /// Sources already typed `Reference<X>` are skipped; `reference_checks` covers them.
105
+ fn state_reference_checks(
106
+ aggregate: &Json,
107
+ command: &Json,
108
+ aggregates_by_name: &HashMap<String, &Json>,
109
+ unsupported_names: &[String],
110
+ value_objects_by_name: &HashMap<String, &Json>,
111
+ ) -> Vec<ReferenceCheck> {
112
+ let agg_attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
113
+ let cmd_mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
114
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
115
+
116
+ agg_attrs
117
+ .iter()
118
+ .filter_map(|attr| {
119
+ let target_name = crate::naming::reference_target(crate::attr::type_name(attr))?;
120
+ let attr_name = crate::attr::name(attr);
121
+
122
+ let mutation = cmd_mutations.iter().find(|m| {
123
+ m.get("op").map(Json::to_s).as_deref() == Some("set")
124
+ && m.get("target").map(Json::to_s).as_deref() == Some(attr_name)
125
+ && m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).as_deref() == Some("argument")
126
+ })?;
127
+
128
+ let source_name = mutation.get("source").and_then(|s| s.get("name")).map(Json::to_s).unwrap_or_default();
129
+ let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name)?;
130
+ if crate::naming::reference_target(crate::attr::type_name(source_attr)).is_some() {
131
+ return None;
132
+ }
133
+
134
+ // A `has_many` field gets one `check_reference` per element.
135
+ let (accessor, list_item) = if crate::attr::list(attr) {
136
+ let item = list_reference_check_item(source_attr, value_objects_by_name)?;
137
+ (crate::attr::name(source_attr).to_string(), Some(item))
138
+ } else {
139
+ (
140
+ state_reference_check_accessor(source_attr, value_objects_by_name)?,
141
+ None,
142
+ )
143
+ };
144
+
145
+ let target = aggregates_by_name.get(target_name)?;
146
+ if unsupported_names.iter().any(|n| n == target_name) {
147
+ return None;
148
+ }
149
+
150
+ let identified_by = target.get("identified_by").map(Json::each).unwrap_or(&[]);
151
+ let heads = identified_by
152
+ .iter()
153
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
154
+ .collect::<Vec<_>>()
155
+ .join(", ");
156
+
157
+ Some(ReferenceCheck {
158
+ field: accessor,
159
+ optional: crate::attr::optional(source_attr),
160
+ list_item,
161
+ target_mod: target
162
+ .get("name")
163
+ .and_then(Json::as_str)
164
+ .unwrap_or("")
165
+ .to_lowercase(),
166
+ target_name: target
167
+ .get("name")
168
+ .and_then(Json::as_str)
169
+ .unwrap_or("")
170
+ .to_string(),
171
+ heads,
172
+ })
173
+ })
174
+ .collect()
175
+ }
176
+
177
+ /// The tenant field read off the first query that declares `authorize ..., tenant:`.
178
+ fn tenant_field_for(aggregate: &Json) -> Option<String> {
179
+ aggregate
180
+ .get("queries")
181
+ .map(Json::each)
182
+ .unwrap_or(&[])
183
+ .iter()
184
+ .find_map(|q| q.get("authorization").and_then(|a| a.get("tenant")).map(Json::to_s))
185
+ }
186
+
187
+ /// Write-side tenant boundary checks for a command's references.
188
+ ///
189
+ /// Only emitted when both aggregates declare a tenant field and both tenant attributes have
190
+ /// `state_reference_check_accessor`'s shape; any other shape is left unchecked.
191
+ fn tenant_boundary_checks(
192
+ aggregate: &Json,
193
+ command: &Json,
194
+ aggregates_by_name: &HashMap<String, &Json>,
195
+ unsupported_names: &[String],
196
+ value_objects_by_name: &HashMap<String, &Json>,
197
+ ) -> Vec<TenantBoundaryCheck> {
198
+ let Some(own_tenant_field) = tenant_field_for(aggregate) else {
199
+ return Vec::new();
200
+ };
201
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
202
+ let Some(own_tenant_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == own_tenant_field) else {
203
+ return Vec::new();
204
+ };
205
+ let Some(own_accessor) = state_reference_check_accessor(own_tenant_attr, value_objects_by_name) else {
206
+ return Vec::new();
207
+ };
208
+ let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
209
+
210
+ cmd_attrs
211
+ .iter()
212
+ .filter_map(|attr| {
213
+ let target_name = crate::naming::reference_target(crate::attr::type_name(attr))?;
214
+ let target = aggregates_by_name.get(target_name)?;
215
+ if unsupported_names.iter().any(|n| n == target_name) {
216
+ return None;
217
+ }
218
+ let target_tenant_field = tenant_field_for(target)?;
219
+ let target_attrs = target.get("attributes").map(Json::each).unwrap_or(&[]);
220
+ let target_tenant_attr = target_attrs
221
+ .iter()
222
+ .find(|a| crate::attr::name(a) == target_tenant_field)?;
223
+ let target_accessor = state_reference_check_accessor(target_tenant_attr, value_objects_by_name)?;
224
+ let target_decl_name = target.get("name").and_then(Json::as_str).unwrap_or("").to_string();
225
+
226
+ Some(TenantBoundaryCheck {
227
+ reference_field: crate::attr::name(attr).to_string(),
228
+ target_mod: target_decl_name.to_lowercase(),
229
+ target_name: target_decl_name,
230
+ aggregate_name: aggregate_name.clone(),
231
+ own_tenant_field: own_tenant_field.clone(),
232
+ own_accessor: own_accessor.clone(),
233
+ target_tenant_field,
234
+ target_accessor,
235
+ })
236
+ })
237
+ .collect()
238
+ }
239
+
240
+ /// Per-element key expression for a list argument: `item` for `list_of(String)`,
241
+ /// `&item.<field>` for single-String-attribute value objects, `None` otherwise.
242
+ fn list_reference_check_item(
243
+ source_attr: &Json,
244
+ value_objects_by_name: &HashMap<String, &Json>,
245
+ ) -> Option<String> {
246
+ if !crate::attr::list(source_attr) {
247
+ return None;
248
+ }
249
+ let type_name = crate::attr::type_name(source_attr);
250
+ if type_name == "String" {
251
+ return Some("item".to_string());
252
+ }
253
+ let vo = value_objects_by_name.get(type_name)?;
254
+ let vo_attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
255
+ if vo_attrs.len() != 1 || crate::attr::type_name(&vo_attrs[0]) != "String" {
256
+ return None;
257
+ }
258
+ Some(format!(
259
+ "&item.{}",
260
+ crate::naming::rust_ident_field(crate::attr::name(&vo_attrs[0]))
261
+ ))
262
+ }
263
+
264
+ /// Accessor path for the value a reference check reads off a source argument.
265
+ ///
266
+ /// `None` for a multi-attribute value object, or an optional one: `check_reference`'s
267
+ /// optional template assumes `Option<String>`.
268
+ fn state_reference_check_accessor(source_attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
269
+ let name = crate::attr::name(source_attr);
270
+ if crate::naming::effective_scalar_type(crate::attr::type_name(source_attr)).is_some() {
271
+ return Some(name.to_string());
272
+ }
273
+
274
+ let vo = value_objects_by_name.get(crate::attr::type_name(source_attr))?;
275
+ if crate::attr::optional(source_attr) {
276
+ return None;
277
+ }
278
+ let vo_attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
279
+ if vo_attrs.len() != 1 {
280
+ return None;
281
+ }
282
+
283
+ Some(format!("{name}.{}", crate::naming::rust_ident_field(crate::attr::name(&vo_attrs[0]))))
284
+ }
285
+
286
+ pub fn generate(
287
+ exemplar: &Exemplar,
288
+ ir: &Json,
289
+ source_label: &str,
290
+ mod_name: &str,
291
+ ) -> GeneratedDomain {
292
+ let domain_name = ir.get("name").and_then(Json::as_str).unwrap_or("");
293
+ let all_aggregates = ir.get("aggregates").map(Json::each).unwrap_or(&[]);
294
+ let aggregates_by_name: HashMap<String, &Json> = all_aggregates
295
+ .iter()
296
+ .map(|a| {
297
+ (
298
+ a.get("name")
299
+ .and_then(Json::as_str)
300
+ .unwrap_or("")
301
+ .to_string(),
302
+ a,
303
+ )
304
+ })
305
+ .collect();
306
+
307
+ let unsupported_names: Vec<String> = all_aggregates
308
+ .iter()
309
+ .filter(|a| {
310
+ let vos = a.get("value_objects").map(Json::each).unwrap_or(&[]);
311
+ let vo_by_name: HashMap<String, &Json> = vos
312
+ .iter()
313
+ .map(|vo| {
314
+ (
315
+ vo.get("name")
316
+ .and_then(Json::as_str)
317
+ .unwrap_or("")
318
+ .to_string(),
319
+ vo,
320
+ )
321
+ })
322
+ .collect();
323
+ !types::unsupported_attribute_types(a, &vo_by_name).is_empty()
324
+ })
325
+ .map(|a| {
326
+ a.get("name")
327
+ .and_then(Json::as_str)
328
+ .unwrap_or("")
329
+ .to_string()
330
+ })
331
+ .collect();
332
+
333
+ let mut aggregate_files: Vec<GeneratedFile> = Vec::new();
334
+ let mut registry_aggregates: Vec<AggregateEntry> = Vec::new();
335
+ let mut manifest = Manifest::default();
336
+ let process_managers: Vec<Json> = ir
337
+ .get("process_managers")
338
+ .map(Json::each)
339
+ .unwrap_or(&[])
340
+ .to_vec();
341
+
342
+ // Owner of each value object across the whole domain: a command attribute can name a value
343
+ // object declared by another aggregate. Last writer wins per name.
344
+ let mut domain_value_object_owner: HashMap<String, String> = HashMap::new();
345
+ for a in all_aggregates {
346
+ let owner = a.get("name").and_then(Json::as_str).unwrap_or("").to_string();
347
+ for vo in a.get("value_objects").map(Json::each).unwrap_or(&[]) {
348
+ domain_value_object_owner.insert(
349
+ vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(),
350
+ owner.clone(),
351
+ );
352
+ }
353
+ }
354
+ // The value objects themselves, domain-wide; `bridging.rs` needs the definition to bridge
355
+ // a cross-aggregate argument. Each aggregate's own map wins a name collision.
356
+ let mut domain_value_objects_by_name: HashMap<String, &Json> = HashMap::new();
357
+ for a in all_aggregates {
358
+ for vo in a.get("value_objects").map(Json::each).unwrap_or(&[]) {
359
+ domain_value_objects_by_name.insert(vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo);
360
+ }
361
+ }
362
+
363
+ for aggregate in all_aggregates {
364
+ let agg_name = aggregate.get("name").and_then(Json::as_str).unwrap_or("");
365
+
366
+ let value_objects = aggregate
367
+ .get("value_objects")
368
+ .map(Json::each)
369
+ .unwrap_or(&[]);
370
+ let mut value_objects_by_name: HashMap<String, &Json> = domain_value_objects_by_name.clone();
371
+ value_objects_by_name.extend(value_objects.iter().map(|vo| {
372
+ (
373
+ vo.get("name")
374
+ .and_then(Json::as_str)
375
+ .unwrap_or("")
376
+ .to_string(),
377
+ vo,
378
+ )
379
+ }));
380
+
381
+ // A skipped aggregate cascades into the manifest: everything it owns gets an entry
382
+ // tracing back to the same root cause.
383
+ let unsupported = types::unsupported_attribute_types(aggregate, &value_objects_by_name);
384
+ if !unsupported.is_empty() {
385
+ let aggregate_reason = format!(
386
+ "attribute type(s) {} not generated yet (a bare, non-list entity-typed attribute isn't resolved to a Rust type)",
387
+ unsupported.join(", ")
388
+ );
389
+ manifest.skipped_as("aggregate", format!("{domain_name}::{agg_name}"), "attribute_type", aggregate_reason.clone());
390
+ let cascade = format!("owning aggregate not generated: {aggregate_reason}");
391
+ for command in aggregate.get("commands").map(Json::each).unwrap_or(&[]) {
392
+ manifest.skipped_as("command", format!("{domain_name}::{agg_name}.{}", node_name(command)), "owning_aggregate", cascade.clone());
393
+ }
394
+ for entity in aggregate.get("entities").map(Json::each).unwrap_or(&[]) {
395
+ let entity_verb = format!("{domain_name}::{agg_name}.{}", node_name(entity));
396
+ manifest.skipped_as("entity", entity_verb.clone(), "owning_aggregate", cascade.clone());
397
+ for command in entity.get("commands").map(Json::each).unwrap_or(&[]) {
398
+ manifest.skipped_as("entity_command", format!("{entity_verb}.{}", node_name(command)), "owning_aggregate", cascade.clone());
399
+ }
400
+ }
401
+ for port in aggregate.get("ports").map(Json::each).unwrap_or(&[]) {
402
+ for operation in port.get("operations").map(Json::each).unwrap_or(&[]) {
403
+ manifest.skipped_as(
404
+ "port_operation",
405
+ format!("{domain_name}::{agg_name}.{}.{}", node_name(port), node_name(operation)),
406
+ "owning_aggregate",
407
+ cascade.clone(),
408
+ );
409
+ }
410
+ }
411
+ continue;
412
+ }
413
+ manifest.generated("aggregate", format!("{domain_name}::{agg_name}"));
414
+
415
+ let record_name = crate::naming::rust_ident(agg_name);
416
+ let can_route = json_codec::extract_id_supported(aggregate);
417
+
418
+ let mut out = String::new();
419
+ puts_str(
420
+ &mut out,
421
+ &format!("// GENERATED by hecks project_rust from {source_label}'s canonical IR."),
422
+ );
423
+ puts_str(
424
+ &mut out,
425
+ "// Do not hand-edit — re-run hecks project_rust instead.",
426
+ );
427
+ puts_str(&mut out, "#![allow(dead_code, unused_variables)]");
428
+ puts_str(&mut out, "use crate::kernel::Expr;");
429
+ for line in crate::bridging::cross_aggregate_vo_imports(aggregate, &domain_value_object_owner, mod_name) {
430
+ puts_str(&mut out, &line);
431
+ }
432
+ puts_blank(&mut out);
433
+
434
+ for vo in value_objects {
435
+ puts_str(
436
+ &mut out,
437
+ &types::emit_value_object(
438
+ exemplar,
439
+ vo,
440
+ &value_objects_by_name,
441
+ &aggregates_by_name,
442
+ ),
443
+ );
444
+ puts_blank(&mut out);
445
+ let closed_set = vo.get("closed_set").map(Json::as_bool).unwrap_or(false);
446
+ let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
447
+ if closed_set && attrs.len() == 1 {
448
+ puts_str(&mut out, &json_codec::emit_closed_set_codec(exemplar, vo));
449
+ } else if closed_set {
450
+ puts_str(
451
+ &mut out,
452
+ &json_codec::emit_closed_set_table_codec(exemplar, vo),
453
+ );
454
+ } else {
455
+ let name =
456
+ crate::naming::rust_ident(vo.get("name").and_then(Json::as_str).unwrap_or(""));
457
+ puts_str(
458
+ &mut out,
459
+ &json_codec::emit_to_json_flat(exemplar, &name, attrs, &value_objects_by_name, false, &[], None),
460
+ );
461
+ puts_blank(&mut out);
462
+ // `Some(&[])` refuses unknown keys: without it a mistyped nested field (`value`
463
+ // for a declared `count`) silently falls through to the field's `default:`.
464
+ let empty_allowlist: [String; 0] = [];
465
+ puts_str(
466
+ &mut out,
467
+ &json_codec::emit_from_json_flat(
468
+ exemplar,
469
+ &name,
470
+ attrs,
471
+ &value_objects_by_name,
472
+ Some(&empty_allowlist),
473
+ None,
474
+ false,
475
+ false,
476
+ None,
477
+ ),
478
+ );
479
+ }
480
+ puts_blank(&mut out);
481
+ }
482
+
483
+ let mut entity_commands: Vec<EntityCommandEntry> = Vec::new();
484
+ let mut nested_entity_commands: Vec<NestedEntityCommandEntry> = Vec::new();
485
+ // Every entity this aggregate declares, routable or not: `reactions.rs` needs its
486
+ // identity shape, not its command routability.
487
+ let mut registry_entities: Vec<EntityIdentityEntry> = Vec::new();
488
+
489
+ for entity in aggregate.get("entities").map(Json::each).unwrap_or(&[]) {
490
+ let entity_verb = format!("{domain_name}::{agg_name}.{}", node_name(entity));
491
+ manifest.generated("entity", entity_verb.clone());
492
+ registry_entities.push(EntityIdentityEntry {
493
+ name: entity.get("name").and_then(Json::as_str).unwrap_or("").to_string(),
494
+ identified_by: entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect(),
495
+ });
496
+ puts_str(
497
+ &mut out,
498
+ &types::emit_entity(exemplar, entity, &value_objects_by_name),
499
+ );
500
+ puts_blank(&mut out);
501
+ let entity_name_ident =
502
+ crate::naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
503
+ let entity_attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
504
+ let extra = lifecycle_extra_field(entity);
505
+ puts_str(
506
+ &mut out,
507
+ &json_codec::emit_to_json_flat(
508
+ exemplar,
509
+ &entity_name_ident,
510
+ entity_attrs,
511
+ &value_objects_by_name,
512
+ false,
513
+ &extra,
514
+ None,
515
+ ),
516
+ );
517
+ puts_blank(&mut out);
518
+ puts_str(
519
+ &mut out,
520
+ &json_codec::emit_from_json_state(
521
+ exemplar,
522
+ &entity_name_ident,
523
+ entity_attrs,
524
+ &value_objects_by_name,
525
+ false,
526
+ &extra,
527
+ None,
528
+ ),
529
+ );
530
+ puts_blank(&mut out);
531
+
532
+ // Entities nested one level deeper get a struct, JSON codec and routed commands
533
+ // (`to: { aggregate:, entities: [...] }`). Not generalized past two levels.
534
+ //
535
+ // `entity_can_route` gates `extract_id`/`extract_wants` for `entity`; flat-args
536
+ // dispatch at depth 2 additionally needs the same check on `nested`.
537
+ let entity_can_route = json_codec::extract_id_supported(entity);
538
+
539
+ for nested in entity.get("entities").map(Json::each).unwrap_or(&[]) {
540
+ let nested_verb = format!("{entity_verb}.{}", node_name(nested));
541
+ manifest.generated("entity", nested_verb.clone());
542
+ puts_str(
543
+ &mut out,
544
+ &types::emit_entity(exemplar, nested, &value_objects_by_name),
545
+ );
546
+ puts_blank(&mut out);
547
+ let nested_name_ident = crate::naming::rust_ident(
548
+ nested.get("name").and_then(Json::as_str).unwrap_or(""),
549
+ );
550
+ let nested_attrs = nested.get("attributes").map(Json::each).unwrap_or(&[]);
551
+ let nested_extra = lifecycle_extra_field(nested);
552
+ puts_str(
553
+ &mut out,
554
+ &json_codec::emit_to_json_flat(
555
+ exemplar,
556
+ &nested_name_ident,
557
+ nested_attrs,
558
+ &value_objects_by_name,
559
+ false,
560
+ &nested_extra,
561
+ None,
562
+ ),
563
+ );
564
+ puts_blank(&mut out);
565
+ puts_str(
566
+ &mut out,
567
+ &json_codec::emit_from_json_state(
568
+ exemplar,
569
+ &nested_name_ident,
570
+ nested_attrs,
571
+ &value_objects_by_name,
572
+ false,
573
+ &nested_extra,
574
+ None,
575
+ ),
576
+ );
577
+ puts_blank(&mut out);
578
+ // `identity()` is what a routed dispatch matches a doubly-nested element by.
579
+ puts_str(&mut out, &json_codec::emit_self_identity(exemplar, nested));
580
+ puts_blank(&mut out);
581
+
582
+ // Flat-args addressing resolves `hop1_id` off `entity`'s own `extract_id` too, so
583
+ // both hops must support it.
584
+ let nested_can_route = json_codec::extract_id_supported(nested);
585
+ let unrouted_supported = entity_can_route && nested_can_route;
586
+ if unrouted_supported {
587
+ puts_str(&mut out, &json_codec::emit_extract_id(exemplar, nested));
588
+ puts_blank(&mut out);
589
+ puts_str(&mut out, &json_codec::emit_extract_id_lenient(exemplar, nested));
590
+ puts_blank(&mut out);
591
+ puts_str(&mut out, &json_codec::emit_extract_wants(exemplar, nested));
592
+ puts_blank(&mut out);
593
+ }
594
+
595
+ let nested_identified_by = nested.get("identified_by").map(Json::each).unwrap_or(&[]);
596
+ for command in nested.get("commands").map(Json::each).unwrap_or(&[]) {
597
+ let nested_command_verb = format!("{nested_verb}.{}", node_name(command));
598
+ if let Some(reason) = commands::entity_command_skip_reason(command, nested, &value_objects_by_name) {
599
+ manifest.skipped("entity_command", nested_command_verb, reason);
600
+ continue;
601
+ }
602
+
603
+ puts_str(
604
+ &mut out,
605
+ &commands::emit_nested_entity_command(
606
+ exemplar,
607
+ command,
608
+ nested,
609
+ entity,
610
+ aggregate,
611
+ domain_name,
612
+ &value_objects_by_name,
613
+ &aggregates_by_name,
614
+ &process_managers,
615
+ ),
616
+ );
617
+ puts_blank(&mut out);
618
+
619
+ manifest.record(
620
+ "entity_command",
621
+ nested_command_verb,
622
+ true,
623
+ Some(true),
624
+ None,
625
+ None,
626
+ Some(if unrouted_supported {
627
+ "routed (`to: { entities: [...] }`) and flat-args (one identity head per hop) both supported (BUG#19)".to_string()
628
+ } else {
629
+ "routed (`to: { entities: [...] }`) only — no legacy/flat-argument fallback at this depth (identity shape isn't extract_id-supported at one or both hops; BUG#19's own gate)".to_string()
630
+ }),
631
+ );
632
+
633
+ let nested_command_name = command.get("name").and_then(Json::as_str).unwrap_or("");
634
+ let entity_name_str = entity.get("name").and_then(Json::as_str).unwrap_or("");
635
+ let nested_name_str = nested.get("name").and_then(Json::as_str).unwrap_or("");
636
+ nested_entity_commands.push(NestedEntityCommandEntry {
637
+ verb: format!("{domain_name}::{agg_name}.{entity_name_str}.{nested_name_str}.{nested_command_name}"),
638
+ name: nested_command_name.to_string(),
639
+ entity_record: entity_name_ident.clone(),
640
+ nested_record: nested_name_ident.clone(),
641
+ fn_name: format!(
642
+ "{}_{}_{}",
643
+ entity_name_str.to_lowercase(),
644
+ nested_name_str.to_lowercase(),
645
+ crate::naming::dispatch_fn_name(&crate::naming::rust_ident(nested_command_name))
646
+ ),
647
+ args_struct: format!(
648
+ "{nested_name_ident}{}NestedEntityArgs",
649
+ crate::naming::rust_ident(nested_command_name)
650
+ ),
651
+ reference_checks: reference_checks(command, &aggregates_by_name, &unsupported_names),
652
+ reference_specs: crate::reference_specs::reference_specs(
653
+ domain_name,
654
+ command.get("attributes").map(Json::each).unwrap_or(&[]),
655
+ ),
656
+ attributes: command
657
+ .get("attributes")
658
+ .map(Json::each)
659
+ .unwrap_or(&[])
660
+ .iter()
661
+ .map(|a| a.get("name").map(Json::to_s).unwrap_or_default())
662
+ .collect(),
663
+ invariant_check_lines: commands::invariant_checks_for(
664
+ exemplar,
665
+ command,
666
+ &aggregates_by_name,
667
+ &value_objects_by_name,
668
+ ),
669
+ role: command.get("role").map(Json::to_s),
670
+ entity_name: entity_name_str.to_string(),
671
+ entity_identity_reading: entity
672
+ .get("identified_by")
673
+ .map(Json::each)
674
+ .unwrap_or(&[])
675
+ .iter()
676
+ .map(Json::to_s)
677
+ .collect::<Vec<_>>()
678
+ .join(", "),
679
+ nested_name: nested_name_str.to_string(),
680
+ nested_identity_reading: nested_identified_by
681
+ .iter()
682
+ .map(Json::to_s)
683
+ .collect::<Vec<_>>()
684
+ .join(", "),
685
+ unrouted_supported,
686
+ });
687
+ }
688
+ }
689
+
690
+ if entity_can_route {
691
+ puts_str(&mut out, &json_codec::emit_extract_id(exemplar, entity));
692
+ puts_blank(&mut out);
693
+ puts_str(&mut out, &json_codec::emit_extract_id_lenient(exemplar, entity));
694
+ puts_blank(&mut out);
695
+ puts_str(&mut out, &json_codec::emit_extract_wants(exemplar, entity));
696
+ puts_blank(&mut out);
697
+ puts_str(&mut out, &json_codec::emit_self_identity(exemplar, entity));
698
+ puts_blank(&mut out);
699
+ }
700
+
701
+ for command in entity.get("commands").map(Json::each).unwrap_or(&[]) {
702
+ let entity_command_verb = format!("{entity_verb}.{}", node_name(command));
703
+ if let Some(reason) = commands::entity_command_skip_reason(command, entity, &value_objects_by_name) {
704
+ manifest.skipped("entity_command", entity_command_verb, reason);
705
+ continue;
706
+ }
707
+
708
+ puts_str(
709
+ &mut out,
710
+ &commands::emit_entity_command(
711
+ exemplar,
712
+ command,
713
+ entity,
714
+ aggregate,
715
+ domain_name,
716
+ &value_objects_by_name,
717
+ &aggregates_by_name,
718
+ &process_managers,
719
+ ),
720
+ );
721
+ puts_blank(&mut out);
722
+
723
+ // The function above is generated, but the JSON router cannot reach it unless the
724
+ // entity identity is `extract_id`-supported.
725
+ if !entity_can_route {
726
+ manifest.unrouted(
727
+ "entity_command",
728
+ entity_command_verb,
729
+ "router_identity",
730
+ format!(
731
+ "generated as a real Rust function, but not JSON-dispatchable — identity {} isn't a shape extract_id resolves yet (json_codec.rb)",
732
+ ruby_inspect(entity.get_raw("identified_by"))
733
+ ),
734
+ );
735
+ continue;
736
+ }
737
+ manifest.routed("entity_command", entity_command_verb);
738
+
739
+ let entity_command_name = command.get("name").and_then(Json::as_str).unwrap_or("");
740
+ let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
741
+ entity_commands.push(EntityCommandEntry {
742
+ verb: format!(
743
+ "{domain_name}::{agg_name}.{entity_name_ident}.{entity_command_name}",
744
+ entity_name_ident = entity.get("name").and_then(Json::as_str).unwrap_or("")
745
+ ),
746
+ name: entity_command_name.to_string(),
747
+ entity_record: entity_name_ident.clone(),
748
+ fn_name: format!(
749
+ "{}_{}",
750
+ entity
751
+ .get("name")
752
+ .and_then(Json::as_str)
753
+ .unwrap_or("")
754
+ .to_lowercase(),
755
+ crate::naming::dispatch_fn_name(&crate::naming::rust_ident(
756
+ entity_command_name
757
+ ))
758
+ ),
759
+ args_struct: format!(
760
+ "{entity_name_ident}{}EntityArgs",
761
+ crate::naming::rust_ident(entity_command_name)
762
+ ),
763
+ reference_checks: reference_checks(
764
+ command,
765
+ &aggregates_by_name,
766
+ &unsupported_names,
767
+ ),
768
+ reference_specs: crate::reference_specs::reference_specs(
769
+ domain_name,
770
+ command.get("attributes").map(Json::each).unwrap_or(&[]),
771
+ ),
772
+ attributes: command
773
+ .get("attributes")
774
+ .map(Json::each)
775
+ .unwrap_or(&[])
776
+ .iter()
777
+ .map(|a| a.get("name").map(Json::to_s).unwrap_or_default())
778
+ .collect(),
779
+ invariant_check_lines: commands::invariant_checks_for(
780
+ exemplar,
781
+ command,
782
+ &aggregates_by_name,
783
+ &value_objects_by_name,
784
+ ),
785
+ role: command.get("role").map(Json::to_s),
786
+ entity_name: entity
787
+ .get("name")
788
+ .and_then(Json::as_str)
789
+ .unwrap_or("")
790
+ .to_string(),
791
+ entity_identity_reading: identified_by
792
+ .iter()
793
+ .map(Json::to_s)
794
+ .collect::<Vec<_>>()
795
+ .join(", "),
796
+ });
797
+ }
798
+ }
799
+
800
+ // The record carries a pseudo-attribute per `projects` field; a command's Args struct
801
+ // never sees it.
802
+ let record_for_struct = types::with_projected_field_pseudo_attributes(aggregate);
803
+ puts_str(
804
+ &mut out,
805
+ &types::emit_record(exemplar, &record_for_struct, &value_objects_by_name),
806
+ );
807
+ puts_blank(&mut out);
808
+ let record_attrs_owned = record_for_struct.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
809
+ let record_attrs = record_attrs_owned.as_slice();
810
+ let extra = lifecycle_extra_field(aggregate);
811
+ puts_str(
812
+ &mut out,
813
+ &json_codec::emit_to_json_flat(
814
+ exemplar,
815
+ &record_name,
816
+ record_attrs,
817
+ &value_objects_by_name,
818
+ true,
819
+ &extra,
820
+ Some(aggregate),
821
+ ),
822
+ );
823
+ puts_blank(&mut out);
824
+ puts_str(
825
+ &mut out,
826
+ &json_codec::emit_from_json_state(
827
+ exemplar,
828
+ &record_name,
829
+ record_attrs,
830
+ &value_objects_by_name,
831
+ true,
832
+ &extra,
833
+ Some(aggregate),
834
+ ),
835
+ );
836
+ puts_blank(&mut out);
837
+ puts_str(&mut out, &format!("impl crate::kernel::ToJson for {record_name} {{\n fn to_json(&self) -> crate::kernel::Json {{\n {record_name}::to_json(self)\n }}\n}}\n"));
838
+ puts_blank(&mut out);
839
+ puts_str(&mut out, &types::emit_set_projected_field(aggregate));
840
+ puts_blank(&mut out);
841
+ puts_str(&mut out, &types::emit_projected_field_table(aggregate));
842
+ puts_blank(&mut out);
843
+
844
+ if can_route {
845
+ puts_str(&mut out, &json_codec::emit_extract_id(exemplar, aggregate));
846
+ puts_blank(&mut out);
847
+ }
848
+
849
+ puts_str(&mut out, &commands::emit_invariants_fn(aggregate));
850
+ puts_blank(&mut out);
851
+
852
+ let mut registry_commands: Vec<CommandEntry> = Vec::new();
853
+ for command in aggregate.get("commands").map(Json::each).unwrap_or(&[]) {
854
+ let command_verb = format!("{domain_name}::{agg_name}.{}", node_name(command));
855
+ if let Some(reason) = commands::command_skip_reason(command, aggregate, &value_objects_by_name) {
856
+ manifest.skipped("command", command_verb, reason);
857
+ continue;
858
+ }
859
+
860
+ puts_str(
861
+ &mut out,
862
+ &commands::emit_command(
863
+ exemplar,
864
+ command,
865
+ aggregate,
866
+ domain_name,
867
+ &value_objects_by_name,
868
+ &aggregates_by_name,
869
+ ),
870
+ );
871
+ puts_blank(&mut out);
872
+
873
+ let command_name = command.get("name").and_then(Json::as_str).unwrap_or("");
874
+ let args_struct = format!("{}Args", crate::naming::rust_ident(command_name));
875
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
876
+ puts_str(
877
+ &mut out,
878
+ &json_codec::emit_to_json_flat_sparse(exemplar, &args_struct, cmd_attrs, &value_objects_by_name),
879
+ );
880
+ puts_blank(&mut out);
881
+ let allowlist =
882
+ json_codec::command_argument_allowlist(aggregate, command, &process_managers, &[]);
883
+ puts_str(
884
+ &mut out,
885
+ &json_codec::emit_from_json_flat(
886
+ exemplar,
887
+ &args_struct,
888
+ cmd_attrs,
889
+ &value_objects_by_name,
890
+ Some(&allowlist),
891
+ Some(command_name),
892
+ true,
893
+ true,
894
+ Some(&aggregates_by_name),
895
+ ),
896
+ );
897
+ puts_blank(&mut out);
898
+ // One generated gate function per declared argument-gate step, called in vocabulary
899
+ // order by `kernel::decode_aggregate_arguments`.
900
+ puts_str(
901
+ &mut out,
902
+ &json_codec::emit_argument_gates(&args_struct, command_name, cmd_attrs, Some(&allowlist)),
903
+ );
904
+ puts_blank(&mut out);
905
+
906
+ let creates = crate::shared::creates_owner(aggregate, command, &value_objects_by_name);
907
+ let identity = mutations::identity_components(aggregate, command);
908
+ let identity_extra_params: Vec<String> = if creates {
909
+ identity.iter().filter_map(|c| c.head.clone()).collect()
910
+ } else {
911
+ Vec::new()
912
+ };
913
+
914
+ if !creates && !can_route {
915
+ manifest.unrouted(
916
+ "command",
917
+ command_verb,
918
+ "router_identity",
919
+ format!(
920
+ "generated as a real Rust function, but not JSON-dispatchable — identity {} isn't a shape extract_id resolves yet (json_codec.rb)",
921
+ ruby_inspect(aggregate.get_raw("identified_by"))
922
+ ),
923
+ );
924
+ continue;
925
+ }
926
+ manifest.routed("command", command_verb);
927
+
928
+ registry_commands.push(CommandEntry {
929
+ verb: format!("{domain_name}::{agg_name}.{command_name}"),
930
+ name: command_name.to_string(),
931
+ fn_name: crate::naming::dispatch_fn_name(&crate::naming::rust_ident(command_name)),
932
+ args_struct,
933
+ creates,
934
+ identity_extra_params,
935
+ // `state_reference_checks` only adds entries `reference_checks` did not cover.
936
+ reference_checks: {
937
+ let mut checks = reference_checks(command, &aggregates_by_name, &unsupported_names);
938
+ checks.extend(state_reference_checks(aggregate, command, &aggregates_by_name, &unsupported_names, &value_objects_by_name));
939
+ checks
940
+ },
941
+ tenant_boundary_checks: tenant_boundary_checks(
942
+ aggregate,
943
+ command,
944
+ &aggregates_by_name,
945
+ &unsupported_names,
946
+ &value_objects_by_name,
947
+ ),
948
+ reference_specs: crate::reference_specs::reference_specs(domain_name, cmd_attrs),
949
+ attributes: cmd_attrs
950
+ .iter()
951
+ .map(|a| a.get("name").map(Json::to_s).unwrap_or_default())
952
+ .collect(),
953
+ invariant_check_lines: commands::invariant_checks_for(
954
+ exemplar,
955
+ command,
956
+ &aggregates_by_name,
957
+ &value_objects_by_name,
958
+ ),
959
+ role: command.get("role").map(Json::to_s),
960
+ });
961
+ }
962
+
963
+ let mut port_operations: Vec<PortEntry> = Vec::new();
964
+ for port in aggregate.get("ports").map(Json::each).unwrap_or(&[]) {
965
+ let port_name = port.get("name").and_then(Json::as_str).unwrap_or("");
966
+ for operation in port.get("operations").map(Json::each).unwrap_or(&[]) {
967
+ let operation_verb = format!("{domain_name}::{agg_name}.{port_name}.{}", node_name(operation));
968
+ if let Some(reason) = ports::port_operation_skip_reason(operation, agg_name, &value_objects_by_name) {
969
+ manifest.skipped("port_operation", operation_verb, reason);
970
+ continue;
971
+ }
972
+ manifest.routed("port_operation", operation_verb);
973
+
974
+ puts_str(
975
+ &mut out,
976
+ &ports::emit_port_operation(
977
+ exemplar,
978
+ operation,
979
+ port_name,
980
+ agg_name,
981
+ domain_name,
982
+ &value_objects_by_name,
983
+ &aggregates_by_name,
984
+ ),
985
+ );
986
+ puts_blank(&mut out);
987
+
988
+ let operation_name = operation.get("name").and_then(Json::as_str).unwrap_or("");
989
+ let operation_attrs = operation.get("attributes").map(Json::each).unwrap_or(&[]);
990
+ let legacy_receiver_field = operation_attrs
991
+ .iter()
992
+ .find(|attr| {
993
+ crate::naming::reference_target(crate::attr::type_name(attr))
994
+ == Some(agg_name)
995
+ })
996
+ .map(|attr| crate::attr::name(attr).to_string());
997
+ // A `to:`-declared operation has no Reference-typed attribute; its receiver is a
998
+ // plain attribute named for the aggregate's identified_by field. Kept apart from
999
+ // legacy_receiver_field because `split_aggregate_receiver` must not strip it.
1000
+ let to_receiver_field = if operation.get("to").and_then(Json::as_str) == Some(agg_name) {
1001
+ // `identified_by` holds identity paths: a value-object identity is dotted
1002
+ // ("reference.value"), while the attribute is named "reference".
1003
+ aggregate
1004
+ .get("identified_by")
1005
+ .map(Json::each)
1006
+ .unwrap_or(&[])
1007
+ .first()
1008
+ .map(Json::to_s)
1009
+ .and_then(|s| s.split('.').next().map(str::to_string))
1010
+ } else {
1011
+ None
1012
+ };
1013
+ let operation_reference_checks =
1014
+ reference_checks(operation, &aggregates_by_name, &unsupported_names)
1015
+ .into_iter()
1016
+ .filter(|check| check.target_name != agg_name)
1017
+ .collect();
1018
+ port_operations.push(PortEntry {
1019
+ verb: format!("{domain_name}::{agg_name}.{port_name}.{operation_name}"),
1020
+ name: operation_name.to_string(),
1021
+ fn_name: format!(
1022
+ "{}_{}",
1023
+ port_name.to_lowercase(),
1024
+ crate::naming::dispatch_fn_name(&crate::naming::rust_ident(operation_name))
1025
+ ),
1026
+ args_struct: format!(
1027
+ "{}{}Args",
1028
+ crate::naming::rust_ident(port_name),
1029
+ crate::naming::rust_ident(operation_name)
1030
+ ),
1031
+ reference_checks: operation_reference_checks,
1032
+ legacy_receiver_field,
1033
+ to_receiver_field,
1034
+ });
1035
+ }
1036
+ }
1037
+
1038
+ aggregate_files.push(GeneratedFile {
1039
+ name: format!("{}.rs", agg_name.to_lowercase()),
1040
+ content: out,
1041
+ });
1042
+
1043
+ registry_aggregates.push(AggregateEntry {
1044
+ name: agg_name.to_string(),
1045
+ module_name: agg_name.to_lowercase(),
1046
+ record: record_name,
1047
+ commands: registry_commands,
1048
+ entity_commands,
1049
+ nested_entity_commands,
1050
+ ports: port_operations,
1051
+ chapter_mod: mod_name.to_string(),
1052
+ domain_name: domain_name.to_string(),
1053
+ reference_specs: crate::reference_specs::reference_specs(
1054
+ domain_name,
1055
+ aggregate.get("attributes").map(Json::each).unwrap_or(&[]),
1056
+ ),
1057
+ identified_by: aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect(),
1058
+ entities: registry_entities,
1059
+ });
1060
+ }
1061
+
1062
+ let mut query_defs: Vec<queries::QueryDef> = Vec::new();
1063
+ let assignments_verb = queries::provided_assignments(ir);
1064
+ for aggregate in all_aggregates {
1065
+ let agg_name = aggregate.get("name").and_then(Json::as_str).unwrap_or("");
1066
+ let value_objects = aggregate
1067
+ .get("value_objects")
1068
+ .map(Json::each)
1069
+ .unwrap_or(&[]);
1070
+ let value_objects_by_name: HashMap<String, &Json> = value_objects
1071
+ .iter()
1072
+ .map(|vo| {
1073
+ (
1074
+ vo.get("name")
1075
+ .and_then(Json::as_str)
1076
+ .unwrap_or("")
1077
+ .to_string(),
1078
+ vo,
1079
+ )
1080
+ })
1081
+ .collect();
1082
+
1083
+ for query in aggregate.get("queries").map(Json::each).unwrap_or(&[]) {
1084
+ let query_name = query.get("name").and_then(Json::as_str).unwrap_or("");
1085
+ if let Some(reason) = queries::query_skip_reason(query, aggregate, &value_objects_by_name, &aggregates_by_name) {
1086
+ manifest.skipped("query", format!("{domain_name}::{agg_name}.{query_name}"), reason);
1087
+ continue;
1088
+ }
1089
+ manifest.generated("query", format!("{domain_name}::{agg_name}.{query_name}"));
1090
+
1091
+ let (conditions, reference_hop_conditions) = queries::query_conditions_and_hops(domain_name, query, aggregate, &aggregates_by_name);
1092
+ query_defs.push(queries::QueryDef {
1093
+ verb: format!("{domain_name}::{agg_name}.{query_name}"),
1094
+ aggregate: format!("{domain_name}::{agg_name}"),
1095
+ arg_checks: queries::query_arg_checks(query, &format!("crate::generated::{mod_name}::{}", agg_name.to_lowercase()), &value_objects_by_name),
1096
+ conditions,
1097
+ reference_hop_conditions,
1098
+ order_by: query.get("order_by").map(|ob| queries::emit_query_order_by(ob, query.get("null_semantics"))),
1099
+ offset: query.get("offset").map(queries::emit_query_offset),
1100
+ limit: query.get("limit").map(queries::emit_query_limit),
1101
+ authorization: queries::emit_query_authorization(query_name, query.get("authorization")),
1102
+ assignments: assignments_verb.as_deref() == Some(format!("{agg_name}.{query_name}").as_str()),
1103
+ entity: None,
1104
+ });
1105
+ }
1106
+
1107
+ // Entity queries, one level down.
1108
+ let aggregate_id = format!("{domain_name}::{agg_name}");
1109
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
1110
+ for entity in aggregate.get("entities").map(Json::each).unwrap_or(&[]) {
1111
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("");
1112
+ let list_attr = attrs.iter().find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity_name);
1113
+ for query in entity.get("queries").map(Json::each).unwrap_or(&[]) {
1114
+ let query_name = query.get("name").and_then(Json::as_str).unwrap_or("");
1115
+ let query_verb = format!("{aggregate_id}.{entity_name}.{query_name}");
1116
+ if let Some(reason) = queries::entity_query_skip_reason(query, entity, list_attr.is_some(), &value_objects_by_name) {
1117
+ manifest.skipped("query", query_verb, reason);
1118
+ continue;
1119
+ }
1120
+ let Some(list_attr) = list_attr else { continue };
1121
+ manifest.generated("query", query_verb);
1122
+ let identity_keys = entity
1123
+ .get("identified_by")
1124
+ .map(Json::each)
1125
+ .unwrap_or(&[])
1126
+ .iter()
1127
+ .map(|path| Json::to_s(path).split('.').next().unwrap_or("").to_string())
1128
+ .collect();
1129
+ query_defs.push(queries::QueryDef {
1130
+ verb: format!("{aggregate_id}.{entity_name}.{query_name}"),
1131
+ aggregate: aggregate_id.clone(),
1132
+ arg_checks: Vec::new(),
1133
+ conditions: queries::query_conditions(query),
1134
+ reference_hop_conditions: Vec::new(),
1135
+ order_by: query.get("order_by").map(|ob| queries::emit_query_order_by(ob, query.get("null_semantics"))),
1136
+ offset: query.get("offset").map(queries::emit_query_offset),
1137
+ limit: query.get("limit").map(queries::emit_query_limit),
1138
+ authorization: None,
1139
+ assignments: false,
1140
+ entity: Some(queries::EntityScope {
1141
+ list_field: crate::attr::name(list_attr).to_string(),
1142
+ parent_key: crate::naming::snake(agg_name),
1143
+ identity_keys,
1144
+ }),
1145
+ });
1146
+ }
1147
+ manifest.entity_query_gaps(&format!("{aggregate_id}.{entity_name}"), entity.get("entities").map(Json::each).unwrap_or(&[]));
1148
+ }
1149
+ }
1150
+
1151
+ let mut read_model_defs: Vec<read_models::ReadModelDef> = Vec::new();
1152
+ for read_model in ir.get("read_models").map(Json::each).unwrap_or(&[]) {
1153
+ let read_model_id = format!("{domain_name}::{}", node_name(read_model));
1154
+ if let Some(reason) = read_models::read_model_skip_reason(read_model, &aggregates_by_name, &unsupported_names) {
1155
+ manifest.skipped("read_model", read_model_id, reason);
1156
+ continue;
1157
+ }
1158
+ manifest.generated("read_model", read_model_id);
1159
+ read_model_defs.push(read_models::read_model_def(
1160
+ domain_name,
1161
+ read_model,
1162
+ &aggregates_by_name,
1163
+ ));
1164
+ }
1165
+
1166
+ // Policies and process managers have no per-instance skip condition.
1167
+ for policy in ir.get("policies").map(Json::each).unwrap_or(&[]) {
1168
+ manifest.routed("policy", format!("{domain_name}::{}", node_name(policy)));
1169
+ }
1170
+ for pm in &process_managers {
1171
+ manifest.routed("process_manager", format!("{domain_name}::{}", node_name(pm)));
1172
+ }
1173
+ // Lineage-capable aggregates are always generated and routed; the journal path is generic.
1174
+ let lineage_aggregates = ir.get("lineage").and_then(|l| l.get("capable_aggregates")).map(Json::each).unwrap_or(&[]);
1175
+ for lineage_aggregate in lineage_aggregates {
1176
+ manifest.record(
1177
+ "lineage_aggregate",
1178
+ format!("{domain_name}::{}", node_name(lineage_aggregate)),
1179
+ true,
1180
+ Some(true),
1181
+ None,
1182
+ None,
1183
+ Some(format!(
1184
+ "read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::append_lineage_mutation — both generic over storage_name (\"{}\"), dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own CommandInterpreter routing for a Postgres-bound aggregate (rust/project.rb's own header)",
1185
+ lineage_aggregate.get("storage_name").map(Json::to_s).unwrap_or_default()
1186
+ )),
1187
+ );
1188
+ }
1189
+
1190
+ let policies: Vec<Json> = ir.get("policies").map(Json::each).unwrap_or(&[]).to_vec();
1191
+ // Raw aggregate JSON: a fan-out's addressing key comes from the target command's declared
1192
+ // `references`/`attributes`, which `AggregateEntry` does not carry.
1193
+ let policy_aggregates: Vec<Json> = ir.get("aggregates").map(Json::each).unwrap_or(&[]).to_vec();
1194
+
1195
+ let mut registry_rs = String::new();
1196
+ puts_str(
1197
+ &mut registry_rs,
1198
+ &crate::registry::emit_registry(exemplar, &registry_aggregates),
1199
+ );
1200
+ puts_blank(&mut registry_rs);
1201
+ puts_str(
1202
+ &mut registry_rs,
1203
+ &crate::registry::emit_reference_lookup(&registry_aggregates),
1204
+ );
1205
+ puts_blank(&mut registry_rs);
1206
+ puts_str(
1207
+ &mut registry_rs,
1208
+ &reactions::emit_policy_table(exemplar, domain_name, &policies, &policy_aggregates),
1209
+ );
1210
+ puts_blank(&mut registry_rs);
1211
+ puts_str(
1212
+ &mut registry_rs,
1213
+ &reactions::emit_cross_domain_policy_table(exemplar, domain_name, &policies),
1214
+ );
1215
+ puts_blank(&mut registry_rs);
1216
+ puts_str(
1217
+ &mut registry_rs,
1218
+ &reactions::emit_process_manager_table(exemplar, &process_managers),
1219
+ );
1220
+ puts_blank(&mut registry_rs);
1221
+ let generated_names: Vec<String> = registry_aggregates.iter().map(|a| a.name.clone()).collect();
1222
+ puts_str(
1223
+ &mut registry_rs,
1224
+ &reactions::emit_reference_key_table(
1225
+ exemplar,
1226
+ &[(domain_name.to_string(), generated_names)],
1227
+ ),
1228
+ );
1229
+ puts_blank(&mut registry_rs);
1230
+ puts_str(
1231
+ &mut registry_rs,
1232
+ &reactions::emit_creates_table(exemplar, &registry_aggregates),
1233
+ );
1234
+ puts_blank(&mut registry_rs);
1235
+ puts_str(
1236
+ &mut registry_rs,
1237
+ &reactions::emit_identity_head_table(exemplar, &registry_aggregates),
1238
+ );
1239
+ puts_blank(&mut registry_rs);
1240
+ puts_str(
1241
+ &mut registry_rs,
1242
+ &reactions::emit_entity_identity_head_table(exemplar, &registry_aggregates),
1243
+ );
1244
+ puts_blank(&mut registry_rs);
1245
+ puts_str(
1246
+ &mut registry_rs,
1247
+ &reactions::emit_command_attributes_table(exemplar, &registry_aggregates),
1248
+ );
1249
+ puts_blank(&mut registry_rs);
1250
+ puts_str(
1251
+ &mut registry_rs,
1252
+ &queries::emit_query_table(exemplar, &query_defs),
1253
+ );
1254
+ puts_blank(&mut registry_rs);
1255
+ puts_str(&mut registry_rs, &queries::emit_query_arg_check_table(&query_defs));
1256
+ puts_blank(&mut registry_rs);
1257
+ for rmd in &read_model_defs {
1258
+ if let Some(body) = &rmd.group_by_fn_body {
1259
+ puts_str(&mut registry_rs, body);
1260
+ puts_blank(&mut registry_rs);
1261
+ }
1262
+ }
1263
+ puts_str(
1264
+ &mut registry_rs,
1265
+ &read_models::emit_read_model_table(exemplar, &read_model_defs),
1266
+ );
1267
+
1268
+ let mut mod_rs = String::new();
1269
+ puts_str(
1270
+ &mut mod_rs,
1271
+ "// GENERATED by hecks project_rust — re-run it to refresh this list.",
1272
+ );
1273
+ puts_str(&mut mod_rs, "pub mod metadata;");
1274
+ puts_str(&mut mod_rs, "pub mod registry;");
1275
+ for a in &registry_aggregates {
1276
+ puts_str(&mut mod_rs, &format!("pub mod {};", a.name.to_lowercase()));
1277
+ }
1278
+
1279
+ GeneratedDomain {
1280
+ aggregate_files,
1281
+ registry_rs,
1282
+ mod_rs,
1283
+ registry_aggregates,
1284
+ query_defs,
1285
+ read_model_defs,
1286
+ manifest_json: manifest.to_json_text(),
1287
+ }
1288
+ }