/** * VL Code Generation Pipeline – multi-agent orchestration * * Implements the 8-agent pipeline from VL_FullStack_CodeGen: * Agent-100: User Req → PRD.json * Agent-200: PRD → Database (.vdb) * Agent-300: PRD + DB → ServiceMap.json * Agent-400: PRD + ServiceMap → UIMap.json * Agent-500: UIMap.components[] → .cp files (parallel) * Agent-600: ServiceMap.serviceDomains[] → .vs files (parallel) * Agent-700: UIMap.sections[] → .sc files (parallel) * Agent-800: UIMap.apps[] → .vx files (parallel) * * Each stage uses Claude as the LLM with VL-specific system prompts. * Stages 5-8 run in parallel per item within each stage. */ import { createProvider } from '../core/llm-provider.js'; import { VL_VERSION, VL_VERSION_HEADER } from '../data/versions.js'; import fs from 'fs/promises'; import path from 'path'; const STAGES = [ { id: 'prd', agent: 'Agent-100', title: 'PRD Analysis', output: 'Process/Artifacts/PRD.json' }, { id: 'database', agent: 'Agent-200', title: 'Database Generation', output: 'Database/{project}.vdb' }, { id: 'serviceMap', agent: 'Agent-300', title: 'Service Map', output: 'Process/Artifacts/ServiceMap.json' }, { id: 'uiMap', agent: 'Agent-400', title: 'UI Map', output: 'Process/Artifacts/UIMap.json' }, { id: 'components', agent: 'Agent-500', title: 'Components (.cp)', output: 'ExtComponents/{name}.cp', parallel: true }, { id: 'services', agent: 'Agent-600', title: 'Services (.vs)', output: 'Services/{name}.vs', parallel: true }, { id: 'sections', agent: 'Agent-700', title: 'Sections (.sc)', output: 'Sections/{name}.sc', parallel: true }, { id: 'apps', agent: 'Agent-800', title: 'Apps (.vx)', output: 'Apps/{name}.vx', parallel: true }, ]; export class GenerationPipeline { constructor(config) { this.config = config; this.client = createProvider(config); this.vars = {}; // Pipeline variables: $prdJson, $vdbContent, etc. } /** * Run the full generation pipeline * @param {Object} params - { userRequest, targetLang, mode } * @param {Object} callbacks - { onStepStart, onStepDone, onFileWritten, onToken, onDone, onError } */ async run(params, callbacks = {}) { const { userRequest, targetLang = 'en', mode = 'create', inlineTheme = false } = params; this.config.inlineTheme = inlineTheme; const cb = callbacks; const filesWritten = []; try { // Stage 1: PRD cb.onStepStart?.({ id: 'prd', title: 'Generating PRD...' }); const prdResult = await this.callLLM( this.buildPRDPrompt(userRequest, targetLang), { json: true, stream: true, onToken: cb.onToken } ); this.vars.$prdJson = JSON.parse(prdResult); this.vars.$prdJsonStr = prdResult; const prdPath = await this.writeArtifact('Process/Artifacts/PRD.json', prdResult); filesWritten.push(prdPath); cb.onStepDone?.({ id: 'prd', title: 'PRD complete' }); cb.onFileWritten?.(prdPath); // Stage 2: Database cb.onStepStart?.({ id: 'database', title: 'Generating database schema...' }); const dbResult = await this.callLLM( this.buildDatabasePrompt(), { stream: true, onToken: cb.onToken } ); this.vars.$vdbContent = dbResult; const projectName = this.vars.$prdJson?.projectName || 'Project'; const dbPath = await this.writeArtifact(`Database/${projectName}.vdb`, dbResult); filesWritten.push(dbPath); cb.onStepDone?.({ id: 'database', title: 'Database complete' }); cb.onFileWritten?.(dbPath); // Stage 3: ServiceMap cb.onStepStart?.({ id: 'serviceMap', title: 'Generating service contracts...' }); const smResult = await this.callLLM( this.buildServiceMapPrompt(), { json: true, stream: true, onToken: cb.onToken } ); this.vars.$serviceMapJson = JSON.parse(smResult); this.vars.$serviceMapJsonStr = smResult; const smPath = await this.writeArtifact('Process/Artifacts/ServiceMap.json', smResult); filesWritten.push(smPath); cb.onStepDone?.({ id: 'serviceMap', title: 'ServiceMap complete' }); cb.onFileWritten?.(smPath); // Stage 4: UIMap cb.onStepStart?.({ id: 'uiMap', title: 'Generating UI architecture...' }); const uiResult = await this.callLLM( this.buildUIMapPrompt(), { json: true, stream: true, onToken: cb.onToken } ); this.vars.$uiMapJson = JSON.parse(uiResult); this.vars.$uiMapJsonStr = uiResult; const uiPath = await this.writeArtifact('Process/Artifacts/UIMap.json', uiResult); filesWritten.push(uiPath); cb.onStepDone?.({ id: 'uiMap', title: 'UIMap complete' }); cb.onFileWritten?.(uiPath); // Stage 5: Components (parallel) const components = this.vars.$uiMapJson?.components || []; if (components.length > 0) { cb.onStepStart?.({ id: 'components', title: `Generating ${components.length} components...` }); const cpPaths = await this.parallelGenerate( components, (item) => this.buildComponentPrompt(item), (item) => item.filePath || `ExtComponents/${item.componentId}.cp`, cb ); filesWritten.push(...cpPaths); cb.onStepDone?.({ id: 'components', title: `${components.length} components done` }); } // Stage 6: Services (parallel) const domains = this.vars.$serviceMapJson?.serviceDomains || []; if (domains.length > 0) { cb.onStepStart?.({ id: 'services', title: `Generating ${domains.length} service domains...` }); const vsPaths = await this.parallelGenerate( domains, (item) => this.buildServiceDomainPrompt(item), (item) => item.filePath || `Services/${item.domainId}.vs`, cb ); filesWritten.push(...vsPaths); cb.onStepDone?.({ id: 'services', title: `${domains.length} services done` }); } // Stage 7: Sections (parallel) const sections = this.vars.$uiMapJson?.sections || []; if (sections.length > 0) { cb.onStepStart?.({ id: 'sections', title: `Generating ${sections.length} sections...` }); const scPaths = await this.parallelGenerate( sections, (item) => this.buildSectionPrompt(item), (item) => item.filePath || `Sections/${item.sectionId}.sc`, cb ); filesWritten.push(...scPaths); cb.onStepDone?.({ id: 'sections', title: `${sections.length} sections done` }); } // Stage 8: Apps (parallel) const apps = this.vars.$uiMapJson?.apps || []; if (apps.length > 0) { cb.onStepStart?.({ id: 'apps', title: `Generating ${apps.length} apps...` }); const vxPaths = await this.parallelGenerate( apps, (item) => this.buildAppPrompt(item), (item) => item.filePath || `Apps/${item.appId}.vx`, cb ); filesWritten.push(...vxPaths); cb.onStepDone?.({ id: 'apps', title: `${apps.length} apps done` }); } // --- Extract ProjectMeta from generated auxiliary files --- try { const meta = this.extractProjectMeta(); if (meta) { const metaPath = await this.writeArtifact('.vl-code/ProjectMeta.json', JSON.stringify(meta, null, 2)); cb.onMetadataReady?.(meta); } } catch { /* non-critical */ } cb.onDone?.({ filesWritten, stageCount: 8 }); } catch (err) { cb.onError?.(err.message); throw err; } } /** Run a batch of items in parallel through LLM */ async parallelGenerate(items, promptFn, pathFn, cb) { const CONCURRENCY = 3; const paths = []; for (let i = 0; i < items.length; i += CONCURRENCY) { const batch = items.slice(i, i + CONCURRENCY); const results = await Promise.all( batch.map(async (item) => { const prompt = promptFn(item); const result = await this.callLLM(prompt, { stream: false }); const filePath = pathFn(item); await this.writeArtifact(filePath, result); cb.onFileWritten?.(filePath); return filePath; }) ); paths.push(...results); } return paths; } /** Call Claude LLM */ async callLLM(messages, opts = {}) { const jsonRule = opts.json ? '- CRITICAL: Return ONLY raw JSON. Do NOT wrap in ```json or ``` code fences. No markdown. Just the JSON object.' : '- Return VL source code only, no markdown wrapping.'; const themeRule = this.config.inlineTheme ? `- INLINE THEME MODE: Write ALL style properties (both skeleton AND skin) directly on components. Do NOT reference Theme tokens or .vth files. Use literal CSS values (e.g. color:#2563EB, background-color:#F9FAFB). Include hover/active/disabled states via StateStyle with literal values. No Theme file will be created.` : '- Theme tokens use -- prefix (e.g. --colorBrandPrimary)'; const systemPrompt = `You are a VL language expert code generator. You generate code strictly following VL ${VL_VERSION} syntax. Rules: - Always start files with ${VL_VERSION_HEADER} - Use dash (-) indentation, NOT spaces - PascalCase for types/files/components, camelCase for variables/methods - No escape characters (\\), no semicolons except in FOR loops ${themeRule} ${jsonRule}`; const params = { model: this.config.model, max_tokens: 16000, system: systemPrompt, messages: Array.isArray(messages) ? messages : [{ role: 'user', content: messages }], }; let result; if (opts.stream && opts.onToken) { // Streaming mode const stream = this.client.messages.stream(params); let fullText = ''; for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta?.text) { fullText += event.delta.text; opts.onToken(event.delta.text); } } result = fullText; } else { const response = await this.client.messages.create(params); result = response.content[0]?.text || ''; } // Strip markdown code fences if present (common LLM mistake) if (opts.json) { result = this.stripCodeFences(result); } return result; } /** Strip markdown code fences from LLM output */ stripCodeFences(text) { let cleaned = text.trim(); // Remove ```json ... ``` or ``` ... ``` cleaned = cleaned.replace(/^\s*```(?:json|JSON)?\s*\n?/, ''); cleaned = cleaned.replace(/\n?\s*```\s*$/, ''); return cleaned.trim(); } /** Write a file to the project directory */ async writeArtifact(relativePath, content) { const fullPath = path.resolve(this.config.workDir, relativePath); await fs.mkdir(path.dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content, 'utf-8'); return relativePath; } /** * Extract ProjectMeta/1.0 from generated auxiliary files (PRD, ServiceMap, UIMap) * Called after stages 1-4 complete. Returns structured metadata for visualization. */ extractProjectMeta() { const prd = this.vars.$prdJson; const sm = this.vars.$serviceMapJson; const ui = this.vars.$uiMapJson; if (!prd) return null; const meta = { $schema: 'VL-ProjectMeta/3.0', projectName: prd.projectName || 'Unknown', projectDescription: prd.summary || prd.description || '', vlVersion: VL_VERSION, config: { deviceTarget: prd.deviceTarget || 'web', vlVersion: VL_VERSION, }, apps: [], sections: [], components: [], services: [], dataSchema: { tables: [], relations: [] }, theme: this.config.inlineTheme ? null : { id: 'Theme', name: 'Theme', filePath: 'Theme/Theme.vth', vlVersion: VL_VERSION, meta: {}, slots: {}, }, }; // Extract apps and pages from PRD if (prd.apps) { for (const app of prd.apps) { const appEntry = { id: app.appId || app.id || app.name, filePath: `Apps/${app.appId || app.name}.vx`, pages: [], }; if (app.pages) { for (const page of app.pages) { appEntry.pages.push({ id: page.pageId || page.id || page.name, sections: (page.sections || []).map(s => typeof s === 'string' ? s : s.sectionId || s), }); } } meta.apps.push(appEntry); } } // Extract sections from UIMap if (ui?.sections) { for (const sec of ui.sections) { meta.sections.push({ id: sec.sectionId || sec.id, filePath: sec.filePath || `Sections/${sec.sectionId || sec.id}.sc`, consumesServices: (sec.serviceBindings || sec.services || []).map((s) => { if (typeof s === 'string') return s; return s.serviceId || (s.domainId && s.method ? `${s.domainId}.${s.method}` : s.domainId) || null; }).filter(Boolean), usesComponents: (sec.components || []).map((c) => typeof c === 'string' ? c : c.componentId || c.id || null ).filter(Boolean), }); } } // Extract components from UIMap if (ui?.components) { for (const cp of ui.components) { meta.components.push({ id: cp.componentId || cp.id, filePath: cp.filePath || `ExtComponents/${cp.componentId || cp.id}.cp`, props: cp.props || {}, }); } } // Extract services from ServiceMap if (sm?.serviceDomains) { for (const domain of sm.serviceDomains) { const svcEntry = { domainId: domain.domainId || domain.id, filePath: domain.filePath || `Services/${domain.domainId || domain.id}.vs`, methods: (domain.services || []).map(s => ({ id: s.id || s.name || s.serviceId || null, params: s.input ? Object.keys(s.input) : [], returns: s.output ? Object.keys(s.output) : [], })), virtualTables: (domain.virtualTables || []).map(vt => ({ id: vt.vTableId || vt.id, source: vt.source || vt.sourceTable, fields: (vt.fields || []).map(f => typeof f === 'string' ? f : f.name || f.field), })), }; meta.services.push(svcEntry); } } // Extract tables from PRD dataModel or DB schema if (prd.dataModel?.entities) { for (const entity of prd.dataModel.entities) { meta.dataSchema.tables.push({ id: entity.entityId || entity.name || entity.id, fields: (entity.fields || []).map(f => typeof f === 'string' ? f : f.name || f.field), }); } if (prd.dataModel.relations) { meta.dataSchema.relations = prd.dataModel.relations; } } return meta; } // --- Prompt builders --- buildPRDPrompt(userRequest, targetLang) { return `Analyze the following user requirement and generate a complete PRD.json: User Requirement: ${userRequest} Target Language: ${targetLang} Generate a complete PRD.json following the VL auxiliary file spec. Include: projectName, deviceTarget, roles, apps (with pages and sections), dataModel (entities and relations), valueDomains (enums and constants), and wiringPlan (interactions). Return minified JSON.`; } buildDatabasePrompt() { return [ { role: 'user', content: `Based on this PRD, generate the VL database schema (.vdb file):\n\n${this.vars.$prdJsonStr}` }, { role: 'user', content: 'Generate a complete .vdb file with tables, fields, indexes, and relations. Start with ${VL_VERSION_HEADER}' }, ]; } buildServiceMapPrompt() { return [ { role: 'user', content: `PRD:\n${this.vars.$prdJsonStr}\n\nDatabase:\n${this.vars.$vdbContent}` }, { role: 'user', content: 'Generate the ServiceMap.json with complete service domain contracts, virtual tables, input/output params, and query plans. Return minified JSON.' }, ]; } buildUIMapPrompt() { return [ { role: 'user', content: `PRD:\n${this.vars.$prdJsonStr}\n\nServiceMap:\n${this.vars.$serviceMapJsonStr}` }, { role: 'user', content: 'Generate the UIMap.json with complete app definitions, layout trees, section/component definitions, and page bindings. Return minified JSON.' }, ]; } buildComponentPrompt(item) { const themeCtx = this.config.inlineTheme ? 'INLINE THEME MODE: Write all skin properties (color, background-color, border-color, box-shadow, opacity, border-radius) directly on components using literal CSS values. No Theme tokens.' : `Theme file available.`; return [ { role: 'user', content: `UIMap component spec:\n${JSON.stringify(item)}` }, { role: 'user', content: `${themeCtx} ServiceMap:\n${this.vars.$serviceMapJsonStr?.substring(0, 2000)}` }, { role: 'user', content: 'Generate the complete .cp file for this component. Follow VL ${VL_VERSION} syntax strictly.' }, ]; } buildServiceDomainPrompt(item) { return [ { role: 'user', content: `ServiceMap domain spec:\n${JSON.stringify(item)}` }, { role: 'user', content: `Database:\n${this.vars.$vdbContent?.substring(0, 3000)}` }, { role: 'user', content: 'Generate the complete .vs file. Follow VL ${VL_VERSION} syntax strictly.' }, ]; } buildSectionPrompt(item) { const themeNote = this.config.inlineTheme ? ' INLINE THEME MODE: Write all skin properties directly using literal CSS values. No Theme tokens.' : ''; return [ { role: 'user', content: `UIMap section spec:\n${JSON.stringify(item)}` }, { role: 'user', content: `ServiceMap:\n${this.vars.$serviceMapJsonStr?.substring(0, 3000)}${themeNote}` }, { role: 'user', content: 'Generate the complete .sc file. Follow VL ${VL_VERSION} syntax strictly.' }, ]; } buildAppPrompt(item) { return [ { role: 'user', content: `UIMap app spec:\n${JSON.stringify(item)}` }, { role: 'user', content: `All sections: ${this.vars.$uiMapJson?.sections?.map(s => s.sectionId).join(', ')}` }, { role: 'user', content: 'Generate the complete .vx app entry file. Follow VL ${VL_VERSION} syntax strictly.' }, ]; } }