1use std::collections::{HashMap, HashSet, VecDeque};
33
34use indexmap::IndexMap;
35use serde::{Deserialize, Serialize};
36
37use crate::diagnostic::{SchemaDiagnostic, Severity};
38use crate::filter::FilterMode;
39use crate::item::{DefinitionKind, ItemKind};
40use crate::names::ItemName;
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct Request {
45 pub name: String,
46 pub filter: FilterMode,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct Item {
54 pub kind: ItemKind,
55 pub slug: String,
56 pub path: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct Definition {
63 pub kind: DefinitionKind,
64 pub slug: String,
65 pub output_path: String,
66 pub skills: Vec<String>,
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
73pub struct SelectionFacts {
74 pub definitions: Vec<Definition>,
75 pub edges: Vec<Request>,
78 pub uses_providers: Vec<String>,
79}
80
81#[derive(Debug, Clone)]
82pub struct PackageView {
83 pub facts: SelectionFacts,
84 pub extra_items: Vec<Item>,
87}
88
89#[derive(Debug, Deserialize)]
93#[serde(deny_unknown_fields)]
94pub struct PackageInput {
95 prepared: PreparedInput,
96 #[serde(default, deserialize_with = "extra_items")]
97 extra_items: Option<Vec<Item>>,
98}
99
100fn extra_items<'de, D: serde::Deserializer<'de>>(
103 deserializer: D,
104) -> Result<Option<Vec<Item>>, D::Error> {
105 let items = Option::<Vec<Item>>::deserialize(deserializer)?;
106 match items
107 .iter()
108 .flatten()
109 .find(|item| item.kind == ItemKind::McpServer)
110 {
111 Some(item) => Err(serde::de::Error::custom(format!(
112 "extra_items `{}`: MCP servers are not selected by selectItems",
113 item.slug
114 ))),
115 None => Ok(items),
116 }
117}
118
119#[derive(Debug, Deserialize)]
120struct PreparedInput {
121 selection: SelectionFacts,
122}
123
124impl From<PackageInput> for PackageView {
125 fn from(input: PackageInput) -> Self {
126 Self {
127 facts: input.prepared.selection,
128 extra_items: input.extra_items.unwrap_or_default(),
129 }
130 }
131}
132
133#[derive(Debug)]
134pub struct GraphView {
135 pub requests: Vec<Request>,
137 pub root: Option<PackageView>,
141 pub packages: HashMap<String, PackageView>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148#[serde(tag = "by", rename_all = "kebab-case")]
149pub enum Requester {
150 Consumer,
153 Package { package: String },
155 Item {
157 package: String,
158 kind: ItemKind,
159 slug: String,
160 },
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164pub struct SelectedItem {
165 pub package: Option<String>,
167 pub kind: ItemKind,
168 pub slug: String,
169 pub path: String,
172 pub required_by: Vec<Requester>,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
177pub struct MissingSkill {
178 pub skill: String,
179 pub agent: String,
181 pub package: Option<String>,
183 pub searched: Vec<String>,
185 pub hint: Option<String>,
187}
188
189#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
190pub struct SelectionResult {
191 pub items: Vec<SelectedItem>,
193 pub missing_skills: Vec<MissingSkill>,
194 pub diagnostics: Vec<SchemaDiagnostic>,
197}
198
199struct Entry<'a> {
201 item: Item,
202 definition: Option<&'a Definition>,
203}
204
205type Key<'a> = (&'a str, ItemKind, &'a str);
206
207const ROOT: &str = "";
209
210struct Graph<'v> {
212 packages: HashMap<&'v str, &'v PackageView>,
213 order: Vec<&'v str>,
214}
215
216impl<'v> Graph<'v> {
217 fn new(view: &'v GraphView) -> Self {
218 let mut packages: HashMap<&str, &PackageView> = view
219 .packages
220 .iter()
221 .map(|(name, package)| (name.as_str(), package))
222 .collect();
223 if let Some(root) = &view.root {
224 packages.insert(ROOT, root);
225 }
226 let mut order = Vec::new();
228 let mut seen: HashSet<&str> = HashSet::new();
229 let mut stack: Vec<&str> = Vec::new();
230 let starts = view
231 .root
232 .iter()
233 .map(|_| ROOT)
234 .chain(view.requests.iter().map(|request| request.name.as_str()));
235 for start in starts {
236 stack.push(start);
237 while let Some(name) = stack.pop() {
238 let Some((&name, package)) = packages.get_key_value(name) else {
239 continue;
240 };
241 if seen.insert(name) {
242 order.push(name);
243 stack.extend(
244 package
245 .facts
246 .edges
247 .iter()
248 .rev()
249 .map(|edge| edge.name.as_str()),
250 );
251 }
252 }
253 }
254 Self { packages, order }
255 }
256}
257
258pub fn select(view: &GraphView) -> SelectionResult {
259 let graph = Graph::new(view);
260 let entries: HashMap<&str, Vec<Entry>> = graph
261 .order
262 .iter()
263 .map(|&name| (name, package_entries(graph.packages[name])))
264 .collect();
265
266 let seeds = seeds(view, &graph);
267 let excluded: HashMap<&str, Vec<&FilterMode>> = {
268 let mut excluded: HashMap<&str, Vec<&FilterMode>> = HashMap::new();
269 for seed in &seeds {
270 if matches!(seed.filter, FilterMode::Exclude(_)) {
271 excluded.entry(seed.package).or_default().push(seed.filter);
272 }
273 }
274 excluded
275 };
276
277 let mut selected: IndexMap<Key, Vec<Requester>> = IndexMap::new();
278 let mut queue: VecDeque<Key> = VecDeque::new();
279 for seed in &seeds {
280 for entry in &entries[seed.package] {
281 if !seed_accepts(seed.filter, &entry.item)
282 || is_excluded(&excluded, seed.package, &entry.item)
283 {
284 continue;
285 }
286 let key = (seed.package, entry.item.kind, entry.item.slug.as_str());
287 let requesters = selected.entry(key).or_default();
288 if !requesters.contains(&seed.requester) {
289 requesters.push(seed.requester.clone());
290 }
291 if matches!(entry.item.kind, ItemKind::Agent | ItemKind::Skill) {
292 queue.push_back(key);
293 }
294 }
295 }
296
297 let mut missing_skills = Vec::new();
298 let mut pulled: IndexMap<&str, Requester> = IndexMap::new();
301 let mut processed: HashSet<Key> = HashSet::new();
302 while let Some(key) = queue.pop_front() {
303 if !processed.insert(key) {
304 continue;
305 }
306 let (package, kind, slug) = key;
307 let Some(entry) = entries[package]
308 .iter()
309 .find(|entry| entry.item.kind == kind && entry.item.slug == slug)
310 else {
311 continue;
312 };
313 let requester = if package == ROOT {
314 Requester::Consumer
315 } else {
316 Requester::Item {
317 package: package.to_string(),
318 kind,
319 slug: slug.to_string(),
320 }
321 };
322 for skill in entry
323 .definition
324 .map_or(&[][..], |definition| definition.skills.as_slice())
325 {
326 match resolve_skill(&graph, &entries, package, skill) {
327 Resolved::Deferred => {}
328 Resolved::Missing => {
329 missing_skills.push(missing_skill(&graph, package, slug, skill))
330 }
331 Resolved::In(target) => {
332 let target_entry = entries[target]
333 .iter()
334 .find(|entry| {
335 entry.item.kind == ItemKind::Skill && entry.item.slug == *skill
336 })
337 .expect("a package that has the skill lists it");
338 if is_excluded(&excluded, target, &target_entry.item) {
339 continue;
340 }
341 pulled.entry(target).or_insert_with(|| requester.clone());
342 let key = (target, ItemKind::Skill, target_entry.item.slug.as_str());
343 let requesters = selected.entry(key).or_default();
344 if !requesters.contains(&requester) {
345 requesters.push(requester.clone());
346 }
347 queue.push_back(key);
348 }
349 }
350 }
351 }
352 for (package, requester) in &pulled {
353 for entry in &entries[package] {
354 if entry.item.kind == ItemKind::BootstrapDoc
355 && !is_excluded(&excluded, package, &entry.item)
356 {
357 let key = (*package, entry.item.kind, entry.item.slug.as_str());
358 let requesters = selected.entry(key).or_default();
359 if !requesters.contains(requester) {
360 requesters.push(requester.clone());
361 }
362 }
363 }
364 }
365
366 let mut items = Vec::with_capacity(selected.len());
367 for &package in &graph.order {
368 for entry in &entries[package] {
369 let key = (package, entry.item.kind, entry.item.slug.as_str());
370 if let Some(required_by) = selected.get(&key) {
371 items.push(SelectedItem {
372 package: (package != ROOT).then(|| package.to_string()),
373 kind: entry.item.kind,
374 slug: entry.item.slug.clone(),
375 path: entry.item.path.clone(),
376 required_by: required_by.clone(),
377 });
378 }
379 }
380 }
381 let diagnostics = duplicate_slugs(&items);
382 SelectionResult {
383 items,
384 missing_skills,
385 diagnostics,
386 }
387}
388
389fn duplicate_slugs(items: &[SelectedItem]) -> Vec<SchemaDiagnostic> {
390 let mut packages: IndexMap<(ItemKind, &str), Vec<&str>> = IndexMap::new();
391 for item in items
392 .iter()
393 .filter(|item| matches!(item.kind, ItemKind::Agent | ItemKind::Skill))
394 {
395 packages
396 .entry((item.kind, item.slug.as_str()))
397 .or_default()
398 .push(item.package.as_deref().unwrap_or("the root package"));
399 }
400 packages
401 .into_iter()
402 .filter(|(_, packages)| packages.len() > 1)
403 .map(|((kind, slug), packages)| {
404 SchemaDiagnostic::new(
405 "selection.duplicate-slug",
406 Severity::Warning,
407 Vec::new(),
408 format!("{kind} `{slug}` is installed from {}", packages.join(", ")),
409 )
410 })
411 .collect()
412}
413
414struct Seed<'a> {
415 package: &'a str,
416 filter: &'a FilterMode,
417 requester: Requester,
418}
419
420fn seeds<'a>(view: &'a GraphView, graph: &Graph<'a>) -> Vec<Seed<'a>> {
425 static ALL: FilterMode = FilterMode::All;
426 let mut seeds = Vec::new();
427 let mut open: HashSet<&str> = HashSet::new();
428 let mut queue: VecDeque<&str> = VecDeque::new();
429 if view.root.is_some() {
430 seeds.push(Seed {
431 package: ROOT,
432 filter: &ALL,
433 requester: Requester::Consumer,
434 });
435 }
436 let root_edges = view.root.iter().flat_map(|root| &root.facts.edges);
437 for request in root_edges.chain(&view.requests) {
438 let Some((&package, _)) = graph.packages.get_key_value(request.name.as_str()) else {
439 continue;
440 };
441 seeds.push(Seed {
442 package,
443 filter: &request.filter,
444 requester: Requester::Consumer,
445 });
446 if request.filter == FilterMode::All && open.insert(package) {
447 queue.push_back(package);
448 }
449 }
450 while let Some(from) = queue.pop_front() {
451 for edge in &graph.packages[from].facts.edges {
452 let Some((&package, _)) = graph.packages.get_key_value(edge.name.as_str()) else {
453 continue;
454 };
455 if edge.filter != FilterMode::All {
456 continue;
457 }
458 seeds.push(Seed {
459 package,
460 filter: &edge.filter,
461 requester: Requester::Package {
462 package: from.to_string(),
463 },
464 });
465 if open.insert(package) {
466 queue.push_back(package);
467 }
468 }
469 }
470 seeds
471}
472
473fn package_entries<'a>(package: &'a PackageView) -> Vec<Entry<'a>> {
474 let definitions = package.facts.definitions.iter().map(|definition| {
475 let (kind, path) = match definition.kind {
476 DefinitionKind::Agent => (ItemKind::Agent, definition.output_path.clone()),
477 DefinitionKind::Skill => (
478 ItemKind::Skill,
479 match definition.output_path.rsplit_once('/') {
480 Some((parent, _)) if !parent.is_empty() => parent.to_string(),
481 _ => ".".to_string(),
482 },
483 ),
484 };
485 Entry {
486 item: Item {
487 kind,
488 slug: definition.slug.clone(),
489 path,
490 },
491 definition: Some(definition),
492 }
493 });
494 let extras = package.extra_items.iter().map(|item| Entry {
495 item: item.clone(),
496 definition: None,
497 });
498 definitions.chain(extras).collect()
499}
500
501fn seed_accepts(filter: &FilterMode, item: &Item) -> bool {
504 match filter {
505 FilterMode::All => true,
506 FilterMode::Include { agents, skills } => match item.kind {
507 ItemKind::Agent => agents.iter().any(|name| name.as_str() == item.slug),
508 ItemKind::Skill => skills.iter().any(|name| name.as_str() == item.slug),
509 ItemKind::BootstrapDoc => true,
511 ItemKind::Hook | ItemKind::McpServer => false,
512 },
513 FilterMode::Exclude(excluded) => !names_item(excluded, item),
514 FilterMode::OnlySkills => item.kind == ItemKind::Skill,
515 FilterMode::OnlyAgents => item.kind == ItemKind::Agent,
516 }
517}
518
519fn names_item(excluded: &[ItemName], item: &Item) -> bool {
521 excluded
522 .iter()
523 .any(|name| name.as_str() == item.slug || name.as_str() == item.path)
524}
525
526fn is_excluded(excluded: &HashMap<&str, Vec<&FilterMode>>, package: &str, item: &Item) -> bool {
527 excluded.get(package).is_some_and(|filters| {
528 filters.iter().any(|filter| match filter {
529 FilterMode::Exclude(names) => names_item(names, item),
530 _ => false,
531 })
532 })
533}
534
535enum Resolved<'a> {
536 In(&'a str),
537 Deferred,
539 Missing,
540}
541
542fn resolve_skill<'a>(
545 graph: &Graph<'a>,
546 entries: &HashMap<&str, Vec<Entry>>,
547 requester: &'a str,
548 skill: &str,
549) -> Resolved<'a> {
550 let has_skill = |package: &str| {
551 entries[package]
552 .iter()
553 .any(|entry| entry.item.kind == ItemKind::Skill && entry.item.slug == skill)
554 };
555 if has_skill(requester) {
556 return Resolved::In(requester);
557 }
558
559 let mut closure: Vec<&str> = Vec::new();
561 let mut seen: HashSet<&str> = HashSet::from([requester]);
562 let mut queue: VecDeque<&str> = VecDeque::from([requester]);
563 while let Some(package) = queue.pop_front() {
564 if package != requester {
565 closure.push(package);
566 }
567 for edge in &graph.packages[package].facts.edges {
568 if let Some((&name, _)) = graph.packages.get_key_value(edge.name.as_str())
569 && seen.insert(name)
570 {
571 queue.push_back(name);
572 }
573 }
574 }
575 if let Some(package) = closure.into_iter().find(|package| has_skill(package)) {
576 return Resolved::In(package);
577 }
578 for &name in &graph.order {
579 if !seen.contains(name) && has_skill(name) {
580 return Resolved::In(name);
581 }
582 }
583
584 if crate::provider::deferred_provider(
585 skill,
586 Some(&graph.packages[requester].facts.uses_providers),
587 )
588 .is_some()
589 {
590 return Resolved::Deferred;
591 }
592 Resolved::Missing
593}
594
595fn missing_skill(graph: &Graph, package: &str, agent: &str, skill: &str) -> MissingSkill {
596 let mut searched: Vec<String> = graph
597 .order
598 .iter()
599 .filter(|&&name| name != ROOT)
600 .map(|name| name.to_string())
601 .collect();
602 searched.sort();
603 MissingSkill {
604 skill: skill.to_string(),
605 agent: agent.to_string(),
606 package: (package != ROOT).then(|| package.to_string()),
607 searched,
608 hint: crate::provider::provider_of(skill).map(crate::provider::undeclared_provider_hint),
609 }
610}