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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +21 -23
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/interview_draft.rb +88 -33
  10. data/lib/hecks/cli/interview_session.rb +77 -26
  11. data/lib/hecks/cli/interview_shape.rb +198 -0
  12. data/lib/hecks/corpus.rb +3 -5
  13. data/lib/hecks/doors/cli_runner.rb +14 -1
  14. data/lib/hecks/doors/launcher_options.rb +13 -3
  15. data/lib/hecks/doors/mcp_door_scope.rb +2 -1
  16. data/lib/hecks/doors.rb +0 -17
  17. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  18. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  19. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  20. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  21. data/lib/hecks/fuzzing/isolated_boot.rb +1 -4
  22. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  23. data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
  24. data/lib/hecks/gate/stages.yml +113 -8
  25. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  26. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  27. data/lib/hecks/hecks/adapters/git.rb +59 -0
  28. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  29. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  30. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  31. data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
  32. data/lib/hecks/hecks/codebase.behaviors +6 -0
  33. data/lib/hecks/hecks/codebase.bluebook +21 -0
  34. data/lib/hecks/hecks/custodian.behaviors +6 -0
  35. data/lib/hecks/hecks/custodian.bluebook +217 -0
  36. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  37. data/lib/hecks/hecks/hecks.world +8 -3
  38. data/lib/hecks/hecks/site.world +5 -2
  39. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  40. data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
  41. data/lib/hecks/language/hecksagon/hecksagon.bluebook +0 -5
  42. data/lib/hecks/language/oidc.json +5 -0
  43. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  44. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  45. data/lib/hecks/projections/deploy/box/settings.rb +17 -4
  46. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  47. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  48. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  49. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  50. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  51. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  52. data/lib/hecks/projections/deploy/box.rb +15 -2
  53. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  54. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  55. data/lib/hecks/projections/model/deviations.rb +1 -1
  56. data/lib/hecks/projections/site/live_edge.rb +162 -0
  57. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  58. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  59. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  60. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  61. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  62. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  63. data/lib/hecks/runtime/interpreting.rb +3 -22
  64. data/lib/hecks/runtime/loader.rb +6 -7
  65. data/lib/hecks/runtime/needs.rb +48 -0
  66. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  67. data/lib/hecks/runtime/registry/verification.rb +3 -3
  68. data/lib/hecks/runtime.rb +6 -11
  69. data/lib/hecks/rust_build/host.rb +147 -0
  70. data/lib/hecks/rust_build/kernel_input.rb +40 -18
  71. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  72. data/lib/hecks/rust_build/project_rust.rb +3 -10
  73. data/lib/hecks/rust_build.rb +1 -0
  74. data/lib/hecks/site/bluebook/site.bluebook +53 -1
  75. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  76. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  77. data/lib/hecks/tools/gate.rb +1 -1
  78. data/lib/hecks/tools/rust_comment_style.rb +1 -3
  79. data/lib/hecks/tools/tools_doc.rb +204 -0
  80. data/lib/hecks/tools.rb +1 -0
  81. data/lib/hecks/version.rb +1 -1
  82. data/lib/hecks/vocabulary.rb +151 -0
  83. data/lib/hecks.rb +6 -12
  84. data/qa/settings.yml +1 -2
  85. data/rust/build/src/lineage_pass.rs +30 -15
  86. data/rust/build/src/main.rs +4 -2
  87. data/rust/build/src/pipeline.rs +8 -19
  88. data/rust/codegen/src/bridging.rs +7 -7
  89. data/rust/codegen/src/json.rs +214 -23
  90. data/rust/codegen/src/literal.rs +6 -3
  91. data/rust/codegen/src/main.rs +20 -9
  92. data/rust/codegen/src/naming.rs +4 -2
  93. data/rust/codegen/src/optional_pass.rs +226 -0
  94. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  95. data/rust/host/HECKS_RELEASE +1 -1
  96. data/rust/host/src/api.rs +1 -1
  97. data/rust/host/src/dispatch.rs +5 -0
  98. data/rust/host/src/needs.rs +99 -2
  99. data/rust/host/src/resend.rs +4 -4
  100. data/rust/host/src/ui_schema.rs +1 -1
  101. data/rust/parser/src/emit.rs +12 -0
  102. data/rust/parser/src/ir.rs +2 -0
  103. data/rust/parser/src/keywords.rs +2 -4
  104. data/rust/parser/src/lex.rs +1 -1
  105. data/rust/parser/src/main.rs +1 -0
  106. data/rust/parser/src/parse/command.rs +2 -44
  107. data/rust/parser/src/parse/hecksagon.rs +15 -24
  108. data/rust/parser/src/parse/mod.rs +1 -0
  109. data/rust/parser/src/parse/needs.rs +60 -0
  110. data/rust/parser/src/parse/query.rs +3 -0
  111. data/rust/src/kernel/cli.rs +16 -6
  112. data/rust/src/kernel/needs.rs +104 -9
  113. metadata +22 -9
  114. data/lib/hecks/facade.rb +0 -31
  115. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  116. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  117. data/lib/hecks/three_zero/forms.yml +0 -127
  118. data/lib/hecks/three_zero.rb +0 -12
  119. data/rust/build/src/json.rs +0 -421
  120. data/rust/build/src/optional_pass.rs +0 -150
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b963870a34a1483bc0156042a2967a72f8fc1b8e907ba5d8cac5a5382d4999d
4
- data.tar.gz: 26697af02778cda282a44c1ca099e36a9866bf2d204ab5abb23df8ac2a86283a
3
+ metadata.gz: b6180e44456a48f89b02b8812b4f327fe1642f98756e8e4c0b1bb295a04f826b
4
+ data.tar.gz: 6bc10cd76fb9c112ce9d7a5b82295a2e2a4c101d1b10b195ab48cd1afefd75c7
5
5
  SHA512:
6
- metadata.gz: a81b0ffb4b0e04024518169e3c6dd51a3f55b5da39659a852c2a0c9528453fe0eb9facf07d7b18bd35d7924c5a08331b2f1ad82a7e5c4c43c61fae55490d46ed
7
- data.tar.gz: f6780cb0c58069a5049081c7a319424787c9fea1fd94a5085153d7b99533c0e1974c23dfeb3515b66f3b2d8d3c7ca83a08dcd5f024df02da2b2fc9609c3ba6b7
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
- # The release that drops the deprecated spellings of `attaches`; independent of
61
- # `Hecks::Doors::REMOVAL`, which covers the older `Facade` aliases.
62
- REMOVAL = "3.3.0".freeze
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
 
@@ -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` follows, only when the query declares one, so a query that
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
- @returns ? shape.merge(returns: @returns) : shape
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
- private_constant :PATTERN, :STUB_NOTE
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` and `rules`, each a
69
- # finding with its `number`, `source` and `status` and the fields of its kind
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 { |f| slice(f, :number, :name, :thing, :event, :creates, :source, :status) },
75
- rules: interview.rule_findings.map { |f| slice(f, :number, :statement, :source, :status) } }
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
- field = Naming.snake(word(thing[:identifier]))
132
- field_type = identifier_type(name, field)
133
- mine = actions.select { |a| word(a[:thing]) == name }
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
- rest = mine - creating
136
- lines = [" # #{source_note(interview, thing)}".rstrip,
137
- " aggregate #{name.inspect} do",
138
- " description #{"TODO: describe what a #{name} is.".inspect}", "",
139
- " identified_by :#{field}", "",
140
- " attribute :#{field}, #{field_type}", "",
141
- " value_object #{field_type.inspect} do",
142
- " attribute :value, String, pattern: #{PATTERN}",
143
- " end"]
144
- creating = [stub_create(name)] if creating.empty?
145
- creating.each { |action| lines.concat(creating_command(interview, action, field, field_type)) }
146
- rest.each { |action| lines.concat(mutating_command(interview, action, name)) }
147
- (lines << " end").join("\n")
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, field, field_type)
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
- ["", note, " command #{verb.inspect} do", " goal #{"TODO: say what #{verb} does".inspect}", "",
159
- " attribute :#{field}, #{field_type}", "", " emits #{word(action[:event]).inspect}", " end"]
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, name)
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}", "", " reference_to #{name}", "",
167
- " emits #{word(action[:event]).inspect}", " end"]
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 do |f|
200
- creates = f[:creates] ? ", creates it" : ""
201
- "- Action #{f[:number]}, #{f[:status]}: **#{f[:name]}** on #{f[:thing]}, " \
202
- "announcing `#{f[:event]}`#{creates} (exchange #{f[:source]})"
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 accepted(interview, kind) = interview.fetch(kind).select { |finding| finding[:status] == ACCEPTED }
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