You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
query: relational-intent verbs ("calls"/"uses") survive stopwording, prefix-match unrelated identifiers, and the #1445 per-term guarantee seats the junk match as a seed #2507
In a relational natural-language query — "Who calls X?", "what code uses X?" — the relational verb survives stopword filtering, prefix-matches an unrelated camelCase identifier (calls → .callStoreWithAmount()), and the per-term guaranteed-seed mechanism from #1445 then seats that junk match as a BFS root regardless of how well the real identifier scored. The traversal floods the token budget with the junk seed's neighborhood (typically test files), and the answer contains zero of the correct nodes while looking authoritative.
The sharp part: _infer_context_filtersalready recognizes the verb as relational intent — the same query emits Context: call (heuristic) via _CONTEXT_HINTS (serve.py:725) — but that recognition only filters traversal edges. It never reaches _query_terms or _pick_seeds, so one word is simultaneously treated as an edge-filter directive and as an identifier competing for (and winning) a guaranteed seed slot.
Measured failure (real corpus)
Observed on a ~46k-node / ~139k-edge graph of a production PHP monolith (graphifyy 0.9.28 baseline; identically on a 0.9.34-based build with richer calls edges — the failure predates and survives both). Ground truth, grep-verified: ChargeCustomerService has exactly 3 production callers.
3/3 present but buried in 42 shown of 147 nodes, ~1,617 tok
"what code uses ChargeCustomerService to charge a customer"
5 seeds incl. .charge(), coder.md, a test method
0/3; 2,307-node explosion, 52 shown, ~1,600 tok
All three phrasings are the same question. The only phrasing that gets close ("callers of …") still spends a seed slot on a test method that exact-matched "callers", and buries the answer. Note .callStoreWithAmount() is genuinely unrelated to ChargeCustomerService — it lives in a different subsystem; its only connection to the query is that its normalized label starts with the letters calls.
Minimal corpus-agnostic repro sketch
Any corpus with (a) a service, (b) a few real callers, and (c) one unrelated method whose camelCase name begins with a relational verb reproduces this:
# billing/service.pyclassChargeCustomerService:
defhandle(self, amount): ...
# billing/cancellation_fee.py — real caller 1classChargeCustomerCancellationFee:
defhandle(self):
ChargeCustomerService().handle(self.fee)
# jobs/billing_charge.py — real caller 2classBillingCharge:
defrun(self):
ChargeCustomerService().handle(self.amount)
# billing/balance.py — real caller 3classBalanceCustomerAccountService:
defsettle(self):
ChargeCustomerService().handle(self.delta)
# store/checkout.py — the decoy: unrelated, but norm label# "callstorewithamount" prefix-matches the query term "calls"classStoreCheckout:
defcallStoreWithAmount(self, amount): ...
# tests/test_store_checkout.py — a busy test neighborhood# around the decoy (several test methods exercising StoreCheckout)
graphify query "Who calls ChargeCustomerService?"
Expected: the 3 callers. Actual: seeds include .callStoreWithAmount(), and BFS depth-2 from it pulls in the test_store_checkout.py neighborhood. The larger the decoy's test neighborhood relative to the budget, the more completely the real answer is displaced — on the real corpus above it reached 0/3.
callStore…, callsRemaining, useSWR, usesTrait, getUserCallsign — verb-prefixed camelCase identifiers are idiomatic in most codebases, so nearly every corpus contains a decoy for the common relational verbs.
Mechanism walk (upstream main, graphify/serve.py)
_query_terms (serve.py:253) drops _QUERY_STOPWORDS (serve.py:221). "who" is a stopword; "calls" is not — nor are "uses", "callers", "invokes", "imports". Terms become ['calls', 'chargecustomerservice'].
Scoring (_score_query / _score_nodes): the decoy's normalized label callstorewithamount starts with calls, earning _PREFIX_MATCH_BONUS = 100.0 (serve.py:276) × IDF (serve.py:281) — and since few labels begin with calls, its IDF is high. The query: single generic term exact-matching a short leaf label hijacks seed selection in multi-term queries #1602 coverage scaling dampens this (the decoy matches 1 of 2 terms) but does not zero it, and it does not need to outscore anything to win — see step 4.
_score_query records the per-term winner: the decoy becomes best_seed_by_term['calls'] (serve.py:603-606) because it is the top scorer for that term in isolation.
_pick_seeds (serve.py:633): the main gap-ratio loop correctly seeds the real identifier — and would correctly exclude the decoy (its coverage-scaled score is far below the exact-tier match on ChargeCustomerService). But the per-term guarantee loop (serve.py:702-721, added for BFS path drift on vague Chinese queries: 40-node corpus, 12 unrelated nodes returned for "rate_limit_check 怎么用?" #1445) then appends best_seed_by_term['calls'] unconditionally: the guarantee exists precisely to bypass the gap cutoff, so no score threshold can save the query from a term that should never have been a term.
BFS explores depth-2 from both seeds; the decoy's test neighborhood competes with (and at ~2,000-token budget, displaces) the real callers in _subgraph_to_text.
Meanwhile _infer_context_filters (serve.py:789) matched "calls" against _CONTEXT_HINTS (serve.py:725: ("call", ("call", "calls", "called", "invoke", "invokes", "invoked"))) and emitted Context: call (heuristic) — the pipeline classified the word as a relation, then let it compete as an identifier anyway.
So each stage behaves as designed; the failure is the composition: stopwording says "calls" is content, context inference says "calls" is intent, and the per-term guarantee makes the disagreement unconditionally expensive.
This challenges a deliberate design point — acknowledging it
PR #1597 (the original English stopword design, closed in favor of the merged variant; its semantics carried into the fix commits for #1900) explicitly updated test_query_terms_strips_search_punctuation to pin "what calls extract?" → ['calls', 'extract'] — "calls" was affirmatively kept as a content term, not overlooked. That choice is defensible for lexical safety: calls, callers, uses can be legitimate identifiers in some corpus, and a blanket stopword would make them unreachable via query.
But for relational queries the verb carries traversal intent (which edge context to follow — exactly what _CONTEXT_HINTS extracts from it), not lexical identity. Its lexical value in a multi-term query is almost always negative: the real identifier is present as another term, and the verb's own matches are coincidental prefix/substring hits on verb-prefixed camelCase names. "Who calls X?" is arguably the single most common question an AI agent asks a code graph, and it is precisely the phrasing this composition breaks.
Relationship to existing issues (complementary, not duplicate)
PR fix: bound per-term query seed recovery #2113 (open) — bounds how many seeds per-term recovery can add and prefers multi-term-coverage winners. Also complementary: it caps the count of guarantee-added seeds but a verb's singleton winner can still occupy one of the bounded slots.
Demote intent-consumed terms from seeding. When _resolve_context_filters heuristically consumes a token as relational intent (it already computes exactly this set), exclude that token from the seed-term list in _query_terms/_score_query — with a fallback (mirroring the existing all-stopword fallback) so a query consisting only of hint words, or an explicit explain calls-style lookup, still resolves. A corpus-legit identifier named calls stays reachable via explain/path and via queries that don't also trigger the heuristic differently.
Narrower: drop only the per-term guarantee for intent-consumed terms. Keep the verb in scoring — it can still win a seat on merit through the gap-ratio window (protecting corpora where calls/uses are real identifiers) — but exclude it from best_seed_by_term, so query: single generic term exact-matching a short leaf label hijacks seed selection in multi-term queries #1602's coverage scaling actually gets to decide. This is the minimal change to the composition failure: every stage keeps its current behavior except the unconditional bypass.
Require identifier-shaped evidence for hint-word terms. A term that matches _CONTEXT_HINTS only earns exact-tier credit (never prefix/substring tiers), or only competes as a seed term when the query contains no other identifier-shaped term. Prefix hits on generic verbs are the specific vector here; exact label matches (calls node literally labelled calls) are plausibly intentional.
Happy to provide full query outputs / score traces from the 46k-node corpus, build a synthetic regression fixture along the sketch above, or test a branch.
Summary
In a relational natural-language query — "Who calls X?", "what code uses X?" — the relational verb survives stopword filtering, prefix-matches an unrelated camelCase identifier (
calls→.callStoreWithAmount()), and the per-term guaranteed-seed mechanism from #1445 then seats that junk match as a BFS root regardless of how well the real identifier scored. The traversal floods the token budget with the junk seed's neighborhood (typically test files), and the answer contains zero of the correct nodes while looking authoritative.The sharp part:
_infer_context_filtersalready recognizes the verb as relational intent — the same query emitsContext: call (heuristic)via_CONTEXT_HINTS(serve.py:725) — but that recognition only filters traversal edges. It never reaches_query_termsor_pick_seeds, so one word is simultaneously treated as an edge-filter directive and as an identifier competing for (and winning) a guaranteed seed slot.Measured failure (real corpus)
Observed on a ~46k-node / ~139k-edge graph of a production PHP monolith (graphifyy 0.9.28 baseline; identically on a 0.9.34-based build with richer
callsedges — the failure predates and survives both). Ground truth, grep-verified:ChargeCustomerServicehas exactly 3 production callers.['ChargeCustomerService', '.callStoreWithAmount()']+Context: call (heuristic)['ChargeCustomerService', '.it_ignores_initiator_for_non_ops_callers()'].charge(),coder.md, a test methodAll three phrasings are the same question. The only phrasing that gets close ("callers of …") still spends a seed slot on a test method that exact-matched "callers", and buries the answer. Note
.callStoreWithAmount()is genuinely unrelated toChargeCustomerService— it lives in a different subsystem; its only connection to the query is that its normalized label starts with the letterscalls.Minimal corpus-agnostic repro sketch
Any corpus with (a) a service, (b) a few real callers, and (c) one unrelated method whose camelCase name begins with a relational verb reproduces this:
Expected: the 3 callers. Actual: seeds include
.callStoreWithAmount(), and BFS depth-2 from it pulls in thetest_store_checkout.pyneighborhood. The larger the decoy's test neighborhood relative to the budget, the more completely the real answer is displaced — on the real corpus above it reached 0/3.callStore…,callsRemaining,useSWR,usesTrait,getUserCallsign— verb-prefixed camelCase identifiers are idiomatic in most codebases, so nearly every corpus contains a decoy for the common relational verbs.Mechanism walk (upstream
main,graphify/serve.py)_query_terms(serve.py:253) drops_QUERY_STOPWORDS(serve.py:221). "who" is a stopword; "calls" is not — nor are "uses", "callers", "invokes", "imports". Terms become['calls', 'chargecustomerservice']._score_query/_score_nodes): the decoy's normalized labelcallstorewithamountstarts withcalls, earning_PREFIX_MATCH_BONUS = 100.0(serve.py:276) × IDF (serve.py:281) — and since few labels begin withcalls, its IDF is high. The query: single generic term exact-matching a short leaf label hijacks seed selection in multi-term queries #1602 coverage scaling dampens this (the decoy matches 1 of 2 terms) but does not zero it, and it does not need to outscore anything to win — see step 4._score_queryrecords the per-term winner: the decoy becomesbest_seed_by_term['calls'](serve.py:603-606) because it is the top scorer for that term in isolation._pick_seeds(serve.py:633): the main gap-ratio loop correctly seeds the real identifier — and would correctly exclude the decoy (its coverage-scaled score is far below the exact-tier match onChargeCustomerService). But the per-term guarantee loop (serve.py:702-721, added for BFS path drift on vague Chinese queries: 40-node corpus, 12 unrelated nodes returned for "rate_limit_check 怎么用?" #1445) then appendsbest_seed_by_term['calls']unconditionally: the guarantee exists precisely to bypass the gap cutoff, so no score threshold can save the query from a term that should never have been a term._subgraph_to_text._infer_context_filters(serve.py:789) matched "calls" against_CONTEXT_HINTS(serve.py:725:("call", ("call", "calls", "called", "invoke", "invokes", "invoked"))) and emittedContext: call (heuristic)— the pipeline classified the word as a relation, then let it compete as an identifier anyway.So each stage behaves as designed; the failure is the composition: stopwording says "calls" is content, context inference says "calls" is intent, and the per-term guarantee makes the disagreement unconditionally expensive.
This challenges a deliberate design point — acknowledging it
PR #1597 (the original English stopword design, closed in favor of the merged variant; its semantics carried into the fix commits for #1900) explicitly updated
test_query_terms_strips_search_punctuationto pin"what calls extract?" → ['calls', 'extract']— "calls" was affirmatively kept as a content term, not overlooked. That choice is defensible for lexical safety:calls,callers,usescan be legitimate identifiers in some corpus, and a blanket stopword would make them unreachable viaquery.But for relational queries the verb carries traversal intent (which edge context to follow — exactly what
_CONTEXT_HINTSextracts from it), not lexical identity. Its lexical value in a multi-term query is almost always negative: the real identifier is present as another term, and the verb's own matches are coincidental prefix/substring hits on verb-prefixed camelCase names. "Who calls X?" is arguably the single most common question an AI agent asks a code graph, and it is precisely the phrasing this composition breaks.Relationship to existing issues (complementary, not duplicate)
_query_terms) is a natural home for part of the fix.Candidate directions (not prescribing one)
_resolve_context_filtersheuristically consumes a token as relational intent (it already computes exactly this set), exclude that token from the seed-term list in_query_terms/_score_query— with a fallback (mirroring the existing all-stopword fallback) so a query consisting only of hint words, or an explicitexplain calls-style lookup, still resolves. A corpus-legit identifier namedcallsstays reachable viaexplain/pathand via queries that don't also trigger the heuristic differently.calls/usesare real identifiers) — but exclude it frombest_seed_by_term, so query: single generic term exact-matching a short leaf label hijacks seed selection in multi-term queries #1602's coverage scaling actually gets to decide. This is the minimal change to the composition failure: every stage keeps its current behavior except the unconditional bypass._CONTEXT_HINTSonly earns exact-tier credit (never prefix/substring tiers), or only competes as a seed term when the query contains no other identifier-shaped term. Prefix hits on generic verbs are the specific vector here; exact label matches (callsnode literally labelledcalls) are plausibly intentional.Happy to provide full query outputs / score traces from the 46k-node corpus, build a synthetic regression fixture along the sketch above, or test a branch.