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
Version 0.3.0: new copper reader, dome via filter, every layer read from the photos, net evidence, measured confidence;…
e874d7d
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
//! The flow file (flows/flow.json, compiled in): the ordered steps, the artifacts that prove each one
//! is done, and what the AI must do in it. `next` names the first step whose artifacts are missing.
//! The binary does the deterministic parts; the AI does the thinking the `do` text asks for.
use serde::Deserialize;
use std::path::Path;
#[derive(Deserialize, Clone)]
pub struct Step { pub name: String, pub done: Vec<String>, #[serde(rename = "do")] pub todo: String }
#[derive(Deserialize)]
#[allow(dead_code)]
pub struct Flow { pub name: String, pub description: String, pub steps: Vec<Step> }
pub fn load() -> Flow { serde_json::from_str(include_str!("../flows/flow.json")).expect("flows/flow.json is valid") }
/// A snapshot copies the project as built from these: when one changes, the saved copy no longer shows it.
pub const SNAPSHOT_INPUTS: [&str; 2] = ["copper.json", "parts.json"];
fn is_snapshot(pat: &str) -> bool { pat.starts_with("snapshots/") && pat.ends_with("manifest.json") }
/// A recorded hash: of the file, or with `geometry:` of its copper shapes only (`attrib::geometry_hash`), so attribution,
/// which writes nets and nothing else, does not make what was measured from the shapes stale.
pub fn matches_hash(path: &Path, want: &str) -> bool {
if want.starts_with("geometry:") { return crate::attrib::geometry_hash(path) == want; }
crate::sha256(path) == want
}
/// The files a passed check or a saved snapshot read that changed since (empty when it is fresh).
pub fn stale(flow_dir: &Path, pat: &str) -> Vec<String> {
let Ok(t) = std::fs::read_to_string(flow_dir.join(pat)) else { return vec![] };
let v: serde_json::Value = serde_json::from_str(&t).unwrap_or_default();
if is_snapshot(pat) {
// a snapshot from before inputs were recorded cannot show it is current, so it is not
let Some(inputs) = v["inputs"].as_object() else { return SNAPSHOT_INPUTS.iter().map(|f| format!("{f} (the snapshot does not record it)")).collect() };
return inputs.iter().filter(|(f, h)| h.as_str() != Some(crate::sha256(&flow_dir.join(f.as_str())).as_str())).map(|(f, _)| f.clone()).collect();
}
v["files"].as_object().into_iter().flatten()
.filter(|(f, h)| h.as_str().map(|w| !matches_hash(&flow_dir.join(f.as_str()), w)).unwrap_or(true))
.map(|(f, _)| f.clone()).collect()
}
/// One artifact pattern, relative to the flow folder; `*` matches within the last path segment.
/// A `checks/<name>.ok` or a snapshot counts only while every file it read is unchanged.
pub fn exists(flow_dir: &Path, pat: &str) -> bool {
if (pat.starts_with("checks/") && pat.ends_with(".ok")) || is_snapshot(pat) {
return flow_dir.join(pat).is_file() && stale(flow_dir, pat).is_empty();
}
let full = flow_dir.join(pat);
let name = full.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.contains('*') { return full.exists(); }
let (pre, post) = name.split_once('*').unwrap_or((name, ""));
let dir = full.parent().unwrap_or(flow_dir);
std::fs::read_dir(dir).map(|rd| rd.flatten().any(|e| {
let n = e.file_name().to_string_lossy().to_string();
n.starts_with(pre) && n.ends_with(post) && n.len() >= pre.len() + post.len()
})).unwrap_or(false)
}
pub fn current<'a>(flow: &'a Flow, flow_dir: &Path) -> Option<&'a Step> {
flow.steps.iter().find(|s| !s.done.iter().all(|p| exists(flow_dir, p)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_step_is_the_first_with_missing_artifacts() {
let d = std::env::temp_dir().join(format!("flow-test-{}", std::process::id()));
std::fs::create_dir_all(d.join("checks")).unwrap();
std::fs::write(d.join("state.json"), "{}").unwrap();
let f = load();
assert_eq!(current(&f, &d).map(|s| s.name.as_str()), Some("intake"));
std::fs::write(d.join("state.json"), "{}").unwrap();
std::fs::write(d.join("prep.json"), "{}").unwrap();
let h = crate::sha256(&d.join("prep.json"));
std::fs::write(d.join("checks/prep.ok"), format!("{{\"files\": {{\"prep.json\": \"{h}\"}}}}")).unwrap();
assert_eq!(current(&f, &d).map(|s| s.name.as_str()), Some("scale"));
// editing the artifact after its check makes the check stale and the step current again
std::fs::write(d.join("prep.json"), "{\"photos\": []}").unwrap();
assert_eq!(stale(&d, "checks/prep.ok"), vec!["prep.json".to_string()]);
assert_eq!(current(&f, &d).map(|s| s.name.as_str()), Some("intake"));
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn attribution_does_not_make_the_copper_check_stale_but_a_new_shape_does() {
let d = std::env::temp_dir().join(format!("flow-geom-{}", std::process::id()));
std::fs::create_dir_all(d.join("checks")).unwrap();
let c = |net: &str, x: f64| serde_json::json!({"zones": [{"id": "z", "net": net, "fieldTags": {"net": "MEASURED"}, "netEvidence": {"rung": "copper"}, "polygon_mm": [[x, 0], [1, 0], [1, 1]]}]}).to_string();
std::fs::write(d.join("copper.json"), c("UNK_1", 0.0)).unwrap();
let g = crate::attrib::geometry_hash(&d.join("copper.json"));
std::fs::write(d.join("checks/copper.ok"), format!("{{\"files\": {{\"copper.json\": \"{g}\"}}}}")).unwrap();
std::fs::write(d.join("copper.json"), c("GND", 0.0)).unwrap();
assert!(stale(&d, "checks/copper.ok").is_empty());
std::fs::write(d.join("copper.json"), c("GND", 0.5)).unwrap();
assert_eq!(stale(&d, "checks/copper.ok"), vec!["copper.json".to_string()]);
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn a_snapshot_goes_stale_when_the_copper_changes() {
let d = std::env::temp_dir().join(format!("flow-snap-{}", std::process::id()));
std::fs::create_dir_all(d.join("snapshots/mimic")).unwrap();
std::fs::write(d.join("copper.json"), "{}").unwrap();
std::fs::write(d.join("parts.json"), "{}").unwrap();
let m = "snapshots/mimic/manifest.json";
// one from before the input record is stale, the state of the published run
std::fs::write(d.join(m), "{\"name\": \"mimic\", \"files\": {\"a.kicad_pcb\": \"x\"}}").unwrap();
assert!(!exists(&d, m));
assert!(stale(&d, m)[0].contains("copper.json"));
let (c, p) = (crate::sha256(&d.join("copper.json")), crate::sha256(&d.join("parts.json")));
std::fs::write(d.join(m), format!("{{\"inputs\": {{\"copper.json\": \"{c}\", \"parts.json\": \"{p}\"}}}}")).unwrap();
assert!(exists(&d, m));
assert!(stale(&d, m).is_empty());
std::fs::write(d.join("copper.json"), "{\"zones\": []}").unwrap();
assert!(!exists(&d, m));
assert_eq!(stale(&d, m), vec!["copper.json".to_string()]);
std::fs::remove_dir_all(&d).ok();
}
}