September 22, 2026

One of Taproot's automated checks verifies that the design tokens referenced in our code actually exist. A token is a named value from our design system, such as a color or spacing choice. A plausible-looking name is not enough: the check compares it with the design system's published inventory.

Phone_Security_Guard.png

That is a small example of how we work with AI coding assistants. We document the standards we expect, then turn the rules we can check reliably into executable checks. When a change violates one, the check reports a failure that the assistant and I can investigate.

The same approach extends to API calls, dependencies between layers, generated clients, and tests. It gives us a repeatable way to catch specific mistakes while leaving review time for decisions that require judgment.

We call these checks guards. They need scrutiny too: a review of our architecture guards found ways that violations could slip through. We had to fix the code that was supposed to catch mistakes in other code.

Standards are important

Taproot is a publishing platform with a frontend, a backend API, a site generator, and an image processor. A change can cross several of those parts. As I described in the article about moving to a monorepo, having the related code together makes those changes easier to investigate and integrate.

It also makes consistent rules important. An implementation may work on its own while conflicting with how the rest of the product is designed.

Our repository has a canonical AGENTS.md guide. It explains the architecture, the development workflow, and rules that address known failure modes. It points to examples in the code so an assistant can see what following a rule looks like. The instructions for different coding tools point back to that shared guide.

For example, our frontend is supposed to call Taproot's API through a generated client. The client comes from the API contract and supplies typed operations the frontend can use. If an operation is missing, the intended fix is to update the contract and regenerate the client.

The instruction identifies the decision, the approved path, and what to do when that path doesn't yet support the feature. It gives the assistant something concrete to act on.

Written guidance helps the assistant choose an approach. A check gives us a separate way to inspect whether the resulting code follows a particular rule.

Guards create useful failures

For that API-client rule, we have a source guard that flags direct fetch() calls in frontend TypeScript unless they carry an explicit exception marker. Its failure output identifies the file and line, explains the restriction, and directs the developer toward the generated SDK.

The reason is practical. Handwritten API calls can introduce another place where endpoint paths and request details need to be maintained. Keeping normal API access on the generated client helps the frontend follow the contract.

This check catches a specific call pattern; it isn't a comprehensive network policy. A passing result means we didn't find that pattern in the code we checked.

An exception marker makes an intentional departure visible. It does not explain whether the departure is justified. That still belongs in the change and its review.

A useful failure should leave the next person, or the assistant, knowing where to look and which decision to reconsider. Otherwise, the guard creates another investigation before the original work can continue.

Some standards protect the architecture

Our frontend has an intended dependency direction: pages can use components, and components can use common code. Common code should not depend on a particular page or component, and reusable components should not depend on page-specific code.

ESLint checks restricted import paths for those directories. If a component tries to import page-specific code through one of those paths, lint fails. The message describes the intended dependency direction.

That catches a shortcut that may look convenient during implementation but makes the component harder to reuse later. The fix might be to move genuinely shared behavior into an appropriate common module, or to pass the necessary information into the component.

Choosing between those fixes still takes judgment. ESLint's restricted-import rule also has a defined scope: it checks static imports, so it should not be mistaken for a complete model of every runtime relationship.

On the backend, architecture tests enforce selected boundaries around API entry points, command handlers, and data access. Taproot mutations are meant to go through a command pipeline with authorization, validation, and commit behavior. Checks for prohibited dependencies and direct mutation patterns help keep entry points from taking shortcuts around that structure.

Check against a source of truth

The design-token check is useful because it has something authoritative to compare against.

Espalier, our design system, publishes a token manifest. The check reads that manifest and scans relevant frontend and generator sources for token references and overrides. When it finds a name absent from the manifest, it reports the name and file.

That turns a potentially subtle styling mistake into an explicit failure. The assistant can inspect the available tokens and choose the intended one. If the design system needs a new capability, that is work to define there; inventing a local token name doesn't supply it.

