Skip to main content

xtask/patterns/
util.rs

1use anyhow::{Context, Result};
2use std::{fs, path::Path};
3
4pub(super) fn read_file(path: &str) -> String {
5    fs::read_to_string(path).unwrap_or_default()
6}
7
8pub(super) fn display_path(path: &Path) -> String {
9    path.strip_prefix(".")
10        .unwrap_or(path)
11        .to_string_lossy()
12        .trim_start_matches('/')
13        .to_string()
14}
15
16pub(super) fn contains_top_level_json_key(text: &str, key: &str) -> bool {
17    // Avoids serde_json in xtask. Handles both formatted JSON (key on its own
18    // line) and compact/single-line JSON (key after `{`).
19    let pattern = format!("\"{key}\"");
20    text.lines().any(|line| {
21        let content = line.trim_start().trim_start_matches('{').trim_start();
22        content.starts_with(&pattern) && content[pattern.len()..].trim_start().starts_with(':')
23    })
24}
25
26pub(super) fn size_limit(path: &Path) -> Option<usize> {
27    if path == Path::new("crates/shared/auth/src/sqlite.rs") {
28        // Vendored auth storage logic is larger than Soma surface modules;
29        // keep it visible as a warning without blocking unrelated CI gates.
30        return Some(700);
31    }
32    if path == Path::new("crates/soma/application/src/provider_registry.rs") {
33        // Provider registration and dispatch is intentionally centralized while
34        // the drop-in provider contract is settling. Keep it warning-visible.
35        return Some(500);
36    }
37    if path == Path::new("crates/shared/provider-adapters/src/python/supervisor.rs")
38        || path == Path::new("crates/soma/application/src/providers/filesystem.rs")
39    {
40        // Persistent Python supervision and file-backed candidate activation
41        // are still settling as one atomic lifecycle contract. Keep these
42        // warning-visible until the shared worker/activation seams stabilize.
43        return Some(500);
44    }
45    if path == Path::new("crates/shared/codex-app-server-client/src/rest/openapi/paths.rs") {
46        // OpenAPI route rendering is mechanically repetitive and currently
47        // split from schemas; retain a warning without blocking unrelated work.
48        return Some(400);
49    }
50    if path == Path::new("xtask/src/generated_surfaces.rs") {
51        // Generated surface docs, plugin metadata, and package catalogs are
52        // coupled by design; split once the generated contract stabilizes.
53        return Some(500);
54    }
55    if path == Path::new("xtask/src/scaffold.rs") {
56        // This xtask module orchestrates cross-cutting repo automation and is
57        // being split as the automation surface settles. Keep it visible as a
58        // warning without making unrelated workflow fixes fail CI.
59        //
60        // `xtask/src/rmcp_release_monitor.rs` used to share this exemption. It
61        // has since been split along its real seams (CLI options, the schema
62        // and conformance watches, the shared impact scan, issue-body
63        // rendering), so it is back under the ordinary limit and needs no
64        // bespoke number. Do not re-add it - grow the submodules instead.
65        return Some(600);
66    }
67    if path
68        .to_string_lossy()
69        .starts_with("xtask/src/scripts_lane_")
70    {
71        // Transitional script ports are grouped by migration lane. Keep them
72        // visible as warnings while avoiding a hard block during the shell to
73        // xtask handoff.
74        return Some(1000);
75    }
76
77    match path.extension().and_then(|ext| ext.to_str()) {
78        Some("rs") => Some(350),
79        Some("ts" | "tsx") => Some(300),
80        _ => None,
81    }
82}
83
84pub(super) fn is_size_exempt(path: &Path) -> bool {
85    let path = path.to_string_lossy();
86    // The exact history-preserving Synapse donor import is a temporary nested
87    // workspace. Its source remains byte-for-byte locked until product-native
88    // splitting begins, so root module-size guidance is not actionable there.
89    if path.starts_with("crates/synapse/import/") {
90        return true;
91    }
92    // Vendored upstream MCP schema mirrors.
93    if path.starts_with("docs/references/mcp/schema/") {
94        return true;
95    }
96    // Checked-in code generator output. `docs/PATTERNS.md` already states the
97    // policy ("must split unless generated/fixture/schema mirror"); this is
98    // the `generated` half of it. Splitting is not an option a maintainer has
99    // here - the file is rewritten wholesale by its generator on every run,
100    // and a parity test fails the build if it is hand-edited - so a size
101    // warning on one would be pure noise, never actionable.
102    //
103    // Deliberately narrow: only a `src/generated/` directory counts, not a
104    // file that merely says "generated" somewhere in its path, so this can't
105    // be used to launder a hand-written module past the limit.
106    if path.contains("/src/generated/") {
107        return true;
108    }
109    false
110}
111
112pub(super) fn is_test_file(path: &Path) -> bool {
113    let path = path.to_string_lossy();
114    path.contains("/tests/")
115        || path.ends_with("_test.rs")
116        || path.ends_with("_tests.rs")
117        || path.ends_with("/tests.rs")
118        || path.ends_with(".test.ts")
119        || path.ends_with(".test.tsx")
120        || path.ends_with(".spec.ts")
121        || path.ends_with(".spec.tsx")
122        || path.contains("/__tests__/")
123}
124
125pub(super) fn effective_loc(path: &Path) -> Result<usize> {
126    let text =
127        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
128    Ok(effective_loc_from_text(
129        &text,
130        path.extension().and_then(|ext| ext.to_str()) == Some("rs"),
131    ))
132}
133
134fn effective_loc_from_text(text: &str, strip_tests: bool) -> usize {
135    let text = if strip_tests {
136        strip_inline_test_module(text)
137    } else {
138        text
139    };
140    let mut count = 0usize;
141    let mut in_block = false;
142
143    for raw in text.lines() {
144        let mut line = raw.trim();
145        if line.is_empty() {
146            continue;
147        }
148        if in_block {
149            if let Some((_, after)) = line.split_once("*/") {
150                line = after.trim();
151                in_block = false;
152                if line.is_empty() {
153                    continue;
154                }
155            } else {
156                continue;
157            }
158        }
159        if line.starts_with("//") {
160            continue;
161        }
162        if line.starts_with("/*") {
163            if let Some((_, after)) = line.split_once("*/") {
164                line = after.trim();
165                if line.is_empty() {
166                    continue;
167                }
168            } else {
169                in_block = true;
170                continue;
171            }
172        }
173        count += 1;
174    }
175    count
176}
177
178fn strip_inline_test_module(text: &str) -> &str {
179    let lines = text.lines().collect::<Vec<_>>();
180    for index in 0..lines.len().saturating_sub(1) {
181        if lines[index].trim() != "#[cfg(test)]" {
182            continue;
183        }
184        let next = lines[index + 1].trim_start();
185        if next.starts_with("mod ") || next.starts_with("pub mod ") {
186            let byte_index = lines[..index].iter().map(|line| line.len() + 1).sum();
187            return &text[..byte_index];
188        }
189    }
190    text
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn effective_loc_ignores_comments_blanks_and_trailing_tests() {
199        let text = r#"
200// comment
201pub fn production() {}
202
203/* block
204   comment */
205pub fn more() {}
206
207#[cfg(test)]
208mod tests {
209    fn test_only() {}
210}
211"#;
212        assert_eq!(effective_loc_from_text(text, true), 2);
213    }
214
215    #[test]
216    fn effective_loc_counts_code_after_inline_block_comment() {
217        let text = "/* license */ pub fn one() {}\nlet two = 2;";
218        assert_eq!(effective_loc_from_text(text, false), 2);
219    }
220
221    #[test]
222    fn test_file_detection_accepts_both_common_rust_suffixes() {
223        assert!(is_test_file(Path::new("src/provider_test.rs")));
224        assert!(is_test_file(Path::new("src/provider_tests.rs")));
225        assert!(!is_test_file(Path::new("src/provider.rs")));
226    }
227
228    #[test]
229    fn top_level_json_key_detects_manifest_version_field() {
230        assert!(contains_top_level_json_key(
231            "{\n  \"version\": \"1\"\n}",
232            "version"
233        ));
234        assert!(!contains_top_level_json_key(
235            "{\n  \"not_version\": true\n}",
236            "version"
237        ));
238    }
239
240    #[test]
241    fn top_level_json_key_handles_compact_json() {
242        // Single-line / compact JSON: key appears after `{`
243        assert!(contains_top_level_json_key(
244            "{ \"version\": \"1\" }",
245            "version"
246        ));
247        assert!(contains_top_level_json_key(
248            "{\"version\":\"1\"}",
249            "version"
250        ));
251        // Must not match a different key
252        assert!(!contains_top_level_json_key(
253            "{ \"name\": \"foo\" }",
254            "version"
255        ));
256    }
257
258    #[test]
259    fn size_limits_skip_exact_synapse_import_boundary() {
260        assert!(is_size_exempt(Path::new(
261            "crates/synapse/import/src/host_config.rs"
262        )));
263        assert!(!is_size_exempt(Path::new(
264            "crates/synapse/application/src/host_config.rs"
265        )));
266    }
267
268    #[test]
269    fn size_limits_skip_vendored_mcp_schema_references() {
270        assert!(is_size_exempt(Path::new(
271            "docs/references/mcp/schema/2025-11-25/schema.ts"
272        )));
273        assert!(!is_size_exempt(Path::new("apps/web/src/app/page.tsx")));
274    }
275
276    #[test]
277    fn size_limits_skip_checked_in_generator_output() {
278        assert!(is_size_exempt(Path::new(
279            "crates/shared/codex-app-server-client/clients/typescript/src/generated/openapi-types.ts"
280        )));
281        // Only a real `src/generated/` directory is exempt - a hand-written
282        // module can't opt out by working the word into its name or docs.
283        assert!(!is_size_exempt(Path::new(
284            "crates/soma/mcp/src/generated_schemas.rs"
285        )));
286        assert!(!is_size_exempt(Path::new(
287            "xtask/src/generated_surfaces.rs"
288        )));
289    }
290
291    #[test]
292    fn transitional_xtask_modules_warn_before_hard_failing() {
293        assert_eq!(size_limit(Path::new("xtask/src/scaffold.rs")), Some(600));
294    }
295
296    #[test]
297    fn split_xtask_modules_lose_their_transitional_exemption() {
298        // The monitor was split along its real seams, so it is governed by the
299        // ordinary Rust limit again. A regression here would mean someone
300        // re-added the bespoke number instead of extending a submodule.
301        assert_eq!(
302            size_limit(Path::new("xtask/src/rmcp_release_monitor.rs")),
303            Some(350)
304        );
305        assert_eq!(
306            size_limit(Path::new("xtask/src/rmcp_release_monitor/schema.rs")),
307            Some(350)
308        );
309    }
310}