Skip to content

Sync upstream improvements through d6823d5 - #1

Merged
Rouniy merged 17 commits into
mainfrom
agent/sync-upstream-d6823d5
Aug 28, 2026
Merged

Sync upstream improvements through d6823d5#1
Rouniy merged 17 commits into
mainfrom
agent/sync-upstream-d6823d5

Conversation

@Rouniy

@Rouniy Rouniy commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Merge all 16 commits from the original salitronic/eda-agent repository through d6823d5.
  • Add the live EasyEDA backend and extension, EasyEDA/KiCad library import, multi-provider part sourcing, minimal tool discovery, and the 0.5.0 documentation set.
  • Preserve the fork-specific Altium autonomy, WSL discovery, PCB routing/connectivity, bounded ECO verification, and dialog automation improvements.
  • Resolve seven overlapping files and expose the fork's previously unreachable PCB diagnostic and recovery handlers.

Why

The fork and the original repository had diverged from common base 15c814a: this repository carried eight Altium-focused commits while upstream carried sixteen broader backend, import, provider, documentation, and reliability commits. This merge retains both histories and integrates their overlapping behavior.

During integration, the combined suite exposed several cross-branch defects: Windows drive-list parsing under WSL, duplicate UNC path unescaping, stale backend-specific discipline anchors, inaccurate tool metadata, and an async bridge polling path that could strand executor completions under WSL. These were corrected as part of the merge.

Impact

The project moves to package version 0.5.0 and gains the upstream backends and sourcing/import workflows without dropping the fork's Altium-specific improvements. The merge changes 255 files with 60,915 insertions and 1,793 deletions.

Validation

  • 4050 passed, 107 skipped in the full pytest suite
  • Python compileall completed successfully
  • Altium DelphiScript lint: 11 files, 0 errors, 0 warnings
  • Git conflict-marker and whitespace checks passed

gs-salitronic and others added 17 commits August 6, 2026 11:17
Adds part_search and part_fetch over ten providers, queried equally with
no default and no fallback ordering. Merged hits are sorted by provider
then part, which is not a relevance ranking, and each hit carries a ref
of the form provider:part_id so a fetch names its source without the
caller pairing an id with a provider by hand.

Sources come in two kinds and the distinction is reported per hit.
Library providers yield geometry that can be placed: the Altium and
KiCad libraries already installed, an open registry, and the openly
published KiCad libraries on GitHub. Catalogue providers yield part
identity and a datasheet and no geometry at all: Digi-Key, Mouser,
Nexar, element14 and TME. Learning that a distributor hit has no symbol
after choosing the part is expensive, so kind travels with the result.

Every catalogue endpoint was probed before being written down. Three
further candidates answered 404 on the recalled URL and were dropped
rather than shipped as plausible guesses. What probing could not
establish without a paid credential is the response shape, so each
catalogue publishes verified_live false and the parsers degrade a single
hit on a renamed field instead of losing the whole search.

Mouser answers an invalid API key with HTTP 200 and an error body. A
client judging success by status code alone would report a rejected
credential as a search that ran and matched nothing, so payload level
error detection lives in the shared base rather than in five copies.

No credential ships with the project. An unconfigured or rejected source
raises ProviderUnavailable naming the environment variable it wants,
because "the endpoint is gone" and "no such part exists" are different
answers and only one is a reason to stop looking.

GitHub is read through its documented API with a User-Agent naming this
project, one recursive request per repository, and a week long disk
cache. GitLab is left alone: its robots.txt disallows /api/v*, which is
where KiCad's symbol repository lives, so symbols stay with the local
reader.
Gen_GetErcViolations returned only a description string, so a violation
arrived as a category and a sheet name. That is not actionable: the only
safe response to "floating input pin, somewhere on this sheet" is to do
nothing, because a NoERC marker placed by guesswork silently suppresses
a real disconnection and is worse than the warning it clears.

IViolation.DM_RelatedObjects carries the offending objects and was being
discarded. Each violation now reports them with their kind, their
document, and a cross probe string, which is what Altium itself uses to
jump to an object and therefore identifies the specific pin or net.
related_object_count is reported separately so a violation that exposes
no objects stays distinguishable from one whose objects could not be
read.

Everything read from a related object is declared on IDMObject, the base
interface every one of them implements, so no call here can hit the
undeclared identifier fault that a narrower interface would risk.
Verified on a live project: six object kinds across 27 violations, all
with at least one related object.

Also in this build:

Lib_AddSymbolText places body text in a symbol, so imported text reaches
Altium instead of being dropped. Selection is qualified by part id, so a
multi part component keeps its parts addressable.

PCB_ApplyDnpPasteExclusion removes paste from components marked do not
populate. Restoring is refused rather than guessed at, because the
applied state is not detectable after the fact.

IEEE symbol name conversion moves to Utils with a self test covering the
converters, so DelphiScript disagreement with Free Pascal shows up in
Altium's own engine rather than at the point of use.
Independent implementations from the published format specs, so no third
party converter is involved. Symbol, footprint, pads, drills, silkscreen,
3D model and metadata all convert. The Altium target returns an ordered
plan of this server's own library tools rather than synthesizing Altium's
binary formats, so parts are authored through the API.

The two importers share one neutral geometry model and one Altium
emitter, so they cannot drift apart.

The KiCad reader handles what the standard library actually contains
rather than the simple cases: derived symbols via extends, which are
over half of it and would otherwise convert to a part with no pins;
multi part components, which become one Altium symbol with per pin owner
part ids in a single call instead of a flat merge; hidden pins, kept and
kept hidden; DeMorgan body styles, one taken so pins are not duplicated;
arcs recovered from start, mid and end form; active low and clock pin
markers; and the closing edge of a filled outline, which fp_poly stores
without repeating the final vertex and which leaves a notch in thousands
of shipped footprints if consecutive pairs alone are walked.

The s-expression reader is written here rather than depended on, so its
escaping rules are verified rather than trusted.

Geometry with no faithful equivalent is reported in warnings rather than
quietly approximated. That includes the two cases that look correct
after conversion and are not: a slotted drill emitted round, and an
unplated hole emitted plated.
EDA_AGENT_TOOLSET=minimal, or --toolset minimal, advertises only
tool_catalog and tool_invoke. Every other tool stays registered and
reachable through them, so nothing is lost, but the advertised surface
drops from 396 to 2.

tool_catalog returns a filtered, classified index: by category, by
maturity, by interaction, or by substring. tool_invoke then runs a tool
by name. Filtering is the point, since an unfiltered index is larger
than the surface it replaces.

with_schema is available on the catalog and matters here: under the
minimal toolset no tool schema is loaded up front, so calling
tool_invoke without first asking for the schema means guessing argument
names, and several tools take names that look obvious but are not.

Both toolsets serve the same backend registration, so a tool cannot
exist in one and be missing from the other.
A repair port is a label placed where a connection is missing. Drawing a
real wire stub instead reads correctly on the sheet, but only when there
is room: the stub must clear component bodies, and its glyph must not
land on existing text. Both are checked before the upgrade, and a stub
that cannot be placed safely stays a port rather than being drawn over
something.

Footprint policy and the design audit report what they could not check
rather than passing silently, so a skipped check is visible in the
result instead of looking like a clean pass.

Adds tests for sheet loading, sheet listing, net verification, and the
symbol limits the benchmark relies on.
The batch grammar separates fields with semicolons and records with a
pair of tildes, so a value containing either silently splits into the
wrong shape. payload_safe escapes those, and the importers scan for
characters above the byte boundary the transport can carry, reporting
them as a warning rather than writing text that arrives mangled.

The scan lives in the shared plan builder rather than in one importer,
so both the EasyEDA and KiCad paths get it and cannot drift apart.

obj_batch_delete now enforces the same confirm_delete_all guard that
obj_delete does. Deleting every object of a kind was reachable through
the batch entry point without the confirmation the single object path
requires.

Recovery guidance names the steps to take rather than describing the
fault, so a stalled polling loop can be cleared without reading the
source. A dropped request is distinguishable from a slow one: the trace
records whether Altium ever saw the request file at all.
Several tools ran a sequence and returned ok even when part of it did
not take. A caller reading only the status had no way to tell a complete
run from a partial one. The affected paths now report what succeeded and
what did not, so a partial result is visible as a partial result.

Fixes found while going through the tool surface:

The net delete list was split on a comma, so a net whose name contains
one was cut into pieces and the wrong nets were deleted.

The variant filter accepted words that were never in its vocabulary and
silently matched nothing, so a filter that looked applied did nothing.

Moving library components in dry run mode reported the move as done.

Printing all variants left the active variant changed rather than
restoring it.

The health check could not tell a stale deploy from a healthy one when
the version was unreadable, so it reported a warning with a message that
did not say what was wrong.

Symbol export reported the selection it had changed rather than the one
it was asked for.
The corpus test runs the reader over KiCad's own libraries rather than
over hand written samples, which is what surfaced the cases that matter:
derived symbols, multi part components, pin markers, and the unrepeated
closing vertex in filled outlines.

