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,1262 @@
1
+ //! Word, body, and argument gates, plus the recursive body walker.
2
+ //! Per-construct modules build IR; a word they do not build fails with `not_yet_implemented`.
3
+
4
+ pub mod aggregate;
5
+ pub mod chapter;
6
+ pub mod command;
7
+ pub mod domain_port;
8
+ pub mod entity;
9
+ pub mod file;
10
+ pub mod hecksagon;
11
+ pub mod lifecycle;
12
+ pub mod policy;
13
+ pub mod process_manager;
14
+ pub mod query;
15
+ pub mod read_model;
16
+ pub mod value_object;
17
+
18
+ use crate::diag::{Diagnostic, ParseResult};
19
+ use crate::ir;
20
+ use crate::keywords::{self, ArgumentRow, KeywordRow};
21
+ use crate::lex::{self, Call, LineShape, Opener, SourceLine};
22
+ use crate::ruby_value;
23
+
24
+ /// Live `KeywordRow`s for `(word, context)`; zero rows is an error listing the legal words.
25
+ pub fn word_gate<'a>(
26
+ file: &str,
27
+ word: &str,
28
+ context: &'static str,
29
+ line: usize,
30
+ ) -> ParseResult<Vec<&'a KeywordRow>> {
31
+ // A renamed word matches under either spelling (`sets` / `then_set`); `was` is never a row.
32
+ let candidates: Vec<&KeywordRow> = keywords::KEYWORDS
33
+ .iter()
34
+ .filter(|k| {
35
+ k.live()
36
+ && k.context == context
37
+ && (k.word == word || (!k.was.is_empty() && k.was == word))
38
+ })
39
+ .collect();
40
+
41
+ if candidates.is_empty() {
42
+ let mut legal: Vec<String> = keywords::KEYWORDS
43
+ .iter()
44
+ .filter(|k| k.live() && k.context == context)
45
+ .map(|k| k.word.to_string())
46
+ .collect();
47
+ legal.sort();
48
+ legal.dedup();
49
+ return Err(Diagnostic::new(
50
+ file,
51
+ line,
52
+ format!("'{word}' is not a word {context} admits"),
53
+ )
54
+ .with_expected(legal));
55
+ }
56
+
57
+ Ok(candidates)
58
+ }
59
+
60
+ /// Picks the word-gated row whose declared `body` fits the opener the lexer found.
61
+ ///
62
+ /// `do ... end` and `{ ... }` are interchangeable for `source`/`keywords`/`rows` bodies, so the
63
+ /// delimiter never decides the row. No `(word, context)` declares both a `source` row and a
64
+ /// `keywords`/`rows` row; if one ever does, this widening becomes the tie-break.
65
+ pub fn body_gate<'a>(
66
+ file: &str,
67
+ candidates: &[&'a KeywordRow],
68
+ opener: &Opener,
69
+ line: usize,
70
+ word: &str,
71
+ ) -> ParseResult<&'a KeywordRow> {
72
+ let compatible: fn(&str) -> bool = match opener {
73
+ Opener::None => |body| body == "none",
74
+ Opener::DoBlock { .. } => |body| body == "keywords" || body == "rows" || body == "source",
75
+ Opener::BraceBlock { .. } => {
76
+ |body| body == "keywords" || body == "rows" || body == "source"
77
+ }
78
+ };
79
+
80
+ if let Some(row) = candidates.iter().find(|row| compatible(row.body)) {
81
+ return Ok(row);
82
+ }
83
+
84
+ let legal: Vec<String> = candidates.iter().map(|row| row.body.to_string()).collect();
85
+ let found = match opener {
86
+ Opener::None => "no body",
87
+ Opener::DoBlock { .. } => "a `do ... end` block",
88
+ Opener::BraceBlock { .. } => "a `{ ... }` block",
89
+ };
90
+ Err(
91
+ Diagnostic::new(file, line, format!("'{word}' was written with {found}"))
92
+ .with_expected(legal),
93
+ )
94
+ }
95
+
96
+ /// Checks that `syntax.bluebook`'s `resolves_via` for `(word, context)` matches the caller's use.
97
+ /// Mirrors Ruby's `RuleReference#verify_resolves_via!`; a mismatch means the two have drifted.
98
+ pub fn verify_resolves_via(
99
+ file: &str,
100
+ line: usize,
101
+ word: &str,
102
+ context: &'static str,
103
+ expected: &str,
104
+ ) -> ParseResult<()> {
105
+ let actual = keywords::KEYWORDS
106
+ .iter()
107
+ .find(|k| k.word == word && k.context == context)
108
+ .map_or("", |k| k.resolves_via);
109
+
110
+ if actual == expected {
111
+ return Ok(());
112
+ }
113
+
114
+ Err(Diagnostic::new(
115
+ file,
116
+ line,
117
+ format!(
118
+ "internal: syntax.bluebook says {word}/{context} resolves via '{actual}', but \
119
+ {word}'s own Rust parser is about to use '{expected}' — the grammar table and \
120
+ the implementation have drifted"
121
+ ),
122
+ ))
123
+ }
124
+
125
+ pub(crate) fn kind_matches(declared: &str, actual: &str) -> bool {
126
+ if declared == actual {
127
+ return true;
128
+ }
129
+ // `literal` is the widest kind: a Hash or Array survives its type like a number or symbol does
130
+ // (`sets :toppings, append: { ... }`). Only a `constant` type name is excluded.
131
+ declared == "literal" && actual != "constant"
132
+ }
133
+
134
+ /// A `word(...)` call whose word is a live `Type`-context word (`list_of`, `one_of`).
135
+ /// It fills a `constant` slot although it does not start with an uppercase letter.
136
+ pub(crate) fn type_context_call_word(token: &str) -> Option<&str> {
137
+ let open = token.find('(')?;
138
+ if !token.ends_with(')') {
139
+ return None;
140
+ }
141
+ let word = &token[..open];
142
+ if word.is_empty() || !word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
143
+ return None;
144
+ }
145
+ if keywords::KEYWORDS
146
+ .iter()
147
+ .any(|k| k.live() && k.context == "Type" && k.word == word)
148
+ {
149
+ Some(word)
150
+ } else {
151
+ None
152
+ }
153
+ }
154
+
155
+ pub(crate) fn classify_lexical_kind(token: &str) -> &'static str {
156
+ let t = token.trim();
157
+ if t.starts_with(':') && t.len() > 1 {
158
+ return "symbol";
159
+ }
160
+ if t.len() >= 2 && t.starts_with('"') && t.ends_with('"') {
161
+ return "text";
162
+ }
163
+ if t == "true" || t == "false" {
164
+ return "flag";
165
+ }
166
+ if t == "nil" {
167
+ return "literal";
168
+ }
169
+ if is_number_token(t) {
170
+ return "number";
171
+ }
172
+ if t.starts_with('[') && t.ends_with(']') {
173
+ return "list";
174
+ }
175
+ if t.starts_with('{') && t.ends_with('}') {
176
+ return "pairs";
177
+ }
178
+ if t.chars()
179
+ .next()
180
+ .map(|c| c.is_ascii_uppercase())
181
+ .unwrap_or(false)
182
+ {
183
+ return "constant";
184
+ }
185
+ if type_context_call_word(t).is_some() {
186
+ return "constant";
187
+ }
188
+ // Fallback for an unquoted bare word with no sharper classification.
189
+ "text"
190
+ }
191
+
192
+ fn is_number_token(t: &str) -> bool {
193
+ let body = t.strip_prefix('-').unwrap_or(t);
194
+ if body.is_empty() {
195
+ return false;
196
+ }
197
+ let mut seen_dot = false;
198
+ for ch in body.chars() {
199
+ if ch == '.' && !seen_dot {
200
+ seen_dot = true;
201
+ continue;
202
+ }
203
+ if !ch.is_ascii_digit() {
204
+ return false;
205
+ }
206
+ }
207
+ true
208
+ }
209
+
210
+ /// Splits a named `identifier: value` segment; a leading colon (`:eq`) or `::` is not named.
211
+ pub(crate) fn as_named(segment: &str) -> Option<(&str, &str)> {
212
+ let bytes = segment.as_bytes();
213
+ if bytes.is_empty() || !(bytes[0].is_ascii_alphabetic() || bytes[0] == b'_') {
214
+ return None;
215
+ }
216
+ let mut i = 1;
217
+ while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
218
+ i += 1;
219
+ }
220
+ if i < bytes.len() && bytes[i] == b':' && bytes.get(i + 1) != Some(&b':') {
221
+ let name = &segment[..i];
222
+ let value = segment[i + 1..].trim_start();
223
+ return Some((name, value));
224
+ }
225
+ None
226
+ }
227
+
228
+ #[derive(Debug, Clone, Default)]
229
+ pub struct ArgumentGateResult {
230
+ pub positional: Vec<(usize, String)>,
231
+ pub named: Vec<(String, String)>,
232
+ }
233
+
234
+ /// Checks positional/named count and lexical kind against the `ArgumentRow`s of `(word, context)`.
235
+ /// Rows join on `(word, context)` alone, not on the chosen body row (`identified_by` shares them).
236
+ pub fn argument_gate(
237
+ file: &str,
238
+ word: &str,
239
+ context: &'static str,
240
+ args_text: &str,
241
+ line: usize,
242
+ ) -> ParseResult<ArgumentGateResult> {
243
+ let rows: Vec<&ArgumentRow> = keywords::ARGUMENTS
244
+ .iter()
245
+ .filter(|r| r.live() && r.keyword == word && r.context == context)
246
+ .collect();
247
+
248
+ let segments = if args_text.trim().is_empty() {
249
+ Vec::new()
250
+ } else {
251
+ ruby_value::split_items(args_text)
252
+ };
253
+
254
+ if rows.is_empty() {
255
+ if segments.is_empty() {
256
+ return Ok(ArgumentGateResult::default());
257
+ }
258
+ return Err(Diagnostic::new(
259
+ file,
260
+ line,
261
+ format!("'{word}' takes no arguments, but got '{args_text}'"),
262
+ ));
263
+ }
264
+
265
+ // A positional `pairs` row covers two shapes, split by `pairs_shape`: "fields" is one
266
+ // hash-rocket pair (`transition "Purchase" => "sold"`); "verbatim"/"elements" is many
267
+ // `key: value` segments merged into one open map (`member code: "JPY"`).
268
+ if let Some(pairs_row) = rows.iter().find(|r| r.kind == "pairs" && r.at == "1") {
269
+ if pairs_row.pairs_shape == "fields" {
270
+ return argument_gate_fields_pairs(file, word, &rows, pairs_row, &segments, line);
271
+ }
272
+ return argument_gate_named_pairs(file, word, &rows, pairs_row, &segments, line);
273
+ }
274
+
275
+ let mut positionals: Vec<String> = Vec::new();
276
+ let mut nameds: Vec<(String, String)> = Vec::new();
277
+ for segment in &segments {
278
+ match as_named(segment) {
279
+ Some((name, value)) => nameds.push((name.to_string(), value.to_string())),
280
+ None => positionals.push(segment.clone()),
281
+ }
282
+ }
283
+
284
+ for (idx, text) in positionals.iter().enumerate() {
285
+ let at = (idx + 1).to_string();
286
+ let mut candidates: Vec<&&ArgumentRow> = rows.iter().filter(|r| r.at == at).collect();
287
+ if candidates.is_empty() {
288
+ // A variadic row (`group_by`'s `*fields`) repeats for every position past its `at`;
289
+ // `one_of`'s repetition is hand-parsed in `resolve_type_expression`, not here.
290
+ if let Some(row) = rows.iter().find(|r| r.variadic == "true") {
291
+ candidates.push(row);
292
+ }
293
+ }
294
+ if candidates.is_empty() {
295
+ return Err(Diagnostic::new(
296
+ file,
297
+ line,
298
+ format!("'{word}' takes at most {idx} positional argument(s), but got another: '{text}'"),
299
+ ));
300
+ }
301
+ let kind = classify_lexical_kind(text);
302
+ if !candidates.iter().any(|r| kind_matches(r.kind, kind)) {
303
+ let expected: Vec<String> = candidates.iter().map(|r| r.kind.to_string()).collect();
304
+ return Err(Diagnostic::new(
305
+ file,
306
+ line,
307
+ format!("'{word}'s positional argument {at} ('{text}') reads as {kind}"),
308
+ )
309
+ .with_expected(expected));
310
+ }
311
+ }
312
+
313
+ for (name, value) in &nameds {
314
+ validate_named(file, word, &rows, name, value, line)?;
315
+ }
316
+
317
+ for row in &rows {
318
+ if row.required != "true" {
319
+ continue;
320
+ }
321
+ if !row.at.is_empty() {
322
+ let idx: usize = row.at.parse().unwrap_or(0);
323
+ if idx == 0 || idx > positionals.len() {
324
+ return Err(Diagnostic::new(
325
+ file,
326
+ line,
327
+ format!("'{word}' requires a positional argument at {}", row.at),
328
+ ));
329
+ }
330
+ } else if !row.named.is_empty() && !nameds.iter().any(|(n, _)| n == row.named) {
331
+ return Err(Diagnostic::new(
332
+ file,
333
+ line,
334
+ format!("'{word}' requires '{}:'", row.named),
335
+ ));
336
+ }
337
+ }
338
+
339
+ Ok(ArgumentGateResult {
340
+ positional: positionals
341
+ .into_iter()
342
+ .enumerate()
343
+ .map(|(i, t)| (i + 1, t))
344
+ .collect(),
345
+ named: nameds,
346
+ })
347
+ }
348
+
349
+ /// `pairs_shape: "fields"`: exactly one hash-rocket pair (`"Purchase" => "sold"`).
350
+ ///
351
+ /// An unclaimed `key: value` segment (`transition from: "pending", to: "received"`) is Ruby's hash
352
+ /// shorthand for the same pair, so it is rewritten to `"key" => value` and the transition is named
353
+ /// after the key (`"to"`). Segments a row declares by name (`from:`) are validated as named args.
354
+ fn argument_gate_fields_pairs(
355
+ file: &str,
356
+ word: &str,
357
+ rows: &[&ArgumentRow],
358
+ pairs_row: &ArgumentRow,
359
+ segments: &[String],
360
+ line: usize,
361
+ ) -> ParseResult<ArgumentGateResult> {
362
+ let mut field_pairs: Vec<String> = Vec::new();
363
+ let mut nameds: Vec<(String, String)> = Vec::new();
364
+
365
+ for segment in segments {
366
+ if has_top_level_rocket(segment) {
367
+ field_pairs.push(segment.clone());
368
+ } else if let Some((name, value)) = as_named(segment) {
369
+ let specifically_declared = rows.iter().any(|r| r.kind != "pairs" && r.named == name);
370
+ if specifically_declared {
371
+ validate_named(file, word, rows, name, value, line)?;
372
+ nameds.push((name.to_string(), value.to_string()));
373
+ } else {
374
+ field_pairs.push(format!("\"{name}\" => {value}"));
375
+ }
376
+ } else {
377
+ return Err(Diagnostic::new(
378
+ file,
379
+ line,
380
+ format!("'{word}'s argument '{segment}' is neither a 'key => value' pair nor a named argument"),
381
+ ));
382
+ }
383
+ }
384
+
385
+ if field_pairs.len() > 1 {
386
+ return Err(Diagnostic::new(
387
+ file,
388
+ line,
389
+ format!(
390
+ "'{word}' takes exactly one 'key => value' pair, got {}",
391
+ field_pairs.len()
392
+ ),
393
+ ));
394
+ }
395
+ if pairs_row.required == "true" && field_pairs.is_empty() {
396
+ return Err(Diagnostic::new(
397
+ file,
398
+ line,
399
+ format!("'{word}' requires a 'key => value' pair"),
400
+ ));
401
+ }
402
+
403
+ let named = field_pairs
404
+ .into_iter()
405
+ .map(|pair| ("=>".to_string(), pair))
406
+ .chain(nameds)
407
+ .collect();
408
+ Ok(ArgumentGateResult {
409
+ positional: Vec::new(),
410
+ named,
411
+ })
412
+ }
413
+
414
+ /// `pairs_shape: "verbatim"`/`"elements"`: many segments merged into one open map.
415
+ ///
416
+ /// A key that is not a bare identifier (a dotted query path) needs Ruby's `:"a.b" => v` spelling,
417
+ /// so a top-level rocket segment is accepted beside `key: value`; the two never overlap.
418
+ fn argument_gate_named_pairs(
419
+ file: &str,
420
+ word: &str,
421
+ rows: &[&ArgumentRow],
422
+ pairs_row: &ArgumentRow,
423
+ segments: &[String],
424
+ line: usize,
425
+ ) -> ParseResult<ArgumentGateResult> {
426
+ let mut positionals: Vec<String> = Vec::new();
427
+ let mut nameds: Vec<(String, String)> = Vec::new();
428
+ for segment in segments {
429
+ if let Some((name, value)) = as_named(segment) {
430
+ nameds.push((name.to_string(), value.to_string()));
431
+ continue;
432
+ }
433
+ if has_top_level_rocket(segment) {
434
+ let (key_text, value_text) = split_top_level_rocket(segment);
435
+ let name = rocket_key_name(file, word, key_text, line)?;
436
+ nameds.push((name, value_text.to_string()));
437
+ continue;
438
+ }
439
+ positionals.push(segment.clone());
440
+ }
441
+
442
+ if !positionals.is_empty() {
443
+ return Err(Diagnostic::new(
444
+ file,
445
+ line,
446
+ format!("'{word}' takes an open map of pairs, not a bare positional argument"),
447
+ ));
448
+ }
449
+
450
+ let specific_named: std::collections::BTreeSet<&str> = rows
451
+ .iter()
452
+ .filter(|r| !r.named.is_empty())
453
+ .map(|r| r.named)
454
+ .collect();
455
+ let mut claimed = Vec::new();
456
+ let mut leftover = Vec::new();
457
+ for (name, value) in nameds {
458
+ if specific_named.contains(name.as_str()) {
459
+ claimed.push((name, value));
460
+ } else {
461
+ leftover.push((name, value));
462
+ }
463
+ }
464
+
465
+ if pairs_row.required == "true" && leftover.is_empty() {
466
+ return Err(Diagnostic::new(
467
+ file,
468
+ line,
469
+ format!("'{word}' requires at least one pair"),
470
+ ));
471
+ }
472
+
473
+ for (name, value) in &claimed {
474
+ validate_named(file, word, rows, name, value, line)?;
475
+ }
476
+
477
+ let mut named = claimed;
478
+ named.extend(leftover);
479
+ Ok(ArgumentGateResult {
480
+ positional: Vec::new(),
481
+ named,
482
+ })
483
+ }
484
+
485
+ /// True when `text` holds a `=>` outside quotes, braces and brackets.
486
+ fn has_top_level_rocket(text: &str) -> bool {
487
+ let mut quoting = false;
488
+ let mut escaping = false;
489
+ let mut depth: i32 = 0;
490
+ let chars: Vec<char> = text.chars().collect();
491
+ let mut i = 0;
492
+ while i < chars.len() {
493
+ let ch = chars[i];
494
+ if escaping {
495
+ escaping = false;
496
+ i += 1;
497
+ continue;
498
+ }
499
+ if quoting && ch == '\\' {
500
+ escaping = true;
501
+ i += 1;
502
+ continue;
503
+ }
504
+ if ch == '"' {
505
+ quoting = !quoting;
506
+ i += 1;
507
+ continue;
508
+ }
509
+ if quoting {
510
+ i += 1;
511
+ continue;
512
+ }
513
+ match ch {
514
+ '{' | '[' => depth += 1,
515
+ '}' | ']' => depth -= 1,
516
+ '=' if depth == 0 && chars.get(i + 1) == Some(&'>') => return true,
517
+ _ => {}
518
+ }
519
+ i += 1;
520
+ }
521
+ false
522
+ }
523
+
524
+ /// Splits a segment already known (`has_top_level_rocket`) to contain a
525
+ /// top-level `=>` into `(key_text, value_text)`, both trimmed.
526
+ pub(crate) fn split_top_level_rocket(segment: &str) -> (&str, &str) {
527
+ let mut quoting = false;
528
+ let mut escaping = false;
529
+ let mut depth: i32 = 0;
530
+ let bytes = segment.as_bytes();
531
+ let mut i = 0;
532
+ while i < bytes.len() {
533
+ let ch = bytes[i] as char;
534
+ if escaping {
535
+ escaping = false;
536
+ i += 1;
537
+ continue;
538
+ }
539
+ if quoting && ch == '\\' {
540
+ escaping = true;
541
+ i += 1;
542
+ continue;
543
+ }
544
+ if ch == '"' {
545
+ quoting = !quoting;
546
+ i += 1;
547
+ continue;
548
+ }
549
+ if !quoting {
550
+ match ch {
551
+ '{' | '[' => depth += 1,
552
+ '}' | ']' => depth -= 1,
553
+ '=' if depth == 0 && bytes.get(i + 1) == Some(&b'>') => {
554
+ return (segment[..i].trim(), segment[i + 2..].trim());
555
+ }
556
+ _ => {}
557
+ }
558
+ }
559
+ i += 1;
560
+ }
561
+ (segment.trim(), "")
562
+ }
563
+
564
+ /// The field name of a pair key, a bare or quoted Symbol literal (`:status`, `:"a.b"`).
565
+ /// Any other key is refused; pair keys are always symbols.
566
+ fn rocket_key_name<'a>(
567
+ file: &str,
568
+ word: &str,
569
+ key_text: &'a str,
570
+ line: usize,
571
+ ) -> ParseResult<String> {
572
+ let Some(rest) = key_text.strip_prefix(':') else {
573
+ return Err(Diagnostic::new(file, line, format!("'{word}'s key '{key_text}' is not a symbol — a pair's key is always :name or :\"a.dotted.path\"")));
574
+ };
575
+ if rest.len() >= 2 && rest.starts_with('"') && rest.ends_with('"') {
576
+ return Ok(ruby_value::unquote_for_symbol(rest));
577
+ }
578
+ Ok(rest.to_string())
579
+ }
580
+
581
+ fn validate_named(
582
+ file: &str,
583
+ word: &str,
584
+ rows: &[&ArgumentRow],
585
+ name: &str,
586
+ value: &str,
587
+ line: usize,
588
+ ) -> ParseResult<()> {
589
+ let candidates: Vec<&&ArgumentRow> = rows.iter().filter(|r| r.named == name).collect();
590
+ if candidates.is_empty() {
591
+ let expected: Vec<String> = rows
592
+ .iter()
593
+ .filter(|r| !r.named.is_empty())
594
+ .map(|r| r.named.to_string())
595
+ .collect();
596
+ return Err(
597
+ Diagnostic::new(file, line, format!("'{word}' takes no '{name}:' argument"))
598
+ .with_expected(expected),
599
+ );
600
+ }
601
+ let kind = classify_lexical_kind(value);
602
+ if !candidates.iter().any(|r| kind_matches(r.kind, kind)) {
603
+ let expected: Vec<String> = candidates.iter().map(|r| r.kind.to_string()).collect();
604
+ return Err(Diagnostic::new(
605
+ file,
606
+ line,
607
+ format!("'{word}'s '{name}:' ('{value}') reads as {kind}"),
608
+ )
609
+ .with_expected(expected));
610
+ }
611
+ Ok(())
612
+ }
613
+
614
+ /// Maps a `KeywordRow.inner` context to its per-construct module's `not_implemented` diagnostic.
615
+ pub(crate) fn dispatch_stub(
616
+ inner_context: &str,
617
+ file: &str,
618
+ line: usize,
619
+ word: &str,
620
+ ) -> Diagnostic {
621
+ match inner_context {
622
+ "Bluebook" => chapter::not_implemented(file, line, word),
623
+ "Hecksagon" => hecksagon::not_implemented(file, line, word),
624
+ "Aggregate" => aggregate::not_implemented(file, line, word),
625
+ "Entity" => entity::not_implemented(file, line, word),
626
+ "Command" => command::not_implemented(file, line, word),
627
+ "Query" => query::not_implemented(file, line, word),
628
+ "ValueObject" | "OneOf" => value_object::not_implemented(file, line, word),
629
+ "Lifecycle" => lifecycle::not_implemented(file, line, word),
630
+ "Policy" => policy::not_implemented(file, line, word),
631
+ "ProcessManager" | "Handler" => process_manager::not_implemented(file, line, word),
632
+ "ReadModel" => read_model::not_implemented(file, line, word),
633
+ "DomainPort" | "PortOperation" => domain_port::not_implemented(file, line, word),
634
+ // `World` is deliberately unmapped: its body is the open verb-setting catch-all, so it
635
+ // falls through to the closed word gate and refuses there.
636
+ other => Diagnostic::not_yet_implemented(
637
+ file,
638
+ line,
639
+ format!("{other}.{word} (unmapped inner context)"),
640
+ ),
641
+ }
642
+ }
643
+
644
+ /// Fallback diagnostic for a word this parser does not build IR for.
645
+ /// `tests/gates.rs` depends on its wording staying stable.
646
+ pub(crate) fn not_built_yet(
647
+ context: &str,
648
+ row: &KeywordRow,
649
+ file: &str,
650
+ line: usize,
651
+ word: &str,
652
+ ) -> Diagnostic {
653
+ if row.inner.is_empty() {
654
+ dispatch_stub(context, file, line, word)
655
+ } else {
656
+ dispatch_stub(row.inner, file, line, word)
657
+ }
658
+ }
659
+
660
+ /// One gated line: all four gates have run, and any nested body is left unconsumed.
661
+ /// The caller decides whether to recurse into it.
662
+ pub(crate) struct GatedLine<'src> {
663
+ pub line: SourceLine<'src>,
664
+ pub call: Call,
665
+ // Always `'static`: every `KeywordRow` lives in the generated `KEYWORDS` slice.
666
+ pub row: &'static KeywordRow,
667
+ pub args: ArgumentGateResult,
668
+ }
669
+
670
+ /// Gates the next line in `context`; `None` means `end` was consumed, `Some` a legal call.
671
+ /// `*pos` advances past the line, never past a nested body.
672
+ pub(crate) fn next_line<'src>(
673
+ file: &str,
674
+ lines: &[SourceLine<'src>],
675
+ pos: &mut usize,
676
+ context: &'static str,
677
+ ) -> ParseResult<Option<GatedLine<'src>>> {
678
+ let line = *lines.get(*pos).ok_or_else(|| {
679
+ let last = lines.last().map(|l| l.number).unwrap_or(0);
680
+ Diagnostic::new(
681
+ file,
682
+ last,
683
+ format!("unexpected end of file — still inside {context}"),
684
+ )
685
+ })?;
686
+
687
+ let shape = lex::classify(file, &line)?;
688
+ *pos += 1;
689
+
690
+ match shape {
691
+ LineShape::End => Ok(None),
692
+ LineShape::Call(call) => {
693
+ let candidates: Vec<&'static KeywordRow> =
694
+ word_gate(file, &call.word, context, line.number)?;
695
+ let row: &'static KeywordRow =
696
+ body_gate(file, &candidates, &call.opener, line.number, &call.word)?;
697
+ let args = argument_gate(file, row.word, context, &call.args, line.number)?;
698
+ Ok(Some(GatedLine {
699
+ line,
700
+ call,
701
+ row,
702
+ args,
703
+ }))
704
+ }
705
+ }
706
+ }
707
+
708
+ /// The N'th (1-based) positional's raw text; missing is a defensive error after `argument_gate`.
709
+ fn positional_raw<'a>(
710
+ file: &str,
711
+ line: usize,
712
+ word: &str,
713
+ args: &'a ArgumentGateResult,
714
+ at: usize,
715
+ ) -> ParseResult<&'a str> {
716
+ args.positional
717
+ .iter()
718
+ .find(|(idx, _)| *idx == at)
719
+ .map(|(_, text)| text.as_str())
720
+ .ok_or_else(|| {
721
+ Diagnostic::new(
722
+ file,
723
+ line,
724
+ format!("'{word}' has no positional argument {at}"),
725
+ )
726
+ })
727
+ }
728
+
729
+ /// A required text (quoted-string) positional argument, unquoted —
730
+ /// `aggregate "Widget"`, `given("description")`, `role "Chef"`.
731
+ pub(crate) fn positional_text(
732
+ file: &str,
733
+ line: usize,
734
+ word: &str,
735
+ args: &ArgumentGateResult,
736
+ at: usize,
737
+ ) -> ParseResult<String> {
738
+ let raw = positional_raw(file, line, word, args, at)?;
739
+ Ok(match ruby_value::read(raw) {
740
+ ruby_value::Value::Str(s) => s,
741
+ other => ruby_value::to_s(&other),
742
+ })
743
+ }
744
+
745
+ /// A required symbol positional without its colon (`attribute :name`), bare or quoted (`:"a.b"`).
746
+ /// The quoted form spells dotted paths, e.g. `correlates_by :"reference.value"`.
747
+ pub(crate) fn positional_symbol(
748
+ file: &str,
749
+ line: usize,
750
+ word: &str,
751
+ args: &ArgumentGateResult,
752
+ at: usize,
753
+ ) -> ParseResult<String> {
754
+ let raw = positional_raw(file, line, word, args, at)?.trim();
755
+ symbol_text(raw).ok_or_else(|| {
756
+ Diagnostic::new(
757
+ file,
758
+ line,
759
+ format!("'{word}'s positional argument {at} ('{raw}') is not a symbol"),
760
+ )
761
+ })
762
+ }
763
+
764
+ /// The name in a source symbol token: `:name` -> `name`, `:"a.b"` -> `a.b`.
765
+ /// Not `ruby_value::read`, which reads rendered wire values rather than source syntax.
766
+ fn symbol_text(raw: &str) -> Option<String> {
767
+ let rest = raw.strip_prefix(':')?;
768
+ if rest.len() >= 2 && rest.starts_with('"') && rest.ends_with('"') {
769
+ Some(ruby_value::unquote_for_symbol(rest))
770
+ } else {
771
+ Some(rest.to_string())
772
+ }
773
+ }
774
+
775
+ /// A constant (bareword type name) positional argument, raw — `attribute
776
+ /// :pizza, Pizza`. Never quoted, never colon-prefixed; taken verbatim.
777
+ pub(crate) fn positional_constant<'a>(
778
+ file: &str,
779
+ line: usize,
780
+ word: &str,
781
+ args: &'a ArgumentGateResult,
782
+ at: usize,
783
+ ) -> ParseResult<&'a str> {
784
+ positional_raw(file, line, word, args, at).map(|s| s.trim())
785
+ }
786
+
787
+ /// A command reference: bare constant (`trigger Account::Debit`) or quoted (`"Account.Debit"`).
788
+ /// Derives the same text as `Hecks::Naming.command_ref`.
789
+ pub(crate) fn positional_command_ref(
790
+ file: &str,
791
+ line: usize,
792
+ word: &str,
793
+ args: &ArgumentGateResult,
794
+ at: usize,
795
+ ) -> ParseResult<String> {
796
+ let raw = positional_raw(file, line, word, args, at)?;
797
+ Ok(crate::build::naming::command_ref(raw))
798
+ }
799
+
800
+ /// A process manager's event reference (`starts_on Transfer::TransferRequested`), bare or quoted.
801
+ /// Derives `Hecks::Naming.event_name_ref`, which keeps only the bare final segment.
802
+ pub(crate) fn positional_event_name_ref(
803
+ file: &str,
804
+ line: usize,
805
+ word: &str,
806
+ args: &ArgumentGateResult,
807
+ at: usize,
808
+ ) -> ParseResult<String> {
809
+ let raw = positional_raw(file, line, word, args, at)?;
810
+ Ok(crate::build::naming::event_name_ref(raw))
811
+ }
812
+
813
+ /// A named argument's raw captured text, if the call gave one —
814
+ /// `as: :name`, `optional: true`, `to: "sold"`.
815
+ pub(crate) fn named_raw<'a>(args: &'a ArgumentGateResult, name: &str) -> Option<&'a str> {
816
+ args.named
817
+ .iter()
818
+ .find(|(n, _)| n == name)
819
+ .map(|(_, v)| v.as_str())
820
+ }
821
+
822
+ /// A named constant argument (`to: Payment`): raw text, trimmed; resolved downstream.
823
+ pub(crate) fn named_constant<'a>(args: &'a ArgumentGateResult, name: &str) -> Option<&'a str> {
824
+ named_raw(args, name).map(|s| s.trim())
825
+ }
826
+
827
+ /// A named symbol argument without its colon (`as: :name`); bare or quoted.
828
+ pub(crate) fn named_symbol(args: &ArgumentGateResult, name: &str) -> Option<String> {
829
+ named_raw(args, name).and_then(|raw| symbol_text(raw.trim()))
830
+ }
831
+
832
+ /// A named text argument, unquoted.
833
+ pub(crate) fn named_text(args: &ArgumentGateResult, name: &str) -> Option<String> {
834
+ named_raw(args, name).map(|raw| match ruby_value::read(raw) {
835
+ ruby_value::Value::Str(s) => s,
836
+ other => ruby_value::to_s(&other),
837
+ })
838
+ }
839
+
840
+ /// A named flag (`true`/`false`) argument — `optional: true`.
841
+ pub(crate) fn named_flag(args: &ArgumentGateResult, name: &str) -> bool {
842
+ named_raw(args, name)
843
+ .map(|raw| raw.trim() == "true")
844
+ .unwrap_or(false)
845
+ }
846
+
847
+ /// Raw text of a `source`-shaped body written as `{ ... }` or `do ... end`.
848
+ /// Shared by `identified_by`, `given` and `invariant`.
849
+ pub(crate) fn source_body_text(
850
+ file: &str,
851
+ lines: &[SourceLine],
852
+ pos: &mut usize,
853
+ opener: &Opener,
854
+ ) -> ParseResult<String> {
855
+ match opener {
856
+ Opener::BraceBlock { body } => Ok(body.clone()),
857
+ Opener::DoBlock { .. } => lex::capture_do_block_body(file, lines, pos),
858
+ Opener::None => {
859
+ unreachable!("body_gate only ever admits BraceBlock/DoBlock for a `source`-bodied row")
860
+ }
861
+ }
862
+ }
863
+
864
+ /// Runs `parse` over a nested `keywords`/`rows` body written with `do ... end` or `{ ... }`.
865
+ ///
866
+ /// A brace body is split into synthetic one-statement lines ending in `end`, so the per-construct
867
+ /// `parse_body` functions, which walk `(lines, pos)`, need no second code path.
868
+ pub(crate) fn parse_nested_body<T>(
869
+ file: &str,
870
+ lines: &[SourceLine],
871
+ pos: &mut usize,
872
+ opener: &Opener,
873
+ line_number: usize,
874
+ parse: impl FnOnce(&str, &[SourceLine], &mut usize) -> ParseResult<T>,
875
+ ) -> ParseResult<T> {
876
+ match opener {
877
+ Opener::BraceBlock { body } => {
878
+ let owned = brace_body_statements(body);
879
+ let synthetic: Vec<SourceLine> = owned
880
+ .iter()
881
+ .map(|text| SourceLine {
882
+ number: line_number,
883
+ text: text.as_str(),
884
+ })
885
+ .collect();
886
+ let mut synthetic_pos = 0;
887
+ parse(file, &synthetic, &mut synthetic_pos)
888
+ }
889
+ _ => parse(file, lines, pos),
890
+ }
891
+ }
892
+
893
+ /// Splits a captured `{ ... }` body on top-level `;` (outside quotes, braces, brackets, parens).
894
+ /// Ends with a synthetic `"end"` so callers can treat it as an ordinary body.
895
+ fn brace_body_statements(body: &str) -> Vec<String> {
896
+ let mut statements: Vec<String> = Vec::new();
897
+ let mut current = String::new();
898
+ let mut depth: i32 = 0;
899
+ let mut quoting = false;
900
+ let mut escaping = false;
901
+
902
+ for ch in body.chars() {
903
+ if escaping {
904
+ current.push(ch);
905
+ escaping = false;
906
+ continue;
907
+ }
908
+ if quoting && ch == '\\' {
909
+ current.push(ch);
910
+ escaping = true;
911
+ continue;
912
+ }
913
+ if ch == '"' {
914
+ quoting = !quoting;
915
+ current.push(ch);
916
+ continue;
917
+ }
918
+ if quoting {
919
+ current.push(ch);
920
+ continue;
921
+ }
922
+ match ch {
923
+ '{' | '[' | '(' => depth += 1,
924
+ '}' | ']' | ')' => depth -= 1,
925
+ ';' if depth == 0 => {
926
+ statements.push(current.trim().to_string());
927
+ current.clear();
928
+ continue;
929
+ }
930
+ _ => {}
931
+ }
932
+ current.push(ch);
933
+ }
934
+ statements.push(current.trim().to_string());
935
+
936
+ let mut statements: Vec<String> = statements.into_iter().filter(|s| !s.is_empty()).collect();
937
+ statements.push("end".to_string());
938
+ statements
939
+ }
940
+
941
+ /// An `entity`/`command`/`query` body recorded for later parsing, like Ruby's `#drain_pending!`.
942
+ ///
943
+ /// Nested constructs resolve against the owner's complete `value_objects`/`entities`/
944
+ /// `preconditions`, so the body is located here (skipping a `do ... end` block) and parsed by
945
+ /// `build_deferred` once the owner has been fully walked.
946
+ pub(crate) struct PendingBody {
947
+ opener: Opener,
948
+ line_number: usize,
949
+ body_start: usize,
950
+ }
951
+
952
+ /// Records the body starting at `*pos` once its opening line is gated; see `PendingBody`.
953
+ pub(crate) fn defer_body(
954
+ file: &str,
955
+ lines: &[SourceLine],
956
+ pos: &mut usize,
957
+ opener: &Opener,
958
+ line_number: usize,
959
+ ) -> ParseResult<PendingBody> {
960
+ let body_start = *pos;
961
+ if matches!(opener, Opener::DoBlock { .. }) {
962
+ lex::capture_do_block_body(file, lines, pos)?;
963
+ }
964
+ Ok(PendingBody {
965
+ opener: opener.clone(),
966
+ line_number,
967
+ body_start,
968
+ })
969
+ }
970
+
971
+ /// Parses a deferred body via `parse_nested_body`; `lines` must be the slice `defer_body` saw.
972
+ pub(crate) fn build_deferred<T>(
973
+ file: &str,
974
+ lines: &[SourceLine],
975
+ pending: &PendingBody,
976
+ parse: impl FnOnce(&str, &[SourceLine], &mut usize) -> ParseResult<T>,
977
+ ) -> ParseResult<T> {
978
+ let mut pos = pending.body_start;
979
+ parse_nested_body(
980
+ file,
981
+ lines,
982
+ &mut pos,
983
+ &pending.opener,
984
+ pending.line_number,
985
+ parse,
986
+ )
987
+ }
988
+
989
+ /// The `identified_by` forms resolved for both `parse::aggregate` and `parse::entity`.
990
+ /// `Fields` (ADR 0025) resolves each symbol against an already-declared attribute.
991
+ pub(crate) enum PendingIdentity {
992
+ Type {
993
+ line: usize,
994
+ target: String,
995
+ as_field: Option<String>,
996
+ insert_at: usize,
997
+ },
998
+ Fields {
999
+ line: usize,
1000
+ names: Vec<String>,
1001
+ },
1002
+ Inline {
1003
+ line: usize,
1004
+ value_object: ir::ValueObject,
1005
+ as_field: Option<String>,
1006
+ insert_at: usize,
1007
+ },
1008
+ }
1009
+
1010
+ /// Parses `identified_by` in its three forms: a value object type (`PizzaName, as: :name`), one or
1011
+ /// more field symbols (`:a, :b`), or a `{ ... }`/`do ... end` block declaring an inline value
1012
+ /// object named `inline_type_name`.
1013
+ pub(crate) fn parse_identified_by(
1014
+ file: &str,
1015
+ lines: &[SourceLine],
1016
+ pos: &mut usize,
1017
+ line: usize,
1018
+ args: &ArgumentGateResult,
1019
+ opener: &Opener,
1020
+ insert_at: usize,
1021
+ inline_type_name: &str,
1022
+ owner_value_objects: &[ir::ValueObject],
1023
+ ) -> ParseResult<PendingIdentity> {
1024
+ match opener {
1025
+ Opener::None => {
1026
+ let text = positional_constant(file, line, "identified_by", args, 1)?;
1027
+ match classify_lexical_kind(text) {
1028
+ "constant" => {
1029
+ let as_field = named_symbol(args, "as");
1030
+ Ok(PendingIdentity::Type { line, target: text.to_string(), as_field, insert_at })
1031
+ }
1032
+ "symbol" => {
1033
+ let names: Vec<String> = args.positional.iter().map(|(_, raw)| positional_symbol_text(file, line, "identified_by", raw)).collect::<ParseResult<_>>()?;
1034
+ Ok(PendingIdentity::Fields { line, names })
1035
+ }
1036
+ other => Err(Diagnostic::new(file, line, format!("'identified_by's positional argument reads as {other}, neither a value object nor a field"))),
1037
+ }
1038
+ }
1039
+ Opener::DoBlock { .. } | Opener::BraceBlock { .. } => {
1040
+ if !args.positional.is_empty() {
1041
+ return Err(Diagnostic::new(
1042
+ file,
1043
+ line,
1044
+ "'identified_by' cannot combine a value-object type with a block",
1045
+ ));
1046
+ }
1047
+ let value_object = parse_nested_body(file, lines, pos, opener, line, |f, l, p| {
1048
+ value_object::parse_body(f, l, p, inline_type_name, owner_value_objects)
1049
+ })?;
1050
+ if value_object.attributes.is_empty() {
1051
+ return Err(Diagnostic::new(
1052
+ file,
1053
+ line,
1054
+ "'identified_by do' declares no identity attributes",
1055
+ ));
1056
+ }
1057
+ Ok(PendingIdentity::Inline {
1058
+ line,
1059
+ value_object,
1060
+ as_field: named_symbol(args, "as"),
1061
+ insert_at,
1062
+ })
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ /// Symbol name for one raw positional token, for variadic callers such as `identified_by`.
1068
+ fn positional_symbol_text(file: &str, line: usize, word: &str, raw: &str) -> ParseResult<String> {
1069
+ let trimmed = raw.trim();
1070
+ symbol_text(trimmed).ok_or_else(|| {
1071
+ Diagnostic::new(
1072
+ file,
1073
+ line,
1074
+ format!("'{word}'s positional argument ('{trimmed}') is not a symbol"),
1075
+ )
1076
+ })
1077
+ }
1078
+
1079
+ /// Builds an attribute and, for an inline `one_of(...)` type, its synthesized closed set.
1080
+ /// Only `parse::aggregate` keeps that value object; other callers discard it.
1081
+ pub(crate) fn build_attribute(
1082
+ file: &str,
1083
+ line: usize,
1084
+ word: &str,
1085
+ args: &ArgumentGateResult,
1086
+ ) -> ParseResult<(ir::Attribute, Option<ir::ValueObject>)> {
1087
+ let name = positional_symbol(file, line, word, args, 1)?;
1088
+ // The type position is a bare constant and always required; there is no default.
1089
+ let (_, text) = args
1090
+ .positional
1091
+ .iter()
1092
+ .find(|(idx, _)| *idx == 2)
1093
+ .ok_or_else(|| Diagnostic::new(file, line, format!("'{name}' declares no type — attribute :{name}, SomeType is required, there is no default")))?;
1094
+ let (type_name, list, closed_set) = resolve_type_expression(file, line, word, &name, text)?;
1095
+ let default = named_raw(args, "default").map(ruby_value::read);
1096
+ let optional = named_flag(args, "optional");
1097
+ let pattern = named_text(args, "pattern");
1098
+ let admits = named_text(args, "admits");
1099
+ // Fail closed at declaration time on a pattern outside the shared subset, as Ruby does.
1100
+ if let Some(pat) = &pattern {
1101
+ if let Some(rejection) = crate::build::pattern_subset::validate(pat) {
1102
+ return Err(Diagnostic::new(
1103
+ file,
1104
+ line,
1105
+ format!(
1106
+ "'{name}'s pattern {pat:?} uses a {} — {}",
1107
+ rejection.construct, rejection.reason
1108
+ ),
1109
+ ));
1110
+ }
1111
+ }
1112
+ Ok((
1113
+ ir::Attribute {
1114
+ name,
1115
+ type_name,
1116
+ list,
1117
+ default,
1118
+ optional,
1119
+ pattern,
1120
+ admits,
1121
+ relationship: None,
1122
+ },
1123
+ closed_set,
1124
+ ))
1125
+ }
1126
+
1127
+ /// Resolves an attribute's type: a bare constant, `list_of(T)`, or an inline `one_of(...)`.
1128
+ ///
1129
+ /// `list_of` unwraps to `(inner, true)`. `one_of` synthesizes a closed-set value object named
1130
+ /// after the field, as `AttributeCollector#synthesise_closed_set` does, and returns it.
1131
+ pub(crate) fn resolve_type_expression(
1132
+ file: &str,
1133
+ line: usize,
1134
+ word: &str,
1135
+ field_name: &str,
1136
+ text: &str,
1137
+ ) -> ParseResult<(String, bool, Option<ir::ValueObject>)> {
1138
+ let trimmed = text.trim();
1139
+ match type_context_call_word(trimmed) {
1140
+ Some("list_of") => {
1141
+ let open = trimmed
1142
+ .find('(')
1143
+ .expect("type_context_call_word already confirmed a '('");
1144
+ let inner = trimmed[open + 1..trimmed.len() - 1].trim();
1145
+ if classify_lexical_kind(inner) != "constant" {
1146
+ return Err(Diagnostic::new(
1147
+ file,
1148
+ line,
1149
+ format!("'{word}'s list_of(...) must hold a constant type name, got '{inner}'"),
1150
+ ));
1151
+ }
1152
+ Ok((inner.to_string(), true, None))
1153
+ }
1154
+ Some("one_of") => {
1155
+ let open = trimmed
1156
+ .find('(')
1157
+ .expect("type_context_call_word already confirmed a '('");
1158
+ let inner = &trimmed[open + 1..trimmed.len() - 1];
1159
+ let values: Vec<String> = ruby_value::split_items(inner)
1160
+ .into_iter()
1161
+ .map(|segment| match ruby_value::read(segment.trim()) {
1162
+ ruby_value::Value::Str(s) => s,
1163
+ other => ruby_value::to_s(&other),
1164
+ })
1165
+ .collect();
1166
+ if values.is_empty() {
1167
+ return Err(Diagnostic::new(
1168
+ file,
1169
+ line,
1170
+ format!("'{word}'s inline one_of(...) names no values"),
1171
+ ));
1172
+ }
1173
+ let vo = crate::build::closed_sets::synthesize(field_name, &values);
1174
+ let type_name = vo.name.clone();
1175
+ Ok((type_name, false, Some(vo)))
1176
+ }
1177
+ Some(other) => Err(Diagnostic::not_yet_implemented(
1178
+ file,
1179
+ line,
1180
+ format!("{word}'s inline {other}(...) type"),
1181
+ )),
1182
+ // A quoted type name is refused; the bare constant resolves forward references too.
1183
+ None if classify_lexical_kind(trimmed) == "text" => Err(Diagnostic::new(
1184
+ file,
1185
+ line,
1186
+ format!(
1187
+ "'{field_name}'s type {trimmed} is quoted text — give the bare constant instead"
1188
+ ),
1189
+ )),
1190
+ None => Ok((trimmed.to_string(), false, None)),
1191
+ }
1192
+ }
1193
+
1194
+ /// Walks lines inside an open `context` up to and including the matching `end`, gating each one.
1195
+ /// The first gate failure or not-yet-implemented construct, at any depth, aborts the walk.
1196
+ pub fn walk_body(
1197
+ file: &str,
1198
+ lines: &[SourceLine],
1199
+ pos: &mut usize,
1200
+ context: &'static str,
1201
+ ) -> ParseResult<()> {
1202
+ loop {
1203
+ let line = *lines.get(*pos).ok_or_else(|| {
1204
+ let last = lines.last().map(|l| l.number).unwrap_or(0);
1205
+ Diagnostic::new(
1206
+ file,
1207
+ last,
1208
+ format!("unexpected end of file — still inside {context}"),
1209
+ )
1210
+ })?;
1211
+
1212
+ let shape = lex::classify(file, &line)?;
1213
+ *pos += 1;
1214
+
1215
+ match shape {
1216
+ LineShape::End => return Ok(()),
1217
+ LineShape::Call(call) => handle_call(file, lines, pos, context, &line, call)?,
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ fn handle_call(
1223
+ file: &str,
1224
+ lines: &[SourceLine],
1225
+ pos: &mut usize,
1226
+ context: &'static str,
1227
+ line: &SourceLine,
1228
+ call: Call,
1229
+ ) -> ParseResult<()> {
1230
+ let candidates = word_gate(file, &call.word, context, line.number)?;
1231
+ let row = body_gate(file, &candidates, &call.opener, line.number, &call.word)?;
1232
+ argument_gate(file, &call.word, context, &call.args, line.number)?;
1233
+
1234
+ match &call.opener {
1235
+ Opener::DoBlock { .. } => {
1236
+ if row.inner.is_empty() {
1237
+ return Err(dispatch_stub(context, file, line.number, &call.word));
1238
+ }
1239
+ let inner_context = keywords::CONTEXTS
1240
+ .iter()
1241
+ .find(|c| **c == row.inner)
1242
+ .copied()
1243
+ .ok_or_else(|| {
1244
+ Diagnostic::new(
1245
+ file,
1246
+ line.number,
1247
+ format!(
1248
+ "'{}' opens an undeclared context '{}'",
1249
+ call.word, row.inner
1250
+ ),
1251
+ )
1252
+ })?;
1253
+ walk_body(file, lines, pos, inner_context)?;
1254
+ // The nested body gated cleanly but builds nothing yet, so the failure is reported at
1255
+ // the construct just entered rather than the outer `context`.
1256
+ Err(dispatch_stub(inner_context, file, line.number, &call.word))
1257
+ }
1258
+ Opener::BraceBlock { .. } | Opener::None => {
1259
+ Err(dispatch_stub(context, file, line.number, &call.word))
1260
+ }
1261
+ }
1262
+ }