benchmark-vl-native-generation.js 77 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495
  1. #!/usr/bin/env node
  2. import fs from 'fs/promises';
  3. import fsSync from 'fs';
  4. import os from 'os';
  5. import path from 'path';
  6. import { execSync, spawn } from 'child_process';
  7. import { performance } from 'perf_hooks';
  8. import { chromium } from 'playwright';
  9. import { PARSEVL_URL } from '../src/data/versions.js';
  10. import { getCookie } from '../src/server/helpers.js';
  11. import { WorkflowExecutor } from '../src/vl/workflow-executor.js';
  12. import { VLProjectContext } from '../src/vl/project-context.js';
  13. import { createVLValidateTool } from '../src/tools/vl-validate.js';
  14. import { extractFromFileTree, validateMeta } from '../src/vl/metadata-extractor.js';
  15. const MODEL = process.env.VL_CODE_MODEL || 'claude-opus-4-6';
  16. const REPORT_MODEL = process.env.VL_REPORT_MODEL || MODEL;
  17. const TARGET_LANG = 'zh-CN';
  18. const TODAY = new Date().toISOString().slice(0, 10);
  19. const DATE_SLUG = TODAY.replace(/-/g, '');
  20. const TEST_ROOT = path.join(os.homedir(), 'Documents', 'VLProjects', '_tests');
  21. const REPORT_DIR = path.join(process.cwd(), 'docs', 'benchmarks');
  22. const DOCCENTER_BASE = 'https://v4pre.visuallogic.ai/api/12027022';
  23. const METHOD_ORDER = ['direct-full', 'meta-first', '3-file', '6-file', '9-file'];
  24. const DIRECT_FILE_CONCURRENCY = Number(process.env.VL_FILE_CONCURRENCY || '6');
  25. const METHOD_LABELS = {
  26. 'direct-full': 'Direct Full',
  27. 'meta-first': 'Meta First',
  28. '3-file': 'Workflow 3-File',
  29. '6-file': 'Workflow 6-File',
  30. '9-file': 'Workflow 9-File',
  31. };
  32. const WORKFLOW_DOC_IDS = {
  33. '3-file': 8,
  34. '6-file': 34,
  35. '9-file': 52,
  36. };
  37. const CONTRACT = {
  38. projectName: 'CampusOps',
  39. projectDescription: 'Desktop-first and mobile-friendly multi-campus operations cockpit for facilities dispatchers and supervisors.',
  40. databaseFile: 'Database/CampusOps.vdb',
  41. themeFile: 'Theme/Theme.vth',
  42. app: {
  43. id: 'CampusOpsApp',
  44. filePath: 'Apps/CampusOpsApp.vx',
  45. routes: [
  46. { path: 'overview', sectionId: 'OverviewPage' },
  47. { path: 'schedule', sectionId: 'ScheduleBoard' },
  48. { path: 'work-orders', sectionId: 'WorkOrderDesk' },
  49. { path: 'alerts', sectionId: 'AlertCenter' },
  50. { path: 'settings', sectionId: 'SettingsPage' },
  51. ],
  52. },
  53. tables: [
  54. {
  55. id: 'Campus',
  56. fields: [
  57. { name: 'name', type: 'STRING' },
  58. { name: 'region', type: 'STRING' },
  59. { name: 'manager', type: 'STRING' },
  60. { name: 'activeAlerts', type: 'INT' },
  61. ],
  62. },
  63. {
  64. id: 'Technician',
  65. fields: [
  66. { name: 'name', type: 'STRING' },
  67. { name: 'campusId', type: 'INT' },
  68. { name: 'skillTag', type: 'STRING' },
  69. { name: 'shiftStatus', type: 'STRING' },
  70. { name: 'utilizationRate', type: 'FLOAT' },
  71. ],
  72. },
  73. {
  74. id: 'WorkOrder',
  75. fields: [
  76. { name: 'campusId', type: 'INT' },
  77. { name: 'title', type: 'STRING' },
  78. { name: 'priority', type: 'STRING' },
  79. { name: 'status', type: 'STRING' },
  80. { name: 'assigneeId', type: 'INT' },
  81. { name: 'slaHours', type: 'INT' },
  82. ],
  83. },
  84. {
  85. id: 'AlertRule',
  86. fields: [
  87. { name: 'campusId', type: 'INT' },
  88. { name: 'ruleName', type: 'STRING' },
  89. { name: 'thresholdValue', type: 'FLOAT' },
  90. { name: 'enabled', type: 'BOOL' },
  91. ],
  92. },
  93. {
  94. id: 'AlertEvent',
  95. fields: [
  96. { name: 'campusId', type: 'INT' },
  97. { name: 'ruleId', type: 'INT' },
  98. { name: 'severity', type: 'STRING' },
  99. { name: 'status', type: 'STRING' },
  100. { name: 'message', type: 'STRING' },
  101. ],
  102. },
  103. {
  104. id: 'UserPreference',
  105. fields: [
  106. { name: 'density', type: 'STRING' },
  107. { name: 'defaultCampusId', type: 'INT' },
  108. { name: 'emailDigest', type: 'BOOL' },
  109. ],
  110. },
  111. ],
  112. services: [
  113. {
  114. domainId: 'OperationsOverview',
  115. filePath: 'Services/OperationsOverview.vs',
  116. purpose: 'dashboard KPIs and summary cards',
  117. methods: [
  118. { id: 'GetOverviewMetrics', params: 'campusId(INT)', returns: '{success:BOOL,data:OBJECT}' },
  119. ],
  120. },
  121. {
  122. domainId: 'ScheduleService',
  123. filePath: 'Services/ScheduleService.vs',
  124. purpose: 'technician assignment list and shift filters',
  125. methods: [
  126. { id: 'ListAssignments', params: 'campusId(INT),shiftStatus(STRING)', returns: '{success:BOOL,data:[{}]}' },
  127. ],
  128. },
  129. {
  130. domainId: 'WorkOrderService',
  131. filePath: 'Services/WorkOrderService.vs',
  132. purpose: 'work order list and status updates',
  133. methods: [
  134. { id: 'ListWorkOrders', params: 'campusId(INT),priority(STRING),status(STRING)', returns: '{success:BOOL,data:[{}]}' },
  135. { id: 'UpdateWorkOrderStatus', params: 'workOrderId(INT),status(STRING)', returns: '{success:BOOL}' },
  136. ],
  137. },
  138. {
  139. domainId: 'AlertService',
  140. filePath: 'Services/AlertService.vs',
  141. purpose: 'alert event list and acknowledgement',
  142. methods: [
  143. { id: 'ListAlerts', params: 'campusId(INT),severity(STRING),status(STRING)', returns: '{success:BOOL,data:[{}]}' },
  144. { id: 'AcknowledgeAlert', params: 'alertId(INT)', returns: '{success:BOOL}' },
  145. ],
  146. },
  147. {
  148. domainId: 'SettingsService',
  149. filePath: 'Services/SettingsService.vs',
  150. purpose: 'settings and threshold preferences',
  151. methods: [
  152. { id: 'GetSettings', params: 'campusId(INT)', returns: '{success:BOOL,data:OBJECT}' },
  153. { id: 'SaveSettings', params: 'campusId(INT),density(STRING),threshold(FLOAT)', returns: '{success:BOOL}' },
  154. ],
  155. },
  156. ],
  157. components: [
  158. {
  159. id: 'KpiCard',
  160. filePath: 'ExtComponents/KpiCard.cp',
  161. purpose: 'title, numeric value, helper text, optional intent tone',
  162. },
  163. {
  164. id: 'StatusPill',
  165. filePath: 'ExtComponents/StatusPill.cp',
  166. purpose: 'compact status chip for priority or lifecycle states',
  167. },
  168. {
  169. id: 'FilterToolbar',
  170. filePath: 'ExtComponents/FilterToolbar.cp',
  171. purpose: 'filter row with campus and status selectors plus clear action',
  172. },
  173. {
  174. id: 'AlertListItem',
  175. filePath: 'ExtComponents/AlertListItem.cp',
  176. purpose: 'alert row with severity, message, meta, and acknowledge button',
  177. },
  178. ],
  179. sections: [
  180. {
  181. id: 'OverviewPage',
  182. filePath: 'Sections/OverviewPage.sc',
  183. consumesServices: ['OperationsOverview.GetOverviewMetrics'],
  184. usesComponents: ['KpiCard'],
  185. purpose: 'overview dashboard with KPI cards and campus summary rows',
  186. },
  187. {
  188. id: 'ScheduleBoard',
  189. filePath: 'Sections/ScheduleBoard.sc',
  190. consumesServices: ['ScheduleService.ListAssignments'],
  191. usesComponents: ['FilterToolbar', 'StatusPill'],
  192. purpose: 'schedule and technician assignment table',
  193. },
  194. {
  195. id: 'WorkOrderDesk',
  196. filePath: 'Sections/WorkOrderDesk.sc',
  197. consumesServices: ['WorkOrderService.ListWorkOrders', 'WorkOrderService.UpdateWorkOrderStatus'],
  198. usesComponents: ['FilterToolbar', 'StatusPill'],
  199. purpose: 'work order list with status change action',
  200. },
  201. {
  202. id: 'AlertCenter',
  203. filePath: 'Sections/AlertCenter.sc',
  204. consumesServices: ['AlertService.ListAlerts', 'AlertService.AcknowledgeAlert'],
  205. usesComponents: ['FilterToolbar', 'AlertListItem'],
  206. purpose: 'alert center with severity list and acknowledgement actions',
  207. },
  208. {
  209. id: 'SettingsPage',
  210. filePath: 'Sections/SettingsPage.sc',
  211. consumesServices: ['SettingsService.GetSettings', 'SettingsService.SaveSettings'],
  212. usesComponents: ['StatusPill'],
  213. purpose: 'alert threshold and density settings form',
  214. },
  215. ],
  216. };
  217. const EXPECTED_PATHS = [
  218. CONTRACT.databaseFile,
  219. CONTRACT.themeFile,
  220. ...CONTRACT.services.map((item) => item.filePath),
  221. ...CONTRACT.components.map((item) => item.filePath),
  222. ...CONTRACT.sections.map((item) => item.filePath),
  223. CONTRACT.app.filePath,
  224. ];
  225. const REQUIREMENT = `
  226. Build a desktop-first and mobile-friendly operations cockpit called CampusOps for a multi-campus facilities team.
  227. Business scope:
  228. - Roles: dispatcher and supervisor.
  229. - Pages: overview, schedule, work-orders, alerts, settings.
  230. - Data entities: Campus, Technician, WorkOrder, AlertRule, AlertEvent, UserPreference.
  231. - Key interactions:
  232. - Overview shows KPI cards for open work orders, overdue SLA, active alerts, and technician utilization.
  233. - Schedule page lists technician assignments by campus and lets users filter by campus, technician, and shift status.
  234. - Work order page lists work orders with filters by campus, priority, and status, and supports changing the order status.
  235. - Alert center lists alert events with severity, source campus, and acknowledgement actions.
  236. - Settings page edits alert thresholds and dashboard density preferences.
  237. Design direction:
  238. - Theme should feel enterprise and operational: deep teal primary, slate surfaces, amber warning, red danger, soft elevated cards, pill filters.
  239. - Prefer card/list/table layouts instead of advanced chart widgets.
  240. - Keep interactions compile-safe and easy to preview.
  241. Exact naming contract:
  242. - Project name: CampusOps
  243. - Database file: Database/CampusOps.vdb
  244. - Theme file: Theme/Theme.vth
  245. - Service files:
  246. - Services/OperationsOverview.vs
  247. - Services/ScheduleService.vs
  248. - Services/WorkOrderService.vs
  249. - Services/AlertService.vs
  250. - Services/SettingsService.vs
  251. - Component files:
  252. - ExtComponents/KpiCard.cp
  253. - ExtComponents/StatusPill.cp
  254. - ExtComponents/FilterToolbar.cp
  255. - ExtComponents/AlertListItem.cp
  256. - Section files:
  257. - Sections/OverviewPage.sc
  258. - Sections/ScheduleBoard.sc
  259. - Sections/WorkOrderDesk.sc
  260. - Sections/AlertCenter.sc
  261. - Sections/SettingsPage.sc
  262. - App file: Apps/CampusOpsApp.vx
  263. Use the current latest VL syntax and current latest THEME 6.5.
  264. Output compile-safe code only.
  265. `.trim();
  266. function normalizeCookie(cookie) {
  267. if (!cookie) return '';
  268. return String(cookie).startsWith('ih5bearer=') ? String(cookie) : `ih5bearer=${cookie}`;
  269. }
  270. function projectNameWithFallback(baseName) {
  271. let candidate = baseName;
  272. let n = 2;
  273. while (fsSync.existsSync(path.join(TEST_ROOT, candidate))) {
  274. candidate = baseName.replace(/Test$/, `Run${n}Test`);
  275. n += 1;
  276. }
  277. return candidate;
  278. }
  279. async function ensureProjectScaffold(projectDir) {
  280. for (const rel of ['Apps', 'Sections', 'ExtComponents', 'Services', 'Database', 'Theme', 'Process', '.vl-code']) {
  281. await fs.mkdir(path.join(projectDir, rel), { recursive: true });
  282. }
  283. }
  284. async function fetchDocInfo(docId, cookie) {
  285. const res = await fetch(`${DOCCENTER_BASE}/SERVICE_DocCenter_GetDocById`, {
  286. method: 'POST',
  287. headers: {
  288. 'Content-Type': 'application/json',
  289. 'Cookie': normalizeCookie(cookie),
  290. },
  291. body: JSON.stringify({ docId }),
  292. });
  293. const data = await res.json();
  294. const doc = data?.data || {};
  295. return {
  296. docId,
  297. title: doc.name || '',
  298. updatedAt: doc._update || '',
  299. content: String(doc.currentContent || doc.content || ''),
  300. };
  301. }
  302. function stripCodeFences(text) {
  303. const trimmed = String(text || '').trim();
  304. const fence = trimmed.match(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/);
  305. return fence ? fence[1].trim() : trimmed;
  306. }
  307. async function runClaudePrompt(prompt, { systemPrompt = '', model = MODEL, timeoutMs = 10 * 60 * 1000 } = {}) {
  308. return await new Promise((resolve, reject) => {
  309. const args = ['--print', '--no-session-persistence', '--model', model, '--tools', ''];
  310. if (systemPrompt) args.push('--system-prompt', systemPrompt);
  311. const env = { ...process.env, NO_PROXY: 'localhost,127.0.0.1,::1' };
  312. delete env.CLAUDECODE;
  313. const proc = spawn('claude', args, {
  314. stdio: ['pipe', 'pipe', 'pipe'],
  315. env,
  316. });
  317. let stdout = '';
  318. let stderr = '';
  319. let finished = false;
  320. const timer = setTimeout(() => {
  321. proc.kill('SIGTERM');
  322. reject(new Error(`claude prompt timed out after ${Math.round(timeoutMs / 1000)}s`));
  323. }, timeoutMs);
  324. proc.stdout.on('data', (chunk) => {
  325. stdout += chunk.toString();
  326. });
  327. proc.stderr.on('data', (chunk) => {
  328. stderr += chunk.toString();
  329. });
  330. proc.on('error', (err) => {
  331. if (finished) return;
  332. finished = true;
  333. clearTimeout(timer);
  334. reject(err);
  335. });
  336. proc.on('close', (code) => {
  337. if (finished) return;
  338. finished = true;
  339. clearTimeout(timer);
  340. if (code !== 0) {
  341. reject(new Error(`claude exited with code ${code}: ${stderr.slice(0, 800)}`));
  342. return;
  343. }
  344. resolve(stdout.trim());
  345. });
  346. proc.stdin.write(prompt);
  347. proc.stdin.end();
  348. });
  349. }
  350. function extractJson(text) {
  351. const candidate = stripCodeFences(text);
  352. try {
  353. return JSON.parse(candidate);
  354. } catch {}
  355. const firstBrace = candidate.indexOf('{');
  356. const lastBrace = candidate.lastIndexOf('}');
  357. if (firstBrace >= 0 && lastBrace > firstBrace) {
  358. return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
  359. }
  360. throw new Error('could not extract JSON from response');
  361. }
  362. function normalizeStringPatches(text, latestVlVersion) {
  363. return String(text || '')
  364. .replace(/VL_VERSION:3\.5/g, `VL_VERSION:${latestVlVersion}`)
  365. .replace(/\bVL 3\.5\b/g, `VL ${latestVlVersion}`)
  366. .replace(/\bVL \(Visual Language\) v3\.5\b/g, `VL (Visual Language) v${latestVlVersion}`)
  367. .replace(/"vlVersion":\s*"3\.5"/g, `"vlVersion": "${latestVlVersion}"`)
  368. .replace(/vlVersion:\s*'3\.5'/g, `vlVersion: '${latestVlVersion}'`);
  369. }
  370. function deepPatchStrings(value, latestVlVersion) {
  371. if (typeof value === 'string') return normalizeStringPatches(value, latestVlVersion);
  372. if (Array.isArray(value)) return value.map((item) => deepPatchStrings(item, latestVlVersion));
  373. if (!value || typeof value !== 'object') return value;
  374. const out = {};
  375. for (const [key, inner] of Object.entries(value)) {
  376. out[key] = deepPatchStrings(inner, latestVlVersion);
  377. }
  378. return out;
  379. }
  380. function buildLatestDigest({ latestVlVersion, themeTitle }) {
  381. return `
  382. Latest reference baseline:
  383. - Latest DocCenter VL syntax document reports version ${latestVlVersion}.
  384. - Latest theme document is ${themeTitle}.
  385. - Use current latest syntax header // VL_VERSION:${latestVlVersion}.
  386. Essential VL rules distilled from the latest docs:
  387. - File types: .vx App, .sc Section, .cp Component, .vs ServiceDomain, .vdb Database, .vth Theme.
  388. - Cross references: App -> Section/Component only; Section -> ServiceDomain/Component only; Service and Component do not cross-reference others.
  389. - Indentation uses leading hyphens, never spaces.
  390. - App required section order: SysConfig, Frontend Global Vars, Frontend Derived Vars, Frontend Tree, Frontend Event Handlers, Frontend Internal Methods, Frontend Pipeline Funcs.
  391. - Section required section order: Frontend Public Props, Frontend Public Events, Frontend Public Methods, Frontend Global Vars, Frontend Derived Vars, Frontend Tree, Frontend Event Handlers, Frontend Internal Methods, Frontend Pipeline Funcs.
  392. - Component required section order: Frontend Public Props, Frontend Public Events, Frontend Derived Vars, Frontend Tree, Frontend Event Handlers, Frontend Internal Methods, Frontend Pipeline Funcs.
  393. - ServiceDomain required section order: Backend Environment Vars, Backend Tree, Services, Backend Event Handlers, Transactions, Backend Internal Methods, Backend Pipeline Funcs.
  394. - Theme file order: # Meta -> optional # Design Tokens -> # Point Slot Values -> optional # Overrides.
  395. - The theme heading must be exactly # Point Slot Values. Never use legacy # Coordinate Values.
  396. - Style values must stay compile-safe. Prefer static string literals in style slots and simple widgets/layouts.
  397. - Do not invent unsupported widgets or speculative syntax.
  398. `.trim();
  399. }
  400. async function mapLimit(items, limit, iterator) {
  401. const results = new Array(items.length);
  402. let nextIndex = 0;
  403. async function worker() {
  404. while (nextIndex < items.length) {
  405. const current = nextIndex;
  406. nextIndex += 1;
  407. results[current] = await iterator(items[current], current);
  408. }
  409. }
  410. const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, () => worker());
  411. await Promise.all(workers);
  412. return results;
  413. }
  414. function buildDirectContract() {
  415. return {
  416. projectName: CONTRACT.projectName,
  417. projectDescription: CONTRACT.projectDescription,
  418. database: {
  419. filePath: CONTRACT.databaseFile,
  420. tables: CONTRACT.tables,
  421. },
  422. theme: {
  423. filePath: CONTRACT.themeFile,
  424. style: 'enterprise-light',
  425. direction: 'deep teal primary, slate neutrals, amber warning, red danger, soft elevated cards, pill filters',
  426. },
  427. services: CONTRACT.services,
  428. components: CONTRACT.components,
  429. sections: CONTRACT.sections,
  430. app: CONTRACT.app,
  431. };
  432. }
  433. function directContextSlice(kind, target) {
  434. if (kind === 'database') {
  435. return {
  436. filePath: target.filePath,
  437. tables: CONTRACT.tables,
  438. };
  439. }
  440. if (kind === 'theme') {
  441. return {
  442. filePath: target.filePath,
  443. themeName: 'CampusOps',
  444. direction: 'deep teal primary, slate surfaces, amber warning, red danger, soft elevated cards, pill filters',
  445. requiredMeta: {
  446. mode: 'light',
  447. version: '6.5.0',
  448. styleSpaceVersion: '1.6',
  449. base_theme: 'Platform/Theme-Default-Light@1',
  450. profile: 'enterprise',
  451. },
  452. requiredGroups: ['intent', 'emphasis', 'shape', 'surface', 'textRole', 'state'],
  453. };
  454. }
  455. if (kind === 'service') {
  456. return {
  457. filePath: target.filePath,
  458. service: target,
  459. databaseTables: CONTRACT.tables.map((table) => ({ id: table.id, fields: table.fields })),
  460. };
  461. }
  462. if (kind === 'component') {
  463. return {
  464. filePath: target.filePath,
  465. component: target,
  466. themeDirection: 'enterprise-light, operational, card/list/table friendly',
  467. };
  468. }
  469. if (kind === 'section') {
  470. return {
  471. filePath: target.filePath,
  472. section: target,
  473. services: CONTRACT.services.filter((service) =>
  474. target.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`))
  475. ),
  476. components: CONTRACT.components.filter((component) => target.usesComponents?.includes(component.id)),
  477. };
  478. }
  479. if (kind === 'app') {
  480. return {
  481. filePath: target.filePath,
  482. app: target,
  483. sections: CONTRACT.sections.map((section) => ({
  484. id: section.id,
  485. filePath: section.filePath,
  486. purpose: section.purpose,
  487. })),
  488. };
  489. }
  490. return { filePath: target.filePath };
  491. }
  492. function metaContextSlice(kind, target, meta) {
  493. if (kind === 'database') {
  494. return {
  495. filePath: target.filePath,
  496. database: meta.database,
  497. dataSchema: meta.dataSchema,
  498. };
  499. }
  500. if (kind === 'theme') {
  501. return {
  502. filePath: target.filePath,
  503. theme: meta.theme,
  504. direction: 'enterprise-light, operational, teal/slate/amber/red',
  505. requiredMeta: {
  506. mode: 'light',
  507. version: '6.5.0',
  508. styleSpaceVersion: '1.6',
  509. base_theme: 'Platform/Theme-Default-Light@1',
  510. profile: 'enterprise',
  511. },
  512. };
  513. }
  514. if (kind === 'service') {
  515. return {
  516. filePath: target.filePath,
  517. service: target,
  518. dataSchema: meta.dataSchema,
  519. };
  520. }
  521. if (kind === 'component') {
  522. return {
  523. filePath: target.filePath,
  524. component: target,
  525. };
  526. }
  527. if (kind === 'section') {
  528. return {
  529. filePath: target.filePath,
  530. section: target,
  531. services: (meta.services || []).filter((service) =>
  532. target.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`))
  533. ),
  534. components: (meta.components || []).filter((component) => target.usesComponents?.includes(component.id)),
  535. };
  536. }
  537. if (kind === 'app') {
  538. return {
  539. filePath: target.filePath,
  540. app: target,
  541. sections: meta.sections || [],
  542. };
  543. }
  544. return { filePath: target.filePath };
  545. }
  546. function uniqueBy(items, keyFn) {
  547. const seen = new Set();
  548. const out = [];
  549. for (const item of items || []) {
  550. const key = keyFn(item);
  551. if (seen.has(key)) continue;
  552. seen.add(key);
  553. out.push(item);
  554. }
  555. return out;
  556. }
  557. function directBatchContextSlice(kind, targets) {
  558. if (kind === 'service') {
  559. return {
  560. services: targets,
  561. databaseTables: CONTRACT.tables.map((table) => ({ id: table.id, fields: table.fields })),
  562. };
  563. }
  564. if (kind === 'component') {
  565. return {
  566. components: targets,
  567. themeDirection: 'enterprise-light, operational, card/list/table friendly',
  568. };
  569. }
  570. if (kind === 'section') {
  571. const services = uniqueBy(
  572. CONTRACT.services.filter((service) =>
  573. targets.some((section) => section.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`)))
  574. ),
  575. (service) => service.domainId,
  576. );
  577. const components = uniqueBy(
  578. CONTRACT.components.filter((component) =>
  579. targets.some((section) => section.usesComponents?.includes(component.id))
  580. ),
  581. (component) => component.id,
  582. );
  583. return { sections: targets, services, components };
  584. }
  585. return { targets };
  586. }
  587. function metaBatchContextSlice(kind, targets, meta) {
  588. if (kind === 'service') {
  589. return {
  590. services: targets,
  591. dataSchema: meta.dataSchema,
  592. };
  593. }
  594. if (kind === 'component') {
  595. return {
  596. components: targets,
  597. };
  598. }
  599. if (kind === 'section') {
  600. const services = uniqueBy(
  601. (meta.services || []).filter((service) =>
  602. targets.some((section) => section.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`)))
  603. ),
  604. (service) => service.domainId,
  605. );
  606. const components = uniqueBy(
  607. (meta.components || []).filter((component) =>
  608. targets.some((section) => section.usesComponents?.includes(component.id))
  609. ),
  610. (component) => component.id,
  611. );
  612. return { sections: targets, services, components };
  613. }
  614. return { targets };
  615. }
  616. function buildBatchPrompt({ kind, targets, digest, latestVlVersion, context }) {
  617. const rootContracts = targets.map((target) => {
  618. const { open, close } = expectedRootInfo(kind, target);
  619. return `- ${target.filePath}: open ${open} | close ${close}`;
  620. }).join('\n');
  621. return `
  622. You are generating multiple VL ${kind} files for the CampusOps benchmark.
  623. ${digest}
  624. Global requirement:
  625. ${REQUIREMENT}
  626. Batch context:
  627. <batch-context>
  628. ${JSON.stringify(context, null, 2)}
  629. </batch-context>
  630. Exact file/root contracts:
  631. ${rootContracts}
  632. Output rules:
  633. - Return ONLY valid JSON.
  634. - The JSON must be an object mapping exact relative file paths to raw VL source strings.
  635. - Include every target file exactly once.
  636. - Every value must start with // VL_VERSION:${latestVlVersion}
  637. - Every value must satisfy its exact root contract.
  638. - Do not include markdown fences or explanations.
  639. `.trim();
  640. }
  641. async function runBatchPhaseWithRepair({
  642. kind,
  643. targets,
  644. projectDir,
  645. processDir,
  646. batchName,
  647. latestVlVersion,
  648. batchPrompt,
  649. batchSystemPrompt,
  650. buildSinglePrompt,
  651. }) {
  652. const records = [];
  653. const repairs = [];
  654. let batchDurationMs = 0;
  655. let batchFailed = false;
  656. try {
  657. const batch = await generateMappedFiles({
  658. prompt: batchPrompt,
  659. projectDir,
  660. promptPath: path.join(processDir, `${batchName}.prompt.txt`),
  661. rawPath: path.join(processDir, `${batchName}.raw.txt`),
  662. systemPrompt: batchSystemPrompt,
  663. });
  664. batchDurationMs = batch.durationMs;
  665. } catch (error) {
  666. batchFailed = true;
  667. await fs.writeFile(path.join(processDir, `${batchName}.error.txt`), String(error?.stack || error?.message || error), 'utf-8');
  668. }
  669. if (!batchFailed) {
  670. for (const target of targets) {
  671. const targetPath = path.join(projectDir, target.filePath);
  672. let content = '';
  673. try {
  674. content = await fs.readFile(targetPath, 'utf-8');
  675. } catch {
  676. repairs.push({ target, issues: ['batch output missing target file'] });
  677. continue;
  678. }
  679. const issues = validateGeneratedVlShape({ kind, target, content, latestVlVersion });
  680. if (issues.length) {
  681. repairs.push({ target, issues });
  682. continue;
  683. }
  684. records.push({
  685. kind,
  686. filePath: target.filePath,
  687. durationMs: batchDurationMs,
  688. attempts: 1,
  689. issues: [],
  690. });
  691. }
  692. } else {
  693. for (const target of targets) {
  694. repairs.push({ target, issues: ['batch generation failed'] });
  695. }
  696. }
  697. const repairRecords = await mapLimit(repairs, Math.min(DIRECT_FILE_CONCURRENCY, Math.max(1, repairs.length)), async ({ target, issues }) => {
  698. const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
  699. const result = await generateSingleFile({
  700. kind,
  701. target,
  702. latestVlVersion,
  703. prompt: buildSinglePrompt(target),
  704. targetPath: path.join(projectDir, target.filePath),
  705. promptPath: path.join(processDir, `${safeName}.repair.prompt.txt`),
  706. rawPath: path.join(processDir, `${safeName}.repair.raw.txt`),
  707. systemPrompt: `Generate only the VL source for ${target.filePath}.`,
  708. });
  709. return {
  710. kind,
  711. filePath: target.filePath,
  712. durationMs: batchDurationMs + result.durationMs,
  713. attempts: 1 + result.attempts,
  714. issues: [...issues, ...result.issues],
  715. };
  716. });
  717. return [...records, ...repairRecords];
  718. }
  719. function singularKind(kind) {
  720. return String(kind || '').replace(/s$/, '');
  721. }
  722. function expectedRootInfo(kind, target) {
  723. switch (singularKind(kind)) {
  724. case 'database':
  725. return { open: '<Database-CampusOps>', close: '</Database-CampusOps>' };
  726. case 'theme':
  727. return { open: '<Theme-CampusOps>', close: '</Theme-CampusOps>' };
  728. case 'service':
  729. return {
  730. open: `<ServiceDomain-${target.domainId}>`,
  731. close: `</ServiceDomain-${target.domainId}>`,
  732. };
  733. case 'component':
  734. return {
  735. open: `<Component-${target.id} "root">`,
  736. close: `</Component-${target.id}>`,
  737. };
  738. case 'section':
  739. return {
  740. open: `<Section-${target.id} "root">`,
  741. close: `</Section-${target.id}>`,
  742. };
  743. case 'app':
  744. return {
  745. open: `<App-${target.id} "root">`,
  746. close: `</App-${target.id}>`,
  747. };
  748. default:
  749. throw new Error(`Unknown kind for root info: ${kind}`);
  750. }
  751. }
  752. function requiredHeadersForKind(kind) {
  753. switch (singularKind(kind)) {
  754. case 'service':
  755. return [
  756. '# Backend Environment Vars',
  757. '# Backend Tree',
  758. '# Services',
  759. '# Backend Event Handlers',
  760. '# Transactions',
  761. '# Backend Internal Methods',
  762. '# Backend Pipeline Funcs',
  763. ];
  764. case 'component':
  765. return [
  766. '# Frontend Public Props',
  767. '# Frontend Public Events',
  768. '# Frontend Derived Vars',
  769. '# Frontend Tree',
  770. '# Frontend Event Handlers',
  771. '# Frontend Internal Methods',
  772. '# Frontend Pipeline Funcs',
  773. ];
  774. case 'section':
  775. return [
  776. '# Frontend Public Props',
  777. '# Frontend Public Events',
  778. '# Frontend Public Methods',
  779. '# Frontend Global Vars',
  780. '# Frontend Derived Vars',
  781. '# Frontend Tree',
  782. '# Frontend Event Handlers',
  783. '# Frontend Internal Methods',
  784. '# Frontend Pipeline Funcs',
  785. ];
  786. case 'app':
  787. return [
  788. '# SysConfig',
  789. '# Frontend Global Vars',
  790. '# Frontend Derived Vars',
  791. '# Frontend Tree',
  792. '# Frontend Event Handlers',
  793. '# Frontend Internal Methods',
  794. '# Frontend Pipeline Funcs',
  795. ];
  796. case 'theme':
  797. return ['# Meta', '# Point Slot Values'];
  798. default:
  799. return [];
  800. }
  801. }
  802. function buildRootGuide(kind, target, latestVlVersion) {
  803. const { open, close } = expectedRootInfo(kind, target);
  804. if (singularKind(kind) === 'database') {
  805. return `
  806. Exact root contract for this file:
  807. - Opening root must start with exactly: ${open}
  808. - Closing root must be exactly: ${close}
  809. Minimal shape example:
  810. // VL_VERSION:${latestVlVersion}
  811. ${open}
  812. <Table-Campus> data:[{"_id":1,"name":"North Campus","region":"North","manager":"Avery Chen","activeAlerts":3,"_user":"1","_create":"2026-03-01 09:00:00","_update":"2026-03-01 09:00:00"}]
  813. -<Field-name> type:STRING notNull:true
  814. ${close}
  815. `.trim();
  816. }
  817. if (singularKind(kind) === 'theme') {
  818. return `
  819. Exact root contract for this file:
  820. - Opening root must start with exactly: ${open}
  821. - Closing root must be exactly: ${close}
  822. Minimal shape example:
  823. // VL_VERSION:${latestVlVersion}
  824. ${open}
  825. # Meta
  826. mode:"light"
  827. version:"6.5.0"
  828. base_theme:"Platform/Theme-Default-Light@1"
  829. # Point Slot Values
  830. intent.primary.intentBg:#0D9488
  831. # Overrides
  832. ${close}
  833. `.trim();
  834. }
  835. if (singularKind(kind) === 'service') {
  836. return `
  837. Exact root contract for this file:
  838. - Opening root must start with exactly: ${open}
  839. - Closing root must be exactly: ${close}
  840. - Do not split the root into "<ServiceDomain> ${target.domainId}".
  841. Minimal shape example:
  842. // VL_VERSION:${latestVlVersion}
  843. ${open}
  844. # Backend Environment Vars
  845. # Backend Tree
  846. # Services
  847. SERVICE Example();RETURN {success:BOOL}
  848. -RETURN {success:true}
  849. # Backend Event Handlers
  850. # Transactions
  851. # Backend Internal Methods
  852. # Backend Pipeline Funcs
  853. ${close}
  854. `.trim();
  855. }
  856. if (singularKind(kind) === 'component') {
  857. return `
  858. Exact root contract for this file:
  859. - Opening root must start with exactly: ${open}
  860. - Closing root must be exactly: ${close}
  861. Minimal shape example:
  862. // VL_VERSION:${latestVlVersion}
  863. // Preview: width:220px height:120px
  864. ${open} padding:"16px"
  865. # Frontend Public Props
  866. $title(STRING) = ""
  867. # Frontend Public Events
  868. # Frontend Derived Vars
  869. # Frontend Tree
  870. <Text-Title> value:$title
  871. # Frontend Event Handlers
  872. # Frontend Internal Methods
  873. # Frontend Pipeline Funcs
  874. ${close}
  875. `.trim();
  876. }
  877. if (singularKind(kind) === 'section') {
  878. return `
  879. Exact root contract for this file:
  880. - Opening root must start with exactly: ${open}
  881. - Closing root must be exactly: ${close}
  882. Minimal shape example:
  883. // VL_VERSION:${latestVlVersion}
  884. ${open} padding:"24px"
  885. # Frontend Public Props
  886. # Frontend Public Events
  887. # Frontend Public Methods
  888. # Frontend Global Vars
  889. $isLoading(BOOL) = false
  890. # Frontend Derived Vars
  891. # Frontend Tree
  892. <Col-Main>
  893. -<Text-Title> value:"Example"
  894. # Frontend Event Handlers
  895. ${open}.@init()
  896. -init()
  897. # Frontend Internal Methods
  898. METHOD init(); RETURN {success:BOOL}
  899. -RETURN {success:true}
  900. # Frontend Pipeline Funcs
  901. ${close}
  902. `.trim();
  903. }
  904. if (singularKind(kind) === 'app') {
  905. return `
  906. Exact root contract for this file:
  907. - Opening root must start with exactly: ${open}
  908. - Closing root must be exactly: ${close}
  909. Minimal shape example:
  910. // VL_VERSION:${latestVlVersion}
  911. ${open}
  912. # SysConfig
  913. DEVICE_TARGET:"PC"
  914. SCREEN_RESOLUTION:"1440x900"
  915. # Frontend Global Vars
  916. $currentRoute(STRING) = "overview"
  917. # Frontend Derived Vars
  918. # Frontend Tree
  919. <Page-Overview "overview"> path:"overview"
  920. -<Section-OverviewPage "overviewPage">
  921. # Frontend Event Handlers
  922. # Frontend Internal Methods
  923. # Frontend Pipeline Funcs
  924. ${close}
  925. `.trim();
  926. }
  927. throw new Error(`Unknown kind for root guide: ${kind}`);
  928. }
  929. function escapeRegex(text) {
  930. return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  931. }
  932. function validateGeneratedVlShape({ kind, target, content, latestVlVersion }) {
  933. const issues = [];
  934. const text = String(content || '').trim();
  935. const { open, close } = expectedRootInfo(kind, target);
  936. const lines = text.split('\n');
  937. if (lines[0]?.trim() !== `// VL_VERSION:${latestVlVersion}`) {
  938. issues.push(`first line must be // VL_VERSION:${latestVlVersion}`);
  939. }
  940. if (!new RegExp(`^${escapeRegex(open)}(?:\\s|$)`, 'm').test(text)) {
  941. issues.push(`opening root must start with ${open}`);
  942. }
  943. if (!new RegExp(`^${escapeRegex(close)}\\s*$`, 'm').test(text)) {
  944. issues.push(`closing root must be ${close}`);
  945. }
  946. const headers = requiredHeadersForKind(kind);
  947. let cursor = -1;
  948. for (const header of headers) {
  949. const index = text.indexOf(header);
  950. if (index === -1) {
  951. issues.push(`missing required header ${header}`);
  952. continue;
  953. }
  954. if (index < cursor) {
  955. issues.push(`header out of order ${header}`);
  956. }
  957. cursor = index;
  958. }
  959. return issues;
  960. }
  961. function buildRepairPrompt({ originalPrompt, kind, target, issues, invalidContent, latestVlVersion }) {
  962. return `
  963. ${originalPrompt}
  964. The previous output for ${target.filePath} was invalid.
  965. Validation failures:
  966. ${issues.map((issue) => `- ${issue}`).join('\n')}
  967. Root and structure reminder:
  968. ${buildRootGuide(kind, target, latestVlVersion)}
  969. Previous invalid output:
  970. <invalid-output>
  971. ${invalidContent}
  972. </invalid-output>
  973. Rewrite the complete file from scratch.
  974. Return only raw VL source.
  975. Do not explain anything.
  976. `.trim();
  977. }
  978. function buildDirectFilePrompt({ kind, target, digest, latestThemeDoc, latestVlVersion }) {
  979. const contextJson = JSON.stringify(directContextSlice(kind, target), null, 2);
  980. const base = `
  981. You are generating one VL file for the CampusOps benchmark.
  982. ${digest}
  983. Global requirement:
  984. ${REQUIREMENT}
  985. File-specific context:
  986. <file-context>
  987. ${contextJson}
  988. </file-context>
  989. Common output rules:
  990. - Output only raw VL source code for the requested file.
  991. - Do not use markdown fences.
  992. - The first line must be // VL_VERSION:${latestVlVersion}
  993. - Keep identifiers, routes, and file references exactly aligned with the contract.
  994. - Use only compile-safe VL constructs.
  995. - Use real VL root declarations with angle brackets.
  996. - Never use prose roots like "ServiceDomain Foo" or freeform HTML tags.
  997. ${buildRootGuide(kind, target, latestVlVersion)}
  998. `.trim();
  999. if (kind === 'database') {
  1000. return `${base}
  1001. Target file: ${target.filePath}
  1002. Generate the .vdb file with realistic seed data for all declared tables.`;
  1003. }
  1004. if (kind === 'theme') {
  1005. return `${base}
  1006. Target file: ${target.filePath}
  1007. Generate a custom Theme 6.5 file for CampusOps.
  1008. Do not paste the official theme doc.
  1009. Keep the file concise but complete, compile-safe, and aligned to the required meta fields and slot groups in file-context.`;
  1010. }
  1011. if (kind === 'service') {
  1012. return `${base}
  1013. Target file: ${target.filePath}
  1014. Service contract:
  1015. ${JSON.stringify(target, null, 2)}
  1016. Generate one ServiceDomain file with safe optional filters and realistic mock responses backed by the declared tables.`;
  1017. }
  1018. if (kind === 'component') {
  1019. return `${base}
  1020. Target file: ${target.filePath}
  1021. Component contract:
  1022. ${JSON.stringify(target, null, 2)}
  1023. Generate one reusable pure UI component.`;
  1024. }
  1025. if (kind === 'section') {
  1026. return `${base}
  1027. Target file: ${target.filePath}
  1028. Section contract:
  1029. ${JSON.stringify(target, null, 2)}
  1030. Available services:
  1031. ${JSON.stringify(CONTRACT.services, null, 2)}
  1032. Available components:
  1033. ${JSON.stringify(CONTRACT.components, null, 2)}
  1034. Generate one Section file with local state, service calls, and simple event handlers.`;
  1035. }
  1036. if (kind === 'app') {
  1037. return `${base}
  1038. Target file: ${target.filePath}
  1039. App contract:
  1040. ${JSON.stringify(target, null, 2)}
  1041. Available sections:
  1042. ${JSON.stringify(CONTRACT.sections, null, 2)}
  1043. Generate one App file with a clear sidebar + content layout and route switching.`;
  1044. }
  1045. throw new Error(`Unknown direct generation kind: ${kind}`);
  1046. }
  1047. async function generateSingleFile({ kind, target, latestVlVersion, prompt, targetPath, promptPath, rawPath, systemPrompt }) {
  1048. await fs.mkdir(path.dirname(promptPath), { recursive: true });
  1049. await fs.writeFile(promptPath, prompt, 'utf-8');
  1050. let finalRaw = '';
  1051. let finalContent = '';
  1052. let totalDurationMs = 0;
  1053. let attempts = 0;
  1054. let issues = [];
  1055. for (let attempt = 1; attempt <= 2; attempt += 1) {
  1056. attempts = attempt;
  1057. const attemptPrompt = attempt === 1 ? prompt : buildRepairPrompt({
  1058. originalPrompt: prompt,
  1059. kind,
  1060. target,
  1061. issues,
  1062. invalidContent: finalContent,
  1063. latestVlVersion,
  1064. });
  1065. if (attempt > 1) {
  1066. await fs.writeFile(promptPath.replace(/\.prompt\.txt$/, `.retry${attempt - 1}.prompt.txt`), attemptPrompt, 'utf-8');
  1067. }
  1068. const startedAt = performance.now();
  1069. const raw = await runClaudePrompt(attemptPrompt, { systemPrompt });
  1070. totalDurationMs += Math.round(performance.now() - startedAt);
  1071. finalRaw = raw;
  1072. finalContent = stripCodeFences(raw).trim() + '\n';
  1073. issues = validateGeneratedVlShape({ kind, target, content: finalContent, latestVlVersion });
  1074. if (attempt > 1) {
  1075. await fs.writeFile(rawPath.replace(/\.raw\.txt$/, `.retry${attempt - 1}.raw.txt`), raw, 'utf-8');
  1076. }
  1077. if (!issues.length) break;
  1078. }
  1079. await fs.writeFile(rawPath, finalRaw, 'utf-8');
  1080. await fs.mkdir(path.dirname(targetPath), { recursive: true });
  1081. await fs.writeFile(targetPath, finalContent, 'utf-8');
  1082. return {
  1083. durationMs: totalDurationMs,
  1084. attempts,
  1085. issues,
  1086. };
  1087. }
  1088. function coerceFileMap(parsed) {
  1089. if (!parsed || typeof parsed !== 'object') {
  1090. throw new Error('expected a JSON object for file map output');
  1091. }
  1092. if (parsed.files && typeof parsed.files === 'object' && !Array.isArray(parsed.files)) {
  1093. return parsed.files;
  1094. }
  1095. if (parsed.artifacts && typeof parsed.artifacts === 'object' && !Array.isArray(parsed.artifacts)) {
  1096. return parsed.artifacts;
  1097. }
  1098. if (Array.isArray(parsed.files)) {
  1099. return Object.fromEntries(
  1100. parsed.files
  1101. .filter((item) => item && typeof item.path === 'string' && typeof item.content === 'string')
  1102. .map((item) => [item.path, item.content])
  1103. );
  1104. }
  1105. return parsed;
  1106. }
  1107. async function writeObjectFiles(projectDir, filesMap) {
  1108. const written = [];
  1109. for (const [relPath, content] of Object.entries(filesMap || {})) {
  1110. if (typeof content !== 'string') continue;
  1111. const target = path.join(projectDir, relPath);
  1112. await fs.mkdir(path.dirname(target), { recursive: true });
  1113. await fs.writeFile(target, content.trim() + '\n', 'utf-8');
  1114. written.push(relPath);
  1115. }
  1116. return written.sort();
  1117. }
  1118. async function generateMappedFiles({ prompt, projectDir, promptPath, rawPath, systemPrompt }) {
  1119. const startedAt = performance.now();
  1120. const raw = await runClaudePrompt(prompt, { systemPrompt });
  1121. const durationMs = Math.round(performance.now() - startedAt);
  1122. await fs.mkdir(path.dirname(promptPath), { recursive: true });
  1123. await fs.writeFile(promptPath, prompt, 'utf-8');
  1124. await fs.writeFile(rawPath, raw, 'utf-8');
  1125. const parsed = extractJson(raw);
  1126. const filesMap = coerceFileMap(parsed);
  1127. const writtenPaths = await writeObjectFiles(projectDir, filesMap);
  1128. return {
  1129. durationMs,
  1130. writtenPaths,
  1131. };
  1132. }
  1133. async function runDirectFullBaseline({ projectDir, digest, latestThemeDoc, latestVlVersion }) {
  1134. await ensureProjectScaffold(projectDir);
  1135. const processDir = path.join(projectDir, 'Process', 'DirectFull');
  1136. const startedAt = performance.now();
  1137. const runSingle = async (kind, target) => {
  1138. const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
  1139. const result = await generateSingleFile({
  1140. kind,
  1141. target,
  1142. latestVlVersion,
  1143. prompt: buildDirectFilePrompt({
  1144. kind,
  1145. target,
  1146. digest,
  1147. latestThemeDoc,
  1148. latestVlVersion,
  1149. }),
  1150. targetPath: path.join(projectDir, target.filePath),
  1151. promptPath: path.join(processDir, `${safeName}.prompt.txt`),
  1152. rawPath: path.join(processDir, `${safeName}.raw.txt`),
  1153. systemPrompt: `Generate only the VL source for ${target.filePath}.`,
  1154. });
  1155. return {
  1156. kind,
  1157. filePath: target.filePath,
  1158. durationMs: result.durationMs,
  1159. attempts: result.attempts,
  1160. issues: result.issues,
  1161. };
  1162. };
  1163. const [databaseRecord, themeRecord, serviceRecords, componentRecords, sectionRecords, appRecord] = await Promise.all([
  1164. runSingle('database', { filePath: CONTRACT.databaseFile }),
  1165. runSingle('theme', { filePath: CONTRACT.themeFile }),
  1166. runBatchPhaseWithRepair({
  1167. kind: 'service',
  1168. targets: CONTRACT.services,
  1169. projectDir,
  1170. processDir,
  1171. batchName: 'services.batch',
  1172. latestVlVersion,
  1173. batchPrompt: buildBatchPrompt({
  1174. kind: 'service',
  1175. targets: CONTRACT.services,
  1176. digest,
  1177. latestVlVersion,
  1178. context: directBatchContextSlice('service', CONTRACT.services),
  1179. }),
  1180. batchSystemPrompt: 'Generate only a JSON object mapping each requested service file path to its VL source.',
  1181. buildSinglePrompt: (target) => buildDirectFilePrompt({
  1182. kind: 'service',
  1183. target,
  1184. digest,
  1185. latestThemeDoc,
  1186. latestVlVersion,
  1187. }),
  1188. }),
  1189. runBatchPhaseWithRepair({
  1190. kind: 'component',
  1191. targets: CONTRACT.components,
  1192. projectDir,
  1193. processDir,
  1194. batchName: 'components.batch',
  1195. latestVlVersion,
  1196. batchPrompt: buildBatchPrompt({
  1197. kind: 'component',
  1198. targets: CONTRACT.components,
  1199. digest,
  1200. latestVlVersion,
  1201. context: directBatchContextSlice('component', CONTRACT.components),
  1202. }),
  1203. batchSystemPrompt: 'Generate only a JSON object mapping each requested component file path to its VL source.',
  1204. buildSinglePrompt: (target) => buildDirectFilePrompt({
  1205. kind: 'component',
  1206. target,
  1207. digest,
  1208. latestThemeDoc,
  1209. latestVlVersion,
  1210. }),
  1211. }),
  1212. runBatchPhaseWithRepair({
  1213. kind: 'section',
  1214. targets: CONTRACT.sections,
  1215. projectDir,
  1216. processDir,
  1217. batchName: 'sections.batch',
  1218. latestVlVersion,
  1219. batchPrompt: buildBatchPrompt({
  1220. kind: 'section',
  1221. targets: CONTRACT.sections,
  1222. digest,
  1223. latestVlVersion,
  1224. context: directBatchContextSlice('section', CONTRACT.sections),
  1225. }),
  1226. batchSystemPrompt: 'Generate only a JSON object mapping each requested section file path to its VL source.',
  1227. buildSinglePrompt: (target) => buildDirectFilePrompt({
  1228. kind: 'section',
  1229. target,
  1230. digest,
  1231. latestThemeDoc,
  1232. latestVlVersion,
  1233. }),
  1234. }),
  1235. runSingle('app', CONTRACT.app),
  1236. ]);
  1237. const records = [
  1238. databaseRecord,
  1239. themeRecord,
  1240. ...serviceRecords,
  1241. ...componentRecords,
  1242. ...sectionRecords,
  1243. appRecord,
  1244. ];
  1245. return {
  1246. method: 'direct-full',
  1247. durationMs: Math.round(performance.now() - startedAt),
  1248. firstArtifactMs: records.length ? Math.min(...records.map((item) => item.durationMs)) : null,
  1249. fileStats: records,
  1250. };
  1251. }
  1252. function buildMetaPrompt({ digest, latestVlVersion }) {
  1253. return `
  1254. Generate the complete ProjectMeta JSON for the CampusOps benchmark.
  1255. ${digest}
  1256. Global requirement:
  1257. ${REQUIREMENT}
  1258. Exact schema and naming requirements:
  1259. - specVersion must be "ProjectMeta/1.0"
  1260. - projectName must be "CampusOps"
  1261. - vlVersion must be "${latestVlVersion}"
  1262. - config.themeFile must be "Theme/Theme.vth"
  1263. - database.file must be "Database/CampusOps.vdb"
  1264. - theme.file must be "Theme/Theme.vth"
  1265. - Service domains must be exactly: OperationsOverview, ScheduleService, WorkOrderService, AlertService, SettingsService
  1266. - Components must be exactly: KpiCard, StatusPill, FilterToolbar, AlertListItem
  1267. - Sections must be exactly: OverviewPage, ScheduleBoard, WorkOrderDesk, AlertCenter, SettingsPage
  1268. - App must be exactly: CampusOpsApp
  1269. - Every service/component/section/app must include its exact filePath
  1270. - Every section must include explicit consumesServices and usesComponents arrays
  1271. - App routes must point to the declared sections
  1272. Required top-level keys:
  1273. {
  1274. "specVersion": "ProjectMeta/1.0",
  1275. "projectName": "CampusOps",
  1276. "projectDescription": "...",
  1277. "vlVersion": "${latestVlVersion}",
  1278. "config": {...},
  1279. "database": {...},
  1280. "theme": {...},
  1281. "dataSchema": {...},
  1282. "services": [...],
  1283. "components": [...],
  1284. "sections": [...],
  1285. "apps": [...]
  1286. }
  1287. Output ONLY valid JSON.
  1288. `.trim();
  1289. }
  1290. function normalizeProjectMeta(meta, latestVlVersion) {
  1291. const out = meta && typeof meta === 'object' ? structuredClone(meta) : {};
  1292. out.specVersion = 'ProjectMeta/1.0';
  1293. out.projectName = CONTRACT.projectName;
  1294. out.projectDescription = out.projectDescription || CONTRACT.projectDescription;
  1295. out.vlVersion = latestVlVersion;
  1296. out.config = out.config || {};
  1297. out.config.themeFile = CONTRACT.themeFile;
  1298. out.config.defaultDevice = out.config.defaultDevice || 'PC';
  1299. out.config.defaultResolution = out.config.defaultResolution || '1440x900';
  1300. out.config.colorScheme = out.config.colorScheme || 'light';
  1301. out.database = out.database || {};
  1302. out.database.file = CONTRACT.databaseFile;
  1303. out.theme = out.theme || {};
  1304. out.theme.file = CONTRACT.themeFile;
  1305. out.dataSchema = out.dataSchema || {};
  1306. out.dataSchema.tables = Array.isArray(out.dataSchema.tables) ? out.dataSchema.tables : [];
  1307. out.dataSchema.relations = Array.isArray(out.dataSchema.relations) ? out.dataSchema.relations : [];
  1308. out.services = Array.isArray(out.services) ? out.services : [];
  1309. out.components = Array.isArray(out.components) ? out.components : [];
  1310. out.sections = Array.isArray(out.sections) ? out.sections : [];
  1311. out.apps = Array.isArray(out.apps) ? out.apps : [];
  1312. const servicePaths = new Map(CONTRACT.services.map((item) => [item.domainId, item.filePath]));
  1313. const componentPaths = new Map(CONTRACT.components.map((item) => [item.id, item.filePath]));
  1314. const sectionPaths = new Map(CONTRACT.sections.map((item) => [item.id, item.filePath]));
  1315. out.services = out.services.map((item) => {
  1316. const domainId = item.domainId || item.id || '';
  1317. return {
  1318. ...item,
  1319. domainId,
  1320. filePath: item.filePath || servicePaths.get(domainId) || `Services/${domainId}.vs`,
  1321. methods: Array.isArray(item.methods) ? item.methods : [],
  1322. virtualTables: Array.isArray(item.virtualTables) ? item.virtualTables : [],
  1323. };
  1324. });
  1325. out.components = out.components.map((item) => {
  1326. const id = item.id || item.componentId || '';
  1327. return {
  1328. ...item,
  1329. id,
  1330. filePath: item.filePath || componentPaths.get(id) || `ExtComponents/${id}.cp`,
  1331. props: Array.isArray(item.props) ? item.props : [],
  1332. slots: Array.isArray(item.slots) ? item.slots : [],
  1333. };
  1334. });
  1335. out.sections = out.sections.map((item) => {
  1336. const id = item.id || item.sectionId || '';
  1337. return {
  1338. ...item,
  1339. id,
  1340. filePath: item.filePath || sectionPaths.get(id) || `Sections/${id}.sc`,
  1341. consumesServices: Array.isArray(item.consumesServices) ? item.consumesServices : [],
  1342. usesComponents: Array.isArray(item.usesComponents) ? item.usesComponents : [],
  1343. bindings: Array.isArray(item.bindings) ? item.bindings : [],
  1344. events: Array.isArray(item.events) ? item.events : [],
  1345. };
  1346. });
  1347. out.apps = out.apps.map((item) => {
  1348. const id = item.id || item.appId || '';
  1349. return {
  1350. ...item,
  1351. id,
  1352. filePath: item.filePath || (id ? `Apps/${id}.vx` : CONTRACT.app.filePath),
  1353. routes: Array.isArray(item.routes) ? item.routes : [],
  1354. layout: item.layout && typeof item.layout === 'object' ? item.layout : {},
  1355. };
  1356. });
  1357. return out;
  1358. }
  1359. function buildMetaBasedFilePrompt({ kind, target, meta, digest, latestThemeDoc, latestVlVersion }) {
  1360. const contextJson = JSON.stringify(metaContextSlice(kind, target, meta), null, 2);
  1361. const base = `
  1362. You are generating one VL file for the CampusOps benchmark from ProjectMeta.
  1363. ${digest}
  1364. Global requirement:
  1365. ${REQUIREMENT}
  1366. Relevant ProjectMeta slice:
  1367. <project-meta-slice>
  1368. ${contextJson}
  1369. </project-meta-slice>
  1370. Common output rules:
  1371. - Output only raw VL source code for the requested file.
  1372. - Do not use markdown fences.
  1373. - The first line must be // VL_VERSION:${latestVlVersion}
  1374. - Keep identifiers and file references aligned with ProjectMeta.
  1375. - Use compile-safe VL only.
  1376. - Use real VL root declarations with angle brackets.
  1377. - Never use prose roots like "ServiceDomain Foo" or freeform HTML tags.
  1378. ${buildRootGuide(kind, target, latestVlVersion)}
  1379. `.trim();
  1380. if (kind === 'database') {
  1381. return `${base}
  1382. Target file: ${target.filePath}
  1383. Generate the database file that matches the ProjectMeta dataSchema with realistic seed data.`;
  1384. }
  1385. if (kind === 'theme') {
  1386. return `${base}
  1387. Target file: ${target.filePath}
  1388. Generate the Theme 6.5 file for CampusOps.
  1389. Do not paste the official theme doc.
  1390. Keep the file concise but complete, compile-safe, and keep base_theme:"Platform/Theme-Default-Light@1".`;
  1391. }
  1392. if (kind === 'service') {
  1393. return `${base}
  1394. Target file: ${target.filePath}
  1395. Service spec:
  1396. ${JSON.stringify(target, null, 2)}
  1397. Generate one ServiceDomain file based on ProjectMeta.`;
  1398. }
  1399. if (kind === 'component') {
  1400. return `${base}
  1401. Target file: ${target.filePath}
  1402. Component spec:
  1403. ${JSON.stringify(target, null, 2)}
  1404. Generate one pure UI component file.`;
  1405. }
  1406. if (kind === 'section') {
  1407. return `${base}
  1408. Target file: ${target.filePath}
  1409. Section spec:
  1410. ${JSON.stringify(target, null, 2)}
  1411. Generate one Section file with service calls consistent with ProjectMeta.`;
  1412. }
  1413. if (kind === 'app') {
  1414. return `${base}
  1415. Target file: ${target.filePath}
  1416. App spec:
  1417. ${JSON.stringify(target, null, 2)}
  1418. Generate one App file consistent with ProjectMeta routes and section usage.`;
  1419. }
  1420. throw new Error(`Unknown meta-based generation kind: ${kind}`);
  1421. }
  1422. async function runMetaFirstBaseline({ projectDir, digest, latestThemeDoc, latestVlVersion }) {
  1423. await ensureProjectScaffold(projectDir);
  1424. const processDir = path.join(projectDir, 'Process', 'MetaFirst');
  1425. const metaPrompt = buildMetaPrompt({ digest, latestVlVersion });
  1426. const metaRawPath = path.join(processDir, 'ProjectMeta.raw.txt');
  1427. const metaPromptPath = path.join(processDir, 'ProjectMeta.prompt.txt');
  1428. await fs.mkdir(processDir, { recursive: true });
  1429. await fs.writeFile(metaPromptPath, metaPrompt, 'utf-8');
  1430. const metaStartedAt = performance.now();
  1431. const metaRaw = await runClaudePrompt(metaPrompt, {
  1432. systemPrompt: 'Generate only valid ProjectMeta JSON.',
  1433. timeoutMs: 8 * 60 * 1000,
  1434. });
  1435. const metaDurationMs = Math.round(performance.now() - metaStartedAt);
  1436. await fs.writeFile(metaRawPath, metaRaw, 'utf-8');
  1437. const parsedMeta = extractJson(metaRaw);
  1438. const meta = normalizeProjectMeta(parsedMeta, latestVlVersion);
  1439. await fs.writeFile(path.join(projectDir, '.vl-code', 'ProjectMeta.json'), JSON.stringify(meta, null, 2), 'utf-8');
  1440. await fs.writeFile(path.join(projectDir, 'Process', 'ProjectMeta.json'), JSON.stringify(meta, null, 2), 'utf-8');
  1441. const startedAt = performance.now();
  1442. const runSingle = async (kind, target) => {
  1443. const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
  1444. const result = await generateSingleFile({
  1445. kind,
  1446. target,
  1447. latestVlVersion,
  1448. prompt: buildMetaBasedFilePrompt({
  1449. kind,
  1450. target,
  1451. meta,
  1452. digest,
  1453. latestThemeDoc,
  1454. latestVlVersion,
  1455. }),
  1456. targetPath: path.join(projectDir, target.filePath),
  1457. promptPath: path.join(processDir, `${safeName}.prompt.txt`),
  1458. rawPath: path.join(processDir, `${safeName}.raw.txt`),
  1459. systemPrompt: `Generate only the VL source for ${target.filePath}.`,
  1460. });
  1461. return {
  1462. kind,
  1463. filePath: target.filePath,
  1464. durationMs: result.durationMs,
  1465. attempts: result.attempts,
  1466. issues: result.issues,
  1467. };
  1468. };
  1469. const appPromises = meta.apps.map((target) => runSingle('app', target));
  1470. const [databaseRecord, themeRecord, serviceRecords, componentRecords, sectionRecords, appRecords] = await Promise.all([
  1471. runSingle('database', { filePath: meta.database?.file || CONTRACT.databaseFile }),
  1472. runSingle('theme', { filePath: meta.theme?.file || CONTRACT.themeFile }),
  1473. runBatchPhaseWithRepair({
  1474. kind: 'service',
  1475. targets: meta.services,
  1476. projectDir,
  1477. processDir,
  1478. batchName: 'services.batch',
  1479. latestVlVersion,
  1480. batchPrompt: buildBatchPrompt({
  1481. kind: 'service',
  1482. targets: meta.services,
  1483. digest,
  1484. latestVlVersion,
  1485. context: metaBatchContextSlice('service', meta.services, meta),
  1486. }),
  1487. batchSystemPrompt: 'Generate only a JSON object mapping each requested service file path to its VL source.',
  1488. buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
  1489. kind: 'service',
  1490. target,
  1491. meta,
  1492. digest,
  1493. latestThemeDoc,
  1494. latestVlVersion,
  1495. }),
  1496. }),
  1497. runBatchPhaseWithRepair({
  1498. kind: 'component',
  1499. targets: meta.components,
  1500. projectDir,
  1501. processDir,
  1502. batchName: 'components.batch',
  1503. latestVlVersion,
  1504. batchPrompt: buildBatchPrompt({
  1505. kind: 'component',
  1506. targets: meta.components,
  1507. digest,
  1508. latestVlVersion,
  1509. context: metaBatchContextSlice('component', meta.components, meta),
  1510. }),
  1511. batchSystemPrompt: 'Generate only a JSON object mapping each requested component file path to its VL source.',
  1512. buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
  1513. kind: 'component',
  1514. target,
  1515. meta,
  1516. digest,
  1517. latestThemeDoc,
  1518. latestVlVersion,
  1519. }),
  1520. }),
  1521. runBatchPhaseWithRepair({
  1522. kind: 'section',
  1523. targets: meta.sections,
  1524. projectDir,
  1525. processDir,
  1526. batchName: 'sections.batch',
  1527. latestVlVersion,
  1528. batchPrompt: buildBatchPrompt({
  1529. kind: 'section',
  1530. targets: meta.sections,
  1531. digest,
  1532. latestVlVersion,
  1533. context: metaBatchContextSlice('section', meta.sections, meta),
  1534. }),
  1535. batchSystemPrompt: 'Generate only a JSON object mapping each requested section file path to its VL source.',
  1536. buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
  1537. kind: 'section',
  1538. target,
  1539. meta,
  1540. digest,
  1541. latestThemeDoc,
  1542. latestVlVersion,
  1543. }),
  1544. }),
  1545. Promise.all(appPromises),
  1546. ]);
  1547. const records = [
  1548. databaseRecord,
  1549. themeRecord,
  1550. ...serviceRecords,
  1551. ...componentRecords,
  1552. ...sectionRecords,
  1553. ...appRecords,
  1554. ];
  1555. return {
  1556. method: 'meta-first',
  1557. durationMs: metaDurationMs + Math.round(performance.now() - startedAt),
  1558. firstArtifactMs: metaDurationMs,
  1559. metaDurationMs,
  1560. meta,
  1561. fileStats: records,
  1562. };
  1563. }
  1564. async function fetchWorkflowJson(docId, cookie) {
  1565. const info = await fetchDocInfo(docId, cookie);
  1566. return {
  1567. ...info,
  1568. workflow: JSON.parse(info.content),
  1569. };
  1570. }
  1571. function patchWorkflowForLatest(workflow, { latestVlVersion, latestThemeContent }) {
  1572. const patched = deepPatchStrings(workflow, latestVlVersion);
  1573. for (const step of patched.steps || []) {
  1574. if (/^SeedTheme_/i.test(step.id || '')) {
  1575. step.in = step.in || {};
  1576. step.in.content = latestThemeContent;
  1577. }
  1578. }
  1579. return patched;
  1580. }
  1581. function buildWorkflowRequirement(latestVlVersion) {
  1582. return `
  1583. ${REQUIREMENT}
  1584. Additional generation constraints:
  1585. - Use latest VL syntax header // VL_VERSION:${latestVlVersion} for generated VL source files.
  1586. - Use Theme 6.5 enterprise semantics.
  1587. - Keep the output compile-safe and deterministic.
  1588. `.trim();
  1589. }
  1590. async function runWorkflowBaseline({ method, projectDir, cookie, latestSyntaxDoc, latestThemeContent, latestVlVersion }) {
  1591. await ensureProjectScaffold(projectDir);
  1592. const workflowDoc = await fetchWorkflowJson(WORKFLOW_DOC_IDS[method], cookie);
  1593. const workflow = patchWorkflowForLatest(workflowDoc.workflow, {
  1594. latestVlVersion,
  1595. latestThemeContent,
  1596. });
  1597. await fs.writeFile(path.join(projectDir, 'Process', `${method}.workflow.source.json`), workflowDoc.content, 'utf-8');
  1598. await fs.writeFile(path.join(projectDir, 'Process', `${method}.workflow.patched.json`), JSON.stringify(workflow, null, 2), 'utf-8');
  1599. const executor = new WorkflowExecutor({
  1600. workDir: projectDir,
  1601. model: REPORT_MODEL,
  1602. llmProvider: 'cli',
  1603. cookie,
  1604. vlVersion: latestVlVersion,
  1605. });
  1606. const originalResolveDocs = executor._resolveDocCenterDocs.bind(executor);
  1607. executor._resolveDocCenterDocs = async (docs, cb) => {
  1608. await originalResolveDocs(docs, cb);
  1609. for (const key of Object.keys(docs)) {
  1610. if (typeof docs[key] === 'string') {
  1611. docs[key] = normalizeStringPatches(docs[key], latestVlVersion);
  1612. }
  1613. }
  1614. if (Object.prototype.hasOwnProperty.call(docs, '1')) {
  1615. docs['1'] = latestSyntaxDoc;
  1616. }
  1617. };
  1618. const timeline = [];
  1619. const logLines = [];
  1620. const fileEvents = [];
  1621. const startedAt = performance.now();
  1622. await new Promise((resolve, reject) => {
  1623. executor.execute(workflow, {
  1624. userRequest: buildWorkflowRequirement(latestVlVersion),
  1625. userRequirement: buildWorkflowRequirement(latestVlVersion),
  1626. targetLang: TARGET_LANG,
  1627. }, {
  1628. onText: (text) => {
  1629. const line = String(text || '').trim();
  1630. if (line) logLines.push(line);
  1631. },
  1632. onNodeStart: (info) => {
  1633. timeline.push({
  1634. nodeId: info.nodeId,
  1635. title: info.title,
  1636. type: info.type,
  1637. status: 'start',
  1638. atMs: Math.round(performance.now() - startedAt),
  1639. });
  1640. },
  1641. onNodeDone: (info) => {
  1642. timeline.push({
  1643. nodeId: info.nodeId,
  1644. title: info.title,
  1645. type: info.type,
  1646. status: 'done',
  1647. durationMs: info.duration_ms || 0,
  1648. atMs: Math.round(performance.now() - startedAt),
  1649. });
  1650. },
  1651. onNodeError: (info) => {
  1652. timeline.push({
  1653. nodeId: info.nodeId,
  1654. title: info.title,
  1655. type: info.type,
  1656. status: 'error',
  1657. error: info.error || 'unknown error',
  1658. atMs: Math.round(performance.now() - startedAt),
  1659. });
  1660. },
  1661. onFileWritten: (filePath) => {
  1662. fileEvents.push({
  1663. filePath,
  1664. atMs: Math.round(performance.now() - startedAt),
  1665. });
  1666. },
  1667. onDone: (info) => resolve(info),
  1668. onError: (message) => reject(new Error(message || `${method} workflow generation failed`)),
  1669. });
  1670. });
  1671. const durationMs = Math.round(performance.now() - startedAt);
  1672. await fs.writeFile(path.join(projectDir, 'Process', `${method}.timeline.json`), JSON.stringify(timeline, null, 2), 'utf-8');
  1673. await fs.writeFile(path.join(projectDir, 'Process', `${method}.log.txt`), logLines.join('\n'), 'utf-8');
  1674. await fs.writeFile(path.join(projectDir, 'Process', `${method}.files.json`), JSON.stringify(fileEvents, null, 2), 'utf-8');
  1675. return {
  1676. method,
  1677. durationMs,
  1678. firstArtifactMs: fileEvents.length ? Math.min(...fileEvents.map((item) => item.atMs)) : null,
  1679. timeline,
  1680. workflowDocTitle: workflowDoc.title,
  1681. workflowDocUpdatedAt: workflowDoc.updatedAt,
  1682. };
  1683. }
  1684. async function packageProjectFiles(projectDir, zipPath) {
  1685. execSync(
  1686. `cd "${projectDir}" && find . -type f \\( -name "*.vx" -o -name "*.sc" -o -name "*.cp" -o -name "*.vs" -o -name "*.vdb" -o -name "*.vth" \\) | zip -q -@ "${zipPath}"`,
  1687. { timeout: 30_000 },
  1688. );
  1689. }
  1690. async function runCloudAction(projectDir, cookie, action) {
  1691. const zipPath = path.join(projectDir, `__${action}.zip`);
  1692. try {
  1693. await packageProjectFiles(projectDir, zipPath);
  1694. const zipBuffer = await fs.readFile(zipPath);
  1695. const res = await fetch(`${PARSEVL_URL}/edtfn/parsevl`, {
  1696. method: 'POST',
  1697. headers: {
  1698. 'Content-Type': 'application/json',
  1699. 'Cookie': normalizeCookie(cookie),
  1700. },
  1701. body: JSON.stringify({
  1702. action,
  1703. file: `data:application/zip;base64,${zipBuffer.toString('base64')}`,
  1704. ...(action === 'parsePjt' ? { download: true } : {}),
  1705. projectName: path.basename(projectDir),
  1706. }),
  1707. });
  1708. const json = await res.json();
  1709. const diagnostics = Array.isArray(json?.data?.errList) ? json.data.errList : [];
  1710. const hardErrors = diagnostics.filter((item) => String(item?.level || '').toLowerCase() !== 'warning');
  1711. const warnings = diagnostics.filter((item) => String(item?.level || '').toLowerCase() === 'warning');
  1712. return {
  1713. action,
  1714. httpOk: res.ok,
  1715. code: json?.code ?? null,
  1716. success: res.ok && hardErrors.length === 0 && (json?.code === 0 || json?.code === 200 || json?.success === true || action === 'lintPjt'),
  1717. errCount: hardErrors.length,
  1718. warningCount: warnings.length,
  1719. errList: diagnostics.slice(0, 30),
  1720. previewUrls: json?.data?.appPreviewUrlMap || {},
  1721. message: json?.message || '',
  1722. raw: json,
  1723. };
  1724. } catch (error) {
  1725. return {
  1726. action,
  1727. success: false,
  1728. error: error.message,
  1729. errCount: null,
  1730. warningCount: null,
  1731. errList: [],
  1732. previewUrls: {},
  1733. };
  1734. } finally {
  1735. try {
  1736. await fs.unlink(zipPath);
  1737. } catch {}
  1738. }
  1739. }
  1740. function parseValidationSummary(validationText) {
  1741. const text = String(validationText || '');
  1742. if (/All \d+ VL files passed validation\./.test(text)) {
  1743. return { errors: 0, warnings: 0, raw: text };
  1744. }
  1745. const match = text.match(/Validation:\s+(\d+)\s+errors,\s+(\d+)\s+warnings/i);
  1746. return {
  1747. errors: match ? Number(match[1]) : null,
  1748. warnings: match ? Number(match[2]) : null,
  1749. raw: text,
  1750. };
  1751. }
  1752. async function collectFileContents(projectDir) {
  1753. const result = {};
  1754. async function walk(currentDir, prefix = '') {
  1755. const entries = await fs.readdir(currentDir, { withFileTypes: true });
  1756. for (const entry of entries) {
  1757. if (entry.name.startsWith('.') && entry.name !== '.vl-code') continue;
  1758. const fullPath = path.join(currentDir, entry.name);
  1759. const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
  1760. if (entry.isDirectory()) {
  1761. await walk(fullPath, relPath);
  1762. } else if (/\.(vx|sc|cp|vs|vdb|vth|json|txt|md)$/i.test(entry.name)) {
  1763. try {
  1764. result[relPath] = await fs.readFile(fullPath, 'utf-8');
  1765. } catch {}
  1766. }
  1767. }
  1768. }
  1769. await walk(projectDir);
  1770. return result;
  1771. }
  1772. function countLocByExt(fileMap) {
  1773. const stats = {};
  1774. for (const [relPath, content] of Object.entries(fileMap || {})) {
  1775. const ext = path.extname(relPath) || 'none';
  1776. const lines = String(content || '').split('\n');
  1777. if (!stats[ext]) stats[ext] = { files: 0, lines: 0, nonEmptyLines: 0 };
  1778. stats[ext].files += 1;
  1779. stats[ext].lines += lines.length;
  1780. stats[ext].nonEmptyLines += lines.filter((line) => line.trim()).length;
  1781. }
  1782. return stats;
  1783. }
  1784. async function renderMetadataSnapshot({ projectDir, label, meta }) {
  1785. const browser = await chromium.launch({ headless: true });
  1786. try {
  1787. const page = await browser.newPage({ viewport: { width: 1800, height: 1400 } });
  1788. const viewerUrl = `file://${path.resolve(process.cwd(), 'public', 'metadata-viewer.html')}`;
  1789. await page.goto(viewerUrl);
  1790. await page.evaluate((payload) => {
  1791. window.postMessage({ type: 'loadMetadata', data: payload }, '*');
  1792. }, meta);
  1793. await page.waitForFunction(() => document.querySelectorAll('.node').length > 0, { timeout: 10_000 });
  1794. const counts = await page.evaluate(() => ({
  1795. nodeCount: document.querySelectorAll('.node').length,
  1796. connectionCount: document.querySelectorAll('.conn').length,
  1797. }));
  1798. const screenshotPath = path.join(projectDir, 'Process', `${label}.meta-viewer.png`);
  1799. await page.screenshot({ path: screenshotPath, fullPage: true });
  1800. return {
  1801. success: true,
  1802. ...counts,
  1803. screenshotPath,
  1804. };
  1805. } catch (error) {
  1806. return {
  1807. success: false,
  1808. error: error.message,
  1809. nodeCount: 0,
  1810. connectionCount: 0,
  1811. screenshotPath: null,
  1812. };
  1813. } finally {
  1814. await browser.close();
  1815. }
  1816. }
  1817. function loadSavedMeta(fileMap) {
  1818. for (const relPath of ['.vl-code/ProjectMeta.json', 'Process/ProjectMeta.json']) {
  1819. if (!fileMap[relPath]) continue;
  1820. try {
  1821. const parsed = JSON.parse(fileMap[relPath]);
  1822. if (parsed && typeof parsed === 'object') return parsed;
  1823. } catch {}
  1824. }
  1825. return null;
  1826. }
  1827. async function analyzeProject(projectDir, { cookie, methodLabel }) {
  1828. const ctx = new VLProjectContext(projectDir);
  1829. await ctx.load();
  1830. const validateTool = createVLValidateTool(ctx);
  1831. const validationText = await validateTool.execute({ file_path: 'all' });
  1832. const validation = parseValidationSummary(validationText);
  1833. const hint = await runCloudAction(projectDir, cookie, 'lintPjt');
  1834. const compile = await runCloudAction(projectDir, cookie, 'parsePjt');
  1835. const fileMap = await collectFileContents(projectDir);
  1836. const vlFileMap = Object.fromEntries(
  1837. Object.entries(fileMap).filter(([relPath]) => /\.(vx|sc|cp|vs|vdb|vth)$/i.test(relPath))
  1838. );
  1839. const extractedMeta = extractFromFileTree(vlFileMap, projectDir);
  1840. const metaValidation = validateMeta(extractedMeta);
  1841. const savedMeta = loadSavedMeta(fileMap);
  1842. const metaForRender = savedMeta || extractedMeta;
  1843. const metaRender = await renderMetadataSnapshot({
  1844. projectDir,
  1845. label: methodLabel,
  1846. meta: metaForRender,
  1847. });
  1848. await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.validation.txt`), validation.raw || '', 'utf-8');
  1849. await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.hint.json`), JSON.stringify(hint, null, 2), 'utf-8');
  1850. await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.compile.json`), JSON.stringify(compile, null, 2), 'utf-8');
  1851. await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.extracted-meta.json`), JSON.stringify(extractedMeta, null, 2), 'utf-8');
  1852. await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.meta-render.json`), JSON.stringify(metaRender, null, 2), 'utf-8');
  1853. const actualPaths = Object.keys(vlFileMap).sort();
  1854. const missingPaths = EXPECTED_PATHS.filter((relPath) => !actualPaths.includes(relPath));
  1855. const extraPaths = actualPaths.filter((relPath) => !EXPECTED_PATHS.includes(relPath));
  1856. return {
  1857. projectDir,
  1858. fileCount: actualPaths.length,
  1859. actualPaths,
  1860. missingPaths,
  1861. extraPaths,
  1862. validation,
  1863. hint: {
  1864. success: hint.success,
  1865. errCount: hint.errCount,
  1866. warningCount: hint.warningCount,
  1867. errList: hint.errList,
  1868. },
  1869. compile: {
  1870. success: compile.success,
  1871. errCount: compile.errCount,
  1872. warningCount: compile.warningCount,
  1873. previewUrls: compile.previewUrls,
  1874. errList: compile.errList,
  1875. },
  1876. metadataValid: metaValidation.valid,
  1877. metadataIssues: metaValidation.issues,
  1878. metaRender,
  1879. extractedMetaSummary: {
  1880. projectName: extractedMeta.projectName || null,
  1881. tables: Array.isArray(extractedMeta.dataSchema?.tables) ? extractedMeta.dataSchema.tables.length : 0,
  1882. services: Array.isArray(extractedMeta.services) ? extractedMeta.services.length : 0,
  1883. components: Array.isArray(extractedMeta.components) ? extractedMeta.components.length : 0,
  1884. sections: Array.isArray(extractedMeta.sections) ? extractedMeta.sections.length : 0,
  1885. apps: Array.isArray(extractedMeta.apps) ? extractedMeta.apps.length : 0,
  1886. },
  1887. locByExt: countLocByExt(vlFileMap),
  1888. };
  1889. }
  1890. function summarizeMethod(label, runMeta, analysis) {
  1891. return {
  1892. label: METHOD_LABELS[label] || label,
  1893. durationMs: runMeta.durationMs,
  1894. firstArtifactMs: runMeta.firstArtifactMs ?? null,
  1895. fileCount: analysis.fileCount,
  1896. missingPaths: analysis.missingPaths.length,
  1897. extraPaths: analysis.extraPaths.length,
  1898. localValidationErrors: analysis.validation.errors,
  1899. localValidationWarnings: analysis.validation.warnings,
  1900. hintErrors: analysis.hint.errCount,
  1901. hintWarnings: analysis.hint.warningCount,
  1902. compileSuccess: analysis.compile.success,
  1903. compileErrors: analysis.compile.errCount,
  1904. compileWarnings: analysis.compile.warningCount,
  1905. previewCount: Object.keys(analysis.compile.previewUrls || {}).length,
  1906. metadataValid: analysis.metadataValid,
  1907. metadataIssueCount: analysis.metadataIssues.length,
  1908. metaRenderSuccess: analysis.metaRender.success,
  1909. metaNodeCount: analysis.metaRender.nodeCount,
  1910. metaConnectionCount: analysis.metaRender.connectionCount,
  1911. };
  1912. }
  1913. function buildMarkdownReport(report) {
  1914. const bundleZipPath = report.bundle?.zipPath || 'pending';
  1915. const lines = [
  1916. `# VL Native Generation Benchmark (${TODAY})`,
  1917. '',
  1918. '## Baseline',
  1919. '',
  1920. `- Model: ${REPORT_MODEL}`,
  1921. `- Target language: ${TARGET_LANG}`,
  1922. `- Latest VL syntax doc: ${report.docs.syntax.title} (${report.docs.syntax.updatedAt})`,
  1923. `- Latest THEME doc: ${report.docs.theme.title} (${report.docs.theme.updatedAt})`,
  1924. `- Workflow spec doc: ${report.docs.workflowSpec.title} (${report.docs.workflowSpec.updatedAt})`,
  1925. '',
  1926. '## Requirement',
  1927. '',
  1928. REQUIREMENT,
  1929. '',
  1930. '## Summary',
  1931. '',
  1932. '| Method | Time(ms) | Hint E/W | Compile E/W | Compile OK | Meta Render | Missing | Preview |',
  1933. '| --- | ---: | ---: | ---: | --- | --- | ---: | ---: |',
  1934. ];
  1935. for (const method of METHOD_ORDER) {
  1936. const summary = report.methods[method].summary;
  1937. lines.push(
  1938. `| ${summary.label} | ${summary.durationMs} | ${summary.hintErrors ?? 'n/a'}/${summary.hintWarnings ?? 'n/a'} | ${summary.compileErrors ?? 'n/a'}/${summary.compileWarnings ?? 'n/a'} | ${summary.compileSuccess} | ${summary.metaRenderSuccess} (${summary.metaNodeCount}) | ${summary.missingPaths} | ${summary.previewCount} |`
  1939. );
  1940. }
  1941. lines.push('');
  1942. lines.push('## Corrections Applied');
  1943. lines.push('');
  1944. lines.push('- Workflow prompt/doc strings containing `3.5` were patched in-memory to the latest syntax version before workflow execution.');
  1945. lines.push('- `SeedTheme` nodes were forced to use the latest Theme 6.5 document content from DocCenter.');
  1946. lines.push('- Workflow doc path `1` was overwritten in-memory with the latest syntax document content from DocCenter instead of the repo-pinned local snapshot.');
  1947. lines.push('');
  1948. lines.push('## Outputs');
  1949. lines.push('');
  1950. for (const method of METHOD_ORDER) {
  1951. const item = report.methods[method];
  1952. lines.push(`### ${item.summary.label}`);
  1953. lines.push('');
  1954. lines.push(`- Project dir: ${item.analysis.projectDir}`);
  1955. lines.push(`- Screenshot: ${item.analysis.metaRender.screenshotPath || 'n/a'}`);
  1956. lines.push(`- Missing paths: ${item.analysis.missingPaths.join(', ') || 'none'}`);
  1957. lines.push(`- Extra paths: ${item.analysis.extraPaths.join(', ') || 'none'}`);
  1958. lines.push(`- Metadata issues: ${item.analysis.metadataIssues.join(' | ') || 'none'}`);
  1959. lines.push('');
  1960. }
  1961. lines.push('## Bundle');
  1962. lines.push('');
  1963. lines.push(`- Zip: ${bundleZipPath}`);
  1964. lines.push('');
  1965. return lines.join('\n');
  1966. }
  1967. async function copyFileSafe(src, dest) {
  1968. await fs.mkdir(path.dirname(dest), { recursive: true });
  1969. await fs.copyFile(src, dest);
  1970. }
  1971. async function createBundle(report, reportJsonPath, reportMdPath) {
  1972. const bundleReportDir = path.join(TEST_ROOT, `VLNativeBenchmarkReports_${DATE_SLUG}`);
  1973. await fs.rm(bundleReportDir, { recursive: true, force: true });
  1974. await fs.mkdir(bundleReportDir, { recursive: true });
  1975. await copyFileSafe(reportJsonPath, path.join(bundleReportDir, path.basename(reportJsonPath)));
  1976. await copyFileSafe(reportMdPath, path.join(bundleReportDir, path.basename(reportMdPath)));
  1977. const zipPath = path.join(TEST_ROOT, `vl-native-benchmark-${DATE_SLUG}.zip`);
  1978. const bundleItems = [
  1979. ...METHOD_ORDER.map((method) => path.basename(report.methods[method].analysis.projectDir)),
  1980. path.basename(bundleReportDir),
  1981. ];
  1982. execSync(`cd "${TEST_ROOT}" && rm -f "${zipPath}" && zip -qr "${zipPath}" ${bundleItems.map((item) => `"${item}"`).join(' ')}`, {
  1983. timeout: 120_000,
  1984. });
  1985. return {
  1986. zipPath,
  1987. bundleReportDir,
  1988. };
  1989. }
  1990. function findLatestExistingProject(baseName, requireComplete = false) {
  1991. const prefix = baseName.replace(/Test$/, '');
  1992. const matches = fsSync.readdirSync(TEST_ROOT, { withFileTypes: true })
  1993. .filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
  1994. .map((entry) => {
  1995. const fullPath = path.join(TEST_ROOT, entry.name);
  1996. let mtimeMs = 0;
  1997. try {
  1998. mtimeMs = fsSync.statSync(fullPath).mtimeMs;
  1999. } catch {}
  2000. return { fullPath, mtimeMs };
  2001. })
  2002. .sort((a, b) => b.mtimeMs - a.mtimeMs);
  2003. for (const match of matches) {
  2004. if (!requireComplete || projectLooksComplete(match.fullPath)) return match.fullPath;
  2005. }
  2006. return null;
  2007. }
  2008. function projectLooksComplete(projectDir) {
  2009. return EXPECTED_PATHS.every((relPath) => fsSync.existsSync(path.join(projectDir, relPath)));
  2010. }
  2011. function estimateRunFromFiles(projectDir, method) {
  2012. const timestamps = [];
  2013. const artifactTimes = [];
  2014. function visit(dir) {
  2015. if (!fsSync.existsSync(dir)) return;
  2016. for (const entry of fsSync.readdirSync(dir, { withFileTypes: true })) {
  2017. const fullPath = path.join(dir, entry.name);
  2018. if (entry.isDirectory()) {
  2019. visit(fullPath);
  2020. continue;
  2021. }
  2022. if (!/(\.prompt\.txt|\.raw\.txt|\.workflow\.(source|patched)\.json|\.timeline\.json|\.log\.txt|\.files\.json|ProjectMeta\.json)$/i.test(entry.name)) {
  2023. continue;
  2024. }
  2025. try {
  2026. const stat = fsSync.statSync(fullPath);
  2027. timestamps.push(stat.mtimeMs);
  2028. } catch {}
  2029. }
  2030. }
  2031. visit(path.join(projectDir, 'Process'));
  2032. for (const relPath of EXPECTED_PATHS) {
  2033. const fullPath = path.join(projectDir, relPath);
  2034. try {
  2035. artifactTimes.push(fsSync.statSync(fullPath).mtimeMs);
  2036. } catch {}
  2037. }
  2038. const start = timestamps.length ? Math.min(...timestamps) : Date.now();
  2039. const end = timestamps.length ? Math.max(...timestamps) : start;
  2040. const firstArtifact = artifactTimes.length ? Math.min(...artifactTimes) : start;
  2041. return {
  2042. method,
  2043. durationMs: Math.max(0, Math.round(end - start)),
  2044. firstArtifactMs: Math.max(0, Math.round(firstArtifact - start)),
  2045. resumed: true,
  2046. };
  2047. }
  2048. function buildFailedAnalysis(projectDir, error) {
  2049. return {
  2050. projectDir,
  2051. fileCount: 0,
  2052. actualPaths: [],
  2053. missingPaths: [...EXPECTED_PATHS],
  2054. extraPaths: [],
  2055. validation: { errors: null, warnings: null, raw: String(error || '') },
  2056. hint: { success: false, errCount: null, warningCount: null, errList: [] },
  2057. compile: { success: false, errCount: null, warningCount: null, previewUrls: {}, errList: [] },
  2058. metadataValid: false,
  2059. metadataIssues: [String(error || 'analysis failed')],
  2060. metaRender: { success: false, error: String(error || 'analysis failed'), nodeCount: 0, connectionCount: 0, screenshotPath: null },
  2061. extractedMetaSummary: { projectName: null, tables: 0, services: 0, components: 0, sections: 0, apps: 0 },
  2062. locByExt: {},
  2063. };
  2064. }
  2065. async function main() {
  2066. await fs.mkdir(TEST_ROOT, { recursive: true });
  2067. await fs.mkdir(REPORT_DIR, { recursive: true });
  2068. const cookie = getCookie({ workDir: process.cwd(), cookie: '' });
  2069. if (!cookie) {
  2070. throw new Error('No DocCenter/cloud cookie found. Cannot run benchmark.');
  2071. }
  2072. const docs = {
  2073. syntax: await fetchDocInfo(1, cookie),
  2074. theme: await fetchDocInfo(4, cookie),
  2075. workflowSpec: await fetchDocInfo(2, cookie),
  2076. };
  2077. const latestVlVersion = docs.syntax.content.match(/Current version:\s*`\/\/\s*VL_VERSION:([^`]+)`/i)?.[1]?.trim() || '3.6';
  2078. const latestSyntaxDoc = normalizeStringPatches(docs.syntax.content, latestVlVersion);
  2079. const latestThemeContent = normalizeStringPatches(docs.theme.content, latestVlVersion);
  2080. const digest = buildLatestDigest({ latestVlVersion, themeTitle: docs.theme.title });
  2081. const resumeExisting = process.env.VL_RESUME_EXISTING === '1';
  2082. const baseProjectNames = {
  2083. 'direct-full': `CampusOpsDirectLatest${DATE_SLUG}Test`,
  2084. 'meta-first': `CampusOpsMetaLatest${DATE_SLUG}Test`,
  2085. '3-file': `CampusOps3FileLatest${DATE_SLUG}Test`,
  2086. '6-file': `CampusOps6FileLatest${DATE_SLUG}Test`,
  2087. '9-file': `CampusOps9FileLatest${DATE_SLUG}Test`,
  2088. };
  2089. const projects = Object.fromEntries(
  2090. Object.entries(baseProjectNames).map(([method, baseName]) => {
  2091. const existing = resumeExisting ? findLatestExistingProject(baseName, true) : null;
  2092. return [
  2093. method,
  2094. existing || path.join(TEST_ROOT, projectNameWithFallback(baseName)),
  2095. ];
  2096. })
  2097. );
  2098. const runs = {};
  2099. const analyses = {};
  2100. if (resumeExisting && projectLooksComplete(projects['direct-full'])) {
  2101. runs['direct-full'] = estimateRunFromFiles(projects['direct-full'], 'direct-full');
  2102. } else {
  2103. runs['direct-full'] = await runDirectFullBaseline({
  2104. projectDir: projects['direct-full'],
  2105. digest,
  2106. latestThemeDoc: latestThemeContent,
  2107. latestVlVersion,
  2108. });
  2109. }
  2110. analyses['direct-full'] = await analyzeProject(projects['direct-full'], {
  2111. cookie,
  2112. methodLabel: 'direct-full',
  2113. });
  2114. if (resumeExisting && projectLooksComplete(projects['meta-first'])) {
  2115. runs['meta-first'] = estimateRunFromFiles(projects['meta-first'], 'meta-first');
  2116. } else {
  2117. runs['meta-first'] = await runMetaFirstBaseline({
  2118. projectDir: projects['meta-first'],
  2119. digest,
  2120. latestThemeDoc: latestThemeContent,
  2121. latestVlVersion,
  2122. });
  2123. }
  2124. analyses['meta-first'] = await analyzeProject(projects['meta-first'], {
  2125. cookie,
  2126. methodLabel: 'meta-first',
  2127. });
  2128. for (const method of ['3-file', '6-file', '9-file']) {
  2129. try {
  2130. if (resumeExisting && projectLooksComplete(projects[method])) {
  2131. runs[method] = estimateRunFromFiles(projects[method], method);
  2132. } else {
  2133. runs[method] = await runWorkflowBaseline({
  2134. method,
  2135. projectDir: projects[method],
  2136. cookie,
  2137. latestSyntaxDoc,
  2138. latestThemeContent,
  2139. latestVlVersion,
  2140. });
  2141. }
  2142. } catch (error) {
  2143. runs[method] = {
  2144. method,
  2145. durationMs: null,
  2146. firstArtifactMs: null,
  2147. error: error.message,
  2148. };
  2149. }
  2150. try {
  2151. analyses[method] = await analyzeProject(projects[method], {
  2152. cookie,
  2153. methodLabel: method,
  2154. });
  2155. } catch (error) {
  2156. analyses[method] = buildFailedAnalysis(projects[method], error.message);
  2157. }
  2158. }
  2159. const report = {
  2160. createdAt: new Date().toISOString(),
  2161. model: MODEL,
  2162. requirement: REQUIREMENT,
  2163. docs: {
  2164. syntax: {
  2165. title: docs.syntax.title,
  2166. updatedAt: docs.syntax.updatedAt,
  2167. latestVlVersion,
  2168. },
  2169. theme: {
  2170. title: docs.theme.title,
  2171. updatedAt: docs.theme.updatedAt,
  2172. },
  2173. workflowSpec: {
  2174. title: docs.workflowSpec.title,
  2175. updatedAt: docs.workflowSpec.updatedAt,
  2176. },
  2177. },
  2178. methods: {},
  2179. };
  2180. for (const method of METHOD_ORDER) {
  2181. report.methods[method] = {
  2182. run: runs[method],
  2183. analysis: analyses[method],
  2184. summary: summarizeMethod(method, runs[method], analyses[method]),
  2185. };
  2186. }
  2187. const reportJsonPath = path.join(REPORT_DIR, `vl-native-benchmark-${DATE_SLUG}.json`);
  2188. const reportMdPath = path.join(REPORT_DIR, `vl-native-benchmark-${DATE_SLUG}.md`);
  2189. await fs.writeFile(reportJsonPath, JSON.stringify(report, null, 2), 'utf-8');
  2190. await fs.writeFile(reportMdPath, buildMarkdownReport(report), 'utf-8');
  2191. report.bundle = await createBundle(report, reportJsonPath, reportMdPath);
  2192. await fs.writeFile(reportJsonPath, JSON.stringify(report, null, 2), 'utf-8');
  2193. await fs.writeFile(reportMdPath, buildMarkdownReport(report), 'utf-8');
  2194. console.log(JSON.stringify({
  2195. reportJsonPath,
  2196. reportMdPath,
  2197. zipPath: report.bundle.zipPath,
  2198. latestVlVersion,
  2199. methods: Object.fromEntries(
  2200. METHOD_ORDER.map((method) => [method, report.methods[method].summary])
  2201. ),
  2202. }, null, 2));
  2203. }
  2204. main().catch((error) => {
  2205. console.error('[benchmark-vl-native-generation] failed:', error.message);
  2206. process.exitCode = 1;
  2207. });