@protoboardai/ext-tscircuit 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +59 -0
  3. package/README.md +493 -2
  4. package/evals/announce-pcb-fallback/case.ts +67 -0
  5. package/evals/catch-swapped-nets/case.ts +69 -0
  6. package/evals/generate-imu-board/case.ts +60 -0
  7. package/evals/locked-board-fix-upstream/case.ts +70 -0
  8. package/lib/checks.ts +464 -0
  9. package/lib/config.ts +384 -0
  10. package/lib/fingerprint.ts +144 -0
  11. package/lib/footprints.ts +370 -0
  12. package/lib/kicad/cli.ts +93 -0
  13. package/lib/kicad/drc.ts +95 -0
  14. package/lib/kicad/fields.ts +65 -0
  15. package/lib/kicad/handoff.ts +112 -0
  16. package/lib/kicad/netlists.ts +311 -0
  17. package/lib/kicad/probe.cjs +21 -0
  18. package/lib/kicad/project.ts +594 -0
  19. package/lib/kicad/refdes.ts +130 -0
  20. package/lib/kicad/schematic.ts +624 -0
  21. package/lib/kicad/sexpr.ts +117 -0
  22. package/lib/land-geometry.ts +177 -0
  23. package/lib/land-patterns.ts +706 -0
  24. package/lib/layout.ts +236 -0
  25. package/lib/models.ts +284 -0
  26. package/lib/native.ts +199 -0
  27. package/lib/outputs.ts +383 -0
  28. package/lib/paste.ts +69 -0
  29. package/lib/probe.cjs +31 -0
  30. package/lib/runtime.ts +185 -0
  31. package/lib/silk.ts +141 -0
  32. package/lib/source.ts +472 -0
  33. package/lib/stepfile.ts +210 -0
  34. package/lib/suppliers.ts +39 -0
  35. package/ops/circuit.ts +270 -0
  36. package/ops/kicad.ts +636 -0
  37. package/ops/package.ts +156 -0
  38. package/package.json +81 -4
  39. package/protoboard-extension.json +313 -0
  40. package/skills/circuit-board/SKILL.md +494 -0
  41. package/skills/circuit-board/references/pcb-workflow.md +87 -0
  42. package/skills/kicad-cli/SKILL.md +210 -0
  43. package/skills/kicad-layout-review/SKILL.md +275 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Reference designators for KiCad, stable between exports
