app
Adom DS2SF - Datasheet to Symbol and Footprint
Public Made by Adomby adom
Datasheet to symbol + footprint + provenance JSON — pins and pads, extracted from the PDF.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
//! Unified result JSON — the agent-to-agent contract.
//!
//! Every `ds2sf extract` invocation writes `<MPN>-extraction.result.json` containing
//! a single object whose shape is stable. Upstream callers (chip-fetcher, automation)
//! switch on `status` + `exit_code` and walk `hints[]` to decide what to do next.
//!
//! Exit codes:
//! 0 → status=ok (extraction completed; review_required may still have entries)
//! 2 → status=needs_better_pdf (recoverable: try a different datasheet PDF)
//! 3 → status=unrecoverable (give up; surface to a human)
use anyhow::{Context as _, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use crate::context::Context;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
Ok,
NeedsBetterPdf,
Unrecoverable,
}
impl Status {
pub fn exit_code(self) -> i32 {
match self {
Status::Ok => 0,
Status::NeedsBetterPdf => 2,
Status::Unrecoverable => 3,
}
}
}
/// Canonical `review_required.reason` strings. Kept in sync with what we instruct
/// the model to emit. Documented in SKILL.md so callers can switch on them.
pub mod reasons {
/// Symbol-pass returned empty pins because the supplied PDF was the internal
/// SoC datasheet, not the module's external castellated-pad datasheet.
pub const MODULE_EXTERNAL_PINOUT_MISSING: &str = "module_external_pinout_missing";
/// info.json's `package` hint disagreed with the datasheet's actual package.
/// Caller should update info.json's package field to match.
pub const PACKAGE_MISMATCH_IN_HINT: &str = "package_mismatch_in_hint";
/// Datasheet covers many variants; the right one couldn't be picked from
/// info.json + MPN alone. Caller should pass --variant or a more-specific PDF.
#[allow(dead_code)]
pub const VARIANT_DISAMBIGUATION_NEEDED: &str = "variant_disambiguation_needed";
/// Claude's response failed schema validation twice in a row. Inspect
/// `.ds2sf-cache/claude-symbol-raw*.txt` for the raw response.
pub const UNRECOVERABLE_EXTRACTION_ERROR: &str = "unrecoverable_extraction_error";
/// Datasheet PDF wasn't found at the auto-detected or --pdf-supplied path.
#[allow(dead_code)]
pub const MISSING_DATASHEET_PDF: &str = "missing_datasheet_pdf";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultDoc {
pub schema_version: String,
pub status: Status,
pub exit_code: i32,
pub mpn: String,
pub manufacturer: Option<String>,
pub datasheet_used: PathBuf,
pub datasheet_sha256: String,
pub summary: String,
/// Merged review_required from symbol + footprint passes (whichever ran).
pub review_required: Vec<Value>,
/// Actionable next-steps for an agent caller.
pub hints: Vec<Hint>,
/// Paths to the three output JSONs. Null when that file wasn't written.
pub outputs: Outputs,
pub extractor_version: String,
pub claude_model: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Outputs {
pub symbol_json: Option<PathBuf>,
pub footprint_json: Option<PathBuf>,
pub provenance_json: Option<PathBuf>,
/// ds2sf's own SVG — synthesized purely from the manufacturer's datasheet
/// mechanical drawing. This is the independent "manufacturer's voice" in
/// chipsmith's cross-validation and the primary visual output of ds2sf.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footprint_svg_ds2sf: Option<PathBuf>,
/// KiCad pcbnew render of the stdlib footprint (when a match exists).
/// A third-party interpretation for visual comparison — NOT the same
/// data source as ds2sf's extraction. chipsmith diffs these two.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footprint_svg_pcbnew: Option<PathBuf>,
/// Legacy field (pre-v0.7). Points to the ds2sf SVG for backwards compat.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footprint_svg: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub footprint_svg_source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Hint {
/// Try a different PDF in the same chip-dir. `candidates` lists plausible
/// alternates ds2sf already located on disk.
TryAlternatePdf {
reason: String,
candidates: Vec<PathBuf>,
},
/// Update an upstream metadata file (e.g. chip-fetcher's info.json) and
/// re-run. `key` + `current` + `extracted` describe the disagreement.
UpdateUpstreamMetadata {
file: PathBuf,
key: String,
current: String,
extracted: String,
},
/// The output is good but a human should inspect the listed pins/fields
/// (muxed protocols, thermal pads, power sequencing, etc.).
HumanReview {
reason: String,
notes: String,
},
/// The package isn't in service-kicad's standard library; downstream
/// footprint-authoring should use the datasheet-extracted dimensions.
UseDatasheetExtractedFootprint {
package: String,
},
}
pub fn write(ctx: &Context, doc: &ResultDoc) -> Result<()> {
let path = result_path(ctx);
fs::write(&path, serde_json::to_string_pretty(doc)? + "\n")
.with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn result_path(ctx: &Context) -> PathBuf {
ctx.out_dir
.join(format!("{}-extraction.result.json", ctx.mpn))
}
/// Scan the chip-dir for alternate PDFs that might be the right datasheet.
/// Returns paths relative to absolute, ordered by likelihood (most-specific first).
pub fn find_alternate_pdfs(ctx: &Context) -> Vec<PathBuf> {
let mut out = Vec::new();
let entries = match fs::read_dir(&ctx.chip_dir) {
Ok(e) => e,
Err(_) => return out,
};
let used = ctx.datasheet_path.clone();
let mut all_pdfs: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().and_then(|s| s.to_str()) == Some("pdf")
&& p != &used
})
.collect();
// Sort by relevance score (lower = better). The most-canonical-looking
// datasheet filenames win. `<MPN>-data-sheet.pdf` and
// `<MPN>-datasheet.pdf` outrank module-guideline / hardware-guideline
// PDFs (which themselves outrank application notes / shield guides /
// terms-of-use / etc.).
//
// Real-world catch (PCA9685, 2026-05-06): chip-fetcher pulled the
// Adafruit shield user guide as the auto-detected `<MPN>.pdf`. The
// canonical NXP datasheet was sitting next to it as
// `PCA9685-data-sheet.pdf` but ranked too low, so the alt-PDF loop
// suggested the LED-driver-selector-guide first. v0.3 ranks the actual
// datasheet ahead of the family-specific guides.
all_pdfs.sort_by_key(|p| {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
let mut score: i32 = 0;
// Strongest positive signal: filename literally contains "data-sheet"
// or "datasheet" — these are the canonical IC datasheets.
if name.contains("data-sheet") || name.contains("datasheet") {
score -= 200;
}
// Per-package pinout supplements. Many vendors (notably Lattice for
// iCE40) split the per-package pin-number assignments out of the
// main datasheet into a separate "Pinout Files" / "Package Diagrams"
// / "Hardware Checklist" PDF. When the main datasheet refuses with
// a `pinout_supplement_required` review_required, this is the path
// chip-fetcher should walk.
if name.contains("package-diagrams") || name.contains("package-diagram")
|| name.contains("pinout") || name.contains("pin-out")
|| name.contains("hardware-checklist") || name.contains("hwchecklist")
|| name.contains("package-information") || name.contains("pkg-info")
{
score -= 150;
}
// Module-specific datasheets (ESP32 case) — second-best when looking
// at a module product.
if name.contains("hardware") || name.contains("guideline") { score -= 100; }
if name.contains("original") { score -= 50; }
if name.contains("module") { score -= 40; }
if name.contains("wroom") || name.contains("wrover") { score -= 30; }
// Negative signals: marketing / non-datasheet documents.
if name.contains("brief") || name.contains("ti-applies") || name.contains("terms-of-use") {
score += 100; // these are NOT what we want
}
if name.contains("application-note") || name.contains("an-")
|| name.contains("errata") || name.contains("trm")
|| name.contains("reference-manual") || name.contains("user-guide")
|| name.contains("user-manual")
|| name.contains("selector-guide") || name.contains("shield")
|| name.contains("flat-panel") || name.contains("white-good")
|| name.contains("application-guide")
|| name.contains("led-controllers") || name.contains("pwm-click")
|| name.contains("reel-pack") || name.contains("get-diagram")
|| name.contains("check-to-add")
{
score += 50;
}
score
});
// Cap to top 5 candidates so the JSON stays digestible.
out.extend(all_pdfs.into_iter().take(5));
out
}