hecks 7.0.0 → 7.1.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 (257) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
  3. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
  4. data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
  5. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
  6. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
  7. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
  8. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
  9. data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
  10. data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
  11. data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
  12. data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
  13. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
  14. data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
  15. data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
  16. data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
  17. data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
  18. data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
  19. data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
  20. data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
  21. data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
  22. data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
  23. data/lib/hecks/adapters/driving/mcp.rb +2 -1
  24. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
  25. data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
  26. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
  27. data/lib/hecks/assembly.rb +18 -4
  28. data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
  29. data/lib/hecks/behaviors/expectations/boot.rb +12 -5
  30. data/lib/hecks/bluebook/acknowledgements.rb +1 -1
  31. data/lib/hecks/bluebook/command.rb +3 -2
  32. data/lib/hecks/bluebook/declaration.rb +2 -1
  33. data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
  34. data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
  35. data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
  36. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
  37. data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
  38. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
  39. data/lib/hecks/bluebook/hecksagon.rb +6 -3
  40. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
  41. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  42. data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
  43. data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
  44. data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
  45. data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
  46. data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
  47. data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
  48. data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
  49. data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
  50. data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
  51. data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
  52. data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
  53. data/lib/hecks/coder/coder.bluebook +3 -1
  54. data/lib/hecks/command_names/index.rb +1 -1
  55. data/lib/hecks/command_names/resolution.rb +15 -5
  56. data/lib/hecks/composer/toolbox.rb +2 -1
  57. data/lib/hecks/corpus.rb +4 -0
  58. data/lib/hecks/doc/reference/readme.rb +1 -1
  59. data/lib/hecks/framework_bluebooks.rb +3 -2
  60. data/lib/hecks/fuzzing/differential.rb +1 -1
  61. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  62. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
  63. data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
  64. data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
  65. data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
  66. data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
  67. data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
  68. data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
  69. data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
  70. data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
  71. data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
  72. data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
  73. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
  74. data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
  75. data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
  76. data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
  77. data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
  78. data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
  79. data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
  80. data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
  81. data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
  82. data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
  83. data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
  84. data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
  85. data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
  86. data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
  87. data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
  88. data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
  89. data/lib/hecks/hecks/adapters/git_train.rb +9 -5
  90. data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
  91. data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
  92. data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
  93. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
  94. data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
  95. data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
  96. data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
  97. data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
  98. data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
  99. data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
  100. data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
  101. data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
  102. data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
  103. data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
  104. data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
  105. data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
  106. data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
  107. data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
  108. data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
  109. data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
  110. data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
  111. data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
  112. data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
  113. data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
  114. data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
  115. data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
  116. data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
  117. data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
  118. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
  119. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
  120. data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
  121. data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
  122. data/lib/hecks/hecks/codebase.behaviors +0 -6
  123. data/lib/hecks/hecks/custodian.behaviors +22 -1
  124. data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
  125. data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
  126. data/lib/hecks/hecks/environments/memory.world +5 -0
  127. data/lib/hecks/hecks/hecks.contextmap +8 -0
  128. data/lib/hecks/hecks/hecks.hecksagon +106 -6
  129. data/lib/hecks/hecks/hecks.world +9 -1
  130. data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
  131. data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
  132. data/lib/hecks/hecks/test/test.bluebook +0 -19
  133. data/lib/hecks/language/adapter.bluebook +13 -0
  134. data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
  135. data/lib/hecks/language/sentence/sentence.bluebook +2 -0
  136. data/lib/hecks/pipeline/pipeline.bluebook +251 -0
  137. data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
  138. data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
  139. data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
  140. data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
  141. data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
  142. data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
  143. data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
  144. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
  145. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
  146. data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
  147. data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
  148. data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
  149. data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
  150. data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
  151. data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
  152. data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
  153. data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
  154. data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
  155. data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
  156. data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
  157. data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
  158. data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
  159. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
  160. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
  161. data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
  162. data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
  163. data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
  164. data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
  165. data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
  166. data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
  167. data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
  168. data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
  169. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
  170. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
  171. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
  172. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
  173. data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
  174. data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
  175. data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
  176. data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
  177. data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
  178. data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
  179. data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
  180. data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
  181. data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
  182. data/lib/hecks/projections/undo.rb +2 -1
  183. data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
  184. data/lib/hecks/projector/cli_projector/renames.rb +4 -2
  185. data/lib/hecks/projector/cli_projector/usage.rb +4 -3
  186. data/lib/hecks/projector/exporter/adapters.rb +3 -2
  187. data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
  188. data/lib/hecks/projector/narrate_projector.rb +10 -3
  189. data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
  190. data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
  191. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
  192. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
  193. data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
  194. data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
  195. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
  196. data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
  197. data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
  198. data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
  199. data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
  200. data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
  201. data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
  202. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
  203. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
  204. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  205. data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
  206. data/lib/hecks/quality_control/quality_control.bluebook +11 -11
  207. data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
  208. data/lib/hecks/query_ir/touchpoints.rb +2 -1
  209. data/lib/hecks/refused.rb +2 -1
  210. data/lib/hecks/release/approval.rb +4 -2
  211. data/lib/hecks/runtime/event_log.rb +27 -0
  212. data/lib/hecks/runtime/event_tap.rb +106 -0
  213. data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
  214. data/lib/hecks/runtime/loader.rb +35 -19
  215. data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
  216. data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
  217. data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
  218. data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
  219. data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
  220. data/lib/hecks/runtime/registry/verification.rb +7 -3
  221. data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
  222. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  223. data/lib/hecks/rust_format.rb +2 -1
  224. data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
  225. data/lib/hecks/shape_ledger.rb +1 -1
  226. data/lib/hecks/storehouse/audit.rb +2 -1
  227. data/lib/hecks/storehouse/identity.rb +6 -3
  228. data/lib/hecks/storehouse.rb +2 -1
  229. data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
  230. data/lib/hecks/tools/ci_workflows.rb +2 -1
  231. data/lib/hecks/tools/document.rb +12 -1
  232. data/lib/hecks/tools/downstream_model_check.rb +4 -2
  233. data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
  234. data/lib/hecks/tools/model_check/examination.rb +25 -2
  235. data/lib/hecks/tools/move_disables.rb +4 -2
  236. data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
  237. data/lib/hecks/tools/rspec_shard_files.rb +12 -15
  238. data/lib/hecks/tools/sequence_script.rb +1 -1
  239. data/lib/hecks/tools/shard_planner.rb +65 -0
  240. data/lib/hecks/tools/stage_progress.rb +2 -1
  241. data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
  242. data/lib/hecks/version.rb +1 -1
  243. data/lib/hecks/vocabulary.rb +8 -9
  244. data/lib/hecks.rb +5 -2
  245. data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
  246. data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
  247. data/rust/host/HECKS_RELEASE +1 -1
  248. data/rust/parser/src/emit.rs +5 -1
  249. data/rust/parser/src/ir.rs +2 -0
  250. data/rust/parser/src/keywords.rs +12 -0
  251. data/rust/parser/src/main.rs +1 -0
  252. data/rust/parser/src/parse/adapter.rs +1 -0
  253. data/rust/parser/src/phrases.rs +16 -0
  254. data/rust/parser/src/sentence/examples.rs +93 -0
  255. data/rust/parser/src/sentence/mod.rs +1 -0
  256. metadata +89 -5
  257. data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
