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.
- checksums.yaml +4 -4
- data/exe/hecks +6 -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/project_cli.rb +6 -2
- 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 +23 -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 +51 -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 +3 -2
- 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/projections/deploy/lambda.rb +3 -4
- data/lib/hecks/quality_control/quality_control.bluebook +6 -0
- data/lib/hecks/runtime/command_interpreter.rb +6 -4
- data/lib/hecks/runtime/entity_interpreter.rb +5 -2
- data/lib/hecks/runtime/interpreting.rb +29 -0
- data/lib/hecks/runtime/invocation.rb +5 -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/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 +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90a718a197fae0287e4fe16033540bf625166bc31bfb3482e0752d1039a2a949
|
|
4
|
+
data.tar.gz: 15fe27abd9a488dfe4f579118f8e8ffbea1302b956ee36fbd2b9475eb3b88429
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d74d07807e22e8899c041adfa96d799b5eb47713a0dec6c05bbb9e61a579a32a35da890b67aaf876b1ba5182e0961a112f370e0e04adcf2719f8ae57a50cfd45
|
|
7
|
+
data.tar.gz: 11e85c77dc6a4f5d34ec3acd54e41873be8d03b980e4ef97a8d0d7c9da4b63ecfc2b04e2a1322f8c1536ad1d58f8f85f50df2ca58f3d6fea44126cb871c05b0d
|
data/exe/hecks
CHANGED
|
@@ -24,7 +24,8 @@ end
|
|
|
24
24
|
require "hecks"
|
|
25
25
|
|
|
26
26
|
# Usage is answered from the projected chapter alone: no adapter is bound and no
|
|
27
|
-
# database is opened. Only a line that runs a verb boots the domain
|
|
27
|
+
# database is opened. Only a line that runs a verb boots the domain,
|
|
28
|
+
# and it boots from what the usage check already loaded.
|
|
28
29
|
domain = File.expand_path("../lib/hecks/hecks", __dir__)
|
|
29
30
|
program = "hecks"
|
|
30
31
|
described = begin
|
|
@@ -38,10 +39,13 @@ text, status, reason = Hecks::Doors::CliRunner.usage(
|
|
|
38
39
|
)
|
|
39
40
|
unless text
|
|
40
41
|
runtime = begin
|
|
41
|
-
Hecks.
|
|
42
|
+
Hecks.boot_described(described, install_doors: false)
|
|
42
43
|
rescue StandardError => e
|
|
43
44
|
abort "cannot open Hecks: #{e.message.lines.first.strip}"
|
|
44
45
|
end
|
|
46
|
+
# A question the world lists under `streams`, given `--stream`, tails until interrupted.
|
|
47
|
+
streamed = Hecks::Doors::CliRunner.stream(runtime: runtime, argv: ARGV, program: program)
|
|
48
|
+
exit streamed unless streamed.nil?
|
|
45
49
|
text, status, reason = Hecks::Doors::CliRunner.call(
|
|
46
50
|
runtime: runtime, argv: ARGV, program: program
|
|
47
51
|
)
|
|
@@ -62,6 +62,7 @@ module Hecks
|
|
|
62
62
|
attributes: [:attributes, [:each, :shape_field]],
|
|
63
63
|
givens: [:givens, [:each, :given]],
|
|
64
64
|
ensures: [:ensures, [:each, :given]],
|
|
65
|
+
needs: [:needs, [:each, :need]],
|
|
65
66
|
mutations: [:mutations, [:each, :mutation]],
|
|
66
67
|
emits: [:emits, :plain],
|
|
67
68
|
# Lifecycle state as a command guard: one state, an array, or nil (ADR 0025).
|
|
@@ -70,7 +71,8 @@ module Hecks
|
|
|
70
71
|
},
|
|
71
72
|
rows: { mutations: :mutation_rows },
|
|
72
73
|
reads: { attributes: [:each, :shape_field], givens: [:each, :rule], ensures: [:each, :rule],
|
|
73
|
-
mutations: [:call, :mutations], emits: :names, provenance: :provenance,
|
|
74
|
+
needs: [:each, :need], mutations: [:call, :mutations], emits: :names, provenance: :provenance,
|
|
75
|
+
from: :from },
|
|
74
76
|
derived: { position: :walk }
|
|
75
77
|
),
|
|
76
78
|
|
|
@@ -74,6 +74,9 @@ module Hecks
|
|
|
74
74
|
Given.new(description: rule[:description], canonical: rule[:canonical])
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# One outside fact a command needs, as the Symbol `Command#needs` holds.
|
|
78
|
+
def need(row) = row[:fact].to_sym
|
|
79
|
+
|
|
77
80
|
# All three fields are identifiers, unlike Invariant/Given's free text, so they
|
|
78
81
|
# come back as Symbols.
|
|
79
82
|
def projected_field(row)
|
|
@@ -54,6 +54,7 @@ module Hecks
|
|
|
54
54
|
attributes: many(:attributes),
|
|
55
55
|
givens: -> { givens.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
56
56
|
ensures: -> { ensures.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
57
|
+
needs: -> { needs.map { |fact| { fact: fact.to_s } } },
|
|
57
58
|
mutations: many(:mutations),
|
|
58
59
|
emits: :emits,
|
|
59
60
|
# The lifecycle state this command is admissible from: a guard, not a transition.
|
|
@@ -62,7 +63,7 @@ module Hecks
|
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
class << self
|
|
65
|
-
attr_reader :role, :goal, :attributes, :givens, :ensures, :mutations, :emits, :references,
|
|
66
|
+
attr_reader :role, :goal, :attributes, :givens, :ensures, :needs, :mutations, :emits, :references,
|
|
66
67
|
:from, :provenance
|
|
67
68
|
|
|
68
69
|
# Mints one command as its own anonymous subclass of the class `declare` is called on.
|
|
@@ -71,18 +72,18 @@ module Hecks
|
|
|
71
72
|
# @param role [String, nil] the declared role text; `goal` is the goal text
|
|
72
73
|
# @param attributes [Array<Bluebook::Attribute>] the declared arguments
|
|
73
74
|
# @param givens [Array<Bluebook::Given>] the declared preconditions
|
|
74
|
-
# @param ensures [Array<Bluebook::Given>] the
|
|
75
|
+
# @param ensures [Array<Bluebook::Given>] the postconditions; `needs` the facts it supplies
|
|
75
76
|
# @param mutations [Array<Bluebook::Mutation>] the state changes it applies
|
|
76
77
|
# @param emits [Array<String>] the event names it may emit
|
|
77
78
|
# @param references [String, Symbol, nil] the aggregate its `reference_to` addresses
|
|
78
79
|
# @param from [String, Array<String>, nil] the lifecycle states it is admissible from
|
|
79
80
|
# @param provenance [Object, nil] the declared canonical source, as written
|
|
80
|
-
def declare(name:, role: nil, goal: nil, attributes: [], givens: [], ensures: [],
|
|
81
|
+
def declare(name:, role: nil, goal: nil, attributes: [], givens: [], ensures: [], needs: [],
|
|
81
82
|
mutations: [], emits: [], references: nil, from: nil, provenance: nil)
|
|
82
83
|
verb = Class.new(self)
|
|
83
84
|
verb.hecks_name = name.to_s
|
|
84
85
|
verb.absorb(role: role, goal: goal, attributes: attributes, givens: givens,
|
|
85
|
-
ensures: ensures, mutations: mutations, emits: emits, references: references&.to_s,
|
|
86
|
+
ensures: ensures, needs: needs, mutations: mutations, emits: emits, references: references&.to_s,
|
|
86
87
|
from: from, provenance: provenance)
|
|
87
88
|
verb
|
|
88
89
|
end
|
|
@@ -93,12 +94,13 @@ module Hecks
|
|
|
93
94
|
#
|
|
94
95
|
# @return [Class] self
|
|
95
96
|
def absorb(role:, goal:, attributes:, givens:, ensures:, mutations:, emits:, references:,
|
|
96
|
-
from: nil, provenance: nil)
|
|
97
|
+
needs: [], from: nil, provenance: nil)
|
|
97
98
|
@role = role
|
|
98
99
|
@goal = goal
|
|
99
100
|
@attributes = attributes
|
|
100
101
|
@givens = givens
|
|
101
102
|
@ensures = ensures
|
|
103
|
+
@needs = needs
|
|
102
104
|
@mutations = mutations
|
|
103
105
|
@emits = emits
|
|
104
106
|
@references = references
|
|
@@ -41,6 +41,7 @@ module Hecks
|
|
|
41
41
|
["Command", "then_set"] => :then_set_impl,
|
|
42
42
|
["Command", "delegates_to"] => :delegates_to_impl,
|
|
43
43
|
["Command", "attribute"] => :attribute_impl,
|
|
44
|
+
["Command", "needs"] => :needs_impl,
|
|
44
45
|
["Command", "corrects"] => :corrects_impl,
|
|
45
46
|
["Entity", "identified_by"] => :identified_by_impl,
|
|
46
47
|
["Entity", "given"] => :given_impl,
|
|
@@ -22,6 +22,7 @@ module Hecks
|
|
|
22
22
|
@owner = owner
|
|
23
23
|
@givens = []
|
|
24
24
|
@ensures = []
|
|
25
|
+
@needs = []
|
|
25
26
|
@mutations = []
|
|
26
27
|
@emits = []
|
|
27
28
|
@named_givens = named_givens
|
|
@@ -66,6 +67,31 @@ module Hecks
|
|
|
66
67
|
# @return [String] the description as stored
|
|
67
68
|
def goal(value) = @goal = value
|
|
68
69
|
|
|
70
|
+
# The outside facts a command may declare it needs, each the name of the attribute the
|
|
71
|
+
# runtime fills when the caller leaves it out. `now` is the clock port's answer, in epoch
|
|
72
|
+
# seconds.
|
|
73
|
+
NEEDABLE_FACTS = %i[now].freeze
|
|
74
|
+
|
|
75
|
+
# Declares an outside fact the runtime supplies before any given runs (ADR 0081):
|
|
76
|
+
# `needs :now` fills the command's own `now` attribute from the clock port when the caller
|
|
77
|
+
# names none.
|
|
78
|
+
#
|
|
79
|
+
# @param fact [Symbol] one of `NEEDABLE_FACTS`
|
|
80
|
+
# @return [Array<Symbol>] the facts declared so far
|
|
81
|
+
# @raise [Bluebook::DSL::Malformed] if the fact is not one the runtime can supply, or is
|
|
82
|
+
# declared twice
|
|
83
|
+
def needs_impl(fact)
|
|
84
|
+
fact = fact.to_sym
|
|
85
|
+
unless NEEDABLE_FACTS.include?(fact)
|
|
86
|
+
raise Malformed,
|
|
87
|
+
"#{@name} needs :#{fact}, which the runtime cannot supply — it supplies " \
|
|
88
|
+
"#{NEEDABLE_FACTS.map { |known| ":#{known}" }.join(', ')}"
|
|
89
|
+
end
|
|
90
|
+
raise Malformed, "#{@name} declares needs :#{fact} twice" if @needs.include?(fact)
|
|
91
|
+
|
|
92
|
+
@needs << fact
|
|
93
|
+
end
|
|
94
|
+
|
|
69
95
|
# Names where a concept adopted from a canonical source came from.
|
|
70
96
|
#
|
|
71
97
|
# @param from [Object] the canonical source, captured exactly as written
|
|
@@ -292,6 +318,7 @@ module Hecks
|
|
|
292
318
|
def build
|
|
293
319
|
resolve_implicit_attributes!
|
|
294
320
|
refuse_duplicate_targets!
|
|
321
|
+
refuse_undeclared_needs!
|
|
295
322
|
|
|
296
323
|
delegation = @mutations.find { |mutation| mutation.op == :delegate }
|
|
297
324
|
if delegation && (@mutations.size > 1 || @emits.any?)
|
|
@@ -308,6 +335,7 @@ module Hecks
|
|
|
308
335
|
attributes: attributes,
|
|
309
336
|
givens: @givens,
|
|
310
337
|
ensures: @ensures,
|
|
338
|
+
needs: @needs,
|
|
311
339
|
mutations: @mutations,
|
|
312
340
|
emits: @emits,
|
|
313
341
|
references: @references,
|
|
@@ -316,6 +344,20 @@ module Hecks
|
|
|
316
344
|
)
|
|
317
345
|
end
|
|
318
346
|
|
|
347
|
+
# A fact is filled into the argument of the same name, so the command declares one.
|
|
348
|
+
#
|
|
349
|
+
# @raise [Bluebook::DSL::Malformed] if a needed fact has no attribute to fill
|
|
350
|
+
def refuse_undeclared_needs!
|
|
351
|
+
declared = attributes.map { |attribute| attribute.name.to_s }
|
|
352
|
+
missing = @needs.reject { |fact| declared.include?(fact.to_s) }
|
|
353
|
+
return if missing.empty?
|
|
354
|
+
|
|
355
|
+
raise Malformed,
|
|
356
|
+
"#{@name} needs :#{missing.first} but declares no attribute :#{missing.first} " \
|
|
357
|
+
"for the runtime to fill — add `attribute :#{missing.first}, <type>`"
|
|
358
|
+
end
|
|
359
|
+
private :refuse_undeclared_needs!
|
|
360
|
+
|
|
319
361
|
# Evaluates a `command` block against a fresh builder and returns what it built.
|
|
320
362
|
#
|
|
321
363
|
# @param name [String] the command's name
|
|
@@ -40,6 +40,7 @@ module Hecks
|
|
|
40
40
|
case rule.strategy
|
|
41
41
|
when "collapse_whitespace" then map_outside_strings(text) { |segment| segment.gsub(/\s+/, " ") }
|
|
42
42
|
when "replace" then replace(text, rule)
|
|
43
|
+
when "scale_call" then scale_call(text, rule)
|
|
43
44
|
else
|
|
44
45
|
raise ArgumentError, "#{rule.strategy.inspect} is not a linked normalisation strategy"
|
|
45
46
|
end
|
|
@@ -55,6 +56,17 @@ module Hecks
|
|
|
55
56
|
end
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
# Folds a duration written as a call on a whole number into seconds: `days(730)` becomes
|
|
60
|
+
# `63072000` when the rule's token is `days` and its replacement `86400`. A call on anything
|
|
61
|
+
# but a literal whole number is left as written, and a method call (`x.days(3)`) is not a
|
|
62
|
+
# duration.
|
|
63
|
+
def scale_call(text, rule)
|
|
64
|
+
call = /(?<![[:alnum:]_.])#{Regexp.escape(rule.source_token)}\(\s*(\d+)\s*\)/
|
|
65
|
+
map_outside_strings(text) do |segment|
|
|
66
|
+
segment.gsub(call) { (Regexp.last_match(1).to_i * rule.replacement.to_i).to_s }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
58
70
|
# Yields each run of `text` outside quoted literals, copying quoted runs through verbatim.
|
|
59
71
|
# Quote-blind rewriting would change what a predicate compares a string against.
|
|
60
72
|
# Handles `"` and `'`; an unterminated quote is passed through raw.
|
|
@@ -225,6 +225,27 @@
|
|
|
225
225
|
"replacement": ".size",
|
|
226
226
|
"boundary": "word",
|
|
227
227
|
"position": 2
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"strategy": "scale_call",
|
|
231
|
+
"source_token": "days",
|
|
232
|
+
"replacement": "86400",
|
|
233
|
+
"boundary": "word",
|
|
234
|
+
"position": 3
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"strategy": "scale_call",
|
|
238
|
+
"source_token": "hours",
|
|
239
|
+
"replacement": "3600",
|
|
240
|
+
"boundary": "word",
|
|
241
|
+
"position": 4
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"strategy": "scale_call",
|
|
245
|
+
"source_token": "minutes",
|
|
246
|
+
"replacement": "60",
|
|
247
|
+
"boundary": "word",
|
|
248
|
+
"position": 5
|
|
228
249
|
}
|
|
229
250
|
]
|
|
230
251
|
}
|
|
@@ -83,6 +83,9 @@ module Hecks
|
|
|
83
83
|
# A rule's plain description/canonical pair, for a `given`/`invariant`/`ensures`.
|
|
84
84
|
def rule(row) = { description: text(row[:description]), canonical: text(row[:canonical]) }
|
|
85
85
|
|
|
86
|
+
# One outside fact a command needs, in the shape `Command#to_h` emits it.
|
|
87
|
+
def need(row) = { fact: text(row[:fact]) }
|
|
88
|
+
|
|
86
89
|
# `projects :name, from: :"reference.remote_field"` (ADR 0025), read back as
|
|
87
90
|
# its plain name/reference/remote_field triple.
|
|
88
91
|
def projected_field(row)
|
|
@@ -212,7 +212,8 @@ module Hecks
|
|
|
212
212
|
def described_entry(name, boot, shown)
|
|
213
213
|
<<~RUBY.chomp
|
|
214
214
|
# Usage is answered from the projected chapter alone: no adapter is bound and no
|
|
215
|
-
# database is opened. Only a line that runs a verb boots the domain
|
|
215
|
+
# database is opened. Only a line that runs a verb boots the domain,
|
|
216
|
+
# and it boots from what the usage check already loaded.
|
|
216
217
|
domain = #{boot}
|
|
217
218
|
program = "#{shown}"
|
|
218
219
|
described = begin
|
|
@@ -226,10 +227,13 @@ module Hecks
|
|
|
226
227
|
)
|
|
227
228
|
unless text
|
|
228
229
|
runtime = begin
|
|
229
|
-
Hecks.
|
|
230
|
+
Hecks.boot_described(described, install_doors: false)
|
|
230
231
|
rescue StandardError => e
|
|
231
232
|
abort "cannot open #{name}: \#{e.message.lines.first.strip}"
|
|
232
233
|
end
|
|
234
|
+
# A question the world lists under `streams`, given `--stream`, tails until interrupted.
|
|
235
|
+
streamed = Hecks::Doors::CliRunner.stream(runtime: runtime, argv: ARGV, program: program)
|
|
236
|
+
exit streamed unless streamed.nil?
|
|
233
237
|
text, status, reason = Hecks::Doors::CliRunner.call(
|
|
234
238
|
runtime: runtime, argv: ARGV, program: program
|
|
235
239
|
)
|
|
@@ -20,6 +20,9 @@ Hecks.bluebook "Deploy" do
|
|
|
20
20
|
attribute :timeout, Timeout
|
|
21
21
|
attribute :database, Database
|
|
22
22
|
attribute :web, Web
|
|
23
|
+
attribute :dispatch, Dispatch, optional: true
|
|
24
|
+
attribute :handler_module, HandlerModule, optional: true
|
|
25
|
+
attribute :secret_env, SecretEnv, optional: true
|
|
23
26
|
|
|
24
27
|
value_object "DomainName" do
|
|
25
28
|
attribute :value, String
|
|
@@ -47,6 +50,27 @@ Hecks.bluebook "Deploy" do
|
|
|
47
50
|
attribute :value, String, one_of: ["None", "Rust"]
|
|
48
51
|
end
|
|
49
52
|
|
|
53
|
+
# "Rust" (default) or "None": a domain with no `.bluebook` command surface to dispatch through
|
|
54
|
+
# (a webhook that dispatches straight into Ruby) skips the rust/host dispatch Lambda, and its
|
|
55
|
+
# WebFunction becomes its only Lambda. That WebFunction's `lambda_handler.rb` defines the module
|
|
56
|
+
# `handler_module` names, so `dispatch "None"` must name one.
|
|
57
|
+
value_object "Dispatch" do
|
|
58
|
+
attribute :value, String, one_of: ["Rust", "None"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The module the domain's `lambda_handler.rb` defines for its WebFunction.
|
|
62
|
+
value_object "HandlerModule" do
|
|
63
|
+
attribute :value, String
|
|
64
|
+
invariant("a handler module is named") { !value.to_s.empty? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The env var the WebFunction's `lambda_handler.rb` finds the fetched secret's plaintext under,
|
|
68
|
+
# once it resolves `<name>_ARN`.
|
|
69
|
+
value_object "SecretEnv" do
|
|
70
|
+
attribute :value, String
|
|
71
|
+
invariant("a secret env var is named") { !value.to_s.empty? }
|
|
72
|
+
end
|
|
73
|
+
|
|
50
74
|
# 128-10240 MB, AWS Lambda's own floor and ceiling.
|
|
51
75
|
value_object "Memory" do
|
|
52
76
|
attribute :value, Integer
|
|
@@ -73,6 +97,13 @@ Hecks.bluebook "Deploy" do
|
|
|
73
97
|
attribute :timeout, Timeout
|
|
74
98
|
attribute :database, Database
|
|
75
99
|
attribute :web, Web
|
|
100
|
+
attribute :dispatch, Dispatch, optional: true
|
|
101
|
+
attribute :handler_module, HandlerModule, optional: true
|
|
102
|
+
attribute :secret_env, SecretEnv, optional: true
|
|
103
|
+
|
|
104
|
+
given('dispatch "None" names its handler_module: add handler_module "YourModuleName" for the module lambda_handler.rb defines') do
|
|
105
|
+
dispatch.unset? || dispatch.value != "None" || handler_module.set?
|
|
106
|
+
end
|
|
76
107
|
|
|
77
108
|
sets :domain
|
|
78
109
|
sets :region
|
|
@@ -80,6 +111,9 @@ Hecks.bluebook "Deploy" do
|
|
|
80
111
|
sets :timeout
|
|
81
112
|
sets :database
|
|
82
113
|
sets :web
|
|
114
|
+
sets :dispatch
|
|
115
|
+
sets :handler_module
|
|
116
|
+
sets :secret_env
|
|
83
117
|
|
|
84
118
|
emits "LambdaTargetDeclared"
|
|
85
119
|
end
|
|
@@ -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
|
|
@@ -15,9 +15,11 @@ module Hecks
|
|
|
15
15
|
# - **run_keys** mints the `run` key of a creating command that was given none.
|
|
16
16
|
# - **failure_states** are the lifecycle states `--wait` reports as a failure (exit 1).
|
|
17
17
|
# - **names** maps a launcher name to the command it stands for (see `CliProjector`).
|
|
18
|
+
# - **streams** lists the questions `--stream` may tail, one JSON line per new entry.
|
|
18
19
|
module LauncherOptions
|
|
19
20
|
SETTING = "launcher".freeze
|
|
20
21
|
WAIT = "--wait".freeze
|
|
22
|
+
STREAM = "--stream".freeze
|
|
21
23
|
RUN_KEY = "run.value".freeze
|
|
22
24
|
|
|
23
25
|
module_function
|
|
@@ -67,6 +69,27 @@ module Hecks
|
|
|
67
69
|
[rest, wait]
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
# Whether a question may be tailed with `--stream`: the chapter's `launcher` setting lists it
|
|
73
|
+
# under `streams`, by the question's own name.
|
|
74
|
+
#
|
|
75
|
+
# @param launcher [Hash, nil] the chapter's `launcher` setting
|
|
76
|
+
# @param spec [Hash] the question's projected spec
|
|
77
|
+
# @return [Boolean] true for a question the setting names
|
|
78
|
+
def streams?(launcher, spec)
|
|
79
|
+
return false unless launcher && spec[:kind] == :query
|
|
80
|
+
|
|
81
|
+
Array(launcher[:streams]).map(&:to_s).include?(spec[:verb].to_s.split(/[.:]+/).last)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Takes `--stream` out of a question's words.
|
|
85
|
+
#
|
|
86
|
+
# @param words [Array<String>] the words after the question
|
|
87
|
+
# @return [Array(Array<String>, Boolean)] the remaining words, and whether `--stream` was
|
|
88
|
+
# given
|
|
89
|
+
def take_stream(words)
|
|
90
|
+
[words.reject { |word| word == STREAM }, words.include?(STREAM)]
|
|
91
|
+
end
|
|
92
|
+
|
|
70
93
|
# @api private
|
|
71
94
|
def wait_word?(word) = word == WAIT || word.start_with?("#{WAIT}=")
|
|
72
95
|
|
|
@@ -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,51 @@
|
|
|
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_blocks
|
|
49
|
+
title: comment blocks, no run of comment lines past ADR 0075's cap
|
|
50
|
+
run: [bundle, exec, exe/hecks, check_comments, "paths=lib,spec,examples", only=long_block, --wait]
|
|
51
|
+
blocked: a comment block ran past the cap; shorten it.
|