aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
208 lines • 6.86 kB
JavaScript
/**
* Cross-Graph Set Queries
*
* Provides neighbor lookup and set operations (intersection, union, difference)
* across dependency graphs. Supports typed edge filtering and cross-graph joins.
*
* @implements #725
* @source @src/artifacts/types.ts
* @tests @test/unit/artifacts/graph-query.test.ts
*/
import { normalizeEdges } from './types.js';
import { loadGraphIndexFile } from './index-reader.js';
/**
* Get neighbors of a node in a dependency graph.
*
* @param graph - Loaded dependency graph
* @param node - Node path to look up
* @param direction - "in" (upstream), "out" (downstream), or "both"
* @param edgeType - Optional filter by edge type
* @returns Array of neighbor paths
*/
export function getNeighbors(graph, node, direction = 'both', edgeType) {
const entry = graph[node];
if (!entry)
return [];
const results = new Set();
if (direction === 'in' || direction === 'both') {
const edges = normalizeEdges(entry.upstream);
for (const edge of edges) {
if (!edgeType || edge.type === edgeType) {
results.add(edge.path);
}
}
}
if (direction === 'out' || direction === 'both') {
const edges = normalizeEdges(entry.downstream);
for (const edge of edges) {
if (!edgeType || edge.type === edgeType) {
results.add(edge.path);
}
}
}
return [...results];
}
/**
* Resolve a node identifier to a graph key.
*
* Supports:
* - Exact path match ("documentation/citations/REF-008-citations.md")
* - REF-XXX shorthand — finds the first key containing the REF identifier
* - Partial path match — finds the first key ending with the given string
*/
export function resolveNode(graph, node) {
// Exact match
if (graph[node])
return node;
// REF-XXX shorthand
if (/^REF-\d+$/.test(node)) {
const key = Object.keys(graph).find(k => k.includes(node));
return key ?? null;
}
// Partial path match
const key = Object.keys(graph).find(k => k.endsWith(node));
return key ?? null;
}
/**
* Set intersection of two arrays
*/
export function setIntersection(a, b) {
const setB = new Set(b);
return a.filter(x => setB.has(x));
}
/**
* Set union of two arrays
*/
export function setUnion(a, b) {
return [...new Set([...a, ...b])];
}
/**
* Set difference: elements in a but not in b
*/
export function setDifference(a, b) {
const setB = new Set(b);
return a.filter(x => !setB.has(x));
}
/**
* Load a dependency graph for a given graph type
*/
function loadGraph(cwd, graphType) {
return loadGraphIndexFile(cwd, 'dependencies.json', graphType);
}
/**
* Execute the `neighbors` subcommand
*/
export async function showNeighbors(cwd, options) {
const { graph: graphType, node, direction = 'both', edgeType, json = false } = options;
const graph = loadGraph(cwd, graphType);
if (!graph) {
console.error(`Error: No index found for graph '${graphType}'.`);
console.log("Run 'aiwg index build --graph " + graphType + "' first.");
process.exit(1);
}
const resolved = resolveNode(graph, node);
if (!resolved) {
console.error(`Error: Node '${node}' not found in graph '${graphType}'.`);
const keys = Object.keys(graph);
// Suggest similar nodes
const similar = keys.filter(k => k.toLowerCase().includes(node.toLowerCase())).slice(0, 5);
if (similar.length > 0) {
console.log('Similar nodes:');
for (const s of similar)
console.log(` ${s}`);
}
process.exit(1);
}
const neighbors = getNeighbors(graph, resolved, direction, edgeType);
if (json) {
console.log(JSON.stringify({
graph: graphType,
node: resolved,
direction,
edgeType: edgeType ?? null,
neighbors,
count: neighbors.length,
}, null, 2));
}
else {
const dirLabel = direction === 'in' ? 'IN' : direction === 'out' ? 'OUT' : 'ALL';
const typeLabel = edgeType ? ` [${edgeType}]` : '';
console.log(`Neighbors of ${resolved} (${dirLabel}${typeLabel}):`);
if (neighbors.length === 0) {
console.log(' (none)');
}
else {
for (const n of neighbors) {
console.log(` ${n}`);
}
}
console.log(`\nTotal: ${neighbors.length}`);
}
}
/**
* Execute a set query (intersection, union, difference) on neighbor sets
*/
export async function executeSetQuery(cwd, options) {
const { graph: graphType, op, nodeA, nodeB, direction = 'in', edgeType, json = false } = options;
const graph = loadGraph(cwd, graphType);
if (!graph) {
console.error(`Error: No index found for graph '${graphType}'.`);
process.exit(1);
}
const resolvedA = resolveNode(graph, nodeA);
const resolvedB = resolveNode(graph, nodeB);
if (!resolvedA) {
console.error(`Error: Node '${nodeA}' not found in graph '${graphType}'.`);
process.exit(1);
}
if (!resolvedB) {
console.error(`Error: Node '${nodeB}' not found in graph '${graphType}'.`);
process.exit(1);
}
const neighborsA = getNeighbors(graph, resolvedA, direction, edgeType);
const neighborsB = getNeighbors(graph, resolvedB, direction, edgeType);
let result;
switch (op) {
case 'intersection':
result = setIntersection(neighborsA, neighborsB);
break;
case 'union':
result = setUnion(neighborsA, neighborsB);
break;
case 'difference':
result = setDifference(neighborsA, neighborsB);
break;
}
if (json) {
console.log(JSON.stringify({
graph: graphType,
op,
nodeA: resolvedA,
nodeB: resolvedB,
direction,
edgeType: edgeType ?? null,
result,
count: result.length,
setA_count: neighborsA.length,
setB_count: neighborsB.length,
}, null, 2));
}
else {
const dirLabel = direction === 'in' ? 'IN' : 'OUT';
const typeLabel = edgeType ? ` [${edgeType}]` : '';
console.log(`${op.toUpperCase()}(${dirLabel}${typeLabel}):`);
console.log(` A: ${resolvedA} (${neighborsA.length} neighbors)`);
console.log(` B: ${resolvedB} (${neighborsB.length} neighbors)`);
console.log('');
if (result.length === 0) {
console.log(' (empty set)');
}
else {
for (const r of result) {
console.log(` ${r}`);
}
}
console.log(`\nResult: ${result.length} nodes`);
}
}
//# sourceMappingURL=graph-query.js.map