@@ -981,6 +981,99 @@ Hecks.bluebook "CiSpeed" do
981
981
  end
982
982
  end
983
983
 
984
+ # How one family of sharded CI jobs (the rspec shards, the postgres io shards) splits its files
985
+ # among its legs. A plan job asks for the split once; every leg reads that plan, so all legs
986
+ # agree. The split is balanced by the seconds the newest successful main run recorded for each
987
+ # file, and by file size when no such run can be read. The chain, never deeper than three:
988
+ #
989
+ # ShardPlanRequested -> TimingsFetched -> ShardPlanned (source timings)
990
+ # ShardPlanRequested -> TimingsUnavailable -> ShardPlanned (source size)
991
+ #
992
+ # The `TimingsSource` port finds the previous run's timings and the `Planner` port splits; the
993
+ # bluebook says neither how. Run the plan job's command line as
994
+ #
995
+ # exe/hecks run lib/hecks/ci_speed '{"steps":[{"verb":"CiSpeed::ShardPlan.Request","args":{
996
+ # "name":{"value":"rspec"},"shards":{"value":8},"items_from":{"value":"glob:spec/**/*_spec.rb"},
997
+ # "timings_from":{"value":"main"},"out_dir":{"value":"tmp/shard-plan"}}}]}'
998
+ aggregate "ShardPlan" do
999
+ description "One family's split of its files among its shard legs, and where the timings behind it came from."
1000
+
1001
+ identified_by :name
1002
+
1003
+ attribute :name, PlanName
1004
+ attribute :shards, ShardCount, optional: true
1005
+ # `glob:<pattern>` splits the files a pattern finds, `list:<path>` the lines of a file.
1006
+ attribute :items_from, ItemsFrom, optional: true
1007
+ # A directory of timing logs, or `main` for the newest successful CI run on main.
1008
+ attribute :timings_from, TimingsFrom, optional: true
1009
+ # Where the plan is written: `group-<n>.txt` per leg, `runtimes.log` and `plan.json`.
1010
+ attribute :out_dir, OutDir, optional: true
1011
+ # The log one leg timed its own files into, kept as an artifact for the next plan.
1012
+ attribute :recorded_log, OutDir, optional: true
1013
+
1014
+ # The family: `rspec`, `postgres_io`.
1015
+ value_object "PlanName" do
1016
+ attribute :value, String, pattern: '^[a-z][a-z0-9_]*$'
1017
+ end
1018
+
1019
+ value_object "ShardCount" do
1020
+ attribute :value, Integer, default: 1
1021
+ invariant("a family has at least one shard") { value >= 1 }
1022
+ end
1023
+
1024
+ value_object "ItemsFrom" do
1025
+ attribute :value, String, pattern: '^(glob|list):[^ \t\n]+$'
1026
+ end
1027
+
1028
+ value_object "TimingsFrom" do
1029
+ attribute :value, String, pattern: '^[^ \t\n]+$'
1030
+ end
1031
+
1032
+ value_object "OutDir" do
1033
+ attribute :value, String, pattern: '^[^ \t\n]+$'
1034
+ end
1035
+
1036
+ # Where a plan's prices came from: the recorded seconds, or file size.
1037
+ value_object "PlanSource" do
1038
+ attribute :value, String, pattern: '^(timings|size)$'
1039
+ end
1040
+
1041
+ # Lines of `path:seconds`, as the shards' runtime loggers write them.
1042
+ value_object "Timings" do
1043
+ attribute :value, String, default: ""
1044
+ end
1045
+
1046
+ command "Request" do
1047
+ goal "Ask for a family's files to be split among its shards, from the last run's timings when they can be read"
1048
+
1049
+ attribute :name, PlanName
1050
+ attribute :shards, ShardCount
1051
+ attribute :items_from, ItemsFrom
1052
+ attribute :timings_from, TimingsFrom
1053
+ attribute :out_dir, OutDir
1054
+
1055
+ sets :name
1056
+ sets :shards
1057
+ sets :items_from
1058
+ sets :timings_from
1059
+ sets :out_dir
1060
+
1061
+ emits "ShardPlanRequested"
1062
+ end
1063
+
1064
+ command "RecordTimings" do
1065
+ goal "Say which log one shard leg timed its own files into, so the next plan can read it"
1066
+
1067
+ attribute :name, PlanName
1068
+ attribute :recorded_log, OutDir
1069
+
1070
+ sets :name
1071
+ sets :recorded_log
1072
+
1073
+ emits "TimingsRecorded"
1074
+ end
1075
+ end
1076
+
984
1077
  # An experiment runs as soon as it is asked for: the port runs it and answers with the numbers.
