app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Add the Rust source, flow definition, assets and install scripts
cf9809b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
//! Every claim the flow makes is an item: a part, a block, a zone, a trace, a via, a net, a plane, the
//! outline, the layer proposal. Each item carries an evidence tag saying how it is known, the files
//! that show it, and (when it is not seen) the tests that would settle it. The checks and the report
//! read items the same way whichever artifact they come from.
use serde_json::Value;
use std::path::{Path, PathBuf};
/// How an item is known, strongest first.
pub const TAGS: [&str; 5] = ["SEEN", "MEASURED", "DATASHEET", "INFERRED", "UNKNOWN"];
/// (artifact file, array key, id key, id prefix): where items live.
pub const KINDS: &[(&str, &str, &str, &str)] = &[
("parts.json", "parts", "ref", ""),
("blocks.json", "blocks", "id", "block:"),
("copper.json", "zones", "id", "zone:"),
("copper.json", "traces", "id", "trace:"),
("copper.json", "vias", "id", "via:"),
("nets.json", "nets", "name", "net:"),
("layers.json", "planes", "layer", "plane:"),
];
/// The artifacts that hold items, in flow order.
pub const ARTIFACTS: [&str; 5] = ["parts.json", "blocks.json", "copper.json", "nets.json", "layers.json"];
#[derive(Clone, Debug)]
pub struct Item {
pub id: String,
pub artifact: String,
pub kind: String,
pub tag: String,
/// Per-field tags (`fieldTags: {"value": "INFERRED"}`), for an item seen in part and inferred in part.
pub field_tags: Vec<(String, String)>,
pub tests: Vec<String>,
pub evidence: Vec<String>,
pub source: String,
}
pub fn is_open(tag: &str) -> bool { tag == "INFERRED" || tag == "UNKNOWN" }
impl Item {
/// The item is not fully known: its own tag or one of its field tags is INFERRED or UNKNOWN.
pub fn open(&self) -> bool { is_open(&self.tag) || self.field_tags.iter().any(|(_, t)| is_open(t)) }
/// The tags of the item and its fields that are open, for messages.
pub fn open_what(&self) -> Vec<String> {
let mut v = vec![];
if is_open(&self.tag) { v.push(self.tag.clone()); }
for (f, t) in &self.field_tags { if is_open(t) { v.push(format!("{f} {t}")); } }
v
}
}
fn strs(v: &Value) -> Vec<String> {
match v {
Value::String(s) if !s.is_empty() => vec![s.clone()],
Value::Array(a) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).filter(|s| !s.is_empty()).collect(),
_ => vec![],
}
}
fn item(id: String, artifact: &str, kind: &str, v: &Value, default_evidence: &[String]) -> Item {
let mut evidence = strs(&v["evidence"]);
if evidence.is_empty() { evidence = default_evidence.to_vec(); }
let field_tags = v["fieldTags"].as_object().map(|o| o.iter().map(|(k, t)| (k.clone(), t.as_str().unwrap_or("").to_string())).collect()).unwrap_or_default();
Item {
id, artifact: artifact.into(), kind: kind.into(),
tag: v["tag"].as_str().unwrap_or("").to_string(),
field_tags, tests: strs(&v["tests"]), evidence,
source: v["source"].as_str().unwrap_or("").to_string(),
}
}
/// The items in one artifact. Arrays per KINDS, plus the singletons: copper.json `outline` and the
/// layer proposal (layers.json's own tag).
pub fn items_of(artifact: &str, doc: &Value) -> Vec<Item> {
let dflt = strs(&doc["defaultEvidence"]);
let mut out = vec![];
for (file, key, idk, pre) in KINDS {
if *file != artifact { continue; }
for v in doc[*key].as_array().into_iter().flatten() {
let id = v[*idk].as_str().unwrap_or("").to_string();
out.push(item(format!("{pre}{id}"), artifact, key, v, &dflt));
}
}
if artifact == "copper.json" && doc["outline"].is_object() { out.push(item("outline".into(), artifact, "outline", &doc["outline"], &dflt)); }
if artifact == "layers.json" && doc.get("proposed").is_some() { out.push(item("layers".into(), artifact, "layers", doc, &dflt)); }
out
}
pub fn read_json(p: &Path) -> Result<Value, String> {
let t = std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display()))?;
serde_json::from_str(&t).map_err(|e| format!("{}: not valid JSON: {e}", p.display()))
}
/// Every item in every artifact that exists in the flow folder.
pub fn all(flow_dir: &Path) -> Vec<Item> {
ARTIFACTS.iter().filter_map(|a| read_json(&flow_dir.join(a)).ok().map(|d| items_of(a, &d))).flatten().collect()
}
/// An evidence path: absolute, or relative to the flow folder, or to the project folder above it.
pub fn resolve(flow_dir: &Path, rel: &str) -> Option<PathBuf> {
let p = Path::new(rel);
if p.is_absolute() { return p.exists().then(|| p.to_path_buf()); }
let a = flow_dir.join(p);
if a.exists() { return Some(a); }
let b = flow_dir.parent().map(|d| d.join(p))?;
b.exists().then_some(b)
}
/// The rules every item meets, whatever artifact it is in. Problems are pushed to `errs`.
pub fn validate(flow_dir: &Path, it: &Item, errs: &mut Vec<String>) {
let id = if it.id.is_empty() || it.id.ends_with(':') { format!("an item in {} with no id", it.artifact) } else { it.id.clone() };
if !TAGS.contains(&it.tag.as_str()) {
errs.push(format!("{id}: tag {:?} is not one of {}", it.tag, TAGS.join(", ")));
}
for (f, t) in &it.field_tags {
if !TAGS.contains(&t.as_str()) { errs.push(format!("{id}: fieldTags.{f} {t:?} is not one of {}", TAGS.join(", "))); }
}
if matches!(it.tag.as_str(), "SEEN" | "MEASURED") {
if it.evidence.is_empty() { errs.push(format!("{id}: tagged {} but names no evidence file (a crop that shows it)", it.tag)); }
}
for e in &it.evidence {
if resolve(flow_dir, e).is_none() { errs.push(format!("{id}: evidence file {e} is not there")); }
}
let ds = it.tag == "DATASHEET" || it.field_tags.iter().any(|(_, t)| t == "DATASHEET");
if ds && it.source.is_empty() { errs.push(format!("{id}: tagged DATASHEET but has no source (document, revision, page or table)")); }
if it.open() && it.tests.is_empty() {
errs.push(format!("{id}: {} but names no test that would settle it (tests: [\"T..\"])", it.open_what().join(", ")));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn items_and_rules() {
let d = std::env::temp_dir().join(format!("items-test-{}", std::process::id()));
std::fs::create_dir_all(d.join("evidence")).unwrap();
std::fs::write(d.join("evidence/u1.jpg"), "x").unwrap();
let doc = json!({"parts": [
{"ref": "U1", "tag": "SEEN", "evidence": ["evidence/u1.jpg"], "fieldTags": {"mpn": "INFERRED"}, "tests": ["T1"]},
{"ref": "R1", "tag": "INFERRED"},
{"ref": "C1", "tag": "DATASHEET"},
{"ref": "Q1", "tag": "SEEN", "evidence": ["evidence/missing.jpg"]}
]});
let items = items_of("parts.json", &doc);
assert_eq!(items.len(), 4);
let mut e = vec![];
for it in &items { validate(&d, it, &mut e); }
assert!(e.iter().any(|m| m.starts_with("R1:") && m.contains("no test")), "{e:?}");
assert!(e.iter().any(|m| m.starts_with("C1:") && m.contains("no source")), "{e:?}");
assert!(e.iter().any(|m| m.starts_with("Q1:") && m.contains("not there")), "{e:?}");
assert!(!e.iter().any(|m| m.starts_with("U1:")), "{e:?}");
assert!(items[0].open());
std::fs::remove_dir_all(&d).ok();
}
}