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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16dca2d33433ad6c6cd98b8dee40f652df3d84c7bb93df5cd1f7734b99542c08
|
|
4
|
+
data.tar.gz: '084b92e2ad954b89a488d44997d754cd15388136f502f918f24274fd26cf9e87'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f2b6dda10ee042c6cb1bbe3ae9100a66c35dad25b17a031ab7ca6fd0eaf7ea5960b1a3e52024d5a40c3090ca999f1730db9275f7a5ec8b803e8ae0a465102df
|
|
7
|
+
data.tar.gz: 8aebabbb0a023e9c2ff989df18f665924ec9acf4043a922bcb995588fb424683d8d44bea2e02f2076932beae2b27d63da097153c054131aa4232c132123d33eb
|
data/exe/hecks
CHANGED
|
@@ -21,10 +21,16 @@ if LEGACY.include?(ARGV.first)
|
|
|
21
21
|
exit Hecks::CLI.start(ARGV) unless Hecks::CLI.launcher_form?(ARGV)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# These verbs keep nothing worth a database, so they run on Memory unless told otherwise, and
|
|
25
|
+
# print nothing when they end well.
|
|
26
|
+
MEMORY_VERBS = %w[console].freeze
|
|
27
|
+
ENV["HECKS_ENVIRONMENT"] ||= "memory" if MEMORY_VERBS.include?(ARGV.first)
|
|
28
|
+
|
|
24
29
|
require "hecks"
|
|
25
30
|
|
|
26
31
|
# 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
|
|
32
|
+
# database is opened. Only a line that runs a verb boots the domain,
|
|
33
|
+
# and it boots from what the usage check already loaded.
|
|
28
34
|
domain = File.expand_path("../lib/hecks/hecks", __dir__)
|
|
29
35
|
program = "hecks"
|
|
30
36
|
described = begin
|
|
@@ -38,10 +44,13 @@ text, status, reason = Hecks::Doors::CliRunner.usage(
|
|
|
38
44
|
)
|
|
39
45
|
unless text
|
|
40
46
|
runtime = begin
|
|
41
|
-
Hecks.
|
|
47
|
+
Hecks::Doors::LauncherOptions.quietly(hold: MEMORY_VERBS.include?(ARGV.first)) { Hecks.boot_described(described, install_doors: false) }
|
|
42
48
|
rescue StandardError => e
|
|
43
49
|
abort "cannot open Hecks: #{e.message.lines.first.strip}"
|
|
44
50
|
end
|
|
51
|
+
# A question the world lists under `streams`, given `--stream`, tails until interrupted.
|
|
52
|
+
streamed = Hecks::Doors::CliRunner.stream(runtime: runtime, argv: ARGV, program: program)
|
|
53
|
+
exit streamed unless streamed.nil?
|
|
45
54
|
text, status, reason = Hecks::Doors::CliRunner.call(
|
|
46
55
|
runtime: runtime, argv: ARGV, program: program
|
|
47
56
|
)
|
|
@@ -53,4 +62,5 @@ if reason
|
|
|
53
62
|
warn reason
|
|
54
63
|
exit status
|
|
55
64
|
end
|
|
65
|
+
exit 0 if status.zero? && MEMORY_VERBS.include?(ARGV.first)
|
|
56
66
|
status.zero? ? puts(text) : abort(text)
|
|
@@ -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)
|
data/lib/hecks/cli/console.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Hecks
|
|
|
9
9
|
# The command behind `hecks console` and Custodian's `Operation.OpenConsole`: boots a domain
|
|
10
10
|
# (pizzas on Memory by default) and drops into IRB with its door installed, for dispatching a
|
|
11
11
|
# real command by hand. No argument needs no database (ADR 0073); a domain directory boots as
|
|
12
|
-
# it is wired, which may bind PostgresEra.
|
|
12
|
+
# it is wired, which may bind PostgresEra (examples/directory does).
|
|
13
13
|
module Console
|
|
14
14
|
PIZZAS = File.expand_path("../../../examples/pizzas", __dir__)
|
|
15
15
|
|
|
@@ -25,14 +25,27 @@ module Hecks
|
|
|
25
25
|
# @param domain [String, nil] a domain directory; the bundled pizzas domain when nil
|
|
26
26
|
# @param launcher [#call] starts the interactive session; IRB unless a caller stands in
|
|
27
27
|
# @return [Runtime] the runtime the session ran against
|
|
28
|
-
def call(domain = nil, launcher: -> {
|
|
28
|
+
def call(domain = nil, launcher: -> { irb_session })
|
|
29
29
|
runtime = domain ? Hecks.boot(domain) : Hecks.boot_files(DEFAULT_FILES)
|
|
30
30
|
puts overview(runtime)
|
|
31
|
-
puts banner
|
|
31
|
+
puts banner(domain)
|
|
32
32
|
launcher.call
|
|
33
33
|
runtime
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Starts IRB with an empty command line. IRB reads `ARGV` for a script to run, and a
|
|
37
|
+
# launcher's `ARGV` holds its own words (`console`, `subject=<domain>`), which IRB would
|
|
38
|
+
# open as a file and then exit without a prompt.
|
|
39
|
+
#
|
|
40
|
+
# @return [void] when the session ends; `ARGV` is restored
|
|
41
|
+
def irb_session
|
|
42
|
+
words = ARGV.dup
|
|
43
|
+
ARGV.clear
|
|
44
|
+
IRB.start(__FILE__)
|
|
45
|
+
ensure
|
|
46
|
+
ARGV.replace(words)
|
|
47
|
+
end
|
|
48
|
+
|
|
36
49
|
# @param runtime [Runtime] a booted domain
|
|
37
50
|
# @return [String] each chapter's vision and its aggregates' commands
|
|
38
51
|
def overview(runtime)
|
|
@@ -46,16 +59,19 @@ module Hecks
|
|
|
46
59
|
end.join
|
|
47
60
|
end
|
|
48
61
|
|
|
49
|
-
# @
|
|
50
|
-
|
|
62
|
+
# @param domain [String, nil] a domain directory, or nil for the bundled pizzas domain
|
|
63
|
+
# @return [String] the version, and for the pizzas domain a first session to try
|
|
64
|
+
def banner(domain = nil)
|
|
65
|
+
return "\nhecks #{Hecks::VERSION}\n\n" if domain
|
|
66
|
+
|
|
51
67
|
<<~BANNER
|
|
52
68
|
|
|
53
69
|
hecks #{Hecks::VERSION}
|
|
54
70
|
|
|
55
71
|
try:
|
|
56
|
-
order = Order.create_pizza!(name:
|
|
57
|
-
order.add_topping!(topping:
|
|
58
|
-
order.purchase!(customer_name:
|
|
72
|
+
order = Order.create_pizza!(name: "Margherita", pizza: { price_cents: { cents: 1200 }, size: "large" })
|
|
73
|
+
order.add_topping!(topping: "Basil", amount: 3)
|
|
74
|
+
order.purchase!(customer_name: "Chris", amount: { cents: 1200 })
|
|
59
75
|
order.status
|
|
60
76
|
order.events.last
|
|
61
77
|
|
|
@@ -103,7 +103,7 @@ module Hecks
|
|
|
103
103
|
label = executable || "#{path}/#{snake}"
|
|
104
104
|
file = File.join(root, label)
|
|
105
105
|
text = launcher(path, name, program, executable: executable, legacy: setting[:legacy],
|
|
106
|
-
opted: !setting.empty?)
|
|
106
|
+
memory_verbs: setting[:memory_verbs], opted: !setting.empty?)
|
|
107
107
|
return skip(path, "#{label} is a directory") if File.directory?(file)
|
|
108
108
|
|
|
109
109
|
return :drifted if check && !(File.file?(file) && File.read(file) == text)
|
|
@@ -150,10 +150,11 @@ module Hecks
|
|
|
150
150
|
# @param executable [String, nil] the file's path under the root when it is not beside the
|
|
151
151
|
# domain; its program name is then that file's basename, and its header names the generator
|
|
152
152
|
# @param legacy [Array<String>, nil] verbs an executable hands to `Hecks::CLI` first
|
|
153
|
+
# @param memory_verbs [Array<String>, nil] verbs run on Memory unless HECKS_ENVIRONMENT is set
|
|
153
154
|
# @param opted [Boolean] whether the chapter's world declares a `launcher` setting
|
|
154
155
|
# @return [String] the Ruby source
|
|
155
156
|
# @raise [ArgumentError] if a name, path, executable or legacy verb is not plain
|
|
156
|
-
def launcher(path, name, program, executable: nil, legacy: nil, opted: !executable.nil?)
|
|
157
|
+
def launcher(path, name, program, executable: nil, legacy: nil, memory_verbs: nil, opted: !executable.nil?)
|
|
157
158
|
plain!("chapter name", name, NAME)
|
|
158
159
|
plain!("domain path", path, PATH)
|
|
159
160
|
snake = Naming.snake(name)
|
|
@@ -174,9 +175,10 @@ module Hecks
|
|
|
174
175
|
where = shown = "#{path}/#{snake}"
|
|
175
176
|
end
|
|
176
177
|
|
|
177
|
-
handoff = legacy_handoff(Array(legacy)) if executable
|
|
178
|
+
handoff = legacy_handoff(Array(legacy)) + memory_default(Array(memory_verbs)) if executable
|
|
178
179
|
encoding = opted ? ENCODING : ""
|
|
179
180
|
ending = opted ? OPTED_ENDING : PLAIN_ENDING
|
|
181
|
+
ending = quiet_ending(ending) if executable && !Array(memory_verbs).empty?
|
|
180
182
|
|
|
181
183
|
<<~RUBY
|
|
182
184
|
#!/usr/bin/env ruby
|
|
@@ -194,7 +196,7 @@ module Hecks
|
|
|
194
196
|
#{handoff}
|
|
195
197
|
require "hecks"
|
|
196
198
|
|
|
197
|
-
#{entry(name, boot, shown, opted)}
|
|
199
|
+
#{entry(name, boot, shown, opted, quiet: !Array(memory_verbs).empty?)}
|
|
198
200
|
#{ending}
|
|
199
201
|
RUBY
|
|
200
202
|
end
|
|
@@ -203,16 +205,19 @@ module Hecks
|
|
|
203
205
|
# @return [String] the launcher's middle: for an opted-in launcher, usage answered from the
|
|
204
206
|
# projection and a boot only for a line that runs a verb; for any other, the boot and
|
|
205
207
|
# dispatch every launcher has always had, byte for byte
|
|
206
|
-
def entry(name, boot, shown, opted)
|
|
207
|
-
opted ? described_entry(name, boot, shown) : plain_entry(name, boot, shown)
|
|
208
|
+
def entry(name, boot, shown, opted, quiet: false)
|
|
209
|
+
opted ? described_entry(name, boot, shown, quiet: quiet) : plain_entry(name, boot, shown)
|
|
208
210
|
end
|
|
209
211
|
|
|
210
212
|
# @api private
|
|
211
213
|
# @return [String] the opted-in launcher's middle
|
|
212
|
-
def described_entry(name, boot, shown)
|
|
214
|
+
def described_entry(name, boot, shown, quiet: false)
|
|
215
|
+
started = "Hecks.boot_described(described, install_doors: false)"
|
|
216
|
+
started = "Hecks::Doors::LauncherOptions.quietly(hold: MEMORY_VERBS.include?(ARGV.first)) { #{started} }" if quiet
|
|
213
217
|
<<~RUBY.chomp
|
|
214
218
|
# 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
|
|
219
|
+
# database is opened. Only a line that runs a verb boots the domain,
|
|
220
|
+
# and it boots from what the usage check already loaded.
|
|
216
221
|
domain = #{boot}
|
|
217
222
|
program = "#{shown}"
|
|
218
223
|
described = begin
|
|
@@ -226,10 +231,13 @@ module Hecks
|
|
|
226
231
|
)
|
|
227
232
|
unless text
|
|
228
233
|
runtime = begin
|
|
229
|
-
|
|
234
|
+
#{started}
|
|
230
235
|
rescue StandardError => e
|
|
231
236
|
abort "cannot open #{name}: \#{e.message.lines.first.strip}"
|
|
232
237
|
end
|
|
238
|
+
# A question the world lists under `streams`, given `--stream`, tails until interrupted.
|
|
239
|
+
streamed = Hecks::Doors::CliRunner.stream(runtime: runtime, argv: ARGV, program: program)
|
|
240
|
+
exit streamed unless streamed.nil?
|
|
233
241
|
text, status, reason = Hecks::Doors::CliRunner.call(
|
|
234
242
|
runtime: runtime, argv: ARGV, program: program
|
|
235
243
|
)
|
|
@@ -260,6 +268,31 @@ module Hecks
|
|
|
260
268
|
raise ArgumentError, "#{what} #{value.to_s.inspect} is not a plain word or path"
|
|
261
269
|
end
|
|
262
270
|
|
|
271
|
+
# @api private
|
|
272
|
+
# @param ending [String] the launcher's closing lines
|
|
273
|
+
# @return [String] the same lines, except that a memory verb that ended well prints nothing: a
|
|
274
|
+
# person is at it, so the settled record is noise after the session
|
|
275
|
+
def quiet_ending(ending)
|
|
276
|
+
ending.sub("status.zero? ? puts(text)",
|
|
277
|
+
"exit 0 if status.zero? && MEMORY_VERBS.include?(ARGV.first)\nstatus.zero? ? puts(text)")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @api private
|
|
281
|
+
# @return [String] the lines that default the listed verbs to Memory, or nothing
|
|
282
|
+
def memory_default(verbs)
|
|
283
|
+
return "" if verbs.empty?
|
|
284
|
+
|
|
285
|
+
verbs.each { |verb| plain!("memory verb", verb, VERB) }
|
|
286
|
+
|
|
287
|
+
<<~RUBY
|
|
288
|
+
|
|
289
|
+
# These verbs keep nothing worth a database, so they run on Memory unless told otherwise, and
|
|
290
|
+
# print nothing when they end well.
|
|
291
|
+
MEMORY_VERBS = %w[#{verbs.join(' ')}].freeze
|
|
292
|
+
ENV["HECKS_ENVIRONMENT"] ||= "memory" if MEMORY_VERBS.include?(ARGV.first)
|
|
293
|
+
RUBY
|
|
294
|
+
end
|
|
295
|
+
|
|
263
296
|
# @api private
|
|
264
297
|
# @return [String] the lines that hand the legacy verbs to `Hecks::CLI`, or nothing
|
|
265
298
|
def legacy_handoff(legacy)
|
|
@@ -7,9 +7,12 @@ require_relative "../fuzzing"
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module CLI
|
|
9
9
|
# The command behind `hecks seed_semantics_corpus`: seeds (or deliberately re-seeds) the
|
|
10
|
-
# `expect` block of
|
|
10
|
+
# `expect` block of corpus fixtures from the Ruby runtime. Review the result
|
|
11
11
|
# against `docs/semantics/bluebook-semantics.md` before committing.
|
|
12
12
|
#
|
|
13
|
+
# Seeds `spec/corpus/semantics/` and the conformance corpus ({FULL_SCRIPTS} plus
|
|
14
|
+
# `spec/corpus/rust_conformance/`, whose `expect` also freezes more).
|
|
15
|
+
#
|
|
13
16
|
# hecks seed_semantics_corpus # fill fixtures missing an expect
|
|
14
17
|
# SEED=refusal_kind_lifecycle hecks seed_semantics_corpus # re-seed one, deliberately
|
|
15
18
|
#
|
|
@@ -18,6 +21,15 @@ module Hecks
|
|
|
18
21
|
module SeedSemanticsCorpus
|
|
19
22
|
module_function
|
|
20
23
|
|
|
24
|
+
# Full corpus scripts held to the conformance bar, keyed by file, valued by domain.
|
|
25
|
+
FULL_SCRIPTS = {
|
|
26
|
+
"spec/corpus/banking.json" => "examples/banking",
|
|
27
|
+
"spec/corpus/chess.json" => "examples/chess"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# The `expect` keys a conformance fixture freezes beyond the semantics three.
|
|
31
|
+
CONFORMANCE_KEYS = %w[queries sagas dry_runs reactions].freeze
|
|
32
|
+
|
|
21
33
|
# Seeds the fixtures that need it and reports which.
|
|
22
34
|
#
|
|
23
35
|
# @param root [String] the checkout whose `spec/corpus/semantics/` is seeded
|
|
@@ -26,12 +38,13 @@ module Hecks
|
|
|
26
38
|
# @return [Integer] the exit status, always 0
|
|
27
39
|
def call(root:, env: ENV, out: $stdout)
|
|
28
40
|
only = env.fetch("SEED", nil)
|
|
29
|
-
seeded =
|
|
41
|
+
seeded = corpus_paths(root).filter_map do |path, domain, full|
|
|
30
42
|
fixture = JSON.parse(File.read(path))
|
|
31
43
|
name = File.basename(path, ".json")
|
|
32
44
|
next if only ? only != name : fixture.key?("expect")
|
|
33
45
|
|
|
34
|
-
fixture["
|
|
46
|
+
fixture["domain"] ||= domain if domain
|
|
47
|
+
fixture["expect"] = expectation_for(root, fixture, full: full)
|
|
35
48
|
File.write(path, "#{JSON.pretty_generate(fixture)}\n")
|
|
36
49
|
name
|
|
37
50
|
end
|
|
@@ -39,19 +52,35 @@ module Hecks
|
|
|
39
52
|
0
|
|
40
53
|
end
|
|
41
54
|
|
|
55
|
+
# @param root [String] the checkout
|
|
56
|
+
# @return [Array<Array>] `[path, domain_to_add_or_nil, full_expect?]` for every seedable file
|
|
57
|
+
def corpus_paths(root)
|
|
58
|
+
semantics = Dir.glob(File.join(root, "spec/corpus/semantics", "*.json")).map { |p| [p, nil, false] }
|
|
59
|
+
conformance = Dir.glob(File.join(root, "spec/corpus/rust_conformance", "*.json")).map { |p| [p, nil, true] }
|
|
60
|
+
scripts = FULL_SCRIPTS.map { |file, domain| [File.join(root, file), domain, true] }
|
|
61
|
+
semantics + conformance + scripts
|
|
62
|
+
end
|
|
63
|
+
|
|
42
64
|
# Replays the fixture's script against the Ruby runtime. Each refusal's `"kind"` is reduced
|
|
43
65
|
# to its class name; `occurred_at` is stripped from events.
|
|
44
66
|
#
|
|
45
67
|
# @param root [String] the checkout the fixture's domain path is relative to
|
|
46
68
|
# @param fixture [Hash] the parsed fixture: `domain` and `steps`
|
|
47
|
-
# @
|
|
48
|
-
|
|
69
|
+
# @param full [Boolean] also freeze queries, sagas, dry runs, reactions (conformance)
|
|
70
|
+
# @return [Hash{String => Array}] its `"expect"` block: refusals, instances, events, and
|
|
71
|
+
# with `full` the {CONFORMANCE_KEYS}
|
|
72
|
+
def expectation_for(root, fixture, full: false)
|
|
49
73
|
result = Hecks::Fuzzing::Replay.call(File.join(root, fixture["domain"]), fixture["steps"])
|
|
50
|
-
{
|
|
74
|
+
expect = {
|
|
51
75
|
"refusals" => JSON.parse(JSON.generate(result[:refusals])).each { |r| r["kind"] = r["kind"]&.split("::")&.last },
|
|
52
76
|
"instances" => JSON.parse(JSON.generate(result[:instances])),
|
|
53
77
|
"events" => JSON.parse(JSON.generate(result[:events])).each { |e| e.delete("occurred_at") }
|
|
54
78
|
}
|
|
79
|
+
return expect unless full
|
|
80
|
+
|
|
81
|
+
# instances_at is Replay's per-query snapshot for the property harness, not an outcome.
|
|
82
|
+
expect["queries"] = JSON.parse(JSON.generate(result[:queries].map { |q| q.except(:instances_at) }))
|
|
83
|
+
expect.merge!(CONFORMANCE_KEYS.drop(1).to_h { |k| [k, JSON.parse(JSON.generate(result[k.to_sym]))] })
|
|
55
84
|
end
|
|
56
85
|
|
|
57
86
|
# @param out [IO] where the report goes
|
|
@@ -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
|