hecks 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
  3. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
  4. data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
  5. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
  6. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
  7. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
  8. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
  9. data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
  10. data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
  11. data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
  12. data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
  13. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
  14. data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
  15. data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
  16. data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
  17. data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
  18. data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
  19. data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
  20. data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
  21. data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
  22. data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
  23. data/lib/hecks/adapters/driving/mcp.rb +2 -1
  24. data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
  25. data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
  26. data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
  27. data/lib/hecks/assembly.rb +18 -4
  28. data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
  29. data/lib/hecks/behaviors/expectations/boot.rb +12 -5
  30. data/lib/hecks/bluebook/acknowledgements.rb +1 -1
  31. data/lib/hecks/bluebook/command.rb +3 -2
  32. data/lib/hecks/bluebook/declaration.rb +2 -1
  33. data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
  34. data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
  35. data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
  36. data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
  37. data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
  38. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
  39. data/lib/hecks/bluebook/hecksagon.rb +6 -3
  40. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
  41. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  42. data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
  43. data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
  44. data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
  45. data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
  46. data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
  47. data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
  48. data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
  49. data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
  50. data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
  51. data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
  52. data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
  53. data/lib/hecks/coder/coder.bluebook +3 -1
  54. data/lib/hecks/command_names/index.rb +1 -1
  55. data/lib/hecks/command_names/resolution.rb +15 -5
  56. data/lib/hecks/composer/toolbox.rb +2 -1
  57. data/lib/hecks/corpus.rb +4 -0
  58. data/lib/hecks/doc/reference/readme.rb +1 -1
  59. data/lib/hecks/framework_bluebooks.rb +3 -2
  60. data/lib/hecks/fuzzing/differential.rb +1 -1
  61. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  62. data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
  63. data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
  64. data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
  65. data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
  66. data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
  67. data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
  68. data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
  69. data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
  70. data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
  71. data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
  72. data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
  73. data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
  74. data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
  75. data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
  76. data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
  77. data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
  78. data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
  79. data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
  80. data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
  81. data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
  82. data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
  83. data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
  84. data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
  85. data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
  86. data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
  87. data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
  88. data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
  89. data/lib/hecks/hecks/adapters/git_train.rb +9 -5
  90. data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
  91. data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
  92. data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
  93. data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
  94. data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
  95. data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
  96. data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
  97. data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
  98. data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
  99. data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
  100. data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
  101. data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
  102. data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
  103. data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
  104. data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
  105. data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
  106. data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
  107. data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
  108. data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
  109. data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
  110. data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
  111. data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
  112. data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
  113. data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
  114. data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
  115. data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
  116. data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
  117. data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
  118. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
  119. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
  120. data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
  121. data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
  122. data/lib/hecks/hecks/codebase.behaviors +0 -6
  123. data/lib/hecks/hecks/custodian.behaviors +22 -1
  124. data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
  125. data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
  126. data/lib/hecks/hecks/environments/memory.world +5 -0
  127. data/lib/hecks/hecks/hecks.contextmap +8 -0
  128. data/lib/hecks/hecks/hecks.hecksagon +106 -6
  129. data/lib/hecks/hecks/hecks.world +9 -1
  130. data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
  131. data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
  132. data/lib/hecks/hecks/test/test.bluebook +0 -19
  133. data/lib/hecks/language/adapter.bluebook +13 -0
  134. data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
  135. data/lib/hecks/language/sentence/sentence.bluebook +2 -0
  136. data/lib/hecks/pipeline/pipeline.bluebook +251 -0
  137. data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
  138. data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
  139. data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
  140. data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
  141. data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
  142. data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
  143. data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
  144. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
  145. data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
  146. data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
  147. data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
  148. data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
  149. data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
  150. data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
  151. data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
  152. data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
  153. data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
  154. data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
  155. data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
  156. data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
  157. data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
  158. data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
  159. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
  160. data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
  161. data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
  162. data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
  163. data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
  164. data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
  165. data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
  166. data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
  167. data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
  168. data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
  169. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
  170. data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
  171. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
  172. data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
  173. data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
  174. data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
  175. data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
  176. data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
  177. data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
  178. data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
  179. data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
  180. data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
  181. data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
  182. data/lib/hecks/projections/undo.rb +2 -1
  183. data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
  184. data/lib/hecks/projector/cli_projector/renames.rb +4 -2
  185. data/lib/hecks/projector/cli_projector/usage.rb +4 -3
  186. data/lib/hecks/projector/exporter/adapters.rb +3 -2
  187. data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
  188. data/lib/hecks/projector/narrate_projector.rb +10 -3
  189. data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
  190. data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
  191. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
  192. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
  193. data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
  194. data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
  195. data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
  196. data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
  197. data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
  198. data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
  199. data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
  200. data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
  201. data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
  202. data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
  203. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
  204. data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
  205. data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
  206. data/lib/hecks/quality_control/quality_control.bluebook +11 -11
  207. data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
  208. data/lib/hecks/query_ir/touchpoints.rb +2 -1
  209. data/lib/hecks/refused.rb +2 -1
  210. data/lib/hecks/release/approval.rb +4 -2
  211. data/lib/hecks/runtime/event_log.rb +27 -0
  212. data/lib/hecks/runtime/event_tap.rb +106 -0
  213. data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
  214. data/lib/hecks/runtime/loader.rb +35 -19
  215. data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
  216. data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
  217. data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
  218. data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
  219. data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
  220. data/lib/hecks/runtime/registry/verification.rb +7 -3
  221. data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
  222. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  223. data/lib/hecks/rust_format.rb +2 -1
  224. data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
  225. data/lib/hecks/shape_ledger.rb +1 -1
  226. data/lib/hecks/storehouse/audit.rb +2 -1
  227. data/lib/hecks/storehouse/identity.rb +6 -3
  228. data/lib/hecks/storehouse.rb +2 -1
  229. data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
  230. data/lib/hecks/tools/ci_workflows.rb +2 -1
  231. data/lib/hecks/tools/document.rb +12 -1
  232. data/lib/hecks/tools/downstream_model_check.rb +4 -2
  233. data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
  234. data/lib/hecks/tools/model_check/examination.rb +25 -2
  235. data/lib/hecks/tools/move_disables.rb +4 -2
  236. data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
  237. data/lib/hecks/tools/rspec_shard_files.rb +12 -15
  238. data/lib/hecks/tools/sequence_script.rb +1 -1
  239. data/lib/hecks/tools/shard_planner.rb +65 -0
  240. data/lib/hecks/tools/stage_progress.rb +2 -1
  241. data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
  242. data/lib/hecks/version.rb +1 -1
  243. data/lib/hecks/vocabulary.rb +8 -9
  244. data/lib/hecks.rb +5 -2
  245. data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
  246. data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
  247. data/rust/host/HECKS_RELEASE +1 -1
  248. data/rust/parser/src/emit.rs +5 -1
  249. data/rust/parser/src/ir.rs +2 -0
  250. data/rust/parser/src/keywords.rs +12 -0
  251. data/rust/parser/src/main.rs +1 -0
  252. data/rust/parser/src/parse/adapter.rs +1 -0
  253. data/rust/parser/src/phrases.rs +16 -0
  254. data/rust/parser/src/sentence/examples.rs +93 -0
  255. data/rust/parser/src/sentence/mod.rs +1 -0
  256. metadata +89 -5
  257. data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
@@ -0,0 +1,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 = Array(suite.loads).map { |path| File.expand_path(path) }
29
- key = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
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 boot
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 listed for
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 one
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 core to the framework, rather than a domain built on it.
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 piece of the framework, rather than a domain
35
- # built on it.
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 infrastructure, rather than a domain built on it.
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 it.
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 managers
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 aggregate
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 none.
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 following
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 the
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 block.
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 the adapter can do beyond its port's contract, such as
23
- # `"set_transform"`; a caller asks before relying on it.
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
- send_to("Adapter::Adapter.Declare", @adapter.name, name: v(@adapter.name), port: v(@adapter.port),
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 acknowledge it
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 explain
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