| name | graph-dependency-resolution |
| description | Graph algorithms and dependency resolution for AI agent systems. DAG construction for action gate ordering, cycle detection in skill/rule graphs, BFS/DFS traversal, semver range resolution for version conflicts, topological sort for execution ordering, and PageRank-based rule importance scoring. Sources: dagrejs/graphlib, trekhleb/javascript-algorithms, npm/node-semver, sindresorhus/toposort, asv/pagerank. |
/graph-dependency-resolution
When to Use
- "Which rule/skill depends on which — what's the safe load order?"
- Cycle detection: skill A depends on B depends on A → deadlock
- Semver range conflicts: two skills require different versions of the same package
- Rank rules by reference frequency to find the most critical ones
- Find shortest path between two gates (optimal middleware chain)
Do NOT use for
- Linear ordered lists with no dependencies (just sort by priority number)
- Single-hop lookups (use a Map)
DAG: Directed Acyclic Graph (graphlib)
import { Graph, alg } from '@dagrejs/graphlib'
const gateGraph = new Graph({ directed: true, multigraph: false, compound: false })
gateGraph.setNode('L0', { label: 'audit-log', tier: 0 })
gateGraph.setNode('L1', { label: 'anti-evasion', tier: 1 })
gateGraph.setNode('L2', { label: 'shell-sanitize', tier: 2 })
gateGraph.setNode('L3', { label: 'network-egress', tier: 3 })
gateGraph.setNode('tool-exec', { label: 'execute', tier: 4 })
gateGraph.setEdge('L0', 'L1')
gateGraph.setEdge('L1', 'L2')
gateGraph.setEdge('L2', 'L3')
gateGraph.setEdge('L3', 'tool-exec')
const cycles = alg.findCycles(gateGraph)
if (cycles.length > 0) {
throw new Error(`Dependency cycle detected: ${cycles.map(c => c.join('→')).join(', ')}`)
}
const entryPoints = gateGraph.sources()
const terminals = gateGraph.sinks()
const reachable = alg.dijkstra(gateGraph, 'L0')
Cycle Detection + BFS/DFS (javascript-algorithms)
function bfs(graph: Map<string, string[]>, start: string): Map<string, number> {
const distances = new Map<string, number>([[start, 0]])
const queue = [start]
while (queue.length) {
const node = queue.shift()!
const neighbors = graph.get(node) ?? []
for (const n of neighbors) {
if (!distances.has(n)) {
distances.set(n, distances.get(node)! + 1)
queue.push(n)
}
}
}
return distances
}
function hasCycle(graph: Map<string, string[]>): boolean {
const visited = new Set<string>()
inStack = <>()
(): {
visited.(node)
inStack.(node)
( neighbor graph.(node) ?? []) {
(!visited.(neighbor) && (neighbor))
(inStack.(neighbor))
}
inStack.(node)
}
( node graph.()) {
(!visited.(node) && (node))
}
}
() {
dist = <, >([[start, ]])
: <{ : ; : }> = [{ : start, : }]
(pq.) {
pq.( a. - b.)
{ node, d } = pq.()!
(d > (dist.(node) ?? ))
( { to, weight } graph.(node) ?? []) {
newDist = d + weight
(newDist < (dist.(to) ?? )) {
dist.(to, newDist)
pq.({ : to, : newDist })
}
}
}
dist
}
Topological Sort: Execution Order (toposort)
import toposort from 'toposort'
const edges: [string, string][] = [
['validate-manifest', 'build-release'],
['run-tests', 'build-release'],
['lint', 'run-tests'],
['typecheck', 'run-tests'],
['install-deps', 'lint'],
['install-deps', 'typecheck'],
]
const executionOrder = toposort(edges).reverse()
const allNodes = ['install-deps', 'lint', 'typecheck', 'run-tests', 'validate-manifest', 'build-release', 'notify']
const fullOrder = toposort.array(allNodes, edges).reverse()
try {
toposort(edges)
} catch (e) {
if ((e )..()) {
()
}
}
Semver Range Resolution (node-semver)
import semver from 'semver'
semver.satisfies('1.3.45', '>=1.3.0 <2.0.0')
semver.satisfies('2.0.0', '>=1.3.0 <2.0.0')
const versions = ['1.2.0', '1.3.0', '1.3.44', '2.0.0', '2.1.0']
semver.maxSatisfying(versions, '^1.3.0')
semver.minSatisfying(versions, '^2.0.0')
function resolveConflicts(
requirements: Array<{ skill: string; range: string }>
): string | null {
const intersection = requirements
.map(r => semver.validRange(r.range))
.filter(Boolean) as string[]
return semver.(
versions,
intersection.()
)
}
v = semver.()!
semver.()?.
PageRank: Rule Importance Scoring
function pageRank(
graph: Map<string, string[]>,
damping = 0.85,
iterations = 50,
tolerance = 1e-6
): Map<string, number> {
const nodes = [...graph.keys()]
const N = nodes.length
let ranks = new Map(nodes.map(n => [n, 1 / N]))
for (let i = 0; i < iterations; i++) {
const next = new Map<string, number>()
let delta = 0
for (const node of nodes) {
let sum = 0
for (const [src, targets] of graph) {
if (targets.includes(node)) {
sum += (ranks.get(src) ?? 0) / targets.
}
}
newRank = ( - damping) / N + damping * sum
next.(node, newRank)
delta += .(newRank - (ranks.(node) ?? ))
}
ranks = next
(delta < tolerance)
}
([...ranks.()].( b[] - a[]))
}
ruleRefs = ([
[, [, ]],
[, [, ]],
[, []],
[, [, ]],
[, []],
])
importance = (ruleRefs)
Anti-Fake-Pass Checklist
❌ No cycle check before executing dependency graph (deadlock on first cycle)
❌ DFS without inStack set (can't distinguish visited from in-current-path)
❌ toposort result not .reverse()'d (execution order is backwards)
❌ semver.satisfies() on un-validated range string (throws on malformed range)
❌ Space-joined semver ranges confused with OR union (space = AND, || = OR)
❌ PageRank without convergence check (fixed iteration on large graphs = wrong)
❌ graphlib.alg.findCycles() skipped before graph traversal (silent infinite loop)