Your code search says nothing uses it. Can you delete it?
Troy Fortin
- mcp
- agents
- api-design
Your code search says nothing uses this function. You are now in one of two situations and they look identical from where you are standing. Either nothing uses it, so deleting it is safe, or the index has not seen the callers yet, so deleting it takes production down.
Same bytes on the wire. Same shape in the client.
A person might notice the answer feels thin and go check. An agent will not. It has no prior about your repository, it cannot ask a follow-up about where the answer came from, and it was told to be decisive.
Kin’s response to that is one additive object, and the interesting decisions are mostly in what it refuses to say.
Two reserved keys
Every response from Kin’s MCP server carries a versioned envelope under a reserved top-level
key, _kin. It reports which runtime answered, how fresh the graph was, embedding coverage
in the same shape the CLI prints, and whichever degraded flags were actually observed.
The contract that makes it useful is stated in crates/kin-mcp/src/envelope.rs: the
envelope never fabricates coverage or freshness. Anything it could not observe is null or
absent, not a defaulted false and not a zeroed count. A flag reading “not degraded” is a
claim, and the envelope only makes claims it can back.
Beside it, retrieval answers carry a second key, negative, built in
crates/kin-mcp/src/negative.rs. The fields it writes:
{
"kind": "no_references",
"subject": "no references to the focal entity were found",
"result_count": 0,
"safe_to_conclude_absent": false,
"trust": "inconclusive",
"trust_reason": "method_call_resolution_incomplete",
"coverage_basis": "...",
"advice": "...",
"degraded_signals": []
}
That is the shape, assembled from the keys and strings the module writes. It is not a capture from a live run.
safe_to_conclude_absent is the field a caller reads first. It is the difference between
“nothing depends on this” and “nobody has looked yet”, stated as a boolean instead of left
to be inferred from an empty array.
The verdict is false more often than you would guess
It is computed, not asserted, and several gates can only push it down.
The gate that leads is coverage. A graph can be loaded, fully embedded and undegraded while holding no cross-file call or import edges at all, and in that state every reference query against it returns an empty array for every symbol in it, healthy or not. So the module keeps a per-tool map of which edge classes each tool’s absence claim actually depends on, and a tool that declares a dependency and publishes no observation for it is inconclusive by construction. Authority is not inherited by a tool that has not earned it.
Then there is a language-level gate worth spelling out, because it is the kind of thing a
graph tool would rather not admit. Calls through a receiver, the x.method() form, are
resolved by bare name in the linker, while method entities are keyed by their qualified
name. So a method’s incoming call edges are frequently dropped. An empty reference list for
a method is therefore not an authoritative “nothing uses this”, and the reason string says
so rather than leaving the verdict clean.
That is a published weakness sitting inside the product’s own answers. It is also the only honest thing to do with it, because the alternative is a caller reading “safe to delete” off an incomplete call graph and being right most of the time.
Most of the time is the problem. The failure is asymmetric. A caveat costs a round trip. A false absence costs an outage.
Gaps accumulate, and the order is load bearing
When several gates apply, the reasons are composed rather than overwritten, limiting factor first.
The comment in the source explains why, and the explanation is the useful part: a correct verdict sitting beside an unrelated reason teaches readers to skip the reason. Get that wrong twice and the field becomes decoration. So every gate routes through one function instead of assigning the reason directly, which is how a cross-repository note once came to overwrite the reason an absence was actually limited by, leaving a correct verdict beside an explanation that had nothing to do with it.
Every answer carries a claim, not only the empty ones
This is the change I would keep if I could keep only one.
The object used to appear only when a collection came back empty, on the reasoning that there is no absence to qualify when there are rows. The consequence showed up in a real session. A neighborhood tool walked two inbound edges of an entity and presented them with no qualifier at all, while a reference tool refused to certify the same entity over the same edges. Both were right about their own scope. Whichever one you reached for first decided what you believed.
So the split moved from “is the answer empty” to “what is being claimed”.
trust is the response’s one verdict either way, authoritative or inconclusive, and it
answers whether the rows you got are the whole set. safe_to_conclude_absent answers the
narrower question, and it is false whenever rows came back, because no absence is being
claimed there and a reader must never be able to read one out of a populated answer.
Where the honest miss comes from
None of this works if the tool quietly falls back to text search when the graph cannot answer.
That is a rule in Kin rather than a preference. Answer paths do not read raw files. Ingestion, import, reconcile, migration and projection do, because turning files into graph truth and back is their job. Once graph truth exists, a runtime query path either answers from it or reports the gap.
A guard script scans those paths for filesystem read, existence and traversal primitives and fails when one appears outside an allowlist, where every entry carries a file, a reason, a named owner and an expiration date. A separate harness copies the tree, poisons every module the guard claims to cover, and requires the guard to fail and to name the file every time.
The reason to hold that line is the same reason negative exists. A silent fallback and a
real answer are indistinguishable to the caller, and this caller cannot ask a follow-up
about where the answer came from before acting on it. When two outcomes are
indistinguishable, the honest one has to be structural.
What this asks of tool authors
If you are designing tools for an agent to call, the design question is not what your happy path returns. It is what your empty result asserts, and whether you can back it.
Three choices carried most of the value here.
One shape for every tool, so a caller learns it once. One code path that attaches it, folded
into the single annotation point rather than added per handler, so two runtimes cannot drift
apart on the contract. And a rule that anything unobserved is null rather than a default,
because a defaulted false is a claim you did not make.
Review in Kin is report-only today. It advises and it never blocks. Kin is early and it is an alpha, and it says so when it cannot answer, which is the whole subject of this post.
AI writes code. Kin proves what changed. Proves means the record, never a verdict on whether the code is good. And a record that cannot say “I do not know” is not a record.