Skip to content

feat(affected): rank results by distance, then link strength, then path - #5069

Open
rohit-jsfreaky wants to merge 2 commits into
Graphify-Labs:v8from
rohit-jsfreaky:feat/affected-ranking
Open

rohit-jsfreaky wants to merge 2 commits into
Graphify-Labs:v8from
rohit-jsfreaky:feat/affected-ranking

Conversation

@rohit-jsfreaky

Copy link
Copy Markdown
Contributor

Closes #5067

What does this PR do?

affected returns the same hits as before, sorted so the most likely impacted come first:

  1. distance from the seed (depth), as before;
  2. how many edges (over the traversed relations) link the hit's file to the seed's file, or to the files one step closer; more first;
  3. path, then line, then node id, so ties come out the same every run.

Hits of one file stay together within a depth. The set and depth never change; only the order does.

$ graphify affected core.py      # v8                 # this PR
- aaa.py [imports] aaa.py:L1                          - zzz.py [imports_from] zzz.py:L1
- zzz.py [imports_from] zzz.py:L1                     - sync() [calls] zzz.py:L4
- reset() [calls] zzz.py:L8                           - reset() [calls] zzz.py:L8
- sync() [calls] zzz.py:L4                            - aaa.py [imports] aaa.py:L1
- helper.py [imports_from] helper.py:L1               - helper.py [imports_from] helper.py:L1
- run() [calls] helper.py:L4                          - run() [calls] helper.py:L4

zzz.py calls into core.py four times; aaa.py only imports it.

affected has no JSON output or MCP tool on v8, so the CLI text is the only output that changes.

Diff: graphify/affected.py +78/-2 (rank_hits, called at the end of affected_nodes), one help line, 6 tests.

Numbers

Git-history replay on six repos (graph at a cut commit about 6 months before HEAD, 2,070 later queries; seed = a changed code file, truth = the other files in that commit). The result set is identical for all 2,070 queries. recall@10 / hit@10:

repo v8 this PR
django (760) 0.114 / 0.250 0.151 / 0.303
flask (31) 0.058 / 0.161 0.145 / 0.258
requests (47) 0.365 / 0.681 0.369 / 0.681
hono (473) 0.318 / 0.414 0.337 / 0.436
jsoup (297) 0.117 / 0.337 0.156 / 0.407
caddy (462) 0.103 / 0.197 0.165 / 0.284
all 0.179 / 0.340 0.221 / 0.395

Paired hit@10: 122 queries only this PR gets right, 8 only v8.

Failing tests (107 mutated functions in click, flask and httpx, failing test files = truth): about neutral. recall@10 0.164 to 0.170, hit@10 0.374 to 0.421 (8 vs 3, p = 0.23); httpx recall@20 drops from 0.209 to 0.174.

Limitations

  • A small gain, and not significant on the failing-tests check.
  • Scripts that parse affected text and rely on the old order will see a different order.

Related

#1478 adds an opt-in weighted traversal with hub penalties, which can change which nodes come back. This PR has no options and keeps the set. #5068 (co-change in affected) also edits affected.py; the two merge cleanly, and with both, the JSON there follows this order.

Type of change

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

Verification & Invariants

Invariant: the hit set and each hit's depth are unchanged; only order changes, and it does not depend on graph insertion order or PYTHONHASHSEED. Tests check the set and depth against v8's, the exact order, tie breaking by path, insertion-order independence, CLI text order and hash-seed stability. No persisted state changes.

  • 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?

uv run --frozen pytest tests/test_affected_ranking.py -q   # 6 passed; on v8: 5 failed, 1 passed (the "set unchanged" guard)
PYTHONPATH="$PWD" uv run --frozen pytest tests -q          # 10204 passed, 21 skipped, 1 xfailed (v8: 10198 passed)
uv run --frozen ruff check .                               # clean
pyright graphify/affected.py                               # no new errors

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments.
  • 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.

`affected` returned hits in breadth-first discovery order, which within a
depth is just graph-iteration order. On a hub file the first lines were
whichever dependents happened to be stored first (django/conf/__init__.py
has 938 dependent files at depth 2), so the top of the list said little.

Hits are now sorted by:
1. distance from the seed (depth), as before;
2. how strongly the hit's file links one step closer to the seed: edges,
   over the traversed relations, from that file into the seed's file
   (depth 1) or into the files first reached at depth - 1; more first;
