Skip to content

fix(path): resolve a Type.Member endpoint to the member, not the type (#4884) - #4893

Closed
SyedFahad7 wants to merge 3 commits into
Graphify-Labs:v8from
SyedFahad7:cursor/path-type-method-resolution-cb7d
Closed

SyedFahad7 wants to merge 3 commits into
Graphify-Labs:v8from
SyedFahad7:cursor/path-type-method-resolution-cb7d

Conversation

@SyedFahad7

@SyedFahad7 SyedFahad7 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #4884.

When graphify path got an endpoint in Type.Method form, it resolved it to the class and printed a misleading route. A member's label is .Build(), so PdfReport.Build matched none of _find_node_tiers' exact tiers. _resolve_path_endpoint then fell through to token scoring, which picked the PdfReport class. With the issue's Report.cs:

  • path "Exporter.Run" "PdfReport.Build" printed Exporter <--contains-- Report.cs --contains--> PdfReport, a route between two classes through the file, presented as if it joined the two methods.
  • path "Run" "PdfReport.Build" found no path, because no edge leads from a method back up to a class.

The change:

After the fix, both commands from the issue print .Run() --calls--> .Build() --dispatches_to--> .Build() (2 hops). IReport.Build and CsvReport.Build() resolve to their own methods. PdfReport.Missing is refused with no type 'PdfReport' declares 'Missing'.

This does not depend on the label change in #4883. It matches on the label as it is stored today (.Build()), and would keep matching a bare Build() or Build label too.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

The invariant is the README contract: "a name that matches several nodes is refused with the candidates listed rather than guessed". A Type.Member endpoint now resolves to that member or is refused. It never resolves silently to the type.

What stays the same:

  • A node whose label literally equals the dotted query still wins, because the new lookup is skipped when _label_has_literal_exact_match holds.
  • pkg.mod.func still resolves through the module file first.
  • A dotted query whose owner is not a type with members still goes to ordinary scoring.
  • No persisted state, cache or extraction output changes. This is lookup-only code in serve.py.

Limitations / unsupported cases:

  • A namespace qualifier (Acme.Io.Outer.Go) cannot narrow the match. The file owns the type, not the namespace node, so there is no edge to walk. Such a query is refused, and the refusal lists the bare type's same-named members with their ids for a retry.

  • Only .-separated forms are handled. Type::Member is still read as path::symbol.

  • Read the CONTRIBUTING.md guide.

  • Reproduced the issue and identified the invariant.

  • Made the smallest fix necessary.

  • Added a regression test (if bug fix) or isolated boundary test.

  • Kept the PR description synchronized with the final implementation.

  • Documented any limitations / unsupported cases explicitly.

How was this tested?

tests/test_path_type_member.py extracts the issue's Report.cs with the real C# extractor and runs the CLI and the MCP shortest_path tool against it. It also uses synthetic graphs for:

  • the same member in two files
  • same-type overloads
  • nested types and edge direction
  • a namespace qualifier
  • a missing member
  • a literal dotted label
  • an owner that is not a type

Without the serve.py change, 11 of the 12 tests fail. The one that still passes guards the unchanged fall-through behaviour.

uv sync --all-extras --frozen
graphify update .   # in a scratch dir holding the issue's Report.cs, then the two `graphify path` commands above
uv run --frozen pytest tests/test_path_type_member.py -q        # 12 passed (11 failed with serve.py reverted)
uv run --frozen pytest tests/test_path_type_member.py tests/test_path_cli.py tests/test_serve.py tests/test_lookup_exit_codes.py -q   # 227 passed
uv run --frozen pytest tests/ -q --tb=short                     # 8978 passed, 17 skipped, 1 xfailed
uv run --frozen ruff check .                                    # All checks passed
uv run --frozen pyright graphify/serve.py tests/test_path_type_member.py   # 67 errors, same as v8 before the change; none in the new code

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (No skill fragments touched.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.
Open in Web Open in Cursor 

Update (follow-up commit c3ad177, addressing bot review)

  • Lookup order: the Type.Member lookup now runs only after exact node id, label and source-path matches find nothing, and never for a query that names a source file (utils.py was previously refused; config.json previously resolved to a Config.Json() member).
  • Direction: owner/member direction comes from _src/_tgt edge markers, else the stored arc in a directed graph; undirected edges without markers are skipped.
  • Limitations: a query that is also a file name resolves to the file (the member stays reachable by node id); unmarked undirected edges are not used for member lookup.
  • Tests: +5 (utils.py endpoint, config.json vs Config.Json(), exact dotted id, undirected with/without markers); tests/test_path_type_member.py 17 passed; full suite 8983 passed, 17 skipped, 1 xfailed.

cursoragent and others added 2 commits October 30, 2026 21:11
…Graphify-Labs#4884)

A member's label is `.Build()`, so `PdfReport.Build` hit no exact tier and
token scoring picked the `PdfReport` class. `path "Exporter.Run"
"PdfReport.Build"` then printed a class-to-class route through the file as if
it joined the two methods, and `path "Run" "PdfReport.Build"` found nothing,
since no edge leads from a method back up to a class.

`_find_node_tiers` now resolves `Type.Member` (and `Outer.Inner.Member`)
through the type's `method` / `defines` / `contains` edges, after the dotted
Python form and only when no node is literally labelled with the query. The
hits form the exact tier, so `path`, `explain`, get_node and `query` share
them, and several hits are refused with their ids instead of guessed.

A `path` endpoint whose type exists but declares no such member fails closed,
like a `file::symbol` miss (Graphify-Labs#4264), instead of scoring its way to the type. A
namespace-qualified name is refused the same way, listing the bare type's
same-named members, since the file rather than the namespace owns the type.

Fixes Graphify-Labs#4884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
…#4884)

Extracts the issue's Report.cs and checks that `Exporter.Run` / `Run` reach
`PdfReport.Build` through the calls and dispatches_to edges, never through the
file's contains edges, for the CLI and the MCP shortest_path tool. Synthetic
graphs cover a member in two files, same-type overloads, nested types,
direction, a namespace qualifier, a missing member, a literal dotted label,
and an owner that is not a type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @SyedFahad7. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Oct 30, 2026

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 3 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 2 changed function(s) (2 sampled, not proven).

Formal verification. PR-changed functions: 2/2 verified (0 proven, 2 may-equivalent, 0 distinguished) · 0 not verified.


Graphify review — findings

Adds Type.Member (and Outer.Inner.Member, Type.Member()) endpoint resolution, so path and explain walk the declaring type's method/defines/contains edges to the member instead of scoring onto the type itself. Several same-named members, such as one per implementer or overloads, come back as the exact tier and are refused with their ids rather than guessed. Namespace qualifiers match nothing. When owner direction can't be told apart in an undirected graph without _src/_tgt markers, _membership_neighbors counts every membership neighbour both ways, which can turn members into owners. Fails closed in _resolve_path_endpoint when a type exists but declares no such member, returning a not-found message that lists same-named members of the bare type for retry instead of routing to the class. This refusal runs before ordinary scoring, so dotted endpoints like file names (utils.py) or exact dotted ids can be caught by it rather than reaching their normal match.

Worth a look

  • Undirected graphs without markers turn members into owners, causing false refusals and wrong matches — graphify/serve.py:1670 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • File-name path endpoints like utils.py can be refused as missing Type.Member queries — graphify/serve.py:2158 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Member-miss refusal shadows exact dotted endpoint IDs — graphify/serve.py:2156 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 710 functions depend on the 195 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: main() — 116 callers, 3 callees
  • worse: _resolve_path_endpoint() — 10 callers, 6 callees
  • worse: _resolve_single_node() — 18 callers, 3 callees
  • worse: _shortest_path_text() — 15 callers, 3 callees
  • worse: _find_node_tiers() — 5 callers, 8 callees

Verification — 710 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 598 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

18 of 470 test file(s) selected (4%) via static blast radius.

  • tests/test_benchmark.py — impact
  • tests/test_benchmark_raw_graph.py — impact
  • tests/test_benchmark_real_query.py — impact
  • tests/test_explain_same_file_tie.py — impact
  • tests/test_extract.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_mcp_graph_view_cache.py — impact
  • tests/test_path_cli.py — impact
  • tests/test_path_type_member.py — impact, changed-test
  • tests/test_query_induced_edges.py — impact
  • tests/test_query_mcp_direction.py — impact
  • tests/test_query_names_its_graph.py — impact
  • tests/test_query_seed_order.py — impact
  • tests/test_query_seeds.py — impact
  • tests/test_serve.py — impact
  • tests/test_serve_http.py — impact
  • tests/test_terraform.py — impact
  • tests/test_truncation_full_answer.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

No difference found (not proven): No behavior difference found in \_find\_node\_tiers (not a proof).

The verifier ran both versions of \_find\_node\_tiers on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_resolve\_path\_endpoint (not a proof).

The verifier ran both versions of \_resolve\_path\_endpoint on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 1 grounded finding(s) anchored inline below; 4 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py
return msg


def _find_node_tiers(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _find_node_tiers()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

…ing (Graphify-Labs#4884)

Review follow-up on Graphify-Labs#4893.

The `Type.Member` lookup ran ahead of the ordinary tiers, and its not-found
refusal only knew that some type of that name existed. A file name has the
same shape, so `config.json` resolved to a `Config.Json()` method, and
`utils.py` was refused as "no type 'utils' declares 'py'" whenever a `Utils`
type existed and no node carried the bare file name as its label.

The member lookup now runs after the ordinary tiers, only when no node's own
id, label or source path matched exactly, and never for a query that names a
source file (case-insensitive, like the source-path tier). The refusal skips
the same queries, so it only fires for a genuine missing member.

Membership direction now comes from the edge's `_src`/`_tgt` markers, else
the stored arc of a directed graph. An undirected edge without markers is
skipped instead of counted both ways, which let a member pose as its type's
owner: `.Report()` "owned" `Builder`, and `Go.Missing` was refused because
`.Go()` looked like a type with members. The CLI loaders that read a graph
undirected stamp the markers on every link, so they keep resolving.

Tests: a `utils.py` endpoint, `config.json` next to `Config.Json()`, an exact
dotted node id, and undirected graphs with and without markers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
safishamsi pushed a commit that referenced this pull request Oct 30, 2026
…ing (#4884)

Review follow-up on #4893.

The `Type.Member` lookup ran ahead of the ordinary tiers, and its not-found
refusal only knew that some type of that name existed. A file name has the
same shape, so `config.json` resolved to a `Config.Json()` method, and
`utils.py` was refused as "no type 'utils' declares 'py'" whenever a `Utils`
type existed and no node carried the bare file name as its label.

The member lookup now runs after the ordinary tiers, only when no node's own
id, label or source path matched exactly, and never for a query that names a
source file (case-insensitive, like the source-path tier). The refusal skips
the same queries, so it only fires for a genuine missing member.

Membership direction now comes from the edge's `_src`/`_tgt` markers, else
the stored arc of a directed graph. An undirected edge without markers is
skipped instead of counted both ways, which let a member pose as its type's
owner: `.Report()` "owned" `Builder`, and `Go.Missing` was refused because
`.Go()` looked like a type with members. The CLI loaders that read a graph
undirected stamp the markers on every link, so they keep resolving.

Tests: a `utils.py` endpoint, `config.json` next to `Config.Json()`, an exact
dotted node id, and undirected graphs with and without markers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Syed Fahad <sfahad2904@gmail.com>
(cherry picked from commit c3ad177)
safishamsi added a commit that referenced this pull request Oct 30, 2026
…els, path weakest-hop confidence, Type.Member path endpoints

Covers the SyedFahad7 batch: #4891 and #4889 merged on GitHub; #4892 and
#4893 landed via authorship-preserving cherry-pick (their CI matrix was
gated pending first-contributor approval, so brought in through the local
gate instead). Credit by GitHub PR-author login @SyedFahad7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Member

Landed in v0.9.98, now on PyPI (pip install graphifyy==0.9.98). Landed via cherry-pick (your CI matrix was gated). A Type.Member path endpoint resolves to the member, not the containing type. Thanks for the fix, @SyedFahad7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor Pull request from a returning community contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: graphify path with a Type.Method name resolves to the class, returning a misleading path

3 participants