test_kicad_accepts_our_output goes the other way and feeds generated
files back to KiCad, so the export is checked by the tool that has to
read it rather than by this project's own parser agreeing with itself.
A fact stated in two places with nothing enforcing agreement drifts.
These guards pin the pairs that had already drifted at least once.

Published tool maturity was derived rather than measured, so 121 tools
were advertised as simulator tested when the simulator rejects the
commands they send. It is now measured by probing an absent action, and
a catch all handler can no longer fake coverage.

The README named tools that do not exist. Tool docstrings recommended
identifiers that are not valid. Pipeline stage names in prose disagreed
with the stage list in code. The mils to millimetres factor was written
out in several places. Enumerated words crossing the bridge were checked
against no vocabulary at all.

Integration tests are gated at collection time on the environment
variable rather than skipped inside a fixture, because a fixture that
contacts Altium before deciding to skip has already contacted Altium.

Each guard was checked by reintroducing the defect it exists to catch.
Three of them passed against the broken code on the first attempt and
were rewritten until they failed.
0.4.0 was already tagged and shipped, so the declared version had to
move before anything here could be released.

CI now fetches tags. The default shallow checkout brings none, and the
check that compares the declared version against the tags had nothing to
compare, so it passed without testing anything in the one place it runs
unattended.

Documents the ten part providers and the two kinds they come in, the
library importers, the minimal toolset, and which sources answer without
a credential. The tool reference is regenerated from the code rather
than edited by hand, and a test fails if the committed copy falls behind.
Both asserted against KiCad's installed libraries, so they passed on a
machine with KiCad and failed in CI, which has none. That tests the
machine rather than the code, and it only shows up after a push.

The no-network test now asserts the part that must hold anywhere: the
fan-out completes and every source that failed says why, since a
provider going quiet is indistinguishable from one reporting no match.
The local provider answering is checked only where it is installed.

The download behaviour it covered is now also tested against a fake with
no download method, so it stays covered where KiCad is absent instead of
skipping in the one place regressions go unnoticed.
The availability check called search with an empty string, and
kicad_local returns early on an empty query before it ever looks for a
library directory. So the probe reported success on a machine with no
KiCad at all, the guard it fed was always true, and the two tests it was
meant to protect failed in CI exactly as before.

A non-empty query reaches the index and raises when no library directory
exists, which is the condition the guard is asking about.

Checked against both states this time, by forcing the symbol directory
away rather than by trusting an environment variable: the probe returns
false with no KiCad and true with it, and the two tests skip or pass
accordingly. The previous fix verified the branches and never verified
the predicate that selects them.
A plated via's copper sits between the finished hole and the drill
wall, because the drill removes material and copper is deposited on
the wall of the hole it leaves. The barrel area was computed for the
annulus on the far side of that wall, r_drill to r_drill + t, which
is copper in space the drill had already cleared.

For the common 0.3 mm drill with 25 um plating this overstated the
conducting cross-section by 18 percent, so thermal resistance came
out low by the same factor: 163 K/W per via where the geometry gives
192. Every count derived from it was low too. A 2 W part with a 20 C
budget was told 17 vias when it needs 20, and an array that looked
adequate was not.

The docstring already described the correct annulus, so only the
arithmetic changed. Plating thicker than the radius closes the hole
and the barrel becomes a solid cylinder; a copper-filled via is
copper across the drilled hole, not beyond it.

Test expectations are derived from the geometry rather than read back
from the implementation.
A mechanical layer's KIND is what resolves it by purpose: courtyard
checking, assembly drawings, 3D body placement and the IPC-4761 via
treatments all read it, and a layer named "Courtyard Top" carrying no
kind is skipped by every one of them. Nothing here could write it.

Kinds come in two enums, which is why the first attempt failed. A
single kind such as Fab Notes belongs to the layer. A paired kind is
held by the layer PAIR under a shorter name with the side dropped, so
"Component Outline Top" is stored as the pair kind "Component
Outline" across two layers. Writing the layer property for a paired
kind reads back unchanged and leaves the mapping stream empty.

Three things follow from that and each cost a live measurement:

AddPair appends without checking, so calling it on a pair that
already exists leaves a duplicate. Repeated sweeps left one library
with fourteen pairs where four were wanted, all visible in the Layer
Stack Manager. Every join now drains existing pairs first, which also
yields the index, since AddPair is the only call that reports one.

A pair kind is exclusive the way a layer kind is, so the first
attempt is refused while another layer still holds it. Releasing that
layer and stopping there left the library with fewer kinds than it
started with. The assignment is retried once the old holder is clear,
and the released kinds are put back if it still will not take.

Joining a pair renames both layers to Altium's own Top and Bottom
keywords, so any name asked for in the same call is reapplied
afterwards, for the partner as well as the target.

