hecks 2.9.0 → 3.0.1

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 +13 -4
  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 +15 -8
  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 +15 -5
  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,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require_relative "../child"
5
+
6
+ module Hecks
7
+ module QualityControlCli
8
+ class QaSweep
9
+ # `--all`: one child sweep per waiting target, in parallel, then the later waves and one
10
+ # consolidated report.
11
+ #
12
+ # A child is a fresh process, never a fork: a forked child would share this process's live
13
+ # Postgres socket.
14
+ module AllMode
15
+ # Every child prints one `resolved depth:` line; the child's log is the single source of
16
+ # truth that the report prints verbatim, so scraping it here cannot disagree.
17
+ RESOLVED_DEPTH_LINE = /^resolved depth: seeds=(\d+) steps=(\d+) \(clean_streak=(\d+)\)$/
18
+
19
+ # Parsed from a child's `resolved modes:` line; wave-2 candidates come from the parent's own
20
+ # inference, not from this.
21
+ RESOLVED_MODES_LINE = /^resolved modes: (\S*) \(capabilities=([^;)]*)(?:; deferred=([^)]*))?\)$/
22
+
23
+ private
24
+
25
+ # Sweeps every waiting target and reports.
26
+ #
27
+ # @param modes [Array<Symbol>] the enabled modes each child is told
28
+ # @param parity_wave [Boolean] whether a `persistence_parity` wave follows a clean wave 1
29
+ # @param deferred_wave_modes [Array<Symbol>] the modes that get a wave of their own
30
+ # @return [Integer] 2 when any child found something, else 1 when any errored, else 0
31
+ def run_all_mode(modes:, parity_wave:, deferred_wave_modes:)
32
+ now = Time.now.to_i
33
+ waiting = query("Target.Rotation").map { |row| row[:reference][:value] }
34
+ stale = stale_holds(now)
35
+ targets = waiting + stale.map { |row| row[:reference][:value] }
36
+ if targets.empty?
37
+ puts "rotation is empty — nothing waiting to sweep, and no stale hold to reclaim. Clean no-op."
38
+ return EXIT_OK
39
+ end
40
+
41
+ announce_all(targets, stale, modes, now)
42
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
+ results = run_pool(targets) do |reference|
44
+ spawn_sweep_child(reference, @seeds_override, @steps_override, @self_consistency, modes: modes)
45
+ end
46
+ results = results.map { |r| r.merge(wave: 1) }
47
+ results += run_parity_wave(results) if parity_wave
48
+ # Each enabled deferred mode gets its own wave over wave 1's clean targets.
49
+ deferred_wave_modes.each do |mode|
50
+ results += run_deferred_wave(results.select { |r| r[:wave] == 1 }, mode)
51
+ end
52
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
53
+
54
+ clean = results.select { |r| r[:exit_status] == EXIT_OK }
55
+ found = results.select { |r| r[:exit_status] == EXIT_FOUND_SOMETHING }
56
+ errored = results.reject { |r| [EXIT_OK, EXIT_FOUND_SOMETHING].include?(r[:exit_status]) }
57
+ print_all_mode_report(results, elapsed, clean, found, errored)
58
+ all_mode_exit_code(found, errored)
59
+ end
60
+
61
+ def announce_all(targets, stale, modes, now)
62
+ puts "sweeping #{targets.size} target(s), at most #{@max_parallel} at once: #{targets.join(', ')} " \
63
+ "(adversarial fraction #{@adversarial}, self-consistency #{@self_consistency}, " \
64
+ "role draw #{@role_draw}, dry-run fraction #{@dry_run}, enabled modes #{modes.join(',')})"
65
+ stale.each do |row|
66
+ puts "reclaimed stale hold: #{row[:reference][:value]} (held by #{row[:held_by][:value]}, " \
67
+ "#{now - row[:claimed_at][:value]}s ago)"
68
+ end
69
+ puts
70
+ end
71
+
72
+ # `Target.Rotation` lists only waiting targets, so stale holds are found here:
73
+ # `Target.Claim`'s given (claimed_at + window <= now) restated in Ruby, since a `where`
74
+ # cannot add two fields.
75
+ def stale_holds(now)
76
+ query("Target.Held").select { |row| row[:claimed_at][:value] + row[:window][:value] <= now }
77
+ end
78
+
79
+ # Spawns `hecks quality_control ask run target=<target>` as a fresh process, not a fork.
80
+ # Seeds/steps forward only when given, so each child derives its own depth from its own
81
+ # streak.
82
+ def spawn_sweep_child(target_reference, seeds_override, steps_override, self_consistency,
83
+ modes:, parity: false)
84
+ # Unlinked at once: the open fd stays readable, nothing is left on disk, and no fixed path
85
+ # collides.
86
+ log = Tempfile.new(["qa_sweep-#{filesystem_safe_component(target_reference)}-", ".log"])
87
+ log.unlink
88
+
89
+ args = [target_reference]
90
+ args += ["--seeds", seeds_override.to_s] if seeds_override
91
+ args += ["--steps", steps_override.to_s] if steps_override
92
+ args += ["--adversarial", @adversarial.to_s, "--self-consistency", self_consistency.to_s]
93
+ args += ["--role-draw", @role_draw.to_s, "--dry-run", @dry_run.to_s]
94
+ args += parity ? ["--persistence-parity"] : ["--modes", modes.join(",")]
95
+
96
+ pid = Process.spawn(*Child.argv(@root, "qa_sweep", *args), out: log, err: log, chdir: @root)
97
+ { target: target_reference, pid: pid, log: log }
98
+ end
99
+
100
+ def collect_sweep_child(child, status)
101
+ child[:log].rewind
102
+ output = child[:log].read
103
+ child[:log].close
104
+
105
+ { target: child[:target], exit_status: status.exitstatus, termsig: status.termsig, output: output }
106
+ end
107
+
108
+ # Keeps at most `@max_parallel` children alive; results come back in queue order so reports
109
+ # are stable.
110
+ def run_pool(targets)
111
+ pending = targets.dup
112
+ running = {}
113
+ results = {}
114
+
115
+ until pending.empty? && running.empty?
116
+ while running.size < @max_parallel && (next_target = pending.shift)
117
+ child = yield(next_target)
118
+ running[child[:pid]] = child
119
+ end
120
+
121
+ pid, status = Process.wait2(-1)
122
+ child = running.delete(pid)
123
+ next unless child
124
+
125
+ results[child[:target]] = collect_sweep_child(child, status)
126
+ end
127
+
128
+ targets.map { |target| results.fetch(target) }
129
+ end
130
+
131
+ # Wave 2: `--persistence-parity` children for wave-1 targets that came back clean and infer
132
+ # `postgres_era`. Skipped by `--no-parity` or when `--modes` already named the mode.
133
+ def run_parity_wave(wave1)
134
+ candidates = wave_candidates(wave1, :persistence_parity)
135
+ if candidates.empty?
136
+ puts "parity wave: no clean target binds PostgresEra — nothing to compare Memory against. Skipped."
137
+ return []
138
+ end
139
+
140
+ puts
141
+ puts "parity wave: Memory vs real PostgresEra for #{candidates.size} target(s), " \
142
+ "at most #{@max_parallel} at once: #{candidates.join(', ')}"
143
+ results = run_pool(candidates) do |reference|
144
+ spawn_sweep_child(reference, @seeds_override, nil, false, modes: [], parity: true)
145
+ end
146
+ # Labelled in the target column so every report section tells a wave-2 row from wave 1's.
147
+ results.map { |r| r.merge(wave: 2, target: "#{r[:target]} [parity wave]") }
148
+ end
149
+
150
+ # Later wave for deferred modes (era_boundary, concurrency): `--modes <mode>` children over
151
+ # the clean wave-1 targets whose capabilities make the mode eligible.
152
+ def run_deferred_wave(wave1, mode)
153
+ candidates = wave_candidates(wave1, mode)
154
+ if candidates.empty?
155
+ puts "#{mode} wave: no clean target qualifies. Skipped."
156
+ return []
157
+ end
158
+
159
+ puts
160
+ puts "#{mode} wave: #{candidates.size} target(s), at most #{@max_parallel} at once: #{candidates.join(', ')}"
161
+ results = run_pool(candidates) do |reference|
162
+ spawn_sweep_child(reference, @seeds_override, nil, false, modes: [mode])
163
+ end
164
+ results.map { |r| r.merge(wave: 2, target: "#{r[:target]} [#{mode} wave]") }
165
+ end
166
+
167
+ # @return [Array<String>] the clean wave-1 targets whose capabilities make `mode` eligible
168
+ def wave_candidates(wave1, mode)
169
+ clean_refs = wave1.select { |r| r[:exit_status] == EXIT_OK }.map { |r| r[:target] }
170
+ rows = query("Target.All").select { |row| clean_refs.include?(row[:reference][:value]) }
171
+ eligible = rows.select do |row|
172
+ capabilities = Hecks::Fuzzing::TargetCapabilities.infer(resolve_target_path(row[:path][:value]),
173
+ rust_dir: @rust_dir)
174
+ Hecks::Fuzzing::TargetCapabilities.eligible?(mode, capabilities)
175
+ end
176
+ eligible.map { |row| row[:reference][:value] }
177
+ end
178
+
179
+ def resolved_depth_for(output)
180
+ match = output.match(RESOLVED_DEPTH_LINE)
181
+ return "depth unknown (target errored before it could claim)" unless match
182
+
183
+ "#{match[1]} seeds x #{match[2]} steps (clean streak #{match[3]})"
184
+ end
185
+
186
+ def resolved_modes_for(output)
187
+ match = output.match(RESOLVED_MODES_LINE)
188
+ return "modes unknown (target errored before it could resolve them)" unless match
189
+
190
+ line = "#{match[1].empty? ? 'none' : match[1]} (capabilities: #{match[2].empty? ? 'none' : match[2]}"
191
+ line += "; deferred: #{match[3]}" if match[3] && !match[3].empty?
192
+ "#{line})"
193
+ end
194
+
195
+ def print_resolved_modes(results)
196
+ return if results.empty?
197
+
198
+ puts
199
+ puts "resolved modes (by target):"
200
+ results.each { |r| puts " #{r[:target]}: #{resolved_modes_for(r[:output])}" }
201
+ end
202
+
203
+ def print_resolved_depths(results)
204
+ return if results.empty?
205
+
206
+ puts
207
+ puts "resolved depth (by target):"
208
+ results.each { |r| puts " #{r[:target]}: #{resolved_depth_for(r[:output])}" }
209
+ end
210
+
211
+ def print_operational_errors(errored)
212
+ return if errored.empty?
213
+
214
+ puts
215
+ puts "OPERATIONAL ERRORS (#{errored.size}) — read each message, fix the actual problem, don't retry blind:"
216
+ errored.each do |r|
217
+ label = r[:exit_status] ? "exit #{r[:exit_status]}" : "killed by signal #{r[:termsig]}"
218
+ puts
219
+ puts "-- #{r[:target]} (#{label}) --"
220
+ puts r[:output].strip
221
+ end
222
+ end
223
+
224
+ def print_found_something(found)
225
+ return if found.empty?
226
+
227
+ puts
228
+ puts "FOUND SOMETHING (#{found.size}) — full report per target below; act on every one:"
229
+ found.each do |r|
230
+ puts
231
+ puts "#" * 72
232
+ puts "# #{r[:target]}"
233
+ puts "#" * 72
234
+ puts r[:output]
235
+ end
236
+ end
237
+
238
+ def print_all_mode_report(results, elapsed, clean, found, errored)
239
+ puts "=" * 72
240
+ puts "CONSOLIDATED REPORT — #{results.size} target(s) swept, #{elapsed.round(1)}s wall-clock " \
241
+ "(genuinely parallel — not the sum of every target's own sweep time)"
242
+ puts "=" * 72
243
+ puts
244
+ puts "clean (#{clean.size}): #{clean.empty? ? 'none' : clean.map { |r| r[:target] }.join(', ')}"
245
+
246
+ print_resolved_depths(results)
247
+ print_resolved_modes(results)
248
+ print_operational_errors(errored)
249
+ print_found_something(found)
250
+ print_trace_output(results)
251
+ end
252
+
253
+ # A clean child's trace would otherwise be discarded; shown only under `QA_SWEEP_TRACE=1`.
254
+ def print_trace_output(results)
255
+ return unless @trace_enabled
256
+ return if results.empty?
257
+
258
+ puts
259
+ puts "QA_SWEEP_TRACE output (by target, in sweep order):"
260
+ results.each do |r|
261
+ trace_lines = r[:output].lines.grep(/^\[qa_sweep_trace /)
262
+ next if trace_lines.empty?
263
+
264
+ puts " #{r[:target]}:"
265
+ trace_lines.each { |line| puts " #{line.chomp}" }
266
+ end
267
+ end
268
+
269
+ # A finding outranks operational errors: 2 if any child found something, else 1 if any
270
+ # errored.
271
+ def all_mode_exit_code(found, errored)
272
+ return EXIT_FOUND_SOMETHING unless found.empty?
273
+ return EXIT_ERROR unless errored.empty?
274
+
275
+ EXIT_OK
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module QualityControlCli
5
+ class QaSweep
6
+ # The comparisons one sweep makes per seed, and the checks it logs for each. A sweep logs one
7
+ # `Check` per mode per seed; the `[mode]` subject prefix tells the ledger which axis held or
8
+ # surprised, and each expectation is written before looking.
9
+ module Checks
10
+ MODE_EXPECTATIONS = {
11
+ differential: "Ruby and the compiled Rust conformance binary agree on instances, events, refusals, " \
12
+ "reactions, sagas, queries and dry runs across every generated step",
13
+ properties_in_differential: "no declared property is violated over the Ruby side of the same " \
14
+ "replayed history",
15
+ self_consistency: "each engine agrees with itself on rehydration/idempotency/value-object round trip",
16
+ ruby_only: "no declared property is violated and the interpreter does not crash across every " \
17
+ "generated step",
18
+ persistence_parity: "Memory and a real, disposable PostgresEra-backed boot agree on instances, " \
19
+ "events, refusals, reactions, sagas and queries across every generated step",
20
+ adapter_parity_sqlite: "Memory and a real, on-disk SQLite-backed boot agree on instances, events, " \
21
+ "refusals, reactions, sagas and queries across every generated step",
22
+ concurrency: "two real, forked, cross-process dispatches of the same generated command against " \
23
+ "the same PostgresEra-backed schema settle on the same set of outcomes the identical " \
24
+ "pair settles on when dispatched sequentially with no contention",
25
+ era_boundary: "no ancestor PostgresEra era this target's own real lineage holds still carries " \
26
+ "post-cut writes nobody has merged forward (Lineage#diverged_count, every ancestor, " \
27
+ "is zero)"
28
+ }.freeze
29
+
30
+ # Modes whose divergences are a pure function of the step list; a race, the per-sweep checks
31
+ # and generator crashes are not.
32
+ SHRINKABLE_MODES = %i[differential properties_in_differential self_consistency ruby_only
33
+ adapter_parity_sqlite persistence_parity].freeze
34
+
35
+ private
36
+
37
+ # Flattens Replay's `:self_consistency` results into the same `{field:, detail:}` shape as
38
+ # every finding.
39
+ def self_consistency_divergences(history) = Hecks::Fuzzing::Differential.self_consistency_divergences(history)
40
+
41
+ def property_divergences(history) = Hecks::Fuzzing::Differential.property_divergences(history)
42
+
43
+ # The Ruby-vs-Rust diff from `spec/rust_conformance_fuzz_spec.rb`, one divergence list per
44
+ # active mode so `run_one_seed` logs a `Check` per mode.
45
+ def diff_ruby_vs_rust(differ, domain_path, steps, binary, modes:)
46
+ Hecks::Fuzzing::Differential.diff(
47
+ differ, domain_path, steps, binary,
48
+ modes: modes, adapter_parity_sqlite: -> { adapter_parity_sqlite_divergences(domain_path, steps) }
49
+ )
50
+ end
51
+
52
+ # Single-runtime fallback (`hecks fuzz`'s `outcome`) for domains with no compiled Rust
53
+ # binary: a violated property, or an exception escaping `Replay.call`.
54
+ def ruby_only_outcome(domain_path, steps, modes:)
55
+ self_consistency = modes.include?(:self_consistency)
56
+ history = Hecks::Fuzzing::Replay.call(domain_path, steps, self_consistency: self_consistency)
57
+ outcomes = { ruby_only: property_divergences(history) }
58
+ outcomes[:self_consistency] = self_consistency_divergences(history) if self_consistency
59
+ if modes.include?(:adapter_parity_sqlite)
60
+ outcomes[:adapter_parity_sqlite] = adapter_parity_sqlite_divergences(domain_path, steps)
61
+ end
62
+ outcomes
63
+ rescue StandardError => e
64
+ # A crash is one mode's finding, not the absence of the others: each other enabled mode
65
+ # gets an explicit not_checked entry so the ledger still holds one Check per resolved
66
+ # mode.
67
+ crashed = { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
68
+ %i[self_consistency adapter_parity_sqlite].each do |mode|
69
+ next unless modes.include?(mode)
70
+
71
+ crashed[mode] = [{ field: "not_checked",
72
+ detail: "the Ruby replay crashed before #{mode} ran — #{e.class}: #{e.message}" }]
73
+ end
74
+ crashed
75
+ end
76
+
77
+ def log_check!(sweep, subject:, expectation:)
78
+ sweep.check!(subject: { value: subject }, expectation: { value: expectation })
79
+ end
80
+
81
+ def check_for(mode, feature, seed, shape, divergences)
82
+ { mode: mode,
83
+ subject: "[#{mode}] #{feature} fuzz seed #{seed} (#{shape})",
84
+ expectation: MODE_EXPECTATIONS.fetch(mode),
85
+ divergences: divergences,
86
+ clean: divergences.empty?,
87
+ observation: divergences.empty? ? "held" : "diverged on: #{divergences.map { |d| d[:field] }.join(', ')}" }
88
+ end
89
+
90
+ # Extra `generate` arguments a guided seed needs in its `reproduce:` line; empty when
91
+ # unguided.
92
+ def plan_arguments(plan)
93
+ return "" unless plan
94
+
95
+ parts = []
96
+ parts << "prefix: #{plan.prefix.inspect}" if plan.prefix
97
+ parts << "favor: #{plan.favor.inspect}" unless plan.favor.empty?
98
+ parts.empty? ? "" : ", #{parts.join(', ')}"
99
+ end
100
+
101
+ # Once-per-sweep report of verbs skipped as not generated: held with the full list,
102
+ # surprised if any skipped verb's construct is outside the documented boundary.
103
+ def structural_skip_check(differ, binary, boundary)
104
+ skipped = differ.structural_skips.to_a.sort
105
+ gaps = Hecks::Fuzzing::RustGapManifest.for_binary(binary)
106
+ attributed = Hecks::Fuzzing::StructuralSkips.attribute(gaps, skipped)
107
+ outside = Hecks::Fuzzing::StructuralSkips.outside_boundary(attributed, boundary)
108
+ listing = attributed.map { |e| "#{e[:verb]} [#{e[:constructs].join(',')}]" }.join("; ")
109
+ divergences = outside.map do |e|
110
+ declares = e[:constructs].empty? ? "nothing" : e[:constructs].join(",")
111
+ { field: "structural_skip", verb: e[:verb], constructs: e[:constructs],
112
+ detail: "#{e[:verb]} was skipped as not generated (construct: #{declares}), outside " \
113
+ "STRUCTURAL_REFUSAL_BOUNDARY — a codegen regression or a new gap, not a documented boundary" }
114
+ end
115
+ observation = if divergences.empty?
116
+ "all #{skipped.size} inside the documented boundary"
117
+ else
118
+ "#{divergences.size} outside the boundary"
119
+ end
120
+ { mode: :structural_skip_report,
121
+ subject: "[structural_skip_report] structurally skipped #{skipped.size} verb(s): [#{listing}]",
122
+ expectation: "every named query/read model skipped because manifest.json declares it not " \
123
+ "generated names a construct inside QualityControlDials::STRUCTURAL_REFUSAL_BOUNDARY",
124
+ divergences: divergences, clean: divergences.empty?, observation: observation }
125
+ end
126
+
127
+ # `id:` addresses the Sweep; `sequence:` is the Check's own identity. The CLI's
128
+ # to.aggregate=/to.entity= split is a CLI-layer translation, not what in-process dispatch
129
+ # takes.
130
+ def mark_held!(sweep, sequence, observation)
131
+ @runtime.dispatch_flat("QualityControl::Sweep.Check.Held",
132
+ { id: sweep.id, sequence: { value: sequence }, observation: { value: observation } })
133
+ end
134
+
135
+ # `target:` is what the SuspendOnSurprise policy reads; the target is suspended inside this
136
+ # dispatch.
137
+ def mark_surprised!(sweep, sequence, observation, target_reference)
138
+ @runtime.dispatch_flat("QualityControl::Sweep.Check.Surprised",
139
+ { id: sweep.id, sequence: { value: sequence }, observation: { value: observation },
140
+ target: { value: target_reference } })
141
+ end
142
+
143
+ # One `PersistenceParity.diff` per seed against this sweep's disposable schema. A
144
+ # `PG::Error` or `WiringError` escaping it is a crash finding, never swallowed.
145
+ def persistence_parity_outcome(domain_path, steps, database, schema)
146
+ pair = @adapter_parity_pairs.fetch(:persistence_parity)
147
+ divergences = Hecks::Fuzzing::PersistenceParity.diff(domain_path, steps, left: pair.fetch(:left),
148
+ right: pair.fetch(:right),
149
+ database: database, schema: schema)
150
+ [divergences.empty? ? :clean : :divergence, divergences]
151
+ rescue StandardError => e
152
+ [:crash, [{ field: "process", detail: "#{e.class}: #{e.message}" }]]
153
+ end
154
+
155
+ # Memory vs SQLite through the same `PersistenceParity.diff`, run inside the primary seat's
156
+ # loop; an operational failure surfaces as a `process` finding.
157
+ def adapter_parity_sqlite_divergences(domain_path, steps)
158
+ pair = @adapter_parity_pairs.fetch(:adapter_parity_sqlite)
159
+ Hecks::Fuzzing::PersistenceParity.diff(domain_path, steps, left: pair.fetch(:left), right: pair.fetch(:right))
160
+ rescue StandardError => e
161
+ [{ field: "process", detail: "#{e.class}: #{e.message}" }]
162
+ end
163
+
164
+ # `ConcurrentDispatch.check` already returns the flat divergence shape and rescues to a
165
+ # `process` finding; `[]` means the sequence had no command step to race.
166
+ def concurrency_outcome(domain_path, steps, database, race_schema, reference_schema)
167
+ Hecks::Fuzzing::ConcurrentDispatch.check(domain_path, steps, database: database,
168
+ race_schema: race_schema,
169
+ reference_schema: reference_schema)
170
+ end
171
+
172
+ # Seedless, once per sweep: audits the target's own persisted lineage. Nothing to audit is a
173
+ # note, not a finding.
174
+ def era_boundary_check(domain_path)
175
+ result = Hecks::Fuzzing::EraBoundary.diverged_ancestor_writes(domain_path)
176
+ return unchecked_era_boundary(domain_path, result) unless result[:checked]
177
+
178
+ divergences =
179
+ if result[:diverged_total].positive?
180
+ [{ field: "era_boundary", breakdown: result[:breakdown],
181
+ detail: "#{result[:diverged_total]} post-cut write(s) across #{result[:era_count]} era(s) " \
182
+ "that nothing has ever merged forward — hecks merge_tail <this target's own domain " \
183
+ "path> is the fix; see this script's own header on `era_boundary` for the class of " \
184
+ "bug this is" }]
185
+ else
186
+ []
187
+ end
188
+
189
+ { mode: :era_boundary, subject: "[era_boundary] #{File.basename(domain_path)} — ancestor-era audit " \
190
+ "(#{result[:era_count]} era(s) on record)",
191
+ expectation: MODE_EXPECTATIONS.fetch(:era_boundary), divergences: divergences,
192
+ clean: divergences.empty?,
193
+ observation: divergences.empty? ? "no ancestor era holds an unmerged write" : "#{result[:diverged_total]} diverged" }
194
+ end
195
+
196
+ # Nothing to audit and could not audit stay apart: `not_applicable` logs no `Check`, while
197
+ # an error is a finding, so an unreachable database never counts as a held `Check` toward
198
+ # the streak.
199
+ def unchecked_era_boundary(domain_path, result)
200
+ if result[:kind] == :not_applicable
201
+ return { mode: :era_boundary, skip: true, observation: "not applicable — #{result[:reason]}" }
202
+ end
203
+
204
+ { mode: :era_boundary, subject: "[era_boundary] #{File.basename(domain_path)} — ancestor-era audit",
205
+ expectation: MODE_EXPECTATIONS.fetch(:era_boundary),
206
+ divergences: [{ field: "era_boundary_unchecked", detail: result[:reason] }], clean: false,
207
+ observation: "could not audit — #{result[:reason]}" }
208
+ end
209
+
210
+ # Runs one seed under every active mode: `{ steps:, trace:, checks: }`, one check per mode;
211
+ # logging is the loop's job. A generator crash is a finding too: a step broke the
212
+ # interpreter before replay.
213
+ def run_one_seed(seat, seed)
214
+ shape = "#{@steps_per_sequence} steps, adversarial #{@adversarial}, role_draw #{@role_draw}, " \
215
+ "dry_run #{@dry_run}"
216
+ plan = @campaign&.plan(seed)
217
+ shape += ", spliced" if plan&.spliced?
218
+ begin
219
+ trace = Hecks::Fuzzing::SequenceGenerator.trace(
220
+ @domain_path, seed: seed, steps: @steps_per_sequence, adversarial: @adversarial,
221
+ role_draw: @role_draw, dry_run: @dry_run, **(plan ? plan.generator_options : {})
222
+ )
223
+ steps = trace.steps
224
+ rescue StandardError => e
225
+ return { plan: plan, steps: [], checks: [generator_crash_check(seed, shape, e)] }
226
+ end
227
+
228
+ outcomes = seed_outcomes(seat, steps)
229
+ { plan: plan, steps: steps, trace: trace,
230
+ checks: outcomes.map { |mode_key, divergences| check_for(mode_key, @feature, seed, shape, divergences) } }
231
+ end
232
+
233
+ def generator_crash_check(seed, shape, error)
234
+ { mode: :generator, subject: "[generator] #{@feature} fuzz seed #{seed} (#{shape}) — sequence generation",
235
+ expectation: "SequenceGenerator builds every step without anything but a domain refusal escaping " \
236
+ "its own inline dispatch",
237
+ divergences: [{ field: "generator_crash",
238
+ detail: "#{error.class}: #{error.message}\n#{error.backtrace.first(6).join("\n")}" }],
239
+ clean: false, observation: "generator crashed: #{error.class}: #{error.message}" }
240
+ end
241
+
242
+ def seed_outcomes(seat, steps)
243
+ case seat
244
+ when :differential then diff_ruby_vs_rust(@differ, @domain_path, steps, @binary, modes: @active_modes)
245
+ when :persistence_parity
246
+ { persistence_parity: persistence_parity_outcome(@domain_path, steps, @parity_database,
247
+ @parity_schema).last }
248
+ when :concurrency
249
+ { concurrency: concurrency_outcome(@domain_path, steps, @concurrency_database, @race_schema,
250
+ @reference_schema) }
251
+ else ruby_only_outcome(@domain_path, steps, modes: @active_modes)
252
+ end
253
+ end
254
+
255
+ # Re-runs only the comparison that surprised; `seat` matters only for `self_consistency`,
256
+ # which reads the Rust binary's rehydration door under a differential seat.
257
+ def candidate_divergences(check_mode, seat, steps)
258
+ case check_mode
259
+ when :differential
260
+ diff_ruby_vs_rust(@differ, @domain_path, steps, @binary, modes: [:differential])[:differential]
261
+ when :properties_in_differential, :ruby_only
262
+ ruby_only_outcome(@domain_path, steps, modes: [])[:ruby_only]
263
+ when :self_consistency then self_consistency_candidate(seat, steps)
264
+ when :adapter_parity_sqlite then adapter_parity_sqlite_divergences(@domain_path, steps)
265
+ when :persistence_parity
266
+ persistence_parity_outcome(@domain_path, steps, @parity_database, @parity_schema).last
267
+ end || []
268
+ end
269
+
270
+ def self_consistency_candidate(seat, steps)
271
+ if seat == :differential
272
+ diff_ruby_vs_rust(@differ, @domain_path, steps, @binary, modes: [:self_consistency])[:self_consistency]
273
+ else
274
+ ruby_only_outcome(@domain_path, steps, modes: [:self_consistency])[:self_consistency]
275
+ end
276
+ end
277
+
278
+ # Keeps a candidate only while its `Shrinker.signature` still contains the original's; a
279
+ # raising candidate is a different finding, so it is rejected.
280
+ def shrink_check(check, seat, steps)
281
+ original = Hecks::Fuzzing::Shrinker.signature(check[:divergences])
282
+ result = Hecks::Fuzzing::Shrinker.call(steps, budget: @shrink_budget) do |candidate|
283
+ divergences = candidate_divergences(check[:mode], seat, candidate)
284
+ Hecks::Fuzzing::Shrinker.reproduces?(original, divergences)
285
+ rescue StandardError
286
+ false
287
+ end
288
+ { mode: check[:mode], steps: result.steps, attempts: result.attempts, exhausted: result.exhausted,
289
+ original_size: steps.size }
290
+ end
291
+
292
+ # Writes the shrunk steps in `hecks fuzz`'s `{name, note, steps}` shape so `hecks run` and
293
+ # `hecks check_conformance` replay it unchanged.
294
+ def write_shrunk!(shrunk, seed)
295
+ dir = File.join(@root, "tmp/qa-shrunk")
296
+ FileUtils.mkdir_p(dir)
297
+ path = File.join(dir, "#{filesystem_safe_component(@sweep_reference)}-#{shrunk[:mode]}.json")
298
+ File.write(path, JSON.pretty_generate(name: "#{@feature}-#{shrunk[:mode]}-shrunk",
299
+ note: "hecks quality_control ask run #{@sweep_reference} seed #{seed}, shrunk " \
300
+ "from #{shrunk[:original_size]} steps",
301
+ steps: shrunk[:steps]))
302
+ path
303
+ end
304
+
305
+ # Coverage-guided generation's corpus, one JSON file per (target, mode): `--all`'s waves can
306
+ # run a target's primary sweep and its persistence-parity/concurrency variants as separate
307
+ # `hecks quality_control ask run` processes within the same tick, and each explores a
308
+ # conceptually independent axis of the same domain, so each keeps its own file rather than
309
+ # sharing one that nothing here locks.
310
+ def coverage_corpus_path(target_reference, mode)
311
+ File.join(@coverage_corpus_dir, "#{filesystem_safe_component(target_reference)}-#{mode}.json")
312
+ end
313
+
314
+ # Nil when nothing was ever saved for this (target, mode), or the file cannot be parsed;
315
+ # either way the campaign starts from an empty corpus.
316
+ def load_coverage_state(path)
317
+ JSON.parse(File.read(path))
318
+ rescue Errno::ENOENT, JSON::ParserError
319
+ nil
320
+ end
321
+
322
+ def save_coverage_state!(path, campaign)
323
+ FileUtils.mkdir_p(File.dirname(path))
324
+ File.write(path, JSON.pretty_generate(campaign.to_h))
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end