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,87 @@
|
|
|
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 `Jobs` port's adapter: answers `Sessions` with the dispatched sessions of the last day,
|
|
10
|
+
# from each job's own `state.json` and `timeline.jsonl` and from `claude agents --json`. It
|
|
11
|
+
# reads the job's state and never the ledger, and writes nothing.
|
|
12
|
+
class PipelineJobs
|
|
13
|
+
WINDOW_SECONDS = 24 * 3600
|
|
14
|
+
FRESH_SECONDS = 600
|
|
15
|
+
|
|
16
|
+
# @param aggregate [Object, nil] unused
|
|
17
|
+
# @param settings [Hash] `jobs_dir:` the directory of jobs, `agents:` a callable answering
|
|
18
|
+
# the parsed `claude agents --json`, `runs:` a callable answering session id => `{row:}`,
|
|
19
|
+
# `now:` a callable answering the time
|
|
20
|
+
# @param root [String, nil] unused
|
|
21
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
22
|
+
@dir = settings[:jobs_dir] || File.join(Dir.home, ".claude", "jobs")
|
|
23
|
+
@agents = settings[:agents] || -> { PipelineBoard::Probe.json("claude", "agents", "--json") }
|
|
24
|
+
@runs = settings[:runs] || -> { {} }
|
|
25
|
+
@now = settings[:now] || -> { Time.now }
|
|
26
|
+
@cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 15))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Hash{Symbol => String}] `text:` the sessions as JSON, busy first, then newest first
|
|
30
|
+
def sessions = { text: JSON.generate(rows) }
|
|
31
|
+
|
|
32
|
+
# @return [Array<Hash>] the sessions, each with `id`, `name`, `status`, `state`, `detail`,
|
|
33
|
+
# `started`, `ended` and the `row` it was dispatched for
|
|
34
|
+
def rows
|
|
35
|
+
agents = listed
|
|
36
|
+
found = Dir.glob(File.join(@dir, "*", "state.json")).filter_map { |path| session(path, agents) }
|
|
37
|
+
found.sort_by { |row| [row[:status] == "busy" ? 0 : 1, row[:started].to_s] }.reverse
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def listed
|
|
43
|
+
@cache.fetch(:agents) { Array(@agents.call).to_h { |agent| [agent["id"].to_s, agent] } }
|
|
44
|
+
rescue PipelineBoard::Unavailable
|
|
45
|
+
{}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def session(path, agents)
|
|
49
|
+
id = File.basename(File.dirname(path))
|
|
50
|
+
age = @now.call - File.mtime(path)
|
|
51
|
+
agent = agents[id]
|
|
52
|
+
return if age > WINDOW_SECONDS && !agent
|
|
53
|
+
|
|
54
|
+
build(id, JSON.parse(File.read(path)), agent, age, File.join(File.dirname(path), "timeline.jsonl"))
|
|
55
|
+
rescue JSON::ParserError, SystemCallError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build(id, state, agent, age, timeline)
|
|
60
|
+
edges = edges_of(timeline)
|
|
61
|
+
status = status_of(state, agent, age)
|
|
62
|
+
{ id: id, name: name_of(id, agent), status: status, state: state["state"].to_s,
|
|
63
|
+
detail: state["detail"].to_s.slice(0, 140), started: stamp(agent, edges),
|
|
64
|
+
ended: status == "busy" ? nil : edges.last, row: @runs.call.dig(id, :row).to_s }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def name_of(id, agent) = (agent && agent["name"]) || id
|
|
68
|
+
|
|
69
|
+
def status_of(state, agent, age)
|
|
70
|
+
return "busy" if (agent && agent["status"] == "busy") || (state["state"] == "working" && age < FRESH_SECONDS)
|
|
71
|
+
|
|
72
|
+
%w[failed error].include?(state["state"]) ? "failed" : "done"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stamp(agent, edges)
|
|
76
|
+
agent ? Time.at(agent["startedAt"].to_i / 1000).utc.iso8601 : edges.first
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def edges_of(timeline)
|
|
80
|
+
lines = File.readlines(timeline)
|
|
81
|
+
[lines.first, lines.last].map { |line| JSON.parse(line)["at"] }
|
|
82
|
+
rescue SystemCallError, JSON::ParserError
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# Turns an event-trace report into board rows: the event, each policy that reacted with the
|
|
9
|
+
# outcome of its delivery and the command it reached, and the events that followed on the same
|
|
10
|
+
# record (the ones the command emitted, read from the journal's own order).
|
|
11
|
+
module Chain
|
|
12
|
+
FOLLOWING = 3
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def chained(report)
|
|
17
|
+
events = JSON.parse(JSON.generate(report)).fetch("events", [])
|
|
18
|
+
events.map.with_index { |entry, index| row(entry, events.drop(index + 1)) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def row(entry, later)
|
|
22
|
+
event = entry["event"]
|
|
23
|
+
{ key: "journal:#{File.basename(entry["database"].to_s)}:#{event["id"]}", source: "journal",
|
|
24
|
+
at: event["occurred_at"], aggregate: event["aggregate"], id: event["aggregate_id"],
|
|
25
|
+
name: event["name"], reactions: entry["subscribers"].to_a.map { |sub| reaction(sub, event, later) } }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reaction(sub, event, later)
|
|
29
|
+
{ policy: sub["consumer"].to_s.sub("policy:", ""), command: sub["reaches"].to_s,
|
|
30
|
+
outcome: sub["outcome"].to_s, emitted: following(event, later) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def following(event, later)
|
|
34
|
+
same = later.map { |entry| entry["event"] }.select { |other| other["aggregate_id"] == event["aggregate_id"] }
|
|
35
|
+
same.first(FOLLOWING).map { |other| other["name"] }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# Reads the runtime's opt-in event log (`Hecks::Runtime::EventTap`): one JSON line per event
|
|
9
|
+
# and one per policy delivery, which this reader attaches to the event it answered. A line
|
|
10
|
+
# that carries its own `policy` is an event with its reaction. It opens the file for reading
|
|
11
|
+
# only; a missing file holds no events.
|
|
12
|
+
class Tap
|
|
13
|
+
TAIL_LINES = 600
|
|
14
|
+
FOLLOWING = 3
|
|
15
|
+
|
|
16
|
+
# @param path [String] the log's path
|
|
17
|
+
def initialize(path)
|
|
18
|
+
@path = path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array<Hash>] the newest events as board rows, oldest first, each with the
|
|
22
|
+
# reactions the log holds for it
|
|
23
|
+
def rows
|
|
24
|
+
return [] unless File.file?(@path)
|
|
25
|
+
|
|
26
|
+
events = parse(File.readlines(@path).last(TAIL_LINES))
|
|
27
|
+
link(events)
|
|
28
|
+
rescue SystemCallError
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse(lines) = lines.filter_map { |line| decode(line) }
|
|
35
|
+
|
|
36
|
+
def decode(line)
|
|
37
|
+
JSON.parse(line)
|
|
38
|
+
rescue JSON::ParserError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def link(lines)
|
|
43
|
+
rows = []
|
|
44
|
+
lines.each { |line| line["kind"] == "reaction" ? attach(rows, line) : rows << event_row(line, rows.size) }
|
|
45
|
+
rows.each_with_index { |row, index| follow(row, rows.drop(index + 1)) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def event_row(data, index)
|
|
49
|
+
{ key: "tap:#{data["at"]}:#{data["aggregate"]}:#{data["id"]}:#{data["event"]}:#{index}", source: "tap",
|
|
50
|
+
at: data["at"], aggregate: data["aggregate"], id: data["id"], name: data["event"], reactions: inline(data) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inline(data) = data["policy"] ? [reaction(data)] : []
|
|
54
|
+
|
|
55
|
+
def reaction(data)
|
|
56
|
+
{ policy: data["policy"], command: data["command"].to_s, outcome: data["outcome"].to_s,
|
|
57
|
+
emitted: Array(data["emitted"]) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attach(rows, line)
|
|
61
|
+
row = rows.reverse.find { |held| held[:id].to_s == line["id"].to_s && held[:name] == bare(line["event"]) }
|
|
62
|
+
row[:reactions] << reaction(line) if row
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bare(name) = name.to_s.split(".").last
|
|
66
|
+
|
|
67
|
+
def follow(row, later)
|
|
68
|
+
names = later.select { |other| other[:id] == row[:id] && other[:name] != row[:name] }.first(FOLLOWING)
|
|
69
|
+
row[:reactions].each { |reaction| reaction[:emitted] = names.map { |other| other[:name] } if reaction[:emitted].empty? }
|
|
70
|
+
row
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Journal` port to `PipelineJournal`, which answers `Events` from the journal stores, read-only.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_journal"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineJournal" do
|
|
6
|
+
port "Journal"
|
|
7
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "cache"
|
|
5
|
+
require_relative "pipeline_journal/tap"
|
|
6
|
+
require_relative "pipeline_journal/chain"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Adapters
|
|
10
|
+
# The `Journal` port's adapter: answers `Events` with the newest events the tooling recorded
|
|
11
|
+
# and, for each, the policies that reacted, what became of the delivery and the command it
|
|
12
|
+
# reached. The persisted stores are read by the same reader `era.event_trace` uses, opened
|
|
13
|
+
# read-only; an opt-in local event log (`events.jsonl`) is tailed for the domains that persist
|
|
14
|
+
# nothing. The board writes to neither.
|
|
15
|
+
class PipelineJournal
|
|
16
|
+
include PipelineBoard::Chain
|
|
17
|
+
|
|
18
|
+
DEFAULT_DOMAIN = "lib/hecks/hecks"
|
|
19
|
+
|
|
20
|
+
# Where the opt-in event log is written when a world turns it on.
|
|
21
|
+
TAP_PATH = File.join(Dir.home, ".claude", "hecks-dev", "events.jsonl")
|
|
22
|
+
|
|
23
|
+
# @param aggregate [Object, nil] unused
|
|
24
|
+
# @param settings [Hash] `trace:` stands in for the reader (a callable taking the row
|
|
25
|
+
# count and answering its report), `tap:` the event log's path, `ttl:` how long a read
|
|
26
|
+
# is kept
|
|
27
|
+
# @param root [String, nil] the checkout whose domain directory is read
|
|
28
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
29
|
+
@root = root || Dir.pwd
|
|
30
|
+
@trace = settings[:trace] || method(:trace)
|
|
31
|
+
@tap = PipelineBoard::Tap.new(settings.fetch(:tap) { TAP_PATH })
|
|
32
|
+
@cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 2))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param limit [Hash, Integer, nil] how many events (40 when none is named)
|
|
36
|
+
# @return [Hash{Symbol => String}] `text:` the events as JSON, newest first
|
|
37
|
+
def events(limit: nil)
|
|
38
|
+
{ text: JSON.generate(recent(limit.is_a?(Hash) ? limit[:value] : limit)) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param limit [Integer, nil] how many events
|
|
42
|
+
# @return [Array<Hash>] the events, newest first, each with its `key`, `reactions` and
|
|
43
|
+
# `source`
|
|
44
|
+
# @raise [PipelineBoard::Unavailable] when no store can be read and the log holds nothing
|
|
45
|
+
def recent(limit = nil)
|
|
46
|
+
count = limit.to_i.positive? ? limit.to_i : 40
|
|
47
|
+
found = journal(count) + @tap.rows
|
|
48
|
+
raise PipelineBoard::Unavailable, "no store was readable and no event log exists" if found.empty?
|
|
49
|
+
|
|
50
|
+
found.sort_by { |row| row[:at].to_s }.reverse.first(count)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def journal(count)
|
|
56
|
+
@cache.fetch([:trace, count]) { chained(@trace.call(count)) }
|
|
57
|
+
rescue PipelineBoard::Unavailable
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def trace(count)
|
|
62
|
+
require "hecks"
|
|
63
|
+
require "hecks/hecks/adapters/journal_store"
|
|
64
|
+
reading = Adapters::JournalStore::Readings::EventTrace
|
|
65
|
+
selection = reading::Selection.new(nil, nil, count, nil)
|
|
66
|
+
reading::Report.new(registry, selection).to_h
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
raise PipelineBoard::Unavailable, "journal: #{e.message[0, 80]}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def registry = @registry ||= Hecks.describe(File.join(owner_root, DEFAULT_DOMAIN)).registry
|
|
72
|
+
|
|
73
|
+
# The checkout whose local overlays name the stores: a linked worktree has none, so its main
|
|
74
|
+
# checkout is read instead.
|
|
75
|
+
def owner_root
|
|
76
|
+
common = PipelineBoard::Probe.run("git", "-C", @root, "rev-parse", "--git-common-dir").strip
|
|
77
|
+
File.basename(common) == ".git" ? File.dirname(File.expand_path(common, @root)) : @root
|
|
78
|
+
rescue PipelineBoard::Unavailable
|
|
79
|
+
@root
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
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 CI: success rate, wall time against the goal, failed jobs on the head,
|
|
9
|
+
# how long main has been red, and how long a commit takes to get a green CI.
|
|
10
|
+
module CiTiles
|
|
11
|
+
include TileKit
|
|
12
|
+
|
|
13
|
+
GOAL_MINUTES = 3
|
|
14
|
+
|
|
15
|
+
# @param runs [Array<Hash>] CI runs, oldest first
|
|
16
|
+
def success(runs)
|
|
17
|
+
spec = { id: "success", label: "CI success", unit: "%", detail: "last #{runs.size} main runs" }
|
|
18
|
+
built = windowed(spec, runs) { |list| success_rate(list) }
|
|
19
|
+
built.merge(state: band(built[:value], 80, 50))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def wall(runs)
|
|
23
|
+
spec = { id: "wall", label: "Median CI wall", unit: "min", lower: true, detail: "goal #{GOAL_MINUTES} min" }
|
|
24
|
+
built = windowed(spec, runs.select { |run| run[:status] == "completed" }) do |list|
|
|
25
|
+
median(list.map { |run| run[:wall_minutes] })
|
|
26
|
+
end
|
|
27
|
+
built.merge(state: over(built[:value], GOAL_MINUTES))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failed_head(runs)
|
|
31
|
+
head, before = runs.last(2).reverse
|
|
32
|
+
return unlisted(head) if head[:failed_jobs].nil?
|
|
33
|
+
|
|
34
|
+
change = before && trend(before[:failed_jobs], head[:failed_jobs], "jobs", lower_is_better: true)
|
|
35
|
+
state = head[:failed_jobs].zero? ? "ok" : "failed"
|
|
36
|
+
tile("failed_head", "Failed jobs on head", head[:failed_jobs], "jobs",
|
|
37
|
+
detail: "run #{head[:sha]}", trend: change, state: state)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unlisted(head)
|
|
41
|
+
unknown_tile("failed_head", "Failed jobs on head", { unknown: "GitHub would not list the jobs of #{head[:sha]}" })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def red_for(runs, now)
|
|
45
|
+
return tile("red", "Main red for", 0, "h", detail: "main is green") if runs.last[:conclusion] == "success"
|
|
46
|
+
|
|
47
|
+
green = runs.reverse.find { |run| run[:conclusion] == "success" }
|
|
48
|
+
hours = green && ((now - finished(green)) / 3600.0).round(1)
|
|
49
|
+
note = green ? "since #{green[:sha]} went green" : "no green run in the window"
|
|
50
|
+
tile("red", "Main red for", hours, "h", state: "failed", detail: note)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lead(data, runs)
|
|
54
|
+
times = data[:commit_times].to_h
|
|
55
|
+
spec = { id: "lead", label: "Commit to green CI", unit: "min", lower: true,
|
|
56
|
+
detail: "landed to pushed is not recorded" }
|
|
57
|
+
windowed(spec, runs.select { |run| times[run[:sha]] }) do |list|
|
|
58
|
+
median(list.map { |run| (finished(run) - Time.parse(times[run[:sha]])) / 60.0 })
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param spec [Hash] `id`, `label`, `unit`, `detail` and `lower` (a fall is an improvement)
|
|
63
|
+
def windowed(spec, list)
|
|
64
|
+
early, late = halves(list)
|
|
65
|
+
change = trend(yield(early), yield(late), spec[:unit], lower_is_better: spec[:lower] || false)
|
|
66
|
+
tile(spec[:id], spec[:label], yield(list), spec[:unit], detail: spec[:detail], trend: change)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def band(value, good, fair)
|
|
70
|
+
return "unknown" if value.nil?
|
|
71
|
+
return "ok" if value >= good
|
|
72
|
+
|
|
73
|
+
value >= fair ? "attention" : "failed"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def over(value, goal)
|
|
77
|
+
return "unknown" if value.nil?
|
|
78
|
+
return "ok" if value <= goal
|
|
79
|
+
|
|
80
|
+
value <= goal * 2 ? "attention" : "failed"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
module PipelineBoard
|
|
6
|
+
# How the work fans out across sessions: which row each session runs, who claimed each row,
|
|
7
|
+
# and the flags that say the two disagree. It reads rows and answers rows; it decides nothing.
|
|
8
|
+
module Fanout
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# @param sessions [Array<Hash>] the board's sessions
|
|
12
|
+
# @param work [Array<Hash>] the open Work items
|
|
13
|
+
# @param landed [Array<String>] the branches the Landing records say were merged
|
|
14
|
+
# @param fill [Hash, nil] the Dispatch.Fill preview
|
|
15
|
+
# @return [Array<Hash>] one row per open Work row: its claim, its sessions and its flags
|
|
16
|
+
def mapping(sessions:, work:, landed:, fill:)
|
|
17
|
+
busy = sessions.select { |s| s[:status] == "busy" }
|
|
18
|
+
world = { busy: busy, landed: landed, fill: fill, areas: work.to_h { |w| [w[:slug], w[:area].to_a] } }
|
|
19
|
+
work.map { |row| mapped(row, world) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Array<Hash>] every flag of every row, each with its `row`, `kind` and `detail`
|
|
23
|
+
def flags_of(mapped) = mapped.flat_map { |m| m[:flags].map { |flag| flag.merge(row: m[:slug]) } }
|
|
24
|
+
|
|
25
|
+
def mapped(row, world)
|
|
26
|
+
on = world[:busy].select { |s| s[:row] == row[:slug] }
|
|
27
|
+
{ slug: row[:slug], status: row[:status], claimed_by: row[:claimed_by], claimed_at: row[:claimed_at],
|
|
28
|
+
sessions: on.map { |s| s[:id] }, flags: flags(row, on, world) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def flags(row, on, world)
|
|
32
|
+
found = []
|
|
33
|
+
found << unclaimed(row, on) if on.any? && row[:claimed_by].to_s.empty?
|
|
34
|
+
found << twice(on) if on.size > 1
|
|
35
|
+
found.concat(overlapping(row, on, world))
|
|
36
|
+
found.concat(stale(row, world))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def unclaimed(row, on)
|
|
40
|
+
{ kind: "unclaimed", detail: "#{on.first[:id]} runs on #{row[:slug]}, which has no claimed_by" }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def twice(on)
|
|
44
|
+
{ kind: "duplicate", detail: "#{on.size} sessions run on one work item: #{on.map { |s| s[:id] }.join(", ")}" }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def overlapping(row, on, world)
|
|
48
|
+
return [] if on.empty?
|
|
49
|
+
|
|
50
|
+
clash = world[:busy].select { |s| s[:row] != row[:slug] && clashes?(s, row, world) }
|
|
51
|
+
clash.map { |s| { kind: "overlap", detail: "#{s[:id]} (#{s[:row]}) works an area that overlaps #{row[:slug]}" } }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clashes?(session, row, world) = touches?(world[:areas][session[:row]], row[:area].to_a)
|
|
55
|
+
|
|
56
|
+
def touches?(mine, theirs)
|
|
57
|
+
mine.to_a.any? { |a| theirs.any? { |b| a.start_with?(b) || b.start_with?(a) } }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stale(row, world)
|
|
61
|
+
return [] unless world[:landed].include?(row[:slug]) && %w[planned started].include?(row[:status])
|
|
62
|
+
|
|
63
|
+
[{ kind: "landed", detail: "#{row[:slug]} already landed but is still #{row[:status]}#{restart(row, world)}" }]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def restart(row, world)
|
|
67
|
+
again = world[:fill].to_h.fetch(:would_start, []).any? { |pick| pick[:slug] == row[:slug] }
|
|
68
|
+
again ? "; Dispatch.Fill would start another session on it" : ""
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# Asks each reader its question and keeps the answer, or `{unknown: <reason>}` when the reader
|
|
9
|
+
# could not answer, so one failed read never takes the others down. The reads run side by
|
|
10
|
+
# side, so a page takes as long as its slowest read.
|
|
11
|
+
module Gather
|
|
12
|
+
# Each section: the reader, its method, whether it answers a query document, and arguments.
|
|
13
|
+
READS = {
|
|
14
|
+
ci_runs: [:github, :ci_runs, :query], slowest_jobs: [:github, :slowest_jobs, :query],
|
|
15
|
+
release: %i[github latest_release plain], local: %i[git local_main plain],
|
|
16
|
+
train: %i[git train plain], stable: %i[git stable plain],
|
|
17
|
+
open_work: [:tickets, :open_work, :query], counts: %i[tickets counts plain],
|
|
18
|
+
landed: %i[tickets landed_branches plain], sessions: [:jobs, :sessions, :query],
|
|
19
|
+
red_flag: %i[git red_flag plain], fill: %i[fill preview plain], events: [:journal, :recent, :plain, 40],
|
|
20
|
+
history: [:journal, :recent, :plain, 300]
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def guard
|
|
26
|
+
yield
|
|
27
|
+
rescue PipelineBoard::Unavailable => e
|
|
28
|
+
{ unknown: e.message }
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
{ unknown: "#{e.class}: #{e.message.to_s.slice(0, 100)}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def queried
|
|
34
|
+
guard { JSON.parse(yield[:text], symbolize_names: true) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read(spec)
|
|
38
|
+
reader, method, kind, *args = spec
|
|
39
|
+
ask = -> { instance_variable_get("@#{reader}").public_send(method, *args) }
|
|
40
|
+
kind == :query ? queried(&ask) : guard(&ask)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def gathered
|
|
44
|
+
threads = READS.transform_values { |spec| Thread.new { read(spec) } }
|
|
45
|
+
threads.transform_values(&:value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dated(data)
|
|
49
|
+
runs = data[:ci_runs]
|
|
50
|
+
return {} unless runs.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
{ commit_times: guard { @git.commit_times(runs.map { |run| run[:sha] }) } }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "ci_tiles"
|
|
5
|
+
require_relative "work_tiles"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
module PipelineBoard
|
|
10
|
+
# The key-metric tiles: each a number, its unit, a state word and a trend against the earlier
|
|
11
|
+
# half of the same window. A section that could not be read gives a tile that says `unknown`.
|
|
12
|
+
module Metrics
|
|
13
|
+
extend CiTiles
|
|
14
|
+
extend WorkTiles
|
|
15
|
+
|
|
16
|
+
# @param data [Hash] the board's sections, each an answer or `{unknown: reason}`
|
|
17
|
+
# @param now [Time] the time the tiles are read at
|
|
18
|
+
# @return [Array<Hash>] the tiles, in the order the page shows them
|
|
19
|
+
def self.tiles(data, now)
|
|
20
|
+
runs = data[:ci_runs]
|
|
21
|
+
return [unknown_tile("ci", "CI", runs)] + work(data, now) unless runs.is_a?(Array) && !runs.empty?
|
|
22
|
+
|
|
23
|
+
ci(data, runs, now) + work(data, now)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.ci(data, runs, now)
|
|
27
|
+
[success(runs), wall(runs), failed_head(runs), red_for(runs, now), lead(data, runs)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.work(data, now)
|
|
31
|
+
[ahead(data[:local], now), working(data), throughput(data[:history], now), release(data[:release], now)]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "hecks/hecks/adapters/red_flag_report"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
module PipelineBoard
|
|
9
|
+
# The pipeline strip: one row per stage, left to right, each with a state word, a count and a
|
|
10
|
+
# one-line detail. A stage whose section could not be read says `unknown: <reason>` and the
|
|
11
|
+
# others still answer.
|
|
12
|
+
module Strip
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param data [Hash] the board's sections
|
|
16
|
+
# @param now [Time] the time the stages are read at
|
|
17
|
+
# @return [Array<Hash>] `name`, `state`, `count` and `detail` for each stage
|
|
18
|
+
def build(data, now)
|
|
19
|
+
[*flag(data[:red_flag], now), work(data[:counts]), sessions(data[:sessions]), local(data[:local]),
|
|
20
|
+
train(data[:train], now), ci(data[:ci_runs]), promote(data[:stable]), release(data[:release])]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The red flag row, first in the strip while a flag stands: its commit, the minutes main has
|
|
24
|
+
# been red, and who has checked in. Nothing while main is not flagged or the read failed.
|
|
25
|
+
def flag(standing, now)
|
|
26
|
+
return [] unless standing.is_a?(Hash) && standing.key?("id")
|
|
27
|
+
|
|
28
|
+
report = Hecks::Adapters::RedFlagReport.new(standing, now: now)
|
|
29
|
+
[row("Red flag", "failed", standing["id"], report.text.tr("\n", "; "))]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def row(name, state, count, detail) = { name: name, state: state, count: count.to_s, detail: detail }
|
|
33
|
+
|
|
34
|
+
def unknown(name, section)
|
|
35
|
+
reason = section.is_a?(Hash) ? section[:unknown] : "no data"
|
|
36
|
+
row(name, "unknown", "-", "unknown: #{reason}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def bad?(section) = section.nil? || (section.is_a?(Hash) && section.key?(:unknown))
|
|
40
|
+
|
|
41
|
+
def work(counts)
|
|
42
|
+
return unknown("Work items", counts) if bad?(counts)
|
|
43
|
+
|
|
44
|
+
planned, started, done = %w[planned started done].map { |status| counts.fetch(status, 0) }
|
|
45
|
+
row("Work items", started.positive? ? "working" : "ok", "#{planned}/#{started}/#{done}", "planned / started / done")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sessions(list)
|
|
49
|
+
return unknown("Sessions", list) unless list.is_a?(Array)
|
|
50
|
+
|
|
51
|
+
busy = list.count { |s| s[:status] == "busy" }
|
|
52
|
+
row("Sessions", busy.positive? ? "working" : "ok", busy, "#{busy} busy of #{list.size} in the last day")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def local(main)
|
|
56
|
+
return unknown("Local main", main) if bad?(main)
|
|
57
|
+
|
|
58
|
+
tree = main[:dirty].zero? ? "clean" : "#{main[:dirty]} changed files"
|
|
59
|
+
note = "#{main[:ahead]} ahead, #{main[:behind]} behind origin/main; #{tree}"
|
|
60
|
+
row("Local main", main[:ahead].zero? ? "ok" : "attention", main[:ahead], note)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def train(info, now)
|
|
64
|
+
return unknown("Train", info) if bad?(info)
|
|
65
|
+
|
|
66
|
+
age = info[:at] ? ((now - Time.parse(info[:at])) / 3600.0).round(1) : "?"
|
|
67
|
+
note = "last push #{age} h ago#{"; not on origin/main" unless info[:pushed]}"
|
|
68
|
+
row("Train", info[:pushed] ? "ok" : "attention", info[:sha], note)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ci(runs)
|
|
72
|
+
return unknown("CI", runs) unless runs.is_a?(Array) && runs.any?
|
|
73
|
+
|
|
74
|
+
last = runs.last
|
|
75
|
+
verdict = last[:conclusion].empty? ? last[:status] : last[:conclusion]
|
|
76
|
+
failed = last[:failed_jobs] || "?"
|
|
77
|
+
note = "#{last[:sha]} #{verdict}, #{last[:wall_minutes]} min, #{failed} failed jobs"
|
|
78
|
+
row("CI", ci_state(last), failed, note)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def ci_state(run)
|
|
82
|
+
return "working" unless run[:status] == "completed"
|
|
83
|
+
|
|
84
|
+
run[:conclusion] == "success" ? "ok" : "failed"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def promote(stable)
|
|
88
|
+
return unknown("Promote", stable) if bad?(stable)
|
|
89
|
+
|
|
90
|
+
note = "stable #{stable[:sha]}; #{stable[:behind]} commits behind main"
|
|
91
|
+
row("Promote", stable[:behind].zero? ? "ok" : "attention", stable[:behind], note)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def release(info)
|
|
95
|
+
return unknown("Release", info) if bad?(info)
|
|
96
|
+
|
|
97
|
+
row("Release", info[:published] ? "ok" : "attention", info[:tag], info[:published] ? "published" : "not published")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|