app
Adom Concur
Public Made by Adomby adom
Cross-source consensus check for chip libraries — scores whether the KiCad, EAGLE, and Altium libraries AND the adom-ds2sf datasheet extraction all AGREE on pins, pads, names, and package, across seve
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
//! Unified result JSON — mirrors ds2sf's agent-to-agent contract.
//!
//! Exit codes:
//! 0 → status=golden (all sources agree exactly)
//! 0 → status=golden_with_variants (all disagreements are known-benign patterns
//! enumerated in `variants[]` — caller can
//! ship, just remember the variants)
//! 2 → status=divergent (recoverable: chip-fetcher should re-fetch
//! a lib or have the user intervene)
//! 3 → status=unrecoverable (parser failure / missing inputs)
use anyhow::{Context as _, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Golden,
GoldenWithVariants,
Divergent,
Unrecoverable,
}
impl Status {
pub fn exit_code(self) -> i32 {
match self {
Status::Golden => 0,
Status::GoldenWithVariants => 0,
Status::Divergent => 2,
Status::Unrecoverable => 3,
}
}
pub fn is_acceptable(self) -> bool {
matches!(self, Status::Golden | Status::GoldenWithVariants)
}
}
/// A variant explains a disagreement as known-benign rather than as a real
/// divergence. When every disagreeing axis is covered by a variant, status
/// becomes `golden_with_variants` (exit 0) instead of `divergent` (exit 2).
///
/// The `covers` method declares which axes a variant explains — used by the
/// status-determination logic in cli.rs to decide whether to ship or block.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Variant {
/// One source counts an exposed/thermal pad as pin N+1 while another doesn't.
/// Pad-count diff is exactly 1 across sources. EE practice varies on whether
/// to expose the EP as a symbol pin; either is acceptable.
ExposedPadVariant {
sources_with_ep: Vec<String>,
sources_without_ep: Vec<String>,
},
/// A module's KiCad/EAGLE footprint includes mounting/shield/test pads
/// beyond the user-facing pinout (the module datasheet's N pads). Caller
/// can use whichever set fits their assembly process.
ModuleExtraPads {
sources_with_extras: Vec<String>,
sources_without_extras: Vec<String>,
extra_count: u32,
},
/// Package-family names disagree (different prefixes / library naming
/// conventions, e.g. TI's `SOT_` prefix on a VSSOP footprint) BUT the
/// underlying geometry — pad count, pad numbers, pad pitch — agrees.
/// Cosmetic only; downstream tooling should look at geometry, not name.
LabelOnlyMismatch {
notes: String,
},
/// One source's symbol uses pin numbers as labels ("1","2","3"…) instead
/// of real pin names (VDD, SCL, …). Symbol is unusable for schematic
/// capture as-is, but a richer source (ds2sf or EAGLE) can supply names.
StubSymbolPinNames {
stub_sources: Vec<String>,
canonical_sources: Vec<String>,
},
/// Body-dim x or y disagrees by >0.5 mm but pad geometry agrees. Body dims
/// are courtyard-only — they don't affect placement. The disagreement is
/// usually a measurement-method artifact (lead-tip span vs body D dim, etc).
BodyDimMeasurementArtifact {
notes: String,
},
/// Pin names disagree but every disagreeing pin pair is a benign naming-
/// convention difference, not a real mismatch. Two cases handled:
/// - Channel-digit position swaps: `OUT1` ↔ `1OUT`, `IN1+` ↔ `1IN+`
/// (op-amps, dual DACs, multi-rail regulators)
/// - Power-pin aliases: `V+` ↔ `VCC` ↔ `VDD`, `V-` ↔ `GND` ↔ `VSS`
/// (chip-vendor convention vs library convention)
NamingConventionMismatch {
examples: Vec<String>,
},
/// One source represents a 4-terminal Kelvin sense resistor (Vishay WSL,
/// Bourns CSL, Susumu PCS) with 4 pads (I+, V+, V-, I- — separate force
/// and sense terminals for precision current measurement) while another
/// source simplifies to 2 pads. Both are valid representations: 4-pad
/// preserves the Kelvin connection benefit (no IR drop in the sense
/// path) while 2-pad is fine for non-precision routing. Downstream
/// tooling should prefer the 4-pad representation when low-µΩ accuracy
/// matters for the design.
KelvinSenseRepresentation {
sources_with_kelvin: Vec<String>,
sources_without_kelvin: Vec<String>,
kelvin_pin_names: Vec<String>,
},
}
impl Variant {
/// Which `axis` names does this variant explain? Used by cli.rs to decide
/// whether the run is `golden_with_variants` (every disagreeing axis
/// covered) or `divergent` (something is uncovered).
pub fn covers(&self) -> Vec<&'static str> {
match self {
Variant::ExposedPadVariant { .. } => vec![
"pad_count", "pad_numbers", "symbol_pin_count", "pin_names", "pin_to_pad_map",
],
Variant::ModuleExtraPads { .. } => vec![
"pad_count", "pad_numbers", "symbol_pin_count", "pin_names", "pin_to_pad_map",
],
Variant::LabelOnlyMismatch { .. } => vec!["package_family"],
Variant::StubSymbolPinNames { .. } => vec!["pin_names", "pin_to_pad_map"],
Variant::BodyDimMeasurementArtifact { .. } => vec!["body_dims"],
Variant::NamingConventionMismatch { .. } => vec!["pin_names", "pin_to_pad_map"],
Variant::KelvinSenseRepresentation { .. } => vec![
"pad_count", "pad_numbers", "symbol_pin_count",
"pin_names", "pin_to_pad_map", "body_dims",
],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultDoc {
pub schema_version: String,
pub status: Status,
pub exit_code: i32,
pub mpn: String,
pub summary: String,
pub sources_compared: Vec<String>,
pub sources_missing: Vec<String>,
pub axes: Vec<crate::compare::AxisOutcome>,
/// Known-benign explanations for any disagreeing axes. When every
/// disagreeing axis is covered by a variant, status is `golden_with_variants`.
pub variants: Vec<Variant>,
pub hints: Vec<Hint>,
pub extractor_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Hint {
/// One specific library source looks suspect — chip-fetcher should re-fetch
/// it from a different upstream (manufacturer site, alternate UL bundle, etc.).
/// `expected_resolution` tells chip-fetcher what to verify after refetch:
/// re-run concur and check that this specific axis now agrees.
RefetchLibrarySource {
/// Which source format to re-fetch — "kicad_sym" (just the symbol),
/// "kicad_mod" (just the footprint), "kicad_both", "eagle", "altium".
format: String,
/// The disagreeing axis (pin_names, package_family, …) so chip-fetcher
/// can correlate to the conflict that triggered this hint.
suspect: String,
notes: String,
/// Concrete next-source recommendations, in priority order. chip-fetcher
/// should walk these in order, re-fetch from each, and re-run concur
/// until status flips to golden / golden_with_variants.
suggested_sources: Vec<String>,
/// What concur expects to see after a successful refetch — chip-fetcher
/// uses this to confirm the refetch worked. e.g.
/// "axis 'pin_to_pad_map' should change from disagree → agree".
expected_resolution: String,
},
/// The KiCad symbol from UL has stub pin names ("1","2",…) instead of real
/// labels (VDD,SCL,…). The footprint is fine. Best response: skip the
/// `<MPN>.kicad_sym` from chip-fetcher entirely and feed ds2sf's symbol
/// JSON to `sym_create` to author a fresh KiCad symbol with real labels.
UlSymbolHasStubNames {
source: String,
pin_count: u32,
suggestion: String,
/// chip-fetcher's recommended next step: NOT a refetch — instead route
/// downstream tooling (sym_create) to use ds2sf's pin labels.
chip_fetcher_action: String,
},
/// ds2sf's output is suspect (e.g. body_dim is the lead-tip span instead of
/// the body D dim — a classic Haiku misread that Opus usually gets right).
/// Suggest re-running ds2sf with a stronger model.
RerunDs2sfWithStrongerModel {
reason: String,
suspect_field: String,
suggested_model: String,
},
/// Differences exist but are within normal tolerance. Caller can accept.
AcceptWithCaveats {
caveats: Vec<String>,
},
/// Surface to a human — automated repair isn't safe.
HumanReview {
reason: String,
notes: String,
},
/// One or more source files weren't parseable at all.
SourceParseFailed {
format: String,
path: PathBuf,
error: String,
},
}
pub fn write(out_path: &std::path::Path, doc: &ResultDoc) -> Result<()> {
fs::write(out_path, serde_json::to_string_pretty(doc)? + "\n")
.with_context(|| format!("writing {}", out_path.display()))?;
Ok(())
}