Conversation
…Graphify-Labs#3782) ensure_named_node only resolved names already in seen_ids, so a struct, enum or trait used above its declaration in the same file got a sourceless stub: fn before(s: &Sink) {} struct Sink { n: u32 } The corpus-level rewire folds that stub back onto the real struct only when the name is unique across the corpus. Once another file declares a `Sink` as well, the reference stays on the stub, and `affected` or `explain` on the local struct miss that user. Pre-scan the struct/enum/trait names the file declares, as the Verilog, SQL and Bash extractors already pre-scan their definitions, and let ensure_named_node resolve against them, so a use above a declaration resolves exactly as a use below it already does. Only these three items are pre-registered: walk() never makes a node of a `type` alias or `union`, so registering one would leave its references to be dropped by the final dangling-edge filter. The scan never descends further than walk(), so every registered id ends up as a node. Adds regression coverage for a struct, enum and trait used above their declarations, a type declared in an inline module, the two-file ambiguous case (cold and warm cache), and the items that must not resolve a forward reference (type alias, union, function body, const and static initializers). Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
|
Thanks for the pull request, @Yyunozor. 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.
Copilot review overview
🟡 Changes recommended
Unresolved identifier-collision and forward-reference import-edge issues remain.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Fixes Rust forward references to local struct, enum, and trait declarations.
Changes:
- Pre-scans eligible local type declarations.
- Adds regression tests for forward references and scan boundaries.
- Preserves exclusions for unsupported declarations and scopes.
| File | Description |
|---|---|
tests/test_multilang.py |
Adds forward-reference regression coverage. |
graphify/extractors/rust.py |
Pre-registers local type declarations during extraction. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if nid in seen_ids or nid in local_type_ids: | ||
| return nid |
There was a problem hiding this comment.
Thanks. The order described (fn handle, then the reference, then struct Handle) goes through seen_ids and resolves to handle() exactly as on v8; this PR leaves it unchanged. The variant with the reference above both declarations was introduced here. Fixed in 93fa872: a type is pre-registered only when no other item (functions, consts, statics, other types, impl blocks) shares its casefolded id, and by its exact name. Regression tests added.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
Resolves forward type references in the Rust extractor by pre-scanning each file for struct, enum, and trait declarations so a type used above its declaration binds to the local node instead of a sourceless stub that only got folded back when the name was unique across the corpus (#3782). The pre-scan (_scan_type_items) deliberately stops at the same boundaries walk() does — skipping function/const/static initializer bodies, type aliases, and unions — so it never registers an id that won't become a node, leaving references to those cases with their stubs intact.
No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 164 functions depend on the 160 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_rust()— 28 callers, 7 callees - new:
walk()— 1 callers, 9 callees
Verification — 164 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: 164 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
2 of 304 test file(s) selected (1%) via static blast radius.
tests/test_multilang.py— impact, changed-testtests/test_rust_self_member_calls.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.
Docs that may be stale (advisory)
CHANGELOG.md§ 0.9.44 (2026-08-15) (lines 346-358): references changed symbolsbodyCHANGELOG.md§ 0.9.28 (2026-07-27) (lines 521-530): references changed symbolsbodyCHANGELOG.md§ 0.8.47 (2026-06-24) (lines 993-1001): references changed symbolsbodydocs/docker-mcp-sqlite.md§ Smoke test (lines 59-75): references changed symbolsbodygraphify/skill-agents.md§ Step 1 - Ensure graphify is installed (lines 65-112): references changed symbolsbodygraphify/skill-aider.md§ Step 1 - Ensure graphify is installed (lines 59-106): references changed symbolsbodygraphify/skill-amp.md§ Step 1 - Ensure graphify is installed (lines 65-112): references changed symbolsbodygraphify/skill-claw.md§ Step 1 - Ensure graphify is installed (lines 65-112): references changed symbolsbodygraphify/skill-codex.md§ Step 1 - Ensure graphify is installed (lines 65-112): references changed symbolsbodygraphify/skill-copilot.md§ Step 1 - Ensure graphify is installed (lines 65-112): references changed symbolsbody
…and 10 more.
· 2 more finding(s) on lines outside this diff (see the check run).
…des with another item (Graphify-Labs#3782) Ids are casefolded, so `fn handle`, `const HANDLE`, `struct HANDLE` or an `impl` for `HANDLE` share the id of `struct Handle`, and walk() keeps whichever item comes first. The pre-scan registered the type id regardless, so a reference placed above the declarations was bound to that shared id and could end on the function or on the other item, where v8's extractor left a sourceless stub. For the same reason a `HANDLE` reference (a `type HANDLE` alias, or an imported name) placed above a local `struct Handle` was bound to the struct. Only pre-register a type when every file-level item behind its id is that same type (`#[cfg]` twins count once), and match it by its exact name. An `impl` block counts as the type it names, since walk() gives it the node of its type text; methods keep impl-qualified ids and are not counted. Otherwise the forward reference keeps its stub. A reference placed after an item that already owns the id still resolves through seen_ids, unchanged from v8. Adds regression cases for a type colliding with a function (also in an `extern` block), a const, a static, another type, an `impl` for another type and a type alias, and controls where the id is not shared: another function name, `#[cfg]` twins, an `impl` for the type itself, a method named like the type, and a generic type only an `impl` names. Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)

What does this PR do?
Fixes #3782.
A Rust struct, enum or trait used above its declaration got a sourceless stub:
ensure_named_nodeonly resolves names already inseen_ids._rewire_unique_stub_nodesfolds it back only for a corpus-unique label, so a same-named type in another file leaves the edge on the stub.extract_rustnow pre-scans the file'sstruct_item/enum_item/trait_itemnames beforewalk()(the Verilog, SQL and Bash pre-scan idiom), andensure_named_noderesolves against them.typealias orunionwithout animplnever becomes a node, so its references would be dropped as dangling.walk()(no function, static or const bodies), so every registered id becomes a node.origin_filein the rewire) would cover other extractors too; a naturalextract.pyfollow-up.Type of change
Verification & Invariants
Invariant: a reference above a same-file struct, enum or trait, spelled the same and alone on its casefolded id, resolves like one below it; anything else keeps v8's per-file result.
Casefold collisions fail closed: when
fn handle,const HANDLE,struct HANDLEorimpl Tr for HANDLEsharesstruct Handle's id, or the reference spellsHANDLE, it keeps the stub v8's extractor gives it; references after the id's owner are unchanged.Known limitation: resolution is flat per file and keeps only the last path segment, inheriting its errors: qualified paths (
io::Error,imp::Child),Self::Xwithout alias, generic parameters,modscopes. On tokio 2 of 85 moved references are wrong (imp::Child); on 351 crates of a local cargo registry, roughly 85 of 2,279 (201 in the suspect classes). Pre-scanning only unqualified names loses 57+ correct ones. Failing closed withholds 29 likely correct links (e.g. type declared before its colliding function).Side effect in the final graph: some targets reached a real node on v8 only via a forward-reference stub sharing their id, later folded onto the type; they now stay unresolved.
imports_from: 53 wrong miouse …::iobindings to a privateIo, 12 tokio ones toScheduler, 5 correct ones (e.g. socket2pub use sys::SockFilter). References: 8 wrong jobserverHANDLEbindings tostruct Handle, the only crate that drops (77 → 69); +2,271 = 2,279 moved − 8.How was this tested?
Removing any of these checks (bodies, modules, collisions,
impl, exact name) turns a new test red.Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments.