The defect that froze the application window was type-correct. It compiled, the type checker passed, the tests passed, and the operating system reported the application as not responding.
The cause was a save dialog called through a blocking API from inside a command, which blocks the event loop the window runs on. Nothing in the type system objects. No headless harness notices. Finding it requires a person at a real desktop, clicking the button.
This is the third of four experiments in AI-assisted reconstruction of legacy systems, and it occupies the least favourable position of the four. The first had a formal specification and an independent interpreter. The second had a novel and built its own physics oracle. This one has neither: the source of truth is what a product felt like, and no test establishes whether a category browser feels like the original. The findings are correspondingly qualitative, and they are also the most transferable in the series. Both facts follow from the same missing oracle.
Every defect that mattered sat at the runtime boundary
The pure logic — archive and URL parsing, API backoff, media-cache rewriting, database ordering, live-title normalisation, cache naming, bookmark deduplication — compiled correct and passed on the first or second attempt, consistently.
Two defects mattered, and both sat at the seam with the host platform. The first was the blocking save dialog. The second was DOM nodes inside an iframe arriving from a different JavaScript realm, so an instanceof check in the parent shell fails on a clicked target that is unambiguously an element.
Both are invisible to the type system and to any headless test. Both required a person at a real window.
The practical consequence for anyone running an agent on desktop software: the logic is rarely where the risk sits. Human validation belongs almost entirely at the host, runtime and operating-system boundary.
The lesson also compounds. Once the blocking-dialog failure was recorded, the archive picker was built on the non-blocking callback pattern from the start: the backend opens the dialog with a callback, validates and persists inside it, and reports back over an event the interface listens for. A runtime failure, written down, becomes a convention instead of a defect relived.
The modern substrate is never the shape its documentation claims
The offline path was intended to use a pure-Rust archive reader. The library looked correct — random access, deferred loading, thread-safe. It panicked with an integer overflow before finishing the open call, against a real modern archive. Offline mode runs on a server subprocess instead.
The pattern recurred in unrelated features. One extraction endpoint returned HTML unusable as a reader format, so article HTML comes from the full parse endpoint. Live image URLs came back protocol-relative rather than the absolute form everything downstream assumed.
Three unrelated features, one rule: for a revival, the modern content, format or API differs slightly from its specification and from the model’s memory of it. Probe a real sample before building on it.
The abstraction that paid for everything
Before it was provably needed, the reader was decoupled from its content source behind a single trait exposing article retrieval, search and image fetch.
Adding a second content source later — live network content alongside the local archive — required two commands and a mode flag, with no reader rewrite. Replacing flat-file storage with a database was invisible above the command boundary.
For software that assumed one sealed data source — a CD, a bundled database, a mainframe — inserting that seam first converts later modernisation from a rewrite into an adapter. It is also the decision an agent is least likely to make unprompted, because at the moment you make it there is exactly one source and the abstraction looks like overhead.
Two late features were paid for by that decision and one like it. The attribution aggregator became a grouping query rather than a re-fetch, because provenance was stamped at save time when the notes feature was built. Bundling the content server binary was a two-line change, because binary discovery had long been a prioritised candidate list rather than a hard-coded path. Packaging work disproportionately rewards or punishes architectural decisions made when the feature that needed them did not yet exist.
Testability as an architectural constraint
The only layer testable without a desktop window is pure functions. The code was therefore shaped so the difficult parts are pure functions, and each feature’s desktop-only remainder shrank to a short explicit list. The backend suite grew from nothing to 23 offline tests.
Asking what can be verified headlessly is a useful question about how to structure code, not only about how to test it. Under an agent it approaches essential, because the agent’s verification loop is only as good as the surface it can reach.
Checking a model with code beats checking it with a model
The quiz generator treats model output as untrusted input: strict parsing, structural validation, then a verbatim grounding check requiring the answer to appear literally in the source article before anything is stored. Questions carry the revision they were generated from, so they can be flagged when the article moves.
A second model had been proposed as a fact-checker. A deterministic string comparison does the work for a fraction of the cost.
The first real run then demonstrated that rule’s limit. Two of three generated questions were strong. The third asked which option is a section heading, and the answer was a word that is a section heading. The question is perfectly grounded and useless, because it tests document structure rather than knowledge. Whether the answer appears in the source and whether the question is worth asking are different properties, and only the first is codeable. The repair belonged at the prompt layer and eliminated the class on the next run.
Quality control for model output is layered: a cheap deterministic gate for the codeable invariant, prompt design for the one that is not.
The last mile is where verification stops
Every earlier phase ended with a headlessly provable artifact. Packaging produced the first deliverable the agent’s environment structurally could not generate, because the bundler and packaging tools are absent from it.
What kept that honest was decomposing the deliverable into independently verifiable layers: configuration validity, the fetch script run end to end against the real archive, runtime path resolution with its own tests. Only the final bundler invocation remained unproven, and the repository says so rather than shipping an untested pipeline to disguise it.
That work also exposed a boundary the configuration layer silently drops. The bundler copies declared resources into the payload without guaranteeing the executable bit survives, so a bundled binary can land non-executable and fail at spawn with an unhelpful permission error. The repair lives in runtime code: the resolver sets the bit before returning the path.
A related result came from the memory pass. The plan prescribed virtual scrolling for an assumed oversized rendered DOM. It was architecturally inapplicable, because articles render inside an isolated browsing context the parent framework cannot window, and only one is live at a time. The actual unbounded growth was one layer up, in a history list retaining every visited article’s full HTML. When the plan and the architecture disagree, re-read the architecture: an inherited plan encodes assumptions that later decisions have already invalidated.
What this case demonstrates
An agent carries a desktop application from nothing to a shippable product, and the code is largely correct. Throughput is not the constraint.
Each feature ends with a short list of conditions only a person at the real window can confirm, and those checks are serialised. That human-in-the-loop runtime check is the bottleneck in AI-assisted desktop revival, and it is precisely where all the defects were.