Skip to content

graph scope stopword pollution, undocumented unresolved_refs fallback, and benchmark vs. grep gap (with repro + eval data) #115

Description

@emmahyde

Ran mex against a large (1600+ file) real-world Rails codebase with a Ruby extractor I contributed (Prism-based). Found three related issues while comparing mex against plain grep on real lookup tasks, with a blind eval to back it up.

1. graph scope's exact-match scoring has no stopword filtering

taskTokens() in src/graph/scope.ts splits the free-text task string on non-identifier chars and gives every token >=2 chars a flat 1.0 "exact-name-match" boost if it happens to collide with a real (often trivial) identifier in the repo — e.g. task phrasing containing "on", "to", "of", "up" collided with Ruby DSL params/block args literally named that, burying the actually-relevant symbol under stopword noise.

Repro: mex graph scope "find every call site that invokes mark_failed! on a model" returned on (score 1.0) x5 as top hits; the real target never appeared in the top 10.

I have a fix (stopword list + filter, with a regression test) ready as a diff/PR if useful.

2. who-calls/scope can't find dynamically-generated methods, with no documented recovery path

Ruby methods generated via metaprogramming (e.g. a state-machine DSL block) have real call sites but no literal def, so no resolved node exists. who-calls <method> returns TARGET_NOT_FOUND with no next step. The call sites are captured, just in the unresolved_refs table, which isn't exposed by any CLI command — recovering them required hand-writing SQL against internal schema that the contributor docs say requires a core/discuss-first issue to even touch.

Would who-calls falling through to unresolved_refs when no resolved node exists (clearly labeled as unresolved/dynamic) be in scope for a lighter-weight contribution?

3. The benchmark doesn't compare against grep, and the README implies it does

evaluate/README.md says outright:

The current harness does not compare an agent with the graph against the same agent using only Read/Grep/Glob. Therefore these results do not support an end-to-end graph-vs-no-graph token-savings claim.

That's accurate and appreciated, but the top-level README's "Results" table (10.74x, 916x) reads as exactly that claim to a new user, and it's measured against a favorable synthetic baseline ("grep's top-3 whole files") rather than a real grep-only agent run.

Our eval

Six blind subagents (fresh context each, no shared answers, no knowledge of the "correct" result), split into a grep-only condition and a mex condition, on the same three real questions against our codebase. Real reported token/tool-call/time usage per agent:

Task Condition Tokens Tool calls Time Correct?
Find every class defining build_commands! (excl. tests) grep 36,719 1 10.3s 19/19
mex (graph query/sqlite) 61,736 2 27.5s 19/19
Find every call site of .mark_failed! in app/ grep 36,700 1 8.1s 15/23
mex (who-calls + unresolved_refs) 63,481 5 46.3s 23/23
Trace CoderSession's superclass chain to the gem boundary grep 47,714 4 63.1s correct
mex (graph scope + sqlite) 62,722 4 45.1s correct

Takeaways:

  • mex ran 1.3-1.7x grep's tokens on every task, and matched or lost on wall time too.
  • mex tied grep on 2/3 tasks (no benefit for the extra cost) and won decisively on 1/3: grep's \.mark_failed! regex missed 8 real call sites that invoke the method without an explicit dot-receiver (bare self-calls inside the model classes); mex's AST-based edges don't have that blind spot. That's a real, non-obvious correctness edge — a naive regex can produce a confidently wrong "15 results, looks complete" answer that nothing flags.
  • Getting that win required falling through to raw sqlite against unresolved_refs (issue Expand KNOWN_RUNTIMES allowlist in dependency checker #2 above) — not something a first-time CLI user would discover from who-calls alone returning TARGET_NOT_FOUND.

Net: mex earns its cost specifically on call-site enumeration where call syntax varies in ways a regex can't enumerate up front. For straightforward name/definition lookups and short inheritance walks, grep was cheaper and just as correct in this test. Worth either softening the README's headline claim or running the real 3-arm comparison the eval docs already call out as missing.

Happy to share the Ruby extractor, the scope.ts stopword fix, and the raw eval transcripts if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions