1use super::model::{Effect, Interactivity, Requirement, Secrecy, ValueSource};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Semantics {
30 pub path: &'static [&'static str],
33 pub effect: Effect,
35 pub interactivity: Interactivity,
37 pub requires: &'static [Requirement],
39}
40
41impl Semantics {
42 pub const DEFAULT: Semantics = Semantics {
44 path: &[],
45 effect: Effect::ReadOnly,
46 interactivity: Interactivity::NonInteractive,
47 requires: &[Requirement::Repository, Requirement::Layer],
48 };
49}
50
51pub const SEMANTICS: &[Semantics] = &[
56 Semantics {
58 path: &["mcp"],
59 effect: Effect::ReadOnly,
60 interactivity: Interactivity::LongRunning,
61 requires: &[Requirement::Repository, Requirement::Layer],
62 },
63 Semantics {
64 path: &["serve"],
65 effect: Effect::ReadOnly,
66 interactivity: Interactivity::LongRunning,
67 requires: &[Requirement::Repository, Requirement::Layer],
68 },
69 Semantics {
71 path: &["generate"],
72 effect: Effect::RepositoryMutation,
73 interactivity: Interactivity::NonInteractive,
74 requires: &[Requirement::Repository, Requirement::Layer],
75 },
76 Semantics {
78 path: &["bench"],
79 effect: Effect::RepositoryMutation,
80 interactivity: Interactivity::NonInteractive,
81 requires: &[Requirement::Repository, Requirement::Layer],
82 },
83 Semantics {
84 path: &["bench", "coverage"],
85 effect: Effect::ReadOnly,
86 interactivity: Interactivity::NonInteractive,
87 requires: &[Requirement::Repository, Requirement::Layer],
88 },
89 Semantics {
90 path: &["bench", "baseline", "update"],
91 effect: Effect::RepositoryMutation,
92 interactivity: Interactivity::NonInteractive,
93 requires: &[Requirement::Repository, Requirement::Layer],
94 },
95 Semantics {
99 path: &["completion", "install"],
100 effect: Effect::LocalMutation,
101 interactivity: Interactivity::NonInteractive,
102 requires: &[],
103 },
104 Semantics {
109 path: &["release", "bump"],
110 effect: Effect::RepositoryMutation,
111 interactivity: Interactivity::NonInteractive,
112 requires: &[Requirement::Repository],
113 },
114 Semantics {
116 path: &["distribution", "build"],
117 effect: Effect::LocalMutation,
118 interactivity: Interactivity::NonInteractive,
119 requires: &[Requirement::Repository, Requirement::RustToolchain],
120 },
121 Semantics {
123 path: &["env"],
124 effect: Effect::ReadOnly,
125 interactivity: Interactivity::NonInteractive,
126 requires: &[Requirement::Repository],
127 },
128 Semantics {
130 path: &["worktree"],
131 effect: Effect::ReadOnly,
132 interactivity: Interactivity::NonInteractive,
133 requires: &[Requirement::Repository],
134 },
135 Semantics {
136 path: &["worktree", "create"],
137 effect: Effect::RepositoryMutation,
138 interactivity: Interactivity::NonInteractive,
139 requires: &[Requirement::Repository],
140 },
141 Semantics {
142 path: &["worktree", "ensure"],
143 effect: Effect::RepositoryMutation,
144 interactivity: Interactivity::NonInteractive,
145 requires: &[Requirement::Repository],
146 },
147 Semantics {
148 path: &["worktree", "migrate"],
149 effect: Effect::RepositoryMutation,
150 interactivity: Interactivity::NonInteractive,
151 requires: &[Requirement::Repository],
152 },
153 Semantics {
154 path: &["worktree", "repair"],
155 effect: Effect::RepositoryMutation,
156 interactivity: Interactivity::NonInteractive,
157 requires: &[Requirement::Repository],
158 },
159 Semantics {
160 path: &["worktree", "remove"],
161 effect: Effect::Destructive,
162 interactivity: Interactivity::NonInteractive,
163 requires: &[Requirement::Repository],
164 },
165];
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub struct ValueBinding {
175 pub path: &'static [&'static str],
177 pub argument: &'static str,
179 pub source: ValueSource,
181}
182
183pub const VALUE_BINDINGS: &[ValueBinding] = &[
185 ValueBinding {
186 path: &["capabilities", "describe"],
187 argument: "id",
188 source: ValueSource::Capability,
189 },
190 ValueBinding {
191 path: &["capabilities", "schema"],
192 argument: "id",
193 source: ValueSource::Capability,
194 },
195 ValueBinding {
196 path: &["why", "show"],
197 argument: "id",
198 source: ValueSource::Moment,
199 },
200 ValueBinding {
201 path: &["worktree", "path"],
202 argument: "branch",
203 source: ValueSource::Branch,
204 },
205 ValueBinding {
206 path: &["worktree", "inspect"],
207 argument: "branch",
208 source: ValueSource::Branch,
209 },
210 ValueBinding {
211 path: &["worktree", "create"],
212 argument: "branch",
213 source: ValueSource::Branch,
214 },
215 ValueBinding {
216 path: &["worktree", "ensure"],
217 argument: "branch",
218 source: ValueSource::Branch,
219 },
220 ValueBinding {
221 path: &["worktree", "remove"],
222 argument: "branch",
223 source: ValueSource::Branch,
224 },
225 ValueBinding {
226 path: &["commands", "show"],
227 argument: "id",
228 source: ValueSource::Command,
229 },
230 ValueBinding {
231 path: &["commands", "explain"],
232 argument: "id",
233 source: ValueSource::Command,
234 },
235 ValueBinding {
236 path: &["web", "explain"],
237 argument: "id",
238 source: ValueSource::Graph,
239 },
240];
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub struct WorkflowAlias {
252 pub alias: &'static str,
254 pub command: &'static str,
256 pub arguments: &'static [&'static str],
258 pub reason: &'static str,
260}
261
262pub const WORKFLOW_ALIASES: &[WorkflowAlias] = &[
268 WorkflowAlias {
269 alias: "capabilities",
270 command: "executable.capabilities.list",
271 arguments: &[],
272 reason: "the recipe before the bridge derived `capabilities-list` from the command",
273 },
274 WorkflowAlias {
275 alias: "describe",
276 command: "executable.capabilities.describe",
277 arguments: &[],
278 reason: "the recipe before the bridge derived `capabilities-describe`",
279 },
280 WorkflowAlias {
281 alias: "validate",
282 command: "executable.capabilities.validate",
283 arguments: &[],
284 reason: "the recipe before the bridge derived `capabilities-validate`",
285 },
286 WorkflowAlias {
287 alias: "generate-check",
288 command: "executable.generate",
289 arguments: &["--check"],
290 reason: "the recipe that spelled `generate --check`",
291 },
292 WorkflowAlias {
293 alias: "inspect",
294 command: "executable.mcp",
295 arguments: &["--inspect"],
296 reason: "the recipe that spelled `mcp --inspect`",
297 },
298 WorkflowAlias {
299 alias: "bench-run",
300 command: "executable.bench",
301 arguments: &[],
302 reason: "the recipe before `bench` was qualified by the program that owns it",
303 },
304 WorkflowAlias {
305 alias: "bench-check",
306 command: "executable.bench",
307 arguments: &["--profile", "ci", "--check", "--no-write"],
308 reason: "the recipe that spelled the continuous-integration benchmark run",
309 },
310 WorkflowAlias {
311 alias: "bench-baseline",
312 command: "executable.bench.baseline.update",
313 arguments: &[],
314 reason: "the recipe before the bridge derived `bench-baseline-update`",
315 },
316 WorkflowAlias {
317 alias: "wt",
318 command: "executable.worktree",
319 arguments: &[],
320 reason: "the command line's own alias for `worktree`, kept for the runner too",
321 },
322 WorkflowAlias {
323 alias: "wt-create",
324 command: "executable.worktree.ensure",
325 arguments: &[],
326 reason: "the recipe that created a worktree if it was absent and answered it if not",
327 },
328 WorkflowAlias {
329 alias: "wt-migrate",
330 command: "executable.worktree.migrate",
331 arguments: &[],
332 reason: "the recipe before the bridge derived `worktree-migrate`",
333 },
334 WorkflowAlias {
335 alias: "wt-doctor",
336 command: "executable.worktree.doctor",
337 arguments: &[],
338 reason: "the recipe before the bridge derived `worktree-doctor`",
339 },
340 WorkflowAlias {
341 alias: "usecase-list",
342 command: "tool.usecase",
343 arguments: &["list"],
344 reason: "the recipe that spelled one subcommand of the shell tool's `usecase`",
345 },
346 WorkflowAlias {
347 alias: "usecase-run",
348 command: "tool.usecase",
349 arguments: &["run"],
350 reason: "the recipe that spelled one subcommand of the shell tool's `usecase`",
351 },
352 WorkflowAlias {
353 alias: "usecase-coverage",
354 command: "tool.usecase",
355 arguments: &["coverage", "--check"],
356 reason: "the recipe that spelled the use-case coverage gate",
357 },
358 WorkflowAlias {
359 alias: "usecase-impact",
360 command: "tool.usecase",
361 arguments: &["impact"],
362 reason: "the recipe that spelled one subcommand of the shell tool's `usecase`",
363 },
364 WorkflowAlias {
365 alias: "mcp-status",
366 command: "executable.serve.status",
367 arguments: &[],
368 reason:
369 "the recipe that read this checkout's lease before the bridge derived `serve-status`",
370 },
371];
372
373pub fn unused_aliases(existing: &[String]) -> Vec<&'static WorkflowAlias> {
375 WORKFLOW_ALIASES
376 .iter()
377 .filter(|a| !existing.iter().any(|id| id == a.command))
378 .collect()
379}
380
381pub fn of(path: &[String]) -> Semantics {
387 let mut best: Option<&Semantics> = None;
388 for entry in SEMANTICS {
389 if entry.path.len() <= path.len()
390 && entry
391 .path
392 .iter()
393 .zip(path.iter())
394 .all(|(a, b)| *a == b.as_str())
395 && best.is_none_or(|b| b.path.len() < entry.path.len())
396 {
397 best = Some(entry);
398 }
399 }
400 best.copied().unwrap_or(Semantics::DEFAULT)
401}
402
403pub fn binding(path: &[String], argument: &str) -> Option<ValueSource> {
405 VALUE_BINDINGS
406 .iter()
407 .find(|b| {
408 b.argument == argument
409 && b.path.len() == path.len()
410 && b.path
411 .iter()
412 .zip(path.iter())
413 .all(|(a, c)| *a == c.as_str())
414 })
415 .map(|b| b.source)
416}
417
418pub fn secrecy(argument: &str, value_name: Option<&str>) -> Secrecy {
423 let hay = format!(
424 "{argument} {}",
425 value_name.unwrap_or_default().to_lowercase()
426 );
427 const SECRET: &[&str] = &["token", "secret", "password", "passwd", "api-key", "apikey"];
428 const SENSITIVE: &[&str] = &["credential", "auth", "key"];
429 if SECRET.iter().any(|w| hay.contains(w)) {
430 Secrecy::Secret
431 } else if SENSITIVE.iter().any(|w| hay.contains(w)) {
432 Secrecy::Sensitive
433 } else {
434 Secrecy::Public
435 }
436}
437
438pub fn unused(existing: &[Vec<String>]) -> Vec<String> {
445 let mut out = Vec::new();
446 for entry in SEMANTICS {
447 if !existing
448 .iter()
449 .any(|p| p.len() == entry.path.len() && p.iter().zip(entry.path).all(|(a, b)| a == b))
450 {
451 out.push(entry.path.join(" "));
452 }
453 }
454 for binding in VALUE_BINDINGS {
455 if !existing.iter().any(|p| {
456 p.len() == binding.path.len() && p.iter().zip(binding.path).all(|(a, b)| a == b)
457 }) {
458 out.push(format!("{} <{}>", binding.path.join(" "), binding.argument));
459 }
460 }
461 out
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 fn p(words: &[&str]) -> Vec<String> {
469 words.iter().map(|w| w.to_string()).collect()
470 }
471
472 #[test]
473 fn the_default_answers_an_unnamed_command() {
474 let s = of(&p(&["scope"]));
475 assert_eq!(s.effect, Effect::ReadOnly);
476 assert_eq!(s.interactivity, Interactivity::NonInteractive);
477 }
478
479 #[test]
480 fn the_most_specific_entry_wins() {
481 assert_eq!(of(&p(&["bench"])).effect, Effect::RepositoryMutation);
482 assert_eq!(of(&p(&["bench", "coverage"])).effect, Effect::ReadOnly);
483 assert_eq!(
484 of(&p(&["bench", "baseline", "update"])).effect,
485 Effect::RepositoryMutation
486 );
487 }
488
489 #[test]
490 fn semantics_are_inherited_down_the_tree() {
491 let s = of(&p(&["worktree", "list"]));
493 assert_eq!(s.requires, &[Requirement::Repository]);
494 assert_eq!(s.effect, Effect::ReadOnly);
495 assert_eq!(of(&p(&["worktree", "remove"])).effect, Effect::Destructive);
496 }
497
498 #[test]
499 fn a_binding_is_exact_not_inherited() {
500 assert_eq!(
501 binding(&p(&["worktree", "path"]), "branch"),
502 Some(ValueSource::Branch)
503 );
504 assert_eq!(binding(&p(&["worktree"]), "branch"), None);
505 }
506
507 #[test]
508 fn secrets_are_recognised_by_name() {
509 assert_eq!(secrecy("token", None), Secrecy::Secret);
510 assert_eq!(secrecy("api_key", Some("API-KEY")), Secrecy::Secret);
511 assert_eq!(secrecy("branch", Some("BRANCH")), Secrecy::Public);
512 }
513
514 #[test]
515 fn an_entry_naming_nothing_is_reported() {
516 let existing = vec![p(&["mcp"])];
517 let stale = unused(&existing);
518 assert!(stale.contains(&"serve".to_string()));
519 }
520}