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
data/rust/build/src/resolve.rs
CHANGED
|
@@ -77,12 +77,45 @@ pub fn grammar_files(root: &Path) -> Result<Vec<PathBuf>, String> {
|
|
|
77
77
|
bluebook_files(&dir)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// The names `attaches "Name"` gave, for a chapter the gem carries.
|
|
81
|
+
pub fn resolve_gem_chapters(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
|
|
82
|
+
resolve_json(parser_bin, chapter_name, hecksagon_path, "gem_chapters")
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// The names `attaches "name", from: :vendor` gave, for a vendored package.
|
|
86
|
+
pub fn resolve_vendored_packages(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
|
|
87
|
+
resolve_json(parser_bin, chapter_name, hecksagon_path, "vendored_packages")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Where the gem keeps the chapters other than framework members, relative to `lib/hecks`; the
|
|
91
|
+
// same places `Hecks::Chapters::GLOBS` lists.
|
|
92
|
+
const OTHER_GEM_CHAPTER_DIRS: &[&str] = &["language", "grammar", "tenancy/bluebook", "deploy/bluebook", "quality_control"];
|
|
93
|
+
|
|
94
|
+
// The names of the chapters the gem carries beside the framework members, found by reading each
|
|
95
|
+
// file's header. Only the Ruby runtime loads them, so the build uses this list to tell a real
|
|
96
|
+
// gem chapter from a misspelt name.
|
|
97
|
+
pub fn other_gem_chapter_names(root: &Path) -> Result<Vec<String>, String> {
|
|
98
|
+
fn walk(directory: &Path, names: &mut Vec<String>) -> Result<(), String> {
|
|
99
|
+
let Ok(entries) = std::fs::read_dir(directory) else { return Ok(()) };
|
|
100
|
+
for entry in entries {
|
|
101
|
+
let path = entry.map_err(|e| format!("reading {}: {e}", directory.display()))?.path();
|
|
102
|
+
if path.is_dir() {
|
|
103
|
+
walk(&path, names)?;
|
|
104
|
+
} else if path.extension().and_then(|e| e.to_str()) == Some("bluebook") {
|
|
105
|
+
if let Ok(name) = header_chapter_name(&path) {
|
|
106
|
+
names.push(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
Ok(())
|
|
111
|
+
}
|
|
112
|
+
let mut names = Vec::new();
|
|
113
|
+
for directory in OTHER_GEM_CHAPTER_DIRS {
|
|
114
|
+
walk(&root.join("lib/hecks").join(directory), &mut names)?;
|
|
115
|
+
}
|
|
116
|
+
names.sort();
|
|
117
|
+
names.dedup();
|
|
118
|
+
Ok(names)
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
fn resolve_json(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path, key: &str) -> Result<Vec<String>, String> {
|
|
@@ -105,13 +138,13 @@ pub fn valid_package_name(name: &str) -> bool {
|
|
|
105
138
|
pub fn vendored_bluebook_files(domain: &Path, pkg_name: &str) -> Result<Vec<PathBuf>, String> {
|
|
106
139
|
if !valid_package_name(pkg_name) {
|
|
107
140
|
return Err(format!(
|
|
108
|
-
"
|
|
141
|
+
"attaches {pkg_name:?}, from: :vendor is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
|
|
109
142
|
));
|
|
110
143
|
}
|
|
111
144
|
let dir = domain.join("vendor").join("embryonaut_bluebooks").join(pkg_name).join("bluebook");
|
|
112
145
|
if !dir.is_dir() {
|
|
113
146
|
return Err(format!(
|
|
114
|
-
"
|
|
147
|
+
"attaches {pkg_name:?}, from: :vendor names no vendored bluebook at {} — run hecks vendor {pkg_name}",
|
|
115
148
|
dir.display()
|
|
116
149
|
));
|
|
117
150
|
}
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.0
|
|
1
|
+
3.1.0
|
data/rust/host/src/api.rs
CHANGED
|
@@ -780,7 +780,7 @@ async fn presentation_save(
|
|
|
780
780
|
const PRESENTATION_WRITE_REFUSAL: &str =
|
|
781
781
|
"this host's kernel carries no ConsoleSettings chapter, so it has no StateStyle/Collection/Overview \
|
|
782
782
|
commands to dispatch, and writing the rows behind its back would skip the invariants those commands \
|
|
783
|
-
enforce. Attach it with `
|
|
783
|
+
enforce. Attach it with `attaches \"ConsoleSettings\"` in this domain's own .hecksagon and \
|
|
784
784
|
rebuild the kernel, or save from the Ruby console engine instead.";
|
|
785
785
|
|
|
786
786
|
// ---- response envelopes ---------------------------------------------
|
data/rust/host/src/dispatch.rs
CHANGED
|
@@ -31,7 +31,9 @@ pub const ANONYMOUS_ROLE: &str = "Anonymous";
|
|
|
31
31
|
pub enum Enforcement {
|
|
32
32
|
/// A caller with no role is unchecked, as before.
|
|
33
33
|
Off,
|
|
34
|
-
/// An unidentified or unassigned caller is let through and logged as `would_refuse_role
|
|
34
|
+
/// An unidentified or unassigned caller is let through and logged as `would_refuse_role`; a
|
|
35
|
+
/// caller that states a role is checked exactly as under `Enforce`, so shadow never accepts
|
|
36
|
+
/// what `Off` would not.
|
|
35
37
|
Shadow,
|
|
36
38
|
/// An unidentified or unassigned caller is refused.
|
|
37
39
|
Enforce,
|
|
@@ -62,6 +64,15 @@ impl Enforcement {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/// Where a dispatch comes from. A host-internal dispatch is the host acting on its own behalf (a
|
|
68
|
+
/// signup, a provisioning step, a presentation write); it names no caller and is never subject to
|
|
69
|
+
/// `HECKS_ROLE_ENFORCEMENT`, which guards what callers outside the host may do.
|
|
70
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
71
|
+
enum Origin {
|
|
72
|
+
External,
|
|
73
|
+
HostInternal,
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/// Whether the kernel refused the command for the caller's role.
|
|
66
77
|
fn refused_for_role(result: &serde_json::Value) -> bool {
|
|
67
78
|
result.get("refusals").and_then(|r| r.as_array()).is_some_and(|all| {
|
|
@@ -189,6 +200,10 @@ pub async fn handle_facts_as(
|
|
|
189
200
|
// `role` is optional, matching `Adapters::Lambda::Client#dispatch`'s own
|
|
190
201
|
// `role: nil` default: a step with no role looks the same on the wire
|
|
191
202
|
// whether or not a caller is bound.
|
|
203
|
+
//
|
|
204
|
+
// This entry point and `handle_facts`/`handle_routed` are the host's own: a call with no role is a
|
|
205
|
+
// host-internal dispatch and is unchecked in every enforcement mode. A call that names a role is
|
|
206
|
+
// still checked against it. Callers outside the host go through the `_as` entry points.
|
|
192
207
|
pub async fn handle(
|
|
193
208
|
client: &Mutex<Client>,
|
|
194
209
|
wasm_path: &Path,
|
|
@@ -198,7 +213,8 @@ pub async fn handle(
|
|
|
198
213
|
config: &LineageConfig,
|
|
199
214
|
invoker: &dyn LambdaInvoker,
|
|
200
215
|
) -> anyhow::Result<Outcome> {
|
|
201
|
-
|
|
216
|
+
let origin = if role.is_none() { Origin::HostInternal } else { Origin::External };
|
|
217
|
+
dispatch_in(Enforcement::from_env(), origin, client, wasm_path, verb, args, Caller { role, actor_id: None }, config, invoker).await
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
/// Dispatches for a caller, applying `HECKS_ROLE_ENFORCEMENT`: unidentified callers are refused (or,
|
|
@@ -212,12 +228,13 @@ pub async fn handle_as(
|
|
|
212
228
|
config: &LineageConfig,
|
|
213
229
|
invoker: &dyn LambdaInvoker,
|
|
214
230
|
) -> anyhow::Result<Outcome> {
|
|
215
|
-
|
|
231
|
+
dispatch_in(Enforcement::from_env(), Origin::External, client, wasm_path, verb, args, caller, config, invoker).await
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
#[allow(clippy::too_many_arguments)]
|
|
219
|
-
async fn
|
|
235
|
+
async fn dispatch_in(
|
|
220
236
|
mode: Enforcement,
|
|
237
|
+
origin: Origin,
|
|
221
238
|
client: &Mutex<Client>,
|
|
222
239
|
wasm_path: &Path,
|
|
223
240
|
verb: &str,
|
|
@@ -226,9 +243,11 @@ async fn handle_with(
|
|
|
226
243
|
config: &LineageConfig,
|
|
227
244
|
invoker: &dyn LambdaInvoker,
|
|
228
245
|
) -> anyhow::Result<Outcome> {
|
|
229
|
-
let checked = mode.effective(caller);
|
|
246
|
+
let checked = if origin == Origin::HostInternal { caller } else { mode.effective(caller) };
|
|
230
247
|
let outcome = run(client, wasm_path, verb, args.clone(), checked, config, invoker).await?;
|
|
231
|
-
|
|
248
|
+
// Only a caller that stated no role (unidentified, or an actor Governance holds no grant for)
|
|
249
|
+
// is let through; a stated role that does not match stays refused, as it is under `Off`.
|
|
250
|
+
if mode == Enforcement::Shadow && caller.role.is_none() && !outcome.accepted && refused_for_role(&outcome.result) {
|
|
232
251
|
crate::log::error("would_refuse_role", serde_json::json!({
|
|
233
252
|
"verb": verb, "role": checked.role, "actor_id": checked.actor_id,
|
|
234
253
|
"refusals": outcome.result.get("refusals"),
|
|
@@ -287,6 +306,8 @@ async fn run(
|
|
|
287
306
|
let mut args = args;
|
|
288
307
|
if let Some(domain_ir) = crate::ir::ir() {
|
|
289
308
|
crate::needs::fill_needs(domain_ir, verb, &mut args, &crate::needs::ProcessClock);
|
|
309
|
+
// An argument with a declared default is filled the same way, so the journal holds it too.
|
|
310
|
+
crate::needs::fill_defaults(domain_ir, verb, &mut args);
|
|
290
311
|
}
|
|
291
312
|
let mut step = serde_json::json!({ "verb": verb, "args": args.clone() });
|
|
292
313
|
if let Some(role) = role {
|
|
@@ -330,6 +351,10 @@ async fn run(
|
|
|
330
351
|
if table.as_object().is_some_and(|t| !t.is_empty()) {
|
|
331
352
|
input["needs"] = table;
|
|
332
353
|
}
|
|
354
|
+
let defaults = crate::needs::defaults_table(domain_ir);
|
|
355
|
+
if defaults.as_object().is_some_and(|t| !t.is_empty()) {
|
|
356
|
+
input["defaults"] = defaults;
|
|
357
|
+
}
|
|
333
358
|
}
|
|
334
359
|
let input = input.to_string();
|
|
335
360
|
// `wasm_runner::run` is sync, and wasmtime-wasi's sync bridge spins up
|
|
@@ -1353,7 +1378,7 @@ pub(crate) mod tests {
|
|
|
1353
1378
|
let client = &client;
|
|
1354
1379
|
let config = &config;
|
|
1355
1380
|
async move {
|
|
1356
|
-
|
|
1381
|
+
dispatch_in(mode, Origin::External, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
|
|
1357
1382
|
.await
|
|
1358
1383
|
.unwrap()
|
|
1359
1384
|
}
|
|
@@ -1375,6 +1400,61 @@ pub(crate) mod tests {
|
|
|
1375
1400
|
assert_eq!(steps.len(), 2, "the enforced refusal was not persisted; off and shadow were");
|
|
1376
1401
|
}
|
|
1377
1402
|
|
|
1403
|
+
// Shadow may only let through what Off would: a caller that stated a wrong role is refused.
|
|
1404
|
+
#[tokio::test]
|
|
1405
|
+
async fn shadow_refuses_a_stated_wrong_role_and_still_lets_an_unidentified_caller_through() {
|
|
1406
|
+
let client = scratch_db("rust_host_dispatch_test_shadow_stated").await;
|
|
1407
|
+
provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
|
|
1408
|
+
let config = test_config("Banking", 1);
|
|
1409
|
+
let go = |mode: Enforcement, origin: Origin, caller: Caller<'static>, id: &'static str| {
|
|
1410
|
+
let client = &client;
|
|
1411
|
+
let config = &config;
|
|
1412
|
+
async move {
|
|
1413
|
+
dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), caller, config, &lambda_client::NeverInvoker)
|
|
1414
|
+
.await
|
|
1415
|
+
.unwrap()
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
let wrong = Caller { role: Some("Auditor"), actor_id: None };
|
|
1419
|
+
|
|
1420
|
+
let shadow_wrong = go(Enforcement::Shadow, Origin::External, wrong, "CUST-0031").await;
|
|
1421
|
+
assert!(!shadow_wrong.accepted, "shadow must not accept a stated wrong role: {:?}", shadow_wrong.result);
|
|
1422
|
+
assert!(refused_for_role(&shadow_wrong.result), "{:?}", shadow_wrong.result);
|
|
1423
|
+
let off_wrong = go(Enforcement::Off, Origin::External, wrong, "CUST-0032").await;
|
|
1424
|
+
assert!(!off_wrong.accepted, "off refuses it too: {:?}", off_wrong.result);
|
|
1425
|
+
|
|
1426
|
+
let nobody = go(Enforcement::Shadow, Origin::External, Caller::default(), "CUST-0033").await;
|
|
1427
|
+
assert!(nobody.accepted, "shadow lets an unidentified caller through: {:?}", nobody.result);
|
|
1428
|
+
|
|
1429
|
+
let steps = journal::load_steps(&*client.lock().await).await.unwrap();
|
|
1430
|
+
assert_eq!(steps.len(), 1, "only the unidentified caller's registration was persisted");
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// A host-internal dispatch names no caller and is unchecked in every mode, while an external
|
|
1434
|
+
// unidentified caller is refused under enforce.
|
|
1435
|
+
#[tokio::test]
|
|
1436
|
+
async fn a_host_internal_dispatch_is_unchecked_under_enforce_but_an_external_one_is_refused() {
|
|
1437
|
+
let client = scratch_db("rust_host_dispatch_test_internal").await;
|
|
1438
|
+
provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
|
|
1439
|
+
let config = test_config("Banking", 1);
|
|
1440
|
+
let go = |mode: Enforcement, origin: Origin, id: &'static str| {
|
|
1441
|
+
let client = &client;
|
|
1442
|
+
let config = &config;
|
|
1443
|
+
async move {
|
|
1444
|
+
dispatch_in(mode, origin, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
|
|
1445
|
+
.await
|
|
1446
|
+
.unwrap()
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
let external = go(Enforcement::Enforce, Origin::External, "CUST-0041").await;
|
|
1451
|
+
assert!(!external.accepted, "{:?}", external.result);
|
|
1452
|
+
for (mode, id) in [(Enforcement::Enforce, "CUST-0042"), (Enforcement::Shadow, "CUST-0043"), (Enforcement::Off, "CUST-0044")] {
|
|
1453
|
+
let internal = go(mode, Origin::HostInternal, id).await;
|
|
1454
|
+
assert!(internal.accepted, "{mode:?}: {:?}", internal.result);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1378
1458
|
#[tokio::test]
|
|
1379
1459
|
async fn a_mid_transaction_failure_rolls_back_any_saga_state_already_written() {
|
|
1380
1460
|
// Deliberately doesn't provision "Transfer": its mutation fails
|
data/rust/host/src/needs.rs
CHANGED
|
@@ -110,6 +110,52 @@ pub fn table(domain_ir: &Value) -> Value {
|
|
|
110
110
|
Value::Object(verbs)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/// The command's attributes that declare a default (`attribute :runs, Count, default: 30`), each
|
|
114
|
+
/// with the declared value. An attribute whose default is null declares none.
|
|
115
|
+
fn declared_defaults<'a>(domain_ir: &'a Value, verb: &str) -> Vec<(&'a str, &'a Value)> {
|
|
116
|
+
let Some((qualified_aggregate, command_name)) = verb.rsplit_once('.') else { return Vec::new() };
|
|
117
|
+
let aggregate = qualified_aggregate.rsplit("::").next().unwrap_or(qualified_aggregate);
|
|
118
|
+
let named = |value: &Value, name: &str| value.get("name").and_then(Value::as_str) == Some(name);
|
|
119
|
+
let list = |value: &'a Value, key: &str| -> Vec<&'a Value> {
|
|
120
|
+
value.get(key).and_then(Value::as_array).map(|a| a.iter().collect()).unwrap_or_default()
|
|
121
|
+
};
|
|
122
|
+
list(domain_ir, "aggregates")
|
|
123
|
+
.into_iter()
|
|
124
|
+
.filter(|a| named(a, aggregate))
|
|
125
|
+
.flat_map(|a| list(a, "commands"))
|
|
126
|
+
.filter(|c| named(c, command_name))
|
|
127
|
+
.flat_map(|c| list(c, "attributes"))
|
|
128
|
+
.filter_map(|a| {
|
|
129
|
+
let default = a.get("default").filter(|d| !d.is_null())?;
|
|
130
|
+
Some((a.get("name").and_then(Value::as_str)?, default))
|
|
131
|
+
})
|
|
132
|
+
.collect()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Every command's declared defaults as the kernel's input `"defaults"` reads them:
|
|
136
|
+
/// `{ verb: { attribute: value } }`. The kernel fills a reaction's command from this the same way
|
|
137
|
+
/// the host fills the outermost one.
|
|
138
|
+
pub fn defaults_table(domain_ir: &Value) -> Value {
|
|
139
|
+
let domain = domain_ir.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
140
|
+
let mut verbs = serde_json::Map::new();
|
|
141
|
+
let aggregates = domain_ir.get("aggregates").and_then(Value::as_array).into_iter().flatten();
|
|
142
|
+
for aggregate in aggregates {
|
|
143
|
+
let aggregate_name = aggregate.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
144
|
+
for command in aggregate.get("commands").and_then(Value::as_array).into_iter().flatten() {
|
|
145
|
+
let command_name = command.get("name").and_then(Value::as_str).unwrap_or_default();
|
|
146
|
+
let verb = format!("{domain}::{aggregate_name}.{command_name}");
|
|
147
|
+
let defaults: serde_json::Map<String, Value> = declared_defaults(domain_ir, &verb)
|
|
148
|
+
.into_iter()
|
|
149
|
+
.map(|(name, value)| (name.to_string(), value.clone()))
|
|
150
|
+
.collect();
|
|
151
|
+
if !defaults.is_empty() {
|
|
152
|
+
verbs.insert(verb, Value::Object(defaults));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
Value::Object(verbs)
|
|
157
|
+
}
|
|
158
|
+
|
|
113
159
|
/// The facts of a command invocation: the `with` object when the call carries one, else the
|
|
114
160
|
/// flat object itself (the kernel's `CommandInvocation` reads both shapes).
|
|
115
161
|
fn facts_mut(args: &mut Value) -> Option<&mut serde_json::Map<String, Value>> {
|
|
@@ -147,6 +193,22 @@ pub fn fill_needs(domain_ir: &Value, verb: &str, args: &mut Value, clock: &dyn C
|
|
|
147
193
|
}
|
|
148
194
|
}
|
|
149
195
|
|
|
196
|
+
/// Gives each argument `verb` declares a default for, and `args` leaves out, that default, after
|
|
197
|
+
/// `fill_needs` and before any gate reads the arguments. A key the caller supplied (even a null)
|
|
198
|
+
/// is kept, as is a fact `fill_needs` already answered.
|
|
199
|
+
pub fn fill_defaults(domain_ir: &Value, verb: &str, args: &mut Value) {
|
|
200
|
+
let defaults = declared_defaults(domain_ir, verb);
|
|
201
|
+
if defaults.is_empty() {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let Some(facts) = facts_mut(args) else { return };
|
|
205
|
+
for (name, value) in defaults {
|
|
206
|
+
if !facts.contains_key(name) {
|
|
207
|
+
facts.insert(name.to_string(), value.clone());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
150
212
|
#[cfg(test)]
|
|
151
213
|
mod tests {
|
|
152
214
|
use super::*;
|
|
@@ -240,4 +302,53 @@ mod tests {
|
|
|
240
302
|
clear_fixed_clock();
|
|
241
303
|
assert!(ProcessClock.now_secs() > 1_700_000_000);
|
|
242
304
|
}
|
|
305
|
+
|
|
306
|
+
// A command with one defaulted attribute, one plain, and one whose default is null.
|
|
307
|
+
fn defaulted_ir() -> Value {
|
|
308
|
+
json!({"name": "Qc", "aggregates": [{"name": "Check", "commands": [
|
|
309
|
+
{"name": "Start", "attributes": [
|
|
310
|
+
{"name": "ref", "type": "Ref", "default": null},
|
|
311
|
+
{"name": "runs", "type": "Count", "default": 30},
|
|
312
|
+
{"name": "note", "type": "String", "default": null}
|
|
313
|
+
], "needs": []}
|
|
314
|
+
]}]})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn an_omitted_argument_is_filled_with_its_declared_default() {
|
|
319
|
+
let mut args = json!({"with": {"ref": {"value": "a"}}});
|
|
320
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
321
|
+
assert_eq!(args["with"]["runs"], 30);
|
|
322
|
+
assert!(args["with"].get("note").is_none(), "a null default declares none");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn a_flat_invocation_gets_its_defaults_at_its_top_level() {
|
|
327
|
+
let mut args = json!({"ref": {"value": "a"}});
|
|
328
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
329
|
+
assert_eq!(args["runs"], 30);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn a_supplied_argument_is_kept_even_when_null() {
|
|
334
|
+
let mut args = json!({"runs": null});
|
|
335
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
336
|
+
assert!(args["runs"].is_null());
|
|
337
|
+
let mut args = json!({"runs": {"value": 7}});
|
|
338
|
+
fill_defaults(&defaulted_ir(), "Qc::Check.Start", &mut args);
|
|
339
|
+
assert_eq!(args["runs"], json!({"value": 7}));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[test]
|
|
343
|
+
fn a_command_that_declares_no_default_is_untouched() {
|
|
344
|
+
let mut args = json!({"with": {}});
|
|
345
|
+
fill_defaults(&integer_ir(), "Qc::Check.Run", &mut args);
|
|
346
|
+
assert_eq!(args, json!({"with": {}}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn the_kernel_table_lists_each_command_that_declares_a_default() {
|
|
351
|
+
assert_eq!(defaults_table(&defaulted_ir()), json!({"Qc::Check.Start": {"runs": 30}}));
|
|
352
|
+
assert_eq!(defaults_table(&integer_ir()), json!({}));
|
|
353
|
+
}
|
|
243
354
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The optional branded confirmation email: an HTML template an operator
|
|
2
|
+
// hosts, fetched at send time. Hecks ships no brand; with no template (or
|
|
3
|
+
// any trouble with one) the caller keeps the plain-text body.
|
|
4
|
+
use super::super::newsletter_send::personalize;
|
|
5
|
+
use std::time::Duration;
|
|
6
|
+
|
|
7
|
+
pub(super) const CONFIRM_TOKEN: &str = "{{CONFIRM_URL}}";
|
|
8
|
+
const FETCH_TIMEOUT: Duration = Duration::from_secs(5);
|
|
9
|
+
pub(super) const MAX_TEMPLATE_BYTES: usize = 256 * 1024;
|
|
10
|
+
|
|
11
|
+
// Escapes a URL for an HTML attribute or text position.
|
|
12
|
+
fn escape_html(value: &str) -> String {
|
|
13
|
+
value.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Fills the placeholders with HTML-escaped URLs. `Err` names why the
|
|
17
|
+
// template is unusable: without {{CONFIRM_URL}} the email would carry no
|
|
18
|
+
// link. {{UNSUBSCRIBE_URL}} is optional. The result starts at its first
|
|
19
|
+
// non-blank character so the mailer sends it as HTML.
|
|
20
|
+
pub(super) fn fill_template(template: &str, confirm_url: &str, unsubscribe_url: &str) -> Result<String, &'static str> {
|
|
21
|
+
if !template.contains(CONFIRM_TOKEN) {
|
|
22
|
+
return Err("the template has no {{CONFIRM_URL}} placeholder");
|
|
23
|
+
}
|
|
24
|
+
let filled = template.replace(CONFIRM_TOKEN, &escape_html(confirm_url));
|
|
25
|
+
Ok(personalize(&filled, &escape_html(unsubscribe_url)).trim_start().to_string())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Reads a response body, stopping as soon as it passes the cap.
|
|
29
|
+
async fn read_capped(mut response: reqwest::Response) -> Result<String, String> {
|
|
30
|
+
let mut bytes: Vec<u8> = Vec::new();
|
|
31
|
+
while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
|
|
32
|
+
bytes.extend_from_slice(&chunk);
|
|
33
|
+
if bytes.len() > MAX_TEMPLATE_BYTES {
|
|
34
|
+
return Err(format!("the template is larger than {MAX_TEMPLATE_BYTES} bytes"));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
String::from_utf8(bytes).map_err(|_| "the template is not UTF-8".to_string())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GETs the template (5 second timeout, size capped); a non-2xx answer or
|
|
41
|
+
// any transport trouble is an `Err` with the reason.
|
|
42
|
+
async fn fetch_template(url: &str) -> Result<String, String> {
|
|
43
|
+
let http = reqwest::Client::builder().timeout(FETCH_TIMEOUT).build().map_err(|e| e.to_string())?;
|
|
44
|
+
let response = http.get(url).send().await.map_err(|e| e.to_string())?;
|
|
45
|
+
if !response.status().is_success() {
|
|
46
|
+
return Err(format!("the template answered {}", response.status()));
|
|
47
|
+
}
|
|
48
|
+
read_capped(response).await
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The HTML body when NEWSLETTER_CONFIRMATION_TEMPLATE_URL names a usable
|
|
52
|
+
// template; None (after logging why) when the caller should send plain text.
|
|
53
|
+
pub(super) async fn branded_body(confirm_url: &str, unsubscribe_url: &str) -> Option<String> {
|
|
54
|
+
let url = std::env::var("NEWSLETTER_CONFIRMATION_TEMPLATE_URL").ok().filter(|u| !u.trim().is_empty())?;
|
|
55
|
+
let template = match fetch_template(url.trim()).await {
|
|
56
|
+
Ok(t) => t,
|
|
57
|
+
Err(e) => {
|
|
58
|
+
eprintln!("newsletter: the confirmation template could not be fetched ({e}), so the plain-text confirmation was sent");
|
|
59
|
+
return None;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
match fill_template(&template, confirm_url, unsubscribe_url) {
|
|
63
|
+
Ok(body) => Some(body),
|
|
64
|
+
Err(e) => {
|
|
65
|
+
eprintln!("newsletter: the confirmation template was rejected ({e}), so the plain-text confirmation was sent");
|
|
66
|
+
None
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -102,3 +102,46 @@ fn a_confirm_token_is_not_an_unsubscribe_token_and_the_reverse() {
|
|
|
102
102
|
assert!(!unsubscribe_token_matches("s3cret-value", &confirm, "a@example.com"));
|
|
103
103
|
assert!(!confirm_token_matches("s3cret-value", &unsubscribe, "a@example.com"));
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
use template::fill_template;
|
|
107
|
+
|
|
108
|
+
const CONFIRM: &str = "https://example.com/newsletter-confirmed.html?email=a%40b.c&token=t";
|
|
109
|
+
const UNSUBSCRIBE: &str = "https://example.com/newsletter-unsubscribed.html?email=a%40b.c&token=u";
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn a_template_gets_the_confirm_link_in_place_of_its_placeholder() {
|
|
113
|
+
let body = fill_template("<p><a href=\"{{CONFIRM_URL}}\">Confirm</a></p>", CONFIRM, UNSUBSCRIBE).unwrap();
|
|
114
|
+
assert_eq!(body, "<p><a href=\"https://example.com/newsletter-confirmed.html?email=a%40b.c&token=t\">Confirm</a></p>");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn a_template_without_the_confirm_placeholder_is_rejected() {
|
|
119
|
+
assert!(fill_template("<p>{{UNSUBSCRIBE_URL}}</p>", CONFIRM, UNSUBSCRIBE).is_err());
|
|
120
|
+
assert!(fill_template("", CONFIRM, UNSUBSCRIBE).is_err());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn the_unsubscribe_placeholder_is_optional_and_replaced_wherever_present() {
|
|
125
|
+
assert!(fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>", CONFIRM, UNSUBSCRIBE).is_ok());
|
|
126
|
+
let body = fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>{{UNSUBSCRIBE_URL}}|{{UNSUBSCRIBE_URL}}", CONFIRM, UNSUBSCRIBE).unwrap();
|
|
127
|
+
assert!(!body.contains("{{"));
|
|
128
|
+
assert_eq!(body.matches("newsletter-unsubscribed.html").count(), 2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn ampersands_and_quotes_in_a_url_are_escaped_inside_the_html() {
|
|
133
|
+
let body = fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>", "https://e.com/?a=1&b=\"2\"<>'", UNSUBSCRIBE).unwrap();
|
|
134
|
+
assert_eq!(body, "<a href=\"https://e.com/?a=1&b="2"<>'\">x</a>");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn a_filled_template_starts_at_its_first_tag_so_the_mailer_sends_html() {
|
|
139
|
+
let body = fill_template("\n <p>{{CONFIRM_URL}}</p>", CONFIRM, UNSUBSCRIBE).unwrap();
|
|
140
|
+
assert!(body.starts_with('<'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn without_a_template_the_plain_text_confirmation_is_unchanged() {
|
|
145
|
+
let body = confirmation_body(CONFIRM);
|
|
146
|
+
assert_eq!(body, format!("Please confirm your newsletter subscription by opening this link:\n\n{CONFIRM}\n\nIf you didn't ask for this, you can ignore this email and nothing will be sent to you.\n"));
|
|
147
|
+
}
|
|
@@ -14,6 +14,7 @@ use tokio::sync::Mutex;
|
|
|
14
14
|
use tokio_postgres::Client;
|
|
15
15
|
|
|
16
16
|
mod cooldown;
|
|
17
|
+
mod template;
|
|
17
18
|
#[cfg(test)]
|
|
18
19
|
mod tests;
|
|
19
20
|
|
|
@@ -194,8 +195,12 @@ pub(super) async fn send_confirmation(email: &str) {
|
|
|
194
195
|
let site = site_url();
|
|
195
196
|
let link = confirm_url(&site, email, &confirm_token(&secret, email));
|
|
196
197
|
let unsubscribe = unsubscribe_url(&site, email, &unsubscribe_token(&secret, email));
|
|
198
|
+
let body = match template::branded_body(&link, &unsubscribe).await {
|
|
199
|
+
Some(html) => html,
|
|
200
|
+
None => confirmation_body(&link),
|
|
201
|
+
};
|
|
197
202
|
let delivery = mailer
|
|
198
|
-
.deliver(&Email { to: email, subject: "Confirm your newsletter subscription", body: &
|
|
203
|
+
.deliver(&Email { to: email, subject: "Confirm your newsletter subscription", body: &body, unsubscribe_url: Some(&unsubscribe) })
|
|
199
204
|
.await;
|
|
200
205
|
if !delivery.ok {
|
|
201
206
|
eprintln!("newsletter: the confirmation email was not delivered: {}", delivery.reason.unwrap_or_default());
|
|
@@ -132,7 +132,7 @@ pub(super) fn unsubscribe_url(site_url: &str, email: &str, token: &str) -> Strin
|
|
|
132
132
|
url.to_string()
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
fn personalize(body: &str, unsubscribe_url: &str) -> String {
|
|
135
|
+
pub(super) fn personalize(body: &str, unsubscribe_url: &str) -> String {
|
|
136
136
|
body.replace(UNSUBSCRIBE_TOKEN, unsubscribe_url)
|
|
137
137
|
}
|
|
138
138
|
|
data/rust/parser/src/keywords.rs
CHANGED
|
@@ -182,9 +182,9 @@ pub static KEYWORDS: &[KeywordRow] = &[
|
|
|
182
182
|
KeywordRow { word: "answers_query", context: "DomainPort", body: "none", inner: "", opens: "", fills: "answered_queries", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
183
183
|
KeywordRow { word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
184
184
|
KeywordRow { word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
185
|
-
KeywordRow { word: "
|
|
186
|
-
KeywordRow { word: "
|
|
187
|
-
KeywordRow { word: "
|
|
185
|
+
KeywordRow { word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
186
|
+
KeywordRow { word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated", was: "", resolves_via: "", disambiguator: "" },
|
|
187
|
+
KeywordRow { word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated", was: "", resolves_via: "", disambiguator: "" },
|
|
188
188
|
KeywordRow { word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
189
189
|
KeywordRow { word: "translates", context: "Hecksagon", body: "keywords", inner: "Policy", opens: "Policy", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
190
190
|
KeywordRow { word: "bounded", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
|
|
@@ -466,9 +466,10 @@ pub static ARGUMENTS: &[ArgumentRow] = &[
|
|
|
466
466
|
ArgumentRow { keyword: "asks", context: "DomainPort", at: "", named: "to", kind: "constant", required: "false", fills: "to", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
467
467
|
ArgumentRow { keyword: "hecksagon", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
468
468
|
ArgumentRow { keyword: "subscribe", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "subscriptions", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
469
|
-
ArgumentRow { keyword: "
|
|
470
|
-
ArgumentRow { keyword: "
|
|
471
|
-
ArgumentRow { keyword: "
|
|
469
|
+
ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
470
|
+
ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "", named: "from", kind: "symbol", required: "false", fills: "source", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
471
|
+
ArgumentRow { keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
472
|
+
ArgumentRow { keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
472
473
|
ArgumentRow { keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
473
474
|
ArgumentRow { keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
|
474
475
|
ArgumentRow { keyword: "reference_to", context: "PortOperation", at: "1", named: "", kind: "constant", required: "true", fills: "type", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
|
data/rust/parser/src/main.rs
CHANGED
|
@@ -91,7 +91,8 @@ fn run_chapter(args: &[String]) -> Result<(), RunError> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/// `hecks-parse resolve --chapter <Name> <file.hecksagon>`: prints that chapter's
|
|
94
|
-
/// `
|
|
94
|
+
/// `attaches` names as JSON, in file order: `gem_chapters` for a chapter the gem carries and
|
|
95
|
+
/// `vendored_packages` for `from: :vendor`.
|
|
95
96
|
///
|
|
96
97
|
/// `--chapter` is required because one file can hold several `Hecks.hecksagon` blocks.
|
|
97
98
|
fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
@@ -120,7 +121,7 @@ fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
|
120
121
|
let value = emit::JsonValue::Object(vec![
|
|
121
122
|
("domain".to_string(), emit::JsonValue::String(chapter_name)),
|
|
122
123
|
(
|
|
123
|
-
"
|
|
124
|
+
"gem_chapters".to_string(),
|
|
124
125
|
emit::JsonValue::Array(
|
|
125
126
|
framework_names
|
|
126
127
|
.into_iter()
|
|
@@ -129,7 +130,7 @@ fn run_resolve(args: &[String]) -> Result<(), RunError> {
|
|
|
129
130
|
),
|
|
130
131
|
),
|
|
131
132
|
(
|
|
132
|
-
"
|
|
133
|
+
"vendored_packages".to_string(),
|
|
133
134
|
emit::JsonValue::Array(
|
|
134
135
|
vendored_names
|
|
135
136
|
.into_iter()
|
|
@@ -306,7 +306,7 @@ pub fn parse_chapter(chapter_name: &str, files: &[(String, String)]) -> ParseRes
|
|
|
306
306
|
Ok(bluebook)
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
/// Lists the chapters `<Name>`'s block pulls in via `
|
|
309
|
+
/// Lists the chapters `<Name>`'s block pulls in via `attaches` (or its deprecated spellings).
|
|
310
310
|
/// Every block is still gated; the IR accumulator is a throwaway.
|
|
311
311
|
pub fn resolve_hecksagon_dependencies(
|
|
312
312
|
chapter_name: &str,
|