design.json
What the model produces and what --design and Import design.json consume. Validated against soldermask/schema.py; every object is closed (additionalProperties: false).
{
"summary": "5V to 3.3V LDO breakout with a power indicator LED.",
"board": { "name": "ldo-breakout", "width_mm": 25, "height_mm": 20 },
"components": [
{ "ref": "U1", "kind": "ic", "search": "AMS1117-3.3", "package": "SOT-223", "value": "3.3V", "purpose": "regulator" },
{ "ref": "C1", "kind": "capacitor", "search": "10uF ceramic", "package": "0805", "value": "10uF", "purpose": "input bulk" },
{ "ref": "D1", "kind": "led", "search": "green LED 0603", "package": "0603", "value": "green", "purpose": "power indicator" },
{ "ref": "R1", "kind": "resistor", "search": "1k resistor", "package": "0603", "value": "1k", "purpose": "LED current limit" },
{ "ref": "J1", "kind": "connector", "search": "2.54mm pin header 2 pin", "package": "HDR-1x2", "value": "", "purpose": "input" }
],
"nets": [
{ "name": "GND", "kind": "ground", "voltage_v": 0, "current_a": null,
"connections": [ { "ref": "U1", "pin": "GND" }, { "ref": "C1", "pin": "2" }, { "ref": "J1", "pin": "2" } ] },
{ "name": "VIN", "kind": "power", "voltage_v": 5, "current_a": 0.5,
"connections": [ { "ref": "J1", "pin": "1" }, { "ref": "U1", "pin": "VI" }, { "ref": "C1", "pin": "1" } ] },
{ "name": "+3V3", "kind": "power", "connections": [ { "ref": "U1", "pin": "VO" }, { "ref": "R1", "pin": "1" } ] },
{ "name": "LED_K", "kind": "signal", "connections": [ { "ref": "R1", "pin": "2" }, { "ref": "D1", "pin": "1" } ] }
],
"assumptions": [ "Input is 5V from the header." ],
"decisions": [],
"unsupported": []
}| Field | Rules |
|---|---|
summary | one sentence |
board.name | a short slug; width_mm, height_mm between 10 and 150 |
components[].ref | unique: R1, C1, U1, J1, D1, SW1 |
components[].kind | ic module resistor capacitor led diode transistor connector switch crystal inductor. module is a pre-built board — an ESP32 or other radio, a radar, a GPS — and gets the same rail, ground and decoupling checks as an ic |
components[].search | what to buy — a manufacturer part number when confident it exists, otherwise a short description of the physical part. Searched against live stock; a net name here finds nothing and the part is dropped |
components[].package | one of the supported packages, or FROM_PART for a part with no standard package: the footprint and the pin names then come from the vendor's CAD data for whatever part is ordered, so search must name that part |
components[].value, purpose | 10k, 100nF, `` for parts without a value; why the part is on the board |
nets[].kind | ground power signal; at least two connections each |
nets[].voltage_v | nominal volts against ground for a power/ground net, null for a signal and null when unknown. Ohm's law downstream needs it: without it the LED-resistor, dissipation and capacitor-rating checks go silent |
nets[].current_a | steady amps a power net carries. Copper is etched to it by IPC-2221, so it decides trace width and via count. null rather than a guess — a null falls back to the measured median, a wrong number gets fabricated |
nets[].connections[].pin | 1/2 for two-terminal parts (on an LED 1 is the anode, as a position -- the pad it lands on comes from the ordered part's own symbol); the datasheet's function name for an IC, never a number; A.+ / B.- for multi-unit parts |
assumptions[] | choices the prompt did not specify |
decisions[] | at most two architectural forks, each with 2–3 options {label, detail, recommended} |
unsupported[] | what the prompt asked for that could not be done, and anything the model is not confident is correct. Published, not hidden |
constraints
One more block, and the only one the model never writes: what a person decided about their own board.
{
"constraints": {
"pinned": { "J1": { "x": 4.0, "y": 22.5, "rot": 90, "by": "human" } },
"edges": { "J1": "left" },
"keepouts": [ { "x": 0, "y": 0, "w": 8, "h": 47, "why": "antenna" } ],
"sheet": { "U1": { "x": 2400, "y": 1800 } },
"tracks": [ { "net": "SDA", "layer": "F.Cu", "width": 0.25,
"pts": [[7.0, 6.0], [7.0, 12.0], [13.0, 18.0]], "by": "human" } ],
"vias": [ { "net": "SDA", "x": 13.0, "y": 18.0, "by": "human" } ],
"wires": { "SDA": -1350 },
"rules": { "min_track_mm": 1.0, "router": "krt", "seed": 3, "pour": false, "precedent": true, "mark": true }
}
}A drag on the board is not a new position in board.json; it is a pin here. That is the difference between an edit the next rebuild overwrites and one it honours, and it is why the canvas can be manipulated at all — constraints.pinned becomes the fixed argument the placer has always taken, so a pinned part is never moved, turned, centred or seated, and everything else is annealed around it.
| Field | Rules |
|---|---|
pinned | refdes → {x, y} in board mm, y up; rot a quarter turn (0 90 180 270, default 0); by is human or import, so the canvas can draw a decision differently from a computed position. side is front (the default, not stored) or back: on the back a part is its footprint mirrored about its own vertical axis and then turned, on the back copper, which is what F on the canvas writes. Read by the placer |
edges | refdes → left right top bottom, for a part whose side is decided but whose position is not. Stored; not yet read by the placer |
keepouts | rectangles in board mm with a positive w/h and an optional why. Stored; the antenna keepout the router already honours still comes from the footprint |
sheet | the schematic's pinned, in schematic.js's own units (mils) rather than millimetres — a sheet has no board to be measured against. Read by the sheet: it overrides the column layout for that symbol, and the wires are re-trunked around wherever it lands |
tracks | copper a person drew, in board mm with y up: the net it belongs to, a copper layer, the pts of the path and an optional width (the net's own is used without one). Read by dsn.write(), which hands it to the router in the wiring section as (type protect) — copper it may not move, and routes around. A track that does not reach both pads leaves the net unconnected as far as the router is concerned, so it finishes the rest around what is there |
vias | where hand-drawn copper changes layer: net, x, y in board mm. Separate from tracks for the reason the board's own via list is separate from its wires — a via belongs to a net and a point, not to one of the two tracks that meet at it |
rules | how the board is built, as against what is on it — the rack's knobs on the workbench's chain tab, saved as named variations per project. seed is the anneal seed (0–10000; a build given no seed uses this, 1 when unset, so a rebuild reproduces the placement); pour false routes ground as traces instead of pouring it; precedent false keeps a corpus board from seeding the anneal (it is still listed); mark false leaves the maker's mark off the silkscreen, in the Gerber, the .kicad_pcb and the viewer at once, since all three ask gerber.insignia_decal() and it is the one reader of this key. router is which program lays the copper first, freerouting (the default and the corpus's judge) or krt (the A* router); the other gives a second opinion on a board left short of a pin, and a board that asks for krt and cannot have it is routed by freerouting with a gap saying why — see Two routers. min_track_mm is a floor under every routed net, 0 < w ≤ 5 (past that a track is a plane, and the thing to ask for is a pour). It can only ever raise a width: electrical.width_for_net_mm() takes the largest of the measured prior, what IPC-2221 asks for the current the net declares, and this — so a rail that needs 1.4mm still gets 1.4mm under a 1.0mm rule. Read by everything that asks what a net is worth in copper: the DSN's per-net classes and its default rule, the session reader that judges the router against them, board.json, the hand copper, the placer's picture of how much room a net needs, the pour's thermal stubs, and the KiCad net class |
board | the outline as a decision rather than the design stage's guess. fixed: true stops placer.fit_board() growing the board — a board that has to go in an enclosure, a panel slot, an existing mounting pattern — and only the size is fixed, the parts still move. sides: 1 holds the board to one side of parts; the default, 2, opens the back to the surface-mount parts nobody has to reach, used only where the front is out of room, and is not stored. A prompt that asks for one side ("single-sided", "all the parts on the top", "nothing on the back") is read into this by the pipeline, since the model cannot write it |
wires | net → the y of the lane its wire runs on in the sheet, in mils. A signal wire is a trunk on one lane with a drop to each pin, so the lane is the path; the lane and not the polyline is stored, because a drawn path stops meeting its pins as soon as the next build moves a symbol and a lane does not. Read by assignTrunks(), which takes a chosen lane before it scores anything and routes the rest around it |
Not in DESIGN_SCHEMA. The schema is what the model is shown and what it must return, and every object in it is closed, so a model cannot emit this block. workspace.commit then puts the project's own constraints back over whatever design it was handed, which covers the ways a design can arrive that a schema cannot police — a fork, a file, a backend running to a relaxed schema. A design with no constraints writes no constraints key at all.
A pin for a part the design no longer has is ignored rather than an error: parts come and go across a conversation, and a stale pin should not stop a build.
rules is where "make the traces wide enough to solder by hand" lands. It is a decision about how a board will be built and by whom, not a fact about the circuit — which is why it is a constraint and not a design field, and why the model does not get a vote on it. What it does not do is enforce itself twice: the KiCad project's min_track_width, which DRC refuses copper outright for, stays at the fab's floor, because turning a preference about soldering into a hard failure would fail a board over the pipeline's own thermal stubs. A net that comes back from the router narrower than it was designed for is reported by ses.width_report(), exactly as it is when the shortfall is about current.