985
1078
  policy "MeasureWhenRequested" do
986
1079
  on "Experiment.ExperimentRequested"
@@ -1070,4 +1163,22 @@ Hecks.bluebook "CiSpeed" do
1070
1163
  on "Budget.MissRunWitnessed"
1071
1164
  trigger Budget::Complete, with: { budget: :name }
1072
1165
  end
1166
+
1167
+ # A plan is asked for, and the first thing to do is look for the previous run's timings.
1168
+ policy "FetchTimingsWhenPlanRequested" do
1169
+ on "ShardPlan.ShardPlanRequested"
1170
+ ask :fetch, with: { name: :name }
1171
+ end
1172
+
1173
+ policy "PlanFromFetchedTimings" do
1174
+ on "ShardPlan.TimingsFetched"
1175
+ ask :partition, with: { name: :name, source: { value: "timings" }, timings: :timings }
1176
+ end
1177
+
1178
+ # The fallback: timings that cannot be read are no reason to fail the run, so the files are
1179
+ # split by size instead.
1180
+ policy "PlanBySizeWhenTimingsUnavailable" do
1181
+ on "ShardPlan.TimingsUnavailable"
1182
+ ask :partition, with: { name: :name, source: { value: "size" }, timings: { value: "" } }
1183
+ end
1073
1184
  end
@@ -18,4 +18,26 @@ Hecks.hecksagon "CiSpeed" do
18
18
  refuses "ExperimentRefused"
19
19
  end
20
20
  end
21
+
22
+ # Finds the timings the newest successful main run recorded for a family's files. The adapter
23
+ # reads a directory of logs or downloads them through `gh`, and refuses when it finds none; the
24
+ # bluebook never says which.
25
+ CiSpeed::ShardPlan.port "TimingsSource" do
26
+ asks "Fetch", to: ShardPlan do
27
+ attribute :name, PlanName
28
+ answers "TimingsFetched"
29
+ refuses "TimingsUnavailable"
30
+ end
31
+ end
32
+
33
+ # Splits a family's files among its shards by price (timings, or size) and writes the plan.
34
+ CiSpeed::ShardPlan.port "Planner" do
35
+ asks "Partition", to: ShardPlan do
36
+ attribute :name, PlanName
37
+ attribute :source, PlanSource
38
+ attribute :timings, Timings
39
+ answers "ShardPlanned"
40
+ refuses "PlanRefused"
41
+ end
42
+ end
21
43
  end
@@ -64,6 +64,9 @@ Hecks.bluebook "CiWorkflows" do
64
64
 
65
65
  member workflow: "ci-rspec.yml", name: "ci_attestation_rspec", runner: "ubuntu-latest", timeout_minutes: 10,
66
66
  note: "Separate job because a job-level `if:` cannot read a step output from its own job.\nLets `rspec_shard` skip wholesale when a local run attested this tree.\nSafe to skip on push: `rspec_shard` never reads this result on that trigger."
67
+ member workflow: "ci-rspec.yml", name: "shard_plan", runner: "ubuntu-latest", timeout_minutes: 10,
68
+ condition: "always() && (needs.ci_attestation_rspec.result != 'success' || needs.ci_attestation_rspec.outputs.attested != 'true')",
69
+ note: "Splits the spec files among the shards once, so every leg reads the same plan. It runs the\nCiSpeed bluebook's ShardPlan: the seconds the newest successful run on main recorded for each file\nbalance the legs, and file size does when that run cannot be read. The events of the plan, and the\npolicy that reacted to each, are in the job summary and the shard-plan-trace-rspec artifact."
67
70
  member workflow: "ci-rspec.yml", name: "rspec_shard", runner: "ubuntu-latest", timeout_minutes: 20,
68
71
  condition: "always() && (needs.ci_attestation_rspec.result != 'success' || needs.ci_attestation_rspec.outputs.attested != 'true')",
69
72
  shard_total: 8, shard_axis: "group",
@@ -75,7 +78,9 @@ Hecks.bluebook "CiWorkflows" do
75
78
  attribute :job, String
76
79
  attribute :needs, String
77
80
 
81
+ member workflow: "ci-rspec.yml", job: "shard_plan", needs: "ci_attestation_rspec"
78
82
  member workflow: "ci-rspec.yml", job: "rspec_shard", needs: "ci_attestation_rspec"
83
+ member workflow: "ci-rspec.yml", job: "rspec_shard", needs: "shard_plan"
79
84
  end
80
85
 
81
86
  value_object "Output" do
@@ -93,6 +98,9 @@ Hecks.bluebook "CiWorkflows" do
93
98
  attribute :job, String
94
99
  attribute :key, String
95
100
  attribute :value, String
101
+
102
+ member workflow: "ci-rspec.yml", job: "shard_plan", key: "GH_TOKEN", value: "${{ github.token }}"
103
+ member workflow: "ci-rspec.yml", job: "rspec_shard", key: "GH_TOKEN", value: "${{ github.token }}"
96
104
  end
97
105
 
98
106
  value_object "Step" do
@@ -110,6 +118,25 @@ Hecks.bluebook "CiWorkflows" do
110
118
  member workflow: "ci-rspec.yml", job: "ci_attestation_rspec", name: "attest", id: "attest",
111
119
  uses: "./.github/actions/check-attestation"
112
120
 
121
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "checkout", uses: "actions/checkout@v4"
122
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "ruby", uses: "./.github/actions/setup-ruby",
123
+ note: "The plan boots the CiSpeed bluebook and splits spec files; it builds no Rust."
124
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "environment",
125
+ uses: "./.github/actions/hecks-environment"
126
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "plan",
127
+ title: "Plan the shards: ShardPlan.Request, from the last main run's timings or file size",
128
+ run: "mkdir -p tmp/shard-plan-trace\ncat > tmp/shard-plan-steps.json <<'JSON'\n{\"steps\":[{\"verb\":\"CiSpeed::ShardPlan.Request\",\"args\":{\"name\":{\"value\":\"rspec\"},\"shards\":{\"value\":8},\"items_from\":{\"value\":\"glob:spec/**/*_spec.rb\"},\"timings_from\":{\"value\":\"main\"},\"out_dir\":{\"value\":\"tmp/plan\"}}}]}\nJSON\nbundle exec exe/hecks Operation.run! subject.value=lib/hecks/ci_speed script.value=tmp/shard-plan-steps.json --wait \\\n > tmp/shard-plan-trace/run.json \\\n || echo \"::warning::the shard plan failed; every leg splits by file size\"\n",
129
+ note: "`shards` is the rspec_shard matrix's group count. A plan that cannot be made leaves tmp/plan empty and\neach leg then splits by file size on its own, so a missing plan never fails the run."
130
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "trace",
131
+ title: "Publish the plan's events and the policies that reacted",
132
+ condition: "always()",
133
+ run: "jq -r -f .github/shard-plan-trace.jq tmp/shard-plan-trace/run.json \\\n | tee tmp/shard-plan-trace/trace.md >> \"$GITHUB_STEP_SUMMARY\" \\\n || echo \"::warning::no shard plan trace to publish\"\n"
134
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "upload_plan",
135
+ title: "Upload the plan every leg reads",
136
+ condition: "always()", uses: "actions/upload-artifact@v4"
137
+ member workflow: "ci-rspec.yml", job: "shard_plan", name: "upload_trace",
138
+ title: "Upload the plan's event trace",
139
+ condition: "always()", uses: "actions/upload-artifact@v4"
113
140
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "checkout", uses: "actions/checkout@v4"
114
141
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "ruby", uses: "./.github/actions/setup-ruby"
115
142
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "zsh",
@@ -121,18 +148,22 @@ Hecks.bluebook "CiWorkflows" do
121
148
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "generated_rust",
122
149
  uses: "./.github/actions/generated-rust",
123
150
  note: "rust/src/generated is committed, so the whole tree is in the checkout and this step only checks it.\nA shard reads only the domains named below (rust_build_kernel_input_spec); the input names them and\nchanges nothing. Specs that read the whole tree are tagged `generated_all` and run in the drift job,\nwhich regenerates every domain."
151
+ member workflow: "ci-rspec.yml", job: "rspec_shard", name: "download_plan",
152
+ title: "Read the plan",
153
+ run: "mkdir -p tmp/plan\ngh run download ${{ github.run_id }} --name shard-plan-rspec --dir tmp/plan \\\n || echo \"::warning::no shard plan to read; this leg splits by file size\"\n",
154
+ note: "Every leg reads the one plan the shard_plan job made, so the legs agree on the partition.\nA plan that cannot be read is no reason to fail: the leg splits by file size on its own."
124
155
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "file_list",
125
156
  title: "Determine this shard's own file list",
126
- run: "mkdir -p tmp\nbundle exec exe/hecks query test_suite_run.shard_specs group=${{ matrix.group }} groups=%{shard_total} runtime_log=.github/rspec_runtime_baseline.log \\\n > tmp/rspec_shard_files.txt\n",
127
- note: "`io: true` specs run elsewhere: rspec_rust_io (cargo/wasm) and rspec_postgres_io.\nEvery shard must compute its file list from identical inputs or the lists stop\npartitioning the suite, so grouping reads only the committed baseline, never a cache."
157
+ run: "if [ -s \"tmp/plan/group-${{ matrix.group }}.txt\" ]; then\n cp \"tmp/plan/group-${{ matrix.group }}.txt\" tmp/rspec_shard_files.txt\nelse\n bundle exec exe/hecks test_suite_run.shard_specs group=${{ matrix.group }} groups=%{shard_total} \\\n > tmp/rspec_shard_files.txt\nfi\n",
158
+ note: "`io: true` specs run elsewhere: rspec_rust_io (cargo/wasm) and rspec_postgres_io."
128
159
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "parallel",
129
160
  title: "rspec: parallel (this shard's own file list only)",
130
- run: "FILES=()\nwhile IFS= read -r line; do FILES+=(\"$line\"); done < tmp/rspec_shard_files.txt\nbundle exec parallel_rspec \"${FILES[@]}\" \\\n --group-by runtime --runtime-log .github/rspec_runtime_baseline.log --allowed-missing 100 \\\n -o \"--tag ~io --tag ~fuzzing --tag ~generated_all --profile 15 --format documentation --format ParallelTests::RSpec::RuntimeLogger --out tmp/runtime_rspec.log\"\n",
131
- note: "--group-by runtime: without it parallel_rspec splits by file size and one worker can\ndraw the slowest files. --allowed-missing: spec files newer than the baseline get the\naverage cost instead of failing."
161
+ run: "if [ -s tmp/plan/runtimes.log ]; then\n GROUPING=(--group-by runtime --runtime-log tmp/plan/runtimes.log --allowed-missing 100)\nelse\n GROUPING=(--group-by filesize)\nfi\nFILES=()\nwhile IFS= read -r line; do FILES+=(\"$line\"); done < tmp/rspec_shard_files.txt\nbundle exec parallel_rspec \"${FILES[@]}\" \"${GROUPING[@]}\" \\\n -o \"--tag ~io --tag ~fuzzing --tag ~generated_all --profile 15 --format documentation --format ParallelTests::RSpec::RuntimeLogger --out tmp/runtime_rspec.log\"\n",
162
+ note: "Grouping by runtime keeps one worker from drawing the slowest files; the plan's runtimes.log\nprices every file, so none is missing. Without a plan the workers group by file size."
132
163
  member workflow: "ci-rspec.yml", job: "rspec_shard", name: "upload_log",