3. file path, then line, then node id, so ties break the same way every
   run whatever order the graph was built in.

The result set, depths and relations are unchanged; only the order is.
Measured on 2,070 queries replayed from six repos (files changed in the
same commit as truth), recall@10 goes from 0.179 to 0.221 and hit@10 from
0.340 to 0.395 (122 vs 8 discordant queries). On 107 mutated functions in
click, flask and httpx (failing test files as truth) the effect is small:
recall@10 0.164 to 0.170, hit@10 0.374 to 0.421, not significant.

`affected` has no JSON output and no MCP tool on v8, so only the CLI text
changes; any later consumer of affected_nodes gets the same order.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

Thanks for the pull request, @rohit-jsfreaky. 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 Nov 6, 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.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

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

Ranks affected output so the most likely impacted hits come first: nearest depth, then files with the most edges toward the seed (or the previous depth's files), then path, line and id as a stable tiebreak. The result set and depths are unchanged, and the order no longer depends on graph build order or hash seed. Link counting treats the seed's file as the depth-1 target, so a seed with no source_file can match unrelated unfiled nodes and inflate those counts. The CLI help line now notes the new ordering.

No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 516 functions depend on the 68 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: main() — 123 callers, 3 callees
  • worse: affected_nodes() — 39 callers, 3 callees

Verification — 516 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: 490 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

59 of 527 test file(s) selected (11%) via static blast radius.

  • tests/test_4874_jvm_resolution.py — impact
  • tests/test_affected_cli.py — impact
  • tests/test_affected_defines.py — impact
  • tests/test_affected_member_seed.py — impact
  • tests/test_affected_overrides.py — impact
  • tests/test_affected_path_file_node.py — impact
  • tests/test_affected_ranking.py — impact, changed-test
  • tests/test_affected_seed_reference_forms.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_benchmark.py — impact
  • tests/test_benchmark_raw_graph.py — impact
  • tests/test_benchmark_real_query.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_confidence_score_output.py — impact
  • tests/test_corrupt_graph_json.py — impact
  • tests/test_dedup_shrink_refuses_force_write.py — impact
  • tests/test_devin.py — impact
  • tests/test_edge_also_seen.py — impact
  • tests/test_explain_cli.py — impact
  • tests/test_explain_same_file_tie.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_extract_cost_ledger.py — impact
  • tests/test_global_add_tag_inference.py — impact
  • tests/test_god_nodes_cli.py — impact
  • tests/test_hollow_chunks_arm_shrink_guard.py — impact
  • tests/test_incomplete_build_guard.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_install.py — impact
  • tests/test_install_references.py — impact
  • tests/test_install_version_warning.py — impact
  • tests/test_js_dynamic_import_affected.py — impact
  • tests/test_js_dynamic_imports.py — impact
  • tests/test_lookup_exit_codes.py — impact
  • tests/test_mcp_graph_view_cache.py — impact
  • tests/test_merge_chunks_validation.py — impact
  • tests/test_multigraph_diagnostics.py — impact
  • tests/test_no_dedup_flag.py — impact
  • tests/test_opencode_plugin_refresh.py — impact
  • tests/test_path_cli.py — impact
  • tests/test_path_confidence.py — impact
  • tests/test_path_type_member.py — impact
  • tests/test_query_cli.py — impact
  • 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
  • … and 9 more

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.

Docs that may be stale (advisory)

…and 10 more.

Formal verification

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli 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 affected\_nodes (not a proof).

The verifier ran both versions of affected\_nodes 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.

· 2 more finding(s) on lines outside this diff (see the check run).

…file

When the seed has no `source_file`, its "file" was the empty string, so
every edge into any other unfiled node counted as a link toward the seed
and could push a hit up the list. The empty file is now never a target.

Co-Authored-By: Claude <noreply@anthropic.com>
@rohit-jsfreaky

Copy link
Copy Markdown
Contributor Author

Thanks, the seed-without-a-file point was real. Fixed in da6be38: when the seed has no source_file, the empty file is no longer a link target, so edges into unrelated unfiled nodes don't push a hit up. test_a_seed_without_a_file_does_not_count_links_to_unfiled_nodes fails without the fix (b1 came before a1) and passes with it. tests/test_affected*.py and tests/test_cli*.py: 136 passed, and ruff is clean.

This branch has not been deployed

No deployments
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.

[Feature]: affected lists hits in walk order; rank them by distance and link count

1 participant