Code Quality Audit: How to Clean Up What AI Coding Tools Leave Behind

calendar_today September 7, 2026
person info@softcrony.com
folder DevOps
Code quality audit illustration showing AI-generated technical debt being identified and refactored into clean maintainable code in Laravel PHP applications

🕮13 min read · 2,566 words

This is Part 4 of our AI Code Audit Series — covering the essential audits every team should run when building with AI agentic editors.

Code quality problems are the quietest of all the issues AI agentic editors introduce. They don’t cause immediate failures. They don’t create security breaches. They don’t produce wrong results in tests. They accumulate silently — in inconsistent patterns, duplicated logic, bloated functions, and architectural decisions that made sense in isolation but create friction across the codebase.

The cost of poor code quality is paid in developer time — every feature takes longer to build because the codebase is harder to understand, every bug takes longer to fix because the logic is spread across too many places, and every new team member takes longer to onboard because the code doesn’t follow consistent patterns. These costs are real and significant, but they’re diffuse enough that they’re rarely attributed to their actual cause.

This post covers the specific code quality problems AI agentic editors introduce consistently, how to find them, and how to fix them before they accumulate to the point where they slow your team significantly.

Why AI Agentic Editors Specifically Create Code Quality Problems

AI agentic editors make local decisions. Each prompt is answered in isolation — the AI generates code that solves the immediate problem without awareness of how similar problems were solved elsewhere in the codebase, what patterns the project has established, or what the long-term implications of its architectural choices are.

The result is code that is often locally reasonable but globally inconsistent. The same concept implemented three different ways in three different files. Functions that grew beyond their original scope because the AI kept adding to them as the prompt evolved. Abstractions that made sense for the original use case but were then copy-pasted and slightly modified for related cases, creating a family of near-duplicate implementations that will all need to be updated when the underlying logic changes.

Speed compounds the problem. AI agentic editors produce code significantly faster than human developers. More code in less time means more surface area for quality problems — and less time spent thinking about whether the approach is right before committing to it.

Quality Problem 1 — Duplicated Logic

This is the most pervasive quality problem in AI-generated codebases. The AI generates a solution for one use case, then generates a slightly different solution for a related use case in a different session, with no awareness that it’s solving the same underlying problem twice.

// In OrderController.php — generated week 1
public function calculateShipping(Order $order): float
{
    if ($order->total >= 999) return 0;
    if ($order->destination === 'local') return 49;
    if ($order->destination === 'metro') return 79;
    return 99;
}

// In CartController.php — generated week 3
public function getShippingCost(Cart $cart): float
{
    $total = $cart->items->sum(fn($i) => $i->price * $i->quantity);
    if ($total >= 999) return 0.0;
    if ($cart->shipping_zone === 'local') return 49.0;
    if ($cart->shipping_zone === 'metro') return 79.0;
    return 99.0;
}

// In CheckoutController.php — generated week 5
public function shippingFee(Request $request): float
{
    if ($request->order_total > 999) return 0; // Bug: should be >= not >
    return match($request->zone) {
        'local' => 49,
        'metro' => 79,
        default => 99,
    };
}

// CORRECT — single canonical implementation
// app/Services/ShippingCalculator.php
class ShippingCalculator
{
    private const FREE_SHIPPING_THRESHOLD = 999;
    private const RATES = [
        'local' => 49,
        'metro' => 79,
        'other' => 99,
    ];

    public function calculate(float $orderTotal, string $zone): float
    {
        if ($orderTotal >= self::FREE_SHIPPING_THRESHOLD) {
            return 0.0;
        }

        return (float) (self::RATES[$zone] ?? self::RATES['other']);
    }
}

// All controllers now inject and use ShippingCalculator
// One change to shipping logic updates everywhere

The three-implementation version has a bug in the third one — it uses > instead of >= for the free shipping threshold. This bug exists because there’s no single source of truth. When shipping rules change, all three need to be updated — and the inconsistency means they’ll drift further apart over time.

Audit check: Find duplicated business logic by searching for similar patterns across files.

# Find potential shipping calculation duplication
grep -rn "shipping\|delivery.*cost\|postage" app/ --include="*.php" -l

# Find similar calculation patterns
grep -rn "total.*999\|999.*total\|free.*ship" app/ --include="*.php"

# Find duplicated validation rules
grep -rn "validate\[" app/Http/Controllers --include="*.php" -A10 | \
  grep -E "required|email|max:" | sort | uniq -d

Quality Problem 2 — Oversized Functions and Controllers

AI agentic editors tend to grow functions as prompts evolve. What starts as a 20-line function becomes 80 lines as additional requirements are added to subsequent prompts. Controllers accumulate logic that belongs in services or models. Functions that were originally focused take on multiple responsibilities.

// PROBLEMATIC — controller method doing too many things (AI-generated over multiple prompts)
public function processOrder(Request $request)
{
    // Validation — 15 lines
    $validated = $request->validate([...]);

    // Inventory check — 20 lines
    foreach ($validated['items'] as $item) {
        $product = Product::find($item['product_id']);
        if ($product->stock < $item['quantity']) {
            return response()->json(['error' => 'Insufficient stock'], 422);
        }
        $product->decrement('stock', $item['quantity']);
    }

    // Order creation — 10 lines
    $order = Order::create([...]);
    foreach ($validated['items'] as $item) {
        $order->items()->create($item);
    }

    // Payment processing — 25 lines
    $payment = new RazorpayService();
    $paymentIntent = $payment->createIntent($order->total);
    $order->update(['payment_intent_id' => $paymentIntent->id]);

    // Email notification — 10 lines
    Mail::to($request->user())->send(new OrderConfirmationMail($order));

    // Loyalty points — 8 lines
    $points = floor($order->total / 100);
    $request->user()->increment('loyalty_points', $points);

    // Analytics — 5 lines
    Analytics::track('order_placed', ['order_id' => $order->id, 'total' => $order->total]);

    return new OrderResource($order);
}

// CORRECT — controller delegates to focused services
public function processOrder(ProcessOrderRequest $request)
{
    $order = $this->orderService->process(
        user: $request->user(),
        items: $request->validated('items'),
        shippingAddress: $request->validated('shipping_address'),
    );

    return new OrderResource($order);
}

// app/Services/OrderService.php — focused, testable, reusable
class OrderService
{
    public function __construct(
        private InventoryService $inventory,
        private PaymentService $payment,
        private LoyaltyService $loyalty,
    ) {}

    public function process(User $user, array $items, array $shippingAddress): Order
    {
        DB::transaction(function () use ($user, $items, $shippingAddress, &$order) {
            $this->inventory->reserveItems($items);
            $order = Order::createForUser($user, $items, $shippingAddress);
            $this->payment->initiate($order);
            $this->loyalty->awardPoints($user, $order);
        });

        OrderConfirmationMail::dispatch($order);

        return $order;
    }
}

Audit check: Find oversized methods using PHP metrics tools.

# Install PHP Metrics
composer require --dev phpmetrics/phpmetrics

# Run analysis
./vendor/bin/phpmetrics --report-html=metrics-report app/

# Quick grep for large methods — find methods over ~30 lines
awk '/function /{start=NR} start && NR-start>30{print FILENAME ":" start; start=0}' \
  $(find app -name "*.php") | head -20

Quality Problem 3 — Inconsistent Patterns Across the Codebase

AI generates code using whatever pattern fits the prompt — which means different patterns appear in different parts of the codebase for the same type of problem. Response formatting, error handling, query building, and validation all end up inconsistent because each was generated independently.

// Three different error response formats — all AI-generated
// In UserController.php
return response()->json(['error' => 'Not found'], 404);

// In OrderController.php  
return response()->json(['message' => 'Order not found', 'status' => 'error'], 404);

// In ProductController.php
return response()->json([
    'success' => false,
    'data' => null,
    'error' => ['code' => 404, 'message' => 'Product not found']
], 404);

