hecks 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door_scope.rb +2 -1
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +17 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +15 -2
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +40 -18
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +3 -10
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +53 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +1 -3
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +151 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +30 -15
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +8 -19
- data/rust/codegen/src/bridging.rs +7 -7
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +20 -9
- data/rust/codegen/src/naming.rs +4 -2
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +99 -2
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/kernel/cli.rs +16 -6
- data/rust/src/kernel/needs.rs +104 -9
- metadata +22 -8
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "../ports/clock"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# Answers the outside facts a command or a query `needs` (ADR 0081). A fact is answered once,
|
|
6
|
+
# before any given or filter reads the arguments, and the answer rides in the declaration's own
|
|
7
|
+
# arguments, so an event records it and a replay re-dispatches the recorded value rather than
|
|
8
|
+
# asking again.
|
|
9
|
+
module Needs
|
|
10
|
+
# What answers each fact a declaration may need: `now` is the clock port's reading in epoch
|
|
11
|
+
# seconds, `today` the day it falls in, whole days since the epoch in UTC.
|
|
12
|
+
ANSWERS = {
|
|
13
|
+
now: ->(registry) { Ports::Clock.now(registry) },
|
|
14
|
+
today: ->(registry) { Ports::Clock.now(registry) / 86_400 }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Fills each outside fact the declaration `needs` and the caller left out. A value the caller
|
|
20
|
+
# supplied is kept, so a test or a back-fill can name its own time.
|
|
21
|
+
#
|
|
22
|
+
# @param declaring [#needs, #attributes] the command or query being run
|
|
23
|
+
# @param args [Hash{Symbol => Object}] the arguments the caller passed
|
|
24
|
+
# @param registry [Runtime::Registry] where the clock port is bound
|
|
25
|
+
# @return [Hash{Symbol => Object}] `args`, with each missing needed fact answered
|
|
26
|
+
# @raise [Runtime::WiringError] when the port that answers a fact is not bound exactly once
|
|
27
|
+
def fill(declaring, args, registry:)
|
|
28
|
+
missing = declaring.needs.reject { |fact| args.key?(fact) || args.key?(fact.to_s) }
|
|
29
|
+
return args if missing.empty?
|
|
30
|
+
|
|
31
|
+
args.merge(missing.to_h { |fact| [fact, value(declaring, fact, registry)] })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The answer to one fact, in the shape the declaration's argument of that name takes: a bare
|
|
35
|
+
# Integer for an Integer attribute, else the one-field value object the language declares.
|
|
36
|
+
#
|
|
37
|
+
# @param declaring [#attributes] the command or query
|
|
38
|
+
# @param fact [Symbol] a key of `ANSWERS`
|
|
39
|
+
# @param registry [Runtime::Registry] where the clock port is bound
|
|
40
|
+
# @return [Integer, Hash{Symbol => Integer}] the answer, shaped for its argument
|
|
41
|
+
def value(declaring, fact, registry)
|
|
42
|
+
answer = ANSWERS.fetch(fact).call(registry)
|
|
43
|
+
attribute = declaring.attributes.find { |held| held.name.to_s == fact.to_s }
|
|
44
|
+
attribute&.type.to_s == "Integer" ? answer : { value: answer }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -5,6 +5,7 @@ require_relative "../ports/query/ordering"
|
|
|
5
5
|
require_relative "../query_specification/field_path"
|
|
6
6
|
require_relative "../query_specification/common/comparison"
|
|
7
7
|
require_relative "errors"
|
|
8
|
+
require_relative "needs"
|
|
8
9
|
require_relative "reference_hop"
|
|
9
10
|
require_relative "refusal_wording"
|
|
10
11
|
require_relative "tenant_scope"
|
|
@@ -203,6 +204,7 @@ module Hecks
|
|
|
203
204
|
def entity_rows(domain, aggregate, dotted, args)
|
|
204
205
|
entity_name, query_name = Naming.split_dotted(dotted)
|
|
205
206
|
entity, declared, list_attr = resolve_entity_query(aggregate, entity_name, query_name)
|
|
207
|
+
args = Needs.fill(declared, args, registry: @registry)
|
|
206
208
|
declared = TenantScope.apply(declared, args)
|
|
207
209
|
|
|
208
210
|
parent_key = Naming.reference_key(aggregate.hecks_name)
|
|
@@ -282,6 +284,7 @@ module Hecks
|
|
|
282
284
|
# from field defaults and only refuses when a field has none. `null_vo_argument!`
|
|
283
285
|
# refuses like any other wrong-shaped value. Command arguments are untouched.
|
|
284
286
|
def normalize_args(aggregate, declared, args)
|
|
287
|
+
args = Needs.fill(declared, args, registry: @registry)
|
|
285
288
|
declared.attributes.each_with_object(args.dup) do |attribute, normalized|
|
|
286
289
|
next unless normalized.key?(attribute.name)
|
|
287
290
|
|
|
@@ -408,9 +408,9 @@ module Hecks
|
|
|
408
408
|
File.expand_path(path.to_s).start_with?("#{GEM_CHAPTER_DIR}/")
|
|
409
409
|
end
|
|
410
410
|
|
|
411
|
-
# Two packages can share a chapter name by coincidence (
|
|
412
|
-
#
|
|
413
|
-
#
|
|
411
|
+
# Two packages can share a chapter name by coincidence (a stale vendored
|
|
412
|
+
# fork of Governance/Identity/Deploy still on an app's load paths).
|
|
413
|
+
# Intentional accumulation (several files declaring the same chapter on
|
|
414
414
|
# purpose) is distinguished by package root, not file identity, and is left
|
|
415
415
|
# untouched here — checked once, after every file has loaded.
|
|
416
416
|
def refuse_cross_package_bluebook_merge!
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require_relative "../rust_build"
|
|
6
|
+
require_relative "wasm"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module RustBuild
|
|
10
|
+
# Builds the Rust host binary for a domain and stages it with the domain's module and IR, the
|
|
11
|
+
# three files a container image copies in.
|
|
12
|
+
#
|
|
13
|
+
# Host.call(["path/to/domain", "--target=aarch64-unknown-linux-gnu", "--stage=build/host"])
|
|
14
|
+
#
|
|
15
|
+
# The host is compiled from the workspace this release carries (`rust/host`), so the binary is
|
|
16
|
+
# the one the installed gem was released with; nothing is fetched. The module and IR come from
|
|
17
|
+
# `Wasm`. `CARGO_TARGET_DIR` names where Cargo writes (the copy's `target/` for a client), so
|
|
18
|
+
# a second build recompiles only what changed. Stages `<domain>-host`, `<domain>.wasm` and
|
|
19
|
+
# `<domain>.ir.json` in `--stage` (default `.hecks/host/<target>/` in the working directory).
|
|
20
|
+
module Host
|
|
21
|
+
# The host crate's binary.
|
|
22
|
+
BINARY = "bootstrap"
|
|
23
|
+
|
|
24
|
+
# The toolchain Cargo runs under, as `Wasm` builds with.
|
|
25
|
+
TOOLCHAIN = "stable"
|
|
26
|
+
|
|
27
|
+
# A Rust target triple: `<arch>-<vendor>-<os>`, with an optional `-<env>`.
|
|
28
|
+
TRIPLE = /\A[a-z0-9_]+(-[a-z0-9_.]+){2,3}\z/
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @param argv [Array<String>] the domain's directory, then optional `--target=<triple>` and
|
|
33
|
+
# `--stage=<directory>`
|
|
34
|
+
# @return [Integer] the exit status
|
|
35
|
+
# @raise [Failure] when the toolchain, the target or a step is missing or fails
|
|
36
|
+
def call(argv)
|
|
37
|
+
domain = argv.find { |arg| !arg.start_with?("--") } or
|
|
38
|
+
raise Failure, "usage: hecks build_host <domain> [target=<triple>] [stage=<directory>]"
|
|
39
|
+
target = flag(argv, "target") || host_triple
|
|
40
|
+
require_buildable!(target)
|
|
41
|
+
stage = flag(argv, "stage") || File.join(Dir.pwd, ".hecks", "host", target)
|
|
42
|
+
|
|
43
|
+
status = Wasm.call([domain])
|
|
44
|
+
raise Failure, "project_wasm failed for #{domain}" unless status.zero?
|
|
45
|
+
|
|
46
|
+
binary = compile(target)
|
|
47
|
+
publish(binary, File.basename(domain.chomp("/")), stage)
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param argv [Array<String>] a tool's arguments
|
|
52
|
+
# @param name [String] a flag's name
|
|
53
|
+
# @return [String, nil] the value of `--<name>=<value>`, nil when absent
|
|
54
|
+
def flag(argv, name)
|
|
55
|
+
argv.filter_map { |arg| arg.delete_prefix("--#{name}=") if arg.start_with?("--#{name}=") }.last
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [String] the triple the Rust toolchain builds for by default
|
|
59
|
+
# @raise [Failure] when there is no Rust toolchain
|
|
60
|
+
def host_triple
|
|
61
|
+
require_rustup!
|
|
62
|
+
query("rustup", "run", TOOLCHAIN, "rustc", "-vV").to_s[/^host: (\S+)/, 1] or
|
|
63
|
+
raise Failure, "rustup has no #{TOOLCHAIN} toolchain. Install it once with:\n\n " \
|
|
64
|
+
"rustup toolchain install #{TOOLCHAIN}\n\nthen re-run the build."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def require_buildable!(target)
|
|
68
|
+
raise Failure, "#{target} is not a Rust target triple (for example aarch64-unknown-linux-gnu)" unless
|
|
69
|
+
TRIPLE.match?(target)
|
|
70
|
+
if target.start_with?("wasm")
|
|
71
|
+
raise Failure, "the host is a server binary; #{target} is a wasm target. Build the module with " \
|
|
72
|
+
"build.build_wasm, or name a native target"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
require_rustup!
|
|
76
|
+
installed = query("rustup", "target", "list", "--installed", "--toolchain", TOOLCHAIN).to_s.split
|
|
77
|
+
return if installed.include?(target) || target == host_triple
|
|
78
|
+
|
|
79
|
+
raise Failure, <<~MSG
|
|
80
|
+
#{target} isn't installed for the #{TOOLCHAIN} toolchain. Install it once with:
|
|
81
|
+
|
|
82
|
+
rustup target add #{target} --toolchain #{TOOLCHAIN}
|
|
83
|
+
|
|
84
|
+
then re-run the build.
|
|
85
|
+
MSG
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def require_rustup!
|
|
89
|
+
return if query("rustup", "--version")
|
|
90
|
+
|
|
91
|
+
raise Failure, "rustup isn't installed. Install the Rust toolchain from https://rustup.rs, " \
|
|
92
|
+
"then re-run the build."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# `rustup run`, not bare `cargo`, as `Wasm` does. A cross target links with the
|
|
96
|
+
# `<arch>-linux-gnu-gcc` on PATH unless `CARGO_TARGET_<TRIPLE>_LINKER` already names a linker.
|
|
97
|
+
def compile(target)
|
|
98
|
+
host_dir = File.join(RustBuild.rust_dir, "host")
|
|
99
|
+
target_dir = ENV.fetch("CARGO_TARGET_DIR", File.join(host_dir, "target"))
|
|
100
|
+
puts "== cargo build --release --target #{target} --bin #{BINARY} (rust/host) =="
|
|
101
|
+
env = { "CARGO_TARGET_DIR" => target_dir }.merge(linker_for(target))
|
|
102
|
+
RustBuild.command!("rustup", "run", TOOLCHAIN, "cargo", "build", "--release", "--target", target,
|
|
103
|
+
"--bin", BINARY, chdir: host_dir, env: env)
|
|
104
|
+
File.join(target_dir, target, "release", BINARY)
|
|
105
|
+
rescue Failure => e
|
|
106
|
+
raise Failure, "#{e.message}#{cross_hint(target)}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def linker_for(target)
|
|
110
|
+
variable = "CARGO_TARGET_#{target.upcase.tr('-.', '__')}_LINKER"
|
|
111
|
+
return {} if ENV.key?(variable) || target == host_triple
|
|
112
|
+
|
|
113
|
+
candidate = "#{target.split('-').first}-linux-gnu-gcc"
|
|
114
|
+
target.end_with?("linux-gnu") && query(candidate, "--version") ? { variable => candidate } : {}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def cross_hint(target)
|
|
118
|
+
return "" if target == host_triple
|
|
119
|
+
|
|
120
|
+
"\n#{target} differs from this machine's #{host_triple}, so linking needs a linker for it: " \
|
|
121
|
+
"put a cross compiler such as #{target.split('-').first}-linux-gnu-gcc on PATH, or set " \
|
|
122
|
+
"CARGO_TARGET_#{target.upcase.tr('-.', '__')}_LINKER"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def publish(binary, name, stage)
|
|
126
|
+
dist = File.join(RustBuild.rust_dir, "dist")
|
|
127
|
+
files = { binary => "#{name}-host", File.join(dist, "#{name}.wasm") => "#{name}.wasm",
|
|
128
|
+
File.join(dist, "#{name}.ir.json") => "#{name}.ir.json" }
|
|
129
|
+
files.each_key do |path|
|
|
130
|
+
raise Failure, "the build left no #{path}" unless File.file?(path)
|
|
131
|
+
end
|
|
132
|
+
FileUtils.mkdir_p(stage)
|
|
133
|
+
files.each { |from, to| FileUtils.cp(from, File.join(stage, to)) }
|
|
134
|
+
File.chmod(0o755, File.join(stage, "#{name}-host"))
|
|
135
|
+
puts "staged in #{stage}: #{files.values.join(', ')}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [String, nil] a program's combined output, nil when it is missing or fails
|
|
139
|
+
def query(*command)
|
|
140
|
+
out, status = Open3.capture2e(*command)
|
|
141
|
+
status.success? ? out : nil
|
|
142
|
+
rescue SystemCallError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -5,12 +5,14 @@ require_relative "../rust_build"
|
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module RustBuild
|
|
8
|
-
# The stdin a compiled domain binary reads: the steps to run, plus the
|
|
9
|
-
#
|
|
8
|
+
# The stdin a compiled domain binary reads: the steps to run, plus what the host would hand its
|
|
9
|
+
# kernel from the domain's IR.
|
|
10
10
|
#
|
|
11
|
-
# The kernel has no IR of its own, so
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# The kernel has no IR of its own, so the facts a declaration reaches it with arrive as
|
|
12
|
+
# top-level tables the Rust host builds from the domain's IR: `"defaults"`, a command's declared
|
|
13
|
+
# argument defaults (`{ "Domain::Aggregate.Command" => { "attribute" => value } }`); `"needs"`,
|
|
14
|
+
# the outside facts a command needs; and `"query_needs"`, the same for a query (kept apart: a
|
|
15
|
+
# command and a query may share a qualified name). A harness that sends only `"steps"` would
|
|
14
16
|
# leave the kernel refusing an argument Ruby fills, so every conformance caller builds its
|
|
15
17
|
# input here.
|
|
16
18
|
module KernelInput
|
|
@@ -18,11 +20,10 @@ module Hecks
|
|
|
18
20
|
|
|
19
21
|
# @param domain_path [String] the domain's directory; its basename names the generated module
|
|
20
22
|
# @param steps [Array<Hash>] the steps to run
|
|
21
|
-
# @return [Hash{String => Object}] the input;
|
|
23
|
+
# @return [Hash{String => Object}] the input; each table is there only when it has an entry
|
|
22
24
|
def build(domain_path, steps)
|
|
23
25
|
input = { "steps" => steps }
|
|
24
|
-
|
|
25
|
-
input["defaults"] = defaults unless defaults.empty?
|
|
26
|
+
tables_for(domain_path).each { |name, table| input[name] = table unless table.empty? }
|
|
26
27
|
input
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -35,27 +36,48 @@ module Hecks
|
|
|
35
36
|
#
|
|
36
37
|
# @param domain_path [String] the domain's directory
|
|
37
38
|
# @return [Hash{String => Hash{String => Object}}] empty when the IR is absent or has none
|
|
38
|
-
def defaults_for(domain_path)
|
|
39
|
-
|
|
39
|
+
def defaults_for(domain_path) = tables_for(domain_path).fetch("defaults")
|
|
40
|
+
|
|
41
|
+
# The three tables of one domain: `"defaults"`, `"needs"` and `"query_needs"`.
|
|
42
|
+
#
|
|
43
|
+
# @param domain_path [String] the domain's directory
|
|
44
|
+
# @return [Hash{String => Hash}] each table, empty when the IR is absent or declares none
|
|
45
|
+
def tables_for(domain_path)
|
|
46
|
+
tables = { "defaults" => {}, "needs" => {}, "query_needs" => {} }
|
|
47
|
+
ir = generated_ir(domain_path) or return tables
|
|
40
48
|
domain = ir["name"].to_s
|
|
41
|
-
Array(ir["aggregates"]).
|
|
42
|
-
|
|
43
|
-
end
|
|
49
|
+
Array(ir["aggregates"]).each { |aggregate| collect(aggregate, "#{domain}::#{aggregate['name']}", tables) }
|
|
50
|
+
tables
|
|
44
51
|
end
|
|
45
52
|
|
|
46
|
-
# Adds
|
|
53
|
+
# Adds what `node`'s commands and queries declare, then what each entity nested in it does,
|
|
54
|
+
# under `prefix`.
|
|
47
55
|
#
|
|
48
56
|
# @param node [Hash] an aggregate or entity of the IR
|
|
49
57
|
# @param prefix [String] the verb path down to `node`
|
|
50
|
-
# @param
|
|
58
|
+
# @param tables [Hash] filled in place
|
|
51
59
|
# @return [void]
|
|
52
|
-
def
|
|
60
|
+
def collect(node, prefix, tables)
|
|
53
61
|
Array(node["commands"]).each do |command|
|
|
62
|
+
verb = "#{prefix}.#{command['name']}"
|
|
54
63
|
held = Array(command["attributes"]).reject { |attribute| attribute["default"].nil? }
|
|
55
64
|
.to_h { |attribute| [attribute["name"], attribute["default"]] }
|
|
56
|
-
|
|
65
|
+
tables["defaults"][verb] = held unless held.empty?
|
|
66
|
+
tables["needs"][verb] = needs_of(command) unless Array(command["needs"]).empty?
|
|
67
|
+
end
|
|
68
|
+
Array(node["queries"]).each do |query|
|
|
69
|
+
tables["query_needs"]["#{prefix}.#{query['name']}"] = needs_of(query) unless Array(query["needs"]).empty?
|
|
70
|
+
end
|
|
71
|
+
Array(node["entities"]).each { |entity| collect(entity, "#{prefix}.#{entity['name']}", tables) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param declaration [Hash] a command or query of the IR that declares `needs`
|
|
75
|
+
# @return [Array<Hash>] each needed fact with the type of the argument of the same name
|
|
76
|
+
def needs_of(declaration)
|
|
77
|
+
Array(declaration["needs"]).map do |need|
|
|
78
|
+
argument = Array(declaration["attributes"]).find { |attribute| attribute["name"] == need["fact"] }
|
|
79
|
+
{ "fact" => need["fact"], "type" => argument ? argument["type"].to_s : "" }
|
|
57
80
|
end
|
|
58
|
-
Array(node["entities"]).each { |entity| collect_defaults(entity, "#{prefix}.#{entity['name']}", table) }
|
|
59
81
|
end
|
|
60
82
|
|
|
61
83
|
# @param domain_path [String] the domain's directory
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
5
|
require "tmpdir"
|
|
6
|
-
require_relative "../rust_literal"
|
|
7
6
|
require_relative "../write_if_changed"
|
|
8
7
|
|
|
9
8
|
module Hecks
|
|
@@ -78,7 +77,6 @@ module Hecks
|
|
|
78
77
|
def generate_meta(stage, tmp)
|
|
79
78
|
ir_path = write_ir(tmp, "meta", @meta)
|
|
80
79
|
run("full", stage, "meta", META_LABEL, ir_path)
|
|
81
|
-
write_sidecars(File.join(stage, "meta"), @meta, META_LABEL)
|
|
82
80
|
end
|
|
83
81
|
|
|
84
82
|
def generate_target(stage, tmp)
|
|
@@ -87,9 +85,6 @@ module Hecks
|
|
|
87
85
|
args.push(chapter.mod_name, chapter.label, write_ir(tmp, chapter.mod_name, chapter.ir))
|
|
88
86
|
end
|
|
89
87
|
run("full", *args)
|
|
90
|
-
[@target, *@chapters].each do |chapter|
|
|
91
|
-
write_sidecars(File.join(stage, chapter.mod_name), chapter.ir, chapter.label)
|
|
92
|
-
end
|
|
93
88
|
end
|
|
94
89
|
|
|
95
90
|
def write_ir(tmp, name, tree)
|
|
@@ -98,19 +93,6 @@ module Hecks
|
|
|
98
93
|
path
|
|
99
94
|
end
|
|
100
95
|
|
|
101
|
-
# `ir.json` is what `rust/host` reads at runtime, and `metadata.rs` embeds the same text;
|
|
102
|
-
# the codegen binary writes neither.
|
|
103
|
-
def write_sidecars(mod_dir, tree, label)
|
|
104
|
-
text = JSON.pretty_generate(tree)
|
|
105
|
-
File.write(File.join(mod_dir, "ir.json"), text)
|
|
106
|
-
File.write(File.join(mod_dir, "metadata.rs"), <<~RUST)
|
|
107
|
-
// GENERATED by hecks project_rust — #{label}'s own canonical IR,
|
|
108
|
-
// embedded for runtime self-description. Not read by any dispatch
|
|
109
|
-
// function in this module — introspection only.
|
|
110
|
-
pub const IR_JSON: &str = #{RustLiteral.string(text)};
|
|
111
|
-
RUST
|
|
112
|
-
end
|
|
113
|
-
|
|
114
96
|
def run(*args)
|
|
115
97
|
return if system(self.class.binary(@rust_dir), *args, out: $stdout, err: $stderr)
|
|
116
98
|
|
|
@@ -5,7 +5,6 @@ require "json"
|
|
|
5
5
|
require_relative "../../hecks"
|
|
6
6
|
require_relative "../bluebook/meta_validator"
|
|
7
7
|
require_relative "../ports/persistence/plugins/era"
|
|
8
|
-
require_relative "append_optionals"
|
|
9
8
|
require_relative "domain_name"
|
|
10
9
|
require_relative "write_if_changed"
|
|
11
10
|
|
|
@@ -83,20 +82,14 @@ module Hecks
|
|
|
83
82
|
vendored = @registry.hecksagons.values.flat_map(&:vendored_packages)
|
|
84
83
|
chapters = (@registry.bluebooks.keys - [@domain_name]).map do |name|
|
|
85
84
|
ir = Hecks::Projector::Exporter.call(@registry).fetch(name)
|
|
86
|
-
CodegenRun::Chapter.new(name.downcase, "#{@domain} (#{attachment(name, vendored)})",
|
|
85
|
+
CodegenRun::Chapter.new(name.downcase, "#{@domain} (#{attachment(name, vendored)})", json_shaped(ir))
|
|
87
86
|
end
|
|
88
87
|
CodegenRun.new(
|
|
89
|
-
rust_dir: @rust_dir, out_root: @out_root, meta:
|
|
90
|
-
target: CodegenRun::Chapter.new(@mod_name, @domain,
|
|
88
|
+
rust_dir: @rust_dir, out_root: @out_root, meta: json_shaped(meta), chapters: chapters,
|
|
89
|
+
target: CodegenRun::Chapter.new(@mod_name, @domain, @ir)
|
|
91
90
|
).call
|
|
92
91
|
end
|
|
93
92
|
|
|
94
|
-
# The IR as the generator reads it: string-keyed through JSON, with the append-optional
|
|
95
|
-
# fields marked, since `ir.json` records them.
|
|
96
|
-
def prepared_ir(tree, shaped: true)
|
|
97
|
-
AppendOptionals.mark(shaped ? json_shaped(tree) : tree)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
93
|
# Round-trips a value through JSON so generators see the string-keyed shape a real `ir.json`
|
|
101
94
|
# carries, never live Ruby symbols.
|
|
102
95
|
def json_shaped(payload) = JSON.parse(JSON.generate(payload), symbolize_names: true)
|
data/lib/hecks/rust_build.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Hecks
|
|
|
31
31
|
TOOLS = {
|
|
32
32
|
"project_rust" => ["rust_build/project_rust", :ProjectRust],
|
|
33
33
|
"project_wasm" => ["rust_build/wasm", :Wasm],
|
|
34
|
+
"project_host" => ["rust_build/host", :Host],
|
|
34
35
|
"project_wasm_browser" => ["rust_build/wasm_browser", :WasmBrowser],
|
|
35
36
|
"rust_conformance" => ["rust_build/conformance", :Conformance],
|
|
36
37
|
"rust_conformance_fuzz" => ["rust_build/conformance_fuzz", :ConformanceFuzz],
|
|
@@ -101,6 +101,10 @@ Hecks.bluebook "Site" do
|
|
|
101
101
|
attribute :template, Location, optional: true
|
|
102
102
|
attribute :extension, Extension, optional: true
|
|
103
103
|
attribute :check, Switch, optional: true
|
|
104
|
+
attribute :live, Location, optional: true
|
|
105
|
+
attribute :distribution, DistributionId, optional: true
|
|
106
|
+
attribute :expect_new, PathList, optional: true
|
|
107
|
+
attribute :refs, PolicyRefs, optional: true
|
|
104
108
|
attribute :output, Report, optional: true
|
|
105
109
|
attribute :refusal, Report, optional: true
|
|
106
110
|
identified_by :run
|
|
@@ -134,6 +138,21 @@ Hecks.bluebook "Site" do
|
|
|
134
138
|
attribute :value, Boolean
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
# A CloudFront distribution's id, whose configuration `check_live` reads.
|
|
142
|
+
value_object "DistributionId" do
|
|
143
|
+
attribute :value, String, pattern: '^[A-Z0-9]+$'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Path patterns, comma separated: the behaviours a change is about to add to the live distribution.
|
|
147
|
+
value_object "PathList" do
|
|
148
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Policy references and the ids they stand for live, as `!Ref Name=id`, comma separated.
|
|
152
|
+
value_object "PolicyRefs" do
|
|
153
|
+
attribute :value, String, pattern: '[^\t\n\r]'
|
|
154
|
+
end
|
|
155
|
+
|
|
137
156
|
value_object "Report" do
|
|
138
157
|
attribute :value, String
|
|
139
158
|
end
|
|
@@ -166,8 +185,36 @@ Hecks.bluebook "Site" do
|
|
|
166
185
|
emits "SiteProjectionRequested"
|
|
167
186
|
end
|
|
168
187
|
|
|
188
|
+
# `hecks site site_projection.check_live <project> live=<saved-config.json> | distribution=<id>
|
|
189
|
+
# [expect_new=<paths>] [refs=<!Ref Name=id,...>]`. Compares the project's generated CloudFront
|
|
190
|
+
# behaviours with a live distribution's, changing neither. `live` is a saved answer of
|
|
191
|
+
# `aws cloudfront get-distribution-config`; `distribution` fetches it with that one read-only call.
|
|
192
|
+
command "CheckLive" do
|
|
193
|
+
goal "Compare a project's generated CloudFront behaviours with a live distribution's"
|
|
194
|
+
|
|
195
|
+
attribute :domain, DomainPath
|
|
196
|
+
attribute :run, RunKey
|
|
197
|
+
attribute :live, Location, optional: true
|
|
198
|
+
attribute :distribution, DistributionId, optional: true
|
|
199
|
+
attribute :expect_new, PathList, optional: true
|
|
200
|
+
attribute :refs, PolicyRefs, optional: true
|
|
201
|
+
|
|
202
|
+
given("exactly one of a saved configuration and a distribution is named: pass live= or distribution=, not both") do
|
|
203
|
+
(live.set? && distribution.unset?) || (live.unset? && distribution.set?)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
sets :domain
|
|
207
|
+
sets :run
|
|
208
|
+
sets :live
|
|
209
|
+
sets :distribution
|
|
210
|
+
sets :expect_new
|
|
211
|
+
sets :refs
|
|
212
|
+
|
|
213
|
+
emits "LiveCheckRequested"
|
|
214
|
+
end
|
|
215
|
+
|
|
169
216
|
command "Complete" do
|
|
170
|
-
goal "Record that routes.ts was written, or found current, and
|
|
217
|
+
goal "Record that routes.ts was written, or found current, or the live edge matched, and what it said"
|
|
171
218
|
|
|
172
219
|
reference_to SiteProjection
|
|
173
220
|
attribute :output, Report
|
|
@@ -207,6 +254,11 @@ Hecks.bluebook "Site" do
|
|
|
207
254
|
trigger SiteProjection::SiteToolchain::Project, with: { run: :run }
|
|
208
255
|
end
|
|
209
256
|
|
|
257
|
+
policy "CheckWhenRequested" do
|
|
258
|
+
on "SiteProjection.LiveCheckRequested"
|
|
259
|
+
trigger SiteProjection::SiteToolchain::Compare, with: { run: :run }
|
|
260
|
+
end
|
|
261
|
+
|
|
210
262
|
policy "RecordTheProjection" do
|
|
211
263
|
on "SiteProjection.SiteAnswered"
|
|
212
264
|
trigger SiteProjection::Complete, with: { output: :output }
|