hecks 2.8.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 (886) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +50 -7
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +32 -100
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +10 -32
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  25. data/lib/hecks/adapters/driven/postgres.rb +149 -136
  26. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  27. data/lib/hecks/adapters/driven/prism.rb +48 -38
  28. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  29. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  30. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  31. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  32. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  33. data/lib/hecks/adapters/driven/tenant_provisioner.rb +76 -62
  34. data/lib/hecks/adapters/driven.rb +5 -36
  35. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  36. data/lib/hecks/behaviors/dsl.rb +7 -25
  37. data/lib/hecks/behaviors/expectations.rb +32 -142
  38. data/lib/hecks/behaviors/ir.rb +7 -20
  39. data/lib/hecks/behaviors/rspec.rb +6 -17
  40. data/lib/hecks/behaviors/runner.rb +10 -34
  41. data/lib/hecks/behaviors.rb +4 -8
  42. data/lib/hecks/bench/cli.rb +62 -0
  43. data/lib/hecks/bench/environment.rb +71 -0
  44. data/lib/hecks/bench/postgres_probe.rb +30 -0
  45. data/lib/hecks/bench/report.rb +76 -0
  46. data/lib/hecks/bench/ruby_runner.rb +93 -0
  47. data/lib/hecks/bench/run.rb +55 -0
  48. data/lib/hecks/bench/rust_runner.rb +101 -0
  49. data/lib/hecks/bench/stats.rb +46 -0
  50. data/lib/hecks/bench/suite.rb +98 -0
  51. data/lib/hecks/bench/workload.rb +89 -0
  52. data/lib/hecks/bench.rb +17 -0
  53. data/lib/hecks/bluebook/aggregate.rb +9 -75
  54. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  55. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  56. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  57. data/lib/hecks/bluebook/assembly/contracts.rb +56 -174
  58. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  59. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  60. data/lib/hecks/bluebook/assembly.rb +6 -45
  61. data/lib/hecks/bluebook/attribute.rb +12 -61
  62. data/lib/hecks/bluebook/behaviour/aggregate.rb +39 -39
  63. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  64. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  65. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  66. data/lib/hecks/bluebook/behaviour/domain_port.rb +11 -20
  67. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  68. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  69. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  70. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  71. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  73. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  75. data/lib/hecks/bluebook/capabilities.rb +17 -49
  76. data/lib/hecks/bluebook/chapter.rb +16 -50
  77. data/lib/hecks/bluebook/command.rb +23 -81
  78. data/lib/hecks/bluebook/domain_port.rb +49 -67
  79. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  80. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  81. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  82. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  83. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +96 -708
  84. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +63 -169
  85. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +3 -1
  86. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  87. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  88. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +42 -89
  89. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  90. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  91. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +148 -197
  92. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  93. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  94. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  95. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  96. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  97. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  98. data/lib/hecks/bluebook/dsl/query_builder.rb +38 -43
  99. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  100. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  101. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  102. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  103. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  104. data/lib/hecks/bluebook/dsl/world_builder.rb +46 -111
  105. data/lib/hecks/bluebook/entity.rb +13 -71
  106. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  107. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  108. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  109. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  110. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  111. data/lib/hecks/bluebook/expression/resolver.rb +39 -661
  112. data/lib/hecks/bluebook/hexagon.rb +18 -33
  113. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  114. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  115. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  116. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  117. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  118. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  119. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +25 -176
  120. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  121. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -306
  122. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  123. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  124. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  125. data/lib/hecks/bluebook/meta_validator.rb +87 -352
  126. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  127. data/lib/hecks/bluebook/model_check.rb +122 -596
  128. data/lib/hecks/bluebook/pattern_subset.rb +49 -69
  129. data/lib/hecks/bluebook/process_manager.rb +13 -83
  130. data/lib/hecks/bluebook/project_loader.rb +3 -39
  131. data/lib/hecks/bluebook/project_register.rb +12 -60
  132. data/lib/hecks/bluebook/query.rb +27 -51
  133. data/lib/hecks/bluebook/read_model.rb +57 -81
  134. data/lib/hecks/bluebook/reference.rb +7 -41
  135. data/lib/hecks/bluebook/smoke_test.rb +9 -99
  136. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  137. data/lib/hecks/bluebook/translation.rb +26 -62
  138. data/lib/hecks/bluebook/value_object.rb +11 -36
  139. data/lib/hecks/bluebook.rb +5 -22
  140. data/lib/hecks/cache_dir.rb +4 -23
  141. data/lib/hecks/canonical_json.rb +30 -0
  142. data/lib/hecks/chapters.rb +102 -0
  143. data/lib/hecks/cli/behaviors.rb +61 -0
  144. data/lib/hecks/cli/canonicalise.rb +25 -0
  145. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  146. data/lib/hecks/cli/check_era.rb +88 -0
  147. data/lib/hecks/cli/console.rb +66 -0
  148. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  149. data/lib/hecks/cli/doc_coverage.rb +32 -0
  150. data/lib/hecks/cli/document.rb +5 -23
  151. data/lib/hecks/cli/expression_projection.rb +41 -0
  152. data/lib/hecks/cli/follow.rb +88 -0
  153. data/lib/hecks/cli/history.rb +44 -0
  154. data/lib/hecks/cli/ir.rb +4 -6
  155. data/lib/hecks/cli/mcp.rb +5 -6
  156. data/lib/hecks/cli/model_check.rb +24 -88
  157. data/lib/hecks/cli/pattern_cases.rb +47 -0
  158. data/lib/hecks/cli/present.rb +39 -0
  159. data/lib/hecks/cli/project_cli.rb +223 -64
  160. data/lib/hecks/cli/project_diagrams.rb +4 -10
  161. data/lib/hecks/cli/project_glossary.rb +44 -0
  162. data/lib/hecks/cli/reference.rb +46 -0
  163. data/lib/hecks/cli/refresh_projections.rb +44 -0
  164. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  165. data/lib/hecks/cli/release.rb +70 -0
  166. data/lib/hecks/cli/release_gem.rb +59 -0
  167. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  168. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  169. data/lib/hecks/cli/run.rb +15 -61
  170. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  171. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  172. data/lib/hecks/cli/shape.rb +70 -0
  173. data/lib/hecks/cli/smoke_http.rb +230 -0
  174. data/lib/hecks/cli/smoke_test.rb +7 -19
  175. data/lib/hecks/cli/spec_example.rb +27 -0
  176. data/lib/hecks/cli/statements.rb +32 -0
  177. data/lib/hecks/cli/stores.rb +4 -6
  178. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  179. data/lib/hecks/cli.rb +216 -73
  180. data/lib/hecks/codemod.rb +324 -0
  181. data/lib/hecks/construct.rb +8 -34
  182. data/lib/hecks/corpus.rb +423 -0
  183. data/lib/hecks/deploy/bluebook/deploy.bluebook +617 -158
  184. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -11
  185. data/lib/hecks/deploy/oidc.json +82 -2
  186. data/lib/hecks/doc/reference.rb +351 -0
  187. data/lib/hecks/doors/cli_door.rb +196 -0
  188. data/lib/hecks/doors/cli_runner.rb +297 -0
  189. data/lib/hecks/{facade → doors}/command_request.rb +14 -34
  190. data/lib/hecks/doors/handle.rb +206 -0
  191. data/lib/hecks/doors/json_door.rb +132 -0
  192. data/lib/hecks/doors/launcher_options.rb +129 -0
  193. data/lib/hecks/{cli → doors}/mcp_door.rb +25 -120
  194. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  195. data/lib/hecks/doors/ruby_door/aggregate_door.rb +162 -0
  196. data/lib/hecks/doors/ruby_door/chapter.rb +72 -0
  197. data/lib/hecks/doors/ruby_door.rb +77 -0
  198. data/lib/hecks/doors.rb +61 -0
  199. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  200. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  201. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  202. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  203. data/lib/hecks/embryonaut_bluebook.rb +5 -72
  204. data/lib/hecks/engine_agreement.rb +156 -0
  205. data/lib/hecks/facade.rb +23 -51
  206. data/lib/hecks/forms/app.rb +21 -76
  207. data/lib/hecks/forms/banking_presentation.rb +50 -0
  208. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  209. data/lib/hecks/forms/field_renderer.rb +17 -156
  210. data/lib/hecks/forms/field_shape.rb +31 -222
  211. data/lib/hecks/forms/html.rb +12 -47
  212. data/lib/hecks/forms/index_renderer.rb +1 -4
  213. data/lib/hecks/forms/page.rb +3 -9
  214. data/lib/hecks/forms/params.rb +14 -60
  215. data/lib/hecks/forms/port_argument.rb +2 -19
  216. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  217. data/lib/hecks/forms/record_renderer.rb +8 -75
  218. data/lib/hecks/forms/record_table.rb +7 -50
  219. data/lib/hecks/forms/reference_options.rb +3 -30
  220. data/lib/hecks/forms/value_object_shape.rb +8 -35
  221. data/lib/hecks/forms.rb +5 -33
  222. data/lib/hecks/fqn.rb +5 -12
  223. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  224. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  225. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  226. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  227. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  228. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  229. data/lib/hecks/framework.rb +11 -80
  230. data/lib/hecks/freezer.rb +5 -39
  231. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  232. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  233. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  234. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  235. data/lib/hecks/fuzzing/differential.rb +156 -0
  236. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  237. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  238. data/lib/hecks/fuzzing/form_census.rb +136 -0
  239. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  240. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  241. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  242. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  243. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  244. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  245. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  246. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  247. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  248. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  249. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  250. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  251. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  252. data/lib/hecks/fuzzing/properties.rb +162 -0
  253. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  254. data/lib/hecks/fuzzing/replay.rb +505 -0
  255. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  256. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  257. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  258. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  259. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  260. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  261. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  262. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  263. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  264. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  265. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  266. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  267. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  268. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  269. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  270. data/lib/hecks/fuzzing.rb +23 -0
  271. data/lib/hecks/grammar/evolve.rb +336 -0
  272. data/lib/hecks/grammar/expression.bluebook +15 -40
  273. data/lib/hecks/grammar/translation.bluebook +3 -6
  274. data/lib/hecks/grammar.rb +16 -41
  275. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  276. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  277. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  278. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  279. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  280. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  281. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  282. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  283. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  284. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  285. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  286. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  287. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  288. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  289. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  290. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  291. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  292. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  293. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  294. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  295. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  296. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  297. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  298. data/lib/hecks/hecks/adapters/git.rb +102 -0
  299. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  300. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  301. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  302. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  303. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  304. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  305. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  306. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  307. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  308. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  309. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  310. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  311. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  312. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  313. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  314. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  315. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  316. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  317. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  318. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  319. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  320. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  321. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  322. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  323. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  324. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  325. data/lib/hecks/hecks/codebase.behaviors +172 -0
  326. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  327. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  328. data/lib/hecks/hecks/custodian.behaviors +179 -0
  329. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  330. data/lib/hecks/hecks/deploy.behaviors +74 -0
  331. data/lib/hecks/hecks/deploy.world +12 -0
  332. data/lib/hecks/hecks/environments/memory.world +10 -0
  333. data/lib/hecks/hecks/governance.world +7 -0
  334. data/lib/hecks/hecks/hecks.behaviors +62 -0
  335. data/lib/hecks/hecks/hecks.bluebook +124 -0
  336. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  337. data/lib/hecks/hecks/hecks.world +42 -0
  338. data/lib/hecks/ir.rb +14 -83
  339. data/lib/hecks/kernel_capabilities.rb +194 -0
  340. data/lib/hecks/language/adapter.bluebook +2 -7
  341. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  342. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  343. data/lib/hecks/language/bluebook/bluebook.bluebook +26 -83
  344. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  345. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  346. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  347. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  348. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  349. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  350. data/lib/hecks/language/bluebook/query.bluebook +19 -43
  351. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  352. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  353. data/lib/hecks/language/bluebook/vocabulary.bluebook +83 -198
  354. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  355. data/lib/hecks/language/hecksagon/domain_port.bluebook +14 -19
  356. data/lib/hecks/language/hecksagon/hecksagon.bluebook +26 -25
  357. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  358. data/lib/hecks/language/oidc.json +35 -0
  359. data/lib/hecks/language/port.bluebook +4 -17
  360. data/lib/hecks/language/translation/translation.bluebook +3 -9
  361. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  362. data/lib/hecks/language/world/wiring.bluebook +1 -3
  363. data/lib/hecks/language/world/world.bluebook +5 -7
  364. data/lib/hecks/literal.rb +8 -50
  365. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  366. data/lib/hecks/naming.rb +29 -137
  367. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  368. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  369. data/lib/hecks/ports/access_control.rb +7 -23
  370. data/lib/hecks/ports/agent/answers.rb +5 -23
  371. data/lib/hecks/ports/agent.rb +47 -172
  372. data/lib/hecks/ports/authentication.rb +12 -39
  373. data/lib/hecks/ports/authorization.rb +16 -43
  374. data/lib/hecks/ports/clock.rb +5 -45
  375. data/lib/hecks/ports/extraction.rb +7 -12
  376. data/lib/hecks/ports/identity_assignment.rb +8 -29
  377. data/lib/hecks/ports/identity_generation.rb +6 -29
  378. data/lib/hecks/ports/identity_resolution.rb +2 -10
  379. data/lib/hecks/ports/key_vault.rb +9 -45
  380. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  381. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  382. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  383. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  384. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  385. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +44 -144
  386. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  387. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +20 -85
  388. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  389. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +18 -24
  390. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  391. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  392. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  393. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  394. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  395. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  396. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  397. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  398. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +19 -76
  399. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  400. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  401. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  402. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  403. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  404. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +43 -127
  405. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  406. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +131 -555
  407. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  408. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  409. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  410. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  411. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  412. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  413. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  414. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +50 -47
  415. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  416. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  417. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  418. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  419. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  420. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  421. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  422. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  423. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  424. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  425. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  426. data/lib/hecks/ports/persistence/state_codec.rb +47 -108
  427. data/lib/hecks/ports/persistence.rb +7 -14
  428. data/lib/hecks/ports/projection.rb +42 -84
  429. data/lib/hecks/ports/query/in_memory.rb +10 -58
  430. data/lib/hecks/ports/query/ordering.rb +11 -32
  431. data/lib/hecks/ports/query.rb +14 -37
  432. data/lib/hecks/ports.rb +2 -8
  433. data/lib/hecks/projection_files.rb +285 -0
  434. data/lib/hecks/projections/bootstrap_table.rb +12 -44
  435. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  436. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  437. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  438. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  439. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  440. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  441. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  442. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  443. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  444. data/lib/hecks/projections/deploy/fargate.rb +40 -207
  445. data/lib/hecks/projections/deploy/lambda.rb +191 -708
  446. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  447. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  448. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  449. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  450. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  451. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  452. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  453. data/lib/hecks/projections/deploy/preview/template.rb +3 -59
  454. data/lib/hecks/projections/deploy/preview.rb +2 -67
  455. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  456. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  457. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  458. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  459. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  460. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  461. data/lib/hecks/projections/deploy/shared.rb +48 -147
  462. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  463. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  464. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  465. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  466. data/lib/hecks/projections/diagrams.rb +22 -584
  467. data/lib/hecks/projections/glossary/html.rb +14 -130
  468. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  469. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  470. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  471. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  472. data/lib/hecks/projections/glossary.rb +30 -222
  473. data/lib/hecks/projections/ir.rb +3 -10
  474. data/lib/hecks/projections/model/deviations.rb +16 -43
  475. data/lib/hecks/projections/model.rb +12 -51
  476. data/lib/hecks/projections/oidc.rb +12 -56
  477. data/lib/hecks/projections/parser_table.rb +13 -32
  478. data/lib/hecks/projections/reference.rb +4 -18
  479. data/lib/hecks/projections/rust_vocabulary.rb +13 -251
  480. data/lib/hecks/projections/shape.rb +3 -29
  481. data/lib/hecks/projections/statements.rb +7 -86
  482. data/lib/hecks/projections/vocabulary.rb +6 -22
  483. data/lib/hecks/projections.rb +2 -16
  484. data/lib/hecks/projector/cli_projector.rb +154 -400
  485. data/lib/hecks/projector/docs_projector.rb +10 -201
  486. data/lib/hecks/projector/exporter.rb +42 -134
  487. data/lib/hecks/projector/ir_projector.rb +2 -9
  488. data/lib/hecks/projector/narrate_projector.rb +15 -189
  489. data/lib/hecks/projector/target.rb +13 -76
  490. data/lib/hecks/projector.rb +30 -121
  491. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  492. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  493. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  494. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  495. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  496. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  497. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  498. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  499. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  500. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  501. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  502. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  503. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  504. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  505. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  506. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  507. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  508. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  509. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  510. data/lib/hecks/quality_control/cli/child.rb +45 -0
  511. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  512. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  513. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  514. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  515. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  516. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  517. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  518. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  519. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  520. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  521. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  522. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  523. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  524. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  525. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  526. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  527. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  528. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  529. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  530. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  531. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  532. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  533. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  534. data/lib/hecks/query_ir.rb +346 -0
  535. data/lib/hecks/query_ir_mcp.rb +198 -0
  536. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  537. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  538. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  539. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  540. data/lib/hecks/query_specification/common/options.rb +2 -6
  541. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  542. data/lib/hecks/query_specification/field_path.rb +37 -77
  543. data/lib/hecks/query_specification/hop_path.rb +12 -76
  544. data/lib/hecks/release/gem_pin.rb +2 -26
  545. data/lib/hecks/release/runner/ci_publisher.rb +5 -8
  546. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  547. data/lib/hecks/release/runner/commands.rb +2 -4
  548. data/lib/hecks/release/runner/console.rb +6 -0
  549. data/lib/hecks/release/runner/gem_publisher.rb +15 -9
  550. data/lib/hecks/release/runner/npm_publisher.rb +9 -53
  551. data/lib/hecks/release/runner/preflight.rb +29 -20
  552. data/lib/hecks/release/runner/published.rb +8 -26
  553. data/lib/hecks/release/runner/tagger.rb +31 -23
  554. data/lib/hecks/release/runner.rb +60 -38
  555. data/lib/hecks/rendering.rb +4 -26
  556. data/lib/hecks/router/namespace_installer.rb +15 -39
  557. data/lib/hecks/router.rb +4 -8
  558. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  559. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  560. data/lib/hecks/runtime/boot_gates.rb +3 -18
  561. data/lib/hecks/runtime/caller.rb +19 -59
  562. data/lib/hecks/runtime/capability_graph.rb +5 -21
  563. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  564. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +27 -160
  565. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  566. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  567. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  568. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  569. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  570. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  571. data/lib/hecks/runtime/dependency_planning.rb +50 -104
  572. data/lib/hecks/runtime/dispatcher.rb +134 -243
  573. data/lib/hecks/runtime/entity_element.rb +40 -462
  574. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  575. data/lib/hecks/runtime/errors.rb +32 -115
  576. data/lib/hecks/runtime/event.rb +3 -25
  577. data/lib/hecks/runtime/identity.rb +18 -154
  578. data/lib/hecks/runtime/instance.rb +18 -150
  579. data/lib/hecks/runtime/interpreting.rb +14 -70
  580. data/lib/hecks/runtime/invocation.rb +28 -164
  581. data/lib/hecks/runtime/loader.rb +109 -194
  582. data/lib/hecks/runtime/outbox.rb +39 -262
  583. data/lib/hecks/runtime/policy_interpreter.rb +43 -202
  584. data/lib/hecks/runtime/port_operation_interpreter.rb +28 -97
  585. data/lib/hecks/runtime/query_interpreter.rb +122 -187
  586. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  587. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  588. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  589. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  590. data/lib/hecks/runtime/reference_hop.rb +8 -81
  591. data/lib/hecks/runtime/refusal_wording.rb +28 -87
  592. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  593. data/lib/hecks/runtime/registry/verification.rb +213 -227
  594. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  595. data/lib/hecks/runtime/registry.rb +156 -247
  596. data/lib/hecks/runtime/remote_dispatcher.rb +68 -154
  597. data/lib/hecks/runtime/routing.rb +4 -15
  598. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  599. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  600. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  601. data/lib/hecks/runtime/tenant_check.rb +6 -68
  602. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  603. data/lib/hecks/runtime/value/admission.rb +16 -67
  604. data/lib/hecks/runtime/value/coercion.rb +162 -760
  605. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  606. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  607. data/lib/hecks/runtime/value.rb +15 -84
  608. data/lib/hecks/runtime.rb +35 -69
  609. data/lib/hecks/rust_build/conformance.rb +158 -0
  610. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  611. data/lib/hecks/rust_build/coverage.rb +218 -0
  612. data/lib/hecks/rust_build/native_build.rb +124 -0
  613. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  614. data/lib/hecks/rust_build/project_rust.rb +332 -0
  615. data/lib/hecks/rust_build/wasm.rb +105 -0
  616. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  617. data/lib/hecks/rust_build.rb +144 -0
  618. data/lib/hecks/storehouse.rb +155 -582
  619. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +10 -34
  620. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  621. data/lib/hecks/three_zero/forms.yml +127 -0
  622. data/lib/hecks/three_zero.rb +12 -0
  623. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  624. data/lib/hecks/tools/comment_style.rb +1070 -0
  625. data/lib/hecks/tools/corpus_report.rb +87 -0
  626. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  627. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  628. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  629. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  630. data/lib/hecks/tools/era_reattest.rb +146 -0
  631. data/lib/hecks/tools/evolve_run.rb +189 -0
  632. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  633. data/lib/hecks/tools/heki_compaction.rb +100 -0
  634. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  635. data/lib/hecks/tools/journal_compaction.rb +89 -0
  636. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  637. data/lib/hecks/tools/query_ir_run.rb +81 -0
  638. data/lib/hecks/tools/regeneration_run.rb +166 -0
  639. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  640. data/lib/hecks/tools/sequence_script.rb +62 -0
  641. data/lib/hecks/tools/tail_merge.rb +117 -0
  642. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  643. data/lib/hecks/tools/translation_audit.rb +208 -0
  644. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  645. data/lib/hecks/tools.rb +86 -0
  646. data/lib/hecks/vendoring/git_environment.rb +2 -8
  647. data/lib/hecks/vendoring/git_source.rb +4 -10
  648. data/lib/hecks/vendoring.rb +7 -46
  649. data/lib/hecks/version.rb +3 -14
  650. data/lib/hecks.rb +52 -90
  651. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  652. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  653. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  654. data/qa/settings.yml +66 -0
  655. data/rust/.cargo/config.toml +6 -0
  656. data/rust/.gitignore +2 -0
  657. data/rust/Cargo.lock +54 -0
  658. data/rust/Cargo.toml +61 -0
  659. data/rust/build/.cargo/config.toml +3 -0
  660. data/rust/build/.gitignore +1 -0
  661. data/rust/build/Cargo.lock +7 -0
  662. data/rust/build/Cargo.toml +19 -0
  663. data/rust/build/src/build_artifact.rs +47 -0
  664. data/rust/build/src/cargo_sync.rs +248 -0
  665. data/rust/build/src/json.rs +421 -0
  666. data/rust/build/src/lineage_pass.rs +285 -0
  667. data/rust/build/src/main.rs +51 -0
  668. data/rust/build/src/optional_pass.rs +150 -0
  669. data/rust/build/src/pipeline.rs +264 -0
  670. data/rust/build/src/reserved_names.rs +76 -0
  671. data/rust/build/src/resolve.rs +124 -0
  672. data/rust/build/src/root.rs +25 -0
  673. data/rust/build/src/sidecars.rs +73 -0
  674. data/rust/build/src/subprocess.rs +51 -0
  675. data/rust/build/src/tmp.rs +31 -0
  676. data/rust/codegen/.cargo/config.toml +3 -0
  677. data/rust/codegen/.gitignore +1 -0
  678. data/rust/codegen/Cargo.lock +7 -0
  679. data/rust/codegen/Cargo.toml +20 -0
  680. data/rust/codegen/src/attr.rs +32 -0
  681. data/rust/codegen/src/bridging.rs +481 -0
  682. data/rust/codegen/src/commands.rs +1226 -0
  683. data/rust/codegen/src/constraints.rs +109 -0
  684. data/rust/codegen/src/dependency_planning.rs +209 -0
  685. data/rust/codegen/src/domain_generator.rs +1288 -0
  686. data/rust/codegen/src/exemplar.rs +230 -0
  687. data/rust/codegen/src/expr_emitter.rs +87 -0
  688. data/rust/codegen/src/fielded.rs +192 -0
  689. data/rust/codegen/src/hecks_naming.rs +33 -0
  690. data/rust/codegen/src/json.rs +335 -0
  691. data/rust/codegen/src/json_codec.rs +697 -0
  692. data/rust/codegen/src/literal.rs +207 -0
  693. data/rust/codegen/src/main.rs +290 -0
  694. data/rust/codegen/src/manifest.rs +227 -0
  695. data/rust/codegen/src/mutations.rs +784 -0
  696. data/rust/codegen/src/naming.rs +286 -0
  697. data/rust/codegen/src/ports.rs +208 -0
  698. data/rust/codegen/src/prelude.rs +116 -0
  699. data/rust/codegen/src/queries.rs +786 -0
  700. data/rust/codegen/src/reactions.rs +532 -0
  701. data/rust/codegen/src/read_models.rs +697 -0
  702. data/rust/codegen/src/reference_specs.rs +40 -0
  703. data/rust/codegen/src/registry.rs +966 -0
  704. data/rust/codegen/src/reserved_names.rs +76 -0
  705. data/rust/codegen/src/shared.rs +93 -0
  706. data/rust/codegen/src/skip_reason.rs +25 -0
  707. data/rust/codegen/src/types.rs +362 -0
  708. data/rust/host/.cargo/config.toml +3 -0
  709. data/rust/host/.gitignore +1 -0
  710. data/rust/host/Cargo.lock +4297 -0
  711. data/rust/host/Cargo.toml +61 -0
  712. data/rust/host/HECKS_RELEASE +1 -0
  713. data/rust/host/rds-ca-bundle.pem +2736 -0
  714. data/rust/host/src/api.rs +1596 -0
  715. data/rust/host/src/approval.rs +652 -0
  716. data/rust/host/src/auth.rs +1287 -0
  717. data/rust/host/src/bin/expr_harness.rs +46 -0
  718. data/rust/host/src/bin/lineage_harness.rs +119 -0
  719. data/rust/host/src/bin/mint_harness.rs +135 -0
  720. data/rust/host/src/checkout.rs +265 -0
  721. data/rust/host/src/dispatch.rs +1443 -0
  722. data/rust/host/src/expr_json.rs +1117 -0
  723. data/rust/host/src/field_hints.rs +52 -0
  724. data/rust/host/src/ir.rs +733 -0
  725. data/rust/host/src/journal.rs +874 -0
  726. data/rust/host/src/lambda_client.rs +378 -0
  727. data/rust/host/src/log.rs +147 -0
  728. data/rust/host/src/main.rs +474 -0
  729. data/rust/host/src/mint.rs +1865 -0
  730. data/rust/host/src/payments/keystore.rs +330 -0
  731. data/rust/host/src/payments/tests.rs +1400 -0
  732. data/rust/host/src/payments.rs +767 -0
  733. data/rust/host/src/presentation.rs +698 -0
  734. data/rust/host/src/presentation_write.rs +1431 -0
  735. data/rust/host/src/query_step.rs +149 -0
  736. data/rust/host/src/rate_limit.rs +904 -0
  737. data/rust/host/src/reference_transform.rs +241 -0
  738. data/rust/host/src/reference_validate.rs +338 -0
  739. data/rust/host/src/resend.rs +296 -0
  740. data/rust/host/src/secrets.rs +106 -0
  741. data/rust/host/src/server.rs +663 -0
  742. data/rust/host/src/storage_shape.rs +175 -0
  743. data/rust/host/src/test_pg.rs +40 -0
  744. data/rust/host/src/ui_schema.rs +1351 -0
  745. data/rust/host/src/wasm_runner.rs +191 -0
  746. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  747. data/rust/host/src/web/newsletter/tests.rs +104 -0
  748. data/rust/host/src/web/newsletter.rs +310 -0
  749. data/rust/host/src/web/newsletter_send.rs +357 -0
  750. data/rust/host/src/web/registrations.rs +1505 -0
  751. data/rust/host/src/web.rs +2937 -0
  752. data/rust/lsp/.gitignore +1 -0
  753. data/rust/lsp/Cargo.lock +7 -0
  754. data/rust/lsp/Cargo.toml +16 -0
  755. data/rust/lsp/README.md +133 -0
  756. data/rust/lsp/src/diagnostics.rs +165 -0
  757. data/rust/lsp/src/json.rs +385 -0
  758. data/rust/lsp/src/main.rs +399 -0
  759. data/rust/lsp/src/outline.rs +177 -0
  760. data/rust/lsp/src/rpc.rs +67 -0
  761. data/rust/parser/.cargo/config.toml +5 -0
  762. data/rust/parser/.gitignore +1 -0
  763. data/rust/parser/Cargo.lock +7 -0
  764. data/rust/parser/Cargo.toml +21 -0
  765. data/rust/parser/src/build/closed_sets.rs +27 -0
  766. data/rust/parser/src/build/identity.rs +276 -0
  767. data/rust/parser/src/build/mod.rs +12 -0
  768. data/rust/parser/src/build/naming.rs +141 -0
  769. data/rust/parser/src/build/pattern_subset.rs +161 -0
  770. data/rust/parser/src/build/query_derive.rs +72 -0
  771. data/rust/parser/src/build/query_inference.rs +269 -0
  772. data/rust/parser/src/build/query_options.rs +100 -0
  773. data/rust/parser/src/build/read_model.rs +94 -0
  774. data/rust/parser/src/build/references.rs +32 -0
  775. data/rust/parser/src/canonical.rs +114 -0
  776. data/rust/parser/src/diag.rs +55 -0
  777. data/rust/parser/src/emit.rs +953 -0
  778. data/rust/parser/src/expr/ast_json.rs +147 -0
  779. data/rust/parser/src/expr/evaluator.rs +81 -0
  780. data/rust/parser/src/expr/mod.rs +84 -0
  781. data/rust/parser/src/expr/resolver.rs +497 -0
  782. data/rust/parser/src/ir.rs +387 -0
  783. data/rust/parser/src/keywords.rs +585 -0
  784. data/rust/parser/src/lex.rs +662 -0
  785. data/rust/parser/src/main.rs +252 -0
  786. data/rust/parser/src/parse/aggregate.rs +612 -0
  787. data/rust/parser/src/parse/chapter.rs +517 -0
  788. data/rust/parser/src/parse/command.rs +637 -0
  789. data/rust/parser/src/parse/domain_port.rs +127 -0
  790. data/rust/parser/src/parse/entity.rs +527 -0
  791. data/rust/parser/src/parse/file.rs +67 -0
  792. data/rust/parser/src/parse/hecksagon.rs +231 -0
  793. data/rust/parser/src/parse/lifecycle.rs +162 -0
  794. data/rust/parser/src/parse/mod.rs +1262 -0
  795. data/rust/parser/src/parse/policy.rs +87 -0
  796. data/rust/parser/src/parse/process_manager.rs +305 -0
  797. data/rust/parser/src/parse/query.rs +100 -0
  798. data/rust/parser/src/parse/read_model.rs +214 -0
  799. data/rust/parser/src/parse/value_object.rs +250 -0
  800. data/rust/parser/src/ruby_value.rs +422 -0
  801. data/rust/project/bridging.rb +257 -0
  802. data/rust/project/commands.rb +877 -0
  803. data/rust/project/constraints.rb +86 -0
  804. data/rust/project/dependency_planning.rb +178 -0
  805. data/rust/project/domain_generator.rb +980 -0
  806. data/rust/project/exemplar.rb +148 -0
  807. data/rust/project/expr_emitter.rb +72 -0
  808. data/rust/project/fielded.rb +141 -0
  809. data/rust/project/json_codec.rb +641 -0
  810. data/rust/project/mutations.rb +721 -0
  811. data/rust/project/naming.rb +205 -0
  812. data/rust/project/ports.rb +65 -0
  813. data/rust/project/queries.rb +493 -0
  814. data/rust/project/reactions.rb +317 -0
  815. data/rust/project/read_models.rb +690 -0
  816. data/rust/project/reference_specs.rb +27 -0
  817. data/rust/project/registry.rb +396 -0
  818. data/rust/project/skip_reason.rb +29 -0
  819. data/rust/project/types.rb +225 -0
  820. data/rust/project/write_if_changed.rb +83 -0
  821. data/rust/project.rb +30 -0
  822. data/rust/project_rust_pipeline.rb +383 -0
  823. data/rust/src/exemplar/commands.rs +279 -0
  824. data/rust/src/exemplar/constraints.rs +18 -0
  825. data/rust/src/exemplar/fielded.rs +280 -0
  826. data/rust/src/exemplar/json.rs +277 -0
  827. data/rust/src/exemplar/mod.rs +17 -0
  828. data/rust/src/exemplar/mutations.rs +80 -0
  829. data/rust/src/exemplar/queries.rs +33 -0
  830. data/rust/src/exemplar/reactions.rs +88 -0
  831. data/rust/src/exemplar/read_models.rs +48 -0
  832. data/rust/src/exemplar/registry.rs +166 -0
  833. data/rust/src/exemplar/types.rs +44 -0
  834. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  835. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  836. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  837. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  838. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  839. data/rust/src/kernel/cli.rs +585 -0
  840. data/rust/src/kernel/dispatch.rs +1090 -0
  841. data/rust/src/kernel/expr.rs +458 -0
  842. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  843. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  844. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  845. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  846. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  847. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  848. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  849. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  850. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  851. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  852. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  853. data/rust/src/kernel/expression_operators/text.rs +79 -0
  854. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  855. data/rust/src/kernel/json.rs +779 -0
  856. data/rust/src/kernel/mod.rs +133 -0
  857. data/rust/src/kernel/named_query.rs +219 -0
  858. data/rust/src/kernel/orchestrate.rs +1810 -0
  859. data/rust/src/kernel/pattern.rs +307 -0
  860. data/rust/src/kernel/query_comparators.rs +206 -0
  861. data/rust/src/kernel/query_ordering.rs +103 -0
  862. data/rust/src/kernel/read_model.rs +914 -0
  863. data/rust/src/kernel/reference_lookup.rs +149 -0
  864. data/rust/src/kernel/repository.rs +468 -0
  865. data/rust/src/kernel/routing.rs +612 -0
  866. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  867. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  868. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  869. data/rust/src/kernel/vocab/mod.rs +20 -0
  870. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  871. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  872. data/rust/src/lib.rs +8 -0
  873. data/rust/src/main.rs +16 -0
  874. data/rust/web/.gitignore +1 -0
  875. data/rust/web/Cargo.lock +119 -0
  876. data/rust/web/Cargo.toml +18 -0
  877. data/rust/web/src/lib.rs +8 -0
  878. metadata +478 -12
  879. data/lib/hecks/facade/cli_door.rb +0 -178
  880. data/lib/hecks/facade/cli_runner.rb +0 -271
  881. data/lib/hecks/facade/handle.rb +0 -293
  882. data/lib/hecks/facade/json_door.rb +0 -247
  883. data/lib/hecks/facade/surface/aggregate_door.rb +0 -222
  884. data/lib/hecks/facade/surface/chapter.rb +0 -116
  885. data/lib/hecks/facade/surface.rb +0 -61
  886. data/lib/hecks/mcp_door_scope.rb +0 -176
@@ -0,0 +1,697 @@
1
+ //! Port of `rust/project/read_models.rb`, mirrored function for function —
2
+ //! read that file's own header for the full algorithm.
3
+
4
+ use crate::exemplar::Exemplar;
5
+ use crate::json::Json;
6
+ use crate::queries;
7
+ use crate::skip_reason::{reskip, skip, SkipReason};
8
+ use std::collections::HashMap;
9
+
10
+ const READ_MODEL_BARE_KEYS: &[&str] = &[
11
+ "name", "description", "reference_name", "reference_target", "query_name", "aggregate_heads", "wheres", "order_by", "offset", "limit", "freshness", "index_hints", "group_by", "null_semantics", "authorization", "count", "median_field",
12
+ "sum_field", "avg_field", "min_field", "max_field", "percentile_field", "percentile_at", "any_field", "all_field",
13
+ ];
14
+
15
+ // Every reduction beyond `count` (which needs no field) — mirrors
16
+ // Runtime::ReadModelInterpreter::REDUCTION_WORD/INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS
17
+ // (ADR 0078).
18
+ const AGGREGATION_FIELD_KEYS: &[&str] = &["median_field", "sum_field", "avg_field", "min_field", "max_field", "percentile_field", "any_field", "all_field"];
19
+ const INTEGER_ONLY_AGGREGATION_FIELDS: &[&str] = &["sum_field", "avg_field"];
20
+ const BOOLEAN_AGGREGATION_FIELDS: &[&str] = &["any_field", "all_field"];
21
+
22
+ pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
23
+ let keys: Vec<&str> = match read_model {
24
+ Json::Object(pairs) => pairs.iter().filter(|(_, v)| !matches!(v, Json::Null)).map(|(k, _)| k.as_str()).collect(),
25
+ _ => Vec::new(),
26
+ };
27
+ let extra: Vec<&str> = keys.into_iter().filter(|k| !READ_MODEL_BARE_KEYS.contains(k)).collect();
28
+ if !extra.is_empty() {
29
+ return Some(read_model_options_skip_reason(&extra));
30
+ }
31
+
32
+ if read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
33
+ return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names);
34
+ }
35
+
36
+ let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
37
+ let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
38
+ let root = heads.iter().find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
39
+ // Rootless (no `reference_to`) is generated — `read_models.rb`'s own
40
+ // comment on this check.
41
+ if root.is_none() && !reference_target.is_empty() {
42
+ return Some(skip(
43
+ "missing_root_head",
44
+ format!("declares reference_to {reference_target}, but includes no matching aggregate head — nothing for this generator's own root fetch to key off"),
45
+ ));
46
+ }
47
+
48
+ let root_name = root.and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
49
+ if !aggregates_by_name.contains_key(&root_name) && nested_entity_names(aggregates_by_name).contains(&root_name) {
50
+ return Some(entity_head_skip_reason(&root_name, "root", "fetch by id"));
51
+ }
52
+
53
+ for head in heads {
54
+ if let Some(reason) = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names) {
55
+ return Some(reason);
56
+ }
57
+ }
58
+
59
+ if let Some(reason) = read_model_options_content_skip_reason(read_model, aggregates_by_name) {
60
+ return Some(reason);
61
+ }
62
+
63
+ aggregation_skip_reason(read_model, aggregates_by_name)
64
+ }
65
+
66
+ fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
67
+ let mut sorted: Vec<&str> = extra.to_vec();
68
+ sorted.sort();
69
+ skip(sorted[0], format!(
70
+ "declares {} — out of scope for this generator: cursor/consistency/inspection are real capabilities Ports::Query::InMemory/Ports::Query::Ordering/TenantScope implement that this generator does not port (this file's own header has the full argument, the same boundary queries.rb already draws for a declared AGGREGATE query); freshness/use_index are never disqualifying on their own — neither is read by the in-memory interpreter path this kernel matches",
71
+ sorted.join(", ")
72
+ ))
73
+ }
74
+
75
+ /// Every many-side head this read model declares.
76
+ fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
77
+ read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]).iter().filter(|h| h.get("many").map(Json::as_bool).unwrap_or(false)).collect()
78
+ }
79
+
80
+ /// Whether one declared option (its own `:target`, or `None` when untargeted) applies to
81
+ /// `head`. `target` names the included aggregate by TYPE (ADR 0055), not by its `as:` alias;
82
+ /// `None` only ever means "the sole many-side head" — Ruby's own `seal_query_options` refuses
83
+ /// an untargeted option whenever a read model declares more than one, so an untargeted option
84
+ /// reaching codegen at all is proof there's exactly one to mean.
85
+ fn option_target_matches_head(target: Option<&str>, head: &Json, many_heads: &[&Json]) -> bool {
86
+ let head_as = head.get("as").map(Json::to_s).unwrap_or_default();
87
+ match target {
88
+ None => many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as,
89
+ Some(target) => target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
90
+ }
91
+ }
92
+
93
+ fn json_target(option: &Json) -> Option<String> {
94
+ option.get("target").map(Json::to_s)
95
+ }
96
+
97
+ /// Whether `head` has at least one declared where/order_by/offset/limit (or, for the sole
98
+ /// many-side head only, a declared `authorize ..., tenant:` — `on:` doesn't extend to
99
+ /// `tenant:`) targeting it.
100
+ fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[&Json]) -> bool {
101
+ let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]);
102
+ if wheres.iter().any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)) {
103
+ return true;
104
+ }
105
+ if let Some(order_by) = read_model.get("order_by") {
106
+ if option_target_matches_head(json_target(order_by).as_deref(), head, many_heads) {
107
+ return true;
108
+ }
109
+ }
110
+ if let Some(limit) = read_model.get("limit") {
111
+ if option_target_matches_head(json_target(limit).as_deref(), head, many_heads) {
112
+ return true;
113
+ }
114
+ }
115
+ if let Some(offset) = read_model.get("offset") {
116
+ if option_target_matches_head(json_target(offset).as_deref(), head, many_heads) {
117
+ return true;
118
+ }
119
+ }
120
+ if read_model.get("authorization").and_then(|a| a.get("tenant")).is_some() && many_heads.len() == 1 {
121
+ return many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default();
122
+ }
123
+ false
124
+ }
125
+
126
+ /// Every many-side head this read model declares options for, in `aggregate_heads`' own
127
+ /// declared order — one entry when there's a single many-side head (the pre-ADR-0055 shape),
128
+ /// more than one only when `on:` targets several.
129
+ fn read_model_filtered_heads<'a>(read_model: &Json, many_heads: &[&'a Json]) -> Vec<&'a Json> {
130
+ many_heads.iter().copied().filter(|head| read_model_option_targets_head(read_model, head, many_heads)).collect()
131
+ }
132
+
133
+ /// One targeted head's own where/order_by/offset/limit, pulled out of the read model's full
134
+ /// declared set. `authorization` carries `read_model`'s own declared authorization only for the
135
+ /// sole many-side head (`on:` doesn't extend to `tenant:`), `None` for every other targeted head.
136
+ struct HeadQuery<'a> {
137
+ wheres: Vec<&'a Json>,
138
+ order_by: Option<&'a Json>,
139
+ limit: Option<&'a Json>,
140
+ offset: Option<&'a Json>,
141
+ authorization: Option<&'a Json>,
142
+ }
143
+
144
+ fn read_model_head_query<'a>(read_model: &'a Json, head: &Json, many_heads: &[&Json]) -> HeadQuery<'a> {
145
+ let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)).collect();
146
+ let order_by = read_model.get("order_by").filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
147
+ let limit = read_model.get("limit").filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
148
+ let offset = read_model.get("offset").filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
149
+ let authorization = if many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default() {
150
+ read_model.get("authorization")
151
+ } else {
152
+ None
153
+ };
154
+ HeadQuery { wheres, order_by, limit, offset, authorization }
155
+ }
156
+
157
+ /// Checks every targeted head's own where/order_by/limit for generability against ITS
158
+ /// aggregate (not the read model's root); `None` means clean.
159
+ fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
160
+ let many_heads = read_model_many_heads(read_model);
161
+
162
+ for head in read_model_filtered_heads(read_model, &many_heads) {
163
+ let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
164
+ let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
165
+ return Some(entity_head_skip_reason(&aggregate_name, "filtered head", "filter, order or authorize"));
166
+ };
167
+ let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
168
+ let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
169
+ let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
170
+ let head_label = format!("head {aggregate_name} (as {as_name})");
171
+ let head_query = read_model_head_query(read_model, head, &many_heads);
172
+
173
+ // A hop through a reference is generated when `query_hop_plan` resolves
174
+ // it — `read_models.rb`'s own loop, check for check.
175
+ for where_clause in head_query.wheres.iter().copied() {
176
+ let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
177
+ let hop = queries::query_hop_plan(aggregate, &field, aggregates_by_name);
178
+ if hop.is_none() && field.contains('/') {
179
+ return Some(skip(
180
+ "reference_hop_where",
181
+ format!(
182
+ "{head_label}'s own where clause on {} hops through a reference this generator can't resolve yet (more than one hop, the head isn't a real reference attribute, or the target aggregate isn't declared in this domain) — not generated yet",
183
+ crate::naming::ruby_inspect_string(&field)
184
+ ),
185
+ ));
186
+ }
187
+
188
+ if let Some(plan) = hop {
189
+ let target_value_objects_by_name = queries::value_objects_of(plan.target);
190
+ if let Some(reason) = queries::query_where_skip_reason(&queries::with_field(where_clause, &plan.inner_field), plan.target, &target_value_objects_by_name) {
191
+ return Some(reskip(&reason, format!("{head_label}'s own hop through {} to {}'s own {reason}", plan.via_field, plan.target_aggregate)));
192
+ }
193
+ continue;
194
+ }
195
+
196
+ if let Some(reason) = queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name) {
197
+ return Some(reskip(&reason, format!("{head_label}'s own {reason}")));
198
+ }
199
+ }
200
+
201
+ if let Some(reason) = queries::declared_authorization_skip_reason(head_query.authorization, aggregate, &value_objects_by_name) {
202
+ return Some(reason);
203
+ }
204
+
205
+ if let Some(reason) = queries::declared_order_by_skip_reason(head_query.order_by, aggregate, &value_objects_by_name) {
206
+ return Some(reason);
207
+ }
208
+
209
+ if let Some(reason) = queries::declared_offset_skip_reason(head_query.offset) {
210
+ return Some(reason);
211
+ }
212
+
213
+ if let Some(reason) = queries::declared_limit_skip_reason(head_query.limit) {
214
+ return Some(reason);
215
+ }
216
+ }
217
+
218
+ None
219
+ }
220
+
221
+ // `seal_aggregation` (Ruby, build time) already guarantees exactly one
222
+ // many-side head and mutual exclusion with `group_by` — only the one declared
223
+ // reduction's own field needs checking here (ADR 0078).
224
+ fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
225
+ let field_key = AGGREGATION_FIELD_KEYS.iter().find(|key| read_model.get(**key).is_some())?;
226
+ let field = Json::to_s(read_model.get(*field_key).expect("checked by find above"));
227
+ let word = field_key.strip_suffix("_field").unwrap_or(field_key);
228
+
229
+ let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
230
+ let target = heads.iter().find(|h| h.get("many").map(Json::as_bool).unwrap_or(false))?;
231
+ let aggregate_name = target.get("aggregate").map(Json::to_s).unwrap_or_default();
232
+ let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
233
+ return Some(entity_head_skip_reason(&aggregate_name, &format!("{word} target"), "reduce"));
234
+ };
235
+ let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
236
+ let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
237
+
238
+ let kind = queries::query_field_kind(aggregate, &field, &value_objects_by_name);
239
+ if kind == queries::FieldKind::Unknown {
240
+ return Some(skip(*field_key, format!("{word} names {field:?}, but {aggregate_name} declares no such attribute — not generated yet")));
241
+ }
242
+
243
+ if BOOLEAN_AGGREGATION_FIELDS.contains(field_key) {
244
+ return if queries::query_field_boolean(aggregate, &field, &value_objects_by_name) {
245
+ None
246
+ } else {
247
+ Some(skip(*field_key, format!("{word} names {field:?} on {aggregate_name}, which is not boolean — {word} needs a true/false field — not generated yet")))
248
+ };
249
+ }
250
+
251
+ if kind != queries::FieldKind::Number {
252
+ return Some(skip(
253
+ *field_key,
254
+ format!("{word} names {field:?} on {aggregate_name}, which is not numeric — {word} needs a numeric field (a bare number, or a value object carrying one) — not generated yet"),
255
+ ));
256
+ }
257
+
258
+ if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key) {
259
+ if queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref() != Some("Integer") {
260
+ return Some(skip(
261
+ *field_key,
262
+ format!("{word} names {field:?} on {aggregate_name}, which is a Float field — {word} admits only an Integer field (summing/averaging Floats cannot be made to agree, byte for byte, between Ruby and Rust) — not generated yet"),
263
+ ));
264
+ }
265
+ }
266
+
267
+ None
268
+ }
269
+
270
+ // Mirrors `rust/project/read_models.rb`'s `group_by_skip_reason`: the one
271
+ // shape the corpus declares — a single rootless head, group_by alone.
272
+ fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
273
+ let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
274
+ if heads.len() != 1 {
275
+ return Some(skip("group_by", format!("declares group_by across {} aggregate heads — not generated yet (only a single, rootless head is)", heads.len())));
276
+ }
277
+ let reference_target = read_model.get("reference_target");
278
+ if reference_target.is_some() && !matches!(reference_target, Some(Json::Null)) {
279
+ return Some(skip("group_by", format!("declares group_by on a NON-rootless read model (reference_to {}) — not generated yet", reference_target.map(Json::to_s).unwrap_or_default())));
280
+ }
281
+ if read_model.get("count").is_some() || AGGREGATION_FIELD_KEYS.iter().any(|key| read_model.get(*key).is_some()) {
282
+ return Some(skip("group_by", "declares group_by alongside a reduction — not generated yet"));
283
+ }
284
+ let has_wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().any(|_| true);
285
+ if has_wheres || read_model.get("order_by").is_some() || read_model.get("limit").is_some() || read_model.get("offset").is_some() {
286
+ return Some(skip("group_by", "declares group_by alongside where/order_by/limit/offset — not generated yet"));
287
+ }
288
+ if read_model.get("authorization").is_some() {
289
+ return Some(skip("group_by", "declares group_by with an authorize policy — not generated yet"));
290
+ }
291
+
292
+ let head = &heads[0];
293
+ if let Some(reason) = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names) {
294
+ return Some(reason);
295
+ }
296
+
297
+ let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
298
+ let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
299
+ return Some(entity_head_skip_reason(&aggregate_name, "group_by head", "group"));
300
+ };
301
+ let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
302
+ let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
303
+ for row in read_model.get("group_by").map(Json::each).unwrap_or(&[]) {
304
+ let field = row.get("field").map(Json::to_s).unwrap_or_default();
305
+ let is_attr = attrs.iter().any(|a| crate::attr::name(a) == field);
306
+ let is_lifecycle = lifecycle_field.as_deref() == Some(field.as_str());
307
+ if !is_attr && !is_lifecycle {
308
+ return Some(skip("group_by", format!("group_by names {field:?}, but {aggregate_name} declares no such attribute — not generated yet")));
309
+ }
310
+ }
311
+ None
312
+ }
313
+
314
+ // Every entity name nested at any depth under a declared aggregate.
315
+ fn nested_entity_names(aggregates_by_name: &HashMap<String, &Json>) -> Vec<String> {
316
+ fn collect(owner: &Json, out: &mut Vec<String>) {
317
+ for entity in owner.get("entities").map(Json::each).unwrap_or(&[]) {
318
+ out.push(entity.get("name").map(Json::to_s).unwrap_or_default());
319
+ collect(entity, out);
320
+ }
321
+ }
322
+ let mut out = Vec::new();
323
+ for aggregate in aggregates_by_name.values() {
324
+ collect(aggregate, &mut out);
325
+ }
326
+ out
327
+ }
328
+
329
+ /// Port of `read_models.rb#entity_head_skip_reason`.
330
+ fn entity_head_skip_reason(aggregate_name: &str, role: &str, purpose: &str) -> SkipReason {
331
+ skip(
332
+ "include_entity_head",
333
+ format!("includes {aggregate_name}, a nested entity, as the {role} — an entity has no rows of its own (ReadModelInterpreter#records reads it as empty), so there is nothing to {purpose} — not generated yet"),
334
+ )
335
+ }
336
+
337
+ fn read_model_head_skip_reason(head: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
338
+ let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
339
+ if !aggregates_by_name.contains_key(&aggregate_name) {
340
+ if nested_entity_names(aggregates_by_name).contains(&aggregate_name) {
341
+ return None;
342
+ }
343
+ return Some(skip("include_undeclared_aggregate", format!("includes {aggregate_name}, which this domain never declares")));
344
+ }
345
+ if unsupported_names.contains(&aggregate_name) {
346
+ return Some(skip(
347
+ "include_unsupported_aggregate",
348
+ format!("includes {aggregate_name}, which this generator couldn't itself generate (unsupported attribute type — see this domain's own aggregate-level manifest entry)"),
349
+ ));
350
+ }
351
+ None
352
+ }
353
+
354
+ pub struct ReferenceField {
355
+ pub target: String,
356
+ pub field: String,
357
+ }
358
+
359
+ fn read_model_reference_fields(head_aggregate: &Json) -> Vec<ReferenceField> {
360
+ let attrs = head_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
361
+ attrs
362
+ .iter()
363
+ .filter_map(|attr| {
364
+ let target = crate::naming::reference_target(crate::attr::type_name(attr))?;
365
+ Some(ReferenceField { target: target.to_string(), field: crate::attr::name(attr).to_string() })
366
+ })
367
+ .collect()
368
+ }
369
+
370
+ fn emit_reference_field(domain_name: &str, rf: &ReferenceField) -> String {
371
+ let qualified_target = format!("{domain_name}::{}", rf.target);
372
+ format!("crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}", crate::naming::ruby_inspect_string(&qualified_target), crate::naming::ruby_inspect_string(&rf.field))
373
+ }
374
+
375
+ fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregates_by_name: &HashMap<String, &Json>) -> String {
376
+ let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
377
+ let reference_fields: Vec<ReferenceField> = if is_root { Vec::new() } else { aggregates_by_name.get(&aggregate_name).map(|a| read_model_reference_fields(a)).unwrap_or_default() };
378
+ let reference_fields_expr = reference_fields.iter().map(|rf| emit_reference_field(domain_name, rf)).collect::<Vec<_>>().join(", ");
379
+ let qualified_aggregate = format!("{domain_name}::{aggregate_name}");
380
+ let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
381
+ let many = head.get("many").map(Json::as_bool).unwrap_or(false);
382
+
383
+ format!(
384
+ "crate::kernel::read_model::ReadModelHead {{ aggregate: {}, as_name: {}, many: {many}, is_root: {is_root}, reference_fields: &[{reference_fields_expr}] }}",
385
+ crate::naming::ruby_inspect_string(&qualified_aggregate),
386
+ crate::naming::ruby_inspect_string(&as_name)
387
+ )
388
+ }
389
+
390
+ fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
391
+ let descending = order_by.get("direction").map(Json::to_s).unwrap_or_default() == "desc";
392
+ format!(
393
+ "crate::kernel::read_model::ReadModelOrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
394
+ crate::naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
395
+ queries::null_semantics_variant(null_semantics)
396
+ )
397
+ }
398
+
399
+ fn emit_read_model_limit(limit: &Json) -> String {
400
+ let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
401
+ if let Some(arg) = raw.strip_prefix(':') {
402
+ return format!("crate::kernel::read_model::ReadModelLimit::Arg({})", crate::naming::ruby_inspect_string(arg));
403
+ }
404
+ format!("crate::kernel::read_model::ReadModelLimit::Literal({})", ruby_to_i(&raw))
405
+ }
406
+
407
+ // `ReadModelOffset` is a type alias for `ReadModelLimit`'s own type, so this
408
+ // reuses `emit_read_model_limit`'s computation and swaps the type name.
409
+ fn emit_read_model_offset(offset: &Json) -> String {
410
+ emit_read_model_limit(offset).replace("read_model::ReadModelLimit::", "read_model::ReadModelOffset::")
411
+ }
412
+
413
+ fn ruby_to_i(s: &str) -> i64 {
414
+ let trimmed = s.trim_start();
415
+ let bytes = trimmed.as_bytes();
416
+ let mut end = 0;
417
+ if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
418
+ end += 1;
419
+ }
420
+ let digits_start = end;
421
+ while end < bytes.len() && bytes[end].is_ascii_digit() {
422
+ end += 1;
423
+ }
424
+ if end == digits_start {
425
+ return 0;
426
+ }
427
+ trimmed[..end].parse().unwrap_or(0)
428
+ }
429
+
430
+ /// One targeted many-side head's own where/order_by/offset/limit — the per-head analogue of
431
+ /// `ReadModelDef` itself. At most one when there's a single many-side head (the pre-ADR-0055
432
+ /// shape); more than one only when `on:` targets several.
433
+ pub struct FilteredHeadDef {
434
+ pub as_name: String,
435
+ pub conditions: Vec<queries::Condition>,
436
+ pub reference_hop_conditions: Vec<queries::HopCondition>,
437
+ pub order_by: Option<String>,
438
+ pub offset: Option<String>,
439
+ pub limit: Option<String>,
440
+ }
441
+
442
+ pub struct ReadModelDef {
443
+ pub verb: String,
444
+ pub reference_name: Option<String>,
445
+ pub heads: Vec<String>,
446
+ pub filtered_heads: Vec<FilteredHeadDef>,
447
+ pub authorization: Option<String>,
448
+ pub group_by_fn: Option<String>,
449
+ pub group_by_fn_body: Option<String>,
450
+ pub count: bool,
451
+ pub median_field: Option<String>,
452
+ pub sum_field: Option<String>,
453
+ pub avg_field: Option<String>,
454
+ pub min_field: Option<String>,
455
+ pub max_field: Option<String>,
456
+ pub percentile_field: Option<String>,
457
+ pub percentile_at: Option<String>,
458
+ pub any_field: Option<String>,
459
+ pub all_field: Option<String>,
460
+ }
461
+
462
+ /// `local_wheres, hop_wheres` compiled for one targeted head, mirroring
463
+ /// `read_models.rb#filtered_head_def`.
464
+ fn filtered_head_def(domain_name: &str, read_model: &Json, head: &Json, many_heads: &[&Json], aggregates_by_name: &HashMap<String, &Json>) -> FilteredHeadDef {
465
+ let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
466
+ let aggregate = aggregates_by_name[&aggregate_name];
467
+ let head_query = read_model_head_query(read_model, head, many_heads);
468
+ let (local_wheres, hop_wheres): (Vec<&Json>, Vec<&Json>) =
469
+ head_query.wheres.iter().copied().partition(|w| queries::query_hop_plan(aggregate, &w.get("field").map(Json::to_s).unwrap_or_default(), aggregates_by_name).is_none());
470
+ let synthetic = with_wheres_and_authorization(&local_wheres, head_query.authorization);
471
+ let conditions = if head_query.authorization.is_some() { queries::query_conditions_with_authorization(&synthetic) } else { queries::query_conditions(&synthetic) };
472
+
473
+ FilteredHeadDef {
474
+ as_name: head.get("as").map(Json::to_s).unwrap_or_default(),
475
+ conditions,
476
+ reference_hop_conditions: queries::read_model_hop_conditions(domain_name, &hop_wheres, aggregate, aggregates_by_name),
477
+ order_by: head_query.order_by.map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
478
+ offset: head_query.offset.map(emit_read_model_offset),
479
+ limit: head_query.limit.map(emit_read_model_limit),
480
+ }
481
+ }
482
+
483
+ pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> ReadModelDef {
484
+ let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
485
+ let heads_json = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
486
+ let heads: Vec<String> = heads_json
487
+ .iter()
488
+ .map(|head| {
489
+ let is_root = head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
490
+ emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
491
+ })
492
+ .collect();
493
+
494
+ let many_heads = read_model_many_heads(read_model);
495
+ let filtered_heads: Vec<FilteredHeadDef> =
496
+ read_model_filtered_heads(read_model, &many_heads).into_iter().map(|head| filtered_head_def(domain_name, read_model, head, &many_heads, aggregates_by_name)).collect();
497
+ let sole_head_as: bool = many_heads.len() == 1;
498
+
499
+ let read_model_name = read_model.get("name").map(Json::to_s).unwrap_or_default();
500
+ let group_by_fields: Vec<String> = read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().map(|row| row.get("field").map(Json::to_s).unwrap_or_default()).collect();
501
+ let (group_by_fn, group_by_fn_body) = if !group_by_fields.is_empty() {
502
+ let fn_name = format!("group_by_{}", read_model_name.to_lowercase());
503
+ let aggregate_name = heads_json.first().and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
504
+ let aggregate = aggregates_by_name[&aggregate_name];
505
+ (Some(fn_name.clone()), Some(emit_group_by_transform(&fn_name, &read_model_name, aggregate, &group_by_fields)))
506
+ } else {
507
+ (None, None)
508
+ };
509
+
510
+ ReadModelDef {
511
+ verb: format!("{domain_name}.{read_model_name}"),
512
+ reference_name: read_model.get("reference_name").map(Json::to_s),
513
+ heads,
514
+ filtered_heads,
515
+ authorization: if sole_head_as { queries::emit_query_authorization(&read_model_name, read_model.get("authorization")) } else { None },
516
+ group_by_fn,
517
+ group_by_fn_body,
518
+ count: read_model.get("count").is_some(),
519
+ median_field: read_model.get("median_field").map(Json::to_s),
520
+ sum_field: read_model.get("sum_field").map(Json::to_s),
521
+ avg_field: read_model.get("avg_field").map(Json::to_s),
522
+ min_field: read_model.get("min_field").map(Json::to_s),
523
+ max_field: read_model.get("max_field").map(Json::to_s),
524
+ percentile_field: read_model.get("percentile_field").map(Json::to_s),
525
+ percentile_at: if read_model.get("percentile_field").is_some() { read_model.get("percentile_at").map(Json::to_s) } else { None },
526
+ any_field: read_model.get("any_field").map(Json::to_s),
527
+ all_field: read_model.get("all_field").map(Json::to_s),
528
+ }
529
+ }
530
+
531
+ /// A minimal `{wheres:, authorization:}` object — everything `query_conditions`/
532
+ /// `query_conditions_with_authorization` actually read — for one targeted head's own local
533
+ /// (non-hop) where clauses.
534
+ fn with_wheres_and_authorization(wheres: &[&Json], authorization: Option<&Json>) -> Json {
535
+ let wheres_json = Json::Array(wheres.iter().map(|w| (*w).clone()).collect());
536
+ let mut pairs = vec![("wheres".to_string(), wheres_json)];
537
+ if let Some(auth) = authorization {
538
+ pairs.push(("authorization".to_string(), auth.clone()));
539
+ }
540
+ Json::Object(pairs)
541
+ }
542
+
543
+ pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
544
+ let heads = rmd.heads.iter().map(|h| format!(" {h},")).collect::<Vec<_>>().join("\n");
545
+ let filtered_heads = rmd.filtered_heads.iter().map(|fh| format!(" {}", emit_filtered_head(fh))).collect::<Vec<_>>().join("\n");
546
+ let authorization = match &rmd.authorization {
547
+ Some(a) => format!("Some({a})"),
548
+ None => "None".to_string(),
549
+ };
550
+ let reference_name = match &rmd.reference_name {
551
+ Some(r) => format!("Some({})", crate::naming::ruby_inspect_string(r)),
552
+ None => "None".to_string(),
553
+ };
554
+ let group_by = match &rmd.group_by_fn {
555
+ Some(f) => format!("Some({f})"),
556
+ None => "None".to_string(),
557
+ };
558
+ let count = if rmd.count { "true" } else { "false" };
559
+ let field_some = |f: &Option<String>| match f {
560
+ Some(v) => format!("Some({})", crate::naming::ruby_inspect_string(v)),
561
+ None => "None".to_string(),
562
+ };
563
+ let median_field = field_some(&rmd.median_field);
564
+ let sum_field = field_some(&rmd.sum_field);
565
+ let avg_field = field_some(&rmd.avg_field);
566
+ let min_field = field_some(&rmd.min_field);
567
+ let max_field = field_some(&rmd.max_field);
568
+ let percentile_field = field_some(&rmd.percentile_field);
569
+ let percentile_at = match &rmd.percentile_at {
570
+ Some(at) => format!("Some({at}_f64)"),
571
+ None => "None".to_string(),
572
+ };
573
+ let any_field = field_some(&rmd.any_field);
574
+ let all_field = field_some(&rmd.all_field);
575
+
576
+ format!(
577
+ "crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n count: {count},\n median_field: {median_field},\n sum_field: {sum_field},\n avg_field: {avg_field},\n min_field: {min_field},\n max_field: {max_field},\n percentile_field: {percentile_field},\n percentile_at: {percentile_at},\n any_field: {any_field},\n all_field: {all_field},\n}},",
578
+ crate::naming::ruby_inspect_string(&rmd.verb)
579
+ )
580
+ }
581
+
582
+ /// One `FilteredHead` entry, always emitted as a single line — matching this generator's own
583
+ /// established idiom for every OTHER leaf struct (`ReadModelHead`, `ReferenceField`,
584
+ /// `ReferenceHopCondition`, `QueryCondition` are all single-line too; only the top-level
585
+ /// `ReadModelDef`/`QueryDef` get pretty multi-line formatting with their arrays broken apart).
586
+ fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
587
+ let conditions = fh.conditions.iter().map(queries::emit_query_condition).collect::<Vec<_>>().join(" ");
588
+ let reference_hop_conditions = fh.reference_hop_conditions.iter().map(queries::emit_reference_hop_condition).collect::<Vec<_>>().join(" ");
589
+ let order_by = match &fh.order_by {
590
+ Some(o) => format!("Some({o})"),
591
+ None => "None".to_string(),
592
+ };
593
+ let offset = match &fh.offset {
594
+ Some(o) => format!("Some({o})"),
595
+ None => "None".to_string(),
596
+ };
597
+ let limit = match &fh.limit {
598
+ Some(l) => format!("Some({l})"),
599
+ None => "None".to_string(),
600
+ };
601
+
602
+ format!(
603
+ "crate::kernel::read_model::FilteredHead {{ as_name: {}, conditions: &[{conditions}], reference_hop_conditions: &[{reference_hop_conditions}], order_by: {order_by}, offset: {offset}, limit: {limit} }},",
604
+ crate::naming::ruby_inspect_string(&fh.as_name)
605
+ )
606
+ }
607
+
608
+ // Keeps this aggregate's declared attributes plus any `projects` field, id
609
+ // and lifecycle field, excluding other capabilities' synthetic fields (like
610
+ // `corrects`'s `emitted_*` flags), and unwraps value objects recursively.
611
+ fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
612
+ let value_objects: Vec<&Json> = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]).iter().collect();
613
+ let value_objects_by_name: HashMap<String, &Json> = value_objects.iter().map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo)).collect();
614
+ let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
615
+ let mut fields: Vec<Json> = aggregate.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
616
+ fields.extend(crate::types::projected_field_pseudo_attributes(aggregate));
617
+
618
+ let mut kept_keys: Vec<String> = fields.iter().map(|a| crate::attr::name(a).to_string()).collect();
619
+ kept_keys.push("id".to_string());
620
+ if let Some(lf) = &lifecycle_field {
621
+ kept_keys.push(lf.clone());
622
+ }
623
+ let keep_cond = kept_keys.iter().map(|k| format!("k == {k:?}")).collect::<Vec<_>>().join(" || ");
624
+
625
+ let arms: Vec<String> = fields
626
+ .iter()
627
+ .map(|a| {
628
+ let unwrapped = unwrap_json_expr("v", crate::attr::type_name(a), crate::attr::list(a), aggregate, &value_objects_by_name);
629
+ format!("{:?} => {unwrapped},", crate::attr::name(a))
630
+ })
631
+ .collect();
632
+ let arms = arms.join("\n ");
633
+
634
+ let fields_literal = format!("&[{}]", group_by_fields.iter().map(|f| format!("{f:?}")).collect::<Vec<_>>().join(", "));
635
+ let leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields);
636
+
637
+ format!(
638
+ "pub fn {fn_name}(rows: Vec<(String, crate::kernel::Json)>) -> Result<crate::kernel::Json, crate::kernel::Refusal> {{\n let unwrapped: Vec<crate::kernel::Json> = rows\n .into_iter()\n .map(|(id, record)| {{\n let wrapped = crate::kernel::repository::row_json(id, record);\n match wrapped {{\n crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(\n fields\n .into_iter()\n .filter(|(k, _)| {keep_cond})\n .map(|(k, v)| {{\n let new_v = match k.as_str() {{\n {arms}\n _ => v,\n }};\n (k, new_v)\n }})\n .collect(),\n ),\n other => other,\n }}\n }})\n .collect();\n crate::kernel::read_model::nest(unwrapped, {fields_literal}, {leaf_check})\n}}"
639
+ )
640
+ }
641
+
642
+ // A key path naming every identity head cannot collide, so its leaves go
643
+ // unchecked; any other refuses a second row (ADR 0061).
644
+ fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
645
+ let mut identity: Vec<String> = Vec::new();
646
+ for path in aggregate.get("identified_by").map(Json::each).unwrap_or(&[]) {
647
+ let head = path.to_s().split('.').next().unwrap_or_default().to_string();
648
+ if !identity.contains(&head) {
649
+ identity.push(head);
650
+ }
651
+ }
652
+ if !identity.is_empty() && identity.iter().all(|head| group_by_fields.contains(head)) {
653
+ return "crate::kernel::read_model::LeafCheck::IdentityCovered".to_string();
654
+ }
655
+ format!("crate::kernel::read_model::LeafCheck::RefuseCollision({})", crate::naming::ruby_inspect_string(read_model_name))
656
+ }
657
+
658
+ // Port of `Value.materialize_unwrapped` (Ruby).
659
+ fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
660
+ if list {
661
+ let inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name);
662
+ return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
663
+ }
664
+
665
+ let vo = value_objects_by_name.get(type_name).copied();
666
+ let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
667
+ let fields_meta: Option<&[Json]> = if let Some(vo) = vo {
668
+ vo.get("attributes").map(Json::each)
669
+ } else {
670
+ entity.and_then(|e| e.get("attributes").map(Json::each))
671
+ };
672
+ let Some(fields_meta) = fields_meta else { return expr.to_string() };
673
+
674
+ if vo.is_some() && fields_meta.len() == 1 {
675
+ let field = &fields_meta[0];
676
+ let unwrapped = unwrap_json_expr("field_value", crate::attr::type_name(field), crate::attr::list(field), aggregate, value_objects_by_name);
677
+ let field_name = crate::attr::name(field);
678
+ return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map(|(_, field_value)| {unwrapped}).unwrap_or(crate::kernel::Json::Null), other => other }}");
679
+ }
680
+
681
+ let inner_arms: Vec<String> = fields_meta
682
+ .iter()
683
+ .map(|f| {
684
+ let unwrapped = unwrap_json_expr("v", crate::attr::type_name(f), crate::attr::list(f), aggregate, value_objects_by_name);
685
+ format!("{:?} => {unwrapped},", crate::attr::name(f))
686
+ })
687
+ .collect();
688
+ let inner_arms = inner_arms.join(" ");
689
+ format!("match {expr} {{ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| {{ let new_v = match k.as_str() {{ {inner_arms} _ => v }}; (k, new_v) }}).collect()), other => other }}")
690
+ }
691
+
692
+ const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n crate::kernel::read_model::FilteredHead { as_name: \"tmpl_as_name\", conditions: &[crate::kernel::QueryCondition { field: \"tmpl_field\", comparator: crate::kernel::query_comparators::QueryComparator::Eq, value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], reference_hop_conditions: &[crate::kernel::read_model::ReferenceHopCondition { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\", through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }], inner_field: \"tmpl_inner_field\", inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq, inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], order_by: Some(crate::kernel::read_model::ReadModelOrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }), offset: Some(crate::kernel::read_model::ReadModelOffset::Literal(1)), limit: Some(crate::kernel::read_model::ReadModelLimit::Literal(5)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n count: false,\n median_field: None,\n sum_field: None,\n avg_field: None,\n min_field: None,\n max_field: None,\n percentile_field: None,\n percentile_at: None,\n any_field: None,\n all_field: None,\n},";
693
+
694
+ pub fn emit_read_model_table(exemplar: &Exemplar, read_model_defs: &[ReadModelDef]) -> String {
695
+ let rows: Vec<String> = read_model_defs.iter().map(emit_read_model_def).collect();
696
+ exemplar.render("read_model_table", &[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))])
697
+ }