const http = require('http'); const https = require('https'); /** * Phase 6+7: LLM Prose Generator * Generates human-readable prose for system documentation using Claude Sonnet. * All structural analysis is deterministic — LLM is ONLY for prose formatting. */ const DEFAULT_URL = process.env.LLM_URL || 'http://192.168.86.11:8000/v1'; const DEFAULT_MODEL = process.env.LLM_MODEL || 'claude-sonnet-4.6'; const DEFAULT_API_KEY = process.env.LLM_API_KEY || 'my-super-secret-password-123'; /** * Call an OpenAI-compatible chat completions API. */ function callLLM(prompt, opts = {}) { const baseUrl = opts.url || DEFAULT_URL; const model = opts.model || DEFAULT_MODEL; const apiKey = opts.apiKey || DEFAULT_API_KEY; const maxTokens = opts.maxTokens || 1024; const temperature = opts.temperature || 0.3; return new Promise((resolve, reject) => { const url = new URL('/v1/chat/completions', baseUrl.replace(/\/v1\/?$/, '')); const body = JSON.stringify({ model, messages: [ { role: 'system', content: 'You are a senior software architect writing concise, precise technical documentation. Write in present tense. Be specific about domain logic, not syntax. No filler.' }, { role: 'user', content: prompt }, ], max_tokens: maxTokens, temperature, }); const client = url.protocol === 'https:' ? https : http; const req = client.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, }, (res) => { let data = ''; res.on('data', c => data += c); res.on('end', () => { try { const parsed = JSON.parse(data); resolve(parsed.choices?.[0]?.message?.content || ''); } catch (e) { reject(new Error(`LLM parse error: ${e.message} — raw: ${data.substring(0, 200)}`)); } }); }); req.on('error', reject); req.setTimeout(120000, () => { req.destroy(); reject(new Error('LLM timeout (120s)')); }); req.write(body); req.end(); }); } /** * Generate a prose overview for a subsystem. */ async function describeSubsystem(sub, deps, llmOpts) { const depList = Object.entries(deps) .filter(([k]) => k.startsWith(sub.name + '→') || k.endsWith('→' + sub.name)) .map(([k, v]) => `${k}: ${v.calls} calls, ${v.imports} imports`) .slice(0, 10); const prompt = `Write a 2-3 sentence technical overview of the "${sub.name}" subsystem. Facts: - Kind: ${sub.kind} - Files: ${sub.files.length} - Functions: ${sub.entities.functions}, Classes: ${sub.entities.classes}, Modules: ${sub.entities.modules} - Public exports: ${sub.publicExports.slice(0, 15).join(', ')}${sub.publicExports.length > 15 ? ` (+${sub.publicExports.length - 15} more)` : ''} ${depList.length > 0 ? `- Dependencies:\n ${depList.join('\n ')}` : '- No cross-subsystem dependencies'} Write ONLY the overview paragraph, no heading.`; return callLLM(prompt, llmOpts); } /** * Generate a prose narrative for a data flow trace. */ async function describeFlow(flowResult, llmOpts) { const steps = flowResult.flow.slice(0, 20).map((s, i) => `${i + 1}. [${s.subsystem}] ${s.entity}${s.crossedVia ? ` (crosses via ${s.crossedVia})` : ''}` ).join('\n'); const prompt = `Write a 3-5 sentence narrative describing this data flow through the system. Entry point: ${flowResult.entryPoint} Subsystem sequence: ${flowResult.subsystemSequence.join(' → ')} ${flowResult.excludedNodes.length > 0 ? `Excluded (high fan-in): ${flowResult.excludedNodes.slice(0, 5).join(', ')}` : ''} ${flowResult.cyclesDetected.length > 0 ? `Cycles detected: ${flowResult.cyclesDetected.length}` : ''} Steps: ${steps}${flowResult.flow.length > 20 ? `\n... (+${flowResult.flow.length - 20} more steps)` : ''} Write ONLY the narrative paragraph, no heading. Explain what happens when this entry point is triggered and how data moves across subsystem boundaries.`; return callLLM(prompt, llmOpts); } /** * Generate a prose description for a contract (interface/type/enum). */ async function describeContract(contract, xref, llmOpts) { const usedBy = xref?.[contract.name]?.usedBy || []; let details = ''; if (contract.type === 'Interface' && contract.fields) { details = `Fields: ${contract.fields.map(f => `${f.name}: ${f.type}`).join(', ')}`; if (contract.extends) details += `\nExtends: ${contract.extends.join(', ')}`; } else if (contract.type === 'Enum' && contract.members) { details = `Members: ${contract.members.join(', ')}`; } else if (contract.type.startsWith('Helm')) { // Helm contract types if (contract.fields) { details = `Fields: ${contract.fields.slice(0, 20).map(f => `${f.name}: ${f.type}`).join(', ')}`; if (contract.fields.length > 20) details += ` (+${contract.fields.length - 20} more)`; } } const typeLabel = contract.type.startsWith('Helm') ? `Helm ${contract.type.replace('Helm', '').toLowerCase()} contract` : `TypeScript ${contract.type.toLowerCase()}`; const prompt = `Write a 1-2 sentence description of this ${typeLabel}. Name: ${contract.name} Type: ${contract.type} Defined in: ${contract.id} Visibility: ${contract.visibility} ${details} ${usedBy.length > 0 ? `Used by subsystems: ${usedBy.join(', ')}` : 'Not referenced cross-subsystem'} Write ONLY the description, no heading. Do not ask for more information.`; return callLLM(prompt, { ...llmOpts, maxTokens: 256 }); } /** * Generate a system-level architecture overview. */ async function describeArchitecture(subsystems, crossCutting, stats, llmOpts) { const subList = subsystems.slice(0, 20).map(s => `- ${s.name} (${s.kind}): ${s.entities.functions} functions, ${s.files.length} files` ).join('\n'); const prompt = `Write a 4-6 sentence architecture overview for this software system. Total subsystems: ${subsystems.length} Cross-cutting concerns: ${crossCutting.join(', ') || 'none detected'} Largest subsystems: ${subList} Write ONLY the overview paragraph, no heading. Describe the high-level architecture, the role of cross-cutting concerns, and the overall system organization.`; return callLLM(prompt, { ...llmOpts, maxTokens: 512 }); } module.exports = { callLLM, describeSubsystem, describeFlow, describeContract, describeArchitecture };