I set out to build an AI document app. I built a PDF parser instead.

· 4 min read #ai #llm #rust

The last status document I wrote for AI Suite is dated October 29th, and it ends with a plan for the next morning: "Fix C++ compilation, 1-2 hours. Debug header includes." It's a confident little note. I was going to clear the blocker over coffee and get back to the real work.

That was almost three weeks ago, and I haven't opened the project since. So before I lie to myself about why, here's the real version.

What I was supposed to be building

The product was a good idea, and I still think so: An app that runs entirely on your own machine and lets you chat with your documents. There's no cloud, no uploading your files to someone else's server, no subscription. You point it at a folder of PDFs and contracts and lab results, and you ask it questions. The whole pitch was privacy and simplicity: The hard AI stuff happens on your laptop, and you never have to think about it.

That's the thing a person would actually use. Hold onto that sentence, because I lost it.

What I actually built

I built architecture, so much architecture.

There's a document in the repo called AUTHORITATIVE_ARCHITECTURE, version 2.1.0, that declares itself "the single source of truth" and "canonical." It has a section of "finalized" decisions. There are roughly twenty of these documents, eighty thousand words in all, each one carefully timestamped down to the timezone. I wrote a nine-point rationale for switching the GUI framework from Slint to iced, complete with a migration checklist, for a window the user had not yet seen because it did not yet exist.

None of this was stupid, exactly. The reasoning in each document is sound. FlatBuffers over named pipes is a defensible way to talk between a Rust core and its modules. iced really is nicer than Slint for a multi-window app. Every individual decision was the decision of someone who knew what he was doing. Put together, they were a way of doing legible, satisfying, finishable work so I didn't have to do the other kind.

The rabbit hole with my name on it

Then I found the perfect hole to climb into, and I want to be precise about how perfect it was.

The app needed to read PDFs. There's a mature Python tool, docling, that does this well, and the whole thing already worked through it. But docling is Python, and I had decided the suite should be pure Rust, so I set out to replace it. I wasn't going to wrap it. I was going to replace it.

I wrote a PDF parser from scratch: Twenty-seven hundred lines of Rust, six layers, character extraction matching docling to within 99.7% across thirty test files, bounding-box and word-grouping algorithms reimplemented to match docling exactly. It's genuinely good code. I was proud of it, and I should be, and that's the problem.

Reimplementing a PDF parser is a known, bounded, deeply satisfying engineering problem with a clear right answer, and building a product that strangers will trust with their private documents is none of those things. One of them has a green test suite at the end. The other has a user who might not like it. I chose the green test suite every single day for a month.

When the pure-Rust path got hard, I didn't step back and ask whether I needed it. I pivoted to calling the original C++ library through an FFI, started compiling qpdf 12.2.0 and untangling header dependencies, and wrote myself that confident note about fixing the includes tomorrow morning. The yak was very well shaved. The yak was, at that point, the entire project.

What actually happened

I never shipped a thing a user could open, not one window they could click. After months of work, the deliverable was an exquisite foundation for an app that did not exist, and a PDF parser that competed with a free tool that already worked.

The failure wasn't technical. Every piece I built, I built well. The failure was that I kept choosing the work that felt like progress over the work that was the point, and the two had quietly stopped being the same thing sometime in September. Architecture is progress you can admire without risk. A from-scratch parser is progress with a scoreboard. A product is just exposure.

What I took from it

I tore it down, and I'm trying to keep the lesson where I can see it.

The lesson isn't "don't write good architecture" or "never reimplement anything." It's that when I notice I'm writing a document that calls itself authoritative, or reimplementing a tool that already works, I should get suspicious, because for me those are tells. They're what avoidance looks like when it's wearing an engineer's clothes. The real question, the one I wasn't asking, is simple: When did a user last touch this?

The things I've shipped since are smaller and uglier, and they have users, or at least they have the chance to. I'd rather have a rough thing someone can open than a canonical document about a beautiful thing nobody can. I knew that already. I'm writing it down because apparently knowing it wasn't enough.

Tagged: #ai #llm #rust