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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../console_capture"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ class MainIntegration
9
+ # The push train's own copy (`~/.claude/hecks-train`), as a boarding uses it: brought up to
10
+ # origin/main when it has no tracked changes, then asked to board the checkout's main. The
11
+ # train runs in its own process and store, so what it pushed is read from its answer. The
12
+ # command line is fixed: no plain push, no force, no skipped hook, no owner override.
13
+ class Train
14
+ # Where the train's copy is.
15
+ ROOT = File.join(Dir.home, ".claude", "hecks-train")
16
+
17
+ # The statuses of a boarding the remote holds: pushed now, or carried by an earlier push.
18
+ BOARDED = %w[pushed carried].freeze
19
+
20
+ # Where the launcher's JSON answer starts in what it printed.
21
+ RECORD_START = /^\{/
22
+
23
+ # @param runner [#call] takes `(argv, chdir:)` and answers `[ok, output]`
24
+ # @param root [String] the train's copy
25
+ def initialize(runner, root: ROOT)
26
+ @runner = runner
27
+ @root = root
28
+ end
29
+
30
+ # Brings the copy up to origin/main, unless it has tracked changes.
31
+ #
32
+ # @return [String] where the copy stands, or why it was left
33
+ def refresh
34
+ changed = run("git", "status", "--porcelain", "--untracked-files=no").last.strip
35
+ return "train copy left as it is: it has tracked changes" unless changed.empty?
36
+
37
+ fetched = run("git", "fetch", "-q", "origin", "main").first
38
+ moved = fetched && run("git", "checkout", "-q", "--detach", "FETCH_HEAD").first
39
+ moved ? "train copy at origin/main" : "train copy left as it is: it could not be refreshed"
40
+ end
41
+
42
+ # @param checkout [String] the checkout whose main is boarded
43
+ # @return [Array<String>] the command line the boarding runs
44
+ def command(checkout)
45
+ [File.join(@root, "exe", "hecks"), "SendTrain.board!", "checkout=#{checkout}", "remote=origin", "branch=main",
46
+ "--confirm", "--wait"]
47
+ end
48
+
49
+ # @param checkout [String] the checkout whose main is boarded
50
+ # @return [Hash{String => Object}] the boarding's record as the train answered it
51
+ # @raise [ConsoleCapture::Failure] when the train refused, faulted or answered unreadably
52
+ def board(checkout)
53
+ ok, output = run(*command(checkout))
54
+ state = record_in(output)
55
+ return state if ok && BOARDED.include?(state["status"].to_s)
56
+
57
+ raise ConsoleCapture::Failure, "the push train did not push: #{state["refusal"] || output.to_s.strip[-300..] || output}"
58
+ end
59
+
60
+ private
61
+
62
+ def run(*argv) = @runner.call(argv, chdir: @root)
63
+
64
+ def record_in(output)
65
+ text = output.to_s
66
+ start = text.index(RECORD_START)
67
+ start ? JSON.parse(text[start..]).fetch("state", {}) : {}
68
+ rescue JSON::ParserError
69
+ {}
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/main_integration"
4
+
5
+ Hecks.adapter "MainIntegration" do
6
+ port "Integrating"
7
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "hecks/runtime/dispatcher"
5
+ require "hecks/runtime/errors"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # The `Integrating` port's adapter for `Integration.Advance`: dispatches the next step of every
10
+ # open MainIntegration run, once. A landed run is verified, a verified one boarded, and a pushed
11
+ # or watched one has its CI run read. A run in any other stage (being verified or boarded,
12
+ # green, red, superseded or waiting for a person) is left alone, so a second advance in the
13
+ # same stage dispatches nothing.
14
+ #
15
+ # Each step is a dispatch of its own through the boot whose advance asked, not a dispatch from
16
+ # inside the saga, so it starts its own reaction chain. A boarding needs the advance's
17
+ # `confirm`, and waits while another run is being pushed or watched.
18
+ class MainIntegration
19
+ # The command each stage's next step dispatches.
20
+ NEXT = { "landed" => "Verify", "verified" => "Board", "pushed" => "Observe", "ci_running" => "Observe" }.freeze
21
+
22
+ # @param registry [Runtime::Registry, nil] the booted registry whose runs are advanced
23
+ # @param aggregate [Object, nil] unused
24
+ def initialize(registry: nil, aggregate: nil)
25
+ @registry = registry
26
+ end
27
+
28
+ # Dispatches each open run's next step.
29
+ #
30
+ # @param held [Hash] the `Integration` record: `confirm` lets a verified run board
31
+ # @return [Hash{Symbol => Hash}] `report:` one line per open run: what was dispatched and
32
+ # the stage it left the run in, or why nothing was
33
+ def step(**held)
34
+ confirm = plain(held[:confirm]) == true
35
+ lines = runs.all.select { |run| NEXT.key?(run.state) }.map { |run| advance(run, confirm) }
36
+ { report: { value: lines.empty? ? "no open integration run" : lines.join("\n") } }
37
+ end
38
+
39
+ private
40
+
41
+ def runs = Runs.new(@registry)
42
+
43
+ def advance(run, confirm)
44
+ held_back = boarding_held_back(run, confirm) if NEXT[run.state] == "Board"
45
+ return "#{run.short} #{run.state}: #{held_back}" if held_back
46
+
47
+ dispatch(run, NEXT[run.state], confirm)
48
+ end
49
+
50
+ def boarding_held_back(run, confirm)
51
+ other = runs.in_flight(except: run.landed).first
52
+ return "waits for #{other.short}, which is #{other.state}" if other
53
+ return "would board; advance with --confirm to push main" unless confirm
54
+
55
+ nil
56
+ end
57
+
58
+ def dispatch(run, command, confirm)
59
+ Runtime::Dispatcher.new(@registry).dispatch_flat("Hecks::Integration.#{command}", arguments(run, command, confirm))
60
+ "#{run.short} #{run.state} -> #{command}: now #{runs.find(run.landed)&.state}"
61
+ rescue *Runtime::DOMAIN_REFUSALS => e
62
+ "#{run.short} #{run.state} -> #{command} refused: #{e.message}"
63
+ end
64
+
65
+ def arguments(run, command, confirm)
66
+ args = { run: SecureRandom.uuid, landed: run.landed }
67
+ args[:checkout] = run.checkout unless run.checkout.empty?
68
+ args[:confirm] = true if command == "Board" && confirm
69
+ args
70
+ end
71
+
72
+ def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative "main_integration/runs"
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/main_verification"
4
+
5
+ Hecks.adapter "MainVerification" do
6
+ port "MainVerifying"
7
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "console_capture"
4
+ require_relative "main_integration"
5
+ require_relative "main_integration/repository"
6
+ require_relative "main_integration/checks"
7
+ require_relative "main_integration/command_runner"
8
+
9
+ module Hecks
10
+ module Adapters
11
+ # The `MainVerifying` port's adapter for `Integration.Verify`: checks a landed commit in a
12
+ # clean detached copy of it, holding no local Hecks files, by running every check `Checks`
13
+ # names there. A commit other than the head of local main is answered `superseded` without a
14
+ # check: the run for the head verifies both. A check that fails refuses the
15
+ # verification, naming each failing check and what it printed last.
16
+ class MainVerification
17
+ # How much of a failing check's output a refusal keeps.
18
+ TAIL = 400
19
+
20
+ class << self
21
+ # @return [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`; the command
22
+ # runner when nil. A spec replaces it so no check has to run.
23
+ attr_accessor :runner
24
+ end
25
+
26
+ # @param aggregate [Object, nil] unused
27
+ # @param settings [Hash] unused
28
+ # @param root [String, nil] unused
29
+ def initialize(aggregate: nil, settings: {}, root: nil)
30
+ @failures = []
31
+ end
32
+
33
+ # @param held [Hash] the `Integration` record: `landed` and `checkout`
34
+ # @return [Hash{Symbol => Hash}] `outcome:` (`verified` or `superseded`) and `report:`
35
+ # @raise [ConsoleCapture::Failure] naming every check that failed
36
+ def check(**held)
37
+ landed = plain(held[:landed]).to_s
38
+ repository = MainIntegration::Repository.new(plain(held[:checkout]) || Dir.pwd)
39
+ head = repository.main
40
+ return superseded(landed, head) unless head == landed
41
+
42
+ passed = run_checks(repository, landed)
43
+ answer("verified", "#{landed[0, 10]} verified: #{passed.join(", ")}")
44
+ end
45
+
46
+ private
47
+
48
+ def run_checks(repository, landed)
49
+ changed = repository.changed(repository.remote_main, landed)
50
+ repository.clean_copy(landed) do |directory|
51
+ checks = MainIntegration::Checks.new(directory, changed).all
52
+ failed = checks.reject { |name, argv| record(name, argv, directory) }
53
+ refuse!(landed) unless failed.empty?
54
+ checks.map(&:first)
55
+ end
56
+ end
57
+
58
+ def record(name, argv, directory)
59
+ ok, output = runner.call(argv, chdir: directory)
60
+ @failures << "#{name}: #{tail(output)}" unless ok
61
+ ok
62
+ end
63
+
64
+ def refuse!(landed)
65
+ raise ConsoleCapture::Failure, "#{landed[0, 10]} failed verification: #{@failures.join("; ")}"
66
+ end
67
+
68
+ def superseded(landed, head)
69
+ answer("superseded", "#{landed[0, 10]} is not the head of main (#{head[0, 10]}); the run for the head carries it")
70
+ end
71
+
72
+ def answer(outcome, report) = { outcome: { value: outcome }, report: { value: report } }
73
+
74
+ def tail(output)
75
+ text = last_lines(output).split.join(" ")
76
+ text.length > TAIL ? text[-TAIL..] : text
77
+ end
78
+
79
+ def last_lines(output)
80
+ lines = output.to_s.strip.lines
81
+ lines.last(3).join(" ")
82
+ end
83
+
84
+ def runner = self.class.runner || MainIntegration::CommandRunner.new
85
+
86
+ def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/push_recorder"
4
+
5
+ Hecks.adapter "PushRecorder" do
6
+ port "PushRecords"
7
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell"
4
+ require_relative "console_capture"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ # The `PushRecords` port's adapter: records how a boarding of the push train ended in the
9
+ # boarded checkout, by running that checkout's own launcher with `PushOutcome.record!`.
10
+ #
11
+ # The train runs in Memory in a checkout of its own; the boarded checkout's launcher boots that
12
+ # checkout's domain, whose own overlay says where its records live, so the outcome lands in the
13
+ # same journal as its landings. It records and decides nothing: the outcome is the run's
14
+ # status, and a recording that fails is refused, which leaves the push as it was.
15
+ class PushRecorder
16
+ # The launcher, relative to the checkout's top directory.
17
+ LAUNCHER = File.join("exe", "hecks")
18
+
19
+ # The fields of the run recorded as they are held, beside the ones the outcome names.
20
+ COPIED = %i[branch pushed commits].freeze
21
+
22
+ # @param aggregate [Object, nil] unused
23
+ # @param settings [Hash] `launcher:` the program to run instead of the checkout's own
24
+ # @param root [String, nil] unused
25
+ def initialize(aggregate: nil, settings: {}, root: nil)
26
+ @launcher = settings[:launcher]
27
+ @shell = Shell.new
28
+ end
29
+
30
+ # @param held [Hash] the `SendTrain` record: `run`, `status`, `checkout`, `branch`, `pushed`,
31
+ # `commits`, and `report` or `refusal`
32
+ # @return [Hash{Symbol => Hash}] `report:` what the checkout's launcher said it recorded
33
+ # @raise [ConsoleCapture::Failure] when no checkout is named or the launcher refuses
34
+ def record_outcome(**held)
35
+ top = top_of(plain(held[:checkout]))
36
+ result = @shell.capture(*command(top), *arguments(held, top), "--wait", env: bundle(top), chdir: top)
37
+ raise ConsoleCapture::Failure, "#{top} did not record the push: #{said(result)}" unless recorded?(result)
38
+
39
+ { report: { value: "recorded the #{plain(held[:status])} push in #{top}" } }
40
+ end
41
+
42
+ private
43
+
44
+ def top_of(checkout)
45
+ raise ConsoleCapture::Failure, "the boarding names no checkout to record in" if checkout.to_s.empty?
46
+
47
+ result = @shell.capture("git", "rev-parse", "--show-toplevel", chdir: checkout)
48
+ raise ConsoleCapture::Failure, "#{checkout} is not a checkout: #{said(result)}" unless result.ok?
49
+
50
+ result.out.strip
51
+ end
52
+
53
+ def command(top) = [@launcher || File.join(top, LAUNCHER)]
54
+
55
+ # The checkout's launcher runs as it runs there: on its own Gemfile, not the train's this
56
+ # process inherited, and with no environment overlay, which would win over the checkout's
57
+ # own store.
58
+ def bundle(top)
59
+ gemfile = @launcher ? {} : { "BUNDLE_GEMFILE" => File.join(top, "Gemfile") }
60
+ { "HECKS_ENVIRONMENT" => nil, **gemfile }
61
+ end
62
+
63
+ def arguments(held, top)
64
+ value = held.to_h { |name, field| [name, plain(field)] }
65
+ report = value[:report].nil? ? value[:refusal] : value[:report]
66
+ fields = { run: "#{value[:run]}-#{value[:status]}", checkout: File.join(top, ""), outcome: value[:status],
67
+ report: report, **value.slice(*COPIED) }
68
+ ["PushOutcome.record!", *fields.compact.map { |name, field| "#{name}.value=#{field}" }]
69
+ end
70
+
71
+ def recorded?(result) = result.ok? && !result.out.include?('"refusal": "')
72
+
73
+ def said(result) = (result.err.strip.empty? ? result.out : result.err).strip[0, 300]
74
+
75
+ def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "red_flag_marker"
4
+ require_relative "console_capture"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ # What a standing red flag refuses. While the flag stands, nothing lands on main and nothing is
9
+ # boarded on the push train except a fix for a red Work row: a branch named for a `red-*` row
10
+ # (with or without a `qa/` lead) lands and records its tip on the flag, and the train carries
11
+ # only that tip. The refusal names the flag: `main is red: flag <id>`.
12
+ class RedFlagGuard
13
+ # The Work rows that say main is red are keyed with this prefix.
14
+ RED_ROW = "red-"
15
+
16
+ # A branch lead the QA loop adds to a row's name.
17
+ QA_LEAD = "qa/"
18
+
19
+ # @param git_dir [String] the repository's common git directory
20
+ def initialize(git_dir)
21
+ @marker = RedFlagMarker.new(git_dir)
22
+ end
23
+
24
+ # @param branch [String] the branch about to land on main
25
+ # @return [void]
26
+ # @raise [ConsoleCapture::Failure] when a flag stands and the branch is not a red row's fix
27
+ def landing!(branch)
28
+ flag = @marker.read
29
+ return unless flag
30
+ return if fix?(branch)
31
+
32
+ raise ConsoleCapture::Failure, "#{refusal(flag)}; #{branch} is not a fix for a red-* Work row"
33
+ end
34
+
35
+ # Records that a red row's fix landed, so the train may carry main's new tip.
36
+ #
37
+ # @param branch [String] the fix's branch
38
+ # @param tip [String] main's commit after the landing
39
+ # @return [void]
40
+ def landed(branch, tip)
41
+ @marker.fixed!(tip: tip, branch: branch) if @marker.read && fix?(branch)
42
+ end
43
+
44
+ # @param boarded [String] the commit the train would push
45
+ # @return [void]
46
+ # @raise [ConsoleCapture::Failure] when a flag stands and the commit is not the fix's tip
47
+ def boarding!(boarded)
48
+ flag = @marker.read
49
+ return unless flag
50
+ return if flag["fix_tip"] && flag["fix_tip"] == boarded
51
+
52
+ raise ConsoleCapture::Failure, "#{refusal(flag)}; only the tip of a red-* row's fix is boarded"
53
+ end
54
+
55
+ private
56
+
57
+ def fix?(branch) = branch.to_s.delete_prefix(QA_LEAD).start_with?(RED_ROW)
58
+
59
+ def refusal(flag) = "main is red: flag #{flag["id"]}"
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require "fileutils"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # The standing red flag of one repository: a JSON file in its common git directory that exists
10
+ # while main is red. Landing and the push train read it, so every worktree of the
11
+ # repository sees the same flag and a spec's temporary repository never sees the owner's.
12
+ #
13
+ # The file holds the flag id, the main commit and CI run that raised it, the failed jobs, the
14
+ # red Work rows, every later red run, who checked in and what they took, and the tip of the
15
+ # last fix landed while the flag stood. Clearing removes the file.
16
+ class RedFlagMarker
17
+ # The marker's file name, in the common git directory.
18
+ FILE = "hecks-red-flag.json"
19
+
20
+ # @param git_dir [String] the repository's common git directory
21
+ # @param clock [#call] answers the current Time
22
+ def initialize(git_dir, clock: -> { Time.now })
23
+ @path = File.join(git_dir, FILE)
24
+ @clock = clock
25
+ end
26
+
27
+ # @return [Hash, nil] the standing flag, or nil when main is not flagged
28
+ def read
29
+ JSON.parse(File.read(@path)) if File.exist?(@path)
30
+ rescue JSON::ParserError
31
+ { "id" => "unreadable", "commit" => "unknown", "runs" => [], "checkins" => [] }
32
+ end
33
+
34
+ # Raises the flag, or records another red run on the standing one.
35
+ #
36
+ # @param commit [String] the main commit whose CI run was red
37
+ # @param run [Hash] `ci_run:`, `jobs:`, `rows:` of that run
38
+ # @return [Array(Symbol, Hash)] `:raised` or `:noted`, and the flag
39
+ def raise!(commit:, run:)
40
+ flag = read
41
+ return [:raised, write(fresh(commit, run))] unless flag
42
+
43
+ flag["runs"] << sighting(commit, run)
44
+ [:noted, write(flag)]
45
+ end
46
+
47
+ # @param session [String] who checks in
48
+ # @param taking [String] the Work row or cause they take
49
+ # @return [Hash, nil] the flag after the check-in, or nil when none stands
50
+ def check_in!(session:, taking:)
51
+ flag = read
52
+ return unless flag
53
+
54
+ flag["checkins"] << { "session" => session, "taking" => taking, "at" => stamp }
55
+ write(flag)
56
+ end
57
+
58
+ # Records the main tip a red-row fix landed at, so the train may board it.
59
+ #
60
+ # @param tip [String] the commit main points at after the fix landed
61
+ # @param branch [String] the fix's branch
62
+ # @return [Hash, nil] the flag, or nil when none stands
63
+ def fixed!(tip:, branch:)
64
+ flag = read
65
+ return unless flag
66
+
67
+ flag["fix_tip"] = tip
68
+ flag["fix_branch"] = branch
69
+ write(flag)
70
+ end
71
+
72
+ # Lowers the flag.
73
+ #
74
+ # @return [Hash, nil] the flag that was standing, or nil when none was
75
+ def clear!
76
+ flag = read
77
+ File.delete(@path) if flag
78
+ flag
79
+ end
80
+
81
+ private
82
+
83
+ def fresh(commit, run)
84
+ { "id" => "flag-#{commit.to_s[0, 10]}", "commit" => commit, "raised_at" => stamp, "runs" => [sighting(commit, run)],
85
+ "checkins" => [] }
86
+ end
87
+
88
+ def sighting(commit, run)
89
+ { "commit" => commit, "ci_run" => run[:ci_run].to_s, "jobs" => run[:jobs].to_s, "rows" => run[:rows].to_s, "at" => stamp }
90
+ end
91
+
92
+ def write(flag)
93
+ FileUtils.mkdir_p(File.dirname(@path))
94
+ File.write(@path, JSON.pretty_generate(flag))
95
+ flag
96
+ end
97
+
98
+ def stamp = @clock.call.utc.iso8601
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # Says a standing red flag in words: its id, the commit, how long main has been red, the failed
8
+ # jobs, and who has checked in to help and what they took. The pipeline board and the status
9
+ # query print the same sentence.
10
+ class RedFlagReport
11
+ # @param flag [Hash] the marker's content
12
+ # @param now [Time] the time the elapsed red time is measured to
13
+ def initialize(flag, now: Time.now)
14
+ @flag = flag
15
+ @now = now
16
+ end
17
+
18
+ # @return [Integer] seconds main has been red
19
+ def elapsed_seconds
20
+ [(@now - Time.iso8601(@flag.fetch("raised_at"))).to_i, 0].max
21
+ rescue ArgumentError, KeyError
22
+ 0
23
+ end
24
+
25
+ # @return [String] the whole sentence, one line per fact
26
+ def text
27
+ [headline, jobs_line, rows_line, *helper_lines].compact.join("\n")
28
+ end
29
+
30
+ # @return [String] the one-line form for a tile
31
+ def headline
32
+ "main is red: #{@flag["id"]} at #{@flag["commit"].to_s[0, 10]}, red for #{elapsed_seconds / 60} min, " \
33
+ "#{@flag["runs"].to_a.size} red run(s), #{@flag["checkins"].to_a.size} helping"
34
+ end
35
+
36
+ private
37
+
38
+ def jobs_line
39
+ jobs = seen("jobs")
40
+ "failing jobs: #{jobs.join("; ")}" unless jobs.empty?
41
+ end
42
+
43
+ def rows_line
44
+ rows = seen("rows")
45
+ "red rows: #{rows.join(", ")}" unless rows.empty?
46
+ end
47
+
48
+ # The distinct, non-empty values of one field across the red runs.
49
+ def seen(field)
50
+ values = @flag["runs"].to_a.map { |run| run[field].to_s }
51
+ values.reject(&:empty?).uniq
52
+ end
53
+
54
+ def helper_lines
55
+ @flag["checkins"].to_a.map { |entry| "helping: #{entry["session"]} takes #{entry["taking"]}" }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "console_capture"
5
+ require_relative "main_integration/command_runner"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # Reads the newest CI run of a push to main through `gh` and says whether main is red, green or
10
+ # still being judged.
11
+ class RedFlagSurvey
12
+ # The workflow whose run is read.
13
+ WORKFLOW = "CI"
14
+
15
+ # Job conclusions that are not a failure.
16
+ CLEAN = %w[success skipped neutral].freeze
17
+
18
+ # @param runner [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`
19
+ # @param checkout [String] a directory in the repository
20
+ def initialize(runner:, checkout:)
21
+ @runner = runner || MainIntegration::CommandRunner.new
22
+ @checkout = checkout
23
+ end
24
+
25
+ # @return [Hash] `verdict:` (`:red`, `:green` or `:running`), `commit:` and `detail:` in the
26
+ # words `RedFlagging` reads a run id and failed jobs from
27
+ # @raise [ConsoleCapture::Failure] when `gh` cannot be read
28
+ def latest
29
+ run = gh("run", "list", "--workflow", WORKFLOW, "--branch", "main", "--event", "push", "--limit", "1",
30
+ "--json", "databaseId,status,conclusion,headSha").first
31
+ return { verdict: :running, commit: "", detail: "no #{WORKFLOW} run of main yet" } unless run
32
+ return running(run) unless run["status"] == "completed"
33
+
34
+ judged(run)
35
+ end
36
+
37
+ private
38
+
39
+ def running(run) = { verdict: :running, commit: run["headSha"], detail: "run #{run["databaseId"]} is #{run["status"]}" }
40
+
41
+ def judged(run)
42
+ id = run["databaseId"]
43
+ return { verdict: :green, commit: run["headSha"], detail: "run #{id} passed" } if run["conclusion"] == "success"
44
+
45
+ said = "#{run["headSha"][0, 10]}: run #{id} #{run["conclusion"]}; failed jobs: #{failed(id)}"
46
+ { verdict: :red, commit: run["headSha"], detail: said }
47
+ end
48
+
49
+ def failed(id)
50
+ jobs = gh("run", "view", id.to_s, "--json", "jobs").fetch("jobs", [])
51
+ names = jobs.reject { |job| CLEAN.include?(job["conclusion"].to_s) }.map { |job| job["name"] }
52
+ names.empty? ? "none named" : names.join(", ")
53
+ end
54
+
55
+ def gh(*args)
56
+ ok, output = @runner.call(["gh", *args], chdir: @checkout)
57
+ raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} failed: #{output.to_s.strip}" unless ok
58
+
59
+ JSON.parse(output)
60
+ rescue JSON::ParserError
61
+ raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} answered no JSON"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ # Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
2
+ # and the implementation must load once, from `lib/`, not from the copy.
3
+ require "hecks/hecks/adapters/red_flagging"
4
+
5
+ Hecks.adapter "RedFlagging" do
6
+ port "Flagging"
7
+ end