generation-pipeline.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. /**
  2. * VL Code Generation Pipeline – multi-agent orchestration
  3. *
  4. * Implements the 8-agent pipeline from VL_FullStack_CodeGen:
  5. * Agent-100: User Req → PRD.json
  6. * Agent-200: PRD → Database (.vdb)
  7. * Agent-300: PRD + DB → ServiceMap.json
  8. * Agent-400: PRD + ServiceMap → UIMap.json
  9. * Agent-500: UIMap.components[] → .cp files (parallel)
  10. * Agent-600: ServiceMap.serviceDomains[] → .vs files (parallel)
  11. * Agent-700: UIMap.sections[] → .sc files (parallel)
  12. * Agent-800: UIMap.apps[] → .vx files (parallel)
  13. *
  14. * Each stage uses Claude as the LLM with VL-specific system prompts.
  15. * Stages 5-8 run in parallel per item within each stage.
  16. */
  17. import { createProvider } from '../core/llm-provider.js';
  18. import { VL_VERSION, VL_VERSION_HEADER } from '../data/versions.js';
  19. import fs from 'fs/promises';
  20. import path from 'path';
  21. const STAGES = [
  22. { id: 'prd', agent: 'Agent-100', title: 'PRD Analysis', output: 'Process/Artifacts/PRD.json' },
  23. { id: 'database', agent: 'Agent-200', title: 'Database Generation', output: 'Database/{project}.vdb' },
  24. { id: 'serviceMap', agent: 'Agent-300', title: 'Service Map', output: 'Process/Artifacts/ServiceMap.json' },
  25. { id: 'uiMap', agent: 'Agent-400', title: 'UI Map', output: 'Process/Artifacts/UIMap.json' },
  26. { id: 'components', agent: 'Agent-500', title: 'Components (.cp)', output: 'ExtComponents/{name}.cp', parallel: true },
  27. { id: 'services', agent: 'Agent-600', title: 'Services (.vs)', output: 'Services/{name}.vs', parallel: true },
  28. { id: 'sections', agent: 'Agent-700', title: 'Sections (.sc)', output: 'Sections/{name}.sc', parallel: true },
  29. { id: 'apps', agent: 'Agent-800', title: 'Apps (.vx)', output: 'Apps/{name}.vx', parallel: true },
  30. ];
  31. export class GenerationPipeline {
  32. constructor(config) {
  33. this.config = config;
  34. this.client = createProvider(config);
  35. this.vars = {}; // Pipeline variables: $prdJson, $vdbContent, etc.
  36. }
  37. /**
  38. * Run the full generation pipeline
  39. * @param {Object} params - { userRequest, targetLang, mode }
  40. * @param {Object} callbacks - { onStepStart, onStepDone, onFileWritten, onToken, onDone, onError }
  41. */
  42. async run(params, callbacks = {}) {
  43. const { userRequest, targetLang = 'en', mode = 'create', inlineTheme = false } = params;
  44. this.config.inlineTheme = inlineTheme;
  45. const cb = callbacks;
  46. const filesWritten = [];
  47. try {
  48. // Stage 1: PRD
  49. cb.onStepStart?.({ id: 'prd', title: 'Generating PRD...' });
  50. const prdResult = await this.callLLM(
  51. this.buildPRDPrompt(userRequest, targetLang),
  52. { json: true, stream: true, onToken: cb.onToken }
  53. );
  54. this.vars.$prdJson = JSON.parse(prdResult);
  55. this.vars.$prdJsonStr = prdResult;
  56. const prdPath = await this.writeArtifact('Process/Artifacts/PRD.json', prdResult);
  57. filesWritten.push(prdPath);
  58. cb.onStepDone?.({ id: 'prd', title: 'PRD complete' });
  59. cb.onFileWritten?.(prdPath);
  60. // Stage 2: Database
  61. cb.onStepStart?.({ id: 'database', title: 'Generating database schema...' });
  62. const dbResult = await this.callLLM(
  63. this.buildDatabasePrompt(),
  64. { stream: true, onToken: cb.onToken }
  65. );
  66. this.vars.$vdbContent = dbResult;
  67. const projectName = this.vars.$prdJson?.projectName || 'Project';
  68. const dbPath = await this.writeArtifact(`Database/${projectName}.vdb`, dbResult);
  69. filesWritten.push(dbPath);
  70. cb.onStepDone?.({ id: 'database', title: 'Database complete' });
  71. cb.onFileWritten?.(dbPath);
  72. // Stage 3: ServiceMap
  73. cb.onStepStart?.({ id: 'serviceMap', title: 'Generating service contracts...' });
  74. const smResult = await this.callLLM(
  75. this.buildServiceMapPrompt(),
  76. { json: true, stream: true, onToken: cb.onToken }
  77. );
  78. this.vars.$serviceMapJson = JSON.parse(smResult);
  79. this.vars.$serviceMapJsonStr = smResult;
  80. const smPath = await this.writeArtifact('Process/Artifacts/ServiceMap.json', smResult);
  81. filesWritten.push(smPath);
  82. cb.onStepDone?.({ id: 'serviceMap', title: 'ServiceMap complete' });
  83. cb.onFileWritten?.(smPath);
  84. // Stage 4: UIMap
  85. cb.onStepStart?.({ id: 'uiMap', title: 'Generating UI architecture...' });
  86. const uiResult = await this.callLLM(
  87. this.buildUIMapPrompt(),
  88. { json: true, stream: true, onToken: cb.onToken }
  89. );
  90. this.vars.$uiMapJson = JSON.parse(uiResult);
  91. this.vars.$uiMapJsonStr = uiResult;
  92. const uiPath = await this.writeArtifact('Process/Artifacts/UIMap.json', uiResult);
  93. filesWritten.push(uiPath);
  94. cb.onStepDone?.({ id: 'uiMap', title: 'UIMap complete' });
  95. cb.onFileWritten?.(uiPath);
  96. // Stage 5: Components (parallel)
  97. const components = this.vars.$uiMapJson?.components || [];
  98. if (components.length > 0) {
  99. cb.onStepStart?.({ id: 'components', title: `Generating ${components.length} components...` });
  100. const cpPaths = await this.parallelGenerate(
  101. components,
  102. (item) => this.buildComponentPrompt(item),
  103. (item) => item.filePath || `ExtComponents/${item.componentId}.cp`,
  104. cb
  105. );
  106. filesWritten.push(...cpPaths);
  107. cb.onStepDone?.({ id: 'components', title: `${components.length} components done` });
  108. }
  109. // Stage 6: Services (parallel)
  110. const domains = this.vars.$serviceMapJson?.serviceDomains || [];
  111. if (domains.length > 0) {
  112. cb.onStepStart?.({ id: 'services', title: `Generating ${domains.length} service domains...` });
  113. const vsPaths = await this.parallelGenerate(
  114. domains,
  115. (item) => this.buildServiceDomainPrompt(item),
  116. (item) => item.filePath || `Services/${item.domainId}.vs`,
  117. cb
  118. );
  119. filesWritten.push(...vsPaths);
  120. cb.onStepDone?.({ id: 'services', title: `${domains.length} services done` });
  121. }
  122. // Stage 7: Sections (parallel)
  123. const sections = this.vars.$uiMapJson?.sections || [];
  124. if (sections.length > 0) {
  125. cb.onStepStart?.({ id: 'sections', title: `Generating ${sections.length} sections...` });
  126. const scPaths = await this.parallelGenerate(
  127. sections,
  128. (item) => this.buildSectionPrompt(item),
  129. (item) => item.filePath || `Sections/${item.sectionId}.sc`,
  130. cb
  131. );
  132. filesWritten.push(...scPaths);
  133. cb.onStepDone?.({ id: 'sections', title: `${sections.length} sections done` });
  134. }
  135. // Stage 8: Apps (parallel)
  136. const apps = this.vars.$uiMapJson?.apps || [];
  137. if (apps.length > 0) {
  138. cb.onStepStart?.({ id: 'apps', title: `Generating ${apps.length} apps...` });
  139. const vxPaths = await this.parallelGenerate(
  140. apps,
  141. (item) => this.buildAppPrompt(item),
  142. (item) => item.filePath || `Apps/${item.appId}.vx`,
  143. cb
  144. );
  145. filesWritten.push(...vxPaths);
  146. cb.onStepDone?.({ id: 'apps', title: `${apps.length} apps done` });
  147. }
  148. // --- Extract ProjectMeta from generated auxiliary files ---
  149. try {
  150. const meta = this.extractProjectMeta();
  151. if (meta) {
  152. const metaPath = await this.writeArtifact('.vl-code/ProjectMeta.json', JSON.stringify(meta, null, 2));
  153. cb.onMetadataReady?.(meta);
  154. }
  155. } catch { /* non-critical */ }
  156. cb.onDone?.({ filesWritten, stageCount: 8 });
  157. } catch (err) {
  158. cb.onError?.(err.message);
  159. throw err;
  160. }
  161. }
  162. /** Run a batch of items in parallel through LLM */
  163. async parallelGenerate(items, promptFn, pathFn, cb) {
  164. const CONCURRENCY = 3;
  165. const paths = [];
  166. for (let i = 0; i < items.length; i += CONCURRENCY) {
  167. const batch = items.slice(i, i + CONCURRENCY);
  168. const results = await Promise.all(
  169. batch.map(async (item) => {
  170. const prompt = promptFn(item);
  171. const result = await this.callLLM(prompt, { stream: false });
  172. const filePath = pathFn(item);
  173. await this.writeArtifact(filePath, result);
  174. cb.onFileWritten?.(filePath);
  175. return filePath;
  176. })
  177. );
  178. paths.push(...results);
  179. }
  180. return paths;
  181. }
  182. /** Call Claude LLM */
  183. async callLLM(messages, opts = {}) {
  184. const jsonRule = opts.json
  185. ? '- CRITICAL: Return ONLY raw JSON. Do NOT wrap in ```json or ``` code fences. No markdown. Just the JSON object.'
  186. : '- Return VL source code only, no markdown wrapping.';
  187. const themeRule = this.config.inlineTheme
  188. ? `- 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.`
  189. : '- Theme tokens use -- prefix (e.g. --colorBrandPrimary)';
  190. const systemPrompt = `You are a VL language expert code generator.
  191. You generate code strictly following VL ${VL_VERSION} syntax.
  192. Rules:
  193. - Always start files with ${VL_VERSION_HEADER}
  194. - Use dash (-) indentation, NOT spaces
  195. - PascalCase for types/files/components, camelCase for variables/methods
  196. - No escape characters (\\), no semicolons except in FOR loops
  197. ${themeRule}
  198. ${jsonRule}`;
  199. const params = {
  200. model: this.config.model,
  201. max_tokens: 16000,
  202. system: systemPrompt,
  203. messages: Array.isArray(messages) ? messages : [{ role: 'user', content: messages }],
  204. };
  205. let result;
  206. if (opts.stream && opts.onToken) {
  207. // Streaming mode
  208. const stream = this.client.messages.stream(params);
  209. let fullText = '';
  210. for await (const event of stream) {
  211. if (event.type === 'content_block_delta' && event.delta?.text) {
  212. fullText += event.delta.text;
  213. opts.onToken(event.delta.text);
  214. }
  215. }
  216. result = fullText;
  217. } else {
  218. const response = await this.client.messages.create(params);
  219. result = response.content[0]?.text || '';
  220. }
  221. // Strip markdown code fences if present (common LLM mistake)
  222. if (opts.json) {
  223. result = this.stripCodeFences(result);
  224. }
  225. return result;
  226. }
  227. /** Strip markdown code fences from LLM output */
  228. stripCodeFences(text) {
  229. let cleaned = text.trim();
  230. // Remove ```json ... ``` or ``` ... ```
  231. cleaned = cleaned.replace(/^\s*```(?:json|JSON)?\s*\n?/, '');
  232. cleaned = cleaned.replace(/\n?\s*```\s*$/, '');
  233. return cleaned.trim();
  234. }
  235. /** Write a file to the project directory */
  236. async writeArtifact(relativePath, content) {
  237. const fullPath = path.resolve(this.config.workDir, relativePath);
  238. await fs.mkdir(path.dirname(fullPath), { recursive: true });
  239. await fs.writeFile(fullPath, content, 'utf-8');
  240. return relativePath;
  241. }
  242. /**
  243. * Extract ProjectMeta/1.0 from generated auxiliary files (PRD, ServiceMap, UIMap)
  244. * Called after stages 1-4 complete. Returns structured metadata for visualization.
  245. */
  246. extractProjectMeta() {
  247. const prd = this.vars.$prdJson;
  248. const sm = this.vars.$serviceMapJson;
  249. const ui = this.vars.$uiMapJson;
  250. if (!prd) return null;
  251. const meta = {
  252. $schema: 'VL-ProjectMeta/3.0',
  253. projectName: prd.projectName || 'Unknown',
  254. projectDescription: prd.summary || prd.description || '',
  255. vlVersion: VL_VERSION,
  256. config: {
  257. deviceTarget: prd.deviceTarget || 'web',
  258. vlVersion: VL_VERSION,
  259. },
  260. apps: [],
  261. sections: [],
  262. components: [],
  263. services: [],
  264. dataSchema: { tables: [], relations: [] },
  265. theme: this.config.inlineTheme ? null : {
  266. id: 'Theme',
  267. name: 'Theme',
  268. filePath: 'Theme/Theme.vth',
  269. vlVersion: VL_VERSION,
  270. meta: {},
  271. slots: {},
  272. },
  273. };
  274. // Extract apps and pages from PRD
  275. if (prd.apps) {
  276. for (const app of prd.apps) {
  277. const appEntry = {
  278. id: app.appId || app.id || app.name,
  279. filePath: `Apps/${app.appId || app.name}.vx`,
  280. pages: [],
  281. };
  282. if (app.pages) {
  283. for (const page of app.pages) {
  284. appEntry.pages.push({
  285. id: page.pageId || page.id || page.name,
  286. sections: (page.sections || []).map(s => typeof s === 'string' ? s : s.sectionId || s),
  287. });
  288. }
  289. }
  290. meta.apps.push(appEntry);
  291. }
  292. }
  293. // Extract sections from UIMap
  294. if (ui?.sections) {
  295. for (const sec of ui.sections) {
  296. meta.sections.push({
  297. id: sec.sectionId || sec.id,
  298. filePath: sec.filePath || `Sections/${sec.sectionId || sec.id}.sc`,
  299. consumesServices: (sec.serviceBindings || sec.services || []).map((s) => {
  300. if (typeof s === 'string') return s;
  301. return s.serviceId || (s.domainId && s.method ? `${s.domainId}.${s.method}` : s.domainId) || null;
  302. }).filter(Boolean),
  303. usesComponents: (sec.components || []).map((c) =>
  304. typeof c === 'string' ? c : c.componentId || c.id || null
  305. ).filter(Boolean),
  306. });
  307. }
  308. }
  309. // Extract components from UIMap
  310. if (ui?.components) {
  311. for (const cp of ui.components) {
  312. meta.components.push({
  313. id: cp.componentId || cp.id,
  314. filePath: cp.filePath || `ExtComponents/${cp.componentId || cp.id}.cp`,
  315. props: cp.props || {},
  316. });
  317. }
  318. }
  319. // Extract services from ServiceMap
  320. if (sm?.serviceDomains) {
  321. for (const domain of sm.serviceDomains) {
  322. const svcEntry = {
  323. domainId: domain.domainId || domain.id,
  324. filePath: domain.filePath || `Services/${domain.domainId || domain.id}.vs`,
  325. methods: (domain.services || []).map(s => ({
  326. id: s.id || s.name || s.serviceId || null,
  327. params: s.input ? Object.keys(s.input) : [],
  328. returns: s.output ? Object.keys(s.output) : [],
  329. })),
  330. virtualTables: (domain.virtualTables || []).map(vt => ({
  331. id: vt.vTableId || vt.id,
  332. source: vt.source || vt.sourceTable,
  333. fields: (vt.fields || []).map(f => typeof f === 'string' ? f : f.name || f.field),
  334. })),
  335. };
  336. meta.services.push(svcEntry);
  337. }
  338. }
  339. // Extract tables from PRD dataModel or DB schema
  340. if (prd.dataModel?.entities) {
  341. for (const entity of prd.dataModel.entities) {
  342. meta.dataSchema.tables.push({
  343. id: entity.entityId || entity.name || entity.id,
  344. fields: (entity.fields || []).map(f => typeof f === 'string' ? f : f.name || f.field),
  345. });
  346. }
  347. if (prd.dataModel.relations) {
  348. meta.dataSchema.relations = prd.dataModel.relations;
  349. }
  350. }
  351. return meta;
  352. }
  353. // --- Prompt builders ---
  354. buildPRDPrompt(userRequest, targetLang) {
  355. return `Analyze the following user requirement and generate a complete PRD.json:
  356. User Requirement:
  357. ${userRequest}
  358. Target Language: ${targetLang}
  359. Generate a complete PRD.json following the VL auxiliary file spec.
  360. Include: projectName, deviceTarget, roles, apps (with pages and sections), dataModel (entities and relations), valueDomains (enums and constants), and wiringPlan (interactions).
  361. Return minified JSON.`;
  362. }
  363. buildDatabasePrompt() {
  364. return [
  365. { role: 'user', content: `Based on this PRD, generate the VL database schema (.vdb file):\n\n${this.vars.$prdJsonStr}` },
  366. { role: 'user', content: 'Generate a complete .vdb file with tables, fields, indexes, and relations. Start with ${VL_VERSION_HEADER}' },
  367. ];
  368. }
  369. buildServiceMapPrompt() {
  370. return [
  371. { role: 'user', content: `PRD:\n${this.vars.$prdJsonStr}\n\nDatabase:\n${this.vars.$vdbContent}` },
  372. { role: 'user', content: 'Generate the ServiceMap.json with complete service domain contracts, virtual tables, input/output params, and query plans. Return minified JSON.' },
  373. ];
  374. }
  375. buildUIMapPrompt() {
  376. return [
  377. { role: 'user', content: `PRD:\n${this.vars.$prdJsonStr}\n\nServiceMap:\n${this.vars.$serviceMapJsonStr}` },
  378. { role: 'user', content: 'Generate the UIMap.json with complete app definitions, layout trees, section/component definitions, and page bindings. Return minified JSON.' },
  379. ];
  380. }
  381. buildComponentPrompt(item) {
  382. const themeCtx = this.config.inlineTheme
  383. ? '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.'
  384. : `Theme file available.`;
  385. return [
  386. { role: 'user', content: `UIMap component spec:\n${JSON.stringify(item)}` },
  387. { role: 'user', content: `${themeCtx} ServiceMap:\n${this.vars.$serviceMapJsonStr?.substring(0, 2000)}` },
  388. { role: 'user', content: 'Generate the complete .cp file for this component. Follow VL ${VL_VERSION} syntax strictly.' },
  389. ];
  390. }
  391. buildServiceDomainPrompt(item) {
  392. return [
  393. { role: 'user', content: `ServiceMap domain spec:\n${JSON.stringify(item)}` },
  394. { role: 'user', content: `Database:\n${this.vars.$vdbContent?.substring(0, 3000)}` },
  395. { role: 'user', content: 'Generate the complete .vs file. Follow VL ${VL_VERSION} syntax strictly.' },
  396. ];
  397. }
  398. buildSectionPrompt(item) {
  399. const themeNote = this.config.inlineTheme
  400. ? ' INLINE THEME MODE: Write all skin properties directly using literal CSS values. No Theme tokens.'
  401. : '';
  402. return [
  403. { role: 'user', content: `UIMap section spec:\n${JSON.stringify(item)}` },
  404. { role: 'user', content: `ServiceMap:\n${this.vars.$serviceMapJsonStr?.substring(0, 3000)}${themeNote}` },
  405. { role: 'user', content: 'Generate the complete .sc file. Follow VL ${VL_VERSION} syntax strictly.' },
  406. ];
  407. }
  408. buildAppPrompt(item) {
  409. return [
  410. { role: 'user', content: `UIMap app spec:\n${JSON.stringify(item)}` },
  411. { role: 'user', content: `All sections: ${this.vars.$uiMapJson?.sections?.map(s => s.sectionId).join(', ')}` },
  412. { role: 'user', content: 'Generate the complete .vx app entry file. Follow VL ${VL_VERSION} syntax strictly.' },
  413. ];
  414. }
  415. }