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
@@ -38,7 +38,9 @@ module Hecks
38
38
  status = Dir.chdir(@tree.root) { runner.call(["--example", example, path], out, out) }
39
39
  raise ConsoleCapture::Failure, out.string.strip unless status.zero?
40
40
 
41
- out.string.strip
41
+ out.string.strip.tap do |report|
42
+ raise ConsoleCapture::Failure, "the test runner printed nothing for #{file}" if report.empty?
43
+ end
42
44
  end
43
45
 
44
46
  private
@@ -47,7 +49,13 @@ module Hecks
47
49
  return self.class.runner if self.class.runner
48
50
 
49
51
  require "rspec/core"
50
- RSpec::Core::Runner.method(:run)
52
+ # A process that runs one example after another, as a resident door does, has to reset
53
+ # RSpec between runs: its reporter keeps the first run's output stream, so a later run
54
+ # would write into that and answer an empty report.
55
+ lambda do |args, err, out|
56
+ RSpec.reset
57
+ RSpec::Core::Runner.run(args, err, out)
58
+ end
51
59
  end
52
60
  end
53
61
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../doors/json_door"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # Gives a booted domain its first administrator through the chapter that provides
8
+ # `"membership"`, the way a project's hand-written bootstrap script does.
9
+ #
10
+ # The chapter names its own verbs (`provides "membership", admit:, grant:, people:`), so no
11
+ # aggregate is recognised by name. Both dispatches run as a caller that asserts the role the
12
+ # command is gated to and binds no `actor_id`: Governance checks an identified caller against
13
+ # its role assignments, which a domain with no administrator cannot have yet, and checks an
14
+ # unidentified one by comparing the role named (ADR 0025). That is the bootstrap step, and it is
15
+ # refused once anyone holds an administrator role, so it cannot add a second.
16
+ class FirstAdmin
17
+ # The roles that count as an administrator besides the one the grant command is gated to.
18
+ OWNER_ROLES = %w[Owner].freeze
19
+
20
+ # What a bootstrap did.
21
+ #
22
+ # @!attribute [r] email
23
+ # @return [String] who was granted access
24
+ # @!attribute [r] role
25
+ # @return [String] the role they now hold
26
+ # @!attribute [r] admitted
27
+ # @return [Boolean] whether the person had to be admitted first
28
+ Result = Struct.new(:email, :role, :admitted, keyword_init: true) do
29
+ # @return [String] the sentence the verb answers with
30
+ def to_s = "Granted #{role} access to #{email}#{' (admitted first)' if admitted}"
31
+ end
32
+
33
+ # @param runtime [Hecks::Runtime] the booted domain
34
+ def initialize(runtime)
35
+ @runtime = runtime
36
+ @registry = runtime.registry
37
+ end
38
+
39
+ # Admits the person if they are not yet, then grants them a role.
40
+ #
41
+ # @param email [String] the person's email, the membership aggregate's identity
42
+ # @param name [String, nil] their name; the part of the email before the `@` when nil
43
+ # @param role [String, nil] the role to grant; the one the grant command is gated to when nil
44
+ # @return [Result] what was done
45
+ # @raise [Runtime::NotFound] when the domain has no membership chapter, the email is not an
46
+ # email, or an administrator already exists
47
+ def call(email:, name: nil, role: nil)
48
+ raise Runtime::NotFound, "#{email.inspect} is not an email address" unless email.to_s.match?(/\A[^\s@]+@[^\s@]+\z/)
49
+
50
+ provider = membership_provider
51
+ verbs = verbs_of(provider)
52
+ gate = gated_role(provider, verbs.fetch(:grant))
53
+ people = rows(verbs.fetch(:people))
54
+ refuse_if_administered!(people, [gate, *OWNER_ROLES].uniq)
55
+
56
+ admitted = people.none? { |person| person.dig(:email, :value) == email }
57
+ admit(provider, verbs.fetch(:admit), email, name || email[/\A[^@]+/]) if admitted
58
+ grant(provider, verbs.fetch(:grant), email, role || gate)
59
+ Result.new(email: email, role: role || gate, admitted: admitted)
60
+ end
61
+
62
+ private
63
+
64
+ def membership_provider
65
+ domain = @registry.bluebooks.values.first&.name
66
+ @registry.membership_provider_for(domain) or
67
+ raise Runtime::NotFound, "this domain attaches no chapter that provides \"membership\""
68
+ end
69
+
70
+ def verbs_of(provider)
71
+ %i[admit grant people].to_h { |key| [key, provider.provided_verb(Bluebook::Capabilities::MEMBERSHIP, key)] }
72
+ end
73
+
74
+ def rows(verb) = @runtime.query(verb).map { |row| Doors::JsonDoor.materialize(row) }
75
+
76
+ # The role a verb's command declares, which the dispatch must assert to be let through.
77
+ def gated_role(provider, verb)
78
+ aggregate, command = verb.split("::").last.split(".")
79
+ found = provider.aggregates.find { |candidate| candidate.hecks_name == aggregate }
80
+ &.commands&.find { |candidate| candidate.hecks_name == command }
81
+ found&.role&.to_s or raise Runtime::NotFound, "#{verb} declares no role to bootstrap as"
82
+ end
83
+
84
+ def refuse_if_administered!(people, roles)
85
+ held = people.select { |person| roles.map(&:downcase).include?(person.dig(:role, :value).to_s.downcase) }
86
+ return if held.empty?
87
+
88
+ who = held.map { |person| person.dig(:email, :value) }.join(", ")
89
+ raise Runtime::NotFound, "an administrator already exists (#{who}); grant roles through the " \
90
+ "domain's own #{roles.first} access instead"
91
+ end
92
+
93
+ def admit(provider, verb, email, name)
94
+ as(provider, verb) do
95
+ @runtime.dispatch(verb, with: { email: { value: email }, name: { value: name } })
96
+ end
97
+ end
98
+
99
+ def grant(provider, verb, email, role)
100
+ as(provider, verb) { @runtime.dispatch(verb, to: email, with: { role: { value: role } }) }
101
+ end
102
+
103
+ def as(provider, verb, &)
104
+ Hecks.as_caller(role: gated_role(provider, verb), &)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -60,6 +60,55 @@ module Hecks
60
60
  { report: { value: out.string } }
