developer-tools #code-review #semantic-diff #ai-generated-code #git #tree-sitter #laravel #vue

A Small Diff Is Not a Small Change: Review AI Patches by Code Entity

A short AI-generated diff can cross validation, request, persistence, and test boundaries. Review the named code entities, the evidence, and the rollback seam—not only the lines that changed.

A coding agent changes seven lines in a validator. The pull request looks safe. The reviewer sees a small diff, a green test run, and a tidy commit message.

But the validator feeds a controller, the controller builds a queue payload, and a fixture still describes the old rejection shape. The diff is seven lines. The behavior boundary is not.

That is the review mistake I see most often with AI-assisted changes: treating line count as a proxy for change size. A diff is an excellent record of text edits. It is not automatically a map of the functions, classes, callers, tests, and rollback points that now have a different contract. The question is not whether this demos well; it is whether it survives maintenance, handoff, and local constraints.

This is a practical review workflow for Laravel and Vue repositories, but the underlying discipline is framework-neutral. It uses Git as the source of textual truth, then adds an entity-level reading pass: name what changed, identify what depends on it, prove the important behavior, and record how to reverse it. The open-source project sem describes this direction as entity-level diffs, blame, and impact analysis on top of Git; its documentation also presents cross-file dependency and affected-test views. [Source: https://github.com/Ataraxy-Labs/sem] [Source: https://ataraxy-labs.github.io/sem/]

A small line diff expands into named code entities, request boundaries, tests, and a rollback seam

Why line diffs become misleading around generated patches

A conventional diff answers a literal question: which lines were removed and added? That is necessary evidence. It becomes incomplete when a patch changes a named unit that carries a contract outside the file.

Consider a generated edit to a Laravel request rule. The agent changes max:64 to max:32, adds a custom message, and updates one feature test. A reviewer who only scans the changed lines can miss at least four questions:

Review lensQuestion the diff alone does not answerEvidence to request
EntityWhich validation rule changed and where is it reused?named request class and rule key
BoundaryWhat JSON response reaches the Vue client?feature test for the error payload
CallerWhich form or API client sends this field?caller search and one UI test
RollbackWhat restores the prior behavior safely?revert commit or feature flag plan

The point is not to inflate every review into an architecture meeting. It is to use a stable trigger: when a generated patch changes a named entity with callers or an external boundary, switch from a line review to an entity review.

GitHub’s pull-request documentation describes reviews as a way to comment on proposed changes before merge; the review UI is deliberately line-oriented because that is how patches are represented. [Source: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews] The reviewer still owns the higher-level question: what behavior changed beyond the highlighted lines?

A semantic view helps because code already has structure. A function has a name and callers. A class has methods and interfaces. A request handler sits on a boundary. A test fixture encodes an expected outcome. Tree-sitter is a parser generator and incremental parsing library designed to build concrete syntax trees; tools can use that structure to identify code entities rather than treating a source file as undifferentiated text. [Source: https://tree-sitter.github.io/tree-sitter/]

Start the review with an entity inventory

The fastest useful review artifact is not a prose summary. It is a small inventory that makes the patch falsifiable. Ask the author—human or agent—to name the entities it believes it changed, then compare that list with the diff.

Use a review template that is small enough to survive ordinary work:

 1# .github/ai-change-review.yml
 2change:
 3  intent: "Reject display names longer than 32 characters"
 4  entities_changed:
 5    - app/Http/Requests/UpdateProfileRequest.php::rules
 6    - resources/js/pages/ProfileForm.vue::submit
 7    - tests/Feature/ProfileUpdateTest.php::profile_name_validation
 8boundaries:
 9  - "PATCH /api/profile -> validation JSON"
10evidence:
11  - "php artisan test --filter=ProfileUpdateTest"
12  - "npm run test:unit -- ProfileForm"
13rollback:
14  method: "revert commit"
15  condition: "unexpected validation failures after release"

This file is not a gate that proves correctness. It is a contract for the review conversation. If the agent cannot name the entities it changed, it has not supplied enough context for a reliable approval. If it names only the request class while the patch also changes the Vue mapping function, the mismatch becomes visible before merge.

The entity list also prevents a familiar failure: the test is green because the test exercises an old path. A feature test named after the request boundary and a unit test named after the UI adapter make the expected scope explicit.

The inventory also creates a useful review stop condition. If an entity has no known callers, no boundary, no evidence, and no rollback story, do not fill the blank with a confident explanation. Mark it unknown, assign the investigation, and keep the merge decision proportional to that uncertainty.

For a normal Git-only workflow, create an inventory from the patch before you ask an agent for explanation:

 1#!/usr/bin/env bash
 2set -euo pipefail
 3
 4base_ref="${1:-origin/main}"
 5
 6git diff --name-only "$base_ref"...HEAD
 7printf '\nChanged declarations (manual review starting point):\n'
 8git diff --unified=0 "$base_ref"...HEAD \
 9  | grep -E '^\+.*(function |class |public function |export (const|function|class)|const [A-Za-z_].*=)' \
10  || true

The command is intentionally modest. It does not pretend regular expressions understand every language. It gives the reviewer a starting list, then the reviewer checks the actual syntax and call sites. This is where entity-aware tooling earns its place: use it to reduce navigation work, not to replace reasoning.

Entity inventory card: intent, changed entities, boundary, evidence, and rollback

Trace the boundary before debating implementation style

The first entity to inspect is usually not the function with the largest diff. It is the closest meaningful boundary: an HTTP request, queue message, database write, permission check, webhook, or browser-facing state transition.

A Laravel request validation change is a clean example. The code below makes the domain rule explicit and keeps the response contract testable.

 1<?php
 2// app/Http/Requests/UpdateProfileRequest.php
 3
 4namespace App\Http\Requests;
 5
 6use Illuminate\Foundation\Http\FormRequest;
 7
 8final class UpdateProfileRequest extends FormRequest
 9{
10    public function authorize(): bool
11    {
12        return true;
13    }
14
15    public function rules(): array
16    {
17        return [
18            'display_name' => ['required', 'string', 'max:32'],
19        ];
20    }
21}

The entity review does not stop at rules(). It checks the controller that receives the validated data, the API response used by the client, and the test that proves a rejected payload remains rejected. One feature test can anchor that behavior:

 1<?php
 2// tests/Feature/ProfileUpdateTest.php
 3
 4it('rejects display names longer than thirty-two characters', function () {
 5    $user = User::factory()->create();
 6
 7    $this->actingAs($user)
 8        ->patchJson('/api/profile', ['display_name' => str_repeat('a', 33)])
 9        ->assertUnprocessable()
10        ->assertJsonPath('errors.display_name.0', 'The display name field must not be greater than 32 characters.');
11});

This test is not valuable because it is long. It is valuable because it proves a named boundary: input arrives at the endpoint, validation decides, and the external response retains the expected shape. A reviewer can now ask a precise follow-up: does the Vue form render this error correctly, and is the string part of a translated contract?

That question is more useful than “does this diff look reasonable?” because it leads to evidence.

Review callers and tests as separate entities

A common AI-patch failure is a local implementation that is internally consistent but wrong for its callers. The generated function accepts a new option. The nearest caller compiles. A second caller still relies on the prior default. The patch passes a narrow test because the test copied the new implementation’s assumptions.

Treat caller review and test review as separate passes.

  1. Caller pass: find every direct call site, then inspect the one that has the least obvious input. That is frequently a batch job, an old admin screen, or a fixture builder.
  2. Test pass: identify the test that would fail if the behavior were reverted. A test that only asserts a mock received a new argument is weaker than a test that observes the boundary outcome.
  3. Negative-path pass: run one invalid, empty, or limit case. Generated code often handles the happy path because that is the easiest local pattern to infer.

In Vue, keep the adapter responsible for mapping an API error into a form state narrow and test it directly:

1// resources/js/lib/profile-errors.ts
2export type ProfileErrors = Record<string, string[]>
3
4export function displayNameError(errors: ProfileErrors): string | null {
5  const first = errors.display_name?.[0]
6  return typeof first === "string" ? first : null
7}
 1// resources/js/lib/profile-errors.spec.ts
 2import { describe, expect, it } from "vitest"
 3import { displayNameError } from "./profile-errors"
 4
 5describe("displayNameError", () => {
 6  it("returns the first server validation error", () => {
 7    expect(displayNameError({ display_name: ["Too long"] })).toBe("Too long")
 8  })
 9
10  it("does not invent an error when the key is absent", () => {
11    expect(displayNameError({})).toBeNull()
12  })
13})

The two tests establish an edge contract. They do not test Laravel. They do not test the browser. They make one entity’s ownership clear: this adapter turns a server-shaped error payload into a displayable message or no message.

This separation matters because it stops review scope from becoming vague. The request class owns input validation. The Vue adapter owns presentation mapping. The feature test owns the HTTP boundary. The unit test owns the adapter behavior. A semantic review connects those named responsibilities without pretending they are one file.

Make impact analysis a question, not a magic verdict

The appeal of semantic diffs is understandable. Entity-level change, blame, and impact analysis are closer to how maintainers reason about a repository than a raw hunk list. The sem project’s public description explicitly frames its work around entity-level diffs, blame, and impact analysis on top of Git, while its site illustrates cross-file dependency and affected-test exploration. [Source: https://github.com/Ataraxy-Labs/sem] [Source: https://ataraxy-labs.github.io/sem/]

That does not mean a tool can certify a safe change. Static structure cannot reliably decide runtime configuration, feature-flag state, data history, vendor behavior, or an undocumented team convention. Treat impact analysis as a queue of questions:

SignalReview questionRequired human decision
Changed functionWhich callers rely on its output?which callers warrant tests now
Modified model or schemaWhat persisted data has the old shape?migration and rollback plan
Changed request/response typeWhich client consumes the boundary?compatibility policy
Affected test candidateDoes the test assert behavior rather than an implementation detail?evidence sufficiency
No discovered callerIs this dead code or dynamic dispatch?delete, document, or trace at runtime

Use the report to focus attention. Do not use it as permission to skip it.

A good agent prompt asks for uncertainty rather than confidence theater. It requires the agent to state what it cannot prove from the repository:

 1You are preparing a code-review handoff.
 2
 3For this patch, return:
 41. named functions, methods, classes, and tests changed;
 52. every direct caller you can establish from repository code;
 63. request, event, persistence, authorization, or UI boundaries touched;
 74. tests executed and the behavior each test proves;
 85. assumptions you could not verify statically;
 96. a reversible rollback step.
10
11Do not claim callers, runtime behavior, or test coverage that you cannot point to in the repository.

The final line is critical. An agent can produce a polished dependency story from incomplete context. A useful review handoff labels unknowns: dynamic event listeners, reflection, external webhooks, configuration-driven routes, and production-only data are all reasons to widen the human investigation.

Review flow diagram: text diff to entity inventory to boundary test to rollback evidence

Put rollback beside evidence, not after approval

A rollback plan does not need a runbook novel. It needs a named action that can restore the prior behavior without improvisation. For a small application change, that often means a revertable commit, a scoped feature flag, a safe database migration sequence, or an explicit deployment command.

The review record should make failure criteria observable:

1release_check:
2  change: "profile display-name validation"
3  observe:
4    - "422 responses for PATCH /api/profile"
5    - "client-side form error rendering"
6  rollback_when:
7    - "valid profile updates are rejected"
8  rollback_command: "git revert <merge-commit>"
9  owner: "release reviewer"

This is deliberately plain. It turns “we can roll it back” into a command, a condition, and an owner. If a change cannot be reverted cleanly because it transforms persisted data or triggers an external side effect, that is not a reason to hide the difficulty. It is a reason to classify the patch as higher risk and add staged migration evidence.

The principle also improves AI-agent work. A coding agent can generate a patch quickly. It cannot be the only source of operational memory after that patch is merged. The review artifacts must remain useful to the next human or agent that investigates a failure.

For more operational patterns, connect this review layer to /developer-tools/, the deployment and maintenance ideas in /ai-agent-operations/, and the starting map at /start-here/. If your team already uses an agent edit contract, add entity inventory and boundary evidence to that contract rather than creating a separate ritual.

What you should do Monday morning

  1. Pick one recently merged AI-assisted pull request. Do not start with the biggest one; start with a change that looked small.
  2. Write an entity inventory: changed function/class/test, external boundary, evidence command, rollback action. Keep it under ten lines.
  3. Trace one caller that the author did not mention and one negative path that the existing tests do not show.
  4. Add a pull-request template section for entities_changed, boundaries, evidence, and rollback.
  5. Make one CI job print the exact test command and preserve its output as a build artifact. A green badge without a named behavior is weak evidence.
  6. Trial an entity-aware diff or impact tool on one repository, but keep Git diff and human boundary review as the final decision path.

Further reading