From f4660c3effa319a8fac471a3f55a254185f0c322 Mon Sep 17 00:00:00 2001 From: caius72 Date: Sat, 20 Jun 2026 13:37:55 +0200 Subject: [PATCH] Remove dead code: unused core exports, deprecated dagre layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prune exported API surface that nothing in the repo (skills, dashboard) consumes, plus the deprecated dagre layout path the dashboard no longer uses: - embedding-search.ts (SemanticSearchEngine/cosineSimilarity) — speculative, never instantiated; only a stale store.ts comment referenced it - analyzer/language-lesson.ts (buildLanguageLessonPrompt and friends) — zero callers; the languageLesson schema field is filled by agent prompts, not this code, and is kept - change-classifier.ts (classifyUpdate) — only the barrel re-exported it - dashboard layout.worker.ts and applyDagreLayout() — both @deprecated/ unreferenced and the sole users of @dagrejs/dagre, which is dropped generateStarterIgnoreFile / ignore-generator.ts is kept — it is live, called by skills/understand/generate-ignore.mjs. ELK, d3-force and graphology-louvain are also retained; each still drives a live view. Build (core + dashboard), lint, and all tests pass. --- pnpm-lock.yaml | 16 +- .../src/__tests__/change-classifier.test.ts | 183 --------------- .../src/__tests__/embedding-search.test.ts | 92 -------- .../src/__tests__/language-lesson.test.ts | 157 ------------- .../core/src/analyzer/language-lesson.ts | 210 ------------------ .../packages/core/src/change-classifier.ts | 143 ------------ .../packages/core/src/embedding-search.ts | 83 ------- .../packages/core/src/index.ts | 15 -- .../packages/dashboard/package.json | 1 - .../packages/dashboard/src/store.ts | 3 +- .../packages/dashboard/src/utils/layout.ts | 60 ----- .../dashboard/src/utils/layout.worker.ts | 47 ---- 12 files changed, 2 insertions(+), 1008 deletions(-) delete mode 100644 understand-anything-plugin/packages/core/src/__tests__/change-classifier.test.ts delete mode 100644 understand-anything-plugin/packages/core/src/__tests__/embedding-search.test.ts delete mode 100644 understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts delete mode 100644 understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts delete mode 100644 understand-anything-plugin/packages/core/src/change-classifier.ts delete mode 100644 understand-anything-plugin/packages/core/src/embedding-search.ts delete mode 100644 understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37350c558..4dd61db81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,9 +124,6 @@ importers: understand-anything-plugin/packages/dashboard: dependencies: - '@dagrejs/dagre': - specifier: ^2.0.4 - version: 2.0.4 '@understand-anything/core': specifier: workspace:* version: link:../core @@ -323,12 +320,6 @@ packages: '@clack/prompts@1.1.0': resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} - '@dagrejs/dagre@2.0.4': - resolution: {integrity: sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==} - - '@dagrejs/graphlib@3.0.4': - resolution: {integrity: sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==} - '@emnapi/runtime@1.9.0': resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} @@ -1283,6 +1274,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} @@ -3515,12 +3507,6 @@ snapshots: '@clack/core': 1.1.0 sisteransi: 1.0.5 - '@dagrejs/dagre@2.0.4': - dependencies: - '@dagrejs/graphlib': 3.0.4 - - '@dagrejs/graphlib@3.0.4': {} - '@emnapi/runtime@1.9.0': dependencies: tslib: 2.8.1 diff --git a/understand-anything-plugin/packages/core/src/__tests__/change-classifier.test.ts b/understand-anything-plugin/packages/core/src/__tests__/change-classifier.test.ts deleted file mode 100644 index dd25373e6..000000000 --- a/understand-anything-plugin/packages/core/src/__tests__/change-classifier.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { classifyUpdate } from "../change-classifier.js"; -import type { ChangeAnalysis } from "../fingerprint.js"; - -function makeAnalysis(overrides: Partial = {}): ChangeAnalysis { - return { - fileChanges: [], - newFiles: [], - deletedFiles: [], - structurallyChangedFiles: [], - cosmeticOnlyFiles: [], - unchangedFiles: [], - ...overrides, - }; -} - -describe("classifyUpdate", () => { - it("returns SKIP when all files are unchanged", () => { - const analysis = makeAnalysis({ - unchangedFiles: ["src/a.ts", "src/b.ts"], - }); - - const decision = classifyUpdate(analysis, 50); - - expect(decision.action).toBe("SKIP"); - expect(decision.filesToReanalyze).toHaveLength(0); - expect(decision.rerunArchitecture).toBe(false); - expect(decision.rerunTour).toBe(false); - }); - - it("returns SKIP when all changes are cosmetic", () => { - const analysis = makeAnalysis({ - cosmeticOnlyFiles: ["src/a.ts", "src/b.ts"], - }); - - const decision = classifyUpdate(analysis, 50); - - expect(decision.action).toBe("SKIP"); - expect(decision.reason).toContain("cosmetic-only"); - }); - - it("returns PARTIAL_UPDATE for a few structural changes", () => { - const analysis = makeAnalysis({ - structurallyChangedFiles: ["src/a.ts", "src/b.ts"], - newFiles: ["src/c.ts"], - cosmeticOnlyFiles: ["src/d.ts"], - }); - - // src/ already exists in the project, so adding src/c.ts is not a directory change - const allKnownFiles = ["src/a.ts", "src/b.ts", "src/d.ts", "lib/util.ts"]; - const decision = classifyUpdate(analysis, 50, allKnownFiles); - - expect(decision.action).toBe("PARTIAL_UPDATE"); - expect(decision.filesToReanalyze).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]); - expect(decision.rerunArchitecture).toBe(false); - expect(decision.rerunTour).toBe(false); - }); - - it("returns ARCHITECTURE_UPDATE when >10 structural files", () => { - const files = Array.from({ length: 12 }, (_, i) => `src/file${i}.ts`); - const analysis = makeAnalysis({ - structurallyChangedFiles: files, - }); - - const decision = classifyUpdate(analysis, 50); - - expect(decision.action).toBe("ARCHITECTURE_UPDATE"); - expect(decision.rerunArchitecture).toBe(true); - expect(decision.rerunTour).toBe(true); - }); - - it("returns ARCHITECTURE_UPDATE when new directories appear", () => { - const analysis = makeAnalysis({ - structurallyChangedFiles: ["src/existing.ts"], - newFiles: ["newdir/file.ts"], - }); - - const allKnownFiles = ["src/existing.ts", "src/other.ts", "lib/util.ts"]; - const decision = classifyUpdate(analysis, 50, allKnownFiles); - - expect(decision.action).toBe("ARCHITECTURE_UPDATE"); - expect(decision.rerunArchitecture).toBe(true); - }); - - it("returns ARCHITECTURE_UPDATE when directories are deleted", () => { - const analysis = makeAnalysis({ - structurallyChangedFiles: ["src/existing.ts"], - deletedFiles: ["olddir/removed.ts"], - }); - - const allKnownFiles = ["src/existing.ts", "src/other.ts"]; - const decision = classifyUpdate(analysis, 50, allKnownFiles); - - expect(decision.action).toBe("ARCHITECTURE_UPDATE"); - expect(decision.rerunArchitecture).toBe(true); - }); - - it("does NOT trigger ARCHITECTURE_UPDATE for new file in existing directory", () => { - const analysis = makeAnalysis({ - newFiles: ["src/newfile.ts"], - }); - - // src/ is already known via other files in the project - const allKnownFiles = ["src/a.ts", "src/b.ts", "lib/util.ts"]; - const decision = classifyUpdate(analysis, 50, allKnownFiles); - - expect(decision.action).toBe("PARTIAL_UPDATE"); - expect(decision.rerunArchitecture).toBe(false); - }); - - it("triggers ARCHITECTURE_UPDATE for new file in genuinely new directory", () => { - const analysis = makeAnalysis({ - newFiles: ["brand-new-pkg/index.ts"], - }); - - // allKnownFiles only contains src/ and lib/ — no brand-new-pkg/ - const allKnownFiles = ["src/a.ts", "src/b.ts", "lib/util.ts"]; - const decision = classifyUpdate(analysis, 50, allKnownFiles); - - expect(decision.action).toBe("ARCHITECTURE_UPDATE"); - expect(decision.rerunArchitecture).toBe(true); - }); - - it("returns FULL_UPDATE when >30 structural files", () => { - const files = Array.from({ length: 35 }, (_, i) => `src/file${i}.ts`); - const analysis = makeAnalysis({ - structurallyChangedFiles: files, - }); - - const decision = classifyUpdate(analysis, 100); - - expect(decision.action).toBe("FULL_UPDATE"); - expect(decision.rerunArchitecture).toBe(true); - expect(decision.rerunTour).toBe(true); - }); - - it("returns FULL_UPDATE when >50% of project is structurally changed", () => { - const files = Array.from({ length: 6 }, (_, i) => `src/file${i}.ts`); - const analysis = makeAnalysis({ - structurallyChangedFiles: files, - }); - - // 6 out of 10 files = 60% - const decision = classifyUpdate(analysis, 10); - - expect(decision.action).toBe("FULL_UPDATE"); - }); - - it("includes new and structural files in filesToReanalyze for PARTIAL", () => { - const analysis = makeAnalysis({ - structurallyChangedFiles: ["src/modified.ts"], - newFiles: ["src/added.ts"], - deletedFiles: ["src/removed.ts"], - }); - - const decision = classifyUpdate(analysis, 50); - - expect(decision.filesToReanalyze).toContain("src/modified.ts"); - expect(decision.filesToReanalyze).toContain("src/added.ts"); - // Deleted files shouldn't be re-analyzed - expect(decision.filesToReanalyze).not.toContain("src/removed.ts"); - }); - - it("handles empty analysis (no changes at all)", () => { - const analysis = makeAnalysis(); - const decision = classifyUpdate(analysis, 50); - - expect(decision.action).toBe("SKIP"); - expect(decision.reason).toContain("No changes detected"); - }); - - it("counts deleted files toward structural total", () => { - // 8 structural + 3 deleted = 11 total structural > 10 threshold - const analysis = makeAnalysis({ - structurallyChangedFiles: Array.from({ length: 8 }, (_, i) => `src/file${i}.ts`), - deletedFiles: ["src/old1.ts", "src/old2.ts", "src/old3.ts"], - }); - - const decision = classifyUpdate(analysis, 50); - - expect(decision.action).toBe("ARCHITECTURE_UPDATE"); - }); -}); diff --git a/understand-anything-plugin/packages/core/src/__tests__/embedding-search.test.ts b/understand-anything-plugin/packages/core/src/__tests__/embedding-search.test.ts deleted file mode 100644 index 4952c7945..000000000 --- a/understand-anything-plugin/packages/core/src/__tests__/embedding-search.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { SemanticSearchEngine, cosineSimilarity } from "../embedding-search.js"; -import type { GraphNode } from "../types.js"; - -const nodes: GraphNode[] = [ - { id: "n1", type: "file", name: "auth.ts", summary: "Authentication module", tags: ["auth"], complexity: "moderate" }, - { id: "n2", type: "file", name: "db.ts", summary: "Database connection", tags: ["db"], complexity: "simple" }, - { id: "n3", type: "function", name: "login", summary: "User login handler", tags: ["auth", "login"], complexity: "moderate" }, -]; - -// Simple unit vectors for testing -const embeddings: Record = { - n1: [1, 0, 0, 0], - n2: [0, 1, 0, 0], - n3: [0.9, 0, 0.1, 0], -}; - -describe("embedding-search", () => { - describe("cosineSimilarity", () => { - it("returns 1 for identical vectors", () => { - expect(cosineSimilarity([1, 0, 0], [1, 0, 0])).toBeCloseTo(1); - }); - - it("returns 0 for orthogonal vectors", () => { - expect(cosineSimilarity([1, 0, 0], [0, 1, 0])).toBeCloseTo(0); - }); - - it("returns high similarity for similar vectors", () => { - const sim = cosineSimilarity([1, 0, 0], [0.9, 0.1, 0]); - expect(sim).toBeGreaterThan(0.9); - }); - - it("handles zero vectors", () => { - expect(cosineSimilarity([0, 0, 0], [1, 0, 0])).toBe(0); - }); - }); - - describe("SemanticSearchEngine", () => { - it("returns results sorted by similarity", () => { - const engine = new SemanticSearchEngine(nodes, embeddings); - const queryEmbedding = [1, 0, 0, 0]; // most similar to n1 and n3 - const results = engine.search(queryEmbedding); - expect(results[0].nodeId).toBe("n1"); - }); - - it("respects limit parameter", () => { - const engine = new SemanticSearchEngine(nodes, embeddings); - const results = engine.search([1, 0, 0, 0], { limit: 2 }); - expect(results).toHaveLength(2); - }); - - it("respects threshold parameter", () => { - const engine = new SemanticSearchEngine(nodes, embeddings); - const results = engine.search([1, 0, 0, 0], { threshold: 0.5 }); - // n2 has 0 similarity, should be filtered out - const ids = results.map((r) => r.nodeId); - expect(ids).not.toContain("n2"); - }); - - it("filters by node type", () => { - const engine = new SemanticSearchEngine(nodes, embeddings); - const results = engine.search([1, 0, 0, 0], { types: ["function"] }); - expect(results.every((r) => { - const node = nodes.find((n) => n.id === r.nodeId); - return node?.type === "function"; - })).toBe(true); - }); - - it("returns empty for nodes without embeddings", () => { - const engine = new SemanticSearchEngine(nodes, {}); - const results = engine.search([1, 0, 0, 0]); - expect(results).toHaveLength(0); - }); - - it("hasEmbeddings returns true when embeddings exist", () => { - const engine = new SemanticSearchEngine(nodes, embeddings); - expect(engine.hasEmbeddings()).toBe(true); - }); - - it("hasEmbeddings returns false when empty", () => { - const engine = new SemanticSearchEngine(nodes, {}); - expect(engine.hasEmbeddings()).toBe(false); - }); - - it("addEmbedding updates the search index", () => { - const engine = new SemanticSearchEngine(nodes, {}); - expect(engine.hasEmbeddings()).toBe(false); - engine.addEmbedding("n1", [1, 0, 0, 0]); - expect(engine.hasEmbeddings()).toBe(true); - }); - }); -}); diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts deleted file mode 100644 index 19ebb0bcf..000000000 --- a/understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - buildLanguageLessonPrompt, - parseLanguageLessonResponse, - detectLanguageConcepts, -} from "../analyzer/language-lesson.js"; -import type { GraphNode, GraphEdge } from "../types.js"; -import { typescriptConfig } from "../languages/configs/typescript.js"; - -const sampleNode: GraphNode = { - id: "function:auth:verifyToken", - type: "function", - name: "verifyToken", - filePath: "src/auth/verify.ts", - lineRange: [10, 35], - summary: "Verifies JWT tokens and extracts user payload using async/await", - tags: ["auth", "jwt", "async"], - complexity: "moderate", -}; - -const sampleEdges: GraphEdge[] = [ - { - source: "function:auth:verifyToken", - target: "file:src/config.ts", - type: "reads_from", - direction: "forward", - weight: 0.6, - }, - { - source: "file:src/middleware.ts", - target: "function:auth:verifyToken", - type: "calls", - direction: "forward", - weight: 0.8, - }, -]; - -describe("language-lesson", () => { - describe("buildLanguageLessonPrompt", () => { - it("includes the node name and summary", () => { - const prompt = buildLanguageLessonPrompt( - sampleNode, - sampleEdges, - "typescript", - ); - expect(prompt).toContain("verifyToken"); - expect(prompt).toContain("JWT tokens"); - }); - - it("includes the target language", () => { - const prompt = buildLanguageLessonPrompt( - sampleNode, - sampleEdges, - "typescript", - typescriptConfig, - ); - expect(prompt).toContain("TypeScript"); - }); - - it("includes relationship context", () => { - const prompt = buildLanguageLessonPrompt( - sampleNode, - sampleEdges, - "typescript", - ); - expect(prompt).toContain("reads_from"); - }); - - it("requests JSON output", () => { - const prompt = buildLanguageLessonPrompt( - sampleNode, - sampleEdges, - "typescript", - ); - expect(prompt).toContain("JSON"); - }); - }); - - describe("parseLanguageLessonResponse", () => { - it("parses a valid response", () => { - const response = JSON.stringify({ - languageNotes: - "Uses async/await for non-blocking token verification.", - concepts: [ - { - name: "async/await", - explanation: - "The function uses async/await to handle asynchronous JWT verification.", - }, - ], - }); - const result = parseLanguageLessonResponse(response); - expect(result.languageNotes).toBe( - "Uses async/await for non-blocking token verification.", - ); - expect(result.concepts).toHaveLength(1); - expect(result.concepts[0].name).toBe("async/await"); - expect(result.concepts[0].explanation).toContain("async/await"); - }); - - it("extracts JSON from code blocks", () => { - const response = `Here is the analysis: -\`\`\`json -{ - "languageNotes": "TypeScript generics used here.", - "concepts": [ - { "name": "generics", "explanation": "Type parameters enable reuse." } - ] -} -\`\`\``; - const result = parseLanguageLessonResponse(response); - expect(result.languageNotes).toBe("TypeScript generics used here."); - expect(result.concepts).toHaveLength(1); - expect(result.concepts[0].name).toBe("generics"); - }); - - it("returns empty result for invalid response", () => { - const result = parseLanguageLessonResponse(""); - expect(result).toEqual({ languageNotes: "", concepts: [] }); - }); - }); - - describe("detectLanguageConcepts", () => { - it("detects async patterns from tags", () => { - const concepts = detectLanguageConcepts(sampleNode, "typescript"); - expect(concepts).toContain("async/await"); - }); - - it("detects middleware pattern", () => { - const middlewareNode: GraphNode = { - id: "function:middleware:auth", - type: "function", - name: "authMiddleware", - filePath: "src/middleware/auth.ts", - summary: "Express middleware for authentication", - tags: ["middleware", "auth"], - complexity: "moderate", - }; - const concepts = detectLanguageConcepts(middlewareNode, "typescript"); - expect(concepts).toContain("middleware pattern"); - }); - - it("returns empty for nodes with no detectable concepts", () => { - const plainNode: GraphNode = { - id: "file:src/config.ts", - type: "file", - name: "config.ts", - filePath: "src/config.ts", - summary: "Exports configuration values from environment variables", - tags: ["config"], - complexity: "simple", - }; - const concepts = detectLanguageConcepts(plainNode, "typescript"); - expect(concepts).toEqual([]); - }); - }); -}); diff --git a/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts b/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts deleted file mode 100644 index 53fcc01a4..000000000 --- a/understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { GraphNode, GraphEdge } from "../types.js"; -import type { LanguageConfig } from "../languages/types.js"; - -export interface LanguageLessonResult { - languageNotes: string; - concepts: Array<{ name: string; explanation: string }>; -} - -/** - * Base concept patterns that apply across all languages. - * These are merged with language-specific concepts from LanguageConfig. - */ -const BASE_CONCEPT_PATTERNS: Record = { - "async/await": ["async", "await", "promise", "asynchronous"], - "middleware pattern": ["middleware", "interceptor", "pipe"], - "generics": ["generic", "type parameter", "template"], - "decorators": ["decorator", "@", "annotation"], - "dependency injection": ["inject", "provider", "container", "di"], - "observer pattern": [ - "subscribe", - "publish", - "event", - "observable", - "listener", - ], - "singleton": ["singleton", "instance", "shared client"], - "type guards": ["type guard", "is", "narrowing", "discriminated union"], - "higher-order functions": [ - "callback", - "factory", - "higher-order", - "closure", - ], - "error handling": [ - "try/catch", - "error boundary", - "exception", - "Result type", - ], - "streams": ["stream", "pipe", "transform", "readable", "writable"], - "concurrency": ["goroutine", "channel", "thread", "worker", "mutex"], -}; - -/** - * Build the full concept patterns map by merging base patterns with - * language-specific concepts from a LanguageConfig (if provided). - */ -function buildConceptPatterns( - langConfig?: LanguageConfig | null, -): Record { - const patterns = { ...BASE_CONCEPT_PATTERNS }; - - if (langConfig?.concepts) { - for (const concept of langConfig.concepts) { - if (!patterns[concept]) { - // Use the concept name itself as a keyword for detection - patterns[concept] = [concept.toLowerCase()]; - } - } - } - - return patterns; -} - -/** - * Detects language concepts present in a graph node based on its tags, summary, and languageNotes. - * When a LanguageConfig is provided, language-specific concepts are also detected. - */ -export function detectLanguageConcepts( - node: GraphNode, - language: string, - langConfig?: LanguageConfig | null, -): string[] { - const text = [ - ...node.tags, - node.summary.toLowerCase(), - node.languageNotes?.toLowerCase() ?? "", - ].join(" "); - - const patterns = buildConceptPatterns(langConfig); - const detected: string[] = []; - - for (const [concept, keywords] of Object.entries(patterns)) { - const found = keywords.some((keyword) => - text.toLowerCase().includes(keyword.toLowerCase()), - ); - if (found) { - detected.push(concept); - } - } - - return detected; -} - -/** - * Get the display name for a language. - * Uses LanguageConfig if provided, otherwise falls back to capitalization. - */ -export function getLanguageDisplayName( - language: string, - langConfig?: LanguageConfig | null, -): string { - if (langConfig?.displayName) { - return langConfig.displayName; - } - return language.charAt(0).toUpperCase() + language.slice(1); -} - -/** - * Builds a prompt that asks an LLM to produce a language-specific lesson for a given node. - */ -export function buildLanguageLessonPrompt( - node: GraphNode, - edges: GraphEdge[], - language: string, - langConfig?: LanguageConfig | null, -): string { - const capitalizedLanguage = getLanguageDisplayName(language, langConfig); - - const concepts = detectLanguageConcepts(node, language, langConfig); - - const relationships = edges - .map((edge) => { - const arrow = edge.direction === "forward" ? "->" : "<-"; - const other = - edge.source === node.id ? edge.target : edge.source; - return ` ${arrow} ${edge.type} ${other}`; - }) - .join("\n"); - - const conceptSection = - concepts.length > 0 - ? `\nDetected concepts to explain:\n${concepts.map((c) => ` - ${c}`).join("\n")}` - : `\nNo specific concepts were pre-detected. Please identify any ${capitalizedLanguage} patterns or idioms present.`; - - return `You are a programming teacher specializing in ${capitalizedLanguage}. Analyze the following code component and create a language-specific lesson. - -Component: ${node.name} -Type: ${node.type} -File: ${node.filePath ?? "N/A"} -Summary: ${node.summary} -Tags: ${node.tags.join(", ")} - -Relationships: -${relationships} -${conceptSection} - -Return a JSON object with the following fields: -- "languageNotes": A concise explanation of the ${capitalizedLanguage}-specific patterns and idioms used in this component. -- "concepts": An array of objects, each with: - - "name": The concept name (e.g., "async/await", "generics"). - - "explanation": A beginner-friendly explanation of this concept as it applies to this component. - -Respond ONLY with the JSON object, no additional text.`; -} - -/** - * Extracts a JSON block from an LLM response, handling markdown fences. - */ -function extractJson(response: string): string { - const fenceMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/); - if (fenceMatch) { - return fenceMatch[1].trim(); - } - - const objectMatch = response.match(/\{[\s\S]*\}/); - if (objectMatch) { - return objectMatch[0].trim(); - } - - return response.trim(); -} - -/** - * Parses an LLM response for language lesson content. - * Returns a safe default on parse failure. - */ -export function parseLanguageLessonResponse( - response: string, -): LanguageLessonResult { - try { - const jsonStr = extractJson(response); - const parsed = JSON.parse(jsonStr); - - const languageNotes = - typeof parsed.languageNotes === "string" ? parsed.languageNotes : ""; - - const concepts = Array.isArray(parsed.concepts) - ? parsed.concepts - .filter( - ( - c: unknown, - ): c is { name: string; explanation: string } => - typeof c === "object" && - c !== null && - typeof (c as Record).name === "string" && - typeof (c as Record).explanation === - "string", - ) - .map((c: { name: string; explanation: string }) => ({ - name: c.name, - explanation: c.explanation, - })) - : []; - - return { languageNotes, concepts }; - } catch { - return { languageNotes: "", concepts: [] }; - } -} diff --git a/understand-anything-plugin/packages/core/src/change-classifier.ts b/understand-anything-plugin/packages/core/src/change-classifier.ts deleted file mode 100644 index e2b9b9eb7..000000000 --- a/understand-anything-plugin/packages/core/src/change-classifier.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { dirname } from "node:path"; -import type { ChangeAnalysis } from "./fingerprint.js"; - -export interface UpdateDecision { - action: "SKIP" | "PARTIAL_UPDATE" | "ARCHITECTURE_UPDATE" | "FULL_UPDATE"; - filesToReanalyze: string[]; - rerunArchitecture: boolean; - rerunTour: boolean; - reason: string; -} - -/** - * Classify the type of graph update needed based on structural change analysis. - * - * Decision matrix: - * - SKIP: all files NONE or COSMETIC only - * - PARTIAL_UPDATE: some STRUCTURAL, same directories - * - ARCHITECTURE_UPDATE: new/deleted directories or >10 structural files - * - FULL_UPDATE: >30 structural files or >50% of total files changed structurally - */ -export function classifyUpdate( - analysis: ChangeAnalysis, - totalFilesInGraph: number, - allKnownFiles: string[] = [], -): UpdateDecision { - const { newFiles, deletedFiles, structurallyChangedFiles, cosmeticOnlyFiles } = analysis; - const structuralCount = structurallyChangedFiles.length + newFiles.length + deletedFiles.length; - - // No structural changes at all — skip - if (structuralCount === 0) { - const cosmeticCount = cosmeticOnlyFiles.length; - const reason = cosmeticCount > 0 - ? `${cosmeticCount} file(s) have cosmetic-only changes (no structural impact)` - : "No changes detected"; - - return { - action: "SKIP", - filesToReanalyze: [], - rerunArchitecture: false, - rerunTour: false, - reason, - }; - } - - // Too many structural changes — suggest full rebuild - const triggeredByCount = structuralCount > 30; - const triggeredByPercentage = totalFilesInGraph > 0 && structuralCount / totalFilesInGraph > 0.5; - if (triggeredByCount || triggeredByPercentage) { - const thresholdReason = - triggeredByCount && triggeredByPercentage - ? ">30 files and >50% of project" - : triggeredByCount - ? ">30 files" - : ">50% of project"; - return { - action: "FULL_UPDATE", - filesToReanalyze: [...structurallyChangedFiles, ...newFiles], - rerunArchitecture: true, - rerunTour: true, - reason: `${structuralCount} files have structural changes (${thresholdReason}) — full rebuild recommended`, - }; - } - - // Check if directory structure changed (new/deleted top-level directories) - const hasDirectoryChanges = detectDirectoryChanges(newFiles, deletedFiles, allKnownFiles); - - if (hasDirectoryChanges || structuralCount > 10) { - return { - action: "ARCHITECTURE_UPDATE", - filesToReanalyze: [...structurallyChangedFiles, ...newFiles], - rerunArchitecture: true, - rerunTour: true, - reason: hasDirectoryChanges - ? `Directory structure changed (${newFiles.length} new, ${deletedFiles.length} deleted files)` - : `${structuralCount} files have structural changes — architecture re-analysis needed`, - }; - } - - // Localized structural changes — partial update - return { - action: "PARTIAL_UPDATE", - filesToReanalyze: [...structurallyChangedFiles, ...newFiles], - rerunArchitecture: false, - rerunTour: false, - reason: `${structuralCount} file(s) have structural changes: ${summarizeChanges(analysis)}`, - }; -} - -/** - * Detect if the changes affect the directory structure (new or removed directories). - * Uses all known files in the project as the baseline for existing directories, - * then checks if any new/deleted files introduce or remove a top-level source directory. - */ -function detectDirectoryChanges( - newFiles: string[], - deletedFiles: string[], - allKnownFiles: string[], -): boolean { - const existingDirs = new Set( - allKnownFiles.map((f) => topDirectory(f)).filter(Boolean), - ); - - for (const f of newFiles) { - const dir = topDirectory(f); - if (dir && !existingDirs.has(dir)) return true; - } - - for (const f of deletedFiles) { - const dir = topDirectory(f); - if (dir && !existingDirs.has(dir)) return true; - } - - return false; -} - -/** - * Get the top-level directory of a file path (first path segment). - */ -function topDirectory(filePath: string): string | null { - const dir = dirname(filePath); - if (dir === "." || dir === "") return null; - const segments = dir.split("/"); - return segments[0] || null; -} - -/** - * Produce a concise human-readable summary of structural changes. - */ -function summarizeChanges(analysis: ChangeAnalysis): string { - const parts: string[] = []; - - if (analysis.newFiles.length > 0) { - parts.push(`${analysis.newFiles.length} new`); - } - if (analysis.deletedFiles.length > 0) { - parts.push(`${analysis.deletedFiles.length} deleted`); - } - if (analysis.structurallyChangedFiles.length > 0) { - parts.push(`${analysis.structurallyChangedFiles.length} modified`); - } - - return parts.join(", "); -} diff --git a/understand-anything-plugin/packages/core/src/embedding-search.ts b/understand-anything-plugin/packages/core/src/embedding-search.ts deleted file mode 100644 index 71192ca2a..000000000 --- a/understand-anything-plugin/packages/core/src/embedding-search.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { GraphNode } from "./types.js"; -import type { SearchResult } from "./search.js"; - -export interface SemanticSearchOptions { - limit?: number; - threshold?: number; - types?: string[]; -} - -/** - * Compute cosine similarity between two vectors. - * Returns 0 if either vector has zero magnitude. - */ -export function cosineSimilarity(a: number[], b: number[]): number { - let dot = 0; - let magA = 0; - let magB = 0; - - for (let i = 0; i < a.length; i++) { - dot += a[i] * b[i]; - magA += a[i] * a[i]; - magB += b[i] * b[i]; - } - - magA = Math.sqrt(magA); - magB = Math.sqrt(magB); - - if (magA === 0 || magB === 0) return 0; - return dot / (magA * magB); -} - -/** - * Semantic search engine using vector embeddings. - * Stores pre-computed embeddings for graph nodes and performs - * cosine similarity search against query embeddings. - */ -export class SemanticSearchEngine { - private nodes: GraphNode[]; - private embeddings: Map; - - constructor(nodes: GraphNode[], embeddings: Record) { - this.nodes = nodes; - this.embeddings = new Map(Object.entries(embeddings)); - } - - hasEmbeddings(): boolean { - return this.embeddings.size > 0; - } - - addEmbedding(nodeId: string, embedding: number[]): void { - this.embeddings.set(nodeId, embedding); - } - - search( - queryEmbedding: number[], - options?: SemanticSearchOptions, - ): SearchResult[] { - const limit = options?.limit ?? 10; - const threshold = options?.threshold ?? 0; - const typeFilter = options?.types; - - const scored: Array<{ nodeId: string; score: number }> = []; - - for (const node of this.nodes) { - if (typeFilter && !typeFilter.includes(node.type)) continue; - - const embedding = this.embeddings.get(node.id); - if (!embedding) continue; - - const similarity = cosineSimilarity(queryEmbedding, embedding); - if (similarity >= threshold) { - scored.push({ nodeId: node.id, score: 1 - similarity }); - } - } - - scored.sort((a, b) => a.score - b.score); - return scored.slice(0, limit); - } - - updateNodes(nodes: GraphNode[]): void { - this.nodes = nodes; - } -} diff --git a/understand-anything-plugin/packages/core/src/index.ts b/understand-anything-plugin/packages/core/src/index.ts index c5f629aa4..0a8c84a11 100644 --- a/understand-anything-plugin/packages/core/src/index.ts +++ b/understand-anything-plugin/packages/core/src/index.ts @@ -48,12 +48,6 @@ export { parseTourGenerationResponse, generateHeuristicTour, } from "./analyzer/tour-generator.js"; -export { - buildLanguageLessonPrompt, - parseLanguageLessonResponse, - detectLanguageConcepts, - type LanguageLessonResult, -} from "./analyzer/language-lesson.js"; export { PluginRegistry } from "./plugins/registry.js"; export { LanguageRegistry, @@ -76,11 +70,6 @@ export { type PluginConfig, type PluginEntry, } from "./plugins/discovery.js"; -export { - SemanticSearchEngine, - cosineSimilarity, - type SemanticSearchOptions, -} from "./embedding-search.js"; export { extractFileFingerprint, compareFingerprints, @@ -96,10 +85,6 @@ export { type FileChangeResult, type ChangeAnalysis, } from "./fingerprint.js"; -export { - classifyUpdate, - type UpdateDecision, -} from "./change-classifier.js"; // Non-code parsers export { MarkdownParser, diff --git a/understand-anything-plugin/packages/dashboard/package.json b/understand-anything-plugin/packages/dashboard/package.json index 63ce6cacd..d576edc5b 100644 --- a/understand-anything-plugin/packages/dashboard/package.json +++ b/understand-anything-plugin/packages/dashboard/package.json @@ -12,7 +12,6 @@ "test:watch": "vitest" }, "dependencies": { - "@dagrejs/dagre": "^2.0.4", "@understand-anything/core": "workspace:*", "@xyflow/react": "^12.0.0", "d3-force": "^3.0.0", diff --git a/understand-anything-plugin/packages/dashboard/src/store.ts b/understand-anything-plugin/packages/dashboard/src/store.ts index b3b2a96c7..465038bb9 100644 --- a/understand-anything-plugin/packages/dashboard/src/store.ts +++ b/understand-anything-plugin/packages/dashboard/src/store.ts @@ -524,8 +524,7 @@ export const useDashboardStore = create()((set, get) => ({ set({ searchQuery: query, searchResults: [] }); return; } - // Currently both modes use the same fuzzy engine - // When embeddings are available, "semantic" mode will use SemanticSearchEngine + // Both search modes use the same fuzzy engine. void mode; const searchResults = engine.search(query); set({ searchQuery: query, searchResults }); diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts index b35328c58..b4851234e 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts @@ -1,4 +1,3 @@ -import dagre from "@dagrejs/dagre"; import { forceSimulation, forceLink, @@ -19,65 +18,6 @@ export const LAYER_CLUSTER_HEIGHT = 180; export const PORTAL_NODE_WIDTH = 240; export const PORTAL_NODE_HEIGHT = 80; -/** - * Synchronous dagre layout — used for small graphs. - * - * @deprecated The dashboard's structural views all use ELK now - * (`applyElkLayout` from `./elk-layout`). This helper is kept for one - * release to allow a quick fallback if ELK has a regression. Slated for - * removal in the version after layout migration is verified stable. - */ -export function applyDagreLayout( - nodes: Node[], - edges: Edge[], - direction: "TB" | "LR" = "TB", - nodeDimensions?: Map, - spacingOverrides?: { nodesep?: number; ranksep?: number }, -): { nodes: Node[]; edges: Edge[] } { - const g = new dagre.graphlib.Graph(); - g.setDefaultEdgeLabel(() => ({})); - - // Scale spacing for larger graphs to reduce overlap - const isLarge = nodes.length > 50; - g.setGraph({ - rankdir: direction, - nodesep: spacingOverrides?.nodesep ?? (isLarge ? 80 : 60), - ranksep: spacingOverrides?.ranksep ?? (isLarge ? 120 : 80), - marginx: 20, - marginy: 20, - }); - - nodes.forEach((node) => { - const dims = nodeDimensions?.get(node.id); - const w = dims?.width ?? NODE_WIDTH; - const h = dims?.height ?? NODE_HEIGHT; - g.setNode(node.id, { width: w, height: h }); - }); - - edges.forEach((edge) => { - g.setEdge(edge.source, edge.target); - }); - - dagre.layout(g); - - const layoutedNodes = nodes.map((node) => { - const pos = g.node(node.id); - if (!pos) return { ...node, position: { x: 0, y: 0 } }; - const dims = nodeDimensions?.get(node.id); - const w = dims?.width ?? NODE_WIDTH; - const h = dims?.height ?? NODE_HEIGHT; - return { - ...node, - position: { - x: pos.x - w / 2, - y: pos.y - h / 2, - }, - }; - }); - - return { nodes: layoutedNodes, edges }; -} - // --------------------------------------------------------------------------- // Force-directed layout (for knowledge graphs) // --------------------------------------------------------------------------- diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts deleted file mode 100644 index 4f466f313..000000000 --- a/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts +++ /dev/null @@ -1,47 +0,0 @@ -import dagre from "@dagrejs/dagre"; - -export interface LayoutMessage { - requestId: number; - nodes: Array<{ id: string; width: number; height: number }>; - edges: Array<{ source: string; target: string }>; - direction: "TB" | "LR"; -} - -export interface LayoutResult { - requestId: number; - positions: Record; -} - -self.onmessage = (e: MessageEvent) => { - const { requestId, nodes, edges, direction } = e.data; - - const g = new dagre.graphlib.Graph(); - g.setDefaultEdgeLabel(() => ({})); - g.setGraph({ - rankdir: direction, - nodesep: 60, - ranksep: 80, - marginx: 20, - marginy: 20, - }); - - for (const node of nodes) { - g.setNode(node.id, { width: node.width, height: node.height }); - } - - for (const edge of edges) { - g.setEdge(edge.source, edge.target); - } - - dagre.layout(g); - - const positions: Record = {}; - for (const node of nodes) { - const pos = g.node(node.id); - positions[node.id] = pos - ? { x: pos.x - node.width / 2, y: pos.y - node.height / 2 } - : { x: 0, y: 0 }; - } - - self.postMessage({ requestId, positions } satisfies LayoutResult); -};