We use a related idea for generated code. Our verification tooling can regenerate supported artifacts into temporary directories and compare them with the generated files in the working tree. A difference gives us a reason to check whether generation was missed or generated output was edited directly.

StandardWhat checks itWhat still needs judgment
Use defined design tokensToken references compared with the manifestWhether the selected token produces the intended appearance
Keep frontend dependencies in the intended directionRestricted-import lint rulesWhere shared behavior belongs
Use the generated API client for normal frontend callsA source check for direct fetch() calls and explicit exceptionsWhether an exception or contract change is appropriate
Keep generated artifacts consistentRegeneration and comparisonWhether the source contract is correct

The guards needed tests too

A review of Taproot's architecture guards found cases where a real violation could pass unnoticed.

One check relied on an exact namespace match. A service placed in a nested namespace could fall outside the set being inspected. The architectural rule still applied, but the check's discovery logic was too narrow.

Another problem involved an exception list. Its key omitted part of a nested file's path. Two files with the same name could collide, allowing one to inherit an exception intended for the other.

We tightened the discovery and path handling and added regression cases for those situations. The current path test explicitly distinguishes a permitted file from a same-named file in a nested directory.

That work illustrates why a guard needs more than a successful run against today's code. It should reject a representative violation and accept a legitimate case. Depending on how it reads source, it may also need to distinguish executable code from comments and examples.

A guard that misses violations creates false confidence. One that rejects legitimate work creates pressure to bypass it. Both need attention.

AI runs the checks, then iterates

The checks are useful to an AI assistant when their results are part of the development loop.

Our root verification command, ./scripts/verify-changed.sh, selects checks for the affected areas. Those include the relevant formatting, linting, typechecking, tests, and artifact checks. Shared changes can require checks across services. Some specialized checks, such as database-backed integration tests, need to be selected separately when the work requires them.

When a check fails, the command output becomes useful evidence. The assistant can inspect the failure, make a correction, and run the appropriate check again. Once a change is integrated, we run the scoped verification before independent review. Confirmed fixes require verification of the revised work.

CI provides another place to run the repository's gates. We aim to resolve failures locally before pushing, where the work can be corrected without waiting for another remote pipeline.

Keeping the checks runnable from the repository makes them available to humans, different assistants, and CI.

The mere presence of a script is not enforcement. It has to run, its failure has to matter to the workflow, and someone has to review changes that weaken or bypass it.

A green result still needs review

The assistant can edit tests and configuration as well as application code. A failing check can be made green by repairing the implementation, but it can also be made green by removing the check or broadening an exception.

That is why the whole diff matters. If the implementation and the rule change together, the reviewer needs to understand why. Sometimes a standard has become inappropriate and should change. That should be a deliberate decision, with the consequences visible.

Our backlog workflow includes an independent review in a fresh context. The reviewer examines the change against the task and repository rules. That provides another opportunity to question the implementation and the checks being used to accept it.

There are questions those checks will never fully settle: whether we solved the right problem, whether an abstraction earns its maintenance cost, whether an interface is understandable, or whether a permission rule reflects the intended product behavior.

An automated guard can make a particular mistake easier to catch. I still need to understand what we're building and the limits of the evidence used to accept it.

The messy beginning

For someone introducing AI into a development process, I would start with one costly, repeatable mistake that can be described precisely.

Write down the rule and its reason. Identify an example of acceptable code. Use an existing compiler, linter, or test capability where it fits. Then demonstrate that the check catches a real violation without rejecting the valid example, and put it in the normal verification path.

That is a manageable first step. A large collection of vague or brittle checks would create its own maintenance problem. We apply the same reasoning to tests: checks that only confirm framework mechanics or mirror implementation details can add noise without protecting useful behavior.

For a founder or business owner, this work supports something practical: software that can keep changing without requiring someone to rediscover every past decision. Clear standards and focused checks make those decisions easier to carry into the next feature.

This is part of how I'm applying my engineering experience to AI-assisted development. The assistant helps with implementation and investigation. The standards give that work direction, and the checks give us specific evidence to evaluate along the way.