Skip to content

feat(check): add tsci check opens to detect unrouted nets - #4326

Draft
Reiss-Cashmore wants to merge 3 commits into
tscircuit:mainfrom
Reiss-Cashmore:feat/check-opens
Draft

feat(check): add tsci check opens to detect unrouted nets#4326
Reiss-Cashmore wants to merge 3 commits into
tscircuit:mainfrom
Reiss-Cashmore:feat/check-opens

Conversation

@Reiss-Cashmore

@Reiss-Cashmore Reiss-Cashmore commented Aug 16, 2026

Copy link
Copy Markdown

Blocked on tscircuit/check-shorts#49. This command calls
findBitmapOpens, which that PR adds. @tscircuit/check-shorts is pinned
here to a fixed tarball (0.0.19) and loaded from jscdn at runtime, so CI on
this PR will fail to resolve the import until #49 lands and publishes. The
code itself is complete: with that branch linked locally, typecheck and all
five tests pass.

Problem

tsci check shorts catches copper joined when it should not be. Nothing catches
the inverse — a connection that was declared but never routed. The build
succeeds, no short exists, and the board is dead.

That happened to me: a board shipped with two push-button ground legs unrouted,
and no part of the toolchain could have told me before I paid for it.

Change

Adds tsci check opens, mirroring check shorts:

  • same options: --mode pcb|gerber, --layer top|bottom|all, --pixels-per-mm
  • --ignore-net <name> (repeatable) for nets joined off the board by design —
    mounting holes bonded through a metal enclosure, signals joined by a cable or
    mating connector. Circuit JSON cannot express "joined outside the board", so
    suppression is explicit and per net name; ignoring an unrelated net cannot
    mask a real open (covered by a test)
  • same CDN-or-package loading (reuses loadCheckShorts)
  • exits 1 when opens are found, so CI and pre-fab scripts can gate on it
$ tsci check opens
Detected 1 open in board.circuit.tsx
1. top/pcb net split across 2 copper islands
   SW1.pin4, R1.pin1
   island at x=6.250mm y=-2.500mm pixels=1840
   island at x=10.000mm y=-10.000mm pixels=612

One behavioural difference from check shorts worth flagging: opens are a
whole-board question. A net routed top -> via -> bottom is fully connected, but
examined one layer at a time it looks split on both. Running per layer reported
an open for essentially every routed net (41 false positives on a real two-layer
board; 0 after). So --layer all runs a single analysis spanning every copper
layer, while an explicit --layer top|bottom still scopes to that layer.

False positives and scope — why this is a command, not a default gate

Before wiring this into the pipeline I probed the detector against hardware
that legitimately expects disjoint copper on one net; the full analysis and the
fixes it produced live in tscircuit/check-shorts#49. Summary:

  • Fixed at the detector: pins joined inside a component body
    (internallyConnectedPins — switch poles, relay contacts) and bridged solder
    jumpers were real false positives; declared internal connections now bridge
    islands the way vias bridge layers. This also keeps the check compatible with
    pushbutton: documented default internal pin connections are absent core#3115 (emitting pushbutton internal connections), which would
    otherwise have made it flag every 4-leg tactile switch.
  • Handled here: off-board joins via --ignore-net.
  • Not fixable in principle: a same-net 0-ohm net tie and an unrouted MCU
    GND pin have the identical circuit-JSON signature — one is a design, the
    other is my dead board. Silencing one masks the other, so same-net
    two-terminal links remain a documented false positive (model as two nets, or
    --ignore-net). Prebuilt circuit JSON captured before external routing
    (FreeRouting-style workflows) also flags everything, truthfully but noisily.

Because of that residual set, check opens is an explicitly-invoked
subcommand
— it is not added to any aggregate check, and it only exits
non-zero when the user runs it. Whether it should ever run by default, and at
what severity, is left as a maintainer decision with the analysis above as
input.

Tests

tests/cli/check/check-opens.test.ts — an unrouted connection is reported; a
fully routed board reports nothing; an enclosure-joined net flags by default;
--ignore-net silences exactly that net (feature-detected against the resolved
check-shorts typings, activates when the pin is bumped past 0.0.19); ignoring
an unrelated net does not mask a real open.

`tsci check shorts` catches copper that is joined when it should not be.
Nothing caught the inverse: a net the autorouter silently failed to
route. The build succeeds, no short exists, and the board is dead — a
real board shipped this way with two unrouted button ground legs.

`tsci check opens` reports any net whose copper is split across more
than one island, using findBitmapOpens from @tscircuit/check-shorts.
Mirrors check shorts: same --mode/--layer/--pixels-per-mm options, same
CDN-or-package loading, and exits 1 when opens are found so CI and
pre-fab scripts can gate on it.
Opens are a whole-board question in a way shorts are not. A net routed
top -> via -> bottom is fully connected, but examined one layer at a
time it looks split on both, so running the detector per layer reported
an open for essentially every routed net (41 false positives on a real
two-layer board; 0 after this change).

`--layer all` now means one analysis spanning every copper layer, which
is the detector's default. An explicit `--layer top|bottom` still scopes
to that single layer.
Some nets are joined off the board by design — mounting holes bonded
through a metal enclosure, signals joined by a cable or mating
connector. Circuit JSON cannot express "joined outside the board", so
the copper for such a net is legitimately split and the check would
report a false open. --ignore-net <name> (repeatable) maps to the
detector's ignoreNets option and suppresses exactly those nets by name;
any other net still reports, so ignoring an unrelated net cannot mask a
real fault (covered by a test).

The suppression test feature-detects ignoreNets in the resolved
check-shorts typings and activates once the pinned dependency is bumped
past 0.0.19.
@github-actions

Copy link
Copy Markdown
Contributor

This PR has been automatically marked as stale because it has had no recent activity. It will be closed if no further activity occurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant