Metadata-Version: 2.4
Name: buildwithtrace-converter
Version: 0.1.5
Summary: 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-URL: Homepage, https://buildwithtrace.com
Project-URL: Documentation, https://docs.buildwithtrace.com
Project-URL: Changelog, https://docs.buildwithtrace.com/resources/cli-changelog
Author-email: Trace <hello@buildwithtrace.com>
License: PROPRIETARY LICENSE
        
        Copyright (C) 2025-2026 Trace Developers Team. All Rights Reserved.
        
        This software and associated documentation files (the "Software") contained
        in this directory and its subdirectories are proprietary and confidential.
        
        RESTRICTIONS:
        - You may NOT copy, modify, merge, publish, distribute, sublicense, or sell
          copies of the Software.
        - You may NOT use the Software for any commercial or non-commercial purpose
          without explicit written permission from the copyright holders.
        - You may NOT reverse engineer, decompile, or disassemble the Software.
        - You may NOT create derivative works based on the Software.
        
        This Software is provided for reference only as part of the Trace open-source
        repository. The inclusion of this code in the repository does not grant any
        license or rights to use, copy, or distribute it.
        
        For licensing inquiries, contact: hello@buildwithtrace.com
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
        DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
License-File: LICENSE
Keywords: converter,eda,kicad,pcb,schematic,trace
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Provides-Extra: vectorsearch
Requires-Dist: numpy>=1.24; extra == 'vectorsearch'
Requires-Dist: onnxruntime>=1.16; extra == 'vectorsearch'
Requires-Dist: tokenizers>=0.15; extra == 'vectorsearch'
Description-Content-Type: text/markdown

# buildwithtrace-converter

<!-- whats-new:start -->
## What's new in v0.1.5



_Full history: [release notes](https://docs.buildwithtrace.com/resources/cli-changelog)._
<!-- whats-new:end -->

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 a `buildwithtrace.*` namespace, to avoid colliding with the CLI's `buildwithtrace` package and the backend's vendored `buildwithtrace.altium`)
- **GitHub:** `buildwithtrace/converter` (org, canonical) / `elcruzo/trace-converter` (personal)

## Install

```bash
# 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:

```bash
pip install -e ../converter   # local dev
```

## Usage

```python
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):

```bash
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_SENSE` in the trace_json ALWAYS force-creates
  `VCC_SENSE` verbatim — 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:x` on 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`/`executeSearchReplace` scan 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
  exactly `power`. Parts from `Power_Protection`/`Power_Management` are 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_sch` parser, 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_sch` sheet declaration moves
  a pin between `ins`/`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 like `bidirectional` when no annotation is given — are
  preserved from the existing file.
- **Sheet-pin `shape` annotation.** A pinmap entry takes an optional trailing
  shape: `outs { D=DATA @ 80,55 rot 0 shape bidirectional }` (also `tri_state`,
  `passive`, `input`, `output`). It is the DSL's way to express the kicad
  sheet-pin shapes the two `ins`/`outs` buckets alone cannot encode, and a
  valid explicit shape is AUTHORITATIVE in the merge (so `output ↔
  bidirectional` edits finally stick instead of reverting every conversion).
  The ingest emits the annotation ONLY for non-default shapes (`ins`→`input`,
  `outs`→`output` stay 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_sch` grew 14→16→18→20 over 4
  cycles). Fixed by the declared-duplicate dedup in `trace_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`/`outs` net; 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 ... keep`** marks a wire exempt from any wire-elimination pass;
  `TRACE_KEEP_ALL_CWIRES=1` (env) disables elimination entirely.
- **Legacy `.trace_pcb` self-heal.** A lexer-level parse failure raises
  `LegacyTraceFileError` (message prefixed `LEGACY_TRACE_FILE:`, a subclass of
  `TraceParseError`) so callers can regenerate the DSL from the `.kicad_pcb`
  via `buildwithtrace_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.py` needs 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 `Trace` desktop 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.json` pointer; only versions in the SAME major.minor series as the bundled
  `Trace/trace/VERSION` are 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.py` diffs 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's `VERSION` file —
  the desktop's auto-update compat gate reads that stamp). Run it locally after editing
  (it defaults to the sibling `trace-cli` + `Trace/trace` bundles):

  ```bash
  python3 scripts/verify_sync.py
  ```

  It is also a CI gate: the `converter-sync` job in `trace-cli/.github/workflows/ci.yml`
  clones this repo and checks the CLI bundle on every PR.
- **License:** see `LICENSE`.
