main
Caleb Version 0.3.0: new copper reader, dome via filter, every layer read from the photos, net evidence, measured confidence;… e874d7d
//! What each step found, read from its artifacts: a one-line summary (shown on the step's line in the
//! AI Flow timeline) and a table (shown in the flow window's Findings tab). Nothing here is
//! judgement; it lists what the files say, so the window always matches the record.
use crate::items::{self, read_json};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::path::Path;

fn s(v: &Value, k: &str) -> String {
    match &v[k] { Value::String(x) => x.clone(), Value::Null => String::new(), Value::Array(a) => a.iter().map(|x| x.as_str().map(str::to_string).unwrap_or_else(|| x.to_string())).collect::<Vec<_>>().join(", "), x => x.to_string() }
}

fn table(cols: &[&str], rows: Vec<Vec<String>>) -> Value { json!({"cols": cols, "rows": rows}) }

fn tags(list: &[Value]) -> String {
    let mut m: BTreeMap<String, usize> = BTreeMap::new();
    for v in list { *m.entry(s(v, "tag").to_lowercase()).or_default() += 1; }
    items::TAGS.iter().filter_map(|k| m.get(&k.to_lowercase()).map(|n| format!("{n} {}", k.to_lowercase()))).collect::<Vec<_>>().join(", ")
}

fn arr(d: &Value, k: &str) -> Vec<Value> { d[k].as_array().cloned().unwrap_or_default() }

