Trace EDA converter: .trace_sch/.trace_pcb <-> KiCad s-expression, plus the trace_parser. Single source of truth for the CLI, backend, and desktop.
Project description
buildwithtrace-converter
What's new in v0.1.6
Added
- New IPC engine extracted as the canonical internal engine, exposed via the converter API
- New Altium writer: export/convert designs to Altium format
- Compliance checks added to conversion pipeline
Full history: release notes.
The Trace EDA converter, extracted into a single installable package so the CLI, backend, and desktop app stop carrying their own copies.
It converts between Trace's intermediate .trace_sch / .trace_pcb format and
KiCad's native .kicad_sch / .kicad_pcb s-expression format (both directions),
and exposes the trace_parser used to read the intermediate format.
- Package (PyPI/dist) name:
buildwithtrace-converter - Import root:
buildwithtrace_converter(a self-contained top-level — deliberately NOT abuildwithtrace.*namespace, to avoid colliding with the CLI'sbuildwithtracepackage and the backend's vendoredbuildwithtrace.altium) - GitHub:
buildwithtrace/converter(org, canonical) /elcruzo/trace-converter(personal)
Install
# core (pure stdlib, no deps)
pip install buildwithtrace-converter
# with the optional vector-search helper (numpy / onnxruntime / tokenizers)
pip install "buildwithtrace-converter[vectorsearch]"
For local development, install from a checkout:
pip install -e ../converter # local dev
Usage
from buildwithtrace_converter.eeschema.trace_parser import parse_trace_sch
from buildwithtrace_converter.eeschema.trace_json_to_sexp import trace_json_to_sexp
from buildwithtrace_converter.eeschema.sexp_to_trace_json import sexp_to_trace_json
from buildwithtrace_converter.pcbnew.trace_parser import parse_trace_pcb
The unified file CLI (used by the desktop via subprocess):
python -m buildwithtrace_converter.eeschema.trace IN OUT -f trace_sch -t kicad_sch
python -m buildwithtrace_converter.pcbnew.trace IN OUT -f trace_pcb -t kicad_pcb
Tolerant reference resolver
common/reference_resolver.py makes the agent-edit merge paths tolerant of
near-miss net/pin references on IMPORTED boards (alien vocabularies like
+3V3, Net-(U1-Pad4), /power/VBUS_5V, ~{CS}). Pure stdlib (difflib).
- EXACT match: behavior is bit-identical to before — nothing changes.
- RESOLVED (unique, high-confidence: normalization equivalence, unique
hierarchical leaf, or fuzzy ratio ≥ 0.88 with runner-up < 0.80): the merge
auto-binds to the EXISTING board net / lib pin number instead of minting a
phantom net (pcbnew pads) or dropping the pin (eeschema), and emits a
[NET_WARNING]/[PIN_WARNING]stderr marker teaching the agent the canonical name. The desktop folds these markers into the tool result. - SUGGEST / MISS: today's fallback behavior is kept (mint the net / drop the pin); SUGGEST enriches the existing warning with up to 3 "did you mean ...?" candidates.
- Resolution applies ONLY in the agent-edit direction (trace_json → kicad
merge). The kicad → trace_json ingest and the DSL parsers are untouched;
net names an ingested file DECLARES (
net/label statements) are never resolved, so round-trips of existing boards stay lossless.
The new: escape hatch (pitfall notes)
- A net written as
new:VCC_SENSEin the trace_json ALWAYS force-createsVCC_SENSEverbatim — never resolved, never suggested, no warning. The prefix is stripped before net-table interning and can never leak into the emitted kicad output. - A net LITERALLY named
new:xon an existing board/schematic is matched EXACTLY first and is never stripped — pathological imports still round-trip (guarded by tests). - Marker contract: reuse the existing
[NET_WARNING]/[PIN_WARNING]tags ONLY (the desktop C++executeWrite/executeSearchReplacescan exactly these). Do not invent new marker tags without adding the matching C++ fold blocks.
Tests: tests/test_reference_resolver.py (unit + pcbnew/eeschema merge
integration).
June 2026 triage fixes (user-facing surface)
From the 41-user transcript triage (tests/test_user_triage_fixes.py):
- Power-symbol detection is exact + flag-aware. A symbol is "power" iff its
resolved lib definition carries the
(power)flag OR its library is named exactlypower. Parts fromPower_Protection/Power_Managementare normal components again (no forced#prefix / BOM exclusion). body_style 0/unit 0(EasyEDA imports) are clamped to 1 at ingest and normalized (with a warning) by the.trace_schparser, so files written by shipped versions re-parse.- Component properties round-trip. Ingest passes through MPN, Manufacturer, Datasheet, Description, and all user-defined instance properties (original name/case). Merge mode matches trace-side prop keys case-insensitively, the trace value always wins, and the library default only fills properties genuinely missing from the existing instance (never overwrites it).
- Sheet-pin direction authority. When a
.trace_schsheet declaration moves a pin betweenins/outs, the trace side wins and the kicad pin type is rewritten (input/output). Pins the trace side doesn't mention — and same-bucket types likebidirectionalwhen no annotation is given — are preserved from the existing file. - Sheet-pin
shapeannotation. A pinmap entry takes an optional trailing shape:outs { D=DATA @ 80,55 rot 0 shape bidirectional }(alsotri_state,passive,input,output). It is the DSL's way to express the kicad sheet-pin shapes the twoins/outsbuckets alone cannot encode, and a valid explicit shape is AUTHORITATIVE in the merge (sooutput ↔ bidirectionaledits finally stick instead of reverting every conversion). The ingest emits the annotation ONLY for non-default shapes (ins→input,outs→outputstay bare), so input/output-only files are byte-identical to before, and old files without annotations parse exactly as before. An invalid shape value is warned about and ignored (bucket default applies). Tests:tests/test_sheet_pin_residuals.py. - Pin-net label materialization is bounded (one cycle), and the unbounded
duplicate-label grower is fixed. The merge/generation path materializes
every component pin's net as a label at the pin position; the first
conversion of an ingested board therefore ADDS labels the source never had
— intentional, one-time normalization (like wire splitting): the next
ingest absorbs pin-position labels back into pin nets and the output is a
fixpoint from then on. AUDITED BUG (June 2026): symbols with DUPLICATE PIN
NUMBERS (e.g. a 2-level terminal block whose pin "1" exists at two physical
positions) defeat that absorption — the two directions pick different
positions for the same pin number — and the kept standalone label plus the
regenerated copy were BOTH emitted: +1 stacked duplicate label per
round-trip, forever (qa
jumpers.kicad_schgrew 14→16→18→20 over 4 cycles). Fixed by the declared-duplicate dedup intrace_json_to_sexp(a generated pin label identical to a trace-declared label — same name, same rounded position — is skipped). Corpus-verified: all 220 qa schematics now reach a stable fixpoint (219 after one cycle, 1 after two). Tests:tests/test_label_growth_stability.py. - Merged-sheet pin labels are emitted again. The ingest absorbs a local
label sitting at a sheet-pin position into the sheet's
ins/outsnet; the reverse direction re-creates that label — previously it was appended to an already-consumed list and silently dropped (round-trips lost the label and agent-added sheet pins were left unconnected). Emission is gated so unaffected round-trips are unchanged: the ingest's pin-name fallback (pin pre-existed, net == pin name, no label in the source) emits nothing, and a label already present at the position is never duplicated. Verified over the full sheet-bearing qa corpus (zero output diff vs the old behavior for ingested boards). Tests:tests/test_sheet_pin_residuals.py. cwiredef ... keepmarks a wire exempt from any wire-elimination pass;TRACE_KEEP_ALL_CWIRES=1(env) disables elimination entirely.- Legacy
.trace_pcbself-heal. A lexer-level parse failure raisesLegacyTraceFileError(message prefixedLEGACY_TRACE_FILE:, a subclass ofTraceParseError) so callers can regenerate the DSL from the.kicad_pcbviabuildwithtrace_converter.pcbnew.trace.regenerate_trace_pcb. Both lexers also tolerate legacy bare tokens (/VPP/MCLR,~{RESET},/GPIO2{slash}SDA1); the writers keep emitting the quoted forms. - Router net-integrity check. A routed cwire whose path would electrically
touch geometry of a different declared net is rejected (no wires/junction/
split committed) and replaced with net labels, with a
[WIRE_OPT]stderr note — a cwire can no longer short two nets through nearby junctions.
Notes
-
Core is pure-stdlib (no runtime dependencies). Only
common/local_vector_search.pyneeds the[vectorsearch]extra, and the embedding model + tokenizer are NOT bundled — pass their paths in. -
Source of truth. Edit here. Consumers (
trace-cli,backend,sdk-python) pip-depend on this package; do not re-fork the sources. -
The desktop SELF-UPDATES from PyPI. The
Tracedesktop ships a bundled copy (Trace/trace/, the offline baseline/fallback) AND silently installs newer wheels from PyPI at launch into the product-wide store~/.trace/converter/<version>/(current.jsonpointer; only versions in the SAME major.minor series as the bundledTrace/trace/VERSIONare auto-taken, non-yanked + sha256-verified). Releasing a PATCH version to PyPI therefore reaches existing desktop installs without an app release; a series (major.minor) bump ships with the next app release. Yanking a bad release on PyPI is the kill switch — clients skip yanked releases on the next launch. Other Trace clients may READ the store as a fallback; only the desktop writes it. -
Drift is enforced.
scripts/verify_sync.pydiffs every shared converter source against the bundled copies and exits non-zero on drift, and enforces the version stamps (pyproject.toml==__init__.__version__== each copy'sVERSIONfile — the desktop's auto-update compat gate reads that stamp). Run it locally after editing (it defaults to the siblingtrace-cli+Trace/tracebundles):python3 scripts/verify_sync.pyIt is also a CI gate: the
converter-syncjob intrace-cli/.github/workflows/ci.ymlclones this repo and checks the CLI bundle on every PR. -
License: see
LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file buildwithtrace_converter-0.1.6.tar.gz.
File metadata
- Download URL: buildwithtrace_converter-0.1.6.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ffc435c1b9731ea75a91963517d4f087b80ccf55bc33331aa98cc99cea9fc2e
|
|
| MD5 |
d0b2984fca4963f18eaf9b602c2b7bea
|
|
| BLAKE2b-256 |
b985118ad907f2ed3d22a3074d5797ef45d5738b20a809c27688ca101135c7a7
|
Provenance
The following attestation bundles were made for buildwithtrace_converter-0.1.6.tar.gz:
Publisher:
release.yml on elcruzo/trace-converter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
buildwithtrace_converter-0.1.6.tar.gz -
Subject digest:
0ffc435c1b9731ea75a91963517d4f087b80ccf55bc33331aa98cc99cea9fc2e - Sigstore transparency entry: 1811604719
- Sigstore integration time:
-
Permalink:
elcruzo/trace-converter@3afe6c57222574a9f614837e6098ab68d60a5d7d -
Branch / Tag:
refs/tags/v0.1.6 - Owner: https://github.com/elcruzo
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3afe6c57222574a9f614837e6098ab68d60a5d7d -
Trigger Event:
push
-
Statement type:
File details
Details for the file buildwithtrace_converter-0.1.6-py3-none-any.whl.
File metadata
- Download URL: buildwithtrace_converter-0.1.6-py3-none-any.whl
- Upload date:
- Size: 1.3 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c43229d46ffae27f06b35e8e695763471e3e621053ec339d18ae2651d0ee966
|
|
| MD5 |
69cb1c7fc9003e6602d9530553a27b95
|
|
| BLAKE2b-256 |
803fa72c60426f20664ca1a9111f034c134afad9752851c2c11d6d9a4336299a
|
Provenance
The following attestation bundles were made for buildwithtrace_converter-0.1.6-py3-none-any.whl:
Publisher:
release.yml on elcruzo/trace-converter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
buildwithtrace_converter-0.1.6-py3-none-any.whl -
Subject digest:
2c43229d46ffae27f06b35e8e695763471e3e621053ec339d18ae2651d0ee966 - Sigstore transparency entry: 1811605202
- Sigstore integration time:
-
Permalink:
elcruzo/trace-converter@3afe6c57222574a9f614837e6098ab68d60a5d7d -
Branch / Tag:
refs/tags/v0.1.6 - Owner: https://github.com/elcruzo
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3afe6c57222574a9f614837e6098ab68d60a5d7d -
Trigger Event:
push
-
Statement type: