@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
package/ops/package.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Contributions to the Manufacturing Package (@protoboardai/ext-release section
3
+ * contract) for the boards this provider designs: each board's fabrication
4
+ * and assembly order in its unit's work order. The fab profile (layers,
5
+ * thickness, copper, finish, solder mask, silkscreen) comes from
6
+ * circuit.config.ts, with the defaults below stated as such; the stack-up
7
+ * follows from the layer count and thickness; the assembly sides and
8
+ * placements from out/pnp.csv; leadless packages (which want a flat finish,
9
+ * a stencil and X-ray inspection) from the parts' package facts; the files
10
+ * are the standard outputs.
11
+ */
12
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { boardNetlist, boardOptions, parseCsv, type CapabilityBinding, type Project } from "@protoboardai/core";
15
+ import type { Block, SectionContext, SectionFn } from "@protoboardai/ext-release";
16
+ import { perBuild, providerModules } from "@protoboardai/ext-release/helpers";
17
+ import { viasInPads } from "../lib/checks.ts";
18
+ import { boardLands } from "../lib/footprints.ts";
19
+ import { readNative, readOverrides } from "../lib/native.ts";
20
+
21
+ const PROVIDER = "@protoboardai/ext-tscircuit";
22
+ /** Package names (as the maker states them) with joints under the body. */
23
+ const LEADLESS = /QFN|DFN|LGA|BGA|CSP|\bW?SON\b/i;
24
+
25
+ export interface FabProfile {
26
+ layers: number;
27
+ thickness_mm: number;
28
+ copper: string;
29
+ finish: string;
30
+ finishWhy: string;
31
+ mask: string;
32
+ silk: string;
33
+ assembly: "fab" | "hand";
34
+ /** Notes for the fabricator: the provider's own (a hold while land patterns are unverified, via-in-pad fill), then circuit.config.ts's. */
35
+ notes: string[];
36
+ /** How many of `notes` are the provider's own (they come first). */
37
+ ownNotes: number;
38
+ /** Land patterns the board uses, and those not verified against their makers' drawings (entry id: instances). */
39
+ lands: { total: number; unverified: string[] };
40
+ /** Vias inside SMD pads in the built board, and what the config says about them. */
41
+ viaInPad: { count: number; policy: "avoid" | "fill_and_cap" };
42
+ leadless: string[];
43
+ sides: Record<string, number>;
44
+ placed: number;
45
+ gerbers: string[];
46
+ drills: string[];
47
+ }
48
+
49
+ /** The fabrication and assembly profile of one board: its config, its parts and its standard outputs. */
50
+ export function fabProfile(p: Project, b: CapabilityBinding): FabProfile {
51
+ const c = b.constraints as { layers?: number; thickness_mm?: number; copper_oz?: number; finish?: string; solder_mask?: string; silkscreen?: string; assembly?: "fab" | "hand"; fab_notes?: string[]; via_in_pad?: "avoid" | "fill_and_cap" };
52
+ const def = p.definitions.get(b.module!)!.def;
53
+ const netlist = boardNetlist(def, p.lookup, boardOptions(p));
54
+ const lands = boardLands(netlist.components, b.native ? readOverrides(p.root, b.native).overrides : {});
55
+ const unverified = lands.filter((l) => l.problems.length).map((l) => `${l.entry.id} (${l.instances.join(", ")})`);
56
+ const native = b.native ? readNative(p.root, b.native) : undefined;
57
+ const viaCount = native ? viasInPads(native.circuitJson).length : 0;
58
+ const policy = c.via_in_pad ?? "avoid";
59
+ const own: string[] = [];
60
+ if (unverified.length)
61
+ own.push(`HOLD: do not order this revision. ${unverified.length} of ${lands.length} land patterns are not verified against the makers' recommended land patterns (circuit:check CIRCUIT_FOOTPRINT_UNVERIFIED): ${unverified.join("; ")}.`);
62
+ if (viaCount && policy === "fill_and_cap") own.push(`Vias in pads (${viaCount}): fill with non-conductive epoxy and cap-plate them flat (IPC-4761 type VII), so they do not wick solder from the joints.`);
63
+ const leadless = [...new Set(netlist.components.filter((x) => x.package?.name && LEADLESS.test(x.package.name)).map((x) => `${x.instance} (${x.package!.name})`))];
64
+ const pnpFile = join(p.root, b.out, "pnp.csv");
65
+ const sides: Record<string, number> = {};
66
+ let placed = 0;
67
+ if (existsSync(pnpFile)) {
68
+ const [head = [], ...rows] = parseCsv(readFileSync(pnpFile, "utf8")).rows;
69
+ const k = head.indexOf("side");
70
+ for (const r of rows.filter((r) => r.length > 1)) {
71
+ const side = (k >= 0 && r[k]) || "top";
72
+ sides[side] = (sides[side] ?? 0) + 1;
73
+ placed++;
74
+ }
75
+ }
76
+ const gdir = join(p.root, b.out, "gerber");
77
+ const g = existsSync(gdir) ? readdirSync(gdir).sort() : [];
78
+ return {
79
+ ownNotes: own.length,
80
+ layers: c.layers ?? 2,
81
+ thickness_mm: c.thickness_mm ?? 1.6,
82
+ copper: c.copper_oz ? `${c.copper_oz} oz outer layers` : "1 oz outer layers (default: the fab's standard)",
83
+ finish: c.finish ?? (leadless.length ? "ENIG" : "lead-free HASL"),
84
+ finishWhy: c.finish ? "circuit.config.ts" : leadless.length ? `default: leadless packages want a flat finish (${leadless.length})` : "default: no leadless packages",
85
+ mask: c.solder_mask ?? "green (default)",
86
+ silk: c.silkscreen ?? "white (default)",
87
+ assembly: c.assembly ?? "fab",
88
+ notes: [...own, ...(c.fab_notes ?? [])],
89
+ lands: { total: lands.length, unverified },
90
+ viaInPad: { count: viaCount, policy },
91
+ leadless,
92
+ sides,
93
+ placed,
94
+ gerbers: g.filter((f) => !/\.(drl|xln)$/i.test(f)),
95
+ drills: g.filter((f) => /\.(drl|xln)$/i.test(f)),
96
+ };
97
+ }
98
+
99
+ function stackup(layers: number, t: number): { layer: string; what: string }[] {
100
+ if (layers === 4)
101
+ return [
102
+ { layer: "Top copper", what: "signal, components" },
103
+ { layer: "Prepreg", what: "" },
104
+ { layer: "Inner 1", what: "copper" },
105
+ { layer: "Core", what: "FR-4" },
106
+ { layer: "Inner 2", what: "copper" },
107
+ { layer: "Prepreg", what: "" },
108
+ { layer: "Bottom copper", what: "signal" },
109
+ ].map((x) => ({ ...x, what: x.what || `the fab's standard 4-layer build for ${t} mm` }));
110
+ return [
111
+ { layer: "Top copper", what: "signal, components" },
112
+ { layer: "Core", what: `FR-4, ${t} mm finished board` },
113
+ { layer: "Bottom copper", what: "signal" },
114
+ ];
115
+ }
116
+
117
+ export const workOrder: SectionFn = ({ project: p, unit, release }: SectionContext) => {
118
+ const boards = providerModules(p, unit, PROVIDER);
119
+ if (!boards.length) return undefined;
120
+ const builds = unit.perProduct * release.build;
121
+ const blocks: Block[] = [];
122
+ for (const b of boards) {
123
+ const def = p.definitions.get(b.module!)!.def;
124
+ const f = fabProfile(p, b);
125
+ const qty = perBuild(unit, def.id) * builds;
126
+ const sides = Object.entries(f.sides).map(([s, k]) => `${k} ${s}`).join(", ");
127
+ blocks.push({ kind: "heading", text: def.name ?? def.id });
128
+ blocks.push({
129
+ kind: "keyvalues",
130
+ rows: [
131
+ { label: "Quantity", value: `${qty} board${qty === 1 ? "" : "s"}`, note: "for every build of this unit; fabs often have a minimum panel, order it" },
132
+ { label: "Layers", value: `${f.layers} copper`, note: b.config },
133
+ { label: "Thickness", value: `${f.thickness_mm} mm` },
134
+ { label: "Copper", value: f.copper },
135
+ { label: "Finish", value: f.finish, note: f.finishWhy },
136
+ { label: "Solder mask / silkscreen", value: `${f.mask} / ${f.silk}` },
137
+ { label: "Assembly", value: f.assembly === "fab" ? `by the fab: ${f.placed} placements${sides ? ` (${sides})` : ""}` : `by hand: ${f.placed} placements${sides ? ` (${sides})` : ""}`, note: Object.keys(f.sides).length > 1 ? "double-sided: two reflow passes" : "single-sided: one reflow pass" },
138
+ { label: "Land patterns", value: f.lands.unverified.length ? `${f.lands.unverified.length} of ${f.lands.total} not verified` : `${f.lands.total} of ${f.lands.total} verified`, note: "against the makers' recommended land patterns (pads, pitch, holes and slots, courtyard, paste, mask)" },
139
+ { label: "Vias in pads", value: f.viaInPad.count ? `${f.viaInPad.count}, ${f.viaInPad.policy === "fill_and_cap" ? "filled and capped (IPC-4761 type VII)" : "not allowed: check reports them"}` : "none" },
140
+ ...(f.leadless.length ? [{ label: "Leadless packages", value: f.leadless.join(", "), note: "stencil paste; X-ray or careful inspection of the hidden joints" }] : []),
141
+ { label: "Files", value: `files/boards/${def.id}/gerber/ (${f.gerbers.length} layers, ${f.drills.length} drill), bom.csv, pnp.csv, board.step` },
142
+ ],
143
+ });
144
+ if (f.notes.length) blocks.push({ kind: "table", columns: [{ key: "n", label: "No.", align: "right" }, { key: "note", label: "Fabrication notes" }], rows: f.notes.map((note, i) => ({ n: i + 1, note })), caption: `${def.name ?? def.id}: fabrication notes`, note: f.ownNotes ? `${f.ownNotes === f.notes.length ? "the provider's own" : `the provider's own (${f.ownNotes}), then ${b.config}: fab_notes`}` : `${b.config}: fab_notes` });
145
+ blocks.push({ kind: "table", columns: [{ key: "layer", label: "Stack-up" }, { key: "what", label: "" }], rows: stackup(f.layers, f.thickness_mm), caption: `${def.name ?? def.id}: stack-up` });
146
+ blocks.push({
147
+ kind: "list",
148
+ items: [
149
+ "Send the fab the Gerber and drill files with the values above and the fabrication notes; for assembly also bom.csv and pnp.csv (rows keyed by the model's instance paths; MPNs from the parts).",
150
+ "Ask for the fab's DFM report before production and keep it with this release.",
151
+ ...(f.assembly === "hand" ? ["Hand assembly: stencil and solder paste, place from pnp.csv, reflow; check each leadless part's joints."] : []),
152
+ ],
153
+ });
154
+ }
155
+ return { title: "Boards", order: 20, blocks };
156
+ };
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@protoboardai/ext-tscircuit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Protoboard extension: circuit boards from your design, with tscircuit and KiCad export.",
5
+ "license": "Apache-2.0",
6
+ "author": "Delta Robotics, Inc.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Delta-Robotics-Inc/protoboard-stack.git",
10
+ "directory": "packages/ext-tscircuit"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "protoboard-extension.json",
15
+ "ops",
16
+ "lib",
17
+ "skills",
18
+ "README.md",
19
+ "evals",
20
+ "NOTICE",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@resvg/resvg-js": "2.6.2",
31
+ "@tscircuit/alphabet": "0.0.25",
32
+ "@tscircuit/capacity-autorouter": "0.0.953",
33
+ "@tscircuit/checks": "0.0.233",
34
+ "@tscircuit/circuit-json-util": "0.0.118",
35
+ "@tscircuit/core": "0.0.2056",
36
+ "@tscircuit/create-fdm-enclosure": "0.0.4",
37
+ "@tscircuit/footprinter": "0.0.430",
38
+ "@tscircuit/image-utils": "0.0.8",
39
+ "@tscircuit/infgrid-ijump-astar": "0.0.35",
40
+ "@tscircuit/matchpack": "0.0.97",
41
+ "@tscircuit/math-utils": "0.0.36",
42
+ "@tscircuit/props": "0.0.677",
43
+ "@tscircuit/schematic-match-adapt": "0.0.18",
44
+ "bpc-graph": "0.0.57",
45
+ "circuit-json": "0.0.510",
46
+ "circuit-json-to-bpc": "0.0.13",
47
+ "circuit-json-to-connectivity-map": "0.0.33",
48
+ "circuit-json-to-gerber": "0.0.111",
49
+ "circuit-json-to-gltf": "0.0.141",
50
+ "circuit-json-to-kicad": "0.0.237",
51
+ "circuit-json-to-spice": "0.0.45",
52
+ "circuit-json-to-step": "0.0.43",
53
+ "circuit-to-svg": "0.0.438",
54
+ "kicadts": "0.0.58",
55
+ "polygon-clipping": "0.15.7",
56
+ "react": "19.3.0",
57
+ "schematic-symbols": "0.0.248",
58
+ "spicets": "0.0.4",
59
+ "sucrase": "3.35.1",
60
+ "three": "0.179.1"
61
+ },
62
+ "peerDependencies": {
63
+ "@deltarobotics/uhd": ">=0.3.0",
64
+ "@protoboardai/core": "^0.2.0",
65
+ "@protoboardai/ext-release": "^0.2.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "@protoboardai/ext-release": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "keywords": [
73
+ "protoboard",
74
+ "hardware",
75
+ "pcb",
76
+ "tscircuit",
77
+ "kicad"
78
+ ],
79
+ "homepage": "https://protoboard.ai",
80
+ "bugs": {
81
+ "url": "https://github.com/Delta-Robotics-Inc/protoboard-stack/issues"
82
+ }
83
+ }
@@ -0,0 +1,313 @@
1
+ {
2
+ "schema": "protoboard.extension/v0",
3
+ "id": "@protoboardai/ext-tscircuit",
4
+ "version": "0.2.1",
5
+ "description": "circuit provider: PCBs from UHD board modules with tscircuit, run in-process under Node and offline (generate, export, extract, check).",
6
+ "compatibility": {
7
+ "protoboard": "^0.2.0",
8
+ "uhd": ">=0.3.0"
9
+ },
10
+ "tools": [
11
+ {
12
+ "id": "tscircuit-node",
13
+ "purpose": "tscircuit libraries (@tscircuit/core 0.0.2056 and its converters, exact versions) on Node 22+, run in Protoboard's process; no tsci, no Bun, no account",
14
+ "probe": [
15
+ "node",
16
+ "-e",
17
+ "require(require.resolve('@protoboardai/ext-tscircuit/lib/probe.cjs', { paths: [process.cwd()] }))"
18
+ ],
19
+ "install": "npm install @protoboardai/ext-tscircuit (its package.json pins every tscircuit package)"
20
+ },
21
+ {
22
+ "id": "kicad-cli",
23
+ "purpose": "KiCad 10's command line: saves the exported KiCad project in KiCad 10's format, and makes a locked board's fabrication outputs (DRC, Gerber, drill, position, STEP, GLB)",
24
+ "probe": [
25
+ "node",
26
+ "-e",
27
+ "require(require.resolve('@protoboardai/ext-tscircuit/lib/kicad/probe.cjs', { paths: [process.cwd()] }))"
28
+ ],
29
+ "install": "install KiCad 10 (kicad.org/download); on Linux the distribution's kicad package, on macOS the KiCad app (its kicad-cli is in KiCad.app/Contents/MacOS; set PROTOBOARD_KICAD_CLI to it)"
30
+ }
31
+ ],
32
+ "skills": [
33
+ {
34
+ "id": "circuit-board",
35
+ "summary": "Design a custom PCB through the circuit capability: the board as a UHD module, circuit.config.ts, circuit:generate/export/extract/check, and what each CIRCUIT_* finding means",
36
+ "path": "skills/circuit-board/SKILL.md"
37
+ },
38
+ {
39
+ "id": "kicad-layout-review",
40
+ "summary": "Review and finish a locked board's layout in KiCad 10: what to check, DRC/ERC reports, Update PCB from Schematic keeping placement, and acting on CIRCUIT_KICAD_* findings",
41
+ "path": "skills/kicad-layout-review/SKILL.md"
42
+ },
43
+ {
44
+ "id": "kicad-cli",
45
+ "summary": "kicad-cli exports and checks for a KiCad 10 project (DRC, ERC, Gerber, drill, position, STEP/GLB, SVG/PDF, netlist, BOM, upgrade), and the fallback before lock",
46
+ "path": "skills/kicad-cli/SKILL.md"
47
+ }
48
+ ],
49
+ "capabilities": [
50
+ {
51
+ "id": "circuit",
52
+ "summary": "PCBs from UHD boards with tscircuit: generated source, local autorouting, Gerber/BOM/PnP/GLB/STEP/manifest/netlist and SVG renders, DRC and netlist drift",
53
+ "native": "tscircuit",
54
+ "operations": {
55
+ "generate": {
56
+ "entry": "./ops/circuit.ts#generate",
57
+ "requires": [
58
+ "tscircuit-node"
59
+ ],
60
+ "summary": "Write tscircuit/board.gen.tsx from the UHD board and circuit.config.ts, then build and route it locally (circuit.json)",
61
+ "inputs": {
62
+ "force": {
63
+ "description": "true: generate a locked board anyway (rebuilds tscircuit's copy; KiCad stays the layout of record)"
64
+ }
65
+ }
66
+ },
67
+ "export": {
68
+ "entry": "./ops/circuit.ts#exportOutputs",
69
+ "requires": [
70
+ "tscircuit-node"
71
+ ],
72
+ "summary": "Write the standard outputs to out/ (rebuilding first if the native source changed); a locked board's come from KiCad (kicad-cli)",
73
+ "inputs": {
74
+ "force": {
75
+ "description": "true: a locked board's export goes on despite KiCad DRC errors"
76
+ }
77
+ }
78
+ },
79
+ "extract": {
80
+ "entry": "./ops/circuit.ts#extract",
81
+ "requires": [
82
+ "tscircuit-node"
83
+ ],
84
+ "summary": "Frames of the board's exports and interfaces, placements and outline, artifact references"
85
+ },
86
+ "check": {
87
+ "entry": "./ops/circuit.ts#check",
88
+ "requires": [
89
+ "tscircuit-node"
90
+ ],
91
+ "summary": "tscircuit DRC, netlist drift against the UHD board, unrouted nets, outline, keep-outs and fixed placements; on a locked board, the model, the handoff record and the KiCad files compared"
92
+ },
93
+ "kicad": {
94
+ "entry": "./ops/kicad.ts#kicad",
95
+ "effect": "export",
96
+ "requires": [
97
+ "tscircuit-node",
98
+ "kicad-cli"
99
+ ],
100
+ "summary": "A KiCad 10 project of the current build in tscircuit/kicad/ (a preview; tscircuit stays the layout of record)"
101
+ },
102
+ "lock": {
103
+ "entry": "./ops/kicad.ts#lock",
104
+ "effect": "export",
105
+ "requires": [
106
+ "tscircuit-node",
107
+ "kicad-cli"
108
+ ],
109
+ "summary": "Hand the board to KiCad: the KiCad 10 project into <board>/kicad/ with handoff.json; KiCad is then the layout of record (a human decision: agents ask first)",
110
+ "inputs": {
111
+ "refresh": {
112
+ "description": "true on a locked board: re-record the handoff from the model and the KiCad files as they are now, without exporting"
113
+ }
114
+ }
115
+ },
116
+ "unlock": {
117
+ "entry": "./ops/kicad.ts#unlock",
118
+ "effect": "export",
119
+ "requires": [
120
+ "tscircuit-node"
121
+ ],
122
+ "summary": "Return a locked board to tscircuit: kicad/ moves to kicad-archive/<stamp>/ (a human decision: agents ask first)"
123
+ }
124
+ },
125
+ "constraints": {
126
+ "layers": {
127
+ "description": "copper layers: 2 or 4 (default 2)"
128
+ },
129
+ "thickness_mm": {
130
+ "description": "board thickness (default 1.6)"
131
+ },
132
+ "outline": {
133
+ "description": "{ circle: { diameter_mm } } | { rect: { width_mm, height_mm } } | { polygon: [[x, y], …] } (mm, board frame); default: the board's design_envelope"
134
+ },
135
+ "mounting_holes": {
136
+ "description": "[{ id, at: [x, y], diameter_mm }]"
137
+ },
138
+ "placements": {
139
+ "description": "{ <instance>: { at: [x, y], rotation_deg, side } }: positions the enclosure fixes (connectors, buttons, displays, sensor windows)"
140
+ },
141
+ "keepouts": {
142
+ "description": "[{ name, rect: { at, size } | circle: { at, diameter_mm }, layers?, reason?, allow_bodies? }] copper keep-outs (an antenna clearance); allow_bodies names parts whose body may lie over it (the module whose antenna it clears)"
143
+ },
144
+ "pours": {
145
+ "description": "[{ net, layers: (top|bottom|inner1|inner2)[], clearance_mm?, reason? }] copper pours (a ground plane), filled after routing around other nets' copper and outside the keep-outs"
146
+ },
147
+ "rules": {
148
+ "description": "{ trace_width_mm, power_trace_width_mm, via_drill_mm, via_pad_mm, edge_clearance_mm, hole_clearance_mm? }; hole_clearance_mm: least copper-to-hole distance for non-plated holes (check tests round holes as circles)"
149
+ },
150
+ "autorouter": {
151
+ "description": "local tscircuit autorouter preset: auto_local (default) or sequential_trace; cloud routing is refused"
152
+ },
153
+ "copper_oz": {
154
+ "description": "outer copper weight for the fabrication order (default: the fab's standard, 1 oz)"
155
+ },
156
+ "finish": {
157
+ "description": "surface finish for the fabrication order, e.g. ENIG or lead-free HASL (default: ENIG when a part is leadless, else lead-free HASL)"
158
+ },
159
+ "solder_mask": {
160
+ "description": "solder mask colour for the fabrication order (default green)"
161
+ },
162
+ "silkscreen": {
163
+ "description": "silkscreen colour for the fabrication order (default white)"
164
+ },
165
+ "assembly": {
166
+ "description": "fab (default: the fab places from pnp.csv) or hand"
167
+ },
168
+ "fab_notes": {
169
+ "description": "[string] notes the fabrication order gives the fab, numbered (e.g. fill and cap vias in pads, IPC-4761 type VII; a hold before ordering)"
170
+ },
171
+ "via_in_pad": {
172
+ "description": "vias inside SMD pads: avoid (default; check reports any as an error) or fill_and_cap (allowed; the fabrication order asks for filled and capped vias, IPC-4761 type VII)"
173
+ },
174
+ "via_touching_pad": {
175
+ "description": "vias whose ring touches or overlaps an SMD pad without sitting in it: avoid (check reports each as an error) or allow (default)"
176
+ },
177
+ "via_under_body": {
178
+ "description": "vias under a component's body (inside its courtyard): avoid (check reports each as an error) or allow (default)"
179
+ },
180
+ "routing": {
181
+ "description": "on (default: the local autorouter routes every net) or off (placement only: no autorouting; check reports unrouted nets as info)"
182
+ },
183
+ "trace_widths": {
184
+ "description": "{ nets?: { <UHD net id>: mm }, classes?: { signal?, power?, ground?: mm } } trace width per net, else per net class, else the rules' widths; check reports a net routed narrower"
185
+ }
186
+ },
187
+ "artifacts": [
188
+ {
189
+ "id": "board_glb",
190
+ "name": "Board (GLB)",
191
+ "type": "3d_model",
192
+ "role": "body",
193
+ "format": "glb",
194
+ "units": "m",
195
+ "path": "{out}/board.glb",
196
+ "generatedFrom": "board_source"
197
+ },
198
+ {
199
+ "id": "board_step",
200
+ "name": "Board (STEP)",
201
+ "type": "3d_model",
202
+ "role": "body",
203
+ "format": "step",
204
+ "units": "mm",
205
+ "path": "{out}/board.step",
206
+ "generatedFrom": "board_source"
207
+ },
208
+ {
209
+ "id": "board_manifest",
210
+ "name": "Board geometry manifest",
211
+ "type": "cad",
212
+ "role": "source",
213
+ "format": "json",
214
+ "path": "{out}/board.manifest.json",
215
+ "generatedFrom": "board_glb"
216
+ },
217
+ {
218
+ "id": "board_gerber",
219
+ "name": "Gerber and drill files",
220
+ "type": "pcb",
221
+ "role": "drawing",
222
+ "format": "gerber",
223
+ "path": "{out}/gerber",
224
+ "generatedFrom": "board_source"
225
+ },
226
+ {
227
+ "id": "board_bom",
228
+ "name": "Bill of materials",
229
+ "type": "documentation",
230
+ "format": "csv",
231
+ "path": "{out}/bom.csv"
232
+ },
233
+ {
234
+ "id": "board_pnp",
235
+ "name": "Pick-and-place",
236
+ "type": "pcb",
237
+ "format": "csv",
238
+ "path": "{out}/pnp.csv"
239
+ },
240
+ {
241
+ "id": "board_netlist",
242
+ "name": "Netlist as laid out",
243
+ "type": "pcb",
244
+ "format": "json",
245
+ "path": "{out}/netlist.json"
246
+ },
247
+ {
248
+ "id": "board_schematic",
249
+ "name": "Schematic",
250
+ "type": "schematic",
251
+ "role": "drawing",
252
+ "format": "svg",
253
+ "path": "{out}/schematic.svg"
254
+ },
255
+ {
256
+ "id": "board_pcb_top",
257
+ "name": "PCB, top",
258
+ "type": "pcb",
259
+ "role": "drawing",
260
+ "format": "svg",
261
+ "path": "{out}/pcb-top.svg"
262
+ },
263
+ {
264
+ "id": "board_pcb_bottom",
265
+ "name": "PCB, bottom",
266
+ "type": "pcb",
267
+ "role": "drawing",
268
+ "format": "svg",
269
+ "path": "{out}/pcb-bottom.svg"
270
+ },
271
+ {
272
+ "id": "board_source",
273
+ "name": "Board design (tscircuit)",
274
+ "type": "pcb",
275
+ "role": "source",
276
+ "format": "tscircuit",
277
+ "path": "{native}/board.tsx"
278
+ },
279
+ {
280
+ "id": "board_routed",
281
+ "name": "Routed board (Circuit JSON)",
282
+ "type": "pcb",
283
+ "format": "json",
284
+ "path": "{native}/circuit.json",
285
+ "generatedFrom": "board_source"
286
+ },
287
+ {
288
+ "id": "board_build",
289
+ "name": "Build record (tscircuit names and UHD instances)",
290
+ "type": "pcb",
291
+ "format": "json",
292
+ "path": "{native}/build.json"
293
+ },
294
+ {
295
+ "id": "board_kicad",
296
+ "name": "KiCad project (the layout of record while locked)",
297
+ "type": "pcb",
298
+ "role": "source",
299
+ "format": "kicad",
300
+ "path": "{dir}/kicad/{module}.kicad_pro"
301
+ }
302
+ ]
303
+ }
304
+ ],
305
+ "operations": [
306
+ {
307
+ "id": "package:work-order",
308
+ "summary": "Manufacturing Package: each board's fabrication and assembly order (layers, thickness, copper, finish, mask, stack-up, assembly sides, leadless parts, files)",
309
+ "entry": "./ops/package.ts#workOrder",
310
+ "effect": "pure"
311
+ }
312
+ ]
313
+ }