133
- title: "Upload this shard's runtime log (for hecks TestSuiteRun.refresh_runtime_baseline! from_run=)",
164
+ title: "Upload this shard's runtime log (the next run's plan reads it)",
134
165
  condition: "always()", uses: "actions/upload-artifact@v4",
135
- note: "Artifact only, never restored: `hecks TestSuiteRun.refresh_runtime_baseline! from_run= <run-id>`\nrebuilds the committed baseline from CI timings."
166
+ note: "Artifact only, never restored by this run: the shard_plan job of the next run downloads\nthe artifacts of the newest successful run on main and balances the legs by these seconds."
136
167
  end
137
168
 
138
169
  value_object "StepInput" do
@@ -146,6 +177,19 @@ Hecks.bluebook "CiWorkflows" do
146
177
  member workflow: "ci-rspec.yml", job: "ci_attestation_rspec", step: "attest", key: "hmac-key",
147
178
  value: "${{ secrets.HECKS_CI_ATTEST_KEY }}"
148
179
 
180
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_plan", key: "name", value: "shard-plan-rspec"
181
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_plan", key: "path", value: "tmp/plan"
182
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_plan", key: "retention-days", value: "14"
183
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_plan", key: "if-no-files-found",
184
+ value: "ignore"
185
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_trace", key: "name",
186
+ value: "shard-plan-trace-rspec"
187
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_trace", key: "path",
188
+ value: "tmp/shard-plan-trace"
189
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_trace", key: "retention-days", value: "14"
190
+ member workflow: "ci-rspec.yml", job: "shard_plan", step: "upload_trace", key: "if-no-files-found",
191
+ value: "ignore"
192
+
149
193
  member workflow: "ci-rspec.yml", job: "rspec_shard", step: "environment", key: "database",
150
194
  value: "${{ secrets.HECKS_JOURNAL_DATABASE }}"
151
195
  member workflow: "ci-rspec.yml", job: "rspec_shard", step: "generated_rust", key: "domains",
@@ -301,6 +301,7 @@ Hecks.bluebook "Coder" do
301
301
  attribute :detail, BriefDetail, optional: true
302
302
  attribute :area, list_of(AreaWord), optional: true
303
303
  attribute :rules, BriefRules
304
+ attribute :review, BriefRules, optional: true
304
305
  attribute :verification, BriefVerification
305
306
  attribute :workflow, BriefRules, optional: true
306
307
  attribute :landing, BriefRules, optional: true
@@ -355,10 +356,11 @@ Hecks.bluebook "Coder" do
355
356
  attribute :area, list_of(AreaWord), optional: true, sets: true
356
357
 
357
358
  sets :rules, to: "Map the whole picture before acting. Act on everything within the area. Stop only for (a) a risk of losing data, (b) an action the owner alone may take: a push, a mint, a release, deleting something that is not yours, or (c) a change outside the area. When stopped, report what was stuck first, then finish everything that does not depend on the stuck part."
359
+ sets :review, to: "Before you edit anything, have your plan reviewed. Write the plan: what you will build, which aggregates, commands, policies, ports, adapters and files it touches, and how it will be checked. Give it to the architect agent (the Agent tool, subagent type architect) and act on its verdict. Sound: build it. Sound with changes: revise the plan as its findings say, then build it. Rethink: edit nothing, hand the row back with exe/hecks tickets Work.yield! to={work} reason.value=\"architect: rethink. <its verdict and first finding>\" --wait, and end. Quote the verdict and each finding you acted on or set aside, with your reason, in your report."
358
360
  sets :verification, to: "Run named specs only through the launcher: exe/hecks test_suite_run.run_spec_example! file.value=<spec> \"example.value= \" --wait. Do not run the full suite: it runs only in CI. Run at most {limit} spec files per call locally (test_suite_run.run_spec_files!), never a whole family. Never use git stash. Keep the quick gate green (exe/hecks gate_run.gate! stage=quick --wait) and RuboCop strict (methods at most 10 lines). Run the quick gate, specs and builds each as ONE blocking command with an explicit Bash timeout of up to 600000 ms; never as a background command with a monitor or poll loop. Run exe/hecks style_run.check_comments! paths=<one file> --wait for each Ruby file you touch."
359
361
  sets :workflow, to: "Pull main first, always: fetch origin, bring local main up to date, then cut the branch. Work in your own worktree named for the row: step 1 is exe/hecks LinkedTree.add! branch.value={work} --wait, which pulls main (a fast-forward, or a rebase of local main's unpushed commits when both moved; it refuses by name when the checkout is dirty) and cuts the branch from it. A session the harness started in a worktree (EnterWorktree cuts from origin/main, not local main) checks git merge-base --is-ancestor origin/main HEAD and rebases onto local main if not, after exe/hecks LinkedTree.pull_main! --wait. Rebase onto local main before landing: local main can hold work that is not pushed, and landing refuses a branch whose base is behind origin/main. Put no Claude attribution trailer in a commit message: no Co-Authored-By line and no Claude-Session line. A worktree that holds ignored files is kept after landing and reported: remove it by hand."
360
362
  sets :landing, to: "Regenerate only if a bluebook, hecksagon, Vocabulary row or generator changed: exe/hecks regeneration_run.regenerate_corpus! --confirm --wait, then --check. Run exe/hecks conformance_run.check_shape_ledger! --wait; a purely additive move uses --confirm --additive, any other move: stop and report it. Land with exe/hecks landing.finish! --confirm: one step that rebases onto local main, runs the quick gate, lands through the queue and removes the worktree and branch. It refuses by name at the first failure (unsaved work, conflict, gate red, queue busy): fix that and run it again. If landing refuses because a landing is in flight, run it again once; it queues. Never poll or sleep on a lock, and never run a lock file by hand."