/// {step: {summary, tables: [{title, cols, rows}]}} for every step with something to show.
pub fn all(flow: &Path) -> Value {
    let mut out = serde_json::Map::new();
    let mut put = |step: &str, summary: String, tables: Vec<(String, Value)>| {
        let t: Vec<Value> = tables.into_iter().map(|(title, mut v)| { v["title"] = json!(title); v }).collect();
        out.insert(step.into(), json!({"summary": summary, "tables": t}));
    };

    if let Ok(d) = read_json(&flow.join("photos.json")) {
        let p = arr(&d, "photos");
        let needs = p.iter().filter(|x| !s(x, "needs").is_empty()).count();
        put("intake", format!("{} photos described{}", p.len(), if needs > 0 { format!(", {needs} with better shots requested") } else { String::new() }),
            vec![("Photos".into(), table(&["Photo", "Side", "Covers", "Quality", "Needs"], p.iter().map(|x| vec![s(x, "file"), s(x, "side"), s(x, "covers"), s(x, "quality"), s(x, "needs")]).collect()))]);
    }
    if let Ok(d) = read_json(&flow.join("prep.json")) {
        let ph = arr(&d, "photos");
        let rows = ph.iter().map(|x| vec![s(x, "file"), s(x, "side"), format!("{:.0}", x["sharpness"].as_f64().unwrap_or(0.0)), x["holes"].as_array().map(|a| a.len()).unwrap_or(0).to_string(), x["rows"].as_array().map(|a| a.len()).unwrap_or(0).to_string(), x["bodies"].as_array().map(|a| a.len()).unwrap_or(0).to_string()]).collect();
        put("intake", format!("{} photos measured; bottom turned over {} ({} holes agree)", ph.len(), if s(&d["flip"], "axis") == "y" { "top-to-bottom" } else { "left-to-right" }, s(&d["flip"], "matches")),
            vec![("Photos".into(), table(&["Photo", "Side", "Sharpness", "Holes", "Hole rows", "Bodies"], rows))]);
    }
    if let Ok(d) = read_json(&flow.join("scale.json")).map(|d| if d["pxPerMm"].is_number() { d } else { Value::Null }) { if d.is_object() {
        put("scale", format!("{} px/mm; board {} x {} mm, corners {} mm; bottom aligned on {} holes ({} mm)", s(&d, "pxPerMm"), d["board_mm"][0], d["board_mm"][1], s(&d, "corner_mm"), s(&d["alignment"], "pairs"), s(&d["alignment"], "residual_mm")),
            vec![("Scale".into(), table(&["Method", "Body", "Body says", "Source"], vec![vec![s(&d, "method"), s(&d["body"], "id"), s(&d["body"], "pxPerMm"), s(&d["body"], "source")]]))]);
    } }
    if let Ok(d) = read_json(&flow.join("scale.json")).map(|d| if d["pxPerMm"].is_number() { Value::Null } else { d }) { if d.is_object() {
        let ph = d["photos"].as_object().cloned().unwrap_or_default();
        let rows: Vec<Vec<String>> = ph.iter().map(|(k, v)| vec![k.clone(), s(v, "pxPerMm"), format!("{:.1}%", v["spread"].as_f64().unwrap_or(0.0) * 100.0), v["refs"].as_array().map(|a| a.iter().map(|r| s(r, "what")).collect::<Vec<_>>().join("; ")).unwrap_or_default()]).collect();
        let sum = ph.iter().map(|(k, v)| format!("{k} {} px/mm", s(v, "pxPerMm"))).collect::<Vec<_>>().join(", ");
        put("scale", sum, vec![("Scale per photo".into(), table(&["Photo", "px/mm", "Spread", "References"], rows))]);
    } }
    if let Ok(d) = read_json(&flow.join("parts.json")) {
        let p = arr(&d, "parts");
        let open = items::items_of("parts.json", &d).iter().filter(|i| i.open()).count();
        let rows = p.iter().map(|x| vec![s(x, "ref"), s(x, "value"), s(x, "package"), s(x, "marking"), s(x, "function"), s(x, "tag"), s(&x["pin1"], "mark"), s(x, "tests")]).collect();
        put("parts", format!("{} parts: {}; {open} need a test", p.len(), tags(&p)),
            vec![("Parts".into(), table(&["Ref", "Value", "Package", "Marking", "Function", "Tag", "Pin 1", "Tests"], rows))]);
    }
    if let Ok(d) = read_json(&flow.join("blocks.json")) {
        let b = arr(&d, "blocks");
        let rows = b.iter().map(|x| vec![s(x, "name"), x["parts"].as_array().map(|a| a.len()).unwrap_or(0).to_string(), s(x, "function"), s(x, "tag")]).collect();
        put("blocks", format!("{} blocks: {}", b.len(), b.iter().map(|x| s(x, "name")).collect::<Vec<_>>().join(", ")),
            vec![("Blocks".into(), table(&["Block", "Parts", "Function", "Tag"], rows))]);
    }
    if let Ok(d) = read_json(&flow.join("copper.json")) {
        let (z, t, v) = (arr(&d, "zones"), arr(&d, "traces"), arr(&d, "vias"));
        let mut kinds: BTreeMap<String, usize> = BTreeMap::new();
        for x in &v { *kinds.entry(s(x, "kind")).or_default() += 1; }
        let kinds_s = kinds.iter().map(|(k, n)| format!("{n} {k}")).collect::<Vec<_>>().join(", ");
        put("copper", format!("{} zones, {} traces, {} vias ({kinds_s})", z.len(), t.len(), v.len()), vec![
            ("Zones".into(), table(&["Zone", "Net", "Layer", "Tag"], z.iter().map(|x| vec![s(x, "id"), s(x, "net"), s(x, "layer"), s(x, "tag")]).collect())),
            ("Vias by kind".into(), table(&["Kind", "Count"], kinds.iter().map(|(k, n)| vec![k.clone(), n.to_string()]).collect())),
            ("Traces".into(), table(&["Trace", "Net", "Layer", "Width mm", "Tag"], t.iter().map(|x| vec![s(x, "id"), s(x, "net"), s(x, "layer"), s(x, "width_mm"), s(x, "tag")]).collect())),
        ]);
    }
    if let Ok(d) = read_json(&flow.join("checks/mimic.ok")) {
        let m = &d["summary"];
        put("mimic", format!("{} footprints on the board, {} zones, matched to parts.json", s(m, "footprints"), s(m, "zones")),
            vec![("Board".into(), table(&["File", "sha256"], vec![vec![s(m, "pcb"), s(m, "pcbSha256").chars().take(16).collect()]]))]);
    }
    if let Ok(rd) = std::fs::read_dir(flow.join("snapshots")) {
        let mut rows = vec![];
        for e in rd.flatten() {
            if let Ok(m) = read_json(&e.path().join("manifest.json")) { rows.push(vec![s(&m, "name"), m["files"].as_object().map(|o| o.len()).unwrap_or(0).to_string(), s(&m, "at"), s(&m, "source")]); }
        }
        if !rows.is_empty() { put("snapshot", format!("{} snapshot(s) saved", rows.len()), vec![("Snapshots".into(), table(&["Name", "Files", "At", "From"], rows))]); }
    }
    let nets = read_json(&flow.join("nets.json")).ok();
    let layers = read_json(&flow.join("layers.json")).ok();
    if nets.is_some() || layers.is_some() {
        let mut tabs = vec![];
        let mut sum = vec![];
        if let Some(l) = &layers {
            sum.push(format!("{} layers proposed", s(l, "proposed")));
            tabs.push(("Layer candidates".into(), table(&["Layers", "Open", "DRC errors", "Run"], arr(l, "candidates").iter().map(|c| vec![s(c, "layers"), s(c, "open"), s(c, "drcErrors"), s(c, "run")]).collect())));
            tabs.push(("Planes (inferred)".into(), table(&["Layer", "Net", "Tag", "Why"], arr(l, "planes").iter().map(|p| vec![s(p, "layer"), s(p, "net"), s(p, "tag"), s(p, "why")]).collect())));
        }
        if let Some(n) = &nets {
            let list = arr(n, "nets");
            sum.push(format!("{} nets ({})", list.len(), tags(&list)));
            tabs.push(("Nets".into(), table(&["Net", "Pins", "Tag", "Tests", "Why"], list.iter().map(|x| vec![s(x, "name"), x["pins"].as_array().map(|a| a.len()).unwrap_or(0).to_string(), s(x, "tag"), s(x, "tests"), s(x, "why")]).collect())));
        }
        put("connect", sum.join(", "), tabs);
    }
    if let Ok(r) = read_json(&flow.join("review.json")) {
        // only measured passes count: typed levels from an older run are not shown as confidence
        let p: Vec<Value> = arr(&r, "passes").into_iter().filter(|x| x["scores"].is_object()).collect();
        let last = p.last().cloned().unwrap_or(json!({}));
        let conf = crate::confidence::CATEGORIES.iter().map(|k| { let c = &last["scores"]["categories"][*k]; vec![k.to_string(), s(c, "score"), s(c, "limit"), s(c, "raise")] }).collect();
        let summary = if p.is_empty() { "no measured pass yet".to_string() } else { format!("{} pass(es): {}, overall {} of target {}", p.len(), s(&last, "verdict"), s(&last["scores"], "overall"), s(&last, "target")) };
        put("review", summary, vec![
            ("Confidence (last pass, computed)".into(), table(&["Category", "Score", "Limited by", "Most likely to raise it"], conf)),
            ("Passes".into(), table(&["Pass", "Verdict", "Overall", "Note", "At"], p.iter().map(|x| vec![s(x, "n"), s(x, "verdict"), s(&x["scores"], "overall"), s(x, "note"), s(x, "at")]).collect())),
        ]);
    }
    if let Ok(t) = read_json(&flow.join("tests.json")) {
        let list = arr(&t, "tests");
        let open = items::all(flow).iter().filter(|i| i.open()).count();
        put("tests", format!("{} tests settle {open} open item(s)", list.len()),
            vec![("Tests".into(), table(&["Test", "Settles", "Method", "Equipment", "Expected", "If not"], list.iter().map(|x| vec![s(x, "id"), s(x, "settles"), s(x, "method"), s(x, "equipment"), s(x, "expected"), s(x, "ifNot")]).collect()))]);
    }
    if flow.join("report.md").is_file() {
        put("report", "report.md and report-tables.md written".into(), vec![]);
    }
    Value::Object(out)
}