| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495 |
- #!/usr/bin/env node
- import fs from 'fs/promises';
- import fsSync from 'fs';
- import os from 'os';
- import path from 'path';
- import { execSync, spawn } from 'child_process';
- import { performance } from 'perf_hooks';
- import { chromium } from 'playwright';
- import { PARSEVL_URL } from '../src/data/versions.js';
- import { getCookie } from '../src/server/helpers.js';
- import { WorkflowExecutor } from '../src/vl/workflow-executor.js';
- import { VLProjectContext } from '../src/vl/project-context.js';
- import { createVLValidateTool } from '../src/tools/vl-validate.js';
- import { extractFromFileTree, validateMeta } from '../src/vl/metadata-extractor.js';
- const MODEL = process.env.VL_CODE_MODEL || 'claude-opus-4-6';
- const REPORT_MODEL = process.env.VL_REPORT_MODEL || MODEL;
- const TARGET_LANG = 'zh-CN';
- const TODAY = new Date().toISOString().slice(0, 10);
- const DATE_SLUG = TODAY.replace(/-/g, '');
- const TEST_ROOT = path.join(os.homedir(), 'Documents', 'VLProjects', '_tests');
- const REPORT_DIR = path.join(process.cwd(), 'docs', 'benchmarks');
- const DOCCENTER_BASE = 'https://v4pre.visuallogic.ai/api/12027022';
- const METHOD_ORDER = ['direct-full', 'meta-first', '3-file', '6-file', '9-file'];
- const DIRECT_FILE_CONCURRENCY = Number(process.env.VL_FILE_CONCURRENCY || '6');
- const METHOD_LABELS = {
- 'direct-full': 'Direct Full',
- 'meta-first': 'Meta First',
- '3-file': 'Workflow 3-File',
- '6-file': 'Workflow 6-File',
- '9-file': 'Workflow 9-File',
- };
- const WORKFLOW_DOC_IDS = {
- '3-file': 8,
- '6-file': 34,
- '9-file': 52,
- };
- const CONTRACT = {
- projectName: 'CampusOps',
- projectDescription: 'Desktop-first and mobile-friendly multi-campus operations cockpit for facilities dispatchers and supervisors.',
- databaseFile: 'Database/CampusOps.vdb',
- themeFile: 'Theme/Theme.vth',
- app: {
- id: 'CampusOpsApp',
- filePath: 'Apps/CampusOpsApp.vx',
- routes: [
- { path: 'overview', sectionId: 'OverviewPage' },
- { path: 'schedule', sectionId: 'ScheduleBoard' },
- { path: 'work-orders', sectionId: 'WorkOrderDesk' },
- { path: 'alerts', sectionId: 'AlertCenter' },
- { path: 'settings', sectionId: 'SettingsPage' },
- ],
- },
- tables: [
- {
- id: 'Campus',
- fields: [
- { name: 'name', type: 'STRING' },
- { name: 'region', type: 'STRING' },
- { name: 'manager', type: 'STRING' },
- { name: 'activeAlerts', type: 'INT' },
- ],
- },
- {
- id: 'Technician',
- fields: [
- { name: 'name', type: 'STRING' },
- { name: 'campusId', type: 'INT' },
- { name: 'skillTag', type: 'STRING' },
- { name: 'shiftStatus', type: 'STRING' },
- { name: 'utilizationRate', type: 'FLOAT' },
- ],
- },
- {
- id: 'WorkOrder',
- fields: [
- { name: 'campusId', type: 'INT' },
- { name: 'title', type: 'STRING' },
- { name: 'priority', type: 'STRING' },
- { name: 'status', type: 'STRING' },
- { name: 'assigneeId', type: 'INT' },
- { name: 'slaHours', type: 'INT' },
- ],
- },
- {
- id: 'AlertRule',
- fields: [
- { name: 'campusId', type: 'INT' },
- { name: 'ruleName', type: 'STRING' },
- { name: 'thresholdValue', type: 'FLOAT' },
- { name: 'enabled', type: 'BOOL' },
- ],
- },
- {
- id: 'AlertEvent',
- fields: [
- { name: 'campusId', type: 'INT' },
- { name: 'ruleId', type: 'INT' },
- { name: 'severity', type: 'STRING' },
- { name: 'status', type: 'STRING' },
- { name: 'message', type: 'STRING' },
- ],
- },
- {
- id: 'UserPreference',
- fields: [
- { name: 'density', type: 'STRING' },
- { name: 'defaultCampusId', type: 'INT' },
- { name: 'emailDigest', type: 'BOOL' },
- ],
- },
- ],
- services: [
- {
- domainId: 'OperationsOverview',
- filePath: 'Services/OperationsOverview.vs',
- purpose: 'dashboard KPIs and summary cards',
- methods: [
- { id: 'GetOverviewMetrics', params: 'campusId(INT)', returns: '{success:BOOL,data:OBJECT}' },
- ],
- },
- {
- domainId: 'ScheduleService',
- filePath: 'Services/ScheduleService.vs',
- purpose: 'technician assignment list and shift filters',
- methods: [
- { id: 'ListAssignments', params: 'campusId(INT),shiftStatus(STRING)', returns: '{success:BOOL,data:[{}]}' },
- ],
- },
- {
- domainId: 'WorkOrderService',
- filePath: 'Services/WorkOrderService.vs',
- purpose: 'work order list and status updates',
- methods: [
- { id: 'ListWorkOrders', params: 'campusId(INT),priority(STRING),status(STRING)', returns: '{success:BOOL,data:[{}]}' },
- { id: 'UpdateWorkOrderStatus', params: 'workOrderId(INT),status(STRING)', returns: '{success:BOOL}' },
- ],
- },
- {
- domainId: 'AlertService',
- filePath: 'Services/AlertService.vs',
- purpose: 'alert event list and acknowledgement',
- methods: [
- { id: 'ListAlerts', params: 'campusId(INT),severity(STRING),status(STRING)', returns: '{success:BOOL,data:[{}]}' },
- { id: 'AcknowledgeAlert', params: 'alertId(INT)', returns: '{success:BOOL}' },
- ],
- },
- {
- domainId: 'SettingsService',
- filePath: 'Services/SettingsService.vs',
- purpose: 'settings and threshold preferences',
- methods: [
- { id: 'GetSettings', params: 'campusId(INT)', returns: '{success:BOOL,data:OBJECT}' },
- { id: 'SaveSettings', params: 'campusId(INT),density(STRING),threshold(FLOAT)', returns: '{success:BOOL}' },
- ],
- },
- ],
- components: [
- {
- id: 'KpiCard',
- filePath: 'ExtComponents/KpiCard.cp',
- purpose: 'title, numeric value, helper text, optional intent tone',
- },
- {
- id: 'StatusPill',
- filePath: 'ExtComponents/StatusPill.cp',
- purpose: 'compact status chip for priority or lifecycle states',
- },
- {
- id: 'FilterToolbar',
- filePath: 'ExtComponents/FilterToolbar.cp',
- purpose: 'filter row with campus and status selectors plus clear action',
- },
- {
- id: 'AlertListItem',
- filePath: 'ExtComponents/AlertListItem.cp',
- purpose: 'alert row with severity, message, meta, and acknowledge button',
- },
- ],
- sections: [
- {
- id: 'OverviewPage',
- filePath: 'Sections/OverviewPage.sc',
- consumesServices: ['OperationsOverview.GetOverviewMetrics'],
- usesComponents: ['KpiCard'],
- purpose: 'overview dashboard with KPI cards and campus summary rows',
- },
- {
- id: 'ScheduleBoard',
- filePath: 'Sections/ScheduleBoard.sc',
- consumesServices: ['ScheduleService.ListAssignments'],
- usesComponents: ['FilterToolbar', 'StatusPill'],
- purpose: 'schedule and technician assignment table',
- },
- {
- id: 'WorkOrderDesk',
- filePath: 'Sections/WorkOrderDesk.sc',
- consumesServices: ['WorkOrderService.ListWorkOrders', 'WorkOrderService.UpdateWorkOrderStatus'],
- usesComponents: ['FilterToolbar', 'StatusPill'],
- purpose: 'work order list with status change action',
- },
- {
- id: 'AlertCenter',
- filePath: 'Sections/AlertCenter.sc',
- consumesServices: ['AlertService.ListAlerts', 'AlertService.AcknowledgeAlert'],
- usesComponents: ['FilterToolbar', 'AlertListItem'],
- purpose: 'alert center with severity list and acknowledgement actions',
- },
- {
- id: 'SettingsPage',
- filePath: 'Sections/SettingsPage.sc',
- consumesServices: ['SettingsService.GetSettings', 'SettingsService.SaveSettings'],
- usesComponents: ['StatusPill'],
- purpose: 'alert threshold and density settings form',
- },
- ],
- };
- const EXPECTED_PATHS = [
- CONTRACT.databaseFile,
- CONTRACT.themeFile,
- ...CONTRACT.services.map((item) => item.filePath),
- ...CONTRACT.components.map((item) => item.filePath),
- ...CONTRACT.sections.map((item) => item.filePath),
- CONTRACT.app.filePath,
- ];
- const REQUIREMENT = `
- Build a desktop-first and mobile-friendly operations cockpit called CampusOps for a multi-campus facilities team.
- Business scope:
- - Roles: dispatcher and supervisor.
- - Pages: overview, schedule, work-orders, alerts, settings.
- - Data entities: Campus, Technician, WorkOrder, AlertRule, AlertEvent, UserPreference.
- - Key interactions:
- - Overview shows KPI cards for open work orders, overdue SLA, active alerts, and technician utilization.
- - Schedule page lists technician assignments by campus and lets users filter by campus, technician, and shift status.
- - Work order page lists work orders with filters by campus, priority, and status, and supports changing the order status.
- - Alert center lists alert events with severity, source campus, and acknowledgement actions.
- - Settings page edits alert thresholds and dashboard density preferences.
- Design direction:
- - Theme should feel enterprise and operational: deep teal primary, slate surfaces, amber warning, red danger, soft elevated cards, pill filters.
- - Prefer card/list/table layouts instead of advanced chart widgets.
- - Keep interactions compile-safe and easy to preview.
- Exact naming contract:
- - Project name: CampusOps
- - Database file: Database/CampusOps.vdb
- - Theme file: Theme/Theme.vth
- - Service files:
- - Services/OperationsOverview.vs
- - Services/ScheduleService.vs
- - Services/WorkOrderService.vs
- - Services/AlertService.vs
- - Services/SettingsService.vs
- - Component files:
- - ExtComponents/KpiCard.cp
- - ExtComponents/StatusPill.cp
- - ExtComponents/FilterToolbar.cp
- - ExtComponents/AlertListItem.cp
- - Section files:
- - Sections/OverviewPage.sc
- - Sections/ScheduleBoard.sc
- - Sections/WorkOrderDesk.sc
- - Sections/AlertCenter.sc
- - Sections/SettingsPage.sc
- - App file: Apps/CampusOpsApp.vx
- Use the current latest VL syntax and current latest THEME 6.5.
- Output compile-safe code only.
- `.trim();
- function normalizeCookie(cookie) {
- if (!cookie) return '';
- return String(cookie).startsWith('ih5bearer=') ? String(cookie) : `ih5bearer=${cookie}`;
- }
- function projectNameWithFallback(baseName) {
- let candidate = baseName;
- let n = 2;
- while (fsSync.existsSync(path.join(TEST_ROOT, candidate))) {
- candidate = baseName.replace(/Test$/, `Run${n}Test`);
- n += 1;
- }
- return candidate;
- }
- async function ensureProjectScaffold(projectDir) {
- for (const rel of ['Apps', 'Sections', 'ExtComponents', 'Services', 'Database', 'Theme', 'Process', '.vl-code']) {
- await fs.mkdir(path.join(projectDir, rel), { recursive: true });
- }
- }
- async function fetchDocInfo(docId, cookie) {
- const res = await fetch(`${DOCCENTER_BASE}/SERVICE_DocCenter_GetDocById`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Cookie': normalizeCookie(cookie),
- },
- body: JSON.stringify({ docId }),
- });
- const data = await res.json();
- const doc = data?.data || {};
- return {
- docId,
- title: doc.name || '',
- updatedAt: doc._update || '',
- content: String(doc.currentContent || doc.content || ''),
- };
- }
- function stripCodeFences(text) {
- const trimmed = String(text || '').trim();
- const fence = trimmed.match(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/);
- return fence ? fence[1].trim() : trimmed;
- }
- async function runClaudePrompt(prompt, { systemPrompt = '', model = MODEL, timeoutMs = 10 * 60 * 1000 } = {}) {
- return await new Promise((resolve, reject) => {
- const args = ['--print', '--no-session-persistence', '--model', model, '--tools', ''];
- if (systemPrompt) args.push('--system-prompt', systemPrompt);
- const env = { ...process.env, NO_PROXY: 'localhost,127.0.0.1,::1' };
- delete env.CLAUDECODE;
- const proc = spawn('claude', args, {
- stdio: ['pipe', 'pipe', 'pipe'],
- env,
- });
- let stdout = '';
- let stderr = '';
- let finished = false;
- const timer = setTimeout(() => {
- proc.kill('SIGTERM');
- reject(new Error(`claude prompt timed out after ${Math.round(timeoutMs / 1000)}s`));
- }, timeoutMs);
- proc.stdout.on('data', (chunk) => {
- stdout += chunk.toString();
- });
- proc.stderr.on('data', (chunk) => {
- stderr += chunk.toString();
- });
- proc.on('error', (err) => {
- if (finished) return;
- finished = true;
- clearTimeout(timer);
- reject(err);
- });
- proc.on('close', (code) => {
- if (finished) return;
- finished = true;
- clearTimeout(timer);
- if (code !== 0) {
- reject(new Error(`claude exited with code ${code}: ${stderr.slice(0, 800)}`));
- return;
- }
- resolve(stdout.trim());
- });
- proc.stdin.write(prompt);
- proc.stdin.end();
- });
- }
- function extractJson(text) {
- const candidate = stripCodeFences(text);
- try {
- return JSON.parse(candidate);
- } catch {}
- const firstBrace = candidate.indexOf('{');
- const lastBrace = candidate.lastIndexOf('}');
- if (firstBrace >= 0 && lastBrace > firstBrace) {
- return JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
- }
- throw new Error('could not extract JSON from response');
- }
- function normalizeStringPatches(text, latestVlVersion) {
- return String(text || '')
- .replace(/VL_VERSION:3\.5/g, `VL_VERSION:${latestVlVersion}`)
- .replace(/\bVL 3\.5\b/g, `VL ${latestVlVersion}`)
- .replace(/\bVL \(Visual Language\) v3\.5\b/g, `VL (Visual Language) v${latestVlVersion}`)
- .replace(/"vlVersion":\s*"3\.5"/g, `"vlVersion": "${latestVlVersion}"`)
- .replace(/vlVersion:\s*'3\.5'/g, `vlVersion: '${latestVlVersion}'`);
- }
- function deepPatchStrings(value, latestVlVersion) {
- if (typeof value === 'string') return normalizeStringPatches(value, latestVlVersion);
- if (Array.isArray(value)) return value.map((item) => deepPatchStrings(item, latestVlVersion));
- if (!value || typeof value !== 'object') return value;
- const out = {};
- for (const [key, inner] of Object.entries(value)) {
- out[key] = deepPatchStrings(inner, latestVlVersion);
- }
- return out;
- }
- function buildLatestDigest({ latestVlVersion, themeTitle }) {
- return `
- Latest reference baseline:
- - Latest DocCenter VL syntax document reports version ${latestVlVersion}.
- - Latest theme document is ${themeTitle}.
- - Use current latest syntax header // VL_VERSION:${latestVlVersion}.
- Essential VL rules distilled from the latest docs:
- - File types: .vx App, .sc Section, .cp Component, .vs ServiceDomain, .vdb Database, .vth Theme.
- - Cross references: App -> Section/Component only; Section -> ServiceDomain/Component only; Service and Component do not cross-reference others.
- - Indentation uses leading hyphens, never spaces.
- - App required section order: SysConfig, Frontend Global Vars, Frontend Derived Vars, Frontend Tree, Frontend Event Handlers, Frontend Internal Methods, Frontend Pipeline Funcs.
- - 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.
- - Component required section order: Frontend Public Props, Frontend Public Events, Frontend Derived Vars, Frontend Tree, Frontend Event Handlers, Frontend Internal Methods, Frontend Pipeline Funcs.
- - ServiceDomain required section order: Backend Environment Vars, Backend Tree, Services, Backend Event Handlers, Transactions, Backend Internal Methods, Backend Pipeline Funcs.
- - Theme file order: # Meta -> optional # Design Tokens -> # Point Slot Values -> optional # Overrides.
- - The theme heading must be exactly # Point Slot Values. Never use legacy # Coordinate Values.
- - Style values must stay compile-safe. Prefer static string literals in style slots and simple widgets/layouts.
- - Do not invent unsupported widgets or speculative syntax.
- `.trim();
- }
- async function mapLimit(items, limit, iterator) {
- const results = new Array(items.length);
- let nextIndex = 0;
- async function worker() {
- while (nextIndex < items.length) {
- const current = nextIndex;
- nextIndex += 1;
- results[current] = await iterator(items[current], current);
- }
- }
- const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, () => worker());
- await Promise.all(workers);
- return results;
- }
- function buildDirectContract() {
- return {
- projectName: CONTRACT.projectName,
- projectDescription: CONTRACT.projectDescription,
- database: {
- filePath: CONTRACT.databaseFile,
- tables: CONTRACT.tables,
- },
- theme: {
- filePath: CONTRACT.themeFile,
- style: 'enterprise-light',
- direction: 'deep teal primary, slate neutrals, amber warning, red danger, soft elevated cards, pill filters',
- },
- services: CONTRACT.services,
- components: CONTRACT.components,
- sections: CONTRACT.sections,
- app: CONTRACT.app,
- };
- }
- function directContextSlice(kind, target) {
- if (kind === 'database') {
- return {
- filePath: target.filePath,
- tables: CONTRACT.tables,
- };
- }
- if (kind === 'theme') {
- return {
- filePath: target.filePath,
- themeName: 'CampusOps',
- direction: 'deep teal primary, slate surfaces, amber warning, red danger, soft elevated cards, pill filters',
- requiredMeta: {
- mode: 'light',
- version: '6.5.0',
- styleSpaceVersion: '1.6',
- base_theme: 'Platform/Theme-Default-Light@1',
- profile: 'enterprise',
- },
- requiredGroups: ['intent', 'emphasis', 'shape', 'surface', 'textRole', 'state'],
- };
- }
- if (kind === 'service') {
- return {
- filePath: target.filePath,
- service: target,
- databaseTables: CONTRACT.tables.map((table) => ({ id: table.id, fields: table.fields })),
- };
- }
- if (kind === 'component') {
- return {
- filePath: target.filePath,
- component: target,
- themeDirection: 'enterprise-light, operational, card/list/table friendly',
- };
- }
- if (kind === 'section') {
- return {
- filePath: target.filePath,
- section: target,
- services: CONTRACT.services.filter((service) =>
- target.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`))
- ),
- components: CONTRACT.components.filter((component) => target.usesComponents?.includes(component.id)),
- };
- }
- if (kind === 'app') {
- return {
- filePath: target.filePath,
- app: target,
- sections: CONTRACT.sections.map((section) => ({
- id: section.id,
- filePath: section.filePath,
- purpose: section.purpose,
- })),
- };
- }
- return { filePath: target.filePath };
- }
- function metaContextSlice(kind, target, meta) {
- if (kind === 'database') {
- return {
- filePath: target.filePath,
- database: meta.database,
- dataSchema: meta.dataSchema,
- };
- }
- if (kind === 'theme') {
- return {
- filePath: target.filePath,
- theme: meta.theme,
- direction: 'enterprise-light, operational, teal/slate/amber/red',
- requiredMeta: {
- mode: 'light',
- version: '6.5.0',
- styleSpaceVersion: '1.6',
- base_theme: 'Platform/Theme-Default-Light@1',
- profile: 'enterprise',
- },
- };
- }
- if (kind === 'service') {
- return {
- filePath: target.filePath,
- service: target,
- dataSchema: meta.dataSchema,
- };
- }
- if (kind === 'component') {
- return {
- filePath: target.filePath,
- component: target,
- };
- }
- if (kind === 'section') {
- return {
- filePath: target.filePath,
- section: target,
- services: (meta.services || []).filter((service) =>
- target.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`))
- ),
- components: (meta.components || []).filter((component) => target.usesComponents?.includes(component.id)),
- };
- }
- if (kind === 'app') {
- return {
- filePath: target.filePath,
- app: target,
- sections: meta.sections || [],
- };
- }
- return { filePath: target.filePath };
- }
- function uniqueBy(items, keyFn) {
- const seen = new Set();
- const out = [];
- for (const item of items || []) {
- const key = keyFn(item);
- if (seen.has(key)) continue;
- seen.add(key);
- out.push(item);
- }
- return out;
- }
- function directBatchContextSlice(kind, targets) {
- if (kind === 'service') {
- return {
- services: targets,
- databaseTables: CONTRACT.tables.map((table) => ({ id: table.id, fields: table.fields })),
- };
- }
- if (kind === 'component') {
- return {
- components: targets,
- themeDirection: 'enterprise-light, operational, card/list/table friendly',
- };
- }
- if (kind === 'section') {
- const services = uniqueBy(
- CONTRACT.services.filter((service) =>
- targets.some((section) => section.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`)))
- ),
- (service) => service.domainId,
- );
- const components = uniqueBy(
- CONTRACT.components.filter((component) =>
- targets.some((section) => section.usesComponents?.includes(component.id))
- ),
- (component) => component.id,
- );
- return { sections: targets, services, components };
- }
- return { targets };
- }
- function metaBatchContextSlice(kind, targets, meta) {
- if (kind === 'service') {
- return {
- services: targets,
- dataSchema: meta.dataSchema,
- };
- }
- if (kind === 'component') {
- return {
- components: targets,
- };
- }
- if (kind === 'section') {
- const services = uniqueBy(
- (meta.services || []).filter((service) =>
- targets.some((section) => section.consumesServices?.some((entry) => String(entry).startsWith(`${service.domainId}.`)))
- ),
- (service) => service.domainId,
- );
- const components = uniqueBy(
- (meta.components || []).filter((component) =>
- targets.some((section) => section.usesComponents?.includes(component.id))
- ),
- (component) => component.id,
- );
- return { sections: targets, services, components };
- }
- return { targets };
- }
- function buildBatchPrompt({ kind, targets, digest, latestVlVersion, context }) {
- const rootContracts = targets.map((target) => {
- const { open, close } = expectedRootInfo(kind, target);
- return `- ${target.filePath}: open ${open} | close ${close}`;
- }).join('\n');
- return `
- You are generating multiple VL ${kind} files for the CampusOps benchmark.
- ${digest}
- Global requirement:
- ${REQUIREMENT}
- Batch context:
- <batch-context>
- ${JSON.stringify(context, null, 2)}
- </batch-context>
- Exact file/root contracts:
- ${rootContracts}
- Output rules:
- - Return ONLY valid JSON.
- - The JSON must be an object mapping exact relative file paths to raw VL source strings.
- - Include every target file exactly once.
- - Every value must start with // VL_VERSION:${latestVlVersion}
- - Every value must satisfy its exact root contract.
- - Do not include markdown fences or explanations.
- `.trim();
- }
- async function runBatchPhaseWithRepair({
- kind,
- targets,
- projectDir,
- processDir,
- batchName,
- latestVlVersion,
- batchPrompt,
- batchSystemPrompt,
- buildSinglePrompt,
- }) {
- const records = [];
- const repairs = [];
- let batchDurationMs = 0;
- let batchFailed = false;
- try {
- const batch = await generateMappedFiles({
- prompt: batchPrompt,
- projectDir,
- promptPath: path.join(processDir, `${batchName}.prompt.txt`),
- rawPath: path.join(processDir, `${batchName}.raw.txt`),
- systemPrompt: batchSystemPrompt,
- });
- batchDurationMs = batch.durationMs;
- } catch (error) {
- batchFailed = true;
- await fs.writeFile(path.join(processDir, `${batchName}.error.txt`), String(error?.stack || error?.message || error), 'utf-8');
- }
- if (!batchFailed) {
- for (const target of targets) {
- const targetPath = path.join(projectDir, target.filePath);
- let content = '';
- try {
- content = await fs.readFile(targetPath, 'utf-8');
- } catch {
- repairs.push({ target, issues: ['batch output missing target file'] });
- continue;
- }
- const issues = validateGeneratedVlShape({ kind, target, content, latestVlVersion });
- if (issues.length) {
- repairs.push({ target, issues });
- continue;
- }
- records.push({
- kind,
- filePath: target.filePath,
- durationMs: batchDurationMs,
- attempts: 1,
- issues: [],
- });
- }
- } else {
- for (const target of targets) {
- repairs.push({ target, issues: ['batch generation failed'] });
- }
- }
- const repairRecords = await mapLimit(repairs, Math.min(DIRECT_FILE_CONCURRENCY, Math.max(1, repairs.length)), async ({ target, issues }) => {
- const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
- const result = await generateSingleFile({
- kind,
- target,
- latestVlVersion,
- prompt: buildSinglePrompt(target),
- targetPath: path.join(projectDir, target.filePath),
- promptPath: path.join(processDir, `${safeName}.repair.prompt.txt`),
- rawPath: path.join(processDir, `${safeName}.repair.raw.txt`),
- systemPrompt: `Generate only the VL source for ${target.filePath}.`,
- });
- return {
- kind,
- filePath: target.filePath,
- durationMs: batchDurationMs + result.durationMs,
- attempts: 1 + result.attempts,
- issues: [...issues, ...result.issues],
- };
- });
- return [...records, ...repairRecords];
- }
- function singularKind(kind) {
- return String(kind || '').replace(/s$/, '');
- }
- function expectedRootInfo(kind, target) {
- switch (singularKind(kind)) {
- case 'database':
- return { open: '<Database-CampusOps>', close: '</Database-CampusOps>' };
- case 'theme':
- return { open: '<Theme-CampusOps>', close: '</Theme-CampusOps>' };
- case 'service':
- return {
- open: `<ServiceDomain-${target.domainId}>`,
- close: `</ServiceDomain-${target.domainId}>`,
- };
- case 'component':
- return {
- open: `<Component-${target.id} "root">`,
- close: `</Component-${target.id}>`,
- };
- case 'section':
- return {
- open: `<Section-${target.id} "root">`,
- close: `</Section-${target.id}>`,
- };
- case 'app':
- return {
- open: `<App-${target.id} "root">`,
- close: `</App-${target.id}>`,
- };
- default:
- throw new Error(`Unknown kind for root info: ${kind}`);
- }
- }
- function requiredHeadersForKind(kind) {
- switch (singularKind(kind)) {
- case 'service':
- return [
- '# Backend Environment Vars',
- '# Backend Tree',
- '# Services',
- '# Backend Event Handlers',
- '# Transactions',
- '# Backend Internal Methods',
- '# Backend Pipeline Funcs',
- ];
- case 'component':
- return [
- '# Frontend Public Props',
- '# Frontend Public Events',
- '# Frontend Derived Vars',
- '# Frontend Tree',
- '# Frontend Event Handlers',
- '# Frontend Internal Methods',
- '# Frontend Pipeline Funcs',
- ];
- case 'section':
- return [
- '# 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',
- ];
- case 'app':
- return [
- '# SysConfig',
- '# Frontend Global Vars',
- '# Frontend Derived Vars',
- '# Frontend Tree',
- '# Frontend Event Handlers',
- '# Frontend Internal Methods',
- '# Frontend Pipeline Funcs',
- ];
- case 'theme':
- return ['# Meta', '# Point Slot Values'];
- default:
- return [];
- }
- }
- function buildRootGuide(kind, target, latestVlVersion) {
- const { open, close } = expectedRootInfo(kind, target);
- if (singularKind(kind) === 'database') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- ${open}
- <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"}]
- -<Field-name> type:STRING notNull:true
- ${close}
- `.trim();
- }
- if (singularKind(kind) === 'theme') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- ${open}
- # Meta
- mode:"light"
- version:"6.5.0"
- base_theme:"Platform/Theme-Default-Light@1"
- # Point Slot Values
- intent.primary.intentBg:#0D9488
- # Overrides
- ${close}
- `.trim();
- }
- if (singularKind(kind) === 'service') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- - Do not split the root into "<ServiceDomain> ${target.domainId}".
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- ${open}
- # Backend Environment Vars
- # Backend Tree
- # Services
- SERVICE Example();RETURN {success:BOOL}
- -RETURN {success:true}
- # Backend Event Handlers
- # Transactions
- # Backend Internal Methods
- # Backend Pipeline Funcs
- ${close}
- `.trim();
- }
- if (singularKind(kind) === 'component') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- // Preview: width:220px height:120px
- ${open} padding:"16px"
- # Frontend Public Props
- $title(STRING) = ""
- # Frontend Public Events
- # Frontend Derived Vars
- # Frontend Tree
- <Text-Title> value:$title
- # Frontend Event Handlers
- # Frontend Internal Methods
- # Frontend Pipeline Funcs
- ${close}
- `.trim();
- }
- if (singularKind(kind) === 'section') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- ${open} padding:"24px"
- # Frontend Public Props
- # Frontend Public Events
- # Frontend Public Methods
- # Frontend Global Vars
- $isLoading(BOOL) = false
- # Frontend Derived Vars
- # Frontend Tree
- <Col-Main>
- -<Text-Title> value:"Example"
- # Frontend Event Handlers
- ${open}.@init()
- -init()
- # Frontend Internal Methods
- METHOD init(); RETURN {success:BOOL}
- -RETURN {success:true}
- # Frontend Pipeline Funcs
- ${close}
- `.trim();
- }
- if (singularKind(kind) === 'app') {
- return `
- Exact root contract for this file:
- - Opening root must start with exactly: ${open}
- - Closing root must be exactly: ${close}
- Minimal shape example:
- // VL_VERSION:${latestVlVersion}
- ${open}
- # SysConfig
- DEVICE_TARGET:"PC"
- SCREEN_RESOLUTION:"1440x900"
- # Frontend Global Vars
- $currentRoute(STRING) = "overview"
- # Frontend Derived Vars
- # Frontend Tree
- <Page-Overview "overview"> path:"overview"
- -<Section-OverviewPage "overviewPage">
- # Frontend Event Handlers
- # Frontend Internal Methods
- # Frontend Pipeline Funcs
- ${close}
- `.trim();
- }
- throw new Error(`Unknown kind for root guide: ${kind}`);
- }
- function escapeRegex(text) {
- return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- }
- function validateGeneratedVlShape({ kind, target, content, latestVlVersion }) {
- const issues = [];
- const text = String(content || '').trim();
- const { open, close } = expectedRootInfo(kind, target);
- const lines = text.split('\n');
- if (lines[0]?.trim() !== `// VL_VERSION:${latestVlVersion}`) {
- issues.push(`first line must be // VL_VERSION:${latestVlVersion}`);
- }
- if (!new RegExp(`^${escapeRegex(open)}(?:\\s|$)`, 'm').test(text)) {
- issues.push(`opening root must start with ${open}`);
- }
- if (!new RegExp(`^${escapeRegex(close)}\\s*$`, 'm').test(text)) {
- issues.push(`closing root must be ${close}`);
- }
- const headers = requiredHeadersForKind(kind);
- let cursor = -1;
- for (const header of headers) {
- const index = text.indexOf(header);
- if (index === -1) {
- issues.push(`missing required header ${header}`);
- continue;
- }
- if (index < cursor) {
- issues.push(`header out of order ${header}`);
- }
- cursor = index;
- }
- return issues;
- }
- function buildRepairPrompt({ originalPrompt, kind, target, issues, invalidContent, latestVlVersion }) {
- return `
- ${originalPrompt}
- The previous output for ${target.filePath} was invalid.
- Validation failures:
- ${issues.map((issue) => `- ${issue}`).join('\n')}
- Root and structure reminder:
- ${buildRootGuide(kind, target, latestVlVersion)}
- Previous invalid output:
- <invalid-output>
- ${invalidContent}
- </invalid-output>
- Rewrite the complete file from scratch.
- Return only raw VL source.
- Do not explain anything.
- `.trim();
- }
- function buildDirectFilePrompt({ kind, target, digest, latestThemeDoc, latestVlVersion }) {
- const contextJson = JSON.stringify(directContextSlice(kind, target), null, 2);
- const base = `
- You are generating one VL file for the CampusOps benchmark.
- ${digest}
- Global requirement:
- ${REQUIREMENT}
- File-specific context:
- <file-context>
- ${contextJson}
- </file-context>
- Common output rules:
- - Output only raw VL source code for the requested file.
- - Do not use markdown fences.
- - The first line must be // VL_VERSION:${latestVlVersion}
- - Keep identifiers, routes, and file references exactly aligned with the contract.
- - Use only compile-safe VL constructs.
- - Use real VL root declarations with angle brackets.
- - Never use prose roots like "ServiceDomain Foo" or freeform HTML tags.
- ${buildRootGuide(kind, target, latestVlVersion)}
- `.trim();
- if (kind === 'database') {
- return `${base}
- Target file: ${target.filePath}
- Generate the .vdb file with realistic seed data for all declared tables.`;
- }
- if (kind === 'theme') {
- return `${base}
- Target file: ${target.filePath}
- Generate a custom Theme 6.5 file for CampusOps.
- Do not paste the official theme doc.
- Keep the file concise but complete, compile-safe, and aligned to the required meta fields and slot groups in file-context.`;
- }
- if (kind === 'service') {
- return `${base}
- Target file: ${target.filePath}
- Service contract:
- ${JSON.stringify(target, null, 2)}
- Generate one ServiceDomain file with safe optional filters and realistic mock responses backed by the declared tables.`;
- }
- if (kind === 'component') {
- return `${base}
- Target file: ${target.filePath}
- Component contract:
- ${JSON.stringify(target, null, 2)}
- Generate one reusable pure UI component.`;
- }
- if (kind === 'section') {
- return `${base}
- Target file: ${target.filePath}
- Section contract:
- ${JSON.stringify(target, null, 2)}
- Available services:
- ${JSON.stringify(CONTRACT.services, null, 2)}
- Available components:
- ${JSON.stringify(CONTRACT.components, null, 2)}
- Generate one Section file with local state, service calls, and simple event handlers.`;
- }
- if (kind === 'app') {
- return `${base}
- Target file: ${target.filePath}
- App contract:
- ${JSON.stringify(target, null, 2)}
- Available sections:
- ${JSON.stringify(CONTRACT.sections, null, 2)}
- Generate one App file with a clear sidebar + content layout and route switching.`;
- }
- throw new Error(`Unknown direct generation kind: ${kind}`);
- }
- async function generateSingleFile({ kind, target, latestVlVersion, prompt, targetPath, promptPath, rawPath, systemPrompt }) {
- await fs.mkdir(path.dirname(promptPath), { recursive: true });
- await fs.writeFile(promptPath, prompt, 'utf-8');
- let finalRaw = '';
- let finalContent = '';
- let totalDurationMs = 0;
- let attempts = 0;
- let issues = [];
- for (let attempt = 1; attempt <= 2; attempt += 1) {
- attempts = attempt;
- const attemptPrompt = attempt === 1 ? prompt : buildRepairPrompt({
- originalPrompt: prompt,
- kind,
- target,
- issues,
- invalidContent: finalContent,
- latestVlVersion,
- });
- if (attempt > 1) {
- await fs.writeFile(promptPath.replace(/\.prompt\.txt$/, `.retry${attempt - 1}.prompt.txt`), attemptPrompt, 'utf-8');
- }
- const startedAt = performance.now();
- const raw = await runClaudePrompt(attemptPrompt, { systemPrompt });
- totalDurationMs += Math.round(performance.now() - startedAt);
- finalRaw = raw;
- finalContent = stripCodeFences(raw).trim() + '\n';
- issues = validateGeneratedVlShape({ kind, target, content: finalContent, latestVlVersion });
- if (attempt > 1) {
- await fs.writeFile(rawPath.replace(/\.raw\.txt$/, `.retry${attempt - 1}.raw.txt`), raw, 'utf-8');
- }
- if (!issues.length) break;
- }
- await fs.writeFile(rawPath, finalRaw, 'utf-8');
- await fs.mkdir(path.dirname(targetPath), { recursive: true });
- await fs.writeFile(targetPath, finalContent, 'utf-8');
- return {
- durationMs: totalDurationMs,
- attempts,
- issues,
- };
- }
- function coerceFileMap(parsed) {
- if (!parsed || typeof parsed !== 'object') {
- throw new Error('expected a JSON object for file map output');
- }
- if (parsed.files && typeof parsed.files === 'object' && !Array.isArray(parsed.files)) {
- return parsed.files;
- }
- if (parsed.artifacts && typeof parsed.artifacts === 'object' && !Array.isArray(parsed.artifacts)) {
- return parsed.artifacts;
- }
- if (Array.isArray(parsed.files)) {
- return Object.fromEntries(
- parsed.files
- .filter((item) => item && typeof item.path === 'string' && typeof item.content === 'string')
- .map((item) => [item.path, item.content])
- );
- }
- return parsed;
- }
- async function writeObjectFiles(projectDir, filesMap) {
- const written = [];
- for (const [relPath, content] of Object.entries(filesMap || {})) {
- if (typeof content !== 'string') continue;
- const target = path.join(projectDir, relPath);
- await fs.mkdir(path.dirname(target), { recursive: true });
- await fs.writeFile(target, content.trim() + '\n', 'utf-8');
- written.push(relPath);
- }
- return written.sort();
- }
- async function generateMappedFiles({ prompt, projectDir, promptPath, rawPath, systemPrompt }) {
- const startedAt = performance.now();
- const raw = await runClaudePrompt(prompt, { systemPrompt });
- const durationMs = Math.round(performance.now() - startedAt);
- await fs.mkdir(path.dirname(promptPath), { recursive: true });
- await fs.writeFile(promptPath, prompt, 'utf-8');
- await fs.writeFile(rawPath, raw, 'utf-8');
- const parsed = extractJson(raw);
- const filesMap = coerceFileMap(parsed);
- const writtenPaths = await writeObjectFiles(projectDir, filesMap);
- return {
- durationMs,
- writtenPaths,
- };
- }
- async function runDirectFullBaseline({ projectDir, digest, latestThemeDoc, latestVlVersion }) {
- await ensureProjectScaffold(projectDir);
- const processDir = path.join(projectDir, 'Process', 'DirectFull');
- const startedAt = performance.now();
- const runSingle = async (kind, target) => {
- const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
- const result = await generateSingleFile({
- kind,
- target,
- latestVlVersion,
- prompt: buildDirectFilePrompt({
- kind,
- target,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- targetPath: path.join(projectDir, target.filePath),
- promptPath: path.join(processDir, `${safeName}.prompt.txt`),
- rawPath: path.join(processDir, `${safeName}.raw.txt`),
- systemPrompt: `Generate only the VL source for ${target.filePath}.`,
- });
- return {
- kind,
- filePath: target.filePath,
- durationMs: result.durationMs,
- attempts: result.attempts,
- issues: result.issues,
- };
- };
- const [databaseRecord, themeRecord, serviceRecords, componentRecords, sectionRecords, appRecord] = await Promise.all([
- runSingle('database', { filePath: CONTRACT.databaseFile }),
- runSingle('theme', { filePath: CONTRACT.themeFile }),
- runBatchPhaseWithRepair({
- kind: 'service',
- targets: CONTRACT.services,
- projectDir,
- processDir,
- batchName: 'services.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'service',
- targets: CONTRACT.services,
- digest,
- latestVlVersion,
- context: directBatchContextSlice('service', CONTRACT.services),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested service file path to its VL source.',
- buildSinglePrompt: (target) => buildDirectFilePrompt({
- kind: 'service',
- target,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- runBatchPhaseWithRepair({
- kind: 'component',
- targets: CONTRACT.components,
- projectDir,
- processDir,
- batchName: 'components.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'component',
- targets: CONTRACT.components,
- digest,
- latestVlVersion,
- context: directBatchContextSlice('component', CONTRACT.components),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested component file path to its VL source.',
- buildSinglePrompt: (target) => buildDirectFilePrompt({
- kind: 'component',
- target,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- runBatchPhaseWithRepair({
- kind: 'section',
- targets: CONTRACT.sections,
- projectDir,
- processDir,
- batchName: 'sections.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'section',
- targets: CONTRACT.sections,
- digest,
- latestVlVersion,
- context: directBatchContextSlice('section', CONTRACT.sections),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested section file path to its VL source.',
- buildSinglePrompt: (target) => buildDirectFilePrompt({
- kind: 'section',
- target,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- runSingle('app', CONTRACT.app),
- ]);
- const records = [
- databaseRecord,
- themeRecord,
- ...serviceRecords,
- ...componentRecords,
- ...sectionRecords,
- appRecord,
- ];
- return {
- method: 'direct-full',
- durationMs: Math.round(performance.now() - startedAt),
- firstArtifactMs: records.length ? Math.min(...records.map((item) => item.durationMs)) : null,
- fileStats: records,
- };
- }
- function buildMetaPrompt({ digest, latestVlVersion }) {
- return `
- Generate the complete ProjectMeta JSON for the CampusOps benchmark.
- ${digest}
- Global requirement:
- ${REQUIREMENT}
- Exact schema and naming requirements:
- - specVersion must be "ProjectMeta/1.0"
- - projectName must be "CampusOps"
- - vlVersion must be "${latestVlVersion}"
- - config.themeFile must be "Theme/Theme.vth"
- - database.file must be "Database/CampusOps.vdb"
- - theme.file must be "Theme/Theme.vth"
- - Service domains must be exactly: OperationsOverview, ScheduleService, WorkOrderService, AlertService, SettingsService
- - Components must be exactly: KpiCard, StatusPill, FilterToolbar, AlertListItem
- - Sections must be exactly: OverviewPage, ScheduleBoard, WorkOrderDesk, AlertCenter, SettingsPage
- - App must be exactly: CampusOpsApp
- - Every service/component/section/app must include its exact filePath
- - Every section must include explicit consumesServices and usesComponents arrays
- - App routes must point to the declared sections
- Required top-level keys:
- {
- "specVersion": "ProjectMeta/1.0",
- "projectName": "CampusOps",
- "projectDescription": "...",
- "vlVersion": "${latestVlVersion}",
- "config": {...},
- "database": {...},
- "theme": {...},
- "dataSchema": {...},
- "services": [...],
- "components": [...],
- "sections": [...],
- "apps": [...]
- }
- Output ONLY valid JSON.
- `.trim();
- }
- function normalizeProjectMeta(meta, latestVlVersion) {
- const out = meta && typeof meta === 'object' ? structuredClone(meta) : {};
- out.specVersion = 'ProjectMeta/1.0';
- out.projectName = CONTRACT.projectName;
- out.projectDescription = out.projectDescription || CONTRACT.projectDescription;
- out.vlVersion = latestVlVersion;
- out.config = out.config || {};
- out.config.themeFile = CONTRACT.themeFile;
- out.config.defaultDevice = out.config.defaultDevice || 'PC';
- out.config.defaultResolution = out.config.defaultResolution || '1440x900';
- out.config.colorScheme = out.config.colorScheme || 'light';
- out.database = out.database || {};
- out.database.file = CONTRACT.databaseFile;
- out.theme = out.theme || {};
- out.theme.file = CONTRACT.themeFile;
- out.dataSchema = out.dataSchema || {};
- out.dataSchema.tables = Array.isArray(out.dataSchema.tables) ? out.dataSchema.tables : [];
- out.dataSchema.relations = Array.isArray(out.dataSchema.relations) ? out.dataSchema.relations : [];
- out.services = Array.isArray(out.services) ? out.services : [];
- out.components = Array.isArray(out.components) ? out.components : [];
- out.sections = Array.isArray(out.sections) ? out.sections : [];
- out.apps = Array.isArray(out.apps) ? out.apps : [];
- const servicePaths = new Map(CONTRACT.services.map((item) => [item.domainId, item.filePath]));
- const componentPaths = new Map(CONTRACT.components.map((item) => [item.id, item.filePath]));
- const sectionPaths = new Map(CONTRACT.sections.map((item) => [item.id, item.filePath]));
- out.services = out.services.map((item) => {
- const domainId = item.domainId || item.id || '';
- return {
- ...item,
- domainId,
- filePath: item.filePath || servicePaths.get(domainId) || `Services/${domainId}.vs`,
- methods: Array.isArray(item.methods) ? item.methods : [],
- virtualTables: Array.isArray(item.virtualTables) ? item.virtualTables : [],
- };
- });
- out.components = out.components.map((item) => {
- const id = item.id || item.componentId || '';
- return {
- ...item,
- id,
- filePath: item.filePath || componentPaths.get(id) || `ExtComponents/${id}.cp`,
- props: Array.isArray(item.props) ? item.props : [],
- slots: Array.isArray(item.slots) ? item.slots : [],
- };
- });
- out.sections = out.sections.map((item) => {
- const id = item.id || item.sectionId || '';
- return {
- ...item,
- id,
- filePath: item.filePath || sectionPaths.get(id) || `Sections/${id}.sc`,
- consumesServices: Array.isArray(item.consumesServices) ? item.consumesServices : [],
- usesComponents: Array.isArray(item.usesComponents) ? item.usesComponents : [],
- bindings: Array.isArray(item.bindings) ? item.bindings : [],
- events: Array.isArray(item.events) ? item.events : [],
- };
- });
- out.apps = out.apps.map((item) => {
- const id = item.id || item.appId || '';
- return {
- ...item,
- id,
- filePath: item.filePath || (id ? `Apps/${id}.vx` : CONTRACT.app.filePath),
- routes: Array.isArray(item.routes) ? item.routes : [],
- layout: item.layout && typeof item.layout === 'object' ? item.layout : {},
- };
- });
- return out;
- }
- function buildMetaBasedFilePrompt({ kind, target, meta, digest, latestThemeDoc, latestVlVersion }) {
- const contextJson = JSON.stringify(metaContextSlice(kind, target, meta), null, 2);
- const base = `
- You are generating one VL file for the CampusOps benchmark from ProjectMeta.
- ${digest}
- Global requirement:
- ${REQUIREMENT}
- Relevant ProjectMeta slice:
- <project-meta-slice>
- ${contextJson}
- </project-meta-slice>
- Common output rules:
- - Output only raw VL source code for the requested file.
- - Do not use markdown fences.
- - The first line must be // VL_VERSION:${latestVlVersion}
- - Keep identifiers and file references aligned with ProjectMeta.
- - Use compile-safe VL only.
- - Use real VL root declarations with angle brackets.
- - Never use prose roots like "ServiceDomain Foo" or freeform HTML tags.
- ${buildRootGuide(kind, target, latestVlVersion)}
- `.trim();
- if (kind === 'database') {
- return `${base}
- Target file: ${target.filePath}
- Generate the database file that matches the ProjectMeta dataSchema with realistic seed data.`;
- }
- if (kind === 'theme') {
- return `${base}
- Target file: ${target.filePath}
- Generate the Theme 6.5 file for CampusOps.
- Do not paste the official theme doc.
- Keep the file concise but complete, compile-safe, and keep base_theme:"Platform/Theme-Default-Light@1".`;
- }
- if (kind === 'service') {
- return `${base}
- Target file: ${target.filePath}
- Service spec:
- ${JSON.stringify(target, null, 2)}
- Generate one ServiceDomain file based on ProjectMeta.`;
- }
- if (kind === 'component') {
- return `${base}
- Target file: ${target.filePath}
- Component spec:
- ${JSON.stringify(target, null, 2)}
- Generate one pure UI component file.`;
- }
- if (kind === 'section') {
- return `${base}
- Target file: ${target.filePath}
- Section spec:
- ${JSON.stringify(target, null, 2)}
- Generate one Section file with service calls consistent with ProjectMeta.`;
- }
- if (kind === 'app') {
- return `${base}
- Target file: ${target.filePath}
- App spec:
- ${JSON.stringify(target, null, 2)}
- Generate one App file consistent with ProjectMeta routes and section usage.`;
- }
- throw new Error(`Unknown meta-based generation kind: ${kind}`);
- }
- async function runMetaFirstBaseline({ projectDir, digest, latestThemeDoc, latestVlVersion }) {
- await ensureProjectScaffold(projectDir);
- const processDir = path.join(projectDir, 'Process', 'MetaFirst');
- const metaPrompt = buildMetaPrompt({ digest, latestVlVersion });
- const metaRawPath = path.join(processDir, 'ProjectMeta.raw.txt');
- const metaPromptPath = path.join(processDir, 'ProjectMeta.prompt.txt');
- await fs.mkdir(processDir, { recursive: true });
- await fs.writeFile(metaPromptPath, metaPrompt, 'utf-8');
- const metaStartedAt = performance.now();
- const metaRaw = await runClaudePrompt(metaPrompt, {
- systemPrompt: 'Generate only valid ProjectMeta JSON.',
- timeoutMs: 8 * 60 * 1000,
- });
- const metaDurationMs = Math.round(performance.now() - metaStartedAt);
- await fs.writeFile(metaRawPath, metaRaw, 'utf-8');
- const parsedMeta = extractJson(metaRaw);
- const meta = normalizeProjectMeta(parsedMeta, latestVlVersion);
- await fs.writeFile(path.join(projectDir, '.vl-code', 'ProjectMeta.json'), JSON.stringify(meta, null, 2), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', 'ProjectMeta.json'), JSON.stringify(meta, null, 2), 'utf-8');
- const startedAt = performance.now();
- const runSingle = async (kind, target) => {
- const safeName = path.basename(target.filePath).replace(/[^\w.-]/g, '_');
- const result = await generateSingleFile({
- kind,
- target,
- latestVlVersion,
- prompt: buildMetaBasedFilePrompt({
- kind,
- target,
- meta,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- targetPath: path.join(projectDir, target.filePath),
- promptPath: path.join(processDir, `${safeName}.prompt.txt`),
- rawPath: path.join(processDir, `${safeName}.raw.txt`),
- systemPrompt: `Generate only the VL source for ${target.filePath}.`,
- });
- return {
- kind,
- filePath: target.filePath,
- durationMs: result.durationMs,
- attempts: result.attempts,
- issues: result.issues,
- };
- };
- const appPromises = meta.apps.map((target) => runSingle('app', target));
- const [databaseRecord, themeRecord, serviceRecords, componentRecords, sectionRecords, appRecords] = await Promise.all([
- runSingle('database', { filePath: meta.database?.file || CONTRACT.databaseFile }),
- runSingle('theme', { filePath: meta.theme?.file || CONTRACT.themeFile }),
- runBatchPhaseWithRepair({
- kind: 'service',
- targets: meta.services,
- projectDir,
- processDir,
- batchName: 'services.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'service',
- targets: meta.services,
- digest,
- latestVlVersion,
- context: metaBatchContextSlice('service', meta.services, meta),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested service file path to its VL source.',
- buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
- kind: 'service',
- target,
- meta,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- runBatchPhaseWithRepair({
- kind: 'component',
- targets: meta.components,
- projectDir,
- processDir,
- batchName: 'components.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'component',
- targets: meta.components,
- digest,
- latestVlVersion,
- context: metaBatchContextSlice('component', meta.components, meta),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested component file path to its VL source.',
- buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
- kind: 'component',
- target,
- meta,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- runBatchPhaseWithRepair({
- kind: 'section',
- targets: meta.sections,
- projectDir,
- processDir,
- batchName: 'sections.batch',
- latestVlVersion,
- batchPrompt: buildBatchPrompt({
- kind: 'section',
- targets: meta.sections,
- digest,
- latestVlVersion,
- context: metaBatchContextSlice('section', meta.sections, meta),
- }),
- batchSystemPrompt: 'Generate only a JSON object mapping each requested section file path to its VL source.',
- buildSinglePrompt: (target) => buildMetaBasedFilePrompt({
- kind: 'section',
- target,
- meta,
- digest,
- latestThemeDoc,
- latestVlVersion,
- }),
- }),
- Promise.all(appPromises),
- ]);
- const records = [
- databaseRecord,
- themeRecord,
- ...serviceRecords,
- ...componentRecords,
- ...sectionRecords,
- ...appRecords,
- ];
- return {
- method: 'meta-first',
- durationMs: metaDurationMs + Math.round(performance.now() - startedAt),
- firstArtifactMs: metaDurationMs,
- metaDurationMs,
- meta,
- fileStats: records,
- };
- }
- async function fetchWorkflowJson(docId, cookie) {
- const info = await fetchDocInfo(docId, cookie);
- return {
- ...info,
- workflow: JSON.parse(info.content),
- };
- }
- function patchWorkflowForLatest(workflow, { latestVlVersion, latestThemeContent }) {
- const patched = deepPatchStrings(workflow, latestVlVersion);
- for (const step of patched.steps || []) {
- if (/^SeedTheme_/i.test(step.id || '')) {
- step.in = step.in || {};
- step.in.content = latestThemeContent;
- }
- }
- return patched;
- }
- function buildWorkflowRequirement(latestVlVersion) {
- return `
- ${REQUIREMENT}
- Additional generation constraints:
- - Use latest VL syntax header // VL_VERSION:${latestVlVersion} for generated VL source files.
- - Use Theme 6.5 enterprise semantics.
- - Keep the output compile-safe and deterministic.
- `.trim();
- }
- async function runWorkflowBaseline({ method, projectDir, cookie, latestSyntaxDoc, latestThemeContent, latestVlVersion }) {
- await ensureProjectScaffold(projectDir);
- const workflowDoc = await fetchWorkflowJson(WORKFLOW_DOC_IDS[method], cookie);
- const workflow = patchWorkflowForLatest(workflowDoc.workflow, {
- latestVlVersion,
- latestThemeContent,
- });
- await fs.writeFile(path.join(projectDir, 'Process', `${method}.workflow.source.json`), workflowDoc.content, 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${method}.workflow.patched.json`), JSON.stringify(workflow, null, 2), 'utf-8');
- const executor = new WorkflowExecutor({
- workDir: projectDir,
- model: REPORT_MODEL,
- llmProvider: 'cli',
- cookie,
- vlVersion: latestVlVersion,
- });
- const originalResolveDocs = executor._resolveDocCenterDocs.bind(executor);
- executor._resolveDocCenterDocs = async (docs, cb) => {
- await originalResolveDocs(docs, cb);
- for (const key of Object.keys(docs)) {
- if (typeof docs[key] === 'string') {
- docs[key] = normalizeStringPatches(docs[key], latestVlVersion);
- }
- }
- if (Object.prototype.hasOwnProperty.call(docs, '1')) {
- docs['1'] = latestSyntaxDoc;
- }
- };
- const timeline = [];
- const logLines = [];
- const fileEvents = [];
- const startedAt = performance.now();
- await new Promise((resolve, reject) => {
- executor.execute(workflow, {
- userRequest: buildWorkflowRequirement(latestVlVersion),
- userRequirement: buildWorkflowRequirement(latestVlVersion),
- targetLang: TARGET_LANG,
- }, {
- onText: (text) => {
- const line = String(text || '').trim();
- if (line) logLines.push(line);
- },
- onNodeStart: (info) => {
- timeline.push({
- nodeId: info.nodeId,
- title: info.title,
- type: info.type,
- status: 'start',
- atMs: Math.round(performance.now() - startedAt),
- });
- },
- onNodeDone: (info) => {
- timeline.push({
- nodeId: info.nodeId,
- title: info.title,
- type: info.type,
- status: 'done',
- durationMs: info.duration_ms || 0,
- atMs: Math.round(performance.now() - startedAt),
- });
- },
- onNodeError: (info) => {
- timeline.push({
- nodeId: info.nodeId,
- title: info.title,
- type: info.type,
- status: 'error',
- error: info.error || 'unknown error',
- atMs: Math.round(performance.now() - startedAt),
- });
- },
- onFileWritten: (filePath) => {
- fileEvents.push({
- filePath,
- atMs: Math.round(performance.now() - startedAt),
- });
- },
- onDone: (info) => resolve(info),
- onError: (message) => reject(new Error(message || `${method} workflow generation failed`)),
- });
- });
- const durationMs = Math.round(performance.now() - startedAt);
- await fs.writeFile(path.join(projectDir, 'Process', `${method}.timeline.json`), JSON.stringify(timeline, null, 2), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${method}.log.txt`), logLines.join('\n'), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${method}.files.json`), JSON.stringify(fileEvents, null, 2), 'utf-8');
- return {
- method,
- durationMs,
- firstArtifactMs: fileEvents.length ? Math.min(...fileEvents.map((item) => item.atMs)) : null,
- timeline,
- workflowDocTitle: workflowDoc.title,
- workflowDocUpdatedAt: workflowDoc.updatedAt,
- };
- }
- async function packageProjectFiles(projectDir, zipPath) {
- execSync(
- `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}"`,
- { timeout: 30_000 },
- );
- }
- async function runCloudAction(projectDir, cookie, action) {
- const zipPath = path.join(projectDir, `__${action}.zip`);
- try {
- await packageProjectFiles(projectDir, zipPath);
- const zipBuffer = await fs.readFile(zipPath);
- const res = await fetch(`${PARSEVL_URL}/edtfn/parsevl`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Cookie': normalizeCookie(cookie),
- },
- body: JSON.stringify({
- action,
- file: `data:application/zip;base64,${zipBuffer.toString('base64')}`,
- ...(action === 'parsePjt' ? { download: true } : {}),
- projectName: path.basename(projectDir),
- }),
- });
- const json = await res.json();
- const diagnostics = Array.isArray(json?.data?.errList) ? json.data.errList : [];
- const hardErrors = diagnostics.filter((item) => String(item?.level || '').toLowerCase() !== 'warning');
- const warnings = diagnostics.filter((item) => String(item?.level || '').toLowerCase() === 'warning');
- return {
- action,
- httpOk: res.ok,
- code: json?.code ?? null,
- success: res.ok && hardErrors.length === 0 && (json?.code === 0 || json?.code === 200 || json?.success === true || action === 'lintPjt'),
- errCount: hardErrors.length,
- warningCount: warnings.length,
- errList: diagnostics.slice(0, 30),
- previewUrls: json?.data?.appPreviewUrlMap || {},
- message: json?.message || '',
- raw: json,
- };
- } catch (error) {
- return {
- action,
- success: false,
- error: error.message,
- errCount: null,
- warningCount: null,
- errList: [],
- previewUrls: {},
- };
- } finally {
- try {
- await fs.unlink(zipPath);
- } catch {}
- }
- }
- function parseValidationSummary(validationText) {
- const text = String(validationText || '');
- if (/All \d+ VL files passed validation\./.test(text)) {
- return { errors: 0, warnings: 0, raw: text };
- }
- const match = text.match(/Validation:\s+(\d+)\s+errors,\s+(\d+)\s+warnings/i);
- return {
- errors: match ? Number(match[1]) : null,
- warnings: match ? Number(match[2]) : null,
- raw: text,
- };
- }
- async function collectFileContents(projectDir) {
- const result = {};
- async function walk(currentDir, prefix = '') {
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
- for (const entry of entries) {
- if (entry.name.startsWith('.') && entry.name !== '.vl-code') continue;
- const fullPath = path.join(currentDir, entry.name);
- const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
- if (entry.isDirectory()) {
- await walk(fullPath, relPath);
- } else if (/\.(vx|sc|cp|vs|vdb|vth|json|txt|md)$/i.test(entry.name)) {
- try {
- result[relPath] = await fs.readFile(fullPath, 'utf-8');
- } catch {}
- }
- }
- }
- await walk(projectDir);
- return result;
- }
- function countLocByExt(fileMap) {
- const stats = {};
- for (const [relPath, content] of Object.entries(fileMap || {})) {
- const ext = path.extname(relPath) || 'none';
- const lines = String(content || '').split('\n');
- if (!stats[ext]) stats[ext] = { files: 0, lines: 0, nonEmptyLines: 0 };
- stats[ext].files += 1;
- stats[ext].lines += lines.length;
- stats[ext].nonEmptyLines += lines.filter((line) => line.trim()).length;
- }
- return stats;
- }
- async function renderMetadataSnapshot({ projectDir, label, meta }) {
- const browser = await chromium.launch({ headless: true });
- try {
- const page = await browser.newPage({ viewport: { width: 1800, height: 1400 } });
- const viewerUrl = `file://${path.resolve(process.cwd(), 'public', 'metadata-viewer.html')}`;
- await page.goto(viewerUrl);
- await page.evaluate((payload) => {
- window.postMessage({ type: 'loadMetadata', data: payload }, '*');
- }, meta);
- await page.waitForFunction(() => document.querySelectorAll('.node').length > 0, { timeout: 10_000 });
- const counts = await page.evaluate(() => ({
- nodeCount: document.querySelectorAll('.node').length,
- connectionCount: document.querySelectorAll('.conn').length,
- }));
- const screenshotPath = path.join(projectDir, 'Process', `${label}.meta-viewer.png`);
- await page.screenshot({ path: screenshotPath, fullPage: true });
- return {
- success: true,
- ...counts,
- screenshotPath,
- };
- } catch (error) {
- return {
- success: false,
- error: error.message,
- nodeCount: 0,
- connectionCount: 0,
- screenshotPath: null,
- };
- } finally {
- await browser.close();
- }
- }
- function loadSavedMeta(fileMap) {
- for (const relPath of ['.vl-code/ProjectMeta.json', 'Process/ProjectMeta.json']) {
- if (!fileMap[relPath]) continue;
- try {
- const parsed = JSON.parse(fileMap[relPath]);
- if (parsed && typeof parsed === 'object') return parsed;
- } catch {}
- }
- return null;
- }
- async function analyzeProject(projectDir, { cookie, methodLabel }) {
- const ctx = new VLProjectContext(projectDir);
- await ctx.load();
- const validateTool = createVLValidateTool(ctx);
- const validationText = await validateTool.execute({ file_path: 'all' });
- const validation = parseValidationSummary(validationText);
- const hint = await runCloudAction(projectDir, cookie, 'lintPjt');
- const compile = await runCloudAction(projectDir, cookie, 'parsePjt');
- const fileMap = await collectFileContents(projectDir);
- const vlFileMap = Object.fromEntries(
- Object.entries(fileMap).filter(([relPath]) => /\.(vx|sc|cp|vs|vdb|vth)$/i.test(relPath))
- );
- const extractedMeta = extractFromFileTree(vlFileMap, projectDir);
- const metaValidation = validateMeta(extractedMeta);
- const savedMeta = loadSavedMeta(fileMap);
- const metaForRender = savedMeta || extractedMeta;
- const metaRender = await renderMetadataSnapshot({
- projectDir,
- label: methodLabel,
- meta: metaForRender,
- });
- await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.validation.txt`), validation.raw || '', 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.hint.json`), JSON.stringify(hint, null, 2), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.compile.json`), JSON.stringify(compile, null, 2), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.extracted-meta.json`), JSON.stringify(extractedMeta, null, 2), 'utf-8');
- await fs.writeFile(path.join(projectDir, 'Process', `${methodLabel}.meta-render.json`), JSON.stringify(metaRender, null, 2), 'utf-8');
- const actualPaths = Object.keys(vlFileMap).sort();
- const missingPaths = EXPECTED_PATHS.filter((relPath) => !actualPaths.includes(relPath));
- const extraPaths = actualPaths.filter((relPath) => !EXPECTED_PATHS.includes(relPath));
- return {
- projectDir,
- fileCount: actualPaths.length,
- actualPaths,
- missingPaths,
- extraPaths,
- validation,
- hint: {
- success: hint.success,
- errCount: hint.errCount,
- warningCount: hint.warningCount,
- errList: hint.errList,
- },
- compile: {
- success: compile.success,
- errCount: compile.errCount,
- warningCount: compile.warningCount,
- previewUrls: compile.previewUrls,
- errList: compile.errList,
- },
- metadataValid: metaValidation.valid,
- metadataIssues: metaValidation.issues,
- metaRender,
- extractedMetaSummary: {
- projectName: extractedMeta.projectName || null,
- tables: Array.isArray(extractedMeta.dataSchema?.tables) ? extractedMeta.dataSchema.tables.length : 0,
- services: Array.isArray(extractedMeta.services) ? extractedMeta.services.length : 0,
- components: Array.isArray(extractedMeta.components) ? extractedMeta.components.length : 0,
- sections: Array.isArray(extractedMeta.sections) ? extractedMeta.sections.length : 0,
- apps: Array.isArray(extractedMeta.apps) ? extractedMeta.apps.length : 0,
- },
- locByExt: countLocByExt(vlFileMap),
- };
- }
- function summarizeMethod(label, runMeta, analysis) {
- return {
- label: METHOD_LABELS[label] || label,
- durationMs: runMeta.durationMs,
- firstArtifactMs: runMeta.firstArtifactMs ?? null,
- fileCount: analysis.fileCount,
- missingPaths: analysis.missingPaths.length,
- extraPaths: analysis.extraPaths.length,
- localValidationErrors: analysis.validation.errors,
- localValidationWarnings: analysis.validation.warnings,
- hintErrors: analysis.hint.errCount,
- hintWarnings: analysis.hint.warningCount,
- compileSuccess: analysis.compile.success,
- compileErrors: analysis.compile.errCount,
- compileWarnings: analysis.compile.warningCount,
- previewCount: Object.keys(analysis.compile.previewUrls || {}).length,
- metadataValid: analysis.metadataValid,
- metadataIssueCount: analysis.metadataIssues.length,
- metaRenderSuccess: analysis.metaRender.success,
- metaNodeCount: analysis.metaRender.nodeCount,
- metaConnectionCount: analysis.metaRender.connectionCount,
- };
- }
- function buildMarkdownReport(report) {
- const bundleZipPath = report.bundle?.zipPath || 'pending';
- const lines = [
- `# VL Native Generation Benchmark (${TODAY})`,
- '',
- '## Baseline',
- '',
- `- Model: ${REPORT_MODEL}`,
- `- Target language: ${TARGET_LANG}`,
- `- Latest VL syntax doc: ${report.docs.syntax.title} (${report.docs.syntax.updatedAt})`,
- `- Latest THEME doc: ${report.docs.theme.title} (${report.docs.theme.updatedAt})`,
- `- Workflow spec doc: ${report.docs.workflowSpec.title} (${report.docs.workflowSpec.updatedAt})`,
- '',
- '## Requirement',
- '',
- REQUIREMENT,
- '',
- '## Summary',
- '',
- '| Method | Time(ms) | Hint E/W | Compile E/W | Compile OK | Meta Render | Missing | Preview |',
- '| --- | ---: | ---: | ---: | --- | --- | ---: | ---: |',
- ];
- for (const method of METHOD_ORDER) {
- const summary = report.methods[method].summary;
- lines.push(
- `| ${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} |`
- );
- }
- lines.push('');
- lines.push('## Corrections Applied');
- lines.push('');
- lines.push('- Workflow prompt/doc strings containing `3.5` were patched in-memory to the latest syntax version before workflow execution.');
- lines.push('- `SeedTheme` nodes were forced to use the latest Theme 6.5 document content from DocCenter.');
- 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.');
- lines.push('');
- lines.push('## Outputs');
- lines.push('');
- for (const method of METHOD_ORDER) {
- const item = report.methods[method];
- lines.push(`### ${item.summary.label}`);
- lines.push('');
- lines.push(`- Project dir: ${item.analysis.projectDir}`);
- lines.push(`- Screenshot: ${item.analysis.metaRender.screenshotPath || 'n/a'}`);
- lines.push(`- Missing paths: ${item.analysis.missingPaths.join(', ') || 'none'}`);
- lines.push(`- Extra paths: ${item.analysis.extraPaths.join(', ') || 'none'}`);
- lines.push(`- Metadata issues: ${item.analysis.metadataIssues.join(' | ') || 'none'}`);
- lines.push('');
- }
- lines.push('## Bundle');
- lines.push('');
- lines.push(`- Zip: ${bundleZipPath}`);
- lines.push('');
- return lines.join('\n');
- }
- async function copyFileSafe(src, dest) {
- await fs.mkdir(path.dirname(dest), { recursive: true });
- await fs.copyFile(src, dest);
- }
- async function createBundle(report, reportJsonPath, reportMdPath) {
- const bundleReportDir = path.join(TEST_ROOT, `VLNativeBenchmarkReports_${DATE_SLUG}`);
- await fs.rm(bundleReportDir, { recursive: true, force: true });
- await fs.mkdir(bundleReportDir, { recursive: true });
- await copyFileSafe(reportJsonPath, path.join(bundleReportDir, path.basename(reportJsonPath)));
- await copyFileSafe(reportMdPath, path.join(bundleReportDir, path.basename(reportMdPath)));
- const zipPath = path.join(TEST_ROOT, `vl-native-benchmark-${DATE_SLUG}.zip`);
- const bundleItems = [
- ...METHOD_ORDER.map((method) => path.basename(report.methods[method].analysis.projectDir)),
- path.basename(bundleReportDir),
- ];
- execSync(`cd "${TEST_ROOT}" && rm -f "${zipPath}" && zip -qr "${zipPath}" ${bundleItems.map((item) => `"${item}"`).join(' ')}`, {
- timeout: 120_000,
- });
- return {
- zipPath,
- bundleReportDir,
- };
- }
- function findLatestExistingProject(baseName, requireComplete = false) {
- const prefix = baseName.replace(/Test$/, '');
- const matches = fsSync.readdirSync(TEST_ROOT, { withFileTypes: true })
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
- .map((entry) => {
- const fullPath = path.join(TEST_ROOT, entry.name);
- let mtimeMs = 0;
- try {
- mtimeMs = fsSync.statSync(fullPath).mtimeMs;
- } catch {}
- return { fullPath, mtimeMs };
- })
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
- for (const match of matches) {
- if (!requireComplete || projectLooksComplete(match.fullPath)) return match.fullPath;
- }
- return null;
- }
- function projectLooksComplete(projectDir) {
- return EXPECTED_PATHS.every((relPath) => fsSync.existsSync(path.join(projectDir, relPath)));
- }
- function estimateRunFromFiles(projectDir, method) {
- const timestamps = [];
- const artifactTimes = [];
- function visit(dir) {
- if (!fsSync.existsSync(dir)) return;
- for (const entry of fsSync.readdirSync(dir, { withFileTypes: true })) {
- const fullPath = path.join(dir, entry.name);
- if (entry.isDirectory()) {
- visit(fullPath);
- continue;
- }
- if (!/(\.prompt\.txt|\.raw\.txt|\.workflow\.(source|patched)\.json|\.timeline\.json|\.log\.txt|\.files\.json|ProjectMeta\.json)$/i.test(entry.name)) {
- continue;
- }
- try {
- const stat = fsSync.statSync(fullPath);
- timestamps.push(stat.mtimeMs);
- } catch {}
- }
- }
- visit(path.join(projectDir, 'Process'));
- for (const relPath of EXPECTED_PATHS) {
- const fullPath = path.join(projectDir, relPath);
- try {
- artifactTimes.push(fsSync.statSync(fullPath).mtimeMs);
- } catch {}
- }
- const start = timestamps.length ? Math.min(...timestamps) : Date.now();
- const end = timestamps.length ? Math.max(...timestamps) : start;
- const firstArtifact = artifactTimes.length ? Math.min(...artifactTimes) : start;
- return {
- method,
- durationMs: Math.max(0, Math.round(end - start)),
- firstArtifactMs: Math.max(0, Math.round(firstArtifact - start)),
- resumed: true,
- };
- }
- function buildFailedAnalysis(projectDir, error) {
- return {
- projectDir,
- fileCount: 0,
- actualPaths: [],
- missingPaths: [...EXPECTED_PATHS],
- extraPaths: [],
- validation: { errors: null, warnings: null, raw: String(error || '') },
- hint: { success: false, errCount: null, warningCount: null, errList: [] },
- compile: { success: false, errCount: null, warningCount: null, previewUrls: {}, errList: [] },
- metadataValid: false,
- metadataIssues: [String(error || 'analysis failed')],
- metaRender: { success: false, error: String(error || 'analysis failed'), nodeCount: 0, connectionCount: 0, screenshotPath: null },
- extractedMetaSummary: { projectName: null, tables: 0, services: 0, components: 0, sections: 0, apps: 0 },
- locByExt: {},
- };
- }
- async function main() {
- await fs.mkdir(TEST_ROOT, { recursive: true });
- await fs.mkdir(REPORT_DIR, { recursive: true });
- const cookie = getCookie({ workDir: process.cwd(), cookie: '' });
- if (!cookie) {
- throw new Error('No DocCenter/cloud cookie found. Cannot run benchmark.');
- }
- const docs = {
- syntax: await fetchDocInfo(1, cookie),
- theme: await fetchDocInfo(4, cookie),
- workflowSpec: await fetchDocInfo(2, cookie),
- };
- const latestVlVersion = docs.syntax.content.match(/Current version:\s*`\/\/\s*VL_VERSION:([^`]+)`/i)?.[1]?.trim() || '3.6';
- const latestSyntaxDoc = normalizeStringPatches(docs.syntax.content, latestVlVersion);
- const latestThemeContent = normalizeStringPatches(docs.theme.content, latestVlVersion);
- const digest = buildLatestDigest({ latestVlVersion, themeTitle: docs.theme.title });
- const resumeExisting = process.env.VL_RESUME_EXISTING === '1';
- const baseProjectNames = {
- 'direct-full': `CampusOpsDirectLatest${DATE_SLUG}Test`,
- 'meta-first': `CampusOpsMetaLatest${DATE_SLUG}Test`,
- '3-file': `CampusOps3FileLatest${DATE_SLUG}Test`,
- '6-file': `CampusOps6FileLatest${DATE_SLUG}Test`,
- '9-file': `CampusOps9FileLatest${DATE_SLUG}Test`,
- };
- const projects = Object.fromEntries(
- Object.entries(baseProjectNames).map(([method, baseName]) => {
- const existing = resumeExisting ? findLatestExistingProject(baseName, true) : null;
- return [
- method,
- existing || path.join(TEST_ROOT, projectNameWithFallback(baseName)),
- ];
- })
- );
- const runs = {};
- const analyses = {};
- if (resumeExisting && projectLooksComplete(projects['direct-full'])) {
- runs['direct-full'] = estimateRunFromFiles(projects['direct-full'], 'direct-full');
- } else {
- runs['direct-full'] = await runDirectFullBaseline({
- projectDir: projects['direct-full'],
- digest,
- latestThemeDoc: latestThemeContent,
- latestVlVersion,
- });
- }
- analyses['direct-full'] = await analyzeProject(projects['direct-full'], {
- cookie,
- methodLabel: 'direct-full',
- });
- if (resumeExisting && projectLooksComplete(projects['meta-first'])) {
- runs['meta-first'] = estimateRunFromFiles(projects['meta-first'], 'meta-first');
- } else {
- runs['meta-first'] = await runMetaFirstBaseline({
- projectDir: projects['meta-first'],
- digest,
- latestThemeDoc: latestThemeContent,
- latestVlVersion,
- });
- }
- analyses['meta-first'] = await analyzeProject(projects['meta-first'], {
- cookie,
- methodLabel: 'meta-first',
- });
- for (const method of ['3-file', '6-file', '9-file']) {
- try {
- if (resumeExisting && projectLooksComplete(projects[method])) {
- runs[method] = estimateRunFromFiles(projects[method], method);
- } else {
- runs[method] = await runWorkflowBaseline({
- method,
- projectDir: projects[method],
- cookie,
- latestSyntaxDoc,
- latestThemeContent,
- latestVlVersion,
- });
- }
- } catch (error) {
- runs[method] = {
- method,
- durationMs: null,
- firstArtifactMs: null,
- error: error.message,
- };
- }
- try {
- analyses[method] = await analyzeProject(projects[method], {
- cookie,
- methodLabel: method,
- });
- } catch (error) {
- analyses[method] = buildFailedAnalysis(projects[method], error.message);
- }
- }
- const report = {
- createdAt: new Date().toISOString(),
- model: MODEL,
- requirement: REQUIREMENT,
- docs: {
- syntax: {
- title: docs.syntax.title,
- updatedAt: docs.syntax.updatedAt,
- latestVlVersion,
- },
- theme: {
- title: docs.theme.title,
- updatedAt: docs.theme.updatedAt,
- },
- workflowSpec: {
- title: docs.workflowSpec.title,
- updatedAt: docs.workflowSpec.updatedAt,
- },
- },
- methods: {},
- };
- for (const method of METHOD_ORDER) {
- report.methods[method] = {
- run: runs[method],
- analysis: analyses[method],
- summary: summarizeMethod(method, runs[method], analyses[method]),
- };
- }
- const reportJsonPath = path.join(REPORT_DIR, `vl-native-benchmark-${DATE_SLUG}.json`);
- const reportMdPath = path.join(REPORT_DIR, `vl-native-benchmark-${DATE_SLUG}.md`);
- await fs.writeFile(reportJsonPath, JSON.stringify(report, null, 2), 'utf-8');
- await fs.writeFile(reportMdPath, buildMarkdownReport(report), 'utf-8');
- report.bundle = await createBundle(report, reportJsonPath, reportMdPath);
- await fs.writeFile(reportJsonPath, JSON.stringify(report, null, 2), 'utf-8');
- await fs.writeFile(reportMdPath, buildMarkdownReport(report), 'utf-8');
- console.log(JSON.stringify({
- reportJsonPath,
- reportMdPath,
- zipPath: report.bundle.zipPath,
- latestVlVersion,
- methods: Object.fromEntries(
- METHOD_ORDER.map((method) => [method, report.methods[method].summary])
- ),
- }, null, 2));
- }
- main().catch((error) => {
- console.error('[benchmark-vl-native-generation] failed:', error.message);
- process.exitCode = 1;
- });
|