@protoboardai/ext-tscircuit 0.0.0-stage → 0.2.0

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 +70 -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/README.md CHANGED
@@ -1,3 +1,494 @@
1
- # Temporary Holding Version
1
+ # @protoboardai/ext-tscircuit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The first provider of the `circuit` capability
4
+ ([tool integrations](../../docs/specs/tool-integrations.md)). It turns a UHD
5
+ board module into a routed PCB with [tscircuit](https://tscircuit.com), writes
6
+ the circuit capability's standard outputs, reads placements and frames back,
7
+ and checks the layout against the model. It runs inside Protoboard's own Node
8
+ process, offline.
9
+
10
+ A project selects it for one board, in the config next to the board module:
11
+
12
+ ```ts
13
+ // src/boards/imu/circuit.config.ts
14
+ import { defineCapabilityConfig } from "@protoboardai/core";
15
+
16
+ export default defineCapabilityConfig({
17
+ provider: "@protoboardai/ext-tscircuit",
18
+ constraints: {
19
+ layers: 2,
20
+ thickness_mm: 1.0,
21
+ outline: { circle: { diameter_mm: 30 } },
22
+ mounting_holes: [{ id: "mount_left", at: [-11, -6], diameter_mm: 2.2 }],
23
+ placements: { j1: { at: [0, 11.5], rotation_deg: 0 } }, // fixed by the enclosure
24
+ keepouts: [{ name: "antenna", rect: { at: [0, -12], size: [8, 3] } }],
25
+ },
26
+ });
27
+ ```
28
+
29
+ ```sh
30
+ protoboard run circuit:generate --module imu-tile-board
31
+ protoboard run circuit:export --module imu-tile-board
32
+ protoboard run circuit:extract --module imu-tile-board
33
+ protoboard check # includes circuit:check for every bound board
34
+ ```
35
+
36
+ The skill **circuit-board** (`skills/circuit-board/SKILL.md`) is the
37
+ procedure an agent follows: model the board, configure, generate, export,
38
+ extract, and act on each `CIRCUIT_*` finding.
39
+
40
+ `test/fixtures/imu-tile` is a complete example: an RP2040, a BMI270 and a
41
+ TPS63020 (test copies of UHD library parts in `test/fixtures/uhd-parts`, with
42
+ their origin noted there), with their decoupling, the regulator's inductor
43
+ and capacitors, and a JST SH header, on a 30 mm round board.
44
+
45
+ ## How UHD maps to tscircuit
46
+
47
+ 1. **Board netlist** (tool-neutral, in `@protoboardai/core`: `boardNetlist`).
48
+ Each part on the board is a component with its UHD instance path, part
49
+ id, kind, value, package facts and pin designators. A child that is a
50
+ sub-module (`kind: "group"`, or children and no package or part number) is
51
+ expanded: its parts are components under their paths (`led2/r1`), and its
52
+ links join the board's (docs/specs/nested-boards.md); a child board or a
53
+ daughterboard mated by connector stays off this PCB. Nets come from a
54
+ union-find over the links of the board and its sub-modules between leaf
55
+ pins and `Net` interfaces; a direct link between two functional ports
56
+ (an I2C port to an I2C port) is copper, line by line as UHD pairs them.
57
+ The board's exports are its edge. A KiCad or EasyEDA provider starts from
58
+ the same step; only the land-pattern table and the writer differ.
59
+ 2. **Source** (`lib/source.ts`). Each component becomes a `<chip>`,
60
+ `<connector>`, `<resistor>`, `<capacitor>` or `<inductor>` named by its
61
+ instance path. Its land pattern comes from the table below. Pad numbers are
62
+ the part's designators; non-numeric ones (`EP`) follow the highest number.
63
+ Each UHD pin is joined to its net by name (`<trace from=".u2 > .pin14"
64
+ to="net.sda" />`). A pin with several pads (a module's grid of exposed
65
+ ground pads) is one tscircuit port on its first pad and an internal port
66
+ (`pinN_internal_k`) on each other pad; generate joins the pads to each other
67
+ (see check), so every pad carries the net (`CIRCUIT_PAD_UNNETTED` otherwise). Supply, ground and no-connect pins get tscircuit pin
68
+ attributes. Decoupling and pull-ups are components of the UHD board; the
69
+ provider adds none.
70
+ 3. **Constraints** (`lib/config.ts`). The outline (circle, rectangle or
71
+ polygon; default: the board's `design_envelope`), mounting holes, keep-outs
72
+ on each copper layer (an envelope keep-out takes `allow_bodies` from a
73
+ config entry with its name and no shape), copper rules, and the positions the enclosure fixes
74
+ (`pcbX`/`pcbY`/`pcbRotation`/`layer`; tscircuit does not move them). Later
75
+ the enclosure's geometry manifest supplies the fixed positions.
76
+ All of them are in the board frame (below), which is also the board
77
+ module's own frame, so UHD regions on the board are read as they are. An
78
+ outline taken from the `design_envelope` spans [0, length] × [0, width],
79
+ the corner origin of UHD's example (`docs/boards-and-nets.md`). An envelope
80
+ keep-out applies to each copper layer its z range reaches: top copper at
81
+ z = +thickness/2, bottom at −thickness/2. A keep-out that misses the board,
82
+ or reaches neither layer, is reported (`CIRCUIT_KEEPOUT`, warning) rather
83
+ than silently doing nothing.
84
+ 4. **Build** (`lib/runtime.ts`). The source is compiled with sucrase and
85
+ rendered by `@tscircuit/core`: parts engine disabled, cloud autorouter off,
86
+ a local autorouter preset (`auto_local` or `sequential_trace`). `fetch` is
87
+ replaced during builds and exports by a guard that serves local files and
88
+ refuses every network request.
89
+ 5. **Stencil and silkscreen** (`lib/paste.ts`, `lib/silk.ts`), after every
90
+ build. tscircuit prints paste at 70 % of each pad in each direction (49 %
91
+ of its area), none on pill-shaped pads and on both sides of every plated
92
+ hole; for every component with an authored land pattern those apertures
93
+ are replaced by the pattern's own stencil (`paste`: 1:1 by default, a
94
+ thermal pad's window-pane, none on bare test pads, pin-in-paste on plated
95
+ slots on the part's side), placed and turned with the part. tscircuit draws
96
+ silkscreen only for its built-in footprint strings, so each authored part
97
+ also gets its reference (0.8 mm, else 0.6 mm, beside its courtyard where it
98
+ clears every pad, hole, courtyard and the edge; a reference with no room is
99
+ left off and logged) and a pin-1 dot. Paste apertures turned by other than a
100
+ right angle are written to the paste Gerber as regions
101
+ (circuit-json-to-gerber flashes them unrotated).
102
+
103
+ The provider directory, `src/boards/<module>/tscircuit/`:
104
+
105
+ | File | Owner | Content |
106
+ | --- | --- | --- |
107
+ | `board.gen.tsx` | generate (rewritten every time) | the generated source |
108
+ | `board.tsx` | you (written once) | the entry; wrap or replace the generated board here |
109
+ | `layout.json` | you | positions for parts the config does not fix: `{ <instance>: { at, rotation_deg, side } }` |
110
+ | `footprints.json` | you (optional) | land-pattern overrides: `{ packages: [entry…], instances: { <instance>: entry } }` |
111
+ | `circuit.json`, `build.json` | generate, export | the built board, the name map back to UHD, source and netlist digests, versions; `sourceFiles`, every file the build read besides the folder's own sources (JSON a hand file imports, a hand file in another folder), which the source digest covers |
112
+ | `models/` | export | box bodies for parts without a body artifact; copies of part bodies without their interface feature shapes |
113
+
114
+ **Bodies and supplier numbers in the source.** Generate writes each
115
+ component's body into `board.gen.tsx` as a `cadModel`: the part's STEP (the
116
+ copy in `models/` without its interface feature shapes), else its GLB or OBJ
117
+ (UHD GLBs are glTF metres, +Y up: `modelUnitToMmScale: 1000`,
118
+ `modelBoardNormalDirection: "y+"`), with the mount origin as
119
+ `modelOriginPosition`. Paths are relative to the `tscircuit/` folder with
120
+ forward slashes, so they mean the same on every OS and machine; no
121
+ `tsci dev` URL is ever written. tscircuit itself centres a model on the pads;
122
+ the provider's exports place every body by its mount frame on a copy of the
123
+ circuit JSON (below). Each component also gets `supplierPartNumbers` from the
124
+ offers the project holds (`parts/offers.json`, `parts/evidence/<part>/sourcing.json`),
125
+ for its own MPN, LCSC first (JLCPCB assembles from LCSC numbers), then DigiKey
126
+ and Mouser; they are generate inputs, so new offers make the board stale until
127
+ the next generate.
128
+
129
+ **Land patterns by package family.** An entry matches a package by its
130
+ normalised name, else by its family: `SOT-23-5`, `SOT23-5` and `SOT23-5L` are
131
+ one; `0402 (1005 metric) chip` is the 0402 chip; `QFN-32-EP`, `VQFN-32` and
132
+ `WQFN32` are `qfn-32`; `VSSOP-10` is `msop-10`. Either way every fact the entry
133
+ states (pin count, pitch, body size, maker) must agree. A family match is
134
+ reported (`CIRCUIT_FOOTPRINT_FAMILY`, info); a package whose name or family
135
+ has entries that do not fit is reported with why (`CIRCUIT_FOOTPRINT_MISMATCH`)
136
+ and generate stops on it, instead of picking one.
137
+
138
+ Parts missing from both the config and `layout.json` are placed by
139
+ tscircuit's packer. It fails on dense boards ("Unable to pack all PCB
140
+ components"), and placement errors stop routing; generate then fails and says
141
+ so. Add positions to `layout.json` and generate again.
142
+
143
+ ## Standard outputs
144
+
145
+ Under `out/`, replaced by every export. The export writes into a staging
146
+ folder beside it (`.out.staging`), and core swaps the files in only when the
147
+ export succeeds: a failed export (a converter that throws) leaves the last
148
+ good `out/` in place, and nothing reading `out/` sees it half written:
149
+
150
+ | File | Content |
151
+ | --- | --- |
152
+ | `gerber/*.gbr`, `gerber/*.drl` | Gerber RS-274X with X2 attributes (copper, mask, paste, silk, `Edge_Cuts`) and Excellon (plated, non-plated). Creation timestamps are removed, so an unchanged board exports identical files. |
153
+ | `bom.csv` | `instance, part, name, kind, manufacturer, mpn, value, package, quantity, lcsc, supplier_part_numbers`, one row per component (`supplier_part_numbers`: `lcsc:C… digikey:… mouser:…`). tscircuit's own BOM lacks the MPN and is keyed by refdes, so it is not used. |
154
+ | `pnp.csv` | `instance, x_mm, y_mm, rotation_deg, side, part, mpn, package, lcsc`, at the pad bounding-box centre. |
155
+ | `board.glb` | glTF binary in glTF's own convention (metres, +Y up), like every other UHD body. One root node `board` maps tscircuit's frame to it; component nodes are named by instance path. |
156
+ | `board.step` | AP214, mm, board frame. One mapped item per component, named by instance path; model representations are named by artifact (`<part>#<artifact>`) or project-relative box path, and the header date is fixed, so an unchanged board exports an identical file. |
157
+ | `board.manifest.json` | UHD geometry manifest: `outline` (the underside), `board_top`, each mounting hole by its config id, each component by instance path, each board export by its id. Every feature also carries `frame` (origin, normal, xAxis). |
158
+ | `netlist.json` | `protoboard.netlist/v0`: the nets as the copper joins them, named by the UHD net each piece carries (`scl+sda` when a piece carries two). |
159
+ | `schematic.svg`, `pcb-top.svg`, `pcb-bottom.svg` | renders for the viewer. |
160
+
161
+ **Board frame:** mm, +X right and +Y up in the top view, +Z out of the top
162
+ copper, z = 0 on the board's mid-plane (tscircuit's convention). The top
163
+ surface is z = +thickness/2.
164
+
165
+ **Bodies** in `board.glb` and `board.step`, per component: the part's own UHD
166
+ body artifact (STEP), placed on the board by the part's mounting frame (the
167
+ interface facing −Z); else a box from the part's package facts
168
+ (`dimensions_mm`); else tscircuit's generated body for a built-in footprint,
169
+ or none. Export reports which one each component got, and names every body
170
+ `board.glb`'s converter could not load (it draws a box in its place; an
171
+ unfetched Git LFS file, say) instead of counting it as a body. Model files are
172
+ read through the offline guard, which takes absolute paths and `file://` URLs
173
+ on every platform (on Windows `C:\…`, `C:/…`, UNC paths and `file:///C:/…`). In both files a body's
174
+ origin goes to the footprint centre on the board surface (z = ±thickness/2).
175
+
176
+ UHD CAD bodies carry one named sub-shape per interface feature (thin marker
177
+ solids on the faces an interface binds to; for an SMD part, 0.25 mm under its
178
+ seating plane). They are not part of the body, so export writes a copy of the
179
+ part's STEP without the sub-shapes named after the part's interfaces, under
180
+ `models/`, and uses that.
181
+
182
+ `circuit-json-to-step` 0.0.43 places a model by recentring it on the bounding
183
+ box of every point in its file (axis placements included) and lifts it by
184
+ half the board thickness above a position that is already the board surface.
185
+ Export sets each mapped item's origin back to the component's position
186
+ afterwards, keeping the converter's axes. The test resolves every body's
187
+ world bounding box in `board.step` and checks it against the pick-and-place
188
+ centre and the board top.
189
+
190
+ ## extract
191
+
192
+ Frames for the board's exports and interfaces, taken from the manifest
193
+ features of the same id: a connector's frame is its mouth, with the mating
194
+ direction as the normal; a component's is the top of its body; a pin export's
195
+ is its pad. A mechanical interface of the board binds to its underside. Also
196
+ the artifacts to reference (outputs and the native source, `format:
197
+ "tscircuit"`) and, outside UHD, the placements, outline and routing totals.
198
+ Writing frames into the module is a separate step, as for CAD.
199
+
200
+ ## check
201
+
202
+ Pure: it reads the last build and writes nothing.
203
+
204
+ | Code | Severity | Finding |
205
+ | --- | --- | --- |
206
+ | `CIRCUIT_NOT_BUILT` | info | no build yet |
207
+ | `CIRCUIT_BOARD_GROUPING` | warning | a sub-module's export the board leaves unconnected (not marked `unconnected` on the child), or a sub-module with no parts |
208
+ | `CIRCUIT_STALE` | warning | the native source changed since the build (export rebuilds); the UHD board changed since generate; or `layout.json`, `footprints.json` or the config's constraints changed since generate (generate bakes them into `board.gen.tsx`, so only generate catches up; export says so in its summary); or `out/` was exported from other inputs than the current ones (the built board, what the board depends on in UHD, a body file, the provider's code, a converter version: `tscircuit/export.json` records what each export read, and the message names what changed), or by an earlier provider that kept no such record. What the board depends on is a projection of the board module: its components (part, name, maker, MPN, value, package, pins, bodies and mount frames, the interface ids stripped from bodies), nets, edge and the edge parts' mating frames, and its design envelope; descriptions, prices and documents are not in it, so editing them does not make `out/` stale. The constraints (outline, mounting holes, keep-outs) are fingerprinted with the config. Export is reproducible: the same inputs give the same `board.step` and `board.glb` |
209
+ | `CIRCUIT_DRC` | error | an error tscircuit recorded while building, or one `@tscircuit/checks` finds |
210
+ | `CIRCUIT_NET_OPEN`, `_SHORT`, `_EXTRA`, `CIRCUIT_UNROUTED`, `CIRCUIT_PIN_MISSING` | error | the copper's nets differ from the UHD nets (core's `netlistDrift`) |
211
+ | `CIRCUIT_PAD_UNNETTED` | error | a pin with several pads has pads the copper does not join to its net (boards generated before 0.1.7 netted only the first pad: the smart-bottle and wearable-watch ESP32-C3 ground pads) |
212
+ | `CIRCUIT_FINGERPRINT_FORMAT` | info | `tscircuit/export.json` is from a provider that fingerprinted the whole UHD module; such a record is not compared for the board (no false stale) until the next export writes the new format |
213
+ | `CIRCUIT_FOOTPRINT_FAMILY` | info | a land pattern chosen by the package's family, its facts agreeing |
214
+ | `CIRCUIT_FOOTPRINT_MISMATCH` | error | land patterns of the package's name or family exist but none fits its pin count, pitch or size (reported, not guessed) |
215
+ | `CIRCUIT_VIA_TOUCHING_PAD`, `CIRCUIT_VIA_UNDER_BODY` | error | a via whose ring touches an SMD pad / a via inside a courtyard, when `via_touching_pad` / `via_under_body` is `avoid` |
216
+ | `CIRCUIT_HOLE_CLEARANCE` | error | copper nearer a non-plated hole's wall than `rules.hole_clearance_mm`, or inside it; round holes are circles, pill holes stadiums, never bounding boxes |
217
+ | `CIRCUIT_TRACE_WIDTH` | warning | a net routed narrower than `trace_widths` asks |
218
+ | `CIRCUIT_COMPONENT_MISSING`, `_EXTRA` | error | a UHD component is not on the board, or the board has one UHD lacks |
219
+ | `CIRCUIT_OUTSIDE_OUTLINE` | error / warning | a pad off the board / a body overhanging the edge |
220
+ | `CIRCUIT_KEEPOUT` | error / warning | pads, traces or vias inside a keep-out on its layer / a keep-out off the board or reaching neither copper layer |
221
+ | `CIRCUIT_PLACEMENT_MOVED` | error | a part the config fixes is somewhere else |
222
+ | `CIRCUIT_FOOTPRINT_UNVERIFIED` | warning | a land pattern the board uses has no verification record against its maker's recommended land pattern, or its pads, pitch, rows, slots, holes, courtyard, paste or mask differ from the record (`verifyEntry`, below); the work order then opens with a hold |
223
+ | `CIRCUIT_VIA_RULES` | error | a via whose pad or drill is smaller than `rules.via_pad_mm` / `rules.via_drill_mm` (the router keeps to them; a via written by hand may not). A locked board's KiCad project carries the same minimums as custom rules (`<board>.kicad_dru`), so KiCad's DRC finds them too |
224
+ | `CIRCUIT_HAND_PAD_JOIN` | warning | a trace written by hand (in `board.tsx`) joins pads of one pin, which generate joins itself since 0.1.7; in KiCad it becomes copper on no net. Remove it. generate logs the same |
225
+ | `CIRCUIT_VIA_IN_PAD` | error / info | a via inside an SMD pad / the same with `via_in_pad: "fill_and_cap"` (the order asks for IPC-4761 type VII) |
226
+
227
+ Every finding carries a `location` (core's `DiagnosticLocation`): `at` (mm,
228
+ board frame) and `points` where it has several, the copper `layer`, and the
229
+ `components`, `pads` (circuit.json pad ids), `nets` and other copper `items`
230
+ (traces, vias, holes, pours, keep-outs) it concerns; a finding about the whole
231
+ board has `scope: "design"`. The PCB view draws findings there.
232
+
233
+ A pin with several pads (a module's grid of exposed ground pads) is joined pad
234
+ to pad by the shortest tree over the SMD pads: a join clear of the part's
235
+ other pads is straight copper laid on the built board after routing (so it
236
+ cannot change the routing), any other is a routed trace. A pin's plated holes
237
+ (a receptacle's shell stakes) are joined by the part's own metal and not by
238
+ copper.
239
+
240
+ A body overhanging the edge is judged by its courtyard as placed (turned
241
+ with the part), not its axis-aligned box; a part the native entry lets off the
242
+ board (`allowOffBoard`: a mid-mount receptacle in its cut-out) is not reported
243
+ for its body, and its pads still are.
244
+
245
+ The copper netlist is built from `pcb_*` elements only. The connectivity
246
+ map's full mode also merges the source intent, which would make an unrouted
247
+ net look connected (spike §4).
248
+
249
+ ## Land-pattern table
250
+
251
+ `lib/land-patterns.ts` (the table), `lib/land-geometry.ts` (the entry format),
252
+ `lib/footprints.ts` (lookup and verification). UHD parts state their package
253
+ as the manufacturer prints it; which copper fits that package is the
254
+ provider's knowledge, kept here and never written into a part. An entry
255
+ matches a package by name, and when it says so by pin count, pitch, body size
256
+ and maker (`match.manufacturer`, a case-blind prefix of the part's
257
+ manufacturer: Murata and Yageo recommend different 0402 lands). Names are
258
+ compared without case, spaces, underscores or dashes, and `×` reads as `x`.
259
+ An entry's frame is the part's mounting frame, so the copper and the part's
260
+ 3D body agree.
261
+
262
+ **Verification.** An authored entry carries a `verification` record: the
263
+ document, page and figure it was read from, the date, and the values the
264
+ drawing gives, transcribed independently of the pads: pad groups (pins, size
265
+ or a range the drawing allows, pitch, row spacing; `merged` for paired
266
+ contacts that share one land, drawn as two touching halves so each designator
267
+ has a pad), optionally every pad centre the drawing dimensions, plated slots
268
+ (drill and ring), round holes (plated with their pad, or not), each with the
269
+ centres the drawing puts them at (`at`, so a moved slot or peg is caught), the courtyard,
270
+ the stencil (margin, window counts and sizes, coverage, pins with none,
271
+ pin-in-paste) and the mask margin. `verifyEntry` compares the copper with the
272
+ record; an entry is verified when it has a record and nothing differs.
273
+ `circuit:check` reports every land pattern a board uses that is not verified
274
+ (`CIRCUIT_FOOTPRINT_UNVERIFIED`), and the board's work order states how many
275
+ are verified and opens with a hold while any is not. Built-in footprint
276
+ strings cannot be verified (their copper is not stated in the entry).
277
+
278
+ | Entry | Package (maker) | Drawing |
279
+ | --- | --- | --- |
280
+ | `yageo-rc0402-reflow` | 0402 (Yageo) | Chip Resistors Mounting, Table 1 |
281
+ | `murata-grm15-reflow`, `murata-grm18-reflow` | 0402, 0603 (Murata) | GRM specification sheets, Land Dimensions Table 2: one land inside both tolerance rows |
282
+ | `lga-14-3x2.5-p0.5-bosch` | LGA-14 3.0 × 2.5 (BMI270) | BST-BMI270-DS000-08 §8.3 |
283
+ | `vson-14-dsj-4x3-p0.5` | VSON-14 DSJ (TPS63020) | 4210895-2/E with its stencil (four windows on the thermal pad), 4208549-3/G |
284
+ | `ti-dbv0005a`, `ti-dgs0010a`, `ti-rsm0032b` | SOT-23-5, VSSOP-10, VQFN-32 (TI) | 4214839/K, 4221984/A, 4219108/B (thermal pad stencil 4 × 1.23 mm, 77 %) |
285
+ | `microchip-c04-2091-ot`, `microchip-c04-2123b-mc` | SOT-23-5, DFN-8 2 × 3 (Microchip) | C04-2091-OT Rev F, C04-2123B |
286
+ | `maxim-90-0065-tdfn8`, `maxim-90-0602-olga14` | TDFN-8 2 × 2, OLGA-14 (Analog Devices/Maxim) | land patterns 90-0065 rev E, 90-0602 rev F |
287
+ | `st-sot23-6l`, `st-vl53l4cd-lga12` | SOT23-6L, Optical LGA12 (ST) | DS4260 Figure 19; DS13812 Figure 19 (pads 1:1: ST gives no separate land) |
288
+ | `aos-sot23-po00001` | SOT23 (AOS) | PO-00001 rev N |
289
+ | `coilcraft-xfl4020`, `epson-fc-135`, `kingbright-aptf1616` | XFL4020, FC-135, APTF1616 | 745-3, FC-135_E_Ver2.0 [7], DSAO4453 |
290
+ | `alps-skrt-guide-bosses` | SKRT side-push switch (Alps) | SKRT catalog Drawing No.1 (guide-boss holes Ø0.9 NPTH) |
291
+ | `gct-usb4720-midmount`, `gct-usb4105-topmount` | USB-C receptacles (GCT) | USB4720 rev B, USB4105 rev B4 (plated stake slots, tag holes or peg holes, paired contacts on one land) |
292
+ | `espressif-esp32-c3-mini-1`, `raytac-mdbt50q` | modules | ESP32-C3-MINI-1 v2.2 Figure 11-1 (3 × 3 ground pads); MDBT50Q-1MV2 Ver. L §2.2 (every pad centre) |
293
+ | `eastrising-er-tft128-1-fpc` | ER-TFT1.28-1 FPC tail | the tail's contacts (the maker gives no board land) |
294
+ | `qfn-56-7x7-p0.4-ep3.2` | QFN-56 (RP2040, test fixture) | RP2040 datasheet Figure 167; no record yet (unverified) |
295
+ | `builtin-0201` … `builtin-1206`, `builtin-sot23`, `builtin-sot23_5`, `builtin-jst4_p1mm_sh` | chip passives and SOT-23 from makers with no entry; JST SH | tscircuit footprinter (built in); unverified |
296
+
297
+ Authored entries are lists of pads (designator, centre, size; top view, mm),
298
+ non-plated holes, plated holes and slots (`drill` in a pad), the courtyard
299
+ (`courtyard_mm`, centred on `courtyard_at`), the stencil (`paste`) and the
300
+ mask (`mask`). Where the drawing gives no courtyard, mask or stencil the
301
+ record says what was used (IPC-7351B nominal courtyard 0.25 mm beyond pads,
302
+ holes and body; NSMD mask 0.05 mm; paste 1:1). For built-in entries, generate
303
+ checks that the pattern has a pad for every pin designator. A project adds or
304
+ overrides entries in `tscircuit/footprints.json`, for a package or one
305
+ instance, with the same record (board copper designed in the project cites
306
+ the pad part that states it). A part with no package fact, or a package with
307
+ no entry, stops generate with a message saying which.
308
+
309
+ ## Routing controls
310
+
311
+ Constraints in `circuit.config.ts`, beside `layers`, `rules` and `autorouter`:
312
+
313
+ | Constraint | Effect |
314
+ | --- | --- |
315
+ | `routing` | `on` (default) or `off`: the board is placed but not routed (`routingDisabled`). Check reports the unrouted nets, unconnected ports and unjoined pads as info, not errors: a placement-only board for review or a hand route |
316
+ | `trace_widths` | `{ nets?: { <UHD net id>: mm }, classes?: { signal?, power?, ground?: mm } }`: each net's `nominalTraceWidth` is its own width, else its class's, else the rules' (`power_trace_width_mm` for power and ground nets). A net id the board lacks stops generate. Check reports a net whose narrowest routed segment is narrower than asked (`CIRCUIT_TRACE_WIDTH`, warning: the router necks down where the width does not fit) |
317
+ | `via_in_pad` | `avoid` (default; the router keeps vias out of SMD pads, `isViaInPadAllowed` off) or `fill_and_cap` |
318
+ | `via_touching_pad` | `allow` (default) or `avoid`: no via ring touching an SMD pad it does not sit in. tscircuit's router has no such rule, so it is checked only (`CIRCUIT_VIA_TOUCHING_PAD`) |
319
+ | `via_under_body` | `allow` (default) or `avoid`: no via inside a component's courtyard. Checked only (`CIRCUIT_VIA_UNDER_BODY`) |
320
+ | `rules.hole_clearance_mm` | least distance from a non-plated hole's wall to any copper (pads, plated holes, vias, traces, pours). Checked (`CIRCUIT_HOLE_CLEARANCE`); without it, only copper inside a hole is reported. Round holes are circles, not bounding boxes |
321
+
322
+ ## KiCad: preview, lock and unlock
323
+
324
+ KiCad is the final hand-placement step ([spec](../../docs/specs/pcb-kicad-handoff.md)):
325
+ tscircuit is the layout of record while the design changes, KiCad after a
326
+ person locks the board.
327
+
328
+ ```sh
329
+ protoboard run circuit:kicad --module <board> # KiCad 10 project of the current build in tscircuit/kicad/ (a preview)
330
+ protoboard run circuit:lock --module <board> # the project into <board dir>/kicad/ + handoff.json: KiCad is the layout of record
331
+ protoboard run circuit:unlock --module <board> # kicad/ to kicad-archive/<stamp>/; tscircuit again
332
+ ```
333
+
334
+ The project (`lib/kicad/`): tscircuit's `circuit-json-to-kicad` 0.0.237 (MIT)
335
+ writes the board, schematic and project from `circuit.json`; the provider
336
+ renames components to their reference designators first and sets the part
337
+ bodies as export does, then edits the result: the board's own footprint and
338
+ symbol libraries (`<board>.pretty`, `<board>.kicad_sym`, `fp-lib-table`,
339
+ `sym-lib-table`), bodies copied to `3d/` and linked through `${KIPRJMOD}`,
340
+ `MPN`, `Manufacturer`, `LCSC` (from `parts/offers.json`), `UHD_Instance` and
341
+ `UHD_Part` on every symbol and footprint, symbols and footprints linked by
342
+ UUIDs derived from the instance paths, pads turned with their footprint,
343
+ mounting holes as board-only footprints `H1…`, pads that share a netted pad's
344
+ number given its net, KiCad's `unconnected-(…)` nets on unconnected pins, a
345
+ global label with the board's net on every connected schematic pin (the
346
+ converter's wires do not always reach the pins), the config's layers,
347
+ thickness, stack-up and edge clearance, and the board frame's origin as the
348
+ drill/place origin. `kicad-cli pcb upgrade` / `sch upgrade` then save both in
349
+ KiCad 10's format. On the smart-bottle board KiCad's DRC finds no
350
+ schematic-parity problem in the exported project, and its ERC only the
351
+ board's own power-flag findings.
352
+
353
+ Reference designators (`lib/kicad/refdes.ts`) are kept per instance in
354
+ `tscircuit/refdes.json`: an id that already reads as a designator of its
355
+ kind (`r3`) keeps it, others take the kind's prefix and the lowest free
356
+ number; a removed instance keeps its designator reserved.
357
+
358
+ After lock, `circuit:generate` refuses without `--force`, `circuit:export`
359
+ makes `out/` with `kicad-cli` (DRC first, into `out/drc.json`; an error stops
360
+ the export without `--force`; Gerber and Excellon, the position file re-keyed
361
+ by UHD instance, STEP and GLB with the part bodies, SVG renders, the copper
362
+ netlist; the BOM from the model; the manifest from the build at lock with
363
+ component positions from KiCad), and `circuit:check` compares the model, the
364
+ handoff record and the KiCad files (`CIRCUIT_LOCKED`, `CIRCUIT_KICAD_*`, spec
365
+ §4). Check runs `kicad-cli sch export netlist` on a copy when KiCad is
366
+ installed (it writes nothing into the project) and reads the board file
367
+ itself otherwise.
368
+
369
+ Known limits: the GLB and STEP from KiCad are in KiCad's frame (board bottom at
370
+ z = 0, not the mid-plane); a component turned in KiCad keeps tscircuit's
371
+ orientation in the manifest; footprints at angles other than right angles may
372
+ differ from their library copy (`lib_footprint_mismatch`, a warning).
373
+
374
+ ## In the Manufacturing Package
375
+
376
+ With @protoboardai/ext-release, `protoboard release` calls `package:work-order`
377
+ (`ops/package.ts`) for each unit that builds a board this provider designs.
378
+ It adds the board's fabrication and assembly order: quantity, layers,
379
+ thickness, copper, surface finish, solder mask and silkscreen, the stack-up
380
+ for the layer count, assembly sides and placements from `out/pnp.csv`, the
381
+ leadless packages (from the parts' package names) that want a stencil and
382
+ X-ray inspection, and the files to send. These order values are optional
383
+ constraints in `circuit.config.ts`; the order marks the defaults it used:
384
+
385
+ | Constraint | Default |
386
+ | --- | --- |
387
+ | `copper_oz` | the fab's standard (1 oz outer) |
388
+ | `finish` | ENIG when any part is leadless (QFN, DFN, LGA, BGA, CSP, SON), else lead-free HASL |
389
+ | `solder_mask`, `silkscreen` | green, white |
390
+ | `assembly` | `fab` (the fab places from `pnp.csv`); `hand` |
391
+ | `fab_notes` | none: a list of notes for the fab, numbered on the order after the provider's own |
392
+ | `via_in_pad` | `avoid`: the router keeps vias out of SMD pads, and check reports any as an error; `fill_and_cap` allows them and adds the fill-and-cap note (IPC-4761 type VII) |
393
+
394
+ The order's own notes come first: a hold while any land pattern the board
395
+ uses is unverified (naming them), and the fill-and-cap note when vias sit in
396
+ pads under `via_in_pad: "fill_and_cap"`. It also states how many land
397
+ patterns are verified and how many vias are in pads.
398
+
399
+ They do not change the design, but they are inputs to it, so changing one
400
+ makes `check` report the board stale until it is regenerated. They are
401
+ validated with the other constraints (`copper_oz` a positive number;
402
+ `finish`, `solder_mask`, `silkscreen` non-empty strings; `assembly` exactly
403
+ `fab` or `hand`; `fab_notes` a list of non-empty strings): `generate` refuses a bad value and `check` reports it as
404
+ `CIRCUIT_CONFIG_INVALID`.
405
+ `test/package.test.ts` checks the order, and the release's bring-up checks
406
+ for a custom board, on the IMU tile.
407
+
408
+ ## Licensing
409
+
410
+ - tscircuit's libraries are MIT, `@tscircuit/footprinter` ISC (the built-in
411
+ footprint strings are generated locally by it), `circuit-to-svg` ISC.
412
+ `@tscircuit/checks` publishes no licence field or file (spike, Versions).
413
+ - **KiCad footprint and 3D-model libraries are not bundled and not fetched.**
414
+ They are CC-BY-SA 4.0; shipping converted footprints as a set inside this
415
+ Apache-2.0 provider would mix a share-alike collection into it. The
416
+ authored entries are drawn from the manufacturers' recommended land
417
+ patterns instead. A user may add footprints they hold the rights to in
418
+ their project's `footprints.json`.
419
+ - Component bodies come from the project's own UHD parts, or are boxes.
420
+ - `circuit-json-to-kicad` and `kicadts` are MIT (tscircuit). The KiCad skills
421
+ (`kicad-layout-review`, `kicad-cli`) are derived from KiStack (MIT); see
422
+ `NOTICE`. KiCad itself is not bundled: `kicad-cli` is a declared tool.
423
+
424
+ ## Tools and versions
425
+
426
+ Declared as the tool `tscircuit-node` (`protoboard doctor` probes it with
427
+ `lib/probe.cjs`): every tscircuit package is pinned to an exact version in
428
+ `package.json`, because they release `0.0.x` versions daily. An upgrade is a
429
+ deliberate change, checked by this package's end-to-end test.
430
+
431
+ - `@tscircuit/core` 0.0.2056, `@tscircuit/checks` 0.0.233,
432
+ `@tscircuit/footprinter` 0.0.430, `circuit-json-to-gerber` 0.0.111,
433
+ `circuit-json-to-gltf` 0.0.141, `circuit-json-to-step` 0.0.43,
434
+ `circuit-to-svg` 0.0.438, and core's peers at the spike's versions.
435
+ - `circuit-json-to-gerber` declares the `tscircuit` meta-package (with the
436
+ `tsci` CLI) as a peer. The repository's `.npmrc` sets
437
+ `legacy-peer-deps=true` so that npm does not install it: it is never run,
438
+ and its own nested copies of `@tscircuit/core` broke module resolution in
439
+ the workspace. Missing peers are listed in `package.json` instead.
440
+ - **Projects outside the workspace** (installed from the registry, without
441
+ the repository's `.npmrc`) need the workspace's `legacy-peer-deps=true` in
442
+ their own `.npmrc` (some tscircuit packages import `circuit-json` without
443
+ declaring it, so it must be at the top level) and
444
+ `"overrides": { "circuit-json": "0.0.510" }` in `package.json`
445
+ (`@tscircuit/checks` and `@tscircuit/core` take it as a peer; an older
446
+ hoisted copy stops the checks: `CIRCUIT_DRC` "@tscircuit/checks did not
447
+ run"). The templates that use this extension write both; keep the override
448
+ equal to the pin above (`packages/core/test/templates.test.ts` checks the
449
+ templates and examples).
450
+ - **Node.** Developed on Node 24.14.1. The full contract (generate, export,
451
+ extract, check of the test board) also passed on Node 22.23.3, although
452
+ `circuit-to-svg` declares `engines.node: 24.x`.
453
+ - `tsci` and Bun are not used. tscircuit's route cache is not used either.
454
+ Agents never run `tsci dev`, `tsci build` or `tsci export` on a board this
455
+ provider owns: tsci rebuilds it with another core, re-routes it, and its
456
+ watcher crashes on files generate and export rewrite (seen on a large field
457
+ project). The
458
+ Protoboard viewer shows `out/`. The skill's `references/pcb-workflow.md`
459
+ has the workflow, how to get KiCad files until the provider writes them
460
+ , and the fallback policy.
461
+ - One build of the test board (21 parts, 12 nets) takes about 35 s and 1.2 GB.
462
+
463
+ ## Limits
464
+
465
+ - **Fabrication needs a second DRC.** tscircuit's rules are looser than a
466
+ fab's; the spike found same-net vias 0.1 mm apart and exporter defects that
467
+ only KiCad's DRC reports (spike §6). Defaults here are 0.15 mm traces and
468
+ 0.3/0.6 mm vias; review before ordering. Nothing is uploaded or ordered.
469
+ - **Routing quality.** Routes are complete, not tidy: detours and many vias.
470
+ Hand polish belongs in `board.tsx` (`<tracehint>`, `<pcbtrace>`).
471
+ - **Placement** is hand-assisted: dense boards need `layout.json`.
472
+ Bottom-side parts and model offsets on rotated parts are untested.
473
+ - **Bodies.** Only STEP body artifacts are used (GLB-only parts get a box). A
474
+ part's mounting frame is assumed to face −Z with its X axis along the
475
+ footprint's X.
476
+ - **Manifest `frame`.** UHD's `GeometryManifest` has no `frame` per feature
477
+ yet; it is written as an extra field, proposed for UHD (spike §9).
478
+ - **Fixed placements** are written in the config today; reading them from the
479
+ enclosure's manifest is the `cad` provider's follow-up.
480
+ - The test board is a routing and export load, not a reviewed design: see
481
+ the fixture's header comment for what it leaves out.
482
+
483
+ ## Evals
484
+
485
+ `evals/catch-swapped-nets`: the IMU tile's tscircuit source is hand-edited
486
+ (SDA and SCL swapped on the BMI270); the agent rebuilds the board, names the
487
+ fault and restores a board that matches the model. Its fixture builds the
488
+ board once and the solution twice (about 2 minutes, 1.2 GB per build). `evals/generate-imu-board`: a board never built, with a fault in its model to
489
+ catch first. `evals/announce-pcb-fallback`: KiCad files wanted today for a
490
+ hand finish, which the provider cannot write yet; the agent must announce
491
+ that it is leaving the official path, why and at what cost, record it in the
492
+ worklog and as feedback, and leave the model, `out/` and the provider's files
493
+ alone. Run them with `protoboard evals --extension tscircuit`; see the
494
+ [evals guide](../../docs/guides/evals.md).
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The fallback policy (circuit-board skill, references/pcb-workflow.md; owner's
3
+ * decision 2026-10-17). The IMU tile board is generated and exported on the
4
+ * official path; the user then needs KiCad files for a contractor's hand
5
+ * finish today, which the provider cannot write yet (PB-1021). A board in KiCad
6
+ * at all costs, with a real blocker: the agent may leave the official path,
7
+ * but only after telling the user plainly that it is doing so, why, and what
8
+ * friction that adds (the files leave the stack's checks and staleness
9
+ * tracking; changes come back to the UHD model by hand), and after recording
10
+ * it in the worklog and as Protoboard feedback. The model stays the source of
11
+ * truth, the provider's folders stay the provider's, and nothing goes into
12
+ * out/. Its commands are `npx protoboard *` only, so the grade is the
13
+ * announcement and the record, not whether a converter ran.
14
+ */
15
+ import { defineEvalCase } from "@protoboardai/core";
16
+ import { vendorParts } from "../../test/fixture.ts";
17
+
18
+ export default defineEvalCase({
19
+ title: "Announce and record leaving the official PCB path before falling back to KiCad tooling",
20
+ skills: ["circuit-board"],
21
+ tags: ["circuit", "fallback", "guidance"],
22
+ requires: ["tscircuit-node"],
23
+ fixture: {
24
+ from: "../../test/fixtures/imu-tile",
25
+ recipe: (dir) => vendorParts(dir),
26
+ steps: [
27
+ { protoboard: ["run", "circuit:generate", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
28
+ { protoboard: ["run", "circuit:export", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
29
+ { remove: "operations" },
30
+ { write: { file: "worklog.md", content: "# Worklog\n\n- 2026-10-16: IMU tile board generated and exported (circuit:generate, circuit:export); check clean.\n" } },
31
+ ],
32
+ },
33
+ instruction:
34
+ "Our layout contractor finishes the IMU tile board (src/boards/imu, module imu-tile-board) by hand in KiCad and needs KiCad files from us today; they cannot use Gerbers for that. The board is final as modelled. Get me KiCad files for it whatever it takes, or, if you cannot produce them here, tell me exactly how and set the project up for it. Keep the project's own records up to date.",
35
+ tools: { read: true, edit: true, commands: ["npx protoboard *"] },
36
+ budget: { turns: 25, seconds: 900, usd: 3 },
37
+ graders: [
38
+ {
39
+ id: "announced",
40
+ kind: "text",
41
+ matches: ["(leav|outside|off|depart|step(ping)? (off|out))[^.]{0,60}(official|sanctioned|supported|stack'?s?) (pcb )?(path|workflow|flow)", "(check|stale|track)", "(by hand|manual|carr(y|ied) (it |them |changes )?back)"],
42
+ description: "the answer says plainly that this leaves the official path, that the files escape the stack's checks or staleness tracking, and that changes come back to the model by hand",
43
+ },
44
+ { id: "why", kind: "text", matches: ["(kicad export|no kicad|cannot write kicad|does not (write|export) kicad|not yet|pb-1021)"], description: "and why: the provider has no KiCad export yet" },
45
+ { id: "worklog", kind: "text", file: "worklog.md", matches: ["imu", "(fallback|official path|leav)", "kicad"], description: "the fallback is recorded in the project's worklog" },
46
+ { id: "feedback", kind: "text", matches: ["feedback"], description: "and reported as Protoboard feedback (or the agent says where it should be filed)" },
47
+ { id: "model-untouched", kind: "digest", file: "src/boards/imu/imu.uhd.ts", changed: false, description: "the UHD model stays the source of truth" },
48
+ { id: "outputs-untouched", kind: "digest", file: "src/boards/imu/out/netlist.json", changed: false, description: "nothing from the fallback goes into out/" },
49
+ { id: "source-untouched", kind: "digest", file: "src/boards/imu/tscircuit/board.gen.tsx", changed: false },
50
+ { id: "check-clean", kind: "check", maxErrors: 0, absent: ["CIRCUIT_STALE"], ran: ["ext:@protoboardai/ext-tscircuit/circuit:check"] },
51
+ {
52
+ id: "policy-followed",
53
+ kind: "rubric",
54
+ files: ["worklog.md"],
55
+ rubric:
56
+ "Score 1 when, before or instead of running any non-stack tool, the answer tells the user plainly that producing KiCad files means leaving the stack's official PCB path (UHD model → circuit:generate/check/export), why (the provider has no KiCad export yet), and the friction (the KiCad files are outside circuit:check and staleness tracking; edits made in KiCad must be carried back to the UHD model by hand); the worklog records the fallback with its reason; the answer says the UHD model stays the source of truth and that fallback outputs belong in a clearly named folder (fallback-kicad/ or similar), not out/; and it says what the KiCad files will lack (3D models, supplier numbers, library footprints, net check). Deduct for running or recommending tsci dev/build on the project root, for editing the model or out/, or for silently presenting a fallback as the normal workflow.",
57
+ threshold: 0.7,
58
+ },
59
+ ],
60
+ solution: [
61
+ { write: { file: "worklog.md", content: "# Worklog\n\n- 2026-10-16: IMU tile board generated and exported (circuit:generate, circuit:export); check clean.\n- 2026-10-17: KiCad files needed today for the contractor's hand finish of imu-tile-board. Blocker: the circuit provider has no KiCad export yet (PB-1021). Fallback, leaving the official path: tscircuit's KiCad converter on a copy of src/boards/imu/tscircuit/circuit.json, output to src/boards/imu/fallback-kicad/ (not run here: only protoboard commands allowed). The KiCad files are outside circuit:check and staleness tracking; changes made in KiCad must be carried back to the UHD model by hand. The model stays the source of truth. Reported as Protoboard feedback.\n" } },
62
+ { protoboard: ["check"] },
63
+ {
64
+ say: "This leaves the official PCB path. The stack's path is the UHD model, circuit:generate, circuit:check and circuit:export, and the provider has no KiCad export yet (PB-1021), so KiCad files mean a fallback: tscircuit's KiCad converter on a copy of tscircuit/circuit.json, written to src/boards/imu/fallback-kicad/, never into out/. The friction: those files are outside circuit:check and the staleness tracking, and anything the contractor changes in KiCad has to be carried back to the UHD model by hand; the model stays the source of truth. They will lack 3D models, supplier numbers and library footprints, and their netlist must be compared with out/netlist.json. I recorded this in worklog.md and it should go to Protoboard as feedback (the missing KiCad export blocked us). I could not run the converter here: only protoboard commands are allowed.",
65
+ },
66
+ ],
67
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The IMU tile board (test/fixtures/imu-tile) generated and exported clean,
3
+ * then its tscircuit source hand-edited: the BMI270's SDA and SCL traces go
4
+ * to each other's nets. `protoboard check` can only say the source is newer
5
+ * than the outputs; the agent has to rebuild the board to see what the
6
+ * copper does, name the fault, and restore a board that matches the model.
7
+ * One routed build takes about 35 s and 1.2 GB; the fixture builds once and
8
+ * the known-good solution twice.
9
+ */
10
+ import { defineEvalCase } from "@protoboardai/core";
11
+ import { vendorParts } from "../../test/fixture.ts";
12
+
13
+ const SDA = '<trace from=".u2 > .pin14" to="net.sda" />';
14
+ const SCL = '<trace from=".u2 > .pin13" to="net.scl" />';
15
+ const SOURCE = "src/boards/imu/tscircuit/board.gen.tsx";
16
+
17
+ export default defineEvalCase({
18
+ title: "Rebuild a hand-edited board, catch the netlist fault, and restore the board",
19
+ skills: ["circuit:generate", "circuit:export", "circuit:check"],
20
+ tags: ["circuit", "fault"],
21
+ requires: ["tscircuit-node"],
22
+ fixture: {
23
+ from: "../../test/fixtures/imu-tile",
24
+ recipe: (dir) => vendorParts(dir),
25
+ steps: [
26
+ { protoboard: ["run", "circuit:generate", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
27
+ { protoboard: ["run", "circuit:export", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
28
+ { remove: "operations" },
29
+ {
30
+ note: "the seeded fault: pins 13 and 14 of the BMI270 (u2) traced to each other's nets",
31
+ replace: { file: SOURCE, find: SDA, with: "@@SDA@@", count: 1 },
32
+ },
33
+ { replace: { file: SOURCE, find: SCL, with: SDA.replace("pin14", "pin13"), count: 1 } },
34
+ { replace: { file: SOURCE, find: "@@SDA@@", with: SCL.replace("pin13", "pin14"), count: 1 } },
35
+ ],
36
+ },
37
+ instruction:
38
+ "Someone hand-edited the tscircuit source of the IMU board (src/boards/imu, module imu-tile-board) after its last export. Rebuild the board's outputs from that source and check them: tell me exactly which nets the copper gets wrong and on which component. Then put the board back in agreement with its UHD model (the model is right) so that `npx protoboard check` reports no errors and nothing stale, with fresh standard outputs.",
39
+ tools: { read: true, edit: true, commands: ["npx protoboard *"] },
40
+ budget: { turns: 30, seconds: 1500, usd: 4 },
41
+ graders: [
42
+ { id: "fault-caught", kind: "fault", code: "CIRCUIT_STALE", seeded: ["CIRCUIT_STALE"], caught: "(sda[\\s\\S]{0,200}scl|scl[\\s\\S]{0,200}sda)", description: "the answer names the two nets (SDA and SCL)" },
43
+ { id: "fault-located", kind: "text", matches: ["u2|bmi270|pin ?1[34]"], description: "and where: the BMI270 (u2), pins 13/14" },
44
+ { id: "board-clean", kind: "check", maxErrors: 0, absent: ["CIRCUIT_NET_SHORT", "CIRCUIT_NET_OPEN", "CIRCUIT_STALE"], ran: ["ext:@protoboardai/ext-tscircuit/circuit:check"] },
45
+ { id: "outputs-valid", kind: "outputs", module: "imu-tile-board" },
46
+ {
47
+ id: "netlist-matches-model",
48
+ kind: "facts",
49
+ file: "src/boards/imu/out/netlist.json",
50
+ facts: [
51
+ { path: "nets[id=sda].pins", includes: "u2:pin_14" },
52
+ { path: "nets[id=scl].pins", includes: "u2:pin_13" },
53
+ { path: "nets[id=scl+sda]", exists: false },
54
+ ],
55
+ },
56
+ { id: "source-restored", kind: "text", file: SOURCE, matches: [SDA.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), SCL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")] },
57
+ { id: "model-untouched", kind: "digest", file: "src/boards/imu/imu.uhd.ts", changed: false },
58
+ { id: "netlist-as-clean-export", kind: "digest", file: "src/boards/imu/out/netlist.json", changed: false, description: "the restored board's netlist is the clean export's, byte for byte" },
59
+ ],
60
+ solution: [
61
+ { protoboard: ["check"] },
62
+ { protoboard: ["run", "circuit:export", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
63
+ { protoboard: ["check"], expect: { exit: 1 } },
64
+ { protoboard: ["run", "circuit:generate", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
65
+ { protoboard: ["run", "circuit:export", "--module", "imu-tile-board"], expect: { status: "succeeded" } },
66
+ { protoboard: ["check"] },
67
+ { say: "Rebuilt from the edited source, the copper shorts SDA and SCL (net scl+sda) and splits both nets: the BMI270 (u2) pin 13 and pin 14 traces were swapped. Regenerated the source from the UHD board and exported again; check is clean." },
68
+ ],
69
+ });