← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.69\ No newline at end of file+1.13.70\ No newline at end of file
CHANGELOG.md+3−1
@@ -3,7 +3,9 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` -## Next release+## 1.13.70 (2026-11-04)++`fusion_build_library_3d` re-exports faster: a part already minted on this machine and account, with the same model, footprint, placement and destination, reuses its 3D package when its cloud file still exists, so only new or changed parts mint. Parts say `reused: true`; the reply has `partsReused` and `partsMinted`, and `freshMint: true` makes them again. A deleted cloud file falls back to a fresh mint. The last `_3d` scratch document is closed once the library is open (`scratchClosedAfterOpen`).  A library Fusion refuses is reported, not called opened (PR #35 by Colby Knox). `fusion_preflight_lbr` has a new check, `package3d-refs-resolve`, for a 3D binding naming a package the library doesn't have, and `fusion_open_lbr` refuses such a file before Fusion sees it. Any other refusal answers `success: false` with the dialog closed, and `fusion_build_library_3d` reports failure when the finished library won't open, even if every part minted. 
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.69",+  "version": "1.13.70",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
contracts/describe.json+1−1
@@ -1065,7 +1065,7 @@         "success": "bool"       },       "statusVerb": "fusion_get_app_state",-      "summary": "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",+      "summary": "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. A part minted before on this machine and account, with the same model, footprint, placement and destination, reuses its 3D package when its cloud file still exists (reused:true on the part, partsReused and partsMinted in the reply; freshMint:true, on the call or a part, makes it again). This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",       "timeoutSeconds": 1800     },     {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.69"+version = "1.13.70" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.69"+version = "1.13.70" edition = "2021"  [dependencies]
describe.py+1−1
@@ -889,7 +889,7 @@ _T = [      {"projectName": "optional str: save the .f3d into this project by name (never the active one; a missing name is refused)", "folderPath": "optional str under that project, created if missing (default Adom AI Workspace); the task subfolder goes under it", "lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true). false = import exactly as authored: NO bounding-box reorientation and NO pin-1 rotation (1.10.8)",         "placement": "optional {translation:[x,y,z] mm, rotation:[rx,ry,rz] deg (about X, then Y, then Z, extrinsic, about the origin), scale:1} from the manufacturer's authored transform. Bypasses every heuristic; the reply returns the applied 4x4 matrix (mm), verification (verified|mismatch), source {path, sha256, bytes} and warnings. Non-finite values, wrong shapes and any scale other than 1 are refused before Fusion is touched (invalid_placement / unsupported_scale)"},      {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),-    ("fusion_build_library_3d", "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",+    ("fusion_build_library_3d", "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. A part minted before on this machine and account, with the same model, footprint, placement and destination, reuses its 3D package when its cloud file still exists (reused:true on the part, partsReused and partsMinted in the reply; freshMint:true, on the call or a part, makes it again). This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",      {"projectName": "optional str: save the .f3d into this project by name (never the active one; a missing name is refused)", "folderPath": "optional str under that project, created if missing (default Adom AI Workspace); the task subfolder goes under it", "lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"},      {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),     ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.13.69"+DASH_VERSION="1.13.70" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.69",+  "version": "1.13.70",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+2−2
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.69",+  "version": "1.13.70",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -152,4 +152,4 @@   "confirmed_user_skills": [     "fusion-design-efficiency"   ]-}\ No newline at end of file+}
rust/Cargo.lock+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.69"+version = "1.13.70" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.69"+version = "1.13.70" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.69"+version = "1.13.70" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.69"+version = "1.13.70" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.69"+version = "1.13.70" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.69"+version = "1.13.70" edition = "2021" license = "MIT" 
rust/bridge/src/mint.rs+142−2
@@ -325,6 +325,64 @@ pub fn close_docs_opened_since(ctx: &Ctx, before: &[String], keep: &[String], pa }  /// `_build_library_3d_core`: mint every part, inject all bindings in one pass, open once.+/// Bumped when make_3d_package's recipe changes what it produces, so older mints are made again.+const MINT_RECIPE: u32 = 1;+const MINT_CACHE_MAX: usize = 5000;++fn mint_cache_path(state_dir: &std::path::Path) -> std::path::PathBuf { state_dir.join("mint-cache.json") }++/// The `<package>` element a part's footprint comes from, as written: a wiki re-export rewrites the rest of the file.+fn package_xml(lbr_text: &str, pkg: &str) -> Option<String> {+    let doc = roxmltree::Document::parse_with_options(lbr_text, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).ok()?;+    let found = doc.descendants().find(|n| n.has_tag_name("package") && n.attribute("name").map(|a| a.eq_ignore_ascii_case(pkg)).unwrap_or(false))?;+    Some(lbr_text[found.range()].to_string())+}++/// Everything that changes what a mint makes, and nothing else: the model's bytes, the footprint, how the model+/// is placed, whose account it lands in, and the destination the caller named (one is never swapped for another).+fn mint_cache_key(step: &[u8], footprint: &str, pkg: &str, orient: bool, placement: &Value, account: &str, project: &Value, folder: &Value) -> String {+    use sha2::{Digest, Sha256};+    let mut h = Sha256::new();+    h.update(json!({"recipe": MINT_RECIPE, "package": pkg, "orient": orient, "placement": placement, "account": account, "project": project, "folder": folder}).to_string().as_bytes());+    h.update(Sha256::digest(step));+    h.update(footprint.as_bytes());+    format!("{:x}", h.finalize())+}++fn mint_cache_read(state_dir: &std::path::Path) -> Value {+    std::fs::read_to_string(mint_cache_path(state_dir)).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(json!({}))+}++fn mint_cache_store(state_dir: &std::path::Path, key: &str, record: Value) {+    let mut c = mint_cache_read(state_dir);+    if !c.is_object() { c = json!({}); }+    let m = c.as_object_mut().unwrap();+    m.remove(key);+    m.insert(key.to_string(), record);+    while m.len() > MINT_CACHE_MAX {+        let oldest = m.iter().min_by(|a, b| a.1["at"].as_f64().unwrap_or(0.0).total_cmp(&b.1["at"].as_f64().unwrap_or(0.0))).map(|(k, _)| k.clone());+        match oldest { Some(k) => { m.remove(&k); } None => break }+    }+    let p = mint_cache_path(state_dir);+    let tmp = p.with_extension("json.tmp");+    if std::fs::write(&tmp, c.to_string()).is_ok() { let _ = std::fs::rename(&tmp, &p); }+}++/// The signed-in Autodesk account, or None: with no account known nothing is reused.+fn mint_account(pass: &dyn Fn(&str, Value) -> Value) -> Option<String> {+    pass("run_modeling_script", json!({"script": "import adsk.core\nresult={'user': adsk.core.Application.get().userId}"}))["data"]["result"]["user"].as_str().filter(|u| !u.is_empty()).map(str::to_string)+}++/// A cached mint whose cloud file still exists. A deleted one, or any doubt, means None and a fresh mint:+/// a dead urn would put a broken package in the library.+fn cached_mint(state_dir: &std::path::Path, key: &str, pass: &dyn Fn(&str, Value) -> Value) -> Option<Value> {+    let rec = mint_cache_read(state_dir)[key].clone();+    let wip = rec["wipUrn"].as_str()?.to_string();+    let lineage = wip.split("vf.").nth(1)?.split('?').next()?.to_string();+    let script = format!("import adsk.core\ntry:\n    f=adsk.core.Application.get().data.findFileById('urn:adsk.wipprod:dm.lineage:{lineage}')\n    result={{'exists': f is not None and bool(f.name)}}\nexcept Exception:\n    result={{'exists': False}}");+    (pass("run_modeling_script", json!({"script": script}))["data"]["result"]["exists"].as_bool() == Some(true)).then_some(rec)+}+ pub fn build_library_3d_core(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value, on_part: &dyn Fn(&Value, usize, usize)) -> Value {     let combined = args["lbrPath"].as_str().unwrap_or("").replace('\\', "/");     let parts = args["parts"].as_array().cloned().unwrap_or_default();@@ -337,11 +395,27 @@ pub fn build_library_3d_core(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)     let (mut results, mut bindings, mut shots): (Vec<Value>, Vec<fusion_lbr::Binding>, Vec<Value>) = (vec![], vec![], vec![]);     let mut cancelled = false;     let total = parts.len();+    let fresh_all = args["freshMint"].as_bool() == Some(true);+    let account = if fresh_all { None } else { mint_account(pass) };+    let mut reused_count = 0usize;     for p in &parts {         let pkg = p["package"].as_str().unwrap_or("").to_string();         let per_lbr = p["lbrPath"].as_str().unwrap_or("").replace('\\', "/");         let step = p["modelPath"].as_str().unwrap_or("").replace('\\', "/");         if let Some(key) = args["_cancelKey"].as_str() { if crate::chains::cancel_requested_pub(key) { cancelled = true; break; } }+        // this part minted before, same model, footprint and placement: its 3D package is reused, about a minute saved each+        let cache_key = account.as_ref().filter(|_| p["freshMint"].as_bool() != Some(true)).and_then(|acct| {+            let (bytes, text) = (std::fs::read(&step).ok()?, std::fs::read_to_string(&per_lbr).ok()?);+            Some(mint_cache_key(&bytes, &package_xml(&text, &pkg)?, &pkg, p["orient"].as_bool().unwrap_or(true), &p["placement"], acct, &args["projectName"], &args["folderPath"]))+        });+        if let Some(rec) = cache_key.as_ref().and_then(|k| cached_mint(&ctx.state_dir, k, pass)) {+            let wip = rec["wipUrn"].as_str().unwrap_or("").to_string();+            bindings.push(fusion_lbr::Binding { package: pkg.clone(), wip_urn: wip.clone() });+            results.push(json!({"package": pkg, "success": true, "reused": true, "wip_urn": wip, "dims_mm": rec["dims"], "pin1": rec["pin1"], "stepStory": rec["stepStory"]}));+            reused_count += 1;+            on_part(results.last().unwrap(), results.len(), total);+            continue;+        }         let docs_before: Vec<String> = pass("document_info", json!({}))["data"]["openDocuments"].as_array().map(|a| a.iter().filter_map(|d| d["name"].as_str().map(str::to_string)).collect()).unwrap_or_default();         if pkg.is_empty() || per_lbr.is_empty() || step.is_empty() { results.push(json!({"package": pkg, "success": false, "error": "package, lbrPath (per-part .lbr) and modelPath (.step) all required"})); on_part(results.last().unwrap(), results.len(), total); continue; }         let mk_args = json!({"lbrPath": per_lbr, "modelPath": step, "projectId": project_id, "folderId": folder_id, "projectName": args["projectName"], "folderPath": args["folderPath"], "fileName": format!("{pkg}_3d"), "task": task, "placement": p["placement"].clone(), "orient": p["orient"].clone(), "captureLabel": if capture { json!(pkg) } else { Value::Null }, "orient": p["orient"].as_bool().unwrap_or(true)});@@ -349,7 +423,13 @@ pub fn build_library_3d_core(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)         if mk["success"].as_bool() != Some(true) { std::thread::sleep(Duration::from_secs(2)); mk = make_3d_package(ctx, &mk_args, pass); }         let mut entry = json!({"package": pkg, "success": mk["success"].as_bool().unwrap_or(false)});         if mk["success"].as_bool() == Some(true) {-            entry["wip_urn"] = mk["wip_urn"].clone(); entry["dims_mm"] = mk["dims_mm"].clone(); entry["pin1"] = mk["pin1"].clone(); entry["stepStory"] = json!(step_layer_story(&mk["bodies"]));+            let dims = if mk["dims_mm"].is_null() { mk["dims"].clone() } else { mk["dims_mm"].clone() };+            let story = json!(step_layer_story(&mk["bodies"]));+            entry["wip_urn"] = mk["wip_urn"].clone(); entry["dims_mm"] = dims.clone(); entry["pin1"] = mk["pin1"].clone(); entry["stepStory"] = story.clone();+            if let Some(k) = &cache_key {+                let at = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0);+                mint_cache_store(&ctx.state_dir, k, json!({"wipUrn": mk["wip_urn"], "dims": dims, "pin1": mk["pin1"], "stepStory": story, "package": pkg, "at": at}));+            }             bindings.push(fusion_lbr::Binding { package: pkg.clone(), wip_urn: mk["wip_urn"].as_str().unwrap_or("").to_string() });         } else { entry["error"] = mk["error"].clone(); }         if !mk["before"].is_null() { shots.push(json!({"package": pkg, "stage": "before", "path": mk["before"]})); }@@ -368,18 +448,78 @@ pub fn build_library_3d_core(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)     }     let mut opened = Value::Null;     let mut open_error = Value::Null;+    let mut scratch_after_open = Value::Null;     if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) {         let op = crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass);         let ok = op["success"].as_bool() == Some(true);         opened = json!(ok);         if !ok { open_error = op["error"].clone(); }+        // each part's close leaves the last open document alone, so the last _3d survives until the library is+        // open; now it's safe to close, and nothing else is touched (bridge-owned scratch only)+        if ok {+            let keep = [std::path::Path::new(b).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default()];+            let open_now: Vec<String> = pass("document_info", json!({}))["data"]["openDocuments"].as_array().map(|a| a.iter().filter_map(|d| d["name"].as_str().map(str::to_string)).collect()).unwrap_or_default();+            scratch_after_open = json!(close_docs_opened_since(ctx, &open_now, &keep, pass));+        }     } }     let n_ok = results.iter().filter(|r| r["success"].as_bool() == Some(true)).count();     // Parts minted but a library Fusion will not open is a failed build, whatever each part did.-    json!({"success": n_ok > 0 && open_error.is_null(), "error": open_error, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,+    json!({"success": n_ok > 0 && open_error.is_null(), "error": open_error, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "partsReused": reused_count, "partsMinted": n_ok - reused_count, "scratchClosedAfterOpen": scratch_after_open, "parts": results, "screenshots": shots, "opened": opened,         "_hint": if !open_error.is_null() { format!("{n_ok}/{total} parts minted, but the finished library would not open in Fusion: read error, and fix it before using {}.", bound.as_deref().unwrap_or("the library")) } else { format!("{n_ok}/{total} parts got a RENDERING footprint+chip 3D package; all bindings injected into {} and the library opened once. NEXT: fusion_check_dialogs should be 0 (no broken-ref). For clean device previews + screenshots, save to the cloud and REOPEN from the cloud (fusion_aps_open) - the in-session view often gets an empty 'Untitled' shoved in front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] (on the box in %TEMP%/adom-fusion-shots; ab pulls them to you as localFullPath) - pull them with desktop_pull_file (or re-capture via desktop_screenshot_window) for the demo video. AFTER (recommended): call fusion_capture_library_views per showcase part to grab the symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust the library, and the 3D-only shots miss them. PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the Adom Bridge relay times out the REQUEST at ~60s - you may get 'Request timed out' even though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs (%TEMP%/adom-fusion-shots on the box, pulled to you as localFullPath). If a part is missing, just re-run build_library_3d for the missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE and idempotent, so it accumulates onto the existing bindings (it no longer wipes the parts that already succeeded). Each part also self-retries once on a transient first-part failure. Full recipe: the fusion-multipart-libraries skill.", bound.as_deref().unwrap_or("None")) }}) } +#[cfg(test)]+mod mint_cache_tests {+    use super::*;+    fn dir(t: &str) -> std::path::PathBuf { let d = std::env::temp_dir().join(format!("adom-mint-cache-{t}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); d }+    fn key(step: &[u8], fp: &str, pkg: &str, orient: bool, pl: Value, acct: &str, proj: Value) -> String { mint_cache_key(step, fp, pkg, orient, &pl, acct, &proj, &Value::Null) }+    #[test] fn the_key_changes_with_what_changes_the_result_and_nothing_else() {+        let base = key(b"s", "<package/>", "P", true, Value::Null, "me", Value::Null);+        assert_eq!(base, key(b"s", "<package/>", "P", true, Value::Null, "me", Value::Null));+        assert_ne!(base, key(b"t", "<package/>", "P", true, Value::Null, "me", Value::Null));+        assert_ne!(base, key(b"s", "<package a=\"1\"/>", "P", true, Value::Null, "me", Value::Null));+        assert_ne!(base, key(b"s", "<package/>", "Q", true, Value::Null, "me", Value::Null));+        assert_ne!(base, key(b"s", "<package/>", "P", false, Value::Null, "me", Value::Null));+        assert_ne!(base, key(b"s", "<package/>", "P", true, json!({"translation": [1, 0, 0]}), "me", Value::Null));+        assert_ne!(base, key(b"s", "<package/>", "P", true, Value::Null, "you", Value::Null));+        assert_ne!(base, key(b"s", "<package/>", "P", true, Value::Null, "me", json!("Other project")));+    }+    #[test] fn the_footprint_is_only_that_package_so_a_rewrite_of_the_file_keeps_it() {+        let a = r#"<eagle><library><packages><package name="R1"><smd name="1"/></package><package name="R2"/></packages></library></eagle>"#;+        let b = r#"<?xml version="1.0"?><!DOCTYPE eagle SYSTEM "eagle.dtd"><eagle attr="new"><library><description>x</description><packages><package name="R0"/><package name="R1"><smd name="1"/></package></packages></library></eagle>"#;+        assert_eq!(package_xml(a, "R1"), package_xml(b, "R1"));+        assert!(package_xml(a, "R1").unwrap().contains("smd"));+        assert!(package_xml(a, "NOPE").is_none());+    }+    #[test] fn a_stored_mint_is_reused_only_while_its_file_exists() {+        let d = dir("exists");+        mint_cache_store(&d, "k", json!({"wipUrn": "urn:adsk.wipprod:fs.file:vf.ABC?version=1", "dims": [1, 2, 3], "at": 1.0}));+        let asked = std::cell::RefCell::new(String::new());+        let yes = |_: &str, a: Value| { *asked.borrow_mut() = a["script"].as_str().unwrap().to_string(); json!({"data": {"result": {"exists": true}}}) };+        assert_eq!(cached_mint(&d, "k", &yes).unwrap()["dims"], json!([1, 2, 3]));+        assert!(asked.borrow().contains("dm.lineage:ABC'"), "{}", asked.borrow());+        assert!(cached_mint(&d, "k", &|_, _| json!({"data": {"result": {"exists": false}}})).is_none());+        assert!(cached_mint(&d, "k", &|_, _| json!({"success": false, "error": "busy"})).is_none());+        assert!(cached_mint(&d, "other", &yes).is_none());+    }+    #[test] fn no_account_means_no_reuse() {+        assert!(mint_account(&|_, _| json!({"data": {"result": {"user": ""}}})).is_none());+        assert!(mint_account(&|_, _| json!({"success": false})).is_none());+        assert_eq!(mint_account(&|_, _| json!({"data": {"result": {"user": "me"}}})).as_deref(), Some("me"));+    }+    #[test] fn the_cache_keeps_its_newest_entries() {+        let d = dir("evict");+        for i in 0..3 { mint_cache_store(&d, &format!("k{i}"), json!({"at": i as f64})); }+        assert_eq!(mint_cache_read(&d).as_object().unwrap().len(), 3);+        let mut big = serde_json::Map::new();+        for i in 0..MINT_CACHE_MAX { big.insert(format!("o{i}"), json!({"at": i as f64 + 10.0})); }+        std::fs::write(mint_cache_path(&d), Value::Object(big).to_string()).unwrap();+        mint_cache_store(&d, "new", json!({"at": 1e9}));+        let c = mint_cache_read(&d); let m = c.as_object().unwrap();+        assert_eq!(m.len(), MINT_CACHE_MAX); assert!(m.contains_key("new")); assert!(!m.contains_key("o0"));+    }+}+ #[cfg(test)] mod tests {     #[test]