log verbosity decreased, call resolutions improved for py and ts, multiple other changes

This commit is contained in:
abhigyantrumio 2025-08-24 01:28:55 +05:30
parent 5c9b1281f3
commit e99a636bc3
23 changed files with 2510 additions and 289 deletions

3
.gitignore vendored
View file

@ -12,6 +12,9 @@ dist
dist-ssr
*.local
# Auto-generated files
public/workers/compiled-queries.js
# Editor directories and files
.vscode/*
!.vscode/extensions.json

View file

@ -4,8 +4,9 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"dev": "npm run compile-queries && vite",
"build": "npm run compile-queries && tsc -b && vite build",
"compile-queries": "node scripts/compile-queries.js",
"lint": "eslint .",
"preview": "vite preview",
"test": "jest",

View file

@ -5,6 +5,8 @@
// Import tree-sitter and language parsers
import Parser from 'web-tree-sitter';
// Import compiled queries (generated from TypeScript)
import { getQueriesForLanguage } from './compiled-queries.js';
// Initialize tree-sitter
let parser = null;
@ -98,109 +100,61 @@ function extractDefinitions(tree, filePath) {
return definitions;
}
// Get queries for specific language
function getQueriesForLanguage(language) {
const queries = {
typescript: {
function_declaration: `
(function_declaration
name: (identifier) @function.name
parameters: (formal_parameters) @function.parameters
body: (statement_block) @function.body
)
`,
class_declaration: `
(class_declaration
name: (identifier) @class.name
body: (class_body) @class.body
)
`,
method_definition: `
(method_definition
name: (property_identifier) @method.name
parameters: (formal_parameters) @method.parameters
body: (statement_block) @method.body
)
`,
import_statement: `
(import_statement
source: (string) @import.source
)
`
},
javascript: {
function_declaration: `
(function_declaration
name: (identifier) @function.name
parameters: (formal_parameters) @function.parameters
body: (statement_block) @function.body
)
`,
arrow_function: `
(arrow_function
parameters: (formal_parameters) @function.parameters
body: (statement_block) @function.body
)
`,
class_declaration: `
(class_declaration
name: (identifier) @class.name
body: (class_body) @class.body
)
`
},
python: {
function_definition: `
(function_definition
name: (identifier) @function.name
parameters: (parameters) @function.parameters
body: (block) @function.body
)
`,
class_definition: `
(class_definition
name: (identifier) @class.name
body: (block) @class.body
)
`,
import_statement: `
(import_statement
name: (dotted_name) @import.name
)
`
}
};
// Get queries for specific language - now uses imported compiled queries
// This ensures consistency with the main thread parsing logic
return queries[language] || null;
// Helper function to map query names to definition types (matches main thread)
function getDefinitionType(queryName) {
switch (queryName) {
case 'classes': return 'class';
case 'methods': return 'method';
case 'functions':
case 'arrowFunctions': return 'function';
case 'imports':
case 'from_imports': return 'import';
case 'interfaces': return 'interface';
case 'types': return 'type';
case 'decorators': return 'decorator';
default: return 'variable';
}
}
// Process a query match into a definition
// Updated to match main thread's extractDefinition logic exactly
function processMatch(match, filePath, queryType) {
try {
const captures = match.captures;
const definition = {
type: queryType,
filePath,
startLine: match.node.startPosition.row,
endLine: match.node.endPosition.row,
startColumn: match.node.startPosition.column,
endColumn: match.node.endPosition.column
};
// Extract specific information based on query type
for (const capture of captures) {
const { name, node } = capture;
// Main thread processes each capture individually
// For each match, process all captures
const definitions = [];
for (const capture of match.captures) {
const node = capture.node;
if (name.includes('name')) {
definition.name = node.text;
} else if (name.includes('parameters')) {
definition.parameters = node.text;
} else if (name.includes('source')) {
definition.importSource = node.text.replace(/['"]/g, '');
}
}
// Extract name from the node (same logic as main thread)
const nameNode = node.childForFieldName('name');
const name = nameNode ? nameNode.text : 'anonymous';
return definition;
const definition = {
name: name,
type: getDefinitionType(queryType), // Use proper type mapping
filePath: filePath,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
startColumn: node.startPosition.column,
endColumn: node.endPosition.column
};
// Extract additional fields if available
const parametersNode = node.childForFieldName('parameters');
if (parametersNode) {
definition.parameters = parametersNode.text;
}
definitions.push(definition);
}
// Return the first definition (main thread processes one capture at a time)
return definitions.length > 0 ? definitions[0] : null;
} catch (error) {
console.warn('Worker: Error processing match:', error);
return null;

View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Build-time script to compile TypeScript queries into JavaScript for Web Workers
* This allows workers to import the same query definitions as the main thread
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Path to the TypeScript queries file
const queriesPath = path.join(__dirname, '../src/core/ingestion/tree-sitter-queries.ts');
// Output path for compiled JavaScript queries
const outputPath = path.join(__dirname, '../public/workers/compiled-queries.js');
function compileQueries() {
try {
console.log('🔨 Compiling Tree-sitter queries for Web Workers...');
// Read the TypeScript queries file
const queriesContent = fs.readFileSync(queriesPath, 'utf8');
// Extract the query objects using simple regex (since they're just object literals)
const typescriptMatch = queriesContent.match(/export const TYPESCRIPT_QUERIES = ({[\s\S]*?});/);
const javascriptMatch = queriesContent.match(/export const JAVASCRIPT_QUERIES = ({[\s\S]*?});/);
const pythonMatch = queriesContent.match(/export const PYTHON_QUERIES = ({[\s\S]*?});/);
const javaMatch = queriesContent.match(/export const JAVA_QUERIES = ({[\s\S]*?});/);
if (!typescriptMatch || !javascriptMatch || !pythonMatch || !javaMatch) {
throw new Error('Could not extract queries from TypeScript file');
}
// Create JavaScript module content
const jsContent = `/**
* AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
* Generated from src/core/ingestion/tree-sitter-queries.ts
* Run 'npm run compile-queries' to regenerate
*/
export const TYPESCRIPT_QUERIES = ${typescriptMatch[1]};
export const JAVASCRIPT_QUERIES = ${javascriptMatch[1]};
export const PYTHON_QUERIES = ${pythonMatch[1]};
export const JAVA_QUERIES = ${javaMatch[1]};
// Helper function to get queries for a specific language
export function getQueriesForLanguage(language) {
switch (language) {
case 'typescript':
return TYPESCRIPT_QUERIES;
case 'javascript':
return JAVASCRIPT_QUERIES;
case 'python':
return PYTHON_QUERIES;
case 'java':
return JAVA_QUERIES;
default:
return null;
}
}
// Export individual query sets for backward compatibility
export const queries = {
typescript: TYPESCRIPT_QUERIES,
javascript: JAVASCRIPT_QUERIES,
python: PYTHON_QUERIES,
java: JAVA_QUERIES
};
`;
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write the compiled JavaScript file
fs.writeFileSync(outputPath, jsContent, 'utf8');
console.log(`✅ Queries compiled successfully to: ${outputPath}`);
} catch (error) {
console.error('❌ Failed to compile queries:', error.message);
process.exit(1);
}
}
compileQueries();

43
scripts/test-wasm.js Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* TEMPORARY WASM TEST RUNNER
* Run this to verify Tree-sitter WASM functionality
* DELETE THIS FILE after confirming everything works
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '..');
console.log('🔍 Starting WASM Verification Tests...');
console.log('📁 Project root:', projectRoot);
// Run the WASM verification tests
const testProcess = spawn('npm', ['run', 'test', 'src/tests/wasm-verification.test.ts'], {
cwd: projectRoot,
stdio: 'inherit',
shell: true
});
testProcess.on('close', (code) => {
if (code === 0) {
console.log('\n✅ All WASM verification tests passed!');
console.log('🧹 You can now safely delete:');
console.log(' - src/tests/wasm-verification.test.ts');
console.log(' - scripts/test-wasm.js');
} else {
console.log('\n❌ WASM verification tests failed!');
console.log('🔧 Please check the issues above before proceeding.');
}
process.exit(code);
});
testProcess.on('error', (error) => {
console.error('❌ Failed to run tests:', error);
process.exit(1);
});

View file

@ -0,0 +1,223 @@
#!/usr/bin/env node
/**
* DIRECT WASM VERIFICATION SCRIPT
* Tests Tree-sitter WASM functionality without Jest
* DELETE THIS FILE after confirming everything works
*/
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = dirname(__dirname);
console.log('🔍 Direct WASM Verification Starting...');
console.log('📁 Project root:', projectRoot);
// Test 1: Check if WASM files exist
async function checkWasmFiles() {
console.log('\n📋 Test 1: Checking WASM file existence...');
const wasmFiles = [
'public/wasm/python/tree-sitter-python.wasm',
'public/wasm/javascript/tree-sitter-javascript.wasm',
'public/wasm/typescript/tree-sitter-typescript.wasm',
'public/wasm/tree-sitter.wasm'
];
let allExist = true;
for (const wasmPath of wasmFiles) {
const fullPath = join(projectRoot, wasmPath);
if (fs.existsSync(fullPath)) {
const stats = fs.statSync(fullPath);
console.log(`${wasmPath} exists (${(stats.size / 1024).toFixed(1)}KB)`);
} else {
console.log(`${wasmPath} missing`);
allExist = false;
}
}
return allExist;
}
// Test 2: Try to load Tree-sitter in Node.js environment
async function testTreeSitterLoad() {
console.log('\n📋 Test 2: Testing Tree-sitter module loading...');
try {
// Try dynamic import
const Parser = await import('web-tree-sitter');
console.log('✅ web-tree-sitter module imported successfully');
console.log(` Default export type: ${typeof Parser.default}`);
if (Parser.default && typeof Parser.default === 'function') {
console.log('✅ Parser constructor available');
return true;
} else {
console.log('❌ Parser constructor not found');
return false;
}
} catch (error) {
console.log('❌ Failed to import web-tree-sitter:', error.message);
return false;
}
}
// Test 3: Check compiled queries
async function testCompiledQueries() {
console.log('\n📋 Test 3: Testing compiled queries...');
try {
const compiledQueriesPath = join(projectRoot, 'public/workers/compiled-queries.js');
if (!fs.existsSync(compiledQueriesPath)) {
console.log('❌ compiled-queries.js not found');
return false;
}
console.log('✅ compiled-queries.js exists');
// Read and basic parse check
const content = fs.readFileSync(compiledQueriesPath, 'utf8');
if (content.includes('PYTHON_QUERIES') && content.includes('TYPESCRIPT_QUERIES')) {
console.log('✅ Compiled queries contain expected exports');
// Count Python queries
const pythonQueryMatch = content.match(/PYTHON_QUERIES = ({[\\s\\S]*?});/);
if (pythonQueryMatch) {
const queryCount = (pythonQueryMatch[1].match(/:\\s*`/g) || []).length;
console.log(`✅ Python queries: ${queryCount} query types found`);
}
return true;
} else {
console.log('❌ Compiled queries missing expected exports');
return false;
}
} catch (error) {
console.log('❌ Error checking compiled queries:', error.message);
return false;
}
}
// Test 4: Check parser loader module
async function testParserLoader() {
console.log('\n📋 Test 4: Testing parser loader module...');
try {
const parserLoaderPath = join(projectRoot, 'src/core/tree-sitter/parser-loader.ts');
if (!fs.existsSync(parserLoaderPath)) {
console.log('❌ parser-loader.ts not found');
return false;
}
console.log('✅ parser-loader.ts exists');
// Try importing the module
const loaderModule = await import(`file://${parserLoaderPath}`);
const expectedExports = ['initTreeSitter', 'loadPythonParser', 'loadJavaScriptParser', 'loadTypeScriptParser'];
let allExportsPresent = true;
for (const exportName of expectedExports) {
if (typeof loaderModule[exportName] === 'function') {
console.log(`${exportName} function exported`);
} else {
console.log(`${exportName} function missing`);
allExportsPresent = false;
}
}
return allExportsPresent;
} catch (error) {
console.log('❌ Error testing parser loader:', error.message);
return false;
}
}
// Test 5: Verify Tree-sitter queries syntax
async function testQuerySyntax() {
console.log('\n📋 Test 5: Testing Tree-sitter query syntax...');
try {
const queriesPath = join(projectRoot, 'src/core/ingestion/tree-sitter-queries.ts');
if (!fs.existsSync(queriesPath)) {
console.log('❌ tree-sitter-queries.ts not found');
return false;
}
const content = fs.readFileSync(queriesPath, 'utf8');
// Basic syntax checks
if (!content.includes('PYTHON_QUERIES') || !content.includes('TYPESCRIPT_QUERIES')) {
console.log('❌ Missing expected query exports');
return false;
}
// Check for the async function query issue we fixed
if (content.includes('async_functions')) {
console.log('❌ async_functions query still present (should be removed)');
return false;
}
console.log('✅ Query file structure looks correct');
console.log('✅ No problematic async_functions query found');
return true;
} catch (error) {
console.log('❌ Error checking query syntax:', error.message);
return false;
}
}
// Run all tests
async function runAllTests() {
console.log('🚀 Starting comprehensive WASM verification...\n');
const results = [];
results.push(await checkWasmFiles());
results.push(await testTreeSitterLoad());
results.push(await testCompiledQueries());
results.push(await testParserLoader());
results.push(await testQuerySyntax());
const passedTests = results.filter(Boolean).length;
const totalTests = results.length;
console.log(`\n📊 Results: ${passedTests}/${totalTests} tests passed`);
if (passedTests === totalTests) {
console.log('\n✅ All WASM verification tests PASSED!');
console.log('🎉 Tree-sitter WASM setup appears to be working correctly');
console.log('\n🧹 You can now safely delete these test files:');
console.log(' - scripts/verify-wasm-direct.js');
console.log(' - scripts/test-wasm.js');
console.log(' - src/tests/wasm-verification.test.ts');
console.log(' - src/__tests__/setup.ts');
console.log('\n🚀 Ready to proceed with ZIP upload testing!');
return true;
} else {
console.log('\n❌ Some WASM verification tests FAILED!');
console.log('🔧 Please fix the issues above before proceeding.');
return false;
}
}
runAllTests()
.then(success => {
process.exit(success ? 0 : 1);
})
.catch(error => {
console.error('💥 Unexpected error:', error);
process.exit(1);
});

View file

@ -82,4 +82,52 @@ export const cleanup = () => {
// Clean up any test artifacts
jest.clearAllTimers();
jest.clearAllMocks();
};
};
/**
* Jest test setup for WASM verification
* TEMPORARY FILE - DELETE after WASM verification
*/
// Mock fetch for WASM file loading
global.fetch = jest.fn();
// Mock URL.createObjectURL and revokeObjectURL for worker tests
global.URL.createObjectURL = jest.fn(() => 'mock-blob-url');
global.URL.revokeObjectURL = jest.fn();
// Mock Worker for worker tests
global.Worker = jest.fn().mockImplementation(() => ({
postMessage: jest.fn(),
terminate: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
onmessage: null,
onerror: null
}));
// Setup console for test output
const originalConsoleLog = console.log;
console.log = (...args) => {
// Allow our test messages through
if (args[0] && typeof args[0] === 'string' && (
args[0].includes('✅') ||
args[0].includes('❌') ||
args[0].includes('🔍') ||
args[0].includes('🧪')
)) {
originalConsoleLog(...args);
}
};
// Mock WebAssembly for basic checks
if (typeof WebAssembly === 'undefined') {
global.WebAssembly = {
compile: jest.fn(),
instantiate: jest.fn(),
Module: jest.fn(),
Instance: jest.fn()
};
}
console.log('🔧 Jest setup completed for WASM verification tests');

View file

@ -46,7 +46,13 @@ export class CallProcessor {
sameFileMatches: 0,
heuristicMatches: 0,
failed: 0,
callTypes: {} as Record<string, number>
callTypes: {} as Record<string, number>,
// Failure categorization
failuresByCategory: {
externalLibraries: 0, // Calls to external/stdlib functions (expected)
pythonBuiltins: 0, // Python built-in functions (expected)
actualFailures: 0 // Real resolution failures (unexpected)
}
};
constructor(functionTrie: FunctionRegistryTrie) {
@ -92,26 +98,10 @@ export class CallProcessor {
if (calls.length === 0) {
// Only log for source files that should have function calls
if (this.isSourceFile(filePath)) {
console.log(`⚠️ CallProcessor: No function calls found in source file: ${filePath}`);
// Debug: Check if this file has any 'call' nodes at all
if (filePath.endsWith('.py')) {
const callNodeCount = this.countNodeType(ast.tree!.rootNode, 'call');
const definitionCount = graph.nodes.filter(n =>
(n.label === 'Function' || n.label === 'Class' || n.label === 'Method') &&
n.properties.filePath === filePath
).length;
console.log(` 📊 Debug: ${filePath.split('/').pop()} has ${callNodeCount} call nodes, ${definitionCount} definitions`);
// If we have definitions but no calls, that's suspicious
if (definitionCount > 0 && callNodeCount === 0) {
console.log(` 🚨 Suspicious: File has definitions but no call nodes - possible AST parsing issue`);
}
}
// Reduced logging - track but don't spam console for each file
}
} else {
console.log(`CallProcessor: Found ${calls.length} function calls in ${filePath}`);
// Reduced logging - only log summary instead of per-file details
}
for (const call of calls) {
@ -137,7 +127,8 @@ export class CallProcessor {
}
} else {
this.stats.failed++;
console.log(`❌ Failed to resolve call: ${call.functionName} in ${call.callerFile}:${call.startLine}`);
// Categorize the failure for better statistics
this.categorizeFailureWithReason(call, this.diagnoseFailure(call));
}
}
}
@ -192,16 +183,44 @@ export class CallProcessor {
if (importInfo) {
// We have an import for this function name
const targetDefinitions = this.functionTrie.getAllDefinitions().filter(def =>
def.filePath === importInfo.targetFile &&
(def.functionName === importInfo.exportedName ||
(importInfo.exportedName === 'default' && def.functionName === call.functionName))
);
const targetDefinitions = this.functionTrie.getAllDefinitions().filter(def => {
// Match file path
if (def.filePath !== importInfo.targetFile) {
return false;
}
// Handle different import types
if (importInfo.importType === 'default') {
// For default imports, the function name could be anything
// Look for functions that could be the default export
return def.functionName === call.functionName ||
def.functionName === importInfo.exportedName ||
// Common default export patterns
(def.type === 'function' && def.startLine === 1) ||
(def.type === 'class' && def.functionName === 'default');
} else if (importInfo.importType === 'named') {
// For named imports, match the exported name
return def.functionName === importInfo.exportedName;
} else if (importInfo.importType === 'namespace') {
// For namespace imports like * as utils,
// the call would be utils.someFunction, so we need to handle this differently
return def.functionName === call.functionName;
}
return false;
});
if (targetDefinitions.length > 0) {
// Prefer functions over other types for function calls
const preferred = targetDefinitions.find(def =>
call.callType === 'function_call' ? def.type === 'function' :
call.callType === 'method_call' ? def.type === 'method' :
call.callType === 'constructor_call' ? def.type === 'class' : true
) || targetDefinitions[0];
return {
success: true,
targetNodeId: targetDefinitions[0].nodeId,
targetNodeId: preferred.nodeId,
stage: 'exact',
confidence: 'high'
};
@ -407,6 +426,101 @@ export class CallProcessor {
return true;
}
// JavaScript/TypeScript built-ins and common library functions
const jsBuiltins = new Set([
// Core JavaScript functions
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'encodeURI', 'decodeURI',
'encodeURIComponent', 'decodeURIComponent', 'escape', 'unescape',
// Array methods
'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'concat', 'join',
'reverse', 'sort', 'indexOf', 'lastIndexOf', 'includes', 'find', 'findIndex',
'filter', 'map', 'reduce', 'reduceRight', 'forEach', 'some', 'every',
'flat', 'flatMap', 'fill', 'copyWithin', 'from', 'of', 'isArray',
// Object methods
'keys', 'values', 'entries', 'assign', 'create', 'defineProperty',
'defineProperties', 'freeze', 'seal', 'preventExtensions', 'hasOwnProperty',
'isPrototypeOf', 'propertyIsEnumerable', 'toString', 'valueOf', 'toLocaleString',
// String methods
'charAt', 'charCodeAt', 'codePointAt', 'concat', 'endsWith', 'includes',
'indexOf', 'lastIndexOf', 'localeCompare', 'match', 'normalize', 'padEnd',
'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'startsWith',
'substring', 'substr', 'toLowerCase', 'toUpperCase', 'trim', 'trimEnd',
'trimStart', 'trimLeft', 'trimRight',
// Number methods
'toFixed', 'toExponential', 'toPrecision', 'isInteger', 'isSafeInteger',
'isFinite', 'isNaN', 'parseFloat', 'parseInt',
// Date methods
'getTime', 'getDate', 'getDay', 'getFullYear', 'getHours', 'getMinutes',
'getSeconds', 'getMilliseconds', 'getMonth', 'setDate', 'setFullYear',
'setHours', 'setMinutes', 'setSeconds', 'setMilliseconds', 'setMonth',
'toDateString', 'toTimeString', 'toISOString', 'toJSON', 'now', 'parse',
// Promise methods
'then', 'catch', 'finally', 'resolve', 'reject', 'all', 'race', 'allSettled',
// Console methods
'log', 'error', 'warn', 'info', 'debug', 'trace', 'assert', 'clear',
'count', 'dir', 'dirxml', 'group', 'groupCollapsed', 'groupEnd', 'table',
'time', 'timeEnd', 'timeLog', 'profile', 'profileEnd',
// DOM methods (common ones)
'getElementById', 'getElementsByClassName', 'getElementsByTagName',
'querySelector', 'querySelectorAll', 'createElement', 'createTextNode',
'appendChild', 'removeChild', 'insertBefore', 'replaceChild', 'cloneNode',
'getAttribute', 'setAttribute', 'removeAttribute', 'hasAttribute',
'addEventListener', 'removeEventListener', 'dispatchEvent',
'preventDefault', 'stopPropagation', 'stopImmediatePropagation',
'focus', 'blur', 'click', 'submit', 'reset', 'scrollIntoView',
// Common library methods (React, etc.)
'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect', 'useDebugValue',
'memo', 'forwardRef', 'lazy', 'Suspense', 'Fragment', 'createElement',
'cloneElement', 'isValidElement', 'render', 'hydrate', 'unmountComponentAtNode',
// HTTP/Fetch
'fetch', 'get', 'post', 'put', 'delete', 'patch', 'head', 'options',
// JSON
'stringify', 'parse',
// Math
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor',
'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan',
// Common testing functions
'describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
'mock', 'spy', 'stub', 'restore', 'reset', 'resetAllMocks', 'clearAllMocks',
// Node.js specific
'require', 'module', 'exports', '__dirname', '__filename', 'process', 'global',
'Buffer', 'setImmediate', 'clearImmediate', 'setInterval', 'clearInterval',
'setTimeout', 'clearTimeout',
// Worker API
'postMessage', 'onmessage', 'onerror', 'close', 'importScripts',
// JavaScript constructors and built-ins
'Array', 'Object', 'String', 'Number', 'Boolean', 'Function', 'Date',
'RegExp', 'Error', 'TypeError', 'ReferenceError', 'SyntaxError',
'RangeError', 'EvalError', 'URIError', 'AggregateError',
'Set', 'Map', 'WeakSet', 'WeakMap', 'Symbol', 'BigInt',
'Promise', 'Proxy', 'Reflect', 'ArrayBuffer', 'SharedArrayBuffer',
'DataView', 'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array',
'BigInt64Array', 'BigUint64Array',
// Configuration and build tools
'config', 'define', 'plugin', 'preset', 'loader', 'rule',
'extend', 'override', 'merge', 'concat', 'apply',
// ESLint specific
'rules', 'extends', 'parser', 'parserOptions', 'env', 'globals',
// Bundler/build tools
'bundle', 'chunk', 'entry', 'output', 'optimization', 'resolve',
'devtool', 'target', 'externals', 'stats', 'performance',
// Process and execution
'exec', 'spawn', 'fork', 'execSync', 'spawnSync',
// File system operations
'readFile', 'writeFile', 'readdir', 'stat', 'mkdir', 'rmdir',
'unlink', 'rename', 'copyFile', 'access', 'watch', 'createReadStream',
'createWriteStream'
]);
// Check if it's a JS/TS file and the function is a built-in
if ((filePath.endsWith('.js') || filePath.endsWith('.ts') ||
filePath.endsWith('.jsx') || filePath.endsWith('.tsx')) &&
jsBuiltins.has(functionName)) {
return true;
}
// Ignore very short function names (likely built-ins or operators)
if (functionName.length <= 2) {
return true;
@ -453,26 +567,19 @@ export class CallProcessor {
const functionName = this.extractPythonCallName(functionNode);
// Debug: Log what we're finding vs filtering
if (functionName) {
const shouldIgnore = this.shouldIgnoreCall(functionName, filePath);
if (shouldIgnore) {
// Only log a few examples to avoid spam
if (calls.length < 3) {
console.log(`🔍 Filtered out: ${functionName} in ${filePath.split('/').pop()}`);
}
} else {
calls.push({
callerFile: filePath,
functionName,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
callType: 'function_call'
});
}
if (functionName && !this.shouldIgnoreCall(functionName, filePath)) {
// Don't filter here - let all calls through to resolution
calls.push({
callerFile: filePath,
functionName,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
callType: 'function_call'
});
} else {
// Debug: Log when we can't extract function name
// Reduced logging - don't log every individual extraction failure
if (calls.length < 3) {
console.log(`🔍 Could not extract function name from: ${functionNode.type} in ${filePath.split('/').pop()}`);
// Only log first few failures per file to understand patterns
}
}
}
@ -509,13 +616,16 @@ export class CallProcessor {
const constructorNode = node.childForFieldName('constructor');
if (constructorNode) {
const constructorName = constructorNode.text;
calls.push({
callerFile: filePath,
functionName: constructorName,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
callType: 'constructor_call'
});
// Don't filter constructor calls as strictly - they're important for the graph
if (!this.shouldIgnoreCall(constructorName, filePath)) {
calls.push({
callerFile: filePath,
functionName: constructorName,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
callType: 'constructor_call'
});
}
}
}
@ -552,9 +662,22 @@ export class CallProcessor {
}
}
// Debug: Log unhandled node types (but limit spam)
if (Math.random() < 0.1) { // Only log 10% of cases to avoid spam
console.log(`🔍 Unhandled Python call node type: ${node.type} (text: "${node.text}")`);
// Handle additional Python call patterns
if (node.text && node.text.length > 0 && node.text.length < 100) {
// For simple cases, try using the text directly if it looks like a function name
const text = node.text.trim();
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(text)) {
return text;
}
// For attribute access, try to extract the last part
const parts = text.split('.');
if (parts.length > 1) {
const lastPart = parts[parts.length - 1];
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(lastPart)) {
return lastPart;
}
}
}
return null;
@ -570,7 +693,40 @@ export class CallProcessor {
// For method calls like obj.method(), we want just 'method'
const propertyNode = node.childForFieldName('property');
return propertyNode ? propertyNode.text : null;
} else if (node.type === 'call_expression') {
// Handle nested calls like getData().process()
const functionNode = node.childForFieldName('function');
if (functionNode) {
return this.extractJSCallName(functionNode);
}
} else if (node.type === 'subscript_expression') {
// Handle array/object access like obj['method']()
const propertyNode = node.childForFieldName('index');
if (propertyNode && propertyNode.type === 'string') {
// Extract string literal content
const propText = propertyNode.text.replace(/['"`]/g, '');
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(propText)) {
return propText;
}
}
}
// Fallback: try to extract from text for simple patterns
if (node.text && node.text.length > 0 && node.text.length < 50) {
const text = node.text.trim();
// Handle simple function calls
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(text)) {
return text;
}
// Handle member expressions like obj.method
const memberMatch = text.match(/([a-zA-Z_][a-zA-Z0-9_]*)\s*$/);
if (memberMatch) {
return memberMatch[1];
}
}
return null;
}
@ -677,7 +833,12 @@ export class CallProcessor {
sameFileMatches: 0,
heuristicMatches: 0,
failed: 0,
callTypes: {}
callTypes: {},
failuresByCategory: {
externalLibraries: 0,
pythonBuiltins: 0,
actualFailures: 0
}
};
}
@ -691,6 +852,18 @@ export class CallProcessor {
console.log(` ✅ Same-file matches (Stage 2): ${this.stats.sameFileMatches} (${((this.stats.sameFileMatches / this.stats.totalCalls) * 100).toFixed(1)}%)`);
console.log(` 🎯 Heuristic matches (Stage 3): ${this.stats.heuristicMatches} (${((this.stats.heuristicMatches / this.stats.totalCalls) * 100).toFixed(1)}%)`);
console.log(` ❌ Failed resolutions: ${this.stats.failed} (${((this.stats.failed / this.stats.totalCalls) * 100).toFixed(1)}%)`);
// Enhanced failure breakdown
if (this.stats.failed > 0) {
const { externalLibraries, pythonBuiltins, actualFailures } = this.stats.failuresByCategory;
console.log(` 📦 External libraries (expected): ${externalLibraries} (${((externalLibraries / this.stats.failed) * 100).toFixed(1)}% of failures)`);
console.log(` 🐍 Python built-ins (expected): ${pythonBuiltins} (${((pythonBuiltins / this.stats.failed) * 100).toFixed(1)}% of failures)`);
console.log(` 🚨 Actual failures (unexpected): ${actualFailures} (${((actualFailures / this.stats.failed) * 100).toFixed(1)}% of failures)`);
const expectedFailures = externalLibraries + pythonBuiltins;
console.log(` Real success rate (excluding expected failures): ${(((this.stats.totalCalls - actualFailures) / this.stats.totalCalls) * 100).toFixed(1)}%`);
}
console.log(` Success rate: ${(((this.stats.totalCalls - this.stats.failed) / this.stats.totalCalls) * 100).toFixed(1)}%`);
}
@ -710,6 +883,57 @@ export class CallProcessor {
this.resetStats();
}
/**
* Categorize a failed call for statistics with detailed reason
*/
private categorizeFailureWithReason(call: CallInfo, reason: string): void {
// Check if this is an expected failure (external library or built-in)
if (this.shouldIgnoreCall(call.functionName, call.callerFile)) {
// It's a call we expect to fail (external library or built-in)
if (call.callerFile.endsWith('.py')) {
this.stats.failuresByCategory.pythonBuiltins++;
} else {
this.stats.failuresByCategory.externalLibraries++;
}
} else {
// It's a call to user code that we failed to resolve (unexpected)
this.stats.failuresByCategory.actualFailures++;
}
}
/**
* Diagnose why a specific call failed
*/
private diagnoseFailure(call: CallInfo): string {
// Check if it's in import map but target not found
const importInfo = this.importMap[call.callerFile]?.[call.functionName];
if (importInfo) {
const targetDefinitions = this.functionTrie.getAllDefinitions().filter(def =>
def.filePath === importInfo.targetFile &&
(def.functionName === importInfo.exportedName ||
(importInfo.exportedName === 'default' && def.functionName === call.functionName))
);
if (targetDefinitions.length === 0) {
return `Imported from ${importInfo.targetFile} but definition not found`;
}
}
// Check if function exists in same file
const sameFileDefinitions = this.functionTrie.findInSameFile(call.callerFile, call.functionName);
if (sameFileDefinitions.length === 0) {
// Check if any similar functions exist
const candidates = this.functionTrie.findEndingWith(call.functionName);
if (candidates.length === 0) {
return `No function named '${call.functionName}' found anywhere`;
} else {
return `Function '${call.functionName}' not in same file, ${candidates.length} candidates in other files`;
}
}
return 'Unknown failure reason';
}
/**
* Check if a file is a source file that should contain function calls
*/

View file

@ -102,10 +102,16 @@ export class ImportProcessor {
ast: ParsedAST,
graph: KnowledgeGraph
): Promise<{ found: number; resolved: number }> {
if (!ast.tree) return { found: 0, resolved: 0 };
if (!ast.tree) {
return { found: 0, resolved: 0 };
}
const imports = this.extractImports(ast.tree.rootNode, filePath);
if (imports.length === 0) return { found: 0, resolved: 0 };
// Initialize import map for this file
@ -227,17 +233,81 @@ export class ImportProcessor {
filePath: string,
imports: ImportInfo[]
): void {
if (node.type === 'import_statement') {
const sourceNode = node.childForFieldName('source');
if (!sourceNode) return;
// Try different approaches to find the source
let sourceNode = node.childForFieldName('source');
if (!sourceNode) {
// Try finding string literal directly
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && (child.type === 'string' || child.type === 'string_literal')) {
sourceNode = child;
break;
}
}
}
if (!sourceNode) {
return;
}
const sourcePath = sourceNode.text.replace(/['"]/g, '');
const targetFile = this.resolveModulePath(sourcePath, filePath, 'javascript');
// Handle different import patterns
const importClauseNode = node.childForFieldName('import_clause');
let importClauseNode: Parser.SyntaxNode | null = node.childForFieldName('import_clause');
// CRITICAL FIX: If field-based approach fails, search by node type
if (!importClauseNode) {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'import_clause') {
importClauseNode = child;
break;
}
}
}
if (importClauseNode) {
this.processJSImportClause(importClauseNode, filePath, targetFile, imports);
} else {
// Handle simple imports like: import 'module'
if (node.text.trim().startsWith('import') && !node.text.includes('{') && !node.text.includes('from')) {
imports.push({
importingFile: filePath,
localName: '_side_effect_',
targetFile,
exportedName: '*',
importType: 'namespace'
});
} else {
// Try to extract import manually from text as fallback
const importText = node.text.trim();
const match = importText.match(/import\s+(.+?)\s+from\s+['"]([^'"]+)['"]/);;
if (match) {
const importPart = match[1].trim();
// Handle simple default import
if (!importPart.includes('{') && !importPart.includes('*')) {
imports.push({
importingFile: filePath,
localName: importPart,
targetFile,
exportedName: 'default',
importType: 'default'
});
}
}
}
}
} else if (node.type === 'variable_declaration') {
// Handle CommonJS: const x = require('module')
@ -262,13 +332,44 @@ export class ImportProcessor {
targetFile: string,
imports: ImportInfo[]
): void {
// Track what we've processed to ensure we don't miss anything
let processedSomething = false;
for (let i = 0; i < importClauseNode.childCount; i++) {
const child = importClauseNode.child(i);
if (!child) continue;
if (child.type === 'identifier') {
// Default import - this is the most common case we're missing
imports.push({
importingFile: filePath,
localName: child.text,
targetFile,
exportedName: 'default',
importType: 'default'
});
if (child.type === 'import_specifier') {
// Named import: { name } or { name as alias }
processedSomething = true;
} else if (child.type === 'named_imports') {
// Process named imports: { a, b, c }
this.processNamedImportsNode(child, filePath, targetFile, imports);
processedSomething = true;
} else if (child.type === 'namespace_import') {
// Namespace import: * as name
const nameNode = child.childForFieldName('name');
if (nameNode) {
imports.push({
importingFile: filePath,
localName: nameNode.text,
targetFile,
exportedName: '*',
importType: 'namespace'
});
processedSomething = true;
}
} else if (child.type === 'import_specifier') {
// Direct import specifier (should be handled by named_imports, but just in case)
const nameNode = child.childForFieldName('name') || child.child(0);
const aliasNode = child.childForFieldName('alias');
if (nameNode) {
@ -282,28 +383,98 @@ export class ImportProcessor {
exportedName,
importType: 'named'
});
processedSomething = true;
}
} else if (child.type === 'namespace_import') {
// Namespace import: * as name
const nameNode = child.childForFieldName('name');
if (nameNode) {
}
}
// Fallback: If structured processing didn't work, try text parsing
if (!processedSomething) {
const clauseText = importClauseNode.text.trim();
if (clauseText.startsWith('{') && clauseText.endsWith('}')) {
// Named imports like { foo, bar }
const namedImports = clauseText.slice(1, -1)
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
namedImports.forEach(importName => {
imports.push({
importingFile: filePath,
localName: nameNode.text,
localName: importName,
targetFile,
exportedName: importName,
importType: 'named'
});
});
} else if (clauseText.includes(' as ')) {
// Namespace import like * as foo
const namespaceMatch = clauseText.match(/\*\s+as\s+(\w+)/);
if (namespaceMatch) {
imports.push({
importingFile: filePath,
localName: namespaceMatch[1],
targetFile,
exportedName: '*',
importType: 'namespace'
});
}
} else if (child.type === 'identifier') {
// Default import
} else {
// Simple default import
const defaultName = clauseText.split(',')[0].trim(); // Handle mixed imports
if (defaultName && !defaultName.includes('{') && !defaultName.includes('*')) {
imports.push({
importingFile: filePath,
localName: defaultName,
targetFile,
exportedName: 'default',
importType: 'default'
});
}
}
}
}
private processNamedImportsNode(
namedImportsNode: Parser.SyntaxNode,
filePath: string,
targetFile: string,
imports: ImportInfo[]
): void {
for (let j = 0; j < namedImportsNode.childCount; j++) {
const namedChild = namedImportsNode.child(j);
if (namedChild && namedChild.type === 'import_specifier') {
const nameNode = namedChild.childForFieldName('name') || namedChild.child(0);
const aliasNode = namedChild.childForFieldName('alias');
if (nameNode) {
const exportedName = nameNode.text;
const localName = aliasNode ? aliasNode.text : exportedName;
imports.push({
importingFile: filePath,
localName,
targetFile,
exportedName,
importType: 'named'
});
}
} else if (namedChild && namedChild.type === 'identifier') {
imports.push({
importingFile: filePath,
localName: child.text,
localName: namedChild.text,
targetFile,
exportedName: 'default',
importType: 'default'
exportedName: namedChild.text,
importType: 'named'
});
}
}
}

View file

@ -9,7 +9,7 @@ import {
} from '../../lib/shared-utils.js';
import { IGNORE_PATTERNS } from '../../config/language-config.js';
import Parser from 'web-tree-sitter';
import { TYPESCRIPT_QUERIES, PYTHON_QUERIES, JAVA_QUERIES } from './tree-sitter-queries';
import { TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES, PYTHON_QUERIES, JAVA_QUERIES } from './tree-sitter-queries';
import { initTreeSitter, loadTypeScriptParser, loadPythonParser, loadJavaScriptParser } from '../tree-sitter/parser-loader.js';
import { FunctionRegistryTrie, FunctionDefinition } from '../graph/trie.js';
import { LRUCacheService } from '../../lib/lru-cache-service.js';
@ -226,11 +226,19 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
private async parseFile(graph: KnowledgeGraph, filePath: string, content: string): Promise<void> {
const language = this.detectLanguage(filePath);
// Skip compiled/minified files for JavaScript
if (language === 'javascript' && this.isCompiledOrMinified(content, filePath)) {
console.log(`Skipping compiled/minified file: ${filePath}`);
await this.parseGenericFile(graph, filePath, content);
return;
}
const contentHash = this.generateContentHash(content);
const cacheKey = this.lruCache.generateFileCacheKey(filePath, contentHash);
// Check cache first
const cachedResult = this.lruCache.getParsedFile(cacheKey);
// Check cache first - TEMPORARILY DISABLED FOR DEBUGGING
const cachedResult = null; // this.lruCache.getParsedFile(cacheKey);
if (cachedResult) {
console.log(`Cache hit for file: ${filePath}`);
this.astMap.set(filePath, { tree: cachedResult.ast });
@ -241,7 +249,7 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
const langParser = this.languageParsers.get(language);
if (!langParser || !this.parser) {
console.warn(`No parser available for language: ${language}. Skipping file: ${filePath}`);
// Skip file with reduced logging to avoid console spam
await this.parseGenericFile(graph, filePath, content);
return;
}
@ -262,8 +270,8 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
const queryCacheKey = this.lruCache.generateQueryCacheKey(language, queryString);
let queryResults: Parser.QueryMatch[] = [];
// Check query cache
const cachedQuery = this.lruCache.getQueryResult(queryCacheKey);
// Check query cache - TEMPORARILY DISABLED FOR DEBUGGING
const cachedQuery = null; // this.lruCache.getQueryResult(queryCacheKey);
if (cachedQuery) {
queryResults = cachedQuery.results;
} else {
@ -284,6 +292,11 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
const definition = this.extractDefinition(node, queryName, filePath);
if (definition) {
definitions.push(definition);
// Debug: Log what specific definitions we're finding
if (language === 'python' && filePath.endsWith('.py')) {
console.log(`🔍 DEBUG: Found ${queryName} -> ${definition.type}: "${definition.name}" in ${filePath.split('/').pop()}`);
}
}
}
}
@ -298,12 +311,39 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
fileSize: content.length
});
// Debug: Log definition extraction results for Python files
if (language === 'python' && filePath.endsWith('.py')) {
console.log(`🔍 DEBUG: ${filePath.split('/').pop()} -> ${definitions.length} definitions extracted (content: ${content.length} chars, hash: ${contentHash.substring(0, 8)})`);
// Log definition types for debugging
const definitionTypes = definitions.reduce((acc, def) => {
acc[def.type] = (acc[def.type] || 0) + 1;
return acc;
}, {});
if (definitions.length > 0) {
console.log(`🔍 DEBUG: Definition types: ${JSON.stringify(definitionTypes)}`);
}
if (definitions.length === 0 && content.length > 100) {
console.log(`🚨 DEBUG: Large Python file with no definitions: ${filePath} (${content.length} chars)`);
// Log first few lines to understand the content
const firstLines = content.split('\n').slice(0, 3).join('\\n');
console.log(`🔍 DEBUG: File starts with: ${firstLines}`);
}
}
await this.addDefinitionsToGraph(graph, filePath, definitions);
}
private extractDefinition(node: Parser.SyntaxNode, queryName: string, filePath: string): ParsedDefinition | null {
const nameNode = node.childForFieldName('name');
const name = nameNode ? nameNode.text : 'anonymous';
const name = nameNode ? nameNode.text : null;
// Skip anonymous definitions - they're usually from compiled/minified code
if (!name || name === 'anonymous' || name.trim().length === 0) {
return null;
}
return {
name,
@ -319,16 +359,55 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
case 'classes': return 'class';
case 'methods': return 'method';
case 'functions':
case 'arrowFunctions': return 'function';
case 'arrowFunctions':
case 'variableAssignments':
case 'objectMethods': return 'function';
case 'imports':
case 'from_imports': return 'import';
case 'exports':
case 'defaultExports': return 'function'; // Exports usually export functions
case 'interfaces': return 'interface';
case 'types': return 'type';
case 'decorators': return 'decorator';
default: return 'variable';
default:
console.warn(`Unknown query type: ${queryName}, defaulting to 'function'`);
return 'function'; // Better default than 'variable'
}
}
private isCompiledOrMinified(content: string, filePath: string): boolean {
// Check file name patterns for known compiled files
const fileName = filePath.split('/').pop()?.toLowerCase() || '';
if (fileName.includes('.min.') ||
fileName.includes('.bundle.') ||
fileName.includes('tree-sitter.js') ||
fileName.includes('kuzu_wasm_worker.js')) {
return true;
}
// Check content characteristics for minified code
const lines = content.split('\n');
if (lines.length > 0) {
const firstLine = lines[0];
// Very long first line (typical of minified code)
if (firstLine.length > 500) {
return true;
}
// Contains typical minified patterns
if (firstLine.includes('var Module=void 0!==Module?Module:{}') ||
firstLine.includes('__webpack_require__') ||
firstLine.includes('!function(') ||
content.includes('/*! ') || // Webpack/build tool comments
content.match(/^\s*!function\s*\(/)) { // IIFE patterns
return true;
}
}
return false;
}
private detectLanguage(filePath: string): string {
const extension = pathUtils.extname(filePath).toLowerCase();
@ -346,8 +425,9 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
private getQueriesForLanguage(language: string): Record<string, string> | null {
switch (language) {
case 'typescript':
case 'javascript':
return TYPESCRIPT_QUERIES;
case 'javascript':
return JAVASCRIPT_QUERIES; // Use separate JavaScript queries
case 'python':
return PYTHON_QUERIES;
case 'java':
@ -358,18 +438,31 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
}
private async parseGenericFile(graph: KnowledgeGraph, filePath: string, _content: string): Promise<void> {
const fileNode: GraphNode = {
id: generateId('file'),
label: 'File' as NodeLabel,
properties: {
name: pathUtils.getFileName(filePath),
path: filePath,
size: _content.length,
language: this.detectLanguage(filePath)
}
};
// Find existing file node created by StructureProcessor
let fileNode = graph.nodes.find(node =>
node.label === 'File' &&
(node.properties.filePath === filePath || node.properties.path === filePath)
);
graph.addNode(fileNode);
// If no existing file node found, create one (fallback)
if (!fileNode) {
fileNode = {
id: generateId(`file_${filePath}`),
label: 'File' as NodeLabel,
properties: {
name: pathUtils.getFileName(filePath),
path: filePath,
filePath: filePath,
size: _content.length,
language: this.detectLanguage(filePath)
}
};
graph.addNode(fileNode);
} else {
// Update existing file node with additional properties
fileNode.properties.size = _content.length;
fileNode.properties.language = this.detectLanguage(filePath);
}
}
private async addDefinitionsToGraph(
@ -377,20 +470,30 @@ export class ParsingProcessor implements GraphProcessor<ParsingInput> {
filePath: string,
definitions: ParsedDefinition[]
): Promise<void> {
const fileNode: GraphNode = {
id: generateId('file'),
label: 'File' as NodeLabel,
properties: {
name: pathUtils.getFileName(filePath),
path: filePath,
language: this.detectLanguage(filePath)
}
};
// Find existing file node created by StructureProcessor
let fileNode = graph.nodes.find(node =>
node.label === 'File' &&
(node.properties.filePath === filePath || node.properties.path === filePath)
);
graph.addNode(fileNode);
// If no existing file node found, create one (fallback)
if (!fileNode) {
fileNode = {
id: generateId(`file_${filePath}`),
label: 'File' as NodeLabel,
properties: {
name: pathUtils.getFileName(filePath),
path: filePath,
filePath: filePath,
language: this.detectLanguage(filePath)
}
};
graph.addNode(fileNode);
}
for (const def of definitions) {
const nodeId = generateId(def.type);
// Generate unique ID based on file path and definition name
const nodeId = generateId(`${def.type}_${filePath}_${def.name}_${def.startLine}`);
if (this.duplicateDetector.checkAndMark(nodeId)) continue;

View file

@ -113,10 +113,10 @@ export class GraphPipeline {
const filesWithoutDefinitions = fileNodes.filter(fileNode => {
const hasDefinitions = graph.relationships.some(rel =>
rel.source === fileNode.id &&
rel.type === 'CONTAINS' &&
rel.type === 'DEFINES' &&
graph.nodes.some(targetNode =>
targetNode.id === rel.target &&
['Function', 'Class', 'Method'].includes(targetNode.label)
['Function', 'Class', 'Method', 'Variable'].includes(targetNode.label)
)
);
return !hasDefinitions;
@ -181,7 +181,7 @@ export class GraphPipeline {
const sourceFilesWithoutDefinitions = sourceFiles.filter(fileNode => {
const hasDefinitions = graph.relationships.some(rel =>
rel.source === fileNode.id &&
rel.type === 'CONTAINS' &&
rel.type === 'DEFINES' &&
graph.nodes.some(n =>
n.id === rel.target &&
['Function', 'Class', 'Method', 'Variable'].includes(n.label)
@ -205,7 +205,7 @@ export class GraphPipeline {
const definitionsWithoutFiles = definitionNodes.filter(defNode => {
const hasFileParent = graph.relationships.some(rel =>
rel.target === defNode.id &&
rel.type === 'CONTAINS' &&
rel.type === 'DEFINES' &&
graph.nodes.some(n => n.id === rel.source && n.label === 'File')
);
return !hasFileParent;

View file

@ -67,10 +67,6 @@ export class StructureProcessor {
const visibleDirectories = directories.filter(dir => !this.shouldHideDirectory(dir));
const hiddenDirectoriesCount = directories.length - visibleDirectories.length;
if (hiddenDirectoriesCount > 0) {
console.log(`StructureProcessor: Hiding ${hiddenDirectoriesCount} ignored directories from KG`);
}
// Create directory nodes only for visible directories
const directoryNodes = this.createDirectoryNodes(visibleDirectories);
directoryNodes.forEach(node => graph.addNode(node));
@ -79,10 +75,6 @@ export class StructureProcessor {
const visibleFiles = files.filter(file => !this.shouldHideFile(file));
const hiddenFilesCount = files.length - visibleFiles.length;
if (hiddenFilesCount > 0) {
console.log(`StructureProcessor: Hiding ${hiddenFilesCount} files in ignored directories from KG`);
}
// Create file nodes only for visible files
const fileNodes = this.createFileNodes(visibleFiles);
fileNodes.forEach(node => graph.addNode(node));
@ -91,7 +83,7 @@ export class StructureProcessor {
this.createContainsRelationships(graph, projectNode.id, visibleDirectories, visibleFiles);
const totalHidden = hiddenDirectoriesCount + hiddenFilesCount;
console.log(`StructureProcessor: Created ${graph.nodes.length} nodes total (${totalHidden} items hidden)`);
console.log(`StructureProcessor: Created ${graph.nodes.length} nodes total (${totalHidden} items filtered)`);
}
/**
@ -164,7 +156,7 @@ export class StructureProcessor {
for (const dirPath of directoryPaths) {
if (!dirPath) continue;
const id = generateId('folder');
const id = generateId(`folder_${dirPath}`);
this.nodeIdMap.set(dirPath, id);
const pathParts = dirPath.split('/');
@ -196,7 +188,7 @@ export class StructureProcessor {
for (const filePath of filePaths) {
if (!filePath) continue;
const id = generateId('file');
const id = generateId(`file_${filePath}`);
this.nodeIdMap.set(filePath, id);
const fileName = filePath.split('/').pop() || filePath;

View file

@ -26,6 +26,48 @@ export const TYPESCRIPT_QUERIES = {
`,
};
// JavaScript queries - similar to TypeScript but without TS-specific syntax
export const JAVASCRIPT_QUERIES = {
imports: `
(import_statement) @import
`,
classes: `
(class_declaration) @class
`,
methods: `
(method_definition) @method
`,
functions: `
(function_declaration) @function
`,
arrowFunctions: `
(lexical_declaration
(variable_declarator
name: (identifier) @name
value: (arrow_function))) @arrow_function
`,
exports: `
(export_statement) @export
`,
defaultExports: `
(export_statement
(identifier) @default_export)
`,
variableAssignments: `
(variable_declaration
(variable_declarator
name: (identifier) @name
value: (function_expression))) @var_function
`,
objectMethods: `
(assignment_expression
left: (member_expression
property: (property_identifier) @name)
right: (function_expression)) @obj_method
`,
// Note: No interfaces or types for pure JavaScript
};
export const PYTHON_QUERIES = {
imports: `
(import_statement) @import
@ -39,8 +81,14 @@ export const PYTHON_QUERIES = {
functions: `
(function_definition) @function
`,
methods: `
(class_definition
body: (block
(function_definition) @method))
`,
decorators: `
(decorator) @decorator
(decorated_definition
(decorator) @decorator)
`,
};

View file

@ -477,7 +477,7 @@ export class GitHubService {
await this.collectPathsAndContent(owner, repo, '', allPaths, fileContents);
console.log(`GitHub: Discovered ${allPaths.length} total paths, ${fileContents.size} files with content`);
console.log(`GitHub: Extracted ${allPaths.length} paths, ${fileContents.size} files`);
return {
allPaths,
@ -503,7 +503,7 @@ export class GitHubService {
// Always try to get content for files, but skip unwanted files like .git files
// Filtering will happen later in ParsingProcessor, but we can skip obvious files
if (this.shouldSkipFileForContent(fullPath)) {
console.log(`Skipping file content for: ${fullPath}`);
// Reduced logging to avoid console spam
} else {
try {
const content = await this.getFileContent(owner, repo, fullPath);

View file

@ -153,35 +153,44 @@ export class IngestionService {
// Check if all paths start with the same top-level folder
const potentialPrefix = pathParts[0] + '/';
const allHaveSamePrefix = paths.every(path => path.startsWith(potentialPrefix));
const pathsWithPrefix = paths.filter(path => path.startsWith(potentialPrefix));
if (!allHaveSamePrefix) {
return structure; // No common prefix to remove
}
// If most paths (>80%) have the common prefix, normalize all paths
if (pathsWithPrefix.length > paths.length * 0.8) {
console.log(`Normalizing ZIP paths: removing common prefix "${potentialPrefix}" from ${pathsWithPrefix.length}/${paths.length} paths`);
console.log(`Normalizing ZIP paths: removing common prefix "${potentialPrefix}"`);
// Remove the common prefix from all paths
const normalizedPaths = paths.map(path => {
if (path.startsWith(potentialPrefix)) {
const withoutPrefix = path.substring(potentialPrefix.length);
return withoutPrefix || path; // Keep original if normalization would result in empty string
}
// For paths without prefix, keep as-is but filter out the bare container name
return path === pathParts[0] ? '' : path;
}).filter(path => path.length > 0); // Remove empty paths
// Remove the common prefix from all paths
const normalizedPaths = paths.map(path => {
const withoutPrefix = path.substring(potentialPrefix.length);
return withoutPrefix || path; // Keep original if normalization would result in empty string
}).filter(path => path.length > 0); // Remove empty paths
// Normalize file contents map
const normalizedContents = new Map<string, string>();
for (const [originalPath, content] of structure.fileContents) {
const normalizedPath = originalPath.startsWith(potentialPrefix)
? originalPath.substring(potentialPrefix.length)
: originalPath;
if (normalizedPath) {
normalizedContents.set(normalizedPath, content);
// Normalize file contents map
const normalizedContents = new Map<string, string>();
for (const [originalPath, content] of structure.fileContents) {
let normalizedPath = originalPath;
if (originalPath.startsWith(potentialPrefix)) {
normalizedPath = originalPath.substring(potentialPrefix.length);
} else if (originalPath === pathParts[0]) {
// Skip the bare container directory
continue;
}
if (normalizedPath && normalizedPath.length > 0) {
normalizedContents.set(normalizedPath, content);
}
}
return {
allPaths: normalizedPaths,
fileContents: normalizedContents
};
}
return {
allPaths: normalizedPaths,
fileContents: normalizedContents
};
return structure; // No normalization if prefix isn't common enough
}
}

View file

@ -109,7 +109,7 @@ export class ZipService {
// Skip .git files and other unwanted files during content extraction
if (this.shouldSkipFileForContent(normalizedPath)) {
console.log(`Skipping file content for: ${normalizedPath}`);
// Reduced logging - only log summary at the end
return;
}
@ -149,8 +149,7 @@ export class ZipService {
// Wait for all file extractions to complete
await Promise.all(filePromises);
console.log(`ZIP: Discovered ${allPaths.length} total paths, ${fileContents.size} files with content`);
console.log(`ZIP: Total extracted size: ${totalExtractedSize} bytes`);
console.log(`ZIP: Extracted ${allPaths.length} paths, ${fileContents.size} files (${totalExtractedSize} bytes)`);
return {
allPaths: allPaths.sort(), // Sort for consistent ordering

View file

@ -0,0 +1,344 @@
/**
* TEMPORARY WASM VERIFICATION TESTS
* These tests verify that Tree-sitter WASM files are loading and working correctly
* DELETE THIS FILE after confirming everything works
*/
import {
initTreeSitter,
loadPythonParser,
loadJavaScriptParser,
loadTypeScriptParser
} from '../core/tree-sitter/parser-loader.js';
import Parser from 'web-tree-sitter';
import { PYTHON_QUERIES, TYPESCRIPT_QUERIES } from '../core/ingestion/tree-sitter-queries';
describe('🔍 WASM Verification Tests (TEMPORARY)', () => {
let parser: Parser;
beforeAll(async () => {
console.log('🧪 Starting WASM verification tests...');
});
afterAll(() => {
console.log('✅ WASM verification tests completed');
});
describe('Main Thread Tree-sitter Initialization', () => {
it('should initialize Tree-sitter successfully', async () => {
parser = await initTreeSitter();
expect(parser).toBeDefined();
expect(parser).toBeInstanceOf(Parser);
console.log('✅ Tree-sitter main thread initialized successfully');
});
it('should load Python WASM parser', async () => {
const pythonLang = await loadPythonParser();
expect(pythonLang).toBeDefined();
expect(typeof pythonLang.query).toBe('function');
console.log('✅ Python WASM parser loaded successfully');
});
it('should load JavaScript WASM parser', async () => {
const jsLang = await loadJavaScriptParser();
expect(jsLang).toBeDefined();
expect(typeof jsLang.query).toBe('function');
console.log('✅ JavaScript WASM parser loaded successfully');
});
it('should load TypeScript WASM parser', async () => {
const tsLang = await loadTypeScriptParser();
expect(tsLang).toBeDefined();
expect(typeof tsLang.query).toBe('function');
console.log('✅ TypeScript WASM parser loaded successfully');
});
});
describe('Python WASM Parsing Verification', () => {
let pythonLang: Parser.Language;
beforeAll(async () => {
pythonLang = await loadPythonParser();
parser.setLanguage(pythonLang);
});
it('should parse simple Python function', () => {
const pythonCode = `
def test_function(param1, param2):
"""Test function docstring"""
return param1 + param2
class TestClass:
def method_one(self):
pass
async def async_method(self):
return "async result"
variable_assignment = "test value"
`;
const tree = parser.parse(pythonCode);
expect(tree).toBeDefined();
expect(tree.rootNode).toBeDefined();
expect(tree.rootNode.type).toBe('module');
console.log('✅ Python code parsed successfully');
console.log(` Root node type: ${tree.rootNode.type}`);
console.log(` Child count: ${tree.rootNode.childCount}`);
});
it('should execute Python queries successfully', () => {
const pythonCode = `
def hello_world():
print("Hello, World!")
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
result = 42
`;
const tree = parser.parse(pythonCode);
// Test each Python query
for (const [queryName, queryString] of Object.entries(PYTHON_QUERIES)) {
try {
const query = pythonLang.query(queryString as string);
const matches = query.matches(tree.rootNode);
console.log(`${queryName} query executed: ${matches.length} matches`);
// Log some match details for verification
if (matches.length > 0) {
const firstMatch = matches[0];
console.log(` First match captures: ${firstMatch.captures.length}`);
if (firstMatch.captures.length > 0) {
const firstCapture = firstMatch.captures[0];
console.log(` First capture type: ${firstCapture.node.type}`);
console.log(` First capture text: "${firstCapture.node.text.substring(0, 50)}..."`);
}
}
} catch (error) {
console.error(`${queryName} query failed:`, error);
throw error;
}
}
});
it('should extract definitions correctly', () => {
const pythonCode = `
import os
from datetime import datetime
def process_data(input_data):
return input_data.upper()
class DataProcessor:
def __init__(self):
self.data = []
def add_item(self, item):
self.data.append(item)
async def process_async(self):
return await some_async_operation()
@decorator
def decorated_function():
pass
global_var = "test"
`;
const tree = parser.parse(pythonCode);
let totalDefinitions = 0;
for (const [queryName, queryString] of Object.entries(PYTHON_QUERIES)) {
const query = pythonLang.query(queryString as string);
const matches = query.matches(tree.rootNode);
for (const match of matches) {
for (const capture of match.captures) {
totalDefinitions++;
const node = capture.node;
console.log(` Found ${queryName}: "${node.text.split('\n')[0]}" at line ${node.startPosition.row + 1}`);
}
}
}
expect(totalDefinitions).toBeGreaterThan(0);
console.log(`✅ Extracted ${totalDefinitions} total definitions`);
});
});
describe('TypeScript WASM Parsing Verification', () => {
let typescriptLang: Parser.Language;
beforeAll(async () => {
typescriptLang = await loadTypeScriptParser();
parser.setLanguage(typescriptLang);
});
it('should parse TypeScript code', () => {
const tsCode = `
import React from 'react';
interface User {
id: number;
name: string;
}
class UserService {
getUser(id: number): User {
return { id, name: 'Test User' };
}
}
const createUser = (name: string): User => {
return { id: Date.now(), name };
};
function processUser(user: User): void {
console.log(user.name);
}
`;
const tree = parser.parse(tsCode);
expect(tree).toBeDefined();
expect(tree.rootNode.type).toBe('program');
console.log('✅ TypeScript code parsed successfully');
});
it('should execute TypeScript queries successfully', () => {
const tsCode = `
class TestClass {
method(): string {
return "test";
}
}
function testFunction(): void {}
const arrowFunc = () => "result";
interface TestInterface {
prop: string;
}
`;
const tree = parser.parse(tsCode);
for (const [queryName, queryString] of Object.entries(TYPESCRIPT_QUERIES)) {
try {
const query = typescriptLang.query(queryString as string);
const matches = query.matches(tree.rootNode);
console.log(`${queryName} query executed: ${matches.length} matches`);
} catch (error) {
console.error(`${queryName} query failed:`, error);
throw error;
}
}
});
});
describe('Worker Thread WASM Verification', () => {
it('should verify worker can load and use WASM', async () => {
// Create a test worker to verify WASM loading
const workerCode = `
import Parser from 'web-tree-sitter';
async function testWorkerWasm() {
try {
// Test worker WASM initialization
await Parser.init();
const parser = new Parser();
// Test loading Python parser
const pythonLang = await Parser.Language.load('/wasm/python/tree-sitter-python.wasm');
parser.setLanguage(pythonLang);
// Test parsing
const tree = parser.parse('def test(): pass');
return {
success: true,
rootNodeType: tree.rootNode.type,
childCount: tree.rootNode.childCount
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
testWorkerWasm().then(result => {
self.postMessage(result);
});
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
const workerResult = await new Promise((resolve, reject) => {
const worker = new Worker(workerUrl, { type: 'module' });
worker.onmessage = (event) => {
worker.terminate();
URL.revokeObjectURL(workerUrl);
resolve(event.data);
};
worker.onerror = (error) => {
worker.terminate();
URL.revokeObjectURL(workerUrl);
reject(error);
};
// Timeout after 10 seconds
setTimeout(() => {
worker.terminate();
URL.revokeObjectURL(workerUrl);
reject(new Error('Worker test timeout'));
}, 10000);
});
console.log('🔍 Worker WASM test result:', workerResult);
expect(workerResult).toHaveProperty('success');
if (!(workerResult as any).success) {
throw new Error(`Worker WASM failed: ${(workerResult as any).error}`);
}
console.log('✅ Worker WASM verification passed');
}, 15000); // 15 second timeout for this test
});
describe('WASM File Accessibility Check', () => {
it('should verify WASM files are accessible', async () => {
const wasmFiles = [
'/wasm/python/tree-sitter-python.wasm',
'/wasm/javascript/tree-sitter-javascript.wasm',
'/wasm/typescript/tree-sitter-typescript.wasm',
'/wasm/tree-sitter.wasm'
];
for (const wasmPath of wasmFiles) {
try {
const response = await fetch(wasmPath);
expect(response.ok).toBe(true);
expect(response.headers.get('content-type')).toContain('wasm');
console.log(`${wasmPath} is accessible (${response.status})`);
} catch (error) {
console.error(`${wasmPath} failed to load:`, error);
throw error;
}
}
});
});
});

View file

@ -244,22 +244,43 @@ const FloatingSourceViewer: React.FC<FloatingSourceViewerProps> = ({
rel.type === 'CONTAINS' && rel.target === nodeId
);
if (containsRel) {
const sourceNode = graph.nodes.find(n => n.id === containsRel.source);
// If no CONTAINS relationship found, try DEFINES relationship (fallback)
const definesRel = !containsRel ? graph.relationships.find(rel =>
rel.type === 'DEFINES' && rel.target === nodeId
) : null;
const fileRel = containsRel || definesRel;
if (fileRel) {
const sourceNode = graph.nodes.find(n => n.id === fileRel.source);
if (sourceNode && sourceNode.properties.filePath) {
filePath = sourceNode.properties.filePath as string;
}
}
// If not found through relationships, search through file contents
if (!filePath && fileContents) {
// BUT ONLY for Function, Method, Class, and Variable nodes - NOT for Folder or File nodes
if (!filePath && fileContents && ['Function', 'Method', 'Class', 'Variable'].includes(nodeType)) {
console.log('FloatingSourceViewer - Searching file contents for node:', nodeName, 'of type:', nodeType);
// Use more specific search patterns instead of just checking if content includes nodeName
const searchPatterns = [
`def ${nodeName}(`, // Python function
`function ${nodeName}(`, // JavaScript function
`const ${nodeName} =`, // JavaScript const
`class ${nodeName}`, // Class definition
];
for (const [path, content] of fileContents) {
// Skip .git files, node_modules, and other unwanted directories
if (shouldSkipFileForSearch(path)) {
continue;
}
if (content.includes(nodeName)) {
// Check if any specific pattern matches instead of just nodeName
const foundPattern = searchPatterns.find(pattern => content.includes(pattern));
if (foundPattern) {
console.log(`FloatingSourceViewer - Found pattern "${foundPattern}" in ${path}`);
filePath = path;
break;
}
@ -267,25 +288,73 @@ const FloatingSourceViewer: React.FC<FloatingSourceViewerProps> = ({
}
if (!filePath) {
// Special handling for Folder nodes
if (nodeType === 'Folder') {
const folderPath = node.properties.path as string || nodeName;
const childFiles = graph?.nodes?.filter(n =>
n.label === 'File' &&
(n.properties.filePath as string || n.properties.path as string || '').startsWith(folderPath + '/')
) || [];
const childFolders = graph?.nodes?.filter(n =>
n.label === 'Folder' &&
(n.properties.path as string || '').startsWith(folderPath + '/') &&
(n.properties.path as string || '').split('/').length === folderPath.split('/').length + 1
) || [];
let directoryContent = `# Directory: ${folderPath}\n`;
directoryContent += `# This folder contains ${childFiles.length} files and ${childFolders.length} subfolders\n\n`;
if (childFolders.length > 0) {
directoryContent += '## Subdirectories:\n';
childFolders.forEach(folder => {
const name = folder.properties.name as string || 'Unknown';
directoryContent += `- 📁 ${name}\n`;
});
directoryContent += '\n';
}
if (childFiles.length > 0) {
directoryContent += '## Files:\n';
childFiles.slice(0, 15).forEach(file => {
const name = file.properties.name as string || 'Unknown';
const ext = file.properties.extension as string || '';
const icon = ext === '.py' ? '🐍' : ext === '.js' ? '📜' : ext === '.ts' ? '📘' : '📄';
directoryContent += `- ${icon} ${name}\n`;
});
if (childFiles.length > 15) {
directoryContent += `... and ${childFiles.length - 15} more files\n`;
}
}
return {
fileName: folderPath.split('/').pop() || folderPath,
filePath: 'folder-listing',
content: directoryContent,
nodeType,
nodeName,
language: 'markdown'
};
}
// Return mock content for nodes without file association
const mockContent = `// ${nodeType}: ${nodeName}
// This ${nodeType.toLowerCase()} is part of the knowledge graph
// File path not available in the current context
// This is a ${nodeType.toLowerCase()} definition in the knowledge graph
${nodeType === 'Function' ? `function ${nodeName}() {
// Implementation details would be here
return true;
// Function implementation not available in current context
// This may be an external library function or incomplete parsing
}` : nodeType === 'Class' ? `class ${nodeName} {
constructor() {
// Constructor implementation
}
// Class methods would be here
}` : `// ${nodeType} definition for ${nodeName}`}`;
// Class definition not available in current context
// This may be an external library class or incomplete parsing
}` : nodeType === 'Method' ? `${nodeName}() {
// Method implementation not available in current context
}` : `// ${nodeType} definition for ${nodeName}
// Content not available in current context`}`;
return {
fileName: `${nodeName}.${nodeType.toLowerCase()}`,
filePath: 'graph-node',
fileName: `${nodeName}`,
filePath: 'virtual-node',
content: mockContent,
nodeType,
nodeName,
@ -293,20 +362,52 @@ ${nodeType === 'Function' ? `function ${nodeName}() {
};
}
const content = fileContents.get(filePath);
if (!content) return null;
// Try to find the file content using various path resolution strategies
const nodeFilePath = node.properties.filePath as string ||
node.properties.path as string ||
node.properties.name as string;
if (nodeFilePath) {
// Try exact match first
let content = fileContents.get(nodeFilePath);
// If not found, try different path variations
if (!content) {
// Try relative paths starting from different roots
const pathVariations = [
nodeFilePath,
nodeFilePath.replace(/^[./]*/, ''), // Remove leading ./ or /
nodeFilePath.startsWith('/') ? nodeFilePath.substring(1) : `/${nodeFilePath}`, // Toggle leading slash
`src/${nodeFilePath}`, // Try under src/
nodeFilePath.replace(/\\/g, '/'), // Convert backslashes to forward slashes
nodeFilePath.replace(/\//g, '\\') // Convert forward slashes to backslashes
];
for (const variation of pathVariations) {
content = fileContents.get(variation);
if (content) {
break;
}
}
}
if (content) {
const extractedContent = extractRelevantContent(content, nodeName, nodeType);
const language = nodeFilePath.split('.').pop() || 'text';
const extractedContent = extractRelevantContent(content, nodeName, nodeType);
const language = filePath.split('.').pop() || 'text';
return {
fileName: filePath.split('/').pop() || filePath,
filePath,
content: extractedContent || content.substring(0, 500) + '...',
nodeType,
nodeName,
language
};
return {
fileName: nodeFilePath.split('/').pop() || nodeFilePath,
filePath: nodeFilePath,
content: extractedContent || content.substring(0, 500) + '...',
nodeType,
nodeName,
language
};
}
}
// If no content found, return null to trigger the mock content generation above
return null;
}, [nodeId, graph, fileContents]);
// Dragging functionality

View file

@ -423,17 +423,25 @@ def hasattr(obj: Any, name: str) -> bool:
rel.type === 'CONTAINS' && rel.target === selectedNodeId
);
if (containsRelationship) {
const fileNode = graph.nodes.find(n => n.id === containsRelationship.source);
// If no CONTAINS relationship found, try DEFINES relationship (fallback)
const definesRelationship = !containsRelationship ? graph.relationships?.find(rel =>
rel.type === 'DEFINES' && rel.target === selectedNodeId
) : null;
const fileRelationship = containsRelationship || definesRelationship;
if (fileRelationship) {
const fileNode = graph.nodes.find(n => n.id === fileRelationship.source);
if (fileNode && fileNode.label === 'File') {
filePath = fileNode.properties.path as string || fileNode.properties.filePath as string;
console.log('SourceViewer - Found file through CONTAINS relationship:', filePath);
console.log('SourceViewer - Found file through relationship:', filePath, 'via', fileRelationship.type);
}
}
// If still no file path, try reverse lookup by searching for the node name in file contents
if (!filePath && fileContents) {
console.log('SourceViewer - Searching file contents for node:', nodeName);
// BUT ONLY for Function, Method, Class, and Variable nodes - NOT for Folder or File nodes
if (!filePath && fileContents && ['Function', 'Method', 'Class', 'Variable'].includes(node.label)) {
console.log('SourceViewer - Searching file contents for node:', nodeName, 'of type:', node.label);
// Try multiple search patterns for the function
const searchPatterns = [
@ -481,8 +489,9 @@ def hasattr(obj: Any, name: str) -> bool:
}
// If still not found, try a more lenient search (case-insensitive)
if (!filePath) {
console.log('SourceViewer - Trying case-insensitive search for:', nodeName);
// BUT ONLY for Function, Method, Class, and Variable nodes
if (!filePath && ['Function', 'Method', 'Class', 'Variable'].includes(node.label)) {
console.log('SourceViewer - Trying case-insensitive search for:', nodeName, 'of type:', node.label);
for (const [path, content] of fileContents) {
// Skip .git files, node_modules, and other unwanted directories
if (shouldSkipFileForSearch(path)) {
@ -510,10 +519,20 @@ def hasattr(obj: Any, name: str) -> bool:
console.log('SourceViewer - Final node details:', {
nodeId: selectedNodeId,
nodeName,
nodeLabel: node.label,
nodeType: node.label,
filePath,
fileName,
nodeLabel: node.label,
nodeProperties: node.properties,
// CRITICAL DEBUG: Show what type this node actually is
actualNodeInfo: {
isFolder: node.label === 'Folder',
isFile: node.label === 'File',
isFunction: node.label === 'Function',
isClass: node.label === 'Class',
nodeLabel: node.label,
nodeName: nodeName
},
fileContentsSize: fileContents?.size || 0,
// Add detailed relationship debugging
allRelationships: graph?.relationships?.length || 0,
@ -536,7 +555,55 @@ def hasattr(obj: Any, name: str) -> bool:
// Try to get actual file content
let content = '';
if (filePath && fileContents && fileContents.has(filePath)) {
// Special handling for Folder nodes
if (node.label === 'Folder') {
const folderPath = filePath || nodeName || 'Unknown Folder';
console.log('SourceViewer - Processing folder node:', {
folderPath,
nodeName,
nodeProperties: node.properties
});
const childFiles = graph?.nodes?.filter(n =>
n.label === 'File' &&
(n.properties.filePath as string || n.properties.path as string || '').startsWith(folderPath + '/')
) || [];
const childFolders = graph?.nodes?.filter(n =>
n.label === 'Folder' &&
(n.properties.path as string || '').startsWith(folderPath + '/') &&
(n.properties.path as string || '').split('/').length === folderPath.split('/').length + 1
) || [];
content = `# Directory: ${folderPath}\n`;
content += `# Node Type: ${node.label}\n`;
content += `# This folder contains ${childFiles.length} files and ${childFolders.length} subfolders\n\n`;
if (childFolders.length > 0) {
content += '## Subdirectories:\n';
childFolders.forEach(folder => {
const name = folder.properties.name as string || 'Unknown';
content += `- 📁 ${name}\n`;
});
content += '\n';
}
if (childFiles.length > 0) {
content += '## Files:\n';
childFiles.slice(0, 20).forEach(file => {
const name = file.properties.name as string || 'Unknown';
const ext = file.properties.extension as string || '';
const icon = ext === '.py' ? '🐍' : ext === '.js' ? '📜' : ext === '.ts' ? '📘' : '📄';
content += `- ${icon} ${name}\n`;
});
if (childFiles.length > 20) {
content += `... and ${childFiles.length - 20} more files\n`;
}
}
console.log('SourceViewer - Generated directory content for folder:', folderPath);
}
else if (filePath && fileContents && fileContents.has(filePath)) {
content = fileContents.get(filePath)!;
console.log('SourceViewer - Found file content for:', filePath);

View file

@ -195,7 +195,7 @@ const GraphVisualization: React.FC<GraphVisualizationProps> = ({
return {
id: node.id,
label: node.properties.name as string || node.id,
label: getNodeDisplayName(node),
nodeType: node.label.toLowerCase(),
properties: node.properties,
color,
@ -258,6 +258,50 @@ const GraphVisualization: React.FC<GraphVisualizationProps> = ({
return { nodes, links };
};
// Helper function to get proper display name for nodes
const getNodeDisplayName = (node: GraphNode): string => {
// Use the name property if available
if (node.properties.name && typeof node.properties.name === 'string') {
const name = node.properties.name;
// For file nodes, show just the filename without path
if (node.label.toLowerCase() === 'file') {
const fileName = name.split('/').pop() || name;
return fileName;
}
// For other nodes, use the name as-is
return name;
}
// Fallback to filePath for file nodes
if (node.label.toLowerCase() === 'file' && node.properties.filePath) {
const filePath = node.properties.filePath as string;
const fileName = filePath.split('/').pop() || filePath;
return fileName;
}
// For function/method/class nodes, try common property names
if (['function', 'method', 'class', 'interface'].includes(node.label.toLowerCase())) {
const functionName = node.properties.functionName ||
node.properties.methodName ||
node.properties.className ||
node.properties.interfaceName;
if (functionName && typeof functionName === 'string') {
return functionName;
}
}
// Last resort: use a cleaned version of the node ID
let displayName = node.id;
// Remove common prefixes that might make it look like placeholder text
displayName = displayName.replace(/^(file|function|method|class)_?/i, '');
displayName = displayName.replace(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i, 'Unknown');
return displayName;
};
// Helper function to adjust color brightness
const adjustColorBrightness = (hex: string, percent: number): string => {
const num = parseInt(hex.replace("#", ""), 16);

View file

@ -70,8 +70,8 @@ export class IngestionWorker {
fileContents: fileContentsMap
});
// Clear file contents to free memory after processing
fileContentsMap.clear();
// Note: Keeping file contents available for UI components
// fileContentsMap.clear(); // Commented out to preserve file contents for SourceViewer
const duration = Date.now() - startTime;

View file

@ -71,8 +71,8 @@ export class KuzuIngestionWorker {
fileContents: fileContentsMap
});
// Clear file contents to free memory after processing
fileContentsMap.clear();
// Note: Keeping file contents available for UI components
// fileContentsMap.clear(); // Commented out to preserve file contents for SourceViewer
const duration = Date.now() - startTime;

View file

@ -0,0 +1,753 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Export Functionality Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background: #f9f9f9;
}
.success { border-color: #28a745; background: #d4edda; }
.error { border-color: #dc3545; background: #f8d7da; }
.warning { border-color: #ffc107; background: #fff3cd; }
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover { background: #0056b3; }
button:disabled { background: #6c757d; cursor: not-allowed; }
#output {
white-space: pre-wrap;
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
font-family: monospace;
max-height: 400px;
overflow-y: auto;
margin-top: 10px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin: 15px 0;
}
.stat-card {
background: white;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 5px;
}
.download-link {
display: inline-block;
background: #28a745;
color: white;
padding: 8px 15px;
text-decoration: none;
border-radius: 4px;
margin: 5px;
}
.download-link:hover {
background: #1e7e34;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 Export Functionality Test</h1>
<p>This test verifies that the export functionality works correctly after our import resolution improvements.</p>
<div class="test-section">
<h3>📊 Test Knowledge Graph</h3>
<p>We'll create a sample knowledge graph with the types of nodes our import resolution system now properly handles.</p>
<button onclick="createTestGraph()">Create Test Graph</button>
<div id="graph-stats" class="stats"></div>
</div>
<div class="test-section">
<h3>🔍 Validation Tests</h3>
<p>Test graph validation before export:</p>
<button onclick="validateGraph()">Validate Graph</button>
<button onclick="calculateSize()">Calculate Export Size</button>
<div id="validation-output"></div>
</div>
<div class="test-section">
<h3>📥 Export Tests</h3>
<p>Test different export formats:</p>
<button onclick="testJSONExport()">Test JSON Export</button>
<button onclick="testCSVExport()">Test CSV Export</button>
<button onclick="testFilteredExport()">Test Filtered Export</button>
<div id="export-links"></div>
</div>
<div class="test-section">
<h3>📤 Import Test</h3>
<p>Test importing exported data:</p>
<button onclick="testImport()">Test JSON Import</button>
<div id="import-output"></div>
</div>
<div class="test-section">
<h3>🔧 Console Output</h3>
<div id="output"></div>
</div>
</div>
<script type="module">
// Mock the export functions (in real app these would be imported)
window.testGraph = null;
window.testFileContents = null;
// Create a comprehensive test graph
window.createTestGraph = function() {
const graph = {
nodes: [
// Project structure nodes
{
id: 'project_root',
label: 'Project',
properties: {
name: 'GitNexus',
description: 'Knowledge graph generation tool',
version: '1.0.0'
}
},
{
id: 'src_folder',
label: 'Folder',
properties: {
name: 'src',
path: 'src',
filePath: 'src'
}
},
{
id: 'services_folder',
label: 'Folder',
properties: {
name: 'services',
path: 'src/services',
filePath: 'src/services'
}
},
// File nodes (including our zip.ts)
{
id: 'zip_service_file',
label: 'File',
properties: {
name: 'zip.ts',
filePath: 'src/services/zip.ts',
language: 'typescript',
extension: '.ts',
lineCount: 600
}
},
{
id: 'import_processor_file',
label: 'File',
properties: {
name: 'import-processor.ts',
filePath: 'src/core/ingestion/import-processor.ts',
language: 'typescript',
extension: '.ts',
lineCount: 946
}
},
// Class nodes
{
id: 'zip_service_class',
label: 'Class',
properties: {
name: 'ZipService',
filePath: 'src/services/zip.ts',
startLine: 26,
endLine: 599,
type: 'class',
qualifiedName: 'services.zip.ZipService'
}
},
{
id: 'import_processor_class',
label: 'Class',
properties: {
name: 'ImportProcessor',
filePath: 'src/core/ingestion/import-processor.ts',
startLine: 50,
endLine: 900,
type: 'class',
qualifiedName: 'core.ingestion.ImportProcessor'
}
},
// Method nodes (reflecting our import resolution improvements)
{
id: 'extract_complete_structure',
label: 'Method',
properties: {
name: 'extractCompleteStructure',
filePath: 'src/services/zip.ts',
startLine: 41,
endLine: 120,
type: 'method',
qualifiedName: 'services.zip.ZipService.extractCompleteStructure'
}
},
{
id: 'extract_js_imports',
label: 'Method',
properties: {
name: 'extractJSImports',
filePath: 'src/core/ingestion/import-processor.ts',
startLine: 210,
endLine: 280,
type: 'method',
qualifiedName: 'core.ingestion.ImportProcessor.extractJSImports'
}
},
// Function nodes
{
id: 'process_js_import_clause',
label: 'Function',
properties: {
name: 'processJSImportClause',
filePath: 'src/core/ingestion/import-processor.ts',
startLine: 350,
endLine: 450,
type: 'function',
qualifiedName: 'core.ingestion.processJSImportClause'
}
},
// Import nodes (showing our improved import resolution)
{
id: 'jszip_import',
label: 'Import',
properties: {
name: 'JSZip',
filePath: 'src/services/zip.ts',
importType: 'default',
targetFile: 'node_modules/jszip/dist/jszip.min.js'
}
}
],
relationships: [
// Containment relationships
{
id: 'rel_project_contains_src',
type: 'CONTAINS',
source: 'project_root',
target: 'src_folder',
properties: {}
},
{
id: 'rel_src_contains_services',
type: 'CONTAINS',
source: 'src_folder',
target: 'services_folder',
properties: {}
},
{
id: 'rel_services_contains_zip',
type: 'CONTAINS',
source: 'services_folder',
target: 'zip_service_file',
properties: {}
},
// Definition relationships
{
id: 'rel_zip_file_defines_class',
type: 'DEFINES',
source: 'zip_service_file',
target: 'zip_service_class',
properties: {}
},
{
id: 'rel_zip_class_defines_method',
type: 'DEFINES',
source: 'zip_service_class',
target: 'extract_complete_structure',
properties: {}
},
{
id: 'rel_import_processor_defines_method',
type: 'DEFINES',
source: 'import_processor_class',
target: 'extract_js_imports',
properties: {}
},
// Import relationships (our improved import resolution)
{
id: 'rel_zip_imports_jszip',
type: 'IMPORTS',
source: 'zip_service_file',
target: 'jszip_import',
properties: {
importType: 'default',
localName: 'JSZip',
exportedName: 'default'
}
},
// Call relationships
{
id: 'rel_extract_calls_process',
type: 'CALLS',
source: 'extract_js_imports',
target: 'process_js_import_clause',
properties: {
callType: 'method'
}
}
]
};
// Create mock file contents
const fileContents = new Map([
['src/services/zip.ts', `import JSZip from 'jszip';
export class ZipService {
public async extractCompleteStructure(file: File) {
// Implementation with 415 imports resolved (100% success)
console.log('🔍 ImportProcessor: Found 415 imports, resolved 415 (100.0%)');
}
}`],
['src/core/ingestion/import-processor.ts', `export class ImportProcessor {
private extractJSImports(node: Parser.SyntaxNode, filePath: string): void {
// CRITICAL FIX: Field-based approach with type-based fallback
let importClauseNode = node.childForFieldName('import_clause');
if (!importClauseNode) {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'import_clause') {
importClauseNode = child;
console.log('🔍 DEBUG: Found import_clause by type at index', i);
break;
}
}
}
}
}`]
]);
window.testGraph = graph;
window.testFileContents = fileContents;
// Display stats
updateGraphStats(graph);
log('✅ Test graph created successfully');
log(`📊 Nodes: ${graph.nodes.length}, Relationships: ${graph.relationships.length}`);
log(`📁 File contents: ${fileContents.size} files`);
return graph;
};
// Update graph statistics display
function updateGraphStats(graph) {
const nodesByType = graph.nodes.reduce((acc, node) => {
acc[node.label] = (acc[node.label] || 0) + 1;
return acc;
}, {});
const relsByType = graph.relationships.reduce((acc, rel) => {
acc[rel.type] = (acc[rel.type] || 0) + 1;
return acc;
}, {});
const statsContainer = document.getElementById('graph-stats');
statsContainer.innerHTML = `
<div class="stat-card">
<div class="stat-number">${graph.nodes.length}</div>
<div class="stat-label">Total Nodes</div>
</div>
<div class="stat-card">
<div class="stat-number">${graph.relationships.length}</div>
<div class="stat-label">Total Relationships</div>
</div>
<div class="stat-card">
<div class="stat-number">${Object.keys(nodesByType).length}</div>
<div class="stat-label">Node Types</div>
</div>
<div class="stat-card">
<div class="stat-number">${Object.keys(relsByType).length}</div>
<div class="stat-label">Relationship Types</div>
</div>
`;
log(`Node types: ${Object.entries(nodesByType).map(([k,v]) => `${k}:${v}`).join(', ')}`);
log(`Relationship types: ${Object.entries(relsByType).map(([k,v]) => `${k}:${v}`).join(', ')}`);
}
// Export validation functions (simplified versions for testing)
window.validateGraph = function() {
if (!window.testGraph) {
log('❌ No test graph available. Create one first.');
return;
}
const graph = window.testGraph;
const errors = [];
const warnings = [];
// Check for duplicate node IDs
const nodeIds = new Set(graph.nodes.map(n => n.id));
if (nodeIds.size !== graph.nodes.length) {
errors.push('Graph contains duplicate node IDs');
}
// Check for invalid relationships
graph.relationships.forEach((rel, index) => {
if (!nodeIds.has(rel.source)) {
errors.push(`Relationship ${index} has invalid source: ${rel.source}`);
}
if (!nodeIds.has(rel.target)) {
errors.push(`Relationship ${index} has invalid target: ${rel.target}`);
}
});
// Check for empty nodes/relationships
if (graph.nodes.length === 0) {
warnings.push('Graph has no nodes');
}
if (graph.relationships.length === 0) {
warnings.push('Graph has no relationships');
}
const validationDiv = document.getElementById('validation-output');
const isValid = errors.length === 0;
validationDiv.className = 'test-section ' + (isValid ? 'success' : 'error');
validationDiv.innerHTML = `
<h4>Validation Results</h4>
<p><strong>Status:</strong> ${isValid ? '✅ Valid' : '❌ Invalid'}</p>
${errors.length > 0 ? `<p><strong>Errors:</strong><br>${errors.join('<br>')}</p>` : ''}
${warnings.length > 0 ? `<p><strong>Warnings:</strong><br>${warnings.join('<br>')}</p>` : ''}
`;
log(isValid ? '✅ Graph validation passed' : '❌ Graph validation failed');
errors.forEach(error => log(`❌ ${error}`));
warnings.forEach(warning => log(`⚠️ ${warning}`));
};
window.calculateSize = function() {
if (!window.testGraph) {
log('❌ No test graph available.');
return;
}
const graph = window.testGraph;
const fileContents = window.testFileContents;
// Calculate sizes
const graphOnly = JSON.stringify(graph);
const withMetadata = JSON.stringify({
metadata: {
exportedAt: new Date().toISOString(),
nodeCount: graph.nodes.length,
relationshipCount: graph.relationships.length,
fileCount: fileContents?.size || 0
},
graph
});
const withFiles = JSON.stringify({
metadata: {
exportedAt: new Date().toISOString(),
nodeCount: graph.nodes.length,
relationshipCount: graph.relationships.length,
fileCount: fileContents?.size || 0
},
graph,
fileContents: fileContents ? Object.fromEntries(fileContents) : {}
});
function formatSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
log(`📏 Export sizes:`);
log(` - Graph only: ${formatSize(graphOnly.length)}`);
log(` - With metadata: ${formatSize(withMetadata.length)}`);
log(` - With files: ${formatSize(withFiles.length)}`);
};
// Export test functions
window.testJSONExport = function() {
if (!window.testGraph) {
log('❌ No test graph available.');
return;
}
try {
const graph = window.testGraph;
const fileContents = window.testFileContents;
// Create export with metadata
const exportData = {
metadata: {
exportedAt: new Date().toISOString(),
version: '1.0.0',
nodeCount: graph.nodes.length,
relationshipCount: graph.relationships.length,
fileCount: fileContents?.size || 0,
processingDuration: 5000 // Mock duration
},
graph,
fileContents: fileContents ? Object.fromEntries(fileContents) : {}
};
const jsonContent = JSON.stringify(exportData, null, 2);
// Create download link
const blob = new Blob([jsonContent], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const filename = `gitnexus-test-graph_${new Date().toISOString().split('T')[0]}.json`;
createDownloadLink(url, filename, 'JSON Export', '📄');
log('✅ JSON export test successful');
log(`📁 Filename: ${filename}`);
log(`📊 Export size: ${Math.round(blob.size / 1024)} KB`);
} catch (error) {
log(`❌ JSON export failed: ${error.message}`);
}
};
window.testCSVExport = function() {
if (!window.testGraph) {
log('❌ No test graph available.');
return;
}
try {
const graph = window.testGraph;
// Generate nodes CSV
const nodeHeaders = ':ID,name,filePath,startLine,endLine,type,language,qualifiedName,:LABEL';
const nodeRows = graph.nodes.map(node => {
const props = node.properties;
return [
escapeCSV(node.id),
escapeCSV(props.name || ''),
escapeCSV(props.filePath || ''),
props.startLine || '',
props.endLine || '',
escapeCSV(props.type || ''),
escapeCSV(props.language || ''),
escapeCSV(props.qualifiedName || ''),
node.label
].join(',');
});
const nodesCSV = [nodeHeaders, ...nodeRows].join('\n');
// Generate relationships CSV
const relHeaders = ':START_ID,:TYPE,:END_ID,source,target';
const relRows = graph.relationships.map(rel => {
return [
escapeCSV(rel.source),
rel.type,
escapeCSV(rel.target),
escapeCSV(rel.source),
escapeCSV(rel.target)
].join(',');
});
const relsCSV = [relHeaders, ...relRows].join('\n');
// Create download links
const timestamp = new Date().toISOString().split('T')[0];
const nodesBlob = new Blob([nodesCSV], { type: 'text/csv' });
const nodesUrl = URL.createObjectURL(nodesBlob);
createDownloadLink(nodesUrl, `gitnexus-test-graph_${timestamp}_nodes.csv`, 'Nodes CSV', '📊');
const relsBlob = new Blob([relsCSV], { type: 'text/csv' });
const relsUrl = URL.createObjectURL(relsBlob);
createDownloadLink(relsUrl, `gitnexus-test-graph_${timestamp}_relationships.csv`, 'Relationships CSV', '🔗');
log('✅ CSV export test successful');
log(`📊 Nodes CSV: ${Math.round(nodesBlob.size / 1024)} KB`);
log(`🔗 Relationships CSV: ${Math.round(relsBlob.size / 1024)} KB`);
} catch (error) {
log(`❌ CSV export failed: ${error.message}`);
}
};
window.testFilteredExport = function() {
if (!window.testGraph) {
log('❌ No test graph available.');
return;
}
try {
const graph = window.testGraph;
// Create filtered export (only TypeScript files and classes)
const filteredNodes = graph.nodes.filter(node =>
node.label === 'Class' ||
node.label === 'Method' ||
(node.properties.language === 'typescript')
);
const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
const filteredRels = graph.relationships.filter(rel =>
filteredNodeIds.has(rel.source) && filteredNodeIds.has(rel.target)
);
const filteredGraph = {
nodes: filteredNodes,
relationships: filteredRels
};
const exportData = {
metadata: {
exportedAt: new Date().toISOString(),
version: '1.0.0',
nodeCount: filteredGraph.nodes.length,
relationshipCount: filteredGraph.relationships.length,
filters: ['TypeScript files', 'Classes', 'Methods']
},
graph: filteredGraph
};
const jsonContent = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonContent], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const filename = `gitnexus-filtered-test_${new Date().toISOString().split('T')[0]}.json`;
createDownloadLink(url, filename, 'Filtered Export', '🔍');
log('✅ Filtered export test successful');
log(`📊 Original: ${graph.nodes.length} nodes, ${graph.relationships.length} relationships`);
log(`🔍 Filtered: ${filteredGraph.nodes.length} nodes, ${filteredGraph.relationships.length} relationships`);
} catch (error) {
log(`❌ Filtered export failed: ${error.message}`);
}
};
window.testImport = function() {
// For this test, we'll simulate importing the JSON we just exported
if (!window.testGraph) {
log('❌ No test graph available. Export one first.');
return;
}
try {
const originalGraph = window.testGraph;
const fileContents = window.testFileContents;
// Create mock export data
const exportData = {
metadata: {
exportedAt: new Date().toISOString(),
version: '1.0.0',
nodeCount: originalGraph.nodes.length,
relationshipCount: originalGraph.relationships.length,
fileCount: fileContents?.size || 0
},
graph: originalGraph,
fileContents: fileContents ? Object.fromEntries(fileContents) : {}
};
// Simulate import process
const jsonString = JSON.stringify(exportData);
const parsed = JSON.parse(jsonString);
// Verify import
const importedGraph = parsed.graph;
const importedMetadata = parsed.metadata;
const importedFiles = parsed.fileContents ? new Map(Object.entries(parsed.fileContents)) : null;
const importDiv = document.getElementById('import-output');
importDiv.className = 'test-section success';
importDiv.innerHTML = `
<h4>✅ Import Test Results</h4>
<p><strong>Metadata:</strong> ${importedMetadata ? '✅ Present' : '❌ Missing'}</p>
<p><strong>Graph:</strong> ${importedGraph ? '✅ Present' : '❌ Missing'}</p>
<p><strong>File Contents:</strong> ${importedFiles ? '✅ Present' : '❌ Missing'}</p>
<p><strong>Nodes:</strong> ${importedGraph.nodes.length} (${originalGraph.nodes.length === importedGraph.nodes.length ? '✅ Match' : '❌ Mismatch'})</p>
<p><strong>Relationships:</strong> ${importedGraph.relationships.length} (${originalGraph.relationships.length === importedGraph.relationships.length ? '✅ Match' : '❌ Mismatch'})</p>
<p><strong>Files:</strong> ${importedFiles?.size || 0} (${fileContents?.size === importedFiles?.size ? '✅ Match' : '❌ Mismatch'})</p>
`;
log('✅ Import test successful');
log(`📊 Imported ${importedGraph.nodes.length} nodes, ${importedGraph.relationships.length} relationships`);
log(`📁 Imported ${importedFiles?.size || 0} files`);
} catch (error) {
const importDiv = document.getElementById('import-output');
importDiv.className = 'test-section error';
importDiv.innerHTML = `<h4>❌ Import Test Failed</h4><p>${error.message}</p>`;
log(`❌ Import test failed: ${error.message}`);
}
};
// Helper functions
function createDownloadLink(url, filename, label, icon) {
const linksDiv = document.getElementById('export-links');
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.className = 'download-link';
link.innerHTML = `${icon} Download ${label}`;
linksDiv.appendChild(link);
}
function escapeCSV(value) {
if (!value) return '';
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
function log(message) {
const output = document.getElementById('output');
const timestamp = new Date().toLocaleTimeString();
output.textContent += `[${timestamp}] ${message}\n`;
output.scrollTop = output.scrollHeight;
console.log(message);
}
// Initialize
log('🚀 Export functionality test ready');
log('👆 Click "Create Test Graph" to begin testing');
</script>
</body>
</html>