Layers above 16 are reached through LayerUtils.MechanicalLayer, and
tidy_pairs removes pairs that no kind justifies, bounded at layer 64
and reported rather than silent.

lib_run_across runs one library command against many libraries inside
Altium, failing per library so one that will not open does not
abandon the rest. pcb_set_mech_layer_kind takes partner_layer, since
it acts on one layer per call and could not otherwise pair;
pcb_get_layer_display and pcb_set_layer_color expose the view
settings, which follow the installation rather than the file.
Everything here was found by connecting to a running EasyEDA Pro and
watching what it actually does. None of it was visible offline,
because the fixtures had been written from the same assumptions as
the code and so agreed with it.

A schematic wire is a list of flat [x1, y1, x2, y2] SEGMENTS, not a
list of [x, y] points. Three call sites sent points, which is a list
of malformed segments, so create returned null and drew nothing. The
single wire, the bulk wires and the bus had none of them ever drawn
anything. The editor applies no transformation, so what a caller
sends is what the sheet gets, and wire geometry counts y downward
while sch_PrimitiveComponent counts it upward.

A refusal is not an empty result. The adapter read reply["result"] or
{} and ignored reply["error"], so design.snapshot refusing on a
schematic became a snapshot of nothing and review_design reported a
clean board with zero findings. It read violations past ran:false in
the same way, so an ERC whose reply said "This is NOT a clean result"
was reported as a clean ERC.

Tool registration and backend resolution had separate sources of
truth: registration took the backend as an argument while the
resolver re-read the environment. Anything embedding the registry
diverged, and review_design returned a complete, plausible review of
a different EDA's open document. An unrecognised backend name is now
refused rather than silently becoming Altium.

A JSON string where a list was declared hung the editor. list() over
a string yields its characters, so eight arguments reached a method
that never answers and the connection was dead for the whole timeout.

Search declared query optional and then always failed with the
default. Only lib_Symbol.search hangs on an empty query; the other
three answer with a default page. The second search argument turns
out to be a library uuid, and the ten-result cap is now reported
rather than left to look like a complete answer.

lib_Device.get omits the 3D model that search reports for the same
uuid, so a device read straight looked unmodelled. pcb_Document has
no autoRouting method at all: the API exposes importing routing
produced elsewhere, not running a router.

Re-importing the extension leaves the previous instance running with
its socket open. One editor held seven connections across three
builds, and which one answered a command was a coin toss, so a fix
verified against one build measured as absent minutes later. The
bridge now records each socket's build and retires superseded ones.
The extension could also wedge and never reconnect, because a health
probe with no timeout held the in-flight flag forever.

The EasyEDA test corpus is deleted rather than repaired. It was
written alongside the extension from shared assumptions and passed
green through every defect above. What replaces it is grounded in two
captures from a live editor: the API surface, 92 classes and 675
methods, and the shapes of 37 replies. Shapes only, never values.
Four files so far, each guard checked by reintroducing the bug it
exists to catch. The first found a call to a method the runtime does
not have.
The README had grown to 1031 lines and 18000 words, of which GitHub
renders only the first part, so roughly half of it was invisible on the
repository front page. Three sections carried more than half the file.

The tool catalog is deleted rather than moved. It was a hand-curated
copy of docs/TOOL_REFERENCE.md, which is generated from the code, so it
was a second source of truth for a fact the code already states. It had
drifted: the headers read 65 Library and 105 PCB while the code
registered 67 and 108. The README now links the generated file, and the
guard that compared the two is replaced by one that fails if
per-section counts reappear.

Part sourcing and the backend descriptions move to docs/PART_SOURCING.md
and docs/BACKENDS.md, each with a short summary left behind. What
remains answers what this is, whether it runs here, how to install it
and what it cannot do, in 458 lines.

Six guards read the README. They now read the documentation set, named
once in tests/documentation_set.py, because a tool named in BACKENDS is
the same promise to a reader as one named in the README and a guard
that stops looking at it stops protecting it.

Three of those guards were only found by running the suite. They build
the path inline rather than naming a constant, so searching for the
filename did not reveal them.

Two of them were also scoped to nothing without saying so: both matched
only the hand-written "### Name (N tools)" heading, while the generated
reference writes "## library (67)". Their own vacuity checks caught it.
A third compared against the altium and kicad surfaces only, so every
easyeda tool read as undocumented once the set covered three backends.

The list of canonical circuit blocks moved to the feature list rather
than being dropped. It was only in the deleted table, and the generated
reference truncates that docstring before the block names, so it would
have left the documentation entirely.
@Rouniy
Rouniy merged commit ebf1ab6 into main Aug 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants