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,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open3"
6
+ require_relative "../../../hecks"
7
+ require "hecks/rust_build"
8
+ require_relative "../../fuzzing"
9
+ require_relative "../../fuzzing/domain_generator"
10
+ require_relative "../../fuzzing/generated_domain_check"
11
+ require_relative "child"
12
+
13
+ module Hecks
14
+ module QualityControlCli
15
+ # The command behind `hecks quality_control check_generated_domains`: generates domains nobody
16
+ # wrote and checks them the way the rotation does (`USAGE` lists the forms: generate,
17
+ # `--blueprint`, `--source` and `--promote`).
18
+ #
19
+ # Exit codes: 0 every valid domain clean; 2 at least one finding; 1 operational error.
20
+ #
21
+ # Every generated domain is named `QaGenerated`, so each is checked in its own child process
22
+ # (`--check <dir>`, prints one `QA_GENERATED_RESULT <json>` line). Findings are shrunk twice:
23
+ # the domain, then the steps. `--rust` builds a scratch crate under `tmp/qa-generated/rust`.
24
+ # `--source` adopts a bluebook written elsewhere; a `HYPOTHESIS.md` beside it rides into
25
+ # `NOTES.md`. It never touches the QA ledger; a domain that does not boot is `invalid`, never a
26
+ # finding.
27
+ class QaGeneratedDomains
28
+ EXIT_OK = 0
29
+ EXIT_ERROR = 1
30
+ EXIT_FOUND = 2
31
+ RESULT_MARKER = "QA_GENERATED_RESULT "
32
+
33
+ USAGE = "usage: hecks quality_control check_generated_domains [--domains N] [--start SEED] " \
34
+ "[--forms a,b] [--seeds K] [--steps M] [--adversarial F] [--rust] [--shrink-budget B] " \
35
+ "[--domain-shrink-budget D]\n " \
36
+ "hecks quality_control check_generated_domains --source <file.bluebook> [--source …] [--rust] …\n " \
37
+ "hecks quality_control check_generated_domains --promote <finding-dir> --name <stress_domain_name>"
38
+
39
+ Generator = Hecks::Fuzzing::DomainGenerator
40
+
41
+ # Generates, checks and reports.
42
+ #
43
+ # @param argv [Array<String>] the flags in the usage line
44
+ # @param root [String] the repository root
45
+ # @param env [Hash{String => String}] `QA_GENERATED_DOMAINS_PER_TICK` overrides the count of
46
+ # `--from-dials`
47
+ # @return [Integer] 0 clean, 2 a finding, 1 an operational error
48
+ # @raise [SystemExit] on a bad argument or a missing file
49
+ def self.call(argv, root:, env: ENV)
50
+ new(root: root, env: env).call(argv)
51
+ end
52
+
53
+ # @param root [String] the repository root
54
+ # @param env [Hash{String => String}] `QA_GENERATED_DOMAINS_PER_TICK` overrides the count of
55
+ # `--from-dials`
56
+ def initialize(root:, env: ENV)
57
+ @root = root
58
+ @env = env
59
+ end
60
+
61
+ # @param argv [Array<String>] the flags in the usage line
62
+ # @return [Integer] the exit status
63
+ # @raise [SystemExit] on a bad argument or a missing file
64
+ def call(argv)
65
+ @options = parse(argv.dup)
66
+ return EXIT_OK if @options == :help
67
+ return check_one if @options[:check]
68
+ return promote if @options[:promote]
69
+ return EXIT_OK if @options[:from_dials] && !dials_on?
70
+
71
+ run
72
+ end
73
+
74
+ # Each flag that takes a value: the option it sets and how the value is read.
75
+ VALUE_FLAGS = {
76
+ "--domains" => [:domains, ->(v) { Integer(v) }], "--start" => [:start, ->(v) { Integer(v) }],
77
+ "--forms" => [:forms, ->(v) { v.split(",") }], "--seeds" => [:seeds, ->(v) { Integer(v) }],
78
+ "--steps" => [:steps, ->(v) { Integer(v) }], "--adversarial" => [:adversarial, ->(v) { Float(v) }],
79
+ "--shrink-budget" => [:shrink_budget, ->(v) { Integer(v) }],
80
+ "--domain-shrink-budget" => [:domain_shrink_budget, ->(v) { Integer(v) }],
81
+ "--check" => [:check, :itself.to_proc], "--binary" => [:binary, :itself.to_proc],
82
+ "--match" => [:match, ->(v) { JSON.parse(v) }], "--promote" => [:promote, :itself.to_proc],
83
+ "--name" => [:name, :itself.to_proc], "--blueprint" => [:blueprint, :itself.to_proc]
84
+ }.freeze
85
+
86
+ # Each flag that stands alone, and the option it turns on.
87
+ SWITCH_FLAGS = { "--rust" => :rust, "--from-dials" => :from_dials }.freeze
88
+
89
+ private
90
+
91
+ def parse(argv)
92
+ options = { domains: 3, start: nil, forms: nil, seeds: 5, steps: 25, adversarial: 0.3, rust: false,
93
+ shrink_budget: 200, domain_shrink_budget: 40, check: nil, binary: nil, match: nil,
94
+ promote: nil, name: nil, sources: [] }
95
+ until argv.empty?
96
+ arg = argv.shift
97
+ if %w[-h --help].include?(arg)
98
+ puts USAGE
99
+ return :help
100
+ end
101
+ parse_flag(options, arg, argv)
102
+ end
103
+ options
104
+ end
105
+
106
+ def parse_flag(options, arg, argv)
107
+ if VALUE_FLAGS.key?(arg)
108
+ key, reader = VALUE_FLAGS.fetch(arg)
109
+ options[key] = reader.call(argv.shift)
110
+ elsif SWITCH_FLAGS.key?(arg)
111
+ options[SWITCH_FLAGS.fetch(arg)] = true
112
+ elsif arg == "--source"
113
+ options[:sources] << argv.shift
114
+ else
115
+ abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
116
+ end
117
+ end
118
+
119
+ # `--check DIR`: one domain in this process, one result line.
120
+ def check_one
121
+ differ = nil
122
+ if @options[:binary]
123
+ require File.join(@root, "spec/support/rust_conformance_helpers")
124
+ differ = Class.new do
125
+ include RustConformanceHelpers
126
+
127
+ attr_reader :structural_skips
128
+
129
+ def initialize
130
+ @structural_skips = Set.new
131
+ end
132
+ end.new
133
+ end
134
+ result = Hecks::Fuzzing::GeneratedDomainCheck.run(
135
+ @options[:check], seeds: @options[:seeds], steps: @options[:steps], adversarial: @options[:adversarial],
136
+ binary: @options[:binary], differ: differ, match: @options[:match],
137
+ shrink_budget: @options[:shrink_budget]
138
+ )
139
+ puts "#{RESULT_MARKER}#{JSON.generate(result)}"
140
+ EXIT_OK
141
+ end
142
+
143
+ def camelize(name) = name.split("_").map(&:capitalize).join
144
+
145
+ def promote
146
+ name = @options[:name] or abort "#{USAGE}\n--promote needs --name"
147
+ unless name.match?(/\A[a-z][a-z0-9_]*\z/)
148
+ abort "--name must be a lowercase identifier (it becomes a Rust module and Cargo feature)"
149
+ end
150
+
151
+ source = File.join(@options[:promote], Generator::DIRECTORY, "bluebook", "#{Generator::DIRECTORY}.bluebook")
152
+ abort "no generated domain at #{source}" unless File.exist?(source)
153
+ target = File.join(@root, "qa/stress_domains", name)
154
+ abort "#{target} already exists — pick another --name" if File.exist?(target)
155
+
156
+ FileUtils.mkdir_p(File.join(target, "bluebook"))
157
+ File.write(File.join(target, "bluebook", "#{name}.bluebook"),
158
+ File.read(source).gsub(Generator::DOMAIN_NAME, camelize(name)))
159
+ File.write(File.join(target, "NOTES.md"), notes_for(name))
160
+ puts "promoted: qa/stress_domains/#{name}"
161
+ puts "next:"
162
+ puts " hecks quality_control judge_novelty qa/stress_domains/#{name}"
163
+ puts " hecks project_rust qa/stress_domains/#{name} # when the finding needs the Rust comparison"
164
+ # `hecks quality_control target.seed` derives targets from
165
+ # `Hecks::Corpus.rotation_targets`, so a hand-typed `target.identify` line is not printed:
166
+ # nobody ran it and the domain never
167
+ # entered the rotation.
168
+ puts " hecks quality_control target.seed # idempotent; picks this domain up from the corpus"
169
+ EXIT_OK
170
+ end
171
+
172
+ def notes_for(name)
173
+ blueprint = File.join(@options[:promote], "blueprint.json")
174
+ finding = File.join(@options[:promote], "finding.json")
175
+ recorded = File.exist?(blueprint) ? JSON.parse(File.read(blueprint)) : nil
176
+ hypothesis = recorded&.dig("hypothesis")
177
+ <<~NOTES
178
+ # #{name}
179
+
180
+ Promoted from a `hecks quality_control check_generated_domains` finding — #{recorded&.key?('source') ? 'written by `hecks quality_control mine_combinations`\' agent' : 'generated'}, not
181
+ hand-written. The bluebook is the minimal form of the domain that
182
+ surprised; `QaGenerated` was renamed to `#{camelize(name)}` and nothing
183
+ else changed.
184
+ #{"\n## Hypothesis\n\n#{hypothesis.strip}\n" if hypothesis}
185
+ ## Blueprint
186
+
187
+ ```json
188
+ #{recorded ? JSON.pretty_generate(recorded.except('source', 'hypothesis')) : '(not recorded)'}
189
+ ```
190
+
191
+ ## Finding
192
+
193
+ ```json
194
+ #{File.exist?(finding) ? File.read(finding).strip : '(not recorded)'}
195
+ ```
196
+ NOTES
197
+ end
198
+
199
+ def dial(name, fallback)
200
+ return fallback unless defined?(::QualityControlDials) && ::QualityControlDials.const_defined?(name)
201
+
202
+ ::QualityControlDials.const_get(name)
203
+ end
204
+
205
+ # `--from-dials` is how `hecks quality_control tick` runs this. The dials come from the text
206
+ # of the bluebook before `Hecks.bluebook`, so the ledger's Postgres is not needed.
207
+ # `QA_GENERATED_DOMAINS_PER_TICK` overrides the count for one run.
208
+ #
209
+ # @return [Boolean] false when the dials turn the step off, true once they are applied
210
+ def dials_on?
211
+ dials_path = File.join(@root, "lib/hecks/quality_control/quality_control.bluebook")
212
+ eval(File.read(dials_path).split(/^Hecks\.bluebook/).first, TOPLEVEL_BINDING, dials_path)
213
+ # rubocop:enable Security/Eval
214
+ per_tick = Integer(@env.fetch("QA_GENERATED_DOMAINS_PER_TICK", dial(:GENERATED_DOMAINS_PER_TICK, 0)))
215
+ if per_tick.zero?
216
+ puts "generated domains: off (QualityControlDials::GENERATED_DOMAINS_PER_TICK is 0)"
217
+ return false
218
+ end
219
+ @options.merge!(domains: per_tick, rust: dial(:GENERATED_DOMAINS_RUST, false),
220
+ seeds: dial(:GENERATED_DOMAIN_SEEDS, 5), adversarial: dial(:ADVERSARIAL_FRACTION, 0.3).to_f)
221
+ true
222
+ end
223
+
224
+ def child_check(dir, binary: nil, match: nil, shrink_budget: 0)
225
+ args = ["--check", dir, "--seeds", @options[:seeds].to_s, "--steps", @options[:steps].to_s,
226
+ "--adversarial", @options[:adversarial].to_s, "--shrink-budget", shrink_budget.to_s]
227
+ args += ["--binary", binary] if binary
228
+ args += ["--match", JSON.generate(match)] if match
229
+ output, status = Open3.capture2e(*Child.argv(@root, "qa_generated_domains", *args), chdir: @root)
230
+ line = output.lines.reverse.find { |candidate| candidate.start_with?(RESULT_MARKER) }
231
+ return JSON.parse(line.delete_prefix(RESULT_MARKER)) if line
232
+
233
+ { "status" => "error",
234
+ "error" => "child exited #{status.exitstatus} with no result: #{output.lines.last(5).join.strip}" }
235
+ end
236
+
237
+ def build_rust(dir)
238
+ project = Hecks::RustBuild.capture("project_rust", [File.expand_path(dir, @root)],
239
+ env: { "HECKS_RUST_DIR" => @scratch })
240
+ unless project.ok?
241
+ return [nil, { "mode" => "rust_projection", "detail" => "#{project.out}#{project.err}".lines.last(12).join }]
242
+ end
243
+
244
+ cargo, status = Open3.capture2e("cargo", "build", "--no-default-features", "--features",
245
+ Generator::DIRECTORY, chdir: @scratch)
246
+ return [nil, { "mode" => "rust_build", "detail" => cargo.lines.grep(/error/).first(12).join }] unless status.success?
247
+
248
+ binary = File.join(@scratch, "target/debug/rust-#{Generator::DIRECTORY}")
249
+ FileUtils.cp(File.join(@scratch, "target/debug/rust"), binary)
250
+ [binary, nil]
251
+ end
252
+
253
+ def sync_scratch!
254
+ FileUtils.mkdir_p(@scratch)
255
+ sources = %w[Cargo.toml Cargo.lock src].map { |entry| File.join(@root, "rust", entry) }
256
+ system("rsync", "-a", "--delete", "--exclude", "target", *sources, "#{@scratch}/") or
257
+ abort "rsync into #{@scratch} failed"
258
+ end
259
+
260
+ # Builds one candidate (when --rust) and runs a child check. A build failure is its own result
261
+ # shape, so a rust_build finding shrinks by "does it still fail to build".
262
+ def evaluate(blueprint, dir_root, match: nil, shrink_budget: 0)
263
+ dir = Generator.write(blueprint, dir_root)
264
+ binary = nil
265
+ if @options[:rust]
266
+ binary, failure = build_rust(dir)
267
+ if failure
268
+ return { "status" => "clean" } unless match.nil? || match["mode"] == failure["mode"]
269
+
270
+ return { "status" => "found", "mode" => failure["mode"], "signature" => [failure["mode"]], "dir" => dir,
271
+ "divergences" => [{ "field" => failure["mode"], "detail" => failure["detail"] }], "steps" => [] }
272
+ end
273
+ return { "status" => "clean" } if match && match["mode"].start_with?("rust_")
274
+ end
275
+ child_check(dir, binary: binary, match: match, shrink_budget: shrink_budget)
276
+ .merge("binary" => binary, "dir" => dir)
277
+ end
278
+
279
+ def shrink_domain(blueprint, found, root)
280
+ match = { "mode" => found["mode"], "signature" => found["signature"] }
281
+ current = blueprint
282
+ attempts = 0
283
+ loop do
284
+ accepted = false
285
+ Generator.shrink_candidates(current).each do |candidate|
286
+ break if attempts >= @options[:domain_shrink_budget]
287
+
288
+ attempts += 1
289
+ next unless evaluate(candidate, File.join(root, "candidate"), match: match)["status"] == "found"
290
+
291
+ current = candidate
292
+ accepted = true
293
+ break
294
+ end
295
+ break unless accepted && attempts < @options[:domain_shrink_budget]
296
+ end
297
+ [current, attempts]
298
+ end
299
+
300
+ def relative(path) = path.delete_prefix("#{@root}/")
301
+
302
+ def print_finding_header(report)
303
+ puts
304
+ puts "=" * 72
305
+ puts "GENERATED DOMAIN FOUND SOMETHING — domain seed #{report[:seed]}, forms #{report[:forms].join(' + ')}"
306
+ puts "=" * 72
307
+ puts "domain: #{relative(report[:dir])}"
308
+ puts " shrunk from #{report[:size_before]} to #{report[:size_after]} removable element(s) " \
309
+ "in #{report[:domain_attempts]} candidate check(s)"
310
+ puts "blueprint: #{relative(File.join(report[:root], 'blueprint.json'))}"
311
+ end
312
+
313
+ def print_finding(report)
314
+ found = report[:final]
315
+ options = report[:options]
316
+ print_finding_header(report)
317
+ puts "mode: #{found['mode']}"
318
+ puts "signature: #{found['signature'].join(', ')}"
319
+ if found["seed"]
320
+ puts "sequence: seed #{found['seed']} of --seeds #{options[:seeds]}, #{options[:steps]} steps, " \
321
+ "adversarial #{options[:adversarial]}"
322
+ end
323
+ print_shrunk(report, found["steps"], found["shrunk_steps"])
324
+ puts "promote: hecks quality_control check_generated_domains --promote " \
325
+ "#{relative(report[:root])} --name <stress_domain_name>"
326
+ puts
327
+ found["divergences"].each do |divergence|
328
+ puts "-- #{divergence['field']} --"
329
+ divergence.except("field").each do |key, value|
330
+ puts "#{key}: #{value.is_a?(String) ? value : JSON.generate(value)}"
331
+ end
332
+ puts
333
+ end
334
+ end
335
+
336
+ def print_shrunk(report, steps, shrunk)
337
+ return unless shrunk
338
+
339
+ puts "shrunk: #{steps.size} -> #{shrunk.size} step(s) — #{relative(report[:steps_file])}"
340
+ replay = if report[:binary]
341
+ "hecks check_conformance #{report[:dir]} script=#{report[:steps_file]} " \
342
+ "artifact=#{report[:binary]}"
343
+ else
344
+ "hecks run #{report[:dir]} #{report[:steps_file]}"
345
+ end
346
+ puts "replay: #{replay}"
347
+ shrunk.each_with_index do |step, index|
348
+ kind = %w[verb query dry_run].find { |key| step.key?(key) }
349
+ puts " #{index}: #{"#{kind} " unless kind == 'verb'}#{step[kind]} args: #{JSON.generate(step['args'])}"
350
+ end
351
+ end
352
+
353
+ def record_finding(blueprint, result, root)
354
+ minimal, attempts = shrink_domain(blueprint, result, root)
355
+ final_root = File.join(root, "minimal")
356
+ match = { "mode" => result["mode"], "signature" => result["signature"] }
357
+ final = evaluate(minimal, final_root, match: match, shrink_budget: @options[:shrink_budget])
358
+ unless final["status"] == "found"
359
+ minimal = blueprint
360
+ final = evaluate(blueprint, final_root, shrink_budget: @options[:shrink_budget])
361
+ end
362
+ File.write(File.join(final_root, "finding.json"), JSON.pretty_generate(final.except("dir", "binary")))
363
+ steps_file = File.join(final_root, "shrunk_steps.json")
364
+ note = "hecks quality_control check_generated_domains finding"
365
+ File.write(steps_file, JSON.pretty_generate(name: "qa_generated-shrunk", note: note,
366
+ steps: final.fetch("shrunk_steps") { final.fetch("steps", []) }))
367
+ { forms: blueprint["forms"], root: final_root, dir: final["dir"], binary: final["binary"], final: final,
368
+ options: @options, steps_file: steps_file, domain_attempts: attempts,
369
+ size_before: Generator.removals(blueprint).size, size_after: Generator.removals(minimal).size }
370
+ end
371
+
372
+ def blueprints(start)
373
+ if @options[:blueprint]
374
+ saved = JSON.parse(File.read(@options[:blueprint]))
375
+ { saved.fetch("seed", start) => saved }
376
+ elsif @options[:sources].any?
377
+ @options[:sources].each_with_index.to_h { |file, index| [start + index, source_blueprint(file, start + index)] }
378
+ else
379
+ (start...(start + @options[:domains])).to_h do |seed|
380
+ [seed, Generator.generate(seed: seed, forms: @options[:forms])]
381
+ end
382
+ end
383
+ end
384
+
385
+ def source_blueprint(file, seed)
386
+ abort "no bluebook at #{file}" unless File.file?(file)
387
+
388
+ hypothesis = File.join(File.dirname(file), "HYPOTHESIS.md")
389
+ { "seed" => seed, "forms" => ["source:#{File.basename(File.dirname(file))}"],
390
+ "origin" => File.expand_path(file), "source" => File.read(file),
391
+ "hypothesis" => (File.read(hypothesis) if File.exist?(hypothesis)),
392
+ "aggregates" => [], "policies" => [] }.compact
393
+ end
394
+
395
+ def run
396
+ start = @options[:start] || (Time.now.to_i % 1_000_000)
397
+ # `-<pid>`: two runs started in the same second must not share domain directories.
398
+ run_dir = File.join(@root, "tmp/qa-generated/run-#{start}-#{Process.pid}")
399
+ @scratch = File.join(@root, "tmp/qa-generated/rust")
400
+ @options[:domains] = @options[:sources].size if @options[:sources].any?
401
+ sync_scratch! if @options[:rust]
402
+ puts "generated domains: #{@options[:domains]} starting at seed #{start}; #{@options[:seeds]} seed(s) x " \
403
+ "#{@options[:steps]} steps each, adversarial #{@options[:adversarial]}" \
404
+ "#{', against Rust' if @options[:rust]} — #{relative(run_dir)}"
405
+
406
+ counts = Hash.new(0)
407
+ findings = []
408
+ # `--blueprint FILE` re-checks one saved blueprint through the same check/shrink/report
409
+ # path.
410
+ blueprints(start).each do |domain_seed, blueprint|
411
+ check_blueprint(domain_seed, blueprint, run_dir, counts, findings)
412
+ end
413
+ report(counts, findings)
414
+ end
415
+
416
+ def check_blueprint(domain_seed, blueprint, run_dir, counts, findings)
417
+ root = File.join(run_dir, domain_seed.to_s)
418
+ result = evaluate(blueprint, root)
419
+ label = "domain #{domain_seed} [#{blueprint['forms'].join('+')}]"
420
+ label += " #{blueprint['aggregates'].size} aggregate(s)" unless blueprint["source"]
421
+
422
+ case result["status"]
423
+ when "clean"
424
+ counts[:clean] += 1
425
+ puts " #{label}: clean (#{result['seeds_run']} seed(s))"
426
+ when "invalid", "error"
427
+ counts[result["status"].to_sym] += 1
428
+ puts " #{label}: #{result['status'].upcase} — #{result['error']}"
429
+ else
430
+ counts[:found] += 1
431
+ puts " #{label}: FOUND SOMETHING (#{result['mode']}: #{result['signature'].join(', ')}) — " \
432
+ "shrinking the domain…"
433
+ findings << record_finding(blueprint, result, root).merge(seed: domain_seed)
434
+ end
435
+ end
436
+
437
+ def report(counts, findings)
438
+ puts
439
+ puts "generated domains: #{counts[:clean]} clean, #{counts[:found]} found something, " \
440
+ "#{counts[:invalid]} invalid, #{counts[:error]} errored"
441
+ findings.each { |finding| print_finding(finding) }
442
+
443
+ if findings.any? then EXIT_FOUND
444
+ elsif counts[:clean].zero? then EXIT_ERROR
445
+ else EXIT_OK
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "../../../hecks"
5
+ # The era subsystem does not load with core; the `PostgresEra` binding needs it (ADR 0033).
6
+ require_relative "../../ports/persistence/plugins/era"
7
+
8
+ module Hecks
9
+ module QualityControlCli
10
+ # The command behind `hecks quality_control log`: logs a bug to the QualityControl ledger,
11
+ # proven, numbered and triaged. It runs `--demonstration` first and refuses unless it exits
12
+ # non-zero.
13
+ #
14
+ # hecks quality_control log --sweep <sweep-id> --title "…" \
15
+ # --demonstration "bundle exec rspec spec/x_spec.rb -e 'the case'" \
16
+ # --symptom "…" --expectation "…" --submitter "<who>" \
17
+ # --triage self_contained|bigger [--tag word]… [--reproduced yes|no]
18
+ #
19
+ # `--reproduced no` skips the run for findings that will not reproduce reliably; the
20
+ # demonstration must still look like runnable code, not prose. Exit 0: logged, with
21
+ # `logged BUG#n …` as the last stdout line. Exit 1: refused, nothing written.
22
+ class QaLogBug
23
+ EXIT_OK = 0
24
+
25
+ USAGE = "usage: hecks quality_control log --sweep <sweep-id> --title \"…\" " \
26
+ "--demonstration \"<command that must FAIL>\" --symptom \"…\" --expectation \"…\" " \
27
+ "--submitter \"<who>\" --triage self_contained|bigger [--tag word]… [--reproduced yes|no]"
28
+
29
+ DISPOSITIONS = %w[self_contained bigger].freeze
30
+ REPRODUCED = %w[yes no].freeze
31
+ VALUE_FLAGS = %w[--sweep --title --demonstration --symptom --expectation --submitter --triage
32
+ --reproduced].freeze
33
+
34
+ # Logs the bug.
35
+ #
36
+ # @param argv [Array<String>] the flags in the usage line
37
+ # @param root [String] the repository root, where the demonstration runs
38
+ # @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` points at another ledger
39
+ # @return [Integer] 0 once logged
40
+ # @raise [SystemExit] when the arguments are wrong or a rule refuses; nothing is written
41
+ def self.call(argv, root:, env: ENV)
42
+ new(root: root, env: env).call(argv)
43
+ end
44
+
45
+ # @param root [String] the repository root
46
+ # @param env [Hash{String => String}] `QA_SWEEP_DOMAIN_DIR` points at another ledger
47
+ def initialize(root:, env: ENV)
48
+ @root = root
49
+ @domain_dir = env.fetch("QA_SWEEP_DOMAIN_DIR", File.join(root, "qa/bluebook"))
50
+ end
51
+
52
+ # @param argv [Array<String>] the flags in the usage line
53
+ # @return [Integer] the exit status
54
+ # @raise [SystemExit] when the arguments are wrong or a rule refuses; nothing is written
55
+ def call(argv)
56
+ options = parse(argv.dup)
57
+ return EXIT_OK if options == :help
58
+
59
+ validate(options)
60
+ prove(options)
61
+ puts
62
+ runtime = boot_ledger
63
+ sweep = ::QualityControl::Sweep.find(options[:sweep])
64
+ unless sweep
65
+ abort "no such sweep: #{options[:sweep].inspect} — the sweep id is on hecks quality_control ask run's own " \
66
+ "FOUND SOMETHING report"
67
+ end
68
+
69
+ bug = mint_and_log!(runtime, sweep, options).triage!(disposition: { value: options[:triage] })
70
+ puts "logged #{bug.id} (sequence #{bug.sequence.value}, #{bug.disposition.value}, " \
71
+ "reproduced=#{bug.reproduced.value}) against #{sweep.id}: #{options[:title]}"
72
+ EXIT_OK
73
+ end
74
+
75
+ private
76
+
77
+ def parse(argv)
78
+ options = { tags: [], reproduced: "yes" }
79
+ until argv.empty?
80
+ arg = argv.shift
81
+ case arg
82
+ when "-h", "--help"
83
+ puts USAGE
84
+ return :help
85
+ when *VALUE_FLAGS
86
+ value = argv.shift
87
+ abort "#{USAGE}\n#{arg} needs a value" if value.nil? || value.empty?
88
+ options[arg.delete_prefix("--").to_sym] = value
89
+ when "--tag"
90
+ value = argv.shift
91
+ abort "#{USAGE}\n--tag needs a word" if value.nil? || value.empty?
92
+ options[:tags] << value
93
+ else
94
+ abort "#{USAGE}\nunexpected argument: #{arg.inspect}"
95
+ end
96
+ end
97
+ options
98
+ end
99
+
100
+ def validate(options)
101
+ %i[sweep title demonstration symptom expectation submitter triage].each do |key|
102
+ abort "#{USAGE}\n--#{key} is required" unless options[key]
103
+ end
104
+ unless DISPOSITIONS.include?(options[:triage])
105
+ abort "#{USAGE}\n--triage must be one of #{DISPOSITIONS.join('|')}, got #{options[:triage].inspect}"
106
+ end
107
+ return if REPRODUCED.include?(options[:reproduced])
108
+
109
+ abort "#{USAGE}\n--reproduced must be one of #{REPRODUCED.join('|')}, got #{options[:reproduced].inspect}"
110
+ end
111
+
112
+ # Only used with `--reproduced no`, which never runs the string: a file path, an interpreter
113
+ # or shell invocation, or a `Hecks::Fuzzing::Replay` snippet counts as code; prose does not.
114
+ def looks_runnable?(str)
115
+ first = str.to_s.strip.split(/\s+/).first.to_s
116
+ return true if first.start_with?("/") && File.exist?(first)
117
+ return true if File.exist?(File.expand_path(first, @root))
118
+ return true if first.match?(%r{\A(bundle|bin/|\./|rspec|ruby|sh|bash|zsh|python3?|node|cargo)\b})
119
+ return true if str.match?(/\.(rb|sh|py|rs|js)\b/)
120
+ return true if str.include?("Hecks::Fuzzing::Replay")
121
+
122
+ false
123
+ end
124
+
125
+ # Runs through `sh -c` from the repository root; the output tail prints either way.
126
+ def prove(options)
127
+ if options[:reproduced] == "no"
128
+ unless looks_runnable?(options[:demonstration])
129
+ abort "#{USAGE}\nrefused: --demonstration doesn't look like a runnable script or command — " \
130
+ "#{options[:demonstration].inspect}\n" \
131
+ "with --reproduced no there is no pass/fail signal to check, but the demonstration must " \
132
+ "still be real, saved, runnable code (a file path or a shell/ruby invocation), never " \
133
+ "prose describing what happened. Nothing was logged."
134
+ end
135
+ puts "reproduced=no — skipping the must-fail check (no reliable pass/fail signal for this finding)"
136
+ puts "demonstration (best-effort, not run): #{options[:demonstration]}"
137
+ return
138
+ end
139
+
140
+ out, status = Open3.capture2e("sh", "-c", options[:demonstration], chdir: @root)
141
+ tail = out.lines.last(15).join
142
+ if status.success?
143
+ puts tail
144
+ abort "refused: the demonstration PASSED (exit 0) — #{options[:demonstration].inspect}\n" \
145
+ "a bug is logged with the failing test that proves it; a passing command proves nothing. " \
146
+ "Nothing was logged."
147
+ end
148
+
149
+ puts "demonstration failed as required (exit #{status.exitstatus.inspect}):"
150
+ puts tail
151
+ end
152
+
153
+ def boot_ledger
154
+ Hecks.boot(@domain_dir)
155
+ rescue StandardError => e
156
+ abort "the QualityControl ledger did not boot — fix the ledger itself before logging against it " \
157
+ "(#{e.class}: #{e.message})"
158
+ end
159
+
160
+ # Re-read on every attempt: concurrent minters see the same maximum, and the loser's `Log` is
161
+ # refused (`AlreadyExists`) so `mint_and_log!` retries from a fresh read.
162
+ def next_reference(runtime)
163
+ rows = runtime.query("QualityControl::Bug.All")
164
+ taken = rows.map { |row| row[:reference][:value] }
165
+ sequence = rows.map { |row| row[:sequence][:value] }.max.to_i + 1
166
+ sequence += 1 while taken.include?("BUG##{sequence}")
167
+ [sequence, "BUG##{sequence}"]
168
+ end
169
+
170
+ # Retries up to three times: the reference can be taken between reading and logging.
171
+ def mint_and_log!(runtime, sweep, options)
172
+ 3.times do
173
+ sequence, reference = next_reference(runtime)
174
+ begin
175
+ return log(sweep, options, sequence, reference)
176
+ rescue Hecks::Runtime::AlreadyExists
177
+ puts "#{reference} was taken between reading Bug.All and logging — minting again"
178
+ end
179
+ end
180
+ abort "could not mint a free BUG# reference in three attempts — another minter is racing this one; " \
181
+ "run again"
182
+ end
183
+
184
+ def log(sweep, options, sequence, reference)
185
+ tags = options[:tags].empty? ? {} : { tags: options[:tags].map { |tag| { value: tag } } }
186
+ ::QualityControl::Bug.log!(
187
+ sweep: sweep.id, reference: { value: reference }, sequence: { value: sequence },
188
+ title: { value: options[:title] }, demonstration: { value: options[:demonstration] },
189
+ symptom: { value: options[:symptom] }, expectation: { value: options[:expectation] },
190
+ submitter: { value: options[:submitter] }, reproduced: { value: options[:reproduced] }, **tags
191
+ )
192
+ end
193
+ end
194
+ end
195
+ end