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,82 @@
|
|
|
1
|
+
require_relative "../../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Behaviors
|
|
5
|
+
module Expectations
|
|
6
|
+
# Which `.adapter` files a behaviors suite boots, chosen without a list (ADR 0171): every
|
|
7
|
+
# project adapter that says `stays_inside`, every stand-in in `behavior_adapters/` beside
|
|
8
|
+
# the suite, and any adapter the suite names in `loads`. A stand-in or a named adapter takes
|
|
9
|
+
# its port from the project's. A project adapter that reaches outside is never loaded, and
|
|
10
|
+
# its Ruby never runs: its declaration is read with its `require` lines left out.
|
|
11
|
+
#
|
|
12
|
+
# A project opts in by having a `behavior_adapters/` directory beside the suite or by any of
|
|
13
|
+
# its adapters saying `stays_inside`; until then its suites keep the loader's own choice.
|
|
14
|
+
module AdapterSelection
|
|
15
|
+
STAND_INS = "behavior_adapters".freeze
|
|
16
|
+
PROJECT_PATTERNS = %w[*.adapter */*.adapter */*/*.adapter].freeze
|
|
17
|
+
REQUIRE_LINE = /\A\s*require(?:_relative)?\s+["'][^"']+["']\s*\z/
|
|
18
|
+
SELECTIONS = {} # rubocop:disable Style/MutableConstant -- a per-process cache, filled under the lock
|
|
19
|
+
SELECTIONS_LOCK = Mutex.new
|
|
20
|
+
private_constant :SELECTIONS, :SELECTIONS_LOCK
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param suite_path [String] the `.behaviors` file
|
|
25
|
+
# @param files [Array<String>] the suite's `loads`, absolute
|
|
26
|
+
# @return [Array<String>, nil] the `.adapter` files to boot, absolute, in a stable
|
|
27
|
+
# order; nil when the suite's project has not opted in
|
|
28
|
+
def call(suite_path, files)
|
|
29
|
+
SELECTIONS_LOCK.synchronize do
|
|
30
|
+
SELECTIONS.fetch([suite_path, files]) { SELECTIONS[[suite_path, files]] = choose(suite_path, files) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def choose(suite_path, files)
|
|
35
|
+
project = project_adapters(files)
|
|
36
|
+
return nil unless opted_in?(suite_path, project)
|
|
37
|
+
|
|
38
|
+
chosen = files.select { |file| file.end_with?(".adapter") } + stand_ins(suite_path)
|
|
39
|
+
claimed = chosen.flat_map { |file| ports_of(file) }
|
|
40
|
+
(chosen + project.select { |file| inside_and_unclaimed?(file, claimed) }).uniq.sort
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def opted_in?(suite_path, project)
|
|
44
|
+
stand_ins(suite_path).any? || project.any? { |file| declarations(file).any?(&:stays_inside?) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array<String>] the stand-ins beside the suite
|
|
48
|
+
def stand_ins(suite_path) = Dir.glob(File.join(File.dirname(suite_path), STAND_INS, "*.adapter"))
|
|
49
|
+
|
|
50
|
+
# @return [Array<String>] the project's own adapters, found as the loader finds them
|
|
51
|
+
def project_adapters(files)
|
|
52
|
+
root = Ports::Loading.bootstrap.shared_root(nil, File.dirname(files.first))
|
|
53
|
+
return [] unless root
|
|
54
|
+
|
|
55
|
+
PROJECT_PATTERNS.flat_map { |pattern| Dir.glob(File.join(root, "adapters", pattern)) }.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inside_and_unclaimed?(file, claimed)
|
|
59
|
+
declared = declarations(file)
|
|
60
|
+
declared.any? && declared.all?(&:stays_inside?) && !declared.map(&:port).intersect?(claimed)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ports_of(file) = declarations(file).map(&:port)
|
|
64
|
+
|
|
65
|
+
# The adapters a file declares, read in a throwaway registry with its `require` lines left
|
|
66
|
+
# out, so an adapter that reaches outside is never constructed and its code never loads.
|
|
67
|
+
#
|
|
68
|
+
# @param file [String] an `.adapter` file
|
|
69
|
+
# @return [Array<Bluebook::Adapter>] none when the file cannot be read on its own
|
|
70
|
+
def declarations(file)
|
|
71
|
+
text = File.readlines(file).grep_v(REQUIRE_LINE).join
|
|
72
|
+
registry = Runtime::Registry.new
|
|
73
|
+
Hecks.with_registry(registry) { TOPLEVEL_BINDING.eval(text, file) }
|
|
74
|
+
registry.adapters.values
|
|
75
|
+
rescue StandardError, ScriptError
|
|
76
|
+
# A declaration that cannot be read without its code is treated as reaching outside.
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -2,6 +2,7 @@ require_relative "../ir"
|
|
|
2
2
|
require_relative "../../runtime/loader"
|
|
3
3
|
require_relative "../../runtime/errors"
|
|
4
4
|
require_relative "../../ports/persistence/binding_policy"
|
|
5
|
+
require_relative "adapter_selection"
|
|
5
6
|
|
|
6
7
|
module Hecks
|
|
7
8
|
module Behaviors
|
|
@@ -25,24 +26,30 @@ module Hecks
|
|
|
25
26
|
# @raise [Malformed] if any aggregate the suite boots is not bound to the
|
|
26
27
|
# default (Memory) adapter
|
|
27
28
|
def runtime_for(suite)
|
|
28
|
-
files
|
|
29
|
-
|
|
29
|
+
files = Array(suite.loads).map { |path| File.expand_path(path) }
|
|
30
|
+
adapters = suite.path && AdapterSelection.call(suite.path, files)
|
|
31
|
+
key = boot_key(files + Array(adapters))
|
|
30
32
|
|
|
31
33
|
RUNTIMES_LOCK.synchronize do
|
|
32
|
-
RUNTIMES[key] ||= boot_and_guard(files)
|
|
34
|
+
RUNTIMES[key] ||= boot_and_guard(files, adapters)
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Each file with its mtime, so an edited file boots fresh.
|
|
39
|
+
def boot_key(files) = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
|
|
40
|
+
|
|
36
41
|
# Refuses at boot when any aggregate binds to a non-Memory adapter —
|
|
37
42
|
# `reset_runtime_state!` only resets Memory's own per-instance store.
|
|
38
43
|
#
|
|
39
44
|
# @param files [Array<String>] absolute paths to the bluebook/hecksagon/world
|
|
40
45
|
# files to boot
|
|
46
|
+
# @param adapters [Array<String>, nil] the `.adapter` files `AdapterSelection` chose, or
|
|
47
|
+
# nil for a suite with no file of its own
|
|
41
48
|
# @return [Runtime::Dispatcher, Adapters::Lambda::RemoteDispatcher] the booted,
|
|
42
49
|
# Memory-only-guarded runtime
|
|
43
50
|
# @raise [Malformed] if any aggregate is bound to a non-Memory adapter
|
|
44
|
-
def boot_and_guard(files)
|
|
45
|
-
runtime = Hecks.boot_files(files, install_driving: false)
|
|
51
|
+
def boot_and_guard(files, adapters = nil)
|
|
52
|
+
runtime = Hecks.boot_files(files, install_driving: false, adapters: adapters)
|
|
46
53
|
guard_memory_only!(runtime)
|
|
47
54
|
runtime
|
|
48
55
|
end
|
|
@@ -15,7 +15,7 @@ module Hecks
|
|
|
15
15
|
# itself. An undeclared coupling or a separate-ways breach has no relationship to carry the
|
|
16
16
|
# allowance; the fix is to declare one.
|
|
17
17
|
RELATIONSHIP_KINDS = %i[relationship_kind_mismatch conformist_translates unpublished_dependency
|
|
18
|
-
unused_relationship redundant_relationship].freeze
|
|
18
|
+
unused_relationship redundant_relationship core_conforms].freeze
|
|
19
19
|
|
|
20
20
|
# @param owner [String] the declaration carrying the word, for the message
|
|
21
21
|
# @param finding [Symbol, String] the kind being acknowledged
|
|
@@ -85,7 +85,8 @@ module Hecks
|
|
|
85
85
|
# @return [Boolean] whether a second delivery leaves the state a first one did
|
|
86
86
|
def idempotent? = @idempotent ? true : false
|
|
87
87
|
|
|
88
|
-
# @return [Boolean] whether this command's own record may be written through an unminted
|
|
88
|
+
# @return [Boolean] whether this command's own record may be written through an unminted
|
|
89
|
+
# boot
|
|
89
90
|
def mints_era? = @mints_era ? true : false
|
|
90
91
|
|
|
91
92
|
# @return [Array<String>] every role that may run it when more than one may, else empty
|
|
@@ -97,7 +98,7 @@ module Hecks
|
|
|
97
98
|
# Mints one command as its own anonymous subclass of the class `declare` is called on.
|
|
98
99
|
#
|
|
99
100
|
# @param name [String, Symbol] the command's declared name
|
|
100
|
-
# @param role [String, nil] the declared role; `roles` all of them, `audience` who it is
|
|
101
|
+
# @param role [String, nil] the declared role; `roles` all of them, `audience` who it is for
|
|
101
102
|
# @param attributes [Array<Bluebook::Attribute>] the declared arguments
|
|
102
103
|
# @param givens [Array<Bluebook::Given>] the declared preconditions
|
|
103
104
|
# @param ensures [Array<Bluebook::Given>] the postconditions; `needs` the facts it supplies
|
|
@@ -65,7 +65,8 @@ module Hecks
|
|
|
65
65
|
#
|
|
66
66
|
# @param row [Bluebook::Declaration::ModuleRow, #to_h] a `ModuleRow` already, or anything
|
|
67
67
|
# answering `to_h` with `name`/`vision`/`aggregate` entries
|
|
68
|
-
# @return [Bluebook::Declaration::ModuleRow] `row` itself if it already is one, else a new
|
|
68
|
+
# @return [Bluebook::Declaration::ModuleRow] `row` itself if it already is one, else a new
|
|
69
|
+
# one
|
|
69
70
|
def self.from(row)
|
|
70
71
|
return row if row.is_a?(self)
|
|
71
72
|
|
|
@@ -61,6 +61,13 @@ module Hecks
|
|
|
61
61
|
# @return [Array<String>] every capability named so far, this one last
|
|
62
62
|
def provides(capability) = @provides << capability.to_s
|
|
63
63
|
|
|
64
|
+
# Says this adapter reaches nothing outside the process and the checkout: no network, no
|
|
65
|
+
# model, no host side effect. A behaviors suite loads it instead of asking for a stand-in
|
|
66
|
+
# (ADR 0171); an adapter that does not say it is kept out of every suite.
|
|
67
|
+
#
|
|
68
|
+
# @return [true]
|
|
69
|
+
def stays_inside = @stays_inside = true
|
|
70
|
+
|
|
64
71
|
# Names the wasm module this adapter runs, beside its `.adapter` file.
|
|
65
72
|
#
|
|
66
73
|
# @param file [String] the module's file name, such as `"occurrences.wasm"`
|
|
@@ -75,7 +82,8 @@ module Hecks
|
|
|
75
82
|
def build
|
|
76
83
|
MetaValidator.call_adapter(
|
|
77
84
|
Adapter.new(name: @name, port: @port, fields: @fields, secrets: @secrets, defaults: @defaults,
|
|
78
|
-
answers: @answers, runs: @runs, provides: @provides, source_dir: @source_dir
|
|
85
|
+
answers: @answers, runs: @runs, provides: @provides, source_dir: @source_dir,
|
|
86
|
+
stays_inside: @stays_inside == true)
|
|
79
87
|
)
|
|
80
88
|
end
|
|
81
89
|
|
|
@@ -26,18 +26,20 @@ module Hecks
|
|
|
26
26
|
@namespace = value.to_s
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# Classifies this bluebook as
|
|
29
|
+
# Classifies this bluebook as the core subdomain: where the domain's advantage lies, which
|
|
30
|
+
# no generic context may depend on and which translates rather than conforms (ADR 0172).
|
|
30
31
|
#
|
|
31
32
|
# @return [Symbol] `:core`
|
|
32
33
|
def core = classify(:core)
|
|
33
34
|
|
|
34
|
-
# Classifies this bluebook as a supporting
|
|
35
|
-
#
|
|
35
|
+
# Classifies this bluebook as a supporting subdomain: specific to this business, but not
|
|
36
|
+
# where its advantage lies.
|
|
36
37
|
#
|
|
37
38
|
# @return [Symbol] `:supporting`
|
|
38
39
|
def supporting = classify(:supporting)
|
|
39
40
|
|
|
40
|
-
# Classifies this bluebook as generic
|
|
41
|
+
# Classifies this bluebook as a generic subdomain: a solved problem that could be bought
|
|
42
|
+
# or attached, so it may not depend on the core (ADR 0172).
|
|
41
43
|
#
|
|
42
44
|
# @return [Symbol] `:generic`
|
|
43
45
|
def generic = classify(:generic)
|
|
@@ -5,15 +5,18 @@ module Hecks
|
|
|
5
5
|
# Groups a bluebook's aggregates in the modules it declares (ADR 0163). `domain_module
|
|
6
6
|
# "Deliver" do … end` opens a block whose `aggregate` words are recorded under the module;
|
|
7
7
|
# the module changes no aggregate, it names the task each one belongs to. Policies, read
|
|
8
|
-
# models and process managers inside the block are the bluebook's own, as they are outside
|
|
8
|
+
# models and process managers inside the block are the bluebook's own, as they are outside
|
|
9
|
+
# it.
|
|
9
10
|
module Modules
|
|
10
11
|
# Opens the module `name` and evaluates its body; a module of that name that an earlier
|
|
11
12
|
# file opened gains the aggregates this body declares.
|
|
12
13
|
#
|
|
13
14
|
# @param name [String] the module's own name, such as `"Deliver"`
|
|
14
|
-
# @yield the module body: its vision, aggregates, policies, read models and process
|
|
15
|
+
# @yield the module body: its vision, aggregates, policies, read models and process
|
|
16
|
+
# managers
|
|
15
17
|
# @return [Array<Bluebook::Declaration::ModuleRow>] every membership declared so far
|
|
16
|
-
# @raise [Bluebook::DSL::Malformed] if a module opens inside another, or holds no
|
|
18
|
+
# @raise [Bluebook::DSL::Malformed] if a module opens inside another, or holds no
|
|
19
|
+
# aggregate
|
|
17
20
|
def domain_module(name, &)
|
|
18
21
|
refuse_nested_module!(name)
|
|
19
22
|
@open_module = name.to_s
|
|
@@ -47,7 +47,8 @@ module Hecks
|
|
|
47
47
|
bluebook.aggregates.find { |candidate| candidate.name.to_s == Naming.demodulise(key) }
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
# The one field an aggregate is correlated by: its identity, or `id` when it declares
|
|
50
|
+
# The one field an aggregate is correlated by: its identity, or `id` when it declares
|
|
51
|
+
# none.
|
|
51
52
|
# A value-object identity drops its trailing `.value`, so `Thing` reads as `Thing.id`.
|
|
52
53
|
def identity_path(process_manager, aggregate)
|
|
53
54
|
paths = aggregate.identity_paths.map(&:to_s)
|
|
@@ -2,10 +2,12 @@ module Hecks
|
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
4
|
class CommandBuilder
|
|
5
|
-
# `attribute :name, Type, sets: true`: the argument and the plain `sets :name` that a
|
|
5
|
+
# `attribute :name, Type, sets: true`: the argument and the plain `sets :name` that a
|
|
6
|
+
# following
|
|
6
7
|
# line would record, at the same position among the command's mutations.
|
|
7
8
|
module FoldedSets
|
|
8
|
-
# Declares one argument and, with `sets: true`, the plain `sets` of the owner's field of
|
|
9
|
+
# Declares one argument and, with `sets: true`, the plain `sets` of the owner's field of
|
|
10
|
+
# the
|
|
9
11
|
# same name.
|
|
10
12
|
#
|
|
11
13
|
# @param name [Symbol] the argument's name
|
|
@@ -10,7 +10,8 @@ module Hecks
|
|
|
10
10
|
|
|
11
11
|
COERCE_BY_KIND = { "text" => :to_s, "symbol" => :to_sym }.freeze
|
|
12
12
|
|
|
13
|
-
# The arguments of one word call: its positional arguments, its keyword arguments, and its
|
|
13
|
+
# The arguments of one word call: its positional arguments, its keyword arguments, and its
|
|
14
|
+
# block.
|
|
14
15
|
Invocation = Struct.new(:args, :kwargs, :block)
|
|
15
16
|
|
|
16
17
|
# Not derivable from `fills:` (these rows all carry `fills: ""`) — the append target
|
|
@@ -19,10 +19,10 @@ module Hecks
|
|
|
19
19
|
# and names the queries it `answers` (`"Bluebook::Aggregate.Query"`); it needs no port, and the
|
|
20
20
|
# runtime binds each query it answers without a hecksagon line.
|
|
21
21
|
#
|
|
22
|
-
# `provides` names what
|
|
23
|
-
# `
|
|
22
|
+
# `provides` names what it can do beyond its port's contract (`"set_transform"`), and
|
|
23
|
+
# `stays_inside` is true when it reaches nothing outside the process and checkout (ADR 0171).
|
|
24
24
|
Adapter = Struct.new(:name, :port, :fields, :secrets, :defaults, :answers, :runs, :provides, :source_dir,
|
|
25
|
-
keyword_init: true) do
|
|
25
|
+
:stays_inside, keyword_init: true) do
|
|
26
26
|
def declares?(field) = all_fields.include?(field.to_sym)
|
|
27
27
|
|
|
28
28
|
def all_fields = (fields || []) + (secrets || [])
|
|
@@ -40,6 +40,9 @@ module Hecks
|
|
|
40
40
|
# @return [Boolean] whether this adapter declares `provides` for it
|
|
41
41
|
def provides?(capability) = capabilities.include?(capability.to_s)
|
|
42
42
|
|
|
43
|
+
# @return [Boolean] whether this adapter says it stays inside
|
|
44
|
+
def stays_inside? = stays_inside == true
|
|
45
|
+
|
|
43
46
|
# @return [Boolean] whether this adapter is a wasm plugin
|
|
44
47
|
def plugin? = !runs.nil?
|
|
45
48
|
|
|
@@ -36,14 +36,21 @@ module Hecks
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def judge!
|
|
39
|
-
|
|
40
|
-
runs: v(@adapter.runs))
|
|
39
|
+
declare
|
|
41
40
|
add_fields
|
|
42
41
|
add_all("AddSecret", @adapter.secrets)
|
|
43
42
|
add_all("AnswerQuery", @adapter.answered_queries)
|
|
44
43
|
add_all("Provide", @adapter.capabilities)
|
|
45
44
|
end
|
|
46
45
|
|
|
46
|
+
# Offers the `Declare` that names the adapter, its port, its plugin module and its reach.
|
|
47
|
+
def declare
|
|
48
|
+
send_to("Adapter::Adapter.Declare", @adapter.name, name: v(@adapter.name), port: v(@adapter.port),
|
|
49
|
+
runs: v(@adapter.runs), stays_inside: v(containment))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def containment = ("inside" if @adapter.stays_inside?)
|
|
53
|
+
|
|
47
54
|
# Offers one `AddField` per field, each with its declared default when it has one.
|
|
48
55
|
def add_fields
|
|
49
56
|
Array(@adapter.fields).each do |field|
|
|
@@ -73,7 +73,7 @@ module Hecks
|
|
|
73
73
|
def self.call_adapter(adapter)
|
|
74
74
|
judge_artifact(adapter, AdapterJudge, "#{adapter.name}'s adapter") do
|
|
75
75
|
JSON.generate([adapter.name, adapter.port, adapter.fields, adapter.secrets, adapter.declared_defaults,
|
|
76
|
-
adapter.answered_queries, adapter.runs, adapter.capabilities])
|
|
76
|
+
adapter.answered_queries, adapter.runs, adapter.capabilities, adapter.stays_inside?])
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -36,7 +36,8 @@ module Hecks
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# @return [Array<Bluebook::Aggregate>] the aggregates with no commands that do not
|
|
39
|
+
# @return [Array<Bluebook::Aggregate>] the aggregates with no commands that do not
|
|
40
|
+
# acknowledge it
|
|
40
41
|
def unacknowledged_commandless(bluebook)
|
|
41
42
|
commandless = bluebook.aggregates.select { |aggregate| aggregate.commands.empty? }
|
|
42
43
|
commandless.reject { |aggregate| Acknowledgements.held?(aggregate, :no_command_surface) }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
module ModelCheck
|
|
4
|
+
# The checks a bluebook's `core`, `supporting` or `generic` keys (ADR 0172): a generic
|
|
5
|
+
# subdomain may not depend on the core, and a core context should translate what it takes
|
|
6
|
+
# from another rather than conform to it. Classifications are read from each bluebook, never
|
|
7
|
+
# restated in a context map.
|
|
8
|
+
module ClassificationChecks
|
|
9
|
+
# @param edges [Array<Hash>] `Projections::ContextRelationships.call`, from the context that
|
|
10
|
+
# depends to the one it depends on
|
|
11
|
+
# @param classifications [Hash{String => String}] each context to its classification
|
|
12
|
+
# @return [Array<Finding>] one error per generic context that depends on a core one
|
|
13
|
+
def generic_depends_on_core_findings(edges, classifications)
|
|
14
|
+
edges.select { |edge| generic_on_core?(edge, classifications) }.uniq { |edge| [edge[:from], edge[:to]] }.map do |edge|
|
|
15
|
+
Finding.new(kind: :generic_depends_on_core, severity: :error, subject: "#{edge[:from]} -> #{edge[:to]}",
|
|
16
|
+
message: "#{edge[:from]} is generic but depends on #{edge[:to]}, which is core " \
|
|
17
|
+
"(#{edge[:kind]}#{" via #{edge[:via]}" if edge[:via]}); a generic subdomain must be " \
|
|
18
|
+
"replaceable, so invert the dependency or reclassify one of them")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param map [Bluebook::ContextMap] the declared map
|
|
23
|
+
# @param classifications [Hash{String => String}] each context to its classification
|
|
24
|
+
# @return [Array<Finding>] one warning per declared conformist relationship whose downstream
|
|
25
|
+
# is core, unless the relationship acknowledges it. The conformist ADR 0155 implies for an
|
|
26
|
+
# `attaches` is not declared and is not read.
|
|
27
|
+
def core_conforms_findings(map, classifications)
|
|
28
|
+
map.relationships.filter_map do |relationship|
|
|
29
|
+
next unless core_conformist?(relationship, classifications)
|
|
30
|
+
|
|
31
|
+
Finding.new(kind: :core_conforms, severity: :warning,
|
|
32
|
+
subject: "#{relationship.downstream} <- #{relationship.upstream}",
|
|
33
|
+
message: "#{relationship.downstream} is core but conforms to #{relationship.upstream}; " \
|
|
34
|
+
"the core model should translate what it receives (an anticorruption_layer)")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def generic_on_core?(edge, classifications)
|
|
41
|
+
classifications[edge[:from].to_s] == "generic" && classifications[edge[:to].to_s] == "core"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def core_conformist?(relationship, classifications)
|
|
45
|
+
relationship.kind == :conformist && !relationship.implied &&
|
|
46
|
+
classifications[relationship.downstream.to_s] == "core" && !relationship.acknowledges?(:core_conforms)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "context_map_checks"
|
|
2
|
+
require_relative "classification_checks"
|
|
2
3
|
|
|
3
4
|
module Hecks
|
|
4
5
|
module Bluebook
|
|
@@ -7,6 +8,7 @@ module Hecks
|
|
|
7
8
|
# this hecksagon acknowledges, and no two contexts may depend on each other in a cycle.
|
|
8
9
|
module ContextChecks
|
|
9
10
|
include ContextMapChecks
|
|
11
|
+
include ClassificationChecks
|
|
10
12
|
|
|
11
13
|
# The edge kinds that couple two contexts at run time. A shared kernel merges in-process, so
|
|
12
14
|
# two contexts attaching each other is a different question and is left out.
|
|
@@ -21,7 +21,8 @@ module Hecks
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# @return [Boolean] whether the relationship is a bare conformist that attaches alone
|
|
24
|
+
# @return [Boolean] whether the relationship is a bare conformist that attaches alone
|
|
25
|
+
# explain
|
|
25
26
|
def restates_attach?(relationship, edges)
|
|
26
27
|
return false unless relationship.kind == :conformist && relationship.published.to_a.empty?
|
|
27
28
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Planner` port to `ShardPartitioner`, which splits a family's files and writes the plan.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/ci_speed/adapters/shard_partitioner"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "ShardPartitioner" do
|
|
6
|
+
port "Planner"
|
|
7
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "hecks/tools/shard_planner"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The `Planner` port's adapter: splits a family's files among its shards with the shared
|
|
10
|
+
# longest-processing-time planner and writes the plan where the shard legs read it:
|
|
11
|
+
# `group-<n>.txt` (one file per line), `runtimes.log` (every file's price, for the leg's own
|
|
12
|
+
# worker split) and `plan.json`.
|
|
13
|
+
class ShardPartitioner
|
|
14
|
+
# @param aggregate [Object, nil] unused
|
|
15
|
+
# @param root [String, nil] the checkout patterns are read in; the working directory if nil
|
|
16
|
+
def initialize(aggregate: nil, root: nil)
|
|
17
|
+
@root = root || Dir.pwd
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param held [Hash] the `ShardPlan` record and the ask's arguments: `shards`, `items_from`,
|
|
21
|
+
# `out_dir`, `source` (`timings` or `size`) and `timings`
|
|
22
|
+
# @return [Hash] `shards:`, `files:` and the predicted `longest` and `mean` leg price
|
|
23
|
+
# @raise [ArgumentError] when there is nothing to split
|
|
24
|
+
def partition(**held)
|
|
25
|
+
plan = held.transform_values { |value| value.is_a?(Hash) ? value[:value] : value }
|
|
26
|
+
items = items_of(plan.fetch(:items_from))
|
|
27
|
+
raise ArgumentError, "no files to split in #{plan[:items_from]}" if items.empty?
|
|
28
|
+
|
|
29
|
+
prices = prices_of(items, plan)
|
|
30
|
+
groups = Tools::ShardPlanner.partition(items, plan.fetch(:shards), prices)
|
|
31
|
+
write(plan.fetch(:out_dir), groups, prices, plan.fetch(:source))
|
|
32
|
+
answer(groups, prices)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def items_of(from)
|
|
38
|
+
kind, spec = from.split(":", 2)
|
|
39
|
+
found = kind == "glob" ? Dir.glob(spec, base: @root) : File.readlines(File.join(@root, spec), chomp: true)
|
|
40
|
+
named = found.map(&:strip).reject(&:empty?)
|
|
41
|
+
named.uniq.sort
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prices_of(items, plan)
|
|
45
|
+
table = plan[:source] == "timings" ? Tools::ShardPlanner.parse(plan[:timings]) : {}
|
|
46
|
+
Tools::ShardPlanner.prices(items, table, ->(item) { File.size?(File.join(@root, item)) || 1 })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write(dir, groups, prices, source)
|
|
50
|
+
out = File.join(@root, dir)
|
|
51
|
+
FileUtils.mkdir_p(out)
|
|
52
|
+
groups.each_with_index { |files, index| File.write(File.join(out, "group-#{index + 1}.txt"), listing(files)) }
|
|
53
|
+
File.write(File.join(out, "runtimes.log"), listing(prices.map { |file, price| "#{file}:#{price}" }.sort))
|
|
54
|
+
File.write(File.join(out, "plan.json"), JSON.pretty_generate(source: source, groups: groups))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def listing(lines) = lines.empty? ? "" : "#{lines.join("\n")}\n"
|
|
58
|
+
|
|
59
|
+
def answer(groups, prices)
|
|
60
|
+
loads = groups.map { |files| files.sum { |file| prices.fetch(file) } }
|
|
61
|
+
{ shards: { value: groups.size }, files: { value: prices.size },
|
|
62
|
+
longest: { value: loads.max.round(2) }, mean: { value: (loads.sum / loads.size).round(2) } }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `TimingsSource` port to `TimingsSource`, which finds the previous main run's per-file timings.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/ci_speed/adapters/timings_source"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "TimingsSource" do
|
|
6
|
+
port "TimingsSource"
|
|
7
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "hecks/tools/shard_planner"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Adapters
|
|
10
|
+
# The `TimingsSource` port's adapter: finds the seconds each file took in the newest successful
|
|
11
|
+
# CI run on main. `timings_from` is a directory of timing logs already on disk, or `main`, which
|
|
12
|
+
# downloads the runtime-log artifacts of that run through `gh`. It refuses when it finds no
|
|
13
|
+
# timing at all, and the bluebook's fallback policy plans by file size instead.
|
|
14
|
+
class TimingsSource
|
|
15
|
+
# Raised when no timing could be read; it is the reason `TimingsUnavailable` carries.
|
|
16
|
+
class Unavailable < StandardError; end
|
|
17
|
+
|
|
18
|
+
# The CI workflow whose runs on main hold the artifacts.
|
|
19
|
+
WORKFLOW = "ci.yml"
|
|
20
|
+
|
|
21
|
+
# @param aggregate [Object, nil] unused
|
|
22
|
+
def initialize(aggregate: nil); end
|
|
23
|
+
|
|
24
|
+
# @param held [Hash] the `ShardPlan` record: `name` and `timings_from`
|
|
25
|
+
# @return [Hash] `timings:` the merged `path:seconds` lines of every log found
|
|
26
|
+
# @raise [Unavailable] when there is no run, no artifact or no readable timing
|
|
27
|
+
def fetch(**held)
|
|
28
|
+
name, from = held.values_at(:name, :timings_from).map { |value| value.is_a?(Hash) ? value[:value] : value }
|
|
29
|
+
table = Tools::ShardPlanner.merge(logs_in(from == "main" ? download(name) : from))
|
|
30
|
+
raise Unavailable, "no timings found in #{from}" if table.empty?
|
|
31
|
+
|
|
32
|
+
{ timings: { value: table.map { |file, seconds| "#{file}:#{seconds}" }.sort.join("\n") } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# @return [Array<String>] the text of every file under `directory`
|
|
38
|
+
def logs_in(directory)
|
|
39
|
+
raise Unavailable, "#{directory} is not a directory" unless File.directory?(directory.to_s)
|
|
40
|
+
|
|
41
|
+
files = Dir.glob(File.join(directory, "**", "*")).select { |path| File.file?(path) }
|
|
42
|
+
files.sort.map { |path| File.read(path) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Downloads the runtime logs of the newest successful run on main into a new directory.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] the directory
|
|
48
|
+
def download(name)
|
|
49
|
+
run = newest_run
|
|
50
|
+
dir = Dir.mktmpdir("shard-timings-")
|
|
51
|
+
pattern = "#{name.tr("_", "-")}-runtime-group-*"
|
|
52
|
+
_, err, status = Open3.capture3("gh", "run", "download", run, "--pattern", pattern, "--dir", dir)
|
|
53
|
+
raise Unavailable, "could not download #{pattern} of run #{run}: #{err.strip}" unless status.success?
|
|
54
|
+
|
|
55
|
+
dir
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [String] the id of the newest successful run of the CI workflow on main
|
|
59
|
+
def newest_run
|
|
60
|
+
out, err, status = Open3.capture3("gh", "run", "list", "--workflow", WORKFLOW, "--branch", "main",
|
|
61
|
+
"--status", "success", "--limit", "1", "--json", "databaseId")
|
|
62
|
+
raise Unavailable, "could not list CI runs: #{err.strip}" unless status.success?
|
|
63
|
+
|
|
64
|
+
JSON.parse(out).dig(0, "databaseId")&.to_s or raise Unavailable, "no successful CI run on main"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|