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,980 @@
1
+ require "fileutils"
2
+ require "json"
3
+
4
+ module RustProjection
5
+ # Generates one domain's worth of Rust types, Fielded impls and commands into
6
+ # `mod_dir`, plus `metadata.rs` (embedded IR JSON for runtime introspection) and
7
+ # `registry.rs` (the JSON command router `kernel::cli.rs` dispatches through).
8
+ module DomainGenerator
9
+ module_function
10
+
11
+ # One manifest row per generate/skip decision, written to manifest.json
12
+ # alongside ir.json so hecks rust_coverage never has to re-derive "did this
13
+ # generate?" by grepping generated Rust source.
14
+ #
15
+ # gap_class distinguishes a "whole_kind" gap (this construct kind has no
16
+ # code path here at all, for any domain) from a "per_instance" one (the
17
+ # kind generates in general, but this declared instance failed one of
18
+ # this generator's own per-instance checks). construct names the family
19
+ # that forced a gap; gap_class and construct must travel together, since
20
+ # downstream readers (the differential fuzzer, hecks rust_coverage's
21
+ # allowlist) match gaps by these fields, never by reason prose.
22
+ def manifest_entry(kind:, id:, generated:, reason: nil, gap_class: nil, routed: nil, construct: nil)
23
+ if (generated == false || routed == false) && gap_class.nil?
24
+ raise ArgumentError, "manifest entry #{kind} #{id} records a gap with no gap_class"
25
+ end
26
+ if gap_class && construct.to_s.empty?
27
+ raise ArgumentError, "manifest entry #{kind} #{id} declares gap_class #{gap_class.inspect} with no construct — " \
28
+ "every recorded gap names the construct family that forced it"
29
+ end
30
+
31
+ entry = { kind: kind, id: id, generated: generated }
32
+ entry[:routed] = routed unless routed.nil?
33
+ entry[:gap_class] = gap_class if gap_class
34
+ entry[:construct] = construct.to_s if gap_class
35
+ entry[:reason] = reason.to_s if reason
36
+ entry
37
+ end
38
+
39
+ # Entity-scoped queries have no generated code path (the query loop in
40
+ # `call` walks aggregate[:queries] only). Recorded here as the
41
+ # whole-kind gap they are, recursively, one entry per declared entity
42
+ # query at any depth.
43
+ def entity_query_entries(owner_id, entities)
44
+ entities.flat_map do |entity|
45
+ entity_id = "#{owner_id}.#{entity[:name]}"
46
+ own = Array(entity[:queries]).map do |query|
47
+ manifest_entry(kind: "query", id: "#{entity_id}.#{query[:name]}", generated: false, gap_class: "whole_kind",
48
+ construct: "entity_query",
49
+ reason: "an entity-scoped query has no generated code path — only an aggregate's own " \
50
+ "declared queries reach the QUERIES table")
51
+ end
52
+ own + entity_query_entries(entity_id, Array(entity[:entities]))
53
+ end
54
+ end
55
+
56
+ def lifecycle_extra_field(node)
57
+ return [] unless node[:lifecycle]
58
+
59
+ field = Projector.rust_ident_field(node[:lifecycle][:field])
60
+ [[Projector.rust_field(node[:lifecycle][:field]), "crate::kernel::Json::Str(self.#{field}.clone())"]]
61
+ end
62
+
63
+ # For each Reference<X> command attribute, resolves X against every
64
+ # aggregate this domain declares, not just the ones already written
65
+ # when this command's file is reached — matching Ruby's lazy
66
+ # attribute-type resolution, which already allows forward references
67
+ # across aggregates in one file.
68
+ def reference_checks(command, aggregates_by_name, unsupported_names)
69
+ command[:attributes].filter_map do |attr|
70
+ target_name = Projector.reference_target(attr[:type])
71
+ next unless target_name
72
+
73
+ target = aggregates_by_name[target_name]
74
+ next unless target
75
+ next if unsupported_names.include?(target_name)
76
+
77
+ {
78
+ field: attr[:name],
79
+ optional: attr[:optional],
80
+ target_mod: target[:name].downcase,
81
+ target_name: target[:name],
82
+ heads: target[:identified_by].map { |path| path.split(".").first }.join(", "),
83
+ }
84
+ end
85
+ end
86
+
87
+ # Catches a reference field redeclared on the command under a plain
88
+ # value object (the attribute isn't reference?-true, so reference_checks
89
+ # above finds nothing to check) by checking against the aggregate's own
90
+ # Reference<X> field of the same name instead. Reuses the same
91
+ # pre-dispatch, args-based check shape as reference_checks, since a bare
92
+ # `sets :field` mutation (no `to:`, `append:`, etc.) copies its source
93
+ # argument straight into the settled state, so the value is already in
94
+ # `args` before dispatch runs (ADR 0037). Only covers a bare :set
95
+ # mutation on a scalar or single-attribute value object; a `has_many`
96
+ # relationship and entity-level recursion are left unchecked.
97
+ def state_reference_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name)
98
+ aggregate[:attributes].filter_map do |attr|
99
+ target_name = Projector.reference_target(attr[:type])
100
+ next unless target_name
101
+
102
+ mutation = command[:mutations].find do |m|
103
+ m[:op].to_s == "set" && m[:target].to_s == attr[:name].to_s && m[:source][:kind] == "argument"
104
+ end
105
+ next unless mutation
106
+
107
+ source_attr = command[:attributes].find { |a| a[:name].to_s == mutation[:source][:name].to_s }
108
+ next unless source_attr
109
+ next if Projector.reference_target(source_attr[:type])
110
+
111
+ # A has_many field needs one check_reference per element
112
+ # (reference_check_list) rather than the scalar accessor, since
113
+ # dotting `.value` into the whole Vec wouldn't compile.
114
+ list_item = nil
115
+ if attr[:list]
116
+ list_item = list_reference_check_item(source_attr, value_objects_by_name)
117
+ next unless list_item
118
+
119
+ accessor = source_attr[:name]
120
+ else
121
+ accessor = state_reference_check_accessor(source_attr, value_objects_by_name)
122
+ next unless accessor
123
+ end
124
+
125
+ target = aggregates_by_name[target_name]
126
+ next unless target
127
+ next if unsupported_names.include?(target_name)
128
+
129
+ {
130
+ field: accessor,
131
+ optional: source_attr[:optional],
132
+ list_item: list_item,
133
+ target_mod: target[:name].downcase,
134
+ target_name: target[:name],
135
+ heads: target[:identified_by].map { |path| path.split(".").first }.join(", "),
136
+ }
137
+ end
138
+ end
139
+
140
+ # The per-element key expression reference_check_list checks: bare
141
+ # `item` for a list_of(String), or `&item.<field>` for a list of
142
+ # single-String-attribute value objects. nil for any other element
143
+ # shape — left unchecked, same as state_reference_check_accessor's own
144
+ # gaps.
145
+ def list_reference_check_item(source_attr, value_objects_by_name)
146
+ return nil unless source_attr[:list]
147
+ return "item" if source_attr[:type].to_s == "String"
148
+
149
+ vo = value_objects_by_name[source_attr[:type]]
150
+ return nil unless vo && vo[:attributes].size == 1
151
+ return nil unless vo[:attributes].first[:type].to_s == "String"
152
+
153
+ "&item.#{Projector.rust_ident_field(vo[:attributes].first[:name])}"
154
+ end
155
+
156
+ # The source argument's own field-access expression, tacked onto
157
+ # `args.` by emit_reference_check. A plain scalar argument needs no
158
+ # accessor (nil means "use the field name as-is"). A single-attribute
159
+ # value object needs `.field` appended, but only for a required
160
+ # argument — check_reference's optional template assumes
161
+ # Option<String>, not Option<Struct>. A multi-attribute value object
162
+ # has no single field to dot into. nil for both: a deliberately narrow
163
+ # gap the real corpus doesn't exercise.
164
+ def state_reference_check_accessor(source_attr, value_objects_by_name)
165
+ return source_attr[:name] if Projector.effective_scalar_type(source_attr[:type])
166
+
167
+ vo = value_objects_by_name[source_attr[:type]]
168
+ return nil unless vo
169
+ return nil if source_attr[:optional]
170
+ return nil unless vo[:attributes].size == 1
171
+
172
+ "#{source_attr[:name]}.#{Projector.rust_ident_field(vo[:attributes].first[:name])}"
173
+ end
174
+
175
+ # An aggregate's tenant field is whichever field one of its own queries
176
+ # names via `authorize policy, tenant: :field` — reused here to name a
177
+ # field on the aggregate itself for the write side. nil when no query
178
+ # declares one.
179
+ def tenant_field_for(aggregate)
180
+ query = Array(aggregate[:queries]).find { |q| q[:authorization] && q[:authorization][:tenant] }
181
+ query && query[:authorization][:tenant]
182
+ end
183
+
184
+ # The write-side mirror of the query-side tenant boundary (ADR 0037):
185
+ # reuses the same pre-dispatch, args-based check shape as
186
+ # reference_checks/state_reference_checks above, since a bare `sets
187
+ # :field` mutation copies its source argument straight into settled
188
+ # state before dispatch runs. Fires only when both sides declare a
189
+ # tenant field and it's reachable off this command's args in the same
190
+ # narrow accessor shape state_reference_check_accessor already
191
+ # establishes; every other case bails to `[]`, never raises.
192
+ def tenant_boundary_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name)
193
+ own_tenant_field = tenant_field_for(aggregate)
194
+ return [] unless own_tenant_field
195
+
196
+ own_tenant_attr = command[:attributes].find { |a| a[:name].to_s == own_tenant_field.to_s }
197
+ return [] unless own_tenant_attr
198
+
199
+ own_accessor = state_reference_check_accessor(own_tenant_attr, value_objects_by_name)
200
+ return [] unless own_accessor
201
+
202
+ command[:attributes].filter_map do |attr|
203
+ target_name = Projector.reference_target(attr[:type])
204
+ next unless target_name
205
+
206
+ target = aggregates_by_name[target_name]
207
+ next unless target
208
+ next if unsupported_names.include?(target_name)
209
+
210
+ target_tenant_field = tenant_field_for(target)
211
+ next unless target_tenant_field
212
+
213
+ target_tenant_attr = target[:attributes].find { |a| a[:name].to_s == target_tenant_field.to_s }
214
+ next unless target_tenant_attr
215
+
216
+ target_accessor = state_reference_check_accessor(target_tenant_attr, value_objects_by_name)
217
+ next unless target_accessor
218
+
219
+ {
220
+ reference_field: attr[:name],
221
+ target_mod: target[:name].downcase,
222
+ target_name: target[:name],
223
+ aggregate_name: aggregate[:name],
224
+ own_tenant_field: own_tenant_field,
225
+ own_accessor: own_accessor,
226
+ target_tenant_field: target_tenant_field,
227
+ target_accessor: target_accessor,
228
+ }
229
+ end
230
+ end
231
+
232
+ # Writes one chapter's generated Rust (per-aggregate files, registry.rs,
233
+ # metadata.rs, mod.rs) into mod_dir, and returns its aggregates, queries
234
+ # and read_models so a multi-chapter caller can merge them.
235
+ def call(ir, source_label, mod_dir, mod_name, merged_module: true)
236
+ # Validated before any side effects (mkdir_p included) so every name
237
+ # collision in one domain is reported at once, rather than after some
238
+ # aggregates already had files written.
239
+ refusal = Projector.reserved_name_refusal(source_label, mod_name, ir[:aggregates].map { |a| a[:name] })
240
+ raise refusal if refusal
241
+
242
+ FileUtils.mkdir_p(mod_dir)
243
+ domain_name = ir[:name]
244
+ generated_aggregates = []
245
+ registry_aggregates = []
246
+ # See manifest_entry's own header. Written to manifest.json at the
247
+ # end of this method, alongside the ir.json sidecar.
248
+ manifest = []
249
+ # Accumulated the same way registry_aggregates is, and returned so a
250
+ # multi-chapter caller can union query defs across chapters too.
251
+ query_defs = []
252
+ aggregates_by_name = ir[:aggregates].to_h { |a| [a[:name], a] }
253
+ unsupported_names = ir[:aggregates].select do |a|
254
+ vo_by_name = a[:value_objects].to_h { |vo| [vo[:name], vo] }
255
+ Projector.unsupported_attribute_types(a, vo_by_name).any?
256
+ end.map { |a| a[:name] }
257
+ # A command attribute can reference any value object the domain
258
+ # declares, not just one its own aggregate also declares.
259
+ # cross_aggregate_vo_imports uses this map to emit the `use` line
260
+ # that makes the identifier resolve; codegen call sites still emit
261
+ # the same bare type name.
262
+ domain_value_object_owner = ir[:aggregates].each_with_object({}) do |a, index|
263
+ a[:value_objects].each { |vo| index[vo[:name]] = a[:name] }
264
+ end
265
+ # Same domain-wide reach as above: bridging a cross-aggregate command
266
+ # argument's type into its target field needs the actual definition,
267
+ # not just which aggregate owns it. Merged into each aggregate's own
268
+ # local map below, with the local map winning any name collision.
269
+ domain_value_objects_by_name = ir[:aggregates].flat_map { |a| a[:value_objects] }.to_h { |vo| [vo[:name], vo] }
270
+ ir[:aggregates].each do |aggregate|
271
+ value_objects_by_name = domain_value_objects_by_name.merge(aggregate[:value_objects].to_h { |vo| [vo[:name], vo] })
272
+ # Must run before anything below reads attr[:optional] on an
273
+ # entity/value-object field, since those reads need the derived
274
+ # fact, not just what the domain author wrote by hand.
275
+ Projector.mark_append_optional_fields!(aggregate, value_objects_by_name)
276
+ Projector.derive_reverses_mutations!(aggregate)
277
+
278
+ unsupported = Projector.unsupported_attribute_types(aggregate, value_objects_by_name)
279
+ if unsupported.any?
280
+ aggregate_reason = "attribute type(s) #{unsupported.join(', ')} not generated yet " \
281
+ "(a bare, non-list entity-typed attribute isn't resolved to a Rust type)"
282
+ puts "skipping #{domain_name}::#{aggregate[:name]}: #{aggregate_reason}"
283
+ manifest << manifest_entry(kind: "aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: false,
284
+ gap_class: "per_instance", construct: "attribute_type", reason: aggregate_reason)
285
+ # Every command/entity/port-op this aggregate owns is recorded
286
+ # too, tracing back to the same root cause, rather than silently
287
+ # vanishing from the manifest with the aggregate.
288
+ aggregate[:commands].each do |command|
289
+ manifest << manifest_entry(kind: "command", id: "#{domain_name}::#{aggregate[:name]}.#{command[:name]}",
290
+ generated: false, gap_class: "per_instance",
291
+ construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
292
+ end
293
+ aggregate[:entities].each do |entity|
294
+ manifest << manifest_entry(kind: "entity", id: "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}",
295
+ generated: false, gap_class: "per_instance",
296
+ construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
297
+ entity[:commands].each do |command|
298
+ manifest << manifest_entry(kind: "entity_command",
299
+ id: "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}.#{command[:name]}",
300
+ generated: false, gap_class: "per_instance",
301
+ construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
302
+ end
303
+ end
304
+ aggregate[:ports].each do |port|
305
+ port[:operations].each do |operation|
306
+ manifest << manifest_entry(kind: "port_operation",
307
+ id: "#{domain_name}::#{aggregate[:name]}.#{port[:name]}.#{operation[:name]}",
308
+ generated: false, gap_class: "per_instance",
309
+ construct: "owning_aggregate", reason: "owning aggregate not generated: #{aggregate_reason}")
310
+ end
311
+ end
312
+ next
313
+ end
314
+
315
+ manifest << manifest_entry(kind: "aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: true)
316
+ generated_aggregates << aggregate
317
+ can_route = Projector.extract_id_supported?(aggregate)
318
+ registry_commands = []
319
+ entity_commands = []
320
+ nested_entity_commands = []
321
+ port_operations = []
322
+ record_name = Projector.rust_ident(aggregate[:name])
323
+
324
+ path = File.join(mod_dir, "#{aggregate[:name].downcase}.rs")
325
+ wrote = WriteIfChanged.block(path) do |f|
326
+ f.puts "// GENERATED by hecks project_rust from #{source_label}'s canonical IR."
327
+ f.puts "// Do not hand-edit — re-run hecks project_rust instead."
328
+ f.puts "#![allow(dead_code, unused_variables)]"
329
+ f.puts 'use crate::kernel::Expr;'
330
+ Projector.cross_aggregate_vo_imports(aggregate, domain_value_object_owner, mod_name).each { |line| f.puts line }
331
+ f.puts
332
+
333
+ aggregate[:value_objects].each do |vo|
334
+ f.puts Projector.emit_value_object(vo, value_objects_by_name, aggregates_by_name)
335
+ f.puts
336
+ if vo[:closed_set] && vo[:attributes].size == 1
337
+ # A single-field closed set collapses to a Rust enum
338
+ # (emit_value_object's own branch), which needs the
339
+ # tag<->member codec.
340
+ f.puts Projector.emit_closed_set_codec(vo)
341
+ elsif vo[:closed_set]
342
+ # A multi-field closed set is a data table whose String
343
+ # fields are &'static str, not String — from_json can only
344
+ # select one of the table's fixed rows, not construct one.
345
+ f.puts Projector.emit_closed_set_table_codec(vo)
346
+ else
347
+ # A value object gets the same unknown-key refusal a
348
+ # command's args struct gets, with no extra allowed keys —
349
+ # without it, a mistyped nested VO field silently falls
350
+ # through to any declared default instead of being refused.
351
+ name = Projector.rust_ident(vo[:name])
352
+ f.puts Projector.emit_to_json_flat(name, vo[:attributes], value_objects_by_name)
353
+ f.puts
354
+ f.puts Projector.emit_from_json_flat(name, vo[:attributes], value_objects_by_name, unknown_argument_allowlist: [])
355
+ end
356
+ f.puts
357
+ end
358
+
359
+ aggregate[:entities].each do |entity|
360
+ entity_verb = "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}"
361
+ manifest << manifest_entry(kind: "entity", id: entity_verb, generated: true)
362
+ f.puts Projector.emit_entity(entity, value_objects_by_name)
363
+ f.puts
364
+ entity_name = Projector.rust_ident(entity[:name])
365
+ f.puts Projector.emit_to_json_flat(entity_name, entity[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(entity))
366
+ f.puts
367
+ f.puts Projector.emit_from_json_state(entity_name, entity[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(entity))
368
+ f.puts
369
+
370
+ # An entity nested inside this one (one level past what this
371
+ # loop otherwise reaches). Its struct and JSON codec are needed
372
+ # the moment a sibling field references it as a Vec<...>
373
+ # element type. Its own commands route only via `to: {
374
+ # entities: [...] }` addressing (ADR 0026); a third level is
375
+ # deliberately not generalized.
376
+ #
377
+ # entity_can_route (used again in the nested-entities loop
378
+ # below) gates whether this entity's identity shape supports
379
+ # extract_id/extract_wants at all; nested_can_route is the same
380
+ # check one hop deeper. Both true is what unrouted_supported
381
+ # (below) gates.
382
+ entity_can_route = Projector.extract_id_supported?(entity)
383
+
384
+ entity[:entities].each do |nested|
385
+ nested_verb = "#{entity_verb}.#{nested[:name]}"
386
+ manifest << manifest_entry(kind: "entity", id: nested_verb, generated: true)
387
+ f.puts Projector.emit_entity(nested, value_objects_by_name)
388
+ f.puts
389
+ nested_rust_name = Projector.rust_ident(nested[:name])
390
+ f.puts Projector.emit_to_json_flat(nested_rust_name, nested[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(nested))
391
+ f.puts
392
+ f.puts Projector.emit_from_json_state(nested_rust_name, nested[:attributes], value_objects_by_name, extra_fields: lifecycle_extra_field(nested))
393
+ f.puts
394
+ # identity() is what a routed dispatch needs off a
395
+ # doubly-nested element, emitted unconditionally the same
396
+ # way entity's own always is.
397
+ f.puts Projector.emit_self_identity(nested)
398
+ f.puts
399
+
400
+ # extract_id/extract_wants for flat-args addressing at this
401
+ # depth, gated on both hops' identity shape supporting it
402
+ # (entity_can_route && nested_can_route) since a flat
403
+ # dispatch must also resolve the first hop's id.
404
+ nested_can_route = Projector.extract_id_supported?(nested)
405
+ unrouted_supported = entity_can_route && nested_can_route
406
+ if unrouted_supported
407
+ f.puts Projector.emit_extract_id(nested)
408
+ f.puts
409
+ # The addressing sibling generated alongside extract_id: a
410
+ # present-but-blank value is a valid non-matching
411
+ # component here, never a refusal.
412
+ f.puts Projector.emit_extract_id_lenient(nested)
413
+ f.puts
414
+ f.puts Projector.emit_extract_wants(nested)
415
+ f.puts
416
+ else
417
+ reason = entity_can_route ? "identity #{nested[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)" : "entity #{entity[:name]}'s own identity isn't extract_id-supported either"
418
+ puts "skipping #{nested_verb}'s flat-args fallback: #{reason} — routed (`to:`) addressing still works"
419
+ end
420
+
421
+ nested[:commands].each do |command|
422
+ nested_command_verb = "#{nested_verb}.#{command[:name]}"
423
+ reason = Projector.entity_command_skip_reason(command, nested, value_objects_by_name)
424
+ if reason
425
+ puts "skipping #{nested_command_verb}: #{reason}"
426
+ manifest << manifest_entry(kind: "entity_command", id: nested_command_verb, generated: false,
427
+ gap_class: "per_instance", construct: reason.construct, reason: reason)
428
+ next
429
+ end
430
+
431
+ f.puts Projector.emit_nested_entity_command(command, nested, entity, aggregate, domain_name, value_objects_by_name, aggregates_by_name,
432
+ process_managers: ir[:process_managers])
433
+ f.puts
434
+
435
+ manifest << manifest_entry(kind: "entity_command", id: nested_command_verb, generated: true, routed: true,
436
+ reason: unrouted_supported ? "routed (`to: { entities: [...] }`) and flat-args (one identity head per hop) both supported (BUG#19)" : "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)")
437
+
438
+ nested_entity_commands << {
439
+ verb: nested_command_verb,
440
+ name: command[:name],
441
+ entity_record: Projector.rust_ident(entity[:name]),
442
+ nested_record: nested_rust_name,
443
+ # Matches commands.rb's emit_nested_entity_command naming
444
+ # exactly: dispatch_entity_#{entity.downcase}_#{nested.downcase}_#{dispatch_fn_name(cmd)}.
445
+ fn: "#{entity[:name].downcase}_#{nested[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(command[:name]))}",
446
+ args_struct: "#{nested_rust_name}#{Projector.rust_ident(command[:name])}NestedEntityArgs",
447
+ reference_checks: reference_checks(command, aggregates_by_name, unsupported_names),
448
+ reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
449
+ attributes: command[:attributes].map { |a| a[:name].to_s },
450
+ role: command[:role],
451
+ invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
452
+ entity_name: entity[:name],
453
+ entity_identity_reading: entity[:identified_by].join(", "),
454
+ nested_name: nested[:name],
455
+ nested_identity_reading: nested[:identified_by].join(", "),
456
+ # Whether the router gets a flat-args fallback branch for
457
+ # this command, or stays routed-only; both hops must
458
+ # support extract_id/extract_wants (see this loop's
459
+ # header above).
460
+ unrouted_supported: unrouted_supported,
461
+ # registry.rb builds kernel::ArgumentGates from this
462
+ # command's generated gate functions (emit_argument_gates)
463
+ # and the kernel calls them in vocabulary order, ahead of
464
+ # identity resolution.
465
+ }
466
+ end
467
+ end
468
+
469
+ # Computed once above (the nested-entities loop needs it too).
470
+ entity_router_reason = "identity #{entity[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)"
471
+ if entity_can_route
472
+ f.puts Projector.emit_extract_id(entity)
473
+ f.puts
474
+ # commands.rb's delegate_prelude/emit_entity_command and
475
+ # registry.rb's entity_arms route-less branch both address
476
+ # this entity by extract_id_lenient, not extract_id.
477
+ f.puts Projector.emit_extract_id_lenient(entity)
478
+ f.puts
479
+ f.puts Projector.emit_extract_wants(entity)
480
+ f.puts
481
+ f.puts Projector.emit_self_identity(entity)
482
+ f.puts
483
+ else
484
+ puts "skipping #{domain_name}::#{aggregate[:name]}.#{entity[:name]}'s JSON router entries: #{entity_router_reason}"
485
+ end
486
+
487
+ entity[:commands].each do |command|
488
+ entity_command_verb = "#{domain_name}::#{aggregate[:name]}.#{entity[:name]}.#{command[:name]}"
489
+ reason = Projector.entity_command_skip_reason(command, entity, value_objects_by_name)
490
+ if reason
491
+ puts "skipping #{entity_command_verb}: #{reason}"
492
+ manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: false,
493
+ gap_class: "per_instance", construct: reason.construct, reason: reason)
494
+ next
495
+ end
496
+
497
+ f.puts Projector.emit_entity_command(command, entity, aggregate, domain_name, value_objects_by_name, aggregates_by_name,
498
+ process_managers: ir[:process_managers])
499
+ f.puts
500
+
501
+ # This command's Rust function was already emitted
502
+ # unconditionally above; whether anything can dispatch to it
503
+ # depends on the entity's identity shape, not this command's
504
+ # own. `generated: true, routed: false` distinguishes a
505
+ # real-but-unreachable function from one that was never
506
+ # generated at all.
507
+ unless entity_can_route
508
+ manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: true,
509
+ routed: false, gap_class: "per_instance",
510
+ construct: "router_identity", reason: "generated as a real Rust function, but not JSON-dispatchable —#{entity_router_reason}")
511
+ next
512
+ end
513
+
514
+ manifest << manifest_entry(kind: "entity_command", id: entity_command_verb, generated: true, routed: true)
515
+
516
+ entity_commands << {
517
+ verb: entity_command_verb,
518
+ name: command[:name],
519
+ entity_record: entity_name,
520
+ # Matches commands.rb's emit_entity_command naming exactly:
521
+ # dispatch_entity_#{entity[:name].downcase}_#{dispatch_fn_name(cmd)}.
522
+ fn: "#{entity[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(command[:name]))}",
523
+ # EntityArgs, not Args — an aggregate command named after the
524
+ # entity command it delegates to would otherwise share its
525
+ # args struct's name.
526
+ args_struct: "#{entity_name}#{Projector.rust_ident(command[:name])}EntityArgs",
527
+ reference_checks: reference_checks(command, aggregates_by_name, unsupported_names),
528
+ reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
529
+ # Declared attribute names; see the identical field on
530
+ # registry_commands, above.
531
+ attributes: command[:attributes].map { |a| a[:name].to_s },
532
+ role: command[:role],
533
+ # The same VO invariant/admits/pattern checks
534
+ # emit_entity_command already bakes into dispatch_entity_*,
535
+ # computed here too so registry.rb's router can run them
536
+ # before refuse_role_mismatch/resolve_references, matching
537
+ # Ruby's own dispatch order.
538
+ invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
539
+ # entity_name/entity_identity_reading come from the entity's
540
+ # own declared name, threaded through registry.rb's dispatch
541
+ # call the same way the parent aggregate's already do.
542
+ entity_name: entity[:name],
543
+ entity_identity_reading: entity[:identified_by].join(", "),
544
+ # One generated function per declared argument-gate step,
545
+ # called in vocabulary order by
546
+ # kernel::decode_aggregate_arguments.
547
+ }
548
+ end
549
+ end
550
+
551
+ # aggregate[:attributes] plus a String-typed, always-optional
552
+ # pseudo-attribute per `projects` field: the record's
553
+ # struct/Fielded/JSON shape needs to carry a seeded projection
554
+ # like any other attribute. A command's own Args struct never
555
+ # sees this merge.
556
+ record_attributes = aggregate[:attributes] + Projector.projected_field_pseudo_attributes(aggregate)
557
+ record_for_struct = aggregate.merge(attributes: record_attributes)
558
+
559
+ f.puts Projector.emit_record(record_for_struct, value_objects_by_name)
560
+ f.puts
561
+ f.puts Projector.emit_to_json_flat(record_name, record_attributes, value_objects_by_name, optional: true, extra_fields: lifecycle_extra_field(aggregate) + Projector.corrects_extra_fields(aggregate), aggregate: aggregate)
562
+ f.puts
563
+ f.puts Projector.emit_from_json_state(record_name, record_attributes, value_objects_by_name, optional: true, extra_fields: lifecycle_extra_field(aggregate) + Projector.corrects_extra_fields(aggregate), aggregate: aggregate)
564
+ f.puts
565
+ # dispatch/dispatch_entity are generic over the record type and
566
+ # need a trait-bound to_json, since the inherent method above
567
+ # can't be called on a bare generic T. Emitted only for
568
+ # aggregate records, never value objects/entities/Args structs.
569
+ f.puts <<~RUST
570
+ impl crate::kernel::ToJson for #{record_name} {
571
+ fn to_json(&self) -> crate::kernel::Json {
572
+ #{record_name}::to_json(self)
573
+ }
574
+ }
575
+ RUST
576
+ f.puts
577
+ f.puts Projector.emit_set_projected_field(aggregate)
578
+ f.puts
579
+ f.puts Projector.emit_projected_field_table(aggregate)
580
+ f.puts
581
+ acting_router_reason = "identity #{aggregate[:identified_by].inspect} isn't a shape extract_id resolves yet (json_codec.rb)"
582
+ if can_route
583
+ f.puts Projector.emit_extract_id(aggregate)
584
+ f.puts
585
+ else
586
+ puts "skipping #{domain_name}::#{aggregate[:name]}'s JSON router acting-command entries: #{acting_router_reason}"
587
+ end
588
+
589
+ f.puts Projector.emit_invariants_fn(aggregate)
590
+ f.puts
591
+
592
+ aggregate[:commands].each do |command|
593
+ command_verb = "#{domain_name}::#{aggregate[:name]}.#{command[:name]}"
594
+ reason = Projector.command_skip_reason(command, aggregate, value_objects_by_name)
595
+ if reason
596
+ puts "skipping #{command_verb}: #{reason}"
597
+ manifest << manifest_entry(kind: "command", id: command_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
598
+ next
599
+ end
600
+
601
+ f.puts Projector.emit_command(command, aggregate, domain_name, value_objects_by_name, aggregates_by_name)
602
+ f.puts
603
+ args_struct = "#{Projector.rust_ident(command[:name])}Args"
604
+ # An event's payload is args.to_json() directly, matching what
605
+ # a policy or process-manager reaction needs to forward real
606
+ # data into a re-triggered command's from_json.
607
+ #
608
+ # Not sparse: true here — that would diverge this generator's
609
+ # output from the separate hecks-codegen reimplementation that
610
+ # spec/codegen_parity_spec.rb holds byte-identical to this one,
611
+ # across nearly every domain with an optional command attribute.
612
+ f.puts Projector.emit_to_json_flat(args_struct, command[:attributes], value_objects_by_name, sparse: true)
613
+ f.puts
614
+ allowlist = Projector.command_argument_allowlist(aggregate, command, ir[:process_managers])
615
+ f.puts Projector.emit_from_json_flat(args_struct, command[:attributes], value_objects_by_name, unknown_argument_allowlist: allowlist, command_name: command[:name].to_s, absent_argument_check: true, interleave_checks: true, aggregates_by_name: aggregates_by_name)
616
+ f.puts
617
+ # One generated function per declared argument-gate step,
618
+ # called in vocabulary order by
619
+ # kernel::decode_aggregate_arguments.
620
+ f.puts Projector.emit_argument_gates(args_struct, command[:name].to_s, command[:attributes], allowlist)
621
+ f.puts
622
+
623
+ # A creating command's identity comes from its own typed args
624
+ # (build_identity_expr, already emitted by emit_command), so
625
+ # it's routable regardless of extract_id — including when
626
+ # identity needs an extra `head:` parameter read off the raw
627
+ # JSON rather than the typed Args struct. An acting command's
628
+ # id comes from extract_id instead, so it's routable only
629
+ # when that is.
630
+ creates = Projector.creates_owner?(aggregate, command, value_objects_by_name)
631
+ # identity_components only applies to a creating command: an
632
+ # acting command reaches an existing record via
633
+ # extract_id/id_line, not by minting one, so it never needs
634
+ # these extra params.
635
+ identity_extra_params = creates ? Projector.identity_components(aggregate, command).filter_map { |c| c[:head] } : []
636
+
637
+ unless creates || can_route
638
+ puts "skipping #{command_verb}'s JSON router entry: #{acting_router_reason}"
639
+ manifest << manifest_entry(kind: "command", id: command_verb, generated: true, routed: false,
640
+ gap_class: "per_instance",
641
+ construct: "router_identity", reason: "generated as a real Rust function, but not JSON-dispatchable —#{acting_router_reason}")
642
+ next
643
+ end
644
+
645
+ manifest << manifest_entry(kind: "command", id: command_verb, generated: true, routed: true)
646
+ registry_commands << {
647
+ verb: command_verb,
648
+ name: command[:name],
649
+ fn: Projector.dispatch_fn_name(Projector.rust_ident(command[:name])),
650
+ args_struct: args_struct,
651
+ creates: creates,
652
+ identity_extra_params: identity_extra_params,
653
+ # Adds only entries reference_checks above doesn't already
654
+ # cover — a reference field redeclared under a plain value
655
+ # object, checked against the aggregate's own field of the
656
+ # same name (ADR 0037).
657
+ reference_checks: reference_checks(command, aggregates_by_name, unsupported_names) +
658
+ state_reference_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name),
659
+ # tenant_boundary_checks' own header has the full argument.
660
+ # Empty for every command outside tenant_ledger today — no
661
+ # other aggregate in the corpus declares a tenant-scoping
662
+ # query.
663
+ tenant_boundary_checks: tenant_boundary_checks(aggregate, command, aggregates_by_name, unsupported_names, value_objects_by_name),
664
+ reference_specs: Projector.reference_specs(domain_name, command[:attributes]),
665
+ # Read by reactions.rb's emit_command_attributes_table and
666
+ # ReactionInvocation.command_facts on the Ruby side.
667
+ attributes: command[:attributes].map { |a| a[:name].to_s },
668
+ role: command[:role],
669
+ # See the identical field on entity_commands, above, for the
670
+ # full reasoning.
671
+ invariant_check_lines: Projector.invariant_checks_for(command, aggregates_by_name, value_objects_by_name),
672
+ # registry.rb builds kernel::ArgumentGates from this
673
+ # command's generated gate functions (emit_argument_gates)
674
+ # and the kernel calls them in vocabulary order, ahead of
675
+ # identity resolution.
676
+ }
677
+ end
678
+
679
+ # The primary/driving half (ports.rb's own header): no Hydrate,
680
+ # no repo, so can_route never gates these — the operation's own
681
+ # reference attribute already names the record.
682
+ aggregate[:ports].each do |port|
683
+ port[:operations].each do |operation|
684
+ operation_verb = "#{domain_name}::#{aggregate[:name]}.#{port[:name]}.#{operation[:name]}"
685
+ reason = Projector.port_operation_skip_reason(operation, aggregate[:name], value_objects_by_name)
686
+ if reason
687
+ puts "skipping #{operation_verb}: #{reason}"
688
+ manifest << manifest_entry(kind: "port_operation", id: operation_verb, generated: false,
689
+ gap_class: "per_instance", construct: reason.construct, reason: reason)
690
+ next
691
+ end
692
+
693
+ f.puts Projector.emit_port_operation(operation, port[:name], aggregate[:name], domain_name, value_objects_by_name, aggregates_by_name)
694
+ f.puts
695
+
696
+ manifest << manifest_entry(kind: "port_operation", id: operation_verb, generated: true, routed: true)
697
+ operation_args_struct = "#{Projector.rust_ident(port[:name])}#{Projector.rust_ident(operation[:name])}Args"
698
+ # If this operation still declares a Reference-typed
699
+ # attribute pointing back at its own aggregate,
700
+ # emit_port_operation excludes it from the generated args
701
+ # struct (routing supplies the receiver now), so filter it
702
+ # out of reference checks too.
703
+ legacy_receiver_field = operation[:attributes]
704
+ .find { |attr| Projector.reference_target(attr[:type]) == aggregate[:name] }
705
+ &.dig(:name)
706
+ # Mirrors Dispatcher#port_invocation's additive branch: no
707
+ # Reference-typed attribute exists for a `to:`-declared
708
+ # operation, so the receiver comes from a plain fact
709
+ # attribute named for the owning aggregate's own
710
+ # identified_by field instead. Kept as a separate field
711
+ # since split_aggregate_receiver must not strip it from the
712
+ # payload — it's a real declared fact, not routing-only
713
+ # state. Uses `.split(".").first` to take just the head of
714
+ # identified_by, since a value-object-typed identity
715
+ # resolves to a dotted internal path rather than the flat
716
+ # name the operation's own attribute is actually named.
717
+ to_receiver_field = operation[:to] == aggregate[:name] ? aggregate[:identified_by]&.first&.split(".")&.first : nil
718
+ operation_reference_checks = reference_checks(operation, aggregates_by_name, unsupported_names)
719
+ .reject { |check| check[:target_name] == aggregate[:name] }
720
+ port_operations << {
721
+ verb: operation_verb,
722
+ name: operation[:name],
723
+ fn: "#{port[:name].downcase}_#{Projector.dispatch_fn_name(Projector.rust_ident(operation[:name]))}",
724
+ args_struct: operation_args_struct,
725
+ reference_checks: operation_reference_checks,
726
+ legacy_receiver_field: legacy_receiver_field,
727
+ to_receiver_field: to_receiver_field,
728
+ }
729
+ end
730
+ end
731
+ end
732
+ puts(wrote ? "wrote #{path}" : "#{path} unchanged")
733
+
734
+ registry_aggregates << {
735
+ name: aggregate[:name],
736
+ mod: aggregate[:name].downcase,
737
+ record: record_name,
738
+ commands: registry_commands,
739
+ entity_commands: entity_commands,
740
+ nested_entity_commands: nested_entity_commands,
741
+ ports: port_operations,
742
+ # Carried through verbatim; emit_identity_head_table only
743
+ # resolves the single-component case — a composite identity is
744
+ # a documented gap there, not silently assumed to work.
745
+ identified_by: aggregate[:identified_by],
746
+ # Name + identity paths only; a saga-dispatched entity command
747
+ # needs the entity's own identity, not just its parent
748
+ # aggregate's.
749
+ entities: aggregate[:entities].map { |e| { name: e[:name], identified_by: e[:identified_by] } },
750
+ # Which top-level generated module this aggregate's .rs file
751
+ # lives under, so a per-chapter registry.rs can qualify
752
+ # cross-file paths as crate::generated::#{chapter_mod}::...
753
+ # instead of a bare super::...
754
+ chapter_mod: mod_name,
755
+ # This aggregate's own declared reference_to/belongs_to
756
+ # attributes, for the domain-wide REFERENCE_TABLE and an acting
757
+ # command's owner_deref fetch — computed once here rather than
758
+ # re-derived per command.
759
+ reference_specs: Projector.reference_specs(domain_name, aggregate[:attributes]),
760
+ # The bluebook's declared name (e.g. "Governance", not the
761
+ # lowercase module), so a merged multi-chapter registry labels
762
+ # each record by its own chapter correctly.
763
+ domain_name: domain_name,
764
+ }
765
+ end
766
+
767
+ # A declared query now generates for real for the subset
768
+ # query_skip_reason admits (field-comparator conditions, order_by,
769
+ # limit, offset, a nulls override); anything else is a
770
+ # per-instance gap, not a whole-kind one, since the construct kind
771
+ # has a real code path.
772
+ query_aggregates_by_name = ir[:aggregates].to_h { |a| [a[:name], a] }
773
+ assignments_verb = Projector.provided_assignments(ir)
774
+ ir[:aggregates].each do |aggregate|
775
+ value_objects_by_name = aggregate[:value_objects].to_h { |vo| [vo[:name], vo] }
776
+
777
+ aggregate[:queries].each do |query|
778
+ query_verb = "#{domain_name}::#{aggregate[:name]}.#{query[:name]}"
779
+ reason = Projector.query_skip_reason(query, aggregate, value_objects_by_name, query_aggregates_by_name)
780
+ if reason
781
+ puts "skipping query #{query_verb}: #{reason}"
782
+ manifest << manifest_entry(kind: "query", id: query_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
783
+ next
784
+ end
785
+
786
+ manifest << manifest_entry(kind: "query", id: query_verb, generated: true)
787
+ conditions, reference_hop_conditions = Projector.query_conditions_and_hops(domain_name, query, aggregate, query_aggregates_by_name)
788
+ query_defs << {
789
+ verb: query_verb,
790
+ aggregate: "#{domain_name}::#{aggregate[:name]}",
791
+ arg_checks: Projector.query_arg_checks(query, "crate::generated::#{mod_name}::#{aggregate[:name].downcase}",
792
+ value_objects_by_name),
793
+ conditions: conditions,
794
+ reference_hop_conditions: reference_hop_conditions,
795
+ order_by: query[:order_by] ? Projector.emit_query_order_by(query[:order_by], query[:null_semantics]) : nil,
796
+ offset: query[:offset] ? Projector.emit_query_offset(query[:offset]) : nil,
797
+ limit: query[:limit] ? Projector.emit_query_limit(query[:limit]) : nil,
798
+ authorization: Projector.emit_query_authorization(query[:name], query[:authorization]),
799
+ assignments: assignments_verb == "#{aggregate[:name]}.#{query[:name]}",
800
+ }
801
+ end
802
+
803
+ # Generated when the aggregate holds the entity in a list
804
+ # (kernel::named_query::run_entity). A query on an entity nested
805
+ # inside another entity stays a recorded gap.
806
+ aggregate_id = "#{domain_name}::#{aggregate[:name]}"
807
+ aggregate[:entities].each do |entity|
808
+ list_attr = aggregate[:attributes].find { |a| a[:list] && a[:type].to_s == entity[:name].to_s }
809
+ Array(entity[:queries]).each do |query|
810
+ query_verb = "#{aggregate_id}.#{entity[:name]}.#{query[:name]}"
811
+ reason = Projector.entity_query_skip_reason(query, entity, list_attr, value_objects_by_name)
812
+ if reason
813
+ puts "skipping query #{query_verb}: #{reason}"
814
+ manifest << manifest_entry(kind: "query", id: query_verb, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
815
+ next
816
+ end
817
+
818
+ manifest << manifest_entry(kind: "query", id: query_verb, generated: true)
819
+ query_defs << {
820
+ verb: query_verb,
821
+ aggregate: aggregate_id,
822
+ entity: { list_field: list_attr[:name].to_s, parent_key: Projector.snake(aggregate[:name]),
823
+ identity_keys: Array(entity[:identified_by]).map { |path| path.to_s.split(".").first } },
824
+ arg_checks: [],
825
+ conditions: Projector.query_conditions(query),
826
+ order_by: query[:order_by] ? Projector.emit_query_order_by(query[:order_by], query[:null_semantics]) : nil,
827
+ offset: query[:offset] ? Projector.emit_query_offset(query[:offset]) : nil,
828
+ limit: query[:limit] ? Projector.emit_query_limit(query[:limit]) : nil,
829
+ authorization: nil,
830
+ }
831
+ end
832
+ manifest.concat(entity_query_entries("#{aggregate_id}.#{entity[:name]}", Array(entity[:entities])))
833
+ end
834
+ end
835
+
836
+ # A declared report block now generates for real for the subset
837
+ # read_model_skip_reason admits (a root aggregate fetched by
838
+ # reference id, plus reference-matched sibling heads — no
839
+ # where/order_by/limit). Anything else is a per-instance gap, not a
840
+ # whole-kind one, matching the query codegen above.
841
+ read_model_defs = []
842
+ ir[:read_models].each do |read_model|
843
+ read_model_id = "#{domain_name}::#{read_model[:name]}"
844
+ reason = Projector.read_model_skip_reason(read_model, aggregates_by_name, unsupported_names)
845
+ if reason
846
+ puts "skipping read_model #{read_model_id}: #{reason}"
847
+ manifest << manifest_entry(kind: "read_model", id: read_model_id, generated: false, gap_class: "per_instance", construct: reason.construct, reason: reason)
848
+ next
849
+ end
850
+
851
+ manifest << manifest_entry(kind: "read_model", id: read_model_id, generated: true)
852
+ read_model_defs << Projector.read_model_def(domain_name, read_model, aggregates_by_name)
853
+ end
854
+
855
+ # A same-domain policy generates into POLICIES and dispatches
856
+ # locally; a cross-domain policy generates into the separate
857
+ # CROSS_DOMAIN_POLICIES table instead, delivered by rust/host's
858
+ # lambda_client.rs rather than a local dispatch_by_name call. This
859
+ # manifest entry can't attest that a live cross-domain reaction's
860
+ # target actually exists — that's an operational fact about a real
861
+ # deploy, not something codegen can check.
862
+ ir[:policies].each do |policy|
863
+ manifest << manifest_entry(kind: "policy", id: "#{domain_name}::#{policy[:name]}", generated: true, routed: true)
864
+ end
865
+
866
+ # Unlike emit_policy_table above, no process manager has a
867
+ # per-instance skip condition: every declared dispatch target is
868
+ # already fully domain-qualified on the wire.
869
+ ir[:process_managers].each do |pm|
870
+ manifest << manifest_entry(kind: "process_manager", id: "#{domain_name}::#{pm[:name]}", generated: true, routed: true)
871
+ end
872
+
873
+ # Which adapter an aggregate is bound to is a deployment fact, not
874
+ # something re-derived from the bluebook's shape. Always generated
875
+ # and routed: rust/host's journal read/write path is generic over
876
+ # storage_name, and reachable outside the WASM kernel/cli.rs match
877
+ # arm entirely.
878
+ ir.fetch(:lineage, {}).fetch(:capable_aggregates, []).each do |aggregate|
879
+ manifest << manifest_entry(
880
+ kind: "lineage_aggregate", id: "#{domain_name}::#{aggregate[:name]}", generated: true, routed: true,
881
+ reason: "read via rust/host's journal::read_lineage_head_all/_by_id, written via journal::" \
882
+ "append_lineage_mutation — both generic over storage_name (\"#{aggregate[:storage_name]}\"), " \
883
+ "dispatched OUTSIDE the WASM kernel/InMemoryRepository path entirely, matching Ruby's own " \
884
+ "CommandInterpreter routing for a Postgres-bound aggregate (rust/project.rb's own header)"
885
+ )
886
+ end
887
+
888
+ metadata_path = File.join(mod_dir, "metadata.rs")
889
+ wrote = WriteIfChanged.block(metadata_path) do |f|
890
+ f.puts "// GENERATED by hecks project_rust — #{source_label}'s own canonical IR,"
891
+ f.puts "// embedded for runtime self-description. Not read by any dispatch"
892
+ f.puts "// function in this module — introspection only."
893
+ f.puts "pub const IR_JSON: &str = #{Projector.rust_string_literal(JSON.pretty_generate(ir))};"
894
+ end
895
+ puts(wrote ? "wrote #{metadata_path}" : "#{metadata_path} unchanged")
896
+
897
+ # rust/host carries no path dependency on this crate — its .wasm
898
+ # module is an opaque, untrusted artifact loaded via wasmtime, so
899
+ # metadata.rs's IR_JSON is unreachable from rust/host at compile
900
+ # time. hecks build_wasm copies this sidecar into rust/dist/ so a
901
+ # Rust-native web layer can read it at runtime via HECKS_IR_PATH.
902
+ ir_json_path = File.join(mod_dir, "ir.json")
903
+ wrote = WriteIfChanged.call(ir_json_path, JSON.pretty_generate(ir))
904
+ puts(wrote ? "wrote #{ir_json_path}" : "#{ir_json_path} unchanged")
905
+
906
+ # Sits alongside ir.json for the same reason: one is this call's
907
+ # account of what it read, the other of what it did with what it
908
+ # read. hecks rust_coverage reads both and diffs against an
909
+ # allowlist; nothing in this generator reads manifest.json back.
910
+ manifest_path = File.join(mod_dir, "manifest.json")
911
+ wrote = WriteIfChanged.call(manifest_path, JSON.pretty_generate(manifest))
912
+ puts(wrote ? "wrote #{manifest_path}" : "#{manifest_path} unchanged")
913
+
914
+ registry_path = File.join(mod_dir, "registry.rs")
915
+ wrote_registry = WriteIfChanged.block(registry_path) do |f|
916
+ f.puts Projector.emit_registry(registry_aggregates)
917
+ f.puts
918
+ f.puts Projector.emit_reference_lookup(registry_aggregates)
919
+ f.puts
920
+ f.puts Projector.emit_policy_table(domain_name, ir[:policies], ir[:aggregates])
921
+ f.puts
922
+ f.puts Projector.emit_cross_domain_policy_table(domain_name, ir[:policies])
923
+ f.puts
924
+ f.puts Projector.emit_process_manager_table(ir[:process_managers])
925
+ f.puts
926
+ f.puts Projector.emit_reference_key_table([[domain_name, generated_aggregates.map { |a| a[:name] }]])
927
+ f.puts
928
+ f.puts Projector.emit_creates_table(registry_aggregates)
929
+ f.puts
930
+ f.puts Projector.emit_identity_head_table(registry_aggregates)
931
+ f.puts
932
+ f.puts Projector.emit_entity_identity_head_table(registry_aggregates)
933
+ f.puts
934
+ f.puts Projector.emit_command_attributes_table(registry_aggregates)
935
+ f.puts
936
+ f.puts Projector.emit_query_table(query_defs)
937
+ f.puts
938
+ f.puts Projector.emit_query_arg_check_table(query_defs)
939
+ f.puts
940
+ read_model_defs.each do |rmd|
941
+ next unless rmd[:group_by_fn_body]
942
+
943
+ f.puts rmd[:group_by_fn_body]
944
+ f.puts
945
+ end
946
+ f.puts Projector.emit_read_model_table(read_model_defs)
947
+ end
948
+ puts(wrote_registry ? "wrote #{registry_path}" : "#{registry_path} unchanged")
949
+
950
+ mod_path = File.join(mod_dir, "mod.rs")
951
+ # This generator never writes `pub mod merged;` itself (`hecks project_rust`
952
+ # appends it separately, only once a chapter gets its own
953
+ # merged.rs). Re-including an already-present trailer keeps
954
+ # WriteIfChanged's before/after comparison apples-to-apples —
955
+ # without it, comparing base-only content against base-plus-merged
956
+ # content on disk would look changed every run. Requires
957
+ # merged_module too, not just presence on disk: a framework chapter
958
+ # never regenerates merged.rs, and an orphaned trailer here would
959
+ # leave a dangling `pub mod merged;` in the chapter's mod.rs once
960
+ # pruning removes the file.
961
+ merged_trailer = merged_module && File.exist?(mod_path) && File.read(mod_path).include?("pub mod merged;")
962
+ wrote_mod = WriteIfChanged.block(mod_path) do |f|
963
+ f.puts "// GENERATED by hecks project_rust — re-run it to refresh this list."
964
+ f.puts "pub mod metadata;"
965
+ f.puts "pub mod registry;"
966
+ generated_aggregates.each { |a| f.puts "pub mod #{a[:name].downcase};" }
967
+ f.puts "pub mod merged;" if merged_trailer
968
+ end
969
+ puts(wrote_mod ? "wrote #{mod_path}" : "#{mod_path} unchanged")
970
+
971
+ # Returned so hecks project_rust can concatenate :aggregates across
972
+ # every chapter a domain attaches into one merged
973
+ # Store/dispatch_by_name, and :queries the same way. Each aggregate
974
+ # already carries chapter_mod (set above); a query_def needs no
975
+ # chapter tag since its verb/aggregate are already fully
976
+ # domain-qualified.
977
+ { aggregates: registry_aggregates, queries: query_defs, read_models: read_model_defs }
978
+ end
979
+ end
980
+ end