361
- sets :restraints, to: "Do not push. Never set HECKS_OWNER_PUSH and never use --no-verify: main is pushed only by boarding the push train. Do not mint an era. While main CI is red, only a push that fixes a red-* Work row goes out (exe/hecks tickets query Work.undone lists them), through the push train; everything else lands locally with landing.land! and waits until CI on origin/main is green. Do not run the full spec suite locally; it runs in CI. Do not write to a live database (tickets.db, agent_run.db or any other under ~/.claude): use Memory or a temp file in specs. Never touch a local database that may hold client data."
363
+ sets :restraints, to: "Do not push. Never set HECKS_OWNER_PUSH and never use --no-verify: main is pushed only by boarding the push train. Do not mint an era. A red main raises a red flag (the RedFlagRaised event; exe/hecks RedFlag.status says whether one stands, and `exe/hecks Operation.follow domain.value=Hecks aggregate.value=RedFlag from_now=true wait=600` is the one line that listens, and it returns when RedFlagRaised or RedFlagCleared is recorded). On RedFlagRaised stop feature work and check in with exe/hecks RedFlag.check_in! session.value=<you> taking.value=<a red-* Work row or a cause> (exe/hecks tickets Work.undone lists the rows); Landing and the push train then refuse everything except the fix of a red-* row, by name (main is red: flag <id>), and a red-* row's fix goes out through the push train. Resume on RedFlagCleared, when CI on main is green again. Do not run the full spec suite locally; it runs in CI. Do not write to a live database (tickets.db, agent_run.db or any other under ~/.claude): use Memory or a temp file in specs. Never touch a local database that may hold client data."
362
364
  sets :report, to: "End with a report: what changed, each with file:line; the spec counts (examples and failures); the shape-ledger action you took; and the sha you landed, or why nothing landed."
363
365
  sets :coordination, to: "COORDINATION: other sessions are working in these areas. Do not edit them:"
364
366
  sets :freeze, to: "FREEZE: the persisted shape of these may not move. If your change moves one, commit and hold, do not land, and report 'ready, held for the freeze':"
@@ -21,7 +21,7 @@ module Hecks
21
21
  typed = rest.drop(asking ? 1 : 0).first
22
22
  return if typed.nil? || free?(scope, typed)
23
23
 
24
- Resolution.new(scope, typed, asking).problem
24
+ Resolution.new(scope, typed, asking, old_spelling: rest.first == "query").problem
25
25
  end
26
26
 
27
27
  private
@@ -15,13 +15,15 @@ module Hecks
15
15
 
16
16
  # @param scope [Hash] one scope of `CliRunner.name_index`
17
17
  # @param typed [String] the word, with its `!` if it has one
18
- # @param asking [Boolean] whether `query` came before it
19
- def initialize(scope, typed, asking)
18
+ # @param asking [Boolean] whether `query` (or `ask`) came before it
19
+ # @param old_spelling [Boolean] whether the word before it was `query`, the old spelling
20
+ def initialize(scope, typed, asking, old_spelling: false)
20
21
  @scope = scope
21
22
  @cli = { names: { command: scope["command"], question: scope["question"] } }
22
23
  @typed = typed.chomp("!")
23
24
  @bang = typed.end_with?("!")
24
25
  @asking = asking
26
+ @old_spelling = old_spelling
25
27
  @name = RUNNER.canonical_name(@cli, @typed)
26
28
  end
27
29
 
@@ -29,7 +31,7 @@ module Hecks
29
31
  def problem
30
32
  return unknown unless command || question
31
33
 
32
- bare_command || banged_query || query_of_command
34
+ bare_command || banged_query || query_of_command || query_word
33
35
  end
34
36
 
35
37
  private
@@ -47,7 +49,15 @@ module Hecks
47
49
  def banged_query
48
50
  return unless @bang && !@asking && !command && question
49
51
 
50
- Problem.new("#{@typed}! is a query and reads; write it without the !", "query #{question}")
52
+ Problem.new("#{@typed}! is a query and reads; write it without the !", question.to_s)
53
+ end
54
+
55
+ # A query is written bare (ADR 0163 rule 6); `query` stays only where a command shares the
56
+ # name.
57
+ def query_word
58
+ return unless @old_spelling && question && !command
59
+
60
+ Problem.new("query is the old spelling; write #{@typed} bare", question.to_s)
51
61
  end
52
62
 
53
63
  def query_of_command
@@ -74,7 +84,7 @@ module Hecks
74
84
  def listed(near)
75
85
  return "#{@scope["command"][near]}!" if @scope["command"][near]
76
86
 
77
- "query #{@scope["question"][near]}"
87
+ @scope["question"][near].to_s
78
88
  end
79
89
  end
80
90
  end
@@ -157,7 +157,8 @@ module Hecks
157
157
  Hecks.boot(File.join(Parse::ROOT, "lib/hecks/hecks"), install_driving: false, environment: "memory")
158
158
  end
159
159
 
160
- # A safe-listed command writes through the runner only with its bang; the list names bare commands.
160
+ # A safe-listed command writes through the runner only with its bang; the list names bare
161
+ # commands.
161
162
  def writing(argv)
162
163
  return argv unless argv.first && SAFE_COMMANDS.include?(argv.first)
163
164
 
data/lib/hecks/corpus.rb CHANGED
@@ -28,6 +28,10 @@ module Hecks
28
28
  # The Coder and Fixer domains keep their bluebook beside their hecksagon in their own folder.
29
29
  coder: "lib/hecks/coder",
30
30
  fixer: "lib/hecks/fixer",
