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