hecks 3.2.0 → 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.
Files changed (187) 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 +3 -3
  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/check_era.rb +3 -39
  10. data/lib/hecks/cli/interview_draft.rb +86 -31
  11. data/lib/hecks/cli/interview_session.rb +77 -26
  12. data/lib/hecks/cli/interview_shape.rb +179 -0
  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.rb +16 -2
  16. data/lib/hecks/doors/mcp_door_scope.rb +45 -5
  17. data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
  18. data/lib/hecks/doors.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  20. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  21. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  22. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  23. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
  24. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  25. data/lib/hecks/gate/stages.yml +113 -8
  26. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
  28. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  29. data/lib/hecks/hecks/adapters/git.rb +59 -0
  30. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  31. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  32. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  33. data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
  34. data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
  35. data/lib/hecks/hecks/codebase.behaviors +6 -0
  36. data/lib/hecks/hecks/codebase.bluebook +21 -0
  37. data/lib/hecks/hecks/custodian.behaviors +6 -0
  38. data/lib/hecks/hecks/custodian.bluebook +217 -0
  39. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  40. data/lib/hecks/hecks/hecks.world +8 -3
  41. data/lib/hecks/hecks/site.world +5 -2
  42. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  43. data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
  44. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  45. data/lib/hecks/language/oidc.json +5 -0
  46. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
  47. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  48. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  49. data/lib/hecks/projections/deploy/box/settings.rb +40 -6
  50. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
  51. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  52. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  53. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  54. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  55. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  56. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
  57. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  58. data/lib/hecks/projections/deploy/box.rb +28 -4
  59. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  60. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  61. data/lib/hecks/projections/model/deviations.rb +1 -1
  62. data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
  63. data/lib/hecks/projections/site/edge/checks.rb +31 -5
  64. data/lib/hecks/projections/site/edge.rb +25 -11
  65. data/lib/hecks/projections/site/live_edge.rb +162 -0
  66. data/lib/hecks/projections/site/routes_ts.rb +50 -27
  67. data/lib/hecks/projections/site/site_cdn.rb +7 -4
  68. data/lib/hecks/projections/site/table/checks.rb +53 -7
  69. data/lib/hecks/projections/site/table/link_builder.rb +64 -0
  70. data/lib/hecks/projections/site/table/row_builder.rb +21 -6
  71. data/lib/hecks/projections/site/table.rb +16 -5
  72. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  73. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  74. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  75. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  76. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  77. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  78. data/lib/hecks/runtime/interpreting.rb +3 -22
  79. data/lib/hecks/runtime/needs.rb +48 -0
  80. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  81. data/lib/hecks/runtime/registry/verification.rb +3 -3
  82. data/lib/hecks/rust_build/host.rb +147 -0
  83. data/lib/hecks/rust_build/kernel_input.rb +51 -17
  84. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  85. data/lib/hecks/rust_build/project_rust.rb +7 -140
  86. data/lib/hecks/rust_build.rb +1 -0
  87. data/lib/hecks/site/bluebook/site.bluebook +80 -14
  88. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  89. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  90. data/lib/hecks/storehouse.rb +35 -1
  91. data/lib/hecks/tools/comment_style.rb +4 -2
  92. data/lib/hecks/tools/gate.rb +1 -1
  93. data/lib/hecks/tools/rust_comment_style.rb +2 -4
  94. data/lib/hecks/tools/site_routes.rb +69 -28
  95. data/lib/hecks/tools/tools_doc.rb +204 -0
  96. data/lib/hecks/tools.rb +1 -0
  97. data/lib/hecks/version.rb +1 -1
  98. data/lib/hecks/vocabulary.rb +152 -1
  99. data/lib/hecks.rb +1 -0
  100. data/qa/settings.yml +1 -2
  101. data/rust/build/src/lineage_pass.rs +31 -16
  102. data/rust/build/src/main.rs +4 -2
  103. data/rust/build/src/pipeline.rs +9 -20
  104. data/rust/codegen/src/attr.rs +1 -1
  105. data/rust/codegen/src/bridging.rs +8 -8
  106. data/rust/codegen/src/commands.rs +1 -1
  107. data/rust/codegen/src/constraints.rs +1 -1
  108. data/rust/codegen/src/dependency_planning.rs +1 -1
  109. data/rust/codegen/src/domain_generator.rs +2 -2
  110. data/rust/codegen/src/exemplar.rs +1 -1
  111. data/rust/codegen/src/expr_emitter.rs +1 -1
  112. data/rust/codegen/src/fielded.rs +1 -1
  113. data/rust/codegen/src/json.rs +214 -23
  114. data/rust/codegen/src/json_codec.rs +1 -1
  115. data/rust/codegen/src/literal.rs +6 -3
  116. data/rust/codegen/src/main.rs +23 -13
  117. data/rust/codegen/src/mutations.rs +1 -1
  118. data/rust/codegen/src/naming.rs +5 -3
  119. data/rust/codegen/src/optional_pass.rs +226 -0
  120. data/rust/codegen/src/ports.rs +1 -1
  121. data/rust/codegen/src/queries.rs +3 -3
  122. data/rust/codegen/src/reactions.rs +1 -1
  123. data/rust/codegen/src/read_models.rs +2 -2
  124. data/rust/codegen/src/reference_specs.rs +1 -1
  125. data/rust/codegen/src/registry.rs +4 -4
  126. data/rust/codegen/src/shared.rs +1 -1
  127. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  128. data/rust/codegen/src/skip_reason.rs +1 -1
  129. data/rust/codegen/src/types.rs +4 -4
  130. data/rust/host/HECKS_RELEASE +1 -1
  131. data/rust/host/src/api.rs +1 -1
  132. data/rust/host/src/dispatch.rs +5 -0
  133. data/rust/host/src/needs.rs +193 -29
  134. data/rust/host/src/resend.rs +4 -4
  135. data/rust/host/src/ui_schema.rs +1 -1
  136. data/rust/parser/src/emit.rs +12 -0
  137. data/rust/parser/src/ir.rs +2 -0
  138. data/rust/parser/src/keywords.rs +2 -0
  139. data/rust/parser/src/main.rs +1 -0
  140. data/rust/parser/src/parse/command.rs +2 -44
  141. data/rust/parser/src/parse/mod.rs +1 -0
  142. data/rust/parser/src/parse/needs.rs +60 -0
  143. data/rust/parser/src/parse/query.rs +3 -0
  144. data/rust/src/exemplar/commands.rs +1 -1
  145. data/rust/src/exemplar/constraints.rs +1 -1
  146. data/rust/src/exemplar/fielded.rs +1 -1
  147. data/rust/src/exemplar/json.rs +1 -1
  148. data/rust/src/exemplar/mod.rs +1 -1
  149. data/rust/src/exemplar/mutations.rs +1 -1
  150. data/rust/src/exemplar/queries.rs +1 -1
  151. data/rust/src/exemplar/reactions.rs +1 -1
  152. data/rust/src/exemplar/read_models.rs +1 -1
  153. data/rust/src/exemplar/registry.rs +1 -1
  154. data/rust/src/exemplar/types.rs +1 -1
  155. data/rust/src/kernel/cli.rs +18 -8
  156. data/rust/src/kernel/needs.rs +104 -9
  157. data/rust/src/kernel/read_model.rs +1 -1
  158. data/rust/src/lib.rs +1 -1
  159. metadata +24 -30
  160. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  161. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  162. data/lib/hecks/three_zero/forms.yml +0 -127
  163. data/lib/hecks/three_zero.rb +0 -12
  164. data/rust/build/src/json.rs +0 -421
  165. data/rust/build/src/optional_pass.rs +0 -150
  166. data/rust/project/bridging.rb +0 -257
  167. data/rust/project/commands.rb +0 -877
  168. data/rust/project/constraints.rb +0 -86
  169. data/rust/project/dependency_planning.rb +0 -178
  170. data/rust/project/domain_generator.rb +0 -983
  171. data/rust/project/exemplar.rb +0 -148
  172. data/rust/project/expr_emitter.rb +0 -72
  173. data/rust/project/fielded.rb +0 -141
  174. data/rust/project/json_codec.rb +0 -641
  175. data/rust/project/mutations.rb +0 -695
  176. data/rust/project/naming.rb +0 -223
  177. data/rust/project/ports.rb +0 -65
  178. data/rust/project/queries.rb +0 -493
  179. data/rust/project/reactions.rb +0 -317
  180. data/rust/project/read_models.rb +0 -690
  181. data/rust/project/reference_specs.rb +0 -27
  182. data/rust/project/registry.rb +0 -396
  183. data/rust/project/skip_reason.rb +0 -29
  184. data/rust/project/types.rb +0 -225
  185. data/rust/project/write_if_changed.rb +0 -7
  186. data/rust/project.rb +0 -30
  187. data/rust/project_rust_pipeline.rb +0 -391
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c79ef6cb53e23b9faa0f452f116af208c659512f00716e47c7f3cdaad3d51e86
4
- data.tar.gz: b693ea21f847607ca2964759f9e0cec9dc7c4f8e1ea341b6636e99f8b4b1d294
3
+ metadata.gz: 288ec2962d96e6d9772c6fd6155d15461f69259cd7f69e107e308a2710fcb668
4
+ data.tar.gz: e71d1ec056df32604dc6ef8de8cab5069ae998faa74d89acd377d79b217820ed
5
5
  SHA512:
6
- metadata.gz: 38d1467d95484bcf093d6671982bf5e4edc4fc1be8907d849a88df9b85ab2b9810f88ce9533a9d1d125187fd47c26191fbe3e104e90307ee6fb19b3cffa37546
7
- data.tar.gz: 329631cf0d425b281c4a4770318d2956ae95ca7d2e0b12e2e0054bd2f0d72a3310a92d1ce2fa125d5a056c76026c27472230a0710335790267db173ada8e48db
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
@@ -57,9 +57,9 @@ module Hecks
57
57
  end
58
58
  end
59
59
 
60
- # The release that drops the deprecated spellings of `attaches`; kept in step with
61
- # `Hecks::Doors::REMOVAL`, which this file cannot require.
62
- REMOVAL = "3.2.0".freeze
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.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
 
@@ -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
@@ -2,11 +2,9 @@ require_relative "../ports/persistence/plugins/era/expected_era"
2
2
 
3
3
  module Hecks
4
4
  module CLI
5
- # The command behind `hecks check_era` and Custodian's `Host.CheckEra`: asks a running host
6
- # which era it reports and compares it with an allow-list file. Read-only: one GET, no writes.
5
+ # The check behind Custodian's `Host.CheckEra`: asks a running host which era it reports and
6
+ # compares it with an allow-list file. Read-only: one GET, no writes.
7
7
  module CheckEra
