hecks 3.0.3 → 3.0.5

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +12 -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/console.rb +24 -8
  12. data/lib/hecks/cli/project_cli.rb +42 -9
  13. data/lib/hecks/cli/seed_semantics_corpus.rb +35 -6
  14. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -0
  15. data/lib/hecks/doors/cli_runner.rb +57 -12
  16. data/lib/hecks/doors/launcher_options.rb +41 -0
  17. data/lib/hecks/fuzzing/properties.rb +1 -1
  18. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +11 -3
  19. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +9 -7
  20. data/lib/hecks/gate/stages.yml +55 -0
  21. data/lib/hecks/grammar/expression.bluebook +6 -1
  22. data/lib/hecks/grammar/expression_operators.json +135 -0
  23. data/lib/hecks/hecks/adapters/codebase/gate.rb +37 -0
  24. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +24 -4
  25. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  26. data/lib/hecks/hecks/codebase.behaviors +12 -0
  27. data/lib/hecks/hecks/codebase.bluebook +194 -0
  28. data/lib/hecks/hecks/custodian.bluebook +2 -0
  29. data/lib/hecks/hecks/hecks.hecksagon +14 -0
  30. data/lib/hecks/hecks/hecks.world +5 -1
  31. data/lib/hecks/language/bluebook/command.bluebook +26 -0
  32. data/lib/hecks/language/bluebook/vocabulary.bluebook +29 -1
  33. data/lib/hecks/language/oidc.json +5 -0
  34. data/lib/hecks/ports/clock.rb +3 -1
  35. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +2 -1
  36. data/lib/hecks/projections/deploy/lambda.rb +3 -4
  37. data/lib/hecks/projector/cli_projector.rb +63 -4
  38. data/lib/hecks/quality_control/quality_control.bluebook +6 -0
  39. data/lib/hecks/runtime/command_interpreter.rb +8 -4
  40. data/lib/hecks/runtime/entity_interpreter.rb +5 -2
  41. data/lib/hecks/runtime/interpreting.rb +50 -0
  42. data/lib/hecks/runtime/invocation.rb +7 -1
  43. data/lib/hecks/runtime/loader.rb +16 -13
  44. data/lib/hecks/runtime.rb +11 -0
  45. data/lib/hecks/three_zero/forms.yml +1 -1
  46. data/lib/hecks/tools/ci_gate_decision.rb +109 -0
  47. data/lib/hecks/tools/ci_gates.rb +124 -0
  48. data/lib/hecks/tools/gate.rb +117 -0
  49. data/lib/hecks/tools.rb +3 -0
  50. data/lib/hecks/version.rb +1 -1
  51. data/lib/hecks/vocabulary.rb +6 -1
  52. data/lib/hecks.rb +12 -2
  53. data/rust/codegen/src/naming.rs +13 -2
  54. data/rust/host/HECKS_RELEASE +1 -1
  55. data/rust/host/src/dispatch.rs +207 -1
  56. data/rust/host/src/main.rs +1 -0
  57. data/rust/host/src/needs.rs +243 -0
  58. data/rust/host/src/resend.rs +52 -10
  59. data/rust/host/src/server.rs +7 -3
  60. data/rust/host/src/web/newsletter/template.rs +69 -0
  61. data/rust/host/src/web/newsletter/tests.rs +43 -0
  62. data/rust/host/src/web/newsletter.rs +6 -1
  63. data/rust/host/src/web/newsletter_send.rs +1 -1
  64. data/rust/parser/src/canonical.rs +137 -2
  65. data/rust/parser/src/emit.rs +25 -19
  66. data/rust/parser/src/ir.rs +2 -0
  67. data/rust/parser/src/keywords.rs +2 -0
  68. data/rust/parser/src/main.rs +1 -0
  69. data/rust/parser/src/parse/command.rs +44 -0
  70. data/rust/project/naming.rb +4 -2
  71. data/rust/src/kernel/cli.rs +3 -0
  72. data/rust/src/kernel/mod.rs +1 -0
  73. data/rust/src/kernel/needs.rs +194 -0
  74. data/rust/src/kernel/orchestrate.rs +84 -0
  75. data/rust/src/kernel/repository.rs +43 -2
  76. metadata +10 -2
