mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-12 23:02:45 +00:00
feat(embeddings): multi-language class text generation with language-patterns
This commit is contained in:
parent
d4a8ce5623
commit
b3d8964b03
2 changed files with 152 additions and 56 deletions
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import type { EmbeddableNode, EmbeddingConfig } from './types.js';
|
||||
import { DEFAULT_EMBEDDING_CONFIG, isShortLabel } from './types.js';
|
||||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import { getPatternsForLanguage } from './language-patterns.js';
|
||||
|
||||
/**
|
||||
* Truncate description to max length at sentence/word boundary
|
||||
|
|
@ -118,9 +120,7 @@ const generateConstructorText = (
|
|||
/**
|
||||
* Generate embedding text for Class nodes
|
||||
* Signature + properties + method name list only (no method bodies)
|
||||
*
|
||||
* NOTE: Method/property regex is currently tuned for JS/TS syntax.
|
||||
* Multi-language support (Python, Kotlin, Rust, etc.) is a TODO.
|
||||
* Multi-language: dispatches to language-patterns for method/property extraction.
|
||||
*/
|
||||
const generateClassText = (
|
||||
node: EmbeddableNode,
|
||||
|
|
@ -130,67 +130,56 @@ const generateClassText = (
|
|||
const header = buildMetadataHeader(node, config);
|
||||
const parts: string[] = [header];
|
||||
|
||||
// Extract method names and properties from content
|
||||
const cleaned = cleanContent(codeBody);
|
||||
const lines = cleaned.split('\n');
|
||||
|
||||
const methods: string[] = [];
|
||||
const properties: string[] = [];
|
||||
const classBodyLines: string[] = [];
|
||||
let inClass = false;
|
||||
// Try language-specific patterns first
|
||||
const language = getLanguageFromFilename(node.filePath);
|
||||
const patterns = language ? getPatternsForLanguage(language) : undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Detect class opening (JS/TS only — Python `class Foo:`, Kotlin `class Foo` etc. are not yet handled)
|
||||
if (
|
||||
trimmed.match(/^(?:export\s+)?(?:abstract\s+)?class\s/) ||
|
||||
trimmed.startsWith('data class ')
|
||||
) {
|
||||
inClass = true;
|
||||
classBodyLines.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
if (!inClass) {
|
||||
classBodyLines.push(trimmed);
|
||||
continue;
|
||||
if (patterns) {
|
||||
const methods = patterns.extractMethods(cleaned);
|
||||
const properties = patterns.extractProperties(cleaned);
|
||||
if (methods.length > 0) parts.push(`Methods: ${methods.join(', ')}`);
|
||||
if (properties.length > 0) parts.push(`Properties: ${properties.join(', ')}`);
|
||||
} else {
|
||||
// Fallback: original JS/TS regex
|
||||
const methods: string[] = [];
|
||||
const properties: string[] = [];
|
||||
const lines = cleaned.split('\n');
|
||||
let inClass = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
trimmed.match(/^(?:export\s+)?(?:abstract\s+)?class\s/) ||
|
||||
trimmed.startsWith('data class ')
|
||||
) {
|
||||
inClass = true;
|
||||
continue;
|
||||
}
|
||||
if (!inClass) continue;
|
||||
|
||||
const methodMatch = trimmed.match(
|
||||
/^(?:public|private|protected|static|async|abstract|\s)*\s*(\w+)\s*\(/,
|
||||
);
|
||||
if (methodMatch && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||
methods.push(methodMatch[1]);
|
||||
}
|
||||
|
||||
const propMatch = trimmed.match(
|
||||
/^(?:public|private|protected|static|readonly)\s+(\w+)\s*[=:(]/,
|
||||
);
|
||||
if (propMatch) {
|
||||
properties.push(propMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract method names
|
||||
const methodMatch = trimmed.match(
|
||||
/^(?:public|private|protected|static|async|abstract|\s)*\s*(\w+)\s*\(/,
|
||||
);
|
||||
if (methodMatch && !trimmed.startsWith('//') && !trimmed.startsWith('*')) {
|
||||
methods.push(methodMatch[1]);
|
||||
}
|
||||
|
||||
// Extract property declarations
|
||||
const propMatch = trimmed.match(
|
||||
/^(?:public|private|protected|static|readonly)\s+(\w+)\s*[=:(]/,
|
||||
);
|
||||
if (propMatch) {
|
||||
properties.push(propMatch[1]);
|
||||
}
|
||||
|
||||
// Keep class declaration + property lines (no method bodies)
|
||||
if (
|
||||
trimmed.match(/^(?:export\s+)?(?:abstract\s+)?class\s/) ||
|
||||
trimmed.startsWith('data class ') ||
|
||||
trimmed.startsWith('{') ||
|
||||
trimmed.startsWith('}') ||
|
||||
trimmed === '' ||
|
||||
propMatch ||
|
||||
trimmed.endsWith(';') ||
|
||||
!trimmed.includes('{')
|
||||
) {
|
||||
classBodyLines.push(trimmed);
|
||||
}
|
||||
if (methods.length > 0) parts.push(`Methods: ${methods.join(', ')}`);
|
||||
if (properties.length > 0) parts.push(`Properties: ${properties.join(', ')}`);
|
||||
}
|
||||
|
||||
if (methods.length > 0) parts.push(`Methods: ${methods.join(', ')}`);
|
||||
if (properties.length > 0) parts.push(`Properties: ${properties.join(', ')}`);
|
||||
|
||||
// Class declaration only (no method bodies)
|
||||
const declarationOnly = classBodyLines.join('\n').trim();
|
||||
const declarationOnly = extractDeclarationOnly(cleaned);
|
||||
if (declarationOnly) {
|
||||
parts.push('', declarationOnly);
|
||||
}
|
||||
|
|
@ -198,6 +187,39 @@ const generateClassText = (
|
|||
return parts.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract class/interface/struct declaration lines (no method bodies)
|
||||
*/
|
||||
const extractDeclarationOnly = (content: string): string => {
|
||||
const lines = content.split('\n');
|
||||
const declLines: string[] = [];
|
||||
let depth = 0;
|
||||
let started = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
!started &&
|
||||
(trimmed.match(/^(?:export\s+)?(?:abstract\s+)?(?:data\s+)?class\s/) ||
|
||||
trimmed.match(/^(?:pub\s+)?struct\s/) ||
|
||||
trimmed.match(/^(?:pub\s+)?enum\s/) ||
|
||||
trimmed.match(/^type\s+\w+\s+struct/) ||
|
||||
trimmed.match(/^class\s/) ||
|
||||
trimmed.match(/^interface\s/))
|
||||
) {
|
||||
started = true;
|
||||
}
|
||||
if (started) {
|
||||
depth += (trimmed.match(/\{/g) || []).length;
|
||||
depth -= (trimmed.match(/\}/g) || []).length;
|
||||
declLines.push(trimmed);
|
||||
if (depth <= 0 && declLines.length > 1) break;
|
||||
}
|
||||
}
|
||||
|
||||
return declLines.join('\n').trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate embedding text for short nodes (TypeAlias, Const, etc.)
|
||||
* No chunking, just metadata + full content
|
||||
|
|
|
|||
|
|
@ -108,6 +108,80 @@ describe('text-generator', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('multi-language class text', () => {
|
||||
it('extracts methods from Python class', () => {
|
||||
const node: EmbeddableNode = {
|
||||
id: 'Class:src/models.py:User',
|
||||
name: 'User',
|
||||
label: 'Class',
|
||||
filePath: 'src/models/user.py',
|
||||
content: `class User:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def get_full_name(self):
|
||||
return self.name`,
|
||||
};
|
||||
const text = generateEmbeddingText(node, node.content);
|
||||
expect(text).toContain('Class: User');
|
||||
expect(text).toContain('Methods:');
|
||||
expect(text).toContain('__init__');
|
||||
expect(text).toContain('get_full_name');
|
||||
});
|
||||
|
||||
it('extracts methods from Kotlin class', () => {
|
||||
const node: EmbeddableNode = {
|
||||
id: 'Class:User.kt:User',
|
||||
name: 'User',
|
||||
label: 'Class',
|
||||
filePath: 'src/models/User.kt',
|
||||
content: `class User(val name: String) {
|
||||
fun greet(): String = "Hello"
|
||||
private fun validate() { }
|
||||
}`,
|
||||
};
|
||||
const text = generateEmbeddingText(node, node.content);
|
||||
expect(text).toContain('Class: User');
|
||||
expect(text).toContain('Methods:');
|
||||
expect(text).toContain('greet');
|
||||
expect(text).toContain('validate');
|
||||
expect(text).toContain('Properties:');
|
||||
expect(text).toContain('name');
|
||||
});
|
||||
|
||||
it('extracts struct fields from Rust', () => {
|
||||
const node: EmbeddableNode = {
|
||||
id: 'Struct:src/user.rs:User',
|
||||
name: 'User',
|
||||
label: 'Struct',
|
||||
filePath: 'src/models/user.rs',
|
||||
content: `struct User {
|
||||
name: String,
|
||||
age: u32,
|
||||
}`,
|
||||
};
|
||||
const text = generateEmbeddingText(node, node.content);
|
||||
expect(text).toContain('Struct: User');
|
||||
});
|
||||
|
||||
it('falls back to JS/TS regex for unknown extension', () => {
|
||||
const node: EmbeddableNode = {
|
||||
...baseNode,
|
||||
label: 'Class',
|
||||
name: 'Parser',
|
||||
filePath: 'src/parser.unknown',
|
||||
content: `class Parser {
|
||||
parse(input) { }
|
||||
validate() { }
|
||||
}`,
|
||||
};
|
||||
const text = generateEmbeddingText(node, node.content);
|
||||
expect(text).toContain('Class: Parser');
|
||||
expect(text).toContain('parse');
|
||||
expect(text).toContain('validate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateDescription', () => {
|
||||
it('returns short text unchanged', () => {
|
||||
expect(truncateDescription('short text', 150)).toBe('short text');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue