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,721 @@
1
+ module RustProjection
2
+ module Projector
3
+ module_function
4
+
5
+ # True when a list attribute reads `nil` rather than `[]`: a creating command `sets` it from
6
+ # an optional argument the caller omitted (`CardPayment.tags`). `Account.ledger` stays `[]`.
7
+ def list_attr_creation_optional?(aggregate, attr_name, value_objects_by_name)
8
+ aggregate[:commands].any? do |command|
9
+ next false unless creates_owner?(aggregate, command, value_objects_by_name)
10
+
11
+ command[:mutations].any? do |m|
12
+ next false unless m[:op].to_s == "set" && m[:target].to_s == attr_name.to_s && m[:source][:kind] == "argument"
13
+
14
+ source_attr = command[:attributes].find { |a| a[:name].to_s == m[:source][:name].to_s }
15
+ source_attr && source_attr[:optional]
16
+ end
17
+ end
18
+ end
19
+
20
+ # Whether `command` builds the owner record from scratch. Needs `references: nil` plus a
21
+ # supplied required owner field, by `:set` or a same-named argument. Arguments claimed by an
22
+ # `append` don't count (`ValueObject.Member#position` is not the owner's). Completeness of
23
+ # state is not the test: `Analyzer#complete_state?` disagrees on both sides.
24
+ def creates_owner?(aggregate, command, value_objects_by_name)
25
+ return false unless command[:references].nil?
26
+
27
+ owner_fields = aggregate[:attributes].map { |a| a[:name].to_s }.to_set
28
+ owner_fields << aggregate[:lifecycle][:field].to_s if aggregate[:lifecycle]
29
+ required_fields = aggregate[:attributes].reject { |a| a[:list] || a[:optional] }.map { |a| a[:name].to_s }.to_set
30
+
31
+ # Arguments claimed by an append never bare-name-match an owner field.
32
+ append_claimed = Set.new
33
+ command[:mutations].each do |m|
34
+ next unless m[:op].to_s == "append"
35
+
36
+ Array(m[:fields]&.values).each do |v|
37
+ source = append_field_source(v)
38
+ append_claimed << source.to_s if source.is_a?(Symbol)
39
+ end
40
+ end
41
+
42
+ known_writes = Set.new
43
+
44
+ # A same-named argument is copied across by `record_fields` with no `:set`.
45
+ command[:attributes].each do |attr|
46
+ name = attr[:name].to_s
47
+ known_writes << name if owner_fields.include?(name) && !append_claimed.include?(name)
48
+ end
49
+
50
+ # A `:set` covers the source-from-another-argument shape (`sets :field, to: :other_arg`).
51
+ command[:mutations].each do |m|
52
+ next unless m[:op].to_s == "set"
53
+
54
+ target = m[:target].to_s
55
+ known_writes << target if owner_fields.include?(target)
56
+ end
57
+
58
+ required_fields.any? { |field| known_writes.include?(field) }
59
+ end
60
+
61
+ # Resolves an `append` target to its entity or value object, or nil if it is neither.
62
+ def append_element(aggregate, target_type, value_objects_by_name)
63
+ # LOCAL FIRST — an aggregate's own nested entity is scoped to that
64
+ # Local entities win over the domain-wide value objects: Syntax's local `Argument` entity
65
+ # shares a name with Command's `Argument` value object.
66
+ local = aggregate[:entities].find { |e| e[:name] == target_type }
67
+ return local if local
68
+
69
+ value_objects_by_name[target_type]
70
+ end
71
+
72
+ # Identity attribute and its single-field value object for an auto-minted entity element,
73
+ # or nil for a composite or non-bare identity.
74
+ def entity_identity_mint(entity, value_objects_by_name)
75
+ id_path = entity[:identified_by]&.first
76
+ return nil unless id_path
77
+
78
+ head, *rest = id_path.split(".")
79
+ return nil unless rest.size == 1
80
+
81
+ attr = entity[:attributes].find { |a| a[:name].to_s == head }
82
+ return nil unless attr
83
+
84
+ vo = value_objects_by_name[attr[:type]]
85
+ return nil unless vo && !vo[:closed_set] && vo[:attributes].size == 1 && vo[:attributes].first[:name].to_s == rest.first
86
+
87
+ [attr, vo]
88
+ end
89
+
90
+ # Guard for a whole-list `:set` of entities: refuses duplicate identities pairwise.
91
+ # Returns `[guard_text, effective_rhs]`. Every `identified_by` head is compared at once (not
92
+ # just the first), so a composite identity is guarded the same as a single-field one — see
93
+ # `check_entity_collision` in `entity_element.rb` (Ruby's runtime `heads.all?`).
94
+ def entity_list_replace_guard(aggregate, target_attr, target_field, rhs, value_objects_by_name)
95
+ entity = aggregate[:entities].find { |e| e[:name] == target_attr[:type] }
96
+ return ["", rhs] unless entity && entity[:identified_by]&.any?
97
+
98
+ checks = []
99
+ offered_exprs = []
100
+ entity[:identified_by].each do |path|
101
+ id_head = path.to_s.split(".").first
102
+ id_attr = entity[:attributes].find { |a| a[:name].to_s == id_head }
103
+ return ["", rhs] unless id_attr
104
+
105
+ id_field = rust_ident_field(id_attr[:name])
106
+ checks << "prior.#{id_field} == e.#{id_field}"
107
+
108
+ # Single-field unwrap, so `format!` prints the bare scalar as Ruby's refusal wording does.
109
+ id_vo = value_objects_by_name[id_attr[:type]]
110
+ offered_exprs <<
111
+ if id_vo && !id_vo[:closed_set] && id_vo[:attributes].size == 1
112
+ "e.#{id_field}.#{rust_ident_field(id_vo[:attributes].first[:name])}"
113
+ else
114
+ "e.#{id_field}"
115
+ end
116
+ end
117
+
118
+ local_var = "replaced_#{target_field}"
119
+ entity_lit = entity[:name].to_s.inspect
120
+ aggregate_lit = aggregate[:name].to_s.inspect
121
+ identity_lit = entity[:identified_by].join(", ").inspect
122
+ condition = checks.join(" && ")
123
+ offered_lets = offered_exprs.each_with_index.map { |expr, i| "let offered_#{i} = format!(\"{:?}\", #{expr});" }
124
+ offered_refs = offered_exprs.each_index.map { |i| "offered_#{i}.as_str()" }
125
+ guard =
126
+ "let #{local_var} = #{rhs};\n " \
127
+ "for (i, e) in #{local_var}.iter().enumerate() { if #{local_var}[..i].iter().any(|prior| #{condition}) " \
128
+ "{ #{offered_lets.join(' ')} let offered = [#{offered_refs.join(', ')}]; " \
129
+ "return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs " \
130
+ "{ entity: #{entity_lit}, aggregate: #{aggregate_lit}, identity: #{identity_lit}, " \
131
+ "offered: &offered }.render_args())); } }\n "
132
+ [guard, local_var]
133
+ end
134
+
135
+ # The composite-identity twin of the single-field collision guard inlined in
136
+ # `emit_mutation_line_body`'s `append` branch: builds a duplicate check across every
137
+ # `identified_by` head at once (`e.batch == ... && e.sequence == ...`), matching
138
+ # `EntityElement.check_entity_collision`'s `heads.all?` on the Ruby runtime side. Called only
139
+ # once every head is confirmed present in the append's own `fields` map (composite identities
140
+ # never mint). Kept byte-for-byte with `rust/codegen/src/mutations.rs`'s
141
+ # `composite_append_collision_guard`.
142
+ def composite_append_collision_guard(heads, fields, entity, command, value_objects_by_name, aggregate, target_field, identified_by)
143
+ checks = []
144
+ offered_exprs = []
145
+ heads.each do |head|
146
+ field_attr = entity[:attributes].find { |a| a[:name].to_s == head }
147
+ _, source = fields.find { |field_name, _| field_name.to_s == head }
148
+ rhs = append_field_rhs(source, field_attr, command, value_objects_by_name, aggregate)
149
+ id_field = rust_ident_field(head)
150
+ checks << "e.#{id_field} == #{rhs}"
151
+
152
+ # Unwrap a single-field identity component so `offered` prints the bare scalar, as
153
+ # `Rendering.describe` does; Debug-printing the value object would give
154
+ # `LineSequence { value: 654 }`.
155
+ vo = value_objects_by_name[field_attr[:type]]
156
+ offered_exprs <<
157
+ if vo && !vo[:closed_set] && vo[:attributes].size == 1
158
+ "#{rhs}.#{rust_ident_field(vo[:attributes].first[:name])}"
159
+ else
160
+ rhs
161
+ end
162
+ end
163
+
164
+ entity_lit = entity[:name].to_s.inspect
165
+ aggregate_lit = aggregate[:name].to_s.inspect
166
+ identity_lit = identified_by.join(", ").inspect
167
+ condition = checks.join(" && ")
168
+ offered_lets = offered_exprs.each_with_index.map { |expr, i| "let offered_#{i} = format!(\"{:?}\", #{expr});" }
169
+ offered_refs = offered_exprs.each_index.map { |i| "offered_#{i}.as_str()" }
170
+
171
+ "if record.#{target_field}.iter().any(|e| #{condition}) " \
172
+ "{ #{offered_lets.join(' ')} let offered = [#{offered_refs.join(', ')}]; " \
173
+ "return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs " \
174
+ "{ entity: #{entity_lit}, aggregate: #{aggregate_lit}, identity: #{identity_lit}, " \
175
+ "offered: &offered }.render_args())); }\n "
176
+ end
177
+
178
+ # Why an `append` can't be generated: an unresolvable element, an undeclared field, a source
179
+ # that doesn't bridge, or an entity identity that is neither minted nor supplied.
180
+ def append_field_problems(command, aggregate, value_objects_by_name)
181
+ command[:mutations].select { |m| m[:op].to_s == "append" }.flat_map do |m|
182
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s }
183
+ element = target_attr && append_element(aggregate, target_attr[:type], value_objects_by_name)
184
+ next ["#{m[:target]}: element type #{target_attr&.dig(:type).inspect} not resolvable"] unless element
185
+
186
+ problems = m[:fields].filter_map do |field_name, source|
187
+ field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
188
+ next "#{m[:target]}.#{field_name}: not a declared field" unless field_attr
189
+
190
+ # A Symbol names an argument; anything else is the value (Hecks::Literal).
191
+ parsed = append_field_source(source)
192
+ next nil if parsed.is_a?(Hecks::StateRef) # checked by state_source_problems
193
+ next literal_problem(m, field_name, parsed, field_attr, value_objects_by_name) unless parsed.is_a?(Symbol)
194
+
195
+ arg_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
196
+ if arg_attr.nil?
197
+ "#{m[:target]}.#{field_name}: sources undeclared argument #{parsed}"
198
+ elsif !bridgeable_value_types?(arg_attr[:type], field_attr[:type], value_objects_by_name)
199
+ "#{m[:target]}.#{field_name}: #{arg_attr[:type]} doesn't bridge to #{field_attr[:type]}"
200
+ end
201
+ end
202
+
203
+ entity = aggregate[:entities].find { |e| e[:name] == target_attr[:type] }
204
+ next problems unless entity
205
+
206
+ present = m[:fields].keys.map(&:to_s)
207
+ id_head = entity[:identified_by]&.first.to_s.split(".").first
208
+ problems << "#{m[:target]}: #{entity[:name]}'s identity doesn't auto-mint" if !present.include?(id_head) && !entity_identity_mint(entity, value_objects_by_name)
209
+ problems
210
+ end
211
+ end
212
+
213
+ # Why a `remove:` can't be generated. Only entity-typed lists are supported, matched by a
214
+ # single bridgeable identity field, the same shape `entity_identity_mint` requires.
215
+ def remove_field_problems(command, aggregate, value_objects_by_name)
216
+ command[:mutations].select { |m| m[:op].to_s == "remove" }.filter_map do |m|
217
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s }
218
+ next "#{m[:target]}: not a declared list attribute" unless target_attr && target_attr[:list]
219
+
220
+ entity = aggregate[:entities].find { |e| e[:name] == target_attr[:type] }
221
+ next "#{m[:target]}: remove on a value-object-typed list is not generated yet" unless entity
222
+
223
+ # `entity_identity_mint` inspects only the first head, so a composite identity is
224
+ # excluded here.
225
+ if Array(entity[:identified_by]).size != 1
226
+ next "#{m[:target]}: #{entity[:name]}'s identity is composite — remove not generated yet"
227
+ end
228
+
229
+ id_attr, = entity_identity_mint(entity, value_objects_by_name)
230
+ unless id_attr
231
+ next "#{m[:target]}: #{entity[:name]}'s identity isn't a single bridgeable field — remove not generated yet"
232
+ end
233
+
234
+ unless m[:source].is_a?(Hash) && m[:source][:kind] == "argument"
235
+ next "#{m[:target]}: remove sources a literal or record state, not an argument — not generated yet"
236
+ end
237
+
238
+ source_attr = command[:attributes].find { |a| a[:name].to_s == m[:source][:name].to_s }
239
+ next "#{m[:target]}: remove sources undeclared argument #{m[:source][:name]}" unless source_attr
240
+
241
+ unless bridgeable_value_types?(source_attr[:type], id_attr[:type], value_objects_by_name)
242
+ next "#{m[:target]}: #{source_attr[:type]} doesn't bridge to #{id_attr[:type]}"
243
+ end
244
+
245
+ nil
246
+ end
247
+ end
248
+
249
+ # Decodes an append field source: a Symbol names a command argument, anything else is a
250
+ # literal.
251
+ def append_field_source(source) = Hecks::Bluebook::Assembly::Marks.read(source)
252
+
253
+ def literal_problem(mutation, field_name, literal, field_attr, value_objects_by_name)
254
+ return nil if literal_set_bridgeable?(literal, field_attr[:type], value_objects_by_name)
255
+
256
+ "#{mutation[:target]}.#{field_name}: literal doesn't bridge to #{field_attr[:type]}"
257
+ end
258
+
259
+ # Collapses the transition rows a command names into the `field`/`from_states` hash
260
+ # TransitionCheck reads.
261
+ def lifecycle_transition_for(command, aggregate)
262
+ return nil unless aggregate[:lifecycle]
263
+
264
+ rows = aggregate[:lifecycle][:transitions].select { |t| t[:command] == command[:name] }
265
+ # `from:` without a transition only guards on state; `to_state: nil` means it moves
266
+ # nothing.
267
+ if rows.empty?
268
+ froms = Array(command[:from]).compact.map(&:to_s)
269
+ return nil if froms.empty?
270
+
271
+ return { field: aggregate[:lifecycle][:field], to_state: nil, from_states: froms.uniq, unconstrained: false }
272
+ end
273
+
274
+ # A `from: nil` row is unconstrained and admits any state. It is carried as
275
+ # `unconstrained`
276
+ # rather than dropped: the caller also reads `to_state` to advance the lifecycle.
277
+ froms = rows.map { |r| r[:from_state] }
278
+ {
279
+ field: aggregate[:lifecycle][:field],
280
+ to_state: rows.first[:to_state],
281
+ from_states: froms.compact.uniq,
282
+ unconstrained: froms.any?(&:nil?)
283
+ }
284
+ end
285
+
286
+ # Rust expression for a `:set` source, coerced to the target attribute's declared type.
287
+ # With `target_list:` a list source maps element-wise instead of unwrapping a single-field
288
+ # value object out of a `Vec<T>`.
289
+ def mutation_set_rhs(source, target_type, command, value_objects_by_name, target_list: false)
290
+ if source[:kind] == "literal"
291
+ return literal_rhs_for(source[:value], target_type, value_objects_by_name)
292
+ end
293
+
294
+ # `state(:field)` clones the record's value; `pre` is the pre-dispatch state, so effect
295
+ # order is irrelevant.
296
+ return "pre.#{rust_ident_field(source[:name])}.clone()" if source[:kind] == "state"
297
+
298
+ source_attr = command[:attributes].find { |a| a[:name].to_s == source[:name] }
299
+ source_expr = "args.#{rust_ident_field(source[:name])}"
300
+ if target_list && source_attr[:list] && list_bridge_requires_element_mapping?(source_attr[:type], target_type)
301
+ return list_value_rhs(source_expr, source_attr[:type], target_type, value_objects_by_name)
302
+ end
303
+
304
+ value_rhs(source_expr, source_attr[:type], target_type, value_objects_by_name)
305
+ end
306
+
307
+ # Problems with `state(:field)` sources: only a same-type, same-list-ness copy is generated.
308
+ # generator has no data for, and says so rather than guessing.
309
+ def state_source_problems(command, aggregate, value_objects_by_name)
310
+ command[:mutations].flat_map do |m|
311
+ case m[:op].to_s
312
+ when "set"
313
+ next [] unless m[:source][:kind] == "state"
314
+ [state_source_problem(m[:target], m[:source][:name], aggregate, aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s })].compact
315
+ when "append"
316
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == m[:target].to_s }
317
+ element = target_attr && append_element(aggregate, target_attr[:type], value_objects_by_name)
318
+ next [] unless element
319
+ m[:fields].filter_map do |field_name, source|
320
+ parsed = append_field_source(source)
321
+ next unless parsed.is_a?(Hecks::StateRef)
322
+ state_source_problem("#{m[:target]}.#{field_name}", parsed.name, aggregate, element[:attributes].find { |a| a[:name].to_s == field_name.to_s })
323
+ end
324
+ else
325
+ []
326
+ end
327
+ end
328
+ end
329
+
330
+ def state_source_problem(label, state_name, aggregate, target_attr)
331
+ state_attr = aggregate[:attributes].find { |a| a[:name].to_s == state_name.to_s }
332
+ return "#{label}: sources state(:#{state_name}), which #{aggregate[:name]} does not declare" unless state_attr
333
+ return "#{label}: no such target field" unless target_attr
334
+ same = state_attr[:type] == target_attr[:type] && !!state_attr[:list] == !!target_attr[:list]
335
+ "#{label}: state(:#{state_name}) is #{state_attr[:list] ? 'a list of ' : ''}#{state_attr[:type]}, the target wants #{target_attr[:list] ? 'a list of ' : ''}#{target_attr[:type]} — not generated yet" unless same
336
+ end
337
+
338
+ # One entry per `identified_by` component, as `{ expr:, param:, head: }`. A head that is
339
+ # "declared" reads `args.<head>` (walking a dotted rest); otherwise the caller supplies it
340
+ # as a
341
+ # `&str` parameter, and `head:` is the raw JSON key the router reads it under.
342
+ # Declared means a `:set` targets the head, or a same-named argument not claimed by an
343
+ # append
344
+ # supplies it (bare-name copy, `Governance::RoleAssignment.Assign`). A bare name shared with
345
+ # an
346
+ # appended attribute (`Aggregate.Attribute#name`) is not the owner's identity.
347
+ def identity_components(aggregate, command)
348
+ # Declared: a `:set` targets the head, or a same-named argument not claimed by an append.
349
+ append_claimed = Set.new
350
+ command[:mutations].each do |m|
351
+ next unless m[:op].to_s == "append"
352
+
353
+ Array(m[:fields]&.values).each do |v|
354
+ source = append_field_source(v)
355
+ append_claimed << source.to_s if source.is_a?(Symbol)
356
+ end
357
+ end
358
+ declared_names = command[:attributes].map { |a| a[:name].to_s }.to_set - append_claimed
359
+
360
+ aggregate[:identified_by].map do |path|
361
+ head, *rest = path.split(".")
362
+ set_target = command[:mutations].any? { |m| m[:op].to_s == "set" && m[:target].to_s == head }
363
+ if set_target || declared_names.include?(head)
364
+ if rest.any?
365
+ { expr: "args.#{rust_ident_field(head)}.#{rest.map { |seg| rust_ident_field(seg) }.join('.')}.to_string()", param: nil }
366
+ else
367
+ { expr: "args.#{rust_ident_field(head)}.to_string()", param: nil }
368
+ end
369
+ else
370
+ # `.to_string()` keeps this an owned `String`: a single-component identity is returned
371
+ # unwrapped into a `String` field, and this param is `&str`.
372
+ param = rust_ident_field(head)
373
+ { expr: "#{param}.to_string()", param: "#{param}: &str", head: head }
374
+ end
375
+ end
376
+ end
377
+
378
+ def build_identity_expr(components)
379
+ return components.first[:expr] if components.size == 1
380
+
381
+ placeholders = components.map { "{}" }.join(":")
382
+ "format!(#{placeholders.inspect}, #{components.map { |c| c[:expr] }.join(', ')})"
383
+ end
384
+
385
+ # Value of one `append` field: an argument runs through the `value_rhs` bridge, a literal
386
+ # is built inline. A caller-omittable argument is `Option<T>` in Rust, so it bridges through
387
+ # `optional_value_rhs`; `mark_append_optional_fields!` has already made the field match.
388
+ def append_field_rhs(source, field_attr, command, value_objects_by_name, aggregate = nil)
389
+ parsed = append_field_source(source)
390
+ return state_field_rhs(parsed, field_attr, aggregate) if parsed.is_a?(Hecks::StateRef)
391
+ return literal_rhs_for(parsed, field_attr[:type], value_objects_by_name) unless parsed.is_a?(Symbol)
392
+
393
+ arg_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
394
+ arg_expr = "args.#{rust_ident_field(arg_attr[:name])}"
395
+ if arg_attr[:optional]
396
+ # Same representation on both sides: clone the `Option` as is. Only a cross-type
397
+ # coercion needs a per-element `.map`.
398
+ same_representation = arg_attr[:type] == field_attr[:type] ||
399
+ (effective_scalar_type(arg_attr[:type]) && effective_scalar_type(arg_attr[:type]) == effective_scalar_type(field_attr[:type]))
400
+ return "#{arg_expr}.clone()" if same_representation
401
+
402
+ return optional_value_rhs(arg_expr, arg_attr[:type], field_attr[:type], value_objects_by_name)
403
+ end
404
+
405
+ rhs = value_rhs(arg_expr, arg_attr[:type], field_attr[:type], value_objects_by_name)
406
+ # The struct field may be `Option<T>` because another command sources it optionally,
407
+ # so a required source still needs `Some(...)`.
408
+ field_attr[:optional] ? "Some(#{rhs})" : rhs
409
+ end
410
+
411
+ # Whether any effect reads the record's own state (`state(:field)` in a `set` or `append`);
412
+ # only then does the closure bind `pre`.
413
+ def reads_pre_state?(mutations)
414
+ mutations.any? do |m|
415
+ case m[:op].to_s
416
+ when "set" then m[:source][:kind] == "state"
417
+ when "append" then m[:fields].values.any? { |source| append_field_source(source).is_a?(Hecks::StateRef) }
418
+ else false
419
+ end
420
+ end
421
+ end
422
+
423
+ def pre_state_line = " let pre = record.clone();"
424
+
425
+ # An Integer effect that leaves signed 64-bit is a Fault, never a wrap or panic (C3.3).
426
+ # `amount` is bound once so the wording can quote it.
427
+ CHECKED_OPS = { "+" => "checked_add", "-" => "checked_sub", "*" => "checked_mul" }.freeze
428
+
429
+ def checked_arithmetic(op, field_ident, symbol, amount_expr)
430
+ "{ let amount = #{amount_expr}; current.#{field_ident}.#{CHECKED_OPS.fetch(symbol)}(amount)" \
431
+ ".ok_or_else(|| crate::kernel::Refusal::Fault(format!(\"#{op} overflowed: {} #{symbol} {} does not fit in a " \
432
+ "64-bit integer\", current.#{field_ident}, amount)))? }"
433
+ end
434
+
435
+ # `state(:field)` into an appended element's field. Records `Option`-wrap non-list fields
436
+ # and
437
+ # elements don't, so scalars unwrap and lists clone as is.
438
+ def state_field_rhs(parsed, field_attr, aggregate)
439
+ expr = "pre.#{rust_ident_field(parsed.name)}.clone()"
440
+ state_attr = aggregate && aggregate[:attributes].find { |a| a[:name].to_s == parsed.name.to_s }
441
+ return expr if state_attr && state_attr[:list]
442
+ return expr if field_attr[:optional]
443
+
444
+ "#{expr}.unwrap()"
445
+ end
446
+
447
+ # The `value_rhs` bridge run against the value a `.map` closure unwraps from an `Option<T>`.
448
+ def optional_value_rhs(source_expr, source_type, target_type, value_objects_by_name)
449
+ "#{source_expr}.clone().map(|v| #{value_rhs('v', source_type, target_type, value_objects_by_name)})"
450
+ end
451
+
452
+ # Makes an appended element's field `Option<T>` when any command sources it from an optional
453
+ # argument, since Ruby stores the omitted nil without complaint. Mutates the shared
454
+ # attribute
455
+ # hash once, before the emitters read it, and never resets a `true`.
456
+ def mark_append_optional_fields!(aggregate, value_objects_by_name)
457
+ aggregate[:attributes].each do |target_attr|
458
+ element = append_element(aggregate, target_attr[:type], value_objects_by_name)
459
+ next unless element
460
+
461
+ aggregate[:commands].each do |command|
462
+ command[:mutations].each do |m|
463
+ next unless m[:op].to_s == "append" && m[:target].to_s == target_attr[:name].to_s
464
+
465
+ m[:fields].each do |field_name, source|
466
+ # A Symbol is an argument name (optional ones are what this pass wants); else a
467
+ # literal.
468
+ parsed = append_field_source(source)
469
+ next unless parsed.is_a?(Symbol)
470
+
471
+ source_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
472
+ next unless source_attr && source_attr[:optional]
473
+
474
+ field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
475
+ field_attr[:optional] = true if field_attr
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
481
+
482
+ # Derives the mutations of a `corrects EVENT, reverses: true` command from the commands that
483
+ # emit EVENT: only increment/decrement invert (ADR 0041). Left alone when nothing emits the
484
+ # event
485
+ # or any sibling mutation can't invert, so `command_skip_reason` still refuses it.
486
+ INVERSE_MUTATION_OP = { "increment" => "decrement", "decrement" => "increment" }.freeze
487
+
488
+ def derive_reverses_mutations!(aggregate)
489
+ emitted_by = Hash.new { |hash, key| hash[key] = [] }
490
+ aggregate[:commands].each { |command| Array(command[:emits]).each { |event_name| emitted_by[event_name.to_s] << command } }
491
+
492
+ aggregate[:commands].each do |command|
493
+ correction = corrects_of(command)
494
+ next unless correction && corrects_reverses?(correction)
495
+
496
+ sources = emitted_by[correction[:target].to_s]
497
+ next if sources.empty?
498
+
499
+ derived = sources.flat_map { |c| c[:mutations] }.reject { |m| m[:op].to_s == "corrects" }
500
+ next if derived.empty? || derived.any? { |m| !INVERSE_MUTATION_OP.key?(m[:op].to_s) }
501
+
502
+ # Never derive twice onto a command that already has mutations, so re-runs stay
503
+ # idempotent.
504
+ # generator re-run over already-derived IR stays idempotent.
505
+ next if command[:mutations].any? { |m| m[:op].to_s != "corrects" }
506
+
507
+ derived.each do |m|
508
+ command[:mutations] << { op: INVERSE_MUTATION_OP.fetch(m[:op].to_s), target: m[:target], source: m[:source] }
509
+ end
510
+ end
511
+ end
512
+
513
+ # `optional:` is true for an aggregate record (non-list fields are `Option`-wrapped) and
514
+ # false for an entity element.
515
+ def emit_mutation_line(mutation, aggregate, command, value_objects_by_name, optional: true)
516
+ target_field = rust_ident_field(mutation[:target])
517
+ lifecycle_field = aggregate[:lifecycle] && aggregate[:lifecycle][:field].to_s
518
+
519
+ # Callers join these lines with a newline, expecting each to carry its own 8-space indent.
520
+ " #{emit_mutation_line_body(mutation, aggregate, command, value_objects_by_name, target_field, lifecycle_field, optional)}"
521
+ end
522
+
523
+ def emit_mutation_line_body(mutation, aggregate, command, value_objects_by_name, target_field, lifecycle_field, optional)
524
+ case mutation[:op].to_s
525
+ when "append"
526
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == mutation[:target].to_s }
527
+ vo_type = rust_ident(target_attr[:type])
528
+ entity = aggregate[:entities].find { |e| e[:name] == target_attr[:type] }
529
+ element = entity || value_objects_by_name[target_attr[:type]]
530
+
531
+ fields_assignment = mutation[:fields].map do |field_name, source|
532
+ field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
533
+ "#{rust_ident_field(field_name)}: #{append_field_rhs(source, field_attr, command, value_objects_by_name, aggregate)}"
534
+ end
535
+
536
+ # An entity element also needs a minted identity and its lifecycle default; no
537
+ # `append:` names them.
538
+ collision_guard = ""
539
+ if entity
540
+ present = mutation[:fields].keys.map(&:to_s)
541
+ identified_by = Array(entity[:identified_by])
542
+ if identified_by.size > 1
543
+ # Composite identity: minting a partial key makes no sense, so every head must
544
+ # already be caller-supplied (as `Venue.AddLine`'s `append: { batch: :batch,
545
+ # sequence: :sequence }` does). Refuse a duplicate across the WHOLE tuple, the way
546
+ # `EntityElement.check_entity_collision`'s `heads.all?` does — this branch used to be
547
+ # skipped outright for any composite identity, so two elements sharing every identity
548
+ # component went through unrefused. Root cause of this bug.
549
+ heads = identified_by.map { |path| path.to_s.split(".").first }
550
+ if heads.all? { |head| present.include?(head) }
551
+ collision_guard = composite_append_collision_guard(heads, mutation[:fields], entity, command,
552
+ value_objects_by_name, aggregate, target_field,
553
+ identified_by)
554
+ end
555
+ else
556
+ id_attr, id_vo = entity_identity_mint(entity, value_objects_by_name)
557
+ if id_attr && !present.include?(id_attr[:name].to_s)
558
+ # One past the highest identity held, never `len() + 1`, which repeats after a
559
+ # shrink (C4.5).
560
+ id_field = rust_ident_field(id_attr[:name])
561
+ vo_field = rust_ident_field(id_vo[:attributes].first[:name])
562
+ mint = "#{rust_ident(id_attr[:type])} { #{vo_field}: record.#{target_field}.iter().map(|e| e.#{id_field}.#{vo_field}).max().unwrap_or(0) + 1 }"
563
+ fields_assignment << "#{rust_ident_field(id_attr[:name])}: #{mint}"
564
+ present << id_attr[:name].to_s
565
+ elsif id_attr
566
+ # Caller-supplied single-field identity: refuse a duplicate
567
+ # (`check_entity_collision`), matching rust/codegen byte for byte.
568
+ id_field = rust_ident_field(id_attr[:name])
569
+ _, source = mutation[:fields].find { |field_name, _| field_name.to_s == id_attr[:name].to_s }
570
+ field_attr = entity[:attributes].find { |a| a[:name].to_s == id_attr[:name].to_s }
571
+ id_rhs = append_field_rhs(source, field_attr, command, value_objects_by_name, aggregate)
572
+ entity_lit = entity[:name].to_s.inspect
573
+ aggregate_lit = aggregate[:name].to_s.inspect
574
+ identity_lit = identified_by.join(", ").inspect
575
+ # Single-field unwrap so `format!` prints the bare scalar; `id_vo` is non-nil here.
576
+ offered_field = rust_ident_field(id_vo[:attributes].first[:name])
577
+ offered_expr = "#{id_rhs}.#{offered_field}"
578
+ collision_guard =
579
+ "if record.#{target_field}.iter().any(|e| e.#{id_field} == #{id_rhs}) " \
580
+ "{ let offered = format!(\"{:?}\", #{offered_expr}); " \
581
+ "return Err(crate::kernel::Refusal::AlreadyExists(crate::kernel::refusal_wording::AlreadyExistsEntityDuplicateArgs " \
582
+ "{ entity: #{entity_lit}, aggregate: #{aggregate_lit}, identity: #{identity_lit}, " \
583
+ "offered: &[offered.as_str()] }.render_args())); }\n "
584
+ end
585
+ end
586
+ if entity[:lifecycle] && !present.include?(entity[:lifecycle][:field].to_s)
587
+ fields_assignment << "#{rust_ident_field(entity[:lifecycle][:field])}: #{entity[:lifecycle][:default].inspect}.to_string()"
588
+ present << entity[:lifecycle][:field].to_s
589
+ end
590
+
591
+ # A list attribute no `append:` binding names starts empty. Ruby's fields Hash just
592
+ # lacks the
593
+ # key, but a Rust struct literal must assign every field.
594
+ entity[:attributes].each do |attr|
595
+ next unless attr[:list]
596
+ next if present.include?(attr[:name].to_s)
597
+
598
+ fields_assignment << "#{rust_ident_field(attr[:name])}: Vec::new()"
599
+ present << attr[:name].to_s
600
+ end
601
+
602
+ # An optional scalar attribute is `Option<T>` in Rust, so an unmentioned one defaults
603
+ # to `None`.
604
+ entity[:attributes].each do |attr|
605
+ next if attr[:list] || !attr[:optional]
606
+ next if present.include?(attr[:name].to_s)
607
+
608
+ fields_assignment << "#{rust_ident_field(attr[:name])}: None"
609
+ present << attr[:name].to_s
610
+ end
611
+ end
612
+
613
+ "#{collision_guard}#{Exemplar.render(
614
+ "mutation_append",
615
+ "tmpl_field" => target_field,
616
+ "tmpl_fields_placeholder()" => "#{vo_type} { #{fields_assignment.join(', ')} }"
617
+ )}"
618
+ when "set"
619
+ if mutation[:target].to_s == lifecycle_field
620
+ rhs = mutation_set_rhs(mutation[:source], "String", command, value_objects_by_name)
621
+ Exemplar.render("mutation_set_plain", "tmpl_field" => target_field, "tmpl_rhs_placeholder2()" => rhs)
622
+ else
623
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == mutation[:target].to_s }
624
+ rhs = mutation_set_rhs(mutation[:source], target_attr[:type], command, value_objects_by_name, target_list: !!target_attr[:list])
625
+ source_attr = mutation[:source][:kind] == "argument" ? command[:attributes].find { |a| a[:name].to_s == mutation[:source][:name].to_s } : nil
626
+
627
+ if target_attr[:list] && optional && list_attr_creation_optional?(aggregate, target_attr[:name], value_objects_by_name)
628
+ # `CardPayment.Authorize`'s redundant `sets :tags`: `list_attr_creation_optional?`
629
+ # already
630
+ # Option-wrapped this field, so the value is assigned straight across.
631
+ Exemplar.render("mutation_set_plain", "tmpl_field" => target_field, "tmpl_rhs_placeholder2()" => rhs)
632
+ elsif target_attr[:list] && source_attr && source_attr[:optional]
633
+ # A record's list field is a plain `Vec<T>` here; an optional source unwraps with a
634
+ # `[]` fallback.
635
+ Exemplar.render("mutation_set_unwrap_or_default", "tmpl_field" => target_field, "tmpl_optional_rhs_placeholder()" => rhs)
636
+ elsif target_attr[:list]
637
+ # Duplicate-identity guard for a whole-list replace of entities
638
+ # (`Ledger.ReplaceEntries`).
639
+ guard, effective_rhs = entity_list_replace_guard(aggregate, target_attr, target_field, rhs, value_objects_by_name)
640
+ "#{guard}#{Exemplar.render("mutation_set_plain", "tmpl_field" => target_field, "tmpl_rhs_placeholder2()" => effective_rhs)}"
641
+ else
642
+ # `wrap` for a per-field `Option<T>` target or a whole-record wrap, unless the
643
+ # source argument
644
+ # is already `Option<T>` (wrapping again would give `Option<Option<T>>`).
645
+ wrap = (optional || target_attr[:optional]) && !(source_attr && source_attr[:optional])
646
+ if wrap
647
+ Exemplar.render("mutation_set_wrapped", "tmpl_field" => target_field, "tmpl_rhs_placeholder2()" => rhs)
648
+ else
649
+ Exemplar.render("mutation_set_plain", "tmpl_field" => target_field, "tmpl_rhs_placeholder2()" => rhs)
650
+ end
651
+ end
652
+ end
653
+ when "increment", "decrement"
654
+ target_attr, integer_field = arithmetic_target_field(mutation, aggregate, value_objects_by_name)
655
+ vo_type = rust_ident(target_attr[:type])
656
+ field_ident = rust_ident_field(integer_field)
657
+ amount_expr = arithmetic_amount_expr(mutation[:source], command, value_objects_by_name, integer_field)
658
+ # The sign comes from the IR's `sign` field (`Bluebook::Mutation.sign_for`), not the op
659
+ # name.
660
+ sign = mutation[:sign].to_s == "1" ? "+" : "-"
661
+ current = optional ? "record.#{target_field}.clone().unwrap()" : "record.#{target_field}.clone()"
662
+ updated = "#{vo_type} { #{field_ident}: #{checked_arithmetic(mutation[:op].to_s, field_ident, sign, amount_expr)}, ..current }"
663
+ Exemplar.render(
664
+ "mutation_arithmetic",
665
+ "tmpl_field" => target_field,
666
+ "tmpl_current_placeholder()" => current,
667
+ "tmpl_updated_placeholder()" => (optional ? "Some(#{updated})" : updated)
668
+ )
669
+ when "multiply"
670
+ # `multiply` shares increment/decrement's Integer-only target and amount pairing but has
671
+ # no
672
+ # `sign`; Float value objects are not reachable here.
673
+ target_attr, integer_field = arithmetic_target_field(mutation, aggregate, value_objects_by_name)
674
+ vo_type = rust_ident(target_attr[:type])
675
+ field_ident = rust_ident_field(integer_field)
676
+ amount_expr = arithmetic_amount_expr(mutation[:source], command, value_objects_by_name, integer_field)
677
+ current = optional ? "record.#{target_field}.clone().unwrap()" : "record.#{target_field}.clone()"
678
+ updated = "#{vo_type} { #{field_ident}: #{checked_arithmetic('multiply', field_ident, '*', amount_expr)}, ..current }"
679
+ Exemplar.render(
680
+ "mutation_arithmetic",
681
+ "tmpl_field" => target_field,
682
+ "tmpl_current_placeholder()" => current,
683
+ "tmpl_updated_placeholder()" => (optional ? "Some(#{updated})" : updated)
684
+ )
685
+ when "clamp"
686
+ # `clamp` bounds the current value into a literal `[min, max]`, so there is no amount
687
+ # argument.
688
+ # `i64::clamp` matches Ruby's `Integer#clamp`.
689
+ target_attr, integer_field = arithmetic_target_field(mutation, aggregate, value_objects_by_name)
690
+ vo_type = rust_ident(target_attr[:type])
691
+ field_ident = rust_ident_field(integer_field)
692
+ min, max = clamp_bounds_ints(mutation[:source])
693
+ current = optional ? "record.#{target_field}.clone().unwrap()" : "record.#{target_field}.clone()"
694
+ updated = "#{vo_type} { #{field_ident}: current.#{field_ident}.clamp(#{min}, #{max}), ..current }"
695
+ Exemplar.render(
696
+ "mutation_arithmetic",
697
+ "tmpl_field" => target_field,
698
+ "tmpl_current_placeholder()" => current,
699
+ "tmpl_updated_placeholder()" => (optional ? "Some(#{updated})" : updated)
700
+ )
701
+ when "remove"
702
+ # `remove:` on an entity-typed list matches by identity field (`remove_field_problems`
703
+ # already
704
+ # confirmed one exists); `retain` keeps every element whose identity differs.
705
+ target_attr = aggregate[:attributes].find { |a| a[:name].to_s == mutation[:target].to_s }
706
+ entity = aggregate[:entities].find { |e| e[:name] == target_attr[:type] }
707
+ id_attr, = entity_identity_mint(entity, value_objects_by_name)
708
+ id_field = rust_ident_field(id_attr[:name])
709
+ source_attr = command[:attributes].find { |a| a[:name].to_s == mutation[:source][:name].to_s }
710
+ match_expr = value_rhs("args.#{rust_ident_field(source_attr[:name])}", source_attr[:type],
711
+ id_attr[:type], value_objects_by_name)
712
+ Exemplar.render(
713
+ "mutation_remove",
714
+ "tmpl_field" => target_field,
715
+ "tmpl_id_field" => id_field,
716
+ "tmpl_remove_match_placeholder()" => match_expr
717
+ )
718
+ end
719
+ end
720
+ end
721
+ end