← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "serde",@@ -34,7 +34,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.81"+version = "0.1.82" dependencies = [  "roxmltree",  "serde",@@ -43,7 +43,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "serde",@@ -52,7 +52,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -62,7 +62,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "serde",@@ -71,7 +71,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "serde",@@ -80,7 +80,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -90,7 +90,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.81"+version = "0.1.82" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -100,7 +100,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.81"+version = "0.1.82" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.81"+version = "0.1.82" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+6
@@ -383,3 +383,9 @@ Current acceptance requires `peakJDefinition` declaring an unclipped, unrounded A current report must identify distributed plated-wall coupling with removed drill voids and include every plated connection. AIFlow independently checks signed current divided by barrel area, compares it with the barrel-density limit, and rejects `current-barrel` findings even when surface current passes. Old center-cell reports can contain apparent surface hotspots inside drill holes; do not alter the PCB merely to chase that artifact or stamp new metadata onto cached output. The corrected Fields implementation is under owner review in adom/adom-fields PR 9 (dependencies 8, 6 and 5). Preserve shared runtime pins and continue other authorized work while an owner review is pending.  For native saved-board experiments, preserve a UUID-to-net map before and after connectivity build, refill, save and reload. KiCad can reassign a crossing copper item's net during connectivity repair; zero DRC errors alone do not prove the intended circuit. Reject changed intent, correct geometry and rerun. Keep the live board unchanged until the candidate's geometry, net assignments and engineering checks are reviewed.++### Thermal acceptance uses each part's own budget++AIFlow independently checks every declared hot part against its `maxRiseC` (or `hotMaxRiseC`) and any `maxThetaCPerW`, using matching watts and tab net. Both thermal solves must converge and report finite temperatures. A global Fields issue list cannot hide a tighter part limit. Existing chip summaries round to 0.1 C, so acceptance reserves 0.05 C for reporting precision; do not round a boundary value into a pass. Moving-air results do not waive the still-air check.++Derive continuous heat cases from the actual voltage/current/power envelope and keep overload/conductor screens separate. Do not lower a limit to advance the workflow. Package-to-heatsink transfer, sensor lag, airflow and physical qualification still require their own evidence.
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/aiflow-analyze/src/lib.rs+61
@@ -232,11 +232,43 @@ pub fn field_screen(fields: &Value, spec: &Value, board_bytes: &[u8], spec_bytes             return Err(format!("Fields {key} identity is missing or stale; run adom-fields analyze on this saved board and spec again"));         }     }+    if kind == "thermal" {+        for key in ["stillResidual", "movingResidual"] {+            if !fields["thermalConvergence"][key].as_f64().is_some_and(|v| v.is_finite() && v >= 0.0 && v <= 1e-6) {+                return Err(format!("Fields thermal solve is missing or unconverged: {key}"));+            }+        }+    }     let (group, results, key, demand) = if kind=="current" { ("loads","nets","net","amps") } else { ("hot","chips","reference","watts") };     let entries = fields[results].as_array().ok_or_else(||format!("Fields report has no {results}"))?;     for (name,value) in spec[group].as_object().into_iter().flatten() {         if value[demand].as_f64().unwrap_or(0.0) <= 0.0 { continue; }         let row = entries.iter().find(|r|r[key].as_str()==Some(name.as_str())).ok_or_else(||format!("Fields did not solve {group}.{name}; resolve its missing terminals or copper and rerun analyze"))?;+        if kind == "thermal" {+            let watts = value["watts"].as_f64().unwrap();+            if !row["watts"].as_f64().is_some_and(|v| v.is_finite() && (v-watts).abs() <= 1e-8)+                || row["tabNet"] != value["tabNet"] {+                return Err(format!("Fields {name} thermal source differs from its declared watts or tab net"));+            }+            let rise_limit = value.get("maxRiseC").or_else(|| spec.get("hotMaxRiseC"));+            let theta_limit = value.get("maxThetaCPerW").or_else(|| spec.get("maxThetaCPerW"));+            let mut limit = if rise_limit.is_none() && theta_limit.is_none() { 60.0 * watts } else { f64::INFINITY };+            for (item, multiplier) in [(rise_limit, 1.0), (theta_limit, watts)] {+                if let Some(item) = item {+                    let v = item.as_f64().filter(|v| v.is_finite() && *v > 0.0)+                        .ok_or_else(|| format!("{name} thermal limits must be finite and positive"))?;+                    limit = limit.min(v * multiplier);+                }+            }+            for key in ["riseC", "riseMovingC"] {+                let rise = row[key].as_f64().filter(|v| v.is_finite() && *v >= 0.0)+                    .ok_or_else(|| format!("Fields {name} is missing a finite {key}"))?;+                // Fields chip summaries round to 0.1 C. Reserve half a display step.+                if rise + 0.05 > limit {+                    return Err(format!("Fields {name} {key} plus 0.05 C reporting allowance exceeds its {limit} C rise budget"));+                }+            }+        }         if kind == "current" {             let definition = &fields["peakJDefinition"];             if definition["statistic"] != "unclipped-cell-maximum"@@ -321,6 +353,35 @@ mod field_screen_tests {         let mut f=report();f["nets"][0]["layers"]=json!(["F.Cu","B.Cu"]);f["nets"][0]["vias"]=json!(1);         assert!(field_screen(&f,&spec,b"board",b"spec","current").unwrap_err().contains("omitted plated"));     }+    fn thermal_report() -> Value {+        let mut f=report();+        f["thermalConvergence"]=json!({"stillResidual":1e-8,"movingResidual":1e-8});+        f["chips"]=json!([{"reference":"Q1","watts":5,"tabNet":"VCC","riseC":14.9,"riseMovingC":10.0}]);+        f+    }+    #[test]+    fn individual_thermal_limit_and_rounding_allowance_are_enforced() {+        let spec=json!({"hotMaxRiseC":35,"hot":{"Q1":{"watts":5,"tabNet":"VCC","maxRiseC":15}}});+        field_screen(&thermal_report(),&spec,b"board",b"spec","thermal").unwrap();+        for rise in [15.0,20.0] {+            let mut f=thermal_report();f["chips"][0]["riseC"]=json!(rise);+            assert!(field_screen(&f,&spec,b"board",b"spec","thermal").unwrap_err().contains("15 C rise budget"));+        }+        let mut tighter=spec.clone();tighter["hot"]["Q1"]["maxThetaCPerW"]=json!(2);+        assert!(field_screen(&thermal_report(),&tighter,b"board",b"spec","thermal").is_err());+    }+    #[test]+    fn thermal_coverage_requires_full_heat_finite_values_and_convergence() {+        let spec=json!({"hotMaxRiseC":35,"hot":{"Q1":{"watts":5,"tabNet":"VCC"}}});+        for (key,val) in [("watts",json!(1)),("tabNet",json!("GND")),("riseC",Value::Null),("riseMovingC",json!(-1))] {+            let mut f=thermal_report();f["chips"][0][key]=val;+            assert!(field_screen(&f,&spec,b"board",b"spec","thermal").is_err());+        }+        for key in ["stillResidual","movingResidual"] {+            let mut f=thermal_report();f["thermalConvergence"][key]=json!(0.01);+            assert!(field_screen(&f,&spec,b"board",b"spec","thermal").is_err());+        }+    }     #[test]     fn overloaded_or_inconsistent_barrel_fails_even_without_issue_text() {         let spec=json!({"loads":{"VCC":{"amps":2}}});let mut f=report();
docs/release-0.1.82.mdadded+7
@@ -0,0 +1,7 @@+# 0.1.82 — enforce individual thermal budgets++Thermal acceptance independently verifies each hot part's declared heat, tab net and temperature-rise or thermal-resistance limit. Both still/moving solves must converge and report finite temperatures. A 0.05 C reporting allowance prevents a rounded chip summary at the limit from passing. This does not add package cooling physics or qualify hardware.++The electronic load exposed the gap: Q1 has a 15 C budget while the global setting is 35 C. The existing source-only test is extended with regressions for a suppressed over-temperature issue, rounded boundary, under-declared heat, wrong net, missing values and nonconvergence. Native board checks and current acceptance remain separate.++Validation: production compilation and release build passed; 132 workspace tests passed (one ignored). All 57 installed CLI commands remain available. Continuation, dashboard, 149-component inventory, widget and seven silkscreen workflow scripts passed. This release changes report acceptance, not native PCB geometry or shared Fields deployment.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.81",+  "version": "0.1.82",   "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+2−2
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.81",+  "version": "0.1.82",   "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.",@@ -94,4 +94,4 @@     "aiflow-test-plan"   ],   "license": "MIT"-}\ No newline at end of file+}
skills/adom-aiflow/SKILL.md+6
@@ -349,3 +349,9 @@ Current acceptance requires `peakJDefinition` declaring an unclipped, unrounded A current report must identify distributed plated-wall coupling with removed drill voids and include every plated connection. AIFlow independently checks signed current divided by barrel area, compares it with the barrel-density limit, and rejects `current-barrel` findings even when surface current passes. Old center-cell reports can contain apparent surface hotspots inside drill holes; do not alter the PCB merely to chase that artifact or stamp new metadata onto cached output. The corrected Fields implementation is under owner review in adom/adom-fields PR 9 (dependencies 8, 6 and 5). Preserve shared runtime pins and continue other authorized work while an owner review is pending.  For native saved-board experiments, preserve a UUID-to-net map before and after connectivity build, refill, save and reload. KiCad can reassign a crossing copper item's net during connectivity repair; zero DRC errors alone do not prove the intended circuit. Reject changed intent, correct geometry and rerun. Keep the live board unchanged until the candidate's geometry, net assignments and engineering checks are reviewed.++### Thermal acceptance uses each part's own budget++AIFlow independently checks every declared hot part against its `maxRiseC` (or `hotMaxRiseC`) and any `maxThetaCPerW`, using matching watts and tab net. Both thermal solves must converge and report finite temperatures. A global Fields issue list cannot hide a tighter part limit. Existing chip summaries round to 0.1 C, so acceptance reserves 0.05 C for reporting precision; do not round a boundary value into a pass. Moving-air results do not waive the still-air check.++Derive continuous heat cases from the actual voltage/current/power envelope and keep overload/conductor screens separate. Do not lower a limit to advance the workflow. Package-to-heatsink transfer, sensor lag, airflow and physical qualification still require their own evidence.
skills/aiflow-measurement/SKILL.md+6
@@ -75,3 +75,9 @@ Current acceptance requires `peakJDefinition` declaring an unclipped, unrounded A current report must identify distributed plated-wall coupling with removed drill voids and include every plated connection. AIFlow independently checks signed current divided by barrel area, compares it with the barrel-density limit, and rejects `current-barrel` findings even when surface current passes. Old center-cell reports can contain apparent surface hotspots inside drill holes; do not alter the PCB merely to chase that artifact or stamp new metadata onto cached output. The corrected Fields implementation is under owner review in adom/adom-fields PR 9 (dependencies 8, 6 and 5). Preserve shared runtime pins and continue other authorized work while an owner review is pending.  For native saved-board experiments, preserve a UUID-to-net map before and after connectivity build, refill, save and reload. KiCad can reassign a crossing copper item's net during connectivity repair; zero DRC errors alone do not prove the intended circuit. Reject changed intent, correct geometry and rerun. Keep the live board unchanged until the candidate's geometry, net assignments and engineering checks are reviewed.++### Thermal acceptance uses each part's own budget++AIFlow independently checks every declared hot part against its `maxRiseC` (or `hotMaxRiseC`) and any `maxThetaCPerW`, using matching watts and tab net. Both thermal solves must converge and report finite temperatures. A global Fields issue list cannot hide a tighter part limit. Existing chip summaries round to 0.1 C, so acceptance reserves 0.05 C for reporting precision; do not round a boundary value into a pass. Moving-air results do not waive the still-air check.++Derive continuous heat cases from the actual voltage/current/power envelope and keep overload/conductor screens separate. Do not lower a limit to advance the workflow. Package-to-heatsink transfer, sensor lag, airflow and physical qualification still require their own evidence.