hecks 3.2.1 → 3.4.0
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/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +21 -23
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/interview_draft.rb +88 -33
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +198 -0
- data/lib/hecks/corpus.rb +3 -5
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door_scope.rb +2 -1
- data/lib/hecks/doors.rb +0 -17
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +1 -4
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +0 -5
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +17 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +15 -2
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/loader.rb +6 -7
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/runtime.rb +6 -11
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +40 -18
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +3 -10
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +53 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +1 -3
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +151 -0
- data/lib/hecks.rb +6 -12
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +30 -15
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +8 -19
- data/rust/codegen/src/bridging.rs +7 -7
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +20 -9
- data/rust/codegen/src/naming.rs +4 -2
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +99 -2
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -4
- data/rust/parser/src/lex.rs +1 -1
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/hecksagon.rs +15 -24
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/kernel/cli.rs +16 -6
- data/rust/src/kernel/needs.rs +104 -9
- metadata +22 -9
- data/lib/hecks/facade.rb +0 -31
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6180e44456a48f89b02b8812b4f327fe1642f98756e8e4c0b1bb295a04f826b
|
|
4
|
+
data.tar.gz: 6bc10cd76fb9c112ce9d7a5b82295a2e2a4c101d1b10b195ab48cd1afefd75c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56cfe2cebc08a7aac900d8ef700590aa66855ea0baf28eebb9096f0f74c4d51da5ef42167cffdc6f1bcc733c3cacf8e329bf1352821655247c36e17275614123
|
|
7
|
+
data.tar.gz: 37cc4287ecf68af46b507dff79b3041de0a6e28fe2a1a7e5d89bea393b74d5f84eead4e11509c0f16d47d11ec8abfe9b292d73ec14845d9466e086dcec3bf350
|
|
@@ -101,6 +101,7 @@ module Hecks
|
|
|
101
101
|
order_by: [:order_by, :order_by],
|
|
102
102
|
limit: [:limit, :limit],
|
|
103
103
|
returns: [:returns, :plain],
|
|
104
|
+
needs: [:needs, [:each, :need]],
|
|
104
105
|
# Held by the language as an open map, so every one of these reads the
|
|
105
106
|
# same way and a ninth option needs no new field on either side.
|
|
106
107
|
offset: [:offset, [:option, :offset]],
|
|
@@ -111,7 +112,7 @@ module Hecks
|
|
|
111
112
|
},
|
|
112
113
|
rows: { wheres: :where_rows, options: :option_rows },
|
|
113
114
|
reads: { attributes: [:each, :shape_field], wheres: [:each, :where_clause],
|
|
114
|
-
order_by: [:call, :order_by], limit: [:call, :limit] },
|
|
115
|
+
needs: [:each, :need], order_by: [:call, :order_by], limit: [:call, :limit] },
|
|
115
116
|
derived: {
|
|
116
117
|
position: :walk,
|
|
117
118
|
order_field: [:folded, :order_by, :field],
|
|
@@ -75,6 +75,7 @@ module Hecks
|
|
|
75
75
|
["Query", "where"] => :where_impl,
|
|
76
76
|
["Query", "order_by"] => :order_by_impl,
|
|
77
77
|
["Query", "authorize"] => :authorize_impl,
|
|
78
|
+
["Query", "needs"] => :needs_impl,
|
|
78
79
|
["ValueObject", "attribute"] => :attribute_impl,
|
|
79
80
|
["ValueObject", "one_of"] => :one_of_impl,
|
|
80
81
|
["ValueObject", "invariant"] => :invariant_impl,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "word_gate"
|
|
2
|
+
require_relative "need_word"
|
|
2
3
|
module Hecks
|
|
3
4
|
module Bluebook
|
|
4
5
|
module DSL
|
|
@@ -10,6 +11,7 @@ module Hecks
|
|
|
10
11
|
include AttributeCollector
|
|
11
12
|
include RuleReference
|
|
12
13
|
include WordGate
|
|
14
|
+
include NeedWord
|
|
13
15
|
|
|
14
16
|
# Sentinel for "this keyword was never passed" — distinct from Ruby's own
|
|
15
17
|
# nil/false, so `to: false` doesn't get treated as absent.
|
|
@@ -67,31 +69,6 @@ module Hecks
|
|
|
67
69
|
# @return [String] the description as stored
|
|
68
70
|
def goal(value) = @goal = value
|
|
69
71
|
|
|
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
|
-
|
|
95
72
|
# Names where a concept adopted from a canonical source came from.
|
|
96
73
|
#
|
|
97
74
|
# @param from [Object] the canonical source, captured exactly as written
|
|
@@ -344,20 +321,6 @@ module Hecks
|
|
|
344
321
|
)
|
|
345
322
|
end
|
|
346
323
|
|
|
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
|
-
|
|
361
324
|
# Evaluates a `command` block against a fresh builder and returns what it built.
|
|
362
325
|
#
|
|
363
326
|
# @param name [String] the command's name
|
|
@@ -57,9 +57,12 @@ module Hecks
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
60
|
+
# Spellings of `attaches` that 3.4.0 removed, each with the form that replaces it. They
|
|
61
|
+
# are refused by name so an old hecksagon fails loudly, never as a stray default bind.
|
|
62
|
+
REMOVED_WORDS = {
|
|
63
|
+
"uses_framework" => ->(name) { "attaches #{name.to_s.inspect}" },
|
|
64
|
+
"uses_embryonaut_bluebook" => ->(name) { "attaches #{name.to_s.inspect}, from: :vendor" }
|
|
65
|
+
}.freeze
|
|
63
66
|
|
|
64
67
|
attr_reader :binds, :subscriptions, :attachments
|
|
65
68
|
|
|
@@ -100,20 +103,6 @@ module Hecks
|
|
|
100
103
|
from == :vendor ? attach_vendored(name) : attach_gem(name)
|
|
101
104
|
end
|
|
102
105
|
|
|
103
|
-
# The deprecated spelling of `attaches "Name"` for a framework member.
|
|
104
|
-
def uses_framework(name)
|
|
105
|
-
deprecated_word("uses_framework", "attaches #{name.to_s.inspect}")
|
|
106
|
-
@attachments << Attachment.new(name: name.to_s, source: :gem)
|
|
107
|
-
self.class.with_real_modules { Hecks::Framework.load!(name) }
|
|
108
|
-
Hecks.current_registry&.mark_bounded(name.to_s)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# The deprecated spelling of `attaches "name", from: :vendor`.
|
|
112
|
-
def uses_embryonaut_bluebook(name)
|
|
113
|
-
deprecated_word("uses_embryonaut_bluebook", "attaches #{name.to_s.inspect}, from: :vendor")
|
|
114
|
-
attach_vendored(name)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
106
|
# Declares a port at the hecksagon's root — the chapter as a whole, not one aggregate.
|
|
118
107
|
def port_impl(name, &block)
|
|
119
108
|
bluebook_ir = Hecks.current_registry.bluebook(@domain) or
|
|
@@ -170,15 +159,11 @@ module Hecks
|
|
|
170
159
|
end
|
|
171
160
|
private :attach_vendored
|
|
172
161
|
|
|
173
|
-
# Tells the author that a word is going away.
|
|
174
|
-
def deprecated_word(word, instead)
|
|
175
|
-
warn "[hecks] `#{word}` is deprecated and is removed in #{REMOVAL}; use `#{instead}`"
|
|
176
|
-
end
|
|
177
|
-
private :deprecated_word
|
|
178
|
-
|
|
179
162
|
# Records any verb the grammar doesn't own as a domain-wide default bind, e.g. bare
|
|
180
163
|
# `persisted_by "Heki"` at the top of a block, applied unless an aggregate overrides it.
|
|
181
164
|
def method_missing(verb, *args, **kwargs, &block)
|
|
165
|
+
refuse_removed_word(verb, args.first)
|
|
166
|
+
|
|
182
167
|
# Grammar words (like `port`) get first refusal via explicit dispatch; only when that's
|
|
183
168
|
# not admitted does the open-ended `persisted_by`-style bind vocabulary below apply.
|
|
184
169
|
result = word_gate_dispatch(verb, args, kwargs, block)
|
|
@@ -191,6 +176,19 @@ module Hecks
|
|
|
191
176
|
self
|
|
192
177
|
end
|
|
193
178
|
|
|
179
|
+
# Raises for a word 3.4.0 removed, naming the spelling that replaces it.
|
|
180
|
+
#
|
|
181
|
+
# @param verb [Symbol] the word the hecksagon block called
|
|
182
|
+
# @param name [Object, nil] its first argument, echoed into the replacement
|
|
183
|
+
# @raise [Malformed] when the word is one of `REMOVED_WORDS`
|
|
184
|
+
def refuse_removed_word(verb, name)
|
|
185
|
+
instead = REMOVED_WORDS[verb.to_s]
|
|
186
|
+
return unless instead
|
|
187
|
+
|
|
188
|
+
raise Malformed, "`#{verb}` was removed in 3.4.0; use `#{instead.call(name)}`"
|
|
189
|
+
end
|
|
190
|
+
private :refuse_removed_word
|
|
191
|
+
|
|
194
192
|
def respond_to_missing?(_name, _include_private = false) = true
|
|
195
193
|
|
|
196
194
|
# Evaluates a `Hecks.hecksagon` block with bare `Domain::Aggregate` constants resolving to
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
module DSL
|
|
4
|
+
# The `needs` word, shared by the builders of the two declarations that may use it: a
|
|
5
|
+
# command and a query (ADR 0081). The including builder holds `@name` and `@needs` (an
|
|
6
|
+
# empty list) and answers `attributes`.
|
|
7
|
+
module NeedWord
|
|
8
|
+
# The outside facts a declaration may need, each the name of the attribute the runtime
|
|
9
|
+
# fills when the caller leaves it out. `now` is the clock port's reading in epoch seconds;
|
|
10
|
+
# `today` is the day it falls in, whole days since the epoch in UTC.
|
|
11
|
+
NEEDABLE_FACTS = %i[now today].freeze
|
|
12
|
+
|
|
13
|
+
# Declares an outside fact the runtime supplies before any given or filter runs:
|
|
14
|
+
# `needs :now` fills the declaration's own `now` attribute from the clock port when the
|
|
15
|
+
# caller names none.
|
|
16
|
+
#
|
|
17
|
+
# @param fact [Symbol] one of `NEEDABLE_FACTS`
|
|
18
|
+
# @return [Array<Symbol>] the facts declared so far
|
|
19
|
+
# @raise [Bluebook::DSL::Malformed] if the fact is not one the runtime can supply, or is
|
|
20
|
+
# declared twice
|
|
21
|
+
def needs_impl(fact)
|
|
22
|
+
fact = fact.to_sym
|
|
23
|
+
unless NEEDABLE_FACTS.include?(fact)
|
|
24
|
+
raise Malformed,
|
|
25
|
+
"#{@name} needs :#{fact}, which the runtime cannot supply — it supplies " \
|
|
26
|
+
"#{NEEDABLE_FACTS.map { |known| ":#{known}" }.join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
raise Malformed, "#{@name} declares needs :#{fact} twice" if @needs.include?(fact)
|
|
29
|
+
|
|
30
|
+
@needs << fact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A fact is filled into the argument of the same name, so the declaration has one.
|
|
36
|
+
#
|
|
37
|
+
# @raise [Bluebook::DSL::Malformed] if a needed fact has no attribute to fill
|
|
38
|
+
def refuse_undeclared_needs!
|
|
39
|
+
declared = attributes.map { |attribute| attribute.name.to_s }
|
|
40
|
+
missing = @needs.reject { |fact| declared.include?(fact.to_s) }
|
|
41
|
+
return if missing.empty?
|
|
42
|
+
|
|
43
|
+
raise Malformed,
|
|
44
|
+
"#{@name} needs :#{missing.first} but declares no attribute :#{missing.first} " \
|
|
45
|
+
"for the runtime to fill — add `attribute :#{missing.first}, <type>`"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative "word_gate"
|
|
2
|
+
require_relative "need_word"
|
|
2
3
|
module Hecks
|
|
3
4
|
module Bluebook
|
|
4
5
|
module DSL
|
|
@@ -14,11 +15,13 @@ module Hecks
|
|
|
14
15
|
include AttributeCollector
|
|
15
16
|
include QuerySpecification::Common::DSL
|
|
16
17
|
include WordGate
|
|
18
|
+
include NeedWord
|
|
17
19
|
|
|
18
20
|
# @param name [String] the query's name, as written after `query`
|
|
19
21
|
def initialize(name)
|
|
20
22
|
@name = name
|
|
21
23
|
@wheres = []
|
|
24
|
+
@needs = []
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Sets the human-readable description shown for this query.
|
|
@@ -75,6 +78,7 @@ module Hecks
|
|
|
75
78
|
# implements
|
|
76
79
|
def build
|
|
77
80
|
seal_cursor
|
|
81
|
+
refuse_undeclared_needs!
|
|
78
82
|
Query.new(
|
|
79
83
|
name: @name,
|
|
80
84
|
description: @description,
|
|
@@ -87,7 +91,8 @@ module Hecks
|
|
|
87
91
|
authorization: @authorization,
|
|
88
92
|
null_semantics: @null_semantics,
|
|
89
93
|
inspection: @inspection,
|
|
90
|
-
returns: @returns
|
|
94
|
+
returns: @returns,
|
|
95
|
+
needs: @needs
|
|
91
96
|
)
|
|
92
97
|
end
|
|
93
98
|
|
data/lib/hecks/bluebook/query.rb
CHANGED
|
@@ -25,14 +25,14 @@ module Hecks
|
|
|
25
25
|
limit: one(:limit)
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
attr_reader :name, :description, :attributes, :returns
|
|
28
|
+
attr_reader :name, :description, :attributes, :returns, :needs
|
|
29
29
|
|
|
30
30
|
# @param null_semantics [QuerySpecification::Common::NullSemantics, nil] defaults to
|
|
31
31
|
# `NullSemantics.default`
|
|
32
32
|
def initialize(name:, description: nil, attributes: [], wheres: [],
|
|
33
33
|
order_by: nil, limit: nil, offset: nil, cursor: nil,
|
|
34
34
|
authorization: nil, null_semantics: nil,
|
|
35
|
-
inspection: nil, returns: nil)
|
|
35
|
+
inspection: nil, returns: nil, needs: [])
|
|
36
36
|
null_semantics ||= QuerySpecification::Common::NullSemantics.default
|
|
37
37
|
super(wheres: wheres, order_by: order_by, limit: limit, offset: offset, cursor: cursor,
|
|
38
38
|
authorization: authorization,
|
|
@@ -42,6 +42,7 @@ module Hecks
|
|
|
42
42
|
@description = description
|
|
43
43
|
@attributes = attributes
|
|
44
44
|
@returns = returns&.to_s
|
|
45
|
+
@needs = needs
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# The value object a `returns` names, without the `list_of(...)` wrapper.
|
|
@@ -55,13 +56,15 @@ module Hecks
|
|
|
55
56
|
def returns_list? = !@returns.nil? && @returns.start_with?("list_of(")
|
|
56
57
|
|
|
57
58
|
# `extra_options_to_h` (count, median, group_by, scope_to) stays dynamic; `super` covers the
|
|
58
|
-
# declared emission. `returns`
|
|
59
|
-
# returns nothing keeps its wire shape.
|
|
59
|
+
# declared emission. `returns` and `needs` follow, only when the query declares them, so a
|
|
60
|
+
# query that returns nothing and needs nothing keeps its wire shape.
|
|
60
61
|
#
|
|
61
62
|
# @return [Hash] the declared emission merged with `extra_options_to_h`, then `returns`
|
|
63
|
+
# and `needs`
|
|
62
64
|
def to_h
|
|
63
65
|
shape = super.merge(extra_options_to_h)
|
|
64
|
-
|
|
66
|
+
shape = shape.merge(returns: @returns) if @returns
|
|
67
|
+
@needs.empty? ? shape : shape.merge(needs: @needs.map { |fact| { fact: fact.to_s } })
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "../naming"
|
|
2
2
|
require_relative "domain_stub"
|
|
3
|
+
require_relative "interview_shape"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module CLI
|
|
@@ -20,7 +21,11 @@ module Hecks
|
|
|
20
21
|
|
|
21
22
|
# What stands in for a creating command nothing in the interview named.
|
|
22
23
|
STUB_NOTE = " # TODO: no action that creates was accepted; replace Create with the real one.".freeze
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
# One accepted thing: what it is called and identified by, its fields, its transitions, the
|
|
26
|
+
# actions that create it and the others.
|
|
27
|
+
Shape = Struct.new(:name, :identifier, :type, :fields, :steps, :creating, :rest, keyword_init: true)
|
|
28
|
+
private_constant :PATTERN, :STUB_NOTE, :Shape
|
|
24
29
|
|
|
25
30
|
module_function
|
|
26
31
|
|
|
@@ -65,14 +70,21 @@ module Hecks
|
|
|
65
70
|
#
|
|
66
71
|
# @param interview [Object] the booted record
|
|
67
72
|
# @return [Hash] the plain interview this module takes: `reference`, `subject`, `expert`,
|
|
68
|
-
# `exchanges` (`question`, `answer`, `topic`), and `things`, `actions
|
|
69
|
-
# finding with its `number`, `source` and `status`
|
|
73
|
+
# `exchanges` (`question`, `answer`, `topic`), and `things`, `actions`, `rules`,
|
|
74
|
+
# `fields` and `transitions`, each a finding with its `number`, `source` and `status`
|
|
75
|
+
# and the fields of its kind
|
|
70
76
|
def from_record(interview)
|
|
71
77
|
{ reference: plain(interview.reference), subject: plain(interview.subject), expert: plain(interview.expert),
|
|
72
78
|
exchanges: interview.exchanges.map { |ex| slice(ex, :question, :answer, :topic) },
|
|
73
79
|
things: interview.thing_findings.map { |f| slice(f, :number, :name, :identifier, :source, :status) },
|
|
74
|
-
actions: interview.action_findings.map
|
|
75
|
-
|
|
80
|
+
actions: interview.action_findings.map do |f|
|
|
81
|
+
slice(f, :number, :name, :thing, :event, :creates, :takes, :by, :source, :status)
|
|
82
|
+
end,
|
|
83
|
+
rules: interview.rule_findings.map { |f| slice(f, :number, :statement, :source, :status) },
|
|
84
|
+
fields: interview.field_findings.map { |f| slice(f, :number, :thing, :name, :values, :source, :status) },
|
|
85
|
+
transitions: interview.transition_findings.map do |f|
|
|
86
|
+
slice(f, :number, :thing, :action, :to, :from, :source, :status)
|
|
87
|
+
end }
|
|
76
88
|
end
|
|
77
89
|
|
|
78
90
|
# @api private
|
|
@@ -101,7 +113,7 @@ module Hecks
|
|
|
101
113
|
# The aggregates, with their commands: the part a later interview offers to merge.
|
|
102
114
|
# @api private
|
|
103
115
|
def fragment(interview)
|
|
104
|
-
things = accepted(interview, :things)
|
|
116
|
+
things = accepted(interview, :things).uniq { |t| word(t[:name]) }
|
|
105
117
|
actions = accepted(interview, :actions)
|
|
106
118
|
placed = things.map { |t| word(t[:name]) }
|
|
107
119
|
blocks = things.map { |thing| aggregate(interview, thing, actions) }
|
|
@@ -127,24 +139,48 @@ module Hecks
|
|
|
127
139
|
|
|
128
140
|
# @api private
|
|
129
141
|
def aggregate(interview, thing, actions)
|
|
142
|
+
shape = shape_of(interview, thing, actions)
|
|
143
|
+
lines = head_lines(interview, thing, shape)
|
|
144
|
+
lines.concat(command_lines(interview, shape))
|
|
145
|
+
lines.concat(InterviewShape.lifecycle(shape.steps, shape.creating, shape.rest))
|
|
146
|
+
(lines << " end").join("\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# What one accepted thing is made of: its fields, its actions, and its steps.
|
|
150
|
+
# @api private
|
|
151
|
+
def shape_of(interview, thing, actions)
|
|
130
152
|
name = word(thing[:name])
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
identifier = Naming.snake(word(thing[:identifier]))
|
|
154
|
+
mine = InterviewShape.merged_actions(actions.select { |a| word(a[:thing]) == name })
|
|
155
|
+
steps = accepted(interview, :transitions).select { |s| word(s[:thing]) == name }
|
|
156
|
+
found = accepted(interview, :fields).select { |f| word(f[:thing]) == name }
|
|
157
|
+
type = identifier_type(name, identifier)
|
|
158
|
+
fields = InterviewShape.fields(found, name, identifier, type, lifecycle: !steps.empty?)
|
|
134
159
|
creating = mine.select { |a| a[:creates] }
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
Shape.new(name: name, identifier: identifier, type: type, fields: fields, steps: steps,
|
|
161
|
+
creating: creating, rest: mine - creating)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The aggregate's opening: its identity, its fields, and the value object of each.
|
|
165
|
+
# @api private
|
|
166
|
+
def head_lines(interview, thing, shape)
|
|
167
|
+
required = shape.creating.flat_map { |action| InterviewShape.takes(action) }
|
|
168
|
+
[" # #{source_note(interview, thing)}".rstrip,
|
|
169
|
+
" aggregate #{shape.name.inspect} do",
|
|
170
|
+
" description #{"TODO: describe what a #{shape.name} is.".inspect}", "",
|
|
171
|
+
" identified_by :#{shape.identifier}", "",
|
|
172
|
+
" attribute :#{shape.identifier}, #{shape.type}",
|
|
173
|
+
*InterviewShape.attributes(shape.fields, required), "",
|
|
174
|
+
" value_object #{shape.type.inspect} do",
|
|
175
|
+
" attribute :value, String, pattern: #{PATTERN}", " end",
|
|
176
|
+
*InterviewShape.value_objects(shape.fields)]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @api private
|
|
180
|
+
def command_lines(interview, shape)
|
|
181
|
+
creating = shape.creating.empty? ? [stub_create(shape.name)] : shape.creating
|
|
182
|
+
creating.flat_map { |action| creating_command(interview, action, shape) } +
|
|
183
|
+
shape.rest.flat_map { |action| mutating_command(interview, action, shape) }
|
|
148
184
|
end
|
|
149
185
|
|
|
150
186
|
# The creating command nothing in the interview named: a stub, marked for the developer.
|
|
@@ -152,19 +188,24 @@ module Hecks
|
|
|
152
188
|
def stub_create(name) = { name: "Create", event: "#{name}Created", stub: true }
|
|
153
189
|
|
|
154
190
|
# @api private
|
|
155
|
-
def creating_command(interview, action,
|
|
191
|
+
def creating_command(interview, action, shape)
|
|
156
192
|
verb = word(action[:name])
|
|
157
193
|
note = action[:stub] ? STUB_NOTE : " # #{source_note(interview, action)}"
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
given, unknown = InterviewShape.inputs(action, shape.fields, shape.identifier)
|
|
195
|
+
["", note, " command #{verb.inspect} do", " goal #{"TODO: say what #{verb} does".inspect}",
|
|
196
|
+
*InterviewShape.who_lines(action), "", " attribute :#{shape.identifier}, #{shape.type}",
|
|
197
|
+
*given.map { |f| " attribute :#{f[:name]}, #{f[:type]}" }, *InterviewShape.unknown_lines(unknown), "",
|
|
198
|
+
" emits #{word(action[:event]).inspect}", " end"]
|
|
160
199
|
end
|
|
161
200
|
|
|
162
201
|
# @api private
|
|
163
|
-
def mutating_command(interview, action,
|
|
202
|
+
def mutating_command(interview, action, shape)
|
|
164
203
|
verb = word(action[:name])
|
|
204
|
+
given, unknown = InterviewShape.inputs(action, shape.fields, shape.identifier)
|
|
165
205
|
["", " # #{source_note(interview, action)}", " command #{verb.inspect} do",
|
|
166
|
-
" goal #{"TODO: say what #{verb} does".inspect}",
|
|
167
|
-
"
|
|
206
|
+
" goal #{"TODO: say what #{verb} does".inspect}", *InterviewShape.who_lines(action), "",
|
|
207
|
+
" reference_to #{shape.name}", *InterviewShape.input_lines(given), *InterviewShape.unknown_lines(unknown),
|
|
208
|
+
*InterviewShape.assignment_lines(given), "", " emits #{word(action[:event]).inspect}", " end"]
|
|
168
209
|
end
|
|
169
210
|
|
|
170
211
|
# An action whose thing nobody accepted has nowhere to go; it is kept as a comment, not lost.
|
|
@@ -196,10 +237,15 @@ module Hecks
|
|
|
196
237
|
lines = interview.fetch(:things).map do |f|
|
|
197
238
|
"- Thing #{f[:number]}, #{f[:status]}: **#{f[:name]}**, identified by `#{f[:identifier]}` (exchange #{f[:source]})"
|
|
198
239
|
end
|
|
199
|
-
lines += interview.fetch(:actions).map
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
lines += interview.fetch(:actions).map { |f| action_line(f) }
|
|
241
|
+
lines += interview.fetch(:fields, []).map do |f|
|
|
242
|
+
values = f[:values].to_s.strip.empty? ? "" : ", one of #{f[:values]}"
|
|
243
|
+
"- Field #{f[:number]}, #{f[:status]}: **#{f[:name]}** on #{f[:thing]}#{values} (exchange #{f[:source]})"
|
|
244
|
+
end
|
|
245
|
+
lines += interview.fetch(:transitions, []).map do |f|
|
|
246
|
+
from = f[:from].to_s.strip.empty? ? "" : " from #{f[:from]}"
|
|
247
|
+
"- Transition #{f[:number]}, #{f[:status]}: **#{f[:action]}** on #{f[:thing]} to #{f[:to]}#{from} " \
|
|
248
|
+
"(exchange #{f[:source]})"
|
|
203
249
|
end
|
|
204
250
|
lines + interview.fetch(:rules).map do |f|
|
|
205
251
|
"- Rule #{f[:number]}, #{f[:status]}: #{one_line(f[:statement])} (exchange #{f[:source]})"
|
|
@@ -207,7 +253,16 @@ module Hecks
|
|
|
207
253
|
end
|
|
208
254
|
|
|
209
255
|
# @api private
|
|
210
|
-
def
|
|
256
|
+
def action_line(finding)
|
|
257
|
+
creates = finding[:creates] ? ", creates it" : ""
|
|
258
|
+
takes = finding[:takes].to_s.strip.empty? ? "" : ", takes #{finding[:takes]}"
|
|
259
|
+
by = finding[:by].to_s.strip.empty? ? "" : ", by #{finding[:by]}"
|
|
260
|
+
"- Action #{finding[:number]}, #{finding[:status]}: **#{finding[:name]}** on #{finding[:thing]}, " \
|
|
261
|
+
"announcing `#{finding[:event]}`#{creates}#{takes}#{by} (exchange #{finding[:source]})"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @api private
|
|
265
|
+
def accepted(interview, kind) = interview.fetch(kind, []).select { |finding| finding[:status] == ACCEPTED }
|
|
211
266
|
|
|
212
267
|
# A free-text name as a PascalCase word safe to write into Ruby: `lend a book` is `LendABook`.
|
|
213
268
|
# @api private
|