meerkat_mobkit/
baseline.rs1use std::fs;
4use std::path::{Path, PathBuf};
5
6pub const DEFAULT_MEERKAT_REPO: &str = "/Users/luka/src/raik";
7pub const REQUIRED_MEERKAT_SYMBOLS: &[&str] = &[
8 "MobEventRouter",
9 "send_message(id, msg)",
10 "subscribe_agent_events(id)",
11 "subscribe_all_agent_events()",
12 "SpawnPolicy trait",
13 "respawn(id, msg)",
14 "AttributedEvent",
15 "Roster::session_id(id)",
16 "Roster::find_by_label(k, v)",
17 "SessionBuildOptions.app_context",
18 "SessionBuildOptions.additional_instructions",
19 "CreateSessionRequest.labels",
20 "RosterEntry.labels",
21 "SpawnMemberSpec.resume_session_id",
22];
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct BaselineVerificationReport {
26 pub repo_root: PathBuf,
27 pub missing_symbols: Vec<String>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum BaselineVerificationError {
32 RepoMissing(PathBuf),
33 RepoUnreadable(PathBuf),
34 MissingSymbols(BaselineVerificationReport),
35}
36
37impl std::fmt::Display for BaselineVerificationError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Self::RepoMissing(path) => write!(f, "repo missing: {}", path.display()),
41 Self::RepoUnreadable(path) => write!(f, "repo unreadable: {}", path.display()),
42 Self::MissingSymbols(report) => {
43 write!(
44 f,
45 "missing symbols in {}: {}",
46 report.repo_root.display(),
47 report.missing_symbols.join(", ")
48 )
49 }
50 }
51 }
52}
53
54impl std::error::Error for BaselineVerificationError {}
55
56pub fn verify_meerkat_baseline_symbols(
57 explicit_repo_root: Option<&Path>,
58) -> Result<BaselineVerificationReport, BaselineVerificationError> {
59 let repo_root = explicit_repo_root
60 .map(PathBuf::from)
61 .or_else(|| std::env::var("MEERKAT_REPO").ok().map(PathBuf::from))
62 .unwrap_or_else(|| PathBuf::from(DEFAULT_MEERKAT_REPO));
63
64 if !repo_root.exists() {
65 return Err(BaselineVerificationError::RepoMissing(repo_root));
66 }
67 if !repo_root.is_dir() {
68 return Err(BaselineVerificationError::RepoUnreadable(repo_root));
69 }
70
71 let mut missing: Vec<String> = REQUIRED_MEERKAT_SYMBOLS
72 .iter()
73 .map(std::string::ToString::to_string)
74 .collect();
75
76 scan_dir_for_symbols(&repo_root, &mut missing)
77 .map_err(|_| BaselineVerificationError::RepoUnreadable(repo_root.clone()))?;
78
79 let report = BaselineVerificationReport {
80 repo_root,
81 missing_symbols: missing,
82 };
83
84 if report.missing_symbols.is_empty() {
85 Ok(report)
86 } else {
87 Err(BaselineVerificationError::MissingSymbols(report))
88 }
89}
90
91fn scan_dir_for_symbols(path: &Path, missing: &mut Vec<String>) -> std::io::Result<()> {
92 if missing.is_empty() {
93 return Ok(());
94 }
95
96 for entry in fs::read_dir(path)? {
97 let entry = entry?;
98 let entry_path = entry.path();
99 let file_type = entry.file_type()?;
100 if file_type.is_dir() {
101 if should_skip_dir(&entry_path) {
102 continue;
103 }
104 scan_dir_for_symbols(&entry_path, missing)?;
105 continue;
106 }
107 if !file_type.is_file() {
108 continue;
109 }
110 if should_skip_file(&entry_path) {
111 continue;
112 }
113
114 let content = match fs::read_to_string(&entry_path) {
115 Ok(content) => content,
116 Err(_) => continue,
117 };
118
119 missing.retain(|symbol| !contains_symbol(&content, symbol));
120 if missing.is_empty() {
121 return Ok(());
122 }
123 }
124
125 Ok(())
126}
127
128fn should_skip_dir(path: &Path) -> bool {
129 matches!(
130 path.file_name().and_then(|name| name.to_str()),
131 Some(".git" | "target" | "node_modules" | ".next" | ".turbo")
132 )
133}
134
135fn should_skip_file(path: &Path) -> bool {
136 let Some(extension) = path.extension().and_then(|ext| ext.to_str()) else {
137 return false;
138 };
139 matches!(
140 extension,
141 "png" | "jpg" | "jpeg" | "gif" | "pdf" | "wasm" | "lock"
142 )
143}
144
145fn contains_symbol(content: &str, symbol: &str) -> bool {
146 if content.contains(symbol) {
147 return true;
148 }
149
150 match symbol {
151 "MobEventRouter" => content.contains("MobEventRouter"),
152 "subscribe_agent_events(id)" => content.contains("subscribe_agent_events"),
153 "subscribe_all_agent_events()" => content.contains("subscribe_all_agent_events"),
154 "SpawnPolicy trait" => content.contains("trait SpawnPolicy"),
155 "SessionBuildOptions.app_context" => {
156 content.contains("SessionBuildOptions")
157 && (content.contains("app_context") || content.contains(".app_context"))
158 }
159 "SessionBuildOptions.additional_instructions" => {
160 content.contains("SessionBuildOptions")
161 && (content.contains("additional_instructions")
162 || content.contains(".additional_instructions"))
163 }
164 "CreateSessionRequest.labels" => {
165 content.contains("CreateSessionRequest")
166 && (content.contains("labels") || content.contains(".labels"))
167 }
168 "RosterEntry.labels" => {
169 content.contains("RosterEntry")
170 && (content.contains("labels") || content.contains(".labels"))
171 }
172 "SpawnMemberSpec.resume_session_id" => {
173 content.contains("SpawnMemberSpec")
174 && (content.contains("resume_session_id") || content.contains(".resume_session_id"))
175 }
176 "Roster::session_id(id)" => {
177 content.contains("Roster::session_id")
178 || content.contains("fn session_id")
179 || content.contains(".session_id(")
180 }
181 "Roster::find_by_label(k, v)" => {
182 content.contains("Roster::find_by_label")
183 || content.contains("fn find_by_label")
184 || content.contains(".find_by_label(")
185 }
186 "send_message(id, msg)" => content.contains("send_message"),
187 "respawn(id, msg)" => content.contains("respawn("),
188 _ => false,
189 }
190}