Skip to main content

teamcraft/mcp/tools/
insight_tools.rs

1//! Read-only checks: BOM health, duplicate files, similar parts.
2
3use super::*;
4
5pub(super) async fn bom_health(st: &AppState, me: &Actor, a: &Args<'_>) -> AppResult<Value> {
6    let rev = revision_arg(st, me, a).await?;
7    let rule = a.rule()?.unwrap_or_else(|| default_rule(&rev));
8    Ok(json!(insights::bom_health(&st.db, me, rev.id, rule).await?))
9}
10
11/// Duplicate contents grouped by SHA-256 (largest first, as returned).
12pub fn group_duplicates(rows: &[insights::DuplicateFile]) -> Vec<Value> {
13    let mut order: Vec<&str> = Vec::new();
14    let mut groups: BTreeMap<&str, Vec<&insights::DuplicateFile>> = BTreeMap::new();
15    for r in rows {
16        let entry = groups.entry(r.sha256.as_str()).or_default();
17        if entry.is_empty() {
18            order.push(r.sha256.as_str());
19        }
20        entry.push(r);
21    }
22    order
23        .into_iter()
24        .map(|sha| {
25            let copies = &groups[sha];
26            json!({
27                "sha256": sha,
28                "size_bytes": copies[0].size_bytes,
29                "copies": copies.iter().map(|c| json!({
30                    "item": c.item_number,
31                    "revision": c.revision_code,
32                    "state": c.revision_state,
33                    "filename": c.filename,
34                })).collect::<Vec<_>>(),
35            })
36        })
37        .collect()
38}
39
40pub(super) async fn find_duplicate_files(
41    st: &AppState,
42    me: &Actor,
43    a: &Args<'_>,
44) -> AppResult<Value> {
45    let limit = a.limit("limit", 100, 1000)?;
46    let revision = match a.opt_str("item")? {
47        Some(number) => {
48            Some(revision_at(st, me, &number, a.opt_str("revision")?.as_deref()).await?)
49        }
50        None => None,
51    };
52    let rows =
53        insights::duplicate_files(&st.db, me, revision.as_ref().map(|r| r.id), limit).await?;
54    Ok(json!({
55        "scope": revision.as_ref().map(revisions::label),
56        "groups": group_duplicates(&rows),
57        "truncated": rows.len() as i64 >= limit,
58    }))
59}
60
61pub(super) async fn find_similar_parts(
62    st: &AppState,
63    me: &Actor,
64    a: &Args<'_>,
65) -> AppResult<Value> {
66    let (number, code) = parse_item_ref(&a.req_str("item")?);
67    let code = match a.opt_str("revision")? {
68        Some(c) => Some(c),
69        None => code,
70    };
71    let rev = revision_at(st, me, &number, code.as_deref()).await?;
72    let options = similar::Options {
73        limit: a.limit("limit", 10, similar::MAX_LIMIT)?,
74        ignore_scale: a.flag("ignore_scale", false)?,
75    };
76    let file = a.opt_str("file")?;
77    let report = similar::for_revision(
78        st,
79        me,
80        &rev,
81        file.as_deref(),
82        options,
83        std::time::Duration::from_secs(60),
84    )
85    .await?;
86    Ok(json!({
87        "source": revisions::label(&rev),
88        "status": report.status,
89        "message": report.message,
90        "file": report.source.as_ref().map(|s| &s.filename),
91        "signature": report.signature,
92        "ignore_scale": report.ignore_scale,
93        "similar": report.candidates.iter().map(|c| json!({
94            "item": c.item_number,
95            "revision": c.revision_code,
96            "name": c.name,
97            "type": c.item_type,
98            "state": c.state,
99            "project": c.project_code,
100            "file": c.filename,
101            "score": c.score,
102            "shape": c.shape,
103            "size": c.size,
104            "identical": c.identical,
105            "explanation": c.explanation,
106        })).collect::<Vec<_>>(),
107    }))
108}