Before touching a class, a careful developer asks one question: who depends on this?
An agent answers it the only way it can. It runs a text search, reads whatever comes back, and decides the change is local. Most of the time that's fine. The times it isn't are the expensive ones.
What grep can't see
I've been writing PHP for over 18 years, and the agent-written bugs that cost me the most had the same shape. The edit looked local. The nearby tests passed. Something three modules away broke.
In a modern PHP app a lot of wiring never appears as a string you can search for:
- A handler is connected to its payload by an attribute, not by a call.
- An event listener is discovered at boot by scanning, so nothing "calls" it.
- A template reads a field by name, so renaming a property breaks a page that never imports the class.
Grep finds the string. It doesn't find the edge. So the agent does what a cautious person does with a flashlight in a dark warehouse: it reads more. Half the project lands in context "just to be sure", the answer gets slower and more expensive, and it is still a guess.
Give the agent a map instead
While building Semitexa, my PHP framework, I stopped asking agents to rebuild the structure from text on every task. The framework keeps a project graph: real edges between classes, collected from the same attributes and conventions the runtime itself uses. Edges like implements, handles, serves_route, returns.
Before an edit, the agent asks two questions:
bin/semitexa ai:review-graph:query --usages=<Class>
bin/semitexa ai:review-graph:impact <Class>
The first lists what points at a class. The second walks outward and returns the blast radius: the routes, handlers and templates downstream of a change. It comes back as data, so the agent acts on it instead of guessing.
What changed
The side effect I liked most wasn't accuracy. The agent stopped reading half the project. With the impact list in hand it opens the files that matter and leaves the rest alone. Less context, better decisions.
It also changed code review. "What else did this touch?" used to be a question I answered by reading the diff and remembering the codebase. Now it's a query.
The trade-offs
A map has its own failure modes, and they're worth being honest about.
It goes stale. A graph built once describes code that no longer exists five edits later. Ours refreshes by content hash when a query runs, and says so when it can't.
It has blind spots. Anything wired dynamically, like a class name built from a string, is invisible to a static scan. The useful behaviour is for the graph to say what it could not see instead of pretending coverage is complete. An impact report with an "unknown" section is more honest than a clean one.
It is only as good as the conventions. The graph works because the framework has one way to wire a handler and one way to declare a route. In a codebase where routes come from YAML on Monday and attributes on Friday, there are no clean edges to collect. That is a big part of why I care about frameworks having one way to do each job.
Grep still has a job
I use grep every day, and so does the agent. It is the right tool for finding text. It is the wrong tool for understanding a system, because a system is made of relationships, and relationships are exactly what text search throws away.
If you run agents on a large codebase, here is something cheap to try even without a framework: generate a rough dependency list for the area being changed and put it in front of the agent before it starts editing. Watch how much less it reads.
How do your agents judge the impact of a change today: tests, grep, a map, or trust?
I'm building Semitexa in the open. The project graph is described in more detail here: Project Graph in Semitexa.
Top comments (0)