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