31
+ # The PipelineBoard domain keeps its bluebook beside its hecksagon and adapters.
32
+ board: "lib/hecks/pipeline_board",
33
+ # The Pipeline domain: one passage per change, moved by the other domains' events.
34
+ pipeline: "lib/hecks/pipeline",
31
35
  # A package vendored via `attaches ... from: :vendor` — nested inside the
32
36
  # consuming example's own checkout, not this gem's own framework/bluebook/.
33
37
  vendored: "examples/*/vendor/embryonaut_bluebooks/*"
@@ -44,7 +44,7 @@ module Hecks
44
44
  def diagram_showcase(root)
45
45
  lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
46
46
  <<~MARKDOWN.strip
47
- `hecks query introspection.project_diagrams` reads a booted domain's own declaration and draws it as Mermaid — `<Name>_lifecycle.mmd`, `relationships.mmd`, `dispatch.mmd`, `roles.mmd`, `ports.mmd`, `read_models.mmd`, `<Name>_surface.mmd` (what a command does, and what it writes), `<Name>_saga.mmd`, and `frameworks.mmd`. Nothing hand-drawn — the same reason a domain is data at all. Order's own lifecycle, straight off the bluebook above:
47
+ `hecks introspection.project_diagrams` reads a booted domain's own declaration and draws it as Mermaid — `<Name>_lifecycle.mmd`, `relationships.mmd`, `dispatch.mmd`, `roles.mmd`, `ports.mmd`, `read_models.mmd`, `<Name>_surface.mmd` (what a command does, and what it writes), `<Name>_saga.mmd`, and `frameworks.mmd`. Nothing hand-drawn — the same reason a domain is data at all. Order's own lifecycle, straight off the bluebook above:
48
48
 
49
49
  ```mermaid
50
50
  #{lifecycle}
@@ -7,7 +7,7 @@ require_relative "sentence_form"
7
7
  module Hecks
8
8
  # The bluebooks the gem carries that a hecksagon can attach by name (ADR 0080): the language
9
9
  # declared in itself, Expression, Tenancy, Deploy, Site, Tickets, Composer, QualityControl,
10
- # Fixer, Coder, Bulk and CiPipeline.
10
+ # Fixer, Coder, Bulk, CiPipeline, PipelineBoard and Pipeline.
11
11
  # Framework members stay in `Framework`; `table` and `attach!` find a name across both.
12
12
  #
13
13
  # A bluebook is named by the `Hecks.bluebook "Name"` header of its files, and may span several.
@@ -19,7 +19,8 @@ module Hecks
19
19
  GLOBS = %w[language/**/*.bluebook grammar/expression.bluebook tenancy/bluebook/*.bluebook
20
20
  deploy/bluebook/*.bluebook site/bluebook/*.bluebook rails_app/bluebook/*.bluebook tickets/bluebook/*.bluebook
21
21
  composer/bluebook/*.bluebook quality_control/*.bluebook fixer/*.bluebook coder/*.bluebook
22
- bulk/bluebook/*.bluebook ci_pipeline/*.bluebook].freeze
22
+ bulk/bluebook/*.bluebook ci_pipeline/*.bluebook pipeline_board/*.bluebook
23
+ pipeline/*.bluebook].freeze
23
24
 
24
25
  # A `Hecks.bluebook "Name"` header line, capturing the bluebook's name.
25
26
  BLUEBOOK_HEADER = /\A\s*Hecks\.bluebook\s+"([^"]+)"/
@@ -11,7 +11,7 @@ require_relative "differential/wire_comparison"
11
11
  module Hecks
12
12
  module Fuzzing
13
13
  # The Ruby-vs-Rust comparison of one generated sequence, shared by the QualityControl sweep
14
- # (`hecks quality_control query sweep.run`) and `hecks quality_control
14
+ # (`hecks quality_control sweep.run`) and `hecks quality_control
15
15
  # target.check_generated_domains --rust`.
16
16
  #
17
17
  # `differ` is duck-typed (the `RustConformanceHelpers` comparison helpers plus a
@@ -9,7 +9,7 @@ module Hecks
9
9
 
10
10
  # `kind:` on a `checked: false` result distinguishes a target with no
11
11
  # lineage to audit (`:not_applicable`) from an audit that failed to
12
- # run (`:error`) — `hecks quality_control query sweep.run` holds the first and surfaces the
12
+ # run (`:error`) — `hecks quality_control sweep.run` holds the first and surfaces the
13
13
  # second as a finding, instead of treating both as a clean audit.
14
14
  def diverged_ancestor_writes(domain_path)
15
15
  registry, directory = load_registry(domain_path)
@@ -51,7 +51,7 @@ module Hecks
51
51
  # `:postgres_era` is the only mode that exercises era/lineage-bound SQL; plain
52
52
  # `:postgres` never touches that machinery. Unlike `:postgres`, there's no shared
53
53
  # scratch constant here — `database:`/`schema:` are required keyword args because
54
- # the caller (`hecks quality_control query sweep.run --persistence-parity`) owns that
54
+ # the caller (`hecks quality_control sweep.run --persistence-parity`) owns that
55
55
  # database's lifecycle.
56
56
  def rebind_to_postgres_era!(copy, database:, schema:, disposable: true)
57
57
  require "pg"
@@ -11,7 +11,7 @@ module Hecks
11
11
 
12
12
  # Replays `steps` on both adapters and lists where the results differ.
13
13
  #
14
- # Compares the same six fields as `hecks quality_control query sweep.run`'s Ruby-vs-Rust mode.
14
+ # Compares the same six fields as `hecks quality_control sweep.run`'s Ruby-vs-Rust mode.
15
15
  # Both results are JSON-round-tripped so plain data compares equal on both sides.
16
16
  #
17
17
  # @param left [Symbol] an `IsolatedBoot` adapter: `:memory`, `:sqlite`, `:postgres`,
@@ -41,7 +41,7 @@ module Hecks
41
41
  wasm_front: %w[rust]
