Adding Derived Edges in Grafema Orchestrator
SkillDev toolsPattern for adding derived edge types in the Grafema Rust orchestrator (main.rs). Use when: (1) need to create new graph edges derived from existing edges (e.g., MODULE-level DEPENDS_ON from IMPORTS_FROM), (2) Datalog joins on large edge sets time out, (3) adding enrichment steps that combine data from multiple resolver outputs, (4) need to understand how resolver outputs flow through the orchestrator pipeline.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Adding Derived Edges in Grafema Orchestrator skill
What this skill tells your AI
The instructions your AI receives, as published by disentinel/grafema in .claude/skills/grafema-orchestrator-derived-edges/SKILL.md and read by ahel’s review.
Problem
You need to add a new edge type that is derived from existing edges created by resolvers. For example, MODULE->MODULE DEPENDS_ON edges derived from IMPORTS_FROM edges that connect IMPORT_BINDINGs to their target nodes (CLASS, FUNCTION, MODULE).
Context / Trigger Conditions
- Need to create graph edges that aggregate or transform existing edges
- Datalog joins involving large edge sets (CONTAINS, IMPORTS_FROM) time out
- New edge type requires data from multiple resolver outputs (JS, Haskell, Rust, etc.)
Why NOT Datalog
A Datalog query like:
violation(SrcMod, DstMod) :-
edge(Src, Dst, "IMPORTS_FROM"),
edge(SrcMod, Src, "CONTAINS"),
node(SrcMod, "MODULE"),
edge(DstMod, Dst, "CONTAINS"),
node(DstMod, "MODULE").
Times out on real codebases (~1,964 IMPORTS_FROM edges × CONTAINS joins). The RFDB Datalog engine is not optimized for multi-way joins at this scale.
Solution
Derive edges in-memory in the orchestrator after all resolvers complete.
Step 1: Declare a collector before resolution blocks
// Before step 8 in main.rs
let mut all_imports_from_edges: Vec<(String, String)> = Vec::new();
Step 2: Build index from analysis results
FileAnalysis has file and module_id fields — use them directly:
let file_to_module: HashMap<String, String> = results
.iter()
.filter_map(|r| r.analysis.as_ref())
.map(|a| (a.file.clone(), a.module_id.clone()))
.collect();
Step 3: Collect edges from each resolver output
After each import-related resolver commits to RFDB, collect its IMPORTS_FROM edges:
for edge in &import_output.edges {
if edge.edge_type == "IMPORTS_FROM" {
all_imports_from_edges.push((edge.src.clone(), edge.dst.clone()));
}
}
Do this for ALL language resolvers: JS, Haskell, Rust, Java, Kotlin, Python, JVM cross.
Step 4: Extract file from semantic ID
Semantic IDs follow the pattern path/to/file.ts->TYPE->name[scope].
File path is always the first segment before ->:
let src_file = src_id.split("->").next().unwrap_or("");
Step 5: Derive and deduplicate
let mut depends_on_pairs: HashSet<(String, String)> = HashSet::new();
for (src_id, dst_id) in &all_imports_from_edges {
let src_file = src_id.split("->").next().unwrap_or("");
let dst_file = dst_id.split("->").next().unwrap_or("");
if let (Some(src_mod), Some(dst_mod)) =
(file_to_module.get(src_file), file_to_module.get(dst_file))
{
if src_mod != dst_mod {
depends_on_pairs.insert((src_mod.clone(), dst_mod.clone()));
}
}
}
Step 6: Commit to RFDB
let wire_edges: Vec<rfdb::WireEdge> = depends_on_pairs.iter().map(|(src, dst)| {
rfdb::WireEdge {
src: src.clone(),
dst: dst.clone(),
edge_type: "DEPENDS_ON".to_string(),
metadata: Some(format!(
r#"{{"_source":"module-dependencies","_generation":{generation}}}"#
)),
}
}).collect();
rfdb.commit_batch(&[], &[], &wire_edges, false).await?;
Key Constraints
-
Resolvers receive nodes, not edges — a Haskell/JS resolver cannot query existing edges. Derived edges MUST be computed in the orchestrator (Rust) or via batch-mode plugin.
-
Resolution outputs are scoped — each resolver's output exists only within its
matcharm. Collect edges immediately after each commit, before the pool shuts down. -
Metadata stamping — use
_sourceand_generationfields. The orchestrator'sgc::stamp_edge_metadata()handles this for resolver outputs, but for manually constructed WireEdges, stamp metadata in the JSON string directly. -
Placement — add derived edge computation AFTER all resolvers (including user plugins) but BEFORE the summary println. This ensures all source data is available.
Verification
cargo build --release # Must compile clean
cargo test # All tests pass
After re-analysis:
query_graph('violation(X) :- edge(X, _, "DEPENDS_ON").')
# Should return MODULE nodes
Notes
- For ~2,000 IMPORTS_FROM edges and ~350 modules, derivation is instant (< 1ms)
- The
file_to_modulemap comes fromFileAnalysis.module_id, not from RFDB queries - Self-edges (src_mod == dst_mod) are filtered out
- HashSet deduplication handles multiple imports between the same module pair
Signals
- GitHub stars
- 36
- Forks
- 3
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
grafema-orchestrator-derived-edges- Source
- github.com/disentinel/grafema