3
+ * (docs/specs/pcb-kicad-handoff.md §2.1). Assigned once per UHD instance path
4
+ * and recorded in `tscircuit/refdes.json`, so a re-export (and KiCad's Update
5
+ * PCB from Schematic) keeps every unchanged part's designator and placement.
6
+ *
7
+ * 1. an instance already recorded keeps its designator
8
+ * 2. moved: a new instance whose last path segment is that of exactly one
9
+ * recorded instance no longer on the board, with a designator of its
10
+ * kind, takes it over (a part moved into or out of a sub-module keeps
11
+ * its designator; docs/specs/nested-boards.md §6)
12
+ * 3. a new instance whose id (the last segment of its path: `led1/r1` reads
13
+ * `r1`) reads as a designator with its kind's prefix (`r3`, `U1`) gets
14
+ * it upper-cased, when free
15
+ * 4. otherwise the kind's prefix and the lowest free number, new instances
16
+ * taken in instance-path order
17
+ *
18
+ * A removed instance stays in the record, so its designator is not reused.
19
+ * Designators are unique over the whole board, sub-modules included.
20
+ */
21
+ import type { BoardComponent, BoardComponentKind } from "@protoboardai/core";
22
+
23
+ export const REFDES_SCHEMA = "protoboard.refdes/v0";
24
+
25
+ export interface RefdesRecord {
26
+ schema: typeof REFDES_SCHEMA;
27
+ /** instance path → designator */
28
+ assigned: Record<string, string>;
29
+ }
30
+
31
+ export const PREFIX: Record<BoardComponentKind, string> = {
32
+ resistor: "R",
33
+ capacitor: "C",
34
+ inductor: "L",
35
+ ferrite_bead: "FB",
36
+ connector: "J",
37
+ switch: "SW",
38
+ ic: "U",
39
+ };
40
+
41
+ /**
42
+ * The conventional prefix for a part the board netlist files under `ic`
43
+ * (IEEE 315 / IPC usage): diodes and LEDs D, transistors Q, crystals and
44
+ * resonators Y, fuses F, batteries BT, buzzers and speakers LS. Read from the
45
+ * part's UHD categories: an integrated circuit (`component.ic…`) stays U
46
+ * whatever its name says (an LED driver, a battery charger, a protection IC).
47
+ * Only a part with no categories falls back to its name, never its id (an id
48
+ * starts with the manufacturer: "Diodes Inc." makes MOSFETs). Undefined keeps
49
+ * the kind's prefix.
50
+ */
51
+ export function prefixFromPart(def: { id: string; name?: string; categories?: string[] } | undefined): string | undefined {
52
+ if (!def) return undefined;
53
+ const cats = (def.categories ?? []).map((c) => c.toLowerCase());
54
+ const has = (re: RegExp) => cats.some((c) => re.test(c));
55
+ if (cats.length) {
56
+ if (has(/(^|\.)ic(\.|$)/)) return undefined;
57
+ if (has(/(^|\.)transistors?(\.|$)|mosfet|bjt/)) return "Q";
58
+ if (has(/(^|\.)(diodes?|leds?|tvs|zener|rectifiers?)(\.|$)/)) return "D";
59
+ if (has(/(^|\.)(crystals?|resonators?|oscillators?)(\.|$)/)) return "Y";
60
+ if (has(/(^|\.)fuses?(\.|$)/)) return "F";
61
+ if (has(/(^|\.)(batter(y|ies)|cells?)(\.|$)/) && !has(/charger/)) return "BT";
62
+ if (has(/(^|\.)(buzzers?|speakers?)(\.|$)/)) return "LS";
63
+ return undefined;
64
+ }
65
+ const name = (def.name ?? "").toLowerCase();
66
+ if (!name || /\b(ic|driver|controller|charger|regulator|converter|protection|monitor|gauge|module)\b/.test(name)) return undefined;
67
+ if (/\b(transistor|mosfet|bjt|jfet|igbt)s?\b/.test(name)) return "Q";
68
+ if (/\b(led|diode|zener|schottky|tvs|rectifier)s?\b/.test(name)) return "D";
69
+ if (/\b(crystal|resonator|oscillator)s?\b/.test(name)) return "Y";
70
+ if (/\b(fuse|polyfuse|ptc)s?\b/.test(name)) return "F";
71
+ if (/\bbatter(y|ies)\b/.test(name)) return "BT";
72
+ if (/\b(buzzer|speaker)s?\b/.test(name)) return "LS";
73
+ return undefined;
74
+ }
75
+
76
+ export const refdesFile = (native: string) => `${native}/refdes.json`;
77
+
78
+ export function emptyRefdes(): RefdesRecord {
79
+ return { schema: REFDES_SCHEMA, assigned: {} };
80
+ }
81
+
82
+ /** Designators for every component, extending `record` (returned as a new record) for the ones it lacks. */
83
+ export function assignRefdes(components: Pick<BoardComponent, "instance" | "kind">[], record: RefdesRecord = emptyRefdes(), prefixOf: (c: Pick<BoardComponent, "instance" | "kind">) => string | undefined = () => undefined): { record: RefdesRecord; added: string[] } {
84
+ const assigned = { ...record.assigned };
85
+ const taken = new Set(Object.values(assigned));
86
+ const fresh = components.filter((c) => !assigned[c.instance]).sort((a, b) => a.instance.localeCompare(b.instance));
87
+ const leaf = (path: string) => path.slice(path.lastIndexOf("/") + 1);
88
+ // pass 0: a part that moved (same id, another path) keeps its designator, when the match is unambiguous
89
+ const present = new Set(components.map((c) => c.instance));
90
+ const gone = Object.keys(assigned).filter((i) => !present.has(i));
91
+ for (const c of fresh) {
92
+ const prefix = (c.kind === "ic" ? prefixOf(c) : undefined) ?? PREFIX[c.kind] ?? "U";
93
+ const was = gone.filter((g) => leaf(g) === leaf(c.instance) && new RegExp(`^${prefix}\\d+$`).test(assigned[g]));
94
+ if (was.length !== 1 || fresh.filter((f) => leaf(f.instance) === leaf(c.instance)).length !== 1) continue;
95
+ assigned[c.instance] = assigned[was[0]];
96
+ delete assigned[was[0]];
97
+ }
98
+ // pass 1: ids that already read as designators of their kind keep them
99
+ for (const c of fresh) {
100
+ if (assigned[c.instance]) continue;
101
+ const prefix = (c.kind === "ic" ? prefixOf(c) : undefined) ?? PREFIX[c.kind] ?? "U";
102
+ const m = /^([a-z]+)(\d+)$/i.exec(leaf(c.instance));
103
+ const want = m && m[1].toUpperCase() === prefix ? `${prefix}${Number(m[2])}` : undefined;
104
+ if (want && Number(m![2]) > 0 && !taken.has(want)) {
105
+ assigned[c.instance] = want;
106
+ taken.add(want);
107
+ }
108
+ }
109
+ // pass 2: the lowest free number of the kind's prefix
110
+ for (const c of fresh) {
111
+ if (assigned[c.instance]) continue;
112
+ const prefix = (c.kind === "ic" ? prefixOf(c) : undefined) ?? PREFIX[c.kind] ?? "U";
113
+ let n = 1;
114
+ while (taken.has(`${prefix}${n}`)) n++;
115
+ assigned[c.instance] = `${prefix}${n}`;
116
+ taken.add(`${prefix}${n}`);
117
+ }
118
+ const sorted = Object.fromEntries(Object.entries(assigned).sort(([a], [b]) => a.localeCompare(b)));
119
+ return { record: { schema: REFDES_SCHEMA, assigned: sorted }, added: fresh.map((c) => c.instance) };
120
+ }
121
+
122
+ export function parseRefdes(text: string | undefined): RefdesRecord | undefined {
123
+ if (!text) return undefined;
124
+ try {
125
+ const r = JSON.parse(text) as RefdesRecord;
126
+ return r?.schema === REFDES_SCHEMA && r.assigned && typeof r.assigned === "object" ? r : undefined;
127
+ } catch {
128
+ return undefined;
129
+ }
130
+ }