hecks 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
data/lib/hecks/cli.rb CHANGED
@@ -1,61 +1,116 @@
1
1
  module Hecks
2
2
  # The `hecks` command an installed gem puts on the path: one router over the
3
- # domain-operator tools, each also runnable from a checkout as `bin/<name>`.
3
+ # domain-operator tools, each also runnable from a checkout as `exe/hecks <name>`.
4
4
  #
5
- # Every subcommand's logic lives under `cli/`, with `bin/<name>` a thin wrapper
6
- # over the same entry point, so the two can't drift apart.
5
+ # Every subcommand's logic lives under `cli/`, and `exe/hecks` routes to it.
7
6
  #
8
7
  # See `docs/decisions/0066-the-gem-ships-a-hecks-executable-and-dev-tooling-stays-in-the-repo.md`.
9
8
  module CLI
10
- # One subcommand's help: a one-line summary and its usage line.
11
- Command = Struct.new(:summary, :usage)
9
+ # One subcommand: a one-line summary, its usage line, the file that holds it and the
10
+ # call that runs it (given the argv after the name, its program name and its own name).
11
+ Command = Struct.new(:summary, :usage, :file, :run)
12
12
 
13
13
  # Every subcommand, in the order the help lists them.
14
14
  COMMANDS = {
15
15
  "run" => Command.new(
16
16
  "Dispatch a verb, or run a JSON step list, against a domain.",
17
- "hecks run [domain] <verb [name=value …] | script.json | - | '{\"steps\":[…]}'>"
17
+ "hecks run [domain] <verb [name=value …] | script.json | - | '{\"steps\":[…]}'>",
18
+ "cli/run",
19
+ ->(argv, program, _name) { Run.call(argv, program: program) }
18
20
  ),
19
21
  "docs" => Command.new(
20
22
  "Print a domain's usage document, projected from its bluebook.",
21
- "hecks docs [domain-path] [aggregate]"
23
+ "hecks docs [domain-path] [aggregate]",
24
+ "cli/document",
25
+ ->(argv, program, name) { Document.call(argv, projection: name.to_sym, program: program, root: Dir.pwd) }
22
26
  ),
23
27
  "narrate" => Command.new(
24
28
  "Print a domain read back in English, projected from its bluebook.",
25
- "hecks narrate [domain-path] [aggregate]"
29
+ "hecks narrate [domain-path] [aggregate]",
30
+ "cli/document",
31
+ ->(argv, program, name) { Document.call(argv, projection: name.to_sym, program: program, root: Dir.pwd) }
26
32
  ),
27
33
  "ir" => Command.new(
28
34
  "Print a booted domain's IR as JSON.",
29
- "hecks ir <domain> [--translations] | hecks ir --meta"
35
+ "hecks ir <domain> [--translations] | hecks ir --meta",
36
+ "cli/ir",
37
+ ->(argv, program, _name) { Ir.call(argv, program: program) }
30
38
  ),
31
39
  "stores" => Command.new(
32
40
  "Print every aggregate's current records as JSON.",
33
- "hecks stores <domain>"
41
+ "hecks stores <domain>",
42
+ "cli/stores",
43
+ ->(argv, program, _name) { Stores.call(argv, program: program) }
34
44
  ),
35
45
  "model_check" => Command.new(
36
46
  "Statically check a domain's IR for dead states and unreachable steps.",
37
- "hecks model_check [--strict] [--profile client] <domain> [<domain> …]"
47
+ "hecks model_check [--strict] [--profile client] [--wait] [<domain> …]",
48
+ "cli/model_check",
49
+ ->(argv, program, _name) { ModelCheck.call(argv, program: program, root: checkout_root) }
38
50
  ),
39
51
  "smoke_test" => Command.new(
40
52
  "Boot a domain and dispatch every declared command and report once.",
41
- "hecks smoke_test [domain]"
53
+ "hecks smoke_test [domain]",
54
+ "cli/smoke_test",
55
+ ->(argv, _program, _name) { SmokeTest.call(argv, root: Dir.pwd) }
42
56
  ),
43
57
  "project_diagrams" => Command.new(
44
- "Write a domain's Mermaid diagrams under ./docs/generated/diagrams/.",
45
- "hecks project_diagrams <domain-path> <ChapterName>"
58
+ "Write a domain's Mermaid diagrams under ./docs/generated/diagrams/ (positional form only).",
59
+ "hecks project_diagrams <domain-path> <ChapterName> (writes) | " \
60
+ "hecks project_diagrams domain=<path> chapter=<Name> (prints; writes nothing)",
61
+ "cli/project_diagrams",
62
+ ->(argv, program, _name) { ProjectDiagrams.call(argv, program: program, root: Dir.pwd) }
46
63
  ),
47
64
  "project_cli" => Command.new(
48
65
  "Write a command-line launcher beside each domain, named after its bluebook.",
49
- "hecks project_cli [domain-path …]"
66
+ "hecks project_cli [domain-path …]",
67
+ "cli/project_cli",
68
+ ->(argv, program, _name) { ProjectCli.call(argv, program: program, root: Dir.pwd, remove_stale_bin: false) }
50
69
  ),
51
70
  "mcp" => Command.new(
52
71
  "Serve the MCP door over stdio (no authentication; stdio only).",
53
- "hecks mcp [--stdio]"
72
+ "hecks mcp [--stdio]",
73
+ "cli/mcp",
74
+ ->(argv, _program, _name) { Mcp.call(argv) }
54
75
  )
55
76
  }.freeze
56
77
 
57
78
  HELP_FLAGS = ["--help", "-h"].freeze
58
79
 
80
+ # The launcher's flags that mean the same for every subcommand here: `--wait` (a subcommand
81
+ # exits with its own verdict, so waiting changes nothing) and `--confirm` (nothing here asks
82
+ # for one). Each may carry a Boolean word, as at the launcher.
83
+ GENERIC_FLAGS = ["--wait", "--confirm"].freeze
84
+
85
+ # A launcher word: `name=value`, where the name has no path characters.
86
+ NAME_VALUE = /\A[A-Za-z_][\w-]*=/
87
+
88
+ # The launcher's `name=value` spellings of a subcommand's positional arguments.
89
+ #
90
+ # `slots` lists, in positional order, the names that fill each place (`domain` before
91
+ # `aggregate`); `many` lets the first slot repeat and take a comma list; `exists` makes
92
+ # the first slot's values paths that must exist; `flags` and `options` are `--name`
93
+ # switches and `--name value` pairs; `ignored` names a launcher argument this form has no
94
+ # use for. A subcommand absent here (`run`, whose pairs are the verb's own, `mcp`, and
95
+ # `project_diagrams`, whose launcher form answers instead of writing) keeps its words.
96
+ LAUNCHER_FORMS = {
97
+ "docs" => { slots: [%w[domain], %w[aggregate]], exists: true },
98
+ "narrate" => { slots: [%w[domain], %w[aggregate]], exists: true },
99
+ "ir" => { slots: [%w[domain]], flags: %w[translations meta], exists: true },
100
+ "stores" => { slots: [%w[domain]], exists: true },
101
+ "model_check" => { slots: [%w[domains domain]], many: true, flags: %w[strict],
102
+ options: %w[profile], ignored: %w[run] },
103
+ "smoke_test" => { slots: [%w[subject domain]], ignored: %w[run] },
104
+ "project_cli" => { slots: [%w[domain]], many: true }
105
+ }.freeze
106
+
107
+ # Subcommands whose words are their own: `run`'s pairs and `mcp`'s flags are not the launcher's.
108
+ UNTOUCHED = %w[run mcp].freeze
109
+
110
+ # Subcommands whose `name=value` form is the launcher's own question, which answers
111
+ # differently from the positional form: `project_diagrams` writes files, the question prints.
112
+ LAUNCHER_ANSWERS = ["project_diagrams"].freeze
113
+
59
114
  # Printed above the usage line by `overview`, on every path that reaches it