8
- USAGE = "usage: hecks check_era <url> <expected-era-file> [--timeout=<seconds>]".freeze
9
-
10
8
  # What a check found.
11
9
  #
12
10
  # @!attribute [r] verdict
@@ -14,7 +12,7 @@ module Hecks
14
12
  # @!attribute [r] version
15
13
  # @return [String] the gem version the host reports, empty when it reports none
16
14
  # @!attribute [r] line
17
- # @return [String] the sentence `hecks check_era` prints for this finding
15
+ # @return [String] the sentence the check reports for this finding
18
16
  Finding = Struct.new(:verdict, :version, :line)
19
17
 
20
18
  module_function
@@ -36,35 +34,6 @@ module Hecks
36
34
  Finding.new(verdict, document.fetch("version"), sentence(verdict, file))
37
35
  end
38
36
 
39
- # Runs `hecks check_era`: prints the finding and answers the exit status.
40
- #
41
- # @param argv [Array<String>] the url, the allow-list file and an optional `--timeout=N`
42
- # @param out [IO] where a finding goes
43
- # @param err [IO] where a refusal goes
44
- # @return [Integer] 0 when the era is expected or unlisted, 1 on a mismatch, 2 on bad
45
- # usage or an unreadable file, 3 when the host cannot be reached or answers no era
46
- def run(argv, out: $stdout, err: $stderr)
47
- timeout = 10
48
- args = argv.reject do |arg|
49
- next false unless arg.start_with?("--timeout=")
50
-
51
- timeout = arg.delete_prefix("--timeout=").to_f
52
- true
53
- end
54
- url, file = args
55
- return usage(err) unless url && file && args.size == 2 && timeout.positive?
56
-
57
- finding = assess(url, file, timeout: timeout)
58
- (finding.verdict.ok? ? out : err).puts(finding.line)
59
- finding.verdict.ok? ? 0 : 1
60
- rescue Errno::ENOENT, Errno::EACCES => e
61
- err.puts("cannot read #{file}: #{e.message}")
62
- 2
63
- rescue Runtime::EraCheck::ExpectedEra::Unreachable, Runtime::EraCheck::ExpectedEra::BadResponse => e
64
- err.puts("era check failed: #{e.message}")
65
- 3
66
- end
67
-
68
37
  # @param verdict [Runtime::EraCheck::ExpectedEra::Verdict] the comparison
69
38
  # @param file [String] the allow-list file, named in the sentence
70
39
  # @return [String] the one line that says what the comparison found
@@ -78,11 +47,6 @@ module Hecks
78
47
  "host reports era #{verdict.era}, which is not in #{file} (expected: #{verdict.allowed.join(', ')})"
79
48
  end
80
49
  end
81
-
82
- def usage(err)
83
- err.puts(USAGE)
84
- 2
85
- end
86
50
  end
87
51
  end
88
52
  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
@@ -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)
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
- 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