This post has been updated. The new content will be shown in italics.
Learn how to perform a code audit to efficiently evaluate your codebase’s quality and identify areas for improvement, from architecture all the way down to AI-generated code.
An excellent code report efficiently evaluates the codebase's quality in terms of code cleanness, and identifies areas to be improved. It should also take special considerations about the reader since a report that's difficult to understand or too long, becomes a pain to read and is not useful.
This is why we approach our code reports with a top-down perspective, starting with higher-level descriptions of the system and eventually leading down to lower-level discussions at code level. We begin with a functional description of the system and provide a high-level explanation of its architecture to give context to the reader. Then comes a description of the design, stating why things are organized the way they are, with a brief discussion on tradeoffs that lead to it. Finally, critiques to code quality, considering best practices and principles follow.
This "top-down" approach is also known as the "newspaper metaphor" in Robert C. Martin's book: Clean Code. This book lays the foundation of our best practices guidelines and principles covering all the way, from design patterns, to how to name variables.
One thing has changed dramatically since we first wrote this guide: a large share of the code we audit today is written, or assisted, by AI tools like GitHub Copilot, Cursor, and Claude. As we’ll see, this makes a disciplined audit more important, not less. The principles below are exactly the lens we use to tell solid AI-assisted code from code that merely compiles and passes its tests.
Now without further ado, we'll describe what we look for regarding the different aspects of a codebase.
Architecture
Even though the primary purpose of assessing the system's architecture is to give context to the reader of the code report, it can also be a great time to re-evaluate it given current business needs.
- Does the architecture support the most needed software quality attributes at the time? For example, in high growth times, it would be a good idea to have good scalability and extensibility.
- Is the architecture simple enough, or is it over-engineered? This can also be seen as the average time needed by a new developer to understand the current architecture.
- Does the architecture account for observability and operability? In modern distributed and cloud-native systems, the ability to monitor, trace, and debug in production is itself an architectural quality attribute, not an afterthought.

Design
Like it happens with architecture, we mainly tackle design as an introduction to lower level aspects of the codebase. However, this also proves to be a perfect moment to evaluate the system's design on current business needs.
- Is there room for change in areas that may be subject to change in the future?
- How change within a particular portion of the codebase affects it?
- How hard is it to extend current functionality? Can it be done without modifying the existing code?
- Is it simple enough, or is it over-engineered? Can a new developer understand it relatively quick enough?
To evaluate this as well as the remaining aspects of the code, we rely on the Clean Code's SOLID principles:
- Single Responsibility Principle: every unit of code should have one and only one responsibility (it can also be thought of as having just one reason to change). This leads to more testable, readable, and maintainable code.
- Open Closed Principle: software entities should be open for extension but closed for modification. This allows for a piece of code to be extended (to have added functionality), without affecting existing code, which naturally leads to better extensibility of the codebase.
- Liskov Substitution Principle: a piece of code using a type high in an inheritance hierarchy should be able to support using a subtype within that hierarchy without altering desirable properties of that code (correctness, completeness, etc.). This one is especially suited for Object-Oriented designs as it helps evaluate whether a type hierarchy is correctly built and whether the code that it uses is generic enough.
- Interface Segregation Principle: no piece of code should depend on another piece that it does not use. This principle helps trim unnecessary dependencies.
- Dependency Inversion Principle: high-level entities should not depend upon lower-level entities; they should both depend upon abstractions. This intends to prevent implementation details that are subject to change in the future (thus impacting software) from guiding how software is made.
A note for 2026: these principles are the single best yardstick for reviewing AI-generated code. LLMs are very good at producing code that works in isolation, but they routinely violate SRP (bloated functions that do too much), the Open/Closed Principle (copy-pasted variations instead of extensions), and Dependency Inversion (hard-coded concretions). SOLID gives the reviewer concrete, nameable reasons to push back.
The Code Itself
Finally, we move on to the lowest level of our top-down approach: the code itself. Here we look at how different inherent attributes of the code affect its cleanness, and what do we mean by it.
For the following portion, all aspects are graded with an arbitrary scale -in our case a number from 1 to 10- coinciding with how much the codebase agrees with each criterion we define for each category.
We understand that the grading system has some subjectivity level. Still, we also believe that it is impossible to build an absolute grading system for software as there are many variables to be taken into account when grading.
Complexity
In terms of complexity, we seek the code to be simple enough to be read as prose, and its control flow is easy to follow. The more complexity there is, the harder the code is to read, and the higher probability there is of developers introducing bugs when trying to use or modify the code.
To evaluate complexity, we consider the following criteria:
- Can the code be read as prose?

