skill
EDA Bridge Standard: one way for an AI to drive every PCB tool
Public Made by Adomby adom
The standard every Adom EDA bridge follows so one AI can drive Altium, KiCad, Fusion 360 and any future PCB tool the same way: shared verb names in tiers, the board model JSON, reply and refusal conventions, and a coverage table of each bridge against the Altium reference.
main
Colby Knox
0.31.0: physical_properties, preferences, select_items, cancel_run join (#29); proposals may carry the platform tier
e3abbdb
8d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
#!/usr/bin/env python3
"""Generate the standard's verb files from the reference bridge's catalog and the agreed proposals.
The Altium bridge's verbs are the base: reference/altium-<version>.json is its `altium_describe` reply, and each verb
gets a tier (tools/tiers.json). Verbs proposed by any bridge are added from tools/proposed/<thread>.json once a thread
other than the proposer's has agreed to them (agreedBy). Each bridge's coverage, Altium's included, comes from its verb
list (reference/<bridge>-<version>.json) and the hand-made map of verbs that do the same job under another name
(tools/equivalents/<bridge>.json). One file per owner, so two threads never push the same file.
Writes verbs/standard.json, verbs/coverage.json, docs/coverage.md and the coverage block in README.md.
Run it after refreshing any reference file: python3 tools/generate.py
"""
import glob
import json
import os
import re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TIERS = {
"core": "Read and edit the design. Every bridge provides these: they are how an AI sees and changes a board.",
"native": "Wraps the EDA tool's own feature (its DRC, ERC, Gerbers, pours, Update PCB). Every tool has one; the bridge exposes it under this name.",
"checks": "Engineering computed from the board model, not by the tool: one shared engine, so every bridge that serves the model gets them all.",
"app": "Driving the application itself (launch, licence, dialogs, windows, views). Each bridge has its own; the names are shared where the job is.",
"platform": "Specific to one tool's platform (Altium 365, OutJobs, integrated libraries). Not part of the standard; listed so nothing is lost.",
}
ORDER = ["core", "native", "checks", "app", "platform"]
def load(name):
with open(os.path.join(ROOT, name)) as f:
return json.load(f)
def latest(bridge):
# by version number, not by text: 1.0.10 is newer than 1.0.9
def key(f):
v = os.path.basename(f)[len(bridge) + 1:-len(".json")]
return [int(x) if x.isdigit() else x for x in re.split(r"[.-]", v)]
files = sorted(glob.glob(os.path.join(ROOT, "reference", f"{bridge}-*.json")), key=key)
return load(os.path.relpath(files[-1], ROOT))
def strip(name, prefix):
return name[len(prefix):] if name.startswith(prefix) else name
# exact and partial count as covered; unsupported (the verb answers not_supported for the whole job) is listed, not counted
MATCHES = ("exact", "partial", "unsupported")
PROPOSAL_FIELDS = ["name", "tier", "summary", "input", "output", "hint", "proposedBy", "since", "agreedBy"]
def proposals(tiered):
"""tools/proposed/<thread>.json, checked. A proposal lands only when a thread other than its proposer agreed to it."""
entries, problems, seen = [], [], set()
for path in sorted(glob.glob(os.path.join(ROOT, "tools", "proposed", "*.json"))):
thread = os.path.basename(path)[:-len(".json")]
for i, p in enumerate(load(os.path.relpath(path, ROOT))):
name = p.get("name") or f"{thread} entry {i}"
missing = [k for k in PROPOSAL_FIELDS if k not in p]
if missing:
problems.append(f"{name}: missing {', '.join(missing)}")
continue
if p["proposedBy"] != thread:
problems.append(f"{name}: proposedBy {p['proposedBy']!r} but filed in tools/proposed/{thread}.json")
if p["tier"] not in ORDER:
problems.append(f"{name}: tier {p['tier']!r} is not one of core, native, checks, app, platform")
if not [t for t in p["agreedBy"] if t != p["proposedBy"]]:
problems.append(f"{name}: agreedBy names no thread other than its proposer ({p['proposedBy']})")
# a tiered Altium verb was in the standard first; an untiered one of the same name is Altium implementing this
if name in tiered:
problems.append(f"{name}: already a standard verb from the reference; credit the proposer's verb in tools/equivalents/ instead")
if name in seen:
problems.append(f"{name}: proposed twice")
seen.add(name)
entries.append(p)
if problems:
raise SystemExit("tools/proposed/ refused:\n " + "\n ".join(problems))
return entries
def main():
ref = latest("altium")
tiers = load("tools/tiers.json")
eq = {os.path.basename(f)[:-len(".json")]: load(os.path.relpath(f, ROOT))
for f in glob.glob(os.path.join(ROOT, "tools", "equivalents", "*.json"))}
bridges = {b: latest(b) for b in ("altium", "kicad", "fusion360")}
others = {b: o for b, o in bridges.items() if b != "altium"}
prefixes = {"altium": "altium_", "kicad": "kicad_", "fusion360": "fusion_"}
have = {b: {strip(v if isinstance(v, str) else v["name"], prefixes[b]) for v in o["verbs"]} for b, o in bridges.items()}
proposed = proposals(tiers)
proposed_names = {p["name"] for p in proposed}
untiered = sorted(strip(v["name"], "altium_") for v in ref["verbs"]
if strip(v["name"], "altium_") not in tiers and strip(v["name"], "altium_") not in proposed_names)
if untiered:
raise SystemExit(f"reference/altium-{ref['version']}.json has verbs with no tier: give each one in tools/tiers.json\n "
+ ", ".join(untiered))
standard = []
for v in ref["verbs"]:
name = strip(v["name"], "altium_")
if name in proposed_names:
continue # Altium implements an agreed proposal: the proposal is the spec, and Altium is credited exact
standard.append({
"name": name, "tier": tiers[name], "source": "altium", "summary": v.get("summary", ""),
"input": v.get("input", {}), "output": v.get("output", {}), "hint": v.get("hint", ""),
"pitfalls": v.get("pitfalls", []), "related": [strip(r, "altium_") for r in v.get("related", [])],
"example": v.get("example", {}), "longRunning": v.get("longRunning", False), "timeoutSeconds": v.get("timeoutSeconds"),
})
# the reference still answers some verbs in mils; the standard is mm (docs/conventions.md)
if re.search(r"Mils?\b", json.dumps([v.get("input", {}), v.get("output", {})])):
standard[-1]["unitsNote"] = "The reference bridge answers this verb in mils (xMil). A bridge following the standard answers in mm under Mm names (xMm)."
for p in proposed:
standard.append({
"name": p["name"], "tier": p["tier"], "source": "proposed", "summary": p["summary"],
"input": p["input"], "output": p["output"], "hint": p["hint"], "pitfalls": p.get("pitfalls", []),
"related": p.get("related", []), "example": p.get("example", {}), "longRunning": p.get("longRunning", False),
"timeoutSeconds": p.get("timeoutSeconds"), "proposedBy": p["proposedBy"], "since": p["since"], "agreedBy": p["agreedBy"],
})
standard.sort(key=lambda s: (ORDER.index(s["tier"]), s["name"]))
# an equivalent filed under a name the standard lacks (a typo, a renamed verb) would credit nothing, silently
names = {s["name"] for s in standard}
for b, section in eq.items():
for k in section:
if k not in names:
print(f"warning: tools/equivalents/{b}.json: {k} is not a standard verb, not counted")
coverage = []
for s in standard:
row = {"name": s["name"], "tier": s["tier"], "source": s["source"]}
for b in bridges:
row[b] = None
# the bridge's own equivalents file comes before a name match: a verb can share a standard name and do
# another job (kicad_edit_footprint edited placed parts; the standard edit_footprint is the library verb)
if s["name"] in eq.get(b, {}):
verb, match, *note = eq[b][s["name"]]
if match not in MATCHES:
raise SystemExit(f"tools/equivalents/{b}.json: {s['name']}: match {match!r} is not one of {', '.join(MATCHES)}")
parts = [strip(p.strip(), prefixes[b]) for p in re.split(r"\s*[/+]\s*", verb)]
# an equivalent counts only when its verbs are in that bridge's reference catalog
absent = [p for p in parts if p not in have[b]]
if absent:
print(f"warning: {b} {s['name']}: {', '.join(absent)} not in reference/{b}-{bridges[b]['version']}.json, not counted")
else:
row[b] = {"verb": " / ".join(prefixes[b] + p for p in parts), "match": match}
if note:
row[b]["note"] = note[0]
# a stale entry hides the bridge's own same-named verb (altium-lane, 2026-10-30: four sheet verbs read
# partial for six releases after Altium shipped them exact); keep it only when that verb does another job,
# and say so in the entry's note, which quiets this
if parts != [s["name"]] and s["name"] in have[b] and not note:
print(f"warning: {b} {s['name']}: tools/equivalents/{b}.json maps it to {row[b]['verb']} ({match}), but the bridge has its own {prefixes[b]}{s['name']}: drop the entry, or give it a note saying the same-named verb does another job")
if row[b] is None and s["name"] in have[b]:
row[b] = {"verb": prefixes[b] + s["name"], "match": "exact"}
coverage.append(row)
meta = {"standard": load("package.json")["version"],
"reference": {"bridge": "altium", "version": ref["version"]}, "proposed": len(proposed),
"compared": {b: o["version"] for b, o in others.items()}, "tiers": TIERS}
os.makedirs(os.path.join(ROOT, "verbs"), exist_ok=True)
with open(os.path.join(ROOT, "verbs", "standard.json"), "w") as f:
json.dump({**meta, "verbs": standard}, f, indent=1)
f.write("\n")
with open(os.path.join(ROOT, "verbs", "coverage.json"), "w") as f:
json.dump({**meta, "verbs": coverage}, f, indent=1)
f.write("\n")
# counts per tier and bridge
def count(b, tier, matches=("exact", "partial")):
return sum(1 for r in coverage if r["tier"] == tier and r.get(b) and r[b]["match"] in matches)
lines = ["| Tier | Verbs | Altium | KiCad | Fusion 360 |", "|---|---|---|---|---|"]
tot = {"altium": 0, "kicad": 0, "fusion360": 0, "all": 0}
for t in ORDER:
n = sum(1 for r in coverage if r["tier"] == t)
a, k, fu = count("altium", t), count("kicad", t), count("fusion360", t)
lines.append(f"| **{t}** | {n} | {a} | {k} | {fu} |")
if t != "platform":
tot["all"] += n; tot["altium"] += a; tot["kicad"] += k; tot["fusion360"] += fu
lines.append(f"| standard (all but platform) | {tot['all']} | {tot['altium']} | {tot['kicad']} | {tot['fusion360']} |")
summary = "\n".join(lines)
# docs/coverage.md: the full table
md = [f"# Coverage: every standard verb on each bridge",
"",
f"Altium bridge {ref['version']} (the reference), KiCad bridge {others['kicad']['version']}, Fusion 360 bridge {others['fusion360']['version']}. "
f"{len(proposed)} verbs came from other bridges' proposals (tools/proposed/); they are marked *proposed*.",
"`exact` is the same job under the same or an equivalent name; `partial` does part of it or does it differently (`verbs/coverage.json` names what in `note`); `unsupported` answers `not_supported` for the whole job and is not counted; blank is missing.",
"Generated by `tools/generate.py` from `verbs/coverage.json`; do not edit by hand.", "", summary, ""]
for t in ORDER:
md += [f"## {t}", "", TIERS[t], "", "| Verb | What it does | Altium | KiCad | Fusion 360 |", "|---|---|---|---|---|"]
for r, s in zip(coverage, standard):
if r["tier"] != t:
continue
cell = lambda c: "" if not c else ("unsupported" if c["match"] == "unsupported" else f"`{c['verb']}`" + (" (partial)" if c["match"] == "partial" else ""))
summ = s["summary"].replace("|", "/").replace("\n", " ")
if len(summ) > 160:
summ = summ[:157].rsplit(" ", 1)[0] + "..."
mark = " *proposed*" if r["source"] == "proposed" else ""
md.append(f"| `{r['name']}`{mark} | {summ} | {cell(r.get('altium'))} | {cell(r.get('kicad'))} | {cell(r.get('fusion360'))} |")
md.append("")
with open(os.path.join(ROOT, "docs", "coverage.md"), "w") as f:
f.write("\n".join(md))
# README coverage block
readme = os.path.join(ROOT, "README.md")
if os.path.exists(readme):
text = open(readme).read()
block = f"<!-- coverage:start (tools/generate.py writes this) -->\n{summary}\n<!-- coverage:end -->"
text = re.sub(r"<!-- coverage:start.*?<!-- coverage:end -->", lambda _: block, text, flags=re.S)
open(readme, "w").write(text)
print(f"standard: {len(standard)} verbs ({tot['all']} in the standard, {len(proposed)} proposed); "
f"altium covers {tot['altium']}, kicad {tot['kicad']}, fusion {tot['fusion360']}")
if __name__ == "__main__":
main()