mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
b75235919e
Move packages/{core,dashboard,skill} into understand-anything-plugin/ to
conform to the Claude Code plugin format. Add .claude-plugin/marketplace.json
for plugin discovery. Update workspace config and docs accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
199 lines
5.3 KiB
TypeScript
199 lines
5.3 KiB
TypeScript
import type {
|
|
KnowledgeGraph,
|
|
GraphNode,
|
|
GraphEdge,
|
|
Layer,
|
|
} from "@understand-anything/core";
|
|
|
|
export interface DiffContext {
|
|
projectName: string;
|
|
changedFiles: string[];
|
|
changedNodes: GraphNode[];
|
|
affectedNodes: GraphNode[];
|
|
impactedEdges: GraphEdge[];
|
|
affectedLayers: Layer[];
|
|
unmappedFiles: string[];
|
|
}
|
|
|
|
/**
|
|
* Map a list of changed file paths to knowledge graph nodes and
|
|
* identify the ripple effect (affected nodes, layers, edges).
|
|
*/
|
|
export function buildDiffContext(
|
|
graph: KnowledgeGraph,
|
|
changedFiles: string[],
|
|
): DiffContext {
|
|
const { nodes, edges, layers } = graph;
|
|
|
|
const changedNodeIds = new Set<string>();
|
|
const unmappedFiles: string[] = [];
|
|
|
|
for (const file of changedFiles) {
|
|
let mapped = false;
|
|
for (const node of nodes) {
|
|
if (node.filePath === file) {
|
|
changedNodeIds.add(node.id);
|
|
mapped = true;
|
|
}
|
|
}
|
|
if (!mapped) {
|
|
unmappedFiles.push(file);
|
|
}
|
|
}
|
|
|
|
// Also include "contains" children of changed file nodes
|
|
for (const edge of edges) {
|
|
if (edge.type === "contains" && changedNodeIds.has(edge.source)) {
|
|
changedNodeIds.add(edge.target);
|
|
}
|
|
}
|
|
|
|
const changedNodes = nodes.filter((n) => changedNodeIds.has(n.id));
|
|
|
|
// Find affected nodes: 1-hop neighbors of changed nodes (excluding already changed)
|
|
const affectedNodeIds = new Set<string>();
|
|
const impactedEdges: GraphEdge[] = [];
|
|
|
|
for (const edge of edges) {
|
|
const sourceChanged = changedNodeIds.has(edge.source);
|
|
const targetChanged = changedNodeIds.has(edge.target);
|
|
|
|
if (sourceChanged || targetChanged) {
|
|
impactedEdges.push(edge);
|
|
if (sourceChanged && !changedNodeIds.has(edge.target)) {
|
|
affectedNodeIds.add(edge.target);
|
|
}
|
|
if (targetChanged && !changedNodeIds.has(edge.source)) {
|
|
affectedNodeIds.add(edge.source);
|
|
}
|
|
}
|
|
}
|
|
|
|
const affectedNodes = nodes.filter((n) => affectedNodeIds.has(n.id));
|
|
|
|
const allImpactedIds = new Set([...changedNodeIds, ...affectedNodeIds]);
|
|
const affectedLayers = layers.filter((layer) =>
|
|
layer.nodeIds.some((id) => allImpactedIds.has(id)),
|
|
);
|
|
|
|
return {
|
|
projectName: graph.project.name,
|
|
changedFiles,
|
|
changedNodes,
|
|
affectedNodes,
|
|
impactedEdges,
|
|
affectedLayers,
|
|
unmappedFiles,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format the diff analysis as structured markdown for LLM or human consumption.
|
|
*/
|
|
export function formatDiffAnalysis(ctx: DiffContext): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push(`# Diff Analysis: ${ctx.projectName}`);
|
|
lines.push("");
|
|
|
|
lines.push("## Changed Components");
|
|
lines.push("");
|
|
if (ctx.changedNodes.length === 0) {
|
|
lines.push("No mapped components found for changed files.");
|
|
} else {
|
|
for (const node of ctx.changedNodes) {
|
|
lines.push(`- **${node.name}** (${node.type}) — ${node.summary}`);
|
|
if (node.filePath) lines.push(` - File: \`${node.filePath}\``);
|
|
lines.push(` - Complexity: ${node.complexity}`);
|
|
}
|
|
}
|
|
lines.push("");
|
|
|
|
lines.push("## Affected Components");
|
|
lines.push("");
|
|
if (ctx.affectedNodes.length === 0) {
|
|
lines.push("No downstream impact detected.");
|
|
} else {
|
|
lines.push(
|
|
"These components are connected to changed code and may need attention:",
|
|
);
|
|
lines.push("");
|
|
for (const node of ctx.affectedNodes) {
|
|
lines.push(`- **${node.name}** (${node.type}) — ${node.summary}`);
|
|
}
|
|
}
|
|
lines.push("");
|
|
|
|
lines.push("## Affected Layers");
|
|
lines.push("");
|
|
if (ctx.affectedLayers.length === 0) {
|
|
lines.push("No layers affected.");
|
|
} else {
|
|
for (const layer of ctx.affectedLayers) {
|
|
lines.push(`- **${layer.name}**: ${layer.description}`);
|
|
}
|
|
}
|
|
lines.push("");
|
|
|
|
if (ctx.impactedEdges.length > 0) {
|
|
lines.push("## Impacted Relationships");
|
|
lines.push("");
|
|
for (const edge of ctx.impactedEdges) {
|
|
lines.push(`- ${edge.source} --[${edge.type}]--> ${edge.target}`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
if (ctx.unmappedFiles.length > 0) {
|
|
lines.push("## Unmapped Files");
|
|
lines.push("");
|
|
lines.push("These changed files are not yet in the knowledge graph:");
|
|
lines.push("");
|
|
for (const f of ctx.unmappedFiles) {
|
|
lines.push(`- \`${f}\``);
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
lines.push("## Risk Assessment");
|
|
lines.push("");
|
|
const complexChanges = ctx.changedNodes.filter(
|
|
(n) => n.complexity === "complex",
|
|
);
|
|
const crossLayerCount = new Set(ctx.affectedLayers.map((l) => l.id)).size;
|
|
|
|
if (complexChanges.length > 0) {
|
|
lines.push(
|
|
`- **High complexity**: ${complexChanges.length} complex component(s) changed: ${complexChanges.map((n) => n.name).join(", ")}`,
|
|
);
|
|
}
|
|
if (crossLayerCount > 1) {
|
|
lines.push(
|
|
`- **Cross-layer impact**: Changes span ${crossLayerCount} architectural layers`,
|
|
);
|
|
}
|
|
if (ctx.affectedNodes.length > 5) {
|
|
lines.push(
|
|
`- **Wide blast radius**: ${ctx.affectedNodes.length} components affected downstream`,
|
|
);
|
|
}
|
|
if (ctx.unmappedFiles.length > 0) {
|
|
lines.push(
|
|
`- **New/unmapped files**: ${ctx.unmappedFiles.length} files not in the knowledge graph (may need re-analysis)`,
|
|
);
|
|
}
|
|
if (
|
|
complexChanges.length === 0 &&
|
|
crossLayerCount <= 1 &&
|
|
ctx.affectedNodes.length <= 5 &&
|
|
ctx.unmappedFiles.length === 0
|
|
) {
|
|
lines.push(
|
|
"- **Low risk**: Changes are localized with limited downstream impact.",
|
|
);
|
|
}
|
|
lines.push("");
|
|
|
|
return lines.join("\n");
|
|
}
|