#!/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;
});