- Do components follow the single responsibility principle?
- Do classes follow the single responsibility principle?
- Do functions follow the single responsibility principle?
- Is there a balance between the cohesiveness of code components and how decoupled they are from each other?
- Are SOLID principles considered in general?
- How extensible is the code? Is it extensible just where it needs to be?
- How maintainable is the code?
- Does the code follow the newspaper analogy from clean code? Are ideas introduced in a top-down manner, from a higher level of abstraction to a lower abstraction level?

- Are levels of indentation kept to a minimum?

- Deep vs. shallow modules: are there relatively few modules with substantial functionality behind simple interfaces (“deep”), or many tiny modules with complex interfaces (“shallow”)? This distinction, from John Ousterhout’s A Philosophy of Software Design, matters more than ever with AI: a codebase full of shallow modules is hard for both humans and AI assistants to navigate, while deep modules let you reason, and let the AI work, at the interface without getting lost in the internals.
Automated Tests
Automated tests are essential as they provide implicit documentation of the system and give developers a sense of ease when making changes. This way, make sure that if something goes wrong with the changes, the tests will catch it. Nonetheless, this can be a false sense of security if tests are not appropriately made, and not to mention the fact that more tests translate into more code to be maintained.
To circumvent this, we consider the following criteria:
- Do all critical features have automated tests associated with them?
- Is there a balance between non-critical parts of the code being tested and the amount of extra code generated by tests that need to be maintained?
- Are there any unnecessary tests? We identify these as tests for 3rd party portions of the code that are already known to work or have tests of their own (e.g.: Google maps API), and trivial tests (e.g.: plain setters and getters).

- Do existing tests clearly describe what they are testing?
- Do tests consider edge cases?
- Do tests follow the single responsibility principle? Do they test just one thing?
- Do tests consider just one level of abstraction?

Coverage percentage alone tells you which lines ran, not whether your tests would actually catch a regression. We now also look at:
- Test quality over raw coverage: we use techniques like mutation testing to verify that the test suite actually fails when the code is broken, rather than just executing lines.
- CI pipeline integration: tests are only as valuable as their enforcement. We check that the suite runs automatically on every pull request (GitHub Actions, GitLab CI, etc.) and that a failing build blocks merging.
- AI-generated tests, audited as carefully as the code: LLM-generated tests often assert on the implementation rather than the behavior, or simply re-state what the code does, passing by definition. A test that can never fail is worse than no test, because it manufactures false confidence.
In the end, the idea is to strike a balance between the complexity that adding tests adds to the codebase and the probability of tests finding bugs.
Why this matters more with AI: good codebases are easy to test, and easily-tested code gives the AI tighter feedback loops, which in turn produces better code. As Matt Pocock puts it, the rate of feedback is your speed limit, and AI by default tends to “outrun its headlights,” generating large amounts of code before checking anything. A testable codebase is one of the strongest levers for keeping AI-generated code honest.
Naming
Naming files, functions, and variables seems like an easy, mundane task, but it shouldn't go unattended. Great naming immensely benefits code readability, and consequently also affects many other quality attributes such as maintainability, testability, extensibility, etc. In general, we prefer longer descriptive names over shorter but harder to rapidly identify what that name refers to.
These are the criteria we follow when evaluating the naming of files, functions, variables, and classes:
- Do names describe what they reference logically? Are they intention-revealing?
- Do names avoid encodings? Are they pronounceable and searchable?

Comments
A common rule of thumb is that the code should be clear enough that no comments are needed, and that further comments regarding business logic or design decisions should reside in some kind of documentation. However, there are certain scenarios in which the inherent obscure logic or complexity of the problem makes it a necessity to have clarifying comments (or sometimes even comments explaining why that code exists).
Prioritizing clean self-explanatory code over comments is of paramount importance. Most of the time, comments are not updated at the same pace as code, which can turn an otherwise clean piece of code into a confusing mess.
- Can they be clearly read as "normal" spoken language?
- Are all written comments necessary, or can they be removed by having cleaner code?

- Are there any complex unexplained pieces of code?
- Are there lines of commented code? There shouldn't be any!
- Are there any lingering TODO comments that can be removed?
- Are there any outdated comments?

- Are there boilerplate comments left behind by AI assistants? Tools frequently emit narrating comments like “// loop through the array” that restate the obvious code beneath them. These should be removed.
Style
When it comes to style, the most critical attribute we are looking for is consistency. There are many styling combinations and preferences, and all are equally valid but with a slight preference for the official styling guides, such as pep8 for Python. Where do we look for consistency:
- Pascal case, camel case, snake case on variable names, classes, instances, functions, packages, and every other nameable entity.

