hecks 3.0.5 → 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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +31 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/interview_draft.rb +246 -0
  15. data/lib/hecks/cli/model_check.rb +1 -1
  16. data/lib/hecks/cli/project_cli.rb +51 -29
  17. data/lib/hecks/cli/reference.rb +2 -1
  18. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  19. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  20. data/lib/hecks/cli/spec_example.rb +1 -1
  21. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  22. data/lib/hecks/corpus.rb +11 -8
  23. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  24. data/lib/hecks/deploy/oidc.json +5 -0
  25. data/lib/hecks/doc/reference.rb +4 -3
  26. data/lib/hecks/doors/cli_door.rb +13 -13
  27. data/lib/hecks/doors/cli_runner.rb +52 -32
  28. data/lib/hecks/doors/launcher_options.rb +5 -5
  29. data/lib/hecks/doors/mcp_door.rb +40 -18
  30. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  31. data/lib/hecks/doors.rb +1 -1
  32. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  33. data/lib/hecks/forms/banking_presentation.rb +1 -1
  34. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  35. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  36. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  37. data/lib/hecks/framework.rb +1 -1
  38. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  39. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  40. data/lib/hecks/fuzzing/differential.rb +4 -2
  41. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  42. data/lib/hecks/fuzzing/form_census.rb +2 -2
  43. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  44. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  45. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  46. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  47. data/lib/hecks/gate/stages.yml +5 -5
  48. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  49. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  50. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  51. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  52. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  53. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  54. data/lib/hecks/hecks/codebase.bluebook +4 -3
  55. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  56. data/lib/hecks/hecks/custodian.bluebook +45 -3
  57. data/lib/hecks/hecks/hecks.hecksagon +10 -4
  58. data/lib/hecks/hecks/hecks.world +6 -6
  59. data/lib/hecks/hecks/site.behaviors +17 -0
  60. data/lib/hecks/hecks/site.world +10 -0
  61. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  62. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  63. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  64. data/lib/hecks/projections/deploy/box/settings.rb +248 -0
  65. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
  66. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
  67. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  68. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
  69. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  70. data/lib/hecks/projections/deploy/box.rb +387 -0
  71. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  72. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  73. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  74. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  75. data/lib/hecks/projections/diagrams.rb +2 -2
  76. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  77. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  78. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  79. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  80. data/lib/hecks/projections/site/edge.rb +136 -0
  81. data/lib/hecks/projections/site/regions.rb +44 -0
  82. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  83. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  84. data/lib/hecks/projections/site/table/checks.rb +124 -0
  85. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  86. data/lib/hecks/projections/site/table.rb +98 -0
  87. data/lib/hecks/projections.rb +2 -0
  88. data/lib/hecks/projector/cli_projector.rb +122 -104
  89. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  90. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  91. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  92. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  93. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  94. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  95. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  96. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  97. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  98. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  99. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  100. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  101. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  102. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  103. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  104. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  105. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  106. data/lib/hecks/runtime/registry/verification.rb +15 -29
  107. data/lib/hecks/runtime/registry.rb +14 -20
  108. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  109. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  110. data/lib/hecks/rust_build/domain_name.rb +26 -0
  111. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  112. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  113. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  114. data/lib/hecks/rust_build/project_rust.rb +57 -24
  115. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  116. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  117. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  118. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  119. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  120. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  121. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  122. data/lib/hecks/storehouse.rb +37 -7
  123. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  124. data/lib/hecks/three_zero/forms.yml +88 -88
  125. data/lib/hecks/tools/ci_gates.rb +1 -1
  126. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  127. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  128. data/lib/hecks/tools/sequence_script.rb +2 -2
  129. data/lib/hecks/tools/site_routes.rb +152 -0
  130. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  131. data/lib/hecks/tools.rb +1 -0
  132. data/lib/hecks/version.rb +1 -1
  133. data/rust/build/src/cargo_sync.rs +1 -1
  134. data/rust/build/src/pipeline.rs +23 -16
  135. data/rust/build/src/resolve.rs +39 -6
  136. data/rust/host/HECKS_RELEASE +1 -1
  137. data/rust/host/src/api.rs +1 -1
  138. data/rust/host/src/dispatch.rs +87 -7
  139. data/rust/host/src/needs.rs +111 -0
  140. data/rust/parser/src/keywords.rs +7 -6
  141. data/rust/parser/src/main.rs +4 -3
  142. data/rust/parser/src/parse/chapter.rs +1 -1
  143. data/rust/parser/src/parse/hecksagon.rs +25 -10
  144. data/rust/project/mutations.rb +4 -30
  145. data/rust/project/naming.rb +8 -28
  146. data/rust/project/write_if_changed.rb +4 -80
  147. data/rust/project_rust_pipeline.rb +16 -12
  148. data/rust/src/kernel/cli.rs +2 -0
  149. data/rust/src/kernel/needs.rs +74 -1
  150. metadata +37 -1
@@ -77,12 +77,45 @@ pub fn grammar_files(root: &Path) -> Result<Vec<PathBuf>, String> {
77
77
  bluebook_files(&dir)
78
78
  }
79
79
 
80
- pub fn resolve_uses_framework(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
81
- resolve_json(parser_bin, chapter_name, hecksagon_path, "uses_framework")
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
- pub fn resolve_uses_embryonaut_bluebook(parser_bin: &Path, chapter_name: &str, hecksagon_path: &Path) -> Result<Vec<String>, String> {
85
- resolve_json(parser_bin, chapter_name, hecksagon_path, "uses_embryonaut_bluebook")
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
- "uses_embryonaut_bluebook {pkg_name:?} is not a package name — it must match [a-z][a-z0-9_]* (the name `hecks vendor` accepts)"
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
- "uses_embryonaut_bluebook {pkg_name:?} names no vendored bluebook at {} — run hecks vendor {pkg_name}",
147
+ "attaches {pkg_name:?}, from: :vendor names no vendored bluebook at {} — run hecks vendor {pkg_name}",
115
148
  dir.display()
116
149
  ));
117
150
  }
@@ -1 +1 @@
1
- 3.0.5
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 `uses_framework \"ConsoleSettings\"` in this domain's own .hecksagon and \
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 ---------------------------------------------
@@ -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
- handle_as(client, wasm_path, verb, args, Caller { role, actor_id: None }, config, invoker).await
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
- handle_with(Enforcement::from_env(), client, wasm_path, verb, args, caller, config, invoker).await
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 handle_with(
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
- if mode == Enforcement::Shadow && !outcome.accepted && refused_for_role(&outcome.result) {
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
- handle_with(mode, client, &wasm_path(), "Banking::Customer.Register", register(id), Caller::default(), config, &lambda_client::NeverInvoker)
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
@@ -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
  }
@@ -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: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "framework_members", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
186
- KeywordRow { word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "vendored_bluebooks", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
187
- KeywordRow { word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attached_chapters", status: "admitted", was: "", resolves_via: "", disambiguator: "" },
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: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "framework_members", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
470
- ArgumentRow { keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "vendored_bluebooks", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
471
- ArgumentRow { keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attached_chapters", selects: "", pair_key_fills: "", pair_value_fills: "", pairs_shape: "", status: "admitted", variadic: "", minimum: "" },
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: "" },
@@ -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
- /// `uses_framework` and `uses_embryonaut_bluebook` names as JSON, in file order.
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
- "uses_framework".to_string(),
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
- "uses_embryonaut_bluebook".to_string(),
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 `uses_framework`/`uses_embryonaut_bluebook`.
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,
@@ -1,4 +1,4 @@
1
- //! The `Hecksagon` construct: a `.hecksagon` file's `port` blocks and `uses_framework` words.
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
- /// `uses_framework_names` and `vendored_bluebook_names` collect each argument in file order
18
- /// for `hecks-parse resolve`; `ir.json` itself carries neither.
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
- uses_framework_names: &mut Vec<String>,
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
- // Gated, then collected; `ir.json` carries no `uses_framework` key.
101
+ // Deprecated spelling of `attaches "Name"`; gated, then collected.
100
102
  "uses_framework" => {
101
- uses_framework_names.push(super::positional_text(
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
- // Collected like `uses_framework`; a binding fact, absent from `ir.json`.
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
- // A chapter the gem carries, attached by name (ADR 0080); only the Ruby runtime
121
- // loads it, so its one argument is checked and then dropped like `subscribe`.
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
- super::positional_text(file, gated.line.number, "attaches", &gated.args, 1)?;
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" => {}
@@ -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
- # LOCAL FIRST — an aggregate's own nested entity is scoped to that
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[:attributes].each do |target_attr|
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
@@ -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::Vocabulary.fetch("CargoReservedName")
63
-
64
- # Checks identifier shape locally; delegates the reserved-word half to
65
- # `ModelCheck.rust_reserved_name_findings`, the one shared check.
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
- # Ruby's String#inspect escapes for Ruby's own read-back (e.g. a
224
- # brace-less `\uXXXX`), which isn't valid Rust syntax. This escapes
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