42
42
  }.freeze
43
43
 
44
- # The modes `hecks quality_control query sweep.run` can run; one absent here is refused at
44
+ # The modes `hecks quality_control sweep.run` can run; one absent here is refused at
45
45
  # start, not resolved.
46
46
  RUNNABLE_MODES = %i[differential ruby_only self_consistency properties_in_differential
47
47
  structural_skip_report adapter_parity_sqlite persistence_parity
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/ci_runs"
4
+
5
+ Hecks.adapter "CiRuns" do
6
+ port "CiRuns"
7
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "console_capture"
5
+ require_relative "main_integration/command_runner"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # The `CiRuns` port's adapter for `Integration.Observe`: reads, through `gh`, the CI run of a
10
+ # push of main at a landed commit. It answers `running` while there is no run yet or it has not
11
+ # completed, `passed` when it succeeded, and `failed` with the jobs that did not succeed.
12
+ class CiRuns
13
+ # The workflow whose run is read, as GitHub names it.
14
+ WORKFLOW = "CI"
15
+
16
+ # Job conclusions that are not a failure.
17
+ CLEAN = %w[success skipped neutral].freeze
18
+
19
+ class << self
20
+ # @return [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`; the command
21
+ # runner when nil. A spec replaces it with a fake GitHub.
22
+ attr_accessor :runner
23
+ end
24
+
25
+ # @param aggregate [Object, nil] unused
26
+ # @param settings [Hash] unused
27
+ # @param root [String, nil] unused
28
+ def initialize(aggregate: nil, settings: {}, root: nil); end
29
+
30
+ # @param held [Hash] the `Integration` record: `landed` and `checkout`
31
+ # @return [Hash{Symbol => Hash}] `outcome:` (`running`, `passed` or `failed`) and `report:`
32
+ # @raise [ConsoleCapture::Failure] when `gh` cannot be read
33
+ def watch(**held)
34
+ landed = plain(held[:landed]).to_s
35
+ @checkout = plain(held[:checkout]) || Dir.pwd
36
+ run = latest_run(landed)
37
+ return answer("running", "#{landed[0, 10]}: no #{WORKFLOW} run yet") unless run
38
+ return answer("running", "#{landed[0, 10]}: #{WORKFLOW} run #{run["databaseId"]} is #{run["status"]}") unless
39
+ run["status"] == "completed"
40
+
41
+ verdict(landed, run)
42
+ end
43
+
44
+ private
45
+
46
+ def latest_run(landed)
47
+ gh("run", "list", "--workflow", WORKFLOW, "--branch", "main", "--event", "push", "--commit", landed,
48
+ "--json", "databaseId,status,conclusion", "--limit", "1").first
49
+ end
50
+
51
+ def verdict(landed, run)
52
+ id = run["databaseId"]
53
+ return answer("passed", "#{landed[0, 10]}: #{WORKFLOW} run #{id} passed") if run["conclusion"] == "success"
54
+
55
+ answer("failed", "#{landed[0, 10]}: #{WORKFLOW} run #{id} #{run["conclusion"]}; failed jobs: #{failed_jobs(id)}")
56
+ end
57
+
58
+ def failed_jobs(id)
59
+ jobs = gh("run", "view", id.to_s, "--json", "jobs").fetch("jobs", [])
60
+ names = jobs.reject { |job| CLEAN.include?(job["conclusion"].to_s) }.map { |job| job["name"] }
61
+ names.empty? ? "none named" : names.join(", ")
62
+ end
63
+
64
+ def gh(*args)
65
+ ok, output = runner.call(["gh", *args], chdir: @checkout)
66
+ raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} failed: #{output.to_s.strip}" unless ok
67
+
68
+ JSON.parse(output)
69
+ rescue JSON::ParserError
70
+ raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} answered no JSON"
71
+ end
72
+
73
+ def answer(outcome, report) = { outcome: { value: outcome }, report: { value: report }, checkout: { value: @checkout } }
74
+
75
+ def runner = self.class.runner || MainIntegration::CommandRunner.new
76
+
77
+ def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+ require_relative "console_capture"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ # The agent types the `claude` command can start in a directory: the built-in ones, the user's
9
+ # and the directory's own.
10
+ #
11
+ # `claude` has no listing of its own, but it names every available agent when asked for one it
12
+ # does not have, and then stops before it starts anything. The list is read from that answer,
13
+ # in the directory a session would work in, so the directory's own agents are counted. An
14
+ # answer that names no list is refused with what `claude` said, so a reworded message is seen at
15
+ # once, not taken for an empty list.
16
+ class ClaudeAgentList
17
+ # An agent name no one gives an agent, asked for only to make `claude` name the real ones.
18
+ PROBE = "hecks-probe-not-an-agent"
19
+
20
+ # How `claude` names the agents it has, after refusing one it has not.
21
+ AVAILABLE = /Available agents:\s*(.+)$/
22
+
23
+ # @param command [String] the program that starts sessions
24
+ # @param shell [Shell] runs it
25
+ def initialize(command, shell: Shell.new)
26
+ @command = command
27
+ @shell = shell
28
+ end
29
+
30
+ # @param directory [String] where a session would work
31
+ # @return [Array<String>] the agent names `claude` lists there
32
+ # @raise [ConsoleCapture::Failure] when its answer names no list
33
+ def names(directory)
34
+ result = @shell.capture(@command, "--agent", PROBE, "-p", "list the agents", chdir: directory)
35
+ said = "#{result.out}\n#{result.err}"
36
+ listed = said[AVAILABLE, 1]
37
+ raise ConsoleCapture::Failure, "#{@command} listed no agents: #{said.strip[0, 300]}" unless listed
38
+
39
+ listed.split(",").map(&:strip).reject(&:empty?)
40
+ end
41
+ end
42
+ end
43
+ end