61
61
  end
62
62
 
63
+ # Checks a project's vendored packages against their `bluebook.lock` files and describes
64
+ # them as the manifest a client image carries, through `Hecks::EmbryonautBluebook::Manifest`.
65
+ #
66
+ # The manifest records the project's own commit and whether its tree has uncommitted changes,
67
+ # read from the repository the project stands in (`unknown` outside one).
68
+ #
69
+ # @param held [Hash] the `Package` query's arguments: `root` (the project; the current
70
+ # directory when absent)
71
+ # @return [Hash{Symbol => String}] `text:` the manifest as JSON
72
+ # @raise [Runtime::NotFound] naming each package whose files disagree with its lock
73
+ def verify(**held)
74
+ root = File.expand_path(plain(held[:root]) || Dir.pwd)
75
+ raise Runtime::NotFound, "#{root} is not a directory" unless File.directory?(root)
76
+
77
+ { text: EmbryonautBluebook::Manifest.new(root, built_from: built_from(root)).to_json_text }
78
+ rescue EmbryonautBluebook::Manifest::Mismatch => e
79
+ raise Runtime::NotFound, e.message
80
+ end
81
+
82
+ # Judges every package of a bluebook registry against its latest release, through
83
+ # `Hecks::EmbryonautBluebook::Registry#check`.
84
+ #
85
+ # @param held [Hash] the `Registry` query's arguments: `root` (the registry; the current
86
+ # directory when absent)
87
+ # @return [Hash{Symbol => String}] `text:` the notes and `versions ok`
88
+ # @raise [Runtime::NotFound] naming each package at fault, or when `root` is no repository
89
+ def check(**held)
90
+ report = registry(held).check
91
+ raise Runtime::NotFound, report.to_s unless report.ok?
92
+
93
+ { text: report.to_s }
94
+ rescue Vendoring::Error => e
95
+ raise Runtime::NotFound, e.message
96
+ end
97
+
98
+ # Tags a package of a bluebook registry at the version its `bluebook.yml` carries, through
99
+ # `Hecks::EmbryonautBluebook::Registry#release`. The tag is made in the local repository and
100
+ # never pushed.
101
+ #
102
+ # @param held [Hash] the `Registry` record: `package`, and `root` (the current directory when
103
+ # absent)
104
+ # @return [Hash{Symbol => Hash}] `report:` the tag made and the command that publishes it
105
+ # @raise [ConsoleCapture::Failure] when the package breaks a release rule
106
+ def tag(**held)
107
+ { report: { value: registry(held).release(plain(held[:package])).to_s } }
108
+ rescue Vendoring::Error => e
109
+ raise ConsoleCapture::Failure, e.message
110
+ end
111
+
63
112
  # Who the repository at a directory commits as: its configured name and email.
64
113
  #
65
114
  # @param chdir [String, nil] a directory inside the repository; the current one when nil
@@ -87,6 +136,16 @@ module Hecks
87
136
 
88
137
  private
89
138
 
139
+ def registry(held)
140
+ EmbryonautBluebook::Registry.new(File.expand_path(plain(held[:root]) || Dir.pwd))
141
+ end
142
+
143
+ def built_from(root)
144
+ head = capture("rev-parse", "HEAD", chdir: root)
145
+ status = capture("status", "--porcelain", "--", ".", chdir: root)
146
+ { "commit" => head.ok? ? head.out.strip : "unknown", "dirty" => status.ok? && !status.out.strip.empty? }
147
+ end
148
+
90
149
  def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
91
150
 
92
151
  # A source that is not a repository is worded here; the vendoring's own refusal for it is a
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "first_admin"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ # What the `DomainRuntime` port's adapter does when an `Operation` asks it to give a domain its
8
+ # first administrator.
9
+ module InProcessAccess
10
+ # Boots the domain and grants the first administrator through its membership chapter.
11
+ #
12
+ # @param held [Hash] the `Operation` record: `subject` (the domain), `email`, and optionally
13
+ # `name` and `role`
14
+ # @return [Hash{Symbol => Hash}] `output:` the sentence saying who now holds which role
15
+ # @raise [Runtime::NotFound] if the domain cannot be found or has no membership chapter, or an
16
+ # administrator already exists
17
+ def bootstrap(**held)
18
+ result = FirstAdmin.new(boot(held[:subject])).call(
19
+ email: plain(held[:email]), name: plain(held[:name]), role: plain(held[:role])
20
+ )
21
+ output(result.to_s)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -9,6 +9,7 @@ require_relative "../../cli/stores"
9
9
  require_relative "../../cli/shape"
10
10
  require_relative "../../cli/history"
11
11
  require_relative "in_process_operations"
12
+ require_relative "in_process_access"
12
13
 
13
14
  module Hecks
14
15
  module Adapters
@@ -25,6 +26,7 @@ module Hecks
25
26
  # a projection refresh, a behaviors run, a smoke test and a bounded follow.
26
27
  class InProcessBoot
27
28
  include InProcessOperations
29
+ include InProcessAccess
28
30
 
29
31
  # Accepts the arguments every driven adapter is built with and keeps none of them.
30
32
  #
@@ -19,9 +19,9 @@ module Hecks
19
19
  # minutes, so it starts as a child through `ProcessPool`.
20
20
  class RustToolchain
21
21
  # The `Hecks::RustBuild` tool each ask runs.
22
- TOOLS = { generate: "project_rust", wasm: "project_wasm", browser_wasm: "project_wasm_browser",
23
- conform: "rust_conformance", replay: "rust_conformance_fuzz",
24
- coverage: "rust_coverage" }.freeze
22
+ TOOLS = { generate: "project_rust", wasm: "project_wasm", host: "project_host",
23
+ browser_wasm: "project_wasm_browser", conform: "rust_conformance",
24
+ replay: "rust_conformance_fuzz", coverage: "rust_coverage" }.freeze
25
25
 
