Why documentation-first development helped us build with AI
TL;DR
Taproot is built documentation first: describing how something should work, with a working example, is part of building it.
Every building block in Taproot's design system has a live demo next to its example code.
Written records explain why the big decisions were made, so nobody undoes them later by accident.
That documentation lets the AI tools use each piece the way it was meant to be used, instead of guessing.
Why I started with documentation
Given that LLMs are built on language, it makes sense to me that we need to have documentation to teach AI tools how to use any given piece of code. Creating that documentation from the beginning helped the AI tooling use the pieces of our codebase consistently and effectively from the beginning. Without documentation, AI would have been guessing.
When I started working on Taproot in earnest, I wanted to practice documentation-driven development as much as I could. Describing how something should work, and giving someone a useful example, would be part of building it.

That decision led to WTFM (opens in a new tab), our documentation tooling. While we were building Espalier (opens in a new tab), the component library behind Taproot, we needed somewhere to explain the components and try them out, so an interactive documentation site was an early priority.
I believe that investment has helped guide our work with AI enormously. We've built up a body of language around the software: what things mean, how they behave, why we made particular decisions, and what using them should look like. An AI tool joining the work has something concrete to read.
A place to turn the knobs and press the buttons
Espalier is our design system and web component library: the buttons, inputs, dialogs and layout pieces we build interfaces from. Anyone building a screen with it needs to know what each component lets you configure, which events it sends, and how it behaves next to the others.
WTFM shows example code alongside a live demonstration. When an example contains a single instance of the component, it also adds controls for its attributes and a log of the events it fires. On the button page (opens in a new tab), you can read how the label, icon, and intent work together, then click the result and watch the event arrive.
That's useful while the component is still being built. If an example needs a page of explanation, we have a reason to question the interface. Maybe the behavior really is complicated, or maybe we've made something simple harder than it needs to be. Writing the example brings that up early.
What JSDoc documentation for a web component looks like
In Espalier, component descriptions and examples live in JSDoc comments next to the implementation. The button's class comment covers its events, icon placement, slots, and styling properties, each with a fenced HTML example like this one (trimmed here):
<esp-button
collapsed
label="Save"
icon="save"
icon-position="left">
</esp-button>The property it uses is documented the same way, right above the code that declares it:
/**
* Places the optional icon on the logical leading (`"left"`) or trailing
* (`"right"`) side of the label. Logical placement mirrors in RTL.
*
* ```html
* <esp-button label="Save" icon="save" icon-position="left"></esp-button>
* <esp-button label="Next" icon="arrow-big-right" icon-position="right"></esp-button>
* ```
*
* @type {"left" | "right"}
*/
@property({ attribute: "icon-position", type: String })
public iconPosition: ButtonIconPosition = "right";Our build runs the Custom Elements Manifest analyzer over that source to produce a machine-readable description of the library's public interface. The entry for this attribute comes out like this:
{
"name": "icon-position",
"type": { "text": "\"left\" | \"right\"" },
"default": "\"right\"",
"description": "Places the optional icon on the logical leading ...",
"fieldName": "iconPosition"
}WTFM reads the manifest to build the reference page and drive the demos. Each fenced example becomes a Code tab with the live component under it. When an example has a single button, an Attributes tab appears too: the union type turns icon-position into a pick list of left and right, and the boolean collapsed attribute gets a checkbox. Because the class comment documents an esp-clicked event, an Event Log tab records each click. Nobody wrote those controls by hand.
Someone still has to write the prose. A generator can pull out a property name and its type, but it can't tell you whether the explanation of what the property is for is right. Keeping that explanation next to the implementation means it gets updated and reviewed in the same change.
If you've run into spec-driven development, it's a close relative. GitHub Spec Kit (opens in a new tab) and Kiro (opens in a new tab) help an AI tool turn a feature request into a specification, a plan, and a task list before implementation, kept in a folder per feature. Our documentation is organized around the product instead: a component's examples and decision records stay current for as long as the component exists, and the per-feature specification lives in a backlog task.
Why AI tools like documentation
An AI tool has to discover the conventions of the particular software it's changing. Say Claude Code is adding a Save button to a Taproot screen. The Espalier example gives it the element name and a supported way to position the icon, the manifest describes the rest of the interface, and our repository guidance says when to compose existing components and where the docs live.
Larger decisions work the same way. Espalier has architecture decision records (ADRs) that explain why foundational pieces work the way they do, and its contributor guidance points anyone touching themes, typography, layout, or component behavior to the relevant ones. Code shows the current implementation but not always the constraints that produced it, so a local simplification can undo a decision made for the whole library. A written reason gives the AI tool and the reviewer something to weigh first.
What makes this useful to a large language model is how specific it is: names, examples, constraints, and reasons. Anthropic's write-up on context engineering (opens in a new tab) makes a related point about choosing relevant information and giving AI tools ways to retrieve it. We keep the detailed explanations near what they describe, the same reasoning behind keeping our backlog as Markdown in Git. The AI tool still has to find and read the right material, of course.
How do you keep documentation from going stale?
Written explanations go stale. A generated reference page can faithfully reproduce a comment that's wrong, and a live example can work fine and still miss an important edge case. So we connect documentation to checks wherever there's a clear contract to verify, much like we use contracts to keep our frontend and backend in sync.
Help links are one example. Espalier's help button (opens in a new tab) identifies a topic by its document URL and heading anchor. WTFM writes a help-manifest.json listing every documentation surface and its heading anchors, and the Taproot app keeps a list of the anchors it links to. The docs build ends by comparing the two:
wtfm-check-help-anchors <help-manifest.json> <expected-anchors.json> --strictIf someone renames a heading that the app links to, the build fails before anyone clicks a broken help link.
The other example is one I find especially useful. Espalier's architecture tests read the exception tables in one of its decision records, then look for components that break the base-class rule without a documented exception, and for exceptions that no longer match the code. Part of the document is an input to verification, so the tests catch some of the ways the explanation drifts from the code.
These checks have limits. An anchor check tells you a destination exists, not whether the explanation there will help a confused user. An architecture check can't decide whether the original decision was a wise one. Those are still questions for review and real use, and I went into more of that in how we enforce coding standards.
What documentation-first development costs
Examples need maintenance, and decisions need updating when their assumptions change. WTFM has its own code, tests, and release process, and making it a separate package gave us one more thing to maintain, the same cost I weigh before taking on third-party packages.
We get the value back by using what we write more than once. A component example helps someone build a screen, helps an AI tool find the intended usage, and gives a reviewer something to check a change against. On a small team that matters a lot: a fresh AI tool session needs a way to recover earlier decisions, and so does a person returning to code they haven't touched in a while.
I'd be careful about treating the amount of documentation as progress, though. Explanations that contradict each other create more work, and a long generated description that says little about intent doesn't help much. For Taproot, I think the effort has been worth it.
Start with one useful example
You can try this without building a documentation framework. Pick a feature or component you're about to build and write down:
Who uses it and what they should be able to do.
What a successful result looks like.
One small, working example of using it.
The constraints that would otherwise exist only in your head, and why.
Put it next to the code, in a doc comment or a Markdown file the AI tool will find. If you write web components, the Custom Elements Manifest analyzer will pull your JSDoc into a manifest that other tools can read.
Then ask the AI tool to work from that material. Review the implementation against the behavior you described, try the example, and update the explanation when the design changes. The next contributor, human or AI, starts from what you've already learned.