Skip to content

fix(parse): report draw operations that use an undefined aperture - #21

Merged
CameronBrooks11 merged 1 commit into
mainfrom
fix/undefined-aperture-silent-drop
Sep 6, 2026
Merged

fix(parse): report draw operations that use an undefined aperture#21
CameronBrooks11 merged 1 commit into
mainfrom
fix/undefined-aperture-silent-drop

Conversation

@CameronBrooks11

Copy link
Copy Markdown
Member

A flash or stroke whose D-code selects an aperture that was never defined --
or that draws before any Dnn selection at all -- carried an aperture index
with no definition behind it. Every consumer read that as nothing to draw:
both geometry emit helpers returned early on ap is None
(layer_geometry.py:280, :330) and the raster renderer drew nothing. No
diagnostic was emitted at any severity.

diff and geomdiff therefore reported 0 changes at exit 0, and the JSON
report was byte-identical to a comparison of two identical boards, so a real
fabrication change could pass a --fail-on-diff gate invisibly.

Where the check goes

At the point of use, not at selection -- a deliberate deviation from the
"cheapest: at selection" suggestion in the issue. Use is exactly coextensive
with the harm:

  • It also catches drawing before any selection, where _current_aperture is
    still 0 and apertures.get(0) is None -- the same silent drop, which a
    selection-time check would miss entirely.
  • It does not fail a file that selects a stray D-code and never draws with it.
    That file renders identically, so breaking a CI gate on it would be a false
    alarm.
  • Parse is the shared upstream, so one check covers parse, render, diff
    and geomdiff. Both CLI paths already promote Error correctly; they just
    never received one. No downstream change is needed.

Offending codes are reported once each, so one bad aperture used by thousands
of flashes yields one diagnostic rather than thousands. D02 moves and G36/G37
region contours consume no aperture and are skipped.

test_parse_minimal was itself an instance of this

The fix tripped exactly one pre-existing test, and it turned out to be a real
finding rather than a false positive. test_parse_minimal defined D10 but
never selected it, so its stroke carried aperture 0 and expanded to zero
geometry ops -- while the test asserted no error diagnostics. Its
len(img.draw_ops) > 0 assertion passed only because the parse IR holds the
op; the geometry was empty. A test named "minimal" was asserting silence on a
file that draws nothing. Its D10* selection is now load-bearing.

Every other test, including the real KiCad board fixtures, stayed green --
which is the evidence that the check does not false-positive on real files.

Verification

Before, on main:

$ gerberdiff geomdiff before after --fail-on-diff
geomdiff: 0/1 layers changed, 0 changes  (0 ms)
exit=0            # stderr: 0 bytes

After:

$ gerberdiff geomdiff before after --fail-on-diff
error: board-F_Cu.gbr: Draw operation uses undefined aperture D11 (line 7)
exit=2

diff and parse likewise now exit 2 on the same input; the control pair of
genuinely identical boards still exits 0 with 0 changes.

10 tests added. All 7 positive cases were confirmed to go red with the source
fix reverted and the tests kept; the 3 negative guards (D02 move, region fill,
valid flash) stay green either way, which is the shape that proves they guard
against the check firing on everything rather than merely passing.

Full CI-equivalent gate locally: ruff check, ruff format --check, mypy, the
ASCII scan, and 473 passed at 95.82% coverage (baseline was 463).

Closes #17

A flash or stroke whose D-code selects an aperture that was never
defined -- or that draws before any Dnn selection at all -- carried an
aperture index with no definition behind it. Every consumer read that
as nothing to draw: both geometry emit helpers returned early on
`ap is None` and the raster renderer drew nothing. No diagnostic was
emitted at any severity.

diff and geomdiff therefore reported 0 changes at exit 0, and the JSON
report was byte-identical to a comparison of two identical boards, so a
real fabrication change could pass a --fail-on-diff gate invisibly.

Check at the point of use rather than at selection. That is exactly
coextensive with the harm: it also catches drawing before any selection
(index 0), and it does not fail a file that selects a stray D-code but
never draws with it, which renders identically. Offending codes are
reported once each, so one bad aperture used by thousands of flashes
yields one diagnostic. D02 moves and G36/G37 region contours consume no
aperture and are skipped.

The existing promotion path turns the Error into exit 2 for parse,
render, diff and geomdiff alike, so no downstream change is needed.

test_parse_minimal was itself an instance of this: it defined D10 but
never selected it, so its stroke expanded to zero geometry ops while
the test asserted no errors. Its D10* selection is now load-bearing.

Closes #17
@CameronBrooks11
CameronBrooks11 merged commit d71f6ce into main Sep 6, 2026
8 checks passed
@CameronBrooks11
CameronBrooks11 deleted the fix/undefined-aperture-silent-drop branch September 6, 2026 02:08
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.

diff/geomdiff silently drop flashes using an undefined aperture, reporting 0 changes at exit 0

1 participant