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
@@ -0,0 +1,251 @@
1
+ # Pipeline: one change's passage from a planned Work row to a release, declared as its own
2
+ # aggregate instead of read off six other domains. A Passage is keyed by the Work row's key, which
3
+ # is also the branch a landing finishes, so the events the other domains already emit can find it.
4
+ # Every stage is advanced by a policy reacting to one of those events; nothing here asks the
5
+ # outside anything.
6
+ #
7
+ # Tickets Work.WorkPlanned -> Plan (planned)
8
+ # Tickets Work.WorkStarted -> Dispatch (dispatched)
9
+ # Hecks Landing.LandedWorkFound -> Land (landed)
10
+ # Hecks Integration.MainPushed -> Push (pushed), every landed passage at once
11
+ # Hecks RedFlag.RedFlagRaised -> Hold (held), every pushed passage
12
+ # Hecks RedFlag.RedFlagCleared -> Verify (verified), every pushed or held passage
13
+ # Hecks PromotionRun.PromotionCompleted, promoted -> Promote (promoted), every verified passage
14
+ # Hecks Release.Published -> Release (released), every promoted passage
15
+ #
16
+ # A push, a green run, a promotion and a release each cover every passage waiting at the stage
17
+ # before, because main moves as one line: a commit pushed, verified or promoted carries everything
18
+ # landed before it. Stalling is a mark, not a stage: a passage that sat too long where it is keeps
19
+ # its stage and gains the reason, so the board can show it in place.
20
+
21
+ Hecks.bluebook "Pipeline" do
22
+ vision "Every change is one passage, from the Work row that planned it to the release that shipped it, and where each one stands is a fact the pipeline itself holds."
23
+
24
+ supporting
25
+
26
+ aggregate "Passage" do
27
+ description "One change's journey through the pipeline: the stage it is at, what carried it there, and whether it has stalled."
28
+ created_by "Passage.plan!"
29
+
30
+ identified_by :change
31
+
32
+ attribute :change, ChangeKey
33
+ attribute :title, Title, optional: true
34
+ attribute :session, Word, optional: true
35
+ attribute :landed, Word, optional: true
36
+ attribute :pushed, Word, optional: true
37
+ attribute :flag, Word, optional: true
38
+ attribute :stalled, Reason, optional: true
39
+
40
+ # The Work row's key, which the branch that lands it is named after.
41
+ value_object "ChangeKey" do
42
+ attribute :value, String, matches: :present
43
+ invariant("a passage is named by its change") { !value.to_s.empty? }
44
+ end
45
+
46
+ value_object "Title" do
47
+ attribute :value, String
48
+ end
49
+
50
+ # A session id, a commit, or a red flag's id.
51
+ value_object "Word" do
52
+ attribute :value, String, matches: :present
53
+ end
54
+
55
+ # Why a passage stalled; empty once it moved on.
56
+ value_object "Reason" do
57
+ attribute :value, String, default: ""
58
+ end
59
+
60
+ lifecycle :stage, default: "planned" do
61
+ mark :terminal, "released"
62
+ transition "Dispatch" => "dispatched", from: "planned"
63
+ transition "Land" => "landed", from: %w[planned dispatched]
64
+ transition "Push" => "pushed", from: "landed"
65
+ transition "Hold" => "held", from: "pushed"
66
+ transition "Verify" => "verified", from: %w[pushed held]
67
+ transition "Promote" => "promoted", from: "verified"
68
+ transition "Release" => "released", from: "promoted"
69
+ end
70
+
71
+ command "Plan" do
72
+ role "System"
73
+ goal "Open a passage for a Work row the moment it is planned"
74
+
75
+ attribute :change, ChangeKey, sets: true
76
+ attribute :title, Title, optional: true, sets: true
77
+
78
+ emits PassagePlanned
79
+ end
80
+
81
+ command "Dispatch" do
82
+ role "System"
83
+ goal "Record the session a planned change was handed to"
84
+
85
+ reference_to Passage
86
+ attribute :session, Word, sets: true
87
+ sets :stalled, to: { value: "" }
88
+
89
+ emits PassageDispatched
90
+ end
91
+
92
+ command "Land" do
93
+ role "System"
94
+ goal "Record that the change's branch landed on local main"
95
+
96
+ reference_to Passage
97
+ sets :stalled, to: { value: "" }
98
+
99
+ emits PassageLanded
100
+ end
101
+
102
+ command "Push" do
103
+ role "System"
104
+ goal "Record the main commit that carried the landed change to the remote"
105
+
106
+ reference_to Passage
107
+ attribute :pushed, Word, sets: true
108
+ sets :stalled, to: { value: "" }
109
+
110
+ emits PassagePushed
111
+ end
112
+
113
+ command "Hold" do
114
+ role "System"
115
+ goal "Record that main went red while this change waits for a green run"
116
+
117
+ reference_to Passage
118
+ attribute :flag, Word, sets: true
119
+
120
+ emits PassageHeld
121
+ end
122
+
123
+ command "Verify" do
124
+ role "System"
125
+ goal "Record that a green run of main covers the pushed change"
126
+
127
+ reference_to Passage
128
+ sets :stalled, to: { value: "" }
129
+
130
+ emits PassageVerified
131
+ end
132
+
133
+ command "Promote" do
134
+ role "System"
135
+ goal "Record that stable moved past the verified change"
136
+
137
+ reference_to Passage
138
+ sets :stalled, to: { value: "" }
139
+
140
+ emits PassagePromoted
141
+ end
142
+
143
+ command "Release" do
144
+ role "System"
145
+ goal "Record that a published release carries the change"
146
+
147
+ reference_to Passage
148
+
149
+ emits PassageReleased
150
+ end
151
+
152
+ command "Stall" do
153
+ role "Maintainer"
154
+ goal "Mark a passage that has sat too long at its stage, with the reason, without moving it"
155
+
156
+ reference_to Passage
157
+ attribute :stalled, Reason, sets: true
158
+
159
+ emits PassageStalled
160
+ end
161
+
162
+ query "InFlight" do
163
+ description "Every passage that has not been released, oldest change first."
164
+ where(stage: { in: "planned,dispatched,landed,pushed,held,verified,promoted" })
165
+ order_by :change
166
+ end
167
+
168
+ query "AtStage" do
169
+ description "The passages standing at one stage."
170
+ attribute :stage, String
171
+ where(stage: :stage)
172
+ order_by :change
173
+ end
174
+
175
+ query "Landed" do
176
+ description "The passages on local main that no push has carried yet."
177
+ where(stage: "landed")
178
+ order_by :change
179
+ end
180
+
181
+ query "Pushed" do
182
+ description "The passages on the remote waiting for a green run of main, held by a red flag or not."
183
+ where(stage: { in: "pushed,held" })
184
+ order_by :change
185
+ end
186
+
187
+ query "Verified" do
188
+ description "The passages a green run covers that stable has not moved past."
189
+ where(stage: "verified")
190
+ order_by :change
191
+ end
192
+
193
+ query "Promoted" do
194
+ description "The passages on stable that no release has shipped."
195
+ where(stage: "promoted")
196
+ order_by :change
197
+ end
198
+
199
+ query "Stalled" do
200
+ description "Every passage marked as stalled, with the reason."
201
+ where(stalled: { ne: "" })
202
+ order_by :change
203
+ end
204
+ end
205
+
206
+ policy "PlanWhenWorkPlanned" do
207
+ on "Work.WorkPlanned"
208
+ trigger Passage::Plan, with: { change: :work, title: :title }
209
+ end
210
+
211
+ policy "DispatchWhenWorkStarted" do
212
+ on "Work.WorkStarted"
213
+ trigger Passage::Dispatch, with: { change: :work, session: :claimed_by }
214
+ end
215
+
216
+ policy "LandWhenWorkLanded" do
217
+ on "Landing.LandedWorkFound"
218
+ trigger Passage::Land, with: { change: :branch }
219
+ end
220
+
221
+ policy "PushEveryLandedWhenMainPushed" do
222
+ on "Integration.MainPushed"
223
+ for_each "Passage.Landed"
224
+ trigger Passage::Push, with: { change: :change, pushed: :pushed }
225
+ end
226
+
227
+ policy "HoldEveryPushedWhenMainRed" do
228
+ on "RedFlag.RedFlagRaised"
229
+ for_each "Passage.Pushed"
230
+ trigger Passage::Hold, with: { change: :change, flag: :flag }
231
+ end
232
+
233
+ policy "VerifyEveryPushedWhenMainGreen" do
234
+ on "RedFlag.RedFlagCleared"
235
+ for_each "Passage.Pushed"
236
+ trigger Passage::Verify, with: { change: :change }
237
+ end
238
+
239
+ policy "PromoteEveryVerifiedWhenStableMoves" do
240
+ on "PromotionRun.PromotionCompleted"
241
+ where { promoted.value == true }
242
+ for_each "Passage.Verified"
243
+ trigger Passage::Promote, with: { change: :change }
244
+ end
245
+
246
+ policy "ReleaseEveryPromotedWhenPublished" do
247
+ on "Release.Published"
248
+ for_each "Passage.Promoted"
249
+ trigger Passage::Release, with: { change: :change }
250
+ end
251
+ end
@@ -0,0 +1,17 @@
1
+ # The Pipeline bluebook, bound so it boots standing alone (a behaviors or model_check boot of this
2
+ # directory). It persists in Memory like every tooling domain and declares no port: every stage is
3
+ # advanced by an event another domain already emits.
4
+ Hecks.hecksagon "Pipeline" do
5
+ attaches "Governance"
6
+ persisted_by "Memory"
7
+
8
+ # The events that move a passage, as the Tickets and Hecks domains emit them.
9
+ %w[Tickets.WorkPlanned Tickets.WorkStarted Hecks.LandedWorkFound Hecks.MainPushed
10
+ Hecks.RedFlagRaised Hecks.RedFlagCleared Hecks.PromotionCompleted Hecks.Published]
11
+ .each { |event| subscribe event }
12
+ end
13
+
14
+ # The anti-corruption layer for Governance, which the bluebook's roles are checked against.
15
+ Hecks.hecksagon "Governance" do
16
+ persisted_by "Memory"
17
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "probe"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ module PipelineBoard
8
+ # Keeps each outside read for a few seconds, failures included, so a page polling every ten
9
+ # seconds cannot hammer `gh` or the disk.
10
+ class Cache
11
+ # @param ttl [Numeric] seconds an answer is kept
12
+ # @param clock [#call] the time in seconds; the monotonic clock when none is given
13
+ def initialize(ttl: 20, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
14
+ @ttl = ttl
15
+ @clock = clock
16
+ @held = {}
17
+ @lock = Mutex.new
18
+ end
19
+
20
+ # @param key [Object] what is read
21
+ # @return [Object] the held answer while it is fresh, else the block's; a block that raises
22
+ # {Unavailable} is held as the failure and raised again until it expires
23
+ def fetch(key, &block)
24
+ @lock.synchronize do
25
+ kept = @held[key]
26
+ kept = @held[key] = [@clock.call, attempt(&block)] unless kept && fresh?(kept)
27
+ kept.last.is_a?(Unavailable) ? raise(kept.last) : kept.last
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def fresh?(kept) = @clock.call - kept.first < @ttl
34
+
35
+ def attempt
36
+ yield
37
+ rescue Unavailable => e
38
+ e
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "cache"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ # Reads `Dispatch.Fill` as a dry run: which planned rows it would start and which it skips and
9
+ # why, plus the load and cap it saw. It never passes `confirm`, so nothing starts.
10
+ class PipelineFill
11
+ HEADER = /\Aload ([\d.]+) \(limit ([\d.]+), (\d+) cpus\); cap (\d+), (\d+) started/
12
+ WOULD = /\Awould start (\S+): (.*)\z/
13
+ SKIPPED = /\A(\S+) skipped: (.*)\z/
14
+
15
+ # The JSON document the launcher prints, after any log lines.
16
+ JSON_BODY = /\{.*\}/m
17
+
18
+ # @param aggregate [Object, nil] unused
19
+ # @param settings [Hash] `runner:` stands in for the launcher (a callable taking the
20
+ # arguments, answering its standard output), `ttl:` for how long the preview is kept
21
+ # @param root [String, nil] the checkout the launcher runs in
22
+ def initialize(aggregate: nil, settings: {}, root: nil)
23
+ @root = root || Dir.pwd
24
+ @runner = settings[:runner] || ->(*args) { PipelineBoard::Probe.run(*args, chdir: @root, seconds: 120) }
25
+ @cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 60))
26
+ end
27
+
28
+ # @return [Hash] `load`, `limit`, `cap`, `started`, `would_start` (slug and title) and
29
+ # `skipped` (slug and reason)
30
+ # @raise [PipelineBoard::Unavailable] when the launcher cannot answer
31
+ def preview
32
+ @cache.fetch(:fill) { parse(report) }
33
+ end
34
+
35
+ private
36
+
37
+ def report
38
+ out = @runner.call("exe/hecks", "Dispatch.Fill!", "repo.value=#{@root}", "budget_minutes.value=30", "--wait")
39
+ JSON.parse(out[JSON_BODY].to_s).dig("state", "report").to_s
40
+ rescue JSON::ParserError
41
+ raise PipelineBoard::Unavailable, "Dispatch.Fill printed no report"
42
+ end
43
+
44
+ def parse(text)
45
+ lines = text.lines.map(&:strip)
46
+ counts = HEADER.match(lines.first.to_s) or raise PipelineBoard::Unavailable, "unreadable fill report"
47
+ { load: counts[1].to_f, limit: counts[2].to_f, cap: counts[4].to_i, started: counts[5].to_i,
48
+ would_start: pairs(lines, WOULD, :slug, :title), skipped: pairs(lines, SKIPPED, :slug, :reason) }
49
+ end
50
+
51
+ def pairs(lines, pattern, first, second)
52
+ lines.filter_map { |line| (found = pattern.match(line)) && { first => found[1], second => found[2] } }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "cache"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # The `Git` port's adapter: where local main stands against `origin/main`, what the push
10
+ # train's copy last pushed, and whether `stable` has caught up with main. It reads refs and
11
+ # never fetches, so it writes nothing, not even a remote-tracking ref.
12
+ class PipelineGit
13
+ # @param aggregate [Object, nil] unused
14
+ # @param settings [Hash] `git:` stands in for the program (a callable taking a directory
15
+ # and the arguments, answering standard output), `train:` for the train's checkout,
16
+ # `ttl:` for how long a read is kept
17
+ # @param root [String, nil] the checkout whose main is read
18
+ def initialize(aggregate: nil, settings: {}, root: nil)
19
+ @root = root || Dir.pwd
20
+ @git = settings[:git] || ->(dir, *args) { PipelineBoard::Probe.run("git", "-C", dir, *args) }
21
+ @train = settings.fetch(:train) { File.join(Dir.home, ".claude", "hecks-train") }
22
+ @cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 15))
23
+ end
24
+
25
+ # @return [Hash] `ahead`, `behind` (local main against origin/main), `dirty` (changed files in
26
+ # this checkout) and `sha`
27
+ # @raise [PipelineBoard::Unavailable] when git cannot answer
28
+ def local_main
29
+ behind, ahead = ask(@root, "rev-list", "--left-right", "--count", "origin/main...main").split.map(&:to_i)
30
+ { sha: short("main"), ahead: ahead, behind: behind, oldest_unpushed: oldest_unpushed,
31
+ dirty: ask(@root, "status", "--porcelain").lines.size }
32
+ end
33
+
34
+ # @return [Hash] the commit the train's checkout stands on: `sha`, `at`, and `pushed`
35
+ # (whether origin/main holds it)
36
+ # @raise [PipelineBoard::Unavailable] when the train's checkout cannot be read
37
+ def train
38
+ sha, at = ask(@train, "log", "-1", "--format=%h %cI").split
39
+ { sha: sha, at: at, pushed: contains?("origin/main", sha) }
40
+ end
41
+
42
+ # @return [String, nil] when the oldest commit on local main that origin/main lacks was made
43
+ def oldest_unpushed = ask(@root, "log", "--format=%cI", "origin/main..main").lines.last&.strip
44
+
45
+ # @param shas [Array<String>] commits to date
46
+ # @return [Hash{String => String}] each known commit's short sha with when it was made
47
+ def commit_times(shas)
48
+ dated = ask(@root, "log", "-n", "600", "--format=%H %cI", "origin/main").lines.to_h(&:split)
49
+ shas.filter_map { |sha| (full = dated.keys.find { |key| key.start_with?(sha) }) && [sha, dated[full]] }.to_h
50
+ end
51
+
52
+ # The standing red flag of the repository, as its marker file holds it, or nil when main is
53
+ # not flagged.
54
+ #
55
+ # @return [Hash, nil] the flag: id, commit, raised_at, runs and checkins
56
+ def red_flag
57
+ common = ask(@root, "rev-parse", "--path-format=absolute", "--git-common-dir").strip
58
+ path = File.join(common, "hecks-red-flag.json")
59
+ JSON.parse(File.read(path)) if File.exist?(path)
60
+ end
61
+
62
+ # @return [Hash] `behind`, the commits on origin/main that stable lacks, and `sha`
63
+ # @raise [PipelineBoard::Unavailable] when git cannot answer
64
+ def stable
65
+ { sha: short("origin/stable"), behind: ask(@root, "rev-list", "--count", "origin/stable..origin/main").to_i }
66
+ end
67
+
68
+ private
69
+
70
+ def short(ref) = ask(@root, "rev-parse", "--short", ref).strip
71
+
72
+ def contains?(ref, sha)
73
+ ask(@root, "merge-base", "--is-ancestor", sha, ref)
74
+ true
75
+ rescue PipelineBoard::Unavailable
76
+ false
77
+ end
78
+
79
+ def ask(dir, *args) = @cache.fetch([dir, *args]) { @git.call(dir, *args) }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ module PipelineBoard
8
+ # Turns `gh` run and job records into the rows the board shows. A settled run never changes,
9
+ # so its failed-job count is read once.
10
+ module PipelineRuns
11
+ RUN_FIELDS = "databaseId,headSha,startedAt,updatedAt,conclusion,status"
12
+
13
+ private
14
+
15
+ def rows(limit)
16
+ runs = read("run", "list", "--branch", self.class::BRANCH, "--workflow", self.class::WORKFLOW,
17
+ "--limit", limit.to_s, "--json", RUN_FIELDS)
18
+ runs.reverse.map { |run| Thread.new { row(run) } }.map(&:value)
19
+ end
20
+
21
+ def row(run)
22
+ { id: run["databaseId"], sha: run["headSha"].to_s[0, 7], started: run["startedAt"],
23
+ wall_minutes: minutes(run["startedAt"], run["updatedAt"]), status: run["status"],
24
+ conclusion: run["conclusion"].to_s, failed_jobs: failed_jobs(run) }
25
+ end
26
+
27
+ # The jobs that failed, nil when GitHub would not list the run's jobs (so the board says so
28
+ # instead of counting zero).
29
+ def failed_jobs(run)
30
+ return counted(run) unless run["status"] == "completed"
31
+
32
+ @settled[run["databaseId"]] ||= counted(run)
33
+ rescue PipelineBoard::Unavailable
34
+ nil
35
+ end
36
+
37
+ def counted(run) = jobs_of(run["databaseId"]).count { |job| failed?(job) }
38
+
39
+ def failed?(job) = %w[failure timed_out cancelled startup_failure].include?(job["conclusion"])
40
+
41
+ def jobs_of(id) = read("run", "view", id.to_s, "--json", "jobs")["jobs"] || []
42
+
43
+ def slowest(limit)
44
+ done = read("run", "list", "--branch", self.class::BRANCH, "--workflow", self.class::WORKFLOW,
45
+ "--status", "completed", "--limit", "1", "--json", RUN_FIELDS).first
46
+ return [] unless done
47
+
48
+ timed = jobs_of(done["databaseId"]).filter_map { |job| timed(job) }
49
+ timed.sort_by { |job| -job[:minutes] }.first(limit)
50
+ end
51
+
52
+ def timed(job)
53
+ return unless job["startedAt"] && job["completedAt"]
54
+
55
+ { name: job["name"], minutes: minutes(job["startedAt"], job["completedAt"]), conclusion: job["conclusion"].to_s }
56
+ end
57
+
58
+ def minutes(from, upto)
59
+ return 0.0 unless from && upto
60
+
61
+ ((Time.parse(upto) - Time.parse(from)) / 60.0).round(1)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ # Binds the `GitHub` port to `PipelineGitHub`, which answers `CiRuns` and `SlowestJobs` with CI as GitHub reports it.
2
+ # Required by name, not relative, so a copy of this directory shares the one implementation loaded.
3
+ require "hecks/pipeline_board/adapters/pipeline_github"
4
+
5
+ Hecks.adapter "PipelineGitHub" do
6
+ port "GitHub"
7
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "cache"
6
+ require_relative "pipeline_github/runs"
7
+
8
+ module Hecks
9
+ module Adapters
10
+ # The `GitHub` port's adapter: answers `CiRuns` and `SlowestJobs` with CI as GitHub reports it,
11
+ # read through `gh run list` and `gh run view`. It only reads, and keeps every read for a few
12
+ # seconds.
13
+ class PipelineGitHub
14
+ include PipelineBoard::PipelineRuns
15
+
16
+ WORKFLOW = "ci.yml"
17
+ BRANCH = "main"
18
+
19
+ # A release tag that names a version, such as `v7.0.0`.
20
+ VERSION_TAG = /\Av\d/
21
+
22
+ # @param aggregate [Object, nil] unused
23
+ # @param settings [Hash] `gh:` stands in for the program (a callable taking the arguments and
24
+ # answering parsed JSON), `ttl:` for how long a read is kept
25
+ # @param root [String, nil] the checkout `gh` runs in
26
+ def initialize(aggregate: nil, settings: {}, root: nil)
27
+ @gh = settings[:gh] || ->(*args) { PipelineBoard::Probe.json("gh", *args, chdir: root) }
28
+ @cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 20))
29
+ @settled = {}
30
+ end
31
+
32
+ # @param limit [Hash, Integer, nil] how many runs (20 when none is named)
33
+ # @return [Hash{Symbol => String}] `text:` the runs, oldest first, as JSON
34
+ # @raise [PipelineBoard::Unavailable] when `gh` cannot answer
35
+ def ci_runs(limit: nil)
36
+ { text: JSON.generate(rows(count(limit, 20))) }
37
+ end
38
+
39
+ # @param limit [Hash, Integer, nil] how many jobs (8 when none is named)
40
+ # @return [Hash{Symbol => String}] `text:` the last completed run's slowest jobs as JSON
41
+ # @raise [PipelineBoard::Unavailable] when `gh` cannot answer
42
+ def slowest_jobs(limit: nil)
43
+ { text: JSON.generate(slowest(count(limit, 8))) }
44
+ end
45
+
46
+ # @return [Hash, nil] the newest `v` release: `tag`, `published` and `published_at`
47
+ # @raise [PipelineBoard::Unavailable] when `gh` cannot answer
48
+ def latest_release
49
+ listed = read("release", "list", "--limit", "20", "--json", "tagName,isDraft,isPrerelease,publishedAt")
50
+ found = listed.find { |release| release["tagName"].to_s.match?(VERSION_TAG) }
51
+ raise PipelineBoard::Unavailable, "no v release found" unless found
52
+
53
+ { tag: found["tagName"], published: !found["isDraft"], published_at: found["publishedAt"] }
54
+ end
55
+
56
+ # @return [Hash, nil] the newest CI run on main, as {#ci_runs} rows are
57
+ # @raise [PipelineBoard::Unavailable] when `gh` cannot answer
58
+ def latest_run = rows(1).last
59
+
60
+ private
61
+
62
+ def count(value, default)
63
+ number = value.is_a?(Hash) ? value[:value] : value
64
+ number.to_i.positive? ? number.to_i : default
65
+ end
66
+
67
+ def read(*args) = @cache.fetch(args) { @gh.call(*args) }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,7 @@
1
+ # Binds the `Jobs` port to `PipelineJobs`, which answers `Sessions` from each job's state file and `claude agents`.
2
+ # Required by name, not relative, so a copy of this directory shares the one implementation loaded.
3
+ require "hecks/pipeline_board/adapters/pipeline_jobs"
4
+
5
+ Hecks.adapter "PipelineJobs" do
6
+ port "Jobs"
7
+ end