


Keep a Large Modular Codebase Easy to Read and Change Without Breaking It
Use when an AI works in a big codebase made of many components. It gives rules for code style, file and change size, code review, tests, API naming, and builds, so changes stay small and readable and don't break other parts.
To use this AI rule
## Write clear function calls instead of bare true/false arguments
Favor clarity at the call site over cleverness.
- Inline variables directly into string-formatting placeholders instead of passing them positionally.
- Prefer method references over trivial closures when the language allows it.
- Avoid opaque positional arguments — bare booleans, null/none, or numeric literals — that force readers to guess meaning (e.g. `foo(false)`). Prefer enums, named methods, newtypes, or other self-documenting API shapes.
- When a self-documenting shape is impossible and a positional literal must remain, annotate it with an inline `/*param_name*/` comment that exactly matches the callee's parameter name. Leave string/char literals unannotated unless the comment truly adds clarity.
- Make branching exhaustive (no catch-all wildcard arms) so newly added cases surface at compile time.
- Give newly added abstractions (traits/interfaces) doc comments explaining their role and how implementers should use them.
- Follow file-local conventions. Do not churn code between equivalent forms without a clear readability or functional gain, and do not add type annotations solely to satisfy a shorthand.
- Do not create small helper functions that are referenced only once.
## Keep files and changes small instead of growing the central component
Keep units small so unrelated changes stop colliding.
### Module size
- Prefer adding a new module over growing an existing one. Target modules under roughly 500 lines of non-test code; once a file approaches ~800 lines, put new functionality in a new module unless there is a strong, documented reason not to.
- Apply this most strictly to high-traffic, central orchestration files that already attract unrelated edits; keep such files focused on orchestration rather than accumulating standalone helpers.
- When extracting code from a large module, move its tests and type/module docs alongside the new implementation so invariants stay close to the code that owns them.
### Resist bloating the largest component
- The largest or most central component is the path of least resistance for new code, which is exactly why it keeps growing. Resist adding to it. First ask whether an existing component is a better home, or whether it is time to introduce a new one — refactoring as needed. In review, push back on changes that would needlessly enlarge the central component.
### Change size
- Unless a change is mechanical, keep total changed lines under ~800; for complex logic, under ~500.
- If a change is larger, split it into reviewable stages and land the smallest coherent stage first, basing the split on the actual diff, dependencies, and affected call sites.
## Catch breaking changes and unlimited model context in code review
Review for the risks that are expensive to catch later.
### Keep the public surface small
Keep each component's public/exported API as small as possible. Prefer private internals with an explicitly exported public surface, and avoid proliferating test-only helpers in production code.
### Guard model/inference context
When a system maintains a context that is sent to a model on each inference request, protect it:
- Build the context up incrementally; never rewrite history.
- Avoid frequent context changes that invalidate caching.
- Everything injected must have a bounded size and a hard cap — no unbounded items, and no single item beyond a fixed token ceiling.
- Flag any new item that can individually cross a large size threshold for extra manual review.
- Route all injected fragments through a single typed abstraction rather than ad hoc insertion.
### Detect breaking changes
Actively search for breaking changes in external integration surfaces: public APIs, event payloads (even experimental ones), command-line parameters, configuration loading, and the ability to resume existing sessions or persisted state.
## Test real behavior with integration and snapshot tests
Test behavior, keep tests honest, and keep them out of production code.
- For core-behavior changes, prefer integration tests that exercise a real instance over narrow unit tests. Any change to that behavior MUST add an integration test; enumerate the behaviors that need coverage.
- Put unit tests in a dedicated sibling test file rather than inline in the implementation file, and give the file a descriptive name. Do not add test-only functions to production modules, and do not rewrite existing inline test modules solely to follow this convention.
- Any change to user-visible rendered output must include snapshot coverage — add or update the snapshot and review the diff as part of the change so UI impact stays visible.
- Assert on whole objects with deep equality rather than comparing fields one at a time, using an assertion library that produces clear diffs.
- Do not mutate global process state (for example, environment variables) inside tests; pass dependencies and flags in from above.
- Do not test statically defined constants, and do not add negative tests for logic that was removed.
- Check for existing test helpers before writing new setup so tests stay streamlined and readable.
## Keep API names and field formats the same everywhere
When evolving a networked or cross-language API, consistency at the wire boundary prevents silent breakage.
- Concentrate active development in the current API version; do not add new surface area to deprecated versions.
- Name payloads consistently by role (a suffix for request params, one for responses, one for notifications). Expose methods as a resource name followed by an action name with a consistent singular/plural convention for the resource.
- Fix one wire casing convention for fields and enum values and apply it everywhere, overriding only for explicit compatibility needs. Keep serializer and type-generation annotations aligned so both sides agree; when one uses an explicit rename, the other must match.
- For discriminated unions, use explicit tagging in every serializer.
- Prefer plain string IDs at the boundary and convert internally. Use integer timestamps with consistent `*_at` naming.
- For list endpoints, implement cursor pagination by default (a cursor and limit in; a data array and next-cursor out).
- Gate experimental surface behind an explicit marker, and regenerate docs, examples, and schema fixtures whenever API shapes change.
## Run the formatter and focused tests first, and leave sandbox guards alone
Respect the toolchain and the constraints that already exist.
### Environment and sandbox guards
- Never add or modify code that manages sandbox or environment-capability flags without deliberate intent. These guards exist because code and tests were authored around specific runtime limits — for example, tests that early-exit when a capability such as network access is unavailable. Silently removing or altering a guard breaks those assumptions.
### Build and toolchain hygiene
- Run the formatter automatically after finishing code changes; do not ask for approval to format.
- Run the narrowest relevant tests first, scoped to the changed component. Only expand to the full suite when shared or central code changed, and confirm before kicking off long full-suite runs.
- When you change inputs that feed generated artifacts — schemas, lockfiles, generated types — regenerate them in the same change; drift is verified in CI.
- If the build tool needs source files declared for compile-time access, update that declaration whenever you add such reads.
- Be patient with slow build and test commands; expected lock contention can make them slow. Do not kill them by process id.
### Platform support
- Tests and features must support every officially supported operating system unless a feature is explicitly OS-specific. Where the system lets components run on different operating systems, ensure new tests exercise those cross-OS configurations.
