Layout and principles
soldermask/
design.py schema.py priors.py the model: prompt -> design
wire.py the second turn: the parts as ordered and their real
pin names in front of the model, the nets against those
resolve.py parts.py packages.py parts against live stock, footprints;
resolve.declare/pinout/addressable go the other way,
from a chosen catalogue row to a design line and its pins
jlcparts.py params.py the parts dataset read off disk; the parametric index
jlcapi.py JLCPCB's own parts data through its signed open API: lookup by
code, the library paged, the whole of it as a snapshot (the
dump, then a detail record a code -- the parameters); the
catalogue jlcparts.py reads, written from that snapshot in the
mirror's own format by ingest() (`setup --parts`); the
authority stock and price are reconciled against
orderability.py can this board still be ordered?
symbols.py easyeda.py alternates.py KiCad symbols (fetched by setup: fetch_library), vendor
footprints, substitutes
eeformat.py EasyEDA's own format read -- pads, silk, pins, the 3D
model's uuid and offset -- in place of easyeda2kicad (AGPL)
datasheets.py jev.py the sheet itself: fetched from the listing's link, read by
pdftotext, its pin table transcribed once by the design stage's
model; its names laid on the vendor's symbol (with_sheet_names),
the two compared pin by pin -- a swap holds the pad back from
wiring -- and a refused pin name looked up through it, last of
all by Jev (a closed-question model, optional, taken at its floor)
erc.py electrical.py electrical rule check; IPC-2221 and Ohm's law
spice.py the circuit switched on in ngspice before it is placed; sim.json for the scope
rubric.py the reviewer's rules graded on every board built and said as
notes; the lines and the pro reference are assets/rubric.json
fabrules.py JLCPCB's published table (read 24 Sep 2026) checked on every
board built: below-minimum / extra-cost / risk, the assembly
service it needs; board["fab_rules"], gaps and notes
placer.py dsn.py ses.py placement on either side of the board, antenna keepouts, the anneal watched frame by frame; the freerouting round trip
keepout.py copper kept out from under a connector's body (kind `connector`, or a USB,
card-socket or DC-jack package by name) on the side it is mounted on: the
body less a cut from each of its own pads to the body's edge; the DSN's
keepouts, the .kicad_pcb's rule areas (which the A* router reads) and the
pour's stitching vias all ask it -- and a power corridor's, which a
router's netlist carries (Netlist.power_keepouts)
anneal_terms.py blocks.py what the anneal pays for past wirelength; functional blocks
astar.py pour.py fillet.py the second router; the ground plane -- and, opt-in
(Netlist.power_pour, SOLDERMASK_POWER_POUR), a local
one-layer zone for each high-current rail above it in
priority, its pads accounted one by one; tracks and
rounded corners
corridor.py a poured rail's zone as a corridor from its source to its
loads (SOLDERMASK_POWER_SHAPE / Netlist.power_shape `tree`
or `mould`; `hull` is pour.py's and the default): the source
by pin name, switch, series part or connector, each load's
current, a Steiner tree or physarum.rail()'s tubes sized
leg by leg, rastered, clipped and traced to polygons, and
the keepout every other net's router is handed; the layer
that strands no pad without room for a stitching via first
prune.py copper that joins nothing -- a via reaching its net on one
layer or none, a track end touching nothing of its net --
taken off the board pour.judge keeps, and kept off only if
KiCad, refilled, loses no connection
fold.py physarum.py a placement as a chord diagram; a slime mould routing it --
set up, loop, read out; the loop runs in mould/ when it is built;
rail() grows one power rail alone for corridor.py
congestion.py corpus.py routability.py the placer's picture of the board; every route kept; the model
precedent.py what each corpus board is, and the one most like this design as a seed
genome.py a placed board as one 64-bit word a part, exact to 0.1um, so a
corpus unpacks into model columns with a shift and a mask
gnn.py the graph judge: message passing over genome.graph, with or without
the mould's and the fold's readings; fit in torch, run in numpy
learn.py `soldermask learn`
gerber.py assembly.py Gerbers, drill, BOM, CPL
silk.py a part's silkscreen drawing -- the vendor's own, or drawn from the
pads and body -- and where it lands; the Gerber, the .kicad_pcb and
board.json all ask it, so they carry one drawing
silkfont.py isofont.py insignia.py the stroke font; ISO 3098 outlines; the maker's mark
kicad.py kicadcli.py models3d.py .kicad_pcb, kicad-cli DRC/render/ERC/netlist, vendor 3D models
sheet.py kicad_sch.py the schematic sheet laid out (roles, flow, ports, labels, wires,
text) and stored on the board; the .kicad_sch written from it
and read back for what it connects
kicadpcb.py a finished KiCad board read back as one to place and route;
repair_imported() gives kicad-cli's imports their net table
inventory.py stock and price at several quantities
repair.py evaluate.py the repair loop; pass@1 on this task
export.py render.py viewer.py board.json, board.svg, the standalone viewer
projects.py workspace.py diff.py versions on disk and the project bundle; the one build sequence; design diffs
edits.py a scoped edit, applied to what is already there
platforms.py stackup.py the MCUs a board is built around; what it is made of
version.py release.py config.py the app's version; releases; where things live
cli.py style.py skeleton.py the command line; its colour, and the picture a
terminal can show (inline image or braille); the
board's skeleton drawn from board.json for that
mcp.py the pipeline as an MCP tool server: every stage ours or the
caller's, one judge, no model call of its own (it sets
config.model_calls() off; --model-calls lifts it); JSON-RPC
on stdin/stdout, by hand; SOLDERMASK_MCP_LOG, a line a call
tables.py a catalogue answer -- search rows, a pinout -- as lines a
model reads for half the tokens of the JSON, and back
mcp_http.py the same server over Streamable HTTP for a client that is
not on this machine: bearer keys (digests on disk), a
projects directory a key, one build slot at a time, the
precedent's placement withheld under a non-permissive
licence; nothing ships
app/
__init__.py serve(), the Flask factory, headers, compression
api.py pages.py the JSON API; the HTML pages
benchmarks.py what /benchmarks reads off disk; it computes nothing
jobs.py bus.py turns.py auth.py background builds; the event bus (progress and the placing's frames are transient on it); a turn; access control
templates/ static/ Jinja pages; workbench.js, schematic.js, projects.js (the home:
ledger, disc, foot), platforms.js (the MCU token), vendor/
(fonts.css and the files it names: the three faces, tokens.css
and tests/test_fonts.py say which is set in what)
mould/ the mould's inner loop in Rust (soldermask_mould): the same
loop as physarum.py's numpy, a lane a net, a thread a window;
optional, `pip install ./mould`, numpy carries it otherwise
docs/ mkdocs.yml this manual; MkDocs builds it for the app's /docs
solderdocs/ the same manual public at solderdocs.shwinnie.com: Fumadocs
(Next.js, static export); scripts/sync-docs.mjs turns docs/
into its content before every build, so the two cannot drift
tests/ tools/ deploy/ the suite; snap.py, backfill_models.py, annotate_boards.py,
exa_boards.py, org_boards.py, fetch_kicad.py and kicad_boards.py
(the board bank: found by question or by vendor, fetched over
git, converted from Eagle/Altium/legacy through kicad-cli, kept;
`--layers 4,6` fills the multilayer bank beside the two-layer one),
graph_corpus.py and gnn_train.py (the corpus as graphs; the graph judge),
pic.py, pic_place.py, pic_perturb.py, pic_report.py (a photonic
chip read, placed by the channel fold, and judged by LiDAR);
place_bench.py (the placer against the board's author, the
literature's protocol: placed on the same outline, routed as traces
by A* and freerouting, judged by kicad-cli, paired per board, placer
variants as arms; suites `cypress` and `bank`) and azure_bench.py
(its arms sharded onto rented machines and collected home);
antenna_ab.py (the placer's antenna-keepout term on and off, under
the old search and the new, on a drive's own antenna boards: every
anneal checked for copper in a zone, each board built as the drive
built it, no model);
affinity_census.py (the author's placement against one anneal of the
same board, pair by pair in thirteen netlist roles -- decap, ESD,
fine-pitch ring, edge and the rest -- each read past the board's own
wirelength ratio; rows keep the anneal's placement);
escape_oracle.py (the author's own escape copper on a fine-pitch
board, walked out from the pads 1 or 3 mm and handed to A* as copper
it may not move, against the bare board on the author's placement:
the ceiling of an escape policy before one is trained; `_pour` arms
pour ground as the product does, `ret` hands it escape_policy's);
escape_data.py (every fine-pitch pad's escape in the bank as its author
drew it, in the part's own frame, with where its net goes and a DRC
flag; split by repository, the gate's repositories held out and their
forks excluded; .cache/escape_data/);
escape_policy.py (the first escape learner: another author's fan-out
for the same package off the train split, matched on the whole pad
layout at four turns and chosen for where this board's nets go;
`self-test` scores it against held-out authors);
escape_geom.py (an escape as a decision -- via, direction, exit -- and
copper laid for it by A* at this rule round the part, pad by pad,
legal by construction, or all of a part's pads at once by negotiated
congestion (`negotiate`); escape_oracle's `dec`, `wpt` and `neg` arms);
revision_pairs.py (preference pairs from the git history of the bank's
boards: a later revision by the same designer that kept the parts and
moved them, followed across renames, kept as it goes; `report` asks
which rubric scalars move the way the rubric says is better, `moves`
where the moved parts went, `fit` what weights the pairs support);
rubric_reddit.py (what r/PrintedCircuitBoard's reviewers raise, counted per rule
against the catalogue from a year of the sub's review threads; `--fetch` the
repositories those threads link, pinned before the post; `--boards` writes
them as battle/reddit/ for `rubric.py --calibrate`; a remark is a label only
once a reader has confirmed it in review.json's `confirmed`);
rubric_within.py (can the rubric order one board's own anneals); pair_annotator.py
(trade-off pairs of a board's anneals for a person to choose between, and
the fit that reads the choices back); crystal_ab.py (the crystal term
under the router); deeppcb_board.py (a board DeepPCB shares, routed by ours);
profile_build.py (where a build's seconds go: realise() in a scratch
cache with a timer on every stage, subprocess and request; `--cli`
for the whole command line; JSON under .cache/profile/);
jev.py (TypeSafe's Jev -- a closed-question model with a calibrated
probability, no generation -- asked three of the pipeline's own questions
against stored data: `verify` how a design handles each clause of its
prompt, `--inject` a clause it cannot carry; `resolve` which catalogue row
is the part asked for, the resolver's own rows, `--rule` with its policy
stated; `pins` which real pin a failed pin name meant; calls cached under
.cache/jev/, the key from TYPESAFE_API_KEY or .env);
datasheet_check.py (does the sheet close what the vendor's data
cannot: `fetch` every link for the parts the replay orders, with
the cost of pulling all 613k; `pins` the 20 stored pin-name
failures by the symbol, by the sheet, and by Jev given both;
`audit` every ordered symbol against its sheet, pin by pin);
parametric_index.py, rubric.py, isofont.py and appicon.py; service templatesThe pipeline is plain Python modules with no framework in them. workspace.realise() is the build sequence and both the app (app/turns.py) and the command line call it — the CLI since 11 Sep 2026, before which it ran its own copy and the two drifted four times. realise() hands back a Build, which unpacks to the five things a version stores and carries the placement, the copper, the report and the fill beside them for a caller that prints or checks more; tests/test_cli_build.py counts the calls. Anything that must hold for every board goes in realise() or in a function it calls, as verdict(), apply_constraints() and rubric.report() are.
Principles the code follows
Report, never hide. Every stage returns what it could not do, and that goes next to the result: gaps and notes on the netlist, unsupported from the model, the honest section on the command line, Not guaranteed in the workbench. A silent except: continue is the thing to be most suspicious of in review.
Recall is not evidence. The model names functions and parts; pad numbers come from the symbol library, footprints from data, stock from the supplier. When something has to be borrowed — a pinout from a family, a footprint from a vendor — it is labelled assumed, not verified.
Check with something that did not make it. Gerbers are read back by gerbonara; the KiCad file is re-parsed and compared with the placement; with KiCad installed, its plot is compared with ours.
Measured, not chosen. Trace width, clearance, via size, decoupling values and block idioms come from a corpus of real boards; model orientation comes from the pads; the KiCad rotation sign was found by rendering all four and scoring them.
Repair must not launder. A dropped part costs more than an unresolved one, so deleting a problem never pays.
A model predicts; the oracle decides. The routability model orders the placements the router sees and never replaces the router. Its prediction is written beside the router's verdict on every board, a confident miss is said in words, and the route becomes the next training example. Whether the neural network beats the analytic baseline is a number in the model file, not an argument.
A wrong instrument is worse than none. The evaluator and the harness tests exist because two measurement faults have already cost real time.
Data is not code. projects/ is the user's work: immutable versions, atomic writes, migrations with backups, a trash instead of deletion, and it is not in git.
Conventions
- Python 3.11+, type hints, dataclasses, docstrings that say why. Modules explain their coordinate frame at the top when they have one (mm, origin bottom-left, y up — except KiCad, which is y down).
- Tests are plain scripts with a
check()helper and a finaln/m passedline;python tests/run.pyruns them all. - The front end is plain JavaScript with globals on purpose (
state,render,BOARD,scene2d), sotests/verify_3d.js,tests/verify_scene.pyandschematic.jscan reach it; no build step. - The copper view is a persistent scene: built once into
static/live/overlay, mutated afterwards, torn down only for a different board. A gesture writes one attribute; nothing readsgetBBoxorgetBoundingClientRectinside a move. - Files that another session or person may be editing are patched with exact-string replacements, not rewritten.