| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- /**
- * 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.' },
- ];
- }
- }
|