mars_agents/sync/
filter.rs1use std::collections::HashSet;
8use std::path::Path;
9
10use crate::config::FilterMode;
11use crate::discover;
12use crate::error::MarsError;
13use crate::lock::ItemKind;
14use crate::types::ItemName;
15use crate::validate;
16
17pub(crate) fn apply_filter(
22 discovered: &[discover::DiscoveredItem],
23 filter: &FilterMode,
24 tree_path: &Path,
25) -> Result<Vec<discover::DiscoveredItem>, MarsError> {
26 match filter {
27 FilterMode::All => Ok(discovered.to_vec()),
28
29 FilterMode::Exclude(excluded) => Ok(discovered
30 .iter()
31 .filter(|item| {
32 let path_str = item.source_path.to_string_lossy();
33 !excluded.iter().any(|e| {
34 path_str == e.as_ref() || item.id.name == *e
36 })
37 })
38 .cloned()
39 .collect()),
40
41 FilterMode::Include { agents, skills } => {
42 let mut include_set: HashSet<ItemName> = HashSet::new();
44
45 for a in agents {
47 include_set.insert(a.clone());
48 }
49 for s in skills {
50 include_set.insert(s.clone());
51 }
52
53 resolve_agent_skill_deps(discovered, agents, tree_path, &mut include_set);
55
56 Ok(discovered
57 .iter()
58 .filter(|item| include_set.contains(&item.id.name))
59 .cloned()
60 .collect())
61 }
62
63 FilterMode::OnlySkills => Ok(discovered
64 .iter()
65 .filter(|item| item.id.kind == ItemKind::Skill)
66 .cloned()
67 .collect()),
68
69 FilterMode::OnlyAgents => {
70 let agents: Vec<_> = discovered
72 .iter()
73 .filter(|item| item.id.kind == ItemKind::Agent)
74 .cloned()
75 .collect();
76
77 let agent_names: Vec<ItemName> = agents.iter().map(|a| a.id.name.clone()).collect();
79 let mut skill_deps: HashSet<ItemName> = HashSet::new();
80 resolve_agent_skill_deps(discovered, &agent_names, tree_path, &mut skill_deps);
81
82 let skills: Vec<_> = discovered
84 .iter()
85 .filter(|item| {
86 item.id.kind == ItemKind::Skill && skill_deps.contains(&item.id.name)
87 })
88 .cloned()
89 .collect();
90
91 let mut result = agents;
92 result.extend(skills);
93 Ok(result)
94 }
95 }
96}
97
98fn resolve_agent_skill_deps(
103 discovered: &[discover::DiscoveredItem],
104 agent_names: &[ItemName],
105 tree_path: &Path,
106 skill_deps: &mut HashSet<ItemName>,
107) {
108 for agent_name in agent_names {
109 if let Some(agent_item) = discovered
110 .iter()
111 .find(|i| i.id.kind == ItemKind::Agent && i.id.name == *agent_name)
112 {
113 let agent_path = tree_path.join(&agent_item.source_path);
114 let deps = validate::parse_agent_skills(&agent_path).unwrap_or_default();
115 for skill in deps {
116 skill_deps.insert(ItemName::from(skill));
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::discover;
126 use std::fs;
127 use tempfile::TempDir;
128
129 fn make_source_tree(agents: &[(&str, &str)], skills: &[(&str, &str)]) -> TempDir {
131 let dir = TempDir::new().unwrap();
132 if !agents.is_empty() {
133 let agents_dir = dir.path().join("agents");
134 fs::create_dir_all(&agents_dir).unwrap();
135 for (name, content) in agents {
136 fs::write(agents_dir.join(name), content).unwrap();
137 }
138 }
139 if !skills.is_empty() {
140 let skills_dir = dir.path().join("skills");
141 fs::create_dir_all(&skills_dir).unwrap();
142 for (name, content) in skills {
143 let skill_dir = skills_dir.join(name);
144 fs::create_dir_all(&skill_dir).unwrap();
145 fs::write(skill_dir.join("SKILL.md"), content).unwrap();
146 }
147 }
148 dir
149 }
150
151 #[test]
152 fn filter_all_returns_everything() {
153 let tree = make_source_tree(
154 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
155 &[("planning", "# planning")],
156 );
157 let discovered = discover::discover_source(tree.path(), None).unwrap();
158 let filtered = apply_filter(&discovered, &FilterMode::All, tree.path()).unwrap();
159 assert_eq!(filtered.len(), 3);
160 }
161
162 #[test]
163 fn filter_exclude_removes_items() {
164 let tree = make_source_tree(
165 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
166 &[],
167 );
168 let discovered = discover::discover_source(tree.path(), None).unwrap();
169 let filtered = apply_filter(
170 &discovered,
171 &FilterMode::Exclude(vec!["reviewer".into()]),
172 tree.path(),
173 )
174 .unwrap();
175 assert_eq!(filtered.len(), 1);
176 assert_eq!(filtered[0].id.name, "coder");
177 }
178
179 #[test]
180 fn filter_include_agents_only() {
181 let tree = make_source_tree(
182 &[("coder.md", "# coder"), ("reviewer.md", "# reviewer")],
183 &[("planning", "# planning")],
184 );
185 let discovered = discover::discover_source(tree.path(), None).unwrap();
186 let filtered = apply_filter(
187 &discovered,
188 &FilterMode::Include {
189 agents: vec!["coder".into()],
190 skills: vec![],
191 },
192 tree.path(),
193 )
194 .unwrap();
195 assert_eq!(filtered.len(), 1);
196 assert_eq!(filtered[0].id.name, "coder");
197 }
198
199 #[test]
200 fn filter_include_with_transitive_skill_deps() {
201 let tree = make_source_tree(
202 &[(
203 "coder.md",
204 "---\nskills:\n - planning\n---\n# Coder agent\n",
205 )],
206 &[
207 ("planning", "# Planning skill"),
208 ("review", "# Review skill"),
209 ],
210 );
211 let discovered = discover::discover_source(tree.path(), None).unwrap();
212 let filtered = apply_filter(
213 &discovered,
214 &FilterMode::Include {
215 agents: vec!["coder".into()],
216 skills: vec![],
217 },
218 tree.path(),
219 )
220 .unwrap();
221 assert_eq!(filtered.len(), 2);
223 let names: Vec<&str> = filtered.iter().map(|i| i.id.name.as_str()).collect();
224 assert!(names.contains(&"coder"));
225 assert!(names.contains(&"planning"));
226 }
227}