Skip to content

fix(verbs): measure and render refuse an absent build input too - #389

Merged
CameronBrooks11 merged 2 commits into
mainfrom
fix/355-measure-render-absent-inputs
Sep 6, 2026
Merged

fix(verbs): measure and render refuse an absent build input too#389
CameronBrooks11 merged 2 commits into
mainfrom
fix/355-measure-render-absent-inputs

Conversation

@CameronBrooks11

Copy link
Copy Markdown
Member

check has refused a build whose depfile names a file that is not on disk since #354,
but that fix was scoped to runner.py. The same part was still measured and still
drawn.

Reproduced on both pinned engines:

source check measure render
import("missing.stl") exit 4 exit 0, volume: 7200.0 exit 0, 4 PNGs

7200.0 is a bare 40x30x6 plate — the import contributed nothing, which is the whole
point. An agent measuring that number writes it into a contract that then passes forever.

The two verbs did not come along because they guard on a different signal: both key on
the engine's stderr markers, and an import() of an absent target emits none. It renders
as nothing, the mesh is well-formed, and those guards had nothing to see.

The issue's smallest fix would have refused a correct part

#355 says to refuse when stl_deps[-1].missing is non-empty. That is not safe, and
the repo already knows why: OpenSCAD evaluates a % subtree, so a file named only from
one reaches engine_inputs.missing exactly as a real dependency does. check filters
through _only_in_dropped_subtrees for precisely this (#354 review, B1).

Measured before writing anything:

source missing measure volume correct?
cube(...); import("gone.stl"); gone.stl 7200.0 no — the import is a real dependency
cube(...); %import("gone.stl"); gone.stl 7200.0 yes — a %-ed subtree is not part of the part

Identical evidence, opposite verdicts. The naive guard exits 4 on the second — the same
fault as the one being fixed, with the sign reversed.

So the narrowing moved into a shared runner.absent_build_inputs() and all three verbs
read it. That is #355's own step 3 ("one engine fact is not diagnosed three ways") applied
to the evidence as well as the wording. It costs nothing on the ordinary path:
_only_in_dropped_subtrees returns immediately when nothing is missing, so the extra
engine pass is paid only where a build input is already known absent.

Two guard sites in measure, not one

measure has two build paths. The --out FILE form must refuse inside
_build_to_file, before its rename
— a refusal asked afterwards replaces the caller's
good artifact with the hollowed one and then declines to measure it, which is the round-2
review finding on #306. The directory form is guarded at the shared post-build point.
render refuses before any view moves, so a previous run's four PNGs are byte-for-byte
untouched — verified, 0 PNGs written on refusal.

After

source check measure render
import("missing.stl") 4 4 4, 0 PNGs
%import("missing.stl") 3 0 0, 4 PNGs

Tests

15 added, parametrized over the same seven modifier shapes check already uses, so the
"all three verbs agree" claim is the thing under test rather than a comment. With the two
verb guards reverted and the tests kept, 9 go red and 6 stay green — and the 6 are
exactly the %/* rows across both verbs, the false-refusal controls.

One more asserts the three verbs share cause, hint and origin=None, so a reader who met
this through one verb recognises it through another (#308's rule).

just check clean, 1393 passed (baseline 1378).

Docs

SPEC-report.md §6.1's sibling-payload paragraph was written to describe only what #354
implemented and said so, naming this issue. It now states the wider rule, plus the
requirement that the narrowing be applied identically by every verb.

Closes #355

check has refused a build whose depfile names a file that is not on disk
since #354, but that fix was scoped to runner.py. The same part was still
measured and still drawn: measure printed the volume of a bare plate at
exit 0, render wrote four PNGs of it.

The two verbs did not come along because they are guarded on a different
signal. Both key on the engine's stderr markers, and an import() of an
absent target emits none -- it renders as nothing, so the mesh is
well-formed and there was nothing for those guards to see.

All three verbs now refuse on one shared answer rather than each deriving
its own, which is #308's rule applied to the evidence and not only to the
wording.

That shared answer includes the narrowing, and the narrowing is the half
that matters. A file named only from a % subtree reaches
engine_inputs.missing exactly as a real dependency does, so refusing on
that field alone -- the smallest fix as #355 states it -- exits 4 on a
correct part. Measured: `cube(...); %import("gone.stl");` and
`cube(...); import("gone.stl");` produce identical missing entries, both
measure 7200.0, and only the second is a fault.

Two guard sites in measure, not one, because it has two build paths. The
--out FILE form must refuse before its rename or it overwrites the
caller's file with the hollow artifact and then declines to measure it
(round-2 review of #306). render refuses before any view moves, so a
previous run's PNGs are untouched.

SPEC-report 6.1 widened: the sibling-payload paragraph was deliberately
scoped to what #354 implemented, and closing this is what makes the wider
statement true.

Closes #355
CI's mesh-only job runs apt 2021.01 with no display, which has no EGL
offscreen path, so every render exits 4 with "cannot render PNG without a
display" -- an environment fault with nothing to do with this issue.

The first draft read only the exit code. That failed the three rows that
expect 0, and, worse, PASSED the four refusing rows for the wrong reason:
they exit 4 there whether or not the guard exists.

Both verbs now assert the refusal names the absent input, and render
skips where the engine cannot draw at all. The refusing rows still run
without a display, because the refusal lands before any view is rendered
-- which is the guard's placement, asserted from the other side.
@CameronBrooks11
CameronBrooks11 merged commit 947d0b7 into main Sep 6, 2026
10 checks passed
@CameronBrooks11
CameronBrooks11 deleted the fix/355-measure-render-absent-inputs branch September 6, 2026 13:58
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.

measure and render still answer off a part whose data file is absent

1 participant