# Is a list of callers ever the full list?

> Rule AR-27: Treat \"callers found\" as the full list only when the tool says the list is full.

Every caller search returns a list. Few of them say whether it is the whole list. A list that doesn't say is a guess with good formatting: it may be the full list, and nothing in it tells you. An agent should treat the entries as lower bounds and write down what the tool could not rule out.

## Why it matters
`rg -n "send_invoice"` returns five lines. The agent changes the function, updates five calls, and moves on. The sixth caller is:

```python
handler = getattr(mailer, f"send_{doc_type}")
handler(doc)
```

Text search had no way to match it. A language-server reference search returned the same five, because the sixth has no reference to resolve. Both tools answered the question they could answer. Neither said "and there may be calls I can't see". The agent read five results as "five callers".

The same shape appears with other tools:
- A references request resolves what the server's type information resolves. Where a value's type is unknown, the call isn't listed, and the list doesn't say so.
- A text search finds spellings. It can't find a name that is built or imported under another name.
- A test run exercises the paths the tests take.

A full list is possible in a closed world (one file, no dynamic access). In real code you need a statement from the tool, or your own bound.

## The steps
1. **Ask what the tool's list covers.** Does it say the list is full? Does it report what it could not resolve? If the answer is no, the list is a lower bound.
2. **Write the bound next to the number.** "5 callers found by `rg`; dynamic sites not searched" is a claim you can stand behind. "5 callers" is not.
3. **Search for what the list can't hold** (AR-25 step 4): built names, tables, aliases, wildcard re-exports.
4. **Count the open items separately.** Sites you looked at and read are checked. Sites you saw and did not read are open. Sites a tool said it couldn't resolve are open.
5. **Report the split** in the final message (AR-30): checked, open, and why.
6. **Don't convert absence into proof.** "No other callers found" is a statement about the search. "No other callers exist" needs a tool that says the list is full or a reason the closed world holds.

## Worked example
Two ways to write the same finding after changing `send_invoice(doc)` to `send_invoice(doc, locale)`:

```text
weak:   Found 5 callers and updated all of them.

better: Updated 5 direct calls (rg and find-references agree).
        Not covered: mailer.py:88 builds the name with getattr(mailer, f"send_{doc_type}").
        I read it: for doc_type == "invoice" it calls send_invoice(doc) with one argument.
        I did not run it; no test reaches that line.
```

Both reports are about the same work. The second one lets the next reader, or the next agent, decide where to look.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
## Caller lists
- A list of callers is full only if the tool says so. Otherwise it is a lower bound.
- Say what each search could not see: built names, tables of functions, aliases, wildcard re-exports,
  code outside this repo.
- Write "N found, M open" instead of "N callers". Open means seen but not read, or reported unresolved.
- Do not turn "no other callers found" into "there are no other callers".
- Open items go in the final message with file and line.
```

## Edge cases
- **Closed worlds.** A helper used inside one module only with no `__all__`, no dynamic access in the file, and no re-export is a place where a text search is close to the full list. Say that reason in a clause; don't assume it.
- **Tools that do report completeness.** Use the statement. Quote what it says and what it left out.
- **Test-only callers.** Count them; they are callers. Mocks that patch the name by string (`"billing.money.format_amount"`) are another string-keyed reference to search for.
- **Entry points registered outside code** (config, `pyproject.toml` scripts, `package.json` bin fields, route files) call functions by name from data. Search the config files too.
- **A list of zero.** "Zero callers" is the riskiest sentence in a code change, since deleting the function follows from it. Require both a tool statement and a dynamic-site search first.

## What LLMGRAPH shows
Each caller prints certain or open. A getattr call written on one line prints 'plus 1 dynamic call nearby that could reach anything' with its file. A getattr result held in a variable and called on the next line is not listed.

## References
- LSP 3.17 specification, Find References request: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/. Verified 2026-10-30: the request is sent "to resolve project-wide references" for the symbol at a position and returns `Location[] | null`.
- Python library reference, `getattr`: https://docs.python.org/3/library/functions.html
- Pyright type inference (unknown types in partially typed code): https://github.com/microsoft/pyright/blob/main/docs/type-inference.md. Verified 2026-10-30: when pyright cannot infer a type it sets it to Unknown, "a special form of Any", which can leave blind spots; for a function with all parameters unannotated it infers the return type per call from the argument types.

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: docs checks resolved
- 2026-10-30: copy-gate wording fixes

## Related

- [AR-25: What to check before changing a function signature?](https://beta.llmgraph.in/agent-readable/ar-25.md)
- [AR-26: How does an agent know an edit is finished?](https://beta.llmgraph.in/agent-readable/ar-26.md)
- [AR-30: What should an agent report when it can't verify an edit?](https://beta.llmgraph.in/agent-readable/ar-30.md)
- [AR-07: Why does my coding agent miss calls made through getattr?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-01: Why do star imports make my coding agent miss callers?](https://beta.llmgraph.in/agent-readable/ar-01.md)

Last verified 2026-10-30
