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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +45 -5
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +40 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +28 -4
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +51 -17
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +7 -140
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +80 -14
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +2 -4
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +152 -1
- data/lib/hecks.rb +1 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +31 -16
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +9 -20
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +8 -8
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +23 -13
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -3
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +193 -29
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +18 -8
- data/rust/src/kernel/needs.rs +104 -9
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +24 -30
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- 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
|
-
|
|
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",
|
|
23
|
-
|
|
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
|
|
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
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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`
|
|
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
|
|
31
|
-
#
|
|
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
|
-
|
|
37
|
-
|
|
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
|