26
26
  # The load path the benchmark child runs with: this checkout's or gem's `lib/`.
27
27
  LIB = File.expand_path("../../..", __dir__)
@@ -73,6 +73,21 @@ module Hecks
73
73
  child(:wasm, [plain(held[:domain])])
74
74
  end
75
75
 
76
+ # Generates the domain, compiles it to a `wasm32-wasip1` module, builds the host binary from
77
+ # the workspace and stages the three files in a directory an image copies from.
78
+ #
79
+ # @param held [Hash] the `Build` record: `domain`, `target` (the machine's own when absent)
80
+ # and `stage_dir` (`.hecks/host/<target>/` when absent)
81
+ # @return [Hash{Symbol => Hash}] `output:` where the files were staged
82
+ # @raise [ConsoleCapture::Failure] when the toolchain or the target is missing, or a step
83
+ # fails
84
+ def host(**held)
85
+ argv = [plain(held[:domain])]
86
+ argv << "--target=#{plain(held[:target])}" if plain(held[:target])
87
+ argv << "--stage=#{plain(held[:stage_dir])}" if plain(held[:stage_dir])
88
+ child(:host, argv)
89
+ end
90
+
76
91
  # Generates the domain, then compiles it to an ES module for a browser through wasm-bindgen.
77
92
  #
78
93
  # @param held [Hash] the `Build` record: `domain`
@@ -22,7 +22,7 @@ module Hecks
22
22
  CHECKOUT_MARKER = "hecks.gemspec"
23
23
 
24
24
  # What a copy holds: the workspace manifest, the kernel and every crate a domain build uses.
25
- PACKAGED = %w[Cargo.toml Cargo.lock project.rb project_rust_pipeline.rb project src codegen parser
25
+ PACKAGED = %w[Cargo.toml Cargo.lock src codegen parser
26
26
  host build web lsp].freeze
27
27
 
28
28
  # Left out of a copy: build output anywhere, the corpus tests, and the corpus domains.
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "console_capture"
5
+ require_relative "shell"
4
6
  require_relative "codebase/tree"
5
7
  require_relative "codebase/ruby_child"
8
+ require "hecks/tools"
9
+ require "hecks/tools/site_routes"
10
+ require "hecks/projections/site/live_edge"
6
11
 
7
12
  module Hecks
8
13
  module Adapters
9
14
  # The `SiteToolchain` port's adapter: projects a site's route table into its `routes.ts`, or
10
15
  # checks the file on disk is current.
11
16
  #
12
- # The projection runs the `project_site` tool in a child of the checkout, with its printing and
13
- # exit status captured: a tool that ends non-zero is a refusal whose reason is what it printed.
14
- # Outside a hecks checkout the ask is refused with "needs a hecks checkout".
17
+ # The projection runs the `project_site` tool in this process, with its printing and exit status
18
+ # captured: a tool that ends non-zero is a refusal whose reason is what it printed. The tool
19
+ # ships in the gem and reads only the project it is given, so it needs no hecks checkout.
15
20
  class SiteToolchain
16
21
  # The `Hecks::Tools` tool the ask runs, by ask.
17
22
  SCRIPTS = { project: "project_site" }.freeze
18
23
 
24
+ # The tool's flag for each `SiteProjection` field that takes a value.
25
+ FLAGS = { "out" => :out, "template" => :template, "extension" => :extension }.freeze
26
+
19
27
  # Accepts the arguments every driven adapter is built with and keeps none of them.
20
28
  #
21
29
  # @param aggregate [Object, nil] unused
@@ -25,29 +33,98 @@ module Hecks
25
33
 
26
34
  # Writes a project's `routes.ts` from its declared route table, or with `check` only compares.
27
35
  #
28
- # @param held [Hash] the `SiteProjection` record: `domain`, and `out` and `check` when set
36
+ # @param held [Hash] the `SiteProjection` record: `domain`, and `out`, `template`, `extension`
37
+ # and `check` when set
29
38
  # @return [Hash{Symbol => Hash}] `output:` one line per file written or found current
30
- # @raise [ConsoleCapture::Failure] when the tree is not a checkout, the route table is
31
- # refused (an unknown cache class, a duplicate path), or under `check` a file is out of date
39
+ # @raise [ConsoleCapture::Failure] when the route table is refused (an unknown cache class,
40
+ # a duplicate path), a path or the extension is refused, or under `check` a file is out
41
+ # of date
32
42
  def project(**held)
33
- flags = []
34
- flags << "--out=#{plain(held[:out])}" unless plain(held[:out]).nil?
43
+ flags = FLAGS.filter_map { |flag, key| "--#{flag}=#{located(key, held[key])}" unless plain(held[key]).nil? }
35
44
  flags << "--check" if plain(held[:check]) == true
36
- tree = Codebase::Tree.new
37
- tree.require_checkout!
38
- result = Codebase::RubyChild.new(tree).capture(SCRIPTS.fetch(:project), *flags, plain(held[:domain]))
45
+ result = Codebase::RubyChild.new(Codebase::Tree.new).capture(SCRIPTS.fetch(:project), *flags,
46
+ located(:domain, held[:domain]))
39
47
  raise ConsoleCapture::Failure, message_of(result) unless result.ok?
40
48
 
41
49
  { output: { value: result.out } }
42
50
  end
43
51
 
52
+ # Compares the CloudFront behaviours a project's edge generates with a live distribution's.
53
+ # The live configuration is read from a saved `get-distribution-config` answer (`live`) or
54
+ # fetched with that one read-only call (`distribution`, the distribution's id); nothing is
55
+ # changed on either side.
56
+ #
57
+ # @param held [Hash] the `SiteProjection` record: `domain`, and `live` or `distribution`,
58
+ # with `expect_new` (path patterns, comma separated) and `refs` (`!Ref Name=id` words,
59
+ # comma separated) when set
60
+ # @return [Hash{Symbol => Hash}] `output:` the report of a distribution that matches
61
+ # @raise [ConsoleCapture::Failure] when the project has no edge, the live configuration
62
+ # cannot be read, or the two differ; the message is the report
63
+ def compare(**held)
64
+ edge = edge_of(located(:domain, held[:domain]))
65
+ live = live_configuration(held)
66
+ live_edge = Projections::Site::LiveEdge
67
+ comparison = live_edge.new(edge, live, expect_new: words(held[:expect_new]),
68
+ refs: live_edge.refs_from(plain(held[:refs]))).call
69
+ raise ConsoleCapture::Failure, comparison.to_s unless comparison.clean?
70
+
71
+ { output: { value: comparison.to_s } }
72
+ end
73
+
44
74
  private
45
75
 
76
+ # The project's checked edge, read the way `project_site` reads it.
77
+ def edge_of(root)
78
+ registry = nil
79
+ outcome = ConsoleCapture.capture { registry = Tools::SiteRoutes.registry_for(root) }
80
+ raise ConsoleCapture::Failure, outcome.output.strip unless outcome.ok?
81
+
82
+ site = Projections::Site
83
+ chapter = site::Table.chapter(registry)
84
+ table = site::Table.read(chapter, registry: registry)
85
+ site::Edge.read(chapter, table: table, vocabulary: site::Table.vocabulary(registry)) ||
86
+ raise(ConsoleCapture::Failure, "#{root} declares no Edge rows")
87
+ rescue Projections::Site::Table::Invalid => e
88
+ raise ConsoleCapture::Failure, e.message
89
+ end
90
+
91
+ def live_configuration(held)
92
+ file = plain(held[:live])
93
+ id = plain(held[:distribution])
94
+ text = file ? read_saved(file) : fetch(id)
95
+ JSON.parse(text)
96
+ rescue JSON::ParserError => e
97
+ raise ConsoleCapture::Failure, "the live configuration is not JSON: #{e.message.lines.first.strip}"
98
+ end
99
+
100
+ def read_saved(path)
101
+ File.read(File.expand_path(path))
102
+ rescue SystemCallError => e
103
+ raise ConsoleCapture::Failure, "cannot read the live configuration: #{e.message}"
104
+ end
105
+
106
+ # The one AWS call: reading a distribution's configuration.
107
+ def fetch(id)
108
+ result = Shell.new.capture("aws", "cloudfront", "get-distribution-config", "--id", id.to_s)
109
+ return result.out if result.ok?
110
+
111
+ raise ConsoleCapture::Failure, "aws cloudfront get-distribution-config failed: #{message_of(result)}"
112
+ end
113
+
114
+ def words(argument) = plain(argument).to_s.split(",").map(&:strip).reject(&:empty?)
115
+
46
116
  def message_of(result)
47
117
  text = [result.err, result.out].map(&:strip).reject(&:empty?).join("\n")
48
118
  text.empty? ? "the tool ended with status #{result.status.exitstatus}" : text
49
119
  end
50
120
 
121
+ # The tool runs from the tool's own root, so a path the caller gave relative to where they
122
+ # stand is made absolute first; the extension is not a path.
123
+ def located(key, argument)
124
+ value = plain(argument)
125
+ value.nil? || key == :extension ? value : File.expand_path(value)
126
+ end
127
+
51
128
  def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
52
129
  end
53
130
  end
@@ -85,6 +85,12 @@ Hecks.behaviors "Codebase" do
85
85
  expect refused: "must match"
86
86
  end
87
87
 
88
+ test "ProjectToolsDoc refuses a run that is not named" do
89
+ tests "ProjectToolsDoc", on: "RegenerationRun"
90
+ input run: { value: "" }
91
+ expect refused: "must match"
92
+ end
93
+
88
94
  test "DecideCiGate refuses a run that is not named" do
89
95
  tests "DecideCiGate", on: "RegenerationRun"
90
96
  input run: { value: "" }, gate: { value: "runtime_changed" }
@@ -1027,6 +1027,22 @@ Hecks.bluebook "Hecks" do
1027
1027
  emits ProjectCiGatesRequested
1028
1028
  end
1029
1029
 
1030
+ command "ProjectToolsDoc" do
1031
+ role "Maintainer"
1032
+ goal "Write the launcher forms of docs/tools.md from the RetiredScript rows, or with check only compare them"
1033
+
1034
+ attribute :check, Switch, optional: true
1035
+ attribute :run, RunKey
1036
+ attribute :confirm, Switch, optional: true
1037
+
1038
+ sets :check
1039
+ sets :run
1040
+ sets :confirm
1041
+ sets :operation, to: "project_tools_doc"
1042
+
1043
+ emits ProjectToolsDocRequested
1044
+ end
1045
+
1030
1046
  command "DecideCiGate" do
1031
1047
  role "Maintainer"
1032
1048
  goal "Say whether this run's change touches what a CiGate row guards, as the gated job's detector"
@@ -1105,6 +1121,11 @@ Hecks.bluebook "Hecks" do
1105
1121
  trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
1106
1122
  end
1107
1123
 
1124
+ policy "ExamineWhenProjectToolsDocRequested" do
1125
+ on "RegenerationRun.ProjectToolsDocRequested"
1126
+ trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
1127
+ end
1128
+
1108
1129
  policy "ExamineWhenDecideCiGateRequested" do
1109
1130
  on "RegenerationRun.DecideCiGateRequested"
1110
1131
  trigger RegenerationRun::SourceTree::Examine, with: { run: :run }
@@ -176,4 +176,10 @@ Hecks.behaviors "Custodian" do
176
176
  input domain: { value: "examples/pizzas" }, run: { value: "" }
177
177
  expect refused: "must match"
178
178
  end
179
+
180
+ test "BuildHost refuses a target that is not a triple" do
181
+ tests "BuildHost", on: "Build"
182
+ input domain: { value: "examples/pizzas" }, run: { value: "host" }, target: { value: "arm64" }
183
+ expect refused: "must match"
184
+ end
179
185
  end