{"playbook":"Architecture","url":"https://agentwranglers.se/playbook/architecture","updated":"2026-10-21","license":"https://creativecommons.org/licenses/by/4.0/","credit":"Required by the licence: credit Agent Wranglers with a link to the rule or playbook you used.","how":"Each rule's How text is on its own page: add .md to its url.","rules":[{"chapter":"Code review","name":"Review the whole change against its base","level":"Recommended","rule":"Review a branch as one solution, compared with the branch it will merge into. Never review only the changed lines.","check":"The review names the base branch and the commit range it covered. Every changed file appears in the review.","url":"https://agentwranglers.se/playbook/architecture/code-review/review-against-the-base","related":["/playbook/ai-assisted-development/reviewing-agent-output/read-every-diff","/playbook/engineering/version-control-and-review/protect-the-main-branch"],"sources":[]},{"chapter":"Code review","name":"Check that it solves a named problem","level":"Recommended","rule":"Every change must solve a problem someone can name, or add clear value to the codebase. Code without a reason is cost without return.","check":"The review answers question 0 with the problem in one sentence and why the change solves it.","url":"https://agentwranglers.se/playbook/architecture/code-review/solves-a-named-problem","related":["/playbook/ai-assisted-development/reviewing-agent-output/scope-matches-task"],"sources":[]},{"chapter":"Code review","name":"Prefer the patterns the industry knows","level":"Recommended","rule":"Solve common problems with the patterns and libraries most developers already know. Invent a new pattern only when the known ones fail.","check":"The review answers question 1 by naming the standard pattern, or by saying why none fits.","url":"https://agentwranglers.se/playbook/architecture/code-review/industry-standard-patterns","related":["/playbook/architecture/code-review/consistent-with-the-codebase"],"sources":[]},{"chapter":"Code review","name":"Keep it simple to maintain","level":"Recommended","rule":"Choose the simplest design that solves today's problem. A new developer or agent must understand a change without asking its author.","check":"The review answers question 2. A second reviewer explains what the change does, from the code alone, in five sentences or fewer.","url":"https://agentwranglers.se/playbook/architecture/code-review/simple-to-maintain","related":["/playbook/architecture/solid-and-dry/no-speculative-abstraction"],"sources":[]},{"chapter":"Code review","name":"Name the principle and the fix in every finding","level":"Recommended","rule":"Every finding names the broken principle, the exact file and line, and a concrete fix. \"This feels messy\" is not a finding.","check":"Each finding in the review has all three parts. Findings without a file and line are removed.","url":"https://agentwranglers.se/playbook/architecture/code-review/name-principle-code-fix","related":["/playbook/adversarial-testing/rules-of-engagement/rank-findings"],"sources":[]},{"chapter":"Code review","name":"Stay consistent with the codebase","level":"Recommended","rule":"New code follows the patterns the codebase already uses for the same job. Two ways to do one thing doubles what every reader must learn.","check":"The review answers question 4 by naming the existing pattern and saying whether the change follows it.","url":"https://agentwranglers.se/playbook/architecture/code-review/consistent-with-the-codebase","related":["/playbook/ai-assisted-development/context-and-memory/show-good-examples","/playbook/architecture/code-review/industry-standard-patterns"],"sources":[]},{"chapter":"Code review","name":"Report problems beyond the diff","level":"Recommended","rule":"When the review finds other code that breaks the same rules, report it too, as a separate list.","check":"The review answers question 5 with a list, or with \"none found\".","url":"https://agentwranglers.se/playbook/architecture/code-review/look-beyond-the-diff","related":["/playbook/ai-assisted-development/measuring-and-improving/check-repeated-mistakes"],"sources":[]},{"chapter":"Code review","name":"Write code that agents can parse","level":"Recommended","rule":"Write code that people and AI agents can read in one pass, with clear names, small files, types and a predictable structure.","check":"The review answers question 6. An agent asked to explain a file names its purpose and its inputs and outputs correctly.","url":"https://agentwranglers.se/playbook/architecture/code-review/readable-for-agents","related":["/playbook/ai-assisted-development/setting-up-the-project/agent-sized-modules"],"sources":[]},{"chapter":"Code review","name":"Answer every question by number","level":"Recommended","rule":"Answer every review question with its number, even when the answer is \"no problems found\". A skipped number counts as not reviewed.","check":"A script or grep finds every number from 0 to 10 in the review, plus 3.1 to 3.7 and 3.1.1 to 3.1.6. A missing number fails the review.","url":"https://agentwranglers.se/playbook/architecture/code-review/answer-every-question-by-number","related":["/playbook/ai-assisted-development/reviewing-agent-output/read-every-diff"],"sources":[]},{"chapter":"Code review","name":"Verify every finding from scratch","level":"Recommended","rule":"Check each finding again with a reviewer who starts fresh, such as a second person or a new AI agent. Drop findings that don't hold up.","check":"Every finding in the final review is marked confirmed by a second reviewer. Rejected findings are removed, not softened.","url":"https://agentwranglers.se/playbook/architecture/code-review/verify-with-fresh-agents","related":["/playbook/ai-assisted-development/reviewing-agent-output/fresh-review"],"sources":[]},{"chapter":"Names and functions","name":"Name things for what they mean","level":"Recommended","rule":"Every name says why the thing exists, what it does and how it is used. A reader should never need a comment to learn what a name means.","check":"A linter bans vague names, such as `data`, `info`, `tmp`, `obj` and `foo`. Examples are ESLint `id-denylist` for JavaScript and Pylint `bad-names` for Python. A reviewer can guess each name's meaning without reading its code.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/intention-revealing-names","related":["/playbook/data/schema-design/schema-names","/playbook/architecture/comments/self-documenting-code","/playbook/architecture/structure/consistent-file-names"],"sources":[]},{"chapter":"Names and functions","name":"Never let a name mislead","level":"Recommended","rule":"A name must never suggest a type or behaviour the code does not have. Calling a set a list, or a lookup a getter that writes, misleads every reader.","check":"Type checkers do not read names, so this is a review question. Does any name suggest a wrong type or hide a side effect? A custom lint rule can flag functions named `get` that write.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/no-disinformation","related":["/playbook/architecture/names-and-functions/one-thing-per-function"],"sources":[]},{"chapter":"Names and functions","name":"Use names you can search for and say aloud","level":"Recommended","rule":"Use whole words that people can pronounce and search for. Replace magic numbers and strings with named constants.","check":"A linter rejects short names and bare numbers. Examples are ESLint `id-length` with a minimum of 2 and the allowed letters in `exceptions`. ESLint `no-magic-numbers` with `ignore` set to 0 and 1 and Ruff `PLR2004` catch bare numbers. Pylint `invalid-name` with a `variable-rgx` that needs two or more characters and PHPMD `ShortVariable` reject short names. Searching for a constant's name finds every place it is used.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/searchable-names","related":["/playbook/data/schema-design/schema-names"],"sources":[]},{"chapter":"Names and functions","name":"Keep functions short","level":"Recommended","rule":"Keep each function at 20 lines or fewer. A longer function nearly always does more than one thing.","check":"ESLint `max-lines-per-function` set to 20, PHPMD `ExcessiveMethodLength` with a minimum of 21, or Pylint `too-many-statements` with `max-statements=20`, which counts statements instead of lines. The build fails above the limit, outside the excluded templates folder.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/small-functions","related":["/playbook/ai-assisted-development/setting-up-the-project/agent-sized-modules"],"sources":[]},{"chapter":"Names and functions","name":"Keep nesting shallow","level":"Recommended","rule":"Nest blocks at most two levels deep in business logic. Deep nesting hides which branch runs and when.","check":"The build fails above the limit. Examples are ESLint `max-depth` set to 2, or 3 in files that dispatch with `switch`. PHP_CodeSniffer `Generic.Metrics.NestingLevel` with `absoluteNestingLevel` set to 2 and Pylint `too-many-nested-blocks` with `max-nested-blocks=2` do the same.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/shallow-nesting","related":["/playbook/architecture/names-and-functions/extract-and-name"],"sources":[]},{"chapter":"Names and functions","name":"Pass few arguments","level":"Recommended","rule":"Give a function at most three arguments. When it needs more, pass one named object that groups them.","check":"ESLint `max-params` set to 3, PHPMD `ExcessiveParameterList` with a minimum of 4, or Pylint `too-many-arguments` with `max-args=3`.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/few-arguments","related":["/playbook/architecture/data-shapes/typed-data-objects"],"sources":[]},{"chapter":"Names and functions","name":"Make each function do one thing","level":"Recommended","rule":"Each function does one thing at one level of detail. A function either changes something or answers a question, never both.","check":"A review question asks whether each function's name describes everything it does. A function named as a query, starting with `get`, `find`, `is` or `has`, returns a value and writes nothing. Tests for a query never need to check a side effect.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/one-thing-per-function","related":["/playbook/architecture/names-and-functions/no-disinformation"],"sources":[]},{"chapter":"Names and functions","name":"Split blocks into named functions","level":"Recommended","rule":"When a block of code needs a moment of thought, move it into a function whose name says what it does.","check":"The review answers question 10 with each block worth extracting. No `if` joins more than two clauses without a name. Pylint `too-many-boolean-expressions` with `max-bool-expr=2` enforces this in Python, and eslint-plugin-sonarjs `expression-complexity` gives a similar limit in JavaScript.","url":"https://agentwranglers.se/playbook/architecture/names-and-functions/extract-and-name","related":["/playbook/architecture/names-and-functions/shallow-nesting","/playbook/architecture/comments/refactor-clever-code"],"sources":[]},{"chapter":"Comments","name":"Let the code explain itself","level":"Recommended","rule":"Name things so the code explains itself, and write a comment only for a reason the code cannot show.","check":"A script fails when comment lines pass 10% of a file's code. For names and function length, add a linter, such as ESLint for JavaScript, Pylint or Ruff for Python, or PHPMD for PHP.","url":"https://agentwranglers.se/playbook/architecture/comments/self-documenting-code","related":["/playbook/architecture/names-and-functions/intention-revealing-names"],"sources":[]},{"chapter":"Comments","name":"Refactor clever code instead of explaining it","level":"Recommended","rule":"When a block needs a comment to be understood, rewrite it so it reads plainly. Never leave a puzzle with a note beside it.","check":"Reviewers ask one question for each comment that explains how code works. \"Would a rename or an extracted function make this comment unneeded?\" If yes, the review fails until the code is changed.","url":"https://agentwranglers.se/playbook/architecture/comments/refactor-clever-code","related":["/playbook/architecture/names-and-functions/extract-and-name"],"sources":[]},{"chapter":"Comments","name":"Delete commented-out code","level":"Recommended","rule":"Never commit code that is commented out. Delete it, because version control already keeps every old line.","check":"A linter rule or a script fails the build on commented-out code. Examples are `eslint-plugin-no-commented-code`, SonarQube's commented-out code rule and Ruff's `ERA001` for Python.","url":"https://agentwranglers.se/playbook/architecture/comments/no-commented-out-code","related":["/playbook/architecture/dead-code/delete-unused-code"],"sources":[]},{"chapter":"Comments","name":"Keep change history out of comments","level":"Recommended","rule":"Keep change logs, author names, dates and ticket histories out of the code. Version control records who changed what and why.","check":"A script or linter rule fails on comments such as `@author`, `Changed by`, `Modified on` or a date at the start of a comment.","url":"https://agentwranglers.se/playbook/architecture/comments/no-journal-comments","related":["/playbook/engineering/version-control-and-review/everything-in-version-control"],"sources":[]},{"chapter":"Comments","name":"Say why, not what","level":"Recommended","rule":"A comment states a reason the code cannot show, such as a bug in another system, a legal rule or a measured trade-off. It never repeats what the code does.","check":"Reviewers mark any comment that restates the next line. A comment-share script, such as the one in the first rule, keeps the total low.","url":"https://agentwranglers.se/playbook/architecture/comments/comments-say-why","related":["/playbook/engineering/documentation/decision-records"],"sources":[]},{"chapter":"Comments","name":"Keep docblocks only for what types cannot say","level":"Recommended","rule":"Write a docblock only when it adds a type, a contract or a unit the language cannot express. Delete docblocks that repeat the signature.","check":"A linter flags docblocks that only repeat the parameters. Examples are PHP-CS-Fixer's `no_superfluous_phpdoc_tags` and ESLint's `jsdoc/no-types` in TypeScript files.","url":"https://agentwranglers.se/playbook/architecture/comments/useful-docblocks","related":["/playbook/ai-assisted-development/rules-the-agent-cannot-break/strict-tooling"],"sources":[]},{"chapter":"Comments","name":"Link every TODO to a ticket","level":"Recommended","rule":"A `TODO` or `FIXME` comment names a ticket, or it is not merged. Loose notes in the code are never planned and never found.","check":"The build fails on a `TODO` or `FIXME` without a ticket ID. Examples are Ruff `TD003` for Python, or the grep below for any language.","url":"https://agentwranglers.se/playbook/architecture/comments/todos-with-tickets","related":[],"sources":[]},{"chapter":"Errors and nulls","name":"Throw exceptions instead of returning error codes","level":"Recommended","rule":"Signal failure with an exception or a typed result. Never return a magic value such as `-1`, `false` or an error string that callers must remember to check.","check":"A strict type checker requires a declared return type on every function. Examples are PHPStan at level max, TypeScript `strict` and mypy `--strict`. A grep fails return types that join a value with `false`, such as `int|false`. Reviewers fail any sentinel value, such as `-1` for not found.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/exceptions-not-error-codes","related":["/playbook/api-design/errors/problem-details"],"sources":[]},{"chapter":"Errors and nulls","name":"Use typed domain exceptions","level":"Recommended","rule":"Give each kind of failure its own exception type with a clear name and the data needed to handle it. Never throw a bare generic error with only a message.","check":"A linter rule bans throwing the base type. Examples are Ruff's `TRY002`, ESLint `no-restricted-syntax` on `throw new Error`, and a custom PHPStan rule against `throw new \\Exception`.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/domain-exceptions","related":["/playbook/api-design/errors/problem-details","/playbook/api-design/errors/stable-error-types"],"sources":[]},{"chapter":"Errors and nulls","name":"Never return null","level":"Recommended","rule":"Collections return empty, services use a Null Object, and lookups return one consistent \"absent\" value. A caller should never crash because it forgot a check.","check":"Strict null checks make every absent return visible, such as `strictNullChecks` in TypeScript, PHPStan level 8 or higher, or mypy `--strict`. Reviewers fail collections and services that can return null. A test calls each finder with no matches and expects an empty result.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/never-return-null","related":["/playbook/api-design/lists/empty-lists","/playbook/architecture/errors-and-nulls/never-pass-null"],"sources":[]},{"chapter":"Errors and nulls","name":"Never pass null into a function","level":"Recommended","rule":"Never call a function with `null` to mean \"use the default\" or \"skip this\". Give the function a clear variant or an options object instead.","check":"Strict types reject `null` for parameters that do not allow it. Examples are TypeScript `strict`, PHP native types without `?`, and mypy `--strict`. Reviewers fail calls that pass a literal `null`.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/never-pass-null","related":["/playbook/architecture/errors-and-nulls/never-return-null"],"sources":[]},{"chapter":"Errors and nulls","name":"Make fields required unless absence is valid","level":"Recommended","rule":"Every constructor argument and data object field is required and non-nullable, unless \"not there\" is a valid state of the business.","check":"A type checker rejects missing fields, such as TypeScript `strict`, PHPStan level max or mypy `--strict`. Reviewers fail any nullable field that no code path ever leaves empty.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/required-fields","related":["/playbook/data/schema-design/constraints"],"sources":[]},{"chapter":"Errors and nulls","name":"Never swallow errors","level":"Required","risk":"data","rule":"Never catch an error and carry on as if nothing happened. Handle it, add context and rethrow it, or let it reach the edge.","check":"A linter flags empty catch blocks and broad catches. Examples are ESLint's `no-empty`, Ruff's `BLE001` and `S110`, and PHP_CodeSniffer's `Generic.CodeAnalysis.EmptyStatement`.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/never-swallow-errors","related":["/playbook/adversarial-testing/access-and-accounts/fail-closed","/playbook/architecture/errors-and-nulls/await-every-promise"],"sources":[]},{"chapter":"Errors and nulls","name":"Release every resource you open","level":"Required","risk":"outage","rule":"Close every file, connection, lock and handle with the language's scoped construct, so it is released on success and on error.","check":"A linter fails on resources opened outside a scoped block. Examples are Pylint `consider-using-with`, Ruff `SIM115`, PMD `CloseResource` for Java and the .NET analyzer `CA2000`. A test that throws inside the block finds the resource closed.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/release-resources","related":["/playbook/data/operations/connection-pooling"],"sources":[]},{"chapter":"Errors and nulls","name":"Await every async call","level":"Required","risk":"data","rule":"Await every promise, task or future, or hand it to code that handles its errors. A call that nobody waits for loses its errors without a trace.","check":"A linter fails on unhandled async calls. Examples are typescript-eslint `no-floating-promises` and `no-misused-promises`, Ruff `RUF006` for asyncio tasks, and for C# the compiler warning `CS4014` plus the analyzer `VSTHRD100` against `async void`.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/await-every-promise","related":["/playbook/architecture/errors-and-nulls/never-swallow-errors"],"sources":[]},{"chapter":"Errors and nulls","name":"Cap every collection that grows with use","level":"Required","risk":"outage","rule":"Give every in-memory cache, queue, buffer and batch of parallel work a maximum size. Nothing in a long-running process may grow without a limit.","check":"A grep fails `lru_cache(maxsize=None)` and `@functools.cache` in server code. A test inserts 10 times the cap in distinct keys, and the size stays at or below the cap.","url":"https://agentwranglers.se/playbook/architecture/errors-and-nulls/bounded-collections","related":["/playbook/operations/running-the-service/resource-limits","/playbook/adversarial-testing/abuse-and-limits/resource-limits"],"sources":[{"label":"Python functools.lru_cache","url":"https://docs.python.org/3/library/functools.html#functools.lru_cache"}]},{"chapter":"Coupling","name":"Talk only to your direct neighbours","level":"Recommended","rule":"A function calls methods on its own object, its arguments and the objects it creates. It never reaches through one object to steer another, as in `a.getB().getC().doSomething()`.","check":"A lint rule or a grep fails a method call on an object returned by another object's getter, such as `order.getCustomer().getAddress()`. Reviewers fail any that slip past. Builders and pipelines on one value pass. Example tools are PMD's `LawOfDemeter` rule for Java, Exakat for PHP, or a custom ESLint rule on long member chains.","url":"https://agentwranglers.se/playbook/architecture/coupling/law-of-demeter","related":["/playbook/architecture/coupling/tell-dont-ask"],"sources":[]},{"chapter":"Coupling","name":"Tell objects what to do","level":"Recommended","rule":"Give an object a command instead of reading its state and deciding for it. The rule that uses the data lives next to the data.","check":"Reviewers fail code that reads an object's fields only to change the same object. Tests call the new method directly and cover every rule.","url":"https://agentwranglers.se/playbook/architecture/coupling/tell-dont-ask","related":["/playbook/architecture/coupling/law-of-demeter"],"sources":[]},{"chapter":"Coupling","name":"Make the order of calls impossible to get wrong","level":"Recommended","rule":"If methods must run in a set order, the code must enforce it. Never rely on callers remembering to call `init()` first.","check":"A test creates the object and calls each public method straight away. Each call either works or cannot be written, because the type does not have it.","url":"https://agentwranglers.se/playbook/architecture/coupling/no-hidden-temporal-coupling","related":["/playbook/payments/amounts-and-orders/payment-state-machine"],"sources":[]},{"chapter":"Coupling","name":"Pass state in instead of sharing globals","level":"Required","risk":"data","rule":"Never share changeable data through globals, singletons or module-level variables when it changes how code behaves. Pass state in, and return new state out.","check":"Tests run in random order and still pass, for example with Jest `--randomize`, pytest-randomly or PHPUnit `--order-by=random`. Pylint `global-statement` flags writes to globals. In JavaScript, a grep fails `let` or `var` at the start of a line, which marks a module-level variable.","url":"https://agentwranglers.se/playbook/architecture/coupling/no-global-state","related":["/playbook/architecture/data-shapes/immutable-data","/playbook/architecture/coupling/guard-shared-state","/playbook/testing/flaky-and-slow-tests/random-order"],"sources":[]},{"chapter":"Coupling","name":"Keep internals private","level":"Recommended","rule":"Each module exports only what its callers need. Everything else stays private, so it can change without breaking anyone.","check":"A tool fails on unused exports and on imports of another module's internals. Examples are knip or vulture for unused code, and dependency-cruiser for JavaScript, Deptrac for PHP or import-linter for Python for internal imports.","url":"https://agentwranglers.se/playbook/architecture/coupling/private-internals","related":["/playbook/testing/unit-tests/unit-behaviour"],"sources":[]},{"chapter":"Coupling","name":"Guard state that concurrent code shares","level":"Required","risk":"data","rule":"When two threads, tasks or requests can change the same value, make each read and write one step. Never read a value, wait, then write back a result based on it.","check":"A test starts the same change 100 times at once and finds the expected result, such as a count of 100. Go tests run with `go test -race`. ESLint `require-atomic-updates` fails assignments that span an `await`.","url":"https://agentwranglers.se/playbook/architecture/coupling/guard-shared-state","related":["/playbook/architecture/coupling/no-global-state","/playbook/data/transactions-and-concurrency/conditional-writes"],"sources":[{"label":"Python asyncio synchronization primitives","url":"https://docs.python.org/3/library/asyncio-sync.html"}]},{"chapter":"SOLID and DRY","name":"Keep each piece of knowledge in one place","level":"Recommended","rule":"Every rule, value and decision lives in one place in the code. Repeated text is fine. Repeated knowledge is not.","check":"A duplicate code detector such as jscpd, which covers most languages, or Pylint `duplicate-code` runs in the build. It fails on any block of 10 or more lines that appears twice, such as jscpd with `--min-lines 10`. In review, ask whether one business change would need edits in two places.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/dry","related":["/playbook/engineering/documentation/one-home-per-value","/playbook/website/site-architecture/shared-html-modules-only"],"sources":[]},{"chapter":"SOLID and DRY","name":"Give each module one reason to change","level":"Recommended","rule":"Each class, module or function serves one actor and has one reason to change.","check":"In review, describe the module in one sentence without \"and\". A linter limit on file size flags likely offenders, such as ESLint `max-lines` at 300 or PHPMD `ExcessiveClassLength`.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/single-responsibility","related":["/playbook/architecture/structure/group-by-feature"],"sources":[]},{"chapter":"SOLID and DRY","name":"Extend code without editing it","level":"Recommended","rule":"Add new behaviour by adding a new type or strategy, not by editing a growing switch or if chain.","check":"In review, adding a new case touches no existing branch. A linter limit on cyclomatic complexity, such as ESLint `complexity`, flags long switch chains.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/open-closed","related":["/playbook/architecture/solid-and-dry/no-speculative-abstraction"],"sources":[]},{"chapter":"SOLID and DRY","name":"Keep the contract in every subtype","level":"Recommended","rule":"A subtype must work everywhere its parent type works. It may not demand more, promise less or throw \"not supported\".","check":"Run the parent type's tests against every subtype. A grep for `NotImplemented` or \"not supported\" in overriding methods fails the build.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/liskov-substitution","related":["/playbook/architecture/solid-and-dry/composition-over-inheritance"],"sources":[]},{"chapter":"SOLID and DRY","name":"Prefer composition over inheritance","level":"Recommended","rule":"Reuse behaviour by holding an object that does the job, not by extending a class. Inherit only when the child truly is a kind of the parent.","check":"A linter fails on deep inheritance. Examples are Pylint `too-many-ancestors` and PHPMD `DepthOfInheritance`, set to fail above two levels. Pylint `ignored-parents` skips the framework's own classes. PHP-CS-Fixer `final_class` makes classes final by default, as Kotlin already does.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/composition-over-inheritance","related":["/playbook/architecture/solid-and-dry/liskov-substitution"],"sources":[]},{"chapter":"SOLID and DRY","name":"Keep interfaces small","level":"Recommended","rule":"Split large interfaces into small role interfaces, so no class has to implement methods it does not use.","check":"In review, no implementation has empty or throwing methods. A linter such as PHPMD `TooManyMethods` flags fat interfaces.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/interface-segregation","related":["/playbook/architecture/solid-and-dry/dependency-inversion"],"sources":[]},{"chapter":"SOLID and DRY","name":"Depend on abstractions at the edges","level":"Recommended","rule":"Business rules depend on small interfaces they own. Databases, payment providers and email services implement those interfaces.","check":"Business code lives in its own folder, such as `domain/`. An import rule fails when a file in that folder imports a vendor SDK. Examples are ESLint `no-restricted-imports`, dependency-cruiser, Deptrac and import-linter.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/dependency-inversion","related":["/playbook/architecture/solid-and-dry/interface-segregation","/playbook/ai-features/deciding-to-use-a-model/model-gateway","/playbook/architecture/structure/layers-point-inward","/playbook/testing/integration-and-contract-tests/doubles-at-the-edge"],"sources":[]},{"chapter":"SOLID and DRY","name":"Build only what is needed now","level":"Recommended","rule":"Do not add interfaces, factories, options or layers for needs that do not exist yet.","check":"In review, every interface has two implementations or sits at a boundary. Every option has a caller that sets it.","url":"https://agentwranglers.se/playbook/architecture/solid-and-dry/no-speculative-abstraction","related":["/playbook/ai-assisted-development/working-in-small-steps/smallest-change","/playbook/architecture/code-review/simple-to-maintain","/playbook/architecture/solid-and-dry/open-closed"],"sources":[]},{"chapter":"Data shapes","name":"Use typed objects instead of loose arrays","level":"Recommended","rule":"Pass data between functions as a named type, such as a DTO or value object. Never use `object`, a plain dictionary or an untyped array that says nothing.","check":"The type checker rejects untyped shapes. Examples are PHPStan at level 6 or higher, which requires array value types, TypeScript with `strict`, or mypy with `--strict`.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/typed-data-objects","related":["/playbook/architecture/names-and-functions/few-arguments"],"sources":[]},{"chapter":"Data shapes","name":"Wrap domain values in their own types","level":"Recommended","rule":"Give each domain value with its own rules a small type, such as `EmailAddress`, `OrderId` or `Money`. Never pass it around as a bare string or number.","check":"A type test passes a swapped ID to a typed function and marks the line as an expected error. Examples are `// @ts-expect-error` in TypeScript and `# type: ignore[arg-type]` with mypy `--warn-unused-ignores`. The checker fails if the error ever disappears. A unit test gives each factory an invalid value, such as `\"not-an-email\"`, and expects an error.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/value-objects","related":["/playbook/api-design/requests-and-responses/money","/playbook/payments/amounts-and-orders/money-in-minor-units"],"sources":[]},{"chapter":"Data shapes","name":"Never type anything as mixed or any","level":"Recommended","rule":"No `mixed`, `any` or untyped value in signatures, properties or docblocks. Each one turns off the type checker for everything it touches.","check":"The build fails on it. Examples are TypeScript `noImplicitAny` with `@typescript-eslint/no-explicit-any`, PHPStan level 10, which checks explicit and implicit `mixed`, and mypy `--disallow-any-explicit`.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/no-mixed-or-any","related":["/playbook/ai-assisted-development/rules-the-agent-cannot-break/strict-tooling","/playbook/stack/testing-and-code-quality/typescript-strict"],"sources":[]},{"chapter":"Data shapes","name":"Convert to arrays and JSON only at the edge","level":"Recommended","rule":"Keep typed objects inside the app. Turn them into arrays, JSON or strings only where data leaves, such as an HTTP response or a database write.","check":"A grep or linter rule fails on `toArray()`, `JSON.parse(JSON.stringify(` and `json_decode(json_encode(` outside the serializer and controller folders. In review, no internal function takes or returns an array made from a DTO.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/convert-at-the-edge","related":["/playbook/api-design/requests-and-responses/json-conventions"],"sources":[]},{"chapter":"Data shapes","name":"Parse input once at the boundary","level":"Required","risk":"takeover","rule":"Turn outside input into a typed, valid object as soon as it arrives. Code after that point trusts the type and does not check again.","check":"Each request handler calls a parser before any other logic. A test sends a request body with a missing and a wrong-typed field. It expects a 422 answer that names both fields, as in [validation errors](https://agentwranglers.se/playbook/api-design/errors/validation-errors).","url":"https://agentwranglers.se/playbook/architecture/data-shapes/parse-at-the-boundary","related":["/playbook/api-design/errors/validation-errors","/playbook/api-design/requests-and-responses/strict-input","/playbook/api-design/changes-over-time/tolerant-clients","/playbook/data/product-analytics/event-validation","/playbook/adversarial-testing/injection/unsafe-deserialisation","/playbook/adversarial-testing/requests-and-files/third-party-api-data","/playbook/notifications/text-messages/e164-phone-numbers"],"sources":[{"label":"OWASP Input Validation Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html"}]},{"chapter":"Data shapes","name":"Make data objects immutable","level":"Recommended","rule":"Make DTOs and value objects read-only. Create a new object for a change instead of editing one that others may hold.","check":"The type checker rejects writes to fields. Examples are TypeScript `readonly`, PHP `readonly`, and Python `@dataclass(frozen=True)`.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/immutable-data","related":["/playbook/architecture/coupling/no-global-state"],"sources":[]},{"chapter":"Data shapes","name":"Model each state as its own type","level":"Recommended","rule":"When an object moves through states, give each state its own type with only the fields it has. Never pair a status field with optional fields that some states leave empty.","check":"The type checker rejects reading `paidAt` on a pending order. An exhaustiveness check fails when a state is not handled. Examples are typescript-eslint `switch-exhaustiveness-check`, `assert_never` with mypy, and `when` on a Kotlin sealed class.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/states-as-types","related":["/playbook/payments/amounts-and-orders/payment-state-machine","/playbook/ai-features/agents-in-products/agent-run-status","/playbook/app-ui/data-views/every-data-state"],"sources":[]},{"chapter":"Data shapes","name":"Use enums instead of magic strings","level":"Recommended","rule":"Give every fixed set of values an enum or union type. Never compare against loose strings such as `\"paid\"`.","check":"The tools flag unhandled cases. Examples are typescript-eslint `switch-exhaustiveness-check` and PHPStan's check that a `match` covers every enum case. Reviewers fail comparisons of status fields with string literals.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/enums-not-strings","related":["/playbook/api-design/requests-and-responses/api-enums"],"sources":[]},{"chapter":"Data shapes","name":"Give each function one return type","level":"Recommended","rule":"A function or endpoint always returns the same shape. It never returns an object in one case, an array in another and `false` in a third.","check":"The type checker rejects mixed returns. In plain JavaScript, add `// @ts-check` with JSDoc return types, or validate output with a schema such as a zod output schema. A contract test validates every endpoint response against its schema, for example with an OpenAPI validator.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/one-return-type","related":["/playbook/api-design/requests-and-responses/response-shape"],"sources":[{"label":"OpenAPI Specification 3.2.1","url":"https://spec.openapis.org/oas/v3.2.1.html"}]},{"chapter":"Data shapes","name":"Insert text literally when you replace","level":"Required","risk":"data","rule":"When you insert outside text with a replace function, make sure the text goes in exactly as written. Many replace functions treat parts of the replacement as commands.","check":"Insert the text `$& $1 \\1 $'` through every template or replace step. The output contains those characters unchanged.","url":"https://agentwranglers.se/playbook/architecture/data-shapes/literal-replacement","related":["/playbook/website/site-architecture/render-at-build-time-serve-from-memory"],"sources":[{"label":"ECMA-262: GetSubstitution","url":"https://tc39.es/ecma262/#sec-getsubstitution"}]},{"chapter":"Dead code","name":"Delete code that nothing uses","level":"Recommended","rule":"Remove unused functions, classes, exports, parameters, routes and files as soon as they stop being used.","check":"A tool fails the build on new unused code. Examples are knip, vulture, and PHPStan with a dead code extension such as shipmonk/dead-code-detector.","url":"https://agentwranglers.se/playbook/architecture/dead-code/delete-unused-code","related":["/playbook/architecture/comments/no-commented-out-code"],"sources":[]},{"chapter":"Dead code","name":"Remove flags that never change","level":"Recommended","rule":"Delete a feature flag once it is fully on or fully off, along with the branch that no longer runs.","check":"A script lists flags past their end date and fails the build. In review, every flag has an owner and a date.","url":"https://agentwranglers.se/playbook/architecture/dead-code/remove-stale-flags","related":["/playbook/engineering/running-in-production/deploy-is-not-release","/playbook/app-ui/permissions-flags-and-failures/feature-flags-in-ui"],"sources":[]},{"chapter":"Dead code","name":"Remove unused dependencies","level":"Recommended","rule":"Uninstall packages that no code imports. Each one adds install time, attack surface and update work.","check":"A tool such as knip, composer-unused or deptry fails the build on an unused package.","url":"https://agentwranglers.se/playbook/architecture/dead-code/remove-unused-dependencies","related":["/playbook/adversarial-testing/supply-chain-and-setup/audit-dependencies"],"sources":[]},{"chapter":"Dead code","name":"Delete tests with their feature","level":"Recommended","rule":"When you remove a feature, remove its tests, fixtures and settings in the same change.","check":"The dead code finder covers test folders too. In review, a removal change also deletes the matching tests.","url":"https://agentwranglers.se/playbook/architecture/dead-code/delete-tests-with-feature","related":["/playbook/testing/test-strategy/tests-with-the-change"],"sources":[]},{"chapter":"Structure","name":"Point dependencies inward","level":"Recommended","rule":"Business rules never import the web framework, the database or other outside services. Outer layers depend on inner ones, never the reverse.","check":"A dependency rule fails the build when an inner folder imports an outer one. Examples are dependency-cruiser, Deptrac, import-linter and ArchUnit.","url":"https://agentwranglers.se/playbook/architecture/structure/layers-point-inward","related":["/playbook/architecture/solid-and-dry/dependency-inversion","/playbook/architecture/structure/no-dependency-cycles"],"sources":[]},{"chapter":"Structure","name":"Allow no dependency cycles","level":"Recommended","rule":"No module may depend on itself through a chain of imports. Cycles make code impossible to test, load or change on its own.","check":"A tool fails the build on any cycle. Examples are ESLint `import/no-cycle`, dependency-cruiser `no-circular` and the import-linter `acyclic_siblings` contract.","url":"https://agentwranglers.se/playbook/architecture/structure/no-dependency-cycles","related":["/playbook/architecture/structure/layers-point-inward"],"sources":[]},{"chapter":"Structure","name":"Group code by feature","level":"Recommended","rule":"Put the code for one feature together, such as `orders/` or `invoices/`, instead of spreading it across folders named `controllers/`, `models/` and `services/`.","check":"In review, a change to one feature touches at most one feature folder plus shared code. An import rule blocks imports of another feature's internal files.","url":"https://agentwranglers.se/playbook/architecture/structure/group-by-feature","related":["/playbook/architecture/solid-and-dry/single-responsibility"],"sources":[]},{"chapter":"Structure","name":"Name files after what they hold","level":"Recommended","rule":"Use one naming style for files and folders, and name each file after its main export.","check":"A linter enforces the style. Examples are eslint-plugin-unicorn `filename-case`, ls-lint, and PSR-4 autoloading checks with `composer dump-autoload --optimize --strict-psr`.","url":"https://agentwranglers.se/playbook/architecture/structure/consistent-file-names","related":["/playbook/architecture/names-and-functions/intention-revealing-names"],"sources":[]},{"chapter":"Structure","name":"Format all code with one tool","level":"Recommended","rule":"Format every file with one automatic formatter, with its settings in the repository. Nobody formats by hand, and reviews never discuss layout.","check":"The build runs the formatter in check mode and fails when it would change a file. Note that `gofmt -l` only lists files, so test that its output is empty.","url":"https://agentwranglers.se/playbook/architecture/structure/one-formatter","related":["/playbook/engineering/version-control-and-review/continuous-integration","/playbook/stack/testing-and-code-quality/eslint-and-prettier","/playbook/stack/testing-and-code-quality/ruff"],"sources":[]},{"chapter":"Structure","name":"Write new code in a memory-safe language","level":"Required","risk":"takeover","rule":"Write new code in a memory-safe language, such as Rust, Go, Java, C#, Swift or Python. Never write a new parser of outside input in C or C++.","check":"A build step fails when a change adds a `.c`, `.cc`, `.cpp` or `.h` file outside an allow list. Every Rust crate sets `#![forbid(unsafe_code)]` unless it is on the list. A grep fails `import \"unsafe\"` in Go outside the list.","url":"https://agentwranglers.se/playbook/architecture/structure/memory-safe-languages","related":["/playbook/testing/non-functional-tests/fuzz-parsers"],"sources":[{"label":"CISA and NSA: Memory Safe Languages","url":"https://www.cisa.gov/resources-tools/resources/memory-safe-languages-reducing-vulnerabilities-modern-software-development"},{"label":"CISA Product Security Bad Practices","url":"https://www.cisa.gov/resources-tools/resources/product-security-bad-practices"},{"label":"CWE Top 25 of 2025","url":"https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html"}]}]}