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,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+ require_relative "../../../hecks"
7
+ require_relative "../../fuzzing/combination_miner"
8
+ require_relative "../../fuzzing/domain_generator"
9
+ require_relative "../adapters/agent"
10
+ require_relative "child"
11
+ require_relative "../../cache_dir"
12
+ require_relative "../../hecks/adapters/codebase/tree"
13
+
14
+ module Hecks
15
+ module QualityControlCli
16
+ # The command behind `hecks quality_control mine_combinations`: asks an agent to mine the
17
+ # adversarial corpus for new domain combinations, then checks them. It is opt-in and never run
18
+ # by `hecks quality_control tick` (an agent call costs money and answers differently each run).
19
+ # `--brief` prints the agent's prompt and stops; `--from <dir>` re-checks an earlier run;
20
+ # `--against <domain>` narrows the census; `--agent "cmd"` reads the prompt on stdin.
21
+ #
22
+ # The agent defaults to `claude -p` (`QA_MINER_AGENT` overrides); candidates that boot are
23
+ # checked by `hecks quality_control check_generated_domains --source`, whose report is this
24
+ # command's report. Exit 0: all clean. 2: a finding (see `.claude/skills/hecks_qa/SKILL.md`). 1:
25
+ # an operational
26
+ # error.
27
+ class QaMineCombinations
28
+ EXIT_OK = 0
29
+ EXIT_ERROR = 1
30
+ RESULT_MARKER = "QA_GENERATED_RESULT "
31
+
32
+ USAGE = "usage: hecks quality_control mine_combinations [--candidates N] [--rust] [--seeds K] " \
33
+ "[--steps M] " \
34
+ "[--adversarial F] [--repair-rounds R] [--agent CMD] [--from <candidates-dir>] " \
35
+ "[--against <domain> …] [--brief]"
36
+
37
+ Miner = Hecks::Fuzzing::CombinationMiner
38
+
39
+ # Mines and checks the candidates.
40
+ #
41
+ # @param argv [Array<String>] the flags in the usage line
42
+ # @param root [String] the repository root
43
+ # @return [Integer] 0 all clean, 2 a finding, 1 an operational error
44
+ # @raise [SystemExit] on a bad argument, an agent that fails or writes nothing, or no
45
+ # candidate that boots
46
+ def self.call(argv, root:)
47
+ new(root: root).call(argv)
48
+ end
49
+
50
+ # @param root [String] the repository root
51
+ def initialize(root:)
52
+ @root = root
53
+ end
54
+
55
+ # @param argv [Array<String>] the flags in the usage line
56
+ # @return [Integer] the exit status
57
+ # @raise [SystemExit] on a bad argument, an agent that fails or writes nothing, or no
58
+ # candidate that boots
59
+ def call(argv)
60
+ @options = parse(argv.dup)
61
+ return EXIT_OK if @options == :help
62
+
63
+ @run_dir = File.join(runs_root, "run-#{Time.now.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}")
64
+ @out_dir = @options[:from] || File.join(@run_dir, "candidates")
65
+ @agent_log = File.join(@run_dir, "agent.log")
66
+ @agent = Hecks::Adapters::Agent.new
67
+ @command = @agent.command_for(@options[:agent])
68
+ corpus = @options[:against].empty? ? Miner.corpus_paths(@root) : @options[:against]
69
+ @brief = Miner.brief(corpus, root: @root, bug_titles: Miner.recent_bug_titles(@root))
70
+ prompt = Miner.prompt(@root, @brief, count: @options[:candidates], out_dir: @out_dir)
71
+ if @options[:brief]
72
+ puts prompt
73
+ return EXIT_OK
74
+ end
75
+
76
+ mine(prompt)
77
+ end
78
+
79
+ # Each flag that takes a value: the option it sets and how the value is read.
80
+ VALUE_FLAGS = {
81
+ "--candidates" => [:candidates, ->(v) { Integer(v) }], "--seeds" => [:seeds, ->(v) { Integer(v) }],
82
+ "--steps" => [:steps, ->(v) { Integer(v) }], "--adversarial" => [:adversarial, ->(v) { Float(v) }],
83
+ "--repair-rounds" => [:repair_rounds, ->(v) { Integer(v) }], "--agent" => [:agent, :itself.to_proc],
84
+ "--from" => [:from, ->(v) { File.expand_path(v) }]
85
+ }.freeze
86
+
87
+ private
88
+
89
+ def parse(argv)
90
+ options = { candidates: 3, rust: false, seeds: 5, steps: 25, adversarial: 0.3, repair_rounds: 1,
91
+ agent: nil, from: nil, brief: false, against: [] }
92
+ until argv.empty?
93
+ arg = argv.shift
94
+ if %w[-h --help].include?(arg)
95
+ puts USAGE
96
+ return :help
97
+ end
98
+ parse_flag(options, arg, argv)
99
+ end
100
+ options
101
+ end
102
+
103
+ def parse_flag(options, arg, argv)
104
+ case arg
105
+ when *VALUE_FLAGS.keys
106
+ key, reader = VALUE_FLAGS.fetch(arg)
107
+ options[key] = reader.call(argv.shift)
108
+ when "--rust" then options[:rust] = true
109
+ when "--brief" then options[:brief] = true
110
+ when "--against" then options[:against] << File.expand_path(argv.shift)
111
+ else abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
112
+ end
113
+ end
114
+
115
+ def relative(path) = path.delete_prefix("#{@root}/")
116
+
117
+ # Runs are kept under the checkout's `tmp/`; an installed gem, or a root that cannot be
118
+ # written, keeps them under the cache root instead.
119
+ def runs_root
120
+ checkout = Hecks::Adapters::Codebase::Tree.new(root: @root).checkout? && File.writable?(@root)
121
+ checkout ? File.join(@root, "tmp/qa-mined") : Hecks::CacheDir.path("qa-mined")
122
+ end
123
+
124
+ # Returns nil when the agent finished, else why it did not.
125
+ def ask_agent(prompt)
126
+ @agent.ask(prompt: prompt, command: @command, chdir: @root, log: @agent_log)
127
+ nil
128
+ rescue Hecks::Adapters::Agent::Failed => e
129
+ e.message
130
+ end
131
+
132
+ def boot_error(candidate, boot_root)
133
+ dir = Hecks::Fuzzing::DomainGenerator.write(
134
+ { "source" => File.read(candidate[:bluebook]), "aggregates" => [], "policies" => [] },
135
+ File.join(boot_root, candidate[:slug])
136
+ )
137
+ output, = Open3.capture2e(*Child.argv(@root, "qa_generated_domains", "--check", dir, "--seeds", "0"),
138
+ chdir: @root)
139
+ line = output.lines.reverse.find { |candidate_line| candidate_line.start_with?(RESULT_MARKER) }
140
+ return "boot check produced no result: #{output.lines.last(5).join.strip}" unless line
141
+
142
+ result = JSON.parse(line.delete_prefix(RESULT_MARKER))
143
+ result["status"] == "invalid" ? result["error"] : nil
144
+ end
145
+
146
+ def mine(prompt)
147
+ FileUtils.mkdir_p(@out_dir)
148
+ FileUtils.mkdir_p(@run_dir)
149
+ puts "combination miner: #{@brief['corpus'].size} corpus domain(s), " \
150
+ "#{@brief['unmet_pairs'].size} unmet pair(s) — #{relative(@run_dir)}"
151
+ unless @options[:from]
152
+ puts "asking the agent for #{@options[:candidates]} candidate(s) (#{@command.first}; " \
153
+ "log #{relative(@agent_log)})…"
154
+ failure = ask_agent(prompt)
155
+ abort "combination miner: #{failure}" if failure
156
+ end
157
+
158
+ candidates = Miner.candidates(@out_dir)
159
+ abort "combination miner: the agent wrote no candidates under #{relative(@out_dir)}" if candidates.empty?
160
+
161
+ invalid, repaired = boot_and_repair(candidates)
162
+ report(candidates, invalid, repaired)
163
+ valid = candidates.reject { |candidate| invalid.key?(candidate[:slug]) }
164
+ abort "combination miner: no candidate booted — nothing to check" if valid.empty?
165
+
166
+ puts
167
+ check(valid)
168
+ end
169
+
170
+ # @return [Array(Hash, Array<String>)] the slug-to-error map of candidates that never booted,
171
+ # and the slugs a repair round fixed
172
+ def boot_and_repair(candidates)
173
+ boot_root = File.join(@run_dir, "boot")
174
+ failures = candidates.filter_map { |candidate| (error = boot_error(candidate, boot_root)) && [candidate, error] }
175
+ repaired = []
176
+ @options[:repair_rounds].times do |round|
177
+ break if failures.empty? || @options[:from]
178
+
179
+ puts "repair round #{round + 1}: #{failures.size} candidate(s) did not boot — back to the agent…"
180
+ failure = ask_agent(Miner.repair_prompt(failures, out_dir: @out_dir))
181
+ abort "combination miner: #{failure}" if failure
182
+
183
+ still = failures.filter_map { |candidate, _| (error = boot_error(candidate, boot_root)) && [candidate, error] }
184
+ repaired.concat(failures.map { |candidate, _| candidate[:slug] } - still.map { |candidate, _| candidate[:slug] })
185
+ failures = still
186
+ end
187
+ [failures.to_h { |candidate, error| [candidate[:slug], error] }, repaired]
188
+ end
189
+
190
+ def report(candidates, invalid, repaired)
191
+ puts
192
+ candidates.each do |candidate|
193
+ if invalid.key?(candidate[:slug])
194
+ puts " #{candidate[:slug]}: INVALID — #{invalid[candidate[:slug]]}"
195
+ next
196
+ end
197
+
198
+ pairs = begin
199
+ Miner.new_pairs(candidate[:dir], @brief["covered"])
200
+ rescue StandardError, ScriptError => e
201
+ ["(census failed: #{e.class})"]
202
+ end
203
+ puts " #{candidate[:slug]}: boots#{' (repaired)' if repaired.include?(candidate[:slug])}; " \
204
+ "new pair(s): #{pairs.empty? ? 'none' : pairs.join(', ')}"
205
+ first = candidate[:hypothesis].to_s.lines.map(&:strip).find { |line| !line.empty? && !line.start_with?("#") }
206
+ puts " #{first}" if first
207
+ end
208
+ end
209
+
210
+ def check(valid)
211
+ args = ["--start", (Time.now.to_i % 1_000_000).to_s, "--seeds", @options[:seeds].to_s,
212
+ "--steps", @options[:steps].to_s, "--adversarial", @options[:adversarial].to_s]
213
+ args << "--rust" if @options[:rust]
214
+ args.concat(valid.flat_map { |candidate| ["--source", candidate[:bluebook]] })
215
+ _, status = Process.wait2(Process.spawn(*Child.argv(@root, "qa_generated_domains", *args), chdir: @root))
216
+ status.exitstatus || EXIT_ERROR
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../hecks"
4
+ # The era subsystem does not load with core (ADR 0033).
5
+ require_relative "../../ports/persistence/plugins/era"
6
+ require_relative "../adapters/git_pr"
7
+
8
+ module Hecks
9
+ module QualityControlCli
10
+ # The command behind `hecks quality_control patch.open`: opens a PR and records it in the
11
+ # QualityControl ledger in one step. It is the only door into `QualityControl::Patch.Open` and
12
+ # `QualityControl::Improvement.Open`.
13
+ #
14
+ # It refuses (exit 1, nothing opened or recorded) unless the ledger's own `Open` command would
15
+ # take the record and the `GitPr` adapter agrees. The command's `given`s are the rules, asked
16
+ # as a dry run before anything is opened: the branch starts with the practice's prefix; with
17
+ # `--bug`, the bug is `fixed`; with `--angle`, it is `investigating`. The adapter answers the
18
+ # rest: the tree is clean, `PR_CAP_PER_DAY` (0 = uncapped) is unmet since local midnight, and
19
+ # the bug's commit is an ancestor of `HEAD`. It is idempotent. `QA_REPO_DIR` picks the checkout
20
+ # `git` runs in, so a spec can use a throwaway repo and a fake `gh`.
21
+ class QaOpenPr
22
+ EXIT_OK = 0
23
+
24
+ USAGE = "usage: hecks quality_control patch.open --bug BUG#n --title \"…\" [--body \"…\"]\n " \
25
+ "hecks quality_control improvement.open --improvement [--angle ANGLE-n] --title \"…\" [--body \"…\"]"
26
+
27
+ SHA_PATTERN = /\A[0-9a-fA-F]{7,40}\z/
28
+
29
+ # Stands in for the commit of a bug that is not fixed yet, so its dry run reaches the `given`.
30
+ PLACEHOLDER_COMMIT = "0000000"
31
+
32
+ # Opens and records the PR.
33
+ #
34
+ # @param argv [Array<String>] `--bug`, `--improvement`, `--angle`, `--title` and `--body`
35
+ # @param root [String] the repository root
36
+ # @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` and `QA_REPO_DIR`
37
+ # @return [Integer] 0 once opened and recorded
38
+ # @raise [SystemExit] with `refused: ...` when a rule refuses, or the arguments are wrong
39
+ def self.call(argv, root:, env: ENV)
40
+ new(root: root, env: env).call(argv)
41
+ end
42
+
43
+ # @param root [String] the repository root
44
+ # @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` and `QA_REPO_DIR`
45
+ def initialize(root:, env: ENV)
46
+ @domain_dir = env.fetch("QA_SWEEP_DOMAIN_DIR", File.join(root, "qa/bluebook"))
47
+ @repo_dir = env.fetch("QA_REPO_DIR", root)
48
+ end
49
+
50
+ # @param argv [Array<String>] the command line's arguments
51
+ # @return [Integer] the exit status
52
+ # @raise [SystemExit] with `refused: ...` when a rule refuses, or the arguments are wrong
53
+ def call(argv)
54
+ options = parse(argv.dup)
55
+ return EXIT_OK if options == :help
56
+
57
+ @options = options
58
+ @git_pr = Hecks::Adapters::GitPr.new(repo_dir: @repo_dir)
59
+ @branch = refusing { @git_pr.branch }
60
+ refusing { @git_pr.assert_clean_tree! }
61
+ @runtime = boot_ledger
62
+ find_citations
63
+ check_rules
64
+ view = open_pull_request
65
+ record(view)
66
+ merge_when_green(view[:number]) if dial(:AUTO_MERGE, false)
67
+ puts "#{view[:url]} (#{view[:headRefName]} @ #{view[:headRefOid].to_s[0, 7]}) — #{view[:title]}"
68
+ EXIT_OK
69
+ end
70
+
71
+ private
72
+
73
+ def parse(argv)
74
+ options = {}
75
+ until argv.empty?
76
+ arg = argv.shift
77
+ case arg
78
+ when "-h", "--help"
79
+ puts USAGE
80
+ return :help
81
+ when "--improvement"
82
+ options[:improvement] = true
83
+ when "--bug", "--angle", "--title", "--body"
84
+ value = argv.shift
85
+ abort "#{USAGE}\n#{arg} needs a value" if value.nil? || value.empty?
86
+ options[arg.delete_prefix("--").to_sym] = value
87
+ else
88
+ abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
89
+ end
90
+ end
91
+ validate(options)
92
+ end
93
+
94
+ def validate(options)
95
+ abort "#{USAGE}\n--title is required" unless options[:title]
96
+ abort "#{USAGE}\ngive --bug BUG#n OR --improvement, not both" if options[:bug] && options[:improvement]
97
+ abort "#{USAGE}\ngive --bug BUG#n or --improvement" unless options.key?(:bug) || options.key?(:improvement)
98
+ abort "#{USAGE}\n--angle only means something with --improvement" if options[:angle] && !options[:improvement]
99
+
100
+ options
101
+ end
102
+
103
+ # Every rule the adapter enforces refuses the same way: exit 1, nothing opened or recorded.
104
+ def refusing
105
+ yield
106
+ rescue Hecks::Adapters::GitPr::Refusal, Hecks::Adapters::GitPr::CommandFailed => e
107
+ abort "refused: #{e.message}"
108
+ end
109
+
110
+ def boot_ledger
111
+ Hecks.boot(@domain_dir)
112
+ rescue StandardError => e
113
+ abort "the QualityControl ledger did not boot — fix the ledger itself before opening a PR against it " \
114
+ "(#{e.class}: #{e.message})"
115
+ end
116
+
117
+ def query(name, **args) = @runtime.query("QualityControl::#{name}", **args)
118
+
119
+ # Falls back to a dial-less fixture ledger's values: uncapped, non-draft, no auto-merge.
120
+ def dial(name, fallback)
121
+ return fallback unless defined?(::QualityControlDials) && ::QualityControlDials.const_defined?(name)
122
+
123
+ ::QualityControlDials.const_get(name)
124
+ end
125
+
126
+ def find_citations
127
+ @bug = @angle = nil
128
+ if @options[:bug]
129
+ @bug = ::QualityControl::Bug.find(@options[:bug])
130
+ abort "refused: no such bug #{@options[:bug].inspect}" unless @bug
131
+ end
132
+ return unless @options[:angle]
133
+
134
+ @angle = ::QualityControl::Angle.find(@options[:angle])
135
+ abort "refused: no such angle #{@options[:angle].inspect}" unless @angle
136
+ end
137
+
138
+ # What a refused `given` says to do next, by the rule's own wording; nothing for a rule it
139
+ # does not know.
140
+ def hint_for(rule)
141
+ case rule
142
+ when "the bug is fixed"
143
+ " (#{@bug.id} is #{@bug.status.inspect} — dispatch investigate and fix first " \
144
+ "(hecks run qa/bluebook fix id=#{@bug.id} reference.value=#{@bug.id} commit.value=<sha>))"
145
+ when "the angle is under investigation"
146
+ " (#{@angle.id} is #{@angle.status.inspect} — dispatch angle.investigate first, so the lead " \
147
+ "reads as picked up before something is built from it)"
148
+ when "the branch is one this practice recognises as its own"
149
+ " (the branch is #{@branch.inspect}; a PR this practice opens is on a branch it can recognise " \
150
+ "as its own, QualityControlDials::BRANCH_PREFIX)"
151
+ else ""
152
+ end
153
+ end
154
+
155
+ # Asks the ledger whether `verb` would take these facts, without recording anything. The
156
+ # number is one no record holds yet, so the dry run reaches the `given`s rather than
157
+ # `AlreadyExists`.
158
+ def refuse_unless_ledger_takes(verb, **facts)
159
+ @runtime.dry_run?(verb, **facts)
160
+ rescue Hecks::Runtime::GivenNotMet => e
161
+ hint = hint_for(e.message.split(" — ", 2).last)
162
+ abort "refused: #{e.message}#{hint} — a dry run of #{verb}, so nothing was opened or recorded"
163
+ end
164
+
165
+ def next_number(aggregate)
166
+ query("#{aggregate}.All").map { |row| row[:number][:value] }.max.to_i + 1
167
+ end
168
+
169
+ def check_rules
170
+ recorded = { url: { value: "https://github.com/pull/0" }, title: { value: @options[:title] },
171
+ branch: { value: @branch }, now: { value: Time.now.to_i } }
172
+ if @bug
173
+ commit = @bug.commit.to_h[:value].to_s
174
+ refuse_unless_ledger_takes("QualityControl::Patch.Open",
175
+ bug: @bug.id, number: { value: next_number("Patch") },
176
+ commit: { value: commit.match?(SHA_PATTERN) ? commit : PLACEHOLDER_COMMIT },
177
+ **recorded)
178
+ else
179
+ refuse_unless_ledger_takes("QualityControl::Improvement.Open",
180
+ **(@angle ? { angle: @angle.id } : {}),
181
+ number: { value: next_number("Improvement") }, **recorded)
182
+ end
183
+ check_daily_cap
184
+ check_ancestry
185
+ end
186
+
187
+ def check_daily_cap
188
+ cap = dial(:PR_CAP_PER_DAY, 0)
189
+ return unless cap.positive?
190
+
191
+ today = Time.now
192
+ midnight = Time.new(today.year, today.month, today.day).to_i
193
+ opened_today = query("Patch.OpenedSince", since: { value: midnight }).size +
194
+ query("Improvement.OpenedSince", since: { value: midnight }).size
195
+ refusing { @git_pr.assert_under_daily_cap!(opened_today: opened_today, cap: cap) }
196
+ end
197
+
198
+ def check_ancestry
199
+ return unless @bug
200
+
201
+ commit = @bug.commit.to_h[:value].to_s
202
+ abort "refused: #{@bug.id} carries no commit" unless commit.match?(SHA_PATTERN)
203
+
204
+ refusing do
205
+ @git_pr.assert_ancestor!(commit: commit, owner: @bug.id)
206
+ @git_pr.assert_pushed!(branch: @branch, commit: commit, owner: @bug.id)
207
+ end
208
+ end
209
+
210
+ # @return [Hash] `gh`'s view of the PR, opened here when the branch had none
211
+ def open_pull_request
212
+ view = refusing { @git_pr.open_pull_request(@branch) }
213
+ if view
214
+ puts "PR ##{view[:number]} already open for #{@branch} — not re-creating"
215
+ else
216
+ create_pull_request
217
+ view = refusing { @git_pr.open_pull_request(@branch) }
218
+ abort "gh pr create returned, but gh pr view #{@branch} shows no open PR — record nothing" unless view
219
+ end
220
+ sha = view[:headRefOid].to_s
221
+ unless sha.match?(SHA_PATTERN)
222
+ abort "refused: gh reports head #{sha.inspect} for ##{view[:number]}, which does not look like a sha"
223
+ end
224
+
225
+ check_pr_head(sha)
226
+ view
227
+ end
228
+
229
+ # The PR must carry the fix commit, whether this run opened it or an earlier one did.
230
+ def check_pr_head(sha)
231
+ return unless @bug
232
+
233
+ refusing { @git_pr.assert_pr_head!(head: sha, commit: @bug.commit.to_h[:value].to_s, owner: @bug.id) }
234
+ end
235
+
236
+ def create_pull_request
237
+ # The body names the ledger record so the PR and the ledger point at each other.
238
+ cited = if @bug then "Fixes #{@bug.id}."
239
+ elsif @angle then "Builds #{@angle.id}."
240
+ else "Deliberate work, no Bug or Angle behind it."
241
+ end
242
+ body = @options[:body] ||
243
+ "#{cited}\n\nOpened by hecks quality_control patch.open and recorded in the QualityControl ledger."
244
+ begin
245
+ @git_pr.create_pull_request(branch: @branch, title: @options[:title], body: body,
246
+ draft: dial(:DRAFT_ONLY, false))
247
+ rescue Hecks::Adapters::GitPr::CommandFailed => e
248
+ abort e.message
249
+ end
250
+ end
251
+
252
+ def record(view)
253
+ number = view[:number]
254
+ sha = view[:headRefOid].to_s
255
+ now = Time.now.to_i
256
+ if @bug
257
+ record_patch(view, number, sha, now)
258
+ else
259
+ record_improvement(view, number, sha, now)
260
+ end
261
+ end
262
+
263
+ def record_patch(view, number, sha, now)
264
+ if query("Patch.All").map { |row| row[:number][:value] }.include?(number)
265
+ puts "Patch ##{number} already recorded — not re-recording"
266
+ return
267
+ end
268
+
269
+ ::QualityControl::Patch.open!(
270
+ bug: @bug.id, number: { value: number }, url: { value: view[:url] },
271
+ branch: { value: view[:headRefName] }, commit: { value: sha }, title: { value: view[:title] },
272
+ now: { value: now }
273
+ )
274
+ puts "recorded Patch ##{number} for #{@bug.id} at #{sha[0, 7]}"
275
+ end
276
+
277
+ def record_improvement(view, number, sha, now)
278
+ existing = ::QualityControl::Improvement.find(number)
279
+ if existing && existing.status != "opened"
280
+ puts "Improvement ##{number} already recorded (#{existing.status}) — not re-recording"
281
+ return
282
+ end
283
+
284
+ # A run that stopped between `open!` and `land!` leaves the record `opened`: finish it.
285
+ existing || ::QualityControl::Improvement.open!(
286
+ **(@angle ? { angle: @angle.id } : {}),
287
+ number: { value: number }, url: { value: view[:url] },
288
+ branch: { value: view[:headRefName] }, title: { value: view[:title] },
289
+ now: { value: now }
290
+ )
291
+ ::QualityControl::Improvement.find(number).land!(number: { value: number }, commit: { value: sha })
292
+ puts "recorded Improvement ##{number}#{" for #{@angle.id}" if @angle}, landed at #{sha[0, 7]}"
293
+ end
294
+
295
+ def merge_when_green(number)
296
+ begin
297
+ @git_pr.merge_when_green(number)
298
+ rescue Hecks::Adapters::GitPr::CommandFailed => e
299
+ abort "recorded, but #{e.message}"
300
+ end
301
+ puts "auto-merge queued for ##{number} (QualityControlDials::AUTO_MERGE)"
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../hecks"
4
+ # The era subsystem does not load with core; a `PostgresEra` domain needs it (ADR 0033).
5
+ require_relative "../../ports/persistence/plugins/era"
6
+ require_relative "../../adapters/driven/heki"
7
+
8
+ module Hecks
9
+ module QualityControlCli
10
+ # The command behind `hecks quality_control migrate_ledger_from_heki`: copies each aggregate's
11
+ # current state from a Heki data directory into the repository that the domain's
12
+ # `.hecksagon`/`.world` files bind today (for example `PostgresEra`). It is domain-agnostic.
13
+ #
14
+ # migrate_ledger_from_heki <domain_dir> <heki_data_dir> [aggregate_name ...] # dry run
15
+ # migrate_ledger_from_heki <domain_dir> <heki_data_dir> [aggregate_name ...] --force # apply
16
+ #
17
+ # One `save` per id from `Heki#all`, not a journal replay: state is kept byte-for-byte, the
18
+ # per-append history is not. The Heki side is only read. An id the destination holds under a
19
+ # different state is always refused, so a second run is idempotent. Only aggregates with a
20
+ # `<storage_name>.heki` file in the Heki directory are considered; trailing names (`hecks_name`
21
+ # or `storage_name`) narrow further.
22
+ class QaPostgresMigrate
23
+ USAGE = "usage: hecks quality_control migrate_ledger_from_heki <domain_dir> <heki_data_dir> " \
24
+ "[aggregate_name ...] [--force]"
25
+
26
+ # Migrates, or reports what a migration would do.
27
+ #
28
+ # @param argv [Array<String>] the domain directory, the Heki directory, aggregate names, and
29
+ # `--force` to apply
30
+ # @param out [IO] where the report goes
31
+ # @param err [IO] where a refusal goes
32
+ # @return [Integer] 0 when nothing conflicts, 1 for a usage error or a conflict
33
+ def self.call(argv, out: $stdout, err: $stderr)
34
+ new(out: out, err: err).call(argv)
35
+ end
36
+
37
+ # @param out [IO] where the report goes
38
+ # @param err [IO] where a refusal goes
39
+ def initialize(out: $stdout, err: $stderr)
40
+ @out = out
41
+ @err = err
42
+ end
43
+
44
+ # @param argv [Array<String>] the domain directory, the Heki directory, aggregate names, and
45
+ # `--force` to apply
46
+ # @return [Integer] 0 when nothing conflicts, 1 for a usage error or a conflict
47
+ def call(argv)
48
+ argv = argv.dup
49
+ force = argv.delete("--force")
50
+ domain_dir = argv.shift
51
+ return usage("no domain directory given") if domain_dir.nil?
52
+ return usage("no such domain directory #{domain_dir.inspect}") unless Dir.exist?(domain_dir)
53
+
54
+ heki_dir = argv.shift
55
+ return usage("no Heki data directory given") if heki_dir.nil?
56
+ return usage("no such Heki data directory #{heki_dir.inspect}") unless Dir.exist?(heki_dir)
57
+
58
+ heki_dir = File.expand_path(heki_dir)
59
+ registry = Hecks.boot(domain_dir).registry
60
+ candidates = candidates_in(registry, heki_dir, argv)
61
+ if candidates.empty?
62
+ @out.puts "hecks quality_control migrate_ledger_from_heki: no aggregate matched " \
63
+ "#{argv.empty? ? '(any)' : argv.inspect} with a " \
64
+ "corresponding .heki file under #{heki_dir}"
65
+ return 0
66
+ end
67
+
68
+ migrated, skipped, conflicts = copy(candidates, registry, heki_dir, force)
69
+ report(force, migrated, skipped, conflicts)
70
+ conflicts.empty? ? 0 : 1
71
+ end
72
+
73
+ # Deep comparison independent of key type and hash order; only array order is meaningful.
74
+ # `spec/qa_postgres_migration_spec.rb` pins this for conflict detection.
75
+ #
76
+ # @param value [Object] a state, or any part of one
77
+ # @return [Object] the same with every hash keyed by string and sorted
78
+ def self.canonical(value)
79
+ case value
80
+ when Hash
81
+ value.each_with_object({}) { |(k, v), h| h[k.to_s] = canonical(v) }.sort.to_h
82
+ when Array
83
+ value.map { |v| canonical(v) }
84
+ else
85
+ value
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def usage(message)
92
+ @err.puts "hecks quality_control migrate_ledger_from_heki: #{message}"
93
+ @err.puts USAGE
94
+ 1
95
+ end
96
+
97
+ # A candidate is an aggregate whose own `.heki` file exists, whatever it is bound to now.
98
+ def candidates_in(registry, heki_dir, wanted)
99
+ registry.bluebooks.each_with_object([]) do |(domain_name, bluebook), all|
100
+ bluebook.aggregates.each do |aggregate|
101
+ next unless wanted.empty? || wanted.include?(aggregate.hecks_name) || wanted.include?(aggregate.storage_name)
102
+
103
+ heki_path = File.join(heki_dir, "#{aggregate.storage_name}.heki")
104
+ next unless File.exist?(heki_path)
105
+
106
+ all << { domain_name: domain_name, aggregate: aggregate, heki_path: heki_path }
107
+ end
108
+ end
109
+ end
110
+
111
+ def copy(candidates, registry, heki_dir, force)
112
+ migrated = []
113
+ skipped = []
114
+ conflicts = []
115
+ candidates.each do |candidate|
116
+ aggregate = candidate[:aggregate]
117
+ # Guarded like a factory-built repository so a Heki store decodes through the state codec.
118
+ source = Hecks::Ports::Persistence::CodecBoundary.guard!(
119
+ Hecks::Adapters::Heki.new(aggregate: aggregate, settings: { dir: heki_dir }, root: nil)
120
+ )
121
+ dest = registry.repository(candidate[:domain_name], aggregate)
122
+ source.all.each do |instance|
123
+ name = "#{aggregate.storage_name}/#{instance.id}"
124
+ existing = dest.find(instance.id)
125
+ if existing.nil?
126
+ dest.save(instance) if force
127
+ migrated << name
128
+ elsif self.class.canonical(existing.state) == self.class.canonical(instance.state)
129
+ skipped << name
130
+ else
131
+ conflicts << name
132
+ end
133
+ end
134
+ end
135
+ [migrated, skipped, conflicts]
136
+ end
137
+
138
+ def report(force, migrated, skipped, conflicts)
139
+ label = force ? "MIGRATED" : "WOULD MIGRATE"
140
+ migrated.each { |id| @out.puts "#{label} #{id}" }
141
+ skipped.each { |id| @out.puts "SKIP #{id}: destination already holds the identical state" }
142
+ conflicts.each do |id|
143
+ @err.puts "REFUSED #{id}: the destination already holds a DIFFERENT state for this id — " \
144
+ "not overwritten, --force or not. Resolve by hand (compare the two states, decide " \
145
+ "which is authoritative) before migrating this id again."
146
+ end
147
+ @out.puts "" unless migrated.empty? && skipped.empty? && conflicts.empty?
148
+ @out.puts "#{force ? 'migrated' : 'would migrate'} #{migrated.size}, skipped #{skipped.size} " \
149
+ "(already caught up), refused #{conflicts.size} (conflicting data)"
150
+ @out.puts "re-run with --force to apply" unless force || migrated.empty?
151
+ end
152
+ end
153
+ end
154
+ end