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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
- data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
- data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
- data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
- data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
- data/lib/hecks/adapters/driving/mcp.rb +2 -1
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
- data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
- data/lib/hecks/assembly.rb +18 -4
- data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
- data/lib/hecks/behaviors/expectations/boot.rb +12 -5
- data/lib/hecks/bluebook/acknowledgements.rb +1 -1
- data/lib/hecks/bluebook/command.rb +3 -2
- data/lib/hecks/bluebook/declaration.rb +2 -1
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
- data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
- data/lib/hecks/bluebook/hecksagon.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
- data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
- data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
- data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
- data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
- data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
- data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
- data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
- data/lib/hecks/coder/coder.bluebook +3 -1
- data/lib/hecks/command_names/index.rb +1 -1
- data/lib/hecks/command_names/resolution.rb +15 -5
- data/lib/hecks/composer/toolbox.rb +2 -1
- data/lib/hecks/corpus.rb +4 -0
- data/lib/hecks/doc/reference/readme.rb +1 -1
- data/lib/hecks/framework_bluebooks.rb +3 -2
- data/lib/hecks/fuzzing/differential.rb +1 -1
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
- data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
- data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
- data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
- data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
- data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
- data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
- data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
- data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
- data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
- data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
- data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
- data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
- data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
- data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
- data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
- data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
- data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
- data/lib/hecks/hecks/adapters/git_train.rb +9 -5
- data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
- data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
- data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
- data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
- data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
- data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
- data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
- data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
- data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
- data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
- data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
- data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
- data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
- data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
- data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
- data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
- data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
- data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
- data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
- data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
- data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
- data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
- data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
- data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
- data/lib/hecks/hecks/codebase.behaviors +0 -6
- data/lib/hecks/hecks/custodian.behaviors +22 -1
- data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
- data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
- data/lib/hecks/hecks/environments/memory.world +5 -0
- data/lib/hecks/hecks/hecks.contextmap +8 -0
- data/lib/hecks/hecks/hecks.hecksagon +106 -6
- data/lib/hecks/hecks/hecks.world +9 -1
- data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
- data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
- data/lib/hecks/hecks/test/test.bluebook +0 -19
- data/lib/hecks/language/adapter.bluebook +13 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
- data/lib/hecks/language/sentence/sentence.bluebook +2 -0
- data/lib/hecks/pipeline/pipeline.bluebook +251 -0
- data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
- data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
- data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
- data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
- data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
- data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
- data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
- data/lib/hecks/projections/undo.rb +2 -1
- data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
- data/lib/hecks/projector/cli_projector/renames.rb +4 -2
- data/lib/hecks/projector/cli_projector/usage.rb +4 -3
- data/lib/hecks/projector/exporter/adapters.rb +3 -2
- data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
- data/lib/hecks/projector/narrate_projector.rb +10 -3
- data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
- data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
- data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
- data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
- data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
- data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
- data/lib/hecks/quality_control/quality_control.bluebook +11 -11
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
- data/lib/hecks/query_ir/touchpoints.rb +2 -1
- data/lib/hecks/refused.rb +2 -1
- data/lib/hecks/release/approval.rb +4 -2
- data/lib/hecks/runtime/event_log.rb +27 -0
- data/lib/hecks/runtime/event_tap.rb +106 -0
- data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
- data/lib/hecks/runtime/loader.rb +35 -19
- data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
- data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
- data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
- data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
- data/lib/hecks/runtime/registry/verification.rb +7 -3
- data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/rust_format.rb +2 -1
- data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
- data/lib/hecks/shape_ledger.rb +1 -1
- data/lib/hecks/storehouse/audit.rb +2 -1
- data/lib/hecks/storehouse/identity.rb +6 -3
- data/lib/hecks/storehouse.rb +2 -1
- data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
- data/lib/hecks/tools/ci_workflows.rb +2 -1
- data/lib/hecks/tools/document.rb +12 -1
- data/lib/hecks/tools/downstream_model_check.rb +4 -2
- data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
- data/lib/hecks/tools/model_check/examination.rb +25 -2
- data/lib/hecks/tools/move_disables.rb +4 -2
- data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
- data/lib/hecks/tools/rspec_shard_files.rb +12 -15
- data/lib/hecks/tools/sequence_script.rb +1 -1
- data/lib/hecks/tools/shard_planner.rb +65 -0
- data/lib/hecks/tools/stage_progress.rb +2 -1
- data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +8 -9
- data/lib/hecks.rb +5 -2
- data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
- data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/parser/src/emit.rs +5 -1
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +12 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/adapter.rs +1 -0
- data/rust/parser/src/phrases.rs +16 -0
- data/rust/parser/src/sentence/examples.rs +93 -0
- data/rust/parser/src/sentence/mod.rs +1 -0
- metadata +89 -5
- data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# What every tile builder shares: the tile's shape, its trend against the earlier half of a
|
|
9
|
+
# window, and the small statistics.
|
|
10
|
+
module TileKit
|
|
11
|
+
# @return [Hash] a tile with its number, unit, state word, trend and detail line
|
|
12
|
+
def tile(id, label, value, unit, extra = {})
|
|
13
|
+
state = value.nil? ? "unknown" : "ok"
|
|
14
|
+
{ id: id, label: label, value: value, unit: unit, state: state, detail: "", trend: nil }.merge(extra)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Hash] a tile for a section that could not be read, saying why
|
|
18
|
+
def unknown_tile(id, label, section)
|
|
19
|
+
reason = section.is_a?(Hash) ? section[:unknown] : "nothing to read"
|
|
20
|
+
tile(id, label, nil, "", detail: "unknown: #{reason}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash] the change between two numbers; `better` is true when it improved
|
|
24
|
+
def trend(before, after, unit, lower_is_better: false)
|
|
25
|
+
return nil if before.nil? || after.nil?
|
|
26
|
+
|
|
27
|
+
delta = (after - before).round(1)
|
|
28
|
+
{ delta: delta, unit: unit, better: delta.zero? ? nil : (delta.negative? == lower_is_better) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<Array>] the earlier and the later half of a list
|
|
32
|
+
def halves(list)
|
|
33
|
+
cut = list.size / 2
|
|
34
|
+
[list.first(cut), list.drop(cut)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Float, nil] the middle of the numbers, nil when there are none
|
|
38
|
+
def median(list)
|
|
39
|
+
sorted = list.compact.sort
|
|
40
|
+
return nil if sorted.empty?
|
|
41
|
+
|
|
42
|
+
((sorted[(sorted.size - 1) / 2] + sorted[sorted.size / 2]) / 2.0).round(1)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Integer, nil] the share of runs that succeeded, as a whole percent
|
|
46
|
+
def success_rate(list)
|
|
47
|
+
return nil if list.empty?
|
|
48
|
+
|
|
49
|
+
(100.0 * list.count { |run| run[:conclusion] == "success" } / list.size).round
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Time] when a run ended: its start plus its wall time
|
|
53
|
+
def finished(run) = Time.parse(run[:started]) + (run[:wall_minutes].to_f * 60)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tile_kit"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# The tiles that read the work: commits not yet pushed, sessions against the cap and the load
|
|
9
|
+
# against its limit, rows finished in a day, and the latest release.
|
|
10
|
+
module WorkTiles
|
|
11
|
+
include TileKit
|
|
12
|
+
|
|
13
|
+
NUDGE_COMMITS = 15
|
|
14
|
+
NUDGE_HOURS = 6
|
|
15
|
+
DEFAULT_CAP = 8
|
|
16
|
+
DAY = 86_400
|
|
17
|
+
|
|
18
|
+
def ahead(local, now)
|
|
19
|
+
return unknown_tile("ahead", "Commits not pushed", local) unless readable?(local)
|
|
20
|
+
|
|
21
|
+
age = hours_since(local[:oldest_unpushed], now)
|
|
22
|
+
over = local[:ahead] >= NUDGE_COMMITS || age >= NUDGE_HOURS
|
|
23
|
+
note = "oldest #{age} h; nudge at #{NUDGE_COMMITS} commits or #{NUDGE_HOURS} h"
|
|
24
|
+
tile("ahead", "Commits not pushed", local[:ahead], "commits", state: over ? "attention" : "ok", detail: note)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def working(data)
|
|
28
|
+
return unknown_tile("working", "Sessions busy", data[:sessions]) unless data[:sessions].is_a?(Array)
|
|
29
|
+
|
|
30
|
+
busy = data[:sessions].count { |session| session[:status] == "busy" }
|
|
31
|
+
fill = readable?(data[:fill]) ? data[:fill] : {}
|
|
32
|
+
cap = fill[:cap] || DEFAULT_CAP
|
|
33
|
+
tile("working", "Sessions busy", busy, "of #{cap}", state: busy >= cap ? "attention" : "ok", detail: load_note(fill))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def throughput(history, now)
|
|
37
|
+
return unknown_tile("throughput", "Work finished", history) unless history.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
done = history.count { |event| event[:name] == "WorkFinished" && now - Time.parse(event[:at]) < DAY }
|
|
40
|
+
tile("throughput", "Work finished", done, "items / 24 h", detail: "Work.WorkFinished events in the journal")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def release(release, now)
|
|
44
|
+
return unknown_tile("release", "Latest release", release) unless readable?(release)
|
|
45
|
+
|
|
46
|
+
days = release[:published_at] && ((now - Time.parse(release[:published_at])) / DAY).floor
|
|
47
|
+
word = release[:published] ? "published" : "draft"
|
|
48
|
+
tile("release", "Latest release", days, "days ago", detail: "#{release[:tag]} #{word}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def readable?(section) = section.is_a?(Hash) && !section.key?(:unknown)
|
|
52
|
+
|
|
53
|
+
def hours_since(stamp, now) = stamp ? ((now - Time.parse(stamp)) / 3600.0).round(1) : 0.0
|
|
54
|
+
|
|
55
|
+
def load_note(fill) = fill[:load] ? "load #{fill[:load]} of #{fill[:limit]}" : "load unknown"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Pipeline` port to `PipelineStages`, which answers `Stages` from git, GitHub, Tickets, Jobs and the journal.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_stages"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineStages" do
|
|
6
|
+
port "Pipeline"
|
|
7
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "probe"
|
|
6
|
+
require_relative "pipeline_github"
|
|
7
|
+
require_relative "pipeline_git"
|
|
8
|
+
require_relative "pipeline_tickets"
|
|
9
|
+
require_relative "pipeline_jobs"
|
|
10
|
+
require_relative "pipeline_journal"
|
|
11
|
+
require_relative "pipeline_fill"
|
|
12
|
+
require_relative "pipeline_stages/gather"
|
|
13
|
+
require_relative "pipeline_stages/strip"
|
|
14
|
+
require_relative "pipeline_stages/metrics"
|
|
15
|
+
require_relative "pipeline_stages/fanout"
|
|
16
|
+
|
|
17
|
+
module Hecks
|
|
18
|
+
module Adapters
|
|
19
|
+
# The `Pipeline` port's adapter for `Stages`, and the one the page reads from: it asks each
|
|
20
|
+
# reader (GitHub, Git, Tickets, Jobs, Journal, and the Dispatch.Fill dry run) its question,
|
|
21
|
+
# turns the answers into the pipeline strip, the metric tiles and the fan-out, and gives
|
|
22
|
+
# every section that failed as `{unknown: reason}`. It only reads.
|
|
23
|
+
class PipelineStages
|
|
24
|
+
include PipelineBoard::Gather
|
|
25
|
+
|
|
26
|
+
# @param registry [Runtime::Registry, nil] the booted registry the Tickets reader uses
|
|
27
|
+
# @param aggregate [Object, nil] unused
|
|
28
|
+
# @param settings [Hash] `readers:` replaces any of `github`, `git`, `tickets`, `jobs`,
|
|
29
|
+
# `journal` and `fill`; `now:` is a callable answering the time
|
|
30
|
+
# @param root [String, nil] the checkout the readers run in
|
|
31
|
+
def initialize(registry: nil, aggregate: nil, settings: {}, root: nil)
|
|
32
|
+
@root = root || Dir.pwd
|
|
33
|
+
@now = settings[:now] || -> { Time.now.utc }
|
|
34
|
+
wire(settings.fetch(:readers, {}), registry)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Hash{Symbol => String}] `text:` one row per stage as JSON
|
|
38
|
+
def stages = { text: JSON.generate(PipelineBoard::Strip.build(sections, @now.call)) }
|
|
39
|
+
|
|
40
|
+
# @return [Hash] every section the page shows: `stages`, `metrics`, the four tables, the
|
|
41
|
+
# fan-out and the events, each an answer or `{unknown: reason}`
|
|
42
|
+
def state
|
|
43
|
+
data = sections
|
|
44
|
+
now = @now.call
|
|
45
|
+
{ generated_at: now.iso8601, stages: PipelineBoard::Strip.build(data, now),
|
|
46
|
+
metrics: PipelineBoard::Metrics.tiles(data, now), ci_runs: data[:ci_runs], slowest_jobs: data[:slowest_jobs],
|
|
47
|
+
open_work: data[:open_work], sessions: data[:sessions], events: data[:events], fanout: fanout(data) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array<Hash>, Hash] the newest events with the policies that reacted, or
|
|
51
|
+
# `{unknown: reason}`
|
|
52
|
+
def feed = guard { @journal.recent(40) }
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def wire(readers, registry)
|
|
57
|
+
@github = readers.fetch(:github) { PipelineGitHub.new(root: @root) }
|
|
58
|
+
@git = readers.fetch(:git) { PipelineGit.new(root: @root) }
|
|
59
|
+
@journal = readers.fetch(:journal) { PipelineJournal.new(root: @root) }
|
|
60
|
+
@fill = readers.fetch(:fill) { PipelineFill.new(root: @root) }
|
|
61
|
+
wire_work(readers, registry)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def wire_work(readers, registry)
|
|
65
|
+
@tickets = readers.fetch(:tickets) { PipelineTickets.new(registry: registry) }
|
|
66
|
+
@jobs = readers.fetch(:jobs) { PipelineJobs.new(settings: { runs: -> { guard { @tickets.runs } } }) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sections
|
|
70
|
+
data = gathered
|
|
71
|
+
data.merge(dated(data))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fanout(data)
|
|
75
|
+
return data[:sessions] unless data[:sessions].is_a?(Array)
|
|
76
|
+
|
|
77
|
+
landed = data[:landed].is_a?(Array) ? data[:landed] : []
|
|
78
|
+
work = data[:open_work].is_a?(Array) ? data[:open_work] : []
|
|
79
|
+
mapped = PipelineBoard::Fanout.mapping(sessions: data[:sessions], work: work, landed: landed, fill: data[:fill])
|
|
80
|
+
{ rows: mapped, flags: PipelineBoard::Fanout.flags_of(mapped), fill: data[:fill], sessions: data[:sessions] }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Tickets` port to `PipelineTickets`, which answers `OpenWork` with the Work items that are not done.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_tickets"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineTickets" do
|
|
6
|
+
port "Tickets"
|
|
7
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "probe"
|
|
5
|
+
require_relative "../../hecks/adapters/started_work"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The `Tickets` port's adapter: answers `OpenWork` with the Work items that are not done, read
|
|
10
|
+
# through the booted Tickets domain's repository (never from its database), and reports which
|
|
11
|
+
# session ran each row from the AgentRun records. It only reads.
|
|
12
|
+
class PipelineTickets
|
|
13
|
+
DONE = %w[done dropped].freeze
|
|
14
|
+
ROW_LINE = /^ROW:\s*(\S+)/
|
|
15
|
+
EMPTY = "the Tickets store holds no Work items here (Memory); run the board from the main checkout"
|
|
16
|
+
|
|
17
|
+
# @param registry [Runtime::Registry, nil] the booted registry whose rows are read
|
|
18
|
+
# @param aggregate [Object, nil] unused
|
|
19
|
+
# @param settings [Hash] `rows:` and `landings:` stand in for the registry's Work and
|
|
20
|
+
# Landing records
|
|
21
|
+
# @param root [String, nil] unused
|
|
22
|
+
def initialize(registry: nil, aggregate: nil, settings: {}, root: nil)
|
|
23
|
+
@registry = registry
|
|
24
|
+
@standin = settings[:rows]
|
|
25
|
+
@landed = settings[:landings]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Hash{Symbol => String}] `text:` the open rows as JSON: slug, status, area, and
|
|
29
|
+
# who claimed each and when
|
|
30
|
+
def open_work = { text: JSON.generate(rows.reject { |row| DONE.include?(row[:status]) }) }
|
|
31
|
+
|
|
32
|
+
# @return [Hash{String => Integer}] every Work row counted by status
|
|
33
|
+
def counts = rows.map { |row| row[:status] }.tally
|
|
34
|
+
|
|
35
|
+
# @return [Hash{String => Hash}] each AgentRun's session id with its `row` (named on the
|
|
36
|
+
# first line of its brief) and `status`
|
|
37
|
+
def runs
|
|
38
|
+
records_of("Hecks", "AgentRun").to_h { |run| [field(run, :session).to_s, run_row(run)] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Array<String>] the branches the Landing records say were merged onto main, with any
|
|
42
|
+
# leading `qa/` taken off
|
|
43
|
+
def landed_branches
|
|
44
|
+
landings = @landed || records_of("Hecks", "Landing")
|
|
45
|
+
landed = landings.select { |land| field(land, :status).to_s == "landed" }
|
|
46
|
+
landed.map { |land| field(land, :branch).to_s.delete_prefix("qa/") }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def rows
|
|
52
|
+
found = @standin || records_of("Tickets", "Work").map { |record| row(record) }
|
|
53
|
+
raise PipelineBoard::Unavailable, EMPTY if found.empty?
|
|
54
|
+
|
|
55
|
+
found
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def row(record)
|
|
59
|
+
{ slug: field(record, :work).to_s, status: field(record, :status).to_s, title: field(record, :title).to_s,
|
|
60
|
+
area: StartedWork.words(field(record, :area)), claimed_by: field(record, :claimed_by).to_s,
|
|
61
|
+
claimed_at: field(record, :claimed_at).to_s }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_row(run) = { row: field(run, :brief).to_s[ROW_LINE, 1].to_s, status: field(run, :status).to_s }
|
|
65
|
+
|
|
66
|
+
def records_of(domain, name)
|
|
67
|
+
bluebook = @registry&.bluebook(domain)
|
|
68
|
+
aggregate = bluebook&.aggregates&.find { |candidate| candidate.hecks_name == name }
|
|
69
|
+
raise PipelineBoard::Unavailable, "the #{domain} domain is not booted here" unless aggregate
|
|
70
|
+
|
|
71
|
+
@registry.repository(domain, aggregate).all
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def field(record, name) = StartedWork.value(record, name)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
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 Board</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; --series2: #eb6834;
|
|
13
|
+
--good: #0ca30c; --warning: #fab219; --serious: #ec835a; --critical: #d03b3b;
|
|
14
|
+
--on-fill: #ffffff; --on-warning: #0b0b0b; --good-text: #006300;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root:not([data-theme="light"]) {
|
|
18
|
+
color-scheme: dark;
|
|
19
|
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
|
|
20
|
+
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
|
|
21
|
+
--series1: #3987e5; --series2: #d95926; --good-text: #0ca30c;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
:root[data-theme="dark"] {
|
|
25
|
+
color-scheme: dark;
|
|
26
|
+
--page: #0d0d0d; --surface: #1a1a19; --ink: #f0efec; --ink2: #c3c2b7; --muted: #898781;
|
|
27
|
+
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
|
|
28
|
+
--series1: #3987e5; --series2: #d95926; --good-text: #0ca30c;
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
32
|
+
main { max-width: 1280px; margin: 0 auto; padding: 16px; }
|
|
33
|
+
header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; justify-content: space-between; margin-bottom: 16px; }
|
|
34
|
+
h1 { font-size: 20px; margin: 0; font-weight: 600; }
|
|
35
|
+
h2 { font-size: 13px; margin: 0 0 8px; color: var(--ink2); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
|
36
|
+
.meta { color: var(--ink2); font-size: 13px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
37
|
+
a { color: var(--series1); }
|
|
38
|
+
button { font: inherit; color: var(--ink); background: var(--surface); border: 1px solid var(--axis); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
39
|
+
button:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--series1); outline-offset: 2px; }
|
|
40
|
+
section { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px; margin-bottom: 16px; }
|
|
41
|
+
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; }
|
|
42
|
+
.tile { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; min-height: 108px; }
|
|
43
|
+
.tile .label { color: var(--ink2); font-size: 12px; }
|
|
44
|
+
.tile .num { font-size: 30px; line-height: 1.15; font-weight: 600; }
|
|
45
|
+
.tile .unit { font-size: 13px; color: var(--ink2); font-weight: 400; margin-left: 4px; }
|
|
46
|
+
.tile .note { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
47
|
+
.tile .trend { font-size: 12px; color: var(--ink2); }
|
|
48
|
+
.strip { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0; }
|
|
49
|
+
.node { position: relative; border: 1px solid var(--border); padding: 10px; margin-right: 14px; border-radius: 8px; min-height: 112px; }
|
|
50
|
+
.node:not(:last-child)::after { content: "\2192"; position: absolute; right: -14px; top: 40%; width: 14px; text-align: center; color: var(--muted); }
|
|
51
|
+
.node .name { font-size: 12px; color: var(--ink2); }
|
|
52
|
+
.node .count { font-size: 22px; font-weight: 600; overflow-wrap: anywhere; }
|
|
53
|
+
.node .detail { font-size: 12px; color: var(--ink2); overflow-wrap: anywhere; }
|
|
54
|
+
.badge { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; padding: 1px 7px; border-radius: 999px; color: var(--on-fill); }
|
|
55
|
+
.badge.ok { background: var(--good); } .badge.attention { background: var(--warning); color: var(--on-warning); }
|
|
56
|
+
.badge.failed { background: var(--critical); } .badge.working { background: var(--series1); }
|
|
57
|
+
.badge.unknown { background: transparent; color: var(--ink2); border: 1px dashed var(--muted); }
|
|
58
|
+
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 16px; }
|
|
59
|
+
.cols { display: flex; align-items: flex-end; gap: 4px; height: 140px; border-bottom: 1px solid var(--axis); padding-top: 18px; }
|
|
60
|
+
.col { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; }
|
|
61
|
+
.col .bar { width: 100%; max-width: 34px; background: var(--series1); border-radius: 4px 4px 0 0; min-height: 0; }
|
|
62
|
+
.col .bar.second { background: var(--series2); }
|
|
63
|
+
.col .val { font-size: 11px; color: var(--ink2); }
|
|
64
|
+
.xlabels { display: flex; gap: 4px; }
|
|
65
|
+
.xlabels span { flex: 1; font-size: 10px; color: var(--muted); text-align: center; overflow: hidden; text-overflow: clip; white-space: nowrap; }
|
|
66
|
+
.hbar { display: grid; grid-template-columns: minmax(90px, 36%) 1fr 52px; gap: 8px; align-items: center; margin: 5px 0; font-size: 12px; }
|
|
67
|
+
.hbar .track { height: 12px; }
|
|
68
|
+
.hbar .fill { height: 100%; background: var(--series1); border-radius: 0 4px 4px 0; }
|
|
69
|
+
.hbar .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
70
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
71
|
+
th, td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--grid); vertical-align: top; }
|
|
72
|
+
th { color: var(--ink2); font-weight: 600; font-size: 12px; }
|
|
73
|
+
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; }
|
|
74
|
+
.scroll { overflow-x: auto; }
|
|
75
|
+
.lanes { position: relative; border-left: 1px solid var(--axis); }
|
|
76
|
+
.lane { position: relative; height: 22px; border-bottom: 1px solid var(--grid); }
|
|
77
|
+
.lane .tag { position: absolute; left: 4px; top: 2px; font-size: 11px; color: var(--ink2); z-index: 2; white-space: nowrap; pointer-events: none; }
|
|
78
|
+
.lane .span { position: absolute; top: 3px; height: 16px; border-radius: 4px; min-width: 6px; opacity: .85; }
|
|
79
|
+
.span.busy { background: var(--series1); } .span.done { background: var(--good); } .span.failed { background: var(--critical); }
|
|
80
|
+
.hours { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); margin-top: 2px; }
|
|
81
|
+
.legend { display: flex; gap: 14px; font-size: 12px; color: var(--ink2); margin: 6px 0; flex-wrap: wrap; }
|
|
82
|
+
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; }
|
|
83
|
+
.meter { height: 12px; background: var(--grid); border-radius: 6px; overflow: hidden; margin: 4px 0; }
|
|
84
|
+
.meter > div { height: 100%; background: var(--series1); }
|
|
85
|
+
.flag { border-left: 4px solid var(--warning); padding: 4px 8px; margin: 4px 0; background: var(--page); border-radius: 0 6px 6px 0; font-size: 13px; }
|
|
86
|
+
.flag b { font-weight: 600; }
|
|
87
|
+
.feed { max-height: 560px; overflow: auto; }
|
|
88
|
+
.ev { border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; }
|
|
89
|
+
.ev.fresh { animation: flash 2.4s ease-out; }
|
|
90
|
+
@keyframes flash { from { background: color-mix(in srgb, var(--series1) 28%, transparent); } to { background: transparent; } }
|
|
91
|
+
@media (prefers-reduced-motion: reduce) { .ev.fresh { animation: none; border-color: var(--series1); } }
|
|
92
|
+
.ev .head { display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline; }
|
|
93
|
+
.ev .time { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
94
|
+
.ev .name { font-weight: 600; }
|
|
95
|
+
.ev .agg { color: var(--ink2); font-size: 12px; overflow-wrap: anywhere; }
|
|
96
|
+
.chain { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 6px; font-size: 12px; }
|
|
97
|
+
.chain .arrow { color: var(--muted); }
|
|
98
|
+
.chain .step { border: 1px solid var(--border); border-radius: 6px; padding: 2px 7px; overflow-wrap: anywhere; }
|
|
99
|
+
.chain .step.policy { border-style: dashed; }
|
|
100
|
+
.live { font-size: 12px; }
|
|
101
|
+
.tip { position: fixed; pointer-events: none; background: var(--ink); color: var(--surface); padding: 4px 8px; border-radius: 6px; font-size: 12px; z-index: 10; display: none; max-width: 280px; }
|
|
102
|
+
.unknown-note { color: var(--ink2); font-style: italic; }
|
|
103
|
+
.tables-only .chartview { display: none; } body:not(.tables-only) .tableview { display: none; }
|
|
104
|
+
@media (max-width: 900px) {
|
|
105
|
+
.strip { grid-template-columns: 1fr; gap: 10px; }
|
|
106
|
+
.node { margin-right: 0; min-height: 0; }
|
|
107
|
+
.node:not(:last-child)::after { display: none; }
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<main>
|
|
113
|
+
<header>
|
|
114
|
+
<h1>Pipeline Board</h1>
|
|
115
|
+
<div class="meta">
|
|
116
|
+
<a href="/wiring">Wiring diagram</a>
|
|
117
|
+
<a href="/sequence">Sequence</a>
|
|
118
|
+
<span id="updated">loading</span>
|
|
119
|
+
<span class="live" id="live">stream: connecting</span>
|
|
120
|
+
<button id="toggle" type="button" aria-pressed="false">Show tables</button>
|
|
121
|
+
</div>
|
|
122
|
+
</header>
|
|
123
|
+
|
|
124
|
+
<section aria-labelledby="h-tiles"><h2 id="h-tiles">Key metrics</h2><div class="tiles" id="tiles"></div></section>
|
|
125
|
+
<section aria-labelledby="h-strip"><h2 id="h-strip">Pipeline</h2><div class="strip" id="strip"></div></section>
|
|
126
|
+
|
|
127
|
+
<section aria-labelledby="h-fan"><h2 id="h-fan">Fan-out</h2>
|
|
128
|
+
<div class="grid2">
|
|
129
|
+
<div><div class="legend" id="lanelegend"></div><div id="lanes" class="chartview"></div><div class="tableview scroll" id="lanetable"></div></div>
|
|
130
|
+
<div><div id="meters"></div><div id="flags"></div><div id="fill"></div></div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="scroll" id="mapping"></div>
|
|
133
|
+
</section>
|
|
134
|
+
|
|
135
|
+
<section aria-labelledby="h-ci"><h2 id="h-ci">CI on main</h2>
|
|
136
|
+
<div class="grid2">
|
|
137
|
+
<div><div class="label">Failed jobs per run, oldest to newest</div><div id="failedchart" class="chartview"></div><div class="tableview scroll" id="failedtable"></div></div>
|
|
138
|
+
<div><div class="label">Wall minutes per run (goal 3)</div><div id="wallchart" class="chartview"></div><div class="tableview scroll" id="walltable"></div></div>
|
|
139
|
+
</div>
|
|
140
|
+
<h2 style="margin-top:14px">Slowest jobs of the last completed run</h2>
|
|
141
|
+
<div id="slowchart" class="chartview"></div><div class="tableview scroll" id="slowtable"></div>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<section aria-labelledby="h-work"><h2 id="h-work">Open Work items</h2><div class="scroll" id="work"></div></section>
|
|
145
|
+
<section aria-labelledby="h-sess"><h2 id="h-sess">Sessions</h2><div class="scroll" id="sessions"></div></section>
|
|
146
|
+
<section aria-labelledby="h-ev"><h2 id="h-ev">Events and what answered them</h2><div class="feed" id="feed" aria-live="polite"></div></section>
|
|
147
|
+
</main>
|
|
148
|
+
<div class="tip" id="tip" role="tooltip"></div>
|
|
149
|
+
<script>
|
|
150
|
+
"use strict";
|
|
151
|
+
const ICON = { ok: "✓", working: "◔", attention: "▲", failed: "✕", unknown: "?" };
|
|
152
|
+
const WORD = { ok: "ok", working: "working", attention: "attention", failed: "failed", unknown: "unknown" };
|
|
153
|
+
const $ = (id) => document.getElementById(id);
|
|
154
|
+
const tip = $("tip");
|
|
155
|
+
|
|
156
|
+
function h(tag, attrs, ...kids) {
|
|
157
|
+
const el = document.createElement(tag);
|
|
158
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
159
|
+
if (k === "class") el.className = v; else if (k === "tip") el.dataset.tip = v; else el.setAttribute(k, v);
|
|
160
|
+
}
|
|
161
|
+
for (const kid of kids.flat()) if (kid !== null && kid !== undefined) el.append(kid.nodeType ? kid : String(kid));
|
|
162
|
+
return el;
|
|
163
|
+
}
|
|
164
|
+
function put(id, ...kids) { const el = $(id); el.replaceChildren(...kids.flat()); }
|
|
165
|
+
function badge(state) { return h("span", { class: "badge " + state }, ICON[state] || "?", " ", WORD[state] || state); }
|
|
166
|
+
function isUnknown(x) { return x && !Array.isArray(x) && typeof x === "object" && "unknown" in x; }
|
|
167
|
+
function unknown(x) { return h("p", { class: "unknown-note" }, "unknown: " + x.unknown); }
|
|
168
|
+
function clock(iso) { if (!iso) return "-"; const d = new Date(iso); return isNaN(d) ? String(iso) : d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
|
|
169
|
+
function table(head, rows, numeric) {
|
|
170
|
+
return h("table", {}, h("thead", {}, h("tr", {}, head.map((t, i) => h("th", { class: numeric && numeric.includes(i) ? "n" : "" }, t)))),
|
|
171
|
+
h("tbody", {}, rows.map((r) => h("tr", {}, r.map((c, i) => h("td", { class: numeric && numeric.includes(i) ? "n" : "" }, c))))));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
document.addEventListener("mouseover", (e) => {
|
|
175
|
+
const t = e.target.closest("[data-tip]");
|
|
176
|
+
if (!t) { tip.style.display = "none"; return; }
|
|
177
|
+
tip.textContent = t.dataset.tip; tip.style.display = "block";
|
|
178
|
+
tip.style.left = Math.min(e.clientX + 12, innerWidth - 290) + "px"; tip.style.top = (e.clientY + 14) + "px";
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
function renderTiles(tiles) {
|
|
182
|
+
put("tiles", tiles.map((t) => {
|
|
183
|
+
const value = t.value === null || t.value === undefined ? "-" : t.value;
|
|
184
|
+
const tr = t.trend ? h("div", { class: "trend" }, (t.trend.delta > 0 ? "▲ +" : t.trend.delta < 0 ? "▼ " : "= ") + t.trend.delta + " " + t.trend.unit,
|
|
185
|
+
t.trend.better === null ? " flat" : t.trend.better ? " better" : " worse", " vs earlier half") : null;
|
|
186
|
+
return h("div", { class: "tile", tip: t.label + ": " + t.detail },
|
|
187
|
+
h("div", { class: "label" }, t.label), h("div", { class: "num" }, String(value), h("span", { class: "unit" }, t.unit)),
|
|
188
|
+
badge(t.state), tr, h("div", { class: "note" }, t.detail));
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
function renderStrip(stages) {
|
|
192
|
+
put("strip", stages.map((s) => h("div", { class: "node", tip: s.name + ": " + s.detail },
|
|
193
|
+
h("div", { class: "name" }, s.name), h("div", { class: "count" }, s.count), badge(s.state), h("div", { class: "detail" }, s.detail))));
|
|
194
|
+
}
|
|
195
|
+
function columns(rows, pick, label, second) {
|
|
196
|
+
const max = Math.max(1, ...rows.map(pick));
|
|
197
|
+
const cols = rows.map((r) => h("div", { class: "col", tip: r.sha + " " + (r.conclusion || r.status) + ": " + label(r) },
|
|
198
|
+
h("div", { class: "val" }, String(pick(r))), h("div", { class: "bar" + (second ? " second" : ""), style: "height:" + Math.round(100 * pick(r) / max) + "%" })));
|
|
199
|
+
return [h("div", { class: "cols" }, cols), h("div", { class: "xlabels" }, rows.map((r) => h("span", {}, r.sha.slice(0, 4))))];
|
|
200
|
+
}
|
|
201
|
+
function renderCi(runs, slow) {
|
|
202
|
+
if (isUnknown(runs)) { for (const id of ["failedchart", "wallchart", "failedtable", "walltable"]) put(id, unknown(runs)); }
|
|
203
|
+
else {
|
|
204
|
+
put("failedchart", columns(runs, (r) => r.failed_jobs || 0, (r) => (r.failed_jobs === null ? "failed jobs unknown" : r.failed_jobs + " failed jobs")));
|
|
205
|
+
put("wallchart", columns(runs, (r) => r.wall_minutes, (r) => r.wall_minutes + " min", true));
|
|
206
|
+
put("failedtable", table(["Run", "Started", "Conclusion", "Failed jobs"], runs.map((r) => [r.sha, clock(r.started), r.conclusion || r.status, r.failed_jobs === null ? "?" : r.failed_jobs]), [3]));
|
|
207
|
+
put("walltable", table(["Run", "Wall minutes"], runs.map((r) => [r.sha, r.wall_minutes]), [1]));
|
|
208
|
+
}
|
|
209
|
+
if (isUnknown(slow)) { put("slowchart", unknown(slow)); put("slowtable", unknown(slow)); return; }
|
|
210
|
+
const max = Math.max(1, ...slow.map((j) => j.minutes));
|
|
211
|
+
put("slowchart", slow.map((j) => h("div", { class: "hbar", tip: j.name + ": " + j.minutes + " min (" + j.conclusion + ")" },
|
|
212
|
+
h("div", { class: "name" }, j.name), h("div", { class: "track" }, h("div", { class: "fill", style: "width:" + Math.round(100 * j.minutes / max) + "%" })), h("div", {}, j.minutes + " min"))));
|
|
213
|
+
put("slowtable", table(["Job", "Minutes", "Conclusion"], slow.map((j) => [j.name, j.minutes, j.conclusion]), [1]));
|
|
214
|
+
}
|
|
215
|
+
function renderWork(work) {
|
|
216
|
+
if (isUnknown(work)) return put("work", unknown(work));
|
|
217
|
+
put("work", table(["Work item", "Status", "Area", "Claimed by", "Claimed at"], work.map((w) => [w.slug, w.status, (w.area || []).join(", ") || "-", w.claimed_by || "(nobody)", w.claimed_at || "-"])));
|
|
218
|
+
}
|
|
219
|
+
function renderSessions(list) {
|
|
220
|
+
if (isUnknown(list)) return put("sessions", unknown(list));
|
|
221
|
+
put("sessions", table(["Session", "Status", "Work item", "Started", "Detail"], list.map((s) => [s.name, [badge(s.status === "busy" ? "working" : s.status === "failed" ? "failed" : "ok"), " ", s.state], s.row || "-", clock(s.started), s.detail])));
|
|
222
|
+
}
|
|
223
|
+
function renderFan(fan) {
|
|
224
|
+
if (isUnknown(fan)) { for (const id of ["lanes", "meters", "flags", "fill", "mapping", "lanetable"]) put(id, unknown(fan)); return; }
|
|
225
|
+
const sessions = fan.sessions.filter((s) => s.started), now = Date.now(), span = 24 * 3600 * 1000, start = now - span;
|
|
226
|
+
put("lanelegend", ["busy", "done", "failed"].map((k) => h("span", {}, h("i", { class: "swatch span " + k }), (k === "busy" ? "◔ busy" : k === "done" ? "✓ done" : "✕ failed"))));
|
|
227
|
+
put("lanes", h("div", { class: "lanes" }, sessions.map((s) => {
|
|
228
|
+
const a = Math.max(start, Date.parse(s.started)), b = s.ended ? Date.parse(s.ended) : now;
|
|
229
|
+
const left = 100 * (a - start) / span, width = Math.max(0.5, 100 * (Math.min(b, now) - a) / span);
|
|
230
|
+
return h("div", { class: "lane" }, h("div", { class: "tag" }, (s.status === "busy" ? "◔ " : s.status === "failed" ? "✕ " : "✓ ") + s.name + (s.row ? " · " + s.row : "")),
|
|
231
|
+
h("div", { class: "span " + s.status, style: "left:" + left + "%;width:" + width + "%", tip: s.name + " " + s.status + " " + clock(s.started) + " to " + (s.ended ? clock(s.ended) : "now") }));
|
|
232
|
+
})), h("div", { class: "hours" }, h("span", {}, "-24 h"), h("span", {}, "-12 h"), h("span", {}, "now")));
|
|
233
|
+
put("lanetable", table(["Session", "Status", "Started", "Ended", "Work item"], sessions.map((s) => [s.name, s.status, clock(s.started), s.ended ? clock(s.ended) : "now", s.row || "-"])));
|
|
234
|
+
const fill = fan.fill, ok = fill && !isUnknown(fill), busy = sessions.filter((s) => s.status === "busy").length, cap = ok ? fill.cap : 8;
|
|
235
|
+
put("meters", h("div", {}, h("div", {}, "Sessions busy: " + busy + " of " + cap), h("div", { class: "meter" }, h("div", { style: "width:" + Math.min(100, 100 * busy / cap) + "%" })),
|
|
236
|
+
ok ? [h("div", {}, "Machine load: " + fill.load + " of " + fill.limit), h("div", { class: "meter" }, h("div", { style: "width:" + Math.min(100, 100 * fill.load / fill.limit) + "%" }))] : h("div", { class: "unknown-note" }, "load: unknown")));
|
|
237
|
+
put("flags", fan.flags.length ? fan.flags.map((f) => h("div", { class: "flag" }, badge("attention"), " ", h("b", {}, f.kind), " ", f.row + ": " + f.detail)) : h("div", {}, badge("ok"), " no disagreements between work items and sessions"));
|
|
238
|
+
put("fill", ok ? [h("h2", { style: "margin-top:10px" }, "Dispatch.Fill dry run: would start " + fill.would_start.length),
|
|
239
|
+
table(["Work item", "Result"], fill.would_start.map((w) => [w.slug, "would start"]).concat(fill.skipped.map((w) => [w.slug, "skipped: " + w.reason])))] : unknown(fill || { unknown: "none" }));
|
|
240
|
+
put("mapping", table(["Work item", "Status", "Claimed by", "Claimed at", "Sessions", "Flags"], fan.rows.map((r) => [r.slug, r.status, r.claimed_by || "(nobody)", r.claimed_at || "-", r.sessions.join(", ") || "-", r.flags.map((f) => f.kind).join(", ") || "-"])));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const seen = new Map();
|
|
244
|
+
function chainOf(ev) {
|
|
245
|
+
const nodes = [h("span", { class: "step" }, ev.name)];
|
|
246
|
+
for (const r of ev.reactions || []) {
|
|
247
|
+
const state = /deliver|settled|done/.test(r.outcome) ? "ok" : /refus|fail/.test(r.outcome) ? "failed" : "attention";
|
|
248
|
+
nodes.push(h("span", { class: "arrow" }, "→"), h("span", { class: "step policy", tip: "policy " + r.policy }, r.policy), badge(state), " " + (r.outcome || "no record"));
|
|
249
|
+
if (r.command) nodes.push(h("span", { class: "arrow" }, "→"), h("span", { class: "step" }, r.command));
|
|
250
|
+
for (const e of r.emitted || []) nodes.push(h("span", { class: "arrow" }, "→"), h("span", { class: "step" }, e));
|
|
251
|
+
}
|
|
252
|
+
return h("div", { class: "chain" }, nodes);
|
|
253
|
+
}
|
|
254
|
+
function putEvent(ev, fresh) {
|
|
255
|
+
if (!ev.key) return;
|
|
256
|
+
const card = h("div", { class: "ev" + (fresh ? " fresh" : "") }, h("div", { class: "head" },
|
|
257
|
+
h("span", { class: "time" }, clock(ev.at)), h("span", { class: "name" }, ev.name), h("span", { class: "agg" }, (ev.aggregate || "") + "#" + (ev.id || "")),
|
|
258
|
+
h("span", { class: "agg" }, ev.source === "tap" ? "event log" : "journal")), chainOf(ev));
|
|
259
|
+
const old = seen.get(ev.key), feed = $("feed");
|
|
260
|
+
if (old) old.replaceWith(card); else feed.prepend(card);
|
|
261
|
+
seen.set(ev.key, card);
|
|
262
|
+
while (feed.children.length > 80) { const last = feed.lastChild; for (const [k, v] of seen) if (v === last) seen.delete(k); last.remove(); }
|
|
263
|
+
}
|
|
264
|
+
function renderEvents(events) {
|
|
265
|
+
if (isUnknown(events)) return put("feed", unknown(events));
|
|
266
|
+
if (!seen.size) [...events].reverse().forEach((e) => putEvent(e, false));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function refresh() {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch("/api/state", { cache: "no-store" });
|
|
272
|
+
const s = await res.json();
|
|
273
|
+
renderTiles(s.metrics); renderStrip(s.stages); renderCi(s.ci_runs, s.slowest_jobs);
|
|
274
|
+
renderWork(s.open_work); renderSessions(s.sessions); renderFan(s.fanout); renderEvents(s.events);
|
|
275
|
+
$("updated").textContent = "updated " + clock(s.generated_at);
|
|
276
|
+
} catch (e) { $("updated").textContent = "update failed: " + e.message; }
|
|
277
|
+
}
|
|
278
|
+
function listen() {
|
|
279
|
+
if (!window.EventSource) { $("live").textContent = "stream: unsupported"; return; }
|
|
280
|
+
const es = new EventSource("/api/events");
|
|
281
|
+
es.onopen = () => { $("live").textContent = "stream: live"; };
|
|
282
|
+
es.onerror = () => { $("live").textContent = "stream: reconnecting"; };
|
|
283
|
+
es.addEventListener("board", (m) => { const ev = JSON.parse(m.data); if (ev.key) putEvent(ev, true); });
|
|
284
|
+
}
|
|
285
|
+
$("toggle").addEventListener("click", () => {
|
|
286
|
+
const on = document.body.classList.toggle("tables-only");
|
|
287
|
+
$("toggle").setAttribute("aria-pressed", String(on)); $("toggle").textContent = on ? "Show charts" : "Show tables";
|
|
288
|
+
});
|
|
289
|
+
refresh(); setInterval(refresh, 10000); listen();
|
|
290
|
+
</script>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|