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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `
|
|
1
|
+
//! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `attaches` words.
|
|
2
2
|
//! Adapter binds (`persisted_by`, `projected_by`, `subscribe`) are shape-matched and dropped.
|
|
3
3
|
|
|
4
4
|
use super::domain_port;
|
|
@@ -14,8 +14,10 @@ pub fn not_implemented(file: &str, line: usize, word: &str) -> Diagnostic {
|
|
|
14
14
|
/// Applies a `.hecksagon` body onto an already-built `ir::Bluebook`, attaching
|
|
15
15
|
/// aggregate-scoped ports to the aggregates the sibling `.bluebook` registered.
|
|
16
16
|
///
|
|
17
|
-
/// `
|
|
18
|
-
///
|
|
17
|
+
/// `gem_chapter_names` collects each `attaches "Name"` (and the deprecated `uses_framework`) and
|
|
18
|
+
/// `vendored_bluebook_names` each `attaches "name", from: :vendor` (and the deprecated
|
|
19
|
+
/// `uses_embryonaut_bluebook`), in file order, for `hecks-parse resolve`; `ir.json` itself
|
|
20
|
+
/// carries neither.
|
|
19
21
|
///
|
|
20
22
|
/// `require_matching_aggregate` is `false` for resolve, which never sees a `.bluebook`, so an
|
|
21
23
|
/// aggregate-scoped `port` skips the attach step instead of failing the lookup.
|
|
@@ -24,7 +26,7 @@ pub fn apply(
|
|
|
24
26
|
lines: &[SourceLine],
|
|
25
27
|
pos: &mut usize,
|
|
26
28
|
bluebook: &mut ir::Bluebook,
|
|
27
|
-
|
|
29
|
+
gem_chapter_names: &mut Vec<String>,
|
|
28
30
|
vendored_bluebook_names: &mut Vec<String>,
|
|
29
31
|
require_matching_aggregate: bool,
|
|
30
32
|
) -> ParseResult<()> {
|
|
@@ -96,9 +98,9 @@ pub fn apply(
|
|
|
96
98
|
let built = policy::parse_body(file, lines, pos, &name)?;
|
|
97
99
|
bluebook.policies.push(built);
|
|
98
100
|
}
|
|
99
|
-
//
|
|
101
|
+
// Deprecated spelling of `attaches "Name"`; gated, then collected.
|
|
100
102
|
"uses_framework" => {
|
|
101
|
-
|
|
103
|
+
gem_chapter_names.push(super::positional_text(
|
|
102
104
|
file,
|
|
103
105
|
gated.line.number,
|
|
104
106
|
"uses_framework",
|
|
@@ -106,7 +108,7 @@ pub fn apply(
|
|
|
106
108
|
1,
|
|
107
109
|
)?);
|
|
108
110
|
}
|
|
109
|
-
//
|
|
111
|
+
// Deprecated spelling of `attaches "name", from: :vendor`; collected the same way.
|
|
110
112
|
"uses_embryonaut_bluebook" => {
|
|
111
113
|
vendored_bluebook_names.push(super::positional_text(
|
|
112
114
|
file,
|
|
@@ -117,10 +119,23 @@ pub fn apply(
|
|
|
117
119
|
)?);
|
|
118
120
|
}
|
|
119
121
|
"subscribe" => {}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
+
// One word for a chapter the gem carries and for a vendored package; a binding
|
|
123
|
+
// fact collected for `resolve`, absent from `ir.json`. Only `from: :vendor`
|
|
124
|
+
// is admitted as a source, and anything else is refused.
|
|
122
125
|
"attaches" => {
|
|
123
|
-
|
|
126
|
+
let name =
|
|
127
|
+
super::positional_text(file, gated.line.number, "attaches", &gated.args, 1)?;
|
|
128
|
+
match super::named_symbol(&gated.args, "from").as_deref() {
|
|
129
|
+
None => gem_chapter_names.push(name),
|
|
130
|
+
Some("vendor") => vendored_bluebook_names.push(name),
|
|
131
|
+
Some(other) => {
|
|
132
|
+
return Err(Diagnostic::new(
|
|
133
|
+
file,
|
|
134
|
+
gated.line.number,
|
|
135
|
+
format!("'attaches' from: takes only :vendor, got :{other}"),
|
|
136
|
+
))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
124
139
|
}
|
|
125
140
|
// Consumer-owned bounded-context mark; a wiring fact, dropped like `subscribe`.
|
|
126
141
|
"bounded" => {}
|
data/rust/project/mutations.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "hecks/rust_build/append_optionals"
|
|
2
|
+
|
|
1
3
|
module RustProjection
|
|
2
4
|
module Projector
|
|
3
5
|
module_function
|
|
@@ -60,13 +62,7 @@ module RustProjection
|
|
|
60
62
|
|
|
61
63
|
# Resolves an `append` target to its entity or value object, or nil if it is neither.
|
|
62
64
|
def append_element(aggregate, target_type, value_objects_by_name)
|
|
63
|
-
|
|
64
|
-
# Local entities win over the domain-wide value objects: Syntax's local `Argument` entity
|
|
65
|
-
# shares a name with Command's `Argument` value object.
|
|
66
|
-
local = aggregate[:entities].find { |e| e[:name] == target_type }
|
|
67
|
-
return local if local
|
|
68
|
-
|
|
69
|
-
value_objects_by_name[target_type]
|
|
65
|
+
Hecks::RustBuild::AppendOptionals.element(aggregate, target_type, value_objects_by_name)
|
|
70
66
|
end
|
|
71
67
|
|
|
72
68
|
# Identity attribute and its single-field value object for an auto-minted entity element,
|
|
@@ -454,29 +450,7 @@ module RustProjection
|
|
|
454
450
|
# attribute
|
|
455
451
|
# hash once, before the emitters read it, and never resets a `true`.
|
|
456
452
|
def mark_append_optional_fields!(aggregate, value_objects_by_name)
|
|
457
|
-
aggregate
|
|
458
|
-
element = append_element(aggregate, target_attr[:type], value_objects_by_name)
|
|
459
|
-
next unless element
|
|
460
|
-
|
|
461
|
-
aggregate[:commands].each do |command|
|
|
462
|
-
command[:mutations].each do |m|
|
|
463
|
-
next unless m[:op].to_s == "append" && m[:target].to_s == target_attr[:name].to_s
|
|
464
|
-
|
|
465
|
-
m[:fields].each do |field_name, source|
|
|
466
|
-
# A Symbol is an argument name (optional ones are what this pass wants); else a
|
|
467
|
-
# literal.
|
|
468
|
-
parsed = append_field_source(source)
|
|
469
|
-
next unless parsed.is_a?(Symbol)
|
|
470
|
-
|
|
471
|
-
source_attr = command[:attributes].find { |a| a[:name].to_s == parsed.to_s }
|
|
472
|
-
next unless source_attr && source_attr[:optional]
|
|
473
|
-
|
|
474
|
-
field_attr = element[:attributes].find { |a| a[:name].to_s == field_name.to_s }
|
|
475
|
-
field_attr[:optional] = true if field_attr
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
end
|
|
479
|
-
end
|
|
453
|
+
Hecks::RustBuild::AppendOptionals.mark_aggregate(aggregate, value_objects_by_name)
|
|
480
454
|
end
|
|
481
455
|
|
|
482
456
|
# Derives the mutations of a `corrects EVENT, reverses: true` command from the commands that
|
data/rust/project/naming.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "hecks/vocabulary"
|
|
2
2
|
require "hecks/bluebook/model_check"
|
|
3
|
+
require "hecks/rust_build/domain_name"
|
|
4
|
+
require "hecks/rust_build/rust_literal"
|
|
3
5
|
|
|
4
6
|
module RustProjection
|
|
5
7
|
# Domain-to-Rust codegen support: type mapping, reserved-name checks, and
|
|
@@ -59,15 +61,10 @@ module RustProjection
|
|
|
59
61
|
# feature set.
|
|
60
62
|
#
|
|
61
63
|
# Declared once as the `CargoReservedName` vocabulary.
|
|
62
|
-
CARGO_RESERVED_DOMAIN_NAMES = Hecks::
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
def valid_domain_mod_name?(name)
|
|
67
|
-
str = name.to_s
|
|
68
|
-
str.match?(/\A[a-z_][a-z0-9_]*\z/) &&
|
|
69
|
-
Hecks::Bluebook::ModelCheck.rust_reserved_name_findings(domain_name: str).empty?
|
|
70
|
-
end
|
|
64
|
+
CARGO_RESERVED_DOMAIN_NAMES = Hecks::RustBuild::DomainName::CARGO_RESERVED
|
|
65
|
+
|
|
66
|
+
# Moved to `Hecks::RustBuild::DomainName`, which the codegen-backed `hecks project_rust` uses.
|
|
67
|
+
def valid_domain_mod_name?(name) = Hecks::RustBuild::DomainName.valid?(name)
|
|
71
68
|
|
|
72
69
|
# Identifier-shape check only (a PascalCase name always passes); whether
|
|
73
70
|
# the downcased name is a Rust keyword is `reserved_name_refusal`'s job,
|
|
@@ -220,24 +217,7 @@ module RustProjection
|
|
|
220
217
|
end
|
|
221
218
|
end
|
|
222
219
|
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
# only what Rust's string-literal grammar needs: backslash,
|
|
226
|
-
# double-quote, and control characters.
|
|
227
|
-
def rust_string_literal(str)
|
|
228
|
-
escaped = str.to_s.each_char.map do |ch|
|
|
229
|
-
case ch
|
|
230
|
-
when "\\" then "\\\\"
|
|
231
|
-
when "\"" then "\\\""
|
|
232
|
-
when "\n" then "\\n"
|
|
233
|
-
when "\r" then "\\r"
|
|
234
|
-
when "\t" then "\\t"
|
|
235
|
-
else
|
|
236
|
-
cp = ch.ord
|
|
237
|
-
cp < 0x20 || cp == 0x7F ? format("\\u{%x}", cp) : ch
|
|
238
|
-
end
|
|
239
|
-
end.join
|
|
240
|
-
"\"#{escaped}\""
|
|
241
|
-
end
|
|
220
|
+
# Moved to `Hecks::RustBuild::RustLiteral`, which the codegen-backed `hecks project_rust` uses.
|
|
221
|
+
def rust_string_literal(str) = Hecks::RustBuild::RustLiteral.string(str)
|
|
242
222
|
end
|
|
243
223
|
end
|
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "set"
|
|
1
|
+
require "hecks/rust_build/write_if_changed"
|
|
3
2
|
|
|
4
|
-
# Skips writes whose content is unchanged so file mtimes stay stable.
|
|
5
|
-
# A rewrite alone forces Cargo to rebuild downstream crates.
|
|
6
3
|
module RustProjection
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# Records every path written or confirmed unchanged inside the block, then deletes
|
|
11
|
-
# files left in `dir` that were never touched (orphans of removed aggregates).
|
|
12
|
-
# Every write is recorded against every open ancestor directory.
|
|
13
|
-
# `push_directory`/`pop_and_prune` are the same operation split for non-adjacent writes.
|
|
14
|
-
def track_directory(dir)
|
|
15
|
-
push_directory(dir)
|
|
16
|
-
yield
|
|
17
|
-
ensure
|
|
18
|
-
pop_and_prune(dir)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def push_directory(dir)
|
|
22
|
-
FileUtils.mkdir_p(dir)
|
|
23
|
-
stack.push([File.expand_path(dir), Set.new])
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Matches by directory, not LIFO: nested directories can close out of order.
|
|
27
|
-
def pop_and_prune(dir)
|
|
28
|
-
abs_dir = File.expand_path(dir)
|
|
29
|
-
idx = stack.rindex { |tracked_dir, _| tracked_dir == abs_dir }
|
|
30
|
-
return unless idx
|
|
31
|
-
|
|
32
|
-
_dir, touched = stack.delete_at(idx)
|
|
33
|
-
existing = Dir.glob(File.join(abs_dir, "*"))
|
|
34
|
-
(existing - touched.to_a).each do |orphan|
|
|
35
|
-
FileUtils.rm_rf(orphan)
|
|
36
|
-
puts "pruned #{orphan} (no longer generated)"
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def stack
|
|
41
|
-
Thread.current[:hecks_write_if_changed_stack] ||= []
|
|
42
|
-
end
|
|
43
|
-
private_class_method :stack
|
|
44
|
-
|
|
45
|
-
def record_touch(path)
|
|
46
|
-
abs = File.expand_path(path)
|
|
47
|
-
stack.each do |dir, touched|
|
|
48
|
-
touched << abs if abs.start_with?("#{dir}/")
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
private_class_method :record_touch
|
|
52
|
-
|
|
53
|
-
# Writes `content` to `path` unless it already holds identical bytes.
|
|
54
|
-
# Returns whether it wrote.
|
|
55
|
-
def call(path, content)
|
|
56
|
-
record_touch(path)
|
|
57
|
-
# `.b` on both sides: String#== is encoding-aware, so UTF-8 vs ASCII-8BIT
|
|
58
|
-
# content can compare unequal even when the bytes are identical.
|
|
59
|
-
return false if File.exist?(path) && File.binread(path) == content.b
|
|
60
|
-
|
|
61
|
-
File.write(path, content)
|
|
62
|
-
true
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Like `call`, for callers that build content by writing to an IO. Yields a sibling
|
|
66
|
-
# temp file, then renames it into place only if the bytes differ. Returns whether it wrote.
|
|
67
|
-
def block(path)
|
|
68
|
-
record_touch(path)
|
|
69
|
-
tmp = "#{path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
|
|
70
|
-
begin
|
|
71
|
-
File.open(tmp, "w") { |f| yield f }
|
|
72
|
-
if File.exist?(path) && FileUtils.compare_file(tmp, path)
|
|
73
|
-
false
|
|
74
|
-
else
|
|
75
|
-
FileUtils.mv(tmp, path)
|
|
76
|
-
true
|
|
77
|
-
end
|
|
78
|
-
ensure
|
|
79
|
-
File.delete(tmp) if File.exist?(tmp)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
4
|
+
# The write-if-unchanged helper moved to `Hecks::RustBuild::WriteIfChanged`, which the
|
|
5
|
+
# codegen-backed `hecks project_rust` also uses; this name stays until this generator is deleted.
|
|
6
|
+
WriteIfChanged = Hecks::RustBuild::WriteIfChanged
|
|
83
7
|
end
|
|
@@ -34,10 +34,10 @@ module RustProjectPipeline
|
|
|
34
34
|
target_chapter_name = header_chapter_name(bluebook_paths.first)
|
|
35
35
|
target_bluebooks = bluebook_paths.select { |path| header_chapter_name(path) == target_chapter_name }
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
gem_chapter_names, vendored_package_names =
|
|
38
38
|
if hecksagon_path
|
|
39
39
|
resolved = JSON.parse(run_capture!(PARSER_BIN, "resolve", "--chapter", target_chapter_name, hecksagon_path))
|
|
40
|
-
[resolved.fetch("
|
|
40
|
+
[resolved.fetch("gem_chapters"), resolved.fetch("vendored_packages")]
|
|
41
41
|
else
|
|
42
42
|
[[], []]
|
|
43
43
|
end
|
|
@@ -50,18 +50,22 @@ module RustProjectPipeline
|
|
|
50
50
|
|
|
51
51
|
# Mirrors hecks project_rust's own Framework.load! resolution. A framework
|
|
52
52
|
# member has no .hecksagon of its own, only its .bluebook.
|
|
53
|
-
|
|
53
|
+
# A gem chapter that is not a framework member (the language, Tenancy, Deploy) is loaded by
|
|
54
|
+
# the Ruby runtime alone, so it is checked against the one table and skipped here.
|
|
55
|
+
chapters = gem_chapter_names.filter_map do |fw_name|
|
|
56
|
+
next if Hecks::Chapters.index.key?(fw_name) && !Hecks::Framework.members.key?(fw_name)
|
|
57
|
+
|
|
54
58
|
fw_path = Hecks::Framework.members.fetch(fw_name) do
|
|
55
|
-
abort "hecks project_rust (Rust path):
|
|
59
|
+
abort "hecks project_rust (Rust path): attaches #{fw_name.inspect} names no chapter the gem carries — known: #{Hecks::Chapters.table.keys.sort.join(', ')}"
|
|
56
60
|
end
|
|
57
61
|
fw_chapter_name = header_chapter_name(fw_path)
|
|
58
62
|
unless fw_chapter_name == fw_name
|
|
59
|
-
abort "hecks project_rust (Rust path): #{fw_path} declares chapter #{fw_chapter_name.inspect}, but
|
|
63
|
+
abort "hecks project_rust (Rust path): #{fw_path} declares chapter #{fw_chapter_name.inspect}, but attaches named #{fw_name.inspect}"
|
|
60
64
|
end
|
|
61
65
|
fw_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", fw_chapter_name, fw_path))
|
|
62
66
|
{
|
|
63
67
|
mod_name: fw_name.downcase,
|
|
64
|
-
source_label: "#{domain} (
|
|
68
|
+
source_label: "#{domain} (attaches #{fw_name.inspect})",
|
|
65
69
|
ir_text: fw_ir_text
|
|
66
70
|
}
|
|
67
71
|
end
|
|
@@ -69,22 +73,22 @@ module RustProjectPipeline
|
|
|
69
73
|
# Mirrors EmbryonautBluebook.load!: every .bluebook file directly under
|
|
70
74
|
# vendor/embryonaut_bluebooks/<name>/bluebook/, sorted. A vendored
|
|
71
75
|
# package has no .hecksagon of its own either, just its .bluebook(s).
|
|
72
|
-
chapters +=
|
|
76
|
+
chapters += vendored_package_names.map do |pkg_name|
|
|
73
77
|
unless pkg_name.to_s.match?(Hecks::EmbryonautBluebook::PACKAGE_NAME)
|
|
74
|
-
abort "hecks project_rust (Rust path):
|
|
78
|
+
abort "hecks project_rust (Rust path): attaches #{pkg_name.inspect}, from: :vendor is not a package " \
|
|
75
79
|
"name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
76
80
|
end
|
|
77
81
|
pkg_dir = File.join(domain, "vendor", "embryonaut_bluebooks", pkg_name.to_s, "bluebook")
|
|
78
82
|
pkg_files = Dir.glob(File.join(pkg_dir, "*.bluebook")).sort
|
|
79
83
|
if pkg_files.empty?
|
|
80
|
-
abort "hecks project_rust (Rust path):
|
|
84
|
+
abort "hecks project_rust (Rust path): attaches #{pkg_name.inspect}, from: :vendor names no vendored " \
|
|
81
85
|
"bluebook at #{pkg_dir} — run hecks vendor #{pkg_name}"
|
|
82
86
|
end
|
|
83
87
|
pkg_chapter_name = header_chapter_name(pkg_files.first)
|
|
84
88
|
expected_chapter_name = Hecks::Naming.pascal(pkg_name.to_s)
|
|
85
89
|
unless pkg_chapter_name == expected_chapter_name
|
|
86
90
|
abort "hecks project_rust (Rust path): #{pkg_files.first} declares chapter #{pkg_chapter_name.inspect}, but " \
|
|
87
|
-
"
|
|
91
|
+
"attaches #{pkg_name.inspect}, from: :vendor expects #{expected_chapter_name.inspect}"
|
|
88
92
|
end
|
|
89
93
|
pkg_bluebooks = pkg_files.select { |path| header_chapter_name(path) == pkg_chapter_name }
|
|
90
94
|
pkg_ir_text = derive_append_optionals(run_capture!(PARSER_BIN, "chapter", "--chapter", pkg_chapter_name, *pkg_bluebooks))
|
|
@@ -92,7 +96,7 @@ module RustProjectPipeline
|
|
|
92
96
|
# Derived from the declared chapter name, not pkg_name.downcase
|
|
93
97
|
# directly — the two diverge once a package name has an underscore.
|
|
94
98
|
mod_name: expected_chapter_name.downcase,
|
|
95
|
-
source_label: "#{domain} (
|
|
99
|
+
source_label: "#{domain} (attaches #{pkg_name.inspect}, from: :vendor)",
|
|
96
100
|
ir_text: pkg_ir_text
|
|
97
101
|
}
|
|
98
102
|
end
|
|
@@ -294,7 +298,7 @@ module RustProjectPipeline
|
|
|
294
298
|
f.puts "// chapter (governance/identity — no merged.rs of its own, so absent"
|
|
295
299
|
f.puts "// from `domains`) stays unconditional: it has no single feature of its"
|
|
296
300
|
f.puts "// own to gate behind, and is compiled in by whichever domain(s) attach"
|
|
297
|
-
f.puts "// it via `
|
|
301
|
+
f.puts "// it via `attaches`."
|
|
298
302
|
all_dirs.each do |name|
|
|
299
303
|
f.puts "#[cfg(feature = #{name.inspect})]" if domains.include?(name)
|
|
300
304
|
f.puts "pub mod #{name};"
|
data/rust/src/kernel/cli.rs
CHANGED
|
@@ -18,6 +18,8 @@ pub fn run(input: &str) -> String {
|
|
|
18
18
|
|
|
19
19
|
// Optional `"needs"`: which commands need which outside facts (ADR 0081), passed by the host.
|
|
20
20
|
super::needs::install(parsed.get("needs"));
|
|
21
|
+
// Optional `"defaults"`: each command's declared argument defaults, filled the same way.
|
|
22
|
+
super::needs::install_defaults(parsed.get("defaults"));
|
|
21
23
|
|
|
22
24
|
let steps = match parsed.get("steps").and_then(Json::as_array) {
|
|
23
25
|
Some(s) => s,
|
data/rust/src/kernel/needs.rs
CHANGED
|
@@ -25,6 +25,7 @@ pub struct Need {
|
|
|
25
25
|
|
|
26
26
|
thread_local! {
|
|
27
27
|
static TABLE: RefCell<HashMap<String, Vec<Need>>> = RefCell::new(HashMap::new());
|
|
28
|
+
static DEFAULTS: RefCell<HashMap<String, Vec<(String, Json)>>> = RefCell::new(HashMap::new());
|
|
28
29
|
static FIXED_CLOCK: Cell<Option<i64>> = const { Cell::new(None) };
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -52,6 +53,23 @@ pub fn install(table: Option<&Json>) {
|
|
|
52
53
|
TABLE.with(|t| *t.borrow_mut() = parsed);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/// Installs the declared defaults the host passed, `{ verb: { attribute: value } }` (an absent or
|
|
57
|
+
/// malformed `"defaults"` installs an empty table). They fill after the needs, so a fact that is
|
|
58
|
+
/// both needed and defaulted takes the answer.
|
|
59
|
+
pub fn install_defaults(table: Option<&Json>) {
|
|
60
|
+
let parsed = match table {
|
|
61
|
+
Some(Json::Object(entries)) => entries
|
|
62
|
+
.iter()
|
|
63
|
+
.filter_map(|(verb, attributes)| match attributes {
|
|
64
|
+
Json::Object(held) => Some((verb.clone(), held.clone())),
|
|
65
|
+
_ => None,
|
|
66
|
+
})
|
|
67
|
+
.collect(),
|
|
68
|
+
_ => HashMap::new(),
|
|
69
|
+
};
|
|
70
|
+
DEFAULTS.with(|t| *t.borrow_mut() = parsed);
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
/// Makes the kernel clock answer `secs` on this thread until cleared; the test seam.
|
|
56
74
|
pub fn fix_clock(secs: Option<i64>) {
|
|
57
75
|
FIXED_CLOCK.with(|c| c.set(secs));
|
|
@@ -100,7 +118,11 @@ fn answer(need: &Need, occurred_at: Option<&str>) -> Option<Json> {
|
|
|
100
118
|
/// needed filling (no needs, or every fact supplied). Reads `with` when the call carries one,
|
|
101
119
|
/// else the flat object, as `CommandInvocation` does.
|
|
102
120
|
pub fn enrich(verb: &str, args: &Json, occurred_at: Option<&str>) -> Option<Json> {
|
|
103
|
-
let needs = TABLE.with(|t| t.borrow().get(verb).cloned())
|
|
121
|
+
let needs = TABLE.with(|t| t.borrow().get(verb).cloned()).unwrap_or_default();
|
|
122
|
+
let defaults = DEFAULTS.with(|t| t.borrow().get(verb).cloned()).unwrap_or_default();
|
|
123
|
+
if needs.is_empty() && defaults.is_empty() {
|
|
124
|
+
return None;
|
|
125
|
+
}
|
|
104
126
|
let explicit = matches!(args.get("with"), Some(Json::Object(_)));
|
|
105
127
|
let facts = if explicit { args.get("with")? } else { args };
|
|
106
128
|
let Json::Object(held) = facts else { return None };
|
|
@@ -114,6 +136,11 @@ pub fn enrich(verb: &str, args: &Json, occurred_at: Option<&str>) -> Option<Json
|
|
|
114
136
|
filled.push((need.fact.clone(), value));
|
|
115
137
|
}
|
|
116
138
|
}
|
|
139
|
+
for (name, value) in &defaults {
|
|
140
|
+
if !filled.iter().any(|(k, _)| k == name) {
|
|
141
|
+
filled.push((name.clone(), value.clone()));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
117
144
|
if filled.len() == before {
|
|
118
145
|
return None;
|
|
119
146
|
}
|
|
@@ -191,4 +218,50 @@ mod tests {
|
|
|
191
218
|
assert_eq!(now_secs(Some("1970-01-01T00:00:00Z")), 0);
|
|
192
219
|
assert!(now_secs(None) > 1_700_000_000);
|
|
193
220
|
}
|
|
221
|
+
|
|
222
|
+
fn defaults() -> Json {
|
|
223
|
+
Json::parse(r#"{"D::A.Start": {"runs": 30}, "D::A.Instant": {"holder": "nobody"}}"#).unwrap()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn an_omitted_argument_takes_its_declared_default() {
|
|
228
|
+
install(None);
|
|
229
|
+
install_defaults(Some(&defaults()));
|
|
230
|
+
let filled = enrich("D::A.Start", &with(vec![("ref", Json::int(1))]), None).unwrap();
|
|
231
|
+
assert_eq!(filled.get("with").unwrap().get("runs"), Some(&Json::int(30)));
|
|
232
|
+
assert_eq!(filled.get("with").unwrap().get("ref"), Some(&Json::int(1)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[test]
|
|
236
|
+
fn a_supplied_argument_keeps_its_value_over_the_default() {
|
|
237
|
+
install(None);
|
|
238
|
+
install_defaults(Some(&defaults()));
|
|
239
|
+
assert_eq!(enrich("D::A.Start", &with(vec![("runs", Json::int(7))]), None), None);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn a_flat_invocation_is_filled_at_its_top_level() {
|
|
244
|
+
install(None);
|
|
245
|
+
install_defaults(Some(&defaults()));
|
|
246
|
+
let filled = enrich("D::A.Start", &Json::obj(vec![("ref", Json::int(1))]), None).unwrap();
|
|
247
|
+
assert_eq!(filled.get("runs"), Some(&Json::int(30)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn a_needed_fact_is_answered_and_the_default_fills_beside_it() {
|
|
252
|
+
install(Some(&table()));
|
|
253
|
+
install_defaults(Some(&defaults()));
|
|
254
|
+
fix_clock(Some(3));
|
|
255
|
+
let filled = enrich("D::A.Instant", &with(vec![]), None).unwrap();
|
|
256
|
+
assert_eq!(filled.get("with").unwrap().get("now"), Some(&Json::obj(vec![("value", Json::int(3))])));
|
|
257
|
+
assert_eq!(filled.get("with").unwrap().get("holder"), Some(&Json::str("nobody")));
|
|
258
|
+
fix_clock(None);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn no_installed_defaults_leaves_a_command_untouched() {
|
|
263
|
+
install(None);
|
|
264
|
+
install_defaults(None);
|
|
265
|
+
assert_eq!(enrich("D::A.Start", &with(vec![]), None), None);
|
|
266
|
+
}
|
|
194
267
|
}
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -215,9 +215,11 @@ files:
|
|
|
215
215
|
- lib/hecks/cli/deploy_template_diff.rb
|
|
216
216
|
- lib/hecks/cli/doc_coverage.rb
|
|
217
217
|
- lib/hecks/cli/document.rb
|
|
218
|
+
- lib/hecks/cli/domain_stub.rb
|
|
218
219
|
- lib/hecks/cli/expression_projection.rb
|
|
219
220
|
- lib/hecks/cli/follow.rb
|
|
220
221
|
- lib/hecks/cli/history.rb
|
|
222
|
+
- lib/hecks/cli/interview_draft.rb
|
|
221
223
|
- lib/hecks/cli/ir.rb
|
|
222
224
|
- lib/hecks/cli/mcp.rb
|
|
223
225
|
- lib/hecks/cli/model_check.rb
|
|
@@ -393,6 +395,8 @@ files:
|
|
|
393
395
|
- lib/hecks/hecks/adapters/rust_workspace.rb
|
|
394
396
|
- lib/hecks/hecks/adapters/shell.adapter
|
|
395
397
|
- lib/hecks/hecks/adapters/shell.rb
|
|
398
|
+
- lib/hecks/hecks/adapters/site_toolchain.adapter
|
|
399
|
+
- lib/hecks/hecks/adapters/site_toolchain.rb
|
|
396
400
|
- lib/hecks/hecks/adapters/terminal.adapter
|
|
397
401
|
- lib/hecks/hecks/adapters/terminal.rb
|
|
398
402
|
- lib/hecks/hecks/codebase.behaviors
|
|
@@ -408,6 +412,8 @@ files:
|
|
|
408
412
|
- lib/hecks/hecks/hecks.bluebook
|
|
409
413
|
- lib/hecks/hecks/hecks.hecksagon
|
|
410
414
|
- lib/hecks/hecks/hecks.world
|
|
415
|
+
- lib/hecks/hecks/site.behaviors
|
|
416
|
+
- lib/hecks/hecks/site.world
|
|
411
417
|
- lib/hecks/ir.rb
|
|
412
418
|
- lib/hecks/kernel_capabilities.rb
|
|
413
419
|
- lib/hecks/language/adapter.bluebook
|
|
@@ -521,6 +527,13 @@ files:
|
|
|
521
527
|
- lib/hecks/projection_files.rb
|
|
522
528
|
- lib/hecks/projections.rb
|
|
523
529
|
- lib/hecks/projections/bootstrap_table.rb
|
|
530
|
+
- lib/hecks/projections/deploy/box.rb
|
|
531
|
+
- lib/hecks/projections/deploy/box/settings.rb
|
|
532
|
+
- lib/hecks/projections/deploy/box/templates/box.yaml.tmpl
|
|
533
|
+
- lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl
|
|
534
|
+
- lib/hecks/projections/deploy/box/templates/fetch-secrets.sh
|
|
535
|
+
- lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl
|
|
536
|
+
- lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl
|
|
524
537
|
- lib/hecks/projections/deploy/fargate.rb
|
|
525
538
|
- lib/hecks/projections/deploy/fargate/assembly.rb
|
|
526
539
|
- lib/hecks/projections/deploy/fargate/cdn.rb
|
|
@@ -573,6 +586,17 @@ files:
|
|
|
573
586
|
- lib/hecks/projections/reference.rb
|
|
574
587
|
- lib/hecks/projections/rust_vocabulary.rb
|
|
575
588
|
- lib/hecks/projections/shape.rb
|
|
589
|
+
- lib/hecks/projections/site/edge.rb
|
|
590
|
+
- lib/hecks/projections/site/edge/behaviours.rb
|
|
591
|
+
- lib/hecks/projections/site/edge/checks.rb
|
|
592
|
+
- lib/hecks/projections/site/edge/pattern.rb
|
|
593
|
+
- lib/hecks/projections/site/edge/rules.rb
|
|
594
|
+
- lib/hecks/projections/site/regions.rb
|
|
595
|
+
- lib/hecks/projections/site/routes_ts.rb
|
|
596
|
+
- lib/hecks/projections/site/site_cdn.rb
|
|
597
|
+
- lib/hecks/projections/site/table.rb
|
|
598
|
+
- lib/hecks/projections/site/table/checks.rb
|
|
599
|
+
- lib/hecks/projections/site/table/row_builder.rb
|
|
576
600
|
- lib/hecks/projections/statements.rb
|
|
577
601
|
- lib/hecks/projections/vocabulary.rb
|
|
578
602
|
- lib/hecks/projector.rb
|
|
@@ -715,14 +739,25 @@ files:
|
|
|
715
739
|
- lib/hecks/runtime/value/field_checks.rb
|
|
716
740
|
- lib/hecks/runtime/value/invariant_violation.rb
|
|
717
741
|
- lib/hecks/rust_build.rb
|
|
742
|
+
- lib/hecks/rust_build/append_optionals.rb
|
|
718
743
|
- lib/hecks/rust_build/conformance.rb
|
|
719
744
|
- lib/hecks/rust_build/conformance_fuzz.rb
|
|
720
745
|
- lib/hecks/rust_build/coverage.rb
|
|
746
|
+
- lib/hecks/rust_build/domain_name.rb
|
|
747
|
+
- lib/hecks/rust_build/kernel_input.rb
|
|
721
748
|
- lib/hecks/rust_build/native_build.rb
|
|
722
749
|
- lib/hecks/rust_build/project_rust.rb
|
|
750
|
+
- lib/hecks/rust_build/project_rust/codegen_run.rb
|
|
723
751
|
- lib/hecks/rust_build/project_rust/root_mod.rb
|
|
752
|
+
- lib/hecks/rust_build/rust_literal.rb
|
|
724
753
|
- lib/hecks/rust_build/wasm.rb
|
|
725
754
|
- lib/hecks/rust_build/wasm_browser.rb
|
|
755
|
+
- lib/hecks/rust_build/write_if_changed.rb
|
|
756
|
+
- lib/hecks/site/bluebook/site.bluebook
|
|
757
|
+
- lib/hecks/site/bluebook/site.hecksagon
|
|
758
|
+
- lib/hecks/site/bluebook/site.ports.hecksagon
|
|
759
|
+
- lib/hecks/sme/bluebook/sme.bluebook
|
|
760
|
+
- lib/hecks/sme/bluebook/sme.hecksagon
|
|
726
761
|
- lib/hecks/storehouse.rb
|
|
727
762
|
- lib/hecks/tenancy/bluebook/tenancy.bluebook
|
|
728
763
|
- lib/hecks/tenancy/bluebook/tenancy.hecksagon
|
|
@@ -750,6 +785,7 @@ files:
|
|
|
750
785
|
- lib/hecks/tools/regeneration_run.rb
|
|
751
786
|
- lib/hecks/tools/rust_comment_style.rb
|
|
752
787
|
- lib/hecks/tools/sequence_script.rb
|
|
788
|
+
- lib/hecks/tools/site_routes.rb
|
|
753
789
|
- lib/hecks/tools/tail_merge.rb
|
|
754
790
|
- lib/hecks/tools/tenant_provisioning.rb
|
|
755
791
|
- lib/hecks/tools/translation_audit.rb
|
|
@@ -858,6 +894,7 @@ files:
|
|
|
858
894
|
- rust/host/src/web.rs
|
|
859
895
|
- rust/host/src/web/newsletter.rs
|
|
860
896
|
- rust/host/src/web/newsletter/cooldown.rs
|
|
897
|
+
- rust/host/src/web/newsletter/template.rs
|
|
861
898
|
- rust/host/src/web/newsletter/tests.rs
|
|
862
899
|
- rust/host/src/web/newsletter_send.rs
|
|
863
900
|
- rust/host/src/web/registrations.rs
|