These rules make my defaults explicit for both people and AI coding agents. They are not style preferences. Each one protects the product from a recurring form of ambiguity, coupling, or operational risk.
An exception is valid when the project has a better reason. Consequential exceptions belong in an ADR so the next person understands the trade-off.
Do not implement a new product or meaningful capability until its user outcome, scope, acceptance criteria, and technical impact are written.
Why: generated code makes an unexamined idea look real very quickly. A small document exposes disagreement while changing direction is still cheap. Match depth to risk, but never confuse urgency with clarity.
Define requests, responses, errors, permissions, state transitions, and examples first. Share or generate types from the contract where useful, and keep compatibility or migration explicit.
Why: the contract is where durable behavior becomes visible. Without it, every frontend state makes a private guess about the backend.
Give each module one coherent capability, a small public interface, and controlled dependencies. Keep business rules independent of HTTP, frameworks, databases, and providers where that separation improves testing or changeability.
Why: requirements move. Modularity limits how much code must move with them.
Split a file when it owns several reasons to change, not when it crosses an arbitrary line count. A route should not also implement authorization, SQL, email rendering, and analytics.
Why: large mixed files are easy for AI to extend and hard for humans to reason about. Generation speed is not maintainability.
Leave touched code clearer. Remove duplication when the shared concept is understood, strengthen boundaries when changes expose coupling, and separate risky structural work from behavior changes when review benefits.
Why: the first design had the least evidence. Refactoring is how the architecture incorporates what the product taught us.
Prefer reversible decisions, additive API and data migrations, explicit provider adapters, and small vertical slices. Avoid speculative generalization.
Why: change is not an edge case. The cost of a design is largely the cost of changing it.
Choose readable behavior, meaningful names, safe defaults, focused tests, actionable errors, and repeatable operations. A shortcut that hides failure or transfers unexplained work to the next change is debt.
Why: AI makes it easier to create more code than the team can understand. The repository must remain reviewable.
Treat HTTP input, configuration, webhooks, files, model output, retrieved content, MCP tool output, and third-party responses as untrusted. Validate once at the boundary and keep internal invariants strong.
Why: types disappear at runtime and external systems fail in ways your happy-path example did not predict.
Use structured logs, request or job identifiers, meaningful metrics, error tracking, timeouts, bounded retries, and idempotency. Define what the user sees and what the operator can do. Do not log secrets or unnecessary personal data.
Why: a silent failure is not simpler. It only moves the complexity into support and data repair.
Grant least privilege, keep secrets outside source control, encrypt transport, minimize collection, define retention, test authorization, and audit sensitive actions.
Why: privacy and security cannot be added after the data model and access patterns have already spread through the product.
Unit-test domain rules, integration-test persistence and providers, contract-test APIs, and end-to-end test a small number of critical user journeys. For AI systems, run regression evaluations on representative tasks and unanswerable cases.
Why: tests should make change safer. Tests coupled to private implementation make change slower without protecting users.
Pull requests should explain why a change exists, its trade-offs, and how it was verified. Use ADRs for decisions that cross modules, add operational systems, affect security or data, or are expensive to reverse.
Why: "best practice" and "AI suggested it" are not product constraints.
A change is done when:
- it satisfies documented acceptance criteria;
- relevant tests, type checks, linting, and builds pass;
- failure, permission, security, and recovery behavior have been considered;
- contracts and documentation match what shipped;
- logs and metrics make important production behavior understandable;
- migrations and rollback or forward recovery are safe;
- unrelated scope has not been hidden inside the work.
Working code is necessary. It is not the entire definition.