hecks 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,336 @@
1
+ module Hecks
2
+ module Grammar
3
+ # File surgery under `hecks word_status` and its siblings: reads and rewrites the
4
+ # aggregate-local KeywordSeed/ArgumentSeed rows as text, preserving the table's own formatting.
5
+ module Evolve
6
+ class Refusal < StandardError; end
7
+
8
+ module_function
9
+
10
+ # Only consumes the next argv element as the value when it isn't itself
11
+ # a flag, so `--foo --bar` doesn't swallow `--bar` as `--foo`'s value.
12
+ def option(argv, name, default = nil)
13
+ index = argv.index("--#{name}")
14
+ return default unless index
15
+
16
+ value = argv[index + 1]
17
+ value.nil? || value.start_with?("-") ? default : value
18
+ end
19
+
20
+ # Runs the block against an in-memory copy of the tables: every edit it makes is held, none
21
+ # is written, and later reads see the earlier edits.
22
+ #
23
+ # @yield the edits to rehearse
24
+ # @return [Hash{String => String}] each file the edits changed, with the text it would hold
25
+ def rehearse
26
+ @overlay = {}
27
+ yield
28
+ @overlay.dup
29
+ ensure
30
+ @overlay = nil
31
+ end
32
+
33
+ # @param path [String] a table file
34
+ # @return [String] its text: a rehearsed edit if one was made, else what the file holds
35
+ def read_source(path)
36
+ @overlay&.key?(path) ? @overlay[path] : File.read(path)
37
+ end
38
+
39
+ # @param path [String] a table file
40
+ # @param text [String] what it holds afterwards; held, not written, while rehearsing
41
+ # @return [Object] the text
42
+ def write_source(path, text)
43
+ @overlay ? (@overlay[path] = text) : File.write(path, text)
44
+ end
45
+
46
+ # Snapshots `paths` before running the block and restores every file
47
+ # if it raises, even partway through a multi-file write.
48
+ def restore_on_raise(paths)
49
+ snapshots = paths.to_h { |path| [path, File.read(path)] }
50
+ yield
51
+ rescue StandardError
52
+ snapshots.each { |path, content| File.write(path, content) }
53
+ raise
54
+ end
55
+
56
+ # Every syntax-table file declaring a KeywordSeed or ArgumentSeed value object.
57
+ def syntax_paths
58
+ Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
59
+ source = read_source(path)
60
+ source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
61
+ end
62
+ end
63
+
64
+ # Narrow compatibility door for single-file callers; syntax_paths is normal.
65
+ def syntax_path = syntax_paths.first
66
+
67
+ # Resolves the file(s) a call should search or write.
68
+ def paths_for(path) = path ? Array(path) : syntax_paths
69
+
70
+ # Parses the Keyword one_of's member rows leniently off the text.
71
+ def keyword_rows(path = nil)
72
+ paths_for(path).flat_map do |candidate|
73
+ blocks = seed_blocks(read_source(candidate), "KeywordSeed")
74
+ raise Refusal, "source declares no KeywordSeed value object" if path && blocks.empty?
75
+
76
+ blocks.flat_map do |block|
77
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
78
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
79
+ { word: row["word"], context: row["context"],
80
+ status: row.fetch("status", "admitted"), was: row["was"] }
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Declares a new, proposed keyword row in the syntax table that owns
87
+ # `context` (or the aggregate `opens` names, for a File-context word).
88
+ def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
89
+ if keyword_rows(path).any? do |row|
90
+ row[:word] == word && row[:context] == context
91
+ end
92
+ raise Refusal,
93
+ "#{context}.#{word} is already declared — one row per (word, context, form)"
94
+ end
95
+
96
+ path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
97
+ source = read_source(path)
98
+ block = keyword_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || keyword_block(source)
99
+ indent = block[/^(\s*)member /, 1] || " "
100
+ row = %(#{indent}member word: "#{word}", context: "#{context}", body: "#{body}", ) +
101
+ %(inner: "#{inner}", opens: "#{opens}", fills: "#{fills}", status: "proposed"\n)
102
+
103
+ # Appended at the end; whoever admits it may move it to its group.
104
+ closing = block.rindex(/^\s*end\s*$/)
105
+ updated = block[0...closing] + row + block[closing..]
106
+ write_source(path, source.sub(block, updated))
107
+ end
108
+
109
+ # Rewrites a declared keyword row's status: cell in place.
110
+ def set_status(word:, context:, to:, path: nil)
111
+ raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated
112
+ retired].include?(to)
113
+
114
+ path = path_holding_keyword(word, context, paths_for(path))
115
+ source = read_source(path)
116
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
117
+ rows = block.lines.select { |line| member_row?(line, word, context) }
118
+ raise Refusal, "#{context}.#{word} is not declared" if rows.empty?
119
+
120
+ updated = block.lines.map do |line|
121
+ next line unless member_row?(line, word, context)
122
+
123
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
124
+ # Admitted is the default and stays unspelled in the row.
125
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
126
+ end.join
127
+
128
+ write_source(path, source.sub(block, updated))
129
+ end
130
+
131
+ # Respells a keyword row (was: holds the old spelling) — one hop only;
132
+ # its Argument rows follow, row-aware (see cascade_argument_rename).
133
+ def rename(word:, context:, to:, path: nil)
134
+ row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
135
+ raise Refusal, "#{context}.#{word} is not declared" unless row
136
+ raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
137
+ if keyword_rows(path).any? do |r|
138
+ r[:word] == to && r[:context] == context
139
+ end
140
+ raise Refusal,
141
+ "#{context}.#{to} is already declared — a rename cannot land on a living word"
142
+ end
143
+
144
+ paths = paths_for(path)
145
+ path = path_holding_keyword(word, context, paths)
146
+ source = read_source(path)
147
+ block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
148
+ updated = block.lines.map do |line|
149
+ next line unless member_row?(line, word, context)
150
+
151
+ line.sub(%(word: "#{word}"), %(word: "#{to}"))
152
+ .sub(/\n\z/, %(, was: "#{word}"\n))
153
+ end.join
154
+ source = source.sub(block, updated)
155
+ write_source(path, source)
156
+
157
+ cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
158
+ end
159
+
160
+ # Tells whether `line` is a KeywordSeed member row for (word, context).
161
+ def member_row?(line, word, context)
162
+ line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
163
+ end
164
+
165
+ # Finds which syntax-table path declares a keyword row.
166
+ def path_holding_keyword(word, context, paths = syntax_paths)
167
+ paths.find do |candidate|
168
+ keyword_blocks(read_source(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
169
+ end || raise(Refusal, "#{context}.#{word} is not declared")
170
+ end
171
+
172
+ # Finds which syntax-table path declares an argument row.
173
+ def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
174
+ paths.find do |candidate|
175
+ argument_blocks(read_source(candidate)).any? do |block|
176
+ block.lines.any? do |line|
177
+ argument_row?(line, keyword, context, at, named)
178
+ end
179
+ end
180
+ end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
181
+ end
182
+
183
+ # Picks the owning file for a new row; opens names the aggregate for a File-context word.
184
+ def owner_path(context:, word:, opens: "", paths: syntax_paths)
185
+ if context == "File" && !opens.to_s.empty?
186
+ aggregate_path = paths.find { |candidate| read_source(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
187
+ return aggregate_path if aggregate_path
188
+ end
189
+
190
+ paths.find do |candidate|
191
+ source = read_source(candidate)
192
+ %w[KeywordSeed ArgumentSeed].any? do |seed|
193
+ seed_blocks(source, seed).any? { |block| block.include?(%(context: "#{context}")) }
194
+ end
195
+ end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
196
+ end
197
+
198
+ # Every value_object "<name>" block's full source text, opener to closing end.
199
+ def seed_blocks(source, name)
200
+ opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
201
+ source.to_enum(:scan, opener).map do
202
+ match = Regexp.last_match
203
+ start = match.begin(0)
204
+ indent = match[1]
205
+ closing = source.index(/^#{Regexp.escape(indent)}end\s*$/, match.end(0))
206
+ closing = source.index(/\n/, closing) + 1
207
+ source[start...closing]
208
+ end
209
+ end
210
+
211
+ # The first value_object "<name>" block's source text.
212
+ def seed_block(source, name, required: false)
213
+ block = seed_blocks(source, name).first
214
+ unless block
215
+ raise Refusal, "source declares no #{name} value object" if required
216
+
217
+ return
218
+ end
219
+ block
220
+ end
221
+
222
+ # First `end` after the opener is the block's own — member rows sit bare (ADR 0025).
223
+ def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
224
+
225
+ # The first KeywordSeed block's source text.
226
+ def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
227
+
228
+ # Parses the ArgumentSeed's member rows leniently off the text.
229
+ def argument_rows(path = nil)
230
+ paths_for(path).flat_map do |candidate|
231
+ blocks = seed_blocks(read_source(candidate), "ArgumentSeed")
232
+ raise Refusal, "source declares no ArgumentSeed value object" if path && blocks.empty?
233
+
234
+ blocks.flat_map do |block|
235
+ block.scan(/^\s*member (.+)$/).map do |(cells)|
236
+ row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
237
+ { keyword: row["keyword"], context: row["context"], at: row["at"].to_s,
238
+ named: row["named"].to_s, kind: row["kind"], required: row["required"],
239
+ fills: row["fills"].to_s, status: row.fetch("status", "admitted") }
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ # Declares a new, proposed argument row; pairs_shape marks a `pairs`
246
+ # argument that fills a whole key/value list, not a single field.
247
+ def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
248
+ pairs_shape: nil, path: nil)
249
+ if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
250
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is " \
251
+ "already declared — one row per (keyword, context, at, named)"
252
+ end
253
+
254
+ path = owner_path(context: context, word: keyword, paths: paths_for(path))
255
+ source = read_source(path)
256
+ block = argument_blocks(source).find do |candidate|
257
+ candidate.include?(%(context: "#{context}"))
258
+ end || argument_block(source)
259
+ indent = block[/^(\s*)member /, 1] || " "
260
+ shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
261
+ row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
262
+ %(named: "#{named}", kind: "#{kind}", required: "#{required}", fills: "#{fills}", ) +
263
+ shape + %(status: "proposed"\n)
264
+
265
+ closing = block.rindex(/^\s*end\s*$/)
266
+ updated = block[0...closing] + row + block[closing..]
267
+ write_source(path, source.sub(block, updated))
268
+ end
269
+
270
+ # Rewrites a declared argument row's status: cell in place.
271
+ def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
272
+ raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated
273
+ retired].include?(to)
274
+
275
+ path = path_holding_argument(keyword, context, at, named, paths_for(path))
276
+ source = read_source(path)
277
+ block = argument_blocks(source).find do |candidate|
278
+ candidate.lines.any? do |line|
279
+ argument_row?(line, keyword, context, at, named)
280
+ end
281
+ end
282
+ rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
283
+ if rows.empty?
284
+ raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
285
+ "declared"
286
+ end
287
+
288
+ updated = block.lines.map do |line|
289
+ next line unless argument_row?(line, keyword, context, at, named)
290
+
291
+ stripped = line.sub(/,\s*status: "[^"]*"/, "")
292
+ to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
293
+ end.join
294
+
295
+ write_source(path, source.sub(block, updated))
296
+ end
297
+
298
+ # Tells whether `line` is an ArgumentSeed row for this (keyword, context, at, named) tuple.
299
+ def argument_row?(line, keyword, context, at, named)
300
+ line =~ /^\s*member / &&
301
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
302
+ line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
303
+ end
304
+
305
+ # The (keyword, context, at, named) identity tuple an argument row is keyed
306
+ # by — wider than a Keyword row's two-field key.
307
+ def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
308
+
309
+ # Row-aware rename cascade — updates only the argument rows that belong
310
+ # to the renamed keyword, not a blind gsub across the file.
311
+ def cascade_argument_rename(keyword:, context:, to:, path: nil)
312
+ paths_for(path).each do |candidate|
313
+ source = read_source(candidate)
314
+ original = source
315
+ argument_blocks(source).each do |block|
316
+ updated = block.lines.map do |line|
317
+ next line unless line =~ /^\s*member / &&
318
+ line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}"))
319
+
320
+ line.sub(%(keyword: "#{keyword}"), %(keyword: "#{to}"))
321
+ end.join
322
+ source = source.sub(block, updated) if updated != block
323
+ end
324
+ write_source(candidate, source) if source != original
325
+ end
326
+ end
327
+
328
+ # ArgumentSeed blocks; see keyword_blocks for why the first bare end
329
+ # after the opener is already the block's own.
330
+ def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
331
+
332
+ # The first ArgumentSeed block's source text.
333
+ def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree"
4
+ require_relative "ruby_child"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ module Codebase
9
+ # What Codebase's `CodemodRun` asks of the working tree: the mechanical rewrites of the
10
+ # example bluebooks.
11
+ #
12
+ # Each codemod is a script built on `Hecks::Codemod` (`lib/hecks/codemod.rb`), which reboots
13
+ # every bluebook it edits and puts the file back when the rewrite changes what the bluebook
14
+ # means. The script runs in a child process from the checkout's root. Unconfirmed it runs
15
+ # with `--dry-run`, which restores every file it touched, so tracked files stay changed only
16
+ # when the request is confirmed.
17
+ module Codemods
18
+ # Every operation this family carries out.
19
+ OPERATIONS = %w[hoist_local_givens drop_implicit_append_fields].freeze
20
+
21
+ # The script each operation runs.
22
+ SCRIPTS = { "hoist_local_givens" => "codemod_hoist_local_givens",
23
+ "drop_implicit_append_fields" => "codemod_implicit_append_fields" }.freeze
24
+
25
+ module_function
26
+
27
+ # Carries out one codemod.
28
+ #
29
+ # @param operation [String] one of `OPERATIONS`
30
+ # @param held [Hash] the `CodemodRun` record's fields: `confirm`
31
+ # @param tree [Tree] the working tree, already known to be a hecks checkout
32
+ # @param shell [#capture, nil] unused: the codemod runs in this process
33
+ # @return [String] what the script found for each example, and whether it rewrote it
34
+ # @raise [ConsoleCapture::Failure] when the script ends badly
35
+ def call(operation, held, tree, shell: nil)
36
+ confirm = held[:confirm].is_a?(Hash) ? held[:confirm][:value] : held[:confirm]
37
+ child = RubyChild.new(tree)
38
+ return child.answer(SCRIPTS.fetch(operation)) if confirm == true
39
+
40
+ "dry run, nothing kept (add --confirm to rewrite):\n#{child.answer(SCRIPTS.fetch(operation), '--dry-run')}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree"
4
+ require_relative "ruby_child"
5
+ require "hecks/engine_agreement"
6
+ require "hecks/doc/reference"
7
+
8
+ module Hecks
9
+ module Adapters
10
+ module Codebase
11
+ # What Codebase's `ConformanceRun` asks of the working tree: the checks on the language
12
+ # itself.
13
+ #
14
+ # The query-engine check and the reference-docs check read the tree in this process, with the
15
+ # code the verbs run elsewhere, and write nothing. The argument-gate matrix boots several
16
+ # throwaway domains and replays them, so it runs in a child process; without `confirm` the
17
+ # script only reports, and with it the script rewrites the committed matrix and its fixtures.
18
+ module Conformance
19
+ # Every operation this family carries out.
20
+ OPERATIONS = %w[check_engine_agreement measure_doc_coverage argument_gate_matrix].freeze
21
+
22
+ module_function
23
+
24
+ # Carries out one operation.
25
+ #
26
+ # @param operation [String] one of `OPERATIONS`
27
+ # @param held [Hash] the `ConformanceRun` record's fields
28
+ # @param tree [Tree] the working tree, already known to be a hecks checkout
29
+ # @return [String] what the check found
30
+ # @raise [ConsoleCapture::Failure] when the check finds a disagreement
31
+ def call(operation, held, tree, shell: nil)
32
+ case operation
33
+ when "check_engine_agreement" then engine_agreement(tree)
34
+ when "measure_doc_coverage" then doc_coverage(tree)
35
+ else gate_matrix(held, tree)
36
+ end
37
+ end
38
+
39
+ # @param tree [Tree] the checkout
40
+ # @return [String] the clean verdict
41
+ # @raise [ConsoleCapture::Failure] with every problem, when the engines disagree
42
+ def engine_agreement(tree)
43
+ finding = EngineAgreement.check(root: tree.root)
44
+ return EngineAgreement.clean_line(finding) if finding.problems.empty?
45
+
46
+ raise ConsoleCapture::Failure, EngineAgreement.problem_lines(finding)
47
+ end
48
+
49
+ # @param tree [Tree] the checkout
50
+ # @return [String] the clean verdict
51
+ # @raise [ConsoleCapture::Failure] with each word that lacks prose or an example
52
+ def doc_coverage(tree)
53
+ clean, report = Doc::Reference.coverage_report(tree.path("docs/implemented/reference"))
54
+ raise ConsoleCapture::Failure, report unless clean
55
+
56
+ report
57
+ end
58
+
59
+ # @param held [Hash] the record's fields; `confirm` makes the script write
60
+ # @param tree [Tree] the checkout
61
+ # @return [String] what the script printed: the rows kept and dropped, and what it wrote
62
+ # @raise [ConsoleCapture::Failure] when the script ends badly
63
+ def gate_matrix(held, tree)
64
+ confirm = held[:confirm].is_a?(Hash) ? held[:confirm][:value] : held[:confirm]
65
+ child = RubyChild.new(tree)
66
+ child.answer("argument_gate_matrix", *("--write" if confirm == true))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hecks/corpus"
4
+ require "hecks/query_ir"
5
+ require "hecks/query_ir_mcp"
6
+ require_relative "tree"
7
+ require "hecks/rust_build"
8
+ require_relative "../console_capture"
9
+
10
+ module Hecks
11
+ module Adapters
12
+ module Codebase
13
+ # What Codebase's `CorpusRun` asks of the working tree: which committed domains have a Rust
14
+ # feature, what the language's IR holds, and the two doors that serve it.
15
+ #
16
+ # The questions are pure reads and run in this process, with the code `hecks rust_domains` and
17
+ # `hecks ir_constructs` run (`Hecks::Corpus`, `Hecks::QueryIR`); the coverage question runs
18
+ # `Hecks::RustBuild`'s coverage tool over each generated module, also in this process. The
19
+ # two doors run until they are closed: the query MCP server on stdio, and the forms app on a
20
+ # local port.
21
+ module CorpusTasks
22
+ # Every operation this family carries out.
23
+ OPERATIONS = %w[serve_query_ir_mcp present].freeze
24
+
25
+ # The port `present` listens on when none is named.
26
+ DEFAULT_PORT = 4567
27
+
28
+ class << self
29
+ # @return [#call, nil] serves the MCP door; `Hecks::QueryIrMcp.start` when nil. A spec
30
+ # replaces it so no stdio has to be held.
31
+ attr_accessor :mcp_server
32
+
33
+ # @return [#call, nil] takes `app:` and `port:` and serves until stopped; a WEBrick
34
+ # server through Rack when nil.
35
+ attr_accessor :web_server
36
+
37
+ # @return [#capture, nil] runs each coverage check, as `capture(tool, argv, env:)`
38
+ # answering an object with `out`, `err` and `ok?`; `Hecks::RustBuild` when nil.
39
+ attr_accessor :coverage_runner
40
+ end
41
+
42
+ module_function
43
+
44
+ # Opens one of the doors.
45
+ #
46
+ # @param operation [String] `serve_query_ir_mcp` or `present`
47
+ # @param held [Hash] the `CorpusRun` record's fields: `port`
48
+ # @param tree [Tree] the working tree, already known to be a hecks checkout
49
+ # @param shell [#capture, nil] unused
50
+ # @return [String] a note that the door closed
51
+ # @raise [ConsoleCapture::Failure] when the MCP door refuses the process's setup
52
+ def call(operation, held, tree, shell: nil)
53
+ _ = shell
54
+ return serve_mcp if operation == "serve_query_ir_mcp"
55
+
56
+ present(plain(held)[:port] || DEFAULT_PORT, tree)
57
+ end
58
+
59
+ # A pure read.
60
+ #
61
+ # @param operation [String] a query's name in snake case
62
+ # @param args [Hash] the query's plain arguments
63
+ # @param tree [Tree] the checkout
64
+ # @param shell [#capture, nil] unused; the coverage checks run in this process
65
+ # @return [String] the answer, one line for each row
66
+ # @raise [ConsoleCapture::Failure] when a coverage check fails, or a query is refused
67
+ def report(operation, args, tree, shell: nil)
68
+ _ = shell
69
+ case operation
70
+ when "rust_domains" then rust_domains(tree)
71
+ when "regen_order" then regen_order(tree)
72
+ when "corpus_rust_coverage" then coverage(tree)
73
+ else ir_query(operation, args)
74
+ end
75
+ end
76
+
77
+ # @param held [Hash] fields, each perhaps a value object's `{ value: x }`
78
+ # @return [Hash] the same fields as plain values
79
+ def plain(held) = held.transform_values { |value| value.is_a?(Hash) ? value[:value] : value }
80
+
81
+ # @return [String] a note that the MCP door closed
82
+ # @raise [ConsoleCapture::Failure] when the door refuses to start
83
+ def serve_mcp
84
+ server = CorpusTasks.mcp_server || ->(**options) { QueryIrMcp.start(**options) }
85
+ server.call(argv: [])
86
+ "query ir mcp door closed"
87
+ rescue SystemExit => e
88
+ raise ConsoleCapture::Failure, "the query ir mcp door refused to start (status #{e.status}); see stderr"
89
+ end
90
+
91
+ # @param port [Integer] the port to listen on
92
+ # @param tree [Tree] the checkout
93
+ # @return [String] a note that the server stopped
94
+ def present(port, tree)
95
+ require "hecks/forms/banking_presentation"
96
+ app = Forms::BankingPresentation.app(root: tree.root)
97
+ warn "banking forms, in memory, on http://localhost:#{port}/ - no authentication, no CSRF " \
98
+ "protection, no caller identity; do not expose beyond localhost"
99
+ (CorpusTasks.web_server || method(:rack_serve)).call(app: app, port: port)
100
+ "presented on port #{port}, now stopped"
101
+ end
102
+
103
+ # @param app [#call] the Rack app
104
+ # @param port [Integer] the port
105
+ # @return [void]
106
+ def rack_serve(app:, port:)
107
+ require "rackup"
108
+ Rackup::Server.start(app: app, Port: port, server: "webrick")
109
+ end
110
+
111
+ # @param tree [Tree] the checkout
112
+ # @return [String] each domain with a Rust feature: its feature, a tab, and its directory
113
+ def rust_domains(tree)
114
+ Corpus.rust_domains(root: tree.root).map { |domain| "#{domain.feature}\t#{tree.relative(domain.dir)}" }
115
+ .join("\n")
116
+ end
117
+
118
+ # @param tree [Tree] the checkout
119
+ # @return [String] the directories regeneration walks, in order, one for each line
120
+ def regen_order(tree)
121
+ Corpus.rust_regen_order(root: tree.root).map { |domain| tree.relative(domain.dir) }.join("\n")
122
+ end
123
+
124
+ # Runs the coverage tool over every generated module. A module named in
125
+ # `Corpus::RUST_COVERAGE_PENDING` must still fail; every other one must pass.
126
+ #
127
+ # @param tree [Tree] the checkout
128
+ # @return [String] one line for each module, and how many were checked
129
+ # @raise [ConsoleCapture::Failure] with every module that failed, or that passes now while
130
+ # still listed as pending
131
+ def coverage(tree)
132
+ modules = Corpus.generated_modules(root: tree.root)
133
+ pending = Corpus::RUST_COVERAGE_PENDING
134
+ unknown = pending.keys - modules
135
+ raise ConsoleCapture::Failure, "pending names #{unknown.join(', ')}, with no generated module" if unknown.any?
136
+
137
+ lines, problems = judge(modules, pending, coverage_results(modules, tree))
138
+ raise ConsoleCapture::Failure, [*lines, "", *problems].join("\n") if problems.any?
139
+
140
+ [*lines, "#{modules.size} generated modules checked"].join("\n")
141
+ end
142
+
143
+ # @param modules [Array<String>] the generated modules
144
+ # @param pending [Hash{String => String}] the modules known to fail, and why
145
+ # @param results [Hash{String => Array}] each module's `[passed, output]`
146
+ # @return [Array(Array<String>, Array<String>)] the report lines and the problems
147
+ def judge(modules, pending, results)
148
+ problems = []
149
+ lines = modules.map do |name|
150
+ passed, output = results.fetch(name)
151
+ if pending.key?(name)
152
+ problems << "#{name} passes now - delete it from Hecks::Corpus::RUST_COVERAGE_PENDING" if passed
153
+ "#{name}: pending (#{passed ? 'NOW PASSES' : 'still fails'}) - #{pending[name]}"
154
+ else
155
+ problems << "#{name} failed:\n#{output}" unless passed
156
+ "#{name}: #{passed ? 'ok' : 'FAILED'}"
157
+ end
158
+ end
159
+ [lines, problems]
160
+ end
161
+
162
+ # @param modules [Array<String>] the generated modules
163
+ # @param tree [Tree] the checkout
164
+ # @return [Hash{String => Array}] each module's `[passed, output]`
165
+ def coverage_results(modules, tree)
166
+ runner = CorpusTasks.coverage_runner || RustBuild
167
+ env = { "HECKS_RUST_DIR" => tree.path("rust") }
168
+ modules.to_h do |name|
169
+ result = runner.capture("rust_coverage", [name], env: env)
170
+ [name, [result.ok?, [result.out, result.err].join]]
171
+ end
172
+ end
173
+
174
+ # @param operation [String] `ir_constructs`, `ir_duplicates` or `ir_impact`
175
+ # @param args [Hash] the query's plain arguments
176
+ # @return [String] the formatted answer
177
+ # @raise [ConsoleCapture::Failure] when the query refuses its arguments
178
+ def ir_query(operation, args)
179
+ case operation
180
+ when "ir_constructs" then QueryIR.format_constructs(QueryIR.constructs(words(args[:names])))
181
+ when "ir_duplicates" then duplicates(args)
182
+ else QueryIR.format_impact_preview(QueryIR.impact_preview(args[:name], args[:field]))
183
+ end
184
+ rescue ArgumentError => e
185
+ raise ConsoleCapture::Failure, e.message
186
+ end
187
+
188
+ # @param args [Hash] `domains` and `meta`
189
+ # @return [String] the duplicated rules; the meta-domain is included when asked for, or
190
+ # when no domain is named
191
+ def duplicates(args)
192
+ domains = args[:domains] ? words(args[:domains]) : nil
193
+ QueryIR.format_duplicates(QueryIR.duplicates(domains: domains, include_meta: args[:meta] == true || domains.nil?))
194
+ end
195
+
196
+ # @param list [String, nil] a comma-separated list
197
+ # @return [Array<String>] its items
198
+ def words(list) = list.to_s.split(",").map(&:strip).reject(&:empty?)
199
+ end
200
+ end
201
+ end
202
+ end