hecks 2.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +51 -6
  3. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  6. data/lib/hecks/adapters/driven/memory.rb +16 -3
  7. data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
  8. data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
  9. data/lib/hecks/adapters/driven/postgres.rb +4 -2
  10. data/lib/hecks/adapters/driven/prism.rb +41 -8
  11. data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
  12. data/lib/hecks/behaviors/expectations.rb +1 -1
  13. data/lib/hecks/behaviors.rb +1 -1
  14. data/lib/hecks/bench/cli.rb +62 -0
  15. data/lib/hecks/bench/environment.rb +71 -0
  16. data/lib/hecks/bench/postgres_probe.rb +30 -0
  17. data/lib/hecks/bench/report.rb +76 -0
  18. data/lib/hecks/bench/ruby_runner.rb +93 -0
  19. data/lib/hecks/bench/run.rb +55 -0
  20. data/lib/hecks/bench/rust_runner.rb +101 -0
  21. data/lib/hecks/bench/stats.rb +46 -0
  22. data/lib/hecks/bench/suite.rb +98 -0
  23. data/lib/hecks/bench/workload.rb +89 -0
  24. data/lib/hecks/bench.rb +17 -0
  25. data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
  26. data/lib/hecks/bluebook/attribute.rb +1 -1
  27. data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
  28. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
  29. data/lib/hecks/bluebook/chapter.rb +6 -2
  30. data/lib/hecks/bluebook/domain_port.rb +36 -5
  31. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
  32. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
  33. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
  34. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
  35. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
  36. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
  37. data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
  39. data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
  40. data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
  41. data/lib/hecks/bluebook/expression/resolver.rb +3 -2
  42. data/lib/hecks/bluebook/hexagon.rb +13 -5
  43. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
  44. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
  45. data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
  46. data/lib/hecks/bluebook/meta_validator.rb +9 -4
  47. data/lib/hecks/bluebook/model_check.rb +23 -0
  48. data/lib/hecks/bluebook/pattern_subset.rb +38 -0
  49. data/lib/hecks/bluebook/query.rb +20 -5
  50. data/lib/hecks/bluebook/smoke_test.rb +1 -1
  51. data/lib/hecks/canonical_json.rb +30 -0
  52. data/lib/hecks/chapters.rb +102 -0
  53. data/lib/hecks/cli/behaviors.rb +61 -0
  54. data/lib/hecks/cli/canonicalise.rb +25 -0
  55. data/lib/hecks/cli/check_engine_agreement.rb +32 -0
  56. data/lib/hecks/cli/check_era.rb +88 -0
  57. data/lib/hecks/cli/console.rb +66 -0
  58. data/lib/hecks/cli/deploy_template_diff.rb +40 -0
  59. data/lib/hecks/cli/doc_coverage.rb +32 -0
  60. data/lib/hecks/cli/document.rb +2 -2
  61. data/lib/hecks/cli/expression_projection.rb +41 -0
  62. data/lib/hecks/cli/follow.rb +88 -0
  63. data/lib/hecks/cli/history.rb +44 -0
  64. data/lib/hecks/cli/ir.rb +1 -1
  65. data/lib/hecks/cli/mcp.rb +3 -3
  66. data/lib/hecks/cli/model_check.rb +7 -4
  67. data/lib/hecks/cli/pattern_cases.rb +47 -0
  68. data/lib/hecks/cli/present.rb +39 -0
  69. data/lib/hecks/cli/project_cli.rb +224 -32
  70. data/lib/hecks/cli/project_diagrams.rb +4 -3
  71. data/lib/hecks/cli/project_glossary.rb +44 -0
  72. data/lib/hecks/cli/reference.rb +46 -0
  73. data/lib/hecks/cli/refresh_projections.rb +44 -0
  74. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
  75. data/lib/hecks/cli/release.rb +70 -0
  76. data/lib/hecks/cli/release_gem.rb +59 -0
  77. data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
  78. data/lib/hecks/cli/rspec_shard_files.rb +66 -0
  79. data/lib/hecks/cli/run.rb +3 -3
  80. data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
  81. data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
  82. data/lib/hecks/cli/shape.rb +70 -0
  83. data/lib/hecks/cli/smoke_http.rb +230 -0
  84. data/lib/hecks/cli/smoke_test.rb +5 -3
  85. data/lib/hecks/cli/spec_example.rb +27 -0
  86. data/lib/hecks/cli/statements.rb +32 -0
  87. data/lib/hecks/cli/stores.rb +4 -5
  88. data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
  89. data/lib/hecks/cli.rb +201 -52
  90. data/lib/hecks/codemod.rb +324 -0
  91. data/lib/hecks/corpus.rb +423 -0
  92. data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
  93. data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
  94. data/lib/hecks/deploy/oidc.json +82 -2
  95. data/lib/hecks/doc/reference.rb +351 -0
  96. data/lib/hecks/doors/cli_door.rb +196 -0
  97. data/lib/hecks/doors/cli_runner.rb +297 -0
  98. data/lib/hecks/{facade → doors}/command_request.rb +1 -1
  99. data/lib/hecks/{facade → doors}/handle.rb +2 -2
  100. data/lib/hecks/{facade → doors}/json_door.rb +4 -4
  101. data/lib/hecks/doors/launcher_options.rb +129 -0
  102. data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
  103. data/lib/hecks/doors/mcp_door_scope.rb +97 -0
  104. data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
  105. data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
  106. data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
  107. data/lib/hecks/doors.rb +61 -0
  108. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
  109. data/lib/hecks/embryonaut_bluebook.rb +1 -1
  110. data/lib/hecks/engine_agreement.rb +156 -0
  111. data/lib/hecks/facade.rb +24 -37
  112. data/lib/hecks/forms/app.rb +6 -6
  113. data/lib/hecks/forms/banking_presentation.rb +50 -0
  114. data/lib/hecks/forms/field_shape.rb +2 -1
  115. data/lib/hecks/forms/port_argument.rb +1 -1
  116. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
  117. data/lib/hecks/fuzzing/combination_miner.rb +86 -0
  118. data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
  119. data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
  120. data/lib/hecks/fuzzing/differential.rb +156 -0
  121. data/lib/hecks/fuzzing/domain_generator.rb +615 -0
  122. data/lib/hecks/fuzzing/era_boundary.rb +71 -0
  123. data/lib/hecks/fuzzing/form_census.rb +136 -0
  124. data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
  125. data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
  126. data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
  127. data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
  128. data/lib/hecks/fuzzing/outside_world.rb +45 -0
  129. data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
  130. data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
  131. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
  132. data/lib/hecks/fuzzing/properties/guards.rb +190 -0
  133. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
  134. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
  135. data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
  136. data/lib/hecks/fuzzing/properties/querying.rb +175 -0
  137. data/lib/hecks/fuzzing/properties.rb +162 -0
  138. data/lib/hecks/fuzzing/qa_settings.rb +123 -0
  139. data/lib/hecks/fuzzing/replay.rb +505 -0
  140. data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
  141. data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
  142. data/lib/hecks/fuzzing/self_consistency.rb +339 -0
  143. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
  144. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
  145. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
  146. data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
  147. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
  148. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
  149. data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
  150. data/lib/hecks/fuzzing/shrinker.rb +166 -0
  151. data/lib/hecks/fuzzing/structural_skips.rb +33 -0
  152. data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
  153. data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
  154. data/lib/hecks/fuzzing/value_generator.rb +216 -0
  155. data/lib/hecks/fuzzing.rb +23 -0
  156. data/lib/hecks/grammar/evolve.rb +336 -0
  157. data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
  158. data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
  159. data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
  160. data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
  161. data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
  162. data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
  163. data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
  164. data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
  165. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
  166. data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
  167. data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
  168. data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
  169. data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
  170. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
  171. data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
  172. data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
  173. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
  174. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
  175. data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
  176. data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
  177. data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
  178. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
  179. data/lib/hecks/hecks/adapters/git.adapter +7 -0
  180. data/lib/hecks/hecks/adapters/git.rb +102 -0
  181. data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
  182. data/lib/hecks/hecks/adapters/host_http.rb +65 -0
  183. data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
  184. data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
  185. data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
  186. data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
  187. data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
  188. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
  189. data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
  190. data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
  191. data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
  192. data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
  193. data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
  194. data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
  195. data/lib/hecks/hecks/adapters/local_files.rb +46 -0
  196. data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
  197. data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
  198. data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
  199. data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
  200. data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
  201. data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
  202. data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
  203. data/lib/hecks/hecks/adapters/shell.adapter +7 -0
  204. data/lib/hecks/hecks/adapters/shell.rb +49 -0
  205. data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
  206. data/lib/hecks/hecks/adapters/terminal.rb +74 -0
  207. data/lib/hecks/hecks/codebase.behaviors +172 -0
  208. data/lib/hecks/hecks/codebase.bluebook +2453 -0
  209. data/lib/hecks/hecks/context_map.hecksagon +13 -0
  210. data/lib/hecks/hecks/custodian.behaviors +179 -0
  211. data/lib/hecks/hecks/custodian.bluebook +1847 -0
  212. data/lib/hecks/hecks/deploy.behaviors +74 -0
  213. data/lib/hecks/hecks/deploy.world +12 -0
  214. data/lib/hecks/hecks/environments/memory.world +10 -0
  215. data/lib/hecks/hecks/governance.world +7 -0
  216. data/lib/hecks/hecks/hecks.behaviors +62 -0
  217. data/lib/hecks/hecks/hecks.bluebook +124 -0
  218. data/lib/hecks/hecks/hecks.hecksagon +390 -0
  219. data/lib/hecks/hecks/hecks.world +42 -0
  220. data/lib/hecks/kernel_capabilities.rb +194 -0
  221. data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
  222. data/lib/hecks/language/bluebook/query.bluebook +7 -0
  223. data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
  224. data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
  225. data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
  226. data/lib/hecks/mcp_stdio_guard.rb +2 -2
  227. data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
  228. data/lib/hecks/persistence_legacy_fixture.rb +113 -0
  229. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
  231. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
  237. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
  238. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
  239. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
  240. data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
  241. data/lib/hecks/ports/persistence/state_codec.rb +24 -5
  242. data/lib/hecks/projection_files.rb +285 -0
  243. data/lib/hecks/projections/bootstrap_table.rb +1 -1
  244. data/lib/hecks/projections/deploy/fargate.rb +5 -5
  245. data/lib/hecks/projections/deploy/lambda.rb +43 -32
  246. data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
  247. data/lib/hecks/projections/deploy/preview/template.rb +1 -1
  248. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
  249. data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
  250. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
  251. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
  252. data/lib/hecks/projections/deploy/shared.rb +1 -1
  253. data/lib/hecks/projections/diagrams.rb +3 -3
  254. data/lib/hecks/projections/model/deviations.rb +1 -1
  255. data/lib/hecks/projections/parser_table.rb +4 -4
  256. data/lib/hecks/projections/reference.rb +2 -3
  257. data/lib/hecks/projections/rust_vocabulary.rb +1 -1
  258. data/lib/hecks/projector/cli_projector.rb +106 -37
  259. data/lib/hecks/projector.rb +1 -1
  260. data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
  261. data/lib/hecks/quality_control/adapters/agent.rb +58 -0
  262. data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
  263. data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
  264. data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
  265. data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
  266. data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
  267. data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
  268. data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
  269. data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
  270. data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
  271. data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
  272. data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
  273. data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
  274. data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
  275. data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
  276. data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
  277. data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
  278. data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
  279. data/lib/hecks/quality_control/cli/child.rb +45 -0
  280. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
  281. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
  282. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
  283. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
  284. data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
  285. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
  286. data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
  287. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
  288. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
  289. data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
  290. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
  291. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
  292. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
  293. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
  294. data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
  295. data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
  296. data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
  297. data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
  298. data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
  299. data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
  300. data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
  301. data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
  302. data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
  303. data/lib/hecks/query_ir.rb +346 -0
  304. data/lib/hecks/query_ir_mcp.rb +198 -0
  305. data/lib/hecks/release/runner/ci_publisher.rb +2 -2
  306. data/lib/hecks/release/runner/clean_tree.rb +42 -0
  307. data/lib/hecks/release/runner/console.rb +6 -0
  308. data/lib/hecks/release/runner/gem_publisher.rb +14 -7
  309. data/lib/hecks/release/runner/npm_publisher.rb +6 -36
  310. data/lib/hecks/release/runner/preflight.rb +26 -2
  311. data/lib/hecks/release/runner/published.rb +8 -26
  312. data/lib/hecks/release/runner/tagger.rb +30 -15
  313. data/lib/hecks/release/runner.rb +57 -6
  314. data/lib/hecks/router/namespace_installer.rb +3 -3
  315. data/lib/hecks/runtime/adapter_lookup.rb +124 -0
  316. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
  317. data/lib/hecks/runtime/dependency_planning.rb +20 -2
  318. data/lib/hecks/runtime/dispatcher.rb +53 -3
  319. data/lib/hecks/runtime/interpreting.rb +3 -0
  320. data/lib/hecks/runtime/loader.rb +65 -12
  321. data/lib/hecks/runtime/outbox.rb +1 -1
  322. data/lib/hecks/runtime/policy_interpreter.rb +5 -2
  323. data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
  324. data/lib/hecks/runtime/query_interpreter.rb +77 -0
  325. data/lib/hecks/runtime/reaction_outcome.rb +89 -0
  326. data/lib/hecks/runtime/refusal_wording.rb +23 -1
  327. data/lib/hecks/runtime/registry/verification.rb +163 -4
  328. data/lib/hecks/runtime/registry.rb +57 -3
  329. data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
  330. data/lib/hecks/runtime/value/coercion.rb +23 -135
  331. data/lib/hecks/runtime/value/field_checks.rb +172 -0
  332. data/lib/hecks/runtime/value.rb +4 -1
  333. data/lib/hecks/runtime.rb +20 -6
  334. data/lib/hecks/rust_build/conformance.rb +158 -0
  335. data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
  336. data/lib/hecks/rust_build/coverage.rb +218 -0
  337. data/lib/hecks/rust_build/native_build.rb +124 -0
  338. data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
  339. data/lib/hecks/rust_build/project_rust.rb +332 -0
  340. data/lib/hecks/rust_build/wasm.rb +105 -0
  341. data/lib/hecks/rust_build/wasm_browser.rb +113 -0
  342. data/lib/hecks/rust_build.rb +144 -0
  343. data/lib/hecks/storehouse.rb +16 -16
  344. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
  345. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
  346. data/lib/hecks/three_zero/forms.yml +127 -0
  347. data/lib/hecks/three_zero.rb +12 -0
  348. data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
  349. data/lib/hecks/tools/comment_style.rb +1070 -0
  350. data/lib/hecks/tools/corpus_report.rb +87 -0
  351. data/lib/hecks/tools/deploy_recipe.rb +255 -0
  352. data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
  353. data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
  354. data/lib/hecks/tools/era_projection_backfill.rb +94 -0
  355. data/lib/hecks/tools/era_reattest.rb +146 -0
  356. data/lib/hecks/tools/evolve_run.rb +189 -0
  357. data/lib/hecks/tools/fuzz_sweep.rb +373 -0
  358. data/lib/hecks/tools/heki_compaction.rb +100 -0
  359. data/lib/hecks/tools/hoist_local_givens.rb +246 -0
  360. data/lib/hecks/tools/journal_compaction.rb +89 -0
  361. data/lib/hecks/tools/oidc_manifests.rb +70 -0
  362. data/lib/hecks/tools/query_ir_run.rb +81 -0
  363. data/lib/hecks/tools/regeneration_run.rb +166 -0
  364. data/lib/hecks/tools/rust_comment_style.rb +642 -0
  365. data/lib/hecks/tools/sequence_script.rb +62 -0
  366. data/lib/hecks/tools/tail_merge.rb +117 -0
  367. data/lib/hecks/tools/tenant_provisioning.rb +142 -0
  368. data/lib/hecks/tools/translation_audit.rb +208 -0
  369. data/lib/hecks/tools/translation_scaffold.rb +131 -0
  370. data/lib/hecks/tools.rb +86 -0
  371. data/lib/hecks/version.rb +1 -1
  372. data/lib/hecks.rb +32 -15
  373. data/qa/settings.yml +66 -0
  374. data/rust/.cargo/config.toml +6 -0
  375. data/rust/.gitignore +2 -0
  376. data/rust/Cargo.lock +54 -0
  377. data/rust/Cargo.toml +61 -0
  378. data/rust/build/.cargo/config.toml +3 -0
  379. data/rust/build/.gitignore +1 -0
  380. data/rust/build/Cargo.lock +7 -0
  381. data/rust/build/Cargo.toml +19 -0
  382. data/rust/build/src/build_artifact.rs +47 -0
  383. data/rust/build/src/cargo_sync.rs +248 -0
  384. data/rust/build/src/json.rs +421 -0
  385. data/rust/build/src/lineage_pass.rs +285 -0
  386. data/rust/build/src/main.rs +51 -0
  387. data/rust/build/src/optional_pass.rs +150 -0
  388. data/rust/build/src/pipeline.rs +264 -0
  389. data/rust/build/src/reserved_names.rs +76 -0
  390. data/rust/build/src/resolve.rs +124 -0
  391. data/rust/build/src/root.rs +25 -0
  392. data/rust/build/src/sidecars.rs +73 -0
  393. data/rust/build/src/subprocess.rs +51 -0
  394. data/rust/build/src/tmp.rs +31 -0
  395. data/rust/codegen/.cargo/config.toml +3 -0
  396. data/rust/codegen/.gitignore +1 -0
  397. data/rust/codegen/Cargo.lock +7 -0
  398. data/rust/codegen/Cargo.toml +20 -0
  399. data/rust/codegen/src/attr.rs +32 -0
  400. data/rust/codegen/src/bridging.rs +481 -0
  401. data/rust/codegen/src/commands.rs +1226 -0
  402. data/rust/codegen/src/constraints.rs +109 -0
  403. data/rust/codegen/src/dependency_planning.rs +209 -0
  404. data/rust/codegen/src/domain_generator.rs +1288 -0
  405. data/rust/codegen/src/exemplar.rs +230 -0
  406. data/rust/codegen/src/expr_emitter.rs +87 -0
  407. data/rust/codegen/src/fielded.rs +192 -0
  408. data/rust/codegen/src/hecks_naming.rs +33 -0
  409. data/rust/codegen/src/json.rs +335 -0
  410. data/rust/codegen/src/json_codec.rs +697 -0
  411. data/rust/codegen/src/literal.rs +207 -0
  412. data/rust/codegen/src/main.rs +290 -0
  413. data/rust/codegen/src/manifest.rs +227 -0
  414. data/rust/codegen/src/mutations.rs +784 -0
  415. data/rust/codegen/src/naming.rs +286 -0
  416. data/rust/codegen/src/ports.rs +208 -0
  417. data/rust/codegen/src/prelude.rs +116 -0
  418. data/rust/codegen/src/queries.rs +786 -0
  419. data/rust/codegen/src/reactions.rs +532 -0
  420. data/rust/codegen/src/read_models.rs +697 -0
  421. data/rust/codegen/src/reference_specs.rs +40 -0
  422. data/rust/codegen/src/registry.rs +966 -0
  423. data/rust/codegen/src/reserved_names.rs +76 -0
  424. data/rust/codegen/src/shared.rs +93 -0
  425. data/rust/codegen/src/skip_reason.rs +25 -0
  426. data/rust/codegen/src/types.rs +362 -0
  427. data/rust/host/.cargo/config.toml +3 -0
  428. data/rust/host/.gitignore +1 -0
  429. data/rust/host/Cargo.lock +4297 -0
  430. data/rust/host/Cargo.toml +61 -0
  431. data/rust/host/HECKS_RELEASE +1 -0
  432. data/rust/host/rds-ca-bundle.pem +2736 -0
  433. data/rust/host/src/api.rs +1596 -0
  434. data/rust/host/src/approval.rs +652 -0
  435. data/rust/host/src/auth.rs +1287 -0
  436. data/rust/host/src/bin/expr_harness.rs +46 -0
  437. data/rust/host/src/bin/lineage_harness.rs +119 -0
  438. data/rust/host/src/bin/mint_harness.rs +135 -0
  439. data/rust/host/src/checkout.rs +265 -0
  440. data/rust/host/src/dispatch.rs +1443 -0
  441. data/rust/host/src/expr_json.rs +1117 -0
  442. data/rust/host/src/field_hints.rs +52 -0
  443. data/rust/host/src/ir.rs +733 -0
  444. data/rust/host/src/journal.rs +874 -0
  445. data/rust/host/src/lambda_client.rs +378 -0
  446. data/rust/host/src/log.rs +147 -0
  447. data/rust/host/src/main.rs +474 -0
  448. data/rust/host/src/mint.rs +1865 -0
  449. data/rust/host/src/payments/keystore.rs +330 -0
  450. data/rust/host/src/payments/tests.rs +1400 -0
  451. data/rust/host/src/payments.rs +767 -0
  452. data/rust/host/src/presentation.rs +698 -0
  453. data/rust/host/src/presentation_write.rs +1431 -0
  454. data/rust/host/src/query_step.rs +149 -0
  455. data/rust/host/src/rate_limit.rs +904 -0
  456. data/rust/host/src/reference_transform.rs +241 -0
  457. data/rust/host/src/reference_validate.rs +338 -0
  458. data/rust/host/src/resend.rs +296 -0
  459. data/rust/host/src/secrets.rs +106 -0
  460. data/rust/host/src/server.rs +663 -0
  461. data/rust/host/src/storage_shape.rs +175 -0
  462. data/rust/host/src/test_pg.rs +40 -0
  463. data/rust/host/src/ui_schema.rs +1351 -0
  464. data/rust/host/src/wasm_runner.rs +191 -0
  465. data/rust/host/src/web/newsletter/cooldown.rs +126 -0
  466. data/rust/host/src/web/newsletter/tests.rs +104 -0
  467. data/rust/host/src/web/newsletter.rs +310 -0
  468. data/rust/host/src/web/newsletter_send.rs +357 -0
  469. data/rust/host/src/web/registrations.rs +1505 -0
  470. data/rust/host/src/web.rs +2937 -0
  471. data/rust/lsp/.gitignore +1 -0
  472. data/rust/lsp/Cargo.lock +7 -0
  473. data/rust/lsp/Cargo.toml +16 -0
  474. data/rust/lsp/README.md +133 -0
  475. data/rust/lsp/src/diagnostics.rs +165 -0
  476. data/rust/lsp/src/json.rs +385 -0
  477. data/rust/lsp/src/main.rs +399 -0
  478. data/rust/lsp/src/outline.rs +177 -0
  479. data/rust/lsp/src/rpc.rs +67 -0
  480. data/rust/parser/.cargo/config.toml +5 -0
  481. data/rust/parser/.gitignore +1 -0
  482. data/rust/parser/Cargo.lock +7 -0
  483. data/rust/parser/Cargo.toml +21 -0
  484. data/rust/parser/src/build/closed_sets.rs +27 -0
  485. data/rust/parser/src/build/identity.rs +276 -0
  486. data/rust/parser/src/build/mod.rs +12 -0
  487. data/rust/parser/src/build/naming.rs +141 -0
  488. data/rust/parser/src/build/pattern_subset.rs +161 -0
  489. data/rust/parser/src/build/query_derive.rs +72 -0
  490. data/rust/parser/src/build/query_inference.rs +269 -0
  491. data/rust/parser/src/build/query_options.rs +100 -0
  492. data/rust/parser/src/build/read_model.rs +94 -0
  493. data/rust/parser/src/build/references.rs +32 -0
  494. data/rust/parser/src/canonical.rs +114 -0
  495. data/rust/parser/src/diag.rs +55 -0
  496. data/rust/parser/src/emit.rs +953 -0
  497. data/rust/parser/src/expr/ast_json.rs +147 -0
  498. data/rust/parser/src/expr/evaluator.rs +81 -0
  499. data/rust/parser/src/expr/mod.rs +84 -0
  500. data/rust/parser/src/expr/resolver.rs +497 -0
  501. data/rust/parser/src/ir.rs +387 -0
  502. data/rust/parser/src/keywords.rs +585 -0
  503. data/rust/parser/src/lex.rs +662 -0
  504. data/rust/parser/src/main.rs +252 -0
  505. data/rust/parser/src/parse/aggregate.rs +612 -0
  506. data/rust/parser/src/parse/chapter.rs +517 -0
  507. data/rust/parser/src/parse/command.rs +637 -0
  508. data/rust/parser/src/parse/domain_port.rs +127 -0
  509. data/rust/parser/src/parse/entity.rs +527 -0
  510. data/rust/parser/src/parse/file.rs +67 -0
  511. data/rust/parser/src/parse/hecksagon.rs +231 -0
  512. data/rust/parser/src/parse/lifecycle.rs +162 -0
  513. data/rust/parser/src/parse/mod.rs +1262 -0
  514. data/rust/parser/src/parse/policy.rs +87 -0
  515. data/rust/parser/src/parse/process_manager.rs +305 -0
  516. data/rust/parser/src/parse/query.rs +100 -0
  517. data/rust/parser/src/parse/read_model.rs +214 -0
  518. data/rust/parser/src/parse/value_object.rs +250 -0
  519. data/rust/parser/src/ruby_value.rs +422 -0
  520. data/rust/project/bridging.rb +257 -0
  521. data/rust/project/commands.rb +877 -0
  522. data/rust/project/constraints.rb +86 -0
  523. data/rust/project/dependency_planning.rb +178 -0
  524. data/rust/project/domain_generator.rb +980 -0
  525. data/rust/project/exemplar.rb +148 -0
  526. data/rust/project/expr_emitter.rb +72 -0
  527. data/rust/project/fielded.rb +141 -0
  528. data/rust/project/json_codec.rb +641 -0
  529. data/rust/project/mutations.rb +721 -0
  530. data/rust/project/naming.rb +205 -0
  531. data/rust/project/ports.rb +65 -0
  532. data/rust/project/queries.rb +493 -0
  533. data/rust/project/reactions.rb +317 -0
  534. data/rust/project/read_models.rb +690 -0
  535. data/rust/project/reference_specs.rb +27 -0
  536. data/rust/project/registry.rb +396 -0
  537. data/rust/project/skip_reason.rb +29 -0
  538. data/rust/project/types.rb +225 -0
  539. data/rust/project/write_if_changed.rb +83 -0
  540. data/rust/project.rb +30 -0
  541. data/rust/project_rust_pipeline.rb +383 -0
  542. data/rust/src/exemplar/commands.rs +279 -0
  543. data/rust/src/exemplar/constraints.rs +18 -0
  544. data/rust/src/exemplar/fielded.rs +280 -0
  545. data/rust/src/exemplar/json.rs +277 -0
  546. data/rust/src/exemplar/mod.rs +17 -0
  547. data/rust/src/exemplar/mutations.rs +80 -0
  548. data/rust/src/exemplar/queries.rs +33 -0
  549. data/rust/src/exemplar/reactions.rs +88 -0
  550. data/rust/src/exemplar/read_models.rs +48 -0
  551. data/rust/src/exemplar/registry.rs +166 -0
  552. data/rust/src/exemplar/types.rs +44 -0
  553. data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
  554. data/rust/src/kernel/attribute_shapes/list.rs +18 -0
  555. data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
  556. data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
  557. data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
  558. data/rust/src/kernel/cli.rs +585 -0
  559. data/rust/src/kernel/dispatch.rs +1090 -0
  560. data/rust/src/kernel/expr.rs +458 -0
  561. data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
  562. data/rust/src/kernel/expression_operators/comparison.rs +48 -0
  563. data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
  564. data/rust/src/kernel/expression_operators/logical.rs +13 -0
  565. data/rust/src/kernel/expression_operators/membership.rs +18 -0
  566. data/rust/src/kernel/expression_operators/mod.rs +68 -0
  567. data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
  568. data/rust/src/kernel/expression_operators/positional.rs +126 -0
  569. data/rust/src/kernel/expression_operators/presence.rs +101 -0
  570. data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
  571. data/rust/src/kernel/expression_operators/sized.rs +35 -0
  572. data/rust/src/kernel/expression_operators/text.rs +79 -0
  573. data/rust/src/kernel/expression_operators/to_string.rs +18 -0
  574. data/rust/src/kernel/json.rs +779 -0
  575. data/rust/src/kernel/mod.rs +133 -0
  576. data/rust/src/kernel/named_query.rs +219 -0
  577. data/rust/src/kernel/orchestrate.rs +1810 -0
  578. data/rust/src/kernel/pattern.rs +307 -0
  579. data/rust/src/kernel/query_comparators.rs +206 -0
  580. data/rust/src/kernel/query_ordering.rs +103 -0
  581. data/rust/src/kernel/read_model.rs +914 -0
  582. data/rust/src/kernel/reference_lookup.rs +149 -0
  583. data/rust/src/kernel/repository.rs +468 -0
  584. data/rust/src/kernel/routing.rs +612 -0
  585. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
  586. data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
  587. data/rust/src/kernel/vocab/field_hint.rs +70 -0
  588. data/rust/src/kernel/vocab/mod.rs +20 -0
  589. data/rust/src/kernel/vocab/query_comparator.rs +65 -0
  590. data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
  591. data/rust/src/lib.rs +8 -0
  592. data/rust/src/main.rs +16 -0
  593. data/rust/web/.gitignore +1 -0
  594. data/rust/web/Cargo.lock +119 -0
  595. data/rust/web/Cargo.toml +18 -0
  596. data/rust/web/src/lib.rs +8 -0
  597. metadata +477 -12
  598. data/lib/hecks/facade/cli_door.rb +0 -96
  599. data/lib/hecks/facade/cli_runner.rb +0 -127
  600. data/lib/hecks/mcp_door_scope.rb +0 -95
@@ -0,0 +1,2224 @@
1
+ # Quality control: every check writes its expectation down before it looks at the answer.
2
+ # A bug is logged with the failing test that proves it, or it is not logged.
3
+
4
+ # Tunable dials for the QA loop: values in qa/settings.yml, reasoning here.
5
+ # `require`, not `require_relative`: isolated fuzz boots load a copy of this file from a tmpdir.
6
+ require "hecks/fuzzing/qa_settings"
7
+
8
+ unless defined?(QualityControlDials)
9
+ module QualityControlDials
10
+ # `DEFAULT_PATH` resolves from QaSettings' own location: this directory is copied for isolated
11
+ # boots. HECKS_QA_SETTINGS_PATH lets a subprocess test read a different settings file.
12
+ settings = Hecks::Fuzzing::QaSettings.load(ENV.fetch("HECKS_QA_SETTINGS_PATH", Hecks::Fuzzing::QaSettings::DEFAULT_PATH))
13
+
14
+ # Minimum gap in seconds between one tick's report and the next dispatch; 0 is immediately.
15
+ # Read by the `/loop hecks_qa` caller, not by any script.
16
+ CADENCE_SECONDS = settings.cadence_seconds
17
+
18
+ # Most PRs (Patch plus Improvement) `hecks quality_control patch.open` opens per calendar day; 0 is uncapped.
19
+ PR_CAP_PER_DAY = settings.pr_cap_per_day
20
+
21
+ # Fuzz depth by `Target.clean_streak`, in three discrete tiers, read by
22
+ # `SweepDepth.for_streak`. The last row (`upto: Float::INFINITY`) is the ceiling.
23
+ WIDENING_TIERS = settings.widening_tiers
24
+
25
+ # How many `hecks quality_control ask run <target>` children `--all` keeps running at once.
26
+ SWEEP_MAX_PARALLEL = settings.sweep_max_parallel
27
+
28
+ # How long the `/loop hecks_qa` caller waits for a tick's report before assuming it stalled.
29
+ LIVENESS_FALLBACK_SECONDS = settings.liveness_fallback_seconds
30
+
31
+ # The identity the loop runs as. `WaivedBy` repeats this literal (invariants read literals,
32
+ # not constants); `spec/quality_control_spec.rb` pins the two equal.
33
+ AUTOMATED_ENGINEER = "qa_sweep"
34
+
35
+ # How `hecks quality_control patch.open` opens a PR: not a draft, queued for auto-merge, on a branch that starts
36
+ # with BRANCH_PREFIX.
37
+ DRAFT_ONLY = settings.draft_only
38
+ AUTO_MERGE = settings.auto_merge
39
+ BRANCH_PREFIX = settings.branch_prefix
40
+
41
+ # Fraction (0.0..1.0) of generated command steps mutated adversarially; 0 turns the layer off.
42
+ # Higher finds refusal-ordering splits sooner but reaches deep state less often.
43
+ ADVERSARIAL_FRACTION = settings.adversarial_fraction
44
+
45
+ # Coverage-guided generation across one sweep's seeds (`Hecks::Fuzzing::CoverageCampaign`).
46
+ GUIDED_GENERATION = settings.guided_generation
47
+
48
+ # Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage.
49
+ # Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state
50
+ # tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost.
51
+ CORPUS_SPLICE_PROBABILITY = settings.corpus_splice_probability
52
+
53
+ # How many of the least-hit verbs the next seed weights up; 0 turns the lever off.
54
+ FAVOR_RARE_VERBS = settings.favor_rare_verbs
55
+
56
+ # Whether `hecks quality_control ask run` also runs the self-consistency pass (cold rehydration, replay
57
+ # idempotency, value-object JSON round trip) on the same generated sequence.
58
+ SELF_CONSISTENCY_CHECKS = settings.self_consistency_checks
59
+
60
+ # Candidate replays `hecks quality_control ask run` may spend shrinking one surprised check; 0 turns shrinking
61
+ # off. `concurrency` is never shrunk: a race does not reproduce on demand.
62
+ SHRINK_BUDGET = settings.shrink_budget
63
+
64
+ # Seconds of staleness that one point of yield score is worth in `RotationPriority.pick`.
65
+ # Zero would make yield invisible, so pick a number that lets it matter.
66
+ YIELD_WEIGHT_SECONDS = settings.yield_weight_seconds
67
+
68
+ # Percent of the prior yield score kept at each release, so recent findings dominate.
69
+ # Applied by the caller: the mutation language cannot express "decay, then add" in `Release`.
70
+ YIELD_DECAY_PERCENT = settings.yield_decay_percent
71
+
72
+ # Once a waiting target is this stale, `RotationPriority.pick` takes it whatever its yield
73
+ # score, so a quiet target cannot starve.
74
+ ROTATION_STALE_FLOOR_SECONDS = settings.rotation_stale_floor_seconds
75
+
76
+ # Ceiling on `--seeds` for `--persistence-parity`, which pays for real Postgres per dispatch.
77
+ # `hecks quality_control ask run` clamps down, never up, and prints a note.
78
+ PERSISTENCE_PARITY_SEED_CAP = settings.persistence_parity_seed_cap
79
+
80
+ # Ceiling on `--seeds` for the concurrency mode, which needs two disposable schemas per seed.
81
+ CONCURRENCY_SEED_CAP = settings.concurrency_seed_cap
82
+
83
+ # The two adapters each adapter-parity mode compares, by mode name; a new pairing is a new
84
+ # entry. `adapter_parity_sqlite` needs no `database:`/`schema:` because sqlite has no
85
+ # disposable database.
86
+ ADAPTER_PARITY_PAIRS = settings.adapter_parity_pairs
87
+
88
+ # Which comparison modes `hecks quality_control ask run` may run: the enabled ones intersected with what
89
+ # `TargetCapabilities.infer(path)` finds in the target. `--modes a,b,c` overrides for one run.
90
+ #
91
+ # `wasm_front` and `adapter_parity_postgres` are off because nothing runs them; the others are
92
+ # built and off because their I/O cost across the rotation is unmeasured. `hecks quality_control ask run`
93
+ # refuses to start when a mode outside `TargetCapabilities::RUNNABLE_MODES` is enabled.
94
+ MODES = settings.modes
95
+
96
+ # How often a generated role-gated command step carries a drawn caller (`role:`/`actor_id:`);
97
+ # 0 turns the draw off.
98
+ ROLE_DRAW_PROBABILITY = settings.role_draw_probability
99
+
100
+ # Fraction of generated command steps offered as a dry run instead of a real dispatch; 0 is
101
+ # off. Kept low: a dry run creates no state for later steps to act on.
102
+ DRY_RUN_FRACTION = settings.dry_run_fraction
103
+
104
+ # How many generated domains one `hecks quality_control tick` checks after the rotation sweep; 0 skips the
105
+ # step.
106
+ GENERATED_DOMAINS_PER_TICK = settings.generated_domains_per_tick
107
+
108
+ # Whether generated domains are also compared against a compiled Rust binary built in a scratch
109
+ # copy.
110
+ GENERATED_DOMAINS_RUST = settings.generated_domains_rust
111
+
112
+ # Generated sequences run per generated domain.
113
+ GENERATED_DOMAIN_SEEDS = settings.generated_domain_seeds
114
+
115
+ # Construct families a skipped query verb may belong to. A skip inside the list is logged Held,
116
+ # one outside it Surprised. Shrink-only: spec/fuzzing/rust_gap_manifest_spec.rb fails when a
117
+ # family here is declared by no committed manifest.
118
+ STRUCTURAL_REFUSAL_BOUNDARY = settings.structural_refusal_boundary
119
+ end
120
+ end
121
+
122
+ Hecks.bluebook "QualityControl" do
123
+ vision "A user should never be made to feel stupid by something that took what they said and quietly did otherwise. Every check writes down what it expected before it looks, every bug carries the test that proves it, and nothing reaches the outside world that nobody first tried to fix."
124
+
125
+ core
126
+
127
+ # The rotation and its lock: `Claim` is a lifecycle edge, so when two agents race for a chapter
128
+ # one transition wins and the other is refused.
129
+ aggregate "Target" do
130
+ description "A chapter that can be pressure-tested, and whose turn it is — the rotation, and the claim that stops two agents doing the same work twice."
131
+
132
+ identified_by :reference
133
+
134
+ attribute :reference, TargetReference
135
+ attribute :path, TargetPath
136
+ attribute :held_by, Engineer
137
+ # A number, not a timestamp type: the predicate sublanguage compares only numbers.
138
+ attribute :claimed_at, Instant
139
+ attribute :window, StaleAfter
140
+ attribute :last_swept, SweptIn
141
+ # Recency-decayed count of how often this chapter was worth sweeping; see `YieldScore`.
142
+ attribute :yield_score, YieldScore
143
+ # Consecutive `Release`s that came back clean; `hecks quality_control ask run` reads it to choose sweep depth.
144
+ # Zero means no streak yet, shared on purpose by a never-swept target and a just-surprised one.
145
+ attribute :clean_streak, CleanStreak
146
+ # Comma-joined mode names recorded at release from what the runner inferred. Stored so
147
+ # `EligibleFor` can answer why a mode never ran; inference at sweep time still decides.
148
+ attribute :capabilities, Capabilities
149
+ attribute :reason, TargetReason
150
+
151
+ value_object "TargetReference" do
152
+ attribute :value, String, pattern: '[^ \t\n\r]'
153
+ invariant("a target is referenced") { !value.to_s.empty? }
154
+ end
155
+
156
+ # Required: a name can be disputed, a path can be opened.
157
+ value_object "TargetPath" do
158
+ attribute :value, String, pattern: '[^ \t\n\r]'
159
+ invariant("a target says where it is") { !value.to_s.empty? }
160
+ end
161
+
162
+ value_object "Engineer" do
163
+ attribute :value, String, default: "nobody"
164
+ end
165
+
166
+ value_object "Instant" do
167
+ attribute :value, Integer, default: 0
168
+ invariant("an instant is not before the epoch") { !value.negative? }
169
+ end
170
+
171
+ # A number, not a string: "never" sorts after "SW-9" and inverts the rotation. Zero means
172
+ # never.
173
+ value_object "SweptIn" do
174
+ attribute :value, Integer, default: 0
175
+ invariant("a sweep time is not before the epoch") { !value.negative? }
176
+ end
177
+
178
+ # A count, not a clock. `default: 0` keeps rows written before this field hydrating.
179
+ value_object "CleanStreak" do
180
+ attribute :value, Integer, default: 0
181
+ invariant("a streak is not negative") { !value.negative? }
182
+ end
183
+
184
+ value_object "TargetReason" do
185
+ attribute :value, String, pattern: '[^ \t\n\r]'
186
+ invariant("shelving a target says why") { !value.to_s.empty? }
187
+ end
188
+
189
+ # Comma-joined in one string so `EligibleFor` can test it with `contains`.
190
+ # `default: ""` keeps rows written before this field booting.
191
+ value_object "Capabilities" do
192
+ attribute :value, String, default: ""
193
+ end
194
+
195
+ # One mode name, as `EligibleFor`'s argument; a query attribute, never stored.
196
+ value_object "Capability" do
197
+ attribute :value, String, pattern: '[^ \t\n\r]'
198
+ invariant("a mode is named") { !value.to_s.empty? }
199
+ end
200
+
201
+ # Recency-weighted, not lifetime: `RotationPriority.next_yield_score` decays it at each
202
+ # `Release` by `QualityControlDials::YIELD_DECAY_PERCENT`.
203
+ value_object "YieldScore" do
204
+ attribute :value, Integer, default: 0
205
+ invariant("a yield score is not negative") { !value.negative? }
206
+ end
207
+
208
+ # `suspended` is "surprised, waiting for a person": stale-claim arithmetic never reaches it, so
209
+ # a chapter with an unresolved surprise is not re-fuzzed until a `Release` puts it back.
210
+ lifecycle :status, default: "waiting" do
211
+ transition "Claim" => "held", from: ["waiting", "held"]
212
+ transition "Release" => "waiting", from: ["held", "suspended"]
213
+ transition "Suspend" => "suspended", from: "held"
214
+ transition "Shelve" => "shelved", from: ["waiting", "held"]
215
+ transition "Restore" => "waiting", from: "shelved"
216
+ end
217
+
218
+ command "Identify" do
219
+ role "QA engineer"
220
+ goal "Write down that a chapter exists which nobody has swept"
221
+
222
+ attribute :reference, TargetReference
223
+ attribute :path, TargetPath
224
+
225
+ emits "TargetIdentified"
226
+ end
227
+
228
+ # `from: ["waiting", "held"]` is deliberate: a held target is claimable once its claim is
229
+ # stale. The sublanguage has `+` and no `-`, so `claimed_at + window <= now` stands for `now -
230
+ # claimed_at >= window`. `now` arrives as an argument (Clock port) because a predicate cannot
231
+ # read the time.
232
+ command "Claim" do
233
+ role "QA engineer"
234
+ goal "Take the next chapter in the rotation, or one whose holder has gone quiet"
235
+
236
+ # The window lives on the record, not in an argument: a caller-supplied window would let any
237
+ # agent take a live claim by asking with a window of one second.
238
+ reference_to Target
239
+ attribute :held_by, Engineer
240
+ attribute :now, Instant
241
+
242
+ given("a live claim is not taken from the agent holding it") do
243
+ status != "held" || claimed_at.value + window.value <= now.value
244
+ end
245
+
246
+ sets :held_by
247
+ sets :claimed_at, to: :now
248
+
249
+ emits "TargetClaimed"
250
+ end
251
+
252
+ value_object "StaleAfter" do
253
+ attribute :value, Integer, default: 900
254
+ invariant("a staleness window is positive") { value.positive? }
255
+ end
256
+
257
+ command "Release" do
258
+ role "QA engineer"
259
+ goal "Hand a chapter back to the rotation, stamped with when, what this pass was worth, and how the clean streak moves"
260
+
261
+ # `now` is the argument and `last_swept` the field, so the door can fill the clock.
262
+ #
263
+ # `next_streak` is the caller's to compute (`clean_streak + 1` if every check held and none
264
+ # was ever surprised, else 0): `Target` cannot read `Sweep` state and no policy payload can
265
+ # carry it. Required, so an omitted streak can neither blank the counter nor guess at it.
266
+ reference_to Target
267
+ attribute :now, Instant
268
+ attribute :next_streak, CleanStreak
269
+ # Required: an in-process dispatch does not fill defaults for absent arguments, so an omitted
270
+ # list refuses rather than blanks. `capabilities.value=""` is an honest value.
271
+ attribute :capabilities, Capabilities
272
+
273
+ # Required, not defaulted to zero: a silent zero would erase what the pass found. The caller
274
+ # computes `RotationPriority.next_yield_score`; decay cannot be a second mutation here
275
+ # because build refuses a field written twice in one command.
276
+ attribute :yield_score, YieldScore
277
+
278
+ sets :last_swept, to: :now
279
+ sets :yield_score
280
+ sets :clean_streak, to: :next_streak
281
+ sets :capabilities
282
+
283
+ emits "TargetReleased"
284
+ end
285
+
286
+ # Dispatched by `SuspendOnSurprise` as `System`; `reason` is the literal that policy supplies.
287
+ command "Suspend" do
288
+ role "System"
289
+ goal "Take a chapter out of the rotation because a check surprised, until a person releases it"
290
+
291
+ reference_to Target
292
+ attribute :reason, TargetReason
293
+
294
+ sets :reason
295
+
296
+ emits "TargetSuspended"
297
+ end
298
+
299
+ command "Shelve" do
300
+ role "QA engineer"
301
+ goal "Decide a chapter is not worth the practice's time, and say why"
302
+
303
+ reference_to Target
304
+ attribute :reason, TargetReason
305
+
306
+ sets :reason
307
+
308
+ emits "TargetShelved"
309
+ end
310
+
311
+ # `reason` is required so the record's `reason` reads as the last decision in either direction.
312
+ command "Restore" do
313
+ role "QA engineer"
314
+ goal "Put a shelved chapter back in the rotation, and say what changed"
315
+
316
+ reference_to Target
317
+ attribute :reason, TargetReason
318
+
319
+ sets :reason
320
+
321
+ emits "TargetRestored"
322
+ end
323
+
324
+ # Not a lifecycle move, and reachable whatever the chapter's status: a recorded path can go
325
+ # stale (a worktree removed, a project relocated) at any point in the rotation, and fixing it
326
+ # should not have to wait on whatever status a stale claim or a pending sweep happens to leave
327
+ # the chapter in.
328
+ command "Relocate" do
329
+ role "QA engineer"
330
+ goal "Correct where a chapter's own domain actually lives, once its recorded path stops resolving"
331
+
332
+ reference_to Target
333
+ attribute :path, TargetPath
334
+
335
+ sets :path
336
+
337
+ emits "TargetRelocated"
338
+ end
339
+
340
+ # Ordered by `last_swept` alone: `order_by` sorts one stored field, and the yield-weighted
341
+ # blend needs `now`. `RotationPriority.pick` weights these rows in Ruby.
342
+ query "Rotation" do
343
+ description "Whose turn it is — waiting chapters, least recently swept first. The one reason this aggregate exists. Raw ordering only — see Hecks::Fuzzing::RotationPriority for the yield-weighted pick built on top of these same rows."
344
+ where(status: "waiting")
345
+ order_by :last_swept
346
+ end
347
+
348
+ query "Untouched" do
349
+ description "Never swept at all — zero is the epoch and means nobody has looked. The gap no count of checks can show you, because an unswept chapter leaves no rows anywhere."
350
+ where(status: "waiting")
351
+ where(:"last_swept.value" => 0)
352
+ order_by :reference
353
+ end
354
+
355
+ query "Held" do
356
+ description "Claimed right now, and by whom. Read it before wondering why the rotation looks short — a claim older than the window is free for the taking."
357
+ where(status: "held")
358
+ order_by :claimed_at
359
+ end
360
+
361
+ query "Shelved" do
362
+ description "Deliberately not swept, each with its reason. Re-read before anybody claims a coverage number."
363
+ where(status: "shelved")
364
+ order_by :reference
365
+ end
366
+
367
+ query "Suspended" do
368
+ description "Chapters a check surprised, waiting for a person — each with the reason the policy wrote. Nothing automatic touches these; hecks quality_control ask run <target> --release --notes is how one comes back."
369
+ where(status: "suspended")
370
+ order_by :reference
371
+ end
372
+
373
+ # Answers from the ledger which waiting chapters a mode could run against.
374
+ query "EligibleFor" do
375
+ description "Waiting chapters whose recorded capabilities include one mode — e.g. mode=postgres_era for the persistence-parity axis. The ledger's own answer to which chapters a mode can reach; inference at sweep time is still what decides."
376
+ attribute :mode, Capability
377
+ where(:"capabilities.value" => { contains: :mode })
378
+ where(status: "waiting")
379
+ order_by :last_swept
380
+ end
381
+
382
+ query "All" do
383
+ description "Every chapter ever written down — the inventory a runner walking the repository checks its findings against."
384
+ order_by :reference
385
+ end
386
+
387
+ # The commands that enrol, generate and judge chapters. TargetTools answers each by running the
388
+ # script's own command as a child process (lib/hecks/quality_control/adapters/target_tools.rb) and
389
+ # handing back what it printed; none reads or writes this aggregate's records, so each is a query.
390
+ # `arguments` is the command's own flags as one string, such as `--domains 3 --rust`.
391
+ value_object "Words" do
392
+ attribute :value, String, pattern: '[^\n\r]'
393
+ end
394
+
395
+ value_object "DomainPath" do
396
+ attribute :value, String, pattern: '[^\t\n\r]'
397
+
398
+ invariant("a domain is named by its path") { !value.to_s.empty? }
399
+ end
400
+
401
+ # An answer that is one document of text, exactly as the tool that produced it wrote it.
402
+ value_object "Document" do
403
+ attribute :text, String
404
+ end
405
+
406
+ query "Seed" do
407
+ description "Identify every corpus domain as a rotation target, printing each one's inferred capabilities; one already on file is skipped."
408
+ returns Document
409
+ end
410
+
411
+ query "CheckGeneratedDomains" do
412
+ description "Generate domains nobody wrote and check them the way the rotation does: a domain that does not boot is invalid, never a finding."
413
+ attribute :arguments, Words, optional: true
414
+ returns Document
415
+ end
416
+
417
+ query "MineCombinations" do
418
+ description "Ask an agent to mine the adversarial corpus for new domain combinations, then check the ones that boot; opt-in, since an agent call costs money."
419
+ attribute :arguments, Words, optional: true
420
+ returns Document
421
+ end
422
+
423
+ query "JudgeNovelty" do
424
+ description "Whether a new stress domain puts two forms together on one aggregate that no existing target does: the gate before it becomes a target."
425
+ attribute :domain, DomainPath
426
+ attribute :arguments, Words, optional: true
427
+ returns Document
428
+ end
429
+
430
+ query "DiscoverExternalDomains" do
431
+ description "Sibling-repo domains that already depend on the hecks gem but are not enrolled yet; it reports and never enrols."
432
+ attribute :arguments, Words, optional: true
433
+ returns Document
434
+ end
435
+ end
436
+
437
+ aggregate "Sweep" do
438
+ description "One agent's pass over one chapter — every check it made, what each expected, and what actually came back."
439
+
440
+ reference_to Target
441
+
442
+ identified_by :reference
443
+
444
+ attribute :reference, SweepReference
445
+ attribute :engineer, Engineer
446
+ attribute :made, CheckCount
447
+ attribute :notes, SweepNotes
448
+ # A counter, not a flag (a sweep can bend several gates) or a list (`where` cannot test
449
+ # emptiness).
450
+ attribute :waived, WaiverCount
451
+ # The signature beside the count. No default, since a default would be a forged signature;
452
+ # `optional: true` because a sweep that waived nothing has no signer.
453
+ attribute :waived_by, WaivedBy, optional: true
454
+ attribute :checks, list_of(Check)
455
+
456
+ value_object "SweepReference" do
457
+ attribute :value, String, pattern: '[^ \t\n\r]'
458
+ invariant("a sweep is referenced") { !value.to_s.empty? }
459
+ end
460
+
461
+ value_object "Engineer" do
462
+ attribute :value, String, pattern: '[^ \t\n\r]'
463
+ invariant("a sweep says who ran it") { !value.to_s.empty? }
464
+ end
465
+
466
+ value_object "CheckCount" do
467
+ attribute :value, Integer, default: 0
468
+ invariant("a check count is not negative") { !value.negative? }
469
+ end
470
+
471
+ value_object "WaiverCount" do
472
+ attribute :value, Integer, default: 0
473
+ invariant("a waiver count is not negative") { !value.negative? }
474
+ end
475
+
476
+ value_object "SweepNotes" do
477
+ attribute :value, String, pattern: '[^ \t\n\r]'
478
+ # The number is in the sentence because the sentence is the refusal message.
479
+ invariant("a sweep says what it learned, in at least 40 characters") { value.to_s.size >= 40 }
480
+ end
481
+
482
+ value_object "WaiverReason" do
483
+ attribute :value, String, pattern: '[^ \t\n\r]'
484
+ invariant("a waived gate says why") { !value.to_s.empty? }
485
+ end
486
+
487
+ # The literal "qa_sweep" is `QualityControlDials::AUTOMATED_ENGINEER`; invariants read
488
+ # literals, so `spec/quality_control_spec.rb` pins the two equal. "nobody" is refused: unsigned
489
+ # is anonymous.
490
+ value_object "WaivedBy" do
491
+ attribute :value, String, pattern: '[^ \t\n\r]'
492
+ invariant("a gate is waived by a person, never by the loop") { value != "qa_sweep" }
493
+ invariant("a waiver is signed") { value != "nobody" }
494
+ end
495
+
496
+ # Restated on a surprise so the event carries it: an event holds only its dispatched arguments,
497
+ # and `SuspendOnSurprise` needs a `Target` to address.
498
+ value_object "TargetRef" do
499
+ attribute :value, String
500
+ end
501
+
502
+ value_object "CheckSequence" do
503
+ attribute :value, Integer
504
+ invariant("a check sequence is positive") { value.positive? }
505
+ end
506
+
507
+ # Plain text, no pattern: a test file is newlines by construction and tool output is whatever
508
+ # the tool said.
509
+ value_object "Listing" do
510
+ attribute :value, String
511
+ end
512
+
513
+ value_object "Subject" do
514
+ attribute :value, String, pattern: '[^ \t\n\r]'
515
+ invariant("a check names what it put to the system") { !value.to_s.empty? }
516
+ end
517
+
518
+ # Written before the answer exists: an expectation fitted to the answer cannot be surprised.
519
+ value_object "Expectation" do
520
+ attribute :value, String, pattern: '[^ \t\n\r]'
521
+ invariant("a check says what it expected") { !value.to_s.empty? }
522
+ end
523
+
524
+ value_object "Observation" do
525
+ attribute :value, String
526
+ end
527
+
528
+ # One comparison. An entity: a check has its own identity, verbs and lifecycle, and only makes
529
+ # sense inside the sweep that made it. `Surprising` answers "every surprising check, any
530
+ # sweep".
531
+ entity "Check" do
532
+ description "One dispatch made against a written-down expectation, and what actually came back."
533
+
534
+ identified_by :sequence
535
+
536
+ attribute :sequence, CheckSequence
537
+ attribute :subject, Subject
538
+ attribute :expectation, Expectation
539
+ attribute :observation, Observation, optional: true
540
+ # Sticky: `outcome` is the current verdict and `Remake` resets it, but `Release`'s
541
+ # `next_streak` needs "did anything ever surprise this sweep", which a remake must not erase.
542
+ # `one_of` rather than a Boolean, which this chapter has no precedent for.
543
+ attribute :ever_surprised, one_of("no", "yes"), default: { value: "no" }
544
+
545
+ # `surprising` is not `failed`: it covers the crash and the quiet divergence without
546
+ # prejudging which.
547
+ lifecycle :outcome, default: "made" do
548
+ transition "Held" => "held", from: "made"
549
+ transition "Surprised" => "surprising", from: "made"
550
+ transition "Unsettled" => "unsettled", from: "made"
551
+ transition "Remake" => "made", from: ["held", "surprising", "unsettled"]
552
+ end
553
+
554
+ command "Held" do
555
+ role "QA engineer"
556
+ goal "Record that the system did what the chapter promised"
557
+
558
+ attribute :sequence, CheckSequence
559
+ attribute :observation, Observation
560
+
561
+ sets :observation
562
+
563
+ emits "CheckHeld"
564
+ end
565
+
566
+ command "Surprised" do
567
+ role "QA engineer"
568
+ goal "Record that the system did something other than what was written down"
569
+
570
+ attribute :sequence, CheckSequence
571
+ attribute :observation, Observation
572
+ # Restated so `CheckSurprised` carries a `Target` id for `SuspendOnSurprise`; see
573
+ # `TargetRef`. Required: a caller that omits it is refused (`AbsentArgument`) rather than
574
+ # suspending nothing.
575
+ attribute :target, TargetRef
576
+
577
+ sets :observation
578
+ # A literal, set here and nowhere else; see `ever_surprised`.
579
+ sets :ever_surprised, to: "yes"
580
+
581
+ emits "CheckSurprised"
582
+ end
583
+
584
+ command "Unsettled" do
585
+ role "QA engineer"
586
+ goal "Record a check that ran and settled nothing"
587
+
588
+ attribute :sequence, CheckSequence
589
+ attribute :observation, Observation
590
+
591
+ sets :observation
592
+
593
+ emits "CheckUnsettled"
594
+ end
595
+
596
+ command "Remake" do
597
+ role "QA engineer"
598
+ goal "Put a settled check back, against a system that has since changed"
599
+
600
+ attribute :sequence, CheckSequence
601
+
602
+ emits "CheckRemade"
603
+ end
604
+
605
+ query "Surprising" do
606
+ description "Every check the system surprised, across every sweep — the raw material a bug is written from, and the only list here that is supposed to be interesting."
607
+ where(outcome: "surprising")
608
+ order_by :sequence
609
+ end
610
+
611
+ query "Unsettled" do
612
+ description "Made and never ruled on, plus the runs that settled nothing. A sweep concluded with rows here concluded early."
613
+ where(outcome: { in: "made,unsettled" })
614
+ order_by :sequence
615
+ end
616
+
617
+ query "ForSubject" do
618
+ description "Everything ever put to one verb, across every sweep. Count the rows and that is its coverage."
619
+ attribute :subject, Subject
620
+ where(subject: :subject)
621
+ order_by :sequence
622
+ end
623
+ end
624
+
625
+ lifecycle :status, default: "sweeping" do
626
+ transition "Check" => "sweeping", from: "sweeping"
627
+ transition "Waive" => "sweeping", from: "sweeping"
628
+ transition "Conclude" => "concluded", from: "sweeping"
629
+ transition "Abandon" => "abandoned", from: "sweeping"
630
+ end
631
+
632
+ command "Open" do
633
+ role "QA engineer"
634
+ goal "Begin a pass over one chapter"
635
+
636
+ reference_to Target
637
+ attribute :reference, SweepReference
638
+ attribute :engineer, Engineer
639
+
640
+ emits "SweepOpened"
641
+ end
642
+
643
+ command "Check" do
644
+ role "QA engineer"
645
+ goal "Put one thing to the system, saying what it should do before looking"
646
+
647
+ reference_to Sweep
648
+ attribute :subject, Subject
649
+ attribute :expectation, Expectation
650
+
651
+ # `sequence` is absent because `MutationApplier#entity_element` mints it as `size + 1`.
652
+ # `observation` is absent on purpose: a check is written down when made, carrying only its
653
+ # expectation, so an expectation cannot be fitted to an answer after the fact.
654
+ sets :checks, append: { subject: :subject, expectation: :expectation }
655
+ sets :made, increment: { value: 1 }
656
+
657
+ ensures("checking never loses a check") { made.value == old.made.value + 1 }
658
+
659
+ emits "CheckMade"
660
+ end
661
+
662
+ # Waiving is countable: `Sweep.Waived` can be read afterwards, so going round a gate has a
663
+ # price.
664
+ command "Waive" do
665
+ role "QA engineer"
666
+ goal "Go round a gate, on the record, signed by a person"
667
+
668
+ reference_to Sweep
669
+ attribute :reason, WaiverReason
670
+ # `WaivedBy` refuses "qa_sweep" and "nobody", so only a person can sign.
671
+ attribute :waived_by, WaivedBy
672
+
673
+ sets :waived, increment: { value: 1 }
674
+ sets :waived_by
675
+
676
+ ensures("a waiver is never silent") { waived.value == old.waived.value + 1 }
677
+
678
+ emits "GateWaived"
679
+ end
680
+
681
+ command "Conclude" do
682
+ role "QA engineer"
683
+ goal "End a pass and say what it taught"
684
+
685
+ reference_to Sweep
686
+ attribute :notes, SweepNotes
687
+
688
+ # A sweep that checked nothing tested nothing; the `waived` clause makes this a gate, not a
689
+ # wall.
690
+ given("a sweep that checked nothing tested nothing") { made.value.positive? || waived.value.positive? }
691
+
692
+ sets :notes
693
+
694
+ emits "SweepConcluded"
695
+ end
696
+
697
+ command "Abandon" do
698
+ role "QA engineer"
699
+ goal "End a pass whose output should not be trusted"
700
+
701
+ reference_to Sweep
702
+ emits "SweepAbandoned"
703
+ end
704
+
705
+ query "Sweeping" do
706
+ description "Passes live right now — one per agent, and the reason a target shows as held."
707
+ where(status: "sweeping")
708
+ order_by :reference
709
+ end
710
+
711
+ query "Waived" do
712
+ description "Sweeps that went round a gate. Read as a fraction of all of them: a practice whose waiver rate is climbing is not moving faster, it is agreeing with itself less."
713
+ where(:"waived.value" => { gt: 0 })
714
+ order_by :reference
715
+ end
716
+
717
+ query "All" do
718
+ description "Every pass ever made — what a runner mints the next reference from."
719
+ order_by :reference
720
+ end
721
+
722
+ # The commands that work a whole pass. SweepTools answers each by running the script's own command
723
+ # as a child process (lib/hecks/quality_control/adapters/sweep_tools.rb) and handing back what it
724
+ # printed: a run that found something is an answer, an operational error a refusal with its
725
+ # report. They log their own checks through the commands above; none of these queries reads this
726
+ # aggregate's records. `arguments` is the command's own flags as one string, such as `--all`.
727
+ value_object "Words" do
728
+ attribute :value, String, pattern: '[^\n\r]'
729
+ end
730
+
731
+ value_object "DomainPath" do
732
+ attribute :value, String, pattern: '[^\t\n\r]'
733
+
734
+ invariant("a domain is named by its path") { !value.to_s.empty? }
735
+ end
736
+
737
+ value_object "DataPath" do
738
+ attribute :value, String, pattern: '[^\t\n\r]'
739
+
740
+ invariant("a data directory is named by its path") { !value.to_s.empty? }
741
+ end
742
+
743
+ value_object "DatabaseName" do
744
+ attribute :value, String, pattern: '[^ \t\n\r]'
745
+
746
+ invariant("a database is named") { !value.to_s.empty? }
747
+ end
748
+
749
+ value_object "RoleName" do
750
+ attribute :value, String, pattern: '[^ \t\n\r]'
751
+
752
+ invariant("a role is named") { !value.to_s.empty? }
753
+ end
754
+
755
+ value_object "SchemaName" do
756
+ attribute :value, String, pattern: '[^ \t\n\r]'
757
+
758
+ invariant("a schema is named") { !value.to_s.empty? }
759
+ end
760
+
761
+ value_object "StepVerb" do
762
+ attribute :value, String, pattern: '[^ \t\n\r]'
763
+
764
+ invariant("a step names its verb") { !value.to_s.empty? }
765
+ end
766
+
767
+ # A step's arguments as JSON text, which is how the racer's command line carries them.
768
+ value_object "StepArguments" do
769
+ attribute :value, String
770
+
771
+ invariant("a step carries its arguments, `{}` for none") { !value.to_s.empty? }
772
+ end
773
+
774
+ # An answer that is one document of text, exactly as the tool that produced it wrote it.
775
+ value_object "Document" do
776
+ attribute :text, String
777
+ end
778
+
779
+ query "Tick" do
780
+ description "One QA tick: a clean tree, a rebase on origin/main, the PR check, a sweep of the whole rotation and the generated domains, then one report. It never logs a bug, releases a hold or opens a PR."
781
+ returns Document
782
+ end
783
+
784
+ query "Run" do
785
+ description "One claim, sweep, conclude and release cycle against a target (the rotation's pick when none is named), or every waiting target with `--all`; the first surprised check ends it with a report."
786
+ attribute :target, TargetRef, optional: true
787
+ attribute :arguments, Words, optional: true
788
+ returns Document
789
+ end
790
+
791
+ query "MigrateLedgerFromHeki" do
792
+ description "Copy each aggregate's current state from a Heki data directory into the repository the domain's bindings name today; a dry run unless `--force` is in the arguments, and an id held under a different state is always refused."
793
+ attribute :domain, DomainPath
794
+ attribute :data, DataPath
795
+ attribute :arguments, Words, optional: true
796
+ returns Document
797
+ end
798
+
799
+ query "CreateLedgerRole" do
800
+ description "Create the ordinary Postgres role the ledger connects as and make it own the database; idempotent, and refuses a superuser or BYPASSRLS role, which the era write-fence cannot bind."
801
+ attribute :database, DatabaseName
802
+ attribute :role, RoleName, optional: true
803
+ returns Document
804
+ end
805
+
806
+ # Runs only as a child process a concurrency sweep starts, two at a time, so it is not something a
807
+ # person types.
808
+ query "Race" do
809
+ description "One racer of a concurrent dispatch: boot a domain against a schema and dispatch one step; the answer is `succeeded`, `refused` or `crashed:<class>: <message>`."
810
+ attribute :domain, DomainPath
811
+ attribute :database, DatabaseName
812
+ attribute :schema, SchemaName
813
+ attribute :verb, StepVerb
814
+ attribute :step_arguments, StepArguments
815
+ returns Document
816
+ end
817
+ end
818
+
819
+ # One failing test, one bug, and the test comes first: `Log` requires the demonstration, so a bug
820
+ # that cannot be shown cannot be written down. Hence no `found` state and no `Failure` record.
821
+ aggregate "Bug" do
822
+ description "One thing now known to be wrong, the test that proves it, and how far the fix got."
823
+
824
+ reference_to Sweep
825
+
826
+ identified_by :reference
827
+
828
+ attribute :reference, BugReference
829
+ attribute :sequence, BugSequence
830
+ attribute :title, BugTitle
831
+ attribute :demonstration, Demonstration
832
+ attribute :symptom, Symptom
833
+ attribute :expectation, Expectation
834
+ # Who reported it. Distinct from `held_by` (who is working on it now); a submitter never
835
+ # changes.
836
+ attribute :submitter, Submitter
837
+ attribute :order, BugOrder
838
+ attribute :tags, list_of(Tag)
839
+ attribute :site, BugSite
840
+ attribute :cause, BugCause
841
+ attribute :commit, CommitRef
842
+ attribute :verification, Verification
843
+ attribute :reason, BugReason
844
+ attribute :next_step, NextStep
845
+ attribute :waived, WaiverCount
846
+ # The same signature as `Sweep.waived_by`; absent until a person waives.
847
+ attribute :waived_by, WaivedBy, optional: true
848
+ # A short memorable handle such as `replace-entries-duplicate-identity`. Optional with no
849
+ # default: only a person reading an old bug can write a good slug.
850
+ attribute :name, BugName, optional: true
851
+ # Whether the bug is fixable on the spot or bigger; `Triage` is the only mover.
852
+ # `default:` keeps rows already on file hydrating as `undecided`.
853
+ attribute :disposition, one_of("undecided", "self_contained", "bigger"), default: { value: "undecided" }
854
+ # `default: yes` is true of every bug on file, since `Log` always required a failing
855
+ # demonstration. `no` is the escape hatch for a real but flaky finding (`hecks quality_control log
856
+ # --reproduced no`).
857
+ attribute :reproduced, one_of("yes", "no"), default: { value: "yes" }
858
+ # Bugs are claimed like chapters so two agents do not fix the same one. A `given` rather than a
859
+ # lifecycle edge: holding a bug is orthogonal to how far the fix got.
860
+ attribute :held_by, Engineer
861
+ attribute :claimed_at, Instant
862
+ attribute :window, StaleAfter
863
+
864
+ value_object "BugReference" do
865
+ attribute :value, String, pattern: '[^ \t\n\r]'
866
+ invariant("a bug is referenced") { !value.to_s.empty? }
867
+ end
868
+
869
+ value_object "BugSequence" do
870
+ attribute :value, Integer
871
+ invariant("a bug sequence is positive") { value.positive? }
872
+ end
873
+
874
+ # Prefixed on purpose: `Doors::RubyDoor.install` puts every aggregate name on Object, so a
875
+ # value object sharing a word with any aggregate resolves to that class once the other domain
876
+ # boots.
877
+ value_object "BugTitle" do
878
+ attribute :value, String, pattern: '[^ \t\n\r]'
879
+ invariant("a bug is titled") { !value.to_s.empty? }
880
+ end
881
+
882
+ # Required at `Log`, which is what makes the one absolute rule absolute.
883
+ value_object "Demonstration" do
884
+ attribute :value, String, pattern: '[^ \t\n\r]'
885
+ invariant("a bug is proven by a test somebody can re-run") { !value.to_s.empty? }
886
+ end
887
+
888
+ value_object "Symptom" do
889
+ attribute :value, String, pattern: '[^ \t\n\r]'
890
+ invariant("a bug says what actually happened") { !value.to_s.empty? }
891
+ end
892
+
893
+ value_object "Expectation" do
894
+ attribute :value, String, pattern: '[^ \t\n\r]'
895
+ invariant("a bug says what should have happened") { !value.to_s.empty? }
896
+ end
897
+
898
+ value_object "BugSite" do
899
+ attribute :value, String, pattern: '[^ \t\n\r]'
900
+ invariant("an investigation names where the bug lives") { !value.to_s.empty? }
901
+ end
902
+
903
+ value_object "BugCause" do
904
+ attribute :value, String, pattern: '[^ \t\n\r]'
905
+ invariant("an investigation says why the code is wrong") { !value.to_s.empty? }
906
+ end
907
+
908
+ # 7 to 40 hex: a fix that lives only in a working tree is not a fix.
909
+ value_object "CommitRef" do
910
+ attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
911
+ end
912
+
913
+ # What was actually run, e.g. "rspec --order random, 1335 examples, 0 failures, seed 12345":
914
+ # a count and a seed are checkable, "tests pass" is not.
915
+ value_object "Verification" do
916
+ attribute :value, String, pattern: '[^ \t\n\r]'
917
+ invariant("a verification says what was actually run") { !value.to_s.empty? }
918
+ end
919
+
920
+ value_object "BugReason" do
921
+ attribute :value, String, pattern: '[^ \t\n\r]'
922
+ invariant("stopping on a bug says why") { !value.to_s.empty? }
923
+ end
924
+
925
+ # Who decides and what would unblock it, so "deferred" does not become "forgotten".
926
+ value_object "NextStep" do
927
+ attribute :value, String, pattern: '[^ \t\n\r]'
928
+ invariant("stopping on a bug says what would move it") { !value.to_s.empty? }
929
+ end
930
+
931
+ value_object "WaiverCount" do
932
+ attribute :value, Integer, default: 0
933
+ invariant("a waiver count is not negative") { !value.negative? }
934
+ end
935
+
936
+ value_object "WaiverReason" do
937
+ attribute :value, String, pattern: '[^ \t\n\r]'
938
+ invariant("a waived gate says why") { !value.to_s.empty? }
939
+ end
940
+
941
+ # Redeclared, as elsewhere in this file; the "qa_sweep" literal is pinned to AUTOMATED_ENGINEER
942
+ # by spec.
943
+ value_object "WaivedBy" do
944
+ attribute :value, String, pattern: '[^ \t\n\r]'
945
+ invariant("a gate is waived by a person, never by the loop") { value != "qa_sweep" }
946
+ invariant("a waiver is signed") { value != "nobody" }
947
+ end
948
+
949
+ # Kebab-case, at least two words: one word is too close to a `Tag` to tell apart at a glance.
950
+ # The anchored pattern is the whole contract.
951
+ value_object "BugName" do
952
+ attribute :value, String, pattern: '^[a-z0-9]+(-[a-z0-9]+)+$'
953
+ end
954
+
955
+ value_object "SweepRef" do
956
+ attribute :value, String
957
+ end
958
+
959
+ value_object "Engineer" do
960
+ attribute :value, String, default: "nobody"
961
+ end
962
+
963
+ value_object "Instant" do
964
+ attribute :value, Integer, default: 0
965
+ invariant("an instant is not before the epoch") { !value.negative? }
966
+ end
967
+
968
+ value_object "StaleAfter" do
969
+ attribute :value, Integer, default: 900
970
+ invariant("a staleness window is positive") { value.positive? }
971
+ end
972
+
973
+ # Not an `Engineer`: that type defaults to "nobody" (an unclaimed bug is held by nobody), which
974
+ # would let a report omit its submitter. This type has no default and refuses "nobody".
975
+ value_object "Submitter" do
976
+ attribute :value, String, pattern: '[^ \t\n\r]'
977
+
978
+ invariant("a bug is reported by somebody") { value != "nobody" }
979
+ end
980
+
981
+ # An order is judgement, kept apart from discovery order (`sequence`). Lower is sooner; the
982
+ # default is not zero so an unranked bug sits behind every ranked one.
983
+ value_object "BugOrder" do
984
+ attribute :value, Integer, default: 1_000
985
+
986
+ invariant("an order is a position, not a direction") { !value.negative? }
987
+ end
988
+
989
+ value_object "Tag" do
990
+ attribute :value, String, pattern: "^[a-z0-9][a-z0-9_-]*$"
991
+
992
+ invariant("a tag is a word, not a sentence") { !value.empty? }
993
+ end
994
+
995
+ # A bug is born proven; "somebody thinks something is wrong" is a check, on the sweep.
996
+ lifecycle :status, default: "logged" do
997
+ transition "Investigate" => "investigating", from: "logged"
998
+ transition "Fix" => "fixed", from: "investigating"
999
+ transition "Verify" => "verified", from: "fixed"
1000
+ transition "Pause" => "paused", from: ["logged", "investigating"]
1001
+ # `withdrawn` is for a report whose test proved something other than what it claimed.
1002
+ transition "Withdraw" => "withdrawn", from: ["logged", "investigating", "paused"]
1003
+ transition "Regress" => "investigating", from: ["fixed", "verified"]
1004
+ transition "Revisit" => "investigating", from: "paused"
1005
+ end
1006
+
1007
+ command "Log" do
1008
+ role "QA engineer"
1009
+ goal "Write down something that is wrong, with the test that proves it"
1010
+
1011
+ reference_to Sweep
1012
+ attribute :reference, BugReference
1013
+ attribute :sequence, BugSequence
1014
+ attribute :title, BugTitle
1015
+ attribute :demonstration, Demonstration
1016
+ attribute :symptom, Symptom
1017
+ attribute :expectation, Expectation
1018
+ # Required like the demonstration: one is the evidence, the other is who produced it.
1019
+ attribute :submitter, Submitter
1020
+ attribute :tags, list_of(Tag), optional: true
1021
+ # Optional; `Name` covers bugs logged without one. No `sets`: on a creation command an
1022
+ # argument sharing the aggregate field's name lands there directly.
1023
+ attribute :name, BugName, optional: true
1024
+ # Optional; omitted, the aggregate's default `yes` applies. `hecks quality_control log --reproduced no`
1025
+ # uses it to skip its must-fail check for a finding with no reliable pass/fail signal.
1026
+ attribute :reproduced, one_of("yes", "no"), optional: true
1027
+
1028
+ emits "BugLogged"
1029
+ end
1030
+
1031
+ # Tagging and ranking are not lifecycle moves: they are knowable at any status and change
1032
+ # nothing about what may happen next.
1033
+ command "Rank" do
1034
+ role "QA engineer"
1035
+ goal "Say what should be dealt with first"
1036
+
1037
+ reference_to Bug
1038
+ attribute :order, BugOrder
1039
+
1040
+ sets :order
1041
+
1042
+ emits "BugRanked"
1043
+ end
1044
+
1045
+ # Tags are a set on the bug, not an entity: a tag has no identity or lifecycle. `Tag` replaces
1046
+ # the whole set, so removing one needs no second verb and tagging twice is not two tags.
1047
+ command "Tag" do
1048
+ role "QA engineer"
1049
+ goal "Say what kind of thing this bug is, so it can be found with its kind later"
1050
+
1051
+ reference_to Bug
1052
+ attribute :tags, list_of(Tag)
1053
+
1054
+ sets :tags
1055
+
1056
+ emits "BugTagged"
1057
+ end
1058
+
1059
+ # Not a lifecycle move (the `Rank` shape); `undecided` is only ever the birth value.
1060
+ # `hecks quality_control log --triage` dispatches this right after `Log`.
1061
+ command "Triage" do
1062
+ role "QA engineer"
1063
+ goal "Write down whether this can be fixed on the spot or needs a bigger judgment"
1064
+
1065
+ reference_to Bug
1066
+ attribute :disposition, one_of("self_contained", "bigger")
1067
+
1068
+ # The `given` is the fence: an argument named like the record's own `one_of` field coerces
1069
+ # against the wider set, so the narrower inline set did not refuse "undecided". Predicates
1070
+ # read args first.
1071
+ given("a triage decides — self_contained or bigger, never undecided") { disposition.value != "undecided" }
1072
+
1073
+ sets :disposition
1074
+
1075
+ emits "BugTriaged"
1076
+ end
1077
+
1078
+ # Not a lifecycle move and callable from any status, so an already-logged bug can be named
1079
+ # later.
1080
+ command "Name" do
1081
+ role "QA engineer"
1082
+ goal "Give a bug a short, memorable slug — at log time or any point after"
1083
+
1084
+ reference_to Bug
1085
+ attribute :name, BugName
1086
+
1087
+ sets :name
1088
+
1089
+ emits "BugNamed"
1090
+ end
1091
+
1092
+ # Everything one agent ever reported, as against `InHand`, which is what it holds now.
1093
+ query "From" do
1094
+ description "Every bug one submitter has ever logged, in whatever state — what an agent found, as against what it is holding."
1095
+ attribute :submitter, Submitter
1096
+ where(submitter: :submitter)
1097
+ order_by :reference
1098
+ end
1099
+
1100
+ # `contains` against a bound argument, so it answers whichever tag you have.
1101
+ query "Tagged" do
1102
+ description "Every bug carrying one tag, whatever state it is in — the way to find all the flaky ones, or everything about the projector."
1103
+ attribute :tag, Tag
1104
+ where(tags: { contains: :tag })
1105
+ order_by :reference
1106
+ end
1107
+
1108
+ # Same rule as `Target.Claim`: held by nobody, or stale. `+` because the sublanguage has no
1109
+ # `-`.
1110
+ command "Claim" do
1111
+ role "QA engineer"
1112
+ goal "Take the next bug off the queue, or one whose holder has gone quiet"
1113
+
1114
+ # Same reason as `Target.Claim`: the window is on the record, not the claimer's to name.
1115
+ reference_to Bug
1116
+ attribute :held_by, Engineer
1117
+ attribute :now, Instant
1118
+
1119
+ given("a live claim is not taken from the agent holding it") do
1120
+ held_by.value == "nobody" || claimed_at.value + window.value <= now.value
1121
+ end
1122
+
1123
+ sets :held_by
1124
+ sets :claimed_at, to: :now
1125
+
1126
+ emits "BugClaimed"
1127
+ end
1128
+
1129
+ command "Drop" do
1130
+ role "QA engineer"
1131
+ goal "Put a bug back on the queue without finishing it"
1132
+
1133
+ reference_to Bug
1134
+ attribute :held_by, Engineer
1135
+
1136
+ sets :held_by
1137
+
1138
+ emits "BugDropped"
1139
+ end
1140
+
1141
+ command "Investigate" do
1142
+ role "QA engineer"
1143
+ goal "Name the code that is wrong and say why"
1144
+
1145
+ reference_to Bug
1146
+ attribute :site, BugSite
1147
+ attribute :cause, BugCause
1148
+
1149
+ sets :site
1150
+ sets :cause
1151
+
1152
+ emits "BugInvestigated"
1153
+ end
1154
+
1155
+ # `reference` is restated so `BugFixed` carries it: an event holds only its dispatched
1156
+ # arguments, and `BugCiWatch`'s saga memory is a copy of that payload, so without it the saga
1157
+ # cannot know which bug it watches. Required, so omitting it is refused (`AbsentArgument`).
1158
+ command "Fix" do
1159
+ role "QA engineer"
1160
+ goal "Record the commit that makes it stop happening"
1161
+
1162
+ reference_to Bug
1163
+ attribute :reference, BugReference
1164
+ attribute :commit, CommitRef
1165
+
1166
+ sets :commit
1167
+
1168
+ emits "BugFixed"
1169
+ end
1170
+
1171
+ command "Verify" do
1172
+ role "QA engineer"
1173
+ goal "Record that the fix was actually verified, and exactly how"
1174
+
1175
+ reference_to Bug
1176
+ attribute :evidence, Verification
1177
+
1178
+ sets :verification, to: :evidence
1179
+
1180
+ emits "BugVerified"
1181
+ end
1182
+
1183
+ command "Pause" do
1184
+ role "QA engineer"
1185
+ goal "Stop on a bug that is real and too large to fix here — why, and what would move it"
1186
+
1187
+ reference_to Bug
1188
+ attribute :reason, BugReason
1189
+ attribute :next_step, NextStep
1190
+
1191
+ sets :reason
1192
+ sets :next_step
1193
+
1194
+ emits "BugPaused"
1195
+ end
1196
+
1197
+ command "Withdraw" do
1198
+ role "QA engineer"
1199
+ goal "Take back a report whose test turned out to prove something else"
1200
+
1201
+ reference_to Bug
1202
+ attribute :reason, BugReason
1203
+
1204
+ sets :reason
1205
+
1206
+ emits "BugWithdrawn"
1207
+ end
1208
+
1209
+ command "Regress" do
1210
+ role "QA engineer"
1211
+ goal "Put a bug back when its fix stopped holding"
1212
+
1213
+ reference_to Bug
1214
+ emits "BugRegressed"
1215
+ end
1216
+
1217
+ command "Revisit" do
1218
+ role "QA engineer"
1219
+ goal "Pick a paused bug back up, investigation intact"
1220
+
1221
+ reference_to Bug
1222
+ emits "BugRevisited"
1223
+ end
1224
+
1225
+ command "Waive" do
1226
+ role "QA engineer"
1227
+ goal "Go round a gate on this bug, on the record, signed by a person"
1228
+
1229
+ reference_to Bug
1230
+ attribute :reason, WaiverReason
1231
+ # Signed and checked, as in `Sweep.Waive`.
1232
+ attribute :waived_by, WaivedBy
1233
+
1234
+ sets :waived, increment: { value: 1 }
1235
+ sets :waived_by
1236
+
1237
+ emits "GateWaived"
1238
+ end
1239
+
1240
+ query "Open" do
1241
+ description "Every bug still owed work — the open ledger, derived rather than maintained."
1242
+ where(status: { in: "logged,investigating,paused" })
1243
+ order_by :sequence
1244
+ end
1245
+
1246
+ query "Untriaged" do
1247
+ description "Live bugs (logged or investigating) nobody has triaged as self_contained or bigger yet — the judgment still owed on each."
1248
+ where(status: { in: "logged,investigating" })
1249
+ where(:"disposition.value" => "undecided")
1250
+ order_by :sequence
1251
+ end
1252
+
1253
+ # `fixed` counts as still owed: a landed fix does not answer the bigger judgment call; only
1254
+ # `Verify` or `Withdraw` resolves it.
1255
+ query "NeedsJudgment" do
1256
+ description "Open bugs triaged bigger and not yet resolved — the pile still owed a human or agent judgment call, not one anybody can just fix off the queue."
1257
+ where(:"disposition.value" => "bigger")
1258
+ where(status: { in: "logged,investigating,fixed,paused" })
1259
+ order_by :sequence
1260
+ end
1261
+
1262
+ query "Queue" do
1263
+ description "Open bugs nobody is holding, most important first, then oldest. The queue an agent takes from when it is fixing rather than sweeping."
1264
+ where(status: { in: "logged,investigating" })
1265
+ where(:"held_by.value" => "nobody")
1266
+ # Order by the scalar, not the value object: sorting the whole `{value: n}` put rank 1 behind
1267
+ # two ranked 1000.
1268
+ order_by :"order.value"
1269
+ end
1270
+
1271
+ query "InHand" do
1272
+ description "Open bugs somebody is holding, and since when. A claim older than the window is free for the taking — read claimed_at before assuming somebody is on it."
1273
+ where(status: { in: "logged,investigating" })
1274
+ where(:"held_by.value" => { ne: "nobody" })
1275
+ order_by :claimed_at
1276
+ end
1277
+
1278
+ query "FoundIn" do
1279
+ description "Everything one sweep turned up, in the order it was found."
1280
+ attribute :sweep_id, SweepRef
1281
+ where(sweep: :sweep_id)
1282
+ order_by :sequence
1283
+ end
1284
+
1285
+ query "ReadyToVerify" do
1286
+ description "Fixed and not yet re-run — the queue a full CI pass empties."
1287
+ where(status: "fixed")
1288
+ order_by :sequence
1289
+ end
1290
+
1291
+ query "Paused" do
1292
+ description "Real, unfixed, stopped on, each with what would move it. The only queue a ticket is ever raised from."
1293
+ where(status: "paused")
1294
+ order_by :sequence
1295
+ end
1296
+
1297
+ query "Withdrawn" do
1298
+ description "Reports whose test proved something other than what was claimed. Read the reasons: a run of these is a practice testing the wrong thing, not a runtime behaving."
1299
+ where(status: "withdrawn")
1300
+ order_by :sequence
1301
+ end
1302
+
1303
+ query "Waived" do
1304
+ description "Bugs that went round one of their own gates — the honesty check on any claim about following the protocol."
1305
+ where(:"waived.value" => { gt: 0 })
1306
+ order_by :sequence
1307
+ end
1308
+
1309
+ query "All" do
1310
+ description "Every bug ever logged — what a runner mints the next reference from."
1311
+ order_by :sequence
1312
+ end
1313
+ end
1314
+
1315
+ # The practice's backlog of leads: a citable reason to suspect an uninvestigated corner.
1316
+ # A lead, not a task: `citation` says why it is worth chasing, `resolution` what got built.
1317
+ aggregate "Angle" do
1318
+ description "One concrete, citable reason to suspect an uninvestigated corner exists — proposed, chased down, and either built into something real or set aside with a reason. The practice's own backlog of what to try next, so a future sweep mines its own history instead of guessing."
1319
+
1320
+ identified_by :reference
1321
+
1322
+ attribute :reference, AngleReference
1323
+ attribute :premise, AnglePremise
1324
+ attribute :citation, AngleCitation
1325
+ attribute :proposer, Proposer
1326
+ attribute :proposed_at, Instant
1327
+ attribute :resolution, AngleResolution
1328
+ attribute :reason, AngleReason
1329
+
1330
+ value_object "AngleReference" do
1331
+ attribute :value, String, pattern: '[^ \t\n\r]'
1332
+ invariant("an angle is referenced") { !value.to_s.empty? }
1333
+ end
1334
+
1335
+ # The number is in the sentence because the sentence is the refusal; 60 characters trips a
1336
+ # stub.
1337
+ value_object "AnglePremise" do
1338
+ attribute :value, String, pattern: '[^ \t\n\r]'
1339
+ invariant("an angle says enough to be acted on without its proposer in the room, in at least 60 characters") { value.to_s.size >= 60 }
1340
+ end
1341
+
1342
+ # A Bug reference, ADR number or named gap that a reader can check for themselves.
1343
+ value_object "AngleCitation" do
1344
+ attribute :value, String, pattern: '[^ \t\n\r]'
1345
+ invariant("an angle cites something concrete, not a mood") { !value.to_s.empty? }
1346
+ end
1347
+
1348
+ # Not an `Engineer`, for the reason `Submitter` is not: that default would let authorship go
1349
+ # unrecorded.
1350
+ value_object "Proposer" do
1351
+ attribute :value, String, pattern: '[^ \t\n\r]'
1352
+ invariant("an angle is proposed by somebody") { value != "nobody" }
1353
+ end
1354
+
1355
+ value_object "Instant" do
1356
+ attribute :value, Integer, default: 0
1357
+ invariant("an instant is not before the epoch") { !value.negative? }
1358
+ end
1359
+
1360
+ # What came of it (a Target, a PR number, a domain path); set once, at `Build`.
1361
+ value_object "AngleResolution" do
1362
+ attribute :value, String, pattern: '[^ \t\n\r]'
1363
+ invariant("a built angle says what actually got built") { !value.to_s.empty? }
1364
+ end
1365
+
1366
+ # Redeclared as `Target::Shelve` does: a discard with no reason looks like a lead nobody read.
1367
+ value_object "AngleReason" do
1368
+ attribute :value, String, pattern: '[^ \t\n\r]'
1369
+ invariant("discarding an angle says why") { !value.to_s.empty? }
1370
+ end
1371
+
1372
+ # `Build` is reachable only from `investigating`, so a lead cannot be built unless it was
1373
+ # picked up. `Discard` reads from both live states: a lead can prove a dead end before or after
1374
+ # it starts.
1375
+ lifecycle :status, default: "proposed" do
1376
+ transition "Investigate" => "investigating", from: "proposed"
1377
+ transition "Build" => "built", from: "investigating"
1378
+ transition "Discard" => "discarded", from: ["proposed", "investigating"]
1379
+ end
1380
+
1381
+ command "Propose" do
1382
+ role "QA engineer"
1383
+ goal "Write down a concrete, citable reason to suspect an uninvestigated corner exists, before it is lost to a conversation"
1384
+
1385
+ attribute :reference, AngleReference
1386
+ attribute :premise, AnglePremise
1387
+ attribute :citation, AngleCitation
1388
+ attribute :proposer, Proposer
1389
+ # `now:` is filled from the clock port, as in `Target.Claim`; an explicit value wins.
1390
+ attribute :now, Instant
1391
+
1392
+ sets :proposed_at, to: :now
1393
+
1394
+ emits "AngleProposed"
1395
+ end
1396
+
1397
+ command "Investigate" do
1398
+ role "QA engineer"
1399
+ goal "Pick a lead up for a real look, rather than leaving it to look picked-up by default"
1400
+
1401
+ reference_to Angle
1402
+ emits "AngleInvestigated"
1403
+ end
1404
+
1405
+ command "Build" do
1406
+ role "QA engineer"
1407
+ goal "Say what actually came of chasing this lead"
1408
+
1409
+ reference_to Angle
1410
+ attribute :resolution, AngleResolution
1411
+
1412
+ sets :resolution
1413
+
1414
+ emits "AngleBuilt"
1415
+ end
1416
+
1417
+ command "Discard" do
1418
+ role "QA engineer"
1419
+ goal "Decide a lead is not worth the practice's time, and say why"
1420
+
1421
+ reference_to Angle
1422
+ attribute :reason, AngleReason
1423
+
1424
+ sets :reason
1425
+
1426
+ emits "AngleDiscarded"
1427
+ end
1428
+
1429
+ # Ordered by the scalar `proposed_at.value`, not the value object (see `Bug.Queue`).
1430
+ query "Backlog" do
1431
+ description "Leads nobody has finished chasing yet, oldest first — read this before inventing a new angle from scratch."
1432
+ where(status: { in: "proposed,investigating" })
1433
+ order_by :"proposed_at.value"
1434
+ end
1435
+
1436
+ # Built and discarded share one query: a discarded angle is evidence a citation was already
1437
+ # covered.
1438
+ query "Resolved" do
1439
+ description "Leads already acted on, one way or the other — built into something real, or discarded with a reason. Read beside Backlog for the practice's own hit rate."
1440
+ where(status: { in: "built,discarded" })
1441
+ order_by :reference
1442
+ end
1443
+
1444
+ # The duplicate-premise check, run before `Propose` so a second session finds the first one's
1445
+ # row.
1446
+ query "Citing" do
1447
+ description "Every angle ever proposed on one exact citation, whatever became of it — the check before proposing another on the same fact."
1448
+ attribute :citation, AngleCitation
1449
+ where(citation: :citation)
1450
+ order_by :reference
1451
+ end
1452
+
1453
+ query "All" do
1454
+ description "Every angle ever proposed — what a runner mints the next reference from."
1455
+ order_by :reference
1456
+ end
1457
+
1458
+ # An answer that is one document of text, exactly as the tool that produced it wrote it.
1459
+ value_object "Document" do
1460
+ attribute :text, String
1461
+ end
1462
+
1463
+ # AngleTools answers this by running the script's own command as a child process
1464
+ # (lib/hecks/quality_control/adapters/angle_tools.rb); it reads no record of this aggregate.
1465
+ query "Seed" do
1466
+ description "Propose the practice's starting leads that are not on file yet; a lead already chased down is proposed, investigated and built in one pass."
1467
+ returns Document
1468
+ end
1469
+ end
1470
+
1471
+ # Raise an issue only for a bug you could not fix yourself. `Raise` checks that the reference
1472
+ # resolves but not the bug's state; `RestingOnUnpaused` surfaces that half.
1473
+ aggregate "Ticket" do
1474
+ description "One issue raised in a tracker outside this repository — what it says, whether the tracker took it, and how many times we have asked."
1475
+
1476
+ reference_to Bug
1477
+
1478
+ identified_by :reference
1479
+
1480
+ attribute :reference, TicketReference
1481
+ attribute :repository, TicketRepository
1482
+ attribute :title, TicketTitle
1483
+ attribute :body, TicketBody
1484
+ attribute :pull_request, PullRequest
1485
+ attribute :number, IssueNumber
1486
+ attribute :url, IssueUrl
1487
+ attribute :refusal, RefusalText
1488
+ attribute :attempts, AttemptCount
1489
+ # The cap lives on the record: a policy carries only the event payload, so a `max` passed as an
1490
+ # argument would never reach the retry.
1491
+ attribute :max_attempts, MaxAttempts
1492
+
1493
+ value_object "TicketReference" do
1494
+ attribute :value, String, pattern: '[^ \t\n\r]'
1495
+ invariant("a ticket is referenced") { !value.to_s.empty? }
1496
+ end
1497
+
1498
+ value_object "TicketRepository" do
1499
+ attribute :value, String, pattern: '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'
1500
+ end
1501
+
1502
+ value_object "TicketTitle" do
1503
+ attribute :value, String, pattern: '[^ \t\n\r]'
1504
+ invariant("a ticket is titled") { !value.to_s.empty? }
1505
+ end
1506
+
1507
+ # The URL is on the ticket so the ledger can answer `Ticket.Proposed`. It defaults to ""
1508
+ # because most tickets are raised before anybody has fixed anything.
1509
+ value_object "PullRequest" do
1510
+ attribute :value, String, default: ""
1511
+ end
1512
+
1513
+ value_object "TicketBody" do
1514
+ attribute :value, String, pattern: '[^ \t\n\r]'
1515
+ invariant("a ticket says something") { !value.to_s.empty? }
1516
+ end
1517
+
1518
+ value_object "IssueNumber" do
1519
+ attribute :value, Integer, default: 0
1520
+ invariant("an issue number is not negative") { !value.negative? }
1521
+ end
1522
+
1523
+ value_object "IssueUrl" do
1524
+ attribute :value, String
1525
+ end
1526
+
1527
+ value_object "RefusalText" do
1528
+ attribute :value, String
1529
+ end
1530
+
1531
+ # Three tries, then a human. The cap is on the record because a policy carries no state.
1532
+ value_object "AttemptCount" do
1533
+ attribute :value, Integer, default: 0
1534
+ invariant("an attempt count is not negative") { !value.negative? }
1535
+ end
1536
+
1537
+ value_object "MaxAttempts" do
1538
+ attribute :value, Integer, default: 3
1539
+ invariant("a filing is attempted at least once") { value.positive? }
1540
+ end
1541
+
1542
+ value_object "BugRef" do
1543
+ attribute :value, String
1544
+ end
1545
+
1546
+ # Raising and sending are two verbs: a crash between them leaves a `Raised` row, not a false
1547
+ # filing. `Submit` is non-creating, so `TicketSubmitted` carries `{ id: ... }`, which a
1548
+ # reference slot takes.
1549
+ lifecycle :status, default: "raised" do
1550
+ transition "Submit" => "submitting", from: ["raised", "refused"]
1551
+ transition "Filed" => "filed", from: "submitting"
1552
+ transition "Refused" => "refused", from: "submitting"
1553
+ transition "Retry" => "submitting", from: "refused"
1554
+ transition "Abandon" => "abandoned", from: ["raised", "submitting", "refused"]
1555
+ transition "Close" => "closed", from: "filed"
1556
+ end
1557
+
1558
+ # The port is declared in `quality_control.ports.hecksagon`, where wiring lives.
1559
+
1560
+ command "Raise" do
1561
+ role "QA engineer"
1562
+ goal "Say the outside world needs to know about a bug we could not fix"
1563
+
1564
+ reference_to Bug
1565
+ attribute :reference, TicketReference
1566
+ attribute :repository, TicketRepository
1567
+ attribute :title, TicketTitle
1568
+ attribute :body, TicketBody
1569
+ # `held_state` hands the whole ticket to the adapter, so this field reaches `gh` with no
1570
+ # extra argument.
1571
+ attribute :pull_request, PullRequest, optional: true
1572
+
1573
+ emits "TicketRaised"
1574
+ end
1575
+
1576
+ # Dispatch with `id:`, not through the facade: the facade form addresses by identity heads, so
1577
+ # the payload carries `reference` and the port refuses it ("File was not given id").
1578
+ command "Submit" do
1579
+ role "QA engineer"
1580
+ goal "Send it — the last reversible moment"
1581
+
1582
+ reference_to Ticket
1583
+ emits "TicketSubmitted"
1584
+ end
1585
+
1586
+ command "Filed" do
1587
+ role "System"
1588
+ goal "Record that the tracker took it, and where it landed"
1589
+
1590
+ reference_to Ticket
1591
+ attribute :number, IssueNumber
1592
+ attribute :url, IssueUrl
1593
+
1594
+ sets :number
1595
+ sets :url
1596
+
1597
+ emits "TicketFiled"
1598
+ end
1599
+
1600
+ command "Refused" do
1601
+ role "System"
1602
+ goal "Record that the tracker would not take it, and what it said"
1603
+
1604
+ reference_to Ticket
1605
+ attribute :refusal, RefusalText
1606
+
1607
+ sets :refusal
1608
+ sets :attempts, increment: { value: 1 }
1609
+
1610
+ emits "TicketFilingRefused"
1611
+ end
1612
+
1613
+ # A policy re-enters its target with the event payload verbatim, so `Retry` must accept
1614
+ # `refusal` or be refused outright. It is set rather than dropped: an argument taken and
1615
+ # ignored is exactly what this chapter hunts.
1616
+ command "Retry" do
1617
+ role "System"
1618
+ goal "Ask again, up to the limit, carrying what was refused"
1619
+
1620
+ reference_to Ticket
1621
+ attribute :refusal, RefusalText
1622
+
1623
+ given("a retry is still allowed") { attempts.value < max_attempts.value }
1624
+
1625
+ sets :refusal
1626
+
1627
+ emits "TicketRetried"
1628
+ end
1629
+
1630
+ command "Abandon" do
1631
+ role "QA engineer"
1632
+ goal "Stop asking, and put it in front of a person"
1633
+
1634
+ reference_to Ticket
1635
+ emits "TicketFilingAbandoned"
1636
+ end
1637
+
1638
+ command "Close" do
1639
+ role "QA engineer"
1640
+ goal "Record that the issue is done with"
1641
+
1642
+ reference_to Ticket
1643
+ emits "TicketClosed"
1644
+ end
1645
+
1646
+ query "Proposed" do
1647
+ description "Tickets that already carry a pull request — a fix exists, so more work filed against them is duplicated effort."
1648
+ where(:"pull_request.value" => { ne: "" })
1649
+ order_by :reference
1650
+ end
1651
+
1652
+ query "Raised" do
1653
+ description "Composed and not sent. Nothing will happen to these until somebody submits one."
1654
+ where(status: "raised")
1655
+ order_by :reference
1656
+ end
1657
+
1658
+ query "Submitting" do
1659
+ description "Sent and not yet answered. A row that sticks here is a call whose outcome nobody recorded — look at the tracker before asking again, or a retry files a duplicate."
1660
+ where(status: "submitting")
1661
+ order_by :reference
1662
+ end
1663
+
1664
+ query "Filed" do
1665
+ description "Open issues this practice is responsible for, out in the world."
1666
+ where(status: "filed")
1667
+ order_by :number
1668
+ end
1669
+
1670
+ query "Refused" do
1671
+ description "The tracker said no, and what it said. Retried automatically until the cap."
1672
+ where(status: "refused")
1673
+ order_by :reference
1674
+ end
1675
+
1676
+ query "Abandoned" do
1677
+ description "Asked three times, refused three times. Every row belongs in the report, because nothing automatic will touch it again."
1678
+ where(status: "abandoned")
1679
+ order_by :reference
1680
+ end
1681
+
1682
+ query "ForBug" do
1683
+ description "Every ticket ever raised about one bug — the duplicate check, before a second is raised."
1684
+ attribute :bug_id, BugRef
1685
+ where(bug: :bug_id)
1686
+ order_by :reference
1687
+ end
1688
+
1689
+ # A dotted where hops the reference because this cannot be a `given`: `bug.status` does not
1690
+ # resolve inside a predicate. The rule is surfaced, not enforced.
1691
+ query "RestingOnUnpaused" do
1692
+ description "Tickets about a bug nobody has stopped on. Raise an issue only for something you could not fix yourself — this is the list where that gets skipped."
1693
+ where(status: { in: "raised,filed" })
1694
+ where(:"bug/status" => { in: "logged,investigating" })
1695
+ order_by :reference
1696
+ end
1697
+
1698
+ query "RestingOnFixed" do
1699
+ description "Tickets about a bug this repository went on to fix itself. Each one owes the outside world a Close."
1700
+ where(status: "filed")
1701
+ where(:"bug/status" => { in: "fixed,verified" })
1702
+ order_by :number
1703
+ end
1704
+
1705
+ query "All" do
1706
+ description "Every ticket ever raised."
1707
+ order_by :reference
1708
+ end
1709
+ end
1710
+
1711
+ # A PR opened for a Bug's own fix, recorded at open so `hecks quality_control check_pull_requests` looks it up by number.
1712
+ # Not named `PullRequest`: `Ticket::PullRequest` exists and aggregate names install on Object.
1713
+ aggregate "Patch" do
1714
+ description "One pull request opened for a Bug's own fix — number, branch, and the exact commit that makes checking its CI status a lookup instead of a guess."
1715
+
1716
+ reference_to Bug
1717
+
1718
+ identified_by :number
1719
+
1720
+ attribute :number, PatchNumber
1721
+ attribute :url, PatchUrl
1722
+ attribute :branch, PatchBranch
1723
+ attribute :commit, CommitRef
1724
+ attribute :title, PatchTitle
1725
+ # Makes `PR_CAP_PER_DAY` countable (`OpenedSince`). `default: 0` hydrates older rows at the
1726
+ # epoch, which the cap never counts.
1727
+ attribute :opened_at, Instant
1728
+
1729
+ value_object "Instant" do
1730
+ attribute :value, Integer, default: 0
1731
+ invariant("an instant is not before the epoch") { !value.negative? }
1732
+ end
1733
+
1734
+ value_object "PatchNumber" do
1735
+ attribute :value, Integer
1736
+ invariant("a patch is numbered") { value.positive? }
1737
+ end
1738
+
1739
+ value_object "PatchUrl" do
1740
+ attribute :value, String, pattern: '[^ \t\n\r]'
1741
+ invariant("a patch says where it lives") { !value.to_s.empty? }
1742
+ end
1743
+
1744
+ value_object "PatchBranch" do
1745
+ attribute :value, String, pattern: '[^ \t\n\r]'
1746
+ invariant("a patch says which branch it is on") { !value.to_s.empty? }
1747
+ end
1748
+
1749
+ # Recorded at `Open` so the commit is in hand when the PR needs checking, as `Bug.commit` is.
1750
+ value_object "CommitRef" do
1751
+ attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
1752
+ end
1753
+
1754
+ value_object "PatchTitle" do
1755
+ attribute :value, String, pattern: '[^ \t\n\r]'
1756
+ invariant("a patch is titled") { !value.to_s.empty? }
1757
+ end
1758
+
1759
+ value_object "BugRef" do
1760
+ attribute :value, String
1761
+ end
1762
+
1763
+ # Both endings are terminal: a merged PR does not reopen, and neither does the ledger's memory
1764
+ # of a closed one.
1765
+ lifecycle :status, default: "opened" do
1766
+ transition "Merge" => "merged", from: "opened"
1767
+ transition "Close" => "closed", from: "opened"
1768
+ end
1769
+
1770
+ # Dispatched only by `hecks quality_control patch.open --bug`, which reads the facts back from `gh pr view`, so
1771
+ # the record is written by the process that made it true. `now` is that script's clock read.
1772
+ # The script asks `dry_run?` of this command before it opens anything, so the two `given`s are
1773
+ # the rules for opening a PR at all: the branch is one this practice recognises as its own, and
1774
+ # the bug it fixes is fixed. The prefix repeats `BRANCH_PREFIX` as a literal, as `WaivedBy`
1775
+ # repeats `AUTOMATED_ENGINEER`; `spec/quality_control_spec.rb` pins the two equal.
1776
+ command "Open" do
1777
+ role "QA engineer"
1778
+ goal "Record that a fix has been opened as a pull request, the moment its number, branch and commit are already known"
1779
+
1780
+ reference_to Bug
1781
+ attribute :number, PatchNumber
1782
+ attribute :url, PatchUrl
1783
+ attribute :branch, PatchBranch
1784
+ attribute :commit, CommitRef
1785
+ attribute :title, PatchTitle
1786
+ attribute :now, Instant
1787
+
1788
+ given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
1789
+ given("the bug is fixed") { bug.status == "fixed" }
1790
+
1791
+ sets :opened_at, to: :now
1792
+
1793
+ emits "PatchOpened"
1794
+ end
1795
+
1796
+ command "Merge" do
1797
+ role "QA engineer"
1798
+ goal "Record that GitHub merged it"
1799
+
1800
+ reference_to Patch
1801
+ emits "PatchMerged"
1802
+ end
1803
+
1804
+ command "Close" do
1805
+ role "QA engineer"
1806
+ goal "Record that GitHub closed it without merging"
1807
+
1808
+ reference_to Patch
1809
+ emits "PatchClosed"
1810
+ end
1811
+
1812
+ # The worklist for `hecks quality_control check_pull_requests`: ask `gh` about each row's exact number, never search.
1813
+ query "Open" do
1814
+ description "Every PR we've opened that's still open, by number — hecks quality_control check_pull_requests's own worklist. For each of these, ask gh for exactly this number's CI status; nothing here is a guess."
1815
+ where(status: "opened")
1816
+ order_by :number
1817
+ end
1818
+
1819
+ query "ForBug" do
1820
+ description "Every patch ever opened for one bug — the duplicate check, the same shape Ticket.ForBug already gives for an issue."
1821
+ attribute :bug_id, BugRef
1822
+ where(bug: :bug_id)
1823
+ order_by :number
1824
+ end
1825
+
1826
+ # `hecks quality_control patch.open` compares this and `Improvement.OpenedSince` counts, from local midnight,
1827
+ # against `PR_CAP_PER_DAY`. `gte` on a bound argument, the `Account.AtOrAbove` shape.
1828
+ query "OpenedSince" do
1829
+ description "Every patch opened at or after one instant, in number order — what today's count against PR_CAP_PER_DAY is read from."
1830
+ attribute :since, Instant
1831
+ where(:"opened_at.value" => { gte: :since })
1832
+ order_by :number
1833
+ end
1834
+
1835
+ query "All" do
1836
+ description "Every patch ever opened — what a runner reads back to reconcile against GitHub's own list."
1837
+ order_by :number
1838
+ end
1839
+ end
1840
+
1841
+ # A PR for deliberate, non-bugfix work: a sibling of `Patch`, because `Bug` means "proven wrong"
1842
+ # and `Patch.Open` keeps its required `reference_to Bug`.
1843
+ # Born `opened`, with no investigating state: the work was decided before any code moved.
1844
+ #
1845
+ # `Open` and `Land` are separate so `ImprovementCiWatch` can `starts_on` the one event that is
1846
+ # also re-dispatched after a `Regress`; folding `commit` into `Open` would refuse a re-land
1847
+ # as `AlreadyExists`.
1848
+ aggregate "Improvement" do
1849
+ description "One pull request landed for deliberate, non-bugfix work — a domain-modeling addition, a tool, an aggregate this very practice needed — number, branch, and the exact commit that makes watching its CI a lookup instead of a guess, the same discipline `Patch` already keeps for a bug's own fix."
1850
+
1851
+ # Optional, set only at `Open`: not all deliberate work began as a cited `Angle`, and forcing
1852
+ # one would invent a lead after the fact.
1853
+ reference_to Angle, optional: true
1854
+
1855
+ identified_by :number
1856
+
1857
+ attribute :number, ImprovementNumber
1858
+ attribute :url, ImprovementUrl
1859
+ attribute :branch, ImprovementBranch
1860
+ attribute :commit, CommitRef
1861
+ attribute :title, ImprovementTitle
1862
+ # See `Patch.opened_at`: the same field, for the same cap.
1863
+ attribute :opened_at, Instant
1864
+
1865
+ value_object "Instant" do
1866
+ attribute :value, Integer, default: 0
1867
+ invariant("an instant is not before the epoch") { !value.negative? }
1868
+ end
1869
+
1870
+ value_object "ImprovementNumber" do
1871
+ attribute :value, Integer
1872
+ invariant("an improvement is numbered") { value.positive? }
1873
+ end
1874
+
1875
+ value_object "ImprovementUrl" do
1876
+ attribute :value, String, pattern: '[^ \t\n\r]'
1877
+ invariant("an improvement says where it lives") { !value.to_s.empty? }
1878
+ end
1879
+
1880
+ value_object "ImprovementBranch" do
1881
+ attribute :value, String, pattern: '[^ \t\n\r]'
1882
+ invariant("an improvement says which branch it is on") { !value.to_s.empty? }
1883
+ end
1884
+
1885
+ # Redeclared per aggregate. Absent until `Land`: there is nothing to watch until a commit
1886
+ # exists.
1887
+ value_object "CommitRef" do
1888
+ attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
1889
+ end
1890
+
1891
+ value_object "ImprovementTitle" do
1892
+ attribute :value, String, pattern: '[^ \t\n\r]'
1893
+ invariant("an improvement is titled") { !value.to_s.empty? }
1894
+ end
1895
+
1896
+ value_object "AngleRef" do
1897
+ attribute :value, String
1898
+ end
1899
+
1900
+ # `Merge` and `Close` are reachable from "opened" too: GitHub does not wait for a `Land`, and a
1901
+ # proof-only PR can be closed while still "opened" (`retire_if_settled?` hit
1902
+ # `LifecycleRefused`).
1903
+ lifecycle :status, default: "opened" do
1904
+ transition "Land" => "landed", from: ["opened", "needs_fix"]
1905
+ transition "Regress" => "needs_fix", from: "landed"
1906
+ transition "Merge" => "merged", from: ["opened", "landed", "needs_fix"]
1907
+ transition "Close" => "closed", from: ["opened", "landed", "needs_fix"]
1908
+ end
1909
+
1910
+ # Dispatched by `hecks quality_control patch.open --improvement`, which then dispatches `Land` in the same run so
1911
+ # `ImprovementCiWatch` is watching before the script exits. The `given`s are `Patch.Open`'s
1912
+ # rules with the angle in place of the bug: a lead is picked up (`investigating`) before
1913
+ # something is built from it, and work with no angle behind it is admitted.
1914
+ command "Open" do
1915
+ role "QA engineer"
1916
+ goal "Record that a deliberate, non-bugfix piece of work has been opened as a pull request, before its own commit is known to be worth watching"
1917
+
1918
+ reference_to Angle, optional: true
1919
+ attribute :number, ImprovementNumber
1920
+ attribute :url, ImprovementUrl
1921
+ attribute :branch, ImprovementBranch
1922
+ attribute :title, ImprovementTitle
1923
+ attribute :now, Instant
1924
+
1925
+ given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
1926
+ given("the angle is under investigation") { angle.unset? || angle.status == "investigating" }
1927
+
1928
+ sets :opened_at, to: :now
1929
+
1930
+ emits "ImprovementOpened"
1931
+ end
1932
+
1933
+ # The event `ImprovementCiWatch` starts on, and the one a re-landing fires again.
1934
+ # `number` is restated so the event carries it into the saga's memory (see `Bug::Fix`).
1935
+ command "Land" do
1936
+ role "QA engineer"
1937
+ goal "Record the commit this PR is now at, so CI can be watched for it — whether that is the commit it was opened with, or a fresh one pushed after a NeedsFix"
1938
+
1939
+ reference_to Improvement
1940
+ attribute :number, ImprovementNumber
1941
+ attribute :commit, CommitRef
1942
+
1943
+ sets :commit
1944
+
1945
+ emits "ImprovementLanded"
1946
+ end
1947
+
1948
+ # The same word `Bug` uses; `ImprovementCiWatch` is the only dispatcher.
1949
+ command "Regress" do
1950
+ role "QA engineer"
1951
+ goal "Put landed work back in front of somebody when its own commit stops holding"
1952
+
1953
+ reference_to Improvement
1954
+ emits "ImprovementRegressed"
1955
+ end
1956
+
1957
+ # Both endings are terminal and reachable from `needs_fix`: GitHub can merge or close a PR
1958
+ # whatever this ledger's CI watch last heard.
1959
+ command "Merge" do
1960
+ role "QA engineer"
1961
+ goal "Record that GitHub merged it"
1962
+
1963
+ reference_to Improvement
1964
+ emits "ImprovementMerged"
1965
+ end
1966
+
1967
+ command "Close" do
1968
+ role "QA engineer"
1969
+ goal "Record that GitHub closed it without merging"
1970
+
1971
+ reference_to Improvement
1972
+ emits "ImprovementClosed"
1973
+ end
1974
+
1975
+ query "Open" do
1976
+ description "Every PR we've landed for deliberate, non-bugfix work that's still open, by number — hecks quality_control check_pull_requests's own second worklist, the same shape Patch.Open already gives for a bug's own fix. For each of these, ask gh for exactly this number's CI status; nothing here is a guess."
1977
+ where(status: { in: "opened,landed,needs_fix" })
1978
+ order_by :number
1979
+ end
1980
+
1981
+ query "NeedsFix" do
1982
+ description "Landed work whose own commit came back red — what ImprovementCiWatch put back in front of somebody, and the list a human or agent clears with a fresh Land."
1983
+ where(status: "needs_fix")
1984
+ order_by :number
1985
+ end
1986
+
1987
+ query "ForAngle" do
1988
+ description "Every improvement ever landed against one angle — the duplicate check, the same shape Patch.ForBug already gives for a bug."
1989
+ attribute :angle_id, AngleRef
1990
+ where(angle: :angle_id)
1991
+ order_by :number
1992
+ end
1993
+
1994
+ # `Patch.OpenedSince`'s twin: the cap counts both kinds of PR.
1995
+ query "OpenedSince" do
1996
+ description "Every improvement opened at or after one instant, in number order — the other half of today's count against PR_CAP_PER_DAY."
1997
+ attribute :since, Instant
1998
+ where(:"opened_at.value" => { gte: :since })
1999
+ order_by :number
2000
+ end
2001
+
2002
+ query "All" do
2003
+ description "Every improvement ever landed — what a runner reads back to reconcile against GitHub's own list."
2004
+ order_by :number
2005
+ end
2006
+ end
2007
+
2008
+ # One CI run against one commit. A clearance is minted per commit, so a later commit is stale
2009
+ # by simply having none.
2010
+ aggregate "Clearance" do
2011
+ description "One CI run against one commit, and therefore the only answer to whether that commit is safe to ship."
2012
+
2013
+ identified_by :commit
2014
+
2015
+ attribute :commit, CommitRef
2016
+ attribute :summary, RunSummary
2017
+
2018
+ value_object "CommitRef" do
2019
+ attribute :value, String, pattern: '^[0-9a-fA-F]{7,40}$'
2020
+ end
2021
+
2022
+ value_object "RunSummary" do
2023
+ attribute :value, String, pattern: '[^ \t\n\r]'
2024
+ invariant("a run says what it ran and what came back") { !value.to_s.empty? }
2025
+ end
2026
+
2027
+ lifecycle :status, default: "running" do
2028
+ transition "Passed" => "green", from: "running"
2029
+ transition "Failed" => "red", from: "running"
2030
+ end
2031
+
2032
+ command "Start" do
2033
+ role "QA engineer"
2034
+ goal "Put a commit through CI"
2035
+
2036
+ attribute :commit, CommitRef
2037
+
2038
+ emits "ClearanceStarted"
2039
+ end
2040
+
2041
+ command "Passed" do
2042
+ role "System"
2043
+ goal "Record that the suite went green against this commit"
2044
+
2045
+ reference_to Clearance
2046
+ attribute :summary, RunSummary
2047
+
2048
+ sets :summary
2049
+
2050
+ emits "ClearanceGiven"
2051
+ end
2052
+
2053
+ command "Failed" do
2054
+ role "System"
2055
+ goal "Record that the suite went red against this commit"
2056
+
2057
+ # `refusal`, not `summary`: a policy re-enters with the answering event's payload verbatim,
2058
+ # and the runtime names what a refused ask returned `refusal`.
2059
+ reference_to Clearance
2060
+ attribute :refusal, RunSummary
2061
+
2062
+ sets :summary, to: :refusal
2063
+
2064
+ emits "ClearanceRefused"
2065
+ end
2066
+
2067
+ # Name the commit you are about to ship; an empty answer means nothing has vouched for it.
2068
+ query "For" do
2069
+ description "Is this exact commit cleared? Ask with the sha you are about to deploy; an empty answer is a no, including when nobody has run CI on it at all."
2070
+ attribute :commit, CommitRef
2071
+ where(commit: :commit)
2072
+ where(status: "green")
2073
+ order_by :commit
2074
+ end
2075
+
2076
+ query "Red" do
2077
+ description "Commits CI refused, with what it said."
2078
+ where(status: "red")
2079
+ order_by :commit
2080
+ end
2081
+
2082
+ query "All" do
2083
+ description "Every CI run ever recorded, one per commit."
2084
+ order_by :commit
2085
+ end
2086
+
2087
+ # An answer that is one document of text, exactly as the tool that produced it wrote it.
2088
+ value_object "Document" do
2089
+ attribute :text, String
2090
+ end
2091
+
2092
+ # ClearanceTools answers this by running the script's own command as a child process
2093
+ # (lib/hecks/quality_control/adapters/clearance_tools.rb). The command starts the clearances and
2094
+ # lets the CI port's policies record each verdict; a newly red PR ends it with a report.
2095
+ query "CheckPullRequests" do
2096
+ description "Check CI for every PR the ledger tracks as open, a Patch or an Improvement: retire one GitHub reports merged or closed, and start a clearance for each unsettled head commit."
2097
+ returns Document
2098
+ end
2099
+ end
2100
+
2101
+ # Puts `Bug.Fix`'s commit and CI's `Clearance` for it back together, dispatching `Bug.Regress` on
2102
+ # red. Correlates by commit, which `BugFixed` carries and `Clearance` is identified by.
2103
+ # `ClearanceRefused` has no `ends_on`, as in banking's `ExternalSettlement`: an instance left in
2104
+ # "regressed" costs nothing.
2105
+ process_manager "BugCiWatch" do
2106
+ correlates_by :"commit.value"
2107
+ starts_on Bug::BugFixed
2108
+ ends_on Clearance::ClearanceGiven
2109
+
2110
+ # `bug:` is Bug's own reference key (`Naming.reference_key("Bug")`); the value is the
2111
+ # `reference` that `BugFixed` carries into saga memory.
2112
+ transition Clearance::ClearanceRefused => "regressed", from: "watching" do
2113
+ dispatch Bug::Regress, with: { bug: :reference }
2114
+ end
2115
+ end
2116
+
2117
+ # The `BugCiWatch` mechanism aimed at deliberate work, dispatching `Improvement::Regress`. That
2118
+ # lands in `needs_fix`, not an investigating state; a fresh `Improvement.Land` clears it and
2119
+ # starts a new watch.
2120
+ process_manager "ImprovementCiWatch" do
2121
+ correlates_by :"commit.value"
2122
+ starts_on Improvement::ImprovementLanded
2123
+ ends_on Clearance::ClearanceGiven
2124
+
2125
+ # `improvement:` is the reference key; the value is `:number`, which `ImprovementLanded`
2126
+ # carries.
2127
+ transition Clearance::ClearanceRefused => "regressed", from: "watching" do
2128
+ dispatch Improvement::Regress, with: { improvement: :number }
2129
+ end
2130
+ end
2131
+
2132
+ # Rootless reports: `group_by` gives each group's size, which a query's rows cannot, and they
2133
+ # centre on no record so they take no id.
2134
+
2135
+ # `withdrawn` beside `verified` says whether the practice finds real things or cries wolf.
2136
+ read_model "BugsByStatus" do
2137
+ description "Every bug sorted into what became of it — the tally. The share that is withdrawn, beside the share verified, is the honesty check on the whole practice."
2138
+ include Bug
2139
+ group_by :status, :reference
2140
+ end
2141
+
2142
+ read_model "BugsBySubmitter" do
2143
+ description "Every bug sorted by who reported it, then by its own reference — what each agent has actually found."
2144
+ include Bug
2145
+ group_by :submitter, :reference
2146
+ end
2147
+
2148
+ read_model "BugsByDisposition" do
2149
+ description "Every bug sorted by the call its finder made — self-contained, bigger, or not yet decided — beside its reference. The fix-rate, and the judgment still owed, at a glance."
2150
+ include Bug
2151
+ group_by :disposition, :reference
2152
+ end
2153
+
2154
+ read_model "TicketsByStatus" do
2155
+ description "Every ticket sorted by where it got to — stuck filings and abandoned ones are only visible as a count."
2156
+ include Ticket
2157
+ group_by :status, :reference
2158
+ end
2159
+
2160
+ read_model "ImprovementsByStatus" do
2161
+ description "Every improvement sorted into what became of it — landed, needing a fix, merged, or closed, at a glance."
2162
+ include Improvement
2163
+ group_by :status, :number
2164
+ end
2165
+
2166
+ # `Ticket.Close` is the only way a ticket leaves `filed`, and only the outside can know it
2167
+ # happened, so the fact arrives by whichever route reaches us first.
2168
+ policy "CloseWhenClosedUpstream" do
2169
+ on "Ticket.IssueClosedUpstream"
2170
+ trigger Ticket::Close
2171
+ end
2172
+
2173
+ # The adapter decides by answering or refusing, the only branch a policy can express.
2174
+ policy "ClearOnPass" do
2175
+ on "Clearance.SuitePassed"
2176
+ trigger Clearance::Passed
2177
+ end
2178
+
2179
+ policy "RefuseOnFail" do
2180
+ on "Clearance.SuiteFailed"
2181
+ trigger Clearance::Failed
2182
+ end
2183
+
2184
+ # `Raise` and the call are split so a crash leaves a `Ticket.Raised` row rather than a lie.
2185
+ policy "FileWhenSubmitted" do
2186
+ on "Ticket.TicketSubmitted"
2187
+ trigger Ticket::IssueTracker::File
2188
+ end
2189
+
2190
+ policy "RecordTheIssue" do
2191
+ on "Ticket.IssueFiled"
2192
+ trigger Ticket::Filed
2193
+ end
2194
+
2195
+ policy "RecordTheRefusal" do
2196
+ on "Ticket.IssueFilingRefused"
2197
+ trigger Ticket::Refused
2198
+ end
2199
+
2200
+ # The cap is the `given` on `Retry`, not here: a policy holds no state and would ask forever.
2201
+ policy "AskAgain" do
2202
+ on "Ticket.TicketFilingRefused"
2203
+ trigger Ticket::Retry
2204
+ end
2205
+
2206
+ policy "AskOnceMore" do
2207
+ on "Ticket.TicketRetried"
2208
+ trigger Ticket::IssueTracker::File
2209
+ end
2210
+
2211
+ # Nothing reacts to `TicketRaised` on purpose: a ticket waits until somebody submits it.
2212
+
2213
+ # The rotation's one automatic reaction. Releasing on `SweepConcluded` cannot work: that event
2214
+ # carries no target id, `next_streak` or yield score, so `hecks quality_control ask run` still releases.
2215
+ # `on Sweep::CheckSurprised`, not `Sweep::Check::...`: an entity command's event is stamped with
2216
+ # its parent aggregate. `Check.Surprised` restates `target` so the id rides in.
2217
+ policy "SuspendOnSurprise" do
2218
+ on Sweep::CheckSurprised
2219
+ trigger Target::Suspend, with: {
2220
+ target: :target,
2221
+ reason: { value: "a check surprised — a person releases with hecks quality_control ask run <target> --release --notes" }
2222
+ }
2223
+ end
2224
+ end