Silence must never read as success
The rule everything else follows from — and the record of how often it is broken.
A check that could not run, could not reach its evidence, or could not decide must never be reportable as one that looked and found nothing.
This is the enabling condition, not a quality preference. A person catches a bad render by glancing at it; a script does not glance, and neither does an agent. Strip out the human who would have noticed, and the tool's own honesty is the only thing left holding the result up.
That is the whole reason this org can exist at all — so it is the first rule, and it binds drivers as hard as it binds checkers. A driver that swallows an engine error and returns a truncated artifact poisons the loop upstream of every check that would have caught it. The rule itself, and the discipline that follows from it, live in AGENTS.md §2. This page is the evidence.
How often it is violated is the point
As of 2026-09-12 this exact defect — no state for could not tell, defaulting to green, to a wrong verdict, or to a signal the caller cannot read — has been recorded fifteen times, across five unrelated domains, written at different times by the same author. Thirteen are fixed, one is accepted, one is open.
Three states, because two could not carry the answer. open means nobody has fixed it yet. resolved means it is fixed. accepted means it has been measured and deliberately not fixed, because every channel that could refuse the defect refuses correct work too — case 7 is the only one, and its three issues stay open naming the boundary rather than waiting on anybody. Calling it open said work was pending; calling it resolved would say the tool no longer does it. Both were false, which is the same shape as the defect this page is about, in the page itself. netspec, gerberdiff and partspec each grew a third state this week for the same reason.
Every case here is tracked by an issue labelled
silence-defect.
That label is what links finding an instance to recording it, and it makes the
drift checkable rather than merely regrettable: if the label and this page
disagree, this page is wrong. It went stale twice before the label existed —
once by five cases, and once by two within a single day.
Nobody set out to build any of them that way. Assume you are doing it too.
| # | Where | Layer | Status |
|---|---|---|---|
| 1 | gerberdiff — dropped flash reads as no change | verify | resolved |
| 2 | netspec — no third verdict | verify | resolved |
| 3 | orlab — version fallback is log-only | drive | resolved |
| 4 | prusaslicer-first-layer-flow — OK when never installed | verify | resolved |
| 5 | orlab — unknown flight events dropped | drive | resolved |
| 6 | partspec — tool fault emitted as part fault | verify | resolved |
| 7 | partspec — undef dimension passes green | verify | accepted |
| 8 | partspec — measures a part whose data file is absent | verify | resolved |
| 9 | netspec — explicit engine request silently ignored | drive | resolved |
| 10 | gerberdiff — an unmodelled trace reads as no change | verify | resolved |
| 11 | prusaslicer-py — unusable engine reported as a code failure | drive | resolved |
| 12 | prusaslicer-py — a preamble line returned as the version | drive | resolved |
| 13 | netspec — a real failure blamed on an invented cause | verify | resolved |
| 14 | prusaslicer-py — an installed macOS engine reads as absent | drive | open |
| 15 | prusaslicer-py — an example reports its own failure and exits 0 | drive | resolved |
The record
1. A dropped Gerber flash reports as no change resolved
A flash selecting an undefined aperture is dropped with no diagnostic of
any severity. diff and geomdiff report
0 changes at exit 0 under --fail-on-diff,
and the JSON is byte-identical to diffing a board against a copy of itself.
Two things from that exercise generalise. The first reading was
wrong — the hypothesis under review was that warning-level
diagnostics leaked a silent pass; both engines in fact promote any
Error diagnostic to a raised GerberParseError and exit
2. The repro did not confirm the analysis; it replaced it. And
the tool already held the evidence: gerberdiff parse
prints nets: 2, apertures: 1 and a bounding box reaching the dropped
pad's centre rather than its edge. Nothing had to be measured that was
not already being computed — there was simply no state in which to say it. That
is what a missing third outcome costs: not a measurement, a verdict.
2. netspec reports "could not evaluate" as "the board is wrong" resolved
Resolved: Verdict gained incomplete
and error, and a skipped or unsupported rule now yields
incomplete at exit 2 instead of fail at exit
1. Decided together with A1, since exit 2 had to stop
meaning usage first. Note what the tests were already saying:
test_a_part_absent_from_the_design_is_skipped_not_failed asserted
verdict == "fail" — the name and the assertion contradicted each other,
because fail was the only non-green value the type offered.
Verdict = Literal["pass", "fail"], green only when every rule is
green — so an unsupported rule collapses into fail,
saying the board is wrong when the truth is the tool could not check it. See
A2.
3. orlab's version fallback reached the caller only as a log line resolved
The first instance found in the drive layer rather than the
verify layer: the same failure reaches a caller through a binding exactly as it
does through a checker. get_profile() correctly returned
(profile, exact), but OpenRocketInstance.__init__
consumed exact, logged a warning and discarded it.
Two things the reproduction added to the suspicion. The single signal
was suppressible — setting the root logger to ERROR, an
ordinary application choice, removed the warning entirely, leaving no exception,
no return value and no attribute. And the obvious workaround was
wrong: comparing or_version to
profile.version_string looks equivalent, but
parse_version maps 24.12.RC.01 to (24, 12)
— an exact match whose strings differ — so the reconstruction reports a
fallback that is not one, on every RC and point-release build.
The fix exposes profile_exact on both
OpenRocketInstance and SimulationPool. Verified here by
mutation rather than by trusting the closed issue: discarding the value and
always reporting True fails 4 tests; implementing it as the naive
string comparison fails 2, including the test named for exactly that mutant.
Restored, 72 pass.
4. A G-code validator reports OK when the feature was never installed resolved
Resolved: the positional fallback is gone, and an absent marker
gets its own exit code — 0 verified, 1 wrong,
2 the feature never ran. Worth recording what the fix nearly did: its
first version left "the feature never ran" and "you gave me a bad path" both on
2, one code for two meanings, which is A1 in miniature. The regression
test found it, on the run that was meant to confirm the fix.
Outside this org, and the reason the rule is stated as a general one rather than
a house style: validate-gcode.py reports OK at exit
0 when the feature it verifies was never installed.
5. orlab drops unknown flight events, and the result looks complete resolved
Resolved: get_events returns an
EventTimes mapping carrying unknown_event_types, so a
caller can tell "no such event in this simulation" from "orlab could not interpret
it". It subclasses dict, so existing callers are unaffected. The log
line remains -- the point is that the log can be silenced and the field cannot.
Helper.get_events drops flight-event types orlab's enum does not
know and tells the caller only through a log line. The returned dict then looks
complete: a caller iterating it cannot tell "this simulation had no such event"
from "orlab did not recognise the event and silently dropped it", and the log line
is suppressed entirely by an application that raises its log level.
This is case 3's defect one layer up, and arguably worse — #61 degraded a whole run's fidelity in a way a user might notice, whereas this quietly removes rows from a result the caller is about to analyse. It matters most on exactly the jars #61 concerns: a newer OpenRocket on a nearest-older profile is where unknown event types are most likely to appear.
6. A partspec tool fault is emitted as a fault in the part resolved
Resolved: a refused_by block maps each refused name
to "part" or "tool", and a "tool" refusal now
exits 4. Both halves were needed: a discriminator alone lets a consumer
branch while the default answer still reads as success. The payload is still emitted
in full, so one raising backend still costs one name and not the other thirteen.
When a measurement backend raises, the name lands in refused with a
reason — but the reason is prose, and it is the only signal
distinguishing a partspec bug from a defect in the part.
main(["measure", ...]) returns 0 on a monkeypatched
raise, so an agent reading the JSON must string-match English to tell "this part
has no centre of mass" from "this backend crashed".
The founding property one level in: a tool fault reported at exit 0,
in a block whose other entries mean "the part defeated this measurement", reads as
a statement about the part.
7. A dimension silently defaulted from undef passes green
accepted
An expression whose type error makes OpenSCAD substitute a default into a dimension. The engine prints one warning and exports a clean, watertight, single-solid mesh:
o = undef;
h = o + 1;
linear_extrude(h) square([40,30]);
linear_extrude receives undef and falls back to a height
of 100. partspec measure reports
volume: 120000.0 mm3 — 40 × 30 × 100 — and check with
watertight() and solid_count(1) exits 0. The
part is 100 mm tall because a name was undefined, and partspec reports it green.
The scalar case is narrated by nothing at all on either engine — no
warning to guard on.
8. partspec measures and draws a part whose data file is absent resolved
Resolved: all three verbs now refuse on one shared answer. Worth
recording what the fix nearly got wrong: the issue's own "smallest fix" — refuse when
the depfile lists anything missing — refuses a correct part. OpenSCAD
evaluates a % subtree, so an absent file inside one reaches that field
exactly as a real dependency does. Measured before writing anything:
%import("gone.stl") and import("gone.stl") produce identical
evidence and both measure 7200.0, and only the second is a fault. The narrowing moved
into the shared answer, so the three verbs cannot diverge on it either.
check refuses a build whose depfile names a data file that is not on
disk. measure and render do not ask that question:
against a part importing a missing STL, check exits 4
while measure exits 0 with
volume: 7200.0 exact and render exits 0
with four PNGs. That volume is a bare 40 × 30 × 6 plate — the import contributes
nothing, which is the whole point. An agent measuring that number writes it into a
contract that then passes forever.
9. An explicit engine request is silently ignored resolved
NETSPEC_KICAD_CLI pointing at a path that does not exist is ignored
silently. Discovery falls through to the next candidate and the run succeeds
against a different engine than the one explicitly requested, with
nothing said on stdout, stderr or in the report, at exit 0.
The variable is presented as authoritative, not as a hint — it is what the tool's own error message tells you to set.
10. An unmodelled trace reads as no change, and the two engines disagree resolved
The geometry engine does not model a stroke drawn with a macro or block aperture.
That limitation was documented in docs/geometry-diff.md under
"Known limitations" — the output simply never said so. A board with such a trace
added reported 0 changes at exit 0 under
--fail-on-diff, with the JSON byte-identical to comparing a board
against a copy of itself.
What makes this one different from the rest: the raster engine saw the
change. Same two directories, same question, opposite answers —
diff reported 1/1 layers changed at exit 1
while geomdiff reported nothing at exit 0. A tool that
contradicts itself is at least detectable; the danger was that only the quieter
answer gated CI.
The pattern generalises past this one call site. The same bare return
appeared five times in the emit path, one of them commented
"matches renderer: silently skip over-deep nesting" — the silence was
deliberate, consistent, and reviewed. Documenting a limitation is not the
same as reporting it, and a known gap that the output cannot express is
indistinguishable, at the point of use, from no gap at all.
Resolved: the engine records every operation it could not model,
the comparison reports identical | different | indeterminate, and this
case exits 2 naming the reason. Closes adjudication A3.
11. A PrusaSlicer that cannot start is reported as a failure of the code resolved
Resolved: discovery now answers in three states rather than two.
PrusaSlicer.probe() runs --help and raises
EngineUnusableError unless the engine started and answered, so
absent, present and unusable and usable are separable, and
conftest routes the middle one to the same skip-or-fail decision as the first —
one function, because the half that gets dropped when the decision is duplicated is
always the require half, which is the invisible one. The engine's own
complaint is carried as a field rather than left in prose.
Engine discovery accepts a Flatpak when flatpak info exits
0. That establishes the app is installed, not that it can
run. With an unwritable HOME the launcher fails before the
engine starts:
$ flatpak info com.prusa3d.PrusaSlicer # exit 0
$ flatpak run --command=prusa-slicer com.prusa3d.PrusaSlicer --help
error: mkdirat(.var): Permission denied # exit 1
Discovery is unaffected, because it only ran info.
PrusaSlicer() constructs and reports
engine_kind="flatpak", so conftest.py — which skips
engine tests by catching FileNotFoundError — does not skip.
Same tree: 3 failed, 40 passed, 3 skipped against
43 passed, 3 skipped with a working HOME.
The machinery for this exists and does not fire. conftest's skip message reads
"This is an environment fault, not a verdict on the code under test" —
correct for an absent engine, absent for a present-but-unusable
one. Under PRUSASLICER_PY_REQUIRE_ENGINE=1, whose whole job is
asserting an engine was really exercised, the flag cannot tell "engine ran, code is
wrong" from "engine never started".
Why this is here despite going red. It was first written up as not a case, on the reasoning that it fabricates no success — D5's artifact verification means no artifact, no green. That line is the wrong one, and case 2 is why: netspec reporting could not evaluate as the board is wrong also produced a red, and is case 2. Green-from-nothing is the sharpest form of this defect, not the definition of it. What the record tracks is the state a caller cannot distinguish, and the third outcome having no channel is the same defect whichever neighbouring answer it collapses into.
12. A build that greets you before it identifies itself reports the greeting as its version resolved
Resolved: the banner is matched rather than counted to
— the first line matching ^PrusaSlicer-(\d\S*), on either stream
— and a build that identifies itself nowhere raises
VersionUnreadableError instead of returning something. The signature
stays -> str: there is no line to return, so nothing is returned, and
the third outcome is the exception rather than a widened return type. Note which
narrowings were refused. Requiring based on Slic3r would answer
could not tell for a version plainly present, and §2.1's third outcome is
not licence to refuse more than the question requires.
PrusaSlicer has no --version flag — 2.9.6 answers
Unknown option --version and exits 1 — so the version
is read from the first line of --help. The native Windows console build
prints a preamble first:
System OpenGL library successfully released
PrusaSlicer-2.9.6 based on Slic3r (with GUI support)
check_version() returns "System OpenGL library successfully
released", at exit 0, as the version. Reproduced against a stub
printing those two lines.
The Linux Flatpak has no preamble, which is why the code carries this comment: "the version is only ever printed as the first line of --help", followed by the Flatpak's banner as the example. A claim about every build, measured against one.
This one fabricates rather than misroutes, which puts it on the
worse side of the severity axis from case 11. The signature is
-> str, so there is no third outcome available: a version it could not
read comes back looking exactly like one it could. Found by a sibling project's
four-platform engine matrix — the defect needed a second platform to become
visible, and neither project had one.
13. netspec throws away the cause it was handed and states a different one resolved
Resolved: netspec-mcp now separates "the package is
absent" from "the package is here and the symbol moved", using
find_spec("mcp") as the observation, and prints upstream's own text in
the second case. Both still exit 4, because both still mean netspec
could not tell you anything about your board.
Under mcp 2.x, mcp/server/fastmcp.py is a stub that
raises, and upstream is about as helpful as a library can be about it — naming
the rename to MCPServer, the new import path, the migration guide, and
the mcp<2 pin that keeps v1 code running. Reaching it took a
fresh venv with the constraint overridden: netspec's own extra is
mcp>=1.27,<2 and has been since the scaffold commit, the widening
to <3 is an open pull request, and so no released netspec resolves
2.x. The typecheck that caught it ran on that branch, not on main.
$ python -c "import mcp.server.fastmcp"
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where
FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and
other APIs changed; see the migration guide at ... or pin 'mcp<2' ...
The entry point caught ImportError, and
ModuleNotFoundError is a subclass of it, so every word of that was
discarded and replaced with netspec-mcp needs the mcp extra: pip install
'kicad-netspec[mcp]'. The extra was installed. The user is told to
install what they already have.
Which is not confined to the unreleased pin, and that is why this
is a case rather than a note on a bump. The handler was a bare
except ImportError that read nothing off the exception, so it answered
every import failure with the same sentence. An incomplete install —
mcp 1.30.0, well inside the shipping <2 pin, with a
transitive dependency of it missing — arrives at the same handler and gets the
same wrong cause. 2.x is the sharpest illustration, not the reach.
This one misroutes rather than fabricates, which places it beside
case 11 rather than case 12. The failure is real, exit 4 is the right
channel for it under §2.2, and netspec genuinely could not run; only the cause was
invented. The issue's own prose calls that reason fabricated — on this record's
axis the word belongs to case 12, where a version that could not be read came back
looking exactly like one that could.
It is worth saying where the line runs, because a neighbouring netspec issue sits
on the other side of it and is deliberately not on this page.
netspec#26 carries an
environment fault in the exit code but not as a report field — an omission, and
nothing it says is false. But omission is not the line, because case 3 is a pure
failure to say something and is on this record. The line is whether the channel can be
silenced, which is #26's own argument: case 3's only signal was a log
line, and an application setting its root logger to ERROR removed it
entirely, leaving nothing at all; #26's channel is an exit code, which no caller's
configuration can turn off. #35 needs neither test — it held the right answer
and stated a wrong one.
14. An engine that is installed reads as absent, and every engine test skips open
_find_executable is shutil.which(cls._exec_name()) and
nothing else, and _exec_name() answers prusa-slicer on every
non-Windows platform. The Homebrew cask installs
/Applications/PrusaSlicer.app, whose binary carries the
application's name — PrusaSlicer — and puts nothing
on PATH. Discovery reports the engine absent on a machine that has it.
The second-order effect is the worse half, and it is why this is
fabricate rather than misroute. conftest decides whether to skip an
engine test by constructing PrusaSlicer() and catching
FileNotFoundError; on a cask install that raises, so every engine test
skips on the one machine that could have run them. That is §2.4's a skipped test
is not a passing test, and the same shape as partspec's
195 passed / 23 skipped with no OpenSCAD on any runner.
Whose green it is, precisely. Not prusaslicer-py's own CI:
engine.yml's macOS job symlinks the bundle's binary onto
/usr/local/bin/prusa-slicer and runs under
PRUSASLICER_PY_REQUIRE_ENGINE, so a skip there is a hard failure. It is a
user on a cask install who gets the green that verified nothing, and a
workflow step is what stands between the two.
Not reproduced here, and recorded as case 2 is: there is no macOS
host on this bench, so what is read from source is the discovery path and the skip
cascade. What makes it more than a code reading is that the discovery miss was
observed on a real macOS runner by a sibling project's engine matrix, immediately
after Homebrew installed the engine. slicelab fixed its own discovery for it
in 17f539f — per-platform candidates, because which name a bundle
uses is a packaging decision — and reported the same shape across. This
repository still has the single name.
The repository already knows the bundle layout — slicer.py
handles .app when locating shapes resources, commenting that
looking only beside the binary broke
brew install --cask prusaslicer. The shape is understood for resources
and not for discovery. Against case 11 it is the same module in the opposite
direction: 11 is an engine found but unusable, this is an engine present but not
found, and both are discovery answering with more confidence than it established.
15. A driver's own example reports its failure and exits 0 resolved
Resolved: both examples exit non-zero when they produce no G-code and when the shape they need is absent. All four exit sites are covered by tests that run the examples as real processes and assert the status — asserting it in-process is exactly what would not have caught this.
The example passed layer_height to additional_args, which
builds --{key} verbatim, so the engine answered
Unknown option --layer_height and produced nothing. The example caught
the error, printed it, and ended:
$ python examples/torus_example_attempt.py
No G-code produced: PrusaSlicer exited 1 slicing .../torus.stl
Unknown option --layer_height
$ echo $?
0
The library was scrupulously correct throughout.
slice_model raised SliceError carrying the engine's
returncode, stdout and stderr, exactly as its
D5 promises. Nothing was silenced by prusaslicer_py; the example dropped
the answer. That is why this case is here rather than filed against the driver: an
honest library does not save a caller who discards what it returns, and the file
doing the discarding was the one whose job is to demonstrate the library working.
Both channels were not wrong — only the machine-readable one.
The message is printed and accurate. A person running the example sees the failure;
a smoke test reading $? sees success. This page's own opening is the
argument for counting it: a person catches a bad render by glancing at it, and a
script does not glance.
Two things from the fix generalise. The obvious repair was wrong:
only layer_height becomes a real option by swapping the underscore
— there is no --infill-density (it is --fill-density),
no --print-speed (speeds are per-feature) and no
--extruder-temperature (it is --temperature), so a
mechanical _→- pass would have left the example broken
while looking repaired. And two of the four demo values were the engine's own
defaults, so checking that a value "came back" could not distinguish the
option taking effect from it never being passed.
The examples are not run in CI, so the harm was to a person or an ad-hoc smoke test rather than a green pipeline. It became reachable only when an earlier fix removed an outer failure: until the driver stopped naming a Windows executable, the examples died earlier on Linux and never reached the engine. Removing an outer failure exposes an inner one, and a repository with no example smoke test does not find out.
What the record is for
Three of the first four were found by running the code after a reading of it had concluded something different, twice concluding the wrong mechanism entirely. That is why AGENTS.md §7 requires a reproduction before a report, and why a fix is verified by breaking the thing it fixes and watching the check go red.
This list is what has been written up. It is a floor, not a census.