Skip to main content

xtask/patterns/
surfaces.rs

1use anyhow::Result;
2use std::path::Path;
3
4use super::{
5    reporter::PatternReporter,
6    util::{is_test_file, read_file},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10enum SurfaceKind {
11    CoreRust,
12    OperationalRust,
13    Web,
14}
15
16struct SurfaceFile {
17    path: String,
18    kind: SurfaceKind,
19}
20
21const CORE_RUST_FORBIDDEN: &[&str] = &[
22    "reqwest::",
23    "hyper::Client",
24    "sqlx::",
25    "rusqlite::",
26    "tokio::fs",
27    "std::fs",
28    "std::process::Command",
29    "Command::new",
30    "SomaClient::new",
31    "crate::soma::SomaClient",
32    "soma_client::SomaClient",
33];
34
35const WEB_FORBIDDEN: &[&str] = &[
36    "child_process",
37    "node:child_process",
38    "fs.",
39    "node:fs",
40    "process.env.SOMA_",
41    "fetch(\"https://",
42    "fetch('https://",
43];
44
45const BUSINESS_HELPER_TOKENS: &[&str] = &[
46    "fn normalize_",
47    "fn validate_",
48    "fn transform_",
49    "fn calculate_",
50    "fn enrich_",
51    "fn apply_policy",
52];
53
54const RUST_DELEGATION_TOKENS: &[&str] = &[
55    ".application()",
56    "state.service",
57    "state.provider_registry",
58    "service.",
59    "execute_service_action",
60    "streamable_http_service",
61];
62
63pub(super) fn thin_surfaces(reporter: &mut PatternReporter) -> Result<()> {
64    let files = surface_files()?;
65    let mut failures = Vec::new();
66    let mut warnings = Vec::new();
67
68    for file in &files {
69        let text = read_file(&file.path);
70        match file.kind {
71            SurfaceKind::CoreRust => {
72                let text = strip_inline_test_module(&text);
73                check_core_rust_surface(file, text, &mut failures, &mut warnings)
74            }
75            SurfaceKind::OperationalRust => check_operational_surface(file, &text, &mut warnings),
76            SurfaceKind::Web => check_web_surface(file, &text, &mut failures, &mut warnings),
77        }
78    }
79
80    if !failures.is_empty() {
81        reporter.fail(
82            "surfaces",
83            format!(
84                "business/IO logic appears in surface files: {}. Hint: CLI/API/MCP/web surfaces should parse inputs, delegate to SomaService or API endpoints, and format responses only.",
85                failures.join("; ")
86            ),
87        );
88    }
89    if !warnings.is_empty() {
90        reporter.warn(
91            "surfaces",
92            format!(
93                "suspicious surface logic found: {}. Hint: verify this is protocol/UI glue rather than business logic; move validation, normalization, and transformations into app.rs.",
94                warnings.join("; ")
95            ),
96        );
97    }
98    if failures.is_empty() && warnings.is_empty() {
99        reporter.ok(
100            "surfaces",
101            format!("{} CLI/API/MCP/web surface files look thin", files.len()),
102        );
103    }
104    Ok(())
105}
106
107fn check_core_rust_surface(
108    file: &SurfaceFile,
109    text: &str,
110    failures: &mut Vec<String>,
111    warnings: &mut Vec<String>,
112) {
113    for token in CORE_RUST_FORBIDDEN {
114        if !core_token_applies(file, token) {
115            continue;
116        }
117        if text.contains(token) {
118            failures.push(format!("{} contains `{token}`", file.path));
119        }
120    }
121
122    for token in BUSINESS_HELPER_TOKENS {
123        if text.contains(token) {
124            warnings.push(format!("{} contains helper `{token}`", file.path));
125        }
126    }
127
128    let is_dispatch_surface = matches!(
129        file.path.as_str(),
130        "crates/soma/api/src/api.rs"
131            | "crates/soma/mcp/src/tools.rs"
132            | "crates/soma/cli/src/lib.rs"
133    );
134    if is_dispatch_surface
135        && !RUST_DELEGATION_TOKENS
136            .iter()
137            .any(|token| text.contains(token))
138    {
139        warnings.push(format!(
140            "{} has no obvious service delegation token",
141            file.path
142        ));
143    }
144
145    let json_macro_count = text.matches("json!({").count();
146    if json_macro_count > 12 {
147        warnings.push(format!(
148            "{} has {json_macro_count} json! object literals",
149            file.path
150        ));
151    }
152}
153
154fn core_token_applies(file: &SurfaceFile, token: &str) -> bool {
155    if file.path == "crates/soma/cli/src/lib.rs"
156        && matches!(
157            token,
158            "std::fs" | "std::process::Command" | "Command::new" | "SomaClient::new"
159        )
160    {
161        return false;
162    }
163    true
164}
165
166fn check_operational_surface(file: &SurfaceFile, text: &str, warnings: &mut Vec<String>) {
167    // doctor/ and watch.rs use reqwest for health/connectivity pings — explicitly diagnostics-only.
168    let is_diagnostics = file.path.starts_with("crates/soma/cli/src/doctor/")
169        || file.path == "crates/soma/cli/src/watch.rs";
170    for token in ["reqwest::", "sqlx::", "rusqlite::"] {
171        if text.contains(token) && !(token == "reqwest::" && is_diagnostics) {
172            warnings.push(format!(
173                "{} operational surface contains `{token}`; confirm it is diagnostics-only",
174                file.path
175            ));
176        }
177    }
178}
179
180fn check_web_surface(
181    file: &SurfaceFile,
182    text: &str,
183    failures: &mut Vec<String>,
184    warnings: &mut Vec<String>,
185) {
186    for token in WEB_FORBIDDEN {
187        if text.contains(token) {
188            failures.push(format!("{} contains `{token}`", file.path));
189        }
190    }
191
192    // soma.ts is the canonical action metadata definition for the web UI — it is
193    // expected to list all action names and is explicitly excluded from this check.
194    let is_definition_file = file.path == "apps/web/lib/soma.ts";
195    if !is_definition_file {
196        let hardcoded_action_count = ["greet", "echo", "status", "scaffold_intent"]
197            .iter()
198            .filter(|action| text.contains(&format!("\"{action}\"")))
199            .count();
200        if hardcoded_action_count > 3 {
201            warnings.push(format!(
202                "{} hardcodes {hardcoded_action_count} action names",
203                file.path
204            ));
205        }
206    }
207}
208
209fn strip_inline_test_module(text: &str) -> &str {
210    let lines = text.lines().collect::<Vec<_>>();
211    for index in 0..lines.len().saturating_sub(1) {
212        if lines[index].trim() != "#[cfg(test)]" {
213            continue;
214        }
215        let next = lines[index + 1].trim_start();
216        if next.starts_with("mod ") || next.starts_with("pub mod ") {
217            let byte_index = lines[..index].iter().map(|line| line.len() + 1).sum();
218            return &text[..byte_index];
219        }
220    }
221    text
222}
223
224fn surface_files() -> Result<Vec<SurfaceFile>> {
225    let output = crate::run_cmd_output("git", &["ls-files"])?;
226    Ok(output.lines().filter_map(surface_file).collect::<Vec<_>>())
227}
228
229fn surface_file(path: &str) -> Option<SurfaceFile> {
230    let kind = if matches!(
231        path,
232        "crates/soma/api/src/api.rs"
233            | "apps/soma/src/http.rs"
234            | "crates/soma/mcp/src/tools.rs"
235            | "crates/soma/mcp/src/rmcp_server.rs"
236            | "crates/soma/mcp/src/schemas.rs"
237            | "crates/soma/mcp/src/prompts.rs"
238            | "crates/soma/cli/src/lib.rs"
239    ) {
240        SurfaceKind::CoreRust
241    } else if path.starts_with("crates/soma/cli/src/") && path.ends_with(".rs") {
242        SurfaceKind::OperationalRust
243    } else if is_web_surface(path) {
244        SurfaceKind::Web
245    } else {
246        return None;
247    };
248
249    Some(SurfaceFile {
250        path: path.to_owned(),
251        kind,
252    })
253}
254
255fn is_web_surface(path: &str) -> bool {
256    let p = Path::new(path);
257    if is_test_file(p) {
258        return false;
259    }
260    let is_tsx = p.extension().and_then(|ext| ext.to_str()) == Some("tsx");
261    let is_ts = p.extension().and_then(|ext| ext.to_str()) == Some("ts");
262    (path.starts_with("apps/web/app/") && is_tsx) || (path.starts_with("apps/web/lib/") && is_ts)
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn classifies_core_rust_surfaces() {
271        let file = surface_file("crates/soma/mcp/src/tools.rs").expect("tools should be a surface");
272        assert_eq!(file.kind, SurfaceKind::CoreRust);
273    }
274
275    #[test]
276    fn classifies_operational_cli_surfaces() {
277        let file =
278            surface_file("crates/soma/cli/src/doctor.rs").expect("doctor should be a surface");
279        assert_eq!(file.kind, SurfaceKind::OperationalRust);
280    }
281
282    #[test]
283    fn classifies_web_surfaces() {
284        let file = surface_file("apps/web/app/page.tsx").expect("web page should be a surface");
285        assert_eq!(file.kind, SurfaceKind::Web);
286    }
287
288    #[test]
289    fn ignores_web_test_files() {
290        assert!(surface_file("apps/web/lib/soma.test.ts").is_none());
291    }
292
293    #[test]
294    fn ignores_service_layer() {
295        assert!(surface_file("src/app.rs").is_none());
296    }
297}