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.
- checksums.yaml +4 -4
- data/exe/hecks +12 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +3 -0
- data/lib/hecks/bluebook/command.rb +7 -5
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +42 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +12 -0
- data/lib/hecks/bluebook/expression/projection.json +21 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -0
- data/lib/hecks/cli/console.rb +24 -8
- data/lib/hecks/cli/project_cli.rb +42 -9
- data/lib/hecks/cli/seed_semantics_corpus.rb +35 -6
- data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -0
- data/lib/hecks/doors/cli_runner.rb +57 -12
- data/lib/hecks/doors/launcher_options.rb +41 -0
- data/lib/hecks/fuzzing/properties.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +11 -3
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +9 -7
- data/lib/hecks/gate/stages.yml +55 -0
- data/lib/hecks/grammar/expression.bluebook +6 -1
- data/lib/hecks/grammar/expression_operators.json +135 -0
- data/lib/hecks/hecks/adapters/codebase/gate.rb +37 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +24 -4
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/codebase.behaviors +12 -0
- data/lib/hecks/hecks/codebase.bluebook +194 -0
- data/lib/hecks/hecks/custodian.bluebook +2 -0
- data/lib/hecks/hecks/hecks.hecksagon +14 -0
- data/lib/hecks/hecks/hecks.world +5 -1
- data/lib/hecks/language/bluebook/command.bluebook +26 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +29 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/clock.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +2 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -4
- data/lib/hecks/projector/cli_projector.rb +63 -4
- data/lib/hecks/quality_control/quality_control.bluebook +6 -0
- data/lib/hecks/runtime/command_interpreter.rb +8 -4
- data/lib/hecks/runtime/entity_interpreter.rb +5 -2
- data/lib/hecks/runtime/interpreting.rb +50 -0
- data/lib/hecks/runtime/invocation.rb +7 -1
- data/lib/hecks/runtime/loader.rb +16 -13
- data/lib/hecks/runtime.rb +11 -0
- data/lib/hecks/three_zero/forms.yml +1 -1
- data/lib/hecks/tools/ci_gate_decision.rb +109 -0
- data/lib/hecks/tools/ci_gates.rb +124 -0
- data/lib/hecks/tools/gate.rb +117 -0
- data/lib/hecks/tools.rb +3 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +12 -2
- data/rust/codegen/src/naming.rs +13 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/dispatch.rs +207 -1
- data/rust/host/src/main.rs +1 -0
- data/rust/host/src/needs.rs +243 -0
- data/rust/host/src/resend.rs +52 -10
- data/rust/host/src/server.rs +7 -3
- data/rust/host/src/web/newsletter/template.rs +69 -0
- data/rust/host/src/web/newsletter/tests.rs +43 -0
- data/rust/host/src/web/newsletter.rs +6 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/parser/src/canonical.rs +137 -2
- data/rust/parser/src/emit.rs +25 -19
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +44 -0
- data/rust/project/naming.rb +4 -2
- data/rust/src/kernel/cli.rs +3 -0
- data/rust/src/kernel/mod.rs +1 -0
- data/rust/src/kernel/needs.rs +194 -0
- data/rust/src/kernel/orchestrate.rs +84 -0
- data/rust/src/kernel/repository.rs +43 -2
- metadata +10 -2
|
@@ -34,6 +34,62 @@ module Hecks
|
|
|
34
34
|
bluebook: plan[:bluebook], launcher: plan[:launcher])
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Tails a question: asks, prints each new entry as one JSON line, and asks again from the
|
|
38
|
+
# cursor the answer gave, until the reader interrupts. Only for a question the `launcher`
|
|
39
|
+
# setting lists under `streams`, given `--stream`; `from_now` applies to the first ask only.
|
|
40
|
+
#
|
|
41
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted domain
|
|
42
|
+
# @param argv [Array<String>] as for `call`, with `--stream`
|
|
43
|
+
# @param program [String] how the caller was invoked
|
|
44
|
+
# @param out [IO] where each entry's line goes
|
|
45
|
+
# @param err [IO] where a refusal goes
|
|
46
|
+
# @param max_polls [Integer, nil] stop after this many asks; unbounded when nil
|
|
47
|
+
# @return [Integer, nil] 0 or 1 for a stream, nil when `call` should run the line
|
|
48
|
+
def stream(runtime:, argv:, program: "hecks run", out: $stdout, err: $stderr, max_polls: nil)
|
|
49
|
+
plan = resolve(runtime, argv, program)
|
|
50
|
+
return if plan[:answer] || !LauncherOptions.streams?(plan[:launcher], plan[:spec])
|
|
51
|
+
|
|
52
|
+
words, streaming = LauncherOptions.take_stream(plan[:rest])
|
|
53
|
+
return unless streaming
|
|
54
|
+
|
|
55
|
+
tail(runtime, plan[:spec], CliDoor.arguments(plan[:spec], words), out, max_polls)
|
|
56
|
+
0
|
|
57
|
+
rescue Interrupt, Errno::EPIPE
|
|
58
|
+
0
|
|
59
|
+
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
60
|
+
err.puts("#{e.message}\n\n #{program} #{plan[:name]} --help")
|
|
61
|
+
1
|
|
62
|
+
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
63
|
+
err.puts(e.message)
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# One ask after another, each from the cursor the last gave.
|
|
68
|
+
def tail(runtime, spec, args, out, max_polls)
|
|
69
|
+
args = args.merge(wait: { value: 30 }) unless args.key?(:wait)
|
|
70
|
+
polls = 0
|
|
71
|
+
cursor = nil
|
|
72
|
+
loop do
|
|
73
|
+
ask = cursor ? args.except(:from_now).merge(since: { value: cursor }) : args
|
|
74
|
+
rows = runtime.query(spec[:verb], **ask)
|
|
75
|
+
row = rows.first || {}
|
|
76
|
+
Array(row[:events]).each { |event| out.puts(JSON.generate(entry_line(JsonDoor.materialize(event)))) }
|
|
77
|
+
out.flush
|
|
78
|
+
cursor = row[:cursor]
|
|
79
|
+
polls += 1
|
|
80
|
+
break if cursor.nil? || (max_polls && polls >= max_polls)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# An entry as a line: its payload, which the answer holds as JSON text, back as an object.
|
|
85
|
+
def entry_line(event)
|
|
86
|
+
line = JSON.parse(JSON.generate(event))
|
|
87
|
+
line["payload"] = JSON.parse(line["payload"]) if line["payload"].is_a?(String)
|
|
88
|
+
line
|
|
89
|
+
rescue JSON::ParserError
|
|
90
|
+
line
|
|
91
|
+
end
|
|
92
|
+
|
|
37
93
|
# Answers a command line that asks only for usage, from the projection alone.
|
|
38
94
|
#
|
|
39
95
|
# Needs a registry and no bound adapter, so a `Runtime::Loader::Described` serves: the
|
|
@@ -110,7 +166,7 @@ module Hecks
|
|
|
110
166
|
# @param launcher [Hash, nil] the chapter's `launcher` world setting; nil when not opted in
|
|
111
167
|
def dispatch(runtime, spec, name, rest, program, asking, bluebook: nil, launcher: nil)
|
|
112
168
|
rest, wait = LauncherOptions.take_wait(spec, rest) if launcher
|
|
113
|
-
args =
|
|
169
|
+
args = CliDoor.arguments(spec, rest)
|
|
114
170
|
|
|
115
171
|
return answer_query(runtime, spec, args, wait) if spec[:kind] == :query
|
|
116
172
|
|
|
@@ -231,17 +287,6 @@ module Hecks
|
|
|
231
287
|
rows.first[:text]
|
|
232
288
|
end
|
|
233
289
|
|
|
234
|
-
# Fills in a `now` argument the caller left out, from the clock port; an explicit one wins.
|
|
235
|
-
#
|
|
236
|
-
# Done at the door, not in the runtime: a clock read inside the interpreter would give a
|
|
237
|
-
# replayed corpus step the replay day's time. Matched by the argument's name alone.
|
|
238
|
-
def stamp_time(runtime, spec, args)
|
|
239
|
-
return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
|
|
240
|
-
return args if args.key?(:now)
|
|
241
|
-
|
|
242
|
-
args.merge(now: { value: Ports::Clock.now(runtime.registry) })
|
|
243
|
-
end
|
|
244
|
-
|
|
245
290
|
# The reactions one dispatch caused that the domain refused, as `refused_reactions:`.
|
|
246
291
|
#
|
|
247
292
|
# A policy's trigger that a `given` refuses is not the command's own refusal: the command
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "stringio"
|
|
1
2
|
require_relative "../ports/identity_generation"
|
|
2
3
|
require_relative "../runtime/errors"
|
|
3
4
|
require_relative "cli_door"
|
|
@@ -15,9 +16,11 @@ module Hecks
|
|
|
15
16
|
# - **run_keys** mints the `run` key of a creating command that was given none.
|
|
16
17
|
# - **failure_states** are the lifecycle states `--wait` reports as a failure (exit 1).
|
|
17
18
|
# - **names** maps a launcher name to the command it stands for (see `CliProjector`).
|
|
19
|
+
# - **streams** lists the questions `--stream` may tail, one JSON line per new entry.
|
|
18
20
|
module LauncherOptions
|
|
19
21
|
SETTING = "launcher".freeze
|
|
20
22
|
WAIT = "--wait".freeze
|
|
23
|
+
STREAM = "--stream".freeze
|
|
21
24
|
RUN_KEY = "run.value".freeze
|
|
22
25
|
|
|
23
26
|
module_function
|
|
@@ -44,6 +47,23 @@ module Hecks
|
|
|
44
47
|
{ program: program, names: settings && settings[:names], mint_run_keys: settings && settings[:run_keys] }
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
# Runs the block with standard error held back, so the wiring notes hecks prints about its own
|
|
51
|
+
# chapters (a Memory journal loses sagas on restart) do not open a console session. They are
|
|
52
|
+
# for an operator running a stored journal. A raised error still surfaces, unchanged.
|
|
53
|
+
#
|
|
54
|
+
# @param hold [Boolean] whether to hold standard error back; false runs the block as it is
|
|
55
|
+
# @yield the boot
|
|
56
|
+
# @return [Object] the block's value
|
|
57
|
+
def quietly(hold: true)
|
|
58
|
+
return yield unless hold
|
|
59
|
+
|
|
60
|
+
shown = $stderr
|
|
61
|
+
$stderr = StringIO.new
|
|
62
|
+
yield
|
|
63
|
+
ensure
|
|
64
|
+
$stderr = shown if hold
|
|
65
|
+
end
|
|
66
|
+
|
|
47
67
|
# Takes `--wait` out of a verb's words, unless the verb declares a `wait` argument of its own.
|
|
48
68
|
#
|
|
49
69
|
# `--wait=false` (or `no`, `0`, `off`) is a `--wait` that was switched off; a bare `--wait`
|
|
@@ -67,6 +87,27 @@ module Hecks
|
|
|
67
87
|
[rest, wait]
|
|
68
88
|
end
|
|
69
89
|
|
|
90
|
+
# Whether a question may be tailed with `--stream`: the chapter's `launcher` setting lists it
|
|
91
|
+
# under `streams`, by the question's own name.
|
|
92
|
+
#
|
|
93
|
+
# @param launcher [Hash, nil] the chapter's `launcher` setting
|
|
94
|
+
# @param spec [Hash] the question's projected spec
|
|
95
|
+
# @return [Boolean] true for a question the setting names
|
|
96
|
+
def streams?(launcher, spec)
|
|
97
|
+
return false unless launcher && spec[:kind] == :query
|
|
98
|
+
|
|
99
|
+
Array(launcher[:streams]).map(&:to_s).include?(spec[:verb].to_s.split(/[.:]+/).last)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Takes `--stream` out of a question's words.
|
|
103
|
+
#
|
|
104
|
+
# @param words [Array<String>] the words after the question
|
|
105
|
+
# @return [Array(Array<String>, Boolean)] the remaining words, and whether `--stream` was
|
|
106
|
+
# given
|
|
107
|
+
def take_stream(words)
|
|
108
|
+
[words.reject { |word| word == STREAM }, words.include?(STREAM)]
|
|
109
|
+
end
|
|
110
|
+
|
|
70
111
|
# @api private
|
|
71
112
|
def wait_word?(word) = word == WAIT || word.start_with?("#{WAIT}=")
|
|
72
113
|
|
|
@@ -39,7 +39,7 @@ module Hecks
|
|
|
39
39
|
query_answers_match_reference: %w[Query#wheres Query#order_field Query#order_way Query#limit],
|
|
40
40
|
paging_offset_partitions_correctly: %w[Query#options],
|
|
41
41
|
authorize_scopes_or_refuses: %w[Query#options],
|
|
42
|
-
guard_refusals_are_declared: %w[Command#givens Command#ensures],
|
|
42
|
+
guard_refusals_are_declared: %w[Command#givens Command#ensures Command#needs],
|
|
43
43
|
lifecycle_guard_and_given_violations_are_refused: %w[Command#from Aggregate#preconditions Entity#preconditions],
|
|
44
44
|
# Dispatch#command_name/with_spec aren't claimable feature names — the
|
|
45
45
|
# coverage walk only reaches one level of entity nesting and Dispatch
|
|
@@ -212,9 +212,10 @@ module Hecks
|
|
|
212
212
|
if mapped.empty? && !entry.key?(:entity) && command.creates?
|
|
213
213
|
mapped = command.attributes.map { |attribute| attribute.name.to_s }
|
|
214
214
|
end
|
|
215
|
+
needed = needed_facts_of(entry)
|
|
215
216
|
command.attributes.select do |attribute|
|
|
216
217
|
name = attribute.name.to_s
|
|
217
|
-
mapped.include?(name) && args.key?(name) && !heads.include?(name)
|
|
218
|
+
mapped.include?(name) && args.key?(name) && !heads.include?(name) && !needed.include?(name)
|
|
218
219
|
end
|
|
219
220
|
end
|
|
220
221
|
|
|
@@ -352,12 +353,19 @@ module Hecks
|
|
|
352
353
|
end
|
|
353
354
|
|
|
354
355
|
def droppable_required_attributes(args, entry)
|
|
355
|
-
heads
|
|
356
|
+
heads = identity_heads_of(entry)
|
|
357
|
+
needed = needed_facts_of(entry)
|
|
356
358
|
entry[:command].attributes.reject(&:optional?).select do |attribute|
|
|
357
|
-
|
|
359
|
+
name = attribute.name.to_s
|
|
360
|
+
args.key?(name) && !heads.include?(name) && !needed.include?(name)
|
|
358
361
|
end
|
|
359
362
|
end
|
|
360
363
|
|
|
364
|
+
# The facts the command `needs`: the runtime answers them when a step leaves them out
|
|
365
|
+
# (each engine from its own clock), so a recorded step carries them and dropping one is not
|
|
366
|
+
# an absent-argument case.
|
|
367
|
+
def needed_facts_of(entry) = entry[:command].needs.map(&:to_s)
|
|
368
|
+
|
|
361
369
|
# Aims a grant at a role some command declares; random role text would make `actor_known`
|
|
362
370
|
# unreachable. Does nothing without the role draw.
|
|
363
371
|
def steer_grant!(args, entry, catalog)
|
|
@@ -39,7 +39,7 @@ module Hecks
|
|
|
39
39
|
# The caller draw and then the dry-run coin follow the mutation. The order
|
|
40
40
|
# is part of the seed contract; both draw nothing when off.
|
|
41
41
|
def build_command_step(runtime, catalog, entry)
|
|
42
|
-
args = args_for(entry[:command].attributes, entry[:aggregate])
|
|
42
|
+
args = args_for(entry[:command].attributes, entry[:aggregate], needed: entry[:command].needs.map(&:to_s))
|
|
43
43
|
add_identity!(args, entry)
|
|
44
44
|
steer_grant!(args, entry, catalog)
|
|
45
45
|
mutations = adversarial_mutations!(args, entry, catalog)
|
|
@@ -76,7 +76,9 @@ module Hecks
|
|
|
76
76
|
Hecks.as_caller(role: caller["role"], actor_id: caller["actor_id"], &)
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
# `needed` names the facts the runtime answers when a step leaves them out; a malformation
|
|
80
|
+
# never drops one, since each engine would fill it from its own clock.
|
|
81
|
+
def args_for(attributes, aggregate, needed: [])
|
|
80
82
|
args = attributes.each_with_object({}) do |attribute, built|
|
|
81
83
|
# Omitting an optional argument is an ordinary payload, not a malformation
|
|
82
84
|
# (see OPTIONAL_OMITTED_PROBABILITY).
|
|
@@ -94,7 +96,7 @@ module Hecks
|
|
|
94
96
|
end
|
|
95
97
|
end
|
|
96
98
|
|
|
97
|
-
malform(args, attributes, aggregate)
|
|
99
|
+
malform(args, attributes, aggregate, needed)
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
# An array of 0-3 independently generated elements shaped like the bare
|
|
@@ -108,21 +110,21 @@ module Hecks
|
|
|
108
110
|
|
|
109
111
|
# At most one malformation per step, so the check that fired is identifiable.
|
|
110
112
|
# The rate stays low because refused steps reach no state.
|
|
111
|
-
def malform(args, attributes, aggregate)
|
|
113
|
+
def malform(args, attributes, aggregate, needed)
|
|
112
114
|
return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
|
|
113
115
|
|
|
114
116
|
case @random.rand(3)
|
|
115
117
|
when 0 then corrupt_one(args, attributes, aggregate)
|
|
116
|
-
when 1 then drop_one(args, aggregate)
|
|
118
|
+
when 1 then drop_one(args, aggregate, needed)
|
|
117
119
|
else args.merge([InvalidValueGenerator.undeclared_argument(random: @random)].to_h)
|
|
118
120
|
end
|
|
119
121
|
end
|
|
120
122
|
|
|
121
123
|
# Never drops the identity: an auto-minted id is unreproducible, so the
|
|
122
124
|
# step's outcome could not be replayed.
|
|
123
|
-
def drop_one(args, aggregate)
|
|
125
|
+
def drop_one(args, aggregate, needed)
|
|
124
126
|
identity = (aggregate.identified_by || :id).to_s
|
|
125
|
-
droppable = args.keys - [identity, "id"]
|
|
127
|
+
droppable = args.keys - [identity, "id"] - needed
|
|
126
128
|
return args if droppable.empty?
|
|
127
129
|
|
|
128
130
|
args.reject { |name, _| name == droppable.sample(random: @random) }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# The checks a stage runs before its change is let through. `hecks gate <stage>` reads this file and
|
|
2
|
+
# the git pre-push hook calls it, so the list of checks lives here and nowhere else.
|
|
3
|
+
#
|
|
4
|
+
# A check is one command run from the repository root, all of a stage's checks at once:
|
|
5
|
+
# id the name it is reported and selected by (`only=a,b`)
|
|
6
|
+
# title the heading its output is printed under
|
|
7
|
+
# run the program and its arguments; `{workers}` is half the machine's cores, at least one
|
|
8
|
+
# blocked what a red check means and what to do about it
|
|
9
|
+
#
|
|
10
|
+
# `env` is set only where the caller has not set it: a push check has no use for the Hecks
|
|
11
|
+
# domain's database, so it runs on Memory, as a pull-request run does in CI.
|
|
12
|
+
pre_push:
|
|
13
|
+
env:
|
|
14
|
+
HECKS_ENVIRONMENT: memory
|
|
15
|
+
checks:
|
|
16
|
+
- id: rspec
|
|
17
|
+
title: rspec (parallel)
|
|
18
|
+
run: [bundle, exec, parallel_rspec, spec, -n, "{workers}"]
|
|
19
|
+
blocked: the suite is red.
|
|
20
|
+
- id: fuzzing
|
|
21
|
+
title: spec/fuzzing, which the parallel run above excludes
|
|
22
|
+
run: [bundle, exec, rspec, spec/fuzzing, --tag, fuzzing]
|
|
23
|
+
blocked: spec/fuzzing is red.
|
|
24
|
+
- id: query_agreement
|
|
25
|
+
title: adapter agreement, one declared query and every engine giving the same answer
|
|
26
|
+
run: [bundle, exec, rspec, spec/adapters/query_agreement_spec.rb, --tag, io]
|
|
27
|
+
blocked: the engines disagree about a declared query; one of them is wrong, and a self-consistent adapter spec cannot tell which.
|
|
28
|
+
- id: engine_agreement
|
|
29
|
+
title: engine agreement, every declared comparator on both engines
|
|
30
|
+
run: [bundle, exec, exe/hecks, check_engine_agreement, --wait]
|
|
31
|
+
blocked: a comparator is missing a shared case or a cross-engine spec.
|
|
32
|
+
- id: model_check
|
|
33
|
+
title: model check, unreachable states, dead transitions and orphaned handlers
|
|
34
|
+
run: [bundle, exec, exe/hecks, model_check, --wait]
|
|
35
|
+
blocked: the model checker found something.
|
|
36
|
+
- id: doc_coverage
|
|
37
|
+
title: docs coverage, words with no running example
|
|
38
|
+
run: [bundle, exec, exe/hecks, measure_doc_coverage, --wait]
|
|
39
|
+
blocked: the language ships something undocumented; write the missing sections and examples.
|
|
40
|
+
- id: rubocop
|
|
41
|
+
title: rubocop, the static style and correctness gate
|
|
42
|
+
run: [bundle, exec, rubocop, -c, .rubocop.yml]
|
|
43
|
+
blocked: rubocop found an offense.
|
|
44
|
+
- id: codegen_drift
|
|
45
|
+
title: codegen drift, the committed rust/src/generated/ must match a fresh regeneration
|
|
46
|
+
run: [bundle, exec, exe/hecks, regenerate_corpus, --check, --wait]
|
|
47
|
+
blocked: regenerating hits a diff, so the committed Rust is stale; run `hecks regenerate_corpus --confirm` and commit the result.
|
|
48
|
+
- id: comment_style
|
|
49
|
+
title: comment style, every comment under lib/hecks against docs/COMMENT_STYLE_GUIDE.md
|
|
50
|
+
run: [bundle, exec, exe/hecks, check_comments, paths=lib/hecks, --wait]
|
|
51
|
+
blocked: a comment breaks the style guide, such as a line past 100 characters; `hecks fix_comments` rewrites the mechanical ones, and the rest are shortened by hand. CI runs this same check.
|
|
52
|
+
- id: comment_blocks
|
|
53
|
+
title: comment blocks, no run of comment lines past ADR 0075's cap
|
|
54
|
+
run: [bundle, exec, exe/hecks, check_comments, "paths=lib,spec,examples", only=long_block, --wait]
|
|
55
|
+
blocked: a comment block ran past the cap; shorten it.
|
|
@@ -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
|
|
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(
|
|
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::
|
|
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" }
|