Skip to content

path_between: directed call chains, connector-free related paths, honest misses (#17, step 2) - #89

Merged
gangtao merged 1 commit into
feat/ingest-kinds-and-namesfrom
feat/call-path-traversal
Sep 21, 2026
Merged

gangtao merged 1 commit into
feat/ingest-kinds-and-namesfrom
feat/call-path-traversal

Conversation

@gangtao

@gangtao gangtao commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Step 2 of the plan in #17. Stacked on #88 (base branch feat/ingest-kinds-and-names) because the connector filter relies on the new symbol kind; merge #88 first and GitHub retargets this to main.

What was wrong (measured on proton-enterprise@v3.3.1)

path_between walked undirected over every relation and cut each hop to sorted(frontier)[:200]:

  • the cut is arbitrary (by id) and hid a real depth-4 path entirely — the capped search visits ~9% of the graph;
  • uncapped, the "path" was HTTPHandler.cpp →imports Context ←imports NativeLog_Net.cpp — two files sharing an include, routed through hubs like String (degree 5,226). Not a call path.

What it does now

  • Bidirectional BFS with a total-visited budget (MAX_PATH_VISITED = 5000, IN lists chunked) — no per-hop cut; depth cap raised 6 → 12 (the two-sided search makes that cheap).
  • mode="calls" — directed, calls / indirect_call only; tries A→B then B→A; the chain is always listed caller-first and direction (a_to_b / b_to_a) says which way it really runs, so "B calls A" is never reported as "A calls B".
  • mode="related" — undirected over all relations, but never routed through a connector (kind symbol / file): they can be an endpoint, never the bridge, and the two search sides meeting at one is not a path.
  • mode="auto" (default) — calls first, then related, and the result says which it is.
  • Returns a dict {found, mode, direction, path, note}. When nothing is found: note says why (depth / budget / no connection) and callees_of_a / callers_of_b give the next hops — the extracted C++ call graph is incomplete (virtual dispatch, untyped member calls), so "null" was a dead end for the agent.
  • Agent prompt: a related result must not be described as a call path; when a chain breaks, walk neighbors(rels=["calls"]) and read_source the body at the break to bridge the virtual call. MCP and agent tools expose mode.

On the real graph (local corpus, read-only)

Query Result
execute → buildChainImpl calls, a_to_b, 0.01 s
buildChainImpl → execute (asked backwards) calls, b_to_a, listed execute → buildChainImpl, 1.2 s
HTTP processQuery → NativeLog::processProduceRequest, mode="calls" (the #17 question) not found in 0.7 s, with callees_of_a = [executeQuery, …] and callers_of_b = [NativeLog::doProcessRequest] — the honest answer, plus where to continue

⚠️ Shape change

path_between returned a list or null; it now returns an object. In-repo callers (MCP tool, agent tool, tests) are updated; the web UI / REST API don't use it.

Testing

  • New tests (RED→GREEN, real DB): directed chain; reversed endpoints; related fallback labelled "not a call chain"; mode="calls" doesn't fall back and returns hints; unknown endpoint; depth clamp; 250-wide fan-out whose needed callee sorts last (the old cut would drop it); connector never bridges, including when both sides reach it (this one caught a real bug during development — found on real data, then reproduced in the test); visited budget.
  • MCP scope test tightened: an out-of-scope endpoint yields found: false and neither its id nor its name appears anywhere in the response, hints included.
  • pytest: 326 passed, 10 skipped, 1 failed — tests/test_ingest.py::test_ingest_repo_passes_extraction_and_backend, environment-dependent, failing on main too.
  • Note: the local DB still holds the old kinds (classes stored as file), so related mode is only fully meaningful after re-ingesting with ingest: derive code kinds and class-qualified names from graph structure (#17, step 1) #88.

Next: step 3 — synthetic overrides edges from inherits + method, measured against call-reach (median 3 / p90 17 today) and a small set of golden paths.

🤖 Generated with Claude Code

The old path_between walked UNDIRECTED over EVERY relation and cut each hop to
sorted(frontier)[:200]. On proton-enterprise that (a) hid a depth-4 path entirely
(the arbitrary cut visits ~9% of the graph) and (b) when uncapped returned
shared-include junk -- HTTPHandler.cpp -imports-> Context <-imports- NativeLog.cpp
-- through hubs like String (degree 5,226). Neither is a call path.

Now: a bidirectional BFS with a total-visited budget (no per-hop cut).
mode='calls' follows only calls/indirect_call, directed, trying A->B then B->A,
always listing the chain caller-first with 'direction' saying which way it runs.
mode='related' is undirected over all relations but never routes THROUGH a
connector node (kind symbol/file) -- they may be endpoints, never bridges, and
the two search sides meeting AT one is not a path. mode='auto' (default) tries
calls then related and says which it found. Returns a dict; when nothing is
found it explains why (depth / budget / no connection) and returns the direct
callees of A and callers of B, because the extracted C++ call graph is
incomplete and the agent needs a next step, not 'null'.

Measured on the real graph: execute -> buildChainImpl found in 0.01s; asked
backwards, reported as b_to_a; the #17 question (HTTP processQuery -> NativeLog
produce) returns an honest 'no call chain' with executeQuery and
NativeLog::doProcessRequest as next hops. Agent prompt now tells the model to
bridge a broken chain with neighbors + read_source instead of stopping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gangtao
gangtao merged commit afaeef1 into feat/ingest-kinds-and-names Sep 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant