From 034f6ab37fe87f444996e2f8d1fafa6a1747cacf Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Mon, 12 Jan 2026 20:12:31 +0530 Subject: [PATCH] grounding and prompt enhancement --- package-lock.json | 294 ++++++++++++++++++++++ package.json | 1 + src/components/QueryFAB.tsx | 51 +++- src/components/RightPanel.tsx | 150 +++++++++-- src/config/ignore-service.ts | 2 +- src/core/ingestion/call-processor.ts | 86 ++++++- src/core/ingestion/heritage-processor.ts | 123 +++++++++ src/core/ingestion/import-processor.ts | 13 +- src/core/ingestion/pipeline.ts | 21 +- src/core/ingestion/tree-sitter-queries.ts | 27 ++ src/core/kuzu/schema.ts | 4 +- src/core/llm/agent.ts | 41 +-- src/core/llm/tools.ts | 10 +- src/hooks/useAppState.tsx | 65 +++-- src/index.css | 6 +- src/types/pipeline.ts | 2 +- 16 files changed, 816 insertions(+), 80 deletions(-) create mode 100644 src/core/ingestion/heritage-processor.ts diff --git a/package-lock.json b/package-lock.json index cb7b166ec..f146351bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "remark-gfm": "^4.0.1", "sigma": "^3.0.2", "tailwindcss": "^4.1.18", "uuid": "^13.0.0", @@ -7088,6 +7089,16 @@ "dev": true, "license": "ISC" }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/marked": { "version": "16.4.2", "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", @@ -7121,6 +7132,34 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -7145,6 +7184,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -7394,6 +7534,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -8597,6 +8858,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -8630,6 +8909,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", diff --git a/package.json b/package.json index 3adf3576e..54574349e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "remark-gfm": "^4.0.1", "sigma": "^3.0.2", "tailwindcss": "^4.1.18", "uuid": "^13.0.0", diff --git a/src/components/QueryFAB.tsx b/src/components/QueryFAB.tsx index 55f7d4c7a..ccb3c10e9 100644 --- a/src/components/QueryFAB.tsx +++ b/src/components/QueryFAB.tsx @@ -27,14 +27,14 @@ const EXAMPLE_QUERIES = [ export const QueryFAB = () => { const { setHighlightedNodeIds, setQueryResult, queryResult, clearQueryHighlights, graph, runQuery, isDatabaseReady } = useAppState(); - + const [isExpanded, setIsExpanded] = useState(false); const [query, setQuery] = useState(''); const [isRunning, setIsRunning] = useState(false); const [error, setError] = useState(null); const [showExamples, setShowExamples] = useState(false); const [showResults, setShowResults] = useState(true); - + const textareaRef = useRef(null); const panelRef = useRef(null); @@ -67,7 +67,7 @@ export const QueryFAB = () => { const handleRunQuery = useCallback(async () => { if (!query.trim() || isRunning) return; - + if (!graph) { setError('No project loaded. Load a project first.'); return; @@ -81,25 +81,52 @@ export const QueryFAB = () => { setIsRunning(true); setError(null); - + const startTime = performance.now(); - + try { const rows = await runQuery(query); const executionTime = performance.now() - startTime; - - // Extract node IDs - handle both array and object formats - // First column should be the ID when using example queries + + // Extract node IDs from results - handles various formats + // 1. Array format: first element if it looks like a node ID + // 2. Object format: any field ending with 'id' (case-insensitive) + // 3. Values matching node ID pattern: Label:path:name + const nodeIdPattern = /^(File|Function|Class|Method|Interface|Folder|CodeElement):/; + const nodeIds = rows .flatMap(row => { + const ids: string[] = []; + if (Array.isArray(row)) { - return [row[0]]; + // Array format - check all elements for node ID patterns + row.forEach(val => { + if (typeof val === 'string' && (nodeIdPattern.test(val) || val.includes(':'))) { + ids.push(val); + } + }); + } else if (typeof row === 'object' && row !== null) { + // Object format - check fields ending with 'id' and values matching patterns + Object.entries(row).forEach(([key, val]) => { + const keyLower = key.toLowerCase(); + if (typeof val === 'string') { + // Field name contains 'id' + if (keyLower.includes('id') || keyLower === 'id') { + ids.push(val); + } + // Value matches node ID pattern + else if (nodeIdPattern.test(val)) { + ids.push(val); + } + } + }); } - return [row.id, row.ID, row['n.id'], row['a.id'], row['b.id']].filter(Boolean); + + return ids; }) .filter(Boolean) .filter((id, index, arr) => arr.indexOf(id) === index); - + setQueryResult({ rows, nodeIds, executionTime }); setHighlightedNodeIds(new Set(nodeIds)); } catch (err) { @@ -338,7 +365,7 @@ export const QueryFAB = () => { - + {showResults && queryResult.rows.length > 0 && (
diff --git a/src/components/RightPanel.tsx b/src/components/RightPanel.tsx index f36de931d..5c2e18604 100644 --- a/src/components/RightPanel.tsx +++ b/src/components/RightPanel.tsx @@ -6,6 +6,7 @@ import { import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { useAppState } from '../hooks/useAppState'; import { ToolCallCard } from './ToolCallCard'; import { isProviderConfigured } from '../core/llm/settings-service'; @@ -97,7 +98,8 @@ export const RightPanel = () => { let startLine1: number | undefined; let endLine1: number | undefined; - const lineMatch = raw.match(/^(.*):(\d+)(?:-(\d+))?$/); + // Match line:num or line:num-num (supports both hyphen - and en dash –) + const lineMatch = raw.match(/^(.*):(\d+)(?:[-–](\d+))?$/); if (lineMatch) { rawPath = lineMatch[1].trim(); startLine1 = parseInt(lineMatch[2], 10); @@ -120,15 +122,75 @@ export const RightPanel = () => { }); }, [addCodeReference, findFileNodeIdForUI, resolveFilePathForUI]); + // Handler for node grounding: [[Class:View]], [[Function:trigger]], etc. + const handleNodeGroundingClick = useCallback((nodeTypeAndName: string) => { + const raw = nodeTypeAndName.trim(); + if (!raw || !graph) return; + + // Parse Type:Name format + const match = raw.match(/^(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):(.+)$/); + if (!match) return; + + const [, nodeType, nodeName] = match; + const trimmedName = nodeName.trim(); + + // Find node in graph by type + name + const node = graph.nodes.find(n => + n.label === nodeType && + n.properties.name === trimmedName + ); + + if (!node) { + console.warn(`Node not found: ${nodeType}:${trimmedName}`); + return; + } + + // 1. Highlight in graph (add to AI citation highlights) + // Note: This requires accessing the state setter from parent context + // For now, we'll add to code references which triggers the highlight + + // 2. Add to Code Panel (if node has file/line info) + if (node.properties.filePath) { + const resolvedPath = resolveFilePathForUI(node.properties.filePath); + if (resolvedPath) { + addCodeReference({ + filePath: resolvedPath, + startLine: node.properties.startLine ? node.properties.startLine - 1 : undefined, + endLine: node.properties.endLine ? node.properties.endLine - 1 : undefined, + nodeId: node.id, + label: node.label, + name: node.properties.name, + source: 'ai', + }); + } + } + }, [graph, resolveFilePathForUI, addCodeReference]); + const formatMarkdownForDisplay = useCallback((md: string) => { // Avoid rewriting inside fenced code blocks. const parts = md.split('```'); for (let i = 0; i < parts.length; i += 2) { - parts[i] = parts[i].replace(/\[\[([^\]\n]+?)\]\]/g, (_m, inner: string) => { - const trimmed = inner.trim(); - const href = `code-ref:${encodeURIComponent(trimmed)}`; - return `[${trimmed}](${href})`; - }); + // Pattern 1: File grounding - [[file.ext]] or [[file.ext:line]] or [[file.ext:line-line]] + // Line numbers are optional + parts[i] = parts[i].replace( + /\[\[([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+(?::\d+(?:[-–]\d+)?)?)\]\]/g, + (_m, inner: string) => { + const trimmed = inner.trim(); + const href = `code-ref:${encodeURIComponent(trimmed)}`; + return `[${trimmed}](${href})`; + } + ); + + // Pattern 2: Node grounding - [[Type:Name]] or [[graph:Type:Name]] + // Valid types: Class, Function, Method, Interface, File, Folder, Variable, Enum, Type, CodeElement + parts[i] = parts[i].replace( + /\[\[(?:graph:)?(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):([^\]]+)\]\]/g, + (_m, nodeType: string, nodeName: string) => { + const trimmed = `${nodeType}:${nodeName.trim()}`; + const href = `node-ref:${encodeURIComponent(trimmed)}`; + return `[${trimmed}](${href})`; + } + ); } return parts.join('```'); }, []); @@ -143,7 +205,8 @@ export const RightPanel = () => { // Strip query/hash const cleaned = withoutScheme.split('#')[0].split('?')[0]; - const m = cleaned.match(/^(.*):(\d+)(?:-(\d+))?$/); + // Support both hyphen - and en dash – for line ranges + const m = cleaned.match(/^(.*):(\d+)(?:[-–](\d+))?$/); const path = (m ? m[1] : cleaned).replace(/\\/g, '/'); const base = path.split('/').pop() ?? path; @@ -164,8 +227,8 @@ export const RightPanel = () => { // Strip query/hash const cleaned = h.split('#')[0].split('?')[0]; - // Looks like: path/to/file.ext or path\to\file.ext:12-34 - return /[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+(?::\d+(?:-\d+)?)?$/.test(cleaned); + // Looks like: path/to/file.ext or path\to\file.ext:12-34 (supports both hyphen and en dash) + return /[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+(?::\d+(?:[-–]\d+)?)?$/.test(cleaned); }, []); const extractTextFromChildren = useCallback((children: any): string => { @@ -353,6 +416,7 @@ export const RightPanel = () => { {step.type === 'reasoning' && step.content && (
{ if (href && href.startsWith('code-ref:')) { @@ -365,7 +429,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(inner); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${inner}`} {...props} > @@ -373,6 +437,24 @@ export const RightPanel = () => { ); } + // Handle node grounding: node-ref:Type:Name + if (href && href.startsWith('node-ref:')) { + const inner = decodeURIComponent(href.slice('node-ref:'.length)); + return ( + { + e.preventDefault(); + handleNodeGroundingClick(inner); + }} + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors" + title={`View ${inner} in Code panel`} + {...props} + > + {children} + + ); + } const internalRef = getInternalRefFromLink(href, children); if (internalRef) { const label = formatRefChipLabel(internalRef); @@ -383,7 +465,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(internalRef); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${internalRef}`} {...props} > @@ -437,6 +519,7 @@ export const RightPanel = () => { {step.type === 'content' && step.content && (
{ if (href && href.startsWith('code-ref:')) { @@ -449,7 +532,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(inner); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${inner}`} {...props} > @@ -457,6 +540,24 @@ export const RightPanel = () => { ); } + // Handle node grounding: node-ref:Type:Name + if (href && href.startsWith('node-ref:')) { + const inner = decodeURIComponent(href.slice('node-ref:'.length)); + return ( + { + e.preventDefault(); + handleNodeGroundingClick(inner); + }} + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors" + title={`View ${inner} in Code panel`} + {...props} + > + {children} + + ); + } const internalRef = getInternalRefFromLink(href, children); if (internalRef) { const label = formatRefChipLabel(internalRef); @@ -467,7 +568,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(internalRef); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${internalRef}`} {...props} > @@ -529,6 +630,7 @@ export const RightPanel = () => { // Fallback: render content + toolCalls separately (old format)
{ if (href && href.startsWith('code-ref:')) { @@ -541,7 +643,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(inner); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${inner}`} {...props} > @@ -549,6 +651,24 @@ export const RightPanel = () => { ); } + // Handle node grounding: node-ref:Type:Name + if (href && href.startsWith('node-ref:')) { + const inner = decodeURIComponent(href.slice('node-ref:'.length)); + return ( + { + e.preventDefault(); + handleNodeGroundingClick(inner); + }} + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-amber-300/55 bg-amber-400/10 !text-amber-200 visited:!text-amber-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-amber-400/15 hover:border-amber-200/70 transition-colors" + title={`View ${inner} in Code panel`} + {...props} + > + {children} + + ); + } const internalRef = getInternalRefFromLink(href, children); if (internalRef) { const label = formatRefChipLabel(internalRef); @@ -559,7 +679,7 @@ export const RightPanel = () => { e.preventDefault(); handleGroundingClick(internalRef); }} - className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" + className="code-ref-btn inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors" title={`Open in Code panel • ${internalRef}`} {...props} > diff --git a/src/config/ignore-service.ts b/src/config/ignore-service.ts index fb99605df..affd83578 100644 --- a/src/config/ignore-service.ts +++ b/src/config/ignore-service.ts @@ -19,7 +19,7 @@ const DEFAULT_IGNORE_LIST = new Set([ 'bower_components', 'jspm_packages', 'vendor', // PHP/Go - 'packages', // Sometimes used for deps + // 'packages' removed - commonly used for monorepo source code (lerna, pnpm, yarn workspaces) 'venv', '.venv', 'env', diff --git a/src/core/ingestion/call-processor.ts b/src/core/ingestion/call-processor.ts index 0706767aa..2f866d842 100644 --- a/src/core/ingestion/call-processor.ts +++ b/src/core/ingestion/call-processor.ts @@ -7,6 +7,81 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries'; import { generateId } from '../../lib/utils'; import { getLanguageFromFilename } from './utils'; +/** + * Node types that represent function/method definitions across languages. + * Used to find the enclosing function for a call site. + */ +const FUNCTION_NODE_TYPES = new Set([ + // TypeScript/JavaScript + 'function_declaration', + 'arrow_function', + 'function_expression', + 'method_definition', + 'generator_function_declaration', + // Python + 'function_definition', + // Common async variants + 'async_function_declaration', + 'async_arrow_function', +]); + +/** + * Walk up the AST from a node to find the enclosing function/method. + * Returns null if the call is at module/file level (top-level code). + */ +const findEnclosingFunction = ( + node: any, + filePath: string, + symbolTable: SymbolTable +): string | null => { + let current = node.parent; + + while (current) { + if (FUNCTION_NODE_TYPES.has(current.type)) { + // Found enclosing function - try to get its name + let funcName: string | null = null; + + // Different node types have different name locations + if (current.type === 'function_declaration' || + current.type === 'function_definition' || + current.type === 'async_function_declaration' || + current.type === 'generator_function_declaration') { + // Named function: function foo() {} + const nameNode = current.childForFieldName?.('name') || + current.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier'); + funcName = nameNode?.text; + } else if (current.type === 'method_definition') { + // Method: foo() {} inside class + const nameNode = current.childForFieldName?.('name') || + current.children?.find((c: any) => c.type === 'property_identifier'); + funcName = nameNode?.text; + } else if (current.type === 'arrow_function' || current.type === 'function_expression') { + // Arrow/expression: const foo = () => {} - check parent variable declarator + const parent = current.parent; + if (parent?.type === 'variable_declarator') { + const nameNode = parent.childForFieldName?.('name') || + parent.children?.find((c: any) => c.type === 'identifier'); + funcName = nameNode?.text; + } + } + + if (funcName) { + // Look up the function in symbol table to get its node ID + const nodeId = symbolTable.lookupExact(filePath, funcName); + if (nodeId) return nodeId; + + // Fallback: generate ID based on name and file + return generateId('Function', `${filePath}:${funcName}`); + } + + // Couldn't determine function name - try parent (might be nested) + } + current = current.parent; + } + + return null; // Top-level call (not inside any function) +}; + export const processCalls = async ( graph: KnowledgeGraph, files: { path: string; content: string }[], @@ -78,9 +153,14 @@ export const processCalls = async ( if (!targetNodeId) return; - // 5. Create CALLS relationship (File -> Function/Method) - const sourceId = generateId('File', file.path); - const relId = generateId('CALLS', `${file.path}:${calledName}->${targetNodeId}`); + // 5. Find the enclosing function (caller) + const callNode = captureMap['call']; + const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable); + + // Use enclosing function as source, fallback to file for top-level calls + const sourceId = enclosingFuncId || generateId('File', file.path); + + const relId = generateId('CALLS', `${sourceId}:${calledName}->${targetNodeId}`); graph.addRelationship({ id: relId, diff --git a/src/core/ingestion/heritage-processor.ts b/src/core/ingestion/heritage-processor.ts new file mode 100644 index 000000000..04bf6e532 --- /dev/null +++ b/src/core/ingestion/heritage-processor.ts @@ -0,0 +1,123 @@ +/** + * Heritage Processor + * + * Extracts class inheritance relationships: + * - EXTENDS: Class extends another Class (TS, JS, Python) + * - IMPLEMENTS: Class implements an Interface (TS only) + */ + +import { KnowledgeGraph } from '../graph/types'; +import { ASTCache } from './ast-cache'; +import { SymbolTable } from './symbol-table'; +import { loadParser, loadLanguage } from '../tree-sitter/parser-loader'; +import { LANGUAGE_QUERIES } from './tree-sitter-queries'; +import { generateId } from '../../lib/utils'; +import { getLanguageFromFilename } from './utils'; + +export const processHeritage = async ( + graph: KnowledgeGraph, + files: { path: string; content: string }[], + astCache: ASTCache, + symbolTable: SymbolTable, + onProgress?: (current: number, total: number) => void +) => { + const parser = await loadParser(); + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + onProgress?.(i + 1, files.length); + + // 1. Check language support + const language = getLanguageFromFilename(file.path); + if (!language) continue; + + const queryStr = LANGUAGE_QUERIES[language]; + if (!queryStr) continue; + + // 2. Load the language + await loadLanguage(language, file.path); + + // 3. Get AST + let tree = astCache.get(file.path); + let wasReparsed = false; + + if (!tree) { + tree = parser.parse(file.content); + wasReparsed = true; + } + + let query; + let matches; + try { + query = parser.getLanguage().query(queryStr); + matches = query.matches(tree.rootNode); + } catch (queryError) { + console.warn(`Heritage query error for ${file.path}:`, queryError); + if (wasReparsed) tree.delete(); + continue; + } + + // 4. Process heritage matches + matches.forEach(match => { + const captureMap: Record = {}; + match.captures.forEach(c => { + captureMap[c.name] = c.node; + }); + + // EXTENDS: Class extends another Class + if (captureMap['heritage.class'] && captureMap['heritage.extends']) { + const className = captureMap['heritage.class'].text; + const parentClassName = captureMap['heritage.extends'].text; + + // Resolve both class IDs + const childId = symbolTable.lookupExact(file.path, className) || + symbolTable.lookupFuzzy(className)[0]?.nodeId || + generateId('Class', `${file.path}:${className}`); + + const parentId = symbolTable.lookupFuzzy(parentClassName)[0]?.nodeId || + generateId('Class', `${parentClassName}`); + + if (childId && parentId && childId !== parentId) { + const relId = generateId('EXTENDS', `${childId}->${parentId}`); + + graph.addRelationship({ + id: relId, + sourceId: childId, + targetId: parentId, + type: 'EXTENDS' + }); + } + } + + // IMPLEMENTS: Class implements Interface (TypeScript only) + if (captureMap['heritage.class'] && captureMap['heritage.implements']) { + const className = captureMap['heritage.class'].text; + const interfaceName = captureMap['heritage.implements'].text; + + // Resolve class and interface IDs + const classId = symbolTable.lookupExact(file.path, className) || + symbolTable.lookupFuzzy(className)[0]?.nodeId || + generateId('Class', `${file.path}:${className}`); + + const interfaceId = symbolTable.lookupFuzzy(interfaceName)[0]?.nodeId || + generateId('Interface', `${interfaceName}`); + + if (classId && interfaceId) { + const relId = generateId('IMPLEMENTS', `${classId}->${interfaceId}`); + + graph.addRelationship({ + id: relId, + sourceId: classId, + targetId: interfaceId, + type: 'IMPLEMENTS' + }); + } + } + }); + + // Cleanup + if (wasReparsed) { + tree.delete(); + } + } +}; diff --git a/src/core/ingestion/import-processor.ts b/src/core/ingestion/import-processor.ts index 77fba9a80..e30bf34ac 100644 --- a/src/core/ingestion/import-processor.ts +++ b/src/core/ingestion/import-processor.ts @@ -86,8 +86,17 @@ export const processImports = async ( try { query = parser.getLanguage().query(queryStr); matches = query.matches(tree.rootNode); - } catch (queryError) { - console.warn(`Query error for ${file.path}:`, queryError); + } catch (queryError: any) { + // Detailed debug logging for query failures + console.group(`🔴 Query Error: ${file.path}`); + console.log('Language:', language); + console.log('Query (first 200 chars):', queryStr.substring(0, 200) + '...'); + console.log('Error:', queryError?.message || queryError); + console.log('File content (first 300 chars):', file.content.substring(0, 300)); + console.log('AST root type:', tree.rootNode?.type); + console.log('AST has errors:', tree.rootNode?.hasError); + console.groupEnd(); + if (wasReparsed) tree.delete(); continue; } diff --git a/src/core/ingestion/pipeline.ts b/src/core/ingestion/pipeline.ts index 746f53785..d598c3f28 100644 --- a/src/core/ingestion/pipeline.ts +++ b/src/core/ingestion/pipeline.ts @@ -4,6 +4,7 @@ import { processStructure } from './structure-processor'; import { processParsing } from './parsing-processor'; import { processImports, createImportMap } from './import-processor'; import { processCalls } from './call-processor'; +import { processHeritage } from './heritage-processor'; import { createSymbolTable } from './symbol-table'; import { createASTCache } from './ast-cache'; import { PipelineProgress, PipelineResult } from '../../types/pipeline'; @@ -131,7 +132,7 @@ export const runPipelineFromFiles = async ( }); await processCalls(graph, files, astCache, symbolTable, importMap, (current, total) => { - const callProgress = 82 + ((current / total) * 16); + const callProgress = 82 + ((current / total) * 10); onProgress({ phase: 'calls', percent: Math.round(callProgress), @@ -140,6 +141,24 @@ export const runPipelineFromFiles = async ( }); }); + // Phase 6: Heritage - Class inheritance (92-98%) + onProgress({ + phase: 'heritage', + percent: 92, + message: 'Extracting class inheritance...', + stats: { filesProcessed: 0, totalFiles: files.length, nodesCreated: graph.nodeCount }, + }); + + await processHeritage(graph, files, astCache, symbolTable, (current, total) => { + const heritageProgress = 92 + ((current / total) * 6); + onProgress({ + phase: 'heritage', + percent: Math.round(heritageProgress), + message: 'Extracting class inheritance...', + stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount }, + }); + }); + // Phase 6: Complete (100%) onProgress({ diff --git a/src/core/ingestion/tree-sitter-queries.ts b/src/core/ingestion/tree-sitter-queries.ts index 4eb6686ab..674059649 100644 --- a/src/core/ingestion/tree-sitter-queries.ts +++ b/src/core/ingestion/tree-sitter-queries.ts @@ -53,6 +53,20 @@ export const TYPESCRIPT_QUERIES = ` (call_expression function: (member_expression property: (property_identifier) @call.name)) @call + +; Heritage queries - class extends +(class_declaration + name: (type_identifier) @heritage.class + (class_heritage + (extends_clause + value: (identifier) @heritage.extends))) @heritage + +; Heritage queries - class implements interface +(class_declaration + name: (type_identifier) @heritage.class + (class_heritage + (implements_clause + (type_identifier) @heritage.implements))) @heritage.impl `; // JavaScript queries - works with tree-sitter-javascript @@ -97,6 +111,13 @@ export const JAVASCRIPT_QUERIES = ` (call_expression function: (member_expression property: (property_identifier) @call.name)) @call + +; Heritage queries - class extends (JavaScript uses different AST than TypeScript) +; In tree-sitter-javascript, class_heritage directly contains the parent identifier +(class_declaration + name: (identifier) @heritage.class + (class_heritage + (identifier) @heritage.extends)) @heritage `; // Python queries - works with tree-sitter-python @@ -119,6 +140,12 @@ export const PYTHON_QUERIES = ` (call function: (attribute attribute: (identifier) @call.name)) @call + +; Heritage queries - Python class inheritance +(class_definition + name: (identifier) @heritage.class + superclasses: (argument_list + (identifier) @heritage.extends)) @heritage `; export const LANGUAGE_QUERIES: Record = { diff --git a/src/core/kuzu/schema.ts b/src/core/kuzu/schema.ts index 67fb19f3a..cc14828ac 100644 --- a/src/core/kuzu/schema.ts +++ b/src/core/kuzu/schema.ts @@ -21,7 +21,7 @@ export type NodeTableName = typeof NODE_TABLES[number]; export const REL_TABLE_NAME = 'CodeRelation'; // Valid relation types -export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS'] as const; +export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS'] as const; export type RelType = typeof REL_TYPES[number]; // ============================================================================ @@ -125,6 +125,8 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM Function TO Method, FROM Class TO Method, FROM Class TO Function, + FROM Class TO Class, + FROM Class TO Interface, type STRING )`; diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index 1f04ca108..c665afdfd 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -47,21 +47,17 @@ import { export const BASE_SYSTEM_PROMPT = `You are Nexus, a Code Analysis Agent with access to a Knowledge Graph. Your responses MUST be grounded. ## ⚠️ MANDATORY: GROUNDING -Every factual claim MUST include a citation in [[file:line]] format. -- When you mention a function, class, or file → cite where you found it -- When you describe behavior → cite the code that proves it -- NO citation = NO claim. If you can't cite it, say "I didn't find evidence for this." - -Examples: -- "The AuthService handles login [[src/services/auth.ts:45]]." -- "This function calls validateToken [[src/utils.ts:12]] which throws on invalid tokens [[src/utils.ts:28]]." +Every factual claim MUST include a citation. +- File refs: [[src/auth.ts:45-60]] (line range with hyphen) +- Node refs: [[Class:AuthService]] or [[Function:validate]] +- NO citation = NO claim. Say "I didn't find evidence" instead of guessing. ## 🧠 CORE PROTOCOL You are an investigator. For each question: 1. **Search** → Use search or grep to find relevant code 2. **Read** → Use read to see the actual source 3. **Trace** → Use cypher to follow connections in the graph -4. **Cite** → Ground every finding with [[file:line]] +4. **Cite** → Ground every finding with [[file:line]] or [[Type:Name]] 5. **Highlight** → Visualize key nodes with highlight ## 🛠️ TOOLS @@ -73,20 +69,35 @@ You are an investigator. For each question: ## 📊 GRAPH SCHEMA Nodes: File, Folder, Function, Class, Interface, Method, CodeElement -Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS +Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS Cypher examples: - \`MATCH (f:Function) RETURN f.name LIMIT 10\` - \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\` -## � RULES +## 📝 RULES - **Cite or retract.** Never state something you can't ground. - **Read before concluding.** Don't guess from names alone. -- **Retry on failure.** If a tool fails, fix the input and try again.`; +- **Retry on failure.** If a tool fails, fix the input and try again. -/** - * Create a chat model instance from provider configuration - */ +## 🎯 OUTPUT STYLE +Think like a senior architect. Be concise—no fluff, short, precise and to the point. +- Use tables for comparisons/rankings +- Use mermaid diagrams for flows/dependencies +- Surface deep insights: patterns, coupling, design decisions +- End with **TL;DR** (1-2 sentences) + +## MERMAID RULES +When generating diagrams: +- NO special characters in node labels: quotes, (), /, &, <, > +- Wrap labels with spaces in quotes: A["My Label"] +- Use simple IDs: A, B, C or auth, db, api +- Flowchart: graph TD or graph LR (not flowchart) +- Always test mentally: would this parse? + +BAD: A[User's Data] --> B(Process & Save) +GOOD: A["User Data"] --> B["Process and Save"] +`; export const createChatModel = (config: ProviderConfig): BaseChatModel => { switch (config.provider) { case 'azure-openai': { diff --git a/src/core/llm/tools.ts b/src/core/llm/tools.ts index 50922e9ff..53a377bfc 100644 --- a/src/core/llm/tools.ts +++ b/src/core/llm/tools.ts @@ -186,15 +186,17 @@ export const createGraphRAGTools = ( }, { name: 'cypher', - description: `Execute a Cypher query against the code graph. Use for structural queries like finding callers, tracing imports, or custom traversals. + description: `Execute a Cypher query against the code graph. Use for structural queries like finding callers, tracing imports, class inheritance, or custom traversals. Node tables: File, Folder, Function, Class, Interface, Method, CodeElement -Relation: CodeRelation (single table with 'type' property: CONTAINS, DEFINES, IMPORTS, CALLS) +Relation: CodeRelation (single table with 'type' property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS) Example queries: +- Functions calling a function: MATCH (caller:Function)-[:CodeRelation {type: 'CALLS'}]->(fn:Function {name: 'validate'}) RETURN caller.name, caller.filePath +- Class inheritance: MATCH (child:Class)-[:CodeRelation {type: 'EXTENDS'}]->(parent:Class) RETURN child.name, parent.name +- Classes implementing interface: MATCH (c:Class)-[:CodeRelation {type: 'IMPLEMENTS'}]->(i:Interface) RETURN c.name, i.name - Files importing a file: MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(target:File) WHERE target.name = 'utils.ts' RETURN f.name -- Functions defined in file: MATCH (f:File {name: 'main.ts'})-[:CodeRelation {type: 'DEFINES'}]->(fn:Function) RETURN fn.name -- All connections: MATCH (f:File {name: 'index.ts'})-[r:CodeRelation]-(m) RETURN m.name, r.type +- All connections: MATCH (n)-[r:CodeRelation]-(m) WHERE n.name = 'MyClass' RETURN m.name, r.type For semantic+graph queries, include {{QUERY_VECTOR}} placeholder and provide a 'query' parameter: CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', {{QUERY_VECTOR}}, 10) YIELD node AS emb, distance diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index cee6084d5..bcb31f624 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -646,47 +646,68 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } updateMessage(); - // Parse inline grounding references like [[src/file.ts:10-25]] - // and add them to the Code References panel. + // Parse inline grounding references and add them to the Code References panel. + // Supports: [[file.ts:10-25]] (file refs) and [[Class:View]] (node refs) const currentContentStep = stepsForMessage[stepsForMessage.length - 1]; const fullText = (currentContentStep && currentContentStep.type === 'content') ? (currentContentStep.content || '') : ''; - const refRegex = /\[\[([^\]\n]+?)\]\]/g; - let match: RegExpExecArray | null; - while ((match = refRegex.exec(fullText)) !== null) { - const inner = match[1].trim(); - if (!inner) continue; - - let rawPath = inner; - let startLine1: number | undefined; - let endLine1: number | undefined; - - const lineMatch = inner.match(/^(.*):(\d+)(?:-(\d+))?$/); - if (lineMatch) { - rawPath = lineMatch[1].trim(); - startLine1 = parseInt(lineMatch[2], 10); - endLine1 = parseInt(lineMatch[3] || lineMatch[2], 10); - } + // Pattern 1: File refs - [[path/file.ext]] or [[path/file.ext:line]] or [[path/file.ext:line-line]] + // Line numbers are optional + const fileRefRegex = /\[\[([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)(?::(\d+)(?:[-–](\d+))?)?\]\]/g; + let fileMatch: RegExpExecArray | null; + while ((fileMatch = fileRefRegex.exec(fullText)) !== null) { + const rawPath = fileMatch[1].trim(); + const startLine1 = fileMatch[2] ? parseInt(fileMatch[2], 10) : undefined; + const endLine1 = fileMatch[3] ? parseInt(fileMatch[3], 10) : startLine1; const resolvedPath = resolveFilePath(rawPath); if (!resolvedPath) continue; - const startLine0 = startLine1 ? Math.max(0, startLine1 - 1) : 0; - const endLine0 = endLine1 ? Math.max(0, endLine1 - 1) : startLine0; + const startLine0 = startLine1 !== undefined ? Math.max(0, startLine1 - 1) : undefined; + const endLine0 = endLine1 !== undefined ? Math.max(0, endLine1 - 1) : startLine0; const nodeId = findFileNodeId(resolvedPath); addCodeReference({ filePath: resolvedPath, - startLine: startLine1 ? startLine0 : undefined, - endLine: endLine1 ? endLine0 : (startLine1 ? startLine0 : undefined), + startLine: startLine0, + endLine: endLine0, nodeId, label: 'File', name: resolvedPath.split('/').pop() ?? resolvedPath, source: 'ai', }); } + + // Pattern 2: Node refs - [[Type:Name]] or [[graph:Type:Name]] + const nodeRefRegex = /\[\[(?:graph:)?(Class|Function|Method|Interface|File|Folder|Variable|Enum|Type|CodeElement):([^\]]+)\]\]/g; + let nodeMatch: RegExpExecArray | null; + while ((nodeMatch = nodeRefRegex.exec(fullText)) !== null) { + const nodeType = nodeMatch[1]; + const nodeName = nodeMatch[2].trim(); + + // Find node in graph + if (!graph) continue; + const node = graph.nodes.find(n => + n.label === nodeType && + n.properties.name === nodeName + ); + if (!node || !node.properties.filePath) continue; + + const resolvedPath = resolveFilePath(node.properties.filePath); + if (!resolvedPath) continue; + + addCodeReference({ + filePath: resolvedPath, + startLine: node.properties.startLine ? node.properties.startLine - 1 : undefined, + endLine: node.properties.endLine ? node.properties.endLine - 1 : undefined, + nodeId: node.id, + label: node.label, + name: node.properties.name, + source: 'ai', + }); + } } break; diff --git a/src/index.css b/src/index.css index c62031c0b..207b67fbe 100644 --- a/src/index.css +++ b/src/index.css @@ -305,14 +305,14 @@ body { border-top: 1px solid var(--color-border-subtle); } -/* Links */ -.chat-prose a { +/* Links - but NOT grounding/citation buttons (those have their own Tailwind styles) */ +.chat-prose a:not(.code-ref-btn) { color: var(--color-accent); text-decoration: underline; text-underline-offset: 2px; } -.chat-prose a:hover { +.chat-prose a:not(.code-ref-btn):hover { color: #a78bfa; } diff --git a/src/types/pipeline.ts b/src/types/pipeline.ts index 43e78e975..ca7218593 100644 --- a/src/types/pipeline.ts +++ b/src/types/pipeline.ts @@ -1,6 +1,6 @@ import { GraphNode, GraphRelationship, KnowledgeGraph } from '../core/graph/types'; -export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'complete' | 'error'; +export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'complete' | 'error'; export interface PipelineProgress { phase: PipelinePhase;