fix(graphify): fall back to graph.links when graph.edges is absent (#2323)
Closes #2301 ## Root cause graphify's JSON output uses the key `links` for edges, but graphify.cjs reads `graph.edges` at four sites (buildAdjacencyMap, status edge_count, diff currentEdgeMap/snapshotEdgeMap, snapshot writer). Any graph produced by graphify itself therefore reported edge_count: 0 and adjacency maps with no entries. ## Fix Added `|| graph.links` fallback at all four read sites so both key names are accepted. The snapshot writer now also normalises to `edges` when saving, ensuring round-trips through the snapshot path use a consistent key. ## Test Added LINKS-01/02/03 regression tests covering buildAdjacencyMap, graphifyStatus edge_count, and graphifyDiff edge change detection with links-keyed input graphs. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -165,7 +165,7 @@ function buildAdjacencyMap(graph) {
|
||||
for (const node of (graph.nodes || [])) {
|
||||
adj[node.id] = [];
|
||||
}
|
||||
for (const edge of (graph.edges || [])) {
|
||||
for (const edge of (graph.edges || graph.links || [])) {
|
||||
if (!adj[edge.source]) adj[edge.source] = [];
|
||||
if (!adj[edge.target]) adj[edge.target] = [];
|
||||
adj[edge.source].push({ target: edge.target, edge });
|
||||
@@ -337,7 +337,7 @@ function graphifyStatus(cwd) {
|
||||
exists: true,
|
||||
last_build: stat.mtime.toISOString(),
|
||||
node_count: (graph.nodes || []).length,
|
||||
edge_count: (graph.edges || []).length,
|
||||
edge_count: (graph.edges || graph.links || []).length,
|
||||
hyperedge_count: (graph.hyperedges || []).length,
|
||||
stale: age > STALE_MS,
|
||||
age_hours: Math.round(age / (60 * 60 * 1000)),
|
||||
@@ -384,8 +384,8 @@ function graphifyDiff(cwd) {
|
||||
|
||||
// Diff edges (keyed by source+target+relation)
|
||||
const edgeKey = (e) => `${e.source}::${e.target}::${e.relation || e.label || ''}`;
|
||||
const currentEdgeMap = Object.fromEntries((current.edges || []).map(e => [edgeKey(e), e]));
|
||||
const snapshotEdgeMap = Object.fromEntries((snapshot.edges || []).map(e => [edgeKey(e), e]));
|
||||
const currentEdgeMap = Object.fromEntries((current.edges || current.links || []).map(e => [edgeKey(e), e]));
|
||||
const snapshotEdgeMap = Object.fromEntries((snapshot.edges || snapshot.links || []).map(e => [edgeKey(e), e]));
|
||||
|
||||
const edgesAdded = Object.keys(currentEdgeMap).filter(k => !snapshotEdgeMap[k]);
|
||||
const edgesRemoved = Object.keys(snapshotEdgeMap).filter(k => !currentEdgeMap[k]);
|
||||
@@ -454,7 +454,7 @@ function writeSnapshot(cwd) {
|
||||
version: 1,
|
||||
timestamp: new Date().toISOString(),
|
||||
nodes: graph.nodes || [],
|
||||
edges: graph.edges || [],
|
||||
edges: graph.edges || graph.links || [],
|
||||
};
|
||||
|
||||
const snapshotPath = path.join(cwd, '.planning', 'graphs', '.last-build-snapshot.json');
|
||||
|
||||
@@ -465,6 +465,17 @@ describe('buildAdjacencyMap', () => {
|
||||
assert.strictEqual(entry.edge.label, 'reads_from');
|
||||
assert.strictEqual(entry.edge.confidence, 'EXTRACTED');
|
||||
});
|
||||
|
||||
// LINKS-01: graphify emits 'links' key; reader must fall back to it
|
||||
test('falls back to graph.links when graph.edges is absent (LINKS-01)', () => {
|
||||
const graphWithLinks = {
|
||||
nodes: SAMPLE_GRAPH.nodes,
|
||||
links: SAMPLE_GRAPH.edges,
|
||||
};
|
||||
const adj = buildAdjacencyMap(graphWithLinks);
|
||||
assert.ok(adj['n1'].some(e => e.target === 'n2'), 'adjacency must traverse links');
|
||||
assert.ok(adj['n2'].some(e => e.target === 'n1'), 'reverse adjacency must work');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── seedAndExpand (TEST-01) ───────────────────────────────────────────────
|
||||
@@ -678,6 +689,19 @@ describe('graphifyStatus', () => {
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.hyperedge_count, 1);
|
||||
});
|
||||
|
||||
// LINKS-02: status edge_count must read graph.links when graph.edges is absent
|
||||
test('reports correct edge_count when graph uses links key (LINKS-02)', () => {
|
||||
enableGraphify(planningDir);
|
||||
const graphWithLinks = {
|
||||
nodes: SAMPLE_GRAPH.nodes,
|
||||
links: SAMPLE_GRAPH.edges,
|
||||
hyperedges: [],
|
||||
};
|
||||
writeGraphJson(planningDir, graphWithLinks);
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.edge_count, 5, 'edge_count must equal links array length');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── graphifyDiff (DIFF-01, DIFF-02) ──────────────────────────────────────
|
||||
@@ -770,6 +794,35 @@ describe('graphifyDiff', () => {
|
||||
assert.strictEqual(result.nodes.changed, 1, 'n1 label changed');
|
||||
assert.strictEqual(result.edges.changed, 1, 'edge confidence changed');
|
||||
});
|
||||
|
||||
// LINKS-03: diff must handle links key in both current and snapshot (LINKS-03)
|
||||
test('detects edge changes when graphs use links key (LINKS-03)', () => {
|
||||
enableGraphify(planningDir);
|
||||
const snapshot = {
|
||||
nodes: [
|
||||
{ id: 'n1', label: 'AuthService', description: 'Auth', type: 'service' },
|
||||
{ id: 'n2', label: 'UserModel', description: 'User', type: 'model' },
|
||||
],
|
||||
links: [
|
||||
{ source: 'n1', target: 'n2', label: 'reads_from', confidence: 'INFERRED' },
|
||||
],
|
||||
};
|
||||
const current = {
|
||||
nodes: [
|
||||
{ id: 'n1', label: 'AuthService', description: 'Auth', type: 'service' },
|
||||
{ id: 'n2', label: 'UserModel', description: 'User', type: 'model' },
|
||||
],
|
||||
links: [
|
||||
{ source: 'n1', target: 'n2', label: 'reads_from', confidence: 'EXTRACTED' },
|
||||
],
|
||||
};
|
||||
writeSnapshotJson(planningDir, snapshot);
|
||||
writeGraphJson(planningDir, current);
|
||||
const result = graphifyDiff(tmpDir);
|
||||
assert.strictEqual(result.edges.changed, 1, 'edge confidence change must be detected via links key');
|
||||
assert.strictEqual(result.edges.added, 0);
|
||||
assert.strictEqual(result.edges.removed, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── graphifyBuild (BUILD-01, BUILD-02, TEST-02) ────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user