| name | scalpel-graph-engine |
| description | Use when working on scalpel-core, scalpel-proto, Code Genome graph structure, hybrid merge logic, reachability algorithms, confidence classification, or edge resolution |
Graph Engine Expert Knowledge
Core Principle
The Code Genome is the single source of truth. Every output is a projection of this graph. A wrong edge is worse than a missing edge — it poisons all downstream projections (pruning, AI context, SBOM, visualization).
When to Use
- Modifying any file in
crates/scalpel-core/ or crates/scalpel-proto/
- Working on hybrid merge logic (combining runtime + static edges)
- Modifying reachability algorithms (BFS, SCC)
- Changing confidence classification rules
- Working on edge resolution or import index
- Debugging incorrect node classifications
Domain Model
Runtime Tracing → TraceEvents → Runtime Graph (high confidence, incomplete)
↓
Static Analysis → StaticResult → Hybrid Merge → Code Genome → All Outputs
↑
Import Index (per-file, short_name → full_path)
Confidence Hierarchy
ConfirmedUsed > StaticallyReachable > ConditionallyReachable > Unreachable
↑
MustKeep (ErrorHandler, SideEffect, LifecycleHook) │
— never pruned regardless of reachability │
Only this is prunable
Only Unreachable is prunable. Everything else stays.
Decision Trees
Hybrid Merge — Edge Conflict Resolution
digraph merge_conflict {
"Edge exists in both runtime and static?" [shape=diamond];
"Use runtime confidence (higher signal)" [shape=box style=filled fillcolor=lightgreen];
"Edge in runtime only?" [shape=diamond];
"ConfirmedUsed" [shape=box style=filled fillcolor=lightgreen];
"Edge in static only?" [shape=diamond];
"StaticallyReachable" [shape=box style=filled fillcolor=lightyellow];
"Runtime says unused, static says reachable?" [shape=diamond];
"KEEP as StaticallyReachable" [shape=box style=filled fillcolor=lightyellow];
"Edge exists in both runtime and static?" -> "Use runtime confidence (higher signal)" [label="yes"];
"Edge exists in both runtime and static?" -> "Edge in runtime only?" [label="no"];
"Edge in runtime only?" -> "ConfirmedUsed" [label="yes"];
"Edge in runtime only?" -> "Edge in static only?" [label="no"];
"Edge in static only?" -> "StaticallyReachable" [label="yes"];
"Edge in static only?" -> "Runtime says unused, static says reachable?" [label="no"];
"Runtime says unused, static says reachable?" -> "KEEP as StaticallyReachable" [label="always keep"];
}
Rule: Runtime may not have exercised every path. Static reachability overrides runtime absence.
Reachability Classification (classify_all)
1. Find all entrypoints (ConfirmedUsed nodes)
2. Multi-source BFS following OUTGOING edges only
3. Mark reached nodes: upgrade to StaticallyReachable
4. Find SCCs (Tarjan's): if ANY node in SCC is reachable → ALL stay
5. Mark unreached remainder: Unreachable
6. Override: MustKeep nodes NEVER downgraded regardless of step 5
Import Index — 3-Tier Edge Resolution
Building edge from caller → callee:
Tier 1: find_node(caller_file, callee_name)
→ Same-file lookup. Highest confidence.
Tier 2: import_index[(caller_file, short_name)] → resolve full path
→ Cross-file via import statement. High confidence.
Tier 3: find_nodes_by_suffix(short_name)
→ Exactly 1 match? Use it. Medium confidence.
→ 0 matches? Likely macro/dynamic. Log, skip.
→ 2+ matches? Ambiguous. Do NOT guess. Skip.
Known Traps
1. StableGraph, not DiGraph
Node indices must survive removals during incremental updates. DiGraph compacts indices on removal, breaking all stored NodeIndex references. Always use petgraph::stable_graph::StableGraph.
2. Interned strings (Spur)
Never store raw String in NodeData. Always use lasso::Spur. Comparison is integer equality (fast), not string comparison. Creating a Spur requires access to the ThreadedRodeo — don't try to construct one manually.
3. SCC atomicity
Circular dependency groups (A→B→C→A) are atomic. If A is reachable, B and C MUST stay. Pruning B breaks the cycle and crashes A. classify_all handles this via Tarjan's, but any code that marks nodes unreachable independently (bypassing classify_all) will violate this.
4. Import edge bug (latent)
find_node(&file, "") in import edge creation always returns None (no empty-name nodes exist). Zero import edges are ever created. The 3-tier call resolution compensates, but this is a latent bug at lines 166-168 of merge.rs.
5. body_hash staleness
xxh3 hash for change detection between runs. If hash computation logic changes, ALL previous genomes appear "changed" — triggering full re-analysis even when nothing changed. Keep hash computation stable across versions.
Verification Protocol
After ANY change to scalpel-core or scalpel-proto:
cargo test -p scalpel-core -p scalpel-proto
cargo bench -p scalpel-core — verify BFS <200ms for 100K nodes
- Property tests pass: reachability is monotonic (adding edges never shrinks reachable set)
- Property tests pass: SCC detection is complete
- Re-run self-analysis, compare unreachable count vs baseline (37 / 3.4%)
- Use MCP
trace_call_chain to verify specific cross-crate paths resolve
If changing Confidence enum: check ALL downstream consumers — export, prune, classify, MCP tools.
Red Flags — STOP
- Modifying merge logic without re-running self-analysis
- Using
DiGraph instead of StableGraph
- Adding edges for ambiguous resolutions (2+ candidates)
- Assuming runtime-unseen = unused (static reachability may save it)
- Changing Confidence enum without updating ALL downstream
- Marking nodes unreachable without going through
classify_all
- Bypassing SCC check for "optimization"
Quick Reference
| Operation | Rule |
|---|
| Add edge | Wrong edge > missing edge. Don't guess. |
| Ambiguous resolution | Skip. Log as unresolved. |
| Runtime says unused | Check static reachability before downgrading |
| SCC member | If any reachable, all stay. Atomic. |
| MustKeep node | Never downgrade. ErrorHandler, SideEffect, LifecycleHook. |
| New node field | Check serialization (genome-export.json), export.rs, proto |
| Performance | BFS <200ms for 100K nodes. Benchmark after changes. |
| Confidence change | Grep all consumers: export, prune, classify, MCP, CLI |