hecks 3.0.3 → 3.0.4

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +6 -2
  3. data/lib/hecks/bluebook/assembly/contracts.rb +3 -1
  4. data/lib/hecks/bluebook/assembly/marks.rb +3 -0
  5. data/lib/hecks/bluebook/command.rb +7 -5
  6. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  7. data/lib/hecks/bluebook/dsl/command_builder.rb +42 -0
  8. data/lib/hecks/bluebook/expression/canonical_form.rb +12 -0
  9. data/lib/hecks/bluebook/expression/projection.json +21 -0
  10. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -0
  11. data/lib/hecks/cli/project_cli.rb +6 -2
  12. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -0
  13. data/lib/hecks/doors/cli_runner.rb +57 -12
  14. data/lib/hecks/doors/launcher_options.rb +23 -0
  15. data/lib/hecks/fuzzing/properties.rb +1 -1
  16. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +11 -3
  17. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +9 -7
  18. data/lib/hecks/gate/stages.yml +51 -0
  19. data/lib/hecks/grammar/expression.bluebook +6 -1
  20. data/lib/hecks/grammar/expression_operators.json +135 -0
  21. data/lib/hecks/hecks/adapters/codebase/gate.rb +37 -0
  22. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +24 -4
  23. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  24. data/lib/hecks/hecks/codebase.behaviors +12 -0
  25. data/lib/hecks/hecks/codebase.bluebook +194 -0
  26. data/lib/hecks/hecks/custodian.bluebook +2 -0
  27. data/lib/hecks/hecks/hecks.hecksagon +14 -0
  28. data/lib/hecks/hecks/hecks.world +3 -2
  29. data/lib/hecks/language/bluebook/command.bluebook +26 -0
  30. data/lib/hecks/language/bluebook/vocabulary.bluebook +29 -1
  31. data/lib/hecks/language/oidc.json +5 -0
  32. data/lib/hecks/ports/clock.rb +3 -1
  33. data/lib/hecks/projections/deploy/lambda.rb +3 -4
  34. data/lib/hecks/quality_control/quality_control.bluebook +6 -0
  35. data/lib/hecks/runtime/command_interpreter.rb +6 -4
  36. data/lib/hecks/runtime/entity_interpreter.rb +5 -2
  37. data/lib/hecks/runtime/interpreting.rb +29 -0
  38. data/lib/hecks/runtime/invocation.rb +5 -1
  39. data/lib/hecks/runtime/loader.rb +16 -13
  40. data/lib/hecks/runtime.rb +11 -0
  41. data/lib/hecks/three_zero/forms.yml +1 -1
  42. data/lib/hecks/tools/ci_gate_decision.rb +109 -0
  43. data/lib/hecks/tools/ci_gates.rb +124 -0
  44. data/lib/hecks/tools/gate.rb +117 -0
  45. data/lib/hecks/tools.rb +3 -0
  46. data/lib/hecks/version.rb +1 -1
  47. data/lib/hecks/vocabulary.rb +6 -1
  48. data/lib/hecks.rb +12 -2
  49. data/rust/codegen/src/naming.rs +13 -2
  50. data/rust/host/HECKS_RELEASE +1 -1
  51. data/rust/host/src/dispatch.rs +207 -1
  52. data/rust/host/src/main.rs +1 -0
  53. data/rust/host/src/needs.rs +243 -0
  54. data/rust/host/src/resend.rs +52 -10
  55. data/rust/host/src/server.rs +7 -3
  56. data/rust/parser/src/canonical.rs +137 -2
  57. data/rust/parser/src/emit.rs +25 -19
  58. data/rust/parser/src/ir.rs +2 -0
  59. data/rust/parser/src/keywords.rs +2 -0
  60. data/rust/parser/src/main.rs +1 -0
  61. data/rust/parser/src/parse/command.rs +44 -0
  62. data/rust/project/naming.rb +4 -2
  63. data/rust/src/kernel/cli.rs +3 -0
  64. data/rust/src/kernel/mod.rs +1 -0
  65. data/rust/src/kernel/needs.rs +194 -0
  66. data/rust/src/kernel/orchestrate.rs +84 -0
  67. data/rust/src/kernel/repository.rs +43 -2
  68. metadata +9 -2
