/** * @module custom_cil-context * @audience installer * @layer api-handler * @stability experimental * * Pre-edit context brief for Devin (or any agent) about to touch a chunk. * * Call this BEFORE editing any named chunk to get a full situational brief: * - The chunk's contract (macro, micro, inputs, outputs, side_effects, stability) * - Blast radius: all chunks that transitively call this one (BFS up to depth 4) * - Hash/drift status: is the stored hash current or stale? * - Test paths: which test files cover this chunk * - Config dependencies: which cil_config fields this chunk reads * - Documentation quality: is the contract AI-generated, human-verified, or absent? * - Edit safety verdict: a plain-English summary of risk level * * Actions: * GET ?action=get_context { chunk_id } — full brief by chunk UUID * GET ?action=find_context { chunk_name, project_id? } — look up by name first, then return brief * GET ?action=post_edit { chunk_id } — drift-only report for after an edit */ import { createHandler } from './_shared/middleware' import { adminDb } from './_shared/db' import { resolveLinkTypeId } from './_shared/resolve-ids' // ─── HELPERS ───────────────────────────────────────────────────────────────── async function getChunkById(id: string): Promise { const { data } = await adminDb.from('items').select('id, title, data') .eq('id', id).maybeSingle() return data } async function findChunkByName(name: string, projectId?: string): Promise { let q = adminDb.from('items').select('id, title, data') .filter('data->>kb_type', 'eq', 'cil_chunk') .filter('data->>chunk_name', 'eq', name) .eq('is_active', true) if (projectId) q = q.filter('data->>project_id', 'eq', projectId) const { data } = await q.limit(1).maybeSingle() return data } /** BFS caller traversal up to maxDepth hops, returns all reachable chunk IDs + depth. */ async function blastRadiusBFS( chunkId: string, maxDepth: number, callsLinkTypeId: string ): Promise<{ chunk_id: string; chunk_name: string; file_path: string; depth: number }[]> { const visited = new Set([chunkId]) const queue: { id: string; depth: number }[] = [{ id: chunkId, depth: 0 }] const result: { chunk_id: string; chunk_name: string; file_path: string; depth: number }[] = [] while (queue.length > 0) { const { id, depth } = queue.shift()! if (depth >= maxDepth) continue // Find all callers of `id` (chunks that have a cil_calls link pointing TO id) const { data: callerLinks } = await adminDb.from('links').select('source_id') .eq('link_type_id', callsLinkTypeId) .eq('target_id', id) for (const link of callerLinks || []) { const callerId = link.source_id if (visited.has(callerId)) continue visited.add(callerId) const callerChunk = await getChunkById(callerId) if (!callerChunk) continue result.push({ chunk_id: callerId, chunk_name: callerChunk.data?.chunk_name || '', file_path: callerChunk.data?.file_path || '', depth: depth + 1, }) queue.push({ id: callerId, depth: depth + 1 }) } } return result } /** Fetch config fields (cil_configures links) for a chunk */ async function getConfigDeps( chunkId: string, configuresLinkTypeId: string | null ): Promise<{ field_name: string; source_file: string; config_id: string }[]> { if (!configuresLinkTypeId) return [] const { data: configLinks } = await adminDb.from('links').select('source_id, metadata') .eq('link_type_id', configuresLinkTypeId) .eq('target_id', chunkId) if (!configLinks?.length) return [] const results: { field_name: string; source_file: string; config_id: string }[] = [] for (const link of configLinks) { const configItem = await getChunkById(link.source_id) if (!configItem) continue results.push({ field_name: configItem.data?.field_name || link.metadata?.field_name || '', source_file: configItem.data?.config_table || '', config_id: link.source_id, }) } return results } /** Derive an edit safety verdict from the available data. */ function safetyVerdict( chunk: any, blastRadius: any[], driftStatus: 'current' | 'stale' | 'unknown', configDeps: any[] ): { level: 'safe' | 'caution' | 'risky'; summary: string; flags: string[] } { const d = chunk.data || {} const flags: string[] = [] if (d.stability === 'stable') flags.push('stability=stable: downstream consumers may break on signature changes') if (blastRadius.length >= 10) flags.push(`high blast radius: ${blastRadius.length} transitive callers`) else if (blastRadius.length >= 3) flags.push(`moderate blast radius: ${blastRadius.length} callers`) if (driftStatus === 'stale') flags.push('hash is stale — stored_hash does not match current disk content') if (configDeps.length > 0) flags.push(`reads ${configDeps.length} config field(s): ${configDeps.map(c => c.field_name).join(', ')}`) if (d.doc_confidence === 'unverified_legacy' || (!d.macro && !d.micro)) flags.push('no contract documentation — behaviour is undocumented') if (d.is_dead_code) flags.push('flagged as dead code') const level: 'safe' | 'caution' | 'risky' = flags.length === 0 ? 'safe' : (d.stability === 'stable' || blastRadius.length >= 10 || driftStatus === 'stale') ? 'risky' : 'caution' const summary = level === 'safe' ? 'Low risk. Internal chunk, no significant callers, contract is documented.' : level === 'caution' ? `Moderate risk. ${flags.length} concern(s) — review flags before editing.` : `High risk. ${flags.filter(f => f.includes('stable') || f.includes('blast') || f.includes('stale')).join('; ')}` return { level, summary, flags } } // ─── BUILD BRIEF ───────────────────────────────────────────────────────────── async function buildBrief(chunk: any, maxBlastDepth = 4): Promise> { const d = chunk.data || {} // Resolve link type IDs (best-effort) const [callsLinkTypeId, testedByLinkTypeId, configuresLinkTypeId] = await Promise.all([ resolveLinkTypeId('cil_calls').catch(() => null), resolveLinkTypeId('cil_tested_by').catch(() => null), resolveLinkTypeId('cil_configures').catch(() => null), ]) // Blast radius const blastRadius = callsLinkTypeId ? await blastRadiusBFS(chunk.id, maxBlastDepth, callsLinkTypeId) : [] // Direct callees (what this chunk calls) let callees: { chunk_id: string; chunk_name: string; file_path: string }[] = [] if (callsLinkTypeId) { const { data: calleeLinks } = await adminDb.from('links').select('target_id') .eq('link_type_id', callsLinkTypeId).eq('source_id', chunk.id) for (const l of calleeLinks || []) { const callee = await getChunkById(l.target_id) if (callee) callees.push({ chunk_id: callee.id, chunk_name: callee.data?.chunk_name || '', file_path: callee.data?.file_path || '' }) } } // Test paths (from links or from stored fields) let testPaths: { path: string; is_e2e: boolean }[] = [] if (testedByLinkTypeId) { const { data: testLinks } = await adminDb.from('links').select('target_id, metadata') .eq('link_type_id', testedByLinkTypeId).eq('source_id', chunk.id) for (const l of testLinks || []) { testPaths.push({ path: l.metadata?.test_path || '', is_e2e: l.metadata?.is_e2e || false }) } } // Also include stored test_unit_path / test_e2e_path fields as fallback if (testPaths.length === 0) { if (d.test_unit_path) testPaths.push({ path: d.test_unit_path, is_e2e: false }) if (d.test_e2e_path) testPaths.push({ path: d.test_e2e_path, is_e2e: true }) } // Config dependencies const configDeps = await getConfigDeps(chunk.id, configuresLinkTypeId) // Drift status const driftStatus: 'current' | 'stale' | 'unknown' = d.stored_hash ? 'unknown' // real drift check needs disk access; use 'unknown' when no file read : 'unknown' // Safety verdict const verdict = safetyVerdict(chunk, blastRadius, driftStatus, configDeps) return { chunk_id: chunk.id, chunk_name: d.chunk_name || '', file_path: d.file_path || '', line_start: d.line_start || null, line_end: d.line_end || null, chunk_type: d.chunk_type || '', // Contract contract: { 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', doc_confidence: d.doc_confidence || 'unverified_legacy', }, // Hash hash: { stored_hash: d.stored_hash || null, hash_updated_at: d.hash_updated_at || null, drift_status: driftStatus, }, // Call graph blast_radius: { direct_caller_count: blastRadius.filter(r => r.depth === 1).length, total_affected: blastRadius.length, max_depth_searched: maxBlastDepth, callers: blastRadius, }, calls: callees, // Tests test_paths: testPaths, // Config config_dependencies: configDeps, // Verdict edit_safety: verdict, } } // ─── HANDLER ───────────────────────────────────────────────────────────────── export const handler = createHandler(async (ctx, body) => { const action = ctx.query?.action || 'get_context' // ── GET CONTEXT by chunk_id ─────────────────────────────────────────────── if (action === 'get_context') { const chunkId = ctx.query?.chunk_id || body?.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 maxDepth = parseInt(ctx.query?.max_depth || body?.max_depth || '4', 10) const brief = await buildBrief(chunk, maxDepth) return { ok: true, brief } } // ── FIND CONTEXT by chunk_name (look up first, then return brief) ───────── if (action === 'find_context') { const chunkName = ctx.query?.chunk_name || body?.chunk_name const projectId = ctx.query?.project_id || body?.project_id if (!chunkName) throw new Error('chunk_name is required') const chunk = await findChunkByName(chunkName, projectId) if (!chunk) { return { ok: false, error: `No chunk named "${chunkName}" found${projectId ? ` in project ${projectId}` : ''}`, hint: 'Use custom_cil-graph?action=search to find chunks by description', } } const maxDepth = parseInt(ctx.query?.max_depth || body?.max_depth || '4', 10) const brief = await buildBrief(chunk, maxDepth) return { ok: true, brief } } // ── POST EDIT — drift-only report (call after editing a chunk) ──────────── // Returns only the hash status and a plain-English note. // The caller is responsible for re-ingesting if drift is detected. if (action === 'post_edit') { const chunkId = ctx.query?.chunk_id || body?.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 d = chunk.data || {} const hasHash = !!d.stored_hash return { ok: true, chunk_id: chunkId, chunk_name: d.chunk_name || '', file_path: d.file_path || '', stored_hash: d.stored_hash || null, hash_updated_at: d.hash_updated_at || null, has_stored_hash: hasHash, action_required: hasHash ? 'Run custom_cil-ingest?action=reindex_chunk to refresh the stored hash and code_content after your edit, then run custom_cil-validate?action=validate_file to confirm drift is resolved.' : 'No stored hash — run Pass 2 (custom_cil-ingest?action=pass2) to generate chunk boundaries and hashes for this file.', next_steps: [ `POST /api/custom_cil-ingest?action=reindex_chunk { "project_id": "${d.project_id || ''}", "chunk_id": "${chunkId}", "also_embed": true }`, `GET /api/custom_cil-validate?action=validate_file&file_id=${d.file_id || ''}`, ], } } throw new Error(`Unknown action: ${action}. Valid: get_context, find_context, post_edit`) })