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,784 @@
1
+ //! Port of `rust/project/mutations.rb`, function for function.
2
+ //! `mark_append_optional_fields!` is not ported: `Json` has no mutation API and the corpus
3
+ //! already declares `optional: true` on every field that pass would touch.
4
+
5
+ use crate::exemplar::Exemplar;
6
+ use crate::json::Json;
7
+ use crate::literal::{self, Literal};
8
+ use crate::naming;
9
+ use std::collections::HashMap;
10
+
11
+ /// Resolves `append`'s target to a local entity or a domain-wide value object.
12
+ ///
13
+ /// Local entities are checked first: value objects are merged domain-wide, so a same-named
14
+ /// value object (Command's `Argument`) must not shadow Syntax's own `Argument` entity.
15
+ pub fn append_element<'a>(aggregate: &'a Json, target_type: &str, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<&'a Json> {
16
+ if let Some(local) = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(target_type)) {
17
+ return Some(local);
18
+ }
19
+ value_objects_by_name.get(target_type).copied()
20
+ }
21
+
22
+ /// Returns an entity's identity attribute and its single-field value object, when it can be
23
+ /// auto-minted at append time.
24
+ pub fn entity_identity_mint<'a>(entity: &'a Json, value_objects_by_name: &HashMap<String, &'a Json>) -> Option<(&'a Json, &'a Json)> {
25
+ let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
26
+ let id_path = identified_by.first()?.as_str()?;
27
+ let mut parts = id_path.split('.');
28
+ let head = parts.next()?;
29
+ let rest: Vec<&str> = parts.collect();
30
+ if rest.len() != 1 {
31
+ return None;
32
+ }
33
+
34
+ let attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
35
+ let attr = attrs.iter().find(|a| crate::attr::name(a) == head)?;
36
+ let vo = value_objects_by_name.get(crate::attr::type_name(attr)).copied()?;
37
+ if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
38
+ return None;
39
+ }
40
+ let vo_attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
41
+ if vo_attrs.len() != 1 || crate::attr::name(&vo_attrs[0]) != rest[0] {
42
+ return None;
43
+ }
44
+ Some((attr, vo))
45
+ }
46
+
47
+ /// Returns the duplicate-identity guard for a whole-list `set`, and the right-hand side to assign.
48
+ ///
49
+ /// A missing identity needs no guard: the generated `Entity::from_json` already refuses it.
50
+ /// Every `identified_by` head is compared at once (not just the first), so a composite identity
51
+ /// is guarded the same as a single-field one — see `check_entity_collision` in
52
+ /// `entity_element.rb` (Ruby's `heads.all?`).
53
+ pub fn entity_list_replace_guard<'a>(aggregate: &Json, target_attr: &Json, target_field: &str, rhs: &str, value_objects_by_name: &HashMap<String, &'a Json>) -> (String, String) {
54
+ let target_type = crate::attr::type_name(target_attr);
55
+ let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(target_type));
56
+ let entity = match entity {
57
+ Some(e) => e,
58
+ None => return (String::new(), rhs.to_string()),
59
+ };
60
+ let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
61
+ if identified_by.is_empty() {
62
+ return (String::new(), rhs.to_string());
63
+ }
64
+
65
+ let attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
66
+ let mut checks = Vec::new();
67
+ let mut offered_exprs = Vec::new();
68
+ for path in identified_by {
69
+ let id_head = path.to_s().split('.').next().unwrap_or_default().to_string();
70
+ let id_attr = match attrs.iter().find(|a| crate::attr::name(a) == id_head) {
71
+ Some(a) => a,
72
+ None => return (String::new(), rhs.to_string()),
73
+ };
74
+ let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
75
+ checks.push(format!("prior.{id_field} == e.{id_field}"));
76
+
77
+ // Unwrap a single-field identity so `offered` prints the bare scalar, as
78
+ // `Rendering.describe` does; Debug-printing the value object would give
79
+ // `EntrySequence { value: 1 }`.
80
+ let id_vo = value_objects_by_name.get(crate::attr::type_name(id_attr)).copied();
81
+ let offered_expr = match id_vo {
82
+ Some(vo) if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) && vo.get("attributes").map(Json::each).unwrap_or(&[]).len() == 1 => {
83
+ let inner = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
84
+ format!("e.{id_field}.{inner}")
85
+ }
86
+ _ => format!("e.{id_field}"),
87
+ };
88
+ offered_exprs.push(offered_expr);
89
+ }
90
+
91
+ let local_var = format!("replaced_{target_field}");
92
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
93
+ let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
94
+ let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
95
+ let entity_lit = naming::ruby_inspect_string(entity_name);
96
+ let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
97
+ let identity_lit = naming::ruby_inspect_string(&identity_reading);
98
+ let condition = checks.join(" && ");
99
+ let offered_lets: Vec<String> = offered_exprs.iter().enumerate().map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});")).collect();
100
+ let offered_refs: Vec<String> = (0..offered_exprs.len()).map(|i| format!("offered_{i}.as_str()")).collect();
101
+ let guard = format!(
102
+ "let {local_var} = {rhs};\n for (i, e) in {local_var}.iter().enumerate() {{ if {local_var}[..i].iter().any(|prior| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }} }}\n ",
103
+ offered_lets.join(" "), offered_refs.join(", ")
104
+ );
105
+ (guard, local_var)
106
+ }
107
+
108
+ /// The composite-identity twin of the single-field collision guard inlined in
109
+ /// `emit_mutation_line_body`'s `append` branch: builds a duplicate check across every
110
+ /// `identified_by` head at once (`e.batch == ... && e.sequence == ...`), matching
111
+ /// `EntityElement.check_entity_collision`'s `heads.all?` on the Ruby side. Called only once every
112
+ /// head is confirmed present in the append's own `fields` map (composite identities never mint).
113
+ #[allow(clippy::too_many_arguments)]
114
+ fn composite_append_collision_guard(
115
+ heads: &[String],
116
+ fields: &[(String, String)],
117
+ element_attrs: &[Json],
118
+ command: &Json,
119
+ value_objects_by_name: &HashMap<String, &Json>,
120
+ target_field: &str,
121
+ entity: &Json,
122
+ aggregate: &Json,
123
+ identified_by: &[Json],
124
+ ) -> String {
125
+ let mut checks = Vec::new();
126
+ let mut offered_exprs = Vec::new();
127
+ for head in heads {
128
+ let field_attr = element_attrs
129
+ .iter()
130
+ .find(|a| crate::attr::name(a) == head.as_str())
131
+ .expect("composite identity head must be a declared element attribute");
132
+ let (_, source) = fields
133
+ .iter()
134
+ .find(|(k, _)| k == head)
135
+ .expect("composite identity head must be in the append's own field map");
136
+ let rhs = append_field_rhs(source, field_attr, command, value_objects_by_name);
137
+ let id_field = naming::rust_ident_field(head);
138
+ checks.push(format!("e.{id_field} == {rhs}"));
139
+
140
+ // Unwrap a single-field identity component so `offered` prints the bare scalar, as
141
+ // `Rendering.describe` does; Debug-printing the value object would give
142
+ // `LineSequence { value: 654 }`.
143
+ let vo = value_objects_by_name.get(crate::attr::type_name(field_attr)).copied();
144
+ let offered_expr = match vo {
145
+ Some(vo) if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) && vo.get("attributes").map(Json::each).unwrap_or(&[]).len() == 1 => {
146
+ let inner = naming::rust_ident_field(crate::attr::name(&vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
147
+ format!("{rhs}.{inner}")
148
+ }
149
+ _ => rhs,
150
+ };
151
+ offered_exprs.push(offered_expr);
152
+ }
153
+
154
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
155
+ let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
156
+ let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
157
+ let entity_lit = naming::ruby_inspect_string(entity_name);
158
+ let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
159
+ let identity_lit = naming::ruby_inspect_string(&identity_reading);
160
+ let condition = checks.join(" && ");
161
+ let offered_lets: Vec<String> = offered_exprs
162
+ .iter()
163
+ .enumerate()
164
+ .map(|(i, expr)| format!("let offered_{i} = format!(\"{{:?}}\", {expr});"))
165
+ .collect();
166
+ let offered_refs: Vec<String> = (0..offered_exprs.len()).map(|i| format!("offered_{i}.as_str()")).collect();
167
+
168
+ format!(
169
+ "if record.{target_field}.iter().any(|e| {condition}) {{ {} let offered = [{}]; return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &offered }}.render_args())); }}\n ",
170
+ offered_lets.join(" "), offered_refs.join(", ")
171
+ )
172
+ }
173
+
174
+ /// Inverse of `appended_fields`' spelling: a leading `:` is a command argument, else a literal.
175
+ pub fn append_field_source(source: &str) -> Literal {
176
+ literal::read(source)
177
+ }
178
+
179
+ /// Whether any effect reads the record's own state, so the closure must bind `let pre`.
180
+ pub fn reads_pre_state(mutations: &[Json]) -> bool {
181
+ mutations.iter().any(|m| match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
182
+ "set" => m.get("source").and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default() == "state",
183
+ "append" => match m.get("fields") {
184
+ Some(Json::Object(pairs)) => pairs.iter().any(|(_, source)| source.to_s().starts_with("state(:")),
185
+ _ => false,
186
+ },
187
+ _ => false,
188
+ })
189
+ }
190
+
191
+ pub fn pre_state_line() -> String {
192
+ " let pre = record.clone();".to_string()
193
+ }
194
+
195
+ // Integer arithmetic that leaves signed 64 bits is a `Refusal::Fault`, never a wrap or a panic.
196
+ fn checked_arithmetic(op: &str, field_ident: &str, symbol: &str, amount_expr: &str) -> String {
197
+ let checked = match symbol {
198
+ "+" => "checked_add",
199
+ "-" => "checked_sub",
200
+ "*" => "checked_mul",
201
+ other => panic!("no checked arithmetic for {other:?}"),
202
+ };
203
+ format!(
204
+ "{{ let amount = {amount_expr}; current.{field_ident}.{checked}(amount).ok_or_else(|| crate::kernel::Refusal::Fault(format!(\"{op} overflowed: {{}} {symbol} {{}} does not fit in a 64-bit integer\", current.{field_ident}, amount)))? }}"
205
+ )
206
+ }
207
+
208
+ pub fn literal_problem(mutation: &Json, field_name: &str, lit: &Literal, field_attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
209
+ if crate::bridging::literal_set_bridgeable(lit, Some(crate::attr::type_name(field_attr)), value_objects_by_name) {
210
+ return None;
211
+ }
212
+ let target = mutation.get("target").map(Json::to_s).unwrap_or_default();
213
+ Some(format!("{target}.{field_name}: literal doesn't bridge to {}", crate::attr::type_name(field_attr)))
214
+ }
215
+
216
+ /// Problems with `set`/`append` fields sourced from `state(:field)`: an undeclared state field,
217
+ /// or a type or cardinality that differs from the target's.
218
+ pub fn state_source_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
219
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
220
+ let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
221
+ mutations
222
+ .iter()
223
+ .flat_map(|m| {
224
+ let target = m.get("target").map(Json::to_s).unwrap_or_default();
225
+ match m.get("op").map(Json::to_s).unwrap_or_default().as_str() {
226
+ "set" => {
227
+ let source = m.get("source");
228
+ if source.and_then(|s| s.get("kind")).map(Json::to_s).unwrap_or_default() != "state" {
229
+ return Vec::new();
230
+ }
231
+ let state_name = source.and_then(|s| s.get("name")).map(Json::to_s).unwrap_or_default();
232
+ state_source_problem(&target, &state_name, aggregate, attrs.iter().find(|a| crate::attr::name(a) == target)).into_iter().collect()
233
+ }
234
+ "append" => {
235
+ let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target) else { return Vec::new() };
236
+ let Some(element) = append_element(aggregate, crate::attr::type_name(target_attr), value_objects_by_name) else { return Vec::new() };
237
+ let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
238
+ let Some(Json::Object(fields)) = m.get("fields") else { return Vec::new() };
239
+ fields
240
+ .iter()
241
+ .filter_map(|(field_name, source)| {
242
+ let text = source.to_s();
243
+ let state_name = text.strip_prefix("state(:")?.strip_suffix(')')?;
244
+ state_source_problem(&format!("{target}.{field_name}"), state_name, aggregate, element_attrs.iter().find(|a| crate::attr::name(a) == field_name))
245
+ })
246
+ .collect()
247
+ }
248
+ _ => Vec::new(),
249
+ }
250
+ })
251
+ .collect()
252
+ }
253
+
254
+ fn state_source_problem(label: &str, state_name: &str, aggregate: &Json, target_attr: Option<&Json>) -> Option<String> {
255
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
256
+ let Some(state_attr) = attrs.iter().find(|a| crate::attr::name(a) == state_name) else {
257
+ let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
258
+ return Some(format!("{label}: sources state(:{state_name}), which {aggregate_name} does not declare"));
259
+ };
260
+ let Some(target_attr) = target_attr else {
261
+ return Some(format!("{label}: no such target field"));
262
+ };
263
+ let same = crate::attr::type_name(state_attr) == crate::attr::type_name(target_attr) && crate::attr::list(state_attr) == crate::attr::list(target_attr);
264
+ if same {
265
+ return None;
266
+ }
267
+ let list_of = |a: &Json| if crate::attr::list(a) { "a list of " } else { "" };
268
+ Some(format!(
269
+ "{label}: state(:{state_name}) is {}{}, the target wants {}{} — not generated yet",
270
+ list_of(state_attr),
271
+ crate::attr::type_name(state_attr),
272
+ list_of(target_attr),
273
+ crate::attr::type_name(target_attr)
274
+ ))
275
+ }
276
+
277
+ /// Problems with each `append` mutation's fields, checked against the element they build.
278
+ pub fn append_field_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
279
+ let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
280
+ mutations
281
+ .iter()
282
+ .filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "append")
283
+ .flat_map(|m| {
284
+ let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
285
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
286
+ let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name);
287
+ let element = target_attr.and_then(|ta| append_element(aggregate, crate::attr::type_name(ta), value_objects_by_name));
288
+ let Some(element) = element else {
289
+ let type_desc = target_attr.map(crate::attr::type_name).unwrap_or("");
290
+ return vec![format!("{target_name}: element type {} not resolvable", naming::ruby_inspect_string(type_desc))];
291
+ };
292
+ let target_attr = target_attr.unwrap();
293
+
294
+ let fields = m.get("fields").map(fields_pairs).unwrap_or_default();
295
+ let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
296
+ let mut problems: Vec<String> = fields
297
+ .iter()
298
+ .filter_map(|(field_name, source)| {
299
+ let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str());
300
+ let Some(field_attr) = field_attr else {
301
+ return Some(format!("{target_name}.{field_name}: not a declared field"));
302
+ };
303
+
304
+ let parsed = append_field_source(source);
305
+ let Literal::Symbol(arg_name) = &parsed else {
306
+ return literal_problem(m, field_name, &parsed, field_attr, value_objects_by_name);
307
+ };
308
+
309
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
310
+ match cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()) {
311
+ None => Some(format!("{target_name}.{field_name}: sources undeclared argument {arg_name}")),
312
+ Some(arg_attr) if !crate::bridging::bridgeable_value_types(crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name) => {
313
+ Some(format!("{target_name}.{field_name}: {} doesn't bridge to {}", crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr)))
314
+ }
315
+ _ => None,
316
+ }
317
+ })
318
+ .collect();
319
+
320
+ let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr)));
321
+ if let Some(entity) = entity {
322
+ let present: Vec<String> = fields.iter().map(|(k, _)| k.clone()).collect();
323
+ let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
324
+ let id_head = identified_by.first().and_then(Json::as_str).unwrap_or("").split('.').next().unwrap_or("").to_string();
325
+ if !present.contains(&id_head) && entity_identity_mint(entity, value_objects_by_name).is_none() {
326
+ problems.push(format!("{target_name}: {}'s identity doesn't auto-mint", entity.get("name").and_then(Json::as_str).unwrap_or("")));
327
+ }
328
+ }
329
+ problems
330
+ })
331
+ .collect()
332
+ }
333
+
334
+ /// Problems with `remove:` mutations, which are generatable only against an entity list whose
335
+ /// single-head identity mints, sourced from a declared argument that bridges to its type.
336
+ pub fn remove_field_problems(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
337
+ let mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
338
+ mutations
339
+ .iter()
340
+ .filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "remove")
341
+ .filter_map(|m| {
342
+ let target_name = m.get("target").map(Json::to_s).unwrap_or_default();
343
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
344
+ let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target_name).filter(|a| crate::attr::list(a)) else {
345
+ return Some(format!("{target_name}: not a declared list attribute"));
346
+ };
347
+
348
+ let entities = aggregate.get("entities").map(Json::each).unwrap_or(&[]);
349
+ let Some(entity) = entities.iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))) else {
350
+ return Some(format!("{target_name}: remove on a value-object-typed list is not generated yet"));
351
+ };
352
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("");
353
+
354
+ if entity.get("identified_by").map(Json::each).unwrap_or(&[]).len() != 1 {
355
+ return Some(format!("{target_name}: {entity_name}'s identity is composite — remove not generated yet"));
356
+ }
357
+
358
+ let Some((id_attr, _)) = entity_identity_mint(entity, value_objects_by_name) else {
359
+ return Some(format!("{target_name}: {entity_name}'s identity isn't a single bridgeable field — remove not generated yet"));
360
+ };
361
+
362
+ let source = m.get("source");
363
+ let Some(source) = source.filter(|s| matches!(s, Json::Object(_)) && s.get("kind").map(Json::to_s).unwrap_or_default() == "argument") else {
364
+ return Some(format!("{target_name}: remove sources a literal or record state, not an argument — not generated yet"));
365
+ };
366
+
367
+ let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
368
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
369
+ let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name) else {
370
+ return Some(format!("{target_name}: remove sources undeclared argument {source_name}"));
371
+ };
372
+
373
+ let (source_type, id_type) = (crate::attr::type_name(source_attr), crate::attr::type_name(id_attr));
374
+ if !crate::bridging::bridgeable_value_types(source_type, id_type, value_objects_by_name) {
375
+ return Some(format!("{target_name}: {source_type} doesn't bridge to {id_type}"));
376
+ }
377
+
378
+ None
379
+ })
380
+ .collect()
381
+ }
382
+
383
+ /// A mutation's `fields` object as ordered `(key, raw_wire_value)` pairs.
384
+ fn fields_pairs(fields: &Json) -> Vec<(String, String)> {
385
+ match fields {
386
+ Json::Object(pairs) => pairs.iter().map(|(k, v)| (k.clone(), v.to_s())).collect(),
387
+ _ => Vec::new(),
388
+ }
389
+ }
390
+
391
+ pub struct Transition {
392
+ pub field: String,
393
+ pub to_state: String,
394
+ pub from_states: Vec<String>,
395
+ }
396
+
397
+ /// Collapses a command's transition rows into the shape `TransitionCheck` wants.
398
+ pub fn lifecycle_transition_for(command: &Json, aggregate: &Json) -> Option<Transition> {
399
+ let lifecycle = aggregate.get("lifecycle")?;
400
+ let command_name = command.get("name").map(Json::to_s).unwrap_or_default();
401
+ let transitions = lifecycle.get("transitions").map(Json::each).unwrap_or(&[]);
402
+ let rows: Vec<&Json> = transitions.iter().filter(|t| t.get("command").map(Json::to_s).unwrap_or_default() == command_name).collect();
403
+ if rows.is_empty() {
404
+ // `from:` without a transition guards the current state and moves nothing.
405
+ let froms: Vec<String> = match command.get("from") {
406
+ Some(Json::Null) | None => Vec::new(),
407
+ Some(Json::String(s)) => vec![s.clone()],
408
+ Some(list) => list.each().iter().map(Json::to_s).collect(),
409
+ };
410
+ if froms.is_empty() {
411
+ return None;
412
+ }
413
+ let mut from_states: Vec<String> = Vec::new();
414
+ for from in froms {
415
+ if !from_states.contains(&from) {
416
+ from_states.push(from);
417
+ }
418
+ }
419
+ let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
420
+ return Some(Transition { field, to_state: String::new(), from_states });
421
+ }
422
+
423
+ let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("").to_string();
424
+ let to_state = rows[0].get("to_state").map(Json::to_s).unwrap_or_default();
425
+ let mut from_states: Vec<String> = Vec::new();
426
+ for row in &rows {
427
+ let from = row.get("from_state").map(Json::to_s).unwrap_or_default();
428
+ if !from_states.contains(&from) {
429
+ from_states.push(from);
430
+ }
431
+ }
432
+ Some(Transition { field, to_state, from_states })
433
+ }
434
+
435
+ /// Right-hand side for a `set`, coercing the source into the target attribute's declared type.
436
+ ///
437
+ /// `target_list` routes list-to-list sets through `list_value_rhs`; `value_rhs` is scalar-only.
438
+ pub fn mutation_set_rhs(source: &Json, target_type: &str, command: &Json, value_objects_by_name: &HashMap<String, &Json>, target_list: bool) -> String {
439
+ if source.get("kind").map(Json::to_s).unwrap_or_default() == "literal" {
440
+ let value = source.get("value").unwrap_or(&Json::Null);
441
+ return crate::bridging::literal_rhs_for(&Literal::from_json(value), Some(target_type), value_objects_by_name);
442
+ }
443
+
444
+ let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
445
+ // `pre`, not `record`: the pre-dispatch state, bound when `reads_pre_state`.
446
+ if source.get("kind").map(Json::to_s).unwrap_or_default() == "state" {
447
+ return format!("pre.{}.clone()", naming::rust_ident_field(&source_name));
448
+ }
449
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
450
+ let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name).expect("mutation source argument must be a declared command attribute");
451
+ let source_expr = format!("args.{}", naming::rust_ident_field(&source_name));
452
+ if target_list && crate::attr::list(source_attr) && crate::bridging::list_bridge_requires_element_mapping(crate::attr::type_name(source_attr), target_type) {
453
+ return crate::bridging::list_value_rhs(&source_expr, crate::attr::type_name(source_attr), target_type, value_objects_by_name);
454
+ }
455
+ crate::bridging::value_rhs(&source_expr, crate::attr::type_name(source_attr), target_type, value_objects_by_name)
456
+ }
457
+
458
+ pub struct IdentityComponent {
459
+ pub expr: String,
460
+ pub param: Option<String>,
461
+ pub head: Option<String>,
462
+ }
463
+
464
+ /// Builds one component per identity part: a `set` target or same-named command argument reads
465
+ /// `args.<head>`, and anything else becomes a parameter.
466
+ pub fn identity_components(aggregate: &Json, command: &Json) -> Vec<IdentityComponent> {
467
+ let identified_by = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]);
468
+ let cmd_mutations = command.get("mutations").map(Json::each).unwrap_or(&[]);
469
+ let append_claimed = crate::shared::append_claimed_names(command);
470
+ let declared_names: std::collections::HashSet<String> = command
471
+ .get("attributes")
472
+ .map(Json::each)
473
+ .unwrap_or(&[])
474
+ .iter()
475
+ .map(|a| crate::attr::name(a).to_string())
476
+ .filter(|name| !append_claimed.contains(name))
477
+ .collect();
478
+
479
+ identified_by
480
+ .iter()
481
+ .map(|path| {
482
+ let path = path.as_str().unwrap_or("");
483
+ let mut parts = path.split('.');
484
+ let head = parts.next().unwrap_or("");
485
+ let rest: Vec<&str> = parts.collect();
486
+
487
+ let set_target = cmd_mutations.iter().any(|m| {
488
+ m.get("op").map(Json::to_s).as_deref() == Some("set") && m.get("target").map(Json::to_s).as_deref() == Some(head)
489
+ });
490
+
491
+ if set_target || declared_names.contains(head) {
492
+ if !rest.is_empty() {
493
+ let rest_path = rest.iter().map(|seg| naming::rust_ident_field(seg)).collect::<Vec<_>>().join(".");
494
+ IdentityComponent { expr: format!("args.{}.{}.to_string()", naming::rust_ident_field(head), rest_path), param: None, head: None }
495
+ } else {
496
+ IdentityComponent { expr: format!("args.{}.to_string()", naming::rust_ident_field(head)), param: None, head: None }
497
+ }
498
+ } else {
499
+ let param = naming::rust_ident_field(head);
500
+ IdentityComponent { expr: format!("{param}.to_string()"), param: Some(format!("{param}: &str")), head: Some(head.to_string()) }
501
+ }
502
+ })
503
+ .collect()
504
+ }
505
+
506
+ pub fn build_identity_expr(components: &[IdentityComponent]) -> String {
507
+ if components.len() == 1 {
508
+ return components[0].expr.clone();
509
+ }
510
+ let placeholders = components.iter().map(|_| "{}").collect::<Vec<_>>().join(":");
511
+ format!("format!({}, {})", naming::ruby_inspect_string(&placeholders), components.iter().map(|c| c.expr.as_str()).collect::<Vec<_>>().join(", "))
512
+ }
513
+
514
+ /// The value expression for one `append` field.
515
+ pub fn append_field_rhs(source: &str, field_attr: &Json, command: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
516
+ let parsed = append_field_source(source);
517
+ let Literal::Symbol(arg_name) = &parsed else {
518
+ return crate::bridging::literal_rhs_for(&parsed, Some(crate::attr::type_name(field_attr)), value_objects_by_name);
519
+ };
520
+
521
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
522
+ let arg_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()).expect("append field argument must be a declared command attribute");
523
+ let arg_expr = format!("args.{}", naming::rust_ident_field(crate::attr::name(arg_attr)));
524
+
525
+ if crate::attr::optional(arg_attr) {
526
+ let same_representation = crate::attr::type_name(arg_attr) == crate::attr::type_name(field_attr)
527
+ || match (naming::effective_scalar_type(crate::attr::type_name(arg_attr)), naming::effective_scalar_type(crate::attr::type_name(field_attr))) {
528
+ (Some(a), Some(b)) => a == b,
529
+ _ => false,
530
+ };
531
+ if same_representation {
532
+ return format!("{arg_expr}.clone()");
533
+ }
534
+ return optional_value_rhs(&arg_expr, crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name);
535
+ }
536
+
537
+ let rhs = crate::bridging::value_rhs(&arg_expr, crate::attr::type_name(arg_attr), crate::attr::type_name(field_attr), value_objects_by_name);
538
+ if crate::attr::optional(field_attr) {
539
+ format!("Some({rhs})")
540
+ } else {
541
+ rhs
542
+ }
543
+ }
544
+
545
+ /// The optional-source variant of `value_rhs`.
546
+ pub fn optional_value_rhs(source_expr: &str, source_type: &str, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
547
+ format!("{source_expr}.clone().map(|v| {})", crate::bridging::value_rhs("v", source_type, target_type, value_objects_by_name))
548
+ }
549
+
550
+ /// Emits the closure line for one mutation.
551
+ pub fn emit_mutation_line(exemplar: &Exemplar, mutation: &Json, aggregate: &Json, command: &Json, value_objects_by_name: &HashMap<String, &Json>, optional: bool) -> String {
552
+ let target_field = naming::rust_ident_field(&mutation.get("target").map(Json::to_s).unwrap_or_default());
553
+ let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
554
+ format!(" {}", emit_mutation_line_body(exemplar, mutation, aggregate, command, value_objects_by_name, &target_field, lifecycle_field.as_deref(), optional))
555
+ }
556
+
557
+ fn emit_mutation_line_body(
558
+ exemplar: &Exemplar,
559
+ mutation: &Json,
560
+ aggregate: &Json,
561
+ command: &Json,
562
+ value_objects_by_name: &HashMap<String, &Json>,
563
+ target_field: &str,
564
+ lifecycle_field: Option<&str>,
565
+ optional: bool,
566
+ ) -> String {
567
+ let op = mutation.get("op").map(Json::to_s).unwrap_or_default();
568
+ let target_name = mutation.get("target").map(Json::to_s).unwrap_or_default();
569
+
570
+ match op.as_str() {
571
+ "append" => {
572
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
573
+ let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("append target must be a declared aggregate attribute");
574
+ let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
575
+ let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr)));
576
+ let element = entity.or_else(|| value_objects_by_name.get(crate::attr::type_name(target_attr)).copied()).expect("append target's element type must resolve");
577
+
578
+ let fields = mutation.get("fields").map(fields_pairs).unwrap_or_default();
579
+ let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
580
+ let mut fields_assignment: Vec<String> = fields
581
+ .iter()
582
+ .map(|(field_name, source)| {
583
+ let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str()).expect("append field must be a declared element attribute");
584
+ format!("{}: {}", naming::rust_ident_field(field_name), append_field_rhs(source, field_attr, command, value_objects_by_name))
585
+ })
586
+ .collect();
587
+
588
+ let mut collision_guard = String::new();
589
+ if let Some(entity) = entity {
590
+ let mut present: Vec<String> = fields.iter().map(|(k, _)| k.clone()).collect();
591
+ let identified_by = entity.get("identified_by").map(Json::each).unwrap_or(&[]);
592
+ if identified_by.len() > 1 {
593
+ // Composite identity: minting a partial key makes no sense, so every head must
594
+ // already be caller-supplied (as `Venue.AddLine`'s `append: { batch: :batch,
595
+ // sequence: :sequence }` does). Refuse a duplicate across the WHOLE tuple, the
596
+ // way `EntityElement.check_entity_collision`'s `heads.all?` does — this branch
597
+ // used to be skipped outright for any composite identity (its comment read
598
+ // "Composite identities are excluded"), so two elements sharing every identity
599
+ // component went through unrefused. Root cause of this bug.
600
+ let heads: Vec<String> = identified_by.iter().map(|p| p.to_s().split('.').next().unwrap_or_default().to_string()).collect();
601
+ if heads.iter().all(|h| present.iter().any(|p| p == h)) {
602
+ collision_guard = composite_append_collision_guard(&heads, &fields, element_attrs, command, value_objects_by_name, target_field, entity, aggregate, identified_by);
603
+ }
604
+ } else if let Some((id_attr, id_vo)) = entity_identity_mint(entity, value_objects_by_name) {
605
+ let id_name = crate::attr::name(id_attr);
606
+ if !present.iter().any(|p| p == id_name) {
607
+ let id_vo_attrs = id_vo.get("attributes").map(Json::each).unwrap_or(&[]);
608
+ // Mint one past the highest identity held.
609
+ let id_field = naming::rust_ident_field(id_name);
610
+ let vo_field = naming::rust_ident_field(crate::attr::name(&id_vo_attrs[0]));
611
+ let mint = format!(
612
+ "{} {{ {vo_field}: record.{target_field}.iter().map(|e| e.{id_field}.{vo_field}).max().unwrap_or(0) + 1 }}",
613
+ naming::rust_ident(crate::attr::type_name(id_attr)),
614
+ );
615
+ fields_assignment.push(format!("{}: {mint}", naming::rust_ident_field(id_name)));
616
+ present.push(id_name.to_string());
617
+ } else {
618
+ // Caller-supplied single-field identity: refuse a duplicate, which would
619
+ // be unaddressable later.
620
+ let id_field = naming::rust_ident_field(id_name);
621
+ let id_rhs = fields
622
+ .iter()
623
+ .find(|(k, _)| k == id_name)
624
+ .map(|(_, source)| {
625
+ let field_attr = element_attrs
626
+ .iter()
627
+ .find(|a| crate::attr::name(a) == id_name)
628
+ .expect("append field must be a declared element attribute");
629
+ append_field_rhs(source, field_attr, command, value_objects_by_name)
630
+ })
631
+ .expect("caller-supplied identity field must be in the append's own field map");
632
+ let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or_default();
633
+ let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or_default();
634
+ let identity_reading = identified_by.iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
635
+ let entity_lit = naming::ruby_inspect_string(entity_name);
636
+ let aggregate_lit = naming::ruby_inspect_string(aggregate_name);
637
+ let identity_lit = naming::ruby_inspect_string(&identity_reading);
638
+ // Unwrap to the bare scalar, as `entity_list_replace_guard` does; the mint
639
+ // branch above guarantees exactly one attribute.
640
+ let offered_field = naming::rust_ident_field(crate::attr::name(&id_vo.get("attributes").map(Json::each).unwrap_or(&[])[0]));
641
+ let offered_expr = format!("{id_rhs}.{offered_field}");
642
+ collision_guard = format!(
643
+ "if record.{target_field}.iter().any(|e| e.{id_field} == {id_rhs}) {{ let offered = format!(\"{{:?}}\", {offered_expr}); return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs {{ entity: {entity_lit}, aggregate: {aggregate_lit}, identity: {identity_lit}, offered: &[offered.as_str()] }}.render_args())); }}\n "
644
+ );
645
+ }
646
+ }
647
+ if let Some(entity_lifecycle) = entity.get("lifecycle") {
648
+ let lc_field = entity_lifecycle.get("field").map(Json::to_s).unwrap_or_default();
649
+ if !present.iter().any(|p| p == &lc_field) {
650
+ let default = entity_lifecycle.get("default").map(Json::to_s).unwrap_or_default();
651
+ fields_assignment.push(format!("{}: {}.to_string()", naming::rust_ident_field(&lc_field), naming::ruby_inspect_string(&default)));
652
+ present.push(lc_field);
653
+ }
654
+ }
655
+
656
+ // Fill list attributes no `append:` names; a later command appends to them.
657
+ for attr in element_attrs {
658
+ if !crate::attr::list(attr) {
659
+ continue;
660
+ }
661
+ let attr_name = crate::attr::name(attr);
662
+ if present.iter().any(|p| p == attr_name) {
663
+ continue;
664
+ }
665
+ fields_assignment.push(format!("{}: Vec::new()", naming::rust_ident_field(attr_name)));
666
+ present.push(attr_name.to_string());
667
+ }
668
+
669
+ // Fill optional scalars no `append:` names; a later command sets them.
670
+ for attr in element_attrs {
671
+ if crate::attr::list(attr) || !crate::attr::optional(attr) {
672
+ continue;
673
+ }
674
+ let attr_name = crate::attr::name(attr);
675
+ if present.iter().any(|p| p == attr_name) {
676
+ continue;
677
+ }
678
+ fields_assignment.push(format!("{}: None", naming::rust_ident_field(attr_name)));
679
+ present.push(attr_name.to_string());
680
+ }
681
+ }
682
+
683
+ format!("{collision_guard}{}", exemplar.render("mutation_append", &[("tmpl_field", target_field.to_string()), ("tmpl_fields_placeholder()", format!("{vo_type} {{ {} }}", fields_assignment.join(", ")))]))
684
+ }
685
+ "set" => {
686
+ if lifecycle_field.map(|f| f == target_name).unwrap_or(false) {
687
+ let rhs = mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), "String", command, value_objects_by_name, false);
688
+ exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
689
+ } else {
690
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
691
+ let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("set target must be a declared aggregate attribute");
692
+ let rhs = mutation_set_rhs(mutation.get("source").unwrap_or(&Json::Null), crate::attr::type_name(target_attr), command, value_objects_by_name, crate::attr::list(target_attr));
693
+ let source = mutation.get("source");
694
+ let source_attr = if source.map(|s| s.get("kind").map(Json::to_s).unwrap_or_default()) == Some("argument".to_string()) {
695
+ let name = source.unwrap().get("name").map(Json::to_s).unwrap_or_default();
696
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
697
+ cmd_attrs.iter().find(|a| crate::attr::name(a) == name)
698
+ } else {
699
+ None
700
+ };
701
+
702
+ if crate::attr::list(target_attr) && optional && crate::shared::list_attr_creation_optional(aggregate, crate::attr::name(target_attr), value_objects_by_name) {
703
+ exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
704
+ } else if crate::attr::list(target_attr) && source_attr.map(crate::attr::optional).unwrap_or(false) {
705
+ exemplar.render("mutation_set_unwrap_or_default", &[("tmpl_field", target_field.to_string()), ("tmpl_optional_rhs_placeholder()", rhs)])
706
+ } else if crate::attr::list(target_attr) {
707
+ // Only whole-list replaces carry the duplicate-identity guard.
708
+ let (guard, effective_rhs) = entity_list_replace_guard(aggregate, target_attr, &target_field, &rhs, value_objects_by_name);
709
+ format!("{guard}{}", exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", effective_rhs)]))
710
+ } else {
711
+ let wrap = (optional || crate::attr::optional(target_attr)) && !source_attr.map(crate::attr::optional).unwrap_or(false);
712
+ if wrap {
713
+ exemplar.render("mutation_set_wrapped", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
714
+ } else {
715
+ exemplar.render("mutation_set_plain", &[("tmpl_field", target_field.to_string()), ("tmpl_rhs_placeholder2()", rhs)])
716
+ }
717
+ }
718
+ }
719
+ }
720
+ "increment" | "decrement" => {
721
+ let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("arithmetic target must resolve");
722
+ let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
723
+ let field_ident = naming::rust_ident_field(&integer_field);
724
+ let amount_expr = crate::bridging::arithmetic_amount_expr(mutation.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &integer_field).expect("arithmetic amount must resolve");
725
+ // Use the IR's `sign`, not the op name.
726
+ let sign = if mutation.get("sign").map(Json::to_s).unwrap_or_default() == "1" { "+" } else { "-" };
727
+ let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
728
+ let op = mutation.get("op").map(Json::to_s).unwrap_or_default();
729
+ let updated = format!("{vo_type} {{ {field_ident}: {}, ..current }}", checked_arithmetic(&op, &field_ident, sign, &amount_expr));
730
+ exemplar.render(
731
+ "mutation_arithmetic",
732
+ &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
733
+ )
734
+ }
735
+ "multiply" => {
736
+ // Same Integer-field subset as `increment`/`decrement`, with `*`.
737
+ let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("arithmetic target must resolve");
738
+ let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
739
+ let field_ident = naming::rust_ident_field(&integer_field);
740
+ let amount_expr = crate::bridging::arithmetic_amount_expr(mutation.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &integer_field).expect("arithmetic amount must resolve");
741
+ let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
742
+ let updated = format!("{vo_type} {{ {field_ident}: {}, ..current }}", checked_arithmetic("multiply", &field_ident, "*", &amount_expr));
743
+ exemplar.render(
744
+ "mutation_arithmetic",
745
+ &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
746
+ )
747
+ }
748
+ "clamp" => {
749
+ // The source is always a literal `[min, max]` pair, so there is no amount to resolve.
750
+ let (target_attr, integer_field) = crate::bridging::arithmetic_target_field(mutation, aggregate, value_objects_by_name).expect("clamp target must resolve");
751
+ let vo_type = naming::rust_ident(crate::attr::type_name(target_attr));
752
+ let field_ident = naming::rust_ident_field(&integer_field);
753
+ let (min, max) = mutation.get("source").and_then(crate::bridging::clamp_bounds_ints).expect("clamp bounds must resolve");
754
+ let current = if optional { format!("record.{target_field}.clone().unwrap()") } else { format!("record.{target_field}.clone()") };
755
+ let updated = format!("{vo_type} {{ {field_ident}: current.{field_ident}.clamp({min}, {max}), ..current }}");
756
+ exemplar.render(
757
+ "mutation_arithmetic",
758
+ &[("tmpl_field", target_field.to_string()), ("tmpl_current_placeholder()", current), ("tmpl_updated_placeholder()", if optional { format!("Some({updated})") } else { updated })],
759
+ )
760
+ }
761
+ "remove" => {
762
+ // `remove_field_problems` already confirmed every lookup below resolves.
763
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
764
+ let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target_name).expect("remove target must be a declared aggregate attribute");
765
+ let entities = aggregate.get("entities").map(Json::each).unwrap_or(&[]);
766
+ let entity = entities.iter().find(|e| e.get("name").and_then(Json::as_str) == Some(crate::attr::type_name(target_attr))).expect("remove target must be an entity-typed list");
767
+ let (id_attr, _) = entity_identity_mint(entity, value_objects_by_name).expect("remove target entity's identity must mint");
768
+ let id_field = naming::rust_ident_field(crate::attr::name(id_attr));
769
+ let source_name = mutation.get("source").and_then(|s| s.get("name")).map(Json::to_s).unwrap_or_default();
770
+ let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
771
+ let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name).expect("remove source must be a declared argument");
772
+ let match_expr = crate::bridging::value_rhs(
773
+ &format!("args.{}", naming::rust_ident_field(crate::attr::name(source_attr))),
774
+ crate::attr::type_name(source_attr),
775
+ crate::attr::type_name(id_attr),
776
+ value_objects_by_name,
777
+ );
778
+ exemplar.render("mutation_remove", &[("tmpl_field", target_field.to_string()), ("tmpl_id_field", id_field), ("tmpl_remove_match_placeholder()", match_expr)])
779
+ }
780
+ // `corrects` targets no field; entity-level commands do not filter it out first.
781
+ "corrects" => String::new(),
782
+ other => panic!("unsupported mutation op {other:?} — command_skip_reason should have caught this"),
783
+ }
784
+ }