@@ -211,6 +211,11 @@ Hecks.bluebook "Expression" do
211
211
 
212
212
  member strategy: "collapse_whitespace", source_token: "", replacement: "", boundary: "none", position: 1
213
213
  member strategy: "replace", source_token: ".length", replacement: ".size", boundary: "word", position: 2
214
+ # A duration written as a call on a whole number is that many seconds: `days(730)` is
215
+ # `63072000`. `source_token` names the call and `replacement` the seconds one unit is worth.
216
+ member strategy: "scale_call", source_token: "days", replacement: "86400", boundary: "word", position: 3
217
+ member strategy: "scale_call", source_token: "hours", replacement: "3600", boundary: "word", position: 4
218
+ member strategy: "scale_call", source_token: "minutes", replacement: "60", boundary: "word", position: 5
214
219
  end
215
220
 
216
221
  value_object "Reading" do
@@ -233,7 +238,7 @@ Hecks.bluebook "Expression" do
233
238
  attribute :position, Position, optional: true
234
239
 
235
240
  given("a rule must name a linked strategy") do
236
- strategy.value == "collapse_whitespace" || strategy.value == "replace"
241
+ strategy.value == "collapse_whitespace" || strategy.value == "replace" || strategy.value == "scale_call"
237
242
  end
238
243
 
239
244
  given("a rule must state its boundary") { boundary.value == "none" || boundary.value == "word" }
@@ -1644,6 +1644,141 @@
1644
1644
  }
1645
1645
  }
1646
1646
  },
