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,966 @@
1
+ //! Command router emitter, ported from `rust/project/registry.rb`.
2
+ //! The entry structs mirror the Hashes `domain_generator.rb` accumulates while walking the IR.
3
+
4
+ use crate::exemplar::Exemplar;
5
+ use crate::naming;
6
+ use crate::reference_specs::{self, ReferenceSpec};
7
+
8
+ #[derive(Clone)]
9
+ pub struct ReferenceCheck {
10
+ pub field: String,
11
+ pub optional: bool,
12
+ /// `Some(element key expression)` for a list (`has_many`) check.
13
+ pub list_item: Option<String>,
14
+ pub target_mod: String,
15
+ pub target_name: String,
16
+ pub heads: String,
17
+ }
18
+
19
+ /// One write-side tenant boundary check (`domain_generator.rb#tenant_boundary_checks`), typed.
20
+ pub struct TenantBoundaryCheck {
21
+ pub reference_field: String,
22
+ pub target_mod: String,
23
+ pub target_name: String,
24
+ pub aggregate_name: String,
25
+ pub own_tenant_field: String,
26
+ pub own_accessor: String,
27
+ pub target_tenant_field: String,
28
+ pub target_accessor: String,
29
+ }
30
+
31
+ pub struct CommandEntry {
32
+ pub verb: String,
33
+ pub name: String,
34
+ pub fn_name: String,
35
+ pub args_struct: String,
36
+ pub creates: bool,
37
+ pub identity_extra_params: Vec<String>,
38
+ pub reference_checks: Vec<ReferenceCheck>,
39
+ /// Write-side tenant boundary checks run before dispatch.
40
+ pub tenant_boundary_checks: Vec<TenantBoundaryCheck>,
41
+ /// Reference-typed attributes, the specs behind `command_deref`.
42
+ pub reference_specs: Vec<ReferenceSpec>,
43
+ /// Declared attribute names; read by `reactions.rs#emit_command_attributes_table`.
44
+ pub attributes: Vec<String>,
45
+ /// Value-object invariant/admits/pattern checks as ready-to-splice lines, run before the
46
+ /// role and reference checks. `commands.rs::emit_command` repeats them in the dispatch fn.
47
+ pub invariant_check_lines: Vec<String>,
48
+ pub role: Option<String>,
49
+ }
50
+
51
+ pub struct EntityCommandEntry {
52
+ pub verb: String,
53
+ pub name: String,
54
+ pub entity_record: String,
55
+ pub fn_name: String,
56
+ pub args_struct: String,
57
+ pub reference_checks: Vec<ReferenceCheck>,
58
+ pub reference_specs: Vec<ReferenceSpec>,
59
+ /// Declared attribute names; see `CommandEntry`.
60
+ pub attributes: Vec<String>,
61
+ /// See `CommandEntry::invariant_check_lines`.
62
+ pub invariant_check_lines: Vec<String>,
63
+ pub role: Option<String>,
64
+ /// Carried for shape parity with `domain_generator.rb`'s `entity_commands`; unread here.
65
+ pub entity_name: String,
66
+ pub entity_identity_reading: String,
67
+ }
68
+
69
+ /// A command owned by an entity nested two levels deep (`Aggregate.Entity.Entity.Command`).
70
+ /// Adds the innermost entity's name and identity reading to `EntityCommandEntry`'s fields.
71
+ pub struct NestedEntityCommandEntry {
72
+ pub verb: String,
73
+ pub name: String,
74
+ pub entity_record: String,
75
+ pub nested_record: String,
76
+ pub fn_name: String,
77
+ pub args_struct: String,
78
+ pub reference_checks: Vec<ReferenceCheck>,
79
+ pub reference_specs: Vec<ReferenceSpec>,
80
+ pub attributes: Vec<String>,
81
+ pub invariant_check_lines: Vec<String>,
82
+ pub role: Option<String>,
83
+ pub entity_name: String,
84
+ pub entity_identity_reading: String,
85
+ pub nested_name: String,
86
+ pub nested_identity_reading: String,
87
+ /// Whether the router gets a flat-args (`None`) fallback for this command; true only when both
88
+ /// hops' identity is `extract_id`-supported.
89
+ pub unrouted_supported: bool,
90
+ }
91
+
92
+ pub struct PortEntry {
93
+ pub verb: String,
94
+ pub name: String,
95
+ pub fn_name: String,
96
+ pub args_struct: String,
97
+ pub reference_checks: Vec<ReferenceCheck>,
98
+ /// Self-reference field from older port IR; new operations take the owner from the route.
99
+ pub legacy_receiver_field: Option<String>,
100
+ /// Receiver field of `to:`-declared operations; a real attribute, kept in the payload.
101
+ pub to_receiver_field: Option<String>,
102
+ }
103
+
104
+ /// A nested entity's name and identity paths, from `domain_generator.rb`'s `entities:` field.
105
+ /// `reactions.rs#emit_entity_identity_head_table` reads the single-component case.
106
+ pub struct EntityIdentityEntry {
107
+ pub name: String,
108
+ pub identified_by: Vec<String>,
109
+ }
110
+
111
+ pub struct AggregateEntry {
112
+ pub name: String,
113
+ pub module_name: String,
114
+ pub record: String,
115
+ pub commands: Vec<CommandEntry>,
116
+ pub entity_commands: Vec<EntityCommandEntry>,
117
+ pub nested_entity_commands: Vec<NestedEntityCommandEntry>,
118
+ pub ports: Vec<PortEntry>,
119
+ pub chapter_mod: String,
120
+ pub domain_name: String,
121
+ /// Reference-typed attributes; this aggregate's row in `REFERENCE_TABLE`.
122
+ pub reference_specs: Vec<ReferenceSpec>,
123
+ /// Declared identity paths; `reactions.rs#emit_identity_head_table` reads one-component ones.
124
+ pub identified_by: Vec<String>,
125
+ /// Nested entities, read by `reactions.rs#emit_entity_identity_head_table`.
126
+ pub entities: Vec<EntityIdentityEntry>,
127
+ }
128
+
129
+ pub fn emit_role_check(
130
+ exemplar: &Exemplar,
131
+ role: Option<&str>,
132
+ command_name: &str,
133
+ ) -> Option<String> {
134
+ let role = role?;
135
+ Some(exemplar.render(
136
+ "role_check",
137
+ &[
138
+ ("\"TmplRole\"", naming::ruby_inspect_string(role)),
139
+ (
140
+ "\"TmplCommandName\"",
141
+ naming::ruby_inspect_string(command_name),
142
+ ),
143
+ ],
144
+ ))
145
+ }
146
+
147
+ /// Port of `rust/project/registry.rb#emit_tenant_boundary_check`.
148
+ /// Hand-built: the target accessor differs for a single-attribute value object and a bare scalar.
149
+ pub fn emit_tenant_boundary_check(check: &TenantBoundaryCheck) -> String {
150
+ let ref_ident = naming::rust_ident_field(&check.reference_field);
151
+ let own_expr = format!("args.{}.clone()", naming::rust_ident_field(&check.own_accessor));
152
+ let target_expr = match check.target_accessor.split_once('.') {
153
+ Some((head, inner)) => format!(
154
+ "record.{}.as_ref().map(|v| v.{}.clone())",
155
+ naming::rust_ident_field(head),
156
+ naming::rust_ident_field(inner)
157
+ ),
158
+ None => format!("record.{}.clone()", naming::rust_ident_field(&check.target_accessor)),
159
+ };
160
+
161
+ let mut out = String::new();
162
+ out.push_str(&format!("if let Some(record) = store.{}.find(&args.{ref_ident}) {{ ", check.target_mod));
163
+ out.push_str(&format!("if let Some(target_tenant) = {target_expr} {{ "));
164
+ out.push_str(&format!("let own_tenant = {own_expr}; "));
165
+ out.push_str("if target_tenant != own_tenant { ");
166
+ out.push_str(
167
+ "return Err(crate::kernel::Refusal::Unauthorized(crate::kernel::refusal_wording::UnauthorizedCrossTenantReferenceArgs { ",
168
+ );
169
+ out.push_str(&format!("aggregate: {}, ", naming::ruby_inspect_string(&check.aggregate_name)));
170
+ out.push_str(&format!("field: {}, ", naming::ruby_inspect_string(&check.own_tenant_field)));
171
+ out.push_str("tenant: &format!(\"{:?}\", own_tenant), ");
172
+ out.push_str(&format!("attribute: {}, ", naming::ruby_inspect_string(&check.reference_field)));
173
+ out.push_str(&format!("target: {}, ", naming::ruby_inspect_string(&check.target_name)));
174
+ out.push_str(&format!("target_field: {}, ", naming::ruby_inspect_string(&check.target_tenant_field)));
175
+ out.push_str("other: &format!(\"{:?}\", target_tenant)");
176
+ out.push_str(" }.render_args())); ");
177
+ out.push_str("} } }");
178
+ out
179
+ }
180
+
181
+ /// `kernel::ArgumentGates` literal for one command (`registry.rb#emit_argument_gates_literal`).
182
+ /// The last two fields are closures because they need `store`, which exists only at the router.
183
+ fn emit_argument_gates_literal(args_path: &str, invariant_check_lines: &[String], role_line: Option<String>, reference_lines: &[String]) -> String {
184
+ let mut normalize = vec![format!("let args = {args_path}::from_json(v)?;")];
185
+ normalize.extend(invariant_check_lines.iter().map(|l| squeeze(l)));
186
+ normalize.push("Ok(args)".to_string());
187
+
188
+ let role = match role_line.filter(|l| !l.is_empty()) {
189
+ Some(line) => format!("&|| {{ {} Ok(()) }}", squeeze(&line)),
190
+ None => "&|| Ok(())".to_string(),
191
+ };
192
+ let references: Vec<String> = reference_lines.iter().filter(|l| !l.is_empty()).map(|l| squeeze(l)).collect();
193
+ let references = if references.is_empty() {
194
+ format!("&|_args: &{args_path}| Ok(())")
195
+ } else {
196
+ format!("&|args: &{args_path}| {{ {} Ok(()) }}", references.join(" "))
197
+ };
198
+
199
+ format!(
200
+ "crate::kernel::ArgumentGates {{ decode_arguments: &{args_path}::decode_arguments, \
201
+ refuse_unknown_arguments: &{args_path}::refuse_unknown_arguments, \
202
+ refuse_absent_arguments: &{args_path}::refuse_absent_arguments, \
203
+ normalize_args: &|v: &crate::kernel::Json| {{ {} }}, refuse_role_mismatch: {role}, resolve_references: {references} }}",
204
+ normalize.join(" ")
205
+ )
206
+ }
207
+
208
+ /// Joins a multi-line emitted check onto one line so it can sit inside a closure expression.
209
+ fn squeeze(text: &str) -> String {
210
+ text.lines().map(str::trim).filter(|l| !l.is_empty()).collect::<Vec<_>>().join(" ")
211
+ }
212
+
213
+ pub fn emit_reference_check(exemplar: &Exemplar, check: &ReferenceCheck) -> String {
214
+ let ident = naming::rust_ident_field(&check.field);
215
+ if let Some(list_item) = &check.list_item {
216
+ exemplar.render(
217
+ "reference_check_list",
218
+ &[
219
+ (
220
+ "\"TmplTarget\"",
221
+ naming::ruby_inspect_string(&check.target_name),
222
+ ),
223
+ ("\"tmpl_heads\"", naming::ruby_inspect_string(&check.heads)),
224
+ ("tmpl_target_mod", check.target_mod.clone()),
225
+ ("tmpl_list_field", ident),
226
+ ("&item.tmpl_element_field", list_item.clone()),
227
+ ],
228
+ )
229
+ } else if check.optional {
230
+ exemplar.render(
231
+ "reference_check_optional",
232
+ &[
233
+ (
234
+ "\"TmplTarget\"",
235
+ naming::ruby_inspect_string(&check.target_name),
236
+ ),
237
+ ("\"tmpl_heads\"", naming::ruby_inspect_string(&check.heads)),
238
+ ("tmpl_target_mod", check.target_mod.clone()),
239
+ ("tmpl_optional_field", ident),
240
+ ],
241
+ )
242
+ } else {
243
+ exemplar.render(
244
+ "reference_check_required",
245
+ &[
246
+ (
247
+ "\"TmplTarget\"",
248
+ naming::ruby_inspect_string(&check.target_name),
249
+ ),
250
+ ("\"tmpl_heads\"", naming::ruby_inspect_string(&check.heads)),
251
+ ("tmpl_target_mod", check.target_mod.clone()),
252
+ ("tmpl_field", ident),
253
+ ],
254
+ )
255
+ }
256
+ }
257
+
258
+ fn chapter_path(a: &AggregateEntry) -> String {
259
+ format!("crate::generated::{}::{}", a.chapter_mod, a.module_name)
260
+ }
261
+
262
+ pub fn emit_registry(exemplar: &Exemplar, aggregates: &[AggregateEntry]) -> String {
263
+ let store_fields: Vec<String> = aggregates
264
+ .iter()
265
+ .map(|a| {
266
+ format!(
267
+ " pub {}: crate::kernel::InMemoryRepository<{}::{}>,",
268
+ a.module_name,
269
+ chapter_path(a),
270
+ a.record
271
+ )
272
+ })
273
+ .collect();
274
+ let store_inits: Vec<String> = aggregates
275
+ .iter()
276
+ .map(|a| {
277
+ format!(
278
+ " {}: crate::kernel::InMemoryRepository::new(),",
279
+ a.module_name
280
+ )
281
+ })
282
+ .collect();
283
+
284
+ let dump_arms: Vec<String> = aggregates
285
+ .iter()
286
+ .map(|a| {
287
+ let prefix = format!("{}::{}#", a.domain_name, a.name);
288
+ format!(
289
+ "for (id, record) in self.{}.entries() {{\n instances.push((format!(\"{{}}{{}}\", {}, id), record.to_json()));\n}}",
290
+ a.module_name,
291
+ naming::ruby_inspect_string(&prefix)
292
+ )
293
+ })
294
+ .collect();
295
+
296
+ let seed_arms: Vec<String> = aggregates
297
+ .iter()
298
+ .map(|a| {
299
+ let mod_path = chapter_path(a);
300
+ let prefix = format!("{}::{}#", a.domain_name, a.name);
301
+ format!(
302
+ "if let Some(id) = key.strip_prefix({}) {{\n store.{}.save(id, {mod_path}::{}::from_json(value)?);\n continue;\n}}",
303
+ naming::ruby_inspect_string(&prefix),
304
+ a.module_name,
305
+ a.record
306
+ )
307
+ })
308
+ .collect();
309
+
310
+ let query_arms: Vec<String> = aggregates
311
+ .iter()
312
+ .map(|a| {
313
+ let prefix = format!("{}::{}", a.domain_name, a.name);
314
+ format!(
315
+ "if aggregate == {} {{\n return Some(self.{}.json_entries(|record| record.to_json()).map(|(id, json)| (id.clone(), json.clone())).collect());\n}}",
316
+ naming::ruby_inspect_string(&prefix),
317
+ a.module_name
318
+ )
319
+ })
320
+ .collect();
321
+
322
+ let scan_each_arms: Vec<String> = aggregates
323
+ .iter()
324
+ .map(|a| {
325
+ let prefix = format!("{}::{}", a.domain_name, a.name);
326
+ format!(
327
+ "if aggregate == {} {{\n for (id, json) in self.{}.json_entries(|record| record.to_json()) {{\n visit(id, json);\n }}\n return true;\n}}",
328
+ naming::ruby_inspect_string(&prefix),
329
+ a.module_name
330
+ )
331
+ })
332
+ .collect();
333
+
334
+ let mut aggregate_arms: Vec<String> = Vec::new();
335
+ for a in aggregates {
336
+ let mod_path = chapter_path(a);
337
+ for c in &a.commands {
338
+ let extra_names = &c.identity_extra_params;
339
+ let extra_idents: Vec<String> = extra_names
340
+ .iter()
341
+ .map(|n| naming::rust_ident_field(n))
342
+ .collect();
343
+ let extra_lines: Vec<String> = extra_names
344
+ .iter()
345
+ .zip(extra_idents.iter())
346
+ .map(|(name, ident)| {
347
+ let msg = format!("{} creates a {} — pass {name}", c.verb, a.record);
348
+ format!(
349
+ "let {ident} = facts_json.dig({}).ok_or_else(|| crate::kernel::Refusal::NotFound({}.to_string()))?.to_id_component()?;",
350
+ naming::ruby_inspect_string(name),
351
+ naming::ruby_inspect_string(&msg)
352
+ )
353
+ })
354
+ .collect();
355
+ let extra_pass: String = extra_idents
356
+ .iter()
357
+ .map(|ident| format!("&{ident}, "))
358
+ .collect();
359
+
360
+ // A creating command's dispatch fn takes `route` so it can tell create from find.
361
+ let dispatch_call = format!(
362
+ "{mod_path}::dispatch_{}(&mut store.{}, {}args, mutations, owner_deref, command_deref, tenant_boundary_check)",
363
+ c.fn_name,
364
+ a.module_name,
365
+ if c.creates { format!("route, {extra_pass}") } else { "&id, ".to_string() }
366
+ );
367
+ // `extract_id` has no command context to render `NotFoundActingNoIdentity`, so its Err
368
+ // is wrapped here into the wording Ruby's `acting_no_identity` refusal produces.
369
+ let not_found_expr = |command: &str, aggregate: &str, identity: &str| {
370
+ format!(
371
+ "crate::kernel::Refusal::NotFound(crate::kernel::refusal_wording::NotFoundActingNoIdentityArgs {{ command: {}, aggregate: {}, identity: {} }}.render_args())",
372
+ naming::ruby_inspect_string(command),
373
+ naming::ruby_inspect_string(aggregate),
374
+ naming::ruby_inspect_string(identity)
375
+ )
376
+ };
377
+ // Route depth is checked eagerly. With an explicit `with:` Ruby validates facts before
378
+ // the route, so `args_line` picks the order at runtime. Identity follows every gate.
379
+ let route_precheck_line =
380
+ "if let Some(route) = route { route.require_depth(0)?; }".to_string();
381
+ let id_line = if c.creates {
382
+ String::new()
383
+ } else {
384
+ let not_found = not_found_expr(&c.name, &a.record, &a.identified_by.join(", "));
385
+ format!(
386
+ "let id = match route {{ Some(route) => route.aggregate().to_string(), None => {mod_path}::{}::extract_id(facts_json).map_err(|_| {not_found})?, }};",
387
+ a.record
388
+ )
389
+ };
390
+ // The argument gates go to the kernel, which runs them in `AggregateStep::ORDER`.
391
+ let gates_expr = format!(
392
+ "crate::kernel::decode_aggregate_arguments(facts_json, &{})?",
393
+ emit_argument_gates_literal(
394
+ &format!("{mod_path}::{}", c.args_struct),
395
+ &c.invariant_check_lines,
396
+ emit_role_check(exemplar, c.role.as_deref(), &c.name),
397
+ &c
398
+ .reference_checks
399
+ .iter()
400
+ .map(|check| emit_reference_check(exemplar, check))
401
+ .collect::<Vec<_>>()
402
+ )
403
+ );
404
+ // One `if` expression, so exactly one of the two orders runs per call.
405
+ let args_line = format!(
406
+ "let args = if invocation.explicit_with() {{ let args = {gates_expr}; {route_precheck_line} args }} \
407
+ else {{ {route_precheck_line} {gates_expr} }};"
408
+ );
409
+
410
+ // Checks come from `ir.json` at codegen time; none means `Ok(())`, else one closure.
411
+ let tenant_boundary_check_bodies: Vec<String> = c
412
+ .tenant_boundary_checks
413
+ .iter()
414
+ .map(emit_tenant_boundary_check)
415
+ .collect();
416
+ let tenant_boundary_check_line = if tenant_boundary_check_bodies.is_empty() {
417
+ "let tenant_boundary_check: Result<(), crate::kernel::Refusal> = Ok(());".to_string()
418
+ } else {
419
+ format!(
420
+ "let tenant_boundary_check: Result<(), crate::kernel::Refusal> = \
421
+ (|| -> Result<(), crate::kernel::Refusal> {{ {} Ok(()) }})();",
422
+ tenant_boundary_check_bodies.join(" ")
423
+ )
424
+ };
425
+
426
+ // Resolved before the `&mut store.<mod>` borrow below; it needs `store` whole.
427
+ let owner_deref_expr = if c.creates {
428
+ "Vec::new()".to_string()
429
+ } else {
430
+ format!(
431
+ "crate::kernel::owner_deref(&*store, REFERENCE_TABLE, {}, &id)",
432
+ naming::ruby_inspect_string(&format!("{}::{}", a.domain_name, a.name))
433
+ )
434
+ };
435
+ let deref_lines = vec![
436
+ format!("let owner_deref = {owner_deref_expr};"),
437
+ format!(
438
+ "let command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, {}, &args);",
439
+ reference_specs::emit_reference_specs_literal(&c.reference_specs)
440
+ ),
441
+ ];
442
+
443
+ let mut body: Vec<String> = vec![
444
+ "let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;"
445
+ .to_string(),
446
+ "let route = invocation.route();".to_string(),
447
+ "let facts_json = invocation.facts();".to_string(),
448
+ ];
449
+ body.push(args_line);
450
+ // Identity after the gates; `extra_lines` read identity-extra heads, so they follow.
451
+ if !id_line.is_empty() {
452
+ body.push(id_line);
453
+ }
454
+ body.extend(extra_lines);
455
+ body.push(tenant_boundary_check_line);
456
+ body.extend(deref_lines);
457
+ body.push(
458
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());"
459
+ .to_string(),
460
+ );
461
+ body.push(format!(
462
+ "{dispatch_call}.map(|(_, events)| stamp_payload(events, &payload))"
463
+ ));
464
+
465
+ aggregate_arms.push(format!(
466
+ " {} => {{\n{}\n }}",
467
+ naming::ruby_inspect_string(&c.verb),
468
+ body.iter()
469
+ .map(|l| format!(" {l}"))
470
+ .collect::<Vec<_>>()
471
+ .join("\n")
472
+ ));
473
+ }
474
+ }
475
+
476
+ let mut entity_arms: Vec<String> = Vec::new();
477
+ for a in aggregates {
478
+ let mod_path = chapter_path(a);
479
+ for c in &a.entity_commands {
480
+ // The argument gates; `kernel::decode_entity_arguments` walks `EntityStep::ORDER`.
481
+ let gates_line = format!(
482
+ "let args = crate::kernel::decode_entity_arguments(facts_json, &{})?;",
483
+ emit_argument_gates_literal(
484
+ &format!("{mod_path}::{}", c.args_struct),
485
+ &c.invariant_check_lines,
486
+ emit_role_check(exemplar, c.role.as_deref(), &c.name),
487
+ &c
488
+ .reference_checks
489
+ .iter()
490
+ .map(|check| emit_reference_check(exemplar, check))
491
+ .collect::<Vec<_>>()
492
+ )
493
+ );
494
+ let dispatch_call = format!(
495
+ "{mod_path}::dispatch_entity_{}(&mut store.{}, &parent_id, &element_id, &element_wants, args, mutations, owner_deref, command_deref).map(|(_, events)| stamp_payload(events, &payload))",
496
+ c.fn_name, a.module_name
497
+ );
498
+
499
+ // Gates run before identity for routed and unrouted calls alike; the eager
500
+ // `route.require_depth(1)?` makes a wrong-depth `to:` refuse first, as in Ruby.
501
+ // A missing identity is wrapped as NotFound, as in the aggregate arm's `id_line`.
502
+ let entity_parent_no_identity_message = format!(
503
+ "{} acts on a {}'s {} — pass {}:",
504
+ c.name, a.record, c.entity_name, a.identified_by.join(", ")
505
+ );
506
+ let entity_element_no_identity_message = format!(
507
+ "{} acts on one {} — pass {}:",
508
+ c.name, c.entity_name, c.entity_identity_reading
509
+ );
510
+ // `element_id` uses `extract_id_lenient` (absent vs blank element identity);
511
+ // `parent_id` stays strict.
512
+ let body_entity_match = format!(
513
+ "let (parent_id, element_id, element_wants) = match route {{ Some(route) => {{ let element_id = route.entities()[0].clone(); (route.aggregate().to_string(), element_id.clone(), element_id) }}, None => {{ let parent_id = {mod_path}::{}::extract_id(facts_json).map_err(|_| crate::kernel::Refusal::NotFound({}.to_string()))?; let element_id = {mod_path}::{}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound({}.to_string()))?; let element_wants = {mod_path}::{}::extract_wants(facts_json); (parent_id, element_id, element_wants) }}, }};",
514
+ a.record,
515
+ naming::ruby_inspect_string(&entity_parent_no_identity_message),
516
+ c.entity_record,
517
+ naming::ruby_inspect_string(&entity_element_no_identity_message),
518
+ c.entity_record
519
+ );
520
+
521
+ let mut body: Vec<String> = vec![
522
+ "let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;".to_string(),
523
+ "let route = invocation.route();".to_string(),
524
+ "let facts_json = invocation.facts();".to_string(),
525
+ "if let Some(route) = route { route.require_depth(1)?; }".to_string(),
526
+ gates_line,
527
+ body_entity_match,
528
+ ];
529
+ // `owner_deref` dereferences the parent's reference fields off `parent_id`, so
530
+ // `seeded_projections` can re-seed them on every entity-command save. `command_deref`
531
+ // also carries the parent's dereferenced state under one "parent" key.
532
+ body.push(format!(
533
+ "let owner_deref = crate::kernel::owner_deref(&*store, REFERENCE_TABLE, {}, &parent_id);",
534
+ naming::ruby_inspect_string(&format!("{}::{}", a.domain_name, a.name))
535
+ ));
536
+ body.push(format!(
537
+ "let mut command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, {}, &args);",
538
+ reference_specs::emit_reference_specs_literal(&c.reference_specs)
539
+ ));
540
+ body.push(format!(
541
+ "if let Some(parent_node) = crate::kernel::parent_deref(&*store, REFERENCE_TABLE, {}, &parent_id) {{ command_deref.push((\"parent\", parent_node)); }}",
542
+ naming::ruby_inspect_string(&format!("{}::{}", a.domain_name, a.name))
543
+ ));
544
+ body.push(
545
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());"
546
+ .to_string(),
547
+ );
548
+ body.push(dispatch_call);
549
+
550
+ entity_arms.push(format!(
551
+ " {} => {{\n{}\n }}",
552
+ naming::ruby_inspect_string(&c.verb),
553
+ body.iter()
554
+ .map(|l| format!(" {l}"))
555
+ .collect::<Vec<_>>()
556
+ .join("\n")
557
+ ));
558
+ }
559
+ }
560
+
561
+ // Commands owned by an entity nested two levels deep. `c.unrouted_supported` selects a binding
562
+ // that accepts a route or flat args; otherwise the command is routed-only.
563
+ let mut nested_entity_arms: Vec<String> = Vec::new();
564
+ for a in aggregates {
565
+ let mod_path = chapter_path(a);
566
+ for c in &a.nested_entity_commands {
567
+ // The argument gates; see the entity arm's `gates_line`.
568
+ let gates_line = format!(
569
+ "let args = crate::kernel::decode_entity_arguments(facts_json, &{})?;",
570
+ emit_argument_gates_literal(
571
+ &format!("{mod_path}::{}", c.args_struct),
572
+ &c.invariant_check_lines,
573
+ emit_role_check(exemplar, c.role.as_deref(), &c.name),
574
+ &c
575
+ .reference_checks
576
+ .iter()
577
+ .map(|check| emit_reference_check(exemplar, check))
578
+ .collect::<Vec<_>>()
579
+ )
580
+ );
581
+ let dispatch_call = format!(
582
+ "{mod_path}::dispatch_entity_{}(&mut store.{}, &parent_id, &hop1_id, &hop1_wants, &hop2_id, &hop2_wants, args, mutations, owner_deref, command_deref).map(|(_, events)| stamp_payload(events, &payload))",
583
+ c.fn_name, a.module_name
584
+ );
585
+
586
+ // Gates before identity, as in `entity_arms`; `route.require_depth(2)?` refuses a
587
+ // wrong-depth route first. A missing identity wraps into a per-hop NotFound message.
588
+ let entity_parent_no_identity_message = format!(
589
+ "{} acts on a {}'s {}.{} — pass {}:",
590
+ c.name, a.record, c.entity_name, c.nested_name, a.identified_by.join(", ")
591
+ );
592
+ let hop1_no_identity_message = format!(
593
+ "{} acts on one {} — pass {}:",
594
+ c.name, c.entity_name, c.entity_identity_reading
595
+ );
596
+ let hop2_no_identity_message = format!(
597
+ "{} acts on one {} — pass {}:",
598
+ c.name, c.nested_name, c.nested_identity_reading
599
+ );
600
+ // Both hops resolve through `extract_id_lenient`; `parent_id` stays strict.
601
+ let route_binding = if c.unrouted_supported {
602
+ format!(
603
+ "let (parent_id, hop1_id, hop1_wants, hop2_id, hop2_wants) = match route {{ Some(route) => {{ let hop1_id = route.entities()[0].clone(); let hop2_id = route.entities()[1].clone(); (route.aggregate().to_string(), hop1_id.clone(), hop1_id, hop2_id.clone(), hop2_id) }}, None => {{ let parent_id = {mod_path}::{}::extract_id(facts_json).map_err(|_| crate::kernel::Refusal::NotFound({}.to_string()))?; let hop1_id = {mod_path}::{}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound({}.to_string()))?; let hop1_wants = {mod_path}::{}::extract_wants(facts_json); let hop2_id = {mod_path}::{}::extract_id_lenient(facts_json).map_err(|_| crate::kernel::Refusal::NotFound({}.to_string()))?; let hop2_wants = {mod_path}::{}::extract_wants(facts_json); (parent_id, hop1_id, hop1_wants, hop2_id, hop2_wants) }}, }};",
604
+ a.record,
605
+ naming::ruby_inspect_string(&entity_parent_no_identity_message),
606
+ c.entity_record,
607
+ naming::ruby_inspect_string(&hop1_no_identity_message),
608
+ c.entity_record,
609
+ c.nested_record,
610
+ naming::ruby_inspect_string(&hop2_no_identity_message),
611
+ c.nested_record
612
+ )
613
+ } else {
614
+ let route_error = format!(
615
+ "{} addresses an entity nested two levels deep — requires an explicit to: {{ aggregate:, entities: [...] }} route",
616
+ c.verb
617
+ );
618
+ format!(
619
+ "let route = route.ok_or_else(|| crate::kernel::Refusal::TypeMismatch({}.to_string()))?; let parent_id = route.aggregate().to_string(); let hop1_id = route.entities()[0].clone(); let hop2_id = route.entities()[1].clone(); let hop1_wants = hop1_id.clone(); let hop2_wants = hop2_id.clone();",
620
+ naming::ruby_inspect_string(&route_error)
621
+ )
622
+ };
623
+
624
+ let mut body: Vec<String> = vec![
625
+ "let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;".to_string(),
626
+ "let route = invocation.route();".to_string(),
627
+ "let facts_json = invocation.facts();".to_string(),
628
+ ];
629
+ body.push("if let Some(route) = route { route.require_depth(2)?; }".to_string());
630
+ body.push(gates_line);
631
+ body.push(route_binding);
632
+ // `owner_deref` uses the top-level aggregate's reference fields, since projection
633
+ // seeding scopes to it, not to the nested entity.
634
+ body.push(format!(
635
+ "let owner_deref = crate::kernel::owner_deref(&*store, REFERENCE_TABLE, {}, &parent_id);",
636
+ naming::ruby_inspect_string(&format!("{}::{}", a.domain_name, a.name))
637
+ ));
638
+ body.push(format!(
639
+ "let command_deref = crate::kernel::command_deref(&*store, REFERENCE_TABLE, {}, &args);",
640
+ reference_specs::emit_reference_specs_literal(&c.reference_specs)
641
+ ));
642
+ body.push(
643
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());"
644
+ .to_string(),
645
+ );
646
+ body.push(dispatch_call);
647
+
648
+ nested_entity_arms.push(format!(
649
+ " {} => {{\n{}\n }}",
650
+ naming::ruby_inspect_string(&c.verb),
651
+ body.iter()
652
+ .map(|l| format!(" {l}"))
653
+ .collect::<Vec<_>>()
654
+ .join("\n")
655
+ ));
656
+ }
657
+ }
658
+
659
+ let mut port_arms: Vec<String> = Vec::new();
660
+ for a in aggregates {
661
+ let mod_path = chapter_path(a);
662
+ for p in &a.ports {
663
+ let reference_lines: Vec<String> = p
664
+ .reference_checks
665
+ .iter()
666
+ .map(|check| emit_reference_check(exemplar, check))
667
+ .collect();
668
+ let dispatch_call = format!("{mod_path}::dispatch_operation_{}(&id, args).map(|events| stamp_payload(events, &payload))", p.fn_name);
669
+ let legacy_receiver = p
670
+ .legacy_receiver_field
671
+ .as_deref()
672
+ .map(naming::ruby_inspect_string)
673
+ .map(|field| format!("Some({field})"))
674
+ .unwrap_or_else(|| "None".to_string());
675
+ let to_receiver = p
676
+ .to_receiver_field
677
+ .as_deref()
678
+ .map(naming::ruby_inspect_string)
679
+ .map(|field| format!("Some({field})"))
680
+ .unwrap_or_else(|| "None".to_string());
681
+
682
+ let mut body: Vec<String> = vec![
683
+ "let invocation = crate::kernel::CommandInvocation::from_json(args_json)?;".to_string(),
684
+ format!("let (id, port_facts) = invocation.split_aggregate_receiver({legacy_receiver}, {to_receiver})?;"),
685
+ "let facts_json = &port_facts;".to_string(),
686
+ format!(
687
+ "let _instance = store.{}.find(&id).ok_or_else(|| crate::kernel::Refusal::NotFound(format!(\"{} {{:?}} does not exist\", id)))?;",
688
+ a.module_name, a.name
689
+ ),
690
+ format!("let args = {mod_path}::{}::from_json(facts_json)?;", p.args_struct),
691
+ ];
692
+ body.extend(reference_lines);
693
+ body.push(
694
+ "let payload = crate::kernel::Json::overlay(facts_json, &args.to_json());"
695
+ .to_string(),
696
+ );
697
+ body.push(dispatch_call);
698
+
699
+ port_arms.push(format!(
700
+ " {} => {{\n{}\n }}",
701
+ naming::ruby_inspect_string(&p.verb),
702
+ body.iter()
703
+ .map(|l| format!(" {l}"))
704
+ .collect::<Vec<_>>()
705
+ .join("\n")
706
+ ));
707
+ }
708
+ }
709
+
710
+ let mut dispatch_arms = aggregate_arms;
711
+ dispatch_arms.extend(entity_arms);
712
+ dispatch_arms.extend(nested_entity_arms);
713
+ dispatch_arms.extend(port_arms);
714
+
715
+ let header = "// GENERATED by hecks project_rust — the JSON command router\n// `kernel::cli` dispatches every step through. Do not hand-edit —\n// re-run hecks project_rust instead.\n#![allow(dead_code, unused_variables)]\n\n// `Repository::save` (from_seed, below) is a TRAIT method —\n// `InMemoryRepository`'s own inherent methods (entries(), used\n// by instances()) need no import, but save() does.\nuse crate::kernel::Repository;\n\n";
716
+
717
+ let body = exemplar.render(
718
+ "registry_file",
719
+ &[
720
+ ("TmplStore2", "Store".to_string()),
721
+ (
722
+ " pub tmpl_field: crate::kernel::InMemoryRepository<i64>,",
723
+ store_fields.join("\n"),
724
+ ),
725
+ (
726
+ " tmpl_field: tmpl_store_fields_placeholder(),",
727
+ store_inits.join("\n"),
728
+ ),
729
+ ("tmpl_dump_arm_placeholder();", dump_arms.join("\n")),
730
+ ("tmpl_seed_arm_placeholder();", seed_arms.join("\n")),
731
+ ("tmpl_query_arm_placeholder();", query_arms.join("\n")),
732
+ ("tmpl_scan_each_arm_placeholder();", scan_each_arms.join("\n")),
733
+ (
734
+ "\"tmpl_verb\" => { tmpl_dispatch_arm_placeholder() }",
735
+ dispatch_arms.join("\n"),
736
+ ),
737
+ ],
738
+ );
739
+
740
+ format!("{header}{body}")
741
+ }
742
+
743
+ /// Port of `rust/project/registry.rb#emit_reference_table`.
744
+ pub fn emit_reference_table(aggregates: &[AggregateEntry]) -> String {
745
+ let rows: Vec<String> = aggregates
746
+ .iter()
747
+ .map(|a| {
748
+ let qualified = format!("{}::{}", a.domain_name, a.name);
749
+ format!(
750
+ " ({}, {}),",
751
+ naming::ruby_inspect_string(&qualified),
752
+ reference_specs::emit_reference_specs_literal(&a.reference_specs)
753
+ )
754
+ })
755
+ .collect();
756
+
757
+ format!(
758
+ "pub static REFERENCE_TABLE: crate::kernel::ReferenceTable = &[\n{}\n];\n",
759
+ rows.join("\n")
760
+ )
761
+ }
762
+
763
+ /// Port of `rust/project/registry.rb#emit_reference_lookup`.
764
+ pub fn emit_reference_lookup(aggregates: &[AggregateEntry]) -> String {
765
+ let arms: Vec<String> = aggregates
766
+ .iter()
767
+ .map(|a| {
768
+ let prefix = format!("{}::{}", a.domain_name, a.name);
769
+ format!(
770
+ "if target == {} {{\n return self.{}.find(id).map(|r| Box::new(r) as Box<dyn crate::kernel::Fielded>);\n}}",
771
+ naming::ruby_inspect_string(&prefix),
772
+ a.module_name
773
+ )
774
+ })
775
+ .collect();
776
+
777
+ format!(
778
+ "{}\nimpl crate::kernel::ReferenceLookup for Store {{\n fn find_fielded(&self, target: &str, id: &str) -> Option<Box<dyn crate::kernel::Fielded>> {{\n{}\n None\n }}\n}}\n",
779
+ emit_reference_table(aggregates),
780
+ arms.join("\n")
781
+ )
782
+ }
783
+
784
+ #[cfg(test)]
785
+ mod tests {
786
+ use super::*;
787
+
788
+ #[test]
789
+ fn generated_router_separates_routes_and_supports_compound_create_facts() {
790
+ let aggregate = AggregateEntry {
791
+ name: "SafeDepositBox".to_string(),
792
+ module_name: "safedepositbox".to_string(),
793
+ record: "SafeDepositBox".to_string(),
794
+ commands: vec![
795
+ CommandEntry {
796
+ verb: "Banking::SafeDepositBox.Rent".to_string(),
797
+ name: "Rent".to_string(),
798
+ fn_name: "rent".to_string(),
799
+ args_struct: "RentArgs".to_string(),
800
+ creates: true,
801
+ identity_extra_params: vec![
802
+ "branch_code".to_string(),
803
+ "box_number".to_string(),
804
+ ],
805
+ reference_checks: Vec::new(),
806
+ tenant_boundary_checks: Vec::new(),
807
+ reference_specs: Vec::new(),
808
+ attributes: Vec::new(),
809
+ invariant_check_lines: Vec::new(),
810
+ role: None,
811
+ },
812
+ CommandEntry {
813
+ verb: "Banking::SafeDepositBox.Close".to_string(),
814
+ name: "Close".to_string(),
815
+ fn_name: "close".to_string(),
816
+ args_struct: "CloseArgs".to_string(),
817
+ creates: false,
818
+ identity_extra_params: Vec::new(),
819
+ reference_checks: Vec::new(),
820
+ tenant_boundary_checks: Vec::new(),
821
+ reference_specs: Vec::new(),
822
+ attributes: Vec::new(),
823
+ invariant_check_lines: Vec::new(),
824
+ role: None,
825
+ },
826
+ ],
827
+ entity_commands: vec![EntityCommandEntry {
828
+ verb: "Banking::SafeDepositBox.Visit.Annotate".to_string(),
829
+ name: "Annotate".to_string(),
830
+ entity_record: "Visit".to_string(),
831
+ fn_name: "visit_annotate".to_string(),
832
+ args_struct: "VisitAnnotateArgs".to_string(),
833
+ reference_checks: Vec::new(),
834
+ reference_specs: Vec::new(),
835
+ attributes: Vec::new(),
836
+ invariant_check_lines: Vec::new(),
837
+ role: None,
838
+ entity_name: "Visit".to_string(),
839
+ entity_identity_reading: "date, sequence".to_string(),
840
+ }],
841
+ nested_entity_commands: Vec::new(),
842
+ ports: vec![PortEntry {
843
+ verb: "Banking::SafeDepositBox.PaymentGateway.Receive".to_string(),
844
+ name: "Receive".to_string(),
845
+ fn_name: "paymentgateway_receive".to_string(),
846
+ args_struct: "PaymentGatewayReceiveArgs".to_string(),
847
+ reference_checks: Vec::new(),
848
+ legacy_receiver_field: None,
849
+ to_receiver_field: None,
850
+ }],
851
+ chapter_mod: "banking".to_string(),
852
+ domain_name: "Banking".to_string(),
853
+ reference_specs: Vec::new(),
854
+ identified_by: vec!["branch_code".to_string(), "box_number".to_string()],
855
+ entities: Vec::new(),
856
+ };
857
+
858
+ let generated = emit_registry(&Exemplar::load(), &[aggregate]);
859
+
860
+ assert!(generated.contains("CommandInvocation::from_json(args_json)?"));
861
+ assert!(generated.contains("let branch_code = facts_json.dig(\"branch_code\")"));
862
+ assert!(generated.contains("let box_number = facts_json.dig(\"box_number\")"));
863
+ // `from_json` is reached through the `normalize_args` hook; route depth leads the body and
864
+ // identity resolves after every gate.
865
+ assert!(generated.contains("crate::kernel::decode_aggregate_arguments(facts_json, &crate::kernel::ArgumentGates {"));
866
+ assert!(generated.contains("decode_arguments: &crate::generated::banking::safedepositbox::RentArgs::decode_arguments"));
867
+ assert!(generated.contains("refuse_unknown_arguments: &crate::generated::banking::safedepositbox::RentArgs::refuse_unknown_arguments"));
868
+ assert!(generated.contains("refuse_absent_arguments: &crate::generated::banking::safedepositbox::RentArgs::refuse_absent_arguments"));
869
+ assert!(generated.contains("RentArgs::from_json(v)?"));
870
+ assert!(generated.contains("if let Some(route) = route { route.require_depth(0)?; }"));
871
+ assert!(generated.contains("Some(route) => route.aggregate().to_string()"));
872
+ assert!(generated.contains("if let Some(route) = route { route.require_depth(1)?; }"));
873
+ assert!(generated.contains("crate::kernel::decode_entity_arguments(facts_json, &crate::kernel::ArgumentGates {"));
874
+ assert!(generated.contains("let element_id = route.entities()[0].clone()"));
875
+ assert!(generated.contains("VisitAnnotateArgs::from_json(v)?"));
876
+ assert!(!generated.contains("RentArgs::from_json(facts_json)?"));
877
+ assert!(generated.contains("Json::overlay(facts_json, &args.to_json())"));
878
+ assert!(!generated.contains("VisitAnnotateArgs::from_json(args_json)?"));
879
+ assert!(generated
880
+ .contains("let (id, port_facts) = invocation.split_aggregate_receiver(None, None)?;"));
881
+ assert!(generated.contains("store.safedepositbox.find(&id)"));
882
+ assert!(generated.contains("PaymentGatewayReceiveArgs::from_json(facts_json)?"));
883
+ assert!(generated.contains("dispatch_operation_paymentgateway_receive(&id, args)"));
884
+ }
885
+
886
+ // Pins that the generated code branches on `invocation.explicit_with()` at runtime: both the
887
+ // gates-first and route-depth-first orders are present, neither hard-coded.
888
+ #[test]
889
+ fn explicit_with_gates_precheck_ordering_for_an_acting_command() {
890
+ let aggregate = AggregateEntry {
891
+ name: "Hangar".to_string(),
892
+ module_name: "hangar".to_string(),
893
+ record: "Hangar".to_string(),
894
+ commands: vec![CommandEntry {
895
+ verb: "Fixture::Hangar.Prioritize".to_string(),
896
+ name: "Prioritize".to_string(),
897
+ fn_name: "prioritize".to_string(),
898
+ args_struct: "PrioritizeArgs".to_string(),
899
+ creates: false,
900
+ identity_extra_params: Vec::new(),
901
+ reference_checks: Vec::new(),
902
+ tenant_boundary_checks: Vec::new(),
903
+ reference_specs: Vec::new(),
904
+ attributes: vec!["priority".to_string()],
905
+ invariant_check_lines: Vec::new(),
906
+ role: None,
907
+ }],
908
+ entity_commands: Vec::new(),
909
+ nested_entity_commands: Vec::new(),
910
+ ports: Vec::new(),
911
+ chapter_mod: "fixture".to_string(),
912
+ domain_name: "Fixture".to_string(),
913
+ reference_specs: Vec::new(),
914
+ identified_by: vec!["code".to_string()],
915
+ entities: Vec::new(),
916
+ };
917
+
918
+ let generated = emit_registry(&Exemplar::load(), &[aggregate]);
919
+
920
+ assert!(generated.contains("let args = if invocation.explicit_with() { let args = crate::kernel::decode_aggregate_arguments(facts_json, &crate::kernel::ArgumentGates {"));
921
+ assert!(generated.contains("if let Some(route) = route { route.require_depth(0)?; } args } else { if let Some(route) = route { route.require_depth(0)?; } crate::kernel::decode_aggregate_arguments(facts_json, &crate::kernel::ArgumentGates {"));
922
+ assert!(generated.contains("Some(route) => route.aggregate().to_string()"));
923
+ }
924
+
925
+ // Pins that a failing `extract_id` in an acting command refuses NotFound
926
+ // (`acting_no_identity`), not TypeMismatch, with wording rendered at codegen time.
927
+ #[test]
928
+ fn acting_command_id_resolution_failure_refuses_not_found_not_type_mismatch() {
929
+ let aggregate = AggregateEntry {
930
+ name: "SafeDepositBox".to_string(),
931
+ module_name: "safedepositbox".to_string(),
932
+ record: "SafeDepositBox".to_string(),
933
+ commands: vec![CommandEntry {
934
+ verb: "Banking::SafeDepositBox.Close".to_string(),
935
+ name: "Close".to_string(),
936
+ fn_name: "close".to_string(),
937
+ args_struct: "CloseArgs".to_string(),
938
+ creates: false,
939
+ identity_extra_params: Vec::new(),
940
+ reference_checks: Vec::new(),
941
+ tenant_boundary_checks: Vec::new(),
942
+ reference_specs: Vec::new(),
943
+ attributes: Vec::new(),
944
+ invariant_check_lines: Vec::new(),
945
+ role: None,
946
+ }],
947
+ entity_commands: Vec::new(),
948
+ nested_entity_commands: Vec::new(),
949
+ ports: Vec::new(),
950
+ chapter_mod: "banking".to_string(),
951
+ domain_name: "Banking".to_string(),
952
+ reference_specs: Vec::new(),
953
+ identified_by: vec!["branch_code.value".to_string(), "box_number.value".to_string()],
954
+ entities: Vec::new(),
955
+ };
956
+
957
+ let generated = emit_registry(&Exemplar::load(), &[aggregate]);
958
+
959
+ // The raw `extract_id(facts_json)?` must not appear in an acting command's id_line ...
960
+ assert!(!generated.contains("SafeDepositBox::extract_id(facts_json)?,"));
961
+ // ... replaced by a wrapper converting its failure into NotFound/acting_no_identity.
962
+ assert!(generated.contains(
963
+ "SafeDepositBox::extract_id(facts_json).map_err(|_| crate::kernel::Refusal::NotFound(crate::kernel::refusal_wording::NotFoundActingNoIdentityArgs { command: \"Close\", aggregate: \"SafeDepositBox\", identity: \"branch_code.value, box_number.value\" }.render_args()))?,"
964
+ ));
965
+ }
966
+ }