We almost let a downstream tool execute a graph we knew was broken
TLDR: We built an exporter that turns a visual design into an executable graph for a downstream engine. The first version happily exported graphs with known-fatal errors in them, because "export" felt like a neutral serialization step. It is not neutral when the thing consuming your output will run it. The exporter became the gate.
The assumption that seemed reasonable
We had a design tool upstream and an execution engine downstream. Between them, an exporter serialized the design into a graph the engine could run. The natural mental model: the exporter's job is to faithfully represent whatever is on the canvas. Faithful representation means you export what is there, warts and all. Validation is somebody else's concern.
That assumption is fine when the consumer is a viewer or a storage format. It is quietly dangerous when the consumer executes what you hand it.
When it failed
We had, in the same system, a checker that could tell whether a design was correct. It caught the fatal class of error: two outputs shorted onto the same node, power tied to a signal line, the kinds of mistakes that do not just look wrong but produce garbage when run.
The exporter and the checker did not talk to each other. So the exporter would take a design the checker knew was broken, serialize it into a clean-looking graph, and hand it downstream. The execution engine would receive a well-formed graph with a fatal flaw baked in, and run it. The output would be wrong with no error, because nothing in the handoff said "this is broken."
We caught it by exporting a deliberately shorted design and watching a perfectly valid-looking graph come out the other end, ready to run. The serialization was flawless. The graph was poison.
What was actually wrong
The exporter was treating correctness as out of scope. But it sat at the exact chokepoint where correctness mattered most: the last moment before the design became something executable. Past that point, the error was invisible, because a graph does not carry the visual context that would let a human spot the short. The exporter was the final place a human-legible violation could be turned into a machine-legible refusal.
By declining that responsibility, the exporter was laundering a known-bad design into a trusted-looking artifact. The downstream engine had every reason to believe a graph it received was runnable. Nothing had told it otherwise.
What got deleted
The idea that export is neutral. We deleted the straight-through serialize path and replaced it with a checked one.
We also deleted an overcorrection we tried first: blocking export on any error at all. That was wrong in the other direction. A design with an unconnected pin is unfinished, not broken. Refusing to export unfinished work would make the tool infuriating, because people export works in progress constantly. We had to separate "incomplete" from "incorrect."
What replaced it
The exporter now runs the checker and gates on the fatal class only. Connection-correctness errors (shorts, contention, power-to-signal) block the export with a clear reason. Completeness issues (an unconnected pin) do not, they pass through, because an unfinished graph is a legitimate thing to serialize.
There is an escape hatch, a force flag, for the rare case where you knowingly want the broken graph out, for debugging. But the default is refusal, so the common path cannot silently ship poison.
The result is a guarantee the downstream engine can rely on: any graph it receives through the normal path is free of the errors that produce garbage. The engine no longer has to defend against inputs the exporter should have caught.
The generalizable lesson
A serializer that feeds an execution engine is not a neutral formatter. It is the last checkpoint where a known-bad input can be stopped while the error is still legible. If you already have a way to detect fatal errors, wiring it into the exporter is not scope creep, it is the exporter doing its actual job.
The nuance that makes it usable: gate on incorrect, not incomplete. Block what will run wrong; let through what is merely unfinished. Conflating the two either ships poison or blocks everyone's normal workflow, and both failures come from treating "broken" and "not done yet" as the same thing.