1647
+ {
1648
+ "verb": "Expression::Normalisation.Propose",
1649
+ "args": {
1650
+ "name": {
1651
+ "value": "days_to_seconds"
1652
+ },
1653
+ "strategy": {
1654
+ "value": "scale_call"
1655
+ },
1656
+ "source_token": {
1657
+ "value": "days"
1658
+ },
1659
+ "replacement": {
1660
+ "value": "86400"
1661
+ },
1662
+ "boundary": {
1663
+ "value": "word"
1664
+ },
1665
+ "position": {
1666
+ "value": 3
1667
+ }
1668
+ }
1669
+ },
1670
+ {
1671
+ "verb": "Expression::Normalisation.Read",
1672
+ "args": {
1673
+ "name": {
1674
+ "value": "days_to_seconds"
1675
+ },
1676
+ "target": {
1677
+ "value": "ruby"
1678
+ },
1679
+ "form": {
1680
+ "value": "text.gsub(/(?<![[:alnum:]_.])days\\(\\s*(\\d+)\\s*\\)/) { $1.to_i * 86400 }"
1681
+ }
1682
+ }
1683
+ },
1684
+ {
1685
+ "verb": "Expression::Normalisation.Admit",
1686
+ "args": {
1687
+ "name": {
1688
+ "value": "days_to_seconds"
1689
+ }
1690
+ }
1691
+ },
1692
+ {
1693
+ "verb": "Expression::Normalisation.Propose",
1694
+ "args": {
1695
+ "name": {
1696
+ "value": "hours_to_seconds"
1697
+ },
1698
+ "strategy": {
1699
+ "value": "scale_call"
1700
+ },
1701
+ "source_token": {
1702
+ "value": "hours"
1703
+ },
1704
+ "replacement": {
1705
+ "value": "3600"
1706
+ },
1707
+ "boundary": {
1708
+ "value": "word"
1709
+ },
1710
+ "position": {
1711
+ "value": 4
1712
+ }
1713
+ }
1714
+ },
1715
+ {
1716
+ "verb": "Expression::Normalisation.Read",
1717
+ "args": {
1718
+ "name": {
1719
+ "value": "hours_to_seconds"
1720
+ },
1721
+ "target": {
1722
+ "value": "ruby"
1723
+ },
1724
+ "form": {
1725
+ "value": "text.gsub(/(?<![[:alnum:]_.])hours\\(\\s*(\\d+)\\s*\\)/) { $1.to_i * 3600 }"
1726
+ }
1727
+ }
1728
+ },
1729
+ {
1730
+ "verb": "Expression::Normalisation.Admit",
1731
+ "args": {
1732
+ "name": {
1733
+ "value": "hours_to_seconds"
1734
+ }
1735
+ }
1736
+ },
1737
+ {
1738
+ "verb": "Expression::Normalisation.Propose",
1739
+ "args": {
1740
+ "name": {
1741
+ "value": "minutes_to_seconds"
1742
+ },
1743
+ "strategy": {
1744
+ "value": "scale_call"
1745
+ },
1746
+ "source_token": {
1747
+ "value": "minutes"
1748
+ },
1749
+ "replacement": {
1750
+ "value": "60"
1751
+ },
1752
+ "boundary": {
1753
+ "value": "word"
1754
+ },
1755
+ "position": {
1756
+ "value": 5
1757
+ }
1758
+ }
1759
+ },
1760
+ {
1761
+ "verb": "Expression::Normalisation.Read",
1762
+ "args": {
1763
+ "name": {
1764
+ "value": "minutes_to_seconds"
1765
+ },
1766
+ "target": {
1767
+ "value": "ruby"
1768
+ },
1769
+ "form": {
1770
+ "value": "text.gsub(/(?<![[:alnum:]_.])minutes\\(\\s*(\\d+)\\s*\\)/) { $1.to_i * 60 }"
1771
+ }
1772
+ }
1773
+ },
1774
+ {
1775
+ "verb": "Expression::Normalisation.Admit",
1776
+ "args": {
1777
+ "name": {
1778
+ "value": "minutes_to_seconds"
1779
+ }
1780
+ }
1781
+ },
1647
1782
  {
1648
1783
  "verb": "Expression::Operator.Propose",
1649
1784
  "args": {
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tree"
4
+ require_relative "ruby_child"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ module Codebase
9
+ # What Codebase's `GateRun` asks of the working tree: running a stage's checks.
10
+ #
11
+ # It runs `Hecks::Tools::Gate` in this process. The tool starts the stage's checks together,
12
+ # each as its own process, and answers 1 when any is red; the refusal is what the red checks
13
+ # printed, so `--wait` and the journal both say which check failed.
14
+ module Gate
15
+ # Every operation this family carries out.
16
+ OPERATIONS = %w[gate].freeze
17
+
18
+ module_function
19
+
20
+ # Runs the stage.
21
+ #
22
+ # @param _operation [String] `gate`
23
+ # @param held [Hash] the `GateRun` record's fields: `stage`, and `only` when given
24
+ # @param tree [Tree] the working tree, already known to be a hecks checkout
25
+ # @param shell [#capture, nil] unused: the tool runs in this process
26
+ # @return [String] which checks passed
27
+ # @raise [ConsoleCapture::Failure] with what the red checks printed
28
+ def call(_operation, held, tree, shell: nil)
29
+ plain = ->(name) { held[name].is_a?(Hash) ? held[name][:value] : held[name] }
30
+ only = plain.call(:only).to_s
31
+ argv = [plain.call(:stage).to_s, *("only=#{only}" unless only.empty?)]
32
+ RubyChild.new(tree).answer("gate", *argv).strip
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -7,7 +7,7 @@ module Hecks
7
7
  module Adapters
8
8
  module Codebase
9
9
  # What Codebase's `RegenerationRun` asks of the working tree: regenerating every corpus
10
- # domain's committed Rust output.
10
+ # domain's committed Rust output, and projecting the CI path gates into the workflows.
11
11
  #
12
12
  # It runs `Hecks::Tools::RegenerationRun` in this process: the run forks once for each
13
13
  # domain, in a fixed order, because the domains share the files they stamp. With `check` it
@@ -16,7 +16,7 @@ module Hecks
16
16
  # files reports drift and writes only when it is confirmed.
17
17
  module Regeneration
18
18
  # Every operation this family carries out.
19
- OPERATIONS = %w[regenerate_corpus].freeze
19
+ OPERATIONS = %w[regenerate_corpus project_ci_gates decide_ci_gate].freeze
20
20
 
21
21
  # How many lines of a script's output a refusal keeps.
22
22
  KEPT_LINES = 60
@@ -25,23 +25,43 @@ module Hecks
25
25
 
26
26
  # Carries out the regeneration.
27
27
  #
28
- # @param _operation [String] `regenerate_corpus`
28
+ # @param operation [String] `regenerate_corpus` or `project_ci_gates`
29
29
  # @param held [Hash] the `RegenerationRun` record's fields: `check`, `confirm`
30
30
  # @param tree [Tree] the working tree, already known to be a hecks checkout
31
31
  # @param shell [#capture, nil] unused: the tool runs in this process
32
32
  # @return [String] how many domains were checked or regenerated
33
33
  # @raise [ConsoleCapture::Failure] with the difference, when a check finds drift, or with
34
34
  # what the script printed when it ends badly
35
- def call(_operation, held, tree, shell: nil)
35
+ def call(operation, held, tree, shell: nil)
36
36
  flag = ->(name) { (held[name].is_a?(Hash) ? held[name][:value] : held[name]) == true }
37
37
  check = flag.call(:check) || !flag.call(:confirm)
38
38
  child = RubyChild.new(tree)
39
+ return project_ci_gates(child, check) if operation == "project_ci_gates"
40
+ return decide_ci_gate(child, held) if operation == "decide_ci_gate"
41
+
39
42
  result = child.capture("regen_codegen_domains", *("--check" if check))
40
43
  raise ConsoleCapture::Failure, refusal(result) unless result.ok?
41
44
 
42
45
  summary(result.out, check)
43
46
  end
44
47
 
48
+ # @param child [RubyChild] the checkout's tool runner
49
+ # @param check [Boolean] whether to only compare
50
+ # @return [String] what the tool printed
51
+ # @raise [ConsoleCapture::Failure] with the stale workflows, when a check finds drift
52
+ def project_ci_gates(child, check)
53
+ child.answer("project_ci_gates", *("--check" if check))
54
+ end
55
+
56
+ # @param child [RubyChild] the checkout's tool runner
57
+ # @param held [Hash] the record's fields: `gate`, a `CiGate` row's name
58
+ # @return [String] `touched=true` or `touched=false`
59
+ # @raise [ConsoleCapture::Failure] when no row has that name
60
+ def decide_ci_gate(child, held)
61
+ gate = held[:gate].is_a?(Hash) ? held[:gate][:value] : held[:gate]
62
+ child.answer("decide_ci_gate", "gate=#{gate}")
63
+ end
64
+
45
65
  # @param output [String] what the script printed
46
66
  # @param check [Boolean] whether it only compared
47
67
  # @return [String] the verdict, with how many domains it covered
@@ -5,6 +5,7 @@ require_relative "language"
5
5
  require_relative "kernel_tables"
6
6
  require_relative "conformance"
7
7
  require_relative "regeneration"
8
+ require_relative "gate"
8
9
  require_relative "style"
9
10
  require_relative "codemods"
10
11
  require_relative "test_suite"
@@ -178,7 +179,8 @@ module Hecks
178
179
 
179
180
  # The task families, each carrying out the operations it lists.
180
181
  FAMILIES = [Codebase::Language, Codebase::KernelTables, Codebase::Conformance, Codebase::Regeneration,
181
- Codebase::Style, Codebase::Codemods, Codebase::TestSuite, Codebase::CorpusTasks, Codebase::Publishing].freeze
182
+ Codebase::Gate, Codebase::Style, Codebase::Codemods, Codebase::TestSuite,
183
+ Codebase::CorpusTasks, Codebase::Publishing].freeze
182
184
  private_constant :FAMILIES
183
185
 
184
186
  def tree = Codebase::Tree.new
@@ -79,6 +79,18 @@ Hecks.behaviors "Codebase" do
79
79
  expect refused: "must match"
80
80
  end
81
81
 
82
+ test "ProjectCiGates refuses a run that is not named" do
83
+ tests "ProjectCiGates", on: "RegenerationRun"
84
+ input run: { value: "" }
85
+ expect refused: "must match"
86
+ end
87
+
88
+ test "DecideCiGate refuses a run that is not named" do
89
+ tests "DecideCiGate", on: "RegenerationRun"
90
+ input run: { value: "" }, gate: { value: "runtime_changed" }
91
+ expect refused: "must match"
92
+ end
93
+
82
94
  test "CheckComments keeps a file with no violations as a completed check" do
83
95
  tests "CheckComments", on: "StyleRun"
84
96
  input paths: { value: "lib/hecks/canonical_json.rb" }, run: { value: "clean" }
@@ -960,6 +960,7 @@ Hecks.bluebook "Hecks" do
960
960
  attribute :operation, Operation, optional: true
961
961
  attribute :confirm, Switch, optional: true
962
962
  attribute :check, Switch, optional: true
963
+ attribute :gate, GateName, optional: true
963
964
  attribute :checkout, Switch, optional: true
964
965
  attribute :report, Report, optional: true
965
966
  attribute :refusal, Report, optional: true
@@ -983,6 +984,11 @@ Hecks.bluebook "Hecks" do
983
984
  attribute :value, String
984
985
  end
985
986
 
987
+ # A `CiGate` row of the Vocabulary chapter, by name.
988
+ value_object "GateName" do
989
+ attribute :value, String, pattern: '[^ \t\n\r]'
990
+ end
991
+
986
992
  lifecycle :status, default: "requested" do
987
993
  transition "Accept" => "accepted", from: "requested"
988
994
  transition "Complete" => "completed", from: "accepted"
@@ -1005,6 +1011,36 @@ Hecks.bluebook "Hecks" do
1005
1011
  emits RegenerateCorpusRequested
1006
1012
  end
1007
1013
 
1014
+ command "ProjectCiGates" do
1015
+ role "Maintainer"
1016
+ goal "Project the CiGate rows into the workflows' marked detector jobs, or with check only compare them"
1017
+
1018
+ attribute :check, Switch, optional: true
1019
+ attribute :run, RunKey
1020
+ attribute :confirm, Switch, optional: true
1021
+
1022
+ sets :check
1023
+ sets :run
1024
+ sets :confirm
1025
+ sets :operation, to: "project_ci_gates"
1026
+
1027
+ emits ProjectCiGatesRequested
1028
+ end
1029
+
1030
+ command "DecideCiGate" do
1031
+ role "Maintainer"
1032
+ goal "Say whether this run's change touches what a CiGate row guards, as the gated job's detector"
1033
+
1034
+ attribute :gate, GateName
1035
+ attribute :run, RunKey
1036
+
1037
+ sets :gate
1038
+ sets :run
1039
+ sets :operation, to: "decide_ci_gate"
1040
+
1041
+ emits DecideCiGateRequested
1042
+ end
1043
+
1008
1044
  # What the tree reported about itself, held against every request: nothing here runs
1009
1045
  # outside a hecks checkout (hecks.gemspec beside lib/).
1010
1046
  command "Accept" do
@@ -1064,6 +1100,16 @@ Hecks.bluebook "Hecks" do
1064
1100
  trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
1065
1101
  end
1066
1102
 
1103
+ policy "ExamineWhenProjectCiGatesRequested" do
1104
+ on "RegenerationRun.ProjectCiGatesRequested"
1105
+ trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
1106
+ end
1107
+
1108
+ policy "ExamineWhenDecideCiGateRequested" do
1109
+ on "RegenerationRun.DecideCiGateRequested"
1110
+ trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
1111
+ end
1112
+
1067
1113
  policy "AcceptRegenerationWhenExamined" do
1068
1114
  on "RegenerationRun.RegenerationExamined"
1069
1115
  trigger RegenerationRun::Accept, with: { checkout: :checkout }
@@ -1090,6 +1136,154 @@ Hecks.bluebook "Hecks" do
1090
1136
  trigger RegenerationRun::Fault, with: { refusal: :refusal }
1091
1137
  end
1092
1138
 
1139
+ # Gate: the checks a stage holds a change against before it is let through. The checks are data
1140
+ # (lib/hecks/gate/stages.yml), run together, and a red one faults the run, so `--wait` fails the
1141
+ # push or the build that asked, and the journal keeps when the gate went red.
1142
+ aggregate "GateRun" do
1143
+ description "One run of a stage's checks: every check of the stage, or the ones named."
1144
+
1145
+ attribute :run, RunKey
1146
+ attribute :operation, Operation, optional: true
1147
+ attribute :stage, Stage
1148
+ attribute :only, CheckList, optional: true
1149
+ attribute :checkout, Switch, optional: true
1150
+ attribute :report, Report, optional: true
1151
+ attribute :refusal, Report, optional: true
1152
+ identified_by :run
1153
+
1154
+ value_object "RunKey" do
1155
+ attribute :value, String, pattern: '[^ \t\n\r]'
1156
+
1157
+ invariant("a run is named") { !value.to_s.empty? }
1158
+ end
1159
+
1160
+ value_object "Operation" do
1161
+ attribute :value, String, pattern: '[^ \t\n\r]'
1162
+ end
1163
+
1164
+ value_object "Stage" do
1165
+ attribute :value, String, pattern: '^[a-z][a-z0-9_]*$'
1166
+
1167
+ invariant("a stage is named") { !value.to_s.empty? }
1168
+ end
1169
+
1170
+ value_object "CheckList" do
1171
+ attribute :value, String, pattern: '^[a-z][a-z0-9_]*(,[a-z][a-z0-9_]*)*$'
1172
+ end
1173
+
1174
+ value_object "Switch" do
1175
+ attribute :value, Boolean
1176
+ end
1177
+
1178
+ value_object "Report" do
1179
+ attribute :value, String
1180
+ end
1181
+
1182
+ lifecycle :status, default: "requested" do
1183
+ transition "Accept" => "accepted", from: "requested"
1184
+ transition "Complete" => "completed", from: "accepted"
1185
+ transition "Fault" => "faulted", from: %w[requested accepted]
1186
+ end
1187
+
1188
+ command "Gate" do
1189
+ role "Maintainer"
1190
+ goal "Run every check of a stage, or the ones named, and keep whether the gate is green"
1191
+
1192
+ attribute :stage, Stage
1193
+ attribute :only, CheckList, optional: true
1194
+ attribute :run, RunKey
1195
+
1196
+ sets :stage
1197
+ sets :only
1198
+ sets :run
1199
+ sets :operation, to: "gate"
1200
+
1201
+ emits GateRequested
1202
+ end
1203
+
1204
+ command "Accept" do
1205
+ role "System"
1206
+ goal "Let a request through only when the working tree is a hecks checkout"
1207
+
1208
+ reference_to GateRun
1209
+ attribute :checkout, Switch
1210
+
1211
+ given("needs a hecks checkout: hecks.gemspec stands beside lib/") { checkout.value }
1212
+
1213
+ sets :checkout
1214
+
1215
+ emits GateAccepted
1216
+ end
1217
+
1218
+ command "Complete" do
1219
+ role "System"
1220
+ goal "Record that every check passed, and which"
1221
+
1222
+ reference_to GateRun
1223
+ attribute :report, Report
1224
+
1225
+ sets :report
1226
+
1227
+ emits GateCompleted
1228
+ end
1229
+
1230
+ command "Fault" do
1231
+ role "System"
1232
+ goal "Record that a check was red or the run could not be made, and why"
1233
+
1234
+ reference_to GateRun
1235
+ attribute :refusal, Report
1236
+
1237
+ sets :refusal
1238
+
1239
+ emits GateFaulted
1240
+ end
1241
+
1242
+ query "GateOutcome" do
1243
+ description "How one run ended: its status, and what it reported or why it failed."
1244
+ attribute :run, RunKey
1245
+ where(run: :run)
1246
+ order_by :run
1247
+ end
1248
+
1249
+ query "GateFaulted" do
1250
+ description "Every run that went red or could not be made."
1251
+ where(status: "faulted")
1252
+ order_by :run
1253
+ end
1254
+ end
1255
+
1256
+ policy "ExamineWhenGateRequested" do
1257
+ on "GateRun.GateRequested"
1258
+ trigger GateRun::SourceTree::Examine, with: { run: :run }
1259
+ end
1260
+
1261
+ policy "AcceptGateWhenExamined" do
1262
+ on "GateRun.GateExamined"
1263
+ trigger GateRun::Accept, with: { checkout: :checkout }
1264
+ end
1265
+
1266
+ policy "FaultGateWhenNotAccepted" do
1267
+ on "GateRun.GateExamined"
1268
+ where { !checkout.value }
1269
+ trigger GateRun::Fault, with: { refusal: "needs a hecks checkout: hecks.gemspec stands beside lib/" }
1270
+ end
1271
+
1272
+ policy "PerformGateWhenAccepted" do
1273
+ on "GateRun.GateAccepted"
1274
+ trigger GateRun::SourceTree::Perform, with: { run: :run }
1275
+ end
1276
+
1277
+ policy "RecordGateOutcome" do
1278
+ on "GateRun.GateAnswered"
1279
+ trigger GateRun::Complete, with: { report: :report }
1280
+ end
1281
+
1282
+ policy "RecordGateFault" do
1283
+ on "GateRun.GateRefused"
1284
+ trigger GateRun::Fault, with: { refusal: :refusal }
1285
+ end
1286
+
1093
1287
  # Style: the comment style guides (Ruby and Rust) and the canonical form of a JSON document. A
1094
1288
  # check finds every violation; a fix rewrites files, so it lists what it would rewrite unless
1095
1289
  # it is confirmed. The baseline of tolerated long blocks is written only when confirmed.
@@ -460,6 +460,8 @@ Hecks.bluebook "Hecks" do
460
460
 
461
461
  # A launcher answers once, so a follow is a bounded poll: it answers the entries past `since`
462
462
  # and a `cursor` to pass as the next `since`, waiting up to `wait` seconds for the first one.
463
+ # `hecks follow <domain> --stream` asks again from each cursor and prints every entry as it
464
+ # arrives, until interrupted (the world's `launcher` setting lists the question under `streams`).
463
465
  query "Follow" do
464
466
  description "A domain's persisted event log past a cursor, for tailing it call by call."
465
467
  attribute :domain, Subject
@@ -229,6 +229,20 @@ Hecks.hecksagon "Hecks" do
229
229
  end
230
230
  end
231
231
 
232
+ Hecks::GateRun.port "SourceTree" do
233
+ asks "Examine", to: GateRun do
234
+ attribute :run, RunKey
235
+ answers "GateExamined"
236
+ refuses "GateRefused"
237
+ end
238
+
239
+ asks "Perform", to: GateRun do
240
+ attribute :run, RunKey
241
+ answers "GateAnswered"
242
+ refuses "GateRefused"
243
+ end
244
+ end
245
+
232
246
  Hecks::StyleRun.port "SourceTree" do
233
247
  asks "Examine", to: StyleRun do
234
248
  attribute :run, RunKey
@@ -26,8 +26,8 @@
26
26
  # `failure_states` are the lifecycle states `--wait` reports as exit 1 (every failure state the
27
27
  # chapter's lifecycles declare), `names` gives a command the launcher name clients type,
28
28
  # `executable` is where `hecks project_cli` writes this chapter's launcher (the gem's
29
- # `exe/hecks`), and `legacy` names the verbs that launcher hands to `Hecks::CLI` in the
30
- # positional forms ADR 0066 shipped.
29
+ # `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), and `legacy` names
30
+ # the verbs that launcher hands to `Hecks::CLI` in the positional forms ADR 0066 shipped.
31
31
  Hecks.world "Hecks" do
32
32
  realm "Hecks"
33
33
  default_adapter "PostgresEra"
@@ -36,6 +36,7 @@ Hecks.world "Hecks" do
36
36
  failure_states: %w[flagged failed drifted unreachable refused faulted halted stopped
37
37
  abandoned red needs_fix],
38
38
  names: { "mcp" => "serve_mcp", "console" => "open_console" },
39
+ streams: %w[Follow],
39
40
  executable: "exe/hecks",
40
41
  legacy: %w[run docs narrate ir stores model_check smoke_test
41
42
  project_diagrams project_cli mcp]
@@ -21,6 +21,8 @@ Hecks.bluebook "Bluebook" do
21
21
  attribute :givens, list_of(Rule)
22
22
  # Postconditions: same Rule shape as givens, checked after the mutations.
23
23
  attribute :ensures, list_of(Rule)
24
+ # Outside facts the runtime supplies before any given runs (ADR 0081): `needs :now`.
25
+ attribute :needs, list_of(Need)
24
26
  attribute :mutations, list_of(Change)
25
27
  attribute :provenance, CommandText
26
28
  # Lifecycle guard as a literal (`from: "open"` or `from: ["open", "frozen"]`); ADR 0025.
@@ -48,6 +50,10 @@ Hecks.bluebook "Bluebook" do
48
50
  attribute :name, String
49
51
  end
50
52
 
53
+ value_object "Need" do
54
+ attribute :fact, String
55
+ end
56
+
51
57
  value_object "Argument" do
52
58
  attribute :name, String
53
59
  attribute :type, String
@@ -254,6 +260,23 @@ Hecks.bluebook "Bluebook" do
254
260
  emits AnnouncementNamed
255
261
  end
256
262
 
263
+ command "Need" do
264
+ role "Language"
265
+ goal "Name an outside fact the runtime supplies before any given runs"
266
+
267
+ attribute :fact, FactName
268
+
269
+ given("a fact is named") { !fact.value.to_s.empty? }
270
+
271
+ sets :needs, append: { fact: :fact }
272
+
273
+ emits NeedNamed
274
+ end
275
+
276
+ value_object "FactName" do
277
+ attribute :value, String
278
+ end
279
+
257
280
  # Keyword rows for this chapter; SyntaxBoot discovers aggregate-local seed tables.
258
281
  value_object "KeywordSeed" do
259
282
  attribute :word, String
@@ -283,6 +306,8 @@ Hecks.bluebook "Bluebook" do
283
306
  member word: "state", context: "Command", body: "none", inner: "", opens: "", fills: "mutations"
284
307
  member word: "attribute", context: "Command", body: "none", inner: "", opens: "", fills: "attributes", calls: "attribute_impl"
285
308
  member word: "ensures", context: "Command", body: "source", inner: "", opens: "", fills: "ensures"
309
+ # An outside fact the runtime supplies before any given runs (ADR 0081; CommandBuilder#needs_impl).
310
+ member word: "needs", context: "Command", body: "none", inner: "", opens: "", fills: "needs", calls: "needs_impl"
286
311
  # Amends a specific past event. Stored as a mutation so it round-trips with the other
287
312
  # mutation words (CommandBuilder#corrects_impl).
288
313
  member word: "corrects", context: "Command", body: "none", inner: "", opens: "", fills: "mutations", calls: "corrects_impl"
@@ -314,6 +339,7 @@ Hecks.bluebook "Bluebook" do
314
339
  member keyword: "reference_to", context: "Command", at: "", named: "optional", kind: "flag", required: "false", fills: "optional"
315
340
  member keyword: "given", context: "Command", at: "1", named: "", kind: "text", required: "true", fills: "description"
316
341
  member keyword: "ensures", context: "Command", at: "1", named: "", kind: "text", required: "true", fills: "description"
342
+ member keyword: "needs", context: "Command", at: "1", named: "", kind: "symbol", required: "true", fills: "fact"
317
343
  member keyword: "sets", context: "Command", at: "1", named: "", kind: "symbol", required: "true", fills: "target"
318
344
  member keyword: "sets", context: "Command", at: "", named: "to", kind: "literal", required: "false", fills: "source", selects: "op=set"
319
345
  member keyword: "sets", context: "Command", at: "", named: "append", kind: "literal", required: "false", fills: "source", selects: "op=append"