Repository navigation
feat(affected): show files that change together in git history - #5068
rohit-jsfreaky wants to merge 2 commits into
Conversation
`graphify affected` only follows code links, so it misses the files that are edited with a change but never imported: docs, configs, changelogs, templates, tests reached through strings. On six repos (django, flask, requests, hono, jsoup, caddy; 2,070 queries replayed from the six months after a cut commit) this change raises recall@10 of the files changed in the same commit from 0.18 to about 0.48. On a failing-tests key (107 mutated functions in click, flask and httpx) the gain is smaller, about 0.16 to 0.23, and flask does not improve. What changes: - graphify/git_cochange.py: a small miner, `mine_cochange(repo_root, ...)`. One `git log --no-merges --relative -M --name-status` call (argv list, core.quotepath=false, %x1e/%x1f separators), last 3 years or 10,000 commits counted back from HEAD's commit date, renames followed, commits with more than 30 files skipped, at most 50 partners per file with support >= 2, sorted output and a 5 MB cap. - Only files tracked at HEAD that `graphify update` would itself scan: `corpus_filter` uses detect.ignored_predicate (.graphifyignore/.gitignore chain, persisted --exclude patterns, noise dirs, skipped names) and detect._is_sensitive, plus lockfile/generated/vendored rules. Tracked docs, configs and tests stay. - `graphify update` (and the hooks and watch, which share the locked rebuild) writes graphify-out/cochange.json after a successful rebuild, with the same persisted excludes. It is skipped when HEAD and the ignore settings are unchanged, never fails the rebuild, prints one note without git, and notes a shallow clone with a hint to run `git fetch --unshallow`. GRAPHIFY_NO_COCHANGE=1 turns it off. - `graphify affected` prints a "Changed together in git history (not code links)" section of at most 10 files, with confidence and commit counts and pairs below 0.2 confidence hidden, BEFORE a "Code links:" label and the unchanged graph list. Printed after the graph list it was lost on hub files (1,006 graph lines for django/conf/__init__.py). `--no-cochange` hides it; `--json` lists the same items in the same order, each tagged `graph` or `cochange`. The graph results of `affected` (set, order and direction) are unchanged; with --no-cochange or without cochange.json the text output is byte-identical to before. Co-change is never written into graph.json. Co-Authored-By: Claude <noreply@anthropic.com>
|
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. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 4 changed function(s) (2 sampled, not proven, 2 not verified).
Formal verification. PR-changed functions: 2/4 verified (0 proven, 2 may-equivalent, 0 distinguished) · 2 not verified (1 vacuous, 1 unsupported).
Not verified on this run: dispatch\_command (vacuous: never exercised), \_rebuild\_code (unsupported).
Graphify review — findings
Adds git co-change mining: graphify update now writes graphify-out/cochange.json from commit history (renames followed, large commits skipped, partners limited to files the update would scan), and the refresh keys off the cached file, which is where a shallow clone's data can stay stale after git fetch --unshallow. Extends graphify affected with a short "Changed together in git history (not code links)" section printed before the code links, and a --json mode that tags each result graph or cochange, where the file field mixes repo-relative cochange paths with raw graph source_file values. Adds --no-cochange to hide the section and GRAPHIFY_NO_COCHANGE=1 to skip writing the file; without git the step is skipped with a note, and the README and ARCHITECTURE docs describe both.
Worth a look
- Cached shallow-clone co-change data is never re-mined after
git fetch --unshallow—graphify/git_cochange.py· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1239 functions depend on the 376 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
_rebuild_code()— 262 callers, 74 callees - worse:
dispatch_command()— 5 callers, 148 callees - worse:
main()— 123 callers, 3 callees - new:
refresh_cochange()— 9 callers, 6 callees - new:
mine_cochange()— 3 callers, 5 callees - worse:
format_affected()— 2 callers, 6 callees - new:
test_renames_are_followed_and_pure_renames_add_nothing()— 0 callers, 6 callees
Verification — 1239 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: 1222 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
527 of 527 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety (
noncode-change,uncovered-code) — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/pi.test.ts— full-run-safetytests/test_4874_jvm_resolution.py— impact, full-run-safetytests/test_4909_reference_binding_scale.py— full-run-safetytests/test_4978_reconcile_cost.py— impact, full-run-safetytests/test_4992_ts_import_types.py— full-run-safetytests/test_affected_cli.py— impact, full-run-safetytests/test_affected_cochange.py— impact, changed-test, full-run-safetytests/test_affected_defines.py— impact, full-run-safetytests/test_affected_member_seed.py— impact, full-run-safetytests/test_affected_overrides.py— impact, full-run-safetytests/test_affected_path_file_node.py— impact, full-run-safetytests/test_affected_seed_reference_forms.py— impact, full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_alias_folding.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_backend_opt_in.py— full-run-safetytests/test_benchmark.py— impact, full-run-safetytests/test_benchmark_raw_graph.py— impact, full-run-safetytests/test_benchmark_real_query.py— impact, full-run-safetytests/test_binary_media_words.py— full-run-safetytests/test_blade_extractor.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_located_semantic_identity.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_ghost_fold_4955.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_c_include_resolution.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_cache_long_paths.py— full-run-safetytests/test_cache_stale_import_target.py— impact, full-run-safetytests/test_cache_windows_case_4925.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_cargo_missing_manifest.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_only_decl_ids_4904.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safety- … and 477 more
non-code file(s) changed (
ARCHITECTURE.md,README.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
ARCHITECTURE.md,README.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
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)
CHANGELOG.md§ 0.9.96 (2026-10-25) (lines 164-194): references changed symbols_runCHANGELOG.md§ 0.7.16 (2026-05-12) (lines 2091-2101): references changed symbolsdumpsCHANGELOG.md§ 0.6.8 (2026-05-03) (lines 2252-2260): references changed symbolsdumpsdocs/superpowers/plans/2026-05-04-incremental-updates-dedup.md§ Task 4: Incremental updates — semantic cache + manifest in __main__.py (lines 603-911): references changed symbols_run,dumpsgraphify/skill-agents.md§ Step 2 - Detect files (lines 118-159): references changed symbolsdumpsgraphify/skill-agents.md§ Part A - Structural extraction for code files (lines 183-211): references changed symbolsdumpsgraphify/skill-agents.md§ Part B - Semantic extraction (parallel subagents) (lines 212-420): references changed symbolsdumpsgraphify/skill-agents.md§ Part C - Merge AST + semantic into final extraction (lines 421-466): references changed symbolsdumpsgraphify/skill-agents.md§ Step 4 - Build graph, cluster, analyze, generate outputs (lines 467-529): references changed symbolsdumpsgraphify/skill-agents.md§ Step 5 - Label communities (lines 556-604): references changed symbolsdumps
…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 format\_affected (not a proof).
The verifier ran both versions of format\_affected 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.
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_rebuild\_code.
The verifier did not have enough to check \_rebuild\_code, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: nondeterministic output (both versions disagreed with themselves on the witness) — not a behaviour change; a function whose output is not a function of its inputs (random / time / uuid / os.urandom, or state carried across calls) cannot be soundly distinguished by execution, so this tier abstains for the whole function
· 5 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).
| return lines | ||
|
|
||
|
|
||
| def format_affected( |
There was a problem hiding this comment.
format_affected()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return {"version": FORMAT_VERSION, "status": "unavailable", "reason": reason} | ||
|
|
||
|
|
||
| def mine_cochange( |
There was a problem hiding this comment.
mine_cochange()
high coupling complexity (Ca·Ce = 15).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| "ignore_files_sha256": digest.hexdigest()} | ||
|
|
||
|
|
||
| def refresh_cochange( |
There was a problem hiding this comment.
refresh_cochange()
fans out to 6 callees (efferent coupling); 9 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| print(f"[graphify watch] {note}") | ||
|
|
||
|
|
||
| def _rebuild_code( |
There was a problem hiding this comment.
_rebuild_code()
fans out to 74 callees (efferent coupling); 262 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert cochange_partners(data, "seed.py", min_confidence=0.1)[0]["file"] == "weak.md" | ||
|
|
||
|
|
||
| def test_renames_are_followed_and_pure_renames_add_nothing(tmp_path): |
There was a problem hiding this comment.
test_renames_are_followed_and_pure_renames_add_nothing()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…paths - After `git fetch --unshallow` HEAD does not move, so the cache check kept the shallow-clone history. A cached shallow result is now reused only while the repo is still shallow. - `--json` printed the graph's raw `source_file` next to repo-relative co-change paths; graph items and the seed now use the same repo-relative form (only differs for graphs that stored absolute paths). Co-Authored-By: Claude <noreply@anthropic.com>
|
Thanks, both points were real. Fixed in 233775b:
The coupling notes are about the size of the new functions and of |
Closes #5066
What does this PR do?
graphify affectednow also shows the files that keep changing together with the seed in git history, in its own labelled section. The code-link results do not change.Mining (
graphify/git_cochange.py, one plain functionmine_cochange(repo_root, ...)):git log --no-merges --relative -M --name-statuscall, argv list with no shell,core.quotepath=false,%x1e/%x1fseparators.graphify updateuses:detect.ignored_predicate(.graphifyignore/.gitignorechain, saved--excludepatterns, noise dirs, skipped names) anddetect._is_sensitive, plus lockfile, generated and vendored rules. Tracked docs, configs and tests stay, since they are the point.When it runs: at the end of a successful
graphify update(and so the hooks and watch, which share the locked rebuild), it writesgraphify-out/cochange.json. It is skipped when HEAD and the ignore settings did not change, and it never fails the rebuild. Without git it prints one note. On a shallow clone it uses what is there and suggestsgit fetch --unshallow.GRAPHIFY_NO_COCHANGE=1turns mining off.In
affected: at most 10 files, pairs below 0.2 confidence hidden. The section comes before the graph list because on hub files the graph list is long (1,006 lines fordjango/conf/__init__.py), and printed after it the section was never in the top 10.--no-cochangehides it.--jsonlists the same items in the same order, each withsource: graphorsource: cochange.Without
cochange.jsonor with--no-cochange, the text output is byte for byte the same as before. Co-change is never written intograph.json.Diff:
git_cochange.py+349,affected.py+126,cli.py+49/-11,watch.py+32, help/README/ARCHITECTURE +5, tests +465 (25 tests).Numbers
Git-history replay, six repos, graph and history taken 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). recall@10 / hit@10:
Paired hit@10: 860 queries only this PR gets right, 7 only v8.
Failing tests (a check that does not come from git history):
raise RuntimeErroras the first line of 107 real functions, one at a time, the repo's own suite run, failing test files = truth, co-change mined only from history before the tested commit. recall@10 / recall@20 / hit@10:Paired hit@10: 29 vs 6 (p = 0.0001). Flask is worse on recall@10: up to 10 history lines push its code links down.
Cost,
graphify updateon Django, warm: about 2.8 s with or without this PR when HEAD did not move; about 4.0 s when HEAD moved and history is mined again.Limitations
change_password.htmltemplate..graphifyignorewithout a commit does not re-mine until HEAD moves (the filter itself does read nested files).Related
#4544 adds a standalone
graphify cochangereport. This PR does not copy its code and touches none of its files. The safe choices are the same (argv list,--relative,core.quotepath=false,%x1e, bulk-commit skip, tracked files only), and the miner is one plain function, so whichever lands second can reuse the other.Type of change
Verification & Invariants
Invariant: the graph result set, order and edge direction of
affecteddo not change, andgraph.jsonis untouched. A test checks the text output is identical with--no-cochangeand withoutcochange.json. Mining is deterministic: same repo and HEAD give the same file, also under a differentPYTHONHASHSEED. New persisted state:graphify-out/cochange.jsononly.How was this tested?
Tests cover: no git, shallow clone, renames, the bulk-commit skip,
.graphifyignoreand sensitive files, determinism (twice and with anotherPYTHONHASHSEED),--no-cochange, the JSON tags, and the byte-identical fallback.Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments.