hecks 3.0.4 → 3.1.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/exe/hecks +25 -7
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +31 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/console.rb +24 -8
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +69 -18
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +38 -9
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +11 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +52 -32
- data/lib/hecks/doors/launcher_options.rb +23 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +8 -4
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +7 -4
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +2 -1
- data/lib/hecks/projections/deploy/box/settings.rb +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +172 -95
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +2 -0
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/invocation.rb +5 -3
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/host/src/web/newsletter/template.rs +69 -0
- data/rust/host/src/web/newsletter/tests.rs +43 -0
- data/rust/host/src/web/newsletter.rs +6 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- metadata +39 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hecks/vocabulary"
|
|
4
|
+
require "hecks/bluebook/model_check"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module RustBuild
|
|
8
|
+
# Whether a domain's directory name can double as a Rust module identifier and a Cargo
|
|
9
|
+
# feature name. The reserved-word half is `ModelCheck.rust_reserved_name_findings`, the one
|
|
10
|
+
# shared check.
|
|
11
|
+
module DomainName
|
|
12
|
+
# Every key `rust/Cargo.toml` uses outside `[features]`, plus `default`, which Cargo
|
|
13
|
+
# reserves for the auto-enabled feature set. Declared once as the `CargoReservedName`
|
|
14
|
+
# vocabulary.
|
|
15
|
+
CARGO_RESERVED = Hecks::Vocabulary.fetch("CargoReservedName")
|
|
16
|
+
|
|
17
|
+
# @param name [String] the domain's directory name
|
|
18
|
+
# @return [Boolean] whether it is a plain lowercase identifier and no reserved word
|
|
19
|
+
def self.valid?(name)
|
|
20
|
+
text = name.to_s
|
|
21
|
+
text.match?(/\A[a-z_][a-z0-9_]*\z/) &&
|
|
22
|
+
Hecks::Bluebook::ModelCheck.rust_reserved_name_findings(domain_name: text).empty?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../rust_build"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module RustBuild
|
|
8
|
+
# The stdin a compiled domain binary reads: the steps to run, plus the declared argument
|
|
9
|
+
# defaults the host would hand its kernel.
|
|
10
|
+
#
|
|
11
|
+
# The kernel has no IR of its own, so a command's declared defaults reach it as a top-level
|
|
12
|
+
# `"defaults"` table, `{ "Domain::Aggregate.Command" => { "attribute" => value } }`, the same
|
|
13
|
+
# one the Rust host builds from the domain's IR. A harness that sends only `"steps"` would
|
|
14
|
+
# leave the kernel refusing an argument Ruby fills, so every conformance caller builds its
|
|
15
|
+
# input here.
|
|
16
|
+
module KernelInput
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param domain_path [String] the domain's directory; its basename names the generated module
|
|
20
|
+
# @param steps [Array<Hash>] the steps to run
|
|
21
|
+
# @return [Hash{String => Object}] the input; `"defaults"` is there only when one is declared
|
|
22
|
+
def build(domain_path, steps)
|
|
23
|
+
input = { "steps" => steps }
|
|
24
|
+
defaults = defaults_for(domain_path)
|
|
25
|
+
input["defaults"] = defaults unless defaults.empty?
|
|
26
|
+
input
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param domain_path [String] the domain's directory
|
|
30
|
+
# @return [String] the input as JSON, ready for the binary's stdin
|
|
31
|
+
def json(domain_path, steps) = JSON.generate(build(domain_path, steps))
|
|
32
|
+
|
|
33
|
+
# Each command's declared defaults, read from the domain's generated IR.
|
|
34
|
+
#
|
|
35
|
+
# @param domain_path [String] the domain's directory
|
|
36
|
+
# @return [Hash{String => Hash{String => Object}}] empty when the IR is absent or has none
|
|
37
|
+
def defaults_for(domain_path)
|
|
38
|
+
ir = generated_ir(domain_path) or return {}
|
|
39
|
+
domain = ir["name"].to_s
|
|
40
|
+
Array(ir["aggregates"]).each_with_object({}) do |aggregate, table|
|
|
41
|
+
Array(aggregate["commands"]).each do |command|
|
|
42
|
+
held = Array(command["attributes"]).reject { |attribute| attribute["default"].nil? }
|
|
43
|
+
.to_h { |attribute| [attribute["name"], attribute["default"]] }
|
|
44
|
+
table["#{domain}::#{aggregate['name']}.#{command['name']}"] = held unless held.empty?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param domain_path [String] the domain's directory
|
|
50
|
+
# @return [Hash, nil] the parsed generated IR, or nil when the domain has none
|
|
51
|
+
def generated_ir(domain_path)
|
|
52
|
+
feature = File.basename(domain_path.to_s.chomp("/")).downcase
|
|
53
|
+
path = File.join(RustBuild.rust_dir, "src", "generated", feature, "ir.json")
|
|
54
|
+
File.file?(path) ? JSON.parse(File.read(path)) : nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require_relative "../rust_literal"
|
|
7
|
+
require_relative "../write_if_changed"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module RustBuild
|
|
11
|
+
class ProjectRust
|
|
12
|
+
# Generates a domain's Rust by running `hecks-codegen full` on IR the caller built from the
|
|
13
|
+
# live registry, into a scratch directory, then copies only the files whose bytes changed
|
|
14
|
+
# into `src/generated/`. An unchanged file keeps its mtime, so Cargo does not rebuild.
|
|
15
|
+
class CodegenRun
|
|
16
|
+
META_LABEL = "the self-hosted language (lib/hecks/language/bluebook)"
|
|
17
|
+
|
|
18
|
+
# One chapter's IR, generated into its own module.
|
|
19
|
+
Chapter = Struct.new(:mod_name, :label, :ir)
|
|
20
|
+
|
|
21
|
+
# The `hecks-codegen` crate: the workspace's own copy when it carries one (a packaged
|
|
22
|
+
# workspace does), otherwise the checkout's, so a scratch crate holding only the generated
|
|
23
|
+
# tree still finds it.
|
|
24
|
+
#
|
|
25
|
+
# @param rust_dir [String] the workspace being generated into
|
|
26
|
+
# @return [String] the crate's directory
|
|
27
|
+
def self.crate_dir(rust_dir)
|
|
28
|
+
own = File.join(rust_dir, "codegen")
|
|
29
|
+
File.directory?(own) ? own : File.join(RustBuild::ROOT, "rust/codegen")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Where Cargo puts the binary: under `CARGO_TARGET_DIR` when a build sets it (a copy of the
|
|
33
|
+
# packaged workspace does, so nothing is written into the installed gem), else in the crate.
|
|
34
|
+
#
|
|
35
|
+
# @param rust_dir [String] the workspace being generated into
|
|
36
|
+
# @param env [#fetch] the environment Cargo will run with
|
|
37
|
+
# @return [String] the binary's path
|
|
38
|
+
def self.binary(rust_dir, env: ENV)
|
|
39
|
+
target = env.fetch("CARGO_TARGET_DIR") { File.join(crate_dir(rust_dir), "target") }
|
|
40
|
+
File.join(target, "debug/hecks-codegen")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param rust_dir [String] the workspace being generated into
|
|
44
|
+
# @param out_root [String] the crate's `src/generated`
|
|
45
|
+
# @param target [Chapter] the domain being projected
|
|
46
|
+
# @param chapters [Array<Chapter>] every other bluebook the target attached
|
|
47
|
+
# @param meta [Hash] the self-hosted language's IR
|
|
48
|
+
def initialize(rust_dir:, out_root:, target:, chapters:, meta:)
|
|
49
|
+
@rust_dir = rust_dir
|
|
50
|
+
@out_root = out_root
|
|
51
|
+
@target = target
|
|
52
|
+
@chapters = chapters
|
|
53
|
+
@meta = meta
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [void]
|
|
57
|
+
# @raise [Failure] when `hecks-codegen` cannot be built or exits non-zero
|
|
58
|
+
def call
|
|
59
|
+
build_binary
|
|
60
|
+
Dir.mktmpdir("hecks-codegen-full") do |tmp|
|
|
61
|
+
stage = File.join(tmp, "generated")
|
|
62
|
+
FileUtils.mkdir_p(stage)
|
|
63
|
+
generate_meta(stage, tmp)
|
|
64
|
+
generate_target(stage, tmp)
|
|
65
|
+
sync(stage)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_binary
|
|
72
|
+
dir = self.class.crate_dir(@rust_dir)
|
|
73
|
+
return if system("cargo", "build", "--quiet", chdir: dir, out: $stdout, err: $stderr)
|
|
74
|
+
|
|
75
|
+
raise Failure, "hecks project_rust: cargo build failed in #{dir} — run it there directly to see why"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def generate_meta(stage, tmp)
|
|
79
|
+
ir_path = write_ir(tmp, "meta", @meta)
|
|
80
|
+
run("full", stage, "meta", META_LABEL, ir_path)
|
|
81
|
+
write_sidecars(File.join(stage, "meta"), @meta, META_LABEL)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def generate_target(stage, tmp)
|
|
85
|
+
args = [stage, @target.mod_name, @target.label, write_ir(tmp, @target.mod_name, @target.ir)]
|
|
86
|
+
@chapters.each do |chapter|
|
|
87
|
+
args.push(chapter.mod_name, chapter.label, write_ir(tmp, chapter.mod_name, chapter.ir))
|
|
88
|
+
end
|
|
89
|
+
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
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_ir(tmp, name, tree)
|
|
96
|
+
path = File.join(tmp, "#{name}_ir.json")
|
|
97
|
+
File.write(path, JSON.pretty_generate(tree))
|
|
98
|
+
path
|
|
99
|
+
end
|
|
100
|
+
|
|
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
|
+
def run(*args)
|
|
115
|
+
return if system(self.class.binary(@rust_dir), *args, out: $stdout, err: $stderr)
|
|
116
|
+
|
|
117
|
+
raise Failure, "hecks project_rust: hecks-codegen #{args.first} failed"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Each module directory is tracked, so a file an earlier run wrote and this one did not
|
|
121
|
+
# (a removed aggregate) is pruned. Pruning waits for the whole copy to succeed: after a
|
|
122
|
+
# failure, the files not yet reached would look like orphans and be deleted.
|
|
123
|
+
def sync(stage)
|
|
124
|
+
Dir.children(stage).sort.each do |name|
|
|
125
|
+
source = File.join(stage, name)
|
|
126
|
+
dest = File.join(@out_root, name)
|
|
127
|
+
WriteIfChanged.push_directory(dest)
|
|
128
|
+
copy_changed(source, dest)
|
|
129
|
+
WriteIfChanged.pop_and_prune(dest)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def copy_changed(source, dest)
|
|
134
|
+
Dir.children(source).sort.each do |file|
|
|
135
|
+
path = File.join(dest, file)
|
|
136
|
+
wrote = WriteIfChanged.call(path, File.read(File.join(source, file), encoding: "UTF-8"))
|
|
137
|
+
puts(wrote ? "wrote #{path}" : "#{path} unchanged")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -43,7 +43,7 @@ module Hecks
|
|
|
43
43
|
@file.puts "// chapter (governance/identity — no merged.rs of its own, so absent"
|
|
44
44
|
@file.puts "// from `domains`) stays unconditional: it has no single feature of its"
|
|
45
45
|
@file.puts "// own to gate behind, and is compiled in by whichever domain(s) attach"
|
|
46
|
-
@file.puts "// it via `
|
|
46
|
+
@file.puts "// it via `attaches`."
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def modules
|
|
@@ -5,20 +5,24 @@ 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 "
|
|
8
|
+
require_relative "append_optionals"
|
|
9
|
+
require_relative "domain_name"
|
|
10
|
+
require_relative "write_if_changed"
|
|
9
11
|
|
|
10
12
|
module Hecks
|
|
11
13
|
module RustBuild
|
|
12
14
|
# Generates Rust source for one domain into the workspace's `src/generated/`, and keeps the
|
|
13
|
-
# workspace's `Cargo.toml` features in step
|
|
14
|
-
# architecture of the generator itself.
|
|
15
|
+
# workspace's `Cargo.toml` features in step.
|
|
15
16
|
#
|
|
16
17
|
# ProjectRust.call(["path/to/domain"])
|
|
17
18
|
#
|
|
19
|
+
# `hecks-codegen` is the generator (ADR 0086): this class builds the IR from the live registry
|
|
20
|
+
# and `CodegenRun` runs the binary on it. `HECKS_CODEGEN=ruby` selects the Ruby generator in
|
|
21
|
+
# `rust/project` until that is deleted; `HECKS_PARSER=rust` with `HECKS_CODEGEN=rust` runs
|
|
22
|
+
# `rust/project_rust_pipeline.rb` instead, with no Ruby load of the domain.
|
|
23
|
+
#
|
|
18
24
|
# The era plugin is required unconditionally so the generator works for any domain, lineage
|
|
19
25
|
# capable or not; `pg` stays lazy inside it, so nothing here opens a database connection.
|
|
20
|
-
# With `HECKS_PARSER=rust` and `HECKS_CODEGEN=rust` together, the whole pipeline runs through
|
|
21
|
-
# `rust/project_rust_pipeline.rb` instead, with no Ruby load of the domain.
|
|
22
26
|
class ProjectRust
|
|
23
27
|
# The optional attachments `rust/host` reads from `ir.json`, each omitted when nothing
|
|
24
28
|
# attached provides it: exporter method, then the key it is written under.
|
|
@@ -43,6 +47,7 @@ module Hecks
|
|
|
43
47
|
def call
|
|
44
48
|
validate_name!
|
|
45
49
|
if ENV["HECKS_PARSER"] == "rust" && ENV["HECKS_CODEGEN"] == "rust"
|
|
50
|
+
require_relative "../../../rust/project"
|
|
46
51
|
require_relative "../../../rust/project_rust_pipeline"
|
|
47
52
|
RustProjectPipeline.call(@domain)
|
|
48
53
|
else
|
|
@@ -56,13 +61,13 @@ module Hecks
|
|
|
56
61
|
# The name becomes a directory, a `pub mod` identifier and a Cargo feature, so it is checked
|
|
57
62
|
# before any side effect (the `meta/` directory is rewritten below).
|
|
58
63
|
def validate_name!
|
|
59
|
-
return if
|
|
64
|
+
return if DomainName.valid?(@mod_name)
|
|
60
65
|
|
|
61
66
|
raise Failure, "hecks project_rust: domain name #{@mod_name.inspect} (from #{@domain.inspect}) can't " \
|
|
62
67
|
"be used as-is — it has to double as a Rust module identifier and a Cargo feature " \
|
|
63
68
|
"name, and this one is either not a plain lowercase identifier, is a Rust keyword, " \
|
|
64
69
|
"or collides with a reserved Cargo.toml key " \
|
|
65
|
-
"(#{
|
|
70
|
+
"(#{DomainName::CARGO_RESERVED.join(', ')}). " \
|
|
66
71
|
"Rename the domain directory."
|
|
67
72
|
end
|
|
68
73
|
|
|
@@ -72,17 +77,45 @@ module Hecks
|
|
|
72
77
|
@out_root = File.join(@rust_dir, "src/generated")
|
|
73
78
|
FileUtils.mkdir_p(@out_root)
|
|
74
79
|
build_target_ir
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
FileUtils.rm_rf(File.join(@out_root, "active"))
|
|
81
|
+
ENV["HECKS_CODEGEN"] == "ruby" ? generate_with_ruby : generate_with_codegen
|
|
77
82
|
write_root_mod
|
|
78
83
|
sync_cargo_features
|
|
79
84
|
end
|
|
80
85
|
|
|
86
|
+
# The rollback path (ADR 0086): the Ruby generator in `rust/project`, until it is deleted.
|
|
87
|
+
def generate_with_ruby
|
|
88
|
+
require_relative "../../../rust/project"
|
|
89
|
+
write_meta
|
|
90
|
+
write_target
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# `hecks-codegen` is the generator; this process only builds the IR it reads from the live
|
|
94
|
+
# registry, so a domain's persistence, seams, translations and source text reach `ir.json`.
|
|
95
|
+
def generate_with_codegen
|
|
96
|
+
meta = Hecks::Projector::Exporter.call(Hecks::Bluebook::MetaValidator.grammar_registry).fetch("Bluebook")
|
|
97
|
+
vendored = @registry.hecksagons.values.flat_map(&:vendored_packages)
|
|
98
|
+
chapters = (@registry.bluebooks.keys - [@domain_name]).map do |name|
|
|
99
|
+
ir = Hecks::Projector::Exporter.call(@registry).fetch(name)
|
|
100
|
+
CodegenRun::Chapter.new(name.downcase, "#{@domain} (#{attachment(name, vendored)})", prepared_ir(ir))
|
|
101
|
+
end
|
|
102
|
+
CodegenRun.new(
|
|
103
|
+
rust_dir: @rust_dir, out_root: @out_root, meta: prepared_ir(meta), chapters: chapters,
|
|
104
|
+
target: CodegenRun::Chapter.new(@mod_name, @domain, prepared_ir(@ir, shaped: false))
|
|
105
|
+
).call
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The IR as `rust/project` hands it to its generators: string-keyed through JSON, with the
|
|
109
|
+
# append-optional fields marked, since `ir.json` records them.
|
|
110
|
+
def prepared_ir(tree, shaped: true)
|
|
111
|
+
AppendOptionals.mark(shaped ? json_shaped(tree) : tree)
|
|
112
|
+
end
|
|
113
|
+
|
|
81
114
|
# Round-trips a value through JSON so generators see the string-keyed shape a real `ir.json`
|
|
82
115
|
# carries, never live Ruby symbols.
|
|
83
116
|
def json_shaped(payload) = JSON.parse(JSON.generate(payload), symbolize_names: true)
|
|
84
117
|
|
|
85
|
-
# `root:` is the domain's own directory: a domain that declares `
|
|
118
|
+
# `root:` is the domain's own directory: a domain that declares `attaches ... from: :vendor`
|
|
86
119
|
# resolves its vendored chapters beneath it.
|
|
87
120
|
def load_registry
|
|
88
121
|
@registry = Hecks::Runtime::Registry.new(root: File.expand_path(@domain))
|
|
@@ -100,7 +133,7 @@ module Hecks
|
|
|
100
133
|
@domain_name = @registry.bluebooks.keys.first
|
|
101
134
|
end
|
|
102
135
|
|
|
103
|
-
# Translation edges, hecksagons (whose `
|
|
136
|
+
# Translation edges, hecksagons (whose `attaches` loads a framework chapter into the
|
|
104
137
|
# same registry) and worlds (whose `default_adapter` binds what a hecksagon leaves out), in
|
|
105
138
|
# the order the language loads them.
|
|
106
139
|
def load_siblings
|
|
@@ -134,7 +167,7 @@ module Hecks
|
|
|
134
167
|
if !seams[:membership].empty? && seams[:identity].empty?
|
|
135
168
|
raise Failure, "#{@domain_name} provides membership but not identity — rust/host Google sign-in " \
|
|
136
169
|
"cannot register or link an identity from ir.json. Attach Identity " \
|
|
137
|
-
"(`
|
|
170
|
+
"(`attaches \"Identity\"` plus a sibling Hecks.hecksagon \"Identity\") so the " \
|
|
138
171
|
"hecksagon, not a deploy-time guess, names the identity verbs."
|
|
139
172
|
end
|
|
140
173
|
add_edges_and_source
|
|
@@ -163,8 +196,7 @@ module Hecks
|
|
|
163
196
|
# Tracked per directory: several domains coexist on disk, and `pop_and_prune` deletes only
|
|
164
197
|
# the files this run never touched. `active` is a set of feature re-exports in the root
|
|
165
198
|
# `mod.rs`, never a directory.
|
|
166
|
-
|
|
167
|
-
FileUtils.rm_rf(File.join(@out_root, "active"))
|
|
199
|
+
WriteIfChanged.push_directory(dir)
|
|
168
200
|
result = RustProjection::DomainGenerator.call(
|
|
169
201
|
meta_ir, "the self-hosted language (lib/hecks/language/bluebook)", dir, "meta"
|
|
170
202
|
)
|
|
@@ -176,12 +208,12 @@ module Hecks
|
|
|
176
208
|
managers: meta_ir[:process_managers],
|
|
177
209
|
keys: [[meta_ir[:name], result[:aggregates].map { |a| a[:name] }]])
|
|
178
210
|
append_merged_mod(File.join(dir, "mod.rs"))
|
|
179
|
-
|
|
211
|
+
WriteIfChanged.pop_and_prune(dir)
|
|
180
212
|
end
|
|
181
213
|
|
|
182
214
|
def write_target
|
|
183
215
|
dir = File.join(@out_root, @mod_name)
|
|
184
|
-
|
|
216
|
+
WriteIfChanged.push_directory(dir)
|
|
185
217
|
result = RustProjection::DomainGenerator.call(@ir, @domain, dir, @mod_name)
|
|
186
218
|
chapters = write_chapters
|
|
187
219
|
aggregates = result[:aggregates] + chapters[:aggregates]
|
|
@@ -192,10 +224,10 @@ module Hecks
|
|
|
192
224
|
managers: @ir[:process_managers] + chapters[:process_managers],
|
|
193
225
|
keys: reference_keys(result[:aggregates], chapters))
|
|
194
226
|
append_merged_mod(File.join(dir, "mod.rs"))
|
|
195
|
-
|
|
227
|
+
WriteIfChanged.pop_and_prune(dir)
|
|
196
228
|
end
|
|
197
229
|
|
|
198
|
-
# Every other bluebook the target's `
|
|
230
|
+
# Every other bluebook the target's `attaches` calls pulled into the registry, each
|
|
199
231
|
# generated into its own module. A framework chapter is a function of its bluebook alone, so
|
|
200
232
|
# regenerating one is safe whichever domain last touched it.
|
|
201
233
|
def write_chapters
|
|
@@ -203,7 +235,7 @@ module Hecks
|
|
|
203
235
|
policy_sources: [{ domain_name: @domain_name, policies: @ir[:policies],
|
|
204
236
|
aggregates: @ir[:aggregates] }],
|
|
205
237
|
mod_names: { @mod_name => @domain_name } }
|
|
206
|
-
vendored = @registry.hecksagons.values.flat_map(&:
|
|
238
|
+
vendored = @registry.hecksagons.values.flat_map(&:vendored_packages)
|
|
207
239
|
(@registry.bluebooks.keys - [@domain_name]).each { |chapter| write_chapter(chapter, vendored, totals) }
|
|
208
240
|
totals
|
|
209
241
|
end
|
|
@@ -213,7 +245,7 @@ module Hecks
|
|
|
213
245
|
totals[:mod_names][mod_name] = chapter_name
|
|
214
246
|
chapter_ir = json_shaped(Hecks::Projector::Exporter.call(@registry).fetch(chapter_name))
|
|
215
247
|
result = nil
|
|
216
|
-
|
|
248
|
+
WriteIfChanged.track_directory(File.join(@out_root, mod_name)) do
|
|
217
249
|
result = RustProjection::DomainGenerator.call(chapter_ir, "#{@domain} (#{attachment(chapter_name, vendored)})",
|
|
218
250
|
File.join(@out_root, mod_name), mod_name,
|
|
219
251
|
merged_module: false)
|
|
@@ -230,7 +262,7 @@ module Hecks
|
|
|
230
262
|
# sibling hecksagon vendors in is labelled as vendored.
|
|
231
263
|
def attachment(chapter_name, vendored)
|
|
232
264
|
name = vendored.find { |candidate| Hecks::Naming.pascal(candidate.to_s) == chapter_name }
|
|
233
|
-
name ? "
|
|
265
|
+
name ? "attaches #{name.inspect}, from: :vendor" : "attaches #{chapter_name.inspect}"
|
|
234
266
|
end
|
|
235
267
|
|
|
236
268
|
def reference_keys(target_aggregates, chapters)
|
|
@@ -243,7 +275,7 @@ module Hecks
|
|
|
243
275
|
# Writes a module's `merged.rs`: the registry and every dispatch table over its aggregates.
|
|
244
276
|
def write_merged(path, aggregates, queries, read_models, policies:, cross:, managers:, keys:)
|
|
245
277
|
projector = RustProjection::Projector
|
|
246
|
-
wrote =
|
|
278
|
+
wrote = WriteIfChanged.block(path) do |file|
|
|
247
279
|
[projector.emit_registry(aggregates), projector.emit_reference_lookup(aggregates), policies, cross,
|
|
248
280
|
projector.emit_process_manager_table(managers), projector.emit_reference_key_table(keys),
|
|
249
281
|
projector.emit_creates_table(aggregates), projector.emit_identity_head_table(aggregates),
|
|
@@ -280,7 +312,7 @@ module Hecks
|
|
|
280
312
|
all_dirs = Dir.children(@out_root).select { |name| File.directory?(File.join(@out_root, name)) }.sort
|
|
281
313
|
@domains = all_dirs.select { |name| File.exist?(File.join(@out_root, name, "merged.rs")) }
|
|
282
314
|
path = File.join(@out_root, "mod.rs")
|
|
283
|
-
wrote =
|
|
315
|
+
wrote = WriteIfChanged.block(path) do |file|
|
|
284
316
|
RootMod.new(file, all_dirs, @domains, @mod_name).write
|
|
285
317
|
end
|
|
286
318
|
puts(wrote ? "wrote #{path}" : "#{path} unchanged")
|
|
@@ -296,7 +328,7 @@ module Hecks
|
|
|
296
328
|
@domains.each { |name| manifest = add_feature(manifest, name) }
|
|
297
329
|
manifest = manifest.sub(/^default\s*=.*$/, "default = [#{@mod_name.inspect}]")
|
|
298
330
|
# An unchanged manifest is not rewritten: a new mtime makes Cargo rebuild the whole crate.
|
|
299
|
-
if
|
|
331
|
+
if WriteIfChanged.call(path, manifest)
|
|
300
332
|
puts "wrote #{path} (default feature: #{@mod_name})"
|
|
301
333
|
else
|
|
302
334
|
puts "#{path} unchanged (default feature already #{@mod_name})"
|
|
@@ -330,3 +362,4 @@ module Hecks
|
|
|
330
362
|
end
|
|
331
363
|
|
|
332
364
|
require_relative "project_rust/root_mod"
|
|
365
|
+
require_relative "project_rust/codegen_run"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module RustBuild
|
|
5
|
+
# Rust string literals. Ruby's `String#inspect` escapes for Ruby's own read-back (a
|
|
6
|
+
# brace-less `\uXXXX`, for one), which is not valid Rust syntax.
|
|
7
|
+
module RustLiteral
|
|
8
|
+
# Escapes only what Rust's string-literal grammar needs: backslash, double quote and
|
|
9
|
+
# control characters.
|
|
10
|
+
#
|
|
11
|
+
# @param text [#to_s] the text to embed
|
|
12
|
+
# @return [String] the quoted literal
|
|
13
|
+
def self.string(text)
|
|
14
|
+
escaped = text.to_s.each_char.map { |char| escape(char) }.join
|
|
15
|
+
"\"#{escaped}\""
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.escape(char)
|
|
19
|
+
case char
|
|
20
|
+
when "\\" then "\\\\"
|
|
21
|
+
when "\"" then "\\\""
|
|
22
|
+
when "\n" then "\\n"
|
|
23
|
+
when "\r" then "\\r"
|
|
24
|
+
when "\t" then "\\t"
|
|
25
|
+
else
|
|
26
|
+
code = char.ord
|
|
27
|
+
code < 0x20 || code == 0x7F ? format("\\u{%x}", code) : char
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
private_class_method :escape
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module RustBuild
|
|
7
|
+
# Skips writes whose content is unchanged so file mtimes stay stable.
|
|
8
|
+
# A rewrite alone forces Cargo to rebuild downstream crates.
|
|
9
|
+
module WriteIfChanged
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Records every path written or confirmed unchanged inside the block, then deletes
|
|
13
|
+
# files left in `dir` that were never touched (orphans of removed aggregates).
|
|
14
|
+
# Every write is recorded against every open ancestor directory.
|
|
15
|
+
# `push_directory`/`pop_and_prune` are the same operation split for non-adjacent writes.
|
|
16
|
+
def track_directory(dir)
|
|
17
|
+
push_directory(dir)
|
|
18
|
+
yield
|
|
19
|
+
ensure
|
|
20
|
+
pop_and_prune(dir)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def push_directory(dir)
|
|
24
|
+
FileUtils.mkdir_p(dir)
|
|
25
|
+
stack.push([File.expand_path(dir), Set.new])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Matches by directory, not LIFO: nested directories can close out of order.
|
|
29
|
+
def pop_and_prune(dir)
|
|
30
|
+
abs_dir = File.expand_path(dir)
|
|
31
|
+
idx = stack.rindex { |tracked_dir, _| tracked_dir == abs_dir }
|
|
32
|
+
return unless idx
|
|
33
|
+
|
|
34
|
+
_dir, touched = stack.delete_at(idx)
|
|
35
|
+
existing = Dir.glob(File.join(abs_dir, "*"))
|
|
36
|
+
(existing - touched.to_a).each do |orphan|
|
|
37
|
+
FileUtils.rm_rf(orphan)
|
|
38
|
+
puts "pruned #{orphan} (no longer generated)"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stack
|
|
43
|
+
Thread.current[:hecks_write_if_changed_stack] ||= []
|
|
44
|
+
end
|
|
45
|
+
private_class_method :stack
|
|
46
|
+
|
|
47
|
+
def record_touch(path)
|
|
48
|
+
abs = File.expand_path(path)
|
|
49
|
+
stack.each do |dir, touched|
|
|
50
|
+
touched << abs if abs.start_with?("#{dir}/")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
private_class_method :record_touch
|
|
54
|
+
|
|
55
|
+
# Writes `content` to `path` unless it already holds identical bytes.
|
|
56
|
+
# Returns whether it wrote.
|
|
57
|
+
def call(path, content)
|
|
58
|
+
record_touch(path)
|
|
59
|
+
# `.b` on both sides: String#== is encoding-aware, so UTF-8 vs ASCII-8BIT
|
|
60
|
+
# content can compare unequal even when the bytes are identical.
|
|
61
|
+
return false if File.exist?(path) && File.binread(path) == content.b
|
|
62
|
+
|
|
63
|
+
File.write(path, content)
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Like `call`, for callers that build content by writing to an IO. Yields a sibling
|
|
68
|
+
# temp file, then renames it into place only if the bytes differ. Returns whether it wrote.
|
|
69
|
+
def block(path, &)
|
|
70
|
+
record_touch(path)
|
|
71
|
+
tmp = "#{path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
|
|
72
|
+
begin
|
|
73
|
+
File.open(tmp, "w", &)
|
|
74
|
+
if File.exist?(path) && FileUtils.compare_file(tmp, path)
|
|
75
|
+
false
|
|
76
|
+
else
|
|
77
|
+
FileUtils.mv(tmp, path)
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
ensure
|
|
81
|
+
FileUtils.rm_f(tmp)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|