// CORRECT — consistent response format via base controller or trait
// app/Http/Controllers/Controller.php
abstract class Controller
{
    protected function successResponse(mixed $data, int $status = 200): JsonResponse
    {
        return response()->json([
            'success' => true,
            'data'    => $data,
        ], $status);
    }

    protected function errorResponse(string $message, int $status): JsonResponse
    {
        return response()->json([
            'success' => false,
            'message' => $message,
        ], $status);
    }

    protected function notFound(string $resource = 'Resource'): JsonResponse
    {
        return $this->errorResponse("{$resource} not found", 404);
    }
}

// All controllers now use consistent format
public function show(int $id): JsonResponse
{
    $user = User::find($id);

    if (!$user) {
        return $this->notFound('User');
    }

    return $this->successResponse(new UserResource($user));
}

Audit check: Search for response patterns and identify inconsistencies.

grep -rn "response()->json" app/Http/Controllers --include="*.php" | \
  grep -oP "'\w+'" | sort | uniq -c | sort -rn
# Multiple different key names (error, message, status) are a finding

Quality Problem 4 — Magic Numbers and Hardcoded Values

AI-generated code is full of magic numbers — numeric and string literals that appear without explanation and whose meaning isn’t self-evident. These are particularly common in business logic, time calculations, and configuration values.

// PROBLEMATIC — magic numbers throughout
public function processRefund(Order $order): void
{
    if ($order->created_at->diffInHours(now()) > 48) {
        throw new RefundWindowExpiredException();
    }

    $refundAmount = $order->total * 0.9; // What is 0.9?
    
    if ($refundAmount < 100) { // Why 100?
        throw new RefundAmountTooLowException();
    }

    // Process refund...
    
    $order->update(['status' => 'refunded']); // Magic string
    Cache::forget('user_orders_' . $order->user_id);
    Cache::forget('order_stats'); // Why this specific key?
}

// CORRECT — named constants with clear intent
class Order extends Model
{
    // Status constants
    const STATUS_PENDING   = 'pending';
    const STATUS_CONFIRMED = 'confirmed';
    const STATUS_REFUNDED  = 'refunded';
    const STATUS_CANCELLED = 'cancelled';

    // Business rule constants
    const REFUND_WINDOW_HOURS      = 48;
    const REFUND_PROCESSING_FEE    = 0.10; // 10% processing fee
    const MINIMUM_REFUND_AMOUNT    = 100;  // ₹100 minimum
}

public function processRefund(Order $order): void
{
    if ($order->created_at->diffInHours(now()) > Order::REFUND_WINDOW_HOURS) {
        throw new RefundWindowExpiredException(
            "Refunds must be requested within " . Order::REFUND_WINDOW_HOURS . " hours"
        );
    }

    $processingFee = $order->total * Order::REFUND_PROCESSING_FEE;
    $refundAmount  = $order->total - $processingFee;

    if ($refundAmount < Order::MINIMUM_REFUND_AMOUNT) {
        throw new RefundAmountTooLowException(
            "Minimum refund amount is ₹" . Order::MINIMUM_REFUND_AMOUNT
        );
    }

    $order->update(['status' => Order::STATUS_REFUNDED]);
    $this->clearOrderCache($order);
}

private function clearOrderCache(Order $order): void
{
    Cache::forget("user_orders_{$order->user_id}");
    Cache::forget('order_stats');
}

Audit check: Find magic numbers in business logic.

# Find numeric literals in business logic (excluding 0 and 1 which are often valid)
grep -rn "[^0-9][2-9][0-9]\+[^0-9]" app/Services app/Models --include="*.php" | \
  grep -v "//\|max:\|min:\|Carbon\|created_at"

# Find hardcoded status strings
grep -rn "'pending'\|'confirmed'\|'cancelled'\|'active'\|'inactive'" \
  app/Http/Controllers app/Services --include="*.php"

Quality Problem 5 — Poor Exception Handling

AI-generated code handles exceptions inconsistently — sometimes catching too broadly, sometimes not catching at all, sometimes swallowing exceptions silently, and frequently using generic Exception classes instead of specific ones.

// PROBLEMATIC — inconsistent and dangerous exception handling
public function importProducts(Request $request): JsonResponse
{
    try {
        $file = $request->file('csv');
        $data = array_map('str_getcsv', file($file->path()));
        
        foreach ($data as $row) {
            Product::create([
                'name'  => $row[0],
                'price' => $row[1],
                'sku'   => $row[2],
            ]);
        }

        return response()->json(['message' => 'Import successful']);

    } catch (Exception $e) {
        // Catches everything including out of memory, catches but does nothing useful
        return response()->json(['error' => 'Import failed'], 500);
        // Original exception lost — no logging, no context
    }
}

// CORRECT — specific exceptions, proper logging, transaction safety
public function importProducts(ImportProductsRequest $request): JsonResponse
{
    $file = $request->file('csv');

    try {
        $imported = DB::transaction(function () use ($file) {
            return $this->productImportService->import($file->path());
        });

        return response()->json([
            'message'  => 'Import successful',
            'imported' => $imported,
        ]);

    } catch (InvalidCsvFormatException $e) {
        // Expected business exception — return helpful error to user
        return response()->json([
            'message' => 'Invalid CSV format: ' . $e->getMessage(),
            'line'    => $e->getLine(),
        ], 422);

    } catch (DuplicateSkuException $e) {
        return response()->json([
            'message'       => 'Duplicate SKU found',
            'duplicate_sku' => $e->getSku(),
        ], 422);

    } catch (\Throwable $e) {
        // Unexpected exception — log with full context, return generic message
        Log::error('Product import failed', [
            'user_id'  => auth()->id(),
            'filename' => $file->getClientOriginalName(),
            'error'    => $e->getMessage(),
            'trace'    => $e->getTraceAsString(),
        ]);

        return response()->json([
            'message' => 'Import failed due to an unexpected error. Our team has been notified.',
        ], 500);
    }
}

Audit check: Find broad exception catching and silent exception swallowing.

# Find broad catch blocks
grep -rn "catch (Exception\|catch (\\\Exception" app/ --include="*.php" -A3

# Find empty catch blocks or catch blocks that only return without logging
grep -rn "} catch" app/ --include="*.php" -A5 | \
  grep -v "Log::\|report(\|throw\|return response"

Quality Problem 6 — Missing or Inadequate Tests

AI agentic editors generate feature code readily but generate test code only when explicitly asked — and even then, the tests cover the happy path and miss edge cases. Codebases built with AI tools frequently have significant feature code with minimal test coverage.

// AI-generated test — covers only the happy path
class OrderTest extends TestCase
{
    public function test_order_can_be_created()
    {
        $user = User::factory()->create();
        $response = $this->actingAs($user)->postJson('/api/orders', [
            'items' => [['product_id' => 1, 'quantity' => 2]],
        ]);
        $response->assertStatus(201);
    }
}

// COMPLETE tests — covers happy path AND edge cases
class OrderTest extends TestCase
{
    // Happy path
    public function test_authenticated_user_can_create_order()
    {
        $user    = User::factory()->create();
        $product = Product::factory()->create(['stock' => 10, 'price' => 500]);

        $response = $this->actingAs($user)->postJson('/api/orders', [
            'items' => [['product_id' => $product->id, 'quantity' => 2]],
        ]);

        $response->assertStatus(201)
                 ->assertJsonPath('data.total', 1000);

        $this->assertDatabaseHas('orders', ['user_id' => $user->id]);
        $this->assertEquals(8, $product->fresh()->stock); // Stock decremented
    }

    // Edge cases AI missed
    public function test_cannot_order_more_than_available_stock()
    {
        $product = Product::factory()->create(['stock' => 2]);

        $response = $this->actingAs(User::factory()->create())
                         ->postJson('/api/orders', [
                             'items' => [['product_id' => $product->id, 'quantity' => 5]],
                         ]);

        $response->assertStatus(422)
                 ->assertJsonPath('message', 'Insufficient stock');

        $this->assertEquals(2, $product->fresh()->stock); // Stock unchanged
    }

