← Commit history

0.1.40: per-run clips pages; a later run links its own page instead of overwriting the first run's

John Lauer ·7c8f84deb8 ·6d ago ·parent 91abe7e
7 files changed +45−18
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.39"+version = "0.1.40" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.39"+version = "0.1.40" dependencies = [  "serde",  "serde_json",@@ -41,7 +41,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "serde",@@ -50,7 +50,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -60,7 +60,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "serde",@@ -69,7 +69,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "serde",@@ -78,7 +78,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -88,7 +88,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.39"+version = "0.1.40" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -98,7 +98,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.39"+version = "0.1.40" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.39"+version = "0.1.40" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+29−5
@@ -232,20 +232,44 @@ fn publish_clips(r: &Run, dir: &Path, page: &str, push: bool) -> Result<(String,     let (md, media) = preboard::clips_page(&r.data, dir, &notes, &now());     let stage = dir.join("clips-page");     std::fs::create_dir_all(stage.join("docs").join("clips")).map_err(|e| format!("{}: {e}", stage.display()))?;-    std::fs::write(stage.join("docs").join("clips.md"), &md).map_err(|e| e.to_string())?;+    // docs/clips.md belongs to the first run that published it (a hidden marker says which). A later+    // run on the same page writes docs/clips-<run>.md and adds one link to it in docs/clips.md,+    // so a second run never overwrites the first run's clips.+    let id = r.data["reportId"].as_str().map(str::to_string).unwrap_or_else(|| report_id(r));+    let marker = format!("<!-- aiflow-clips-run: {id} -->");+    let remote = std::process::Command::new("adom-wiki").args(["repo", "show", page, "docs/clips.md"]).output().ok()+        .filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).to_string()).filter(|t| t.trim_start().starts_with('#') || t.contains("aiflow-clips-run"));+    let owner = match &remote { None => true, Some(t) => t.contains(&marker) };+    let doc = if owner { "docs/clips.md".to_string() } else { format!("docs/clips-{id}.md") };+    let md = if owner { format!("{marker}\n{md}") } else { format!("{marker}\n{}", md.replacen("# Build clips", &format!("# Build clips: run {id}"), 1)) };+    std::fs::write(stage.join(&doc), &md).map_err(|e| e.to_string())?;+    let mut index_update: Option<String> = None;+    if let (false, Some(t)) = (owner, &remote) {+        let link = format!("clips-{id}.md");+        if !t.contains(&link) {+            let line = format!("Clips of a later run on this page: [run {id}]({link}).");+            let mut lines: Vec<&str> = t.lines().collect();+            let at = lines.iter().position(|l| l.starts_with("# ")).map(|i| i + 1).unwrap_or(0);+            lines.insert(at, ""); lines.insert(at + 1, &line);+            let updated = lines.join("\n") + "\n";+            std::fs::write(stage.join("docs").join("clips.md"), &updated).map_err(|e| e.to_string())?;+            index_update = Some(updated);+        }+    }     for (src, rel) in &media {         let dest = stage.join(rel);         let same = std::fs::metadata(&dest).ok().zip(std::fs::metadata(src).ok()).map(|(a, b)| a.len() == b.len()).unwrap_or(false);         if !same { std::fs::copy(src, &dest).map_err(|e| format!("copy {}: {e}", src.display()))?; }     }     let clips = md.matches("\n### Clip ").count();-    let mut files: Vec<String> = vec!["docs/clips.md".into()];+    let mut files: Vec<String> = vec![doc.clone()];+    if index_update.is_some() { files.push("docs/clips.md".into()); }     files.extend(media.iter().map(|(_, rel)| rel.clone()));     let banned = std::env::var("AIFLOW_BANNED_RE").ok();     let scrubbed = preboard::scrub(&md, banned.as_deref());-    let url = format!("https://wiki.adom.inc/{page}/files/docs/clips.md");+    let url = format!("https://wiki.adom.inc/{page}/files/{doc}");     if !push {-        return Ok((format!("docs/clips.md rebuilt with {clips} clip(s) and {} media file(s) in {} (dry run, nothing pushed; {}); {url} after --push", media.len(), stage.display(), match &scrubbed { Ok(()) => "the scrub passes".to_string(), Err(e) => format!("a push would be refused: {e}") }), files));+        return Ok((format!("{doc} rebuilt with {clips} clip(s) and {} media file(s) in {} (dry run, nothing pushed; {}); {url} after --push", media.len(), stage.display(), match &scrubbed { Ok(()) => "the scrub passes".to_string(), Err(e) => format!("a push would be refused: {e}") }), files));     }     scrubbed.map_err(|e| format!("not pushed: {e}"))?;     // only what changed since the last push goes up (docs/clips.md always), in batches the gateway takes@@ -255,7 +279,7 @@ fn publish_clips(r: &Run, dir: &Path, page: &str, push: bool) -> Result<(String,     for f in &files {         let p = stage.join(f);         let sum = library_review::hash(&p).unwrap_or_default();-        if f == "docs/clips.md" || cache.get(f).and_then(|v| v.as_str()) != Some(sum.as_str()) { todo.push((f.clone(), std::fs::metadata(&p).map(|m| m.len()).unwrap_or(0), sum)); }+        if f == &doc || f == "docs/clips.md" || cache.get(f).and_then(|v| v.as_str()) != Some(sum.as_str()) { todo.push((f.clone(), std::fs::metadata(&p).map(|m| m.len()).unwrap_or(0), sum)); }     }     let mut batches: Vec<Vec<&(String, u64, String)>> = vec![Vec::new()];     let mut acc = 0u64;
docs/release-0.1.40.mdadded+3
@@ -0,0 +1,3 @@+# AI Flow 0.1.40++- **A second run can no longer overwrite the first run's clips page.** `docs/clips.md` belonged to whichever run published last, so a rework run on the same wiki page (its `report --push` or any clip stop) replaced the first run's clips. The first run to publish `docs/clips.md` now owns it (a hidden `<!-- aiflow-clips-run: <run> -->` marker); a later run writes `docs/clips-<run>.md` and adds one link to it in `docs/clips.md`, leaving the rest of that page as it was.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.39",+  "version": "0.1.40",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.39",+  "version": "0.1.40",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",