import { createHandler } from './_shared/middleware' import { adminDb } from './_shared/db' import { resolveLinkTypeId } from './_shared/resolve-ids' // ─── TYPES ──────────────────────────────────────────────────────────────────── interface ChunkContract { id: string chunk_name: string file_path: string line_start: number line_end: number chunk_type: string macro: string micro: string input_spec: string output_spec: string side_effects: string stability: string test_unit_path: string | null test_e2e_path: string | null doc_confidence: string boundary_confidence: string stored_hash: string | null hash_status: 'current' | 'drift_detected' | 'unknown' callers: { chunk_id: string; chunk_name: string; file_path: string }[] callees: { chunk_id: string; chunk_name: string; file_path: string }[] is_dead_code: boolean } // ─── HELPERS ────────────────────────────────────────────────────────────────── async function getChunkById(chunkId: string): Promise { const { data } = await adminDb .from('items') .select('id, title, data') .eq('id', chunkId) .maybeSingle() return data } async function getLinkedChunks( chunkId: string, linkTypeSlug: string, direction: 'callers' | 'callees' ): Promise<{ chunk_id: string; chunk_name: string; file_path: string }[]> { try { const linkTypeId = await resolveLinkTypeId(linkTypeSlug) const query = direction === 'callees' ? adminDb.from('links').select('target_id').eq('source_id', chunkId).eq('link_type_id', linkTypeId) : adminDb.from('links').select('source_id').eq('target_id', chunkId).eq('link_type_id', linkTypeId) const { data: links } = await query if (!links || links.length === 0) return [] const ids = direction === 'callees' ? links.map((l: any) => l.target_id) : links.map((l: any) => l.source_id) const { data: chunks } = await adminDb .from('items') .select('id, data') .in('id', ids) return (chunks || []).map((c: any) => ({ chunk_id: c.id, chunk_name: c.data?.chunk_name || c.data?.title || 'Unknown', file_path: c.data?.file_path || '', })) } catch { return [] } } // ─── HANDLER ────────────────────────────────────────────────────────────────── export const handler = createHandler(async (ctx, body) => { const action = ctx.query?.action // ── GET CHUNK CONTRACT ──────────────────────────────────────────────────────── if (action === 'get_chunk_contract') { const chunkId = ctx.query?.chunk_id if (!chunkId) throw new Error('chunk_id is required') const chunk = await getChunkById(chunkId) if (!chunk) throw new Error(`Chunk not found: ${chunkId}`) const [callers, callees] = await Promise.all([ getLinkedChunks(chunkId, 'cil_calls', 'callers'), getLinkedChunks(chunkId, 'cil_calls', 'callees'), ]) const d = chunk.data || {} const contract: ChunkContract = { id: chunk.id, chunk_name: d.chunk_name || chunk.title || '', file_path: d.file_path || '', line_start: d.line_start || 0, line_end: d.line_end || 0, chunk_type: d.chunk_type || '', macro: d.macro || '', micro: d.micro || '', input_spec: d.input_spec || '', output_spec: d.output_spec || '', side_effects: d.side_effects || '', stability: d.stability || 'unknown', test_unit_path: d.test_unit_path || null, test_e2e_path: d.test_e2e_path || null, doc_confidence: d.doc_confidence || 'unknown', boundary_confidence: d.boundary_confidence || 'unknown', stored_hash: d.stored_hash || null, hash_status: 'unknown', // validated on demand via custom_cil-validate callers, callees, is_dead_code: d.is_dead_code || false, } return { contract } } // ── GET CALLERS ─────────────────────────────────────────────────────────────── if (action === 'get_callers') { const chunkId = ctx.query?.chunk_id if (!chunkId) throw new Error('chunk_id is required') const callers = await getLinkedChunks(chunkId, 'cil_calls', 'callers') return { chunk_id: chunkId, callers } } // ── GET BLAST RADIUS ────────────────────────────────────────────────────────── if (action === 'get_blast_radius') { const chunkId = ctx.query?.chunk_id const maxDepth = parseInt(ctx.query?.max_depth || '3') if (!chunkId) throw new Error('chunk_id is required') // BFS traversal of caller graph up to maxDepth hops const visited = new Set() const queue: { id: string; depth: number }[] = [{ id: chunkId, depth: 0 }] const affected: { chunk_id: string; chunk_name: string; file_path: string; depth: number }[] = [] while (queue.length > 0) { const { id, depth } = queue.shift()! if (visited.has(id) || depth > maxDepth) continue visited.add(id) if (id !== chunkId) { const chunk = await getChunkById(id) if (chunk) { affected.push({ chunk_id: id, chunk_name: chunk.data?.chunk_name || chunk.title || 'Unknown', file_path: chunk.data?.file_path || '', depth, }) } } if (depth < maxDepth) { const callers = await getLinkedChunks(id, 'cil_calls', 'callers') for (const caller of callers) { if (!visited.has(caller.chunk_id)) { queue.push({ id: caller.chunk_id, depth: depth + 1 }) } } } } return { chunk_id: chunkId, max_depth: maxDepth, affected_count: affected.length, affected, } } // ── GET TEST PATHS ──────────────────────────────────────────────────────────── if (action === 'get_test_paths') { const chunkId = ctx.query?.chunk_id if (!chunkId) throw new Error('chunk_id is required') const chunk = await getChunkById(chunkId) if (!chunk) throw new Error(`Chunk not found: ${chunkId}`) return { chunk_id: chunkId, chunk_name: chunk.data?.chunk_name || '', test_unit_path: chunk.data?.test_unit_path || null, test_e2e_path: chunk.data?.test_e2e_path || null, doc_confidence: chunk.data?.doc_confidence || 'unknown', } } // ── GET FILE GRAPH ──────────────────────────────────────────────────────────── if (action === 'get_file_graph') { const fileId = ctx.query?.file_id if (!fileId) throw new Error('file_id is required') const { data: file } = await adminDb .from('items') .select('id, title, data') .eq('id', fileId) .maybeSingle() if (!file) throw new Error(`File not found: ${fileId}`) // Get all chunks for this file const { data: chunks } = await adminDb .from('items') .select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_chunk') .filter('data->>file_id', 'eq', fileId) .eq('is_active', true) // Get import edges for this file let imports: any[] = [] try { const importLinkTypeId = await resolveLinkTypeId('cil_imports') const { data: importLinks } = await adminDb .from('links') .select('target_id, metadata') .eq('source_id', fileId) .eq('link_type_id', importLinkTypeId) imports = importLinks || [] } catch { /* link type may not exist yet */ } return { file: { id: file.id, file_path: file.data?.file_path || '', language: file.data?.language || '', ingestion_pass: file.data?.ingestion_pass || 0, stored_hash: file.data?.stored_hash || null, is_dead_code: file.data?.is_dead_code || false, }, chunks: (chunks || []).map(c => ({ id: c.id, chunk_name: c.data?.chunk_name || '', line_start: c.data?.line_start || 0, line_end: c.data?.line_end || 0, chunk_type: c.data?.chunk_type || '', boundary_confidence: c.data?.boundary_confidence || '', review_status: c.data?.review_status || '', doc_confidence: c.data?.doc_confidence || '', is_dead_code: c.data?.is_dead_code || false, stored_hash: c.data?.stored_hash || null, })), imports, } } // ── SEARCH — hybrid: semantic (if embeddings exist) + name prefix fallback ───── if (action === 'search') { const query = ctx.query?.q || body?.q const projectId = ctx.query?.project_id || body?.project_id const limit = parseInt(ctx.query?.limit || '20') const mode = ctx.query?.mode || body?.mode || 'hybrid' // 'semantic' | 'text' | 'hybrid' if (!query) throw new Error('q is required') const results: any[] = [] const seenIds = new Set() // ── Semantic search via match_embeddings RPC ───────────────────────────── if (mode !== 'text') { try { const apiKey = process.env.OPENAI_API_KEY || process.env.LLM_API_KEY const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1' if (apiKey) { const embedRes = await fetch(`${baseUrl}/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'text-embedding-3-small', input: [query] }), }) if (embedRes.ok) { const ej: any = await embedRes.json() const vec = ej.data?.[0]?.embedding if (vec) { const vectorLiteral = `[${vec.join(',')}]` const { data: matches } = await adminDb.rpc('match_embeddings', { query_embedding: vectorLiteral, match_count: limit, filter_vector_type: 'semantic', similarity_threshold: 0.2, filter_item_types: ['cil_chunk'], }) const matchIds = [...new Set((matches || []).map((m: any) => m.document_id))] if (matchIds.length > 0) { let chunkQuery = adminDb .from('items') .select('id, title, data') .in('id', matchIds) .eq('is_active', true) if (projectId) { chunkQuery = chunkQuery.filter('data->>project_id', 'eq', projectId) } const { data: chunks } = await chunkQuery const chunkMap = new Map((chunks || []).map((c: any) => [c.id, c])) for (const m of (matches || [])) { const chunk = chunkMap.get(m.document_id) if (!chunk || seenIds.has(chunk.id)) continue seenIds.add(chunk.id) results.push({ id: chunk.id, chunk_name: chunk.data?.chunk_name || chunk.title, file_path: chunk.data?.file_path || '', macro: chunk.data?.macro || '', chunk_type: chunk.data?.chunk_type || '', doc_confidence: chunk.data?.doc_confidence || '', similarity: m.similarity, match_type: 'semantic', }) } } } } } } catch { /* fall through to text search */ } } // ── Text search fallback (name/title prefix) ────────────────────────────── if (mode !== 'semantic' && results.length < limit) { let dbQuery = adminDb .from('items') .select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_chunk') .eq('is_active', true) .limit(limit - results.length) if (projectId) dbQuery = dbQuery.filter('data->>project_id', 'eq', projectId) const { data: textResults } = await dbQuery.ilike('title', `%${query}%`) for (const r of textResults || []) { if (seenIds.has(r.id)) continue seenIds.add(r.id) results.push({ id: r.id, chunk_name: r.data?.chunk_name || r.title, file_path: r.data?.file_path || '', macro: r.data?.macro || '', chunk_type: r.data?.chunk_type || '', doc_confidence: r.data?.doc_confidence || '', similarity: null, match_type: 'text', }) } } return { query, mode, results, count: results.length } } // ── GET FILE CHUNKS — all chunks for one file, sorted by line_start ────────── if (action === 'get_file_chunks') { const fileId = ctx.query?.file_id if (!fileId) throw new Error('file_id is required') const { data: fileItem } = await adminDb.from('items').select('id, title, data') .eq('id', fileId).maybeSingle() if (!fileItem) throw new Error(`File not found: ${fileId}`) const { data: chunks } = await adminDb.from('items').select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_chunk') .filter('data->>file_id', 'eq', fileId) .eq('is_active', true) const sorted = (chunks || []) .map((c: any) => ({ id: c.id, chunk_name: c.data?.chunk_name || '', chunk_type: c.data?.chunk_type || '', line_start: c.data?.line_start || 0, line_end: c.data?.line_end || 0, macro: c.data?.macro || '', doc_confidence: c.data?.doc_confidence || 'unverified_legacy', boundary_confidence: c.data?.boundary_confidence || '', stored_hash: c.data?.stored_hash || null, is_dead_code: c.data?.is_dead_code || false, review_status: c.data?.review_status || '', stability: c.data?.stability || '', file_path: c.data?.file_path || fileItem.data?.file_path || '', project_id: c.data?.project_id || '', })) .sort((a: any, b: any) => a.line_start - b.line_start) return { file_id: fileId, file_path: fileItem.data?.file_path || '', language: fileItem.data?.language || '', chunk_count: sorted.length, chunks: sorted, } } // ── GET PROJECT GRAPH (all files + chunks for graph viz) ────────────────────── if (action === 'get_project_graph') { const projectId = ctx.query?.project_id if (!projectId) throw new Error('project_id is required') const [filesResult, chunksResult] = await Promise.all([ adminDb.from('items').select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_file') .filter('data->>project_id', 'eq', projectId) .eq('is_active', true), adminDb.from('items').select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_chunk') .filter('data->>project_id', 'eq', projectId) .eq('is_active', true) .limit(500), ]) // Get import edges between files let importEdges: any[] = [] try { const importLinkTypeId = await resolveLinkTypeId('cil_imports') const fileIds = (filesResult.data || []).map((f: any) => f.id) if (fileIds.length > 0) { const { data: edges } = await adminDb .from('links') .select('source_id, target_id, metadata') .eq('link_type_id', importLinkTypeId) .in('source_id', fileIds) importEdges = edges || [] } } catch { /* link type may not exist yet */ } // Get call edges between chunks let callEdges: any[] = [] try { const callLinkTypeId = await resolveLinkTypeId('cil_calls') const chunkIds = (chunksResult.data || []).map((c: any) => c.id) if (chunkIds.length > 0) { const { data: edges } = await adminDb .from('links') .select('source_id, target_id, metadata') .eq('link_type_id', callLinkTypeId) .in('source_id', chunkIds) callEdges = edges || [] } } catch { /* link type may not exist yet */ } return { project_id: projectId, nodes: { files: (filesResult.data || []).map((f: any) => ({ id: f.id, file_path: f.data?.file_path || '', language: f.data?.language || '', ingestion_pass: f.data?.ingestion_pass || 0, is_dead_code: f.data?.is_dead_code || false, chunk_count: f.data?.chunk_count || 0, })), chunks: (chunksResult.data || []).map((c: any) => ({ id: c.id, chunk_name: c.data?.chunk_name || '', file_id: c.data?.file_id || '', file_path: c.data?.file_path || '', chunk_type: c.data?.chunk_type || '', boundary_confidence: c.data?.boundary_confidence || '', doc_confidence: c.data?.doc_confidence || '', is_dead_code: c.data?.is_dead_code || false, stored_hash: c.data?.stored_hash || null, })), }, edges: { imports: importEdges, calls: callEdges, }, } } throw new Error(`Unknown action: ${action}. Valid: get_chunk_contract, get_callers, get_blast_radius, get_test_paths, get_file_graph, get_file_chunks, search, get_project_graph`) })