hecks 3.2.1 → 3.3.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 +1 -1
- 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 +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- 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 +1 -1
- 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/qa_settings.rb +15 -34
- 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 +1 -1
- 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/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/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/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 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- 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 -8
- 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: 288ec2962d96e6d9772c6fd6155d15461f69259cd7f69e107e308a2710fcb668
|
|
4
|
+
data.tar.gz: e71d1ec056df32604dc6ef8de8cab5069ae998faa74d89acd377d79b217820ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2586bb29d327f576976c7573236abfde1f60deba2eec7529b13ea34fc1b4ea917abe996fd44076cb76690835fb7089f4d18f38d0693d0aa14da2a106334a1a7
|
|
7
|
+
data.tar.gz: 1101c11307086a6df1833e6cd7b4fb5fdaa5b0d265b0a4569fa9d4df5d85f27039975f46135dd90619ad69e50d553172eb82d203633177595503f9bbfb67998a
|
|
@@ -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
|
|
@@ -59,7 +59,7 @@ module Hecks
|
|
|
59
59
|
|
|
60
60
|
# The release that drops the deprecated spellings of `attaches`; independent of
|
|
61
61
|
# `Hecks::Doors::REMOVAL`, which covers the older `Facade` aliases.
|
|
62
|
-
REMOVAL = "3.
|
|
62
|
+
REMOVAL = "3.4.0".freeze
|
|
63
63
|
|
|
64
64
|
attr_reader :binds, :subscriptions, :attachments
|
|
65
65
|
|
|
@@ -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
|
|
@@ -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
|
-
field_type = identifier_type(name, field)
|
|
153
|
+
identifier = Naming.snake(word(thing[:identifier]))
|
|
133
154
|
mine = 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
|
|
@@ -24,36 +24,59 @@ module Hecks
|
|
|
24
24
|
|
|
25
25
|
TASK = "You interview a subject matter expert, through the developer who sits beside them, about the " \
|
|
26
26
|
"business domain called %<subject>s, so the developer can model it. Ask one plain-language " \
|
|
27
|
-
"question at a time about what the business keeps track of, what
|
|
28
|
-
"
|
|
27
|
+
"question at a time about what the business keeps track of, what each thing has, what happens to " \
|
|
28
|
+
"it and who does it, how it moves from one state to the next, and what must never happen. Use the " \
|
|
29
|
+
"expert's own words. Do not assume what kind of business it is, or an " \
|
|
29
30
|
"industry, from the name of the domain: learn it from what the expert says. When you interpret " \
|
|
30
31
|
"an answer, propose findings only with the verbs listed under `verbs`, using the argument " \
|
|
31
32
|
"names given there, and nothing else.".freeze
|
|
32
33
|
|
|
33
34
|
VERBS = {
|
|
34
|
-
"SME::Interview.ProposeThing"
|
|
35
|
+
"SME::Interview.ProposeThing" => {
|
|
35
36
|
"meaning" => "a thing the business keeps track of, and the field that identifies one of it",
|
|
36
37
|
"arguments" => %w[name identifier]
|
|
37
38
|
},
|
|
38
|
-
"SME::Interview.ProposeAction"
|
|
39
|
-
"meaning" => "something that happens to a thing, and the event it announces, in the past tense"
|
|
40
|
-
|
|
39
|
+
"SME::Interview.ProposeAction" => {
|
|
40
|
+
"meaning" => "something that happens to a thing, and the event it announces, in the past tense; " \
|
|
41
|
+
"creates is true for the action that brings the thing into being; takes lists, comma " \
|
|
42
|
+
"separated, the fields it needs to be told; by says who does it",
|
|
43
|
+
"arguments" => %w[name thing event creates takes by]
|
|
41
44
|
},
|
|
42
|
-
"SME::Interview.
|
|
45
|
+
"SME::Interview.ProposeField" => {
|
|
46
|
+
"meaning" => "something a thing has; values lists, comma separated, what it may be when the " \
|
|
47
|
+
"expert gave a closed set, and is left out for free text",
|
|
48
|
+
"arguments" => %w[thing name values]
|
|
49
|
+
},
|
|
50
|
+
"SME::Interview.ProposeTransition" => {
|
|
51
|
+
"meaning" => "the state an action leaves a thing in, and the state it had to be in before; the " \
|
|
52
|
+
"action that creates the thing has no from",
|
|
53
|
+
"arguments" => %w[thing action to from]
|
|
54
|
+
},
|
|
55
|
+
"SME::Interview.ProposeRule" => {
|
|
43
56
|
"meaning" => "a rule the expert stated, in their own words",
|
|
44
57
|
"arguments" => %w[statement]
|
|
45
58
|
}
|
|
46
59
|
}.freeze
|
|
47
60
|
|
|
48
|
-
# What each finding verb needs,
|
|
61
|
+
# What each finding verb needs, the fields it may leave out, and the SME commands that
|
|
62
|
+
# carry it out and decide it.
|
|
49
63
|
KINDS = {
|
|
50
|
-
"SME::Interview.ProposeThing"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
"SME::Interview.ProposeThing" => { kind: "thing", fields: %w[name identifier], optional: [],
|
|
65
|
+
propose: :propose_thing!, entity: "ThingFinding",
|
|
66
|
+
accept: "AcceptThing", reject: "RejectThing" },
|
|
67
|
+
"SME::Interview.ProposeAction" => { kind: "action", fields: %w[name thing event creates takes by],
|
|
68
|
+
optional: %w[creates takes by], propose: :propose_action!,
|
|
69
|
+
entity: "ActionFinding", accept: "AcceptAction", reject: "RejectAction" },
|
|
70
|
+
"SME::Interview.ProposeRule" => { kind: "rule", fields: %w[statement], optional: [],
|
|
71
|
+
propose: :propose_rule!, entity: "RuleFinding",
|
|
72
|
+
accept: "AcceptRule", reject: "RejectRule" },
|
|
73
|
+
"SME::Interview.ProposeField" => { kind: "field", fields: %w[thing name values], optional: %w[values],
|
|
74
|
+
propose: :propose_field!, entity: "FieldFinding",
|
|
75
|
+
accept: "AcceptField", reject: "RejectField" },
|
|
76
|
+
"SME::Interview.ProposeTransition" => { kind: "transition", fields: %w[thing action to from],
|
|
77
|
+
optional: %w[from], propose: :propose_transition!,
|
|
78
|
+
entity: "TransitionFinding", accept: "AcceptTransition",
|
|
79
|
+
reject: "RejectTransition" }
|
|
57
80
|
}.freeze
|
|
58
81
|
|
|
59
82
|
TRUE_WORDS = %w[true yes y 1].freeze
|
|
@@ -153,7 +176,7 @@ module Hecks
|
|
|
153
176
|
return say("Ignored a proposal for #{proposal.verb}: not a finding I know.") unless config
|
|
154
177
|
|
|
155
178
|
fields = fields_of(proposal, config)
|
|
156
|
-
missing = config[:fields].reject { |f| f
|
|
179
|
+
missing = config[:fields].reject { |f| config[:optional].include?(f) || fields[f].to_s.strip != "" }
|
|
157
180
|
return say("Ignored a #{config[:kind]} proposal with no #{missing.join(', ')}.") unless missing.empty?
|
|
158
181
|
|
|
159
182
|
number = propose(config, fields) or return
|
|
@@ -166,7 +189,7 @@ module Hecks
|
|
|
166
189
|
rows = proposal.arguments.to_h { |row| [row[:name].to_s, row[:value]] }
|
|
167
190
|
fields = rows.slice(*config[:fields])
|
|
168
191
|
fields["creates"] = TRUE_WORDS.include?(fields["creates"].to_s.downcase) if fields.key?("creates")
|
|
169
|
-
fields
|
|
192
|
+
fields.reject { |key, value| key != "creates" && config[:optional].include?(key) && value.to_s.strip.empty? }
|
|
170
193
|
end
|
|
171
194
|
|
|
172
195
|
def propose(config, fields)
|
|
@@ -192,19 +215,27 @@ module Hecks
|
|
|
192
215
|
case config[:kind]
|
|
193
216
|
when "thing" then "thing: #{fields['name']}, identified by #{fields['identifier']}"
|
|
194
217
|
when "action" then "action: #{fields['name']} on #{fields['thing']}, announcing #{fields['event']}" \
|
|
195
|
-
"#{', creating it' if fields['creates']}"
|
|
218
|
+
"#{', creating it' if fields['creates']}#{action_extras(fields)}"
|
|
219
|
+
when "field" then "field: #{fields['name']} of #{fields['thing']}#{", one of #{fields['values']}" if fields['values']}"
|
|
220
|
+
when "transition" then "transition: #{fields['action']} leaves #{fields['thing']} #{fields['to']}" \
|
|
221
|
+
"#{", from #{fields['from']}" if fields['from']}"
|
|
196
222
|
else "rule: #{fields['statement']}"
|
|
197
223
|
end
|
|
198
224
|
end
|
|
199
225
|
|
|
226
|
+
# @api private
|
|
227
|
+
def action_extras(fields)
|
|
228
|
+
"#{", taking #{fields['takes']}" if fields['takes']}#{", by #{fields['by']}" if fields['by']}"
|
|
229
|
+
end
|
|
230
|
+
|
|
200
231
|
# The plain prompt: findings typed by the developer, accepted as they are entered.
|
|
201
232
|
def manual_findings
|
|
202
233
|
loop do
|
|
203
|
-
kind = ask("Add a finding from that answer? thing, action or
|
|
234
|
+
kind = ask("Add a finding from that answer? thing, action, rule, field or transition (enter to skip): ")
|
|
204
235
|
kind = kind.to_s.strip.downcase
|
|
205
236
|
break if kind.empty?
|
|
206
237
|
|
|
207
|
-
typed = typed_finding(kind) or next say(" I only know thing, action and
|
|
238
|
+
typed = typed_finding(kind) or next say(" I only know thing, action, rule, field and transition.")
|
|
208
239
|
config = KINDS["SME::Interview.Propose#{kind.capitalize}"]
|
|
209
240
|
number = propose(config, typed) or next
|
|
210
241
|
decide(config, number, true)
|
|
@@ -216,6 +247,11 @@ module Hecks
|
|
|
216
247
|
when "thing" then named("Name" => "name", "Identified by" => "identifier")
|
|
217
248
|
when "action" then typed_action
|
|
218
249
|
when "rule" then named("The rule, in the expert's words" => "statement")
|
|
250
|
+
when "field" then named({ "On which thing" => "thing", "Name" => "name",
|
|
251
|
+
"Values it may take, comma separated (enter for any)" => "values" }, %w[values])
|
|
252
|
+
when "transition" then named({ "On which thing" => "thing", "Caused by which action" => "action",
|
|
253
|
+
"Leaves it in the state" => "to",
|
|
254
|
+
"From the state (enter if it starts there)" => "from" }, %w[from])
|
|
219
255
|
end
|
|
220
256
|
end
|
|
221
257
|
|
|
@@ -224,9 +260,11 @@ module Hecks
|
|
|
224
260
|
fields.merge("creates" => TRUE_WORDS.include?(ask(" Does it create the thing? [y/N] ").to_s.strip.downcase))
|
|
225
261
|
end
|
|
226
262
|
|
|
227
|
-
def named(prompts)
|
|
263
|
+
def named(prompts, optional = [])
|
|
228
264
|
fields = prompts.to_h { |label, key| [key, ask(" #{label}: ").to_s.strip] }
|
|
229
|
-
fields.
|
|
265
|
+
return nil unless fields.all? { |key, value| optional.include?(key) || !value.empty? }
|
|
266
|
+
|
|
267
|
+
fields.reject { |key, value| optional.include?(key) && value.empty? }
|
|
230
268
|
end
|
|
231
269
|
|
|
232
270
|
def suggest_enough
|
|
@@ -271,9 +309,11 @@ module Hecks
|
|
|
271
309
|
end
|
|
272
310
|
|
|
273
311
|
def accepted_state(plain)
|
|
274
|
-
{ things:
|
|
275
|
-
actions:
|
|
276
|
-
|
|
312
|
+
{ things: InterviewDraft.accepted(plain, :things).map { |f| f.slice(:name, :identifier) },
|
|
313
|
+
actions: InterviewDraft.accepted(plain, :actions).map { |f| f.slice(:name, :thing, :event, :creates, :takes, :by) },
|
|
314
|
+
fields: InterviewDraft.accepted(plain, :fields).map { |f| f.slice(:thing, :name, :values) },
|
|
315
|
+
transitions: InterviewDraft.accepted(plain, :transitions).map { |f| f.slice(:thing, :action, :to, :from) },
|
|
316
|
+
rules: InterviewDraft.accepted(plain, :rules).map { |f| f.slice(:statement) } }
|
|
277
317
|
end
|
|
278
318
|
|
|
279
319
|
# What the interview has not yet pinned down, in words a model can act on.
|
|
@@ -283,7 +323,18 @@ module Hecks
|
|
|
283
323
|
gaps = things.reject { |t| actions.any? { |a| a[:thing] == t } }.map { |t| "nothing is yet said to happen to #{t}" }
|
|
284
324
|
gaps += things.reject { |t| actions.any? { |a| a[:thing] == t && a[:creates] } }.map { |t| "nothing yet creates #{t}" }
|
|
285
325
|
unplaced = actions.reject { |a| things.include?(a[:thing].to_s) }
|
|
286
|
-
gaps + unplaced.map { |a| "#{a[:name]} names #{a[:thing]}, not yet a thing" }
|
|
326
|
+
gaps + unplaced.map { |a| "#{a[:name]} names #{a[:thing]}, not yet a thing" } + shape_gaps(plain, things, actions)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# What a thing is not yet said to have, and how it moves from one state to the next.
|
|
330
|
+
def shape_gaps(plain, things, actions)
|
|
331
|
+
fields = InterviewDraft.accepted(plain, :fields)
|
|
332
|
+
steps = InterviewDraft.accepted(plain, :transitions)
|
|
333
|
+
bare = things.reject { |t| fields.any? { |f| f[:thing] == t } }
|
|
334
|
+
gaps = bare.map { |t| "nothing is yet said #{t} has, beyond its identifier" }
|
|
335
|
+
changing = things.select { |t| actions.count { |a| a[:thing] == t && !a[:creates] } > 1 }
|
|
336
|
+
unmoved = changing.reject { |t| steps.any? { |s| s[:thing] == t } }
|
|
337
|
+
gaps + unmoved.map { |t| "nothing yet says how #{t} moves from one state to the next" }
|
|
287
338
|
end
|
|
288
339
|
|
|
289
340
|
def reason(error) = error.message.sub(/\A[A-Z]\w* refused\s+[—-]\s+/, "").strip
|