@@ -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,7 +26,9 @@
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
29
+ # `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), `memory_verbs` names
30
+ # the verbs that launcher runs on the Memory environment unless `HECKS_ENVIRONMENT` is set (so
31
+ # `hecks console` needs no database), and `legacy` names the verbs it hands to `Hecks::CLI` in the
30
32
  # positional forms ADR 0066 shipped.
31
33
  Hecks.world "Hecks" do
32
34
  realm "Hecks"
@@ -36,7 +38,9 @@ Hecks.world "Hecks" do
36
38
  failure_states: %w[flagged failed drifted unreachable refused faulted halted stopped
37
39
  abandoned red needs_fix],
38
40
  names: { "mcp" => "serve_mcp", "console" => "open_console" },
41
+ streams: %w[Follow],
39
42
  executable: "exe/hecks",
43
+ memory_verbs: %w[console],
40
44
  legacy: %w[run docs narrate ir stores model_check smoke_test
41
45
  project_diagrams project_cli mcp]
42
46
  end
@@ -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"
@@ -73,7 +73,7 @@ Hecks.bluebook "Bluebook" do
73
73
 
74
74
  # Expression::CanonicalForm::STRATEGIES
75
75
  value_object "NormalisationStrategy" do
76
- attribute :name, String, one_of: ["collapse_whitespace", "replace"]
76
+ attribute :name, String, one_of: ["collapse_whitespace", "replace", "scale_call"]
77
77
  end
78
78
 
79
79
  # Ops Runtime::CommandInterpreter applies. `sign` is the arithmetic delta
@@ -602,6 +602,34 @@ Hecks.bluebook "Bluebook" do
602
602
  member name: "textarea", pattern: '\b(text|body|note|notes|description|message|comment)\b', resolves_to: "kind"
603
603
  end
604
604
 
605
+ # The path gates the workflows run before an expensive job: one detector job
606
+ # per row, answering `touched` from the diff of the change against its base.
607
+ # hecks project_ci_gates writes each row into the marked region of its
608
+ # `workflow` (.github/workflows/<workflow>), and the job it writes runs
609
+ # `hecks decide_ci_gate gate=<name>`, which makes the decision from the row.
610
+ #
611
+ # `mode` is how `pattern` (an extended regex over changed paths) decides:
612
+ # `touches` runs the job when any changed path matches, `skips_unless` runs it
613
+ # when any changed path does NOT match (the pattern is the safe-to-skip list).
614
+ # `push` is what a push event does: `skip` leaves the gate out of push runs,
615
+ # `before_sha` diffs against the push's `before` commit.
616
+ value_object "CiGate" do
617
+ attribute :name, String
618
+ attribute :workflow, String
619
+ attribute :mode, String
620
+ attribute :pattern, String
621
+ attribute :push, String
622
+ attribute :label, String
623
+
624
+ member name: "runtime_changed", workflow: "ci.yml", mode: "touches",
625
+ pattern: '^lib/hecks/runtime/', push: "skip",
626
+ label: "does this change touch lib/hecks/runtime/**?"
627
+ member name: "postgres_io_relevant_changed", workflow: "ci-postgres-io-parallel.yml", mode: "skips_unless",
628
+ pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests|project)/|rust/project\.rb$|rust/project_rust_pipeline\.rb$|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
629
+ push: "before_sha",
630
+ label: "does this change touch anything rspec_postgres_io_parallel covers?"
631
+ end
632
+
605
633
  # Rust keywords (strict + reserved) a generated identifier must not
606
634
  # collide with. RustProjection::Projector::RUST_KEYWORDS reads this;
607
635
  # hecks project_reserved_names projects it into
@@ -111,6 +111,11 @@
111
111
  "verb": "Bluebook::Command.Ensure",
112
112
  "role": "Language"
113
113
  },
