The problem: complexity as a working-set limit¶
The conclusion first¶
An agent's effective working set is bounded. The bound is not the context window in tokens. It is the amount of code the model can hold in a coherent enough state to make a correct change to one routine: the routine itself, what it calls, what calls it, the invariants that hold across those, and the data shapes moving between them.
When a routine's own complexity is large enough, that neighbourhood does not fit. What happens then is not a refusal. The agent produces an edit anyway. It is a plausible edit. It is frequently wrong in a way that the existing tests do not catch, because the branches the tests cover are not the branches the edit disturbed.
A more capable model raises the bound. It does not remove it, and the codebase keeps growing.
What "too complex to reason about" looks like as a number¶
These are measured on a 770-file Python repository, with Understand 6.5.1204. The project is
private, so you cannot re-run these yourself; the size of it and the worst of the routines
are recorded in this repository's own source, in the docstring of
analysis/structure/calls.py.
| Routine | Metric | Value | Default limit |
|---|---|---|---|
RunReport.__post_init__ |
CyclomaticStrict |
45 | 10 |
RunReport.__post_init__ |
CountPath |
9.6 × 108 | 100 |
hydrate_canonical |
CyclomaticStrict |
37 | 10 |
validate_portfolio_evidence |
CyclomaticStrict |
29 | 10 |
_parse_rows |
CountPath |
32 800 | 100 |
CyclomaticStrict counts decision points. CountPath counts acyclic execution paths through
the routine, and it is the number that makes the point, because it is what a reader has to
enumerate to be sure a change is safe in every case. 9.6 × 108 is not a number
anyone enumerates. Nor does anyone enumerate 32 800.
Ask what an agent does when asked to add a case to RunReport.__post_init__. It cannot check
the interaction of the new branch with the existing 45 decision points, so it does not. It
inserts the branch where the surrounding code looks most similar and moves on. That is not a
model defect. It is the correct behaviour when the problem does not fit.
A default limit of CyclomaticStrict 10 and CountPath 100 is not a claim that 11 is bad.
It is a claim about where a routine stops being a thing you reason about and starts being a
thing you approximate.
The measurement that says why fan-in cannot be the rule¶
It would be tempting to find the dangerous routines by looking for the ones everything else
calls. The gate deliberately does not, and the reason is a measurement on that same
repository, recorded in analysis/structure/calls.py:
On the same project, the routine with the highest
CyclomaticStrictof all — a dataclass's__post_init__, 45 — has a call-graph fan-in of zero, because the call that runs it is generated by@dataclassand appears in no source file. A "nothing calls this" rule would have named it dead code, which is the opposite of the truth.
The worst routine in the project is invisible to the graph-shaped heuristic. It is not invisible to a metric on the routine itself. That is why the primary rules in this gate are per-entity thresholds and not graph statistics.
Why the graph is still a lower bound, and why the gate says so¶
The same repository gives the other half of the honesty. Over 44 783 call sites, measured on Understand 6.5.1204, 31.4% of Python call sites bound to nothing callable at all — Understand resolves a call through an instance attribute to the attribute, not to the routine behind it. The equivalent figure on a purpose-built C++ fixture was 6.9%, from a small sample of 29 call sites.
The gate's consequence, from the same docstring:
So an edge this graph holds is real and a number this rule reports is genuinely reached — exceeding a limit is a true positive — while staying under one is not a clearance.
Three things follow, and all three are enforced in code rather than left for you to remember:
- A routine the call graph does not hold is not judged at all, rather than judged as reaching nothing.
- Every call-graph finding carries the resolution figure for its language and the number of unresolved call sites inside its own reach.
- A routine reached through a file that failed to parse is named in the finding's details. On that project, three files with parse errors held 26, 18 and 24 routines in their source and Understand saw 12, 5 and 13 of them.
Why this has to be at the commit boundary¶
A quarterly complexity report tells you the codebase got worse. It does not tell you which of the 400 commits since the last report did it, and by then the answer is expensive.
The commit is the only boundary where three things are true at once:
- The before state exists and is exactly one
gitobject away. - The change is small enough that the finding names a specific routine and a specific line.
- The person or the agent that wrote it is still there, with the reasoning still loaded.
At the commit boundary, "your change took CyclomaticStrict from 12 to 13 on
legacy.report.render" is a two-minute fix. Three months later it is a ticket.
This is also why the run only analyses the affected set. A staged run measures the files in
the change plus the files whose dependency set changed because of it, not the repository.
That is what makes it fast enough to sit in front of git commit at all.
What the gate does not claim¶
- It does not claim that a routine under the limits is correct. It is a shape rule, not a correctness rule. Use CodeQL or Semgrep for correctness; see the comparison.
- It does not claim that low complexity makes an agent right. It claims that high complexity makes an agent guess.
- It has no cost data. There is no "saves N hours" number anywhere in these documents, because none was measured. The argument is about the mechanism.