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,190 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Pipeline Sequence</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink2: #52514e; --muted: #898781;
11
+ --grid: #e1e0d9; --axis: #c3c2b7; --border: rgba(11,11,11,.10);
12
+ --series1: #2a78d6; --good: #0ca30c; --warning: #fab219; --critical: #d03b3b;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root:not([data-theme="light"]) {
16
+ color-scheme: dark;
17
+ --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
18
+ --grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10); --series1: #3987e5;
19
+ }
20
+ }
21
+ :root[data-theme="dark"] {
22
+ color-scheme: dark;
23
+ --page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
24
+ --grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10); --series1: #3987e5;
25
+ }
26
+ * { box-sizing: border-box; }
27
+ body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
28
+ main { max-width: 1280px; margin: 0 auto; padding: 16px; }
29
+ header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; justify-content: space-between; margin-bottom: 12px; }
30
+ h1 { font-size: 20px; margin: 0; font-weight: 600; }
31
+ a { color: var(--series1); }
32
+ .meta { color: var(--ink2); font-size: 13px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
33
+ button, input, select { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--axis); border-radius: 6px; padding: 4px 10px; }
34
+ button { cursor: pointer; }
35
+ button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--series1); outline-offset: 2px; }
36
+ section { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px; margin-bottom: 16px; }
37
+ .legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--ink2); margin-bottom: 8px; }
38
+ .scroller { overflow: auto; max-height: 76vh; border: 1px solid var(--grid); border-radius: 8px; background: var(--page); position: relative; }
39
+ .lanes { position: sticky; top: 0; z-index: 2; display: flex; background: var(--surface); border-bottom: 1px solid var(--axis); min-width: max-content; }
40
+ .lane { width: 150px; flex: 0 0 150px; padding: 6px 6px 4px; text-align: center; font-size: 12px; border-right: 1px solid var(--grid); overflow-wrap: anywhere; }
41
+ .lane b { display: block; font-weight: 600; color: var(--ink); }
42
+ .lane span { color: var(--ink2); font-size: 11px; }
43
+ .lane i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; vertical-align: middle; margin-right: 4px; }
44
+ canvas { display: block; }
45
+ .unknown-note { color: var(--ink2); font-style: italic; padding: 8px; }
46
+ @media (max-width: 700px) { .lane { width: 110px; flex-basis: 110px; } }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <main>
51
+ <header>
52
+ <h1>Pipeline Sequence</h1>
53
+ <div class="meta">
54
+ <a href="/">Board</a><a href="/wiring">Wiring</a>
55
+ <span id="count">loading</span>
56
+ <span id="live">stream: connecting</span>
57
+ <label>domain <select id="domain"><option value="">all</option></select></label>
58
+ <button id="follow" type="button" aria-pressed="true">Following</button>
59
+ <button id="clear" type="button">Clear</button>
60
+ </div>
61
+ </header>
62
+ <section aria-labelledby="h-seq">
63
+ <h2 id="h-seq" style="font-size:13px;margin:0 0 8px;color:var(--ink2);text-transform:uppercase;letter-spacing:.04em">One lifeline per aggregate; every event, the policy that heard it and the command it sent, in order</h2>
64
+ <div class="legend">
65
+ <span><b style="color:var(--good)">&#10003;</b> delivered</span>
66
+ <span><b style="color:var(--warning)">&#9650;</b> waiting</span>
67
+ <span><b style="color:var(--critical)">&#10005;</b> refused</span>
68
+ <span>a tick on a lifeline is an event nobody reacted to; an arrow back to the same line is a policy on its own aggregate</span>
69
+ </div>
70
+ <div class="scroller" id="scroller">
71
+ <div class="lanes" id="lanes"></div>
72
+ <canvas id="seq" role="img" aria-label="Sequence diagram of pipeline events and the policies and commands that answered them"></canvas>
73
+ </div>
74
+ </section>
75
+ </main>
76
+ <script>
77
+ "use strict";
78
+ const $ = (id) => document.getElementById(id);
79
+ const css = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
80
+ const SLOTS = { light: ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"],
81
+ dark: ["#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"] };
82
+ const dark = () => matchMedia("(prefers-color-scheme: dark)").matches && document.documentElement.dataset.theme !== "light" || document.documentElement.dataset.theme === "dark";
83
+ const LANE = matchMedia("(max-width: 700px)").matches ? 110 : 150, ROW = 30, TOP = 10;
84
+ const OUTCOME = { ok: ["--good", "✓"], wait: ["--warning", "▲"], bad: ["--critical", "✕"] };
85
+ const canvas = $("seq"), ctx = canvas.getContext("2d");
86
+ let graph = { nodes: [], edges: [] }, byId = new Map(), domainSlot = new Map();
87
+ const lanes = [], laneOf = new Map(), rows = [], seen = new Set();
88
+ let follow = true, filter = "";
89
+
90
+ function h(tag, attrs, ...kids) {
91
+ const el = document.createElement(tag);
92
+ for (const [k, v] of Object.entries(attrs || {})) el.setAttribute(k, v);
93
+ for (const kid of kids.flat()) if (kid !== null && kid !== undefined) el.append(kid.nodeType ? kid : String(kid));
94
+ return el;
95
+ }
96
+ function clock(iso) { const d = new Date(iso); return isNaN(d) ? "" : d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
97
+ function colourOf(domain) { const i = domainSlot.get(domain); return i === undefined || i > 7 ? css("--muted") : SLOTS[dark() ? "dark" : "light"][i]; }
98
+ function contextOf(id) { const n = byId.get(id); return n ? (n.context ? n.domain + " / " + n.context : n.domain) : id.split("::")[0]; }
99
+
100
+ /* ---- lifelines appear the first time an aggregate speaks or is spoken to ---- */
101
+ function lane(id) {
102
+ if (laneOf.has(id)) return laneOf.get(id);
103
+ const domain = id.split("::")[0], label = id.split("::").pop();
104
+ const entry = { id, domain, label, context: contextOf(id), x: TOP + lanes.length * LANE + LANE / 2 };
105
+ lanes.push(entry); laneOf.set(id, entry);
106
+ $("lanes").append(h("div", { class: "lane" }, h("b", {}, h("i", { style: "background:" + colourOf(domain) }), label), h("span", {}, entry.context)));
107
+ return entry;
108
+ }
109
+ function outcomeKind(text) { return /deliver/.test(text) ? "ok" : /refus|fail/.test(text) ? "bad" : "wait"; }
110
+ function targetOf(policy) {
111
+ const bare = String(policy).split("::").pop(), e = graph.edges.find((x) => x.label === bare);
112
+ return e ? e.to : null;
113
+ }
114
+ function take(ev) {
115
+ const reactions = ev.reactions || [];
116
+ if (!reactions.length) { const key = ev.key + "|"; if (seen.has(key)) return; seen.add(key); rows.push({ at: ev.at, from: ev.aggregate, to: null, event: ev.name, fresh: performance.now() }); return; }
117
+ for (const r of reactions) {
118
+ const kind = outcomeKind(r.outcome || ""), key = ev.key + "|" + r.policy + "|" + kind;
119
+ if (seen.has(key)) continue; seen.add(key);
120
+ rows.push({ at: ev.at, from: ev.aggregate, to: targetOf(r.policy) || ev.aggregate, event: ev.name, policy: String(r.policy).split("::").pop(),
121
+ command: r.command, kind, fresh: performance.now() });
122
+ }
123
+ }
124
+ function shown(row) { return !filter || row.from.startsWith(filter + "::") || (row.to || "").startsWith(filter + "::"); }
125
+
126
+ /* ---- drawing ---- */
127
+ function render() {
128
+ const visible = rows.filter(shown);
129
+ for (const row of visible) { lane(row.from); if (row.to) lane(row.to); }
130
+ const dpr = devicePixelRatio || 1, width = Math.max(TOP * 2 + lanes.length * LANE, $("scroller").clientWidth), height = TOP + visible.length * ROW + 20;
131
+ canvas.style.width = width + "px"; canvas.style.height = height + "px"; canvas.width = width * dpr; canvas.height = height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
132
+ ctx.clearRect(0, 0, width, height);
133
+ for (const l of lanes) { ctx.strokeStyle = colourOf(l.domain); ctx.globalAlpha = 0.45; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(l.x, 0); ctx.lineTo(l.x, height); ctx.stroke(); }
134
+ ctx.setLineDash([]); ctx.globalAlpha = 1;
135
+ const now = performance.now();
136
+ visible.forEach((row, i) => drawRow(row, TOP + i * ROW + ROW / 2, now));
137
+ $("count").textContent = lanes.length + " lifelines, " + visible.length + " messages";
138
+ if (follow) $("scroller").scrollTop = $("scroller").scrollHeight;
139
+ }
140
+ function drawRow(row, y, now) {
141
+ const a = laneOf.get(row.from), b = row.to ? laneOf.get(row.to) : null, fresh = Math.max(0, 1 - (now - row.fresh) / 2400);
142
+ ctx.font = "11px system-ui, sans-serif"; ctx.fillStyle = css("--muted"); ctx.textAlign = "left"; ctx.fillText(clock(row.at), 4, y + 4);
143
+ if (!b) {
144
+ ctx.fillStyle = css("--ink2"); ctx.fillRect(a.x - 4, y - 4, 8, 8);
145
+ ctx.fillStyle = css("--ink2"); ctx.fillText(row.event, a.x + 10, y + 4); return;
146
+ }
147
+ const [token, mark] = OUTCOME[row.kind], colour = css(token);
148
+ ctx.strokeStyle = colour; ctx.fillStyle = colour; ctx.lineWidth = 1.5 + fresh * 1.5; ctx.globalAlpha = 0.7 + 0.3 * fresh;
149
+ if (a === b) {
150
+ ctx.beginPath(); ctx.moveTo(a.x, y - 8); ctx.lineTo(a.x + 22, y - 8); ctx.lineTo(a.x + 22, y + 6); ctx.lineTo(a.x + 2, y + 6); ctx.stroke(); head(a.x + 2, y + 6, -1);
151
+ } else {
152
+ const dir = b.x > a.x ? 1 : -1; ctx.beginPath(); ctx.moveTo(a.x, y + 6); ctx.lineTo(b.x - dir * 4, y + 6); ctx.stroke(); head(b.x - dir * 4, y + 6, dir);
153
+ }
154
+ ctx.globalAlpha = 1; ctx.lineWidth = 1;
155
+ const text = mark + " " + row.event + " → " + row.policy + (row.command ? " → " + row.command.split("::").pop() : "");
156
+ const left = Math.min(a.x, b.x) + 8, maxW = Math.max(Math.abs(b.x - a.x) - 16, 150);
157
+ ctx.font = "11px system-ui, sans-serif"; ctx.fillStyle = css("--ink"); ctx.textAlign = "left";
158
+ ctx.fillText(fit(text, maxW), left, y - 2);
159
+ }
160
+ function head(x, y, dir) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - dir * 7, y - 4); ctx.lineTo(x - dir * 7, y + 4); ctx.closePath(); ctx.fill(); }
161
+ function fit(text, maxW) { while (text.length > 8 && ctx.measureText(text).width > maxW) text = text.slice(0, -2); return text.length < 8 ? text : text; }
162
+
163
+ /* ---- stream ---- */
164
+ let backlog = [], liveStarted = false, dirty = false;
165
+ function listen() {
166
+ const es = new EventSource("/api/events");
167
+ es.onopen = () => { $("live").textContent = "stream: live"; };
168
+ es.onerror = () => { $("live").textContent = "stream: reconnecting"; };
169
+ es.addEventListener("board", (m) => {
170
+ const ev = JSON.parse(m.data); if (!ev.key) return;
171
+ if (!liveStarted) { backlog.push(ev); clearTimeout(listen.t); listen.t = setTimeout(startLive, 600); } else { take(ev); dirty = true; }
172
+ });
173
+ }
174
+ function startLive() { liveStarted = true; backlog.sort((a, b) => String(a.at).localeCompare(String(b.at))).forEach(take); dirty = true; }
175
+ function loop() { if (dirty || rows.some((r) => performance.now() - r.fresh < 2600)) { dirty = false; render(); } requestAnimationFrame(loop); }
176
+ $("follow").addEventListener("click", () => { follow = !follow; $("follow").setAttribute("aria-pressed", String(follow)); $("follow").textContent = follow ? "Following" : "Paused"; });
177
+ $("scroller").addEventListener("scroll", () => { const s = $("scroller"); if (s.scrollTop + s.clientHeight < s.scrollHeight - 40 && follow) { follow = false; $("follow").setAttribute("aria-pressed", "false"); $("follow").textContent = "Paused"; } });
178
+ $("clear").addEventListener("click", () => { rows.length = 0; dirty = true; });
179
+ $("domain").addEventListener("change", (e) => { filter = e.target.value; dirty = true; });
180
+
181
+ fetch("/api/wiring", { cache: "no-store" }).then((r) => r.json()).then((g) => {
182
+ if (g.unknown) { $("count").textContent = "unknown: " + g.unknown; }
183
+ else { graph = g; byId = new Map(g.nodes.map((n) => [n.id, n]));
184
+ const count = new Map(); for (const n of g.nodes) if (n.kind === "aggregate") count.set(n.domain, (count.get(n.domain) || 0) + 1);
185
+ [...count.keys()].sort((a, b) => count.get(b) - count.get(a)).forEach((d, i) => { domainSlot.set(d, i); $("domain").append(h("option", { value: d }, d)); }); }
186
+ requestAnimationFrame(loop); listen();
187
+ }).catch((e) => { $("count").textContent = "unknown: " + e.message; requestAnimationFrame(loop); listen(); });
188
+ </script>
189
+ </body>
190
+ </html>
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ module PipelineBoard
9
+ # A small HTTP/1.1 server on the loopback address and nothing else. It answers three GET paths
10
+ # (`/`, `/api/state`, `/api/events`) and refuses everything else, so no command and no write
11
+ # is reachable through it. It serves one request per connection, and `/api/events` holds its
12
+ # connection open as a Server-Sent Events stream.
13
+ class Server
14
+ HOST = "127.0.0.1"
15
+ ROUTES = %w[/ /api/state /api/events /wiring /api/wiring /sequence].freeze
16
+ STATUS = { 200 => "OK", 404 => "Not Found", 405 => "Method Not Allowed" }.freeze
17
+
18
+ # @return [Integer] the port the server is bound to
19
+ attr_reader :port
20
+
21
+ # @return [String] the address the server is bound to
22
+ attr_reader :address
23
+
24
+ # @param port [Integer] 0 binds a free port
25
+ # @param app [#page, #state, #stream] what answers the three routes
26
+ def initialize(port:, app:)
27
+ @listener = TCPServer.new(HOST, port)
28
+ bound = @listener.local_address
29
+ @address = bound.ip_address
30
+ @port = bound.ip_port
31
+ @app = app
32
+ @open = []
33
+ end
34
+
35
+ # Starts accepting connections on a background thread.
36
+ # @return [Server] self
37
+ def start
38
+ @thread = Thread.new { accept_loop }
39
+ self
40
+ end
41
+
42
+ # Blocks until {#stop}.
43
+ def join = @thread&.join
44
+
45
+ # Closes the listener and every open stream.
46
+ def stop
47
+ @listener.close
48
+ @open.each { |socket| socket.close unless socket.closed? }
49
+ end
50
+
51
+ private
52
+
53
+ def accept_loop
54
+ loop { Thread.new(@listener.accept) { |socket| serve(socket) } }
55
+ rescue IOError, Errno::EBADF
56
+ nil
57
+ end
58
+
59
+ def serve(socket)
60
+ @open << socket
61
+ verb, target = socket.gets.to_s.split
62
+ nil while (line = socket.gets) && line != "\r\n"
63
+ answer(socket, verb, target.to_s.split("?").first)
64
+ rescue SystemCallError, IOError
65
+ nil
66
+ ensure
67
+ socket.close unless socket.closed?
68
+ @open.delete(socket)
69
+ end
70
+
71
+ def answer(socket, verb, path)
72
+ return reply(socket, 405, "text/plain", "method not allowed", "Allow" => "GET") unless verb == "GET"
73
+ return reply(socket, 404, "text/plain", "not found") unless ROUTES.include?(path)
74
+
75
+ route(socket, path)
76
+ end
77
+
78
+ def route(socket, path)
79
+ case path
80
+ when "/" then reply(socket, 200, "text/html; charset=utf-8", @app.page)
81
+ when "/api/state" then reply(socket, 200, "application/json", JSON.generate(@app.state))
82
+ when "/wiring" then reply(socket, 200, "text/html; charset=utf-8", @app.wiring_page)
83
+ when "/sequence" then reply(socket, 200, "text/html; charset=utf-8", @app.sequence_page)
84
+ when "/api/wiring" then reply(socket, 200, "application/json", JSON.generate(@app.wiring))
85
+ else stream(socket)
86
+ end
87
+ end
88
+
89
+ def reply(socket, status, type, body, extra = {})
90
+ head = { "Content-Type" => type, "Content-Length" => body.bytesize, "Cache-Control" => "no-store",
91
+ "Connection" => "close" }.merge(extra)
92
+ socket.write("HTTP/1.1 #{status} #{STATUS.fetch(status)}\r\n#{headers(head)}\r\n#{body}")
93
+ end
94
+
95
+ def headers(head) = head.map { |key, value| "#{key}: #{value}\r\n" }.join
96
+
97
+ def stream(socket)
98
+ socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n" \
99
+ "Connection: close\r\n\r\n")
100
+ @app.stream { |event| socket.write("event: board\ndata: #{JSON.generate(event)}\n\n") }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ module PipelineBoard
8
+ # The state the page shows, read in the background and held, so a request is answered at once
9
+ # and a slow read never holds a page. Without the background thread it reads when asked.
10
+ class Snapshot
11
+ WAITING = { unknown: "the first read is still in progress" }.freeze
12
+
13
+ # @param source [#call] answers the state hash
14
+ # @param every [Numeric] seconds between reads
15
+ # @param background [Boolean] read on a thread, not on request
16
+ def initialize(source:, every: 10, background: true)
17
+ @source = source
18
+ @every = every
19
+ @background = background
20
+ @lock = Mutex.new
21
+ end
22
+
23
+ # Starts the reading thread when the snapshot is a background one.
24
+ # @return [Snapshot] self
25
+ def start
26
+ @thread = Thread.new { refresh_forever } if @background
27
+ self
28
+ end
29
+
30
+ # Stops the reading thread.
31
+ def stop = @thread&.kill
32
+
33
+ # @return [Hash] the latest state; a placeholder naming the wait before the first read ends
34
+ def latest
35
+ return @source.call unless @background
36
+
37
+ @lock.synchronize { @held } || placeholder
38
+ end
39
+
40
+ private
41
+
42
+ def refresh_forever
43
+ loop do
44
+ held = @source.call
45
+ @lock.synchronize { @held = held }
46
+ sleep @every
47
+ end
48
+ end
49
+
50
+ def placeholder
51
+ { generated_at: Time.now.utc.iso8601, stages: [], metrics: [], ci_runs: WAITING, slowest_jobs: WAITING,
52
+ open_work: WAITING, sessions: WAITING, events: WAITING, fanout: WAITING }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ module PipelineBoard
8
+ # The events pushed to one reader: each event of the feed once, oldest first, and again when
9
+ # the reactions to it change, so a policy's delivery shows when it settles. It ends when the
10
+ # reader goes away, because the write the caller does for each event then raises.
11
+ class Stream
12
+ HEARTBEAT_EVERY = 15
13
+
14
+ # @param feed [#call] answers the newest events, newest first, or `{unknown: reason}`
15
+ # @param interval [Numeric] seconds between looks at the feed
16
+ def initialize(feed:, interval: 1.0)
17
+ @feed = feed
18
+ @interval = interval
19
+ @seen = {}
20
+ @ticks = 0
21
+ end
22
+
23
+ # @yieldparam event [Hash] an event row, an `unknown` note or a heartbeat
24
+ def each(&emit)
25
+ loop do
26
+ look(&emit)
27
+ beat(&emit)
28
+ sleep @interval
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def look(&emit)
35
+ rows = @feed.call
36
+ return note(rows[:unknown], &emit) if rows.is_a?(Hash)
37
+
38
+ rows.reverse_each { |row| once(row, &emit) }
39
+ end
40
+
41
+ def once(row)
42
+ digest = row.hash
43
+ yield row unless @seen[row[:key]] == digest
44
+ @seen[row[:key]] = digest
45
+ end
46
+
47
+ def note(reason)
48
+ yield({ unknown: reason }) unless @seen[:unknown] == reason
49
+ @seen[:unknown] = reason
50
+ end
51
+
52
+ def beat
53
+ @ticks += 1
54
+ yield({ heartbeat: Time.now.utc.iso8601 }) if (@ticks % HEARTBEAT_EVERY).zero?
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end