yapnr migration plan¶
Status: approved plan; PR0 and PR-R merged, PR1 in review (2026-09-30). This file is committed to the public yapnr repository. It contains no machine paths, host names, network addresses or personal e-mail addresses; operators bind the path variables below locally.
Variable |
Meaning |
|---|---|
|
Local Splanc checkout, branch |
|
|
|
A frozen copy of the Codex agent’s uncommitted engine work |
|
Local yapnr clone |
|
A scratch directory outside both repositories |
0. Summary¶
yapnr (“yet another place and route”) is the PnR system that grew inside Splanc (hardware/pnr,
parts of hardware/tools, the live viewer, and the experiment harness in $HIER). This plan
moves it into its own repository in reviewable pieces, removes the Splanc-specific assumptions,
adds a project abstraction that owns every runtime artifact, and ends with Splanc consuming
yapnr as a headless Bazel ruleset.
0.1 Facts verified for this plan¶
The repository is public.
Studio-Fug/yapnris public;mainholds one bootstrap commit (LICENSE,README.md,.gitignore,branding/).Consequence: everything pushed is public. The privacy scrub (Appendix C) is a hard gate for every push, including imported history.
License text.
LICENSEis byte-identical to Splanc’s (AGPL-3.0 text, 34,504 bytes) and is committed onmain.Consequence: GitHub detects
agpl-3.0; the SPDX identifier is declared as in §1.4.
Splanc’s declarations are inconsistent. Its Sphinx
conf.pysaysAGPL-3.0-or-later, itspackage.jsonsaysUNLICENSED, and there are no SPDX headers.Consequence: yapnr declares one identifier consistently (§1.4).
Imported history has several author identities, not all of them noreply addresses.
Consequence: the history import rewrites identities with a mailmap (§7.2) so that every imported commit uses the owner’s public commit address (decided, Q2 in §9).
History under the PnR paths is small: 516 blobs, 5.5 MB; the largest blob is 554 KB (
testdata/splanc_dev/splanc_dev.kicad_pcb).Consequence: a full-history import is cheap and passes the 600 KB large-file hook.
The newest engine is uncommitted. Snapshots
src8b,src9.frozen,src10*,src11*,src12,src12h,src12iare done;src12b/src13(USB pairs) andsrc12n(N-0001) are in progress.src2tosrc7no longer exist on disk.Consequence: PR2 recreates the lineage as a commit series from the surviving snapshots (§7.3).
Update (PR2, 2026-09-30):
src13andsrc12nwere finished and merged intosrc15(withsrc14, SI v1), the engine of the H7 experiment; PR2 imports the whole lineage up tosrc15plus Electrical221 (§7.3, import manifest).
Splanc CI has never been green for PnR code (lint debt; the Linux test job fails on an unrelated Nix package), and CI never runs the native (KiCad-side) code paths.
Consequence: yapnr gets a lint baseline and a real KiCad test lane before the big mechanical rewrite (§8, PR6a).
0.2 Owner decisions (2026-09-29)¶
License
AGPL-3.0-or-later(SPDX), matching Splanc’s declaration.Owner-only contributions for now.
CONTRIBUTING.mdstates that outside contributions are not accepted yet.Commit identity: the owner’s public commit address, listed in
tools/privacy/allowed_identities.txt, for new commits and for imported history; GitHub noreply addresses remain accepted. This replaces the first form of the decision (noreply addresses only): GitHub accepts only an address verified on the account as the author of a merge made on the website, and the owner chose to publish this address on commits. CI checks every new commit.Viewer Ask agent and AI summaries ship off by default (PR4c).
elkjs is fetched at build time, pinned by sha256, never vendored (PR4b).
THIRD_PARTY.mdlists it and three.js.GitHub issues (
#N) replace Splanc’sFUG-NNNkeys.GitHub Pages is approved. Deploy and per-PR previews are wired like Splanc and gated on the repository variable
YAPNR_PAGES_ENABLED == 'true', which is set after the first green build ofmain.
0.3 Design decisions in this plan (owner can override)¶
Python package
yapnr(renamed frompnr), import root = repository root.The viewer is part of the package (
yapnr/viewer), not a separate web app.The Rust search backend lives in
native/searchand is built byrules_rust.Tests are split by interpreter:
tests/unit(hermetic Python 3.11),tests/kicad(KiCad’s Python),tests/regression(end-to-end ladder).KiCad in CI: official Docker image
kicad/kicad:10.0.6for the required Linux KiCad lane; Homebrew cask onmacos-latestas a nightly parity lane. No Nix in yapnr.The engine’s
PNR_*environment flags keep their names; they get a typed registry. New infrastructure variables useYAPNR_*.History: full history of the PnR paths via
git filter-repo, then the uncommitted snapshots as a commit series, landed with merge commits (everything else is squash-merged).Mechanical changes (format, rename, moves) are separate commits, and their commits on
mainare listed in.git-blame-ignore-revs. Squash merging rehashes, so a hash is added in a follow-up after the merge (PR3a and PR3b, whose format and pure-move commits must survive, are merged with a merge commit). In-flight engine work is ported through them with a checked-in tool.drc_warm(GUI-bound) and the oldkeyhole_{repair,loop,shift_via,deform}adapters are imported with history but deleted during restructuring. They remain recoverable from history.
1. Repository layout¶
yapnr/ repository root
├── LICENSE AGPL-3.0 text (committed on main)
├── README.md AGENTS.md DEVELOPERS.md CONTRIBUTING.md WORKLOG.md THIRD_PARTY.md
├── MODULE.bazel MODULE.bazel.lock .bazelversion .bazeliskrc .bazelrc .bazelignore
├── BUILD.bazel requirements targets, top-level aliases (//:yapnr, //:doctor)
├── requirements.in requirements.lock requirements_linux_x86_64.lock (PR6a)
├── .pre-commit-config.yaml .flake8 .markdownlint.json .markdownlintignore
├── .prettierrc .prettierignore .gitignore .git-blame-ignore-revs setup-precommit.sh
├── .github/ workflows/{ci.yaml,macos.yaml}, CODEOWNERS,
│ pull_request_template.md
├── yapnr/ the Python package
│ ├── __init__.py __main__.py
│ │ `python -m yapnr` == CLI
│ ├── cli/ argparse CLI, one module per command group (PR0: cli.py)
│ ├── core/ graph, constraints, electrical, plane_intent,
│ │ power_topology, fab_profile, flags (registry), rules
│ ├── place/ differentiable placement, legalize, power_first, hull, ...
│ ├── route/ global route, feedback loop, detail/ (maze, keyhole, ...)
│ ├── native/ KiCad-side native loop and electrical stages (stdlib only)
│ ├── shove/ hier/ mc/ feedback/
│ │ as today
│ ├── kicad/ all KiCad I/O: ingest, writeback, library_table, specctra, drc
│ │ ├── toolchain.py KiCad discovery (kicad-cli, KiCad Python, headless copy)
│ │ ├── staging.py stages the KiCad-side closure for worker processes
│ │ └── workers/region.py today's hardware/tools/keyhole_region.py
│ ├── runtime/ live telemetry, runtime controls, profiling, proc (timeouts)
│ ├── frontends/ design-input plugins
│ │ ├── base.py DesignFrontend protocol, canonical annotations
│ │ ├── atopile/ .ato annotations, atopile_address, module hierarchy, index
│ │ └── kicad/ sheet-path addresses, sidecar annotations, footprint fields
│ ├── project/ manifest, store, objects, inputs, snapshots, runs, supervisor,
│ │ profiles, retention, bundle, kicad_export, baseline, legacy_hier
│ ├── diagnostics/ via proximity, pair-contact audit, congestion exports/renders
│ ├── review/ PDF/PNG review exports (optional deps)
│ ├── stages/ experimental native post-stages (plane access, ground)
│ └── viewer/ live viewer: server, services/, notes/, kicad_scripts/, static/
├── native/search/ search.rs (heading-aware A*), BUILD.bazel
├── bazel/ downstream ruleset: defs.bzl, providers.bzl, extensions.bzl,
│ toolchains/, private/ (PR8)
├── tests/
│ ├── unit/<subpackage>/ hermetic Python 3.11 + numpy/torch/yaml; no KiCad
│ ├── unit/repo/ repo checks: test wiring, privacy scan (PR0)
│ ├── kicad/<subpackage>/ run under KiCad's Python and/or need kicad-cli
│ ├── regression/ generic 8-circuit ladder (designs.py, run.py, native.py, ...)
│ ├── fixtures/ boards/splanc_dev (79 parts), feedback/, power_topology/, graphs/
│ ├── e2e/ viewer browser tests (Chrome DevTools client), manual
│ └── downstream/ tiny consumer workspace for the ruleset (PR8)
├── examples/
│ ├── led555/ KiCad-native example project (CI fixture, frontend "kicad")
│ └── atopile-blinky/ atopile-frontend fixture (sources + prebuilt board)
├── third_party/ image-licenses/ (the viewer's elkjs and three.js are fetched)
├── tools/ privacy_scan.py, check_test_wiring.py, repo_root.py,
│ bazel/py_tests.bzl (PR0); kicad_test_runner.sh, migrate/
└── docs/ Sphinx (MyST) site; see §6
1.1 Rationale for the main choices¶
One package, subpackages by subsystem. Today’s
pnr/has about 80 flat modules. The rename maps them intocore,native,kicadandruntime, so the KiCad boundary is visible in the tree.native/stays flat in the first pass so the move is purely mechanical. Appendix A has the full module map.Interpreter boundary is explicit. Modules that run inside KiCad’s Python (3.9 on macOS, 3.13 in the Docker image, 3.14 in nixpkgs) must be stdlib-only and 3.9-parseable. A unit test computes the import closure of every worker entry point and enforces both rules. That closure replaces the hand-maintained
pnr_kicad_srcsfilegroup, which is already missing modules.A single KiCad adapter package. KiCad plans to remove the SWIG
pcbnewbindings in 11.0. Newpcbnewuse goes throughyapnr.kicad. A lint test ratchets the number ofimport pcbnewsites outsideyapnr/kicadandyapnr/native(about 225 lazy imports today) so an IPC-API backend can be added later.Viewer inside the package. The server is stdlib Python that imports engine modules. Keeping it in
yapnr.viewermakesbazel run //:viewerand version pinning trivial. Static assets live inyapnr/viewer/static/.
1.2 Test tiers¶
Tier |
Location |
Interpreter |
Bazel tags |
|---|---|---|---|
unit |
|
hermetic 3.11 ( |
none, or |
kicad |
|
KiCad Python, |
|
regression |
|
both |
|
e2e viewer |
|
hermetic + Chrome |
|
repo checks |
|
hermetic |
|
Where they run:
unit and repo checks: the
testjob on every PR (ubuntu-24.04-arm) and the informational macOS job;kicad: the
kicad-integrationjob on every PR (from PR6a) and the macOS nightly;regression: a smoke case in
kicad-integration, the full ladder nightly;e2e viewer: on demand.
Repo checks include: every tests/**/test_*.py is wired to a Bazel target (in Splanc 66 test files
are not registered); the privacy scan of the whole tree is clean (both in PR0). Later PRs add: the
KiCad-side closure is stdlib-only and 3.9-parseable; every PNR_* read is in the flag registry;
generated docs and schemas are fresh.
The repo checks read the whole working tree, which a Bazel test does not declare as inputs. They
locate the checkout by following the runfiles symlink of //:MODULE.bazel, refuse to run against
a runfiles or execroot copy, and are tagged external so Bazel never serves a cached result.
Tests stay unittest-style (no current test uses pytest). Four files need both torch and pcbnew
(test_intermediate_pair_timing, test_native_electrical, test_native_placement_copper,
test_pair_bridge); they are split during the move.
1.3 Examples and fixtures¶
examples/led555is a real KiCad 10 project generated once fromtests/regression/designs.py(the 555 timer circuit; stock footprints) and saved in KiCad 10 format. It has ayapnr.tomlmanifest, rules and aci-smokeprofile bounded to about 5 minutes. CI uses it end to end (import, run, export, DRC, pack/unpack).examples/atopile-blinkyholds small.atosources with# @pnr-*annotations plus a pre-built.kicad_pcbcarryingatopile_addressfields. It tests the atopile frontend without running atopile in CI.tests/fixtures/boards/splanc_devkeeps the existing 79-part dev board (already public in Splanc under AGPL). Provenance fields with absolute paths are scrubbed.The Splanc Mini inputs (
$HIER/inputs*) are not migrated. They contain absolute URIs and belong to the Splanc project. Tests that use them becomemanualtests that take an external project path fromYAPNR_EXTERNAL_PROJECT.
1.4 License and top-level documents¶
License:
AGPL-3.0-or-later(owner decision).LICENSEis the verbatim AGPL-3.0 text. The SPDX identifier is declared in:README.md(license section);docs/_sphinx/conf.py(copyright, shown in the site footer);CONTRIBUTING.md(inbound = outbound).
No per-file SPDX headers for now: one
LICENSEplus one identifier keeps a later relicense cheap. Code imported from Splanc is already AGPL-3.0-or-later by the same owner, so it is compatible.No license metadata in Bazel for now.
rules_licenseis not a dependency. If metadata is added later, it goes intoREPO.bazelasrepo(default_package_metadata = [...]), which covers every package; never into a rootpackage(default_package_metadata = ...), which covers only the root package.Network use (AGPL section 13). The viewer is served over a network. It gets an About/Source link showing the repository URL and the exact engine commit of the run being viewed.
Third-party material is listed in
THIRD_PARTY.md:three.js 0.186.1 plus
GLTFLoaderand the add-ons the 3D view imports (MIT). Updated in PR4: fetched at build time like elkjs (its build files exceed the 600 KB limit), served with its license undervendor/three/.elkjs 0.9.3 (EPL-2.0, which contains an Apache-2.0 web-worker shim). It is never committed: it is fetched with a sha256-pinned archive and served as a separate, unmodified file (never bundled or minified with AGPL code). Its license text is served next to it from the same archive (PR4; decisions).
KiCad stock footprints and 3D models (CC-BY-SA-4.0 with the KiCad library exception) are referenced, not vendored.
README.md: what yapnr is, status (alpha, KiCad 10 only), quick start (
bazel run //:yapnr), links to docs, license.AGENTS.md carries over the rules that governed the Splanc work:
selection is mechanical (Monte Carlo and halving): never hand-pick seeds or branches;
no model-driven hand routing;
nothing in the loop may be GUI-bound (no
wx.App, no Dock-iconkicad-cli);every worker is time-bounded;
native KiCad DRC is the judge: never suppress DRC or delete nets to claim completion;
new engine behaviour lands behind a default-off flag with an A/B result in the commit body;
public-repository privacy rules and the commit identity (the owner’s public commit address);
do not disturb running experiments; check for other agents before long runs;
keep the WORKLOG convention.
DEVELOPERS.md covers:
setup: bazelisk,
./setup-precommit.sh(prek in isolation, never system-wide), KiCad 10, and on macOS the headless KiCad copy (later automated byyapnr kicad make-headless);everyday commands and test tiers;
the Bazel output base on an internal, case-sensitive disk, and
--config=lowmem;lockfile regeneration and a CI overview.
CONTRIBUTING.md covers:
the policy: outside contributions are not accepted yet (owner decision);
squash-merged PRs; commit style
<Area>: <summary> (#N)with measured results in the body;prek run --all-filesbefore pushing;the commit identity rule and the privacy rules;
the inbound license terms (inbound = outbound).
WORKLOG.md: short live status board (in progress, next, blockers, do-not-retry), rewritten at the end of each session, not a diary.
2. De-Splanc-ification¶
Line numbers refer to $HIER/src12h/hardware/pnr unless another tree is named. Every item is
closed by the PR in brackets.
2.1 Repository layout and worker staging¶
native_loop.py:569:--repodefaults to cwd, thenos.chdir(repo).No repository root; the controller works in the run’s work dir. [3c]
native_loop.py:585-600: copiespnr/toworker_root/hardware/pnr/pnrandhardware/tools/keyhole_region.pynext to it; setsPYTHONPATH=worker_root/hardware/pnr; adapter paths at lines 789, 834, 912, 939, 969, 994.yapnr.kicad.staging.stage(work_dir)copies the computed KiCad-side closure to<work>/.worker/yapnr/. Workers run aspython -m yapnr.kicad.workers.region. [3b/3c]
keyhole_region.py:22:sys.path.insert(parents[1]/'pnr').Module entry point;
PYTHONPATHset by the stager. [3b]
full_iteration.py:61:--repo parents[3];mc/halving.py:43-44,961;hier/native_block.py:16-17.Removed. Staging plus toolchain discovery. [3c]
feedback/signals.py:188,275,287:PNR_ROOT; a regex on.../hardware/splanc_dev/elec/src/*.ato; the package regex^pnr(\....)+$.Source roots come from
frontend.source_roots(); the package regex usesyapnr. [3b/3e]
regression/run.py:14,33-35,64-65;regression/export_reviews.py:5,17.Tools are resolved from the package (
yapnr.diagnostics,yapnr.review), not from repository paths. [3c]
2.2 Board-specific defaults¶
full_iteration.py:23-24,mc/halving.py:317-322,hier/native_block.py:89-94: defaulthardware/splanc_dev/mini-*-fab.json.Required inputs, supplied by the project input set (
rules/*.json). [3c]
full_iteration.py:33,geometric_native.py:11,38:splanc_mini.atohard-coded.Canonical
annotations.jsonin the input set, produced by the frontend. [3c/3e]
full_iteration.py:51: fallback to a Splancoutput/.../fp-lib-table.The input set always carries a
${KIPRJMOD}-relativefp-lib-table; missing means error. [3c]
regression/native.py:22: UUID namespacesplanc-regression/.yapnr-regression/. Regenerate goldens; recorded as an ADR. [3c]
native_loop.py:77,711,717: phase02-usb-pairsused for every differential pair.02-diff-pairs; the viewer maps the old name for old telemetry. [3c]
fab_profile.py:42: engine defaultjlc-pofv.Unchanged during migration.
yapnr initwritesfab_profileexplicitly into each manifest, so a later default change cannot silently alter a project. [3d]
2.3 Toolchain paths (macOS-only today)¶
/Applications/KiCad/...infull_iteration.py:29-30,mc/halving.py:45,hier/native_block.py:15,hier/power_quality.py:37,regression/run.py:15,53-55,pnr.bzl:75, viewerserver.py:129,cost_service.py:36.yapnr.kicad.toolchain(below). [6a/3c]
PNR_KICAD_CLIis honoured only in 5 files;via_coalesce.py:306andpower_bank_stage.py:160use a barekicad-cli.Every call goes through
toolchain.kicad_cli(). A repo check forbids the literalkicad-clielsewhere. [6a]
regression/run.py:55:--pythondefaults to the Splanc experiment venv.Defaults to
sys.executable; workers usetoolchain.kicad_python(). [6a]
drc_warm/launch_host.py: GUIpcbnew.app, KiCad prefs dir,splanc_drc120plugin,SPLANC_DRC120_ROOT.Not migrated (GUI-bound; the owner’s rule). A future headless warm-DRC design is an ADR placeholder. [3b]
PNR_RUST_SEARCH_LIBmust be set by hand, pointing at a.dylibcopied into every inputs dir.The controller resolves the library from runfiles or
YAPNR_SEARCH_LIBand passes it to workers. The library is removed from input sets. [5]
yapnr.kicad.toolchain resolves kicad-cli and KiCad Python in this order:
YAPNR_KICAD_CLI/YAPNR_KICAD_PYTHON(PNR_KICAD_CLIaccepted as an alias).~/.config/yapnr/config.toml[kicad].On macOS,
~/Applications/KiCad-headless.app(background-only clone; no Dock icon). The stock/Applications/KiCad/KiCad.appis used only for KiCad’s Python, never forkicad-cli, which would show a Dock icon on every call.kicad-clionPATHplus apython3that canimport pcbnew.
It checks the version (10.x required), caches the result per process, and yapnr doctor prints
it. yapnr kicad make-headless (macOS) creates the background-only APFS clone that the Splanc work
currently makes by hand (clone, new bundle identifier, LSBackgroundOnly, ad-hoc signature). PR0
ships a doctor stub that reports the Python, numpy and torch versions and whether
YAPNR_KICAD_CLI/PNR_KICAD_CLI is set, without ever running KiCad.
2.4 atopile coupling becomes a frontend¶
ingest.py:118-125reads the footprint fieldatopile_addressintoComponent.address.An
AddressProviderfrom the frontend. atopile usesatopile_address; KiCad-native uses the sheet path plus reference (from the footprint’sSheetname/Sheetfile/path). [3e]
electrical.py:29,41,46,126,135parse# @pnr-current/# @pnr-pairfrom.atoand match addresses with._ptrimmed;plane_intent.py:33parses# @pnr-plane-access.Parsing moves to
frontends/atopile/annotations.py, which emits canonical annotations (yapnr-annotations-v1).core.electrical/core.plane_intentcompile only canonical annotations. [3e]
hier/blocks.py:41-62derives blocks from dotted atopile module paths.frontend.hierarchy(): the atopile module tree, or KiCad hierarchical sheets. [3e]
source_footprints.py,writeback.pyassume an atopile-resolved, row-placed board.Frontend capability
initial_placement = "rows" | "as-is". [3e]
Viewer
ato_index.py,source_service.py(atopile elaboration,SPLANC_ATO_SRC).frontend.source_index(); the atopile implementation moves tofrontends/atopile/index.py. [4b]
pnr.bzlloadsAtopileLayoutInfofrom@atopile_rules.The ruleset takes plain files (
.kicad_pcb, footprints, sources). The atopile adaptation is a few lines in the consumer (Splanc). [8]
Planned annotations (@pnr-inductance N-0002, @pnr-noise-* N-0003) go straight into the
canonical schema, so both frontends get them.
2.5 Tests, fixtures and documentation¶
tests/test_shove.py:224,tests/test_shove_native.py:29-32: absolute case paths and runtime Python.Fixtures under
tests/fixtures; toolchain discovery. [3c]
test_halving_generations.py:26-27,test_feedback_moves.py:19-20,test_place_pair_weights.py:18-19,test_synth_native_generations.py:22,test_src2_library.py:25,28: reach into$HIERviaparents[4].Small extracted fixtures where possible; otherwise
manualtests onYAPNR_EXTERNAL_PROJECT. [3c]
power_topology_golden.py:43-44,test_fab_profile.py:20,test_regression_inputs.py:10.Fixtures. [3c]
testdata/power_topology/{converter,pd}.jsonembed absolute provenance paths.Scrubbed to relative paths before the first push of that data. [2]
tests/test_src2_native-loop.pyhas a hyphen in its name.Renamed. [3b]
__init__.pycites FUG-138 and Splanc docs;fab_profile.py/via_in_pad.pycitedocs/fab-comparison.md(which exists only in$HIER/docs).yapnr docs pages; the fab comparison is imported, scrubbed, as
docs/engine/fab-profiles.md. [7]
2.6 Viewer¶
server.py:11,21,148: repo =parents[3],hardware/pnronsys.path.The viewer imports the installed
yapnr. Subprocess services run the engine snapshot recorded in the run manifest. [4a]
server.py:28-43,127,167:inputs10b,splanc_devdefaults, prefs underoutput/,restart-status.json.--project; paths from the manifest and store (user/settings.json). [4a]
agent_service.py:29,164,175-212,net_llm.py:14: Splanc prompt, file list, absolute path to theclaudeCLI.The prompt and document list come from the manifest
[viewer]. The CLI is found withshutil.which. Off by default (owner decision). [4c]
notes_store.py:29-30,601-602, schemassplanc-notes-v1, MCP serversplanc_notes, envSPLANC_*.yapnr-notes-v1,yapnr_notes,YAPNR_*; store dirnotes/. Existingnotes.jsonllines carry no schema tag, so this is safe. [4c]
UI title “SPLANC”,
window.Splanc*,splanc:*events,splanc-*localStorage keys;agent.js:110strips/elec/src/.yapnrnames; the prefix to strip comes from the frontend. [4a]
cost_compute.py:13importspnr.capacitor_intent, which was never committed anywhere.Recover the module if it exists in a snapshot; otherwise remove the code path and file an issue. [4b]
Some tests and fixtures embed machine-local values (network literals, absolute source roots).
Documentation-range values; fixtures regenerated from
examples/. [4a/4c]
3. Project abstraction¶
A yapnr project is a directory that holds the design inputs, rules and experiment specs
(committed), plus a store that holds everything the engine and viewer produce (not committed,
relocatable, content-addressed). It replaces the ad-hoc $HIER layout: inputs*/, src*/,
runs/, blocks/, live/, notes/, drc-cache*/, env2.json and the launch_*.sh scripts.
3.1 On-disk layout¶
A project may live inside another repository (for example Splanc’s hardware/splanc_dev/pnr).
[C] marks committed files, [L] machine-local ones (gitignored).
<project>/
├── yapnr.toml manifest, schema yapnr-project-v1 [C]
├── yapnr.local.toml machine-local overrides (store path, workers) [L]
├── design/ frontend inputs [C]
│ ├── <name>.kicad_pro, <name>.kicad_pcb
│ │ (kicad frontend; atopile builds its board elsewhere)
│ └── annotations.yaml kicad-frontend contracts (atopile uses .ato comments)
├── rules/ [C]
│ ├── constraints.yaml today's constraints.yaml (schema v0)
│ ├── electrical-fab.json current/copper/temperature/via budgets
│ ├── plane-access-fab.json plating/loss/drop model
│ └── fab-profile.toml optional overrides on a built-in profile
├── profiles/<name>.toml named flag profiles [C]
├── experiments/<name>.toml declarative experiment specs [C]
└── .yapnr/ THE STORE (default location; see store.path) [L]
├── store.json {schema: yapnr-store-v1, project_id, created, layout_version}
├── objects/sha256/<2 hex>/<62 hex>
│ immutable blobs; hard-linked or APFS-cloned into place
├── inputs/<input-id>/ frozen input set (today: $HIER/inputs10b)
│ ├── inputs.json {frontend, source_rev, files: {name: sha256}, created}
│ ├── board.kicad_pcb board.kicad_pro fp-lib-table (${KIPRJMOD} URIs)
│ ├── graph.json rules.json (relative source paths) annotations.json
│ ├── constraints.yaml electrical-fab.json plane-access-fab.json
│ └── footprints/<nick>.pretty/*.kicad_mod
├── engines/<engine-id>/ frozen engine snapshot (today: $HIER/srcNN)
│ ├── engine.json {git_commit, dirty_patch_sha256, tree_sha256,
│ │ native_lib_sha256, python_lock_sha256, created}
│ └── yapnr/... read-only source copy (+ libpnr_search)
├── runs/<run-id>/
│ ├── run.json provenance (below)
│ ├── status.json engine-written progress (unchanged format)
│ ├── result.json normalized objective summary and best-candidate pointer
│ ├── logs/run.log logs/supervisor.log
│ ├── work/ engine work tree, unchanged:
│ │ cand/pNNN/{placed.json,rung1/,native/,deep/}
│ └── .pruned retention marker
├── libraries/<library-id>/ block libraries (today: blocks/<run>/)
│ ├── library.json blocks.json
│ └── <template>/{library.json, trials.jsonl, native/<tag>/...}
├── live/<run-id>/ telemetry (PNR_LIVE_DIR points here)
│ ├── events/<ns>-<hex>.json
│ │ yapnr-live-event-v1 (compatible with pnr-live-event-v1)
│ └── boards/<sha>.kicad_pcb
│ hard links into objects/
├── baselines/<baseline-id>.json
│ hand-edited board lineage
├── user/ viewer user state; never auto-deleted
│ └── pins/ drafts/ snapshots/ controls.json settings.json
├── notes/ notes.jsonl (source of truth), conversations/; never auto-deleted
├── cache/ derived; always safe to delete
│ └── drc/ geometry/ schematic/ source/ component-costs/
└── locks/ advisory fcntl locks (store, per-run)
Artifacts fall into four classes, and every command treats them consistently:
Provenance and results:
run.json,result.json,status.json, final boards,inputs/,engines/. Kept; referenced objects are pinned.Telemetry:
live/. Append-only; GC keeps lanes referenced by pins, snapshots or best candidates.User state:
user/,notes/. Never deleted automatically; always included in bundles unless excluded.Cache and intermediates:
cache/,runs/*/work/**intermediates. Deletable;pruneimplements today’sautoprune.shpolicy.
3.2 Identifiers and provenance¶
Engine id
e-<12 hex>: the first 12 hex digits of the tree hash overyapnr/**/*.py, the native library hash and the flag registry. A run withengine = "workspace"freezes the working tree intoengines/at launch, using clones or hard links. Every run is therefore reproducible, and in-progress development can never change a running experiment. This is today’ssrcNNdiscipline, made automatic.Input id
i-<12 hex>: hash of the canonicalinputs.json.Profile hash: hash of the resolved flag environment.
Run id
r-<YYYYMMDD>-<HHMMSS>-<label>-<4 hex>. The suffix comes from the run key = hash(engine id, input id, profile hash, kind, params, seed).yapnr runwarns when an identical run key already exists.Candidate address:
<run-id>:<cand>[/<stage>], e.g.r-20261004-0930-h7-3f2a:p027/deep. Telemetry lane ids use the same form.Code keys: the feedback subsystem’s code-key provenance keeps working. Keys are rooted at
yapnr.and include the engine id.
run.json (schema yapnr-run-v1) records:
id,kind,label,spec(copy of the experiment TOML),params,seeds;engine(id andengine.json),inputs(id),profile(name, resolved env, hash);argv,after(dependency runs),parent(forresume);host(OS, CPU count, KiCad version; no host name);supervisor(pid, start-time identity),state(queued|running|paused|done|failed|stopped);started,ended,result(pointer toresult.json).
3.3 Manifest (yapnr.toml, schema yapnr-project-v1)¶
The source of truth is a dataclass model in yapnr/project/manifest.py. The JSON Schema
(yapnr/project/schema/project-v1.schema.json) is generated from it and pinned by a freshness
test. TOML is read with tomllib; manifests are only read by the controller, never by KiCad-side
workers.
schema = "yapnr-project-v1"
name = "splanc-mini"
description = "Splanc Mini main board"
[design]
frontend = "atopile" # "kicad" | "atopile" (plugin registry)
[design.atopile]
root = "../elec" # directory with ato.yaml, relative to the project
build = "mini"
entry = "src/splanc_mini.ato:SplancMini"
annotation_sources = ["src/splanc_mini.ato"]
# or ["ato", "build", "-b", "mini"]
build_command = ["bazel", "build", "//hardware/splanc_dev:splanc_mini"]
board_output = "../build/builds/mini/mini.kicad_pcb"
# [design.kicad] # kicad frontend instead:
# project = "design/led555.kicad_pro"
# annotations = "design/annotations.yaml"
[rules]
constraints = "rules/constraints.yaml"
electrical_fab = "rules/electrical-fab.json"
plane_access_fab = "rules/plane-access-fab.json"
fab_profile = "jlc-pofv" # built-in name, or a path to a profile file
[engine]
default_profile = "default"
[store]
path = ".yapnr" # override in yapnr.local.toml or YAPNR_STORE
[retention]
prune_intermediates_after = "2m" # after the run's evaluation.json appears
keep_live = "referenced" # "all" | "referenced" | "none"
[resources]
disk_pause_below_gb = 8
disk_resume_above_gb = 15
keep_awake = true # macOS caffeinate -w <pid>
[viewer]
title = "Splanc Mini"
docs = ["../../docs/hardware/mini.md"] # context offered to the optional agent
agent = false # off by default (owner decision)
Machine-level settings (KiCad paths, the path to the optional agent CLI, the default store root)
live in ~/.config/yapnr/config.toml or YAPNR_* variables. They never go in the committed
manifest.
Profiles (profiles/<name>.toml) replace env.json/env2.json. Machine paths are split out
into toolchain config:
schema = "yapnr-profile-v1"
extends = "default"
[flags]
PNR_POWER_FIRST = true
PNR_SHOVE = true
PNR_FEEDBACK = true
PNR_WORKER_TIMEOUT = 1800
Flags are validated against the registry (yapnr/core/flags.py: name, type, default, owning
module, status experimental|stable|deprecated, doc string). Unknown flags are an error. The
registry generates docs/engine/flags.md.
Experiment specs (experiments/<name>.toml) replace the imperative launch_*.sh scripts:
schema = "yapnr-experiment-v1"
# full | halving | hier | block-library | pair-bench | doe | regression
kind = "halving"
label = "h7"
inputs = "latest" # or an input id
engine = "workspace" # or an engine id
profile = "h6"
seeds = [6]
after = ["nb8"] # wait for these runs (by label or id)
[params] # passed to the kind's argv builder; validated per kind
candidates = 64
deep_seconds = 5400
library = "nb7h" # a library id or label (hier kind)
[flags] # per-experiment overrides on top of the profile
PNR_MACRO_HULL = true
[resources]
workers = 6
Each kind is a small plugin in yapnr/project/kinds/ that maps params to the engine entry
point (yapnr.native.full_iteration, yapnr.mc.halving, yapnr.hier.synth_native,
yapnr.native.paired_bootstrap, …) and knows how to summarize results into result.json. doe
needs a public replacement for the private synth_native._native that doe_block.py uses today.
3.4 CLI¶
All commands take -C <project> (default: nearest ancestor with yapnr.toml) and --json for
machine-readable output. Via Bazel: bazel run //:yapnr -- <args>, with relative paths resolved
against BUILD_WORKING_DIRECTORY.
yapnr init [DIR] --frontend kicad|atopile [--from-kicad PRO] [--from-atopile DIR --build B]Create the manifest,
rules/templates,profiles/default.toml, and a.gitignoreentry for the store.
yapnr doctorReport toolchain discovery (KiCad version, headless status,
pcbnewimport, Python, torch/numpy, native search library) and store health. PR0 ships a stub.
yapnr kicad make-headlessmacOS: create the background-only KiCad copy used for
kicad-cli.
yapnr import kicad <pro|pcb>Freeze a KiCad project into a new input set (portable
fp-lib-table, footprints copied, graph and rules dumped).
yapnr import atopile [--build B] [--board PCB]Run the configured build (or take a prebuilt board), parse annotations, freeze an input set.
yapnr import legacy-hier <dir>Register an existing
$HIER-style tree (engines fromsrc*, inputs, runs, block libraries, live telemetry, notes) read-only, by hard link when on the same volume.
yapnr inputs ls|show|diff A BInspect input sets.
yapnr snapshot create [--label L]|ls|verify ID|diff A BEngine snapshots.
yapnr run <experiment.toml> [--profile P] [--engine E] [--inputs I] [--label L] [--detach]Launch under a supervisor (pid identity, keep-awake, disk guard, telemetry dir, log).
yapnr run --kind K [params...]Ad-hoc run without a spec file (the spec is still recorded).
yapnr resume <run-id>Continue a stopped or failed run where the kind supports it.
yapnr status [<run-id>] [--watch]Today’s
status.sh: disk, load, per-run stage stats, block trial stats, deep evaluations.
yapnr stop|pause|continue <run-id>Signal only processes this store launched (verified by pid and start time).
yapnr ls runs|libraries|inputs|engines|baselinesList.
yapnr show <run-id>[:cand[/stage]]Details, and the objective vector
[violations, blocked, reference, subwidth, unqualified_pairs, unconnected].
yapnr compare <run|cand> <run|cand>...Objective and per-stage tables; engine, input and flag diffs between the runs.
yapnr prune [--dry-run] [--run R]Retention policy on intermediates (today’s
autoprune.sh).
yapnr gc [--cache] [--live] [--objects] [--dry-run]Telemetry and object GC (today’s
prune_live.py); never touchesuser/ornotes/.
yapnr pack <run-id>[:cand[/stage]] -o FPortable bundle (§3.5). Options:
--best,--with-telemetry,--without-user-state,--without-engine.
yapnr unpack <bundle> [-C project]Restore a bundle into a project store.
yapnr export kicad <run-id>[:cand[/stage]] -o DIR [--zip] [--with-3d]Standalone KiCad project (§3.6).
yapnr baseline add <edited.kicad_pcb> --from <run:cand>Re-import a hand-edited board as a new baseline input set (§3.6). Options:
--lock none|placement|copper,--name N.
yapnr viewer [--run R] [--port N] [--listen ADDR]Serve the live viewer for this project (loopback by default).
yapnr notes ls|export|show NDesign notes.
yapnr drc <pcb>Native KiCad DRC through the discovered toolchain (headless).
yapnr review <run:cand>PDF/PNG review export (optional dependencies).
3.5 Save and restore (bundles)¶
Format: a deterministic
tar.gz(sorted entries, mtime 0, uid/gid 0; stdlib only) named*.yapnr.tgz, containing:bundle.json(schemayapnr-bundle-v1: bundle kind, project name, the objects list with sha256 and size, run/input/engine ids);objects/,inputs/<id>/,engines/<id>/(unless--without-engine);runs/<id>/{run.json,result.json,status.json,logs/}plus the selected candidate’s stage directory;optionally
live/<id>/,user/,notes/.
Restore verifies every hash, imports objects, registers ids, and refuses id collisions with different content. User state is merged by id, never overwritten. A restored run can be viewed, compared, exported and resumed like a local one.
Portability: everything inside a bundle is relative. Absolute paths are rejected at pack time (the privacy scanner is reused).
3.6 KiCad portability¶
yapnr export kicad produces a standalone project that opens on another machine:
<name>.kicad_pcb(from the object store) and<name>.kicad_pro(from the input set, with net classes and rules);<name>.kicad_dru(written bycore.fab_profilefor the project’s profile);fp-lib-tablewhose URIs are all${KIPRJMOD}/footprints/<nick>.pretty, with the used footprints copied (reusinglibrary_table --portable);3D models:
--with-3dcopies them to${KIPRJMOD}/3dmodels/and rewrites model paths; otherwise stock${KICAD10_3DMODEL_DIR}references are kept;yapnr-provenance.json(run, candidate, engine id, input id, flags);kicad-cli pcb drcis run in the exported directory and its report is included. The kicad CI lane asserts the export DRC matches the run’s final DRC.
yapnr baseline add closes the loop after a human edits a board in KiCad:
Validate that the netlist matches the source input set (components, pads, nets). Differences are reported; ECOs require re-importing from the frontend (
--allow-netlist-changeonly for renames the frontend can explain).Diff against the source candidate: moved, rotated or flipped parts; added or removed tracks, vias and zones.
Create a new input set with the edited board as the seed.
--lock placementemits fixed placement constraints.--lock copperkeeps the hand copper as fixed copper (fixed_copper).Record lineage in
baselines/<id>.json.yapnr run --inputs <new id>continues from it.
4. Bazel¶
4.1 Module¶
.bazelversion is 7.7.1 (same as Splanc) so yapnr can be a bazel_dep of Splanc; both are
bumped together later. .bazeliskrc is copied. MODULE.bazel.lock is committed and CI runs with
--lockfile_mode=error.
PR0 declares only rules_python; the other dependencies arrive with the PRs that use them:
module(name = "yapnr") # no version since PR-R: the release tag is the version (docs/releases.md)
bazel_dep(name = "rules_python", version = "2.0.3") # PR0
bazel_dep(name = "bazel_skylib", version = "1.9.0") # PR5 (string_flag)
bazel_dep(name = "rules_shell", version = "0.6.1") # PR6a (sh_test runner)
bazel_dep(name = "rules_rust", version = "0.71.3") # PR5
# release tarballs, later:
bazel_dep(name = "rules_pkg", version = "1.2.0", dev_dependency = True)
# viewer JS tests: not added in PR4 (no node toolchain; browser checks move to tests/e2e)
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(python_version = "3.11", is_default = True)
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
hub_name = "yapnr_pypi", # must be unique across modules; Splanc uses "pypi"
python_version = "3.11",
requirements_lock = "//:requirements.lock",
# PR6a: requirements_by_platform adds //:requirements_linux_x86_64.lock (CPU torch)
)
use_repo(pip, "yapnr_pypi")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021", versions = ["1.85.0"]) # root-first: Splanc's wins
use_repo(rust, "rust_toolchains")
register_toolchains("@rust_toolchains//:all")
kicad = use_extension("//bazel:extensions.bzl", "kicad") # PR8
kicad.autodetect(name = "yapnr_kicad")
use_repo(kicad, "yapnr_kicad")
register_toolchains("@yapnr_kicad//:all")
Python deps (
requirements.in):torch>=2.2,<2.4(CPU): Splanc’s ceiling, and the experiment environment runs torch 2.3.1 with numpy 1.26 (numpy 1 ABI). It is not what keeps the aarch64 lock CUDA-free: torch 2.4 to 2.9 also restrict theirnvidia-*andtritondependencies to x86_64;numpy>=1.26,<2(torch 2.3 wheels use the numpy 1 ABI, and the experiment environment runs numpy 1.26); a unit test checkstorch.as_tensor(numpy.zeros(3)).numpy();pyyaml>=6;the docs stack at Splanc’s locked versions:
sphinx9.0.4,myst-parser5.1.0,furo2025.12.19,sphinx-copybutton0.5.2,sphinx-design0.7.0,sphinxcontrib-mermaid2.1.0.matplotlibis added when a page needs generated figures.
The lock is generated on the development Mac (darwin-arm64) with the hermetic Python 3.11 toolchain (
bazel run //:requirements.update). It is also valid on linux-aarch64, which is why the requiredtestjob runs onubuntu-24.04-arm; it is not valid on linux-x86_64 (the x86_64 torch wheel needs CUDA libraries the lock does not list).//:requirements.testchecks freshness; it needs the network, is taggedmanual, and runs in exactly one CI job (test).Parity risk: Splanc’s Bazel lock pairs torch 2.3.1 with numpy 2.4.6, while the experiment venv runs numpy 1.26.4 on Python 3.12. yapnr’s lock follows the experiment venv (numpy 1.26). PR6a runs the regression ladder under both the experiment venv and the lock and records any difference before experiments move to the Bazel environment.
No Nix. yapnr does not need atopile, and KiCad comes from the toolchain (below). If Nix is ever added (for example a KiCad 11 canary), its extension usages must be
dev_dependency = True, becauserules_nixpkgstags are root-module-only. Splanc had to patchrules_atopilefor exactly this.
.bazelrc:
Kept from Splanc:
the
startupJVM flags (the SVE workaround);--enable_bzlmod, the BCR registry, in-tree repository and disk caches;--incompatible_strict_action_env,--symlink_prefix=bazel-;--test_output=errors, colour, timestamps.
Dropped:
workspace_status_command, the Vite strategy, the memory caps,build:td, isolated extension usages.Added in PR0:
test --test_tag_filters=-kicad(the default lane needs no KiCad);test:quick --test_tag_filters=-kicad,-slowandtest:all --test_tag_filters=;build:lowmem --jobs=2 --local_resources=cpu=2 --local_resources=memory=HOST_RAM*.25(for the shared, busy Mac; run niced, one Bazel server at a time);common:ci --lockfile_mode=error;try-import %workspace%/user.bazelrc(per-user output base and defaults).
Added in PR6a:
test:kicad --test_tag_filters=kicadplus--test_env=pass-through forYAPNR_KICAD_CLI,YAPNR_KICAD_PYTHONandYAPNR_REQUIRE_KICAD(strict action env strips them otherwise).
4.2 Targets¶
Libraries: one
py_libraryper subpackage://yapnr/core,//yapnr/place,//yapnr/route,//yapnr/native,//yapnr/kicad,//yapnr/shove,//yapnr/hier,//yapnr/mc,//yapnr/feedback,//yapnr/runtime,//yapnr/frontends/...,//yapnr/project,//yapnr/diagnostics,//yapnr/review,//yapnr/viewer. In Splanc,hier/,mc/andfeedback/are in no target at all. PR0 has//yapnr(version and CLI).//yapnr:headless(py_library): everything exceptviewerandreview; what the ruleset uses.//yapnr/kicad:kicad_side(filegroup): generated from the import closure; the closure test pins it.//:yapnr(alias of//yapnr:cli):bazel run //:yapnr -- <command>. PR0.//:doctor(alias of//yapnr:doctor):bazel run //:doctor. PR0.//:viewer(alias):bazel run //:viewer -- --project <dir> [--run R].//yapnr/viewer:dev(py_binary): viewer with auto-reload on a dev port; never the deployed ports.//yapnr/kicad/workers:region(py_binary): worker entry points are also runnable for debugging.//native/search:pnr_search(rust_shared_library,.so/.dylib) and//native/search:search_test(rust_test).//native:search_backend(skylibstring_flag,python|rust): defaultpythonuntil PR5 proves equivalence.//tests/unit/...: onepy_testper file, generated by theyapnr_py_tests()macro (tools/bazel/py_tests.bzl) from a glob; the wiring test fails on orphans. PR0.//tests/kicad/...:kicad_py_testper file, tagkicad(below).//tests/regression:smoke(py_test, tagskicad,slow): one design, one seed, bounded time.//tests/regression:ladder(py_binary,manual):bazel run //tests/regression:ladder -- --designs all --seeds 1,2(16 cases).//examples/led555:pnr,:pnr_test,:kicad_project: ruleset targets that exercise the downstream API in-repo (PR8).//docs:build,//docs:serve(py_binary): as in Splanc. PR0.//:requirements.updateand//:requirements.test(compile_pip_requirements, taggedmanual). PR0.//tools:privacy_scan,//tools:check_test_wiring(py_binary): the repo checks as runnable tools. PR0.//tools/migrate:port(py_binary): ports an in-flight snapshot through the format and rename commits (§7.4).
kicad_py_test is a macro that expands to an sh_test running tools/kicad_test_runner.sh:
locates KiCad Python through the toolchain or
YAPNR_KICAD_PYTHON;sets
PYTHONPATHto the runfiles copy of//yapnr/kicad:kicad_sideplus the test;sets
HOME=$TEST_TMPDIRso KiCad writes its settings in the sandbox, not the user profile;runs
-m unittest <module>.
Without KiCad it prints a skip notice and exits 0, unless YAPNR_REQUIRE_KICAD=1, in which
case it fails. That setting is used in CI, so there are no silent skips.
4.3 Downstream headless ruleset (API designed now, implemented in PR8)¶
“Headless” means: no viewer, no agent, no GUI, no hand-set environment, time-bounded actions. The
rules depend only on //yapnr:headless, the pip hub and the KiCad toolchain.
load(
"@yapnr//bazel:defs.bzl",
"yapnr_board",
"yapnr_fab",
"yapnr_kicad_project",
"yapnr_pnr",
"yapnr_pnr_test",
"yapnr_project_run",
)
# -> YapnrBoardInfo; freezes an input set as build outputs
yapnr_board(
name = "mini_board",
pcb = ":splanc_mini.kicad_pcb", # any .kicad_pcb (Splanc exposes the atopile layout's pcb)
project_file = None, # optional .kicad_pro (net classes)
footprints = glob(["elec/src/parts/**/*.kicad_mod"]),
frontend = "atopile", # "kicad" | "atopile"
annotation_sources = ["elec/src/splanc_mini.ato"], # atopile frontend
annotations = None, # kicad frontend: annotations.yaml
constraints = "mini-constraints.yaml",
electrical_fab = "mini-routing-electrical-fab.json",
plane_access_fab = "mini-plane-access-fab.json",
fab_profile = "jlc-pofv",
)
# -> YapnrResultInfo + OutputGroupInfo(kicad_project, reports)
yapnr_pnr(
name = "mini_pnr",
board = ":mini_board",
strategy = "full", # "full" | "halving" | "hier"
profile = "pnr/profiles/production.toml", # or flags = {...}; validated by the registry
flags = {},
seed = 1,
time_budget_s = 5400, # hard cap; enforced by the supervisor
placement_rounds = 6,
workers = 4,
library = None, # optional block library (hier strategy): a yapnr_pnr output or files
tags = ["manual", "requires-kicad"],
)
# outputs: mini_pnr.kicad_pcb, .evaluation.json, .drc.json, .provenance.json,
# mini_pnr.report/
yapnr_pnr_test(
name = "mini_pnr_gate",
result = ":mini_pnr",
max_violations = 0,
max_unconnected = 0,
require_qualified_pairs = True,
)
# gerbers, drill, BOM, CPL zip
yapnr_fab(name = "mini_fab", result = ":mini_pnr", vendor = "jlcpcb")
# §3.6 as a build output
yapnr_kicad_project(name = "mini_kicad", result = ":mini_pnr", with_3d = False)
# bazel run //...:h7 -> non-hermetic long experiment writing into the project store
yapnr_project_run(
name = "h7",
project = "pnr/yapnr.toml",
experiment = "pnr/experiments/h7.toml",
)
Plus a convenience macro yapnr_board_pnr(name, ...) = board + pnr + test + fab, which matches
today’s atopile_pnr (so Splanc’s splanc_mini.fab can be expressed in one call).
Providers:
YapnrBoardInfo(input_set_dir, pcb, graph, rules, annotations, footprints);YapnrResultInfo(pcb, evaluation, drc, provenance, report_dir).
Toolchain:
@yapnr//bazel/toolchains:kicad_toolchain_type; rulekicad_toolchain(cli, python, version, env).The module extension
kicad.autodetect()generates a local toolchain using the same order asyapnr.kicad.toolchain.A consumer can instead register its own. Splanc could bind its Nix KiCad, but Splanc’s Nix pins give KiCad 9.0.1, which cannot read KiCad 10 boards. See the Splanc import-back risks.
Actions are tagged
requires-kicadandno-remote, keeplocalas today’s rules do, and pass no repository path. Worker staging happens inside the action’s output tree.Compatibility constraints:
Bazel 7.7.1;
a unique pip hub name;
Rust code compiles with rustc 1.85 (
search.rsdoes);no root-only extension tags outside
dev_dependency;the Python 3.11 toolchain is requested (the root’s default wins).
Consumption:
bazel_dep(name = "yapnr", version = "0.0.0")plus agit_overridewithmodule_name = "yapnr",remote = "https://github.com/Studio-Fug/yapnr.git"and a pinnedcommit. For local co-development:--override_module=yapnr=<local clone>.
5. CI and presubmit¶
5.1 Presubmit (prek, same pins as Splanc)¶
.pre-commit-config.yaml keeps Splanc’s hooks and versions:
black 25.1.0
--line-length=100;isort 6.0.1
--profile=black --line-length=100;flake8 7.0.0, configured in
.flake8(max-line-length = 120,extend-ignore = E203);shellcheck-py v0.9.0.6
--severity=warning;buildifier and buildifier-lint 8.2.0;
mirrors-prettier v3.1.0 (markdown/json/yaml);
markdownlint-cli v0.38.0
--fix(100 columns; tables and code blocks exempt);pre-commit-hooks v4.5.0: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files
--maxkb=600, check-merge-conflict, check-case-conflict, check-json, debug-statements, name-tests-test--pytest-test-first.
Changes from Splanc:
Dropped:
nixpkgs-fmt(no.nixfiles), the container-overlay local hook, and Splanc-specific excludes.Global exclude reserved for
^(docs/hardware/|third_party/)(hardware/too until PR3a): the verbatim Splanc import (until PR7) and vendored upstream files. No other paths go there.check-added-large-filesexcludesthird_party/explicitly (vendored files are size-reviewed when vendored).Added local hook
yapnr-privacy-scan(tools/privacy_scan.py, stdlib only) with generic patterns:absolute home paths (macOS
/Users/<name>in any case, Linux/home/<name>, Windows with either slash, WSL), volume paths (macOS volumes, Linux removable media), macOS per-user temporary directories, and the dash-encoded form agent tooling uses for project directories (-Users-<name>-...), with placeholders such as<name>allowed only as a whole component;*.ts.nethost names and hyphenated or URL*.localmachine names;100.64.0.0/10 (CGNAT, tailnet) and RFC 1918 private addresses;
e-mail addresses other than
users.noreply.github.com,noreply/no-replymailboxes, the git user of code hosts and reserved example domains (the allowlisted commit addresses are findings in files too);API keys and tokens (GitHub, Anthropic, OpenAI, AWS, Slack, Google, GitLab, Tailscale) and private-key blocks.
The scanner allowlists itself, its test, this plan and the commit address allowlist (
tools/privacy/allowed_identities.txt), supports aprivacy-scan: allowline marker, and redacts what it prints (CI logs are public). The same scanner runs as a Bazel test over the whole working tree (//tests/unit/repo:test_privacy_scan), including the globally excluded paths, and in CI over the messages and patches of every new commit, where the file allowlist does not apply (so the allowlisted files must scan clean as patches too) and the allowlisted commit addresses pass (every commit header carries them). Its--identitiesmode is the commit identity gate: one address per line, and only the addresses intools/privacy/allowed_identities.txt(the owner’s public commit address), GitHub noreply addresses andnoreply@github.compass.name-tests-test(--pytest-test-first) applies totests/; helpers live intools/. When the regression helpers (tests/regression/{designs,run,native}.py) and thetests/fixtures/*.pyhelpers land (PR3b), either add a narrow per-hook exclude for exactly those files (Splanc has one) or move the helpers out oftests/.Lint baseline for imported code. Until PR3a,
hardware/anddocs/hardware/(the verbatim import) are skipped by all hooks through the reserved global exclude (and by.flake8extend-excludeand.markdownlintignore). PR3a formats the Python underhardware/and dropshardware/from the global exclude and from.flake8; PR3b moves the code out; PR7 splitsdocs/hardware/into the docs pages. The measured debt before PR3a: about 1,378 lines over 120 characters, about 2,000 lines with;joins, and 149 real F-codes in the committed code, including one genuineF821invia_coalesce.py(items.clear()inmain). After black and isort, 479 findings in 157 files remain (F401 112, E731 106, E741 64, E402 63, E501 60, F841 46, F811 17, F402 6, E721 2, F821 1, F541 1, E262 1). PR3a fixes none by hand (every fix changes a syntax tree, and inside the evaluation modules a code key);.flake8lists each file’s codes inper-file-ignores, and that list only shrinks. The non-Python files underhardware/keep per-hook excludes where a hook would rewrite or reject them (prettier, markdownlint, buildifier, and the whitespace hooks for test data and the built viewer bundle); the comment in.pre-commit-config.yamllists them.
setup-precommit.sh follows Splanc’s (prek 0.4.12, then prek install and
prek run --all-files), except that prek is installed in isolation: a prek on PATH is used
only at the pinned version (with a warning otherwise), and else the pinned version goes into a
private virtualenv under .venv/prek (with uv when available), never into the system Python.
5.2 GitHub Actions¶
ci.yaml triggers on push: main, pull_request (opened, synchronize, reopened, closed) and
workflow_dispatch. It has top-level permissions: contents: read; only the Pages jobs widen it,
for themselves. concurrency groups runs per PR (or ref) with cancel-in-progress for pull
requests. A started run on main is never cancelled, but GitHub keeps one pending run per group,
so a queued main run is replaced by a newer one. Every job has a timeout-minutes.
Jobs in ci.yaml:
lint(ubuntu-latest; required): Splanc’s job (setup-python 3.11,pip install prek==0.4.12, prek cache keyed on the config hash,prek run --all-files --show-diff-on-failure), plus two checks over the new commits (the PR’s range, or the pushed range onmain):the commit identity check:
git log --format='%ae%n%ce' <range>piped throughtools/privacy_scan.py --identities, so only the owner’s public commit address (listed intools/privacy/allowed_identities.txt), GitHub noreply addresses andnoreply@github.com(GitHub’s committer for web merges) pass;the history scan:
git log -p --diff-merges=separate --format='%ae %ce%n%B' <range>piped throughtools/privacy_scan.py --stdin, which covers commit messages and intermediate commits (merge-commit PRs land them all onmain), and the diff of every merge commit against each parent (plaingit log -pshows none;-mwould depend onlog.diffMerges).
test(ubuntu-24.04-arm; required):bazel test //... --config=ci, thenbazel test //:requirements.test(the one job that checks the lock). The aarch64 runner matches the CPU-only torch lock. Moves to ubuntu-latest once the x86_64 lock exists, if faster.kicad-integration(ubuntu-latest,container: kicad/kicad:10.0.6@sha256:<digest>,--user root; required from PR6a): install bazelisk andbuild-essential(linker for rules_rust);YAPNR_KICAD_CLI=/usr/bin/kicad-cli,YAPNR_KICAD_PYTHON=/usr/bin/python3,YAPNR_REQUIRE_KICAD=1;bazel test --config=ci --config=kicad //...including the regression smoke and the example end to end.docs(ubuntu-latest; required):bazel run //docs:build, upload the site as an artifact.deploy-preview/cleanup-preview(ubuntu-latest; not required):rossjrw/pr-preview-action@v1intogh-pagesunderpr-preview/pr-N/; the preview only afterlint,testanddocspass (gh-pageskeeps removed previews in its history);if: vars.YAPNR_PAGES_ENABLED == 'true'and a same-repository head; job-levelcontents: writeandpull-requests: write; a per-PR concurrency group.deploy-pages(ubuntu-latest; not required): on push tomainand on a manual run onmain(the first deploy, which createsgh-pages), afterlint,testanddocs:JamesIves/github-pages-deploy-action@v4withclean-exclude: pr-preview/andforce: false; same gate; job-levelcontents: write; its own concurrency group. Publishing by pushing togh-pagesneeds neitherpages:norid-token:permissions.The Pages jobs use separate concurrency groups because GitHub cancels an older pending job in a group even with
cancel-in-progress: false; pushes from different groups race, and both actions rebase a rejected push instead of forcing it.downstream(ubuntu-24.04-arm; required from PR8):bazel test //...intests/downstreamwith--override_module=yapnr=$GITHUB_WORKSPACE.
macos.yaml (not required):
test-macos(macos-latest; PRs andmain):bazel test //... --config=ci(darwin wheels, later the.dylibbuild). PR0.macos-kicad(macos-latest; nightlyscheduleandworkflow_dispatch; PR6b):brew install --cask kicad(10.0.6; cache the Homebrew download dir), the headless copy,bazel test --config=kicad //..., the full regression ladder, results uploaded as an artifact.
Why the official Docker image for the required KiCad lane:
It is the same KiCad version as the development Mac (10.0.6, published 2026-09-22), pinned by digest.
It is free and pulls quickly (about 0.8 GB compressed).
It ships
kicad-cliand apcbnew-enabled Python 3.13, so it exercises 3.9-to-3.13 compatibility of the KiCad-side code.
Alternatives considered:
The Ubuntu PPA: slow install with large dependencies.
nixpkgs
kicad-small: needs Nix and root-only module tags, has a darwin gap, and Splanc’s pins give 9.0.1.The Homebrew cask: 1.4 GB and macOS minutes; kept as the nightly parity lane with the bundled Python 3.9.
A self-hosted runner on the development Mac: rejected because yapnr is a public repository.
Caching:
bazel-contrib/setup-bazel@0.15.0without its own caches (their keys carry neither the CPU architecture nor the lock files, and its Bazelisk cache path is not where.bazeliskrcpoints). Instead, oneactions/cacheentry for Bazelisk’s downloads (BAZELISK_HOMEset in CI) and the repository cache, keyed on OS, architecture,.bazelversion,MODULE.bazel,MODULE.bazel.lockandrequirements.lock, plus a per-job disk cache keyed on the locks;the prek cache;
the Homebrew download cache (nightly).
Nix caching and runner-space actions from Splanc are not needed.
Repository settings (applied with gh after PR0 is pushed):
Squash merge is the default. Merge commits are allowed only for the history-import PRs (PR1, PR2) and for PR3b (its pure-move commit must survive). No linear-history rule.
A ruleset on
mainrequires a PR and the checkslint,test,docs(andkicad-integrationfrom PR6a), with 0 approvals (single-seat organization)..github/CODEOWNERS:* @fughilli.A PR template checklist:
tests run and which tier;
new behaviour behind a default-off flag;
measured result;
privacy scan and commit identity;
docs and WORKLOG updated.
Pages source: the
gh-pagesbranch root. Publishing is approved; untilYAPNR_PAGES_ENABLEDistruethe deploy jobs are skipped. The first deploy, after the first green build ofmain: set the variable, run CI manually onmain(this createsgh-pages), then point Settings > Pages atgh-pages,/ (root).
6. Documentation¶
Sphinx with MyST, adapted from Splanc:
docs/build_docs.pystages the tree from$BUILD_WORKSPACE_DIRECTORY, mirroring the repository layout so that links between Markdown files resolve on GitHub and on the site alike;docs/index.mdis the root document and the site root redirects to it._make_jekyll_safe()is copied verbatim (needed forpr-preview/subdirectories) and writes.nojekyll.docs/_sphinx/conf.py:extensions:
myst_parser,sphinx_copybutton,sphinx_design,sphinxcontrib.mermaid; plainmermaidcode fences are treated as the directive, so diagrams render on GitHub too;theme
furowith the brand logos (light_logo/dark_logo), the SVG favicon and the palette as accent colours;source_repository= the yapnr GitHub URL;copyrightnamesAGPL-3.0-or-later;mermaid_version = "11.4.1".
Warnings start non-fatal as in Splanc and become
-W --keep-goingonce PR7 lands.PR7 may add
sphinx-autoapifor a static API reference that parses source without importing torch orpcbnew(new dependency; recorded indocs/decisions.md).
PR0 ships index.md, architecture.md (placeholder), decisions.md, about-the-name.md and this
plan. The target structure:
docs/
├── index.md what yapnr is; pipeline diagram (mermaid); where to start
├── getting-started/ install.md, quickstart.md (examples/led555),
│ toolchains.md (KiCad discovery, headless macOS copy, doctor)
├── architecture/ overview.md (place -> route -> native electrical -> DRC ->
│ evaluation -> feedback), interpreters.md (hermetic controller vs
│ KiCad-Python workers, staging, timeouts), data-model.md (BoardGraph,
│ rules.json, evaluation objective), telemetry.md (yapnr-live-event-v1)
├── project/ concepts.md, manifest.md (generated reference), store-layout.md,
│ lifecycle.md, experiments.md (kinds, profiles, flags), bundles.md,
│ kicad-portability.md
├── cli/ reference.md (generated from argparse; freshness-tested)
├── engine/ placement.md, global-routing.md, native-phases.md (00-placement ...
│ 09-final-audit), diff-pairs.md, planes-and-power.md, vias.md,
│ shove.md, hierarchy.md, mc-halving.md, feedback.md,
│ contracts-and-annotations.md, fab-profiles.md, drc.md,
│ native-search-backend.md, flags.md (generated from the registry)
├── frontends/ atopile.md, kicad.md, annotations.md (canonical schema),
│ writing-a-frontend.md
├── viewer/ overview.md, api.md, services.md, notes-and-agent.md, deploying.md,
│ license-notice.md
├── bazel/ targets.md, ruleset.md (§4.3 as reference), downstream-splanc.md
├── development/ contributing.md, developers.md, testing.md (tiers, tags), ci.md
├── decisions.md owner decisions and the pinned-versions table (as in Splanc)
├── adr/ NNNN-*.md architecture decision records (below)
├── history/ splanc-origins.md, import-manifest.md (§7)
├── about-the-name.md the name and the mark
└── migration-plan.md this document
Seed ADRs, written from the Splanc handoff log in scrubbed form (no board-private data, no machine details):
Candidate selection is mechanical (Monte Carlo and successive halving). No hand-picked seeds.
Hierarchical layout: blocks become macros, a library per template, top-level assembly.
Power-first placement: tiers, trunks and hot-loop terms are derived mechanically.
Nothing in the loop may be GUI-bound.
kicad-cliruns headless. Every worker is time-bounded (proc.py).Native KiCad DRC is the judge: final cold DRC and a content-addressed DRC cache.
Fab profiles: the default is JLC 4-layer with epoxy-filled, capped vias; filled via-in-pad.
Electrical contracts are source annotations and are never relaxed without the designer.
Differential pairs:
max_uncoupled_mmapplies per continuous uncoupled run; stub rules.Routing-to-placement feedback with code-key provenance.
Shove support.
KiCad object lifetime:
Deleterather thanRemovefor detached items (the teardown SIGSEGV fix).Design-notes workflow (N-000x proposals accepted by the designer).
Planned: macro shrink and hull (N-0001),
@pnr-inductance(N-0002), noise budgets (N-0003), SPICE-verified signal-integrity requirements.A learned placement policy is deferred; keep the data hooks.
docs/hardware/pnr-system.md and pnr-inputs.md come over with history in PR1 and are split into
the pages above in PR7.
7. History import strategy¶
7.1 What is imported with history¶
git filter-repo runs on a scratch clone:
git clone --no-local --single-branch --branch splanc-mini $SPLANC $SCRATCH/splanc-filter. It
includes the local commits not yet pushed to Splanc. The Splanc working tree is never touched.
Paths kept:
hardware/pnr/ (40 commits since 2026-08-21)
hardware/tools/keyhole_region.py
hardware/tools/keyhole_repair.py keyhole_loop.py keyhole_shift_via.py keyhole_deform.py
hardware/tools/audit_pair_contacts.py (if tracked) scan_via_proximity.py
hardware/tools/export_mini_review.py mini_review_annotations.py rasterize_mini_review.py
hardware/tools/verify_mini_review.py review_mini_contacts.py freeze_mini_inputs.py
hardware/tools/consolidate_ground.py optimize_plane_access.py plane_access_trial.py
hardware/tools/export_plane_access_clusters.py export_pnr_congestion.py
hardware/tools/render_pnr_congestion.py
hardware/tools/export_elastic_experiment.py render_elastic_experiment.py watch_pnr_pdf.py
hardware/tools/pnr_live/ hardware/tools/pnr_viewer/
docs/hardware/pnr-system.md docs/hardware/pnr-inputs.md
hardware/experiments/tscircuit-mini/geometry105/rust/search.rs
hardware/experiments/tscircuit-mini/geometry105/build-rust.sh
hardware/experiments/tscircuit-mini/geometry105/test_search_equivalence.py
Not imported:
Splanc board tools (
place_splanc_mini.py,check_splanc_power.py,check_mini_eol.py, EOL generators, atopile helper scripts,package_pcb_models.py);hardware/tools/BUILD.bazel(shared with the atopile tools; rewritten);hardware/experiments/**/runtime/**(997 MB of stale engine copies);pnr.bzl’s consumers inhardware/splanc_dev.
7.2 Rewrites during filtering¶
--mailmap(a file kept in$SCRATCH, never committed) maps every identity to the owner’s public commit address, the one listed intools/privacy/allowed_identities.txt(owner decision, Q2 in §9): the machine-local (tailnet-derived) addresses, the other personal addresses and the noreply addresses alike, each with the matching name (the owner’s name for the owner’s identities,Claude Agentfor agent identities). Co-Authored-By trailers are preserved.--replace-textand--replace-message(a patterns file in$SCRATCH, never committed) replace:tailnet host names with
tailnet-host.example;tailnet (CGNAT) addresses with the documentation address
192.0.2.1;absolute volume and home-directory prefixes with
<repo>and~.
Blobs already public in Splanc are scrubbed too, so yapnr never re-publishes them.
A
--commit-callbackappendsImported-From: fughilli/splanc@<original sha>for provenance.--prune-empty always.
Gate: before the filtered history is ever pushed, two checks must pass on it:
git log -p --all | tools/privacy_scan.py --stdinis clean;git log --all --format='%ae%n%ce' | tools/privacy_scan.py --identitiespasses (every imported commit carries the owner’s public commit address).
The scan report (counts only) goes into docs/history/import-manifest.md together with the paths
list, source commit, filter-repo version and the commit map size.
The filtered branch is fetched into $YAPNR and merged with
git merge --allow-unrelated-histories onto PR0’s main. PR1 must be merged with a merge
commit (squash would destroy the history). A follow-up commit in the same PR adapts
BUILD.bazel loads to @yapnr_pypi and adds a minimal hardware/tools/BUILD.bazel. Files stay
at their Splanc paths until PR3b, so blame and git log --follow survive the later git mv. The
test-wiring check keeps its tests/ scope; hardware/ is wired when PR3 moves the engine’s tests
under tests/, and the manifest counts the unwired files of the import until then (decided in PR1,
see docs/decisions.md; this replaces a shrink-only baseline for hardware/).
7.3 Landing the uncommitted engine state (PR2)¶
Snapshots are read-only sources. Each one is copied into $SCRATCH, checked against an engine
digest (sha256 over the sorted path<TAB>sha256 lines of hardware/pnr and the engine tools,
without __pycache__; the snapshots’ hashes.json files are F217’s copied along, so they verify
only the source freezes), scrubbed (§2.5, Appendix C), and then committed. Each snapshot commit’s
engine tree equals its snapshot; side lines join through merge commits. What was imported, the
digests and the checks are in the import manifest.
The lineage as verified by content on 2026-09-30 (it corrects this section’s first version):
main ─ F217 ─┬─ src8b ─ src10.frozen ─ src10 ─ src11 ─ src12 ─┬─ src12h ─┬─ src12i ─ src14 ─┐
│ │ └─ src12n ────────┤
│ └─ src12b ─ src13.r8 ────────┤
│ src15.r1 (merge) ──┘
│ src15
└─ Electrical221 ───────────────────────────────────────── merge, Bazel, docs
F217, the fresh217 freeze of Splanc’s working tree minus Electrical221: the Codex agent’s uncommitted work on top of
b009c945(pair_joint,portal_retry,power_bank_*,transaction_cleanup, cost capture, relative rows, …), andhardware/tools/audit_pair_contacts.py.Electrical221 (the fresh222 freeze, byte-identical to Splanc’s working tree): the full electrical pool, partial-cycle cleanup in
track_graph/via_coalesce, apnr.bzlbranch and 3 tests. It forks from F217 and is in no hierarchical snapshot.src8b: everything from
src2tosrc8(their snapshots are gone):fab_profile(defaultjlc-pofv),fanout_reserve,via_in_pad,power_topology,proc(worker timeouts),place/power_first, and the packageshier/andmc/.src10.frozen: the
shove/package (PNR_SHOVE). It precedessrc10.src10: the owner’s 0.15 mm stretch floor in
shove/world.py.src11: the routing-to-placement feedback loop,
feedback/(PNR_FEEDBACK).src12: macro-first
place/legalize.py; pair budget overrides inpaired_bootstrap.py.src12h:
PNR_KICAD_CLI(headless KiCad) in 5 files.src12i:
fanout_reserve.releaseusesboard.Delete(zone)(teardown SIGSEGV fix).src14: SI v1 (
PNR_SI,pnr/si/,si_models/) and the terminal-width contract.src12b and src13.r8 (side line from src12): the USB pair engine (A-D), all flags default off.
src12n (side line from src12h): N-0001 macro shrink, per-side hull, used-area ranking.
src15.r1: the octopus merge of src14, src13.r8 (base src12) and src12n (base src12h), plus headless KiCad defaults and bus-type classes (
PNR_BUS_CLASSES); src15: its review fixes, the engine of the H7 run.Merge of Electrical221 into src15. Two conflicts: the summary dict at the end of
via_coalesce.main(keep both key sets) and two hunks ofpnr.bzl(Electrical221’s pool branch with src15’s headless$_KI_CLI; the SI tool added to the action tools).Bazel adaptation, then the docs.
Engine-identical directories (src9.frozen, src10.base, src10b, src11.base,
src11.frozen, src12.base, src12n.base, src14.base, src15.base, src13.base, src13)
produce no commit.
Author:
Claude Agentwith the owner’s public commit address (owner decision). The committer date is the import time; the author date is the capture time or the snapshot’s newest source mtime. Trailers: the session’s; the F217 and Electrical221 bodies name the Codex agent.Each commit body names the snapshot and its digest, the flags it introduced (and their defaults) and the measured results from the log (for example deepS 33/0, H6 12/0).
The Splanc design (
hardware/splanc_devoutside the engine’s own test data,contracts/) is never included.The four files with machine paths (
testdata/power_topology/{converter,pd}.json,tests/test_shove{,_native}.py) are scrubbed when each snapshot is staged, so no commit carries the paths. (The first version of this plan scrubbed the fixtures in a separate commit, which would have published the paths in history.)Only the head builds in yapnr: the snapshot commits carry Splanc’s
BUILD.bazel, as PR1’s imported commits do.Merged with a merge commit.
PR2e: board.Remove audit (separate). Classify the ~25 remaining board.Remove call sites:
use Delete unless the item is re-added. via_coalesce’s “retry after signal 11” is a likely
instance of the same class. Done after PR2 in the engine hygiene change, together with the worker
time bounds and the Electrical221 gate (decisions); the retries stay as a second
line of defence.
Engine work that starts after PR2 is made on yapnr branches; work still in Splanc snapshots after
PR3a/PR3b have landed goes through //tools/migrate:port (§7.4).
7.4 Porting in-flight work through mechanical commits¶
Formatting and renaming are deterministic functions f. For an in-flight snapshot B built on
base P, the change to apply after the mechanical commit is f(B) - f(P), where f(P) is exactly
what main holds. bazel run //tools/migrate:port -- --base <P dir> --tip <B dir>:
copies both trees;
applies the same module map, import codemod, black and isort to each;
emits a patch against
main.
The tool runs in CI on a fixture pair so it cannot rot.
7.5 Keeping Splanc’s running experiments unaffected¶
Read-only access to
$SPLANCand$HIER. All filtering, cloning and diffing happens in$SCRATCHor$YAPNR. Snapshot trees are cloned before reading.Never stop, signal or
reniceexperiment processes. The deployed viewers keep running from$HIER/viewer. Any yapnr viewer instance uses its own ports and reads copies or a store created byyapnr import legacy-hier(hard links, no writes to the source).Heavy work runs niced with
--config=lowmem, one Bazel server at a time, with the output base on the internal disk, and not while a deep run is in its native phase. Check the load average and free disk first (the disk guard pauses experiments below 8 GB free; yapnr work needs more than 15 GB free on the experiment volume).Check for another agent’s activity (new handoff entries, processes this session did not start) before long operations, and report it rather than competing.
Transition of engine work:
Once PR2 lands, new engine changes are made on yapnr branches.
Until PR3b, yapnr’s
hardware/pnrlayout is identical to a snapshot’s, so the existingrunx.shharness can run a yapnr checkout directly (PYTHONPATH=<yapnr>/hardware/pnr).Between PR3b and PR3f, experiments either stay on frozen snapshots or run
python -m yapnr.mc.halvingfrom a checkout with explicit arguments (PR3c removes the hidden defaults).After PR3f, Splanc experiments move to
yapnr runwith a Splanc project manifest and an external store; older history is registered withyapnr import legacy-hier.
8. PR sequence¶
Merge order: PR0, PR1, PR2, PR6a, PR3a-f, PR5, PR4a-d, PR6b, PR2e (ported via §7.4 if late), PR7, PR8, then the Splanc import-back. (PR2c and PR2d, the USB pair and N-0001 lines, were finished before PR2 and are part of it.)
PR6 is split, and its first half moves ahead of PR3. PR3a/PR3b rewrite every file mechanically,
and today no CI runs the native (KiCad-side) code paths, so the KiCad lane must exist first. The
labels otherwise follow the requested numbering. Each PR updates WORKLOG.md and the docs pages it
touches. “Same pass set” means the recorded list of passing, skipped and known-failing tests is
unchanged.
PR0: bootstrap (about 55 files, about 5.1k lines, excluding the lockfiles)¶
Contents:
README.md(updated),AGENTS.md,DEVELOPERS.md,CONTRIBUTING.md(owner-only policy),WORKLOG.md,THIRD_PARTY.md(three.js and elkjs);the Bazel skeleton (§4.1):
MODULE.bazelwithrules_python, the hermetic Python 3.11 toolchain and theyapnr_pypihub,MODULE.bazel.lock,.bazelversion,.bazeliskrc,.bazelrc,.bazelignore; no license metadata in Bazel;requirements.inandrequirements.lock(generated on the development Mac);presubmit configs (§5.1):
.pre-commit-config.yamlwith the reserved global exclude and the privacy hook,.flake8, markdownlint and prettier configs,setup-precommit.sh;ci.yaml(lint with the identity check, test onubuntu-24.04-armwith the lock check, docs, gated Pages deploy and previews) andmacos.yaml(test-macos, not required);the docs skeleton:
docs/_sphinx/conf.py(furo, MyST, mermaid, brand logos, favicon and palette),index.md,architecture.md,decisions.md,about-the-name.md, this plan;//docs:buildand//docs:serve;yapnr/__init__.py(__version__),yapnr/__main__.py,yapnr/cli.py(--version, adoctorstub that never runs KiCad),//:yapnrand//:doctor;tests: the CLI, the torch/numpy interop smoke, and the repo checks (test wiring, privacy scan of the whole tree), with
tools/privacy_scan.py,tools/check_test_wiring.pyand theyapnr_py_tests()macro;.github/CODEOWNERS, the PR template,.git-blame-ignore-revs(empty).
Acceptance:
bazel test //...green locally (lowmem) and in CI;prek run --all-filesclean;bazel run //docs:buildproducesdocs/site/html;deploy jobs are skipped until
YAPNR_PAGES_ENABLEDis set;GitHub shows AGPL-3.0;
privacy scan clean, and every commit uses an accepted identity (the owner’s public commit address; the earlier PR0 commits keep a noreply address, which also passes).
Risks:
everything pushed is public (the privacy gates above);
the Bazel output base on a case-insensitive external volume (use the documented location);
lock resolution platform (aarch64 lock; see the
testjob).
PR1: core engine import with history (merge commit)¶
Contents:
the filtered history (§7.1-7.2) at Splanc paths, source code unchanged;
the privacy scan’s refined e-mail rule (owner decision,
docs/decisions.md): the imported history holds decorators, a matrix product and constraint endpoints that the old rule read as addresses;one adaptation commit (Bazel loads to
@yapnr_pypi, tools BUILD, stalepnr.bzlkept but unloaded, the four failing tests of appendix B taggedmanual);docs/history/import-manifest.md, and the PR1 decisions indocs/decisions.md.
Acceptance:
git log --followworks on sample files, and the original commit count is preserved minus empty commits;the history-wide privacy scan and identity check are clean;
bazel test //hardware/pnr/...gives the same pass set as Splanc HEAD (recorded in the PR);no file over 600 KB.
Risks:
an identity the mailmap misses (the history-wide identity check rejects it);
a missed scrub pattern (mitigated by the history-wide scan);
the reviewer cannot read 28k lines, so review focuses on the manifest, scrub report and the adaptation commit.
PR2: newer engine state (merge commit)¶
Contents: the §7.3 series: F217, Electrical221, the hierarchical snapshots up to
src15(with the USB pair and N-0001 side lines, formerly PR2c and PR2d), the Electrical221 merge, the Bazel adaptation, and the docs (import manifest PR2 section, this plan, decisions, worklog).Acceptance:
each snapshot commit’s engine tree equals its snapshot, except the four scrubbed files (checked per commit and recorded in the manifest);
bazel test //... --config=cigreen at the head, with the pass set, the skips and themanualtests recorded in the manifest; tests that newly fail are taggedmanualwith a comment (no engine edits in PR2) and listed in appendix B;privacy scan clean, including merge diffs (
git log -p -m), and gitleaks clean;the unwired test files are counted;
hier/,mc/andfeedback/get their Bazel targets in PR3 with the glob macro (moved from this PR: most of their tests need KiCad or Splanc inputs).
Risks:
src2tosrc7are unrecoverable, so the src8b commit is large (mitigated by a detailed body and per-feature notes);the commits before the adaptation do not build in yapnr (as in PR1);
Electrical221’s cleanup is on by default and the H7 run does not use it (decisions);
the merge-commit requirement.
PR6a: KiCad test lane and toolchain discovery¶
Contents:
path-independent KiCad toolchain discovery (initially
hardware/pnr/pnr/kicad_toolchain.py, moved toyapnr.kicad.toolchainin PR3b) used by everykicad-cli/KiCad-Python call site, including the two barekicad-cliones;the
kicad_py_testmacro and runner (rules_shelljoinsMODULE.bazel);the
.bazelrcKiCad config;existing KiCad-needing tests re-tagged
kicad;regression/run.pymade path-independent;//tests/regression:smoke;the
kicad-integrationcontainer job;requirements_linux_x86_64.lock(CPU torch index) throughrequirements_by_platform;the regression ladder run under both numpy 1.26 (experiment venv) and the lock, with the results in the PR.
Acceptance:
bazel test --config=kicad //...passes on the development Mac and in the container, withYAPNR_REQUIRE_KICAD=1;the 16 test files bundled in
power_access_native_testactually execute (no skips);the smoke run takes under 10 minutes on CI.
Risks:
KiCad-side code under Python 3.13 (container) versus 3.9 (Mac);
KiCad 10.0.6 container behaviour differences (fonts, library env vars);
CI minutes (bounded by the smoke design).
PR3a: mechanical format and lint fixes¶
Contents:
code keys that survive formatting (code key scheme 2: a canonical syntax tree per module), committed ahead of the format so no library or trial is invalidated by it;
black and isort on the imported trees, as format-only commits (merged with a merge commit so they survive; a follow-up adds them to
.git-blame-ignore-revs), with isort’s settings pinned in.isort.cfgso its order does not depend on the directory it runs in;the
.flake8baseline: every remaining code per file, F-codes included (no hand fixes; the F821 invia_coalescestays for its own change, Appendix B);hardware/removed from the global exclude and.flake8; per-hook excludes (and.markdownlintignore) keep only the non-Python files a hook would rewrite or reject.
Acceptance:
black’s AST-equivalence check, and for isort a check that every difference is a reordering inside one run of imports;
code keys unchanged by the format;
same pass set in both lanes;
prek run --all-filesclean withhardware/Python covered.
Risks: isort moving imports across
sys.pathbootstrap code in worker scripts. Such files get# isort: skip_fileand are removed in PR3b anyway. (Measured in PR3a: isort 6 moves no import across code, so no file needed it.)
PR3b: package rename and restructure¶
Contents:
tools/migrate/module_map.tomlplus a codemod;commit 1: pure
git mvto the §1 layout (100% similarity); PR3b is merged with a merge commit so this commit survives, and a follow-up lists both commits in.git-blame-ignore-revs;commit 2: import and
-mstring rewrite (about 1,272 import statements, about 100 module strings),feedback/signals.pyregexes, BUILD files regenerated per subpackage;tests moved to
tests/unit|kicad, the four mixed files split, the hyphenated test renamed;drc_warm, the legacy keyhole adapters andpnr_viewerdeleted (pointer indocs/history/splanc-origins.md);the KiCad-side closure test;
//tools/migrate:portwith its fixture test.
Acceptance:
same pass set;
git log --follow/blame continuity on samples;the port tool reproduces
src13againstmainas a clean patch (dry run).
Risks:
a huge diff (reviewed as tool plus map, not line by line);
collisions with in-flight work (§7.4).
PR3c: de-Splanc paths and defaults¶
Contents: every §2.1-2.3 and §2.5 item marked 3c, plus worker staging (
yapnr.kicad.staging).Acceptance:
rg -i 'splanc|splanc_dev|splanc_mini'overyapnr/andtests/returns only the fixture README and history docs;no absolute paths in code (privacy scan);
running from a directory other than the repository works (a test runs the smoke from
$TEST_TMPDIR).
Risks: the UUID namespace change alters generated identifiers, so goldens must be regenerated and reviewed.
PR3d: project store and manifest¶
Contents:
yapnr/project(manifest model and generated schema, store, objects, input sets, engine snapshots, run records);CLI
init,doctor(full),kicad make-headless,import kicad,inputs,snapshot,ls,show;the flag registry and its repo check;
docs
project/*.
Acceptance:
unit tests with a fake engine kind;
schema and CLI-reference freshness tests;
import kicadof the led555 KiCad project produces a portable input set (no absolute URIs).
Risks: store design churn. Mitigated by the
layout_versionfield and a migration hook from the start.
PR3e: frontends and canonical annotations¶
Contents:
the
DesignFrontendprotocol;the atopile frontend (annotation parser moved out of
core,atopile_addressprovider, module hierarchy, build command);the KiCad frontend (sheet-path addresses,
annotations.yaml, footprint fields);the
yapnr-annotations-v1schema;import atopile;the
examples/atopile-blinkyfixture.
Acceptance:
the Splanc Mini annotations (run manually with
YAPNR_EXTERNAL_PROJECT) compile to the samerules.jsonpolicy as before;a KiCad-native hierarchical fixture yields blocks and resolved annotations.
Risks: semantic drift in address matching (
._ptrimming). Mitigated by a golden comparison on the Mini.
PR3f: experiment runner and lifecycle (three sub-PRs)¶
3f-1 contents:
run,resume,status,stop|pause|continue,compare;the supervisor (pid identity, keep-awake, disk guard, log, telemetry dir);
experiment kinds;
profiles.
3f-2 contents:
prune,gc,pack,unpack,import legacy-hier.3f-3 contents:
export kicad,baseline add.Acceptance:
a smoke experiment runs through the CLI in the KiCad lane;
pack then unpack round-trips byte for byte;
export DRC equals run DRC;
import legacy-hieron a copied sample of$HIERregisters runs without writing to the source.
Risks:
process supervision edge cases (orphans, sleep and wake);
the size of
live/(78k event files), handled by hard links and streaming indexing.
PR5: native search backend¶
Contents:
native/search/search.rs(history already imported) withrust_shared_libraryandrust_test;the runfiles lookup in the controller, passed to workers;
the
//native:search_backendflag;the Python equivalence test (
keyholewith the Rust versus Python backend, againstkeyhole_reference);build-rust.shretired.
Acceptance:
builds on linux-x86_64, linux-aarch64 and darwin-arm64 with rustc 1.85;
the equivalence test is green;
the regression smoke gives identical routes with both backends.
Risks: the rules_rust toolchain interplay with Splanc (root-first, 1.85, fine);
ctypescallbacks under the KiCad Python.
PR4: viewer (four sub-PRs)¶
4a:
git mvof the history-importedpnr_livetoyapnr/viewer, then the viewer-dev core delta (server, event schema, settings, extract, static app);project- and run-driven configuration; renames; About/Source (AGPL section 13);
a published
yapnr-live-event-v1JSON Schema (accepts v1 events);node tests through the dev-only node toolchain;
bazel run //:viewer.
4b:
services: component cost (resolve
capacitor_intent), schematic (elkjs as a fetched, sha256-pinned, separately served file; never vendored), source browser throughfrontend.source_index().
4c:
the notes store and MCP, the optional agent and AI net labels (off by default; CLI via
shutil.which, spend caps, web guard), schema and env renames, test literals replaced.
4d: the 3D view (three.js 0.186.1 + GLTFLoader, fetched and pinned like elkjs; GLB through the headless
kicad-cli).Done as one branch (
claude/pr4-viewer): the four parts are one commit series (move, imports, format, packaging and configuration, lint, fetched JavaScript and Bazel targets, tests and docs); the choices that differ from this section are in decisions (“Choices made in PR4”). Still open:--projectand the manifest’s[viewer]table (with PR3d), the publishedyapnr-live-event-v1JSON Schema, the browser (e2e) tests,examples/led555as the smoke run (PR6b).Acceptance:
viewer tests pass;
bazel run //:viewer -- --project examples/led555shows the smoke run;no
splancstrings outside history docs;static fixtures regenerated from examples;
privacy scan clean.
Risks:
four diverging viewer copies (import from viewer-dev only, after confirming it is a superset);
the EPL-2.0 dependency;
agent features in a public repository.
PR6b: tiny example and end-to-end lane¶
Contents:
the
examples/led555project (manifest, rules, profiles,ci-smokeexperiment);an end-to-end test:
init,import kicad,run,export kicad+ DRC,baseline add,pack/unpack;the
macos-kicadnightly job.
Acceptance: green on the container lane and a nightly macOS run.
Risks: runtime budget; stock-library variables differ between the container and macOS (handled by the export copying footprints).
PR7: documentation content¶
Contents:
§6 pages, split from
pnr-system.md/pnr-inputs.md;scrubbed ADRs;
generated references (CLI, manifest, flags, telemetry);
-Wenabled.
Acceptance: the strict docs build is clean; every public CLI command and every flag is documented.
Risks: leaking Splanc-private or machine details from the handoff log. Everything is written fresh, not copied.
PR8: downstream ruleset¶
Contents:
bazel/(providers, rules,kicadextension and toolchain) implementing §4.3;analysis tests (skylib
unittest);examples/led555/BUILD.bazelusing the rules;the
tests/downstreamconsumer workspace plus thedownstreamCI job;docs/bazel/*.
Acceptance:
bazel build //examples/led555:pnrandbazel test //examples/led555:pnr_test --config=kicadgreen in the container;the downstream workspace resolves yapnr through
--override_module;buildifier-lint clean.
Risks:
sandboxing of KiCad actions (keep
local);long actions in Bazel (time budget attribute,
manualtag by default).
Splanc import-back (separate Splanc PR, later)¶
Contents:
bazel_dep(name = "yapnr")+git_override(commit = <sha>);a Splanc macro wrapping
yapnr_board_pnrthat maps the atopile layout’s pcb;splanc_mini.fab,splanc_dev.fabandsplanc_mini.mesh_experimentre-expressed;a Splanc project manifest (
hardware/splanc_dev/pnr/yapnr.toml, atopile frontend, existing rules files) with its store on the external disk;yapnr import legacy-hierof$HIER;delete
hardware/pnr, the moved tools anddocs/hardware/pnr-*(pointer to the yapnr docs).
Must carry the owner-approved contract edits that today exist only in hier snapshots:
the VBUS neck;
the gate-drive branches;
the SW2/VOUT/SW1 envelopes;
the USB ESD stub and the per-run uncoupled rule.
They move into the production
splanc_mini.atowith their datasheet comments.Risks:
Splanc’s Nix KiCad is 9.0.1 and cannot load KiCad 10 boards: bump the pin (nixos-unstable has 10.0.6) or use yapnr’s autodetect toolchain;
hub_nameandrules_rustinterplay;Splanc CI is not green today for unrelated reasons (lint debt, the
freeroutingNix package on x86_64).
Contribution flow afterwards: engine changes land in yapnr first; Splanc bumps the
git_overridecommit.
Migration done when¶
Splanc builds its boards through
@yapnrwith no copy of the engine;all required yapnr checks are green on
main, including a real KiCad lane;every runtime artifact the Splanc experiments produce lives in a project store that can be packed, restored and exported to KiCad;
the docs site is published on GitHub Pages.
9. Open questions for the owner¶
Resolved on 2026-09-29 (see §0.2 and docs/decisions.md):
Q1, license variant and inbound terms:
AGPL-3.0-or-later. Inbound terms are not needed yet, because outside contributions are not accepted; revisit (CLA or DCO) before opening contributions.Q3, public docs: approved. The Pages deploy and previews are wired and gated on
YAPNR_PAGES_ENABLED, which is set after the first green build ofmain(then a manual run onmaincreatesgh-pages).Q4, agent features: they ship in yapnr as optional features, off by default.
Q5, elkjs: acceptable as a separately fetched, sha256-pinned, unmodified asset; never vendored.
Q6, issue keys: GitHub issues (
#N).Q2, identity mapping in the imported history: decided. Every identity in the imported history (machine-local addresses, other personal addresses and noreply addresses) maps to the owner’s public commit address, listed in
tools/privacy/allowed_identities.txt, with the matching name (the owner’s, orClaude Agentfor agent identities); new commits use the same address. GitHub accepts only an address verified on the account as the author of a merge made on the website, which rules out a noreply address for the merges intomain, and the owner chose to publish this address on commits. The mailmap stays in$SCRATCH, never committed.
Still open:
Q2b, full-history import (left over from Q2): confirm that a full-history import is wanted rather than a single snapshot commit. PR1 implements the full-history import (§0.3, item 7); merging it with a merge commit answers this question.
Appendix A: module map (old to new)¶
graph,constraints,electrical,plane_intent,power_topology,fab_profile:yapnr.core.*(annotation parsing moves tofrontends/atopile).place.*,incremental_place:yapnr.place.*.route.*,route.detail.*(includingkeyhole,keyhole_reference,rust_search,coupled,regional,layered,joint,portal_joint,spatial_conflicts):yapnr.route.*.yapnr.native.*(flat first; grouping later):native_loop,native_electrical,full_iteration,electrical_pool,phase_capture,placement_copper,placement_trials,paired_bootstrap,pair_joint,pair_contact_audit,electrical_audit,electrical_repair,plane_access,plane_leaf,plane_leaf_repair,power_bank_stage,power_bank_reuse,power_detour,power_detour_repair,fanout_reserve,via_in_pad,via_coalesce,track_graph,pad_entry,pad_entry_neck,terminal_repair,reference_guard,pad_identity,regional_blockers,transaction_cleanup,obsolete_branch,connectivity_restore,portal_retry,staged_signal,speculative,route_epoch,merge_additive,escape_shove,geometric_tree,geometric_native,geometry_optimize,congestion_diagnostics.ingest,writeback,source_footprints,library_table,specctra,planes,fixed_copper,quality,native_drc:yapnr.kicad.*(native_drcbecomesyapnr.kicad.drc).hardware/tools/keyhole_region.py:yapnr.kicad.workers.region.live,runtime_controls,profile,proc,phase_budget:yapnr.runtime.*(runtime_controlsbecomescontrols).shove.*,hier.*,mc.*,feedback.*,feedback_boundary: the same names underyapnr.;feedback_boundarybecomesyapnr.feedback.boundary.drc_warm.*: not migrated (GUI-bound).scan_via_proximity,audit_pair_contacts,export_pnr_congestion,render_pnr_congestion,export_elastic_experiment,render_elastic_experiment,watch_pnr_pdf,export_plane_access_clusters:yapnr.diagnostics.*(drop “Splanc Mini” titles).export_mini_review,mini_review_annotations,rasterize_mini_review,verify_mini_review,review_mini_contacts:yapnr.review.*(renamed without “mini”; optional deps reportlab, pypdf, Pillow, pdftoppm, rsvg-convert).consolidate_ground,optimize_plane_access,plane_access_trial:yapnr.stages.*(experimental).freeze_mini_inputs:yapnr.project.inputs(generalized freeze).keyhole_{repair,loop,shift_via,deform},pnr_viewer/: history only (deleted in PR3b).hardware/tools/pnr_live/+$HIER/viewer-dev:yapnr.viewer.The
$HIERharness (runx.sh,envexport.py,env*.json,launch_*.sh,status.sh,diskguard.sh,autoprune.sh,prune_live.py,pairtest.py,pairs/*.sh,doe_block.py):yapnr run/status/prune/gc, the supervisor, profiles, and the experiment kindspair-benchanddoe.
Appendix B: known defects to carry as issues¶
via_coalesce.maincallsitems.clear()butitemsexists only inworker()(F821). Kept (baselined) in PR3a, which makes no semantic edits: the fix changes the evaluation code key, so it lands with a test of its own in PR3b or later.About 25Done: 27 discarded-item calls useboard.Removecall sites may detach items that crash KiCad Python at teardown (PR2e).board.Delete; 3Removecalls stay (kept alive or detached withthisown=False), guarded per call by//hardware/pnr:board_delete_test.pnr.capacitor_intentis imported by the viewer’s cost service but was never committed (PR4b).Pre-existing errors: 7 in the KiCad-Python suite (torch/env imports), 1 in the runtime suite (
test_pair_joint_dispatchimport).The
pnr_kicad_srcsfilegroup is incomplete and only works because actions run unsandboxed (replaced by the closure in PR3b).Test files not wired into Bazel (fixed by the glob macro and the wiring check in PR3): 29 after PR1; after PR2, 68 under
hardware/pnr(PR1’s 25 and 43 new) plus the four outside it.tools/check_test_wiring.py --scope hardware/reports five more underhardware/pnr: SI test files that only thesi_%s_testlist comprehension names, which does wire them.Three native-loop controller tests (
placement_budget_test,progress_budget_test,retry_controller_test) fail on the Splanc source commit of PR1: their recorded worker fixture has nographkey, whichnative_loop.congestion_snapshotreads. PR1 tags themmanual. Since src8b (PR2) they fail earlier, withFileNotFoundError: 'fixture-python': they patchnative_loop.subprocess.run, but the native loop starts its workers throughpnr.proc.run, so the fixture’s fake interpreter is executed. Stillmanual.detail_route_testtakes 870 to 1000 s on the development Mac, at or over its 900 slargetimeout, and itstest_drc_clean_by_constructionfails: two nets share a footprint cell. PR1 tags itmanual; unchanged with PR2’s engine (848 s, the same failure).Workers without a time bound (PR2’s engine):Done: all bounded throughelectrical_pool,hier/native_block(2 calls),mc/halving,transaction_cleanup, the parallel pair trials ofpaired_bootstrap(stopped only by an event) and the cleanup pass inhardware/tools/keyhole_region.py; from PR1’s code,drc_warm,electrical_repair,full_iteration,geometry_optimize,paired_bootstrap,plane_leaf_repair,power_detour_repair,staged_signalandregression/run.py. All move topnr.proc.run(PR3).pnr.proc;//hardware/pnr:proc_testscans for new unbounded calls.Engine code tied to the Splanc design (PR2):
feedback/signals.pyfinds annotation sources by ahardware/splanc_dev/elec/src/*.atopattern;hier/native_block.pyandmc/halving.pydefault tohardware/splanc_devinputs (PR3c, PR3d). Tests and test data that use Splanc design files are listed in the import manifest.Electrical221’s partial-cycle cleanup and barrel-contact bridges inGated, default off (via_coalesceandtrack_graphare on by default, without a flag or an A/B result (PR2; decisions).PNR_PARTIAL_CYCLE_CLEANUP,PNR_BARREL_CONTACT_BRIDGES); the A/B is open.hardware/tools/audit_pair_contacts.pystarts awx.App(GUI-bound, outside the loop).
Appendix C: privacy scrub checklist (gate for every push)¶
tools/privacy_scan.pyover every pushed tree (the Bazel repo check) and over the new commits’ identities (the CIlintjob).A history-wide scan of the filtered Splanc history (PR1) and of every snapshot commit (PR2).
Machine paths in:
tests:
test_shove*,testdata/power_topology;tools: review, render and plane-access scripts;
the viewer:
server.py,agent_service.py,net_llm.py,.scratch/*, static HTML fixtures.
Machine-local network names and addresses in documentation and test literals.
Never migrated:
conversation logs;
agent turn logs;
LLM caches under
live/;run logs;
the handoff and continuation documents.
Inputs with absolute library URIs (
inputs*/fp-lib-table,rules.jsonpathentries) are not committed. Input sets are created fresh byyapnr import, which writes relative paths.