skill
PCB Design Rules
Public Made by Adomby adom
Canonical PCB fab design rules (DRC) for every popular vendor: one JSON record per vendor, process and layer count, with generated Fusion/EAGLE .dru and .edru, KiCad .kicad_dru plus board-setup JSON, Altium .RUL exports, and a human comparison table. Every number cites the vendor's own capabilities page and the date it was read.
main
John Lauer
Initial publish: canonical per-vendor PCB fab design rules. 13 vendors, 56 records (vendor, process, layer count) read…
bafc3ce
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
#!/usr/bin/env python3
"""Convert a research-agent output file into canonical records JSON.
Usage: python3 tools/import_agent_json.py <agent-output.log> [<agent-output2.log> ...]
Each input file holds one fenced or bare JSON object (or array of objects)
in the agent schema:
{"vendor": str, "vendorUrl"?: str,
"processes": [{"process": str, "layers": {"2"|"4"|"6": {...fields...}}, "notes"?: str}],
"unreachable"?: [str]}
Field values are {"mm": f, "mil": f, "condition"?: str} or null.
Writes rules/records/<slug>.json in the canonical page schema, preserving
every condition string and source URL. Existing files are overwritten only
for vendors present in the input.
"""
import json
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parent.parent
def slugify(s):
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
def extract_json(text):
dec = json.JSONDecoder()
objs = []
i = 0
while True:
j = len(text)
for ch in "{[":
k = text.find(ch, i)
if k != -1 and k < j:
j = k
if j == len(text):
break
try:
obj, end = dec.raw_decode(text[j:])
except json.JSONDecodeError:
i = j + 1
continue
objs.append(obj)
i = j + end
if not objs:
raise ValueError("no JSON found")
if len(objs) == 1:
return objs[0]
# flatten arrays of vendor objects into one list
flat = []
for o in objs:
if isinstance(o, list):
flat.extend(o)
else:
flat.append(o)
return flat
def num(v, sub, default=None):
if not v:
return default
return v.get(sub, default)
def r2(v):
return round(v, 2) if isinstance(v, (int, float)) else v
def r4(v):
return round(v, 4) if isinstance(v, (int, float)) else v
def conv_dim(v):
"""{mm, mil, condition} -> canonical, or None."""
if not v:
return None
out = {}
if v.get("mm") is not None:
out["mm"] = r4(v["mm"])
if v.get("mil") is not None:
out["mil"] = r2(v["mil"])
if v.get("condition"):
out["condition"] = v["condition"]
return out or None
def conv_record(process, layers_n, f):
ann = f.get("annularRing")
annular = None
if ann and ann.get("mm") is not None:
annular = {"viaMm": r4(ann["mm"]), "viaMil": r2(ann.get("mil"))}
if ann.get("condition"):
annular["note"] = ann["condition"]
via = f.get("minVia") or {}
drill_mm = num(f.get("minDrill"), "mm")
pad_mm = via.get("padMm")
pad_mil = via.get("padMil")
if pad_mm is None and drill_mm is not None and annular and annular.get("viaMm"):
pad_mm = round(drill_mm + 2 * annular["viaMm"], 4)
pad_mil = r2(pad_mm / 0.0254)
rec = {
"process": process,
"layerCount": int(layers_n),
"minTraceWidth": conv_dim(f.get("minTraceWidth")),
"minSpacing": conv_dim(f.get("minSpacing")),
"minDrill": conv_dim(f.get("minDrill")),
"minVia": ({"drillMm": r4(via["drillMm"]), "drillMil": r2(via.get("drillMil")),
"padMm": r4(pad_mm) if pad_mm is not None else None,
"padMil": r2(pad_mil) if pad_mil is not None else None} if via.get("drillMm") is not None else None),
"annularRing": annular,
"copperToBoardEdge": conv_dim(f.get("copperToBoardEdge")),
"holeToHole": conv_dim(f.get("holeToHole")),
"maskExpansion": conv_dim(f.get("maskExpansion")),
"maskSliver": conv_dim(f.get("maskSliver")),
"silkLineWidth": conv_dim(f.get("silkLineWidth")),
"silkToCopper": conv_dim(f.get("silkToCopper")),
"minCopperClearance": conv_dim(f.get("minCopperClearance")),
"boardThicknessOptionsMm": f.get("boardThicknessOptions"),
"copperWeightOptionsOz": f.get("copperWeightOptions"),
"surfaceFinishes": f.get("surfaceFinishes"),
"maxBoardSize": f.get("maxBoardSize"),
"notes": (f.get("notes") or "").strip() or None,
"sources": f.get("sources") or [],
}
return rec
def main():
sess = pathlib.Path("/home/adom/.kimi-code/sessions/wd_project_e6a5dc78aec3/session_500e40b4-c113-4618-948b-e81bb2d33c95/agents/main/tasks")
for arg in sys.argv[1:]:
path = pathlib.Path(arg)
if not path.exists():
path = sess / arg / "output.log"
obj = extract_json(path.read_text())
vendors = obj if isinstance(obj, list) else [obj]
for v in vendors:
recs = []
for proc in v.get("processes", []):
for layers_n in sorted(proc.get("layers", {}), key=int):
recs.append(conv_record(proc["process"], layers_n, proc["layers"][layers_n]))
out = {
"vendor": v["vendor"],
"vendorSlug": slugify(v["vendor"]),
"vendorUrl": v.get("vendorUrl"),
"records": recs,
}
if v.get("unreachable"):
out["unreachable"] = v["unreachable"]
dest = ROOT / "rules" / "records" / f"{out['vendorSlug']}.json"
dest.write_text(json.dumps(out, indent=2) + "\n")
print(f"{dest.name}: {len(recs)} records" + (f" (unreachable: {len(out['unreachable'])})" if out.get("unreachable") else ""))
if __name__ == "__main__":
main()