Skip to main content

meerkat_mobkit/
baseline.rs

1//! Baseline runtime configuration and module bootstrapping.
2
3use 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}