114
+ {
115
+ "scope": "bluebook:command.need",
116
+ "verb": "Bluebook::Command.Need",
117
+ "role": "Language"
118
+ },
114
119
  {
115
120
  "scope": "bluebook:command.reference",
116
121
  "verb": "Bluebook::Command.Reference",
@@ -3,7 +3,9 @@ require_relative "../runtime/registry"
3
3
  module Hecks
4
4
  module Ports
5
5
  # What time it is, as one registry-wide adapter answers it.
6
- # Predicates cannot read the clock (replays must be deterministic), so the door fills `now`.
6
+ # Predicates cannot read the clock (replays must be deterministic), so a command that declares
7
+ # `needs :now` has the runtime read it once, before any given runs, into the command's own
8
+ # `now` argument (ADR 0081). The recorded argument is what a replay sees.
7
9
  module Clock
8
10
  NAME = "clock".freeze
9
11
 
@@ -123,7 +123,8 @@ module Hecks
123
123
  raise unless defined?(PG::Error) && e.is_a?(PG::Error)
124
124
 
125
125
  raise Runtime::WiringError,
126
- "cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
126
+ "cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip} " \
127
+ "-- to run without a database, set HECKS_ENVIRONMENT=memory (a domain needs an environments/memory overlay)"
127
128
  end
128
129
 
129
130
  # Joins the process's shared connection for the declared database and schema, so every
@@ -85,7 +85,9 @@ module Hecks
85
85
  memory: { value: deploy_settings.fetch(:memory, 512) },
86
86
  timeout: { value: deploy_settings.fetch(:timeout, 10) },
87
87
  database: { value: deploy_settings.fetch(:database, "Postgres") },
88
- web: { value: deploy_settings.fetch(:web, "None") }
88
+ web: { value: deploy_settings.fetch(:web, "None") },
89
+ **%i[dispatch handler_module secret_env].select { |key| deploy_settings.key?(key) }
90
+ .to_h { |key| [key, { value: deploy_settings[key].to_s }] }
89
91
  }
90
92
  ).instance
91
93
  rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
@@ -105,9 +107,6 @@ module Hecks
105
107
  # The module name `lambda_handler.rb` actually defines; named here
106
108
  # since `dispatch "None"` domains may each pick their own.
107
109
  webhook_handler_module = deploy_settings.fetch(:handler_module, "WebLambdaHandler")
108
- if dispatch_none && !deploy_settings.key?(:handler_module)
109
- raise ArgumentError, "#{world_file}'s deployed_to(\"AwsLambda\") declares dispatch \"None\" but no handler_module — add handler_module \"YourModuleName\" naming the module #{domain}/lambda_handler.rb defines."
110
- end
111
110
 
112
111
  # Scans every loaded chapter, not just this domain's own — a
113
112
  # `uses_framework`-attached chapter can itself declare a cross-domain
@@ -206,6 +206,7 @@ module Hecks
206
206
 
207
207
  { verb: fqn(bluebook, aggregate, command, entity), kind: :command,
208
208
  summary: command.goal, role: command.role, role_gated: !command.role.to_s.empty?,
209
+ group: aggregate.hecks_name, internal: command.role.to_s == "System",
209
210
  creates: command.creates?,
210
211
  receiver: receiver, legacy_receiver: (receiver == :aggregate ? :id : nil),
211
212
  legacy_arguments: legacy_arguments,
@@ -230,7 +231,7 @@ module Hecks
230
231
  else
231
232
  "#{port.name} telling #{aggregate.hecks_name}"
232
233
  end,
233
- role_gated: false,
234
+ role_gated: false, group: aggregate.hecks_name, internal: true,
234
235
  summary: port_summary(port, operation), arguments: arguments }
235
236
  end
236
237
 
@@ -257,13 +258,30 @@ module Hecks
257
258
  summary: model.description, arguments: arguments }
258
259
  end
259
260
 
261
+ # A question that only reads the aggregate's own records back: it returns no document and
262
+ # filters on nothing but the record's identity ("how one request ended") or its lifecycle
263
+ # status ("every request that was refused"). Each journaled run has such a pair, which a
264
+ # person reads through `hecks <verb> --wait` rather than asking for by name, so the help
265
+ # sets them apart with the bookkeeping verbs. A query that returns a document, or filters
266
+ # on anything else, is a real question. Only an aggregate that journals its own runs (it has
267
+ # system-role commands, the ones `internal` verbs are made of) has such a pair: a release's
268
+ # "every version that was shipped" is a question worth asking by name.
269
+ def bookkeeping_query?(aggregate, query)
270
+ return false if query.returns || query.wheres.empty?
271
+ return false unless aggregate.commands.any? { |command| command.role.to_s == "System" }
272
+
273
+ own = Array(aggregate.identified_by).map(&:to_s) + [aggregate.lifecycle&.field.to_s]
274
+ query.wheres.all? { |clause| own.include?(clause.field.to_s) }
275
+ end
276
+
260
277
  def query_spec(bluebook, aggregate, entity, query)
