Skip to main content

scc_context/
skeleton.rs

1//! Repository Skeleton: deterministic physical-layout evidence for startup.
2//!
3//! The skeleton answers "what physically exists here" from the indexed file
4//! inventory — indisputable layout facts that ground the inferred
5//! architecture in the System Atlas. It is NOT a semantic layer and never
6//! overrides semantic evidence: components, flows, and contracts come from
7//! the graph, not from directory names.
8//!
9//! Invariants: deterministic per file set (BTree iteration only), hard
10//! token-bounded (the caller passes the budget; overflow collapses lines,
11//! never silently exceeds), role-labeled via the shared
12//! [`scc_graph::components::component_role`] classifier.
13
14use scc_core::estimate_tokens;
15use std::collections::BTreeMap;
16
17/// Skeleton budget policy: 10% of the startup total, floored so tiny repos
18/// still show their top level, capped so the skeleton stays in the hundreds
19/// of tokens. Receipt: default startup total is 20_000 → 800-token skeleton.
20// trace:v1 id=impl.crates-scc-context-src-skeleton.skeleton-budget work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
21pub fn skeleton_budget(total: usize) -> usize {
22    (total / 10).clamp(64, 800).min(total.max(64))
23}
24
25/// Rendered skeleton plus honesty counts.
26// trace:v1 id=impl.crates-scc-context-src-skeleton.skeleton work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
27pub struct Skeleton {
28    /// Rendered lines (no section header; the assembler adds that).
29    pub text: String,
30    /// Top-level entries shown.
31    pub top_entries: usize,
32    /// Indexed files folded into the tree (visible or collapsed).
33    pub files_seen: usize,
34    /// Lines collapsed away by caps or the token budget.
35    pub collapsed: usize,
36}
37
38/// Maximum top-level entries before the top list itself collapses.
39const MAX_TOP: usize = 64;
40/// Maximum expansion lines per production top-level directory.
41const MAX_EXPAND_PER_TOP: usize = 10;
42/// Maximum depth-3 lines per priority top-level directory (below).
43const MAX_DEPTH3_PER_TOP: usize = 8;
44/// Depth-3 file/subdir lines each, per priority top.
45const MAX_DEPTH3_KIDS: usize = 4;
46
47/// Manifest filenames that mark a directory as high-information.
48const MANIFESTS: &[&str] = &[
49    "Cargo.toml",
50    "package.json",
51    "go.mod",
52    "pyproject.toml",
53    "setup.cfg",
54    "pom.xml",
55    "build.gradle",
56];
57
58/// A top directory is high-information when it roots a package or a
59/// source tree: a manifest directly under it (`top/Cargo.toml`), a
60/// manifest one level down (`top/crate/Cargo.toml`), or a `src/` tree.
61/// Priority tops expand one level deeper (depth 3, capped) so workspace
62/// members, source files, and entrypoints show without raising the
63/// global depth (deep low-value trees still collapse).
64// trace:exempt reason=internal-detail
65fn priority_top(top: &str, members: &[String]) -> bool {
66    members.iter().any(|m| {
67        let rest = m.strip_prefix(top).unwrap_or(m).trim_start_matches('/');
68        if rest == "src" || rest.starts_with("src/") {
69            return true;
70        }
71        let mut segs = rest.split('/');
72        match (segs.next(), segs.next(), segs.next()) {
73            (Some(_), None, _) => MANIFESTS.contains(&rest),
74            (Some(_), Some(last), None) => MANIFESTS.contains(&last),
75            _ => false,
76        }
77    })
78}
79
80/// Build the skeleton from indexed repo-relative paths. Priority order:
81/// every top-level entry first (mandatory, collapses with a count only
82/// past MAX_TOP), then depth-2 expansions of production/example/sdk
83/// trees (test/fixture/benchmark trees render as one labeled line each),
84/// then depth-3 expansions of priority tops only. Lines that do not fit
85/// the token budget are dropped and counted, never cut mid-line.
86// trace:v1 id=impl.crates-scc-context-src-skeleton.build-skeleton work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-NX53P4B7
87pub fn build_skeleton(paths: &[String], budget_tokens: usize) -> Skeleton {
88    let mut tops: BTreeMap<String, Vec<String>> = BTreeMap::new();
89    let mut loose: Vec<String> = Vec::new();
90    for p in paths {
91        match p.split_once('/') {
92            Some((top, _)) => tops.entry(top.to_string()).or_default().push(p.clone()),
93            None => loose.push(p.clone()),
94        }
95    }
96    loose.sort();
97    loose.dedup();
98
99    let mut top_lines: Vec<String> = Vec::new();
100    let mut expansions: Vec<(bool, Vec<String>, Vec<String>)> = Vec::new();
101    for (top, members) in tops.iter().take(MAX_TOP) {
102        let role = scc_graph::components::component_role(std::slice::from_ref(top));
103        top_lines.push(format!("- {top}/ [{role}] ({} files)", members.len()));
104        if role == "production" || role == "example" || role == "sdk" {
105            let priority = priority_top(top, members);
106            let (d2, d3) = expand_top(top, members, priority);
107            expansions.push((priority, d2, d3));
108        }
109    }
110    for f in &loose {
111        top_lines.push(format!("- {f}"));
112    }
113    let omitted_tops = tops.len().saturating_sub(MAX_TOP);
114
115    let mut lines: Vec<String> = Vec::new();
116    let mut collapsed: usize = omitted_tops;
117    let mut used = 0usize;
118    for line in top_lines {
119        let cost = estimate_tokens(&line);
120        if used + cost > budget_tokens {
121            collapsed += 1;
122            continue;
123        }
124        used += cost;
125        lines.push(line);
126    }
127    for (priority, d2, d3) in &expansions {
128        for line in d2.iter().take(MAX_EXPAND_PER_TOP) {
129            let cost = estimate_tokens(line);
130            if used + cost > budget_tokens {
131                collapsed += 1;
132                continue;
133            }
134            used += cost;
135            lines.push(line.clone());
136        }
137        collapsed += d2.len().saturating_sub(MAX_EXPAND_PER_TOP);
138        // Depth 3 exists only for priority tops (built only for them, so
139        // non-priority collapse counts are untouched by this feature).
140        if *priority {
141            for line in d3.iter().take(MAX_DEPTH3_PER_TOP) {
142                let cost = estimate_tokens(line);
143                if used + cost > budget_tokens {
144                    collapsed += 1;
145                    continue;
146                }
147                used += cost;
148                lines.push(line.clone());
149            }
150            collapsed += d3.len().saturating_sub(MAX_DEPTH3_PER_TOP);
151        }
152    }
153    let text = if collapsed > 0 {
154        format!(
155            "{}\n... (+{collapsed} entries omitted over skeleton budget)",
156            lines.join("\n")
157        )
158    } else {
159        lines.join("\n")
160    };
161    Skeleton {
162        text,
163        top_entries: tops.len().min(MAX_TOP) + loose.len(),
164        files_seen: paths.len(),
165        collapsed,
166    }
167}
168
169/// Second-level entries of one top directory, deterministic. Returns
170/// `(depth2, depth3)`: depth-3 grandchildren are built ONLY for priority
171/// tops (workspace/package/source roots) and stay empty otherwise.
172// trace:exempt reason=internal-detail
173fn expand_top(top: &str, members: &[String], priority: bool) -> (Vec<String>, Vec<String>) {
174    let mut kids: BTreeMap<String, usize> = BTreeMap::new();
175    // kid dir -> its direct members (for depth-3 grandchildren)
176    let mut grand: BTreeMap<String, Vec<String>> = BTreeMap::new();
177    let mut loose_files: Vec<String> = Vec::new();
178    for m in members {
179        let rest = m.strip_prefix(top).unwrap_or(m).trim_start_matches('/');
180        match rest.split_once('/') {
181            Some((kid, _)) => {
182                *kids.entry(kid.to_string()).or_default() += 1;
183                if priority {
184                    grand.entry(kid.to_string()).or_default().push(m.clone());
185                }
186            }
187            None => loose_files.push(rest.to_string()),
188        }
189    }
190    loose_files.sort();
191    loose_files.dedup();
192    let mut d2 = Vec::new();
193    for f in loose_files.into_iter().take(8) {
194        d2.push(format!("  - {top}/{f}"));
195    }
196    for (kid, n) in &kids {
197        d2.push(format!("  - {top}/{kid}/ ({n} files)"));
198    }
199    let mut d3 = Vec::new();
200    if priority {
201        for (kid, paths) in &grand {
202            let prefix = format!("{top}/{kid}/");
203            let mut files: Vec<String> = Vec::new();
204            let mut subdirs: BTreeMap<String, usize> = BTreeMap::new();
205            for full in paths {
206                let rest = full.strip_prefix(&prefix).unwrap_or(full);
207                match rest.split_once('/') {
208                    Some((sub, _)) => *subdirs.entry(sub.to_string()).or_default() += 1,
209                    None => files.push(rest.to_string()),
210                }
211            }
212            files.sort();
213            files.dedup();
214            for f in files.into_iter().take(MAX_DEPTH3_KIDS) {
215                d3.push(format!("    - {prefix}{f}"));
216            }
217            for (sub, n) in subdirs.iter().take(MAX_DEPTH3_KIDS) {
218                d3.push(format!("    - {prefix}{sub}/ ({n} files)"));
219            }
220        }
221    }
222    (d2, d3)
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    // trace:exempt reason=test-helper
230    fn paths(xs: &[&str]) -> Vec<String> {
231        xs.iter().map(|s| s.to_string()).collect()
232    }
233
234    #[test]
235    // trace:v1 id=test.scc.context.skeleton-deterministic verifies=REQ-SI-NX53P4B7 exercises=impl.crates-scc-context-src-skeleton.build-skeleton
236    fn skeleton_is_deterministic_and_role_labeled() {
237        let a = build_skeleton(
238            &paths(&[
239                "crates/scc-core/src/lib.rs",
240                "crates/scc-cli/src/main.rs",
241                "fixtures/http-service-python/main.py",
242                "Cargo.toml",
243            ]),
244            800,
245        );
246        let b = build_skeleton(
247            &paths(&[
248                "Cargo.toml",
249                "fixtures/http-service-python/main.py",
250                "crates/scc-cli/src/main.rs",
251                "crates/scc-core/src/lib.rs",
252            ]),
253            800,
254        );
255        assert_eq!(a.text, b.text, "input order must not matter");
256        assert!(a.text.contains("crates/ [production]"), "{}", a.text);
257        assert!(a.text.contains("fixtures/ [fixture]"), "{}", a.text);
258        assert!(a.text.contains("- Cargo.toml"), "{}", a.text);
259        assert!(a.text.contains("scc-core/"), "{}", a.text);
260        assert_eq!(a.collapsed, 0);
261        assert_eq!(a.files_seen, 4);
262    }
263
264    #[test]
265    // trace:v1 id=test.scc.context.skeleton-budget verifies=REQ-SI-NX53P4B7 exercises=impl.crates-scc-context-src-skeleton.build-skeleton
266    fn skeleton_never_exceeds_its_budget() {
267        let many: Vec<String> = (0..300)
268            .map(|i| format!("crates/svc{i}/src/mod{i}/file{i}.rs"))
269            .collect();
270        let sk = build_skeleton(&many, 200);
271        assert!(
272            estimate_tokens(&sk.text) <= 200,
273            "skeleton must fit budget: {}",
274            estimate_tokens(&sk.text)
275        );
276        assert!(sk.collapsed > 0, "overflow must be counted, not silent");
277        assert!(sk.text.contains("omitted over skeleton budget"));
278    }
279
280    #[test]
281    // trace:v1 id=test.scc.context.skeleton-depth3 verifies=REQ-SI-NX53P4B7 exercises=impl.crates-scc-context-src-skeleton.build-skeleton
282    fn skeleton_expands_priority_tops_one_level_deeper() {
283        // crates/ roots a package (crates/scc-core/Cargo.toml): depth-3
284        // grandchildren show. plains/ has no manifest: depth-2 only.
285        let sk = build_skeleton(
286            &paths(&[
287                "crates/scc-core/Cargo.toml",
288                "crates/scc-core/src/lib.rs",
289                "crates/scc-core/src/resolve.rs",
290                "plains/a.txt",
291                "plains/sub/b.txt",
292            ]),
293            800,
294        );
295        assert!(sk.text.contains("    - crates/scc-core/src/ (2 files)"), "{}", sk.text);
296        assert!(sk.text.contains("    - crates/scc-core/Cargo.toml"), "{}", sk.text);
297        assert!(!sk.text.contains("      "), "no depth-4: {}", sk.text);
298        // budget still binds with depth-3 content
299        let big: Vec<String> = (0..200)
300            .map(|i| format!("crates/svc{i}/Cargo.toml"))
301            .chain((0..200).map(|i| format!("crates/svc{i}/src/lib.rs")))
302            .collect();
303        let sk2 = build_skeleton(&big, 200);
304        assert!(
305            estimate_tokens(&sk2.text) <= 200,
306            "skeleton must fit budget: {}",
307            estimate_tokens(&sk2.text)
308        );
309    }
310
311    #[test]
312    // trace:v1 id=test.scc.context.skeleton-budget-policy verifies=REQ-SI-NX53P4B7 exercises=impl.crates-scc-context-src-skeleton.skeleton-budget
313    fn skeleton_budget_is_bounded_fraction() {
314        assert_eq!(skeleton_budget(20_000), 800);
315        assert_eq!(skeleton_budget(1_000), 100);
316        assert!(skeleton_budget(64) <= 64);
317    }
318}