1use std::collections::HashMap;
2
3use crate::policy::{FlagTolerance, UnknownTolerance};
4use crate::verdict::SafetyLevel;
5
6use super::types::*;
7
8macro_rules! fail {
23 ($($arg:tt)*) => { return Err(format!($($arg)*)) };
24}
25
26macro_rules! ensure {
28 ($cond:expr, $($arg:tt)*) => {
29 if !($cond) {
30 return Err(format!($($arg)*));
31 }
32 };
33}
34
35#[path = "build_lower.rs"]
36mod lower;
37use lower::{lower_behavior, lower_output};
38
39pub(super) fn build_policy(
57 standalone: Vec<String>,
58 valued: Vec<String>,
59 optional_valued: Vec<String>,
60 bare: Option<bool>,
61 max_positional: Option<usize>,
62 tolerate_unknown_short: Option<bool>,
63 tolerate_unknown_long: Option<bool>,
64 numeric_dash: Option<bool>,
65) -> OwnedPolicy {
66 let (mut standalone, mut valued) = (standalone, valued);
67 for flag in optional_valued {
68 standalone.push(flag.clone());
69 valued.push(flag);
70 }
71 let unknown = match (tolerate_unknown_short.unwrap_or(false), tolerate_unknown_long.unwrap_or(false)) {
72 (false, false) => UnknownTolerance::Strict,
73 (true, false) => UnknownTolerance::Short,
74 (false, true) => UnknownTolerance::Long,
75 (true, true) => UnknownTolerance::Both,
76 };
77 OwnedPolicy {
78 standalone,
79 valued,
80 bare: bare.unwrap_or(true),
81 max_positional,
82 tolerance: FlagTolerance { unknown, numeric_dash: numeric_dash.unwrap_or(false) },
83 }
84}
85
86fn build_matrix(toml: TomlMatrix) -> MatrixSpec {
87 let actions = toml
88 .actions
89 .into_iter()
90 .map(|(name, action)| {
91 let built = match action {
92 TomlMatrixAction::Policy(policy_key) => MatrixAction { policy_key, guard: None, guard_short: None },
93 TomlMatrixAction::Detailed(d) => MatrixAction { policy_key: d.policy, guard: d.guard, guard_short: d.guard_short },
94 };
95 (name, built)
96 })
97 .collect();
98 MatrixSpec { parents: toml.parents, level: toml.level.into(), actions }
99}
100
101fn build_handler_policy(toml: TomlHandlerPolicy) -> OwnedPolicy {
102 build_policy(
103 toml.standalone, toml.valued, toml.optional_valued, toml.bare, toml.max_positional, toml.tolerate_unknown_short,
104 toml.tolerate_unknown_long, toml.numeric_dash,
105 )
106}
107
108fn build_verb_chain(toml: TomlVerbChain) -> VerbChainSpec {
109 VerbChainSpec {
110 level: toml.level.unwrap_or(TomlLevel::Inert).into(),
111 separator: toml.separator.unwrap_or_else(|| "then".to_string()),
112 main_standalone: toml.main_standalone,
113 main_valued: toml.main_valued,
114 main_variadic: toml.main_variadic,
115 verbs: toml.verbs.into_iter().collect(),
116 }
117}
118
119fn build_fallback(parent: &str, toml: TomlFallback) -> Result<FallbackSpec, String> {
120 let policy = build_policy(
121 toml.standalone, toml.valued, toml.optional_valued, toml.bare, toml.max_positional, toml.tolerate_unknown_short,
122 toml.tolerate_unknown_long, toml.numeric_dash,
123 );
124 let level: SafetyLevel = toml.level.unwrap_or(TomlLevel::Inert).into();
125 let positional_shape = match toml.positional_shape.as_deref() {
126 None => None,
127 Some(name) => match crate::policy::PositionalShape::from_name(name) {
128 Some(shape) => Some(shape),
129 None => fail!("{parent}: unknown fallback positional_shape `{name}` (known: path)"),
130 },
131 };
132 let executor = match toml.executor.as_deref() {
133 None => None,
134 Some(name) => match ExecutorKind::from_name(name) {
135 Some(kind) => Some(kind),
136 None => fail!("{parent}: unknown fallback executor `{name}` (known: file, project)"),
137 },
138 };
139 Ok(FallbackSpec {
140 policy,
141 level,
142 positional_shape,
143 executor,
144 executor_redirect_flag: toml.executor_redirect_flag,
145 passes_argv: toml.passes_argv.unwrap_or(false),
146 })
147}
148
149fn allow_all_policy() -> OwnedPolicy {
150 OwnedPolicy {
151 standalone: Vec::new(),
152 valued: Vec::new(),
153 bare: true,
154 max_positional: None,
155 tolerance: FlagTolerance { unknown: UnknownTolerance::Both, numeric_dash: false },
156 }
157}
158
159fn check_no_legacy_positional_style(name: &str, ps: Option<bool>) -> Result<(), String> {
160 if ps.is_some() {
161 fail!(
162 "command '{name}': `positional_style` was removed. Use \
163 `tolerate_unknown_short = true` for tools with single-dash \
164 flags (pdftotext -help, sample -mayDie). Use \
165 `tolerate_unknown_long = true` ONLY for tools whose \
166 double-dash flag surface is genuinely unbounded (AWS CLI \
167 style); double-dash unknowns silently pass when this is \
168 on, which has caused safety bugs. Most tools need neither."
169 );
170 }
171 Ok(())
172}
173
174fn filter_candidates(subs: Vec<TomlSub>) -> Result<impl Iterator<Item = TomlSub>, String> {
175 for s in &subs {
184 ensure!(
185 !s.candidate.unwrap_or(false) || s.sub.is_empty(),
186 "sub `{}`: a `candidate` sub is filtered out, so its nested subs are dead — drop the \
187 nested subs, or drop `candidate` if the sub is meant to be live",
188 s.name,
189 );
190 }
191 for s in &subs {
204 if !s.per_database {
205 continue;
206 }
207 let clash = subs.iter().find(|o| o.name.starts_with(&format!("{}:", s.name)));
208 ensure!(
209 clash.is_none(),
210 "sub `{}`: `per_database` is unsound on a task that prefixes another (`{}`) — a \
211 suffix cannot be told from a subtask name. Enumerate the variants, or drop the flag",
212 s.name,
213 clash.map(|c| c.name.as_str()).unwrap_or(""),
214 );
215 }
216 Ok(subs.into_iter().filter(|s| !s.candidate.unwrap_or(false)))
217}
218
219fn first_arg_matches(name: &str, patterns: &[String]) -> bool {
221 patterns.iter().any(|p| match p.strip_suffix('*') {
222 Some(prefix) => name.starts_with(prefix),
223 None => p == name,
224 })
225}
226
227fn assert_no_candidate_shadowed_by_glob(parent: &str, subs: &[TomlSub], first_arg: &[String]) -> Result<(), String> {
234 if first_arg.is_empty() {
235 return Ok(());
236 }
237 for s in subs {
238 if s.candidate.unwrap_or(false) && first_arg_matches(&s.name, first_arg) {
239 fail!(
240 "'{parent}' sub `{}` is `candidate = true` but its name is MATCHED by the sibling \
241 first_arg glob {first_arg:?} — it would fall through the filter and AUTO-APPROVE \
242 (a silent deny→allow inversion). Use `profile`/an explicit sub-sub to deny it, or \
243 drop it from the glob.",
244 s.name,
245 );
246 }
247 }
248 Ok(())
249}
250
251pub(super) fn build_subs(
256 parent: &str,
257 toml: TomlSub,
258 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
259) -> Result<Vec<SubSpec>, String> {
260 let aliases = toml.aliases.clone();
261 let canonical = build_sub(parent, toml, handler_policies)?;
262 let mut out = Vec::with_capacity(1 + aliases.len());
263 for alias in aliases {
264 out.push(SubSpec {
265 name: alias,
266 kind: canonical.kind.clone(),
267 name_match: canonical.name_match,
268 policy_ref: canonical.policy_ref.clone(),
269 profile: canonical.profile.clone(),
270 flags: canonical.flags.clone(),
271 allowed_standalone: canonical.allowed_standalone.clone(),
274 allowed_valued: canonical.allowed_valued.clone(),
275 allowed_unknown: canonical.allowed_unknown,
276 eval_safe: canonical.eval_safe,
277 eval_safe_flags: canonical.eval_safe_flags.clone(),
278 eval_safe_flag_values: canonical.eval_safe_flag_values.clone(),
279 eval_safe_required_flags: canonical.eval_safe_required_flags.clone(),
280 output: canonical.output.clone(),
284 network_destination: canonical.network_destination,
285 destination_flag: canonical.destination_flag.clone(),
286 loopback_valued: canonical.loopback_valued.clone(),
287 loopback_effect: canonical.loopback_effect,
288 output_path_flags: canonical.output_path_flags.clone(),
289 writes_cwd: canonical.writes_cwd,
290 });
291 }
292 out.push(canonical);
293 Ok(out)
294}
295
296fn assert_sub_provenance(parent: &str, toml: &TomlSub) -> Result<(), String> {
303 let cited = |o: &Option<String>| o.as_deref().is_some_and(|s| !s.trim().is_empty());
304 let judged = |o: &Option<String>| o.as_deref().is_none_or(|s| !s.trim().is_empty());
306 if let Some(p) = &toml.profile {
307 ensure!(
308 crate::engine::archetype::archetype(p).is_some(),
309 "{parent} sub `{}`: profile `{p}` is not a known archetype (archetypes.toml)",
310 toml.name,
311 );
312 ensure!(cited(&toml.fact), "{parent} sub `{}`: `profile` requires a `fact`", toml.name);
313 ensure!(cited(&toml.source), "{parent} sub `{}`: `profile` requires a `source`", toml.name);
314 ensure!(judged(&toml.judgment), "{parent} sub `{}`: `judgment`, if given, must not be blank", toml.name);
315 ensure!(
321 !toml.standalone.is_empty()
322 || !toml.valued.is_empty()
323 || toml.tolerate_unknown_short == Some(true)
324 || toml.tolerate_unknown_long == Some(true)
325 || !toml.flag.is_empty(),
326 "{parent} sub `{}`: a profiled sub must declare its flag surface — list `standalone`/\
327 `valued`, or set `tolerate_unknown_long = true` if it is genuinely unbounded",
328 toml.name,
329 );
330 ensure!(toml.sub.is_empty(), "{parent} sub `{}`: a profiled sub must be a leaf (no nested subs)", toml.name);
333 }
334 ensure!(
337 toml.network_destination != Some(true) || toml.profile.is_some(),
338 "{parent} sub `{}`: `network_destination` requires a `profile`",
339 toml.name,
340 );
341 ensure!(
342 toml.destination_flag.is_none() || toml.network_destination == Some(true),
343 "{parent} sub `{}`: `destination_flag` requires `network_destination`",
344 toml.name,
345 );
346 ensure!(
349 toml.output_path_flags.is_empty() || toml.profile.is_some(),
350 "{parent} sub `{}`: `output_path_flags` requires a `profile`",
351 toml.name,
352 );
353 for f in &toml.flag {
354 ensure!(
355 f.classifies == "unclassified" || crate::engine::archetype::archetype(&f.classifies).is_some(),
356 "{parent} sub `{}` flag `{}`: classifies `{}` is not a known archetype",
357 toml.name,
358 f.name,
359 f.classifies,
360 );
361 ensure!(cited(&f.fact), "{parent} sub `{}` flag `{}`: requires a `fact`", toml.name, f.name);
362 ensure!(cited(&f.source), "{parent} sub `{}` flag `{}`: requires a `source`", toml.name, f.name);
363 ensure!(judged(&f.judgment), "{parent} sub `{}` flag `{}`: `judgment`, if given, must not be blank", toml.name, f.name);
364 ensure!(
365 !(f.when_absent == Some(true) && f.value_prefix.is_some()),
366 "{parent} sub `{}` flag `{}`: `when_absent` and `value_prefix` are mutually exclusive",
367 toml.name,
368 f.name,
369 );
370 }
371 Ok(())
372}
373
374fn assert_loopback_localizes_is_coherent(parent: &str, name: &str, toml: &TomlSub) -> Result<(), String> {
384 ensure!(
389 toml.loopback_valued.is_empty() || toml.profile.is_some(),
390 "{parent} sub `{name}`: `loopback_valued` needs `profile` — the gate is only consulted on \
391 a profiled sub, so here it would do nothing at all",
392 );
393 if !toml.loopback_localizes.unwrap_or(false) {
394 return Ok(());
395 }
396 ensure!(
397 !toml.loopback_valued.is_empty(),
398 "{parent} sub `{name}`: `loopback_localizes` requires a `loopback_valued` flag — without \
399 one nothing can establish that the destination is this machine",
400 );
401 let profile = toml.profile.as_deref().unwrap_or_default();
406 let operation = crate::engine::archetype::archetype(profile).map(|c| c.operation);
407 ensure!(
408 operation != Some(crate::engine::facet::Operation::Destroy),
409 "{parent} sub `{name}`: `profile = \"{profile}\"` carries `operation = destroy` and may \
410 not set `loopback_localizes` — a loopback endpoint cannot be verified (an SSH tunnel makes \
411 localhost mean production), and that is only unrecoverable for destroy",
412 );
413 Ok(())
414}
415
416pub(super) fn build_sub(
417 parent: &str,
418 mut toml: TomlSub,
419 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
420) -> Result<SubSpec, String> {
421 check_no_legacy_positional_style(&toml.name, toml.positional_style)?;
422 let name = toml.name.clone();
423 let policy_ref = toml.policy.clone();
424 let profile = toml.profile.clone();
425 assert_sub_provenance(parent, &toml)?;
426 assert_no_candidate_shadowed_by_glob(&format!("{parent} {}", toml.name), &toml.sub, &toml.first_arg)?;
427 let flags = std::mem::take(&mut toml.flag)
428 .into_iter()
429 .map(|f| crate::registry::types::FlagProvenance {
430 name: f.name,
431 classifies: f.classifies,
432 value_prefix: f.value_prefix,
433 when_absent: f.when_absent.unwrap_or(false),
434 })
435 .collect();
436 let (allowed_standalone, allowed_valued) = (toml.standalone.clone(), toml.valued.clone());
445 let allowed_unknown = match (toml.tolerate_unknown_short.unwrap_or(false), toml.tolerate_unknown_long.unwrap_or(false)) {
446 (false, false) => UnknownTolerance::Strict,
447 (true, false) => UnknownTolerance::Short,
448 (false, true) => UnknownTolerance::Long,
449 (true, true) => UnknownTolerance::Both,
450 };
451 if profile.is_some() {
452 toml.bare = Some(false);
453 toml.standalone = Vec::new();
454 toml.valued = Vec::new();
455 toml.max_positional = Some(0);
456 }
457 let eval_safe = toml.eval_safe.unwrap_or(false);
458 let eval_safe_flags = std::mem::take(&mut toml.eval_safe_flags);
459 let eval_safe_flag_values = std::mem::take(&mut toml.eval_safe_flag_values);
460 let eval_safe_required_flags = std::mem::take(&mut toml.eval_safe_required_flags);
461 let network_destination = toml.network_destination.unwrap_or(false);
462 let destination_flag = toml.destination_flag.clone();
463 let output_path_flags = toml.output_path_flags.clone();
464 let loopback_valued = toml.loopback_valued.clone();
465 let loopback_effect = if toml.loopback_localizes.unwrap_or(false) { LoopbackEffect::Localizes } else { LoopbackEffect::AdmitOnly };
466 assert_loopback_localizes_is_coherent(parent, &name, &toml)?;
467 let valued_for_check = toml.valued.clone();
468 assert_eval_safe_flags_require_tag(parent, &name, eval_safe, &eval_safe_flags)?;
469 assert_eval_safe_flag_values_consistent(parent, &name, &eval_safe_flags, &eval_safe_flag_values)?;
470 assert_eval_safe_valued_flags_declared(parent, &name, &eval_safe_flags, &valued_for_check, &eval_safe_flag_values)?;
471 assert_eval_safe_required_flags_consistent(parent, &name, &eval_safe_flags, &eval_safe_required_flags)?;
472 assert_sub_eval_safe_only_on_leaf(parent, &toml)?;
473 let writes_cwd = super::cwd_writes::lower(parent, &name, toml.writes_cwd.as_deref(), &toml.output_dirs, toml.executor.as_deref())?;
474 let output = lower_output(&format!("{parent} {name}"), toml.output.as_ref())?;
475 Ok(SubSpec {
476 name,
477 output,
478 writes_cwd,
479 name_match: if toml.per_database { NameMatch::WithDatabaseSuffix } else { NameMatch::Exact },
480 kind: build_sub_kind(parent, toml, handler_policies)?,
481 policy_ref,
482 profile,
483 flags,
484 allowed_standalone,
485 allowed_valued,
486 allowed_unknown,
487 eval_safe,
488 eval_safe_flags,
489 eval_safe_flag_values,
490 eval_safe_required_flags,
491 network_destination,
492 destination_flag,
493 loopback_valued,
494 loopback_effect,
495 output_path_flags,
496 })
497}
498
499fn assert_eval_safe_flag_values_consistent(
500 parent: &str,
501 name: &str,
502 eval_safe_flags: &[String],
503 eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
504) -> Result<(), String> {
505 for (flag, values) in eval_safe_flag_values {
506 if !eval_safe_flags.iter().any(|f| f == flag) {
507 fail!(
508 "command '{parent}' sub `{name}` lists `{flag}` in \
509 `eval_safe_flag_values` but not in `eval_safe_flags`. \
510 A value allowlist only takes effect when the flag itself \
511 is allowed. Add `{flag}` to `eval_safe_flags` or remove \
512 the value entry."
513 );
514 }
515 for value in values {
516 if value.is_empty() || !value.chars().all(is_bare_literal_char) {
517 fail!(
518 "command '{parent}' sub `{name}` has eval_safe_flag_values \
519 for `{flag}` containing value `{value:?}` with characters \
520 outside `[a-zA-Z0-9_./=-]`. The allowed-value set must \
521 itself be bare-literal so it can never embed a shell-\
522 expansion trigger into the substituted invocation."
523 );
524 }
525 }
526 }
527 Ok(())
528}
529
530fn is_bare_literal_char(c: char) -> bool {
531 c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | '=')
532}
533
534fn assert_eval_safe_required_flags_consistent(
535 parent: &str,
536 name: &str,
537 eval_safe_flags: &[String],
538 eval_safe_required_flags: &[String],
539) -> Result<(), String> {
540 for flag in eval_safe_required_flags {
541 if !eval_safe_flags.iter().any(|f| f == flag) {
542 fail!(
543 "command '{parent}' sub `{name}` lists `{flag}` in \
544 `eval_safe_required_flags` but not in `eval_safe_flags`. \
545 A required-flag constraint must be a subset of the \
546 allowed-flag set — otherwise the flag is required AND \
547 immediately denied. Add `{flag}` to `eval_safe_flags` or \
548 remove it from `eval_safe_required_flags`."
549 );
550 }
551 }
552 Ok(())
553}
554
555fn assert_eval_safe_valued_flags_declared(
563 parent: &str,
564 name: &str,
565 eval_safe_flags: &[String],
566 valued: &[String],
567 eval_safe_flag_values: &std::collections::HashMap<String, Vec<String>>,
568) -> Result<(), String> {
569 for flag in eval_safe_flags {
570 if !valued.iter().any(|v| v == flag) {
571 continue;
572 }
573 if !eval_safe_flag_values.contains_key(flag) {
574 fail!(
575 "command '{parent}' sub `{name}` lists `{flag}` in \
576 `eval_safe_flags` AND in `valued`, but `{flag}` has no \
577 entry in `eval_safe_flag_values`. Every valued flag \
578 tagged eval-safe must declare its value posture: \
579 either a concrete allowlist of safe values \
580 (`{flag} = [\"value-a\", \"value-b\"]`) or the \
581 explicit-unrestricted form (`{flag} = []`) signaling \
582 the contributor vetted that any value preserves shell-\
583 init output. Omitting an entry means the walker can't \
584 tell whether the value-following-flag is supposed to \
585 be checked, and a future short alias of `{flag}` \
586 could silently widen the eval-safe surface."
587 );
588 }
589 }
590 Ok(())
591}
592
593fn assert_eval_safe_flags_require_tag(parent: &str, name: &str, eval_safe: bool, flags: &[String]) -> Result<(), String> {
594 if !flags.is_empty() && !eval_safe {
595 fail!(
596 "command '{parent}' sub `{name}` declares `eval_safe_flags` without \
597 `eval_safe = true`. The flag allowlist only takes effect when the \
598 sub is tagged eval-safe. Add `eval_safe = true` or drop \
599 `eval_safe_flags`."
600 );
601 }
602 Ok(())
603}
604
605fn assert_sub_eval_safe_only_on_leaf(parent: &str, toml: &TomlSub) -> Result<(), String> {
606 if toml.eval_safe != Some(true) {
607 return Ok(());
608 }
609 if !toml.sub.is_empty() {
610 fail!(
611 "command '{parent}' sub `{}` sets `eval_safe = true` AND has \
612 nested [[command.sub.sub]] blocks. eval_safe must be tagged on \
613 a leaf node — move the tag onto the specific sub-sub that emits \
614 shell-init code. Otherwise the walker accepts any unmatched \
615 sub-sub name as a positional, which is counter-intuitive.",
616 toml.name,
617 );
618 }
619 if toml.handler.is_some() {
620 fail!(
621 "command '{parent}' sub `{}` sets `eval_safe = true` AND \
622 `handler = \"...\"`. Handler-based subs run Rust dispatch logic \
623 whose shape the eval walker cannot introspect — eval-safety \
624 requires a declarative leaf the registry can reason about.",
625 toml.name,
626 );
627 }
628 if toml.delegate_after.is_some() || toml.delegate_skip.is_some() {
629 fail!(
630 "command '{parent}' sub `{}` sets `eval_safe = true` AND \
631 delegates to an inner command (delegate_after / delegate_skip). \
632 The inner command's output is unrelated to this sub's vetting \
633 — drop `eval_safe`.",
634 toml.name,
635 );
636 }
637 Ok(())
638}
639
640fn assert_eval_safe_tagged_command_has_researched_version(toml: &TomlCommand) -> Result<(), String> {
641 let command_tagged = toml.eval_safe == Some(true);
642 let any_sub_tagged = toml_has_any_eval_safe_sub(&toml.sub);
643 if !command_tagged && !any_sub_tagged {
644 return Ok(());
645 }
646 if toml.researched_version.is_none() {
647 fail!(
648 "command '{}' has `eval_safe = true` (on the command or a sub) \
649 but no `researched_version`. eval-safe tags pin a per-tag trust \
650 claim against a specific upstream snapshot — add the version \
651 you researched (e.g. `researched_version = \"v2026.5.3\"`) so \
652 the next contributor knows what to diff against.",
653 toml.name,
654 );
655 }
656 Ok(())
657}
658
659fn toml_has_any_eval_safe_sub(subs: &[TomlSub]) -> bool {
660 subs.iter().any(|s| s.eval_safe == Some(true) || toml_has_any_eval_safe_sub(&s.sub))
661}
662
663fn assert_command_eval_safe_only_on_leaf(toml: &TomlCommand) -> Result<(), String> {
664 if toml.eval_safe != Some(true) {
665 return Ok(());
666 }
667 if !toml.sub.is_empty() {
668 fail!(
669 "command '{}' sets `eval_safe = true` at the command level AND \
670 has [[command.sub]] blocks. Move the tag onto the specific \
671 sub that emits shell-init code (e.g. `mise activate`) — \
672 command-level tagging on a structured command is counter-\
673 intuitive.",
674 toml.name,
675 );
676 }
677 if toml.wrapper.is_some() {
686 fail!(
687 "command '{}' sets `eval_safe = true` AND `[command.wrapper]`. \
688 Wrappers forward to an inner command — tagging the wrapper \
689 would tag every wrapped invocation. Drop `eval_safe`.",
690 toml.name,
691 );
692 }
693 if toml.deny.unwrap_or(false) {
694 fail!(
695 "command '{}' sets both `deny = true` and `eval_safe = true`. \
696 These are contradictory — deny silently dominates. Drop one.",
697 toml.name,
698 );
699 }
700 Ok(())
701}
702
703fn build_sub_kind(
704 parent: &str,
705 toml: TomlSub,
706 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
707) -> Result<DispatchKind, String> {
708 if let Some(handler_name) = toml.handler {
709 return Ok(DispatchKind::Custom {
710 handler_name,
711 doc_body: toml.doc_body,
712 subs: Vec::new(),
713 fallback: None,
714 handler_policies: std::collections::HashMap::new(),
715 matrices: Vec::new(),
716 });
717 }
718 if toml.allow_all.unwrap_or(false) {
719 return Ok(DispatchKind::Policy { policy: allow_all_policy(), level: toml.level.unwrap_or(TomlLevel::Inert).into() });
720 }
721 if let Some(sep) = toml.delegate_after {
722 return Ok(DispatchKind::DelegateAfterSeparator { separator: sep });
723 }
724 if let Some(skip) = toml.delegate_skip {
725 return Ok(DispatchKind::DelegateSkip { skip });
726 }
727 if !toml.sub.is_empty() || !toml.credential_first_arg.is_empty() {
731 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
738 return Ok(DispatchKind::Branching {
739 subs: filter_candidates(toml.sub)?
740 .map(|s| build_subs(parent, s, handler_policies))
741 .collect::<Result<Vec<_>, _>>()?
742 .into_iter()
743 .flatten()
744 .collect(),
745 bare_flags: Vec::new(),
746 bare_ok: toml.nested_bare.unwrap_or(false),
747 pre_standalone: toml.standalone,
748 pre_valued: toml.valued,
749 first_arg: toml.first_arg,
750 first_arg_level,
751 first_arg_standalone: toml.first_arg_standalone,
752 first_arg_valued: toml.first_arg_valued,
753 first_arg_loopback_valued: toml.first_arg_loopback_valued,
754 credential_first_arg: toml.credential_first_arg,
755 toolchain_selector: false,
757 });
758 }
759 build_policy_sub_kind(parent, toml, handler_policies)
760}
761
762fn build_policy_sub_kind(
763 parent: &str,
764 toml: TomlSub,
765 handler_policies: &std::collections::HashMap<String, OwnedPolicy>,
766) -> Result<DispatchKind, String> {
767 let policy = if let Some(key) = &toml.policy {
768 if !toml.standalone.is_empty() || !toml.valued.is_empty() {
769 fail!(
770 "command '{parent}' sub `{}` sets both `policy = \"{}\"` and \
771 inline standalone/valued — pick one. Either drop the inline \
772 lists (and rely on the referenced handler_policy) or drop \
773 the `policy` field.",
774 toml.name,
775 key,
776 );
777 }
778 match handler_policies.get(key).cloned() {
779 Some(p) => p,
780 None => fail!(
781 "command '{parent}' sub `{}` references handler_policy \
782 `{key}` which is not declared. Add a \
783 [command.handler_policy.{key}] block or fix the typo.",
784 toml.name,
785 ),
786 }
787 } else {
788 build_policy(
789 toml.standalone, toml.valued, toml.optional_valued, toml.bare, toml.max_positional, toml.tolerate_unknown_short,
790 toml.tolerate_unknown_long, toml.numeric_dash,
791 )
792 };
793 let level: SafetyLevel = toml.level.unwrap_or(TomlLevel::Inert).into();
794 if let Some(name) = toml.executor.as_deref() {
795 let Some(kind) = ExecutorKind::from_name(name) else {
796 fail!("command '{parent}' sub `{}`: unknown executor `{name}` (known: file, project)", toml.name)
797 };
798 let shape = match toml.positional_shape.as_deref() {
799 None => None,
800 Some(s) => match crate::policy::PositionalShape::from_name(s) {
801 Some(shape) => Some(shape),
802 None => fail!("command '{parent}' sub `{}`: unknown positional_shape `{s}`", toml.name),
803 },
804 };
805 return Ok(DispatchKind::Executor {
806 policy,
807 level,
808 kind,
809 redirect_flag: toml.executor_redirect_flag,
810 shape,
811 passes_argv: toml.passes_argv.unwrap_or(false),
812 });
813 }
814 if !toml.write_flags.is_empty() {
815 return Ok(DispatchKind::WriteFlagged { policy, base_level: level, write_flags: toml.write_flags });
816 }
817 if let Some(guard) = toml.guard {
818 let mut require_any = vec![guard];
819 if let Some(short) = toml.guard_short {
820 require_any.push(short);
821 }
822 return Ok(DispatchKind::RequireAny { require_any, policy, level, accept_bare_help: true });
823 }
824 if !toml.first_arg.is_empty() {
825 return Ok(DispatchKind::FirstArg {
826 patterns: toml.first_arg,
827 level,
828 standalone: toml.first_arg_standalone,
829 valued: toml.first_arg_valued,
830 loopback_valued: toml.first_arg_loopback_valued,
831 });
832 }
833 if !toml.require_any.is_empty() {
834 return Ok(DispatchKind::RequireAny { require_any: toml.require_any, policy, level, accept_bare_help: false });
835 }
836 Ok(DispatchKind::Policy { policy, level })
837}
838
839fn assert_flat_or_structured(toml: &TomlCommand) -> Result<(), String> {
847 if toml.sub.is_empty() {
848 return Ok(());
849 }
850 let mut conflicts = Vec::new();
851 if !toml.standalone.is_empty() {
852 conflicts.push("standalone");
853 }
854 if !toml.valued.is_empty() {
855 conflicts.push("valued");
856 }
857 if toml.max_positional.is_some() {
858 conflicts.push("max_positional");
859 }
860 if toml.tolerate_unknown_short.is_some() {
861 conflicts.push("tolerate_unknown_short");
862 }
863 if toml.tolerate_unknown_long.is_some() {
864 conflicts.push("tolerate_unknown_long");
865 }
866 if toml.numeric_dash.is_some() {
867 conflicts.push("numeric_dash");
868 }
869 if !conflicts.is_empty() {
870 fail!(
871 "command '{}' mixes flat-style top-level fields ({}) with [[command.sub]] blocks. \
872 When subs are present these fields are silently dropped. \
873 Either drop the subs (if the command is flat) or move global \
874 flags into a [command.wrapper] block.",
875 toml.name,
876 conflicts.join(", "),
877 );
878 }
879 Ok(())
880}
881
882fn assert_matrix_policy_keys_exist(toml: &TomlCommand) -> Result<(), String> {
883 if toml.matrix.is_empty() {
884 return Ok(());
885 }
886 for matrix in &toml.matrix {
887 for (action_name, action) in &matrix.actions {
888 let policy_key = match action {
889 TomlMatrixAction::Policy(k) => k,
890 TomlMatrixAction::Detailed(d) => &d.policy,
891 };
892 if !toml.handler_policy.contains_key(policy_key) {
893 fail!(
894 "command '{}' matrix action `{}` references \
895 handler_policy `{}` which is not declared. \
896 Add a [command.handler_policy.{}] block or fix the typo.",
897 toml.name,
898 action_name,
899 policy_key,
900 policy_key,
901 );
902 }
903 }
904 }
905 Ok(())
906}
907
908fn assert_matrix_no_duplicate_parent_action(toml: &TomlCommand) -> Result<(), String> {
909 if toml.matrix.len() < 2 {
910 return Ok(());
911 }
912 let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
913 for matrix in &toml.matrix {
914 for parent in &matrix.parents {
915 for action in matrix.actions.keys() {
916 let key = (parent.clone(), action.clone());
917 if !seen.insert(key) {
918 fail!(
919 "command '{}' matrix has duplicate (parent, action) pair \
920 (`{}`, `{}`). The first match would silently win — \
921 consolidate into one matrix block or remove the duplicate.",
922 toml.name,
923 parent,
924 action,
925 );
926 }
927 }
928 }
929 }
930 Ok(())
931}
932
933fn assert_fallback_requires_handler(toml: &TomlCommand) -> Result<(), String> {
934 if toml.fallback.is_some() && toml.handler.is_none() {
935 fail!(
936 "command '{}' declares [command.fallback] without a handler. \
937 Fallback grammars are only consulted via \
938 registry::try_fallback_grammar() from a Rust handler — without \
939 handler = \"...\" the block is silently dropped. \
940 Either set handler or remove [command.fallback].",
941 toml.name,
942 );
943 }
944 Ok(())
945}
946
947fn build_command_archetype_flags(cmd: &str, flags: Vec<TomlSubFlag>) -> Result<Vec<crate::registry::types::FlagProvenance>, String> {
952 let cited = |o: &Option<String>| o.as_deref().is_some_and(|s| !s.trim().is_empty());
953 let judged = |o: &Option<String>| o.as_deref().is_none_or(|s| !s.trim().is_empty());
954 flags
955 .into_iter()
956 .map(|f| {
957 ensure!(
958 f.classifies == "unclassified" || crate::engine::archetype::archetype(&f.classifies).is_some(),
959 "command `{cmd}` flag `{}`: classifies `{}` is not a known archetype",
960 f.name,
961 f.classifies,
962 );
963 ensure!(cited(&f.fact), "command `{cmd}` flag `{}`: requires a `fact`", f.name);
964 ensure!(cited(&f.source), "command `{cmd}` flag `{}`: requires a `source`", f.name);
965 ensure!(judged(&f.judgment), "command `{cmd}` flag `{}`: `judgment`, if given, must not be blank", f.name);
966 ensure!(
967 !(f.when_absent == Some(true) && f.value_prefix.is_some()),
968 "command `{cmd}` flag `{}`: `when_absent` and `value_prefix` are mutually exclusive",
969 f.name,
970 );
971 Ok(crate::registry::types::FlagProvenance {
972 name: f.name,
973 classifies: f.classifies,
974 value_prefix: f.value_prefix,
975 when_absent: f.when_absent.unwrap_or(false),
976 })
977 })
978 .collect()
979}
980
981#[allow(clippy::too_many_lines)]
982pub(super) fn build_command(toml: TomlCommand, category: &str) -> Result<CommandSpec, String> {
983 assert_flat_or_structured(&toml)?;
984 assert_fallback_requires_handler(&toml)?;
985 assert_matrix_policy_keys_exist(&toml)?;
986 assert_no_candidate_shadowed_by_glob(&toml.name, &toml.sub, &toml.first_arg)?;
987 assert_matrix_no_duplicate_parent_action(&toml)?;
988 assert_command_eval_safe_only_on_leaf(&toml)?;
989 assert_eval_safe_tagged_command_has_researched_version(&toml)?;
990 check_no_legacy_positional_style(&toml.name, toml.positional_style)?;
991 let cat = category.to_string();
992 let desc = toml.description.unwrap_or_default();
993 let researched_version = toml.researched_version;
994 let examples_safe = toml.examples_safe;
995 let examples_denied = toml.examples_denied;
996 let eval_safe = toml.eval_safe.unwrap_or(false);
997 let eval_safe_flags = toml.eval_safe_flags;
998 let eval_safe_flag_values = toml.eval_safe_flag_values;
999 let eval_safe_required_flags = toml.eval_safe_required_flags;
1000 if !eval_safe_flags.is_empty() && !eval_safe {
1001 fail!(
1002 "command '{}' declares `eval_safe_flags` without `eval_safe = true`. \
1003 The flag allowlist only takes effect when the command is tagged \
1004 eval-safe. Add `eval_safe = true` or drop `eval_safe_flags`.",
1005 toml.name,
1006 );
1007 }
1008 assert_eval_safe_flag_values_consistent(&toml.name, "<command>", &eval_safe_flags, &eval_safe_flag_values)?;
1009 assert_eval_safe_valued_flags_declared(&toml.name, "<command>", &eval_safe_flags, &toml.valued, &eval_safe_flag_values)?;
1010 assert_eval_safe_required_flags_consistent(&toml.name, "<command>", &eval_safe_flags, &eval_safe_required_flags)?;
1011 let behavior = lower_behavior(&toml.name, toml.behavior.as_ref())?;
1012 let output = lower_output(&toml.name, toml.output.as_ref())?;
1013 let writes_cwd = super::cwd_writes::lower(&toml.name, "<command>", toml.writes_cwd.as_deref(), &toml.output_dirs, None)?;
1014 let env_assignment_positionals = toml.env_assignment_positionals.unwrap_or(false);
1015 let archetype_flags = build_command_archetype_flags(&toml.name, toml.flag)?;
1016 if toml.deny.unwrap_or(false) {
1017 return Ok(CommandSpec {
1018 name: toml.name,
1019 description: desc,
1020 aliases: toml.aliases,
1021 url: toml.url,
1022 category: cat,
1023 researched_version,
1024 examples_safe,
1025 examples_denied,
1026 eval_safe,
1027 eval_safe_flags: eval_safe_flags.clone(),
1028 eval_safe_flag_values: eval_safe_flag_values.clone(),
1029 eval_safe_required_flags: eval_safe_required_flags.clone(),
1030 path_gate: toml.path_gate,
1031 archetype_flags: archetype_flags.clone(),
1032 behavior: behavior.clone(),
1033 output,
1034 writes_cwd,
1035 env_assignment_positionals,
1036 kind: DispatchKind::Policy {
1037 policy: OwnedPolicy {
1038 standalone: Vec::new(),
1039 valued: Vec::new(),
1040 bare: false,
1041 max_positional: Some(0),
1042 tolerance: FlagTolerance::default(),
1043 },
1044 level: SafetyLevel::Inert,
1045 },
1046 });
1047 }
1048 if let Some(vc) = toml.verb_chain {
1049 return Ok(CommandSpec {
1050 name: toml.name,
1051 description: desc,
1052 aliases: toml.aliases,
1053 url: toml.url,
1054 category: cat,
1055 researched_version,
1056 examples_safe,
1057 examples_denied,
1058 eval_safe,
1059 eval_safe_flags: eval_safe_flags.clone(),
1060 eval_safe_flag_values: eval_safe_flag_values.clone(),
1061 eval_safe_required_flags: eval_safe_required_flags.clone(),
1062 path_gate: toml.path_gate,
1063 archetype_flags: archetype_flags.clone(),
1064 behavior: behavior.clone(),
1065 output,
1066 writes_cwd,
1067 env_assignment_positionals,
1068 kind: DispatchKind::VerbChain(build_verb_chain(vc)),
1069 });
1070 }
1071
1072 if let Some(handler_name) = toml.handler {
1073 let handler_policies: std::collections::HashMap<String, OwnedPolicy> =
1076 toml.handler_policy.into_iter().map(|(k, v)| (k, build_handler_policy(v))).collect();
1077 let parent_name = toml.name.clone();
1078 let subs: Vec<SubSpec> = filter_candidates(toml.sub)?
1079 .map(|s| build_subs(&parent_name, s, &handler_policies))
1080 .collect::<Result<Vec<_>, _>>()?
1081 .into_iter()
1082 .flatten()
1083 .collect();
1084 let fallback = toml.fallback.map(|f| build_fallback(&toml.name, f)).transpose()?;
1085 let matrices = toml.matrix.into_iter().map(build_matrix).collect();
1086 return Ok(CommandSpec {
1087 name: toml.name,
1088 description: desc,
1089 aliases: toml.aliases,
1090 url: toml.url,
1091 category: cat,
1092 researched_version,
1093 examples_safe,
1094 examples_denied,
1095 eval_safe,
1096 eval_safe_flags: eval_safe_flags.clone(),
1097 eval_safe_flag_values: eval_safe_flag_values.clone(),
1098 eval_safe_required_flags: eval_safe_required_flags.clone(),
1099 path_gate: toml.path_gate,
1100 archetype_flags: archetype_flags.clone(),
1101 behavior: behavior.clone(),
1102 output,
1103 writes_cwd,
1104 env_assignment_positionals,
1105 kind: DispatchKind::Custom { handler_name, doc_body: toml.doc_body, subs, fallback, handler_policies, matrices },
1106 });
1107 }
1108
1109 if let Some(w) = toml.wrapper {
1110 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
1111 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
1112 let parent_name = toml.name.clone();
1113 return Ok(CommandSpec {
1114 name: toml.name,
1115 description: desc,
1116 aliases: toml.aliases,
1117 url: toml.url,
1118 category: cat,
1119 researched_version,
1120 examples_safe,
1121 examples_denied,
1122 eval_safe,
1123 eval_safe_flags: eval_safe_flags.clone(),
1124 eval_safe_flag_values: eval_safe_flag_values.clone(),
1125 eval_safe_required_flags: eval_safe_required_flags.clone(),
1126 path_gate: toml.path_gate,
1127 archetype_flags: archetype_flags.clone(),
1128 behavior: behavior.clone(),
1129 output,
1130 writes_cwd,
1131 env_assignment_positionals,
1132 kind: DispatchKind::Branching {
1133 bare_flags: toml.bare_flags,
1134 subs: filter_candidates(toml.sub)?
1135 .map(|s| build_subs(&parent_name, s, &std::collections::HashMap::new()))
1136 .collect::<Result<Vec<_>, _>>()?
1137 .into_iter()
1138 .flatten()
1139 .collect(),
1140 pre_standalone: w.standalone,
1141 pre_valued: w.valued,
1142 bare_ok: toml.bare.unwrap_or(false),
1143 first_arg: toml.first_arg,
1144 first_arg_standalone: toml.first_arg_standalone,
1145 first_arg_valued: toml.first_arg_valued,
1146 first_arg_loopback_valued: toml.first_arg_loopback_valued,
1147 first_arg_level,
1148 credential_first_arg: toml.credential_first_arg,
1149 toolchain_selector: w.toolchain_selector.unwrap_or(false),
1150 },
1151 });
1152 }
1153 return Ok(CommandSpec {
1154 name: toml.name,
1155 description: desc,
1156 aliases: toml.aliases,
1157 url: toml.url,
1158 category: cat,
1159 researched_version,
1160 examples_safe,
1161 examples_denied,
1162 eval_safe,
1163 eval_safe_flags: eval_safe_flags.clone(),
1164 eval_safe_flag_values: eval_safe_flag_values.clone(),
1165 eval_safe_required_flags: eval_safe_required_flags.clone(),
1166 path_gate: toml.path_gate,
1167 archetype_flags: archetype_flags.clone(),
1168 behavior: behavior.clone(),
1169 output,
1170 writes_cwd,
1171 env_assignment_positionals,
1172 kind: DispatchKind::Wrapper {
1173 standalone: w.standalone,
1174 valued: w.valued,
1175 positional_skip: w.positional_skip.unwrap_or(0),
1176 separator: w.separator,
1177 bare_ok: w.bare_ok.unwrap_or(false),
1178 },
1179 });
1180 }
1181
1182 if !toml.sub.is_empty() || !toml.bare_flags.is_empty() {
1183 let first_arg_level = toml.level.unwrap_or(TomlLevel::Inert).into();
1184 let parent_name = toml.name.clone();
1185 return Ok(CommandSpec {
1186 name: toml.name,
1187 description: desc,
1188 aliases: toml.aliases,
1189 url: toml.url,
1190 category: cat,
1191 researched_version,
1192 examples_safe,
1193 examples_denied,
1194 eval_safe,
1195 eval_safe_flags: eval_safe_flags.clone(),
1196 eval_safe_flag_values: eval_safe_flag_values.clone(),
1197 eval_safe_required_flags: eval_safe_required_flags.clone(),
1198 path_gate: toml.path_gate,
1199 archetype_flags: archetype_flags.clone(),
1200 behavior: behavior.clone(),
1201 output,
1202 writes_cwd,
1203 env_assignment_positionals,
1204 kind: DispatchKind::Branching {
1205 bare_flags: toml.bare_flags,
1206 subs: filter_candidates(toml.sub)?
1207 .map(|s| build_subs(&parent_name, s, &std::collections::HashMap::new()))
1208 .collect::<Result<Vec<_>, _>>()?
1209 .into_iter()
1210 .flatten()
1211 .collect(),
1212 pre_standalone: Vec::new(),
1213 pre_valued: Vec::new(),
1214 bare_ok: toml.bare.unwrap_or(false),
1215 first_arg: toml.first_arg,
1216 first_arg_level,
1217 first_arg_standalone: toml.first_arg_standalone,
1218 first_arg_valued: toml.first_arg_valued,
1219 first_arg_loopback_valued: toml.first_arg_loopback_valued,
1220 credential_first_arg: toml.credential_first_arg,
1221 toolchain_selector: false,
1223 },
1224 });
1225 }
1226
1227 let policy = build_policy(
1228 toml.standalone, toml.valued, toml.optional_valued, toml.bare, toml.max_positional, toml.tolerate_unknown_short,
1229 toml.tolerate_unknown_long, toml.numeric_dash,
1230 );
1231
1232 let level = toml.level.unwrap_or(TomlLevel::Inert).into();
1233
1234 if !toml.first_arg.is_empty() {
1235 return Ok(CommandSpec {
1236 name: toml.name,
1237 description: desc,
1238 aliases: toml.aliases,
1239 url: toml.url,
1240 category: cat,
1241 researched_version,
1242 examples_safe,
1243 examples_denied,
1244 eval_safe,
1245 eval_safe_flags: eval_safe_flags.clone(),
1246 eval_safe_flag_values: eval_safe_flag_values.clone(),
1247 eval_safe_required_flags: eval_safe_required_flags.clone(),
1248 path_gate: toml.path_gate,
1249 archetype_flags: archetype_flags.clone(),
1250 behavior: behavior.clone(),
1251 output,
1252 writes_cwd,
1253 env_assignment_positionals,
1254 kind: DispatchKind::FirstArg {
1255 patterns: toml.first_arg,
1256 level,
1257 standalone: toml.first_arg_standalone,
1258 valued: toml.first_arg_valued,
1259 loopback_valued: toml.first_arg_loopback_valued,
1260 },
1261 });
1262 }
1263
1264 if !toml.write_flags.is_empty() {
1265 return Ok(CommandSpec {
1266 name: toml.name,
1267 description: desc,
1268 aliases: toml.aliases,
1269 url: toml.url,
1270 category: cat,
1271 researched_version,
1272 examples_safe,
1273 examples_denied,
1274 eval_safe,
1275 eval_safe_flags: eval_safe_flags.clone(),
1276 eval_safe_flag_values: eval_safe_flag_values.clone(),
1277 eval_safe_required_flags: eval_safe_required_flags.clone(),
1278 path_gate: toml.path_gate,
1279 archetype_flags: archetype_flags.clone(),
1280 behavior: behavior.clone(),
1281 output,
1282 writes_cwd,
1283 env_assignment_positionals,
1284 kind: DispatchKind::WriteFlagged { policy, base_level: level, write_flags: toml.write_flags },
1285 });
1286 }
1287
1288 if !toml.require_any.is_empty() {
1289 return Ok(CommandSpec {
1290 name: toml.name,
1291 description: desc,
1292 aliases: toml.aliases,
1293 url: toml.url,
1294 category: cat,
1295 researched_version,
1296 examples_safe,
1297 examples_denied,
1298 eval_safe,
1299 eval_safe_flags: eval_safe_flags.clone(),
1300 eval_safe_flag_values: eval_safe_flag_values.clone(),
1301 eval_safe_required_flags: eval_safe_required_flags.clone(),
1302 path_gate: toml.path_gate,
1303 archetype_flags: archetype_flags.clone(),
1304 behavior: behavior.clone(),
1305 output,
1306 writes_cwd,
1307 env_assignment_positionals,
1308 kind: DispatchKind::RequireAny { require_any: toml.require_any, policy, level, accept_bare_help: false },
1309 });
1310 }
1311
1312 Ok(CommandSpec {
1313 env_assignment_positionals,
1314 name: toml.name,
1315 description: desc,
1316 aliases: toml.aliases,
1317 url: toml.url,
1318 category: cat,
1319 researched_version,
1320 examples_safe,
1321 examples_denied,
1322 eval_safe,
1323 eval_safe_flags,
1324 eval_safe_flag_values,
1325 eval_safe_required_flags,
1326 path_gate: toml.path_gate,
1327 archetype_flags,
1328 behavior,
1329 output,
1330 writes_cwd,
1331 kind: DispatchKind::Policy { policy, level },
1332 })
1333}
1334
1335pub fn load_toml(source: &str, category: &str) -> Result<Vec<CommandSpec>, String> {
1341 let file: TomlFile = match toml::from_str(source) {
1342 Ok(f) => f,
1343 Err(e) => {
1344 let preview: String = source.chars().take(80).collect();
1345 fail!("invalid TOML command definition: {e}\n source begins: {preview}");
1346 }
1347 };
1348 file.command
1349 .into_iter()
1350 .filter(|cmd| !cmd.candidate.unwrap_or(false))
1351 .map(|cmd| build_command(cmd, category))
1352 .collect()
1353}
1354
1355pub fn build_registry(specs: Vec<CommandSpec>) -> HashMap<String, CommandSpec> {
1356 let mut map = HashMap::new();
1357 for spec in specs {
1358 insert_spec(&mut map, spec);
1359 }
1360 map
1361}
1362
1363pub fn insert_spec(map: &mut HashMap<String, CommandSpec>, spec: CommandSpec) {
1369 map.retain(|_, s| s.name != spec.name);
1370 for alias in &spec.aliases {
1371 map.insert(
1372 alias.clone(),
1373 CommandSpec {
1374 env_assignment_positionals: spec.env_assignment_positionals,
1378 name: spec.name.clone(),
1379 description: spec.description.clone(),
1380 aliases: vec![],
1381 url: spec.url.clone(),
1382 category: spec.category.clone(),
1383 researched_version: spec.researched_version.clone(),
1384 examples_safe: vec![],
1385 examples_denied: vec![],
1386 eval_safe: spec.eval_safe,
1387 eval_safe_flags: spec.eval_safe_flags.clone(),
1388 eval_safe_flag_values: spec.eval_safe_flag_values.clone(),
1389 eval_safe_required_flags: spec.eval_safe_required_flags.clone(),
1390 path_gate: None,
1394 archetype_flags: Vec::new(),
1395 behavior: None,
1396 output: None,
1397 writes_cwd: None,
1398 kind: spec.kind.clone(),
1399 },
1400 );
1401 }
1402 map.insert(spec.name.clone(), spec);
1403}