@@ -26,47 +26,56 @@ export const MODELS = {
2626 minilm : {
2727 name : 'Xenova/all-MiniLM-L6-v2' ,
2828 dim : 384 ,
29+ contextWindow : 256 ,
2930 desc : 'Smallest, fastest (~23MB). General text.' ,
3031 quantized : true ,
3132 } ,
3233 'jina-small' : {
3334 name : 'Xenova/jina-embeddings-v2-small-en' ,
3435 dim : 512 ,
36+ contextWindow : 8192 ,
3537 desc : 'Small, good quality (~33MB). General text.' ,
3638 quantized : false ,
3739 } ,
3840 'jina-base' : {
3941 name : 'Xenova/jina-embeddings-v2-base-en' ,
4042 dim : 768 ,
43+ contextWindow : 8192 ,
4144 desc : 'Good quality (~137MB). General text, 8192 token context.' ,
4245 quantized : false ,
4346 } ,
4447 'jina-code' : {
4548 name : 'Xenova/jina-embeddings-v2-base-code' ,
4649 dim : 768 ,
50+ contextWindow : 8192 ,
4751 desc : 'Code-aware (~137MB). Trained on code+text, best for code search.' ,
4852 quantized : false ,
4953 } ,
5054 nomic : {
5155 name : 'Xenova/nomic-embed-text-v1' ,
5256 dim : 768 ,
57+ contextWindow : 8192 ,
5358 desc : 'Good local quality (~137MB). 8192 context.' ,
5459 quantized : false ,
5560 } ,
5661 'nomic-v1.5' : {
5762 name : 'nomic-ai/nomic-embed-text-v1.5' ,
5863 dim : 768 ,
64+ contextWindow : 8192 ,
5965 desc : 'Improved nomic (~137MB). Matryoshka dimensions, 8192 context.' ,
6066 quantized : false ,
6167 } ,
6268 'bge-large' : {
6369 name : 'Xenova/bge-large-en-v1.5' ,
6470 dim : 1024 ,
71+ contextWindow : 512 ,
6572 desc : 'Best general retrieval (~335MB). Top MTEB scores.' ,
6673 quantized : false ,
6774 } ,
6875} ;
6976
77+ export const EMBEDDING_STRATEGIES = [ 'structured' , 'source' ] ;
78+
7079export const DEFAULT_MODEL = 'minilm' ;
7180const BATCH_SIZE_MAP = {
7281 minilm : 32 ,
@@ -89,6 +98,108 @@ function getModelConfig(modelKey) {
8998 return config ;
9099}
91100
101+ /**
102+ * Rough token estimate (~4 chars per token for code/English).
103+ * Conservative — avoids adding a tokenizer dependency.
104+ */
105+ export function estimateTokens ( text ) {
106+ return Math . ceil ( text . length / 4 ) ;
107+ }
108+
109+ /**
110+ * Extract leading comment text (JSDoc, //, #, etc.) above a function line.
111+ * Returns the cleaned comment text or null if none found.
112+ */
113+ function extractLeadingComment ( lines , fnLineIndex ) {
114+ const raw = [ ] ;
115+ for ( let i = fnLineIndex - 1 ; i >= Math . max ( 0 , fnLineIndex - 15 ) ; i -- ) {
116+ const trimmed = lines [ i ] . trim ( ) ;
117+ if ( / ^ ( \/ \/ | \/ \* | \* \/ | \* | # | \/ \/ \/ ) / . test ( trimmed ) ) {
118+ raw . unshift ( trimmed ) ;
119+ } else if ( trimmed === '' ) {
120+ if ( raw . length > 0 ) break ;
121+ } else {
122+ break ;
123+ }
124+ }
125+ if ( raw . length === 0 ) return null ;
126+ return raw
127+ . map ( ( line ) =>
128+ line
129+ . replace ( / ^ \/ \* \* ? \s ? | \* \/ $ / g, '' ) // opening /** or /* and closing */
130+ . replace ( / ^ \* \s ? / , '' ) // middle * lines
131+ . replace ( / ^ \/ \/ \/ ? \s ? / , '' ) // // or ///
132+ . replace ( / ^ # \s ? / , '' ) // # (Python/Ruby)
133+ . trim ( ) ,
134+ )
135+ . filter ( ( l ) => l . length > 0 )
136+ . join ( ' ' ) ;
137+ }
138+
139+ /**
140+ * Build graph-enriched text for a symbol using dependency context.
141+ * Produces compact, semantic text (~100 tokens) instead of full source code.
142+ */
143+ function buildStructuredText ( node , file , lines , calleesStmt , callersStmt ) {
144+ const readable = splitIdentifier ( node . name ) ;
145+ const parts = [ `${ node . kind } ${ node . name } (${ readable } ) in ${ file } ` ] ;
146+ const startLine = Math . max ( 0 , node . line - 1 ) ;
147+
148+ // Extract parameters from signature (best-effort, single-line)
149+ const sigLine = lines [ startLine ] || '' ;
150+ const paramMatch = sigLine . match ( / \( ( [ ^ ) ] * ) \) / ) ;
151+ if ( paramMatch ?. [ 1 ] ?. trim ( ) ) {
152+ parts . push ( `Parameters: ${ paramMatch [ 1 ] . trim ( ) } ` ) ;
153+ }
154+
155+ // Graph context: callees (capped at 10)
156+ const callees = calleesStmt . all ( node . id ) ;
157+ if ( callees . length > 0 ) {
158+ parts . push (
159+ `Calls: ${ callees
160+ . slice ( 0 , 10 )
161+ . map ( ( c ) => c . name )
162+ . join ( ', ' ) } `,
163+ ) ;
164+ }
165+
166+ // Graph context: callers (capped at 10)
167+ const callers = callersStmt . all ( node . id ) ;
168+ if ( callers . length > 0 ) {
169+ parts . push (
170+ `Called by: ${ callers
171+ . slice ( 0 , 10 )
172+ . map ( ( c ) => c . name )
173+ . join ( ', ' ) } `,
174+ ) ;
175+ }
176+
177+ // Leading comment (high semantic value) or first few lines of code
178+ const comment = extractLeadingComment ( lines , startLine ) ;
179+ if ( comment ) {
180+ parts . push ( comment ) ;
181+ } else {
182+ const endLine = Math . min ( lines . length , startLine + 4 ) ;
183+ const snippet = lines . slice ( startLine , endLine ) . join ( '\n' ) . trim ( ) ;
184+ if ( snippet ) parts . push ( snippet ) ;
185+ }
186+
187+ return parts . join ( '\n' ) ;
188+ }
189+
190+ /**
191+ * Build raw source-code text for a symbol (original strategy).
192+ */
193+ function buildSourceText ( node , file , lines ) {
194+ const startLine = Math . max ( 0 , node . line - 1 ) ;
195+ const endLine = node . end_line
196+ ? Math . min ( lines . length , node . end_line )
197+ : Math . min ( lines . length , startLine + 15 ) ;
198+ const context = lines . slice ( startLine , endLine ) . join ( '\n' ) ;
199+ const readable = splitIdentifier ( node . name ) ;
200+ return `${ node . kind } ${ node . name } (${ readable } ) in ${ file } \n${ context } ` ;
201+ }
202+
92203/**
93204 * Lazy-load @huggingface/transformers.
94205 * This is an optional dependency — gives a clear error if not installed.
@@ -203,10 +314,14 @@ function initEmbeddingsSchema(db) {
203314
204315/**
205316 * Build embeddings for all functions/methods/classes in the graph.
317+ * @param {string } rootDir - Project root directory
318+ * @param {string } modelKey - Model identifier from MODELS registry
319+ * @param {string } [customDbPath] - Override path to graph.db
320+ * @param {object } [options] - Embedding options
321+ * @param {string } [options.strategy='structured'] - 'structured' (graph-enriched) or 'source' (raw code)
206322 */
207- export async function buildEmbeddings ( rootDir , modelKey , customDbPath ) {
208- // path already imported at top
209- // fs already imported at top
323+ export async function buildEmbeddings ( rootDir , modelKey , customDbPath , options = { } ) {
324+ const strategy = options . strategy || 'structured' ;
210325 const dbPath = customDbPath || findDbPath ( null ) ;
211326
212327 const db = new Database ( dbPath ) ;
@@ -221,7 +336,24 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
221336 )
222337 . all ( ) ;
223338
224- console . log ( `Building embeddings for ${ nodes . length } symbols...` ) ;
339+ console . log ( `Building embeddings for ${ nodes . length } symbols (strategy: ${ strategy } )...` ) ;
340+
341+ // Prepare graph-context queries for structured strategy
342+ let calleesStmt , callersStmt ;
343+ if ( strategy === 'structured' ) {
344+ calleesStmt = db . prepare ( `
345+ SELECT DISTINCT n.name FROM edges e
346+ JOIN nodes n ON e.target_id = n.id
347+ WHERE e.source_id = ? AND e.kind = 'calls'
348+ ORDER BY n.name
349+ ` ) ;
350+ callersStmt = db . prepare ( `
351+ SELECT DISTINCT n.name FROM edges e
352+ JOIN nodes n ON e.source_id = n.id
353+ WHERE e.target_id = ? AND e.kind = 'calls'
354+ ORDER BY n.name
355+ ` ) ;
356+ }
225357
226358 const byFile = new Map ( ) ;
227359 for ( const node of nodes ) {
@@ -232,6 +364,9 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
232364 const texts = [ ] ;
233365 const nodeIds = [ ] ;
234366 const previews = [ ] ;
367+ const config = getModelConfig ( modelKey ) ;
368+ const contextWindow = config . contextWindow ;
369+ let overflowCount = 0 ;
235370
236371 for ( const [ file , fileNodes ] of byFile ) {
237372 const fullPath = path . join ( rootDir , file ) ;
@@ -244,20 +379,31 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
244379 }
245380
246381 for ( const node of fileNodes ) {
247- const startLine = Math . max ( 0 , node . line - 1 ) ;
248- const endLine = node . end_line
249- ? Math . min ( lines . length , node . end_line )
250- : Math . min ( lines . length , startLine + 15 ) ;
251- const context = lines . slice ( startLine , endLine ) . join ( '\n' ) ;
252-
253- const readable = splitIdentifier ( node . name ) ;
254- const text = `${ node . kind } ${ node . name } (${ readable } ) in ${ file } \n${ context } ` ;
382+ let text =
383+ strategy === 'structured'
384+ ? buildStructuredText ( node , file , lines , calleesStmt , callersStmt )
385+ : buildSourceText ( node , file , lines ) ;
386+
387+ // Detect and handle context window overflow
388+ const tokens = estimateTokens ( text ) ;
389+ if ( tokens > contextWindow ) {
390+ overflowCount ++ ;
391+ const maxChars = contextWindow * 4 ;
392+ text = text . slice ( 0 , maxChars ) ;
393+ }
394+
255395 texts . push ( text ) ;
256396 nodeIds . push ( node . id ) ;
257397 previews . push ( `${ node . name } (${ node . kind } ) -- ${ file } :${ node . line } ` ) ;
258398 }
259399 }
260400
401+ if ( overflowCount > 0 ) {
402+ warn (
403+ `${ overflowCount } symbol(s) exceeded model context window (${ contextWindow } tokens) and were truncated` ,
404+ ) ;
405+ }
406+
261407 console . log ( `Embedding ${ texts . length } symbols...` ) ;
262408 const { vectors, dim } = await embed ( texts , modelKey ) ;
263409
@@ -269,16 +415,19 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
269415 for ( let i = 0 ; i < vectors . length ; i ++ ) {
270416 insert . run ( nodeIds [ i ] , Buffer . from ( vectors [ i ] . buffer ) , previews [ i ] ) ;
271417 }
272- const config = getModelConfig ( modelKey ) ;
273418 insertMeta . run ( 'model' , config . name ) ;
274419 insertMeta . run ( 'dim' , String ( dim ) ) ;
275420 insertMeta . run ( 'count' , String ( vectors . length ) ) ;
421+ insertMeta . run ( 'strategy' , strategy ) ;
276422 insertMeta . run ( 'built_at' , new Date ( ) . toISOString ( ) ) ;
423+ if ( overflowCount > 0 ) {
424+ insertMeta . run ( 'truncated_count' , String ( overflowCount ) ) ;
425+ }
277426 } ) ;
278427 insertAll ( ) ;
279428
280429 console . log (
281- `\nStored ${ vectors . length } embeddings (${ dim } d, ${ getModelConfig ( modelKey ) . name } ) in graph.db` ,
430+ `\nStored ${ vectors . length } embeddings (${ dim } d, ${ config . name } , strategy: ${ strategy } ) in graph.db` ,
282431 ) ;
283432 db . close ( ) ;
284433}
0 commit comments