soldermaskdocs

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 templates

The 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 final n/m passed line; python tests/run.py runs them all.
  • The front end is plain JavaScript with globals on purpose (state, render, BOARD, scene2d), so tests/verify_3d.js, tests/verify_scene.py and schematic.js can 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 reads getBBox or getBoundingClientRect inside a move.
  • Files that another session or person may be editing are patched with exact-string replacements, not rewritten.

On this page