← Commit history
Cargo.lock+14−3
@@ -1126,7 +1126,7 @@ dependencies = [  [[package]] name = "service-kicad"-version = "1.1.1-dev.0"+version = "1.1.1-dev.1" dependencies = [  "anyhow",  "clap",@@ -1134,18 +1134,21 @@ dependencies = [  "serde",  "serde_json",  "service-kicad-project",+ "service-kicad-report",+ "sha2",  "tempfile", ]  [[package]] name = "service-kicad-api"-version = "1.1.1-dev.0"+version = "1.1.1-dev.1" dependencies = [  "anyhow",  "axum",  "serde",  "serde_json",  "service-kicad-project",+ "service-kicad-report",  "tempfile",  "tokio",  "tower-http 0.5.2",@@ -1156,7 +1159,7 @@ dependencies = [  [[package]] name = "service-kicad-project"-version = "1.1.1-dev.0"+version = "1.1.1-dev.1" dependencies = [  "anyhow",  "serde",@@ -1166,6 +1169,14 @@ dependencies = [  "tempfile", ] +[[package]]+name = "service-kicad-report"+version = "1.1.1-dev.1"+dependencies = [+ "serde",+ "serde_json",+]+ [[package]] name = "sha2" version = "0.10.9"
Cargo.toml+2−2
@@ -1,9 +1,9 @@ [workspace] resolver = "2"-members = ["api", "cli", "project"]+members = ["api", "cli", "project", "report"]  [workspace.package]-version = "1.1.1-dev.0"+version = "1.1.1-dev.1" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/service-kicad"
README.md+55
@@ -62,6 +62,61 @@ service-kicad fp svg Connector_PinHeader_2.54mm PinHeader_1x04_P2.54mm_Vertical service-kicad model fetch Connector_PinHeader_2.54mm.3dshapes/PinHeader_1x04_P2.54mm_Vertical.step ``` +### Reading a DRC / ERC result++kicad-cli's JSON carries **no totals**, only the finding collections. The CLI+derives every count from those findings and states the verdict on the first+line, separately from HTTP success (page issues #2, #3):++```+$ service-kicad pcb drc board.kicad_pcb+DRC FAILED: 2 errors, 1 warnings  (schematic_parity 0 · unconnected_items 0 · violations 3; 5 checks ignored)+kicad 10.0.2 · units mm · input board.kicad_pcb (3150 B, sha256 16a6ff56135e…) · service 1.1.0 @08def5a7+  [error] clearance: Clearance violation ( clearance 0.2000 mm; actual 0.1500 mm)  @ (9.45, 9.5)+        Pad 1 [<no net>] of REF** on F.Cu+  ...+hint [DRC_FAILED]: 2 error-severity finding(s); the report was produced but the DRC did not pass; exit code 5+```++**Count policy.** DRC counts span `violations`, `unconnected_items` and+`schematic_parity`; ERC counts span every `sheets[].violations`. `error`+and `warning` count; `exclusion` (user-excluded markers) is listed but never+counts toward pass/fail; any other severity is listed under `other`. A+collection the schema promises but the report lacks makes the status+`unknown`, never `passed`. `ignored_checks` are the checks kicad-cli did not+run: a pass says nothing about them.++**Exit codes.** `0` passed (warnings allowed), `5` the report was produced+and carries error findings, `1` the input could not be loaded, `2`/`3`/`4`+transport, auth, service failures. Pass `--exit-zero` to keep the old+always-zero behaviour while still getting the verdict.++**Formats.** `--format text` (default) is the summary above. `--format json`+is the native report with ONE added top-level key, `adom_summary` (counts,+status, `exit_code`, `input.sha256`, `service` version + git sha, hints,+`erc_scale`); every native key is untouched. `--format native` is the+byte-for-byte kicad-cli report. With `--out`, the file gets the JSON and+the terminal gets the text.++**ERC positions are 100x too small on KiCad 10.0.x.** Upstream+`eeschema/erc/erc_report.cpp` builds `WriteJsonReport`'s units provider+with `pcbIUScale` (1e6 IU/mm) while the text report and the schematic use+`schIUScale` (1e4 IU/mm), so every JSON `pos` is the true millimetre value+divided by 100. The CLI cross-checks item positions against the symbols+they name in the uploaded schematic and, when a factor other than 1 puts+them where the symbols are, prints `hint [ERC_COORDINATES_SCALED]` with the+factor. **Nothing is rescaled**: multiply `pos.x`/`pos.y` yourself and keep+the raw report. A report that checks out prints nothing; one that cannot+be cross-checked says why (`ERC_COORDINATES_UNCHECKED`).++**"Failed to load".** kicad-cli gives no parser location. The CLI reads the+file structurally before uploading and, for a truncated file, wrong file+kind, legacy `.sch`, or missing `(version ...)`, prints the line/column and+paren balance under `INPUT_UNLOADABLE` (exit 1). The service does the same+on its 400 (`diagnostics` + `hint` fields). A file that parses but is still+refused is a semantic problem (unknown `lib_id`, unsupported format+version); open it in KiCad for the message.+ ### 3D models: STEP is the contract  For board 3D models, export **STEP** here and convert with
SKILL.md+25−2
@@ -60,13 +60,13 @@ service-kicad version service-kicad config          # effective URL + which override is active  # PCB-service-kicad pcb drc board.kicad_pcb [--format json] [--out report.json]+service-kicad pcb drc board.kicad_pcb [--format text|json|native] [--out report.json] [--exit-zero] service-kicad pcb export svg board.kicad_pcb [--side top|bottom] [--layers F.Cu,F.Mask,Edge.Cuts] [--out board.svg] service-kicad pcb export gerbers board.kicad_pcb [--out ./gerbers/] service-kicad pcb export step board.kicad_pcb [--out board.step] [-F --include-pads ...]  # Schematic-service-kicad sch erc schematic.kicad_sch [--format json]+service-kicad sch erc schematic.kicad_sch [--format text|json|native] [--out report.json] [--exit-zero] service-kicad sch export svg schematic.kicad_sch [--out sch.svg] service-kicad sch export svg-project ./project-dir [--out ./sheets/]   # hierarchical: one SVG per sheet service-kicad sch export pdf schematic.kicad_sch [--out sch.pdf]@@ -107,6 +107,29 @@ one-script deploy — see the README on the wiki page | **2** | service unreachable | `service-kicad health` to confirm; the container may be restarting | | **3** | auth failed (401/403) | `adom-cli auth refresh` | | **4** | service-side error | CLI prints stderr; usually input-specific |+| **5** | DRC/ERC ran and found error-severity findings | Read the findings; this is a real fail, not a service problem. `--exit-zero` keeps exit 0 |++## Reading DRC / ERC results (read this before quoting a count)++The first line is the verdict: `DRC FAILED: 2 errors, 1 warnings (...)` or+`DRC PASSED`. Counts are derived from every native finding (violations,+unconnected items, schematic parity; all ERC sheets), never from summary+keys, because kicad-cli's JSON has none. HTTP 200 means "a report was+produced", never "the board passed". `ignored_checks` were not run.+`--format json` adds `adom_summary` (status, counts, exit_code, input+sha256, service version + git sha, hints) and leaves the native report+intact; `--format native` is the untouched kicad-cli JSON.++**ERC item positions are 100x too small on KiCad 10.0.x** (upstream bug:+`ERC_REPORT::WriteJsonReport` uses the PCB unit scale). When the CLI prints+`hint [ERC_COORDINATES_SCALED]` with a factor, multiply `pos.x`/`pos.y` by+that factor before highlighting or moving anything. The CLI never rescales;+the raw report is what you get. Do not "fix" a schematic based on unscaled+ERC coordinates.++A `Failed to load` becomes `INPUT_UNLOADABLE` with the line/column and+paren balance of a truncated file, or the real file kind if a board was+posted as a schematic (exit 1). Fix the file; do not retry as-is.  ## Project rule qualification (candidate source; discover before use) 
api/Cargo.toml+1
@@ -24,3 +24,4 @@ tempfile.workspace = true urlencoding = "2.1"  service-kicad-project = { path = "../project" }+service-kicad-report = { path = "../report" }
api/src/main.rs+19−2
@@ -162,6 +162,23 @@ fn err(code: StatusCode, message: &str) -> Response {     (code, Json(json!({ "error": message, "code": code.as_u16() }))).into_response() } +/// kicad-cli says only "Failed to load schematic" / "...board" for a file it+/// cannot parse. Add what a local structural read can see (truncation,+/// wrong file kind, legacy format, missing version) so the caller does not+/// have to guess (adom/service-kicad#2). `root` is `kicad_sch` / `kicad_pcb`.+fn load_err(code: StatusCode, message: &str, body: &Bytes, root: &str) -> Response {+    if code != StatusCode::BAD_REQUEST || !message.contains("Failed to load") {+        return err(code, message);+    }+    let diagnostics = service_kicad_report::diagnose(body, root);+    let hint = if diagnostics.is_empty() {+        "structure parses; kicad-cli rejected it semantically (unknown lib_id, unsupported (version ...), bad token). kicad-cli prints no parser location; open the file in KiCad for the message."+    } else {+        "the input is not a loadable file; fix the listed diagnostics and resubmit."+    };+    (code, Json(json!({ "error": message, "code": code.as_u16(), "diagnostics": diagnostics, "hint": hint }))).into_response()+}+ // ─── PCB operations ───────────────────────────────────────────────────  async fn pcb_drc(body: Bytes) -> Response {@@ -172,7 +189,7 @@ async fn pcb_drc(body: Bytes) -> Response {         pcb.path().to_str().unwrap_or_default(),         "-o", out_tmp.path().to_str().unwrap_or_default(),         "--format", "json",-    ]) { return err(c, &m); }+    ]) { return load_err(c, &m, &body, "kicad_pcb"); }     let bytes = match std::fs::read(out_tmp.path()) { Ok(b) => b, Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &format!("read: {e}")) };     (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "application/json")], bytes).into_response() }@@ -732,7 +749,7 @@ async fn sch_erc(body: Bytes) -> Response {         sch.path().to_str().unwrap_or_default(),         "-o", out_tmp.path().to_str().unwrap_or_default(),         "--format", "json",-    ]) { return err(c, &m); }+    ]) { return load_err(c, &m, &body, "kicad_sch"); }     let bytes = match std::fs::read(out_tmp.path()) { Ok(b) => b, Err(e) => return err(StatusCode::INTERNAL_SERVER_ERROR, &format!("read: {e}")) };     (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "application/json")], bytes).into_response() }
cli/Cargo.toml+2
@@ -20,3 +20,5 @@ serde_json.workspace = true tempfile.workspace = true  service-kicad-project = { path = "../project" }+service-kicad-report = { path = "../report" }+sha2 = "0.10"
cli/src/main.rs+39−56
@@ -6,10 +6,12 @@ //! //! See PLAN Part 29.4 for the canonical command surface. +mod report_cmd;+ use anyhow::{anyhow, Context, Result}; use clap::{Parser, Subcommand}; use reqwest::blocking::Client;-use std::{io::Write, path::PathBuf, time::Duration};+use std::{io::Write, path::{Path, PathBuf}, time::Duration};  const PROD_URL: &str = env!("SERVICE_KICAD_URL"); @@ -65,11 +67,14 @@ enum Cmd {  #[derive(Subcommand)] enum PcbCmd {-    /// POST /kicad/pcb/drc.+    /// POST /kicad/pcb/drc. Counts derive from the native findings; error+    /// findings exit 5. --format text|json|native (native = untouched report).     Drc {         file: PathBuf,         #[arg(long, default_value = "text")] format: String,         #[arg(long)] out: Option<PathBuf>,+        /// Exit 0 even when error-severity findings exist (legacy behavior).+        #[arg(long)] exit_zero: bool,     },     /// DRC with the matching native project and custom rules. Select the board explicitly.     DrcProject {@@ -113,11 +118,14 @@ enum PcbExportCmd {  #[derive(Subcommand)] enum SchCmd {-    /// POST /kicad/sch/erc.+    /// POST /kicad/sch/erc. Counts derive from the native findings; error+    /// findings exit 5; item positions are cross-checked against the source.     Erc {         file: PathBuf,         #[arg(long, default_value = "text")] format: String,         #[arg(long)] out: Option<PathBuf>,+        /// Exit 0 even when error-severity findings exist (legacy behavior).+        #[arg(long)] exit_zero: bool,     },     #[command(subcommand)]     Export(SchExportCmd),@@ -233,12 +241,7 @@ fn post_file_bytes(path: &str, file: &PathBuf) -> Result<Vec<u8>> {     Ok(resp.bytes().map(|b| b.to_vec()).unwrap_or_default()) } -#[allow(dead_code)]-fn _unused() -> Result<()> {-    Ok(())-}--fn write_or_stdout(bytes: &[u8], out: Option<&PathBuf>) -> Result<()> {+fn write_or_stdout(bytes: &[u8], out: Option<&Path>) -> Result<()> {     match out {         Some(p) => {             std::fs::write(p, bytes).with_context(|| format!("write {}", p.display()))?;@@ -264,32 +267,6 @@ fn extract_tarball(bytes: &[u8], dest: &PathBuf) -> Result<()> {     Ok(()) } -fn print_json_pretty_or_write(bytes: &[u8], format: &str, out: Option<&PathBuf>) -> Result<()> {-    if format == "json" || out.is_some() {-        return write_or_stdout(bytes, out);-    }-    // Pretty text for humans. Fall back to raw JSON if it doesn't parse.-    let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) else {-        return write_or_stdout(bytes, None);-    };-    let errs = v.get("errors").and_then(|x| x.as_u64()).unwrap_or(0);-    let warns = v.get("warnings").and_then(|x| x.as_u64())-        .or_else(|| v.get("warning").and_then(|x| x.as_u64()))-        .unwrap_or(0);-    let total = v.get("count").and_then(|x| x.as_u64()).unwrap_or(errs + warns);-    println!("errors: {errs}  warnings: {warns}  total: {total}");-    if let Some(violations) = v.get("violations").and_then(|x| x.as_array()) {-        for violation in violations.iter().take(20) {-            let sev = violation.get("severity").and_then(|s| s.as_str()).unwrap_or("?");-            let typ = violation.get("type").and_then(|s| s.as_str()).unwrap_or("?");-            let msg = violation.get("description").or_else(|| violation.get("message")).and_then(|s| s.as_str()).unwrap_or("");-            println!("  [{sev}] {typ}: {msg}");-        }-        if violations.len() > 20 { println!("  … {} more", violations.len() - 20); }-    }-    Ok(())-}- // ─── Commands ─────────────────────────────────────────────────────────  fn main() -> Result<()> {@@ -325,13 +302,16 @@ fn main() -> Result<()> {             }             Ok(())         }-        Cmd::Pcb(PcbCmd::Drc { file, format, out }) => {+        Cmd::Pcb(PcbCmd::Drc { file, format, out, exit_zero }) => {             let omitted: Vec<_> = ["kicad_pro", "kicad_dru"].iter().map(|ext|file.with_extension(ext)).filter(|p|p.exists()).collect();             if !omitted.is_empty() {                 eprintln!("{}",serde_json::json!({"level":"warning","code":"PROJECT_RULES_OMITTED","files":omitted,"hint":"This command uploads only the board. Use pcb drc-project <directory> --board <relative-board.kicad_pcb> to include project settings and custom rules."}));             }-            let bytes = post_file_bytes("/kicad/pcb/drc", &file)?;-            print_json_pretty_or_write(&bytes, &format, out.as_ref())+            let code = report_cmd::run(report_cmd::Request {+                kind: service_kicad_report::ReportKind::Drc, endpoint: "/kicad/pcb/drc",+                file: &file, format: &format, out: out.as_deref(), exit_zero,+            }, &base_url(), &http_client())?;+            std::process::exit(code);         }         Cmd::Pcb(PcbCmd::DrcProject { directory, board, include, allow_no_custom_rules, expect_rule, out }) => {             if expect_rule.as_ref().is_some_and(|s|s.trim().is_empty()) { anyhow::bail!("expected rule name must not be empty"); }@@ -343,7 +323,7 @@ fn main() -> Result<()> {             let status=response.status();             let result=response.bytes()?.to_vec();             // Preserve the full native report even for a failed expected-rule check.-            write_or_stdout(&result,out.as_ref())?;+            write_or_stdout(&result,out.as_deref())?;             if !status.is_success() {                 eprintln!("service-kicad: project DRC returned {status}; detailed response preserved in output");                 std::process::exit(http_error_exit_code(status.as_u16()));@@ -356,7 +336,7 @@ fn main() -> Result<()> {                 path.push_str(&format!("&layers={}", urlencode(&l)));             }             let bytes = post_file_bytes(&path, &file)?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }         Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Gerbers { file, out })) => {             let bytes = post_file_bytes("/kicad/pcb/export/gerbers", &file)?;@@ -371,27 +351,30 @@ fn main() -> Result<()> {         Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Step { file, out, flags })) => {             let qs = flags_to_query_string(&flags);             let path = format!("/kicad/pcb/export/step{qs}");-            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())+            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_deref())         }         Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Glb { file, out, flags, i_know_this_skips_the_pipeline })) => {             glb_gate(i_know_this_skips_the_pipeline);             let qs = flags_to_query_string(&flags);             let path = format!("/kicad/pcb/export/glb{qs}");-            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())+            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_deref())         }         Cmd::Pcb(PcbCmd::Export(PcbExportCmd::GlbZup { file, out, flags, i_know_this_skips_the_pipeline })) => {             glb_gate(i_know_this_skips_the_pipeline);             let qs = flags_to_query_string(&flags);             let path = format!("/kicad/pcb/export/glb-zup{qs}");-            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())+            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_deref())         } -        Cmd::Sch(SchCmd::Erc { file, format, out }) => {-            let bytes = post_file_bytes("/kicad/sch/erc", &file)?;-            print_json_pretty_or_write(&bytes, &format, out.as_ref())+        Cmd::Sch(SchCmd::Erc { file, format, out, exit_zero }) => {+            let code = report_cmd::run(report_cmd::Request {+                kind: service_kicad_report::ReportKind::Erc, endpoint: "/kicad/sch/erc",+                file: &file, format: &format, out: out.as_deref(), exit_zero,+            }, &base_url(), &http_client())?;+            std::process::exit(code);         }         Cmd::Sch(SchCmd::Export(SchExportCmd::Svg { file, out })) =>-            write_or_stdout(&post_file_bytes("/kicad/sch/export/svg", &file)?, out.as_ref()),+            write_or_stdout(&post_file_bytes("/kicad/sch/export/svg", &file)?, out.as_deref()),         Cmd::Sch(SchCmd::Export(SchExportCmd::SvgProject { dir, out })) => {             if !dir.is_dir() {                 anyhow::bail!("{} is not a directory — pass the project dir (.kicad_pro + sheets)", dir.display());@@ -411,40 +394,40 @@ fn main() -> Result<()> {             Ok(())         }         Cmd::Sch(SchCmd::Export(SchExportCmd::Pdf { file, out })) =>-            write_or_stdout(&post_file_bytes("/kicad/sch/export/pdf", &file)?, out.as_ref()),+            write_or_stdout(&post_file_bytes("/kicad/sch/export/pdf", &file)?, out.as_deref()),         Cmd::Sch(SchCmd::Export(SchExportCmd::Bom { file, out })) =>-            write_or_stdout(&post_file_bytes("/kicad/sch/export/bom", &file)?, out.as_ref()),+            write_or_stdout(&post_file_bytes("/kicad/sch/export/bom", &file)?, out.as_deref()),          Cmd::Sym(SymCmd::Svg { library, name, out }) => {             let bytes = get_bytes(&format!("/sym/export/svg/{}/{}", urlencode(&library), urlencode(&name)))?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }         Cmd::Sym(SymCmd::Fetch { library, out }) => {             let bytes = get_bytes(&format!("/symbols/{}.kicad_sym", urlencode(&library)))?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }         Cmd::Fp(FpCmd::Svg { library, name, out }) => {             let bytes = get_bytes(&format!("/fp/export/svg/{}/{}", urlencode(&library), urlencode(&name)))?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }         Cmd::Fp(FpCmd::Fetch { library, name, out }) => {             let bytes = get_bytes(&format!("/footprints/{}.pretty/{}.kicad_mod", urlencode(&library), urlencode(&name)))?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }         Cmd::Model { cmd: ModelCmd::Fetch { path, out } } => {             let bytes = get_bytes(&format!("/models/{}", path.trim_start_matches('/')))?;-            write_or_stdout(&bytes, out.as_ref())+            write_or_stdout(&bytes, out.as_deref())         }          Cmd::Altium(AltiumCmd::Sym { file, out }) => {             let bytes = post_file_bytes("/kicad/altium/sym", &file)?;             let dest = out.unwrap_or_else(|| file.with_extension("kicad_sym"));-            write_or_stdout(&bytes, Some(&dest))+            write_or_stdout(&bytes, Some(dest.as_path()))         }         Cmd::Altium(AltiumCmd::Fp { file, out }) => {             let bytes = post_file_bytes("/kicad/altium/fp", &file)?;             let dest = out.unwrap_or_else(|| file.with_extension("kicad_mod"));-            write_or_stdout(&bytes, Some(&dest))+            write_or_stdout(&bytes, Some(dest.as_path()))         }          Cmd::Install { skill_only, completions_only } => do_install(skill_only, completions_only),
cli/src/report_cmd.rsadded+182
@@ -0,0 +1,182 @@+//! `pcb drc` / `sch erc`: post the file, then read the native report+//! honestly (adom/service-kicad#2, #3).+//!+//! - Counts come from the findings, never from keys kicad-cli does not+//!   write. Text, JSON and exit code all derive from ONE `Summary`.+//! - The native report is preserved byte-for-byte under `--format native`;+//!   `--format json` adds a single top-level `adom_summary` key and touches+//!   nothing else.+//! - Error-severity findings exit `5` (EXIT_FINDINGS) unless `--exit-zero`.+//!   HTTP 200 is "the report was produced", never "the board passed".+//! - ERC positions are checked against the uploaded schematic; a scale+//!   mismatch is reported as a hint with the factor. Nothing is rescaled.+//! - A "Failed to load" from the service is diagnosed locally (truncation,+//!   wrong file kind, legacy format) and exits `1` (input invalid).+use anyhow::{Context, Result};+use serde_json::{json, Value};+use service_kicad_report::{check_erc_scale, diagnose, summary::Finding, Hint, ReportKind, Summary};+use sha2::{Digest, Sha256};+use std::{io::Write, path::Path};++pub struct Request<'a> {+    pub kind: ReportKind,+    pub endpoint: &'a str,+    pub file: &'a Path,+    /// `text` | `json` | `native`+    pub format: &'a str,+    pub out: Option<&'a Path>,+    pub exit_zero: bool,+}++/// Returns the process exit code. Never returns for transport/auth+/// failures (those exit inline, same codes as every other verb).+pub fn run(req: Request<'_>, base_url: &str, client: &reqwest::blocking::Client) -> Result<i32> {+    if !matches!(req.format, "text" | "json" | "native") {+        anyhow::bail!("--format must be text, json or native (got {:?})", req.format);+    }+    let root = match req.kind { ReportKind::Drc => "kicad_pcb", ReportKind::Erc => "kicad_sch" };+    let body = std::fs::read(req.file).with_context(|| format!("read {}", req.file.display()))?;++    // Pre-flight: an obviously broken file never leaves the machine.+    let local = diagnose(&body, root);+    if !local.is_empty() {+        eprintln!("service-kicad: {} is not a loadable .{root} file:", req.file.display());+        for d in &local { eprintln!("  - {d}"); }+        eprintln!("{}", json!({"level":"error","code":"INPUT_UNLOADABLE","diagnostics":local}));+        return Ok(1);+    }++    let resp = client.post(format!("{base_url}{}", req.endpoint)).body(body.clone()).send().unwrap_or_else(|e| {+        eprintln!("service-kicad: unreachable ({e})");+        std::process::exit(2);+    });+    let status = resp.status();+    let bytes = resp.bytes().map(|b| b.to_vec()).unwrap_or_default();+    if !status.is_success() {+        let text = String::from_utf8_lossy(&bytes).into_owned();+        eprintln!("service-kicad: {status} — {}", text.trim_end());+        if status.as_u16() == 400 && text.contains("Failed to load") {+            eprintln!("{}", json!({"level":"error","code":"INPUT_UNLOADABLE","hint":+                format!("kicad-cli could not load the file; local structure looks fine, so the failure is semantic (unknown lib_id, unsupported format version, bad token). Open it in KiCad {} for the parser message.", "10")}));+            return Ok(1);+        }+        return Ok(super::http_error_exit_code(status.as_u16()));+    }++    let mut report: Value = match serde_json::from_slice(&bytes) {+        Ok(v) => v,+        Err(e) => {+            eprintln!("service-kicad: service returned non-JSON for {} ({e}); raw bytes follow", req.endpoint);+            super::write_or_stdout(&bytes, req.out)?;+            return Ok(4);+        }+    };++    let mut summary = Summary::from_report(req.kind, &report);+    let scale = (req.kind == ReportKind::Erc).then(|| check_erc_scale(&body, &report));+    if let Some(s) = &scale {+        if s.status == "scaled" {+            let f = s.factor.unwrap_or(1.0);+            summary.hints.insert(0, Hint { level: "warning", code: "ERC_COORDINATES_SCALED", message: format!(+                "item positions are {f:.0}x smaller than the source schematic: multiply pos.x/pos.y by {f:.0} to get {} (KiCad {} writes ERC JSON positions with the PCB unit scale; upstream eeschema/erc/erc_report.cpp WriteJsonReport). Raw report preserved, nothing rescaled. {}",+                summary.coordinate_units.as_deref().unwrap_or("mm"),+                summary.kicad_version.as_deref().unwrap_or("?"), s.reason) });+        } else if s.status == "not_checked" {+            summary.hints.push(Hint { level: "info", code: "ERC_COORDINATES_UNCHECKED", message: format!(+                "could not cross-check item positions against the source: {}", s.reason) });+        }+    }++    let input = json!({+        "path": req.file.display().to_string(),+        "bytes": body.len(),+        "sha256": format!("{:x}", Sha256::digest(&body)),+    });+    let service = client.get(format!("{base_url}/version")).send().ok()+        .filter(|r| r.status().is_success())+        .and_then(|r| r.json::<Value>().ok())+        .unwrap_or(Value::Null);++    let exit_code = if req.exit_zero { 0 } else { summary.exit_code };+    let mut adom_summary = serde_json::to_value(&summary)?;+    adom_summary["exit_code"] = json!(exit_code);+    adom_summary["input"] = input;+    adom_summary["service"] = service.clone();+    if let Some(s) = &scale { adom_summary["erc_scale"] = serde_json::to_value(s)?; }++    match (req.format, req.out) {+        ("native", out) => super::write_or_stdout(&bytes, out)?,+        ("json", out) => {+            report["adom_summary"] = adom_summary.clone();+            let mut pretty = serde_json::to_vec_pretty(&report)?;+            pretty.push(b'\n');+            super::write_or_stdout(&pretty, out)?;+        }+        (_, Some(out)) => {+            // text on the terminal, augmented JSON in the file+            report["adom_summary"] = adom_summary.clone();+            let mut pretty = serde_json::to_vec_pretty(&report)?;+            pretty.push(b'\n');+            super::write_or_stdout(&pretty, Some(out))?;+            print_text(&summary, &Summary::findings(req.kind, &report), &adom_summary)?;+        }+        (_, None) => print_text(&summary, &Summary::findings(req.kind, &report), &adom_summary)?,+    }+    if req.exit_zero && summary.exit_code != 0 {+        eprintln!("service-kicad: --exit-zero: {} findings present but exiting 0 as requested", summary.counts.errors);+    }+    Ok(exit_code)+}++/// `service-kicad pcb drc x | head` must not turn into an error exit.+fn print_text(summary: &Summary, findings: &[Finding<'_>], adom: &Value) -> Result<()> {+    match print_text_inner(summary, findings, adom) {+        Err(e) if e.downcast_ref::<std::io::Error>().map(|io| io.kind() == std::io::ErrorKind::BrokenPipe).unwrap_or(false) => Ok(()),+        r => r,+    }+}++fn print_text_inner(summary: &Summary, findings: &[Finding<'_>], adom: &Value) -> Result<()> {+    let mut o = std::io::stdout().lock();+    let label = summary.kind.label();+    let verdict = match summary.status {+        "failed" => "FAILED",+        "passed_with_warnings" => "PASSED with warnings",+        "passed" => "PASSED",+        _ => "UNKNOWN",+    };+    let collections: Vec<String> = summary.collections.iter().map(|(name, c)| {+        if c.present { format!("{name} {}", c.counts.total) } else { format!("{name} absent") }+    }).collect();+    writeln!(o, "{label} {verdict}: {} errors, {} warnings{}  ({}; {} checks ignored)",+        summary.counts.errors, summary.counts.warnings,+        if summary.counts.exclusions > 0 { format!(", {} excluded", summary.counts.exclusions) } else { String::new() },+        collections.join(" · "), summary.ignored_checks.len())?;+    let sha = adom.pointer("/input/sha256").and_then(Value::as_str).map(|s| &s[..12]).unwrap_or("?");+    let svc = match (adom.pointer("/service/version").and_then(Value::as_str), adom.pointer("/service/git_sha_short").and_then(Value::as_str)) {+        (Some(v), Some(g)) => format!("service {v} @{g}"),+        _ => "service version unknown".to_string(),+    };+    writeln!(o, "kicad {} · units {} · input {} ({} B, sha256 {sha}…) · {svc}",+        summary.kicad_version.as_deref().unwrap_or("?"),+        summary.coordinate_units.as_deref().unwrap_or("?"),+        adom.pointer("/input/path").and_then(Value::as_str).unwrap_or("?"),+        adom.pointer("/input/bytes").and_then(Value::as_u64).unwrap_or(0))?;+    // Warnings about the report itself (scaled coordinates, missing+    // collections) go ABOVE the findings they qualify.+    for h in summary.hints.iter().filter(|h| h.level == "warning") {+        writeln!(o, "hint [{}]: {}", h.code, h.message)?;+    }+    for f in findings.iter().take(40) {+        let pos = f.pos.map(|(x, y)| format!("  @ ({x}, {y})")).unwrap_or_default();+        let sheet = f.sheet.filter(|s| *s != "/").map(|s| format!(" [{s}]")).unwrap_or_default();+        let coll = if f.collection == "violations" { String::new() } else { format!(" ({})", f.collection) };+        writeln!(o, "  [{}] {}{coll}{sheet}: {}{pos}", f.severity, f.kind, f.description)?;+        if let Some(item) = f.first_item { writeln!(o, "        {item}")?; }+    }+    if findings.len() > 40 { writeln!(o, "  … {} more (use --format json)", findings.len() - 40)?; }+    for h in summary.hints.iter().filter(|h| h.level != "warning") {+        writeln!(o, "hint [{}]: {}", h.code, h.message)?;+    }+    Ok(())+}
docs/service-kicad-api.md+34−2
@@ -35,7 +35,11 @@ POST /kicad/pcb/drc   Request body:  <raw .kicad_pcb bytes>   Content-Type:  application/octet-stream   Response:      application/json — same shape as `kicad-cli pcb drc-                 --format json` output.+                 --format json` output. NOTE: that shape has NO totals;+                 derive counts from `violations`, `unconnected_items` and+                 `schematic_parity` by `severity` (the CLI does). HTTP 200+                 means a report was produced, not that DRC passed.+  400:           `Failed to load board` plus `diagnostics` (see Error shape).  POST /kicad/pcb/export/svg?side=top|bottom   Request body:  <raw .kicad_pcb bytes>@@ -63,7 +67,15 @@ POST /kicad/pcb/export/glb POST /kicad/sch/erc   Request body:  <raw .kicad_sch bytes>   Response:      application/json — same shape as `kicad-cli sch erc-                 --format json`.+                 --format json`. No totals; derive counts from every+                 `sheets[].violations` by `severity`.+                 KNOWN UPSTREAM BUG (KiCad 10.0.x): item `pos` values are+                 the true millimetres divided by 100 (`WriteJsonReport`+                 uses pcbIUScale). The service returns the report as+                 kicad-cli wrote it; the CLI cross-checks positions+                 against the uploaded schematic and reports the factor+                 (`ERC_COORDINATES_SCALED`). Nothing is rescaled server-side.+  400:           `Failed to load schematic` plus `diagnostics`.  POST /kicad/sch/export/svg   Request body:  <raw .kicad_sch bytes>@@ -125,6 +137,26 @@ Example: `curl -o LM1117.step $KICAD_SERVICE_API/models/Package_TO_SOT_SMD.3dsha Body is always JSON for error responses; HTTP status carries the primary signal. +When kicad-cli refuses to load the posted file (`/kicad/pcb/drc`,+`/kicad/sch/erc`), the 400 also carries what a structural read of the+input can see, since kicad-cli itself prints no parser location:++```json+{+  "error": "Failed to load schematic\n",+  "code": 400,+  "diagnostics": [+    "s-expression error at line 4, col 1815: unterminated list opened at line 4 (depth 4); missing ')'",+    "paren balance: 119 '(' vs 114 ')' (file may be truncated)"+  ],+  "hint": "the input is not a loadable file; fix the listed diagnostics and resubmit."+}+```++`diagnostics` is empty when the file is structurally fine and the+refusal is semantic (unknown lib_id, unsupported format version); the+`hint` says so. Additive fields; the base error shape is unchanged.+ ## Port configuration  The binary honors `$PORT`. Default 8780 (matches the legacy service-
report/Cargo.tomladded+10
@@ -0,0 +1,10 @@+[package]+name = "service-kicad-report"+version.workspace = true+edition.workspace = true+license.workspace = true+description = "Read native kicad-cli DRC/ERC JSON honestly: derive counts from the findings, sanity-check coordinates against the source, diagnose unloadable inputs. Never rescales, never hides a finding."++[dependencies]+serde.workspace = true+serde_json.workspace = true
report/src/erc_scale.rsadded+159
@@ -0,0 +1,159 @@+//! Input-relative sanity check for ERC item positions.+//!+//! KiCad 10.0 `eeschema/erc/erc_report.cpp`: `GetTextReport()` builds+//! `UNITS_PROVIDER( schIUScale, ...)` but `WriteJsonReport()` builds+//! `UNITS_PROVIDER( pcbIUScale, ...)`. Schematic IU is 1e4 per mm, PCB IU is+//! 1e6 per mm, so every JSON `pos` is the true millimetre value divided by+//! 100. This check does not trust that story blindly: it matches ERC items+//! to the symbols they name in the uploaded schematic and asks which+//! factor puts them nearest. Nothing is rescaled; the caller decides.+use crate::sexpr::{parse, Node};+use serde::Serialize;+use serde_json::Value;+use std::collections::BTreeMap;++#[derive(Debug, Clone, Serialize, PartialEq)]+pub struct ScaleCheck {+    /// `consistent` | `scaled` | `not_checked`+    pub status: &'static str,+    /// Multiply report positions by this to get source millimetres.+    /// `Some(1.0)` when consistent.+    pub factor: Option<f64>,+    /// ERC items matched to a source symbol and used in the vote.+    pub matched_items: usize,+    /// Median distance (mm) between item and its symbol anchor at the+    /// chosen factor, and at factor 1.+    pub median_distance_mm: Option<f64>,+    pub median_distance_at_unity_mm: Option<f64>,+    pub reason: String,+}++const CANDIDATE_FACTORS: [f64; 4] = [1.0, 10.0, 100.0, 1000.0];+/// A pin belongs within this radius of its symbol anchor on any sane sheet.+const MAX_PLAUSIBLE_MM: f64 = 60.0;++/// Reference designator -> anchor positions (multi-unit symbols have several).+fn symbol_anchors(root: &Node) -> BTreeMap<String, Vec<(f64, f64)>> {+    let mut map: BTreeMap<String, Vec<(f64, f64)>> = BTreeMap::new();+    for sym in root.children("symbol") {+        let Some(at) = sym.child("at") else { continue };+        let (Some(x), Some(y)) = (at.items().get(1).and_then(Node::as_f64), at.items().get(2).and_then(Node::as_f64)) else { continue };+        let reference = sym.children("property").find_map(|p| {+            let items = p.items();+            (items.get(1).and_then(Node::text) == Some("Reference")).then(|| items.get(2).and_then(Node::text)).flatten()+        });+        if let Some(r) = reference {+            map.entry(r.to_string()).or_default().push((x, y));+        }+    }+    map+}++/// "Symbol U1 Pin 3 [EN, Input, Line]" -> "U1"+fn item_reference(description: &str) -> Option<&str> {+    description.strip_prefix("Symbol ")?.split_whitespace().next()+}++fn median(v: &mut Vec<f64>) -> Option<f64> {+    if v.is_empty() { return None; }+    v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));+    Some(v[v.len() / 2])+}++fn not_checked(reason: impl Into<String>) -> ScaleCheck {+    ScaleCheck { status: "not_checked", factor: None, matched_items: 0, median_distance_mm: None, median_distance_at_unity_mm: None, reason: reason.into() }+}++pub fn check_erc_scale(schematic_bytes: &[u8], report: &Value) -> ScaleCheck {+    let Ok(text) = std::str::from_utf8(schematic_bytes) else { return not_checked("schematic is not UTF-8") };+    let root = match parse(text) { Ok(r) => r, Err(e) => return not_checked(format!("schematic did not parse: {e}")) };+    let anchors = symbol_anchors(&root);+    if anchors.is_empty() { return not_checked("no placed symbols with a Reference in the schematic"); }++    // (item pos, candidate anchors)+    let mut pairs: Vec<((f64, f64), &Vec<(f64, f64)>)> = Vec::new();+    for sheet in report.get("sheets").and_then(Value::as_array).into_iter().flatten() {+        for v in sheet.get("violations").and_then(Value::as_array).into_iter().flatten() {+            for item in v.get("items").and_then(Value::as_array).into_iter().flatten() {+                let Some(reference) = item.get("description").and_then(Value::as_str).and_then(item_reference) else { continue };+                let Some(list) = anchors.get(reference) else { continue };+                let (Some(x), Some(y)) = (item.pointer("/pos/x").and_then(Value::as_f64), item.pointer("/pos/y").and_then(Value::as_f64)) else { continue };+                pairs.push(((x, y), list));+            }+        }+    }+    if pairs.is_empty() { return not_checked("no ERC item names a symbol present in the schematic"); }++    let median_for = |factor: f64| {+        let mut d: Vec<f64> = pairs.iter().map(|((x, y), list)| {+            list.iter().map(|(ax, ay)| ((x * factor - ax).powi(2) + (y * factor - ay).powi(2)).sqrt())+                .fold(f64::INFINITY, f64::min)+        }).collect();+        median(&mut d).unwrap_or(f64::INFINITY)+    };+    let scored: Vec<(f64, f64)> = CANDIDATE_FACTORS.iter().map(|f| (*f, median_for(*f))).collect();+    let unity = scored[0].1;+    let (best, best_d) = scored.iter().copied().fold((1.0, f64::INFINITY), |acc, s| if s.1 < acc.1 { s } else { acc });+    let matched_items = pairs.len();++    if best == 1.0 || best_d > MAX_PLAUSIBLE_MM || unity <= best_d * 3.0 {+        let status = if unity <= MAX_PLAUSIBLE_MM { "consistent" } else { "not_checked" };+        return ScaleCheck {+            status, factor: (status == "consistent").then_some(1.0), matched_items,+            median_distance_mm: Some(unity), median_distance_at_unity_mm: Some(unity),+            reason: if status == "consistent" { format!("{matched_items} ERC item(s) sit within {unity:.2} mm (median) of the symbols they name") }+                    else { format!("no candidate factor places the {matched_items} matched item(s) near their symbols (median {unity:.2} mm at 1x)") },+        };+    }+    ScaleCheck {+        status: "scaled", factor: Some(best), matched_items,+        median_distance_mm: Some(best_d), median_distance_at_unity_mm: Some(unity),+        reason: format!(+            "{matched_items} ERC item(s) land a median {best_d:.2} mm from the symbols they name only after multiplying pos by {best:.0} (at 1x the median is {unity:.2} mm)"),+    }+}++#[cfg(test)]+mod tests {+    use super::*;+    use serde_json::json;++    fn fixture(name: &str) -> Vec<u8> {+        std::fs::read(format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap()+    }++    #[test]+    fn issue_2_kicad_10_erc_json_is_100x_small() {+        let report: Value = serde_json::from_slice(&fixture("erc-native-scaled.json")).unwrap();+        let c = check_erc_scale(&fixture("erc-source-scaled.kicad_sch"), &report);+        assert_eq!(c.status, "scaled", "{c:?}");+        assert_eq!(c.factor, Some(100.0));+        assert!(c.matched_items >= 3);+        assert!(c.median_distance_mm.unwrap() < 30.0, "{c:?}");+    }++    #[test]+    fn consistent_report_is_not_flagged() {+        let sch = br#"(kicad_sch (version 20250114) (symbol (lib_id "X:Y") (at 80 86.59 0) (property "Reference" "U1" (at 0 0 0))))"#;+        let report = json!({"sheets": [{"violations": [{"severity":"error","items":[{"description":"Symbol U1 Pin 3 [EN]","pos":{"x":52.06,"y":67.54}}]}]}]});+        let c = check_erc_scale(sch, &report);+        assert_eq!(c.status, "consistent", "{c:?}");+        assert_eq!(c.factor, Some(1.0));+    }++    #[test]+    fn scaled_synthetic_is_flagged() {+        let sch = br#"(kicad_sch (version 20250114) (symbol (lib_id "X:Y") (at 80 86.59 0) (property "Reference" "U1" (at 0 0 0))))"#;+        let report = json!({"sheets": [{"violations": [{"severity":"error","items":[{"description":"Symbol U1 Pin 3 [EN]","pos":{"x":0.5206,"y":0.6754}}]}]}]});+        let c = check_erc_scale(sch, &report);+        assert_eq!(c.status, "scaled", "{c:?}");+        assert_eq!(c.factor, Some(100.0));+    }++    #[test]+    fn nothing_to_match_is_not_checked() {+        let sch = br#"(kicad_sch (version 20250114) (wire (pts (xy 1 1) (xy 2 2))))"#;+        let c = check_erc_scale(sch, &json!({"sheets": []}));+        assert_eq!(c.status, "not_checked");+    }+}
report/src/lib.rsadded+24
@@ -0,0 +1,24 @@+//! Honest reading of native `kicad-cli` DRC / ERC JSON.+//!+//! Three jobs, all pure functions over bytes the caller already holds:+//!+//! - [`summary`]: the documented COUNT POLICY. kicad-cli's JSON carries no+//!   precomputed totals, so callers that look for `errors` / `count` keys+//!   print zeros next to real violations (adom/service-kicad#3). Counts are+//!   derived from every native finding collection, by severity, and a DRC /+//!   ERC pass is stated explicitly and separately from HTTP success.+//! - [`erc_scale`]: an input-relative sanity check for ERC item positions.+//!   KiCad 10.0's `ERC_REPORT::WriteJsonReport` builds its units provider+//!   with `pcbIUScale` (1e6 IU/mm) while the schematic is stored at+//!   `schIUScale` (1e4 IU/mm), so JSON positions come out 100x too small+//!   (adom/service-kicad#2). The check reports the factor; nothing is+//!   rescaled.+//! - [`sexpr`]: a tiny s-expression reader used both for the scale check and+//!   for diagnosing files kicad-cli refuses with a bare "Failed to load".+pub mod erc_scale;+pub mod sexpr;+pub mod summary;++pub use erc_scale::{check_erc_scale, ScaleCheck};+pub use sexpr::{diagnose, Node};+pub use summary::{Hint, ReportKind, Summary};
report/src/sexpr.rsadded+239
@@ -0,0 +1,239 @@+//! Minimal KiCad s-expression reader. Enough to walk `(kicad_sch ...)` /+//! `(kicad_pcb ...)` files and to say WHERE a malformed one breaks.++#[derive(Debug, Clone, PartialEq)]+pub enum Node {+    /// `(head child child ...)`; `line` is where the list opened (1-based).+    List { items: Vec<Node>, line: usize },+    /// Bare token: numbers, keywords, unquoted identifiers.+    Atom(String),+    /// `"quoted"` string, escapes resolved.+    Str(String),+}++impl Node {+    pub fn items(&self) -> &[Node] {+        match self { Node::List { items, .. } => items, _ => &[] }+    }+    /// Head keyword of a list, e.g. `symbol` for `(symbol ...)`.+    pub fn head(&self) -> Option<&str> {+        match self.items().first() { Some(Node::Atom(a)) => Some(a), _ => None }+    }+    /// Text of an atom or string.+    pub fn text(&self) -> Option<&str> {+        match self { Node::Atom(a) | Node::Str(a) => Some(a), _ => None }+    }+    pub fn as_f64(&self) -> Option<f64> {+        self.text().and_then(|t| t.parse().ok())+    }+    /// Child lists with the given head keyword.+    pub fn children<'a, 'h: 'a>(&'a self, head: &'h str) -> impl Iterator<Item = &'a Node> + 'a {+        self.items().iter().skip(1).filter(move |n| n.head() == Some(head))+    }+    pub fn child(&self, head: &str) -> Option<&Node> {+        self.items().iter().skip(1).find(|n| n.head() == Some(head))+    }+}++#[derive(Debug, Clone, PartialEq)]+pub struct ParseError {+    pub line: usize,+    pub col: usize,+    pub message: String,+}++impl std::fmt::Display for ParseError {+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {+        write!(f, "line {}, col {}: {}", self.line, self.col, self.message)+    }+}++struct Reader<'a> {+    chars: std::iter::Peekable<std::str::CharIndices<'a>>,+    line: usize,+    col: usize,+}++impl<'a> Reader<'a> {+    fn new(s: &'a str) -> Self {+        Reader { chars: s.char_indices().peekable(), line: 1, col: 0 }+    }+    fn bump(&mut self) -> Option<char> {+        let (_, c) = self.chars.next()?;+        if c == '\n' { self.line += 1; self.col = 0; } else { self.col += 1; }+        Some(c)+    }+    fn peek(&mut self) -> Option<char> {+        self.chars.peek().map(|(_, c)| *c)+    }+    fn err(&self, message: impl Into<String>) -> ParseError {+        ParseError { line: self.line, col: self.col, message: message.into() }+    }+    fn skip_ws(&mut self) {+        while matches!(self.peek(), Some(c) if c.is_whitespace()) { self.bump(); }+    }+    fn node(&mut self, depth: usize) -> Result<Node, ParseError> {+        self.skip_ws();+        match self.peek() {+            None => Err(self.err("unexpected end of file")),+            Some('(') => {+                self.bump();+                let line = self.line;+                let mut items = Vec::new();+                loop {+                    self.skip_ws();+                    match self.peek() {+                        None => {+                            return Err(ParseError {+                                line: self.line,+                                col: self.col,+                                message: format!("unterminated list opened at line {line} (depth {depth}); missing ')'"),+                            })+                        }+                        Some(')') => { self.bump(); return Ok(Node::List { items, line }); }+                        Some(_) => items.push(self.node(depth + 1)?),+                    }+                }+            }+            Some(')') => Err(self.err("unexpected ')' with no open list")),+            Some('"') => {+                self.bump();+                let mut s = String::new();+                loop {+                    match self.bump() {+                        None => return Err(self.err("unterminated string")),+                        Some('"') => return Ok(Node::Str(s)),+                        Some('\\') => match self.bump() {+                            Some('n') => s.push('\n'),+                            Some('t') => s.push('\t'),+                            Some('r') => s.push('\r'),+                            Some(c) => s.push(c),+                            None => return Err(self.err("unterminated string escape")),+                        },+                        Some(c) => s.push(c),+                    }+                }+            }+            Some(_) => {+                let mut s = String::new();+                while let Some(c) = self.peek() {+                    if c.is_whitespace() || c == '(' || c == ')' || c == '"' { break; }+                    s.push(c);+                    self.bump();+                }+                Ok(Node::Atom(s))+            }+        }+    }+}++/// Parse one root list. Trailing content after it is an error.+pub fn parse(text: &str) -> Result<Node, ParseError> {+    let mut r = Reader::new(text);+    let root = r.node(0)?;+    r.skip_ws();+    if r.peek().is_some() {+        return Err(r.err("trailing content after the root list"));+    }+    Ok(root)+}++/// Explain why kicad-cli might refuse a file. `expected_root` is+/// `kicad_sch` or `kicad_pcb`. Returns an empty list when nothing obvious+/// is wrong (the parser cannot judge semantic validity).+pub fn diagnose(bytes: &[u8], expected_root: &str) -> Vec<String> {+    let mut out = Vec::new();+    if bytes.is_empty() {+        out.push("input is empty (0 bytes)".into());+        return out;+    }+    if let Some(i) = bytes.iter().position(|b| *b == 0) {+        out.push(format!("binary content: NUL byte at offset {i}; expected a text .{expected_root} file"));+        return out;+    }+    let text = match std::str::from_utf8(bytes) {+        Ok(t) => t,+        Err(e) => {+            out.push(format!("not valid UTF-8 at byte {}", e.valid_up_to()));+            return out;+        }+    };+    let trimmed = text.trim_start_matches('\u{feff}').trim_start();+    if trimmed.starts_with("EESchema Schematic File Version") {+        out.push("legacy KiCad 5 schematic format (.sch); kicad-cli needs the s-expression .kicad_sch format".into());+        return out;+    }+    if trimmed.starts_with("(kicad_pcb") && expected_root == "kicad_sch" {+        out.push("this is a board (.kicad_pcb), not a schematic".into());+        return out;+    }+    if trimmed.starts_with("(kicad_sch") && expected_root == "kicad_pcb" {+        out.push("this is a schematic (.kicad_sch), not a board".into());+        return out;+    }+    if !trimmed.starts_with('(') {+        let first: String = trimmed.chars().take(40).collect();+        out.push(format!("file does not start with '(': first bytes are {first:?}"));+        return out;+    }+    match parse(text) {+        Err(e) => {+            out.push(format!("s-expression error at {e}"));+            let (opens, closes) = text.chars().fold((0usize, 0usize), |(o, c), ch| match ch {+                '(' => (o + 1, c),+                ')' => (o, c + 1),+                _ => (o, c),+            });+            if opens != closes {+                out.push(format!("paren balance: {opens} '(' vs {closes} ')' (file may be truncated)"));+            }+        }+        Ok(root) => {+            match root.head() {+                Some(h) if h == expected_root => {}+                Some(h) => out.push(format!("root list is ({h} ...), expected ({expected_root} ...)")),+                None => out.push(format!("root list has no head keyword, expected ({expected_root} ...)")),+            }+            if let Some(v) = root.child("version").and_then(|n| n.items().get(1)).and_then(Node::text) {+                if v.len() != 8 || !v.chars().all(|c| c.is_ascii_digit()) {+                    out.push(format!("(version {v}) is not a YYYYMMDD format version"));+                }+            } else {+                out.push("no (version ...) token under the root".into());+            }+        }+    }+    out+}++#[cfg(test)]+mod tests {+    use super::*;++    #[test]+    fn parses_nested_lists_and_strings() {+        let n = parse(r#"(kicad_sch (version 20250114) (property "Reference" "U1" (at 1.5 -2 0)))"#).unwrap();+        assert_eq!(n.head(), Some("kicad_sch"));+        let prop = n.child("property").unwrap();+        assert_eq!(prop.items()[1].text(), Some("Reference"));+        assert_eq!(prop.child("at").unwrap().items()[2].as_f64(), Some(-2.0));+    }++    #[test]+    fn truncated_file_names_the_open_list() {+        let d = diagnose(b"(kicad_sch (version 20250114)\n  (symbol (at 1 2", "kicad_sch");+        assert!(d[0].contains("unterminated list"), "{d:?}");+        assert!(d.iter().any(|s| s.contains("paren balance")), "{d:?}");+    }++    #[test]+    fn wrong_kind_is_named() {+        let d = diagnose(b"(kicad_pcb (version 20240108))", "kicad_sch");+        assert_eq!(d, vec!["this is a board (.kicad_pcb), not a schematic".to_string()]);+    }++    #[test]+    fn well_formed_has_no_diagnostics() {+        assert!(diagnose(b"(kicad_sch (version 20250114) (generator x))", "kicad_sch").is_empty());+    }+}
report/src/summary.rsadded+317
@@ -0,0 +1,317 @@+//! The count policy for native kicad-cli DRC / ERC JSON.+//!+//! kicad-cli's JSON (`https://schemas.kicad.org/drc.v1.json`,+//! `erc.v1.json`) has NO precomputed totals. Every count here is derived by+//! walking the finding collections and bucketing on each finding's+//! `severity`:+//!+//! - DRC collections: `violations`, `unconnected_items`, `schematic_parity`.+//! - ERC collections: every `sheets[*].violations`.+//! - severities: `error` and `warning` count; `exclusion` (user-excluded+//!   markers) is reported but never counted toward a pass/fail; anything+//!   else lands in `other` and is reported.+//!+//! Status is stated explicitly: `failed` when any error-severity finding+//! exists in any collection, `passed_with_warnings`, `passed`, or `unknown`+//! when the report lacks the collections the schema promises. HTTP success+//! is never a pass.+use serde::Serialize;+use serde_json::Value;+use std::collections::BTreeMap;++#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]+#[serde(rename_all = "lowercase")]+pub enum ReportKind { Drc, Erc }++impl ReportKind {+    pub fn label(self) -> &'static str { match self { ReportKind::Drc => "DRC", ReportKind::Erc => "ERC" } }+}++#[derive(Debug, Clone, Default, Serialize, PartialEq)]+pub struct Counts {+    pub errors: usize,+    pub warnings: usize,+    pub exclusions: usize,+    pub other: usize,+    pub total: usize,+}++impl Counts {+    fn add(&mut self, severity: Option<&str>) {+        self.total += 1;+        match severity {+            Some("error") => self.errors += 1,+            Some("warning") => self.warnings += 1,+            Some("exclusion") => self.exclusions += 1,+            _ => self.other += 1,+        }+    }+    fn merge(&mut self, o: &Counts) {+        self.errors += o.errors; self.warnings += o.warnings; self.exclusions += o.exclusions;+        self.other += o.other; self.total += o.total;+    }+}++#[derive(Debug, Clone, Serialize, PartialEq)]+pub struct Hint {+    pub level: &'static str,+    pub code: &'static str,+    pub message: String,+}++/// Exit code the CLI uses when a report completed and carries+/// error-severity findings. Distinct from 1..4 (input / transport /+/// auth / service failures) so callers can tell "DRC ran and failed"+/// from "DRC could not run".+pub const EXIT_FINDINGS: i32 = 5;++#[derive(Debug, Clone, Serialize)]+pub struct Summary {+    pub kind: ReportKind,+    /// `failed` | `passed_with_warnings` | `passed` | `unknown`+    pub status: &'static str,+    /// `true` only when status is `passed` or `passed_with_warnings`.+    pub passed: bool,+    #[serde(flatten)]+    pub counts: Counts,+    /// Per-collection counts, in schema order. A collection the schema+    /// promises but the report lacks is listed with `present: false`.+    pub collections: BTreeMap<String, CollectionCounts>,+    /// ERC only: number of sheets in the report.+    #[serde(skip_serializing_if = "Option::is_none")]+    pub sheets: Option<usize>,+    pub kicad_version: Option<String>,+    pub coordinate_units: Option<String>,+    pub included_severities: Vec<String>,+    /// Keys of checks kicad-cli reports as ignored (not run). A pass says+    /// nothing about these.+    pub ignored_checks: Vec<String>,+    pub count_policy: &'static str,+    pub hints: Vec<Hint>,+    pub exit_code: i32,+}++#[derive(Debug, Clone, Serialize, PartialEq)]+pub struct CollectionCounts {+    pub present: bool,+    #[serde(flatten)]+    pub counts: Counts,+}++pub const COUNT_POLICY: &str = "counts derived from every native finding by severity; error => failed; exclusion never counts; a missing collection => unknown; HTTP success is not a pass";++fn count_array(v: Option<&Value>) -> Option<Counts> {+    let arr = v?.as_array()?;+    let mut c = Counts::default();+    for item in arr {+        c.add(item.get("severity").and_then(Value::as_str));+    }+    Some(c)+}++fn strings(v: Option<&Value>, key: Option<&str>) -> Vec<String> {+    v.and_then(Value::as_array).map(|a| a.iter().filter_map(|x| match key {+        Some(k) => x.get(k).and_then(Value::as_str).map(str::to_string),+        None => x.as_str().map(str::to_string),+    }).collect()).unwrap_or_default()+}++impl Summary {+    pub fn from_report(kind: ReportKind, report: &Value) -> Summary {+        let mut collections = BTreeMap::new();+        let mut counts = Counts::default();+        let mut hints = Vec::new();+        let mut missing = Vec::new();+        let mut sheets = None;++        match kind {+            ReportKind::Drc => {+                for name in ["violations", "unconnected_items", "schematic_parity"] {+                    match count_array(report.get(name)) {+                        Some(c) => { counts.merge(&c); collections.insert(name.to_string(), CollectionCounts { present: true, counts: c }); }+                        None => { missing.push(name); collections.insert(name.to_string(), CollectionCounts { present: false, counts: Counts::default() }); }+                    }+                }+            }+            ReportKind::Erc => {+                match report.get("sheets").and_then(Value::as_array) {+                    Some(list) => {+                        sheets = Some(list.len());+                        let mut c = Counts::default();+                        for sheet in list {+                            match count_array(sheet.get("violations")) {+                                Some(sc) => c.merge(&sc),+                                None => missing.push("sheets[].violations"),+                            }+                        }+                        counts.merge(&c);+                        collections.insert("violations".to_string(), CollectionCounts { present: missing.is_empty(), counts: c });+                    }+                    None => { missing.push("sheets"); collections.insert("violations".to_string(), CollectionCounts { present: false, counts: Counts::default() }); }+                }+            }+        }+        missing.dedup();++        let status = if !missing.is_empty() && counts.total == 0 {+            "unknown"+        } else if counts.errors > 0 {+            "failed"+        } else if counts.warnings > 0 {+            "passed_with_warnings"+        } else {+            "passed"+        };+        if !missing.is_empty() {+            hints.push(Hint { level: "warning", code: "REPORT_COLLECTION_MISSING", message: format!(+                "native report lacks {}; counts cover only the collections present and the {} status is {}",+                missing.join(", "), kind.label(), status) });+        }+        if counts.other > 0 {+            hints.push(Hint { level: "warning", code: "UNKNOWN_SEVERITY", message: format!(+                "{} finding(s) carry a severity other than error/warning/exclusion; they are listed but not counted toward pass/fail", counts.other) });+        }+        let ignored_checks = strings(report.get("ignored_checks"), Some("key"));+        if status != "failed" && !ignored_checks.is_empty() {+            hints.push(Hint { level: "info", code: "CHECKS_IGNORED", message: format!(+                "{} check(s) were not run ({}); a {} pass does not cover them",+                ignored_checks.len(), ignored_checks.join(", "), kind.label()) });+        }+        let exit_code = if counts.errors > 0 { EXIT_FINDINGS } else { 0 };+        if counts.errors > 0 {+            hints.push(Hint { level: "error", code: match kind { ReportKind::Drc => "DRC_FAILED", ReportKind::Erc => "ERC_FAILED" }, message: format!(+                "{} error-severity finding(s); the report was produced but the {} did not pass; exit code {}",+                counts.errors, kind.label(), EXIT_FINDINGS) });+        }+        Summary {+            kind, status, passed: matches!(status, "passed" | "passed_with_warnings"), counts, collections, sheets,+            kicad_version: report.get("kicad_version").and_then(Value::as_str).map(str::to_string),+            coordinate_units: report.get("coordinate_units").and_then(Value::as_str).map(str::to_string),+            included_severities: strings(report.get("included_severities"), None),+            ignored_checks, count_policy: COUNT_POLICY, hints, exit_code,+        }+    }++    /// Every finding across every collection, flattened, in report order,+    /// with the collection (and ERC sheet path) it came from.+    pub fn findings<'a>(kind: ReportKind, report: &'a Value) -> Vec<Finding<'a>> {+        let mut out = Vec::new();+        let push = |out: &mut Vec<Finding<'a>>, collection: &'static str, sheet: Option<&'a str>, arr: Option<&'a Value>| {+            for v in arr.and_then(Value::as_array).into_iter().flatten() {+                let first = v.get("items").and_then(Value::as_array).and_then(|a| a.first());+                out.push(Finding {+                    collection, sheet,+                    severity: v.get("severity").and_then(Value::as_str).unwrap_or("?"),+                    kind: v.get("type").and_then(Value::as_str).unwrap_or("?"),+                    description: v.get("description").or_else(|| v.get("message")).and_then(Value::as_str).unwrap_or(""),+                    first_item: first.and_then(|i| i.get("description")).and_then(Value::as_str),+                    pos: first.and_then(|i| i.get("pos")).and_then(|p| Some((p.get("x")?.as_f64()?, p.get("y")?.as_f64()?))),+                });+            }+        };+        match kind {+            ReportKind::Drc => {+                push(&mut out, "violations", None, report.get("violations"));+                push(&mut out, "unconnected_items", None, report.get("unconnected_items"));+                push(&mut out, "schematic_parity", None, report.get("schematic_parity"));+            }+            ReportKind::Erc => {+                for sheet in report.get("sheets").and_then(Value::as_array).into_iter().flatten() {+                    push(&mut out, "violations", sheet.get("path").and_then(Value::as_str), sheet.get("violations"));+                }+            }+        }+        out+    }+}++#[derive(Debug, Clone)]+pub struct Finding<'a> {+    pub collection: &'static str,+    pub sheet: Option<&'a str>,+    pub severity: &'a str,+    pub kind: &'a str,+    pub description: &'a str,+    pub first_item: Option<&'a str>,+    pub pos: Option<(f64, f64)>,+}++#[cfg(test)]+mod tests {+    use super::*;+    use serde_json::json;++    fn fixture(name: &str) -> Value {+        let p = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/");+        serde_json::from_str(&std::fs::read_to_string(format!("{p}{name}")).unwrap()).unwrap()+    }++    #[test]+    fn issue_3_native_drc_counts_come_from_violations() {+        let s = Summary::from_report(ReportKind::Drc, &fixture("drc-native-clearance.json"));+        assert_eq!((s.counts.errors, s.counts.warnings, s.counts.total), (2, 1, 3));+        assert_eq!(s.status, "failed");+        assert!(!s.passed);+        assert_eq!(s.exit_code, EXIT_FINDINGS);+        assert_eq!(s.kicad_version.as_deref(), Some("10.0.2"));+        assert_eq!(s.coordinate_units.as_deref(), Some("mm"));+        assert_eq!(s.ignored_checks.len(), 5);+        assert!(s.collections["unconnected_items"].present);+        assert!(s.hints.iter().any(|h| h.code == "DRC_FAILED"));+    }++    #[test]+    fn unconnected_and_parity_count_too() {+        let r = json!({"violations": [], "unconnected_items": [{"severity":"error"}], "schematic_parity": [{"severity":"warning"}, {"severity":"exclusion"}]});+        let s = Summary::from_report(ReportKind::Drc, &r);+        assert_eq!((s.counts.errors, s.counts.warnings, s.counts.exclusions, s.counts.total), (1, 1, 1, 3));+        assert_eq!(s.status, "failed");+    }++    #[test]+    fn exclusions_never_fail_a_board() {+        let r = json!({"violations": [{"severity":"exclusion"}], "unconnected_items": [], "schematic_parity": []});+        let s = Summary::from_report(ReportKind::Drc, &r);+        assert_eq!(s.status, "passed");+        assert!(s.passed);+        assert_eq!(s.exit_code, 0);+    }++    #[test]+    fn warnings_only_pass_with_warnings() {+        let r = json!({"violations": [{"severity":"warning"}], "unconnected_items": [], "schematic_parity": []});+        let s = Summary::from_report(ReportKind::Drc, &r);+        assert_eq!(s.status, "passed_with_warnings");+        assert_eq!(s.exit_code, 0);+    }++    #[test]+    fn missing_collections_are_unknown_not_passed() {+        let s = Summary::from_report(ReportKind::Drc, &json!({"kicad_version": "10.0.2"}));+        assert_eq!(s.status, "unknown");+        assert!(!s.passed);+        assert!(s.hints.iter().any(|h| h.code == "REPORT_COLLECTION_MISSING"));+        assert!(!s.collections["violations"].present);+    }++    #[test]+    fn erc_counts_span_sheets() {+        let s = Summary::from_report(ReportKind::Erc, &fixture("erc-native-scaled.json"));+        assert_eq!(s.sheets, Some(1));+        assert!(s.counts.errors > 0, "{s:?}");+        assert_eq!(s.status, "failed");+        let multi = json!({"sheets": [{"violations": [{"severity":"warning"}]}, {"violations": [{"severity":"error"}]}]});+        let m = Summary::from_report(ReportKind::Erc, &multi);+        assert_eq!((m.counts.errors, m.counts.warnings, m.sheets), (1, 1, Some(2)));+    }++    #[test]+    fn findings_flatten_with_positions() {+        let report = fixture("drc-native-clearance.json");+        let f = Summary::findings(ReportKind::Drc, &report);+        assert_eq!(f.len(), 3);+        assert_eq!(f[0].kind, "clearance");+        assert_eq!(f[0].pos, Some((9.45, 9.5)));+        assert_eq!(f[2].severity, "warning");+    }+}
report/tests/fixtures/aperture-check.kicad_pcbadded+51
@@ -0,0 +1,51 @@+(kicad_pcb (version 20241229) (generator "DepthLab")+(general (thickness 1.6)) (paper "A4")+(layers (0 "F.Cu" signal) (31 "B.Cu" signal) (35 "F.Paste" user) (37 "F.SilkS" user) (39 "F.Mask" user) (44 "Edge.Cuts" user) (47 "F.CrtYd" user) (49 "F.Fab" user))+(setup (pad_to_mask_clearance 0))+(gr_rect (start 7.5 7.5) (end 12.5 12.5) (stroke (width 0.05) (type default)) (fill none) (layer "Edge.Cuts"))+(footprint "TPS389001DSER"+(at 10 10)+	(version 20240108)+	(generator "adom-lbr")+	(generator_version "8.0")+	(layer "F.Cu")+	(attr smd)+	(fp_text reference "REF**" (at 0 -2.075 0) (layer "F.SilkS")+		(effects (font (size 1 1) (thickness 0.15)))+	)+	(fp_text value "TPS389001DSER" (at 0 2.075 0) (layer "F.Fab")+		(effects (font (size 1 1) (thickness 0.15)))+	)+	(fp_line (start -0.75 -0.5) (end -0.5 -0.75)+		(stroke (width 0.1) (type default)) (layer "F.Fab"))+	(fp_line (start -0.65 -0.9) (end 0.75 -0.9)+		(stroke (width 0.12) (type default)) (layer "F.SilkS"))+	(fp_line (start -0.75 0.9) (end 0.75 0.9)+		(stroke (width 0.12) (type default)) (layer "F.SilkS"))+	(fp_rect (start -0.75 -0.75) (end 0.75 0.75)+		(stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))+	(fp_rect (start -1.25 -1.05) (end 1.25 1.05)+		(stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))+	(fp_circle (center -1.1 -0.95) (end -1.04 -0.95)+		(stroke (width 0.1) (type default)) (fill none) (layer "F.SilkS"))+	(pad "1" smd roundrect (at -0.55 -0.5) (size 0.9 0.35) (layers "F.Cu") (roundrect_rratio 0.285714))+	(pad "" smd roundrect (at -0.55 -0.5) (size 0.8 0.25) (layers "F.Mask") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at -0.55 -0.5) (size 0.8 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+	(pad "2" smd roundrect (at -0.6 0) (size 0.8 0.35) (layers "F.Cu") (roundrect_rratio 0.285714))+	(pad "" smd roundrect (at -0.6 0) (size 0.7 0.25) (layers "F.Mask") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at -0.6 0) (size 0.7 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+	(pad "3" smd roundrect (at -0.6 0.5) (size 0.8 0.35) (layers "F.Cu") (roundrect_rratio 0.285714))+	(pad "" smd roundrect (at -0.6 0.5) (size 0.7 0.25) (layers "F.Mask") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at -0.6 0.5) (size 0.7 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+	(pad "4" smd roundrect (at 0.6 0.5) (size 0.7 0.25) (layers "F.Cu") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at 0.6 0.5) (size 0.8 0.35) (layers "F.Mask") (roundrect_rratio 0.142857))+	(pad "" smd roundrect (at 0.6 0.5) (size 0.7 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+	(pad "5" smd roundrect (at 0.6 0) (size 0.7 0.25) (layers "F.Cu") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at 0.6 0) (size 0.8 0.35) (layers "F.Mask") (roundrect_rratio 0.142857))+	(pad "" smd roundrect (at 0.6 0) (size 0.7 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+	(pad "6" smd roundrect (at 0.6 -0.5) (size 0.7 0.25) (layers "F.Cu") (roundrect_rratio 0.2))+	(pad "" smd roundrect (at 0.6 -0.5) (size 0.8 0.35) (layers "F.Mask") (roundrect_rratio 0.142857))+	(pad "" smd roundrect (at 0.6 -0.5) (size 0.7 0.25) (layers "F.Paste") (roundrect_rratio 0.2))+)++)
report/tests/fixtures/drc-native-clearance.jsonadded+106
@@ -0,0 +1,106 @@+{+    "$schema": "https://schemas.kicad.org/drc.v1.json",+    "coordinate_units": "mm",+    "date": "2026-09-09T20:26:16",+    "ignored_checks": [+        {+            "description": "Footprint has no courtyard defined",+            "key": "missing_courtyard"+        },+        {+            "description": "Track endpoint not centered on via",+            "key": "track_not_centered_on_via"+        },+        {+            "description": "Tuning profile track geometries",+            "key": "tuning_profile_track_geometries"+        },+        {+            "description": "Footprint doesn't match symbol's footprint filters",+            "key": "footprint_filters_mismatch"+        },+        {+            "description": "Footprint component type doesn't match footprint pads",+            "key": "footprint_type_mismatch"+        }+    ],+    "included_severities": [+        "error",+        "warning"+    ],+    "kicad_version": "10.0.2",+    "schematic_parity": [],+    "source": ".tmpsChoCJ.kicad_pcb",+    "unconnected_items": [],+    "violations": [+        {+            "description": "Clearance violation ( clearance 0.2000 mm; actual 0.1500 mm)",+            "items": [+                {+                    "description": "Pad 1 [<no net>] of REF** on F.Cu",+                    "pos": {+                        "x": 9.45,+                        "y": 9.5+                    },+                    "uuid": "4fbc9285-5fbf-4215-8f29-3989385d4f70"+                },+                {+                    "description": "Pad 2 [<no net>] of REF** on F.Cu",+                    "pos": {+                        "x": 9.4,+                        "y": 10.0+                    },+                    "uuid": "566bd8e5-6074-4e17-9d66-5584543defec"+                }+            ],+            "severity": "error",+            "type": "clearance"+        },+        {+            "description": "Clearance violation ( clearance 0.2000 mm; actual 0.1500 mm)",+            "items": [+                {+                    "description": "Pad 2 [<no net>] of REF** on F.Cu",+                    "pos": {+                        "x": 9.4,+                        "y": 10.0+                    },+                    "uuid": "566bd8e5-6074-4e17-9d66-5584543defec"+                },+                {+                    "description": "Pad 3 [<no net>] of REF** on F.Cu",+                    "pos": {+                        "x": 9.4,+                        "y": 10.5+                    },+                    "uuid": "0a6410d9-93b4-4622-b98e-e8adef035143"+                }+            ],+            "severity": "error",+            "type": "clearance"+        },+        {+            "description": "Silkscreen clipped by board edge",+            "items": [+                {+                    "description": "Rectangle on Edge.Cuts",+                    "pos": {+                        "x": 7.5,+                        "y": 7.5+                    },+                    "uuid": "41b66b7f-8775-42fb-951b-c3aa939ec210"+                },+                {+                    "description": "Reference field of REF**",+                    "pos": {+                        "x": 10.0,+                        "y": 7.925+                    },+                    "uuid": "9016ab2b-ced6-4a2b-92e0-a71f62b5c1c2"+                }+            ],+            "severity": "warning",+            "type": "silk_edge_clearance"+        }+    ]+}
report/tests/fixtures/erc-native-scaled.jsonadded+667
@@ -0,0 +1,667 @@+{+    "$schema": "https://schemas.kicad.org/erc.v1.json",+    "coordinate_units": "mm",+    "date": "2026-09-09T17:00:03",+    "ignored_checks": [+        {+            "description": "Global label only appears once in the schematic",+            "key": "single_global_label"+        },+        {+            "description": "Four connection points are joined together",+            "key": "four_way_junction"+        },+        {+            "description": "SPICE model issue",+            "key": "simulation_model_issue"+        },+        {+            "description": "Assigned footprint doesn't match footprint filters",+            "key": "footprint_filter"+        }+    ],+    "included_severities": [+        "error",+        "warning"+    ],+    "kicad_version": "10.0.2",+    "sheets": [+        {+            "path": "/",+            "uuid_path": "/aebf546f-22da-58c9-9780-4f637b9875de",+            "violations": [+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 3 [EN, Input, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.6754+                            },+                            "uuid": "a9b53e30-ce0a-541f-ae72-54b1fee0c9be"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Input pin not driven by any Output pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 3 [EN, Input, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.6754+                            },+                            "uuid": "a9b53e30-ce0a-541f-ae72-54b1fee0c9be"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_driven"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 4 [INT/GPIO6, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.7516+                            },+                            "uuid": "e5068388-5492-50d2-a64e-1ab07b6a7a57"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 10 [SCL/GPIO4, Input, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.8278+                            },+                            "uuid": "b69639ca-29d1-59c6-8d72-a1ff896ac05d"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Input pin not driven by any Output pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 10 [SCL/GPIO4, Input, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.8278+                            },+                            "uuid": "b69639ca-29d1-59c6-8d72-a1ff896ac05d"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_driven"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 11 [SDA/GPIO5, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.8532+                            },+                            "uuid": "562f2504-575c-5744-bd51-96ab7878fbdb"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 5 [MOSI/GPIO0, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.9548+                            },+                            "uuid": "2b20f24f-fd13-5381-b986-9328f1e89911"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 6 [CSN/GPIO1, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.9802+                            },+                            "uuid": "926ad9cb-17dd-5e19-a435-12da99196d75"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 12 [SCLK/GPIO2, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 1.0056+                            },+                            "uuid": "4ca9efe0-ca4f-53ea-9469-163250cd1298"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Pin not connected",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 13 [MISO/GPIO3, Bidirectional, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 1.031+                            },+                            "uuid": "8aeb69e3-8ade-5daf-960b-2e46488d99ec"+                        }+                    ],+                    "severity": "error",+                    "type": "pin_not_connected"+                },+                {+                    "description": "Input Power pin not driven by any Output Power pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 1 [VDDC, Power input, Line]",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.6754+                            },+                            "uuid": "2f73ee23-d6a1-58fe-9e64-7910fa8f278a"+                        }+                    ],+                    "severity": "error",+                    "type": "power_pin_not_driven"+                },+                {+                    "description": "Input Power pin not driven by any Output Power pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 14 [VDDD, Power input, Line]",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.8024+                            },+                            "uuid": "a1ab8eb1-ae5c-54da-a469-67f423f68647"+                        }+                    ],+                    "severity": "error",+                    "type": "power_pin_not_driven"+                },+                {+                    "description": "Input Power pin not driven by any Output Power pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 9 [VIO, Power input, Line]",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.8786+                            },+                            "uuid": "704dc156-b8b2-5580-b24e-570951b984a8"+                        }+                    ],+                    "severity": "error",+                    "type": "power_pin_not_driven"+                },+                {+                    "description": "Input Power pin not driven by any Output Power pins",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 2 [GNDC, Power input, Line]",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.9802+                            },+                            "uuid": "775f03a5-ef61-51ce-bbfe-b8b6cd18eb61"+                        }+                    ],+                    "severity": "error",+                    "type": "power_pin_not_driven"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol U1 Pin 3 [EN, Input, Line]",+                            "pos": {+                                "x": 0.5206,+                                "y": 0.6754+                            },+                            "uuid": "a9b53e30-ce0a-541f-ae72-54b1fee0c9be"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol U1 [TMF8829-1A]",+                            "pos": {+                                "x": 0.8,+                                "y": 0.8659+                            },+                            "uuid": "6f7bc77c-1368-5add-9a67-95a5a3462c0a"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol U1 [TMF8829-1A]",+                            "pos": {+                                "x": 0.8,+                                "y": 0.8659+                            },+                            "uuid": "6f7bc77c-1368-5add-9a67-95a5a3462c0a"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Horizontal Wire, length 0.1706 mm",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.6754+                            },+                            "uuid": "cf17e1c8-f9f4-5de7-81f2-d2c8a0c14155"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Horizontal Wire, length 0.3206 mm",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.7008+                            },+                            "uuid": "462c30b9-4e01-55ed-af1e-56331ab023df"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Horizontal Wire, length 0.4706 mm",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.7262+                            },+                            "uuid": "0ac5425b-58d8-5703-be37-4a764ca2194b"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Horizontal Wire, length 0.7206 mm",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.8024+                            },+                            "uuid": "97eb2c2b-a7c3-5dbd-9c88-185acfbe2b09"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Horizontal Wire, length 0.4706 mm",+                            "pos": {+                                "x": 1.0794,+                                "y": 0.8786+                            },+                            "uuid": "aa6665ac-e22b-51f0-8512-6737a268edf9"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol C1 Pin 1 [1, Passive, Line]",+                            "pos": {+                                "x": 1.25,+                                "y": 0.2619+                            },+                            "uuid": "aea13504-58cf-5139-8064-c34e442eb6a6"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol C1 [885012206026]",+                            "pos": {+                                "x": 1.25,+                                "y": 0.3+                            },+                            "uuid": "b9cf064d-8c20-511d-9785-20a77dd62baf"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol C1 [885012206026]",+                            "pos": {+                                "x": 1.25,+                                "y": 0.3+                            },+                            "uuid": "b9cf064d-8c20-511d-9785-20a77dd62baf"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Vertical Wire, length 0.3373 mm",+                            "pos": {+                                "x": 1.25,+                                "y": 0.6754+                            },+                            "uuid": "dc3d1327-81d8-5588-8969-ae8f681b401f"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol C2 Pin 1 [1, Passive, Line]",+                            "pos": {+                                "x": 1.4,+                                "y": 0.2619+                            },+                            "uuid": "1ccaad20-da65-5aad-bc2b-316a59943304"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol C2 [885012206026]",+                            "pos": {+                                "x": 1.4,+                                "y": 0.3+                            },+                            "uuid": "6067c190-82b8-5fe2-be8b-880363c72758"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol C2 [885012206026]",+                            "pos": {+                                "x": 1.4,+                                "y": 0.3+                            },+                            "uuid": "6067c190-82b8-5fe2-be8b-880363c72758"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Vertical Wire, length 0.3627 mm",+                            "pos": {+                                "x": 1.4,+                                "y": 0.7008+                            },+                            "uuid": "989fb17f-7679-5cb0-9e71-d53cd94a1da2"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol C3 Pin 1 [1, Passive, Line]",+                            "pos": {+                                "x": 1.55,+                                "y": 0.2619+                            },+                            "uuid": "c3bf9de4-1316-5a8b-ab65-da6d95169af4"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol C3 [885012206026]",+                            "pos": {+                                "x": 1.55,+                                "y": 0.3+                            },+                            "uuid": "8d82255a-9e13-5696-9533-0aba3367dad1"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol C3 [885012206026]",+                            "pos": {+                                "x": 1.55,+                                "y": 0.3+                            },+                            "uuid": "8d82255a-9e13-5696-9533-0aba3367dad1"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Vertical Wire, length 0.3881 mm",+                            "pos": {+                                "x": 1.55,+                                "y": 0.7262+                            },+                            "uuid": "68fa1b0a-a818-5309-9976-8bf72416c428"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Vertical Wire, length 0.1333 mm",+                            "pos": {+                                "x": 1.55,+                                "y": 0.8786+                            },+                            "uuid": "dd809811-5f96-567d-9710-2918098f2248"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol C5 Pin 1 [1, Passive, Line]",+                            "pos": {+                                "x": 1.55,+                                "y": 1.0119+                            },+                            "uuid": "37220662-95e8-5118-9734-6516be71180b"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol C5 [885012206027]",+                            "pos": {+                                "x": 1.55,+                                "y": 1.05+                            },+                            "uuid": "131d9fea-dec9-5d0a-b575-142211cc5b23"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol C5 [885012206027]",+                            "pos": {+                                "x": 1.55,+                                "y": 1.05+                            },+                            "uuid": "131d9fea-dec9-5d0a-b575-142211cc5b23"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Symbol C4 Pin 1 [1, Passive, Line]",+                            "pos": {+                                "x": 1.8,+                                "y": 0.5019+                            },+                            "uuid": "be7ebb72-19fe-552e-888e-dd7de6672559"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                },+                {+                    "description": "The current configuration does not include the footprint library ''",+                    "items": [+                        {+                            "description": "Symbol C4 [885012206026]",+                            "pos": {+                                "x": 1.8,+                                "y": 0.54+                            },+                            "uuid": "6f6a6305-7f08-5581-8add-1fbf7ca39231"+                        }+                    ],+                    "severity": "warning",+                    "type": "footprint_link_issues"+                },+                {+                    "description": "The current configuration does not include the symbol library 'Adom'",+                    "items": [+                        {+                            "description": "Symbol C4 [885012206026]",+                            "pos": {+                                "x": 1.8,+                                "y": 0.54+                            },+                            "uuid": "6f6a6305-7f08-5581-8add-1fbf7ca39231"+                        }+                    ],+                    "severity": "warning",+                    "type": "lib_symbol_issues"+                },+                {+                    "description": "Symbol pin or wire end off connection grid",+                    "items": [+                        {+                            "description": "Vertical Wire, length 0.2243 mm",+                            "pos": {+                                "x": 1.8,+                                "y": 0.8024+                            },+                            "uuid": "ebb26951-acaf-512e-90c2-4a491624955a"+                        }+                    ],+                    "severity": "warning",+                    "type": "endpoint_off_grid"+                }+            ]+        }+    ],+    "source": ".tmpKJ0Nzk.kicad_sch"+}
report/tests/fixtures/erc-source-scaled.kicad_schadded+108
@@ -0,0 +1,108 @@+(kicad_sch (version 20250114) (generator "adom-schematic") (uuid aebf546f-22da-58c9-9780-4f637b9875de) (paper "A4")+(title_block (title "TMF8829 sensor decoupling"))+(lib_symbols+(symbol "Adom:885012206026" (pin_numbers (hide yes)) (pin_names (offset 0.254) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 5.08 0) (effects (font (size 1.27 1.27)))) (property "Value" "1 uF" (at 0 -5.08 0) (effects (font (size 1.27 1.27)))) (property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206026.pdf?ki" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer" "Wurth Elektronik" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer URL" "https://www.we-online.com" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Created Date" "20251008" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Series" "WCAP-CSGP" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Size" "0603_R" (at 0 -22.86 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Mounting Technology" "SMT" (at 0 -25.4 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Part Number" "885012206026" (at 0 -27.94 0) (effects (font (size 1.27 1.27)))) (property "Capacitance" "1 uF" (at 0 -30.48 0) (effects (font (size 1.27 1.27)))) (property "Voltage" "10.0" (at 0 -33.02 0) (effects (font (size 1.27 1.27)))) (property "C" "1 uF" (at 0 -35.56 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Tol. C" "±10%" (at 0 -38.1 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "VR (V (DC))" "10.0" (at 0 -40.64 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Operating Temperature" "-55 °C up to +125 °C" (at 0 -43.18 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "DF (%)" "10" (at 0 -45.72 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "RISO" "0.1 Gohm" (at 0 -48.26 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Ceramic Type" "X7R Class II" (at 0 -50.8 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "L (mm)" "1.6" (at 0 -53.34 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "W (mm)" "0.8" (at 0 -55.88 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "H (mm)" "0.8" (at 0 -58.42 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Fl (mm)" "0.4" (at 0 -60.96 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Q (%)" "–" (at 0 -63.5 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Demoboard" "nan" (at 0 -66.04 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_keywords" "SMT" (at 0 -68.58 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_description" "MLCCs 10 V(DC) ED" (at 0 -71.12 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_fp_filters" "*WCAP-CSGP*0603*" (at 0 -73.66 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Critical Parameter" "" (at 0 -76.2 0) (effects (font (size 1.27 1.27)))) (property "Manufacturer Part Number" "885012206026" (at 0 -78.74 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Package" "0603 (1608 metric)" (at 0 -81.28 0) (effects (font (size 1.27 1.27)) (hide yes))) (symbol "885012206026_0_1" (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none))) (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none)))) (symbol "885012206026_1_1" (pin passive line (at 0 3.81 270) (length 2.794) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) (pin passive line (at 0 -3.81 90) (length 2.794) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))))+(symbol "Adom:885012206027" (pin_numbers (hide yes)) (pin_names (offset 0.254) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 5.08 0) (effects (font (size 1.27 1.27)))) (property "Value" "2.2 uF" (at 0 -5.08 0) (effects (font (size 1.27 1.27)))) (property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206027.pdf?ki" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer" "Wurth Elektronik" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer URL" "https://www.we-online.com" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Created Date" "20251008" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Series" "WCAP-CSGP" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Size" "0603_R" (at 0 -22.86 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Mounting Technology" "SMT" (at 0 -25.4 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Part Number" "885012206027" (at 0 -27.94 0) (effects (font (size 1.27 1.27)))) (property "Capacitance" "2.2 uF" (at 0 -30.48 0) (effects (font (size 1.27 1.27)))) (property "Voltage" "10.0" (at 0 -33.02 0) (effects (font (size 1.27 1.27)))) (property "C" "2.2 uF" (at 0 -35.56 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Tol. C" "±10%" (at 0 -38.1 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "VR (V (DC))" "10.0" (at 0 -40.64 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Operating Temperature" "-55 °C up to +125 °C" (at 0 -43.18 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "DF (%)" "10" (at 0 -45.72 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "RISO" "0.05 Gohm" (at 0 -48.26 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Ceramic Type" "X7R Class II" (at 0 -50.8 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "L (mm)" "1.6" (at 0 -53.34 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "W (mm)" "0.8" (at 0 -55.88 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "H (mm)" "0.8" (at 0 -58.42 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Fl (mm)" "0.4" (at 0 -60.96 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Q (%)" "–" (at 0 -63.5 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Demoboard" "nan" (at 0 -66.04 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_keywords" "SMT" (at 0 -68.58 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_description" "MLCCs 10 V(DC) ED" (at 0 -71.12 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "ki_fp_filters" "*WCAP-CSGP*0603*" (at 0 -73.66 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Critical Parameter" "" (at 0 -76.2 0) (effects (font (size 1.27 1.27)))) (property "Manufacturer Part Number" "885012206027" (at 0 -78.74 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Package" "0603 (1608 metric)" (at 0 -81.28 0) (effects (font (size 1.27 1.27)) (hide yes))) (symbol "885012206027_0_1" (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none))) (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none)))) (symbol "885012206027_1_1" (pin passive line (at 0 3.81 270) (length 2.794) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) (pin passive line (at 0 -3.81 90) (length 2.794) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))))+(symbol "Adom:TMF8829-1A" (pin_numbers) (pin_names (offset 0.254)) (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "U" (at 0 25.4 0) (effects (font (size 1.27 1.27)))) (property "Value" "TMF8829-1A" (at 0 -25.4 0) (effects (font (size 1.27 1.27)))) (property "Footprint" "TMF8829-1A" (at 0 -27.94 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Datasheet" "https://look.ams-osram.com/m/4215c1ea1a30e283/original/TMF8829-Time-of-flight-sensor.pdf" (at 0 -30.48 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Description" "Multi-zone direct time-of-flight sensor; grouped multifunction pins; verify interface-mode configuration. GND_EP1/2 name the two center ground pads. 2-column symbol, pins grouped by function." (at 0 -33.02 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer" "ams OSRAM" (at 0 -35.56 0) (effects (font (size 1.27 1.27)) (hide yes))) (property "Manufacturer Part Number" "TMF8829-1A" (at 0 -38.1 0) (effects (font (size 1.27 1.27)) (hide yes))) (symbol "TMF8829-1A_0_1" (polyline (pts (xy 3.0236 2.3369) (xy 3.081 2.46) (xy 3.2009 2.5679) (xy 3.3099 2.6259) (xy 3.4368 2.6698) (xy 3.5767 2.6981) (xy 3.7231 2.7094) (xy 3.8712 2.7035) (xy 4.0133 2.6802) (xy 4.1451 2.6406) (xy 4.2595 2.5869)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.0724 2.2251) (xy 3.023 2.339) (xy 3.0187 2.383)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.0187 2.383) (xy 3.0561 2.5158) (xy 3.164 2.6346) (xy 3.2698 2.7002) (xy 3.3972 2.7517) (xy 3.5409 2.7859) (xy 3.6939 2.8016) (xy 3.849 2.7984) (xy 3.9997 2.7767) (xy 4.1386 2.736) (xy 4.2595 2.6802)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.0284 2.249) (xy 2.9292 2.3157)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.9292 2.3157) (xy 2.7974 2.4551) (xy 2.7356 2.6097) (xy 2.7475 2.7686) (xy 2.8332 2.9193) (xy 2.9861 3.0516) (xy 3.1944 3.1552) (xy 3.3153 3.1937) (xy 3.4439 3.2225) (xy 3.5778 3.2409) (xy 3.715 3.2485) (xy 3.8528 3.2458) (xy 3.9883 3.2322) (xy 4.1196 3.2078) (xy 4.2443 3.1737) (xy 4.3593 3.1297) (xy 4.4634 3.0777)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7579 5.3733) (xy -5.4437 0.0607)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -2.6422) (xy -5.4437 0.0607)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -2.6422) (xy 8.4397 2.6704)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7579 5.3733) (xy 8.4397 2.6704)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.0159 -0.5288) (xy -1.783 1.0874)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.8535 -1.6085) (xy 1.0159 -0.5288)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -3.653 0.0076) (xy -0.8535 -1.6085)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.783 1.0874) (xy -3.653 0.0076)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.4634 3.0777) (xy 4.5881 2.9898) (xy 4.6809 2.89) (xy 4.7395 2.7821) (xy 4.759 2.6313) (xy 4.7113 2.4833) (xy 4.6332 2.3792) (xy 4.5627 2.3157)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.5627 2.3157) (xy 4.4585 2.2463) (xy 4.3376 2.1872) (xy 4.202 2.14) (xy 4.055 2.1053) (xy 3.9005 2.0841) (xy 3.7427 2.0776) (xy 3.5843 2.0847) (xy 3.4303 2.1064) (xy 3.2839 2.1416) (xy 3.1488 2.1894) (xy 3.0284 2.249)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.8432 0.4246) (xy 0.4123 -0.8775)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -3.109 -0.3064) (xy -1.8432 0.4246)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.075) (xy 8.4397 0.2375)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.075) (xy -5.4437 -2.3721)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -2.6422) (xy -0.7619 -5.075)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 2.6704) (xy 8.4397 0.2375)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 0.0607) (xy -5.4437 -2.3721)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.783 1.0874) (xy -1.8432 0.4246)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.2595 2.6802) (xy 4.3571 2.6102) (xy 4.4352 2.5158) (xy 4.4721 2.3981) (xy 4.4726 2.383)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.4726 2.383) (xy 4.4406 2.2599) (xy 4.4189 2.2251)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.2595 2.5869) (xy 4.3571 2.5169) (xy 4.4347 2.4242) (xy 4.4683 2.3369)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.7624 0.378) (xy -1.8828 0.3628) (xy -1.9241 0.378)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.3013 2.2024) (xy 4.2497 2.3352) (xy 4.1505 2.4198) (xy 4.01 2.4822) (xy 3.8425 2.5158) (xy 3.6651 2.5175) (xy 3.4965 2.4865) (xy 3.3527 2.4269)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.1933 2.172) (xy 3.19 2.2056)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.19 2.2056) (xy 3.2323 2.3287) (xy 3.3256 2.416) (xy 3.3527 2.4329)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.3522 2.4329) (xy 3.2475 2.3488) (xy 3.1917 2.2371) (xy 3.1917 2.1725)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.3527 2.4329) (xy 3.4796 2.4871) (xy 3.6277 2.5191) (xy 3.7855 2.5256) (xy 3.9401 2.5066) (xy 4.0789 2.4627) (xy 4.1386 2.4329)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.1386 2.4269) (xy 3.9948 2.4865) (xy 3.8262 2.5175) (xy 3.6488 2.5158) (xy 3.4818 2.4822) (xy 3.3414 2.4198) (xy 3.2416 2.3352) (xy 3.19 2.2024)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.1386 2.4329) (xy 4.2427 2.3493) (xy 4.2985 2.2398) (xy 4.3013 2.2056)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.3013 2.2056) (xy 4.2985 2.172)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.3013 2.2029) (xy 4.2584 2.3238) (xy 4.1657 2.4101) (xy 4.1386 2.4269)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.2996 2.1725) (xy 4.2698 2.3141) (xy 4.1787 2.4074) (xy 4.0399 2.4784) (xy 3.8674 2.5191) (xy 3.7443 2.5267) (xy 3.6206 2.5185) (xy 3.4493 2.4773) (xy 3.3522 2.4329)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.3721) (xy -0.7619 -5.075)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.075) (xy 8.4397 0.2375)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 0.2375) (xy 8.4397 0.2375)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.4188) (xy -0.7619 -5.1217)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1217) (xy 8.4397 0.1909)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.075) (xy -0.7619 -5.1217)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.3721) (xy -5.4437 -2.4188)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 0.2375) (xy 8.4397 0.1909)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.0992 0.3314) (xy -2.1659 0.2381)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.0992 0.2381) (xy -2.1659 0.1448)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.1659 0.2381) (xy -2.8926 -0.1817)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.1659 0.1448) (xy -2.8926 -0.2744)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.8926 -0.1817) (xy -3.0212 -0.2186) (xy -3.1486 -0.2083) (xy -3.2153 -0.1817)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.8926 -0.2744) (xy -3.0098 -0.3108) (xy -3.102 -0.3102)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.5343 -0.807) (xy 0.4025 -0.8254) (xy 0.2761 -0.7978) (xy 0.2555 -0.7875)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3504 -0.9133) (xy 0.2555 -0.8802)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.2555 -0.7875) (xy -1.52 0.2381)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.2555 -0.8802) (xy -1.52 0.1448)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.52 0.2381) (xy -1.5873 0.3314)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.52 0.1448) (xy -1.5873 0.2381)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 -0.0607) (xy -0.7619 -5.3733)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 -0.0607) (xy 8.4397 0.1258)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1868) (xy 8.4397 0.1258)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.3733) (xy -0.7619 -5.1868)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.3733) (xy -5.4437 -2.6704)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.4838) (xy -0.7619 -5.1868)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.6704) (xy -5.4437 -2.4838)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5061 -4.0918) (xy 1.5061 -4.0636)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5869 -4.0452) (xy 1.5869 -4.017)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5061 -4.0636) (xy 1.5869 -4.017)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5061 -4.0918) (xy 1.5869 -4.0452)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.5794 -3.4719) (xy 2.5794 -3.4437)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.6602 -3.4253) (xy 2.6602 -3.3971)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.5794 -3.4437) (xy 2.6602 -3.3971)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.5794 -3.4719) (xy 2.6602 -3.4253)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7096 -2.8195) (xy 3.7096 -2.7913)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7904 -2.7729) (xy 3.7904 -2.7447)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7096 -2.7913) (xy 3.7904 -2.7447)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7096 -2.8195) (xy 3.7904 -2.7729)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.8398 -2.1671) (xy 4.8398 -2.1389)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.9206 -2.1205) (xy 4.9206 -2.0923)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.8398 -2.1389) (xy 4.9206 -2.0923)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.8398 -2.1671) (xy 4.9206 -2.1205)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.97 -1.5147) (xy 5.97 -1.4865)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.0502 -1.4681) (xy 6.0502 -1.4399)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.97 -1.4865) (xy 6.0502 -1.4399)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.97 -1.5147) (xy 6.0502 -1.4681)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3759 -4.7442) (xy 0.3759 -4.716)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.4567 -4.6976) (xy 0.4567 -4.6694)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3759 -4.7442) (xy 0.4567 -4.6976)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3759 -4.716) (xy 0.4567 -4.6694)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.0996 -0.8623) (xy 7.0996 -0.8341)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.1804 -0.8157) (xy 7.1804 -0.7875)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.0996 -0.8623) (xy 7.1804 -0.8157)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.0996 -0.8341) (xy 7.1804 -0.7875)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1868) (xy 8.4397 0.1258)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1868) (xy -5.4437 -2.4838)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.4188) (xy -5.4437 -2.4188)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -5.4437 -2.4838) (xy -5.4437 -2.4188)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 0.1909) (xy 8.4397 0.1909)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 8.4397 0.1258) (xy 8.4397 0.1909)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1217) (xy 8.4397 0.1909)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1868) (xy -0.7619 -5.1217)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -0.7619 -5.1217) (xy -5.4437 -2.4188)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.1097 -4.87) (xy 0.1661 -4.9026)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.7955 -4.7768) (xy -1.8519 -4.8093)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -4.3542 -3.3646) (xy -1.8519 -4.8093)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.9467 -0.9881) (xy 7.5113 -0.6616)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.8898 -0.9556) (xy 6.9467 -0.9881)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.8165 -1.6405) (xy 6.3816 -1.314)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.7601 -1.608) (xy 5.8165 -1.6405)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.6863 -2.2929) (xy 5.2514 -1.9665)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.6299 -2.2604) (xy 4.6863 -2.2929)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.5561 -2.9453) (xy 4.1212 -2.6189)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.4997 -2.9128) (xy 3.5561 -2.9453)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.4265 -3.5978) (xy 2.9916 -3.2713)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.3695 -3.5652) (xy 2.4265 -3.5978)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.2963 -4.2502) (xy 1.8614 -3.9237)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.2399 -4.2176) (xy 1.2963 -4.2502)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.1661 -4.9026) (xy 0.7312 -4.5761)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.7312 -4.5761) (xy 0.7312 -4.511)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.8614 -3.9237) (xy 1.8614 -3.8586)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.9916 -3.2713) (xy 2.9916 -3.2062)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.1212 -2.6189) (xy 4.1212 -2.5538)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.2514 -1.9665) (xy 5.2514 -1.9014)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.3816 -1.314) (xy 6.3816 -1.249)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -4.3542 -3.2995) (xy -4.3542 -3.3646)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.8519 -4.7442) (xy -1.8519 -4.8093)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.1661 -4.8375) (xy 0.1661 -4.9026)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.5113 -0.6616) (xy 7.5113 -0.5966)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.5561 -2.8803) (xy 3.5561 -2.9453)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.6863 -2.2279) (xy 4.6863 -2.2929)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.8165 -1.5749) (xy 5.8165 -1.6405)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.9467 -0.9225) (xy 6.9467 -0.9881)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.2963 -4.1851) (xy 1.2963 -4.2502)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.1661 -4.8375) (xy 0.3759 -4.716)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.4567 -4.6694) (xy 0.7312 -4.511)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5869 -4.017) (xy 1.8614 -3.8586)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.6602 -3.3971) (xy 2.9916 -3.2062)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7904 -2.7447) (xy 4.1212 -2.5538)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.9206 -2.0923) (xy 5.2514 -1.9014)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.0502 -1.4399) (xy 6.3816 -1.249)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.1804 -0.7875) (xy 7.5113 -0.5966)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -4.3542 -3.2995) (xy -1.8519 -4.7442)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.2963 -4.1851) (xy 1.5061 -4.0636)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.4265 -3.5327) (xy 2.5794 -3.4437)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.5561 -2.8803) (xy 3.7096 -2.7913)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.6863 -2.2279) (xy 4.8398 -2.1389)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.8165 -1.5749) (xy 5.97 -1.4865)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.9467 -0.9225) (xy 7.0996 -0.8341)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.4265 -3.5327) (xy 2.4265 -3.5978)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.0996 -0.8623) (xy 7.1804 -0.8157)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.0996 -0.8623) (xy 7.0996 -0.8341)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 7.1804 -0.8157) (xy 7.1804 -0.7875)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.5794 -3.4719) (xy 2.6602 -3.4253)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.5794 -3.4719) (xy 2.5794 -3.4437)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 2.6602 -3.4253) (xy 2.6602 -3.3971)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.97 -1.5147) (xy 6.0502 -1.4681)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 5.97 -1.5147) (xy 5.97 -1.4865)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 6.0502 -1.4681) (xy 6.0502 -1.4399)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3759 -4.7442) (xy 0.3759 -4.716)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.4567 -4.6976) (xy 0.4567 -4.6694)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.3759 -4.7442) (xy 0.4567 -4.6976)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7096 -2.8195) (xy 3.7904 -2.7729)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7096 -2.8195) (xy 3.7096 -2.7913)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 3.7904 -2.7729) (xy 3.7904 -2.7447)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.8398 -2.1671) (xy 4.9206 -2.1205)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.8398 -2.1671) (xy 4.8398 -2.1389)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 4.9206 -2.1205) (xy 4.9206 -2.0923)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5061 -4.0918) (xy 1.5869 -4.0452)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5061 -4.0918) (xy 1.5061 -4.0636)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 1.5869 -4.0452) (xy 1.5869 -4.017)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.0992 0.2381) (xy -2.0992 0.2766)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.5873 0.2381) (xy -1.5873 0.2766)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.1659 0.1448) (xy -2.1659 0.2381)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.8926 -0.2744) (xy -2.8926 -0.1817)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy 0.2555 -0.8802) (xy 0.2555 -0.7875)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -1.52 0.1448) (xy -1.52 0.2381)) (stroke (width 0.1302) (type default)) (fill (type none))) (polyline (pts (xy -2.2738 -0.3221) (xy -1.5086 -0.7636)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -2.5287 -0.4691) (xy -2.019 -0.1746)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -1.8367 -0.0694) (xy -1.071 -0.5114)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -1.8367 -0.0694) (xy -0.7798 -0.3427)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -1.2532 0.2674) (xy -0.7798 -0.3427)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -1.2532 0.2674) (xy -0.488 -0.1746)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -0.962 0.4355) (xy -0.1968 -0.0065)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -0.962 0.4355) (xy -0.488 0.7088)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -0.5975 0.2251) (xy -0.3058 0.3937)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy -0.1604 0.8986) (xy -0.1968 0.7511) (xy -0.0145 0.6882) (xy 0.2772 0.7723) (xy 0.4231 0.814) (xy 0.5685 0.814) (xy 0.7876 0.73) (xy 0.7507 0.5407) (xy 0.6053 0.4566) (xy 0.4595 0.4143) (xy 0.2772 0.4355) (xy 0.1677 0.4984) (xy 0.1313 0.6459) (xy 0.3136 0.8352) (xy 0.3136 0.9615) (xy 0.1313 1.0244) (xy -0.0145 0.9827) (xy -0.1604 0.8986)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 0.5685 1.3195) (xy 0.5321 1.172) (xy 0.7143 1.109) (xy 1.0061 1.1931) (xy 1.152 1.2349) (xy 1.2973 1.2349) (xy 1.5164 1.1508) (xy 1.4796 0.9615) (xy 1.3342 0.8775) (xy 1.1883 0.8352) (xy 1.0061 0.8563) (xy 0.8966 0.9192) (xy 0.8602 1.0667) (xy 1.0424 1.256) (xy 1.0424 1.3824) (xy 0.8602 1.4453) (xy 0.7143 1.4035) (xy 0.5685 1.3195)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 1.3342 1.5505) (xy 1.261 1.6769) (xy 1.4432 1.8244) (xy 1.6254 1.845) (xy 1.8077 1.7821) (xy 1.8809 1.6139) (xy 1.8809 1.1931) (xy 2.3907 1.4876)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 2.5729 2.1818) (xy 2.6461 2.0136) (xy 2.5365 1.9084) (xy 2.3907 1.8661) (xy 2.2453 1.8661) (xy 2.0994 1.9084) (xy 1.9899 2.0136) (xy 2.0631 2.1818) (xy 2.2453 2.2452) (xy 2.3907 2.2452) (xy 2.5729 2.1818) (xy 2.7551 2.0765) (xy 2.901 1.9502) (xy 2.8647 1.7609) (xy 2.6461 1.6769) (xy 2.5365 1.698)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 3.0469 2.2452) (xy 3.7025 2.6238)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 3.7758 3.0023) (xy 3.8121 3.1916) (xy 4.5773 2.7501)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 4.4683 3.5706) (xy 4.9417 2.9605)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 4.4683 3.5706) (xy 5.5247 3.2968)) (stroke (width 0.0542) (type default)) (fill (type none))) (polyline (pts (xy 4.7959 3.171) (xy 5.1603 3.3814)) (stroke (width 0.0542) (type default)) (fill (type none))) (rectangle (start -25.4 24.13) (end 25.4 -24.13) (stroke (width 0.254) (type default)) (fill (type background))) (text "Enable" (at -24.38 21.59 0) (effects (font (size 1.016 1.016)) (justify left))) (text "Interrupt / GPIO" (at -24.38 13.97 0) (effects (font (size 1.016 1.016)) (justify left))) (text "I3C / I2C" (at -24.38 6.35 0) (effects (font (size 1.016 1.016)) (justify left))) (text "SPI / GPIO" (at -24.38 -6.35 0) (effects (font (size 1.016 1.016)) (justify left))) (text "3.3 V supplies" (at 24.38 21.59 0) (effects (font (size 1.016 1.016)) (justify right))) (text "Digital supply" (at 24.38 8.89 0) (effects (font (size 1.016 1.016)) (justify right))) (text "I/O supply" (at 24.38 1.27 0) (effects (font (size 1.016 1.016)) (justify right))) (text "Ground" (at 24.38 -8.89 0) (effects (font (size 1.016 1.016)) (justify right)))) (symbol "TMF8829-1A_1_1" (pin input line (at -27.94 19.05 0) (length 2.54) (name "EN" (effects (font (size 1.27 1.27)))) (number "3" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 11.43 0) (length 2.54) (name "INT/GPIO6" (effects (font (size 1.27 1.27)))) (number "4" (effects (font (size 1.27 1.27))))) (pin input line (at -27.94 3.81 0) (length 2.54) (name "SCL/GPIO4" (effects (font (size 1.27 1.27)))) (number "10" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 1.27 0) (length 2.54) (name "SDA/GPIO5" (effects (font (size 1.27 1.27)))) (number "11" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 -8.89 0) (length 2.54) (name "MOSI/GPIO0" (effects (font (size 1.27 1.27)))) (number "5" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 -11.43 0) (length 2.54) (name "CSN/GPIO1" (effects (font (size 1.27 1.27)))) (number "6" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 -13.97 0) (length 2.54) (name "SCLK/GPIO2" (effects (font (size 1.27 1.27)))) (number "12" (effects (font (size 1.27 1.27))))) (pin bidirectional line (at -27.94 -16.51 0) (length 2.54) (name "MISO/GPIO3" (effects (font (size 1.27 1.27)))) (number "13" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 19.05 180) (length 2.54) (name "VDDC" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 16.51 180) (length 2.54) (name "VDD" (effects (font (size 1.27 1.27)))) (number "8" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 13.97 180) (length 2.54) (name "VDDV" (effects (font (size 1.27 1.27)))) (number "16" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 6.35 180) (length 2.54) (name "VDDD" (effects (font (size 1.27 1.27)))) (number "14" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -1.27 180) (length 2.54) (name "VIO" (effects (font (size 1.27 1.27)))) (number "9" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -11.43 180) (length 2.54) (name "GNDC" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -13.97 180) (length 2.54) (name "GND" (effects (font (size 1.27 1.27)))) (number "7" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -16.51 180) (length 2.54) (name "GNDV" (effects (font (size 1.27 1.27)))) (number "15" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -19.05 180) (length 2.54) (name "GND_EP1" (effects (font (size 1.27 1.27)))) (number "17" (effects (font (size 1.27 1.27))))) (pin power_in line (at 27.94 -21.59 180) (length 2.54) (name "GND_EP2" (effects (font (size 1.27 1.27)))) (number "18" (effects (font (size 1.27 1.27)))))))+)+(symbol (lib_id "Adom:885012206026") (at 125 30 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid b9cf064d-8c20-511d-9785-20a77dd62baf)+(property "Reference" "C1" (at 127.54 27.46 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "1uF" (at 127.54 30 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 125 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206026.pdf?ki" (at 125 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "Wurth Elektronik" (at 125 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Part Number" "885012206026" (at 125 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "1" (uuid aea13504-58cf-5139-8064-c34e442eb6a6))+(pin "2" (uuid fcbe2589-9425-515f-a106-7168e0c5675d))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "C1") (unit 1)))) )+(symbol (lib_id "Adom:885012206026") (at 140 30 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid 6067c190-82b8-5fe2-be8b-880363c72758)+(property "Reference" "C2" (at 142.54 27.46 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "1uF" (at 142.54 30 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 140 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206026.pdf?ki" (at 140 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "Wurth Elektronik" (at 140 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Part Number" "885012206026" (at 140 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "1" (uuid 1ccaad20-da65-5aad-bc2b-316a59943304))+(pin "2" (uuid 3095c973-c3fe-59ad-a4cc-583f7df75f72))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "C2") (unit 1)))) )+(symbol (lib_id "Adom:885012206026") (at 155 30 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid 8d82255a-9e13-5696-9533-0aba3367dad1)+(property "Reference" "C3" (at 157.54 27.46 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "1uF" (at 157.54 30 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 155 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206026.pdf?ki" (at 155 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "Wurth Elektronik" (at 155 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Part Number" "885012206026" (at 155 30 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "1" (uuid c3bf9de4-1316-5a8b-ab65-da6d95169af4))+(pin "2" (uuid 50e35c64-3a55-5955-b369-01f3d7c372b3))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "C3") (unit 1)))) )+(symbol (lib_id "Adom:885012206026") (at 180 54 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid 6f6a6305-7f08-5581-8add-1fbf7ca39231)+(property "Reference" "C4" (at 182.54 51.46 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "1uF" (at 182.54 54 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 180 54 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206026.pdf?ki" (at 180 54 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "Wurth Elektronik" (at 180 54 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Part Number" "885012206026" (at 180 54 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "1" (uuid be7ebb72-19fe-552e-888e-dd7de6672559))+(pin "2" (uuid 18ba4a2e-4caa-52c6-a847-6411aafec554))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "C4") (unit 1)))) )+(symbol (lib_id "Adom:885012206027") (at 155 105 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid 131d9fea-dec9-5d0a-b575-142211cc5b23)+(property "Reference" "C5" (at 157.54 102.46 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "2.2uF" (at 157.54 105 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "C_Wurth_WCAP-CSGP-0603_R" (at 155 105 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://www.we-online.com/catalog/datasheet/885012206027.pdf?ki" (at 155 105 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "Wurth Elektronik" (at 155 105 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Part Number" "885012206027" (at 155 105 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "1" (uuid 37220662-95e8-5118-9734-6516be71180b))+(pin "2" (uuid f35081fb-1231-583b-853f-b50a2844ad05))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "C5") (unit 1)))) )+(symbol (lib_id "Adom:TMF8829-1A") (at 80 86.59 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid 6f7bc77c-1368-5add-9a67-95a5a3462c0a)+(property "Reference" "U1" (at 82.54 84.05 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Value" "TMF8829-1A" (at 82.54 86.59 0) (effects (font (size 1.27 1.27)) (justify left)))+(property "Footprint" "TMF8829-1A" (at 80 86.59 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Datasheet" "https://look.ams-osram.com/m/4215c1ea1a30e283/original/TMF8829-Time-of-flight-sensor.pdf" (at 80 86.59 0) (effects (font (size 1.27 1.27)) (hide yes)))+(property "Manufacturer" "ams OSRAM" (at 80 86.59 0) (effects (font (size 1.27 1.27)) (hide yes)))+(pin "3" (uuid a9b53e30-ce0a-541f-ae72-54b1fee0c9be))+(pin "4" (uuid e5068388-5492-50d2-a64e-1ab07b6a7a57))+(pin "10" (uuid b69639ca-29d1-59c6-8d72-a1ff896ac05d))+(pin "11" (uuid 562f2504-575c-5744-bd51-96ab7878fbdb))+(pin "5" (uuid 2b20f24f-fd13-5381-b986-9328f1e89911))+(pin "6" (uuid 926ad9cb-17dd-5e19-a435-12da99196d75))+(pin "12" (uuid 4ca9efe0-ca4f-53ea-9469-163250cd1298))+(pin "13" (uuid 8aeb69e3-8ade-5daf-960b-2e46488d99ec))+(pin "1" (uuid 2f73ee23-d6a1-58fe-9e64-7910fa8f278a))+(pin "8" (uuid 7865771c-57c3-5fcb-9d69-77eec72db4eb))+(pin "16" (uuid eb3139ab-a74d-5458-a91f-512cf22558d7))+(pin "14" (uuid a1ab8eb1-ae5c-54da-a469-67f423f68647))+(pin "9" (uuid 704dc156-b8b2-5580-b24e-570951b984a8))+(pin "2" (uuid 775f03a5-ef61-51ce-bbfe-b8b6cd18eb61))+(pin "7" (uuid 962b4be0-14eb-5c4b-8dda-e8cd79ea0cf0))+(pin "15" (uuid f8c16c7d-7108-5ef2-ba83-48fdbd808095))+(pin "17" (uuid 70e4c166-ea42-5421-9558-367cdf83490b))+(pin "18" (uuid 3ed30fc2-a93f-5cd1-887c-13a1ef7e7a1a))+(instances (project "TMF8829 sensor decoupling" (path "/aebf546f-22da-58c9-9780-4f637b9875de" (reference "U1") (unit 1)))) )+(wire (pts (xy 107.94 67.54) (xy 125 67.54)) (stroke (width 0) (type default)) (uuid cf17e1c8-f9f4-5de7-81f2-d2c8a0c14155))+(wire (pts (xy 125 67.54) (xy 125 33.81)) (stroke (width 0) (type default)) (uuid dc3d1327-81d8-5588-8969-ae8f681b401f))+(wire (pts (xy 107.94 70.08) (xy 140 70.08)) (stroke (width 0) (type default)) (uuid 462c30b9-4e01-55ed-af1e-56331ab023df))+(wire (pts (xy 140 70.08) (xy 140 33.81)) (stroke (width 0) (type default)) (uuid 989fb17f-7679-5cb0-9e71-d53cd94a1da2))+(wire (pts (xy 107.94 72.62) (xy 155 72.62)) (stroke (width 0) (type default)) (uuid 0ac5425b-58d8-5703-be37-4a764ca2194b))+(wire (pts (xy 155 72.62) (xy 155 33.81)) (stroke (width 0) (type default)) (uuid 68fa1b0a-a818-5309-9976-8bf72416c428))+(wire (pts (xy 107.94 80.24000000000001) (xy 180 80.24000000000001)) (stroke (width 0) (type default)) (uuid 97eb2c2b-a7c3-5dbd-9c88-185acfbe2b09))+(wire (pts (xy 180 80.24000000000001) (xy 180 57.81)) (stroke (width 0) (type default)) (uuid ebb26951-acaf-512e-90c2-4a491624955a))+(wire (pts (xy 107.94 87.86) (xy 155 87.86)) (stroke (width 0) (type default)) (uuid aa6665ac-e22b-51f0-8512-6737a268edf9))+(wire (pts (xy 155 87.86) (xy 155 101.19)) (stroke (width 0) (type default)) (uuid dd809811-5f96-567d-9710-2918098f2248))+(label "GND" (at 107.94 98.02000000000001 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid e50e21fd-2a85-5e8f-aaf3-914f0d555a88))+(label "GND" (at 107.94 100.56 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid e189966b-ecf0-5902-ba18-8e3847bc185d))+(label "GND" (at 107.94 103.10000000000001 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 43b0da25-0d59-53e1-b3da-bef755b22b67))+(label "GND" (at 107.94 105.64 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid a3e3bde4-95ca-51ee-8361-2aa43899b88f))+(label "GND" (at 107.94 108.18 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 8fc9f5ec-fba2-54db-9407-655c77f7c13c))+(label "GND" (at 125 26.19 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 9df222f4-450d-556f-8f69-5c3f27f68b48))+(label "GND" (at 140 26.19 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 57712552-bf7f-5c81-832c-ec920dd82147))+(label "GND" (at 155 26.19 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 8032e99f-0a56-5f6e-9bf6-1862e879d3a1))+(label "GND" (at 180 50.19 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 554719e1-5b3c-5e80-bf14-c3aa8aaaf610))+(label "GND" (at 155 108.81 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid bc5c3dfe-4f32-5d35-afae-9014899072fa))+(label "+3V3_SENSOR" (at 107.94 67.54 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 2353194a-f44b-5a00-9705-6b3703bc4a63))+(label "+3V3_SENSOR" (at 107.94 70.08 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 4b7b9bdd-dba2-5705-8ae1-43d8cf0fc835))+(label "+3V3_SENSOR" (at 107.94 72.62 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 6df69acd-3f88-556b-8591-4dc26384a604))+(label "+1V8_CORE" (at 107.94 80.24000000000001 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 85bfcefd-1e43-5983-938d-92017cd3f760))+(label "+3V3_IO" (at 107.94 87.86 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid 4053e2dd-3868-59f2-a7f1-45c64981f453))+)
verification/test_report_cli.pyadded+71
@@ -0,0 +1,71 @@+#!/usr/bin/env python3+"""Exercise `service-kicad pcb drc` / `sch erc` against a LIVE service using+the fixtures attached to adom/service-kicad#2 and #3.++    python3 verification/test_report_cli.py [path/to/service-kicad]++Needs network access to the service the CLI resolves (SERVICE_KICAD_URL+honoured). Asserts the count policy, the exit-code contract, the native+passthrough, and the ERC coordinate-scale hint. It does not deploy or+change anything.+"""+import json, os, subprocess, sys, tempfile++ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+FIX = os.path.join(ROOT, "report", "tests", "fixtures")+CLI = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "target", "release", "service-kicad")+BOARD = os.path.join(FIX, "aperture-check.kicad_pcb")+SCH = os.path.join(FIX, "erc-source-scaled.kicad_sch")++def run(*args):+    p = subprocess.run([CLI, *args], capture_output=True, text=True)+    return p.returncode, p.stdout, p.stderr++failures = []+def check(name, cond, detail=""):+    print(("ok   " if cond else "FAIL ") + name + (f"  {detail}" if detail and not cond else ""))+    if not cond: failures.append(name)++# issue 3: counts from findings, explicit verdict, exit 5+code, out, err = run("pcb", "drc", BOARD)+check("drc: exit 5 on error findings", code == 5, f"exit={code} err={err}")+check("drc: verdict line says FAILED with 2 errors, 1 warning", out.startswith("DRC FAILED: 2 errors, 1 warning"), out.splitlines()[:1])+check("drc: provenance line has kicad version + sha256", "kicad 10." in out and "sha256" in out, out.splitlines()[1:2])+check("drc: DRC_FAILED hint", "hint [DRC_FAILED]" in out)++code, out, err = run("pcb", "drc", BOARD, "--exit-zero")+check("drc: --exit-zero exits 0 and says so", code == 0 and "--exit-zero" in err, f"exit={code}")++with tempfile.TemporaryDirectory() as d:+    j = os.path.join(d, "r.json"); n = os.path.join(d, "n.json")+    code, out, err = run("pcb", "drc", BOARD, "--format", "json", "--out", j)+    doc = json.load(open(j)); a = doc.get("adom_summary", {})+    check("drc json: adom_summary added, native keys intact", "violations" in doc and a.get("errors") == 2 and a.get("status") == "failed")+    check("drc json: exit_code in summary matches process", a.get("exit_code") == code == 5)+    check("drc json: input sha256 + service provenance", len(a.get("input", {}).get("sha256", "")) == 64 and a.get("service", {}).get("version"))+    code, out, err = run("pcb", "drc", BOARD, "--format", "native", "--out", n)+    nat = json.load(open(n))+    check("drc native: untouched report (no adom_summary), still exit 5", "adom_summary" not in nat and code == 5)++# issue 2: ERC scale hint, nothing rescaled+code, out, err = run("sch", "erc", SCH, "--format", "json")+doc = json.loads(out); a = doc["adom_summary"]+check("erc: exit 5 (incomplete circuit has real errors)", code == 5)+check("erc: scale check says scaled by 100", a["erc_scale"]["status"] == "scaled" and a["erc_scale"]["factor"] == 100.0, a["erc_scale"])+check("erc: ERC_COORDINATES_SCALED hint first", a["hints"][0]["code"] == "ERC_COORDINATES_SCALED")+first = doc["sheets"][0]["violations"][0]["items"][0]["pos"]+check("erc: raw positions preserved (not rescaled)", first["x"] < 5, first)++# malformed input: diagnosed locally, exit 1+with tempfile.TemporaryDirectory() as d:+    broken = os.path.join(d, "broken.kicad_sch")+    open(broken, "wb").write(open(SCH, "rb").read()[:2000])+    code, out, err = run("sch", "erc", broken)+    check("truncated schematic: exit 1 with INPUT_UNLOADABLE + paren balance", code == 1 and "INPUT_UNLOADABLE" in err and "paren balance" in err, err)+    wrong = os.path.join(d, "board.kicad_sch")+    open(wrong, "wb").write(open(BOARD, "rb").read())+    code, out, err = run("sch", "erc", wrong)+    check("board posted as schematic: named as such, exit 1", code == 1 and "not a schematic" in err, err)++print(f"\n{len(failures)} failure(s)" if failures else "\nall checks passed")+sys.exit(1 if failures else 0)