261
278
  arguments = Array(query.to_h[:attributes]).flat_map do |declared|
262
279
  attribute = query.attributes.find { |a| a.name.to_s == declared[:name].to_s }
263
280
  attribute ? options_for(attribute, entity || aggregate, aggregate) : []
264
281
  end
265
282
 
266
- { verb: fqn(bluebook, aggregate, query, entity), kind: :query,
283
+ { verb: fqn(bluebook, aggregate, query, entity), kind: :query, group: aggregate.hecks_name,
284
+ internal: entity.nil? && bookkeeping_query?(aggregate, query),
267
285
  summary: query.description, arguments: arguments, returns: query.returns }
268
286
  end
269
287
 
@@ -373,16 +391,57 @@ module Hecks
373
391
  " #{program} ask <question> [name=value …] read something", ""]
374
392
 
375
393
  out << "verbs:"
376
- verbs.each_value { |spec| out << " #{spec[:short].ljust(width)} #{spec[:summary]}" }
394
+ out.concat(listing(verbs, width) { |spec| spec[:summary] })
377
395
  out << ""
378
396
  out << "questions (nothing here changes anything):"
379
- questions.each_value { |spec| out << " #{spec[:short].ljust(width)} #{first_sentence(spec[:summary])}" }
397
+ out.concat(listing(questions, width) { |spec| first_sentence(spec[:summary]) })
380
398
  out << ""
381
399
  out << " #{program} <verb> --help what one verb wants, and every way it refuses"
382
400
  out << " a verb can always be spelled in full — #{example_qualified(verbs)}"
383
401
  out.join("\n")
384
402
  end
385
403
 
404
+ # The verb or question lines of the help. A domain with more than one aggregate is listed
405
+ # under a heading per aggregate, so related verbs sit together; the bookkeeping a run records
406
+ # about itself (`internal`: system-role commands and port operations) is set apart as names
407
+ # only, since a person never types them. A single-aggregate domain keeps the plain list.
408
+ def listing(specs, width)
409
+ shown, internal = specs.values.partition { |spec| !spec[:internal] }
410
+ groups = shown.group_by { |spec| spec[:group] }
411
+ lines = []
412
+ if groups.length > 1
413
+ groups.each do |group, members|
414
+ lines << " #{heading(group)}" if group
415
+ members.each { |spec| lines << " #{spec[:short].ljust(width)} #{yield(spec)}" }
416
+ end
417
+ else
418
+ shown.each { |spec| lines << " #{spec[:short].ljust(width)} #{yield(spec)}" }
419
+ end
420
+ lines.concat(internal_lines(internal)) unless internal.empty?
421
+ lines
422
+ end
423
+
424
+ # "LanguageRun" reads "Language": the Run suffix names the journaled-command shape all of
425
+ # these aggregates share, so it adds nothing under a heading ("Test suite", "Model check").
426
+ def heading(group)
427
+ words = Naming.snake(group.sub(/Run\z/, "")).tr("_", " ")
428
+ "#{words.capitalize}:"
429
+ end
430
+
431
+ # The internal verbs as bare names, wrapped, under one line saying what they are.
432
+ def internal_lines(specs)
433
+ lines = [" internal — what a run records about itself, named here only (`--help` still works):"]
434
+ line = " "
435
+ specs.map { |spec| spec[:short] }.each do |name|
436
+ if line.length + name.length + 1 > 98
437
+ lines << line
438
+ line = " "
439
+ end
440
+ line += " #{name}"
441
+ end
442
+ lines << line
443
+ end
444
+
386
445
  # One verb whose full spelling differs from its short one, or `""` when none does.
387
446
  def example_qualified(verbs)
388
447
  name, spec = verbs.find { |key, value| key != value[:short] } || verbs.first
@@ -238,6 +238,7 @@ Hecks.bluebook "QualityControl" do
238
238
  reference_to Target
239
239
  attribute :held_by, Engineer
240
240
  attribute :now, Instant
241
+ needs :now
241
242
 
242
243
  given("a live claim is not taken from the agent holding it") do
243
244
  status != "held" || claimed_at.value + window.value <= now.value