    public function test_unauthenticated_user_cannot_create_order()
    {
        $response = $this->postJson('/api/orders', [
            'items' => [['product_id' => 1, 'quantity' => 1]],
        ]);
        $response->assertStatus(401);
    }

    public function test_cannot_access_another_users_order()
    {
        $order = Order::factory()->create(); // Belongs to a different user
        $response = $this->actingAs(User::factory()->create())
                         ->getJson("/api/orders/{$order->id}");
        $response->assertStatus(403);
    }

    public function test_order_creation_is_atomic_on_stock_failure()
    {
        // First item valid, second item out of stock
        // Entire order should fail — first item stock should be unchanged
        $product1 = Product::factory()->create(['stock' => 10]);
        $product2 = Product::factory()->create(['stock' => 0]);

        $this->actingAs(User::factory()->create())->postJson('/api/orders', [
            'items' => [
                ['product_id' => $product1->id, 'quantity' => 1],
                ['product_id' => $product2->id, 'quantity' => 1],
            ],
        ])->assertStatus(422);

        $this->assertEquals(10, $product1->fresh()->stock); // Unchanged
        $this->assertDatabaseCount('orders', 0); // No order created
    }
}

Audit check: Check test coverage and identify untested edge cases.

# Check test coverage
php artisan test --coverage --min=80

# Find files with no corresponding test
for f in app/Services/*.php; do
    basename=$(basename "$f" .php)
    if [ ! -f "tests/Unit/Services/${basename}Test.php" ] && \
       [ ! -f "tests/Feature/${basename}Test.php" ]; then
        echo "No test found for: $f"
    fi
done

The Code Quality Audit Checklist

Duplication

  • Every business rule has one canonical implementation
  • Shared logic lives in services or traits, not duplicated across controllers
  • Validation rules for the same resource are consistent across create/update endpoints
  • No copy-pasted blocks of more than 5 lines exist without refactoring

Structure and Size

  • No controller method exceeds 20–25 lines
  • No function has more than one primary responsibility
  • Complex business logic lives in service classes, not controllers or models
  • No class file exceeds 200–250 lines without clear justification

Consistency

  • API response format is consistent across all endpoints
  • Error handling follows the same pattern throughout the codebase
  • Naming conventions are consistent — camelCase methods, snake_case database columns
  • Query building style is consistent — Eloquent or Query Builder, not mixed arbitrarily

Clarity

  • No magic numbers — all numeric constants are named
  • No magic strings — all status values, types, and categories use constants or enums
  • Method names describe what they do, not how they do it
  • Complex logic has explanatory comments, not obvious operations

Testing

  • Every public service method has at least one test
  • Every API endpoint has tests for happy path, authentication failure, and authorisation failure
  • Business logic edge cases are tested explicitly
  • Database transactions are tested for atomicity where relevant

Tooling for Code Quality Audits

# PHP CodeSniffer — style and standards
composer require --dev squizlabs/php_codesniffer
./vendor/bin/phpcs app --standard=PSR12

# PHP Mess Detector — complexity and duplication
composer require --dev phpmd/phpmd
./vendor/bin/phpmd app text cleancode,codesize,controversial,design,naming,unusedcode

# PHP Copy Paste Detector — duplicated code
composer require --dev sebastian/phpcpd
./vendor/bin/phpcpd app --min-lines=5

# PHP Insights — comprehensive quality score
composer require --dev nunomaduro/phpinsights
php artisan insights

# Laravel Pint — automatic code formatting
composer require --dev laravel/pint
./vendor/bin/pint

Run these in your CI pipeline. PHP Insights gives a single quality score across architecture, complexity, style, and duplication — a useful overall health indicator. PHP Copy Paste Detector is particularly valuable for AI-generated codebases where duplication is the most common quality problem.

Next in the series: Performance Audit — finding the N+1 queries, missing indexes, and scaling problems before they affect real users.

If you want a code quality audit run on your existing codebase — a clear picture of technical debt and a prioritised plan to address it — our team at Softcrony is happy to help.

Leave a comment