60
115
  # (a bare `hecks`, `hecks --help`, and an unknown subcommand alike).
61
116
  HERO = <<~BANNER.freeze
@@ -73,6 +128,15 @@ module Hecks
73
128
 
74
129
  module_function
75
130
 
131
+ # The checkout this library is loaded from, which a corpus sweep needs; an installed gem has
132
+ # none.
133
+ #
134
+ # @return [String, nil] the repository root, or nil when `hecks.gemspec` is not beside `lib/`
135
+ def checkout_root
136
+ root = File.expand_path("../..", __dir__)
137
+ root if File.exist?(File.join(root, "hecks.gemspec"))
138
+ end
139
+
76
140
  # Routes `argv` to its subcommand and runs it.
77
141
  #
78
142
  # A subcommand followed only by `--help`/`-h` prints its usage without
@@ -81,9 +145,12 @@ module Hecks
81
145
  # @param argv [Array<String>] the command line, subcommand first
82
146
  # @param out [#puts] where help goes
83
147
  # @param err [#puts] where usage errors go
84
- # @return [Integer] the process exit status for a subcommand that returns; a
85
- # subcommand that fails exits the process itself
148
+ # @return [Integer] the process exit status: 2 for a bad command, 1 for a bad word, the
149
+ # subcommand's own Integer when it returns one, else 0; a subcommand that fails exits
150
+ # the process itself
86
151
  def start(argv, out: $stdout, err: $stderr)
152
+ Encoding.default_external = Encoding::UTF_8
153
+ Encoding.default_internal = Encoding::UTF_8
87
154
  name, *rest = argv
88
155
  if name.nil? || !COMMANDS.key?(name)
89
156
  asked = HELP_FLAGS.include?(name) || name == "help"
@@ -92,12 +159,121 @@ module Hecks
92
159
  return asked ? 0 : USAGE_STATUS
93
160
  end
94
161
 
95
- if rest.length == 1 && HELP_FLAGS.include?(rest.first)
162
+ if help_asked?(name, rest)
96
163
  help(COMMANDS.fetch(name), out)