@@ -265,6 +266,7 @@ Hecks.bluebook "QualityControl" do
265
266
  # carry it. Required, so an omitted streak can neither blank the counter nor guess at it.
266
267
  reference_to Target
267
268
  attribute :now, Instant
269
+ needs :now
268
270
  attribute :next_streak, CleanStreak
269
271
  # Required: an in-process dispatch does not fill defaults for absent arguments, so an omitted
270
272
  # list refuses rather than blanks. `capabilities.value=""` is an honest value.
@@ -1115,6 +1117,7 @@ Hecks.bluebook "QualityControl" do
1115
1117
  reference_to Bug
1116
1118
  attribute :held_by, Engineer
1117
1119
  attribute :now, Instant
1120
+ needs :now
1118
1121
 
1119
1122
  given("a live claim is not taken from the agent holding it") do
1120
1123
  held_by.value == "nobody" || claimed_at.value + window.value <= now.value
@@ -1388,6 +1391,7 @@ Hecks.bluebook "QualityControl" do
1388
1391
  attribute :proposer, Proposer
1389
1392
  # `now:` is filled from the clock port, as in `Target.Claim`; an explicit value wins.
1390
1393
  attribute :now, Instant
1394
+ needs :now
1391
1395
 
1392
1396
  sets :proposed_at, to: :now
1393
1397
 
@@ -1784,6 +1788,7 @@ Hecks.bluebook "QualityControl" do
1784
1788
  attribute :commit, CommitRef
1785
1789
  attribute :title, PatchTitle
1786
1790
  attribute :now, Instant
1791
+ needs :now
1787
1792
 
1788
1793
  given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
1789
1794
  given("the bug is fixed") { bug.status == "fixed" }
@@ -1921,6 +1926,7 @@ Hecks.bluebook "QualityControl" do
1921
1926
  attribute :branch, ImprovementBranch
1922
1927
  attribute :title, ImprovementTitle
1923
1928
  attribute :now, Instant
1929
+ needs :now
1924
1930
 
1925
1931
  given("the branch is one this practice recognises as its own") { branch.value.start_with?("qa/") }
1926
1932
  given("the angle is under investigation") { angle.unset? || angle.status == "investigating" }
@@ -83,10 +83,12 @@ module Hecks
83
83
 
84
84
  private
85
85
 
86
- # No-op: `Routing` has already handed `call` a decoded argument hash,
87
- # so there is nothing left to decode here yet. Kept as a step so the
88
- # generated step enum has a slot to move the real decoder into later.
89
- def step_decode_arguments(_ctx); end
86
+ # `Routing` has already handed `call` a decoded argument hash, so the one thing left to do
87
+ # here is answer the outside facts the command `needs`, before any refusal or given reads
88
+ # its arguments. Not traced: the step has always been invisible to a trace observer.
89
+ def step_decode_arguments(ctx)
90
+ ctx.args = enrich_arguments(ctx.command, ctx.args)
91
+ end
90
92
 
91
93
  def step_refuse_unknown_arguments(ctx)
92
94
  step(:refuse_unknown_arguments) { refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.command, ctx.args) }
@@ -241,6 +243,8 @@ module Hecks
241
243
  target_args = ctx.args.merge(
242
244
  delegation.source.to_h { |target_key, source_key| [target_key.to_sym, ctx.args[source_key]] }
243
245
  )
246
+ # The target is dispatched as if called directly, so its needs and defaults are filled too.
247
+ target_args = enrich_arguments(target_command, target_args)
244
248
 
245
249
  refuse_unknown_arguments(ctx.domain, ctx.aggregate, target_command, target_args,
246
250
  extra_identity_heads: entity.identity_heads)
@@ -133,8 +133,11 @@ module Hecks
133
133
 
134
134
  private
135
135
 
136
- # No-op — entities have no `decode_arguments` step of their own.
137
- def step_decode_arguments(_ctx); end
136
+ # Answers the outside facts the entity command `needs`, as the aggregate interpreter does;
137
+ # the arguments are otherwise already decoded.
138
+ def step_decode_arguments(ctx)
139
+ ctx.args = enrich_arguments(ctx.command, ctx.args)
140
+ end
138
141
 
139
142
  # `extra_identity_heads:` covers every entity in `ctx.chain`, not just
140
143
  # the root — each hop is addressed by its own identity fields, which