perf(global-graph): batch repository updates - #3444
Conversation
(cherry picked from commit 416be1e)
`from . import brain, ledger` in a directory with no __init__.py — a namespace package, which `python -m pkg.mod` runs without complaint — emitted nothing: _resolve_python_module_path returns None when there is no module file to probe, and the Graphify-Labs#1146 submodule branch sits behind that None. So every `brain.think()` / `ledger.write()` call in such a repo was invisible to the Graphify-Labs#1883 module arm, and the most-called functions carried in-degree 0 in the graph. _resolve_python_namespace_dir mirrors the module-path walk (relative base, scan root, then sys.path-root ancestors per Graphify-Labs#2072) and returns only a directory that exists inside the root and has no __init__.py. The fact collector uses it when the module path resolves to no file; the existing submodule probe then emits the same imports_from edges a regular package gets. A name that is not a module file on disk still emits nothing, and a namespace package binds no symbols of its own. Measured on a 275-node Python repo laid out this way, same graphify version with and without the change: calls 201 -> 307, nodes unchanged; think() 0 -> 15 callers, remember() 0 -> 10, ledger write() 0 -> 20. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTNEBgonu6ASph6w8R2Wcp (cherry picked from commit e2ec796)
(cherry picked from commit d00d3ae)
An aliased use clause (use GuzzleHttp\Client as HttpClient) always targeted the bare imported name, ignoring the alias entirely. Two files importing the same external class under different local aliases, a real pattern for disambiguating two same named classes from different namespaces, produced two different stub targets for one class, splitting its identity. The cross file resolver repoints a stub through the file's own alias to FQN map, keyed by the alias when the import has one, so a bare name derived stub could never be found under that key and stayed stuck, unresolved. The alias, when present, is also what the rest of the file actually references, so preferring it here keeps this edge's target consistent with those reference edges too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh (cherry picked from commit 73f9f16)
…-Labs#3347) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q (cherry picked from commit eca5af3)
…the first 20 Review follow-up: a wide TS outDir keeping compiled files only under a late-sorted module dir slipped past the 20-subdirectory cap. The level-two probe now covers every subdirectory, bounded by total entries scanned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q (cherry picked from commit ff0a8f6)
`script_invocation` edges (Graphify-Labs#1756) only ever resolved a path that is a bare literal -- `./helpers.sh`, or `bash ./helpers.sh`. Both paths into that branch go through `literal()`, which rejects by design any token containing `$`, so the single most common way a shell script calls a sibling is invisible: script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" "$script_dir/helpers.sh" --flag `cmd` is None for that command node, the `elif cmd and ...` guard short- circuits, and no edge is emitted. On a 206-file shell tree that uses the `script_dir` idiom throughout, `script_invocation` produced *zero* edges -- the feature was effectively dead for that whole style of codebase. The `source` branch already solved the same problem for `source "$DIR/lib/x.sh"` (Graphify-Labs#2079): strip the leading `${VAR}/` segments with `_bash_source_suffix` and resolve the literal remainder against the sourcing file's own directory, because the canonical `script_dir` idiom makes that variable equal to exactly that. This applies the identical treatment to the exec position -- read the command-name node's raw text rather than its `literal()`, strip the same way, and fall through to the existing `resolved.is_file()` gate. Nothing new is fabricated. `_bash_source_suffix` still returns None when the remainder holds another expansion (`"$dir/$name.sh"`) or a `..` segment, and the on-disk check still means a wrong script-dir guess emits nothing at all. The three existing negative tests -- missing, shadowed, and dynamic invocations -- pass unchanged. Measured on the same 206-file tree: 0 -> 16 edges across 10 distinct file pairs, every one hand-checked to a real invocation of a real file, no false positives. Full suite: no change in outcomes (the 25 failures here are pre-existing and unrelated -- terraform, ollama, skillgen). Fixes Graphify-Labs#3416. (cherry picked from commit 2e112c3)
…global-add-many
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 5 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Replaces global_add with global_add_many, which processes a batch of (path, tag) sources in one manifest load — deduplicating externals across the whole batch, computing a batch-total cross_repo_calls, and rejecting duplicate tags or missing source files up front. Adds an on_error policy ("abort" vs "skip") so a failing source can either halt the batch or be recorded as failed while the rest proceed, with per-repo changes staged and rolled back on error rather than partially applied. Extends graphify global add to accept multiple graph paths plus a --keep-going flag, still inferring each repo tag from the path (or requiring --as for a single graph), and prints per-source added/skipped/failed lines.
Worth a look
- Load/size-check errors occur after prune, so error rollback path (except) does not restore pruned nodes on skip —
graphify/global_graph.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- on_error='skip' failure leaves repo's stale nodes deleted but manifest unchanged, corrupting graph vs manifest consistency —
graphify/global_graph.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Rollback removes nodes by data['repo'] but prefixed nodes may not carry repo attr —
graphify/global_graph.py:217· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- on_error='skip' persists partial mutations for failed repo without full rollback —
graphify/global_graph.py:240· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- --as can consume --keep-going as the repo tag —
graphify/cli.py:3122· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 333 functions depend on the 124 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 125 callees - new:
global_add_many()— 18 callers, 8 callees - new:
_stale_graph_sources()— 7 callers, 6 callees - new:
_run_hook_guard()— 4 callers, 8 callees - new:
global_remove()— 5 callers, 5 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees - new:
test_global_add_many_replacement_failure_preserves_original()— 0 callers, 6 callees
Verification — 333 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: 279 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
25 of 263 test file(s) selected (10%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_cli_global_add_many.py— changed-testtests/test_codebuddy.py— impacttests/test_devin.py— impacttests/test_explain_cli.py— impacttests/test_extract_cli.py— impacttests/test_global_graph.py— impact, changed-testtests/test_god_nodes_cli.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_hook_guard_token_match.py— impacttests/test_hook_out_of_project_paths.py— impacttests/test_hook_strict.py— impacttests/test_incomplete_build_guard.py— impacttests/test_install.py— impacttests/test_install_references.py— impacttests/test_merge_chunks_validation.py— impacttests/test_multigraph_diagnostics.py— impacttests/test_no_dedup_flag.py— impacttests/test_partial_cache.py— impacttests/test_path_cli.py— impacttests/test_query_cli.py— impacttests/test_query_induced_edges.py— impacttests/test_stale_prune.py— impacttests/test_unverified_semantic_shrink.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
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: not verifiable: all 23 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 global\_add.
The verifier did not have enough to check global\_add, 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: parameter `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 3 grounded finding(s) anchored inline below; 4 more finding(s) on lines outside this diff (see the check run).
|
|
||
| def global_add(source_path: Path, repo_tag: str) -> dict: | ||
| """Add or update a project graph in the global graph. | ||
| def global_add_many( |
There was a problem hiding this comment.
global_add_many()
fans out to 8 callees (efferent coupling); 18 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return global_add_many([(source_path, repo_tag)])[0] | ||
|
|
||
|
|
||
| def global_remove(repo_tag: str) -> int: |
There was a problem hiding this comment.
global_remove()
high coupling complexity (Ca·Ce = 25).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| # It does not scale with the number of repositories. | ||
| assert scan_count == 2 | ||
|
|
||
| def test_global_add_many_replacement_failure_preserves_original(tmp_path): |
There was a problem hiding this comment.
test_global_add_many_replacement_failure_preserves_original()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
🟡 Changes recommended
The batch-merge rollback and persistence logic has edge cases that can leave partial mutations or write to disk even when nothing was applied, and the new CLI subprocess tests currently aren’t isolated from the user’s real ~/.graphify state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a batched global-graph update pathway so multiple repository graphs can be merged into the global graph with a single load → resolve → save cycle, addressing the repeated O(K²)-style work described in #3438.
Changes:
- Added
global_add_many()as the primary batch API and refactoredglobal_add()to delegate to it. - Reworked the merge flow to initialize external-label and repo-node indexes once per batch, and to support rollback on apply-time failures.
- Updated the CLI to accept multiple graph paths and
--keep-going, and added regression tests for batch equivalence and CLI behavior.
File summaries
| File | Description |
|---|---|
graphify/global_graph.py |
Adds global_add_many() batching + rollback logic and delegates global_add() to it. |
graphify/cli.py |
Extends graphify global add to accept multiple graphs and --keep-going mapped to on_error="skip". |
tests/test_global_graph.py |
Adds extensive regression coverage for batching, skip/abort, replacement, rollback, and cross-repo call resolution. |
tests/test_cli_global_add_many.py |
Adds subprocess-based CLI tests for single/multi add, --as validation, and inferred-tag errors. |
Review details
Suppressed comments (5)
tests/test_cli_global_add_many.py:22
- This subprocess-based CLI test currently writes to the real user HOME (~/.graphify) because graphify.global_graph uses Path.home(); set HOME (and PYTHONPATH) in the subprocess env to isolate test state under tmp_path.
res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", str(g), "--as", "myrepo"], capture_output=True, text=True)
tests/test_cli_global_add_many.py:37
- This subprocess-based CLI test currently writes to the real user HOME (~/.graphify) because graphify.global_graph uses Path.home(); set HOME (and PYTHONPATH) in the subprocess env to isolate test state under tmp_path.
res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", str(g1), str(g2)], capture_output=True, text=True)
tests/test_cli_global_add_many.py:53
- This subprocess-based CLI test currently writes to the real user HOME (~/.graphify) because graphify.global_graph uses Path.home(); set HOME (and PYTHONPATH) in the subprocess env to isolate test state under tmp_path.
res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", str(g1), str(g2), "--keep-going"], capture_output=True, text=True)
tests/test_cli_global_add_many.py:64
- This subprocess-based CLI test should set HOME (and PYTHONPATH) in the subprocess env to avoid depending on or mutating ~/.graphify on the machine running the test suite.
res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", str(g1), str(g2), "--as", "myrepo"], capture_output=True, text=True)
tests/test_cli_global_add_many.py:77
- This subprocess invocation sets PYTHONPATH but not HOME; setting HOME to tmp_path isolates graphify's global graph location (Path.home()/.graphify) so the test doesn't depend on or mutate the user's real ~/.graphify.
env = os.environ.copy()
env["PYTHONPATH"] = str(Path.cwd())
res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", "graph.json"], cwd=tmp_path, env=env, capture_output=True, text=True)
- Files reviewed: 4/4 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| nodes_to_add = [(n, d) for n, d in prefixed.nodes(data=True) if n not in remap] | ||
| edges_to_add = [] | ||
| for u, v, data in prefixed.edges(data=True): | ||
| u = remap.get(u, u) | ||
| v = remap.get(v, v) | ||
| if u != v: | ||
| edges_to_add.append((u, v, data)) | ||
|
|
||
| # --- APPLY --- | ||
| for node, data in nodes_to_add: | ||
| G.add_node(node, **data) | ||
| if not data.get("source_file") and data.get("label"): | ||
| external_labels[data["label"]] = node | ||
|
|
||
| for u, v, data in edges_to_add: | ||
| G.add_edge(u, v, **data) | ||
|
|
||
| except Exception as apply_error: | ||
| G.remove_nodes_from([n for n, d in G.nodes(data=True) if d.get("repo") == repo_tag]) | ||
| G.add_nodes_from(to_remove_nodes) | ||
| G.add_edges_from(to_remove_edges) | ||
| external_labels.clear() | ||
| external_labels.update(old_external_labels) | ||
| raise apply_error |
| cross_repo_calls = link_cross_repo_member_calls(G) | ||
| _save_global_graph(G) | ||
|
|
||
| manifest["repos"][repo_tag] = { | ||
| "added_at": datetime.now(timezone.utc).isoformat(), | ||
| "source_path": str(source_path.resolve()), | ||
| "node_count": added, | ||
| "edge_count": prefixed.number_of_edges(), | ||
| "source_hash": src_hash, | ||
| } | ||
| _save_global_graph(G) | ||
| _save_manifest(manifest) |
|
|
||
| res = subprocess.run([sys.executable, "-m", "graphify", "global", "add", str(g)], capture_output=True, text=True) |
| # The node for X in repoB should have repo label repoB, because the old one was from repoA and was deleted. | ||
| # Wait, external nodes in prefixed graph don't necessarily get repo tag prefix. Actually they do get repo in prefix_graph_for_global. | ||
| # The node from repoA should be deleted. |
| # Should exactly be 2 scans (1 for our upfront index, 1 for cross_repo_calls pass)! | ||
| # It does not scale with the number of repositories. | ||
| assert scan_count == 2 |
build.py annotates chunks and ast_sources with Iterable but never imports it, so ruff reports four F821 on v8 and typing.get_type_hints() raises on _tier_replacement_sources, merge_raw_extraction and build_merge. Nothing breaks at import because `from __future__ import annotations` keeps the hints as strings. The added test walks build.py's own functions rather than the package: nx under if TYPE_CHECKING in cross_repo_calls.py, cross_repo_types.py and prs.py is unresolvable on purpose, while build.py imports every annotated name at module scope. (cherry picked from commit c315a15)
`tag or source.parent.parent.name` is empty whenever the graph is not two levels below a named directory: `graphify global add /tmp/graph.json`, or any relative path. Every node is then prefixed `::`, the prune key is `""`, and the manifest entry cannot be removed because the `remove` subcommand reads an empty tag as a missing argument. Infer through `distinct_repo_tags`, the helper `merge-graphs` already uses, so the two entry points cannot diverge again and the degrade is `"repo"`; absolutize first so a relative path names the repo dir rather than the cwd placeholder. An omitted `remove` tag stays a usage error, while an explicitly empty one is now addressable so stores written by earlier revisions can be cleaned up. (cherry picked from commit 6a317e6)
A workspace-barrel import (`import { evaluate } from '@scope/domain'`)
resolves through `export *` by probing each star target for a symbol
node with the imported name. The symbol map normalizes labels by
stripping the leading dot, so an interface/class METHOD `.evaluate()`
in the first star target satisfied the probe before the exported
function in the second one was reached. The import edge and the
cross-file call then bound to the method node, and the real function
lost its inbound `calls` edge (read as dead code).
Track member nodes separately and skip them in the star-export walk: a
module can only re-export top-level bindings. A member never shadows a
same-named top-level symbol in the map anymore either.
Fixes Graphify-Labs#3436
(cherry picked from commit 52e4a2b)
Adds an AST branch for create_index in the SQL extractor, emitting an index node with an 'indexes' edge to its table (cross-file stub when the table is defined elsewhere), mirroring the existing trigger handling. (Graphify-Labs#3470)
The Rust extractor gated every non-function item on struct_item, enum_item and trait_item, so tree-sitter-rust's static_item and const_item never produced a node: a constant existed in the graph only through the files that referenced it, never from the Rust that defines it (0 of 783 names in a jdx/mise snapshot). Add a branch for both node types: a module-level declaration gets a node with a file-level contains edge, an associated const inside an impl is attributed to the impl like a method, and the declared type is referenced the way a struct field type is. Fixes Graphify-Labs#3471 (cherry picked from commit 70ff464)
(cherry picked from commit fa1284a)
… unwritable `install()` copies the skill files first and writes the always-on registration afterwards, so an unguarded write to `~/.claude/CLAUDE.md` left a half-completed install plus a traceback whenever that file could not be written. That is the normal shape for a declaratively managed dotfile: nix/home-manager symlinks it into a read-only /nix/store, and chezmoi or stow with read-only sources do the same. Route the two registrations inside `install()` (CLAUDE.md and CODEBUDDY.md, which had the identical unguarded pattern) through one helper that reports the skip on stderr and lets the install finish, so the skill files that were already laid down stay usable. Refs Graphify-Labs#3474 Co-Authored-By: Claude Code <noreply@anthropic.com> (cherry picked from commit 47041ee)
…expires Two independent failures, both silent, found while repairing 16 repositories whose graphs had quietly stopped tracking their code. 1. `graphify label` downgraded graphs while reporting success `detect_backend()` is key-based, and `claude-cli` is the one backend with no API key to find — `_get_backend_api_key` can never return one for it, and the fallback loop excludes it by name. Nothing else looked for it. So on a machine with the Claude Code CLI installed and no API key, labelling announced "no LLM backend configured", replaced every real community name with a `Community N` placeholder, and exited 0. Run over an existing graph, that overwrites good names with worse ones and looks like success; taking the exit code at face value commits the damage. The fallback is placed in the labelling path, NOT in `detect_backend()`. Widening detection itself was the first attempt and it broke `test_mixed_repo_without_key_errors_and_points_at_code_only`: extraction deliberately refuses to run without a configured backend and points at `--code-only`, and on any machine with the CLI present that contract would have silently changed to "shell out to the CLI instead". That contract is unchanged here, and a test now pins it. 2. The post-commit hook pinned a path with an expiry date `_pinned_python()` wrote `sys.executable` verbatim. graphify installed from inside a snap-confined editor lives under `~/snap/<app>/<revision>/`, and snap swaps that revision on update and prunes the old tree. Every hook pinned to it then dies — observed across 15 repositories at once when an editor snap moved past revision 259. Each commit printed "could not locate a Python with graphify installed" and the graphs stopped following the code, with nothing failing loudly enough to notice. Two changes: `_pinned_python()` declines to pin a path under a rotating revision directory, which its docstring already describes as safe degradation; and the uv-tools probe — which already scans tool environments — additionally scans snap-confined HOMEs by glob, since an install made inside a snap lands in that snap's private HOME and the plain `$HOME` roots never see it once the hook runs from an ordinary shell. Verified: same 25 pre-existing failures before and after (unrelated: terraform fixtures, a TS scaling test); passing goes 5110 -> 5119, the nine tests added here. Each change was mutation-tested — removing it fails a test that the others still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit c6065e2)
…cli fallback Graphify-Labs#3475 added a PATH probe for a claude-cli labelling fallback; the no-backend test must neutralize it so it stays deterministic on machines with claude installed (follow-up to Graphify-Labs#3475).
Autouse conftest fixture clears every backend env var detect_backend() reads (API keys, Azure/AWS/Ollama), with a self-guard test that the cleared list covers every variable the detector reads. (Graphify-Labs#3484)
The postgres extra's psycopg[binary] was the sole dep missing from all; a new union test guards the all-extra against future drift. (Graphify-Labs#3483)
…h normalization, SQL indexes, rust statics, install/labelling robustness, all-extra psycopg
…ph cost once `global_add` reads the global graph, runs the cross-repo member call pass over it and writes it back on every call, so registering K units did each of those K times on a graph that grows with every one — quadratic in K. `global_add_many` composes an ordered batch against one in-memory graph: one read, one cross-repo pass over the finished graph, one write. `global_add` delegates to it with a batch of one, so there is a single composition implementation and the two paths cannot drift. Composition order is unchanged. Each unit is still pruned and composed in turn, and the external-label dedup is still read off the graph as it stands when that unit arrives, so a later unit merges onto a stub an earlier one composed. The cross-repo pass recomputes from the parked entries, so one run over the composed batch lands where a run per unit does. An unchanged batch returns before the graph is read. Missing sources, oversized sources and one tag naming two sources in a batch are all rejected before the first unit is composed, since the batch writes only at the end and a late failure discards everything composed before it.
The index has to be built after the prune, not before it. A stub the previous revision owned is deleted by the prune; an index built ahead of it still names that id, the unit's own stub is then treated as a duplicate and never composed, and rewiring an edge onto the missing id has add_edge invent an attribute-less node in its place. The assertion is on the stored graph rather than on the dedup, so it keeps its meaning if the per-repo-stub change removes the dedup entirely. Hoisting the index above the prune turns it red.
…hes merge-graphs `global_add` merged nodes with no `source_file` by label, so an external stub's `repo` tag became a function of merge order. `merge-graphs` composes the prefixed units as-is, so the two paths produced different graphs from identical inputs. Ownership by merge order also fabricated cross-repo edges (one endpoint with no `source_file`) and let `prune_repo_from_graph` delete other repos' real edges along with the stub they pointed at on re-scan. `added` subtracted the remap size, under-reporting each repo's `node_count` in the manifest. Compose each unit's stubs directly. The self-loop guard went with the remap that produced the self-loops.
… list operations - Added `global_add`, `global_add_many`, `global_remove`, and `global_list` functions to manage project graphs in a global context. - Introduced manifest handling to track repositories and their associated graphs. - Implemented graph loading and saving using NetworkX and JSON serialization. - Added error handling for corrupted manifest files and oversized graph files. - Developed unit tests to ensure functionality and correctness of global graph operations, including handling of duplicate nodes and edge rewiring. - Created helper functions for graph creation and JSON conversion for testing purposes.
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 5 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Resolves Python calls to a nested inner function by lexical scope while keeping direct recursion as a self-loop, and fixes namespace-package, flat-script-sibling, and PHP use ... as alias import resolution. Normalizes source_file/definition_file to the build root before dedup so semantic absolute paths no longer collide with their AST twins and drop rationale/summary, skips an out/ directory only with build-output evidence, resolves variable-path shell script invocations, and adds the missing Iterable import in build.py. Extends graphify global add to accept multiple graphs via global_add_many with per-path tag inference (degrading to a non-empty tag instead of an empty one), makes install skip an unwritable always-on registration with a warning rather than aborting, and resolves the claude CLI at run time so community labelling survives updates.
Worth a look
- Unresolved calls no longer recorded for cross-file resolution —
graphify/extractors/engine.py:5872· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Duplicate global_add_many definition; second overwrites first and has incompatible return type —
graphify/global_graph.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Duplicate global_add_many definitions; second shadows first —
graphify/global_graph.py· Escalate · high · 2 independent checks- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Duplicate definition of global_add_many with incompatible signature and return type —
graphify/global_graph.py:81· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Concurrent global_add_many calls can lose graph updates —
graphify/global_graph.py:269· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 5734 functions depend on the 3079 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 585 callers, 44 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
build_from_json()— 204 callers, 19 callees - new:
deduplicate_entities()— 79 callers, 22 callees - new:
detect()— 110 callers, 15 callees - new:
build_merge()— 68 callers, 14 callees - new:
to_obsidian()— 38 callers, 14 callees - new:
dispatch_command()— 4 callers, 123 callees - …and 141 more — each is listed as a finding
Verification — 5734 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 23f2ffa (diverged from this PR's base — delta is approximate).
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: 5650 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
270 of 270 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— impact, 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— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— impact, full-run-safetytests/test_atomic_version_stamp.py— impact, full-run-safetytests/test_atomic_writes.py— impact, full-run-safetytests/test_backend_env_isolation.py— impact, changed-test, full-run-safetytests/test_backend_extras.py— impact, changed-test, full-run-safetytests/test_benchmark.py— impact, full-run-safetytests/test_benchmark_raw_graph.py— impact, full-run-safetytests/test_build.py— impact, changed-test, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— impact, full-run-safetytests/test_chunking.py— impact, full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— impact, full-run-safetytests/test_claude_md.py— impact, full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— impact, full-run-safetytests/test_cli_global_add_many.py— changed-test, full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— impact, full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— impact, full-run-safetytests/test_corrupt_graph_json.py— impact, full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/test_csharp_enum_members.py— impact, full-run-safety- … and 220 more
non-code file(s) changed (
CHANGELOG.md,README.md,docs/graphify-card.png,pyproject.toml,uv.lock) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md,README.md,pyproject.toml,tests/conftest.py,tests/fixtures/sample.rs) — 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.
Formal verification
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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: parameter `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
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: not verifiable: all 23 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 \_is\_noise\_dir.
The verifier did not have enough to check \_is\_noise\_dir, 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: parameter `parent` is annotated `'Path | None'` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_import\_php.
The verifier did not have enough to check \_import\_php, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract\_bash.
The verifier did not have enough to check extract\_bash, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.
The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, 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: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_collect\_python\_symbol\_resolution\_facts.
The verifier did not have enough to check \_collect\_python\_symbol\_resolution\_facts, 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: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_rust.
The verifier did not have enough to check extract\_rust, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_sql.
The verifier did not have enough to check extract\_sql, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify global\_add.
The verifier did not have enough to check global\_add, 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: parameter `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_pinned\_python (not a proof).
The verifier ran both versions of \_pinned\_python 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 install.
The verifier did not have enough to check install, 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: not verifiable: all 23 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 generate\_community\_labels.
The verifier did not have enough to check generate\_community\_labels, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 8 grounded finding(s) anchored inline below; 141 more finding(s) on lines outside this diff (see the check run).
| e["target"] = alias_map[tgt] | ||
|
|
||
|
|
||
| def _repoint_python_sibling_imports(paths, all_nodes, all_edges, root) -> None: |
There was a problem hiding this comment.
_repoint_python_sibling_imports()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -59,7 +59,7 @@ def _rust_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[ | |||
| }) | |||
|
|
|||
| def extract_rust(path: Path) -> dict: | |||
There was a problem hiding this comment.
extract_rust()
fans out to 6 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return h.hexdigest()[:16] | ||
|
|
||
|
|
||
| def global_add_many( |
There was a problem hiding this comment.
global_add_many()
fans out to 9 callees (efferent coupling); 25 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return {"results": results, "cross_repo_calls": cross_repo_calls} | ||
|
|
||
|
|
||
| def global_remove(repo_tag: str) -> int: |
There was a problem hiding this comment.
global_remove()
8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return result | ||
|
|
||
|
|
||
| def global_add_many(sources: Sequence[tuple[Path, str]]) -> dict: |
There was a problem hiding this comment.
global_add_many()
fans out to 9 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return tuple(seen) | ||
|
|
||
|
|
||
| def detect_backend() -> str | None: |
There was a problem hiding this comment.
detect_backend()
19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert G.has_edge("repoC::worker", "repoA::logging") | ||
|
|
||
|
|
||
| def test_global_add_many_re_adding_a_repo_keeps_its_own_stub_in_the_graph(tmp_path): |
There was a problem hiding this comment.
test_global_add_many_re_adding_a_repo_keeps_its_own_stub_in_the_graph()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| @pytest.mark.parametrize("suffix", ["ts", "js"]) | ||
| def test_js_namespace_reexport_import_targets_real_binding( |
There was a problem hiding this comment.
test_js_namespace_reexport_import_targets_real_binding()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
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 5 advisory finding(s) below merit a look before merge.
Formal verification. 1 change(s) tested, no difference found (not proven).
Graphify review — findings
Adds SQL index nodes for CREATE [UNIQUE] INDEX linked to their tables, and infers non-empty repo tags for graphify global add when given a bare path — now also accepting multiple graph paths in one invocation via global_add_many (rejecting --as with more than one). Normalizes source_file/definition_file to the build root before dedup so a semantic node's absolute path no longer collides falsely with its relative AST twin and drops the node's rationale/summary. Skips a directory named out as build output only when there's actual build-output evidence, resolves variable-path exec-position shell invocations ("$SCRIPT_DIR/foo.sh") to their constant target, and resolves community-labelling's claude CLI at run time instead of a pinned path that expires under snap/nvm. Makes graphify install skip an unwritable always-on registration target with a warning and still install the skill instead of aborting, adds the missing Iterable import in build.py, and swaps the README logo for graphify-card.png.
Worth a look
- build_merge references undefined _eff_root —
graphify/build.py:2031· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- global_add_many no longer dedups external-library nodes across repos —
graphify/global_graph.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Concurrent global_add_many calls can lose graph updates —
graphify/global_graph.py:170· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- '.d.ts' suffix check also matches legitimate TypeScript source files —
graphify/detect.py:903· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Any out/production directory is treated as generated output —
graphify/detect.py:918· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 5667 functions depend on the 3012 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 586 callers, 44 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
build_from_json()— 204 callers, 19 callees - new:
deduplicate_entities()— 75 callers, 22 callees - new:
detect()— 110 callers, 15 callees - new:
build_merge()— 68 callers, 14 callees - new:
to_obsidian()— 38 callers, 14 callees - new:
dispatch_command()— 4 callers, 125 callees - …and 139 more — each is listed as a finding
Verification — 5667 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 522ea96 (diverged from this PR's base — delta is approximate).
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: 5583 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
269 of 269 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— impact, 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— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— impact, full-run-safetytests/test_atomic_version_stamp.py— impact, full-run-safetytests/test_atomic_writes.py— impact, full-run-safetytests/test_backend_env_isolation.py— impact, changed-test, full-run-safetytests/test_backend_extras.py— impact, changed-test, full-run-safetytests/test_benchmark.py— impact, full-run-safetytests/test_benchmark_raw_graph.py— impact, full-run-safetytests/test_build.py— impact, changed-test, full-run-safetytests/test_build_merge_hyperedges_and_prune.py— impact, full-run-safetytests/test_build_merge_shrink_guard.py— impact, full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— impact, full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— impact, full-run-safetytests/test_chunking.py— impact, full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— impact, full-run-safetytests/test_claude_md.py— impact, full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— impact, full-run-safetytests/test_cli_global_add_many.py— impact, changed-test, full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— impact, full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— impact, full-run-safetytests/test_corrupt_graph_json.py— impact, full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— impact, full-run-safetytests/test_csharp_enum_members.py— impact, full-run-safety- … and 219 more
non-code file(s) changed (
CHANGELOG.md,README.md,docs/graphify-card.png,pyproject.toml,uv.lock) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md,README.md,pyproject.toml,tests/conftest.py,tests/fixtures/sample.rs) — 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.
Formal verification
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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: parameter `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
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: not verifiable: all 23 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 \_is\_noise\_dir.
The verifier did not have enough to check \_is\_noise\_dir, 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: parameter `parent` is annotated `'Path | None'` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_import\_php.
The verifier did not have enough to check \_import\_php, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify extract\_bash.
The verifier did not have enough to check extract\_bash, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.
The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, 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: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_collect\_python\_symbol\_resolution\_facts.
The verifier did not have enough to check \_collect\_python\_symbol\_resolution\_facts, 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: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_rust.
The verifier did not have enough to check extract\_rust, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_sql.
The verifier did not have enough to check extract\_sql, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify global\_add.
The verifier did not have enough to check global\_add, 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: parameter `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_pinned\_python (not a proof).
The verifier ran both versions of \_pinned\_python 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 install.
The verifier did not have enough to check install, 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: not verifiable: all 23 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 generate\_community\_labels.
The verifier did not have enough to check generate\_community\_labels, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
· 6 grounded finding(s) anchored inline below; 141 more finding(s) on lines outside this diff (see the check run).
| e["target"] = alias_map[tgt] | ||
|
|
||
|
|
||
| def _repoint_python_sibling_imports(paths, all_nodes, all_edges, root) -> None: |
There was a problem hiding this comment.
_repoint_python_sibling_imports()
high coupling complexity (Ca·Ce = 12).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -59,7 +59,7 @@ def _rust_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[ | |||
| }) | |||
|
|
|||
| def extract_rust(path: Path) -> dict: | |||
There was a problem hiding this comment.
extract_rust()
fans out to 6 callees (efferent coupling); 19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| "cross_repo_calls": 0} | ||
|
|
||
| # Load source graph | ||
| def global_add_many(sources: Sequence[tuple[Path, str]]) -> dict: |
There was a problem hiding this comment.
global_add_many()
fans out to 9 callees (efferent coupling); 14 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return {"results": results, "cross_repo_calls": cross_repo_calls} | ||
|
|
||
|
|
||
| def global_remove(repo_tag: str) -> int: |
There was a problem hiding this comment.
global_remove()
high coupling complexity (Ca·Ce = 25).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return tuple(seen) | ||
|
|
||
|
|
||
| def detect_backend() -> str | None: |
There was a problem hiding this comment.
detect_backend()
19 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| @pytest.mark.parametrize("suffix", ["ts", "js"]) | ||
| def test_js_namespace_reexport_import_targets_real_binding( |
There was a problem hiding this comment.
test_js_namespace_reexport_import_targets_real_binding()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #3438.
This PR batches global graph repository updates so the graph is loaded once, all changed repositories are composed, cross-repository member calls are resolved once, and the graph plus manifest are saved once per batch.
global_add()remains backward-compatible and delegates toglobal_add_many().Key changes:
global_add_many()with ordered repository replacement, duplicate-tag/source validation, unchanged-source skipping, and batch-level cross-repository resolution.Verification:
tests/test_global_graph.py: 30 relevant tests passed; three unrelated dedup tests require missingrapidfuzz.tests/test_cli_global_add_many.py: 4 passed.tests/test_global_add_tag_inference.py: 6 passed.tree_sitter_pythonis unavailable; it should execute in CI/an environment with the parser dependency installed.git diff --checkpassed andpython -m graphify update .completed.Coupling-health findings are advisory metrics and are intentionally not addressed by unrelated architectural restructuring.