97
- else
98
- dispatch(name, rest)
164
+ return 0
165
+ end
166
+
167
+ words, problem = launcher_words(name, rest)
168
+ if problem
169
+ err.puts "hecks #{name}: #{problem}"
170
+ return 1
171
+ end
172
+
173
+ result = dispatch(name, words)
174
+ result.is_a?(Integer) ? result : 0
175
+ end
176
+
177
+ # Whether the words ask for a subcommand's usage: a lone flag for `run`, whose other words are
178
+ # the verb's own, and the flag anywhere for the rest.
179
+ # @api private
180
+ def help_asked?(name, rest)
181
+ return rest.length == 1 && HELP_FLAGS.include?(rest.first) if name == "run"
182
+
183
+ rest.any? { |word| HELP_FLAGS.include?(word) }
184
+ end
185
+
186
+ # Whether a command line is the launcher's own form of a subcommand that answers differently
187
+ # from its positional form, so the executable leaves it to the launcher.
188
+ #
189
+ # @param argv [Array<String>] the command line, subcommand first
190
+ # @return [Boolean]
191
+ def launcher_form?(argv)
192
+ LAUNCHER_ANSWERS.include?(argv.first) && argv.drop(1).any? { |word| word.match?(NAME_VALUE) }
193
+ end
194
+
195
+ # Rewrites the launcher's spellings into the subcommand's own: drops the generic flags and
196
+ # turns `name=value` words into the positionals and flags the subcommand reads.
197
+ #
198
+ # @param name [String] a key of `COMMANDS`
199
+ # @param rest [Array<String>] the words after the subcommand
200
+ # @return [Array(Array<String>, String)] the words to run with, and a refusal (nil when
201
+ # there is none)
202
+ def launcher_words(name, rest)
203
+ return [rest, nil] if UNTOUCHED.include?(name)
204
+
205
+ require_relative "doors/cli_door"
206
+ words = strip_generic(rest)
207
+ form = LAUNCHER_FORMS[name]
208
+ return [words, nil] unless form && words.any? { |word| word.match?(NAME_VALUE) }
209
+
210
+ slots, extra, refusal = bind_named_words(form, words)
211
+ return [nil, refusal] if refusal
212
+
213
+ missing = slots.first.find { |path| !File.exist?(path) } if form[:exists]
214
+ return [nil, "no such domain #{missing.inspect}"] if missing
215
+
216
+ [words.grep_v(NAME_VALUE) + extra + slots.flatten, nil]
217
+ rescue Runtime::TypeMismatch => e
218
+ [nil, e.message]
219
+ end
220
+
221
+ # Sorts the `name=value` words of `words` into the form's positional slots and its flags.
222
+ # @api private
223
+ #
224
+ # @param form [Hash] an entry of `LAUNCHER_FORMS`
225
+ # @param words [Array<String>] the words after the subcommand, generic flags removed
226
+ # @return [Array(Array<Array<String>>, Array<String>, String)] the slot contents, the extra
227
+ # flag words, and a refusal (nil when there is none)
228
+ def bind_named_words(form, words)
229
+ slots = Array.new(form[:slots].length) { [] }
230
+ extra = []
231
+ words.grep(NAME_VALUE).each do |word|
232
+ key, value = word.split("=", 2)
233
+ key = key.tr("-", "_")
234
+ slot = form[:slots].index { |names| names.include?(key) }
235
+ if slot
236
+ slots[slot].concat(form[:many] ? value.split(",") : [value])
237
+ elsif (refusal = bind_flag_or_option(form, key, value, extra))
238
+ return [nil, nil, refusal]
239
+ end
240
+ end
241
+ [slots, extra, nil]
242
+ end
243
+
244
+ # Appends the flag or option word for one `name=value` into `extra`.
245
+ # @api private
246
+ #
247
+ # @return [String, nil] a refusal when the form takes no such argument
248
+ def bind_flag_or_option(form, key, value, extra)
249
+ if Array(form[:flags]).include?(key)
250
+ extra << "--#{key.tr('_', '-')}" if Doors::CliDoor.boolean(value)
251
+ nil
252
+ elsif Array(form[:options]).include?(key)
253
+ extra.push("--#{key}", value)
254
+ nil
255
+ elsif !Array(form[:ignored]).include?(key)
256
+ known = form[:slots].flatten + Array(form[:flags]) + Array(form[:options])
257
+ "no argument #{key.inspect} — this verb takes #{known.sort.join(', ')}"
99
258
  end
100
- 0
259
+ end
260
+
261
+ # Takes `--wait` and `--confirm`, each with an optional Boolean word, out of `words`.
262
+ # @api private
263
+ def strip_generic(words)
264
+ queue = words.dup
265
+ kept = []
266
+ until queue.empty?
267
+ word = queue.shift
268
+ flag, value = word.split("=", 2)
269
+ if GENERIC_FLAGS.include?(flag)
270
+ value ||= Doors::CliDoor::BOOLEAN_WORDS.key?(queue.first.to_s.downcase) ? queue.shift : "true"
271
+ Doors::CliDoor.boolean(value)
272
+ else
273
+ kept << word
274
+ end
275
+ end
276
+ kept
101
277
  end
102
278
 
103
279
  # Runs one known subcommand with the working directory as its root.
@@ -107,36 +283,9 @@ module Hecks
107
283
  # @return [void]
108
284
  # @raise [SystemExit] when the subcommand exits the process itself
109
285
  def dispatch(name, argv)
110
- program = "hecks #{name}"
111
- case name
112
- when "run"
113
- require_relative "cli/run"
114
- Run.call(argv, program: program)
115
- when "docs", "narrate"
116
- require_relative "cli/document"
117
- Document.call(argv, projection: name.to_sym, program: program, root: Dir.pwd)
118
- when "ir"
119
- require_relative "cli/ir"
120
- Ir.call(argv, program: program)
121
- when "stores"
122
- require_relative "cli/stores"
123
- Stores.call(argv, program: program)
124
- when "model_check"
125
- require_relative "cli/model_check"
126
- ModelCheck.call(argv, program: program)
127
- when "smoke_test"
128
- require_relative "cli/smoke_test"
129
- SmokeTest.call(argv, root: Dir.pwd)
130
- when "project_diagrams"
131
- require_relative "cli/project_diagrams"
132
- ProjectDiagrams.call(argv, program: program, root: Dir.pwd)
133
- when "project_cli"
134
- require_relative "cli/project_cli"
135
- ProjectCli.call(argv, program: program, root: Dir.pwd, remove_stale_bin: false)
136
- when "mcp"
137
- require_relative "cli/mcp"
138
- Mcp.call(argv)
139
- end
286
+ command = COMMANDS.fetch(name)
287
+ require_relative command.file
288
+ command.run.call(argv, "hecks #{name}", name)
140
289
  end
141
290
 
142
291
  # @api private
@@ -0,0 +1,324 @@
1
+ require_relative "bluebook/meta_validator"
2
+ require_relative "corpus"
3
+
4
+ module Hecks
5
+ # Shared machinery for a codemod that migrates real `.bluebook` source: boot,
6
+ # find candidates, edit, then verify by diffing the re-booted IR before keeping it.
7
+ module Codemod
8
+ ROOT = File.expand_path("../..", __dir__)
9
+
10
+ EXAMPLE_ROOTS = Corpus.members(:example, root: ROOT).map(&:path)
11
+ META_FILES = (Dir.glob(File.join(ROOT, "lib/hecks/grammar/*.bluebook")) +
12
+ Dir.glob(File.join(ROOT, "lib/hecks/framework/bluebook/*.bluebook")) +
13
+ Dir.glob(File.join(ROOT, "lib/hecks/language/bluebook/**/*.bluebook"))).sort
14
+
15
+ # Boots the lightweight, in-memory path — a codemod only reads a chapter's
16
+ # own declared IR, never a stored record, so it has no reason to need Postgres.
17
+ PERSISTENCE_PORT = File.join(ROOT, "lib/hecks/ports/persistence.port")
18
+ EXTRACTION_PORT = File.join(ROOT, "lib/hecks/ports/extraction.port")
19
+ MEMORY_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/memory.adapter")
20
+ PRISM_ADAPTER = File.join(ROOT, "lib/hecks/adapters/driven/prism.adapter")
21
+
22
+ def self.export_json(registry) = Hecks::Projector::Exporter.json(registry)
23
+
24
+ # Text that stands in for a file while a dry run judges an edit.
25
+ #
26
+ # `Kernel.load` evaluates the staged text in place of the file, under the file's own path, so
27
+ # a codemod can boot the edited bluebook without writing a tracked file: a dry run that is
28
+ # interrupted, or read by a parallel process, leaves the checkout exactly as it was.
29
+ module Shadow
30
+ # Prepended to `Kernel`'s singleton: loads staged text when there is any for the file.
31
+ module Loader
32
+ # @param file [String] the path being loaded
33
+ # @return [Boolean] true, as `Kernel.load` answers
34
+ def load(file, *)
35
+ text = Shadow.texts[file.to_s]
36
+ return super unless text
37
+
38
+ Hecks::Adapters::Prism::TREES[file.to_s] = ::Prism.parse(text).value
39
+ eval(text, TOPLEVEL_BINDING.dup, file.to_s, 1)
40
+ true
41
+ end
42
+ end
43
+ Kernel.singleton_class.prepend(Loader)
44
+
45
+ module_function
46
+
47
+ # @return [Hash{String => String}] the staged text by absolute path
48
+ def texts = (@texts ||= {})
49
+
50
+ # Stages `text` as `file`'s contents and drops the file's cached parse.
51
+ #
52
+ # @param file [String] the path the text stands in for
53
+ # @param text [String] the staged source
54
+ # @return [String] the text
55
+ def put(file, text)
56
+ Hecks::Adapters::Prism.forget(file)
57
+ texts[file] = text
58
+ end
59
+
60
+ # Stops standing in for `file`.
61
+ #
62
+ # @param file [String] the path to read from disk again
63
+ # @return [void]
64
+ def drop(file)
65
+ texts.delete(file)
66
+ Hecks::Adapters::Prism.forget(file)
67
+ end
68
+
69
+ # @return [Boolean] whether any text is staged
70
+ def active? = !texts.empty?
71
+ end
72
+
73
+ # Puts `text` where `file`'s next load will find it: on disk, or (a dry run) staged in memory.
74
+ #
75
+ # @param file [String] the bluebook's path
76
+ # @param text [String] the new source
77
+ # @param dry_run [Boolean] whether to leave the file untouched
78
+ # @return [void]
79
+ def self.stage(file, text, dry_run:)
80
+ dry_run ? Shadow.put(file, text) : File.write(file, text)
81
+ end
82
+
83
+ # Undoes `stage`: rewrites the original, or stops staging.
84
+ #
85
+ # @param file [String] the bluebook's path
86
+ # @param original [String] the source as it was on disk
87
+ # @param dry_run [Boolean] whether the edit was only staged
88
+ # @return [void]
89
+ def self.unstage(file, original, dry_run:)
90
+ dry_run ? Shadow.drop(file) : File.write(file, original)
91
+ end
92
+
93
+ # Forgets each path's cached AST first — a stale tree after editing a file
94
+ # misreports that file's own source locations.
95
+ def self.load_bluebook(path)
96
+ paths = if path.is_a?(Array)
97
+ path
98
+ elsif File.directory?(path)
99
+ Dir.glob(File.join(path, "*.bluebook"))
100
+ else
101
+ [path]
102
+ end
103
+ paths.each { |file| Hecks::Adapters::Prism.forget(file) }
104
+
105
+ registry = Hecks::Runtime::Registry.new
106
+ Hecks.with_registry(registry) do
107
+ Kernel.load(PERSISTENCE_PORT)
108
+ Kernel.load(EXTRACTION_PORT)
109
+ Kernel.load(MEMORY_ADAPTER)
110
+ Kernel.load(PRISM_ADAPTER)
111
+ Hecks::Bluebook::MetaValidator.defer { paths.each { |file| Kernel.load(file) } }
112
+ Hecks::Bluebook::MetaValidator.judge_deferred!(registry)
113
+ end
114
+ registry
115
+ end
116
+
117
+ # Forgets every tree, not just one — callers rarely know which of the
118
+ # meta-domain's several files they just edited.
119
+ def self.boot_meta
120
+ Hecks::Adapters::Prism.forget_all
121
+ Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
122
+ export_json(Hecks::Bluebook::MetaValidator.grammar_registry)
123
+ end
124
+
125
+ def self.meta_registry
126
+ Hecks::Adapters::Prism.forget_all
127
+ Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil)
128
+ Hecks::Bluebook::MetaValidator.grammar_registry
129
+ end
130
+
131
+ # Walks nested entities recursively too, not just each aggregate's own commands.
132
+ def self.each_command(registry)
133
+ registry.bluebooks.each_value do |chapter|
134
+ chapter.aggregates.each do |aggregate|
135
+ walk = lambda do |construct|
136
+ construct.commands.each { |command| yield construct, command }
137
+ construct.entities.each(&walk) if construct.respond_to?(:entities)
138
+ end
139
+ walk.call(aggregate)
140
+ end
141
+ end
142
+ end
143
+
144
+ def self.owner_attribute(construct, name)
145
+ construct.attributes.find { |attr| attr.name.to_s == name.to_s }
146
+ end
147
+
148
+ # Raises on a name collision between a value object and an entity — nothing
149
+ # in the DSL prevents two from sharing a `hecks_name`, so it can't be resolved silently.
150
+ def self.element_construct_for(construct, list_field)
151
+ list_attr = owner_attribute(construct, list_field)
152
+ return nil unless list_attr&.list?
153
+
154
+ value_objects = construct.respond_to?(:value_objects) ? construct.value_objects : []
155
+ entities = construct.respond_to?(:entities) ? construct.entities : []
156
+ matches = (value_objects + entities).select { |c| c.hecks_name.to_s == list_attr.type.to_s }
157
+
158
+ if matches.size > 1
159
+ raise "#{construct.hecks_name}##{list_field} names #{list_attr.type}, held by both a value " \
160
+ "object and an entity — ambiguous, cannot resolve which one the list holds"
161
+ end
162
+
163
+ matches.first
164
+ end
165
+
166
+ # Rescues StandardError only, so a genuine bug still raises rather than
167
+ # silently counting as an unsafe candidate to revert.
168
+ def self.safely
169
+ [yield, nil]
170
+ rescue StandardError => e
171
+ [nil, "#{e.class}: #{e.message}"]
172
+ end
173
+
174
+ # Runs one codemod rule across every example domain and the meta-domain,
175
+ # verifying each candidate edit via IR diff before keeping it.
176
+ class Runner
177
+ def initialize(find_candidates:, apply_candidate:, label:)
178
+ @find_candidates = find_candidates
179
+ @apply_candidate = apply_candidate
180
+ @label = label
181
+ end
182
+
183
+ def run(dry_run: false)
184
+ results = { applied: [], skipped: [], clean: [] }
185
+ run_example_domains(results, dry_run)
186
+ run_meta_domain(results, dry_run)
187
+ results
188
+ end
189
+
190
+ def report(results, dry_run:)
191
+ puts "== results (#{dry_run ? 'DRY RUN — nothing written' : 'applied'}) =="
192
+ puts "clean (no candidates): #{results[:clean].join(', ')}" unless results[:clean].empty?
193
+ results[:applied].each { |r| puts "APPLIED #{r[:file]}: #{r[:candidates].join(', ')}" }
194
+ results[:skipped].each { |r| puts "SKIPPED #{r[:file]} (#{r[:reason]}): #{Array(r[:candidates]).join(', ')}" }
195
+ end
196
+
197
+ private
198
+
199
+ def apply_many(text, candidates)
200
+ applied = []
201
+ candidates.each do |c|
202
+ text, changed = @apply_candidate.call(text, c)
203
+ applied << c if changed
204
+ end
205
+ [text, applied]
206
+ end
207
+
208
+ # Kept as one method: the write/verify/revert sequence is a single unit,
209
+ # and splitting it would scatter shared state for no readability gain.
210
+ # rubocop:disable-next Metrics/AbcSize
211
+ # rubocop:disable-next Metrics/BlockLength
212
+ # rubocop:disable-next Metrics/CyclomaticComplexity
213
+ # rubocop:disable-next Metrics/MethodLength
214
+ # rubocop:disable-next Metrics/PerceivedComplexity
215
+ def run_example_domains(results, dry_run)
216
+ Codemod::EXAMPLE_ROOTS.each do |domain_dir|
217
+ bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
218
+ next if bluebook_files.empty?
219
+
220
+ before_json = Codemod.export_json(Codemod.load_bluebook(bluebook_files))
221
+ candidates = @find_candidates.call(Codemod.load_bluebook(bluebook_files))
222
+
223
+ if candidates.empty?
224
+ results[:clean] << domain_dir
225
+ next
226
+ end
227
+
228
+ live = bluebook_files.to_h { |file| [file, File.read(file)] }
229
+ applied_by_file = Hash.new { |hash, file| hash[file] = [] }
230
+
231
+ candidates.each do |candidate|
232
+ target_file = bluebook_files.find { |file| apply_many(live[file], [candidate]).last.any? }
233
+ unless target_file
234
+ results[:skipped] << { file: domain_dir, reason: "no matching source line", candidates: [@label.call(candidate)] }
235
+ next
236
+ end
237
+
238
+ original_text = live[target_file]
239
+ text, = apply_many(original_text, [candidate])
240
+ live[target_file] = text
241
+ Codemod.stage(target_file, text, dry_run: dry_run)
242
+ after_json, error = Codemod.safely do
243
+ Codemod.export_json(Codemod.load_bluebook(bluebook_files))
244
+ end
245
+
246
+ if after_json == before_json && !dry_run
247
+ applied_by_file[target_file] << candidate
248
+ elsif after_json == before_json
249
+ live[target_file] = original_text
250
+ Codemod.unstage(target_file, original_text, dry_run: dry_run)
251
+ Codemod.safely { Codemod.load_bluebook(bluebook_files) }
252
+ applied_by_file[target_file] << candidate
253
+ else
254
+ live[target_file] = original_text
255
+ Codemod.unstage(target_file, original_text, dry_run: dry_run)
256
+ Codemod.safely { Codemod.load_bluebook(bluebook_files) }
257
+ reason = error ? "reboot raised after edit (#{error}) — reverted" : "IR changed after edit — reverted"
258
+ results[:skipped] << { file: target_file, reason: reason, candidates: [@label.call(candidate)] }
259
+ end
260
+ end
261
+
262
+ applied_by_file.each do |file, applied|
263
+ results[:applied] << { file: file, candidates: applied.map(&@label) }
264
+ end
265
+ end
266
+ end
267
+
268
+ # Per-candidate, not batched — the meta-domain is one shared,
269
+ # self-dispatching registry (S14), so a single unsafe candidate could
270
+ # otherwise sink every other, genuinely safe one in the same run.
271
+ # rubocop:disable-next Metrics/AbcSize
272
+ # rubocop:disable-next Metrics/CyclomaticComplexity
273
+ # rubocop:disable-next Metrics/PerceivedComplexity
274
+ def run_meta_domain(results, dry_run)
275
+ before_meta = Codemod.boot_meta
276
+ meta_candidates = @find_candidates.call(Codemod.meta_registry)
277
+
278
+ if meta_candidates.empty?
279
+ results[:clean] << "meta-domain"
280
+ return
281
+ end
282
+
283
+ live_meta = Codemod::META_FILES.to_h { |f| [f, File.read(f)] }
284
+ applied_by_file = Hash.new { |h, k| h[k] = [] }
285
+
286
+ meta_candidates.each do |candidate|
287
+ target_file = Codemod::META_FILES.find { |f| apply_many(live_meta[f], [candidate]).last.any? }
288
+ unless target_file
289
+ results[:skipped] << { file: "meta-domain", reason: "no matching source line", candidates: [@label.call(candidate)] }
290
+ next
291
+ end
292
+
293
+ text, = apply_many(live_meta[target_file], [candidate])
294
+ original_text = live_meta[target_file]
295
+ live_meta[target_file] = text
296
+ Codemod::META_FILES.each { |f| Codemod.stage(f, live_meta[f], dry_run: dry_run) }
297
+
298
+ # A dry run stages the text in memory and reboots from it, then drops it — so the next
299
+ # candidate is judged against the true original state and no file is written.
300
+ after_meta, error = Codemod.safely { Codemod.boot_meta }
301
+
302
+ if after_meta == before_meta && !dry_run
303
+ applied_by_file[target_file] << candidate
304
+ elsif after_meta == before_meta # dry-run, safe — revert, but count as applied
305
+ live_meta[target_file] = original_text
306
+ Codemod::META_FILES.each { |f| Codemod.unstage(f, live_meta[f], dry_run: dry_run) }
307
+ Codemod.safely { Codemod.boot_meta }
308
+ applied_by_file[target_file] << candidate
309
+ else
310
+ live_meta[target_file] = original_text
311
+ Codemod::META_FILES.each { |f| Codemod.unstage(f, live_meta[f], dry_run: dry_run) }
312
+ Codemod.safely { Codemod.boot_meta } # resync memoized state to the reverted text
313
+ reason = error ? "reboot raised (#{error})" : "IR changed"
314
+ results[:skipped] << { file: "meta-domain", reason: reason, candidates: [@label.call(candidate)] }
315
+ end
316
+ end
317
+
318
+ applied_by_file.each do |file, candidates|
319
+ results[:applied] << { file: file, candidates: candidates.map(&@label) }
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end