- Indentation with the same amount of spaces or tabs, exclusively.
- Opening braces on either on the same line or the next.
- Use of semicolons in languages where it is optional, such as Javascript.
That said, modern development teams should rarely spend human review time discussing these issues. Automated formatters (such as Prettier, Black, gofmt, or PHP CS Fixer) and linters (such as ESLint or Ruff) can enforce style mechanically and consistently as part of the development workflow and CI pipeline.
As a result, style-related findings in an audit are often less about the specific formatting choices and more about the absence of automation. If developers are manually debating indentation, brace placement, semicolon usage, or other formatting concerns, that may indicate an opportunity to improve tooling and reduce cognitive overhead during code reviews.
A well-configured codebase should make style decisions automatic, allowing developers and reviewers to focus their attention on architecture, correctness, maintainability, and business logic instead.
Static Analysis & Security
This category didn’t make our original 2020 guide, but today it’s non-negotiable. A meaningful slice of code quality and risk can be assessed automatically, and a modern audit should confirm these tools exist and are wired into the workflow:
- Static analysis (SAST): are tools running to catch bugs, code smells, and security anti-patterns before review?
- Dependency & vulnerability scanning: are third-party dependencies monitored for known vulnerabilities and kept reasonably up to date? (e.g., Dependabot, npm/pip audit.)
Auditing AI-Assisted Code
Since this guide first went out, AI coding assistants have gone from novelty to default. That doesn’t replace the audit, it raises the stakes. AI-generated code tends to be locally plausible and globally inconsistent: it works in isolation and passes a happy-path test, while quietly introducing duplication, unnecessary dependencies, and patterns that don’t match the rest of the codebase.
Everything above still applies; AI code is graded by the exact same criteria as human code. On top of that, we pay specific attention to:
- Hidden duplication: LLMs love to re-implement logic that already exists elsewhere instead of reusing it. Watch for near-identical functions and copy-pasted blocks.
- Unnecessary dependencies: AI will reach for a library to solve a one-line problem. Check that each added dependency earns its place.
- Consistency with the existing codebase: generated code often follows the model’s default conventions rather than the project’s. It should look like it was written by the same team.
- Confident-but-wrong logic: the code reads cleanly and looks authoritative, which makes subtle correctness bugs harder to spot. Edge cases and error handling deserve extra scrutiny.
- Does anyone understand it? the bar is unchanged: a human on the team must understand and be able to maintain every line, regardless of who, or what, wrote it.
The takeaway: clean-code principles are now also a quality gate for machine-generated code. The teams getting the most out of AI assistants are the ones with the strongest review discipline, not the weakest.
This isn’t just our view. In his 2026 talk “It Ain’t Broke: Why Software Fundamentals Matter More Than Ever,” Matt Pocock, who has spent over a year teaching developers to build with AI agents, makes the same case from the opposite direction. The popular “specs-to-code” idea holds that code is cheap: you write a spec, let the AI generate the code, and never look at the code itself. Pocock’s experience is that ignoring the code this way produces progressively worse output on each pass, a textbook case of software entropy. His conclusion: bad code is the most expensive it has ever been, because a codebase that’s hard to change is one where AI can’t deliver its value. Good codebases let AI shine, which is exactly why a disciplined audit is worth more now, not less.
The mental model he offers is a useful one for any team adopting these tools: treat the AI as an excellent tactical programmer, capable of implementing changes quickly and efficiently, while a human remains responsible for the strategic level: the architecture, the module boundaries, and the design decisions. That strategic layer is precisely what a code audit evaluates.
Documentation
Even though documentation is not inherent to the code, it is a great practice to have some kind of it, possibly in the form of a README file, helping new developers to set themselves up to speed with minimal additional human help. What we look for:
- A way to run the project locally with one or two commands (e.g., a documented setup script or a containerized dev environment), so onboarding doesn’t depend on tribal knowledge.

Conclusion
The main objective is to evaluate the current state of the codebase in terms of code cleanness and identify areas that should be improved while doing it so the reader quickly understands what's going on and can come back and measure the improvement as quantitatively as possible.
To summarize, Light-it achieves this objective by sticking to a top-down approach when writing code reports. Starting from higher levels of abstraction that aim to give context and prepare the reader for what follows, all the way down to the lowest level where the code stands and the actual critique of it exists. Within that level, we identify several categories and criteria to rank the codebase in those categories.
What hasn’t changed in all these years is the foundation: architecture, design, SOLID, and Clean Code. What has changed is the context around it, AI-assisted development, automated static analysis, security scanning, and CI/CD are now part of the picture. These reports help us minimize technical debt and keep our company “agile,” adapting to change at the lowest possible cost while bringing the most value to our customers.