import { existsSync, readFileSync } from 'node:fs' const requiredFiles = [ 'src/app/App.tsx', 'src/pages/login.tsx', 'src/pages/workspace-body.tsx', 'src/pages/workspace-home.tsx', 'src/pages/workspace-explore.tsx', 'src/pages/projects/project-overview.tsx', 'src/pages/projects/project-channel-page.tsx', 'src/pages/projects/project-tasks.tsx', 'src/pages/projects/project-ai.tsx', 'src/pages/projects/project-notes.tsx', 'src/pages/projects/project-cron.tsx', 'src/pages/projects/project-new-channel.tsx', 'src/pages/projects/project-rail.tsx', 'src/pages/projects/project-sidebar.tsx', 'src/pages/projects/project-topbar.tsx', 'src/pages/projects/project-statusbar.tsx', 'src/pages/projects/project-types.ts', 'src/api/client.ts', 'src/api/projects.ts', 'src/api/mappers.tsx', 'src/api/search.ts', 'src/api/inbox.ts', ] const failures = requiredFiles.filter((file) => !existsSync(file)).map((file) => `missing ${file}`) const tokenSource = existsSync('src/styles/tokens.css') ? readFileSync('src/styles/tokens.css', 'utf8') : '' for (const token of ['--color-primary: #165dff', '--font-sans', '--space-4: 16px']) { if (!tokenSource.toLowerCase().includes(token)) failures.push(`missing token ${token}`) } for (const forbidden of ['森林Agent', 'AI智能体', '实时接口', '进度 60%']) { for (const file of requiredFiles.filter((name) => name.endsWith('.tsx'))) { if (readFileSync(file, 'utf8').includes(forbidden)) failures.push(`${file} contains forbidden copy ${forbidden}`) } } if (existsSync('src/App.tsx')) { failures.push('legacy src/App.tsx should be removed after page split') } for (const removed of ['src/pages/projects/project-data.tsx', 'src/pages/projects/project-inspector.tsx', 'src/pages/projects/project-inbox.tsx']) { if (existsSync(removed)) { failures.push(`${removed} should be removed; frontend data must come from backend APIs`) } } if (existsSync('src/app/App.tsx')) { const appSource = readFileSync('src/app/App.tsx', 'utf8') for (const forbidden of ['function LoginPage', 'function Workbench', 'function OverviewContent']) { if (appSource.includes(forbidden)) { failures.push(`src/app/App.tsx still contains ${forbidden}`) } } } const apiFiles = existsSync('src/api') ? ['client.ts', 'projects.ts', 'mappers.tsx', 'search.ts', 'inbox.ts'] .map((name) => `src/api/${name}`) .filter((file) => existsSync(file)) : [] const apiSource = apiFiles.map((file) => readFileSync(file, 'utf8')).join('\n') for (const forbidden of [ { pattern: /\bID\s*\?\s*:/, label: 'optional PascalCase ID compatibility field' }, { pattern: /\bName\s*\?\s*:/, label: 'optional PascalCase Name compatibility field' }, { pattern: /['"`]\/api\/projects/, label: 'legacy /api/projects path' }, { pattern: /\b(?:project|task|inbox)Id\s*\??\s*:\s*(?:string\s*\|\s*)?number\b/i, label: 'numeric project/task/inbox identity', }, ]) { if (forbidden.pattern.test(apiSource)) failures.push(`src/api contains ${forbidden.label}`) } const clientSource = existsSync('src/api/client.ts') ? readFileSync('src/api/client.ts', 'utf8') : '' if (!clientSource.includes("'/api/v1/auth/login'")) failures.push('login must use /api/v1/auth/login') if (!clientSource.includes('export class ApiError extends Error')) failures.push('client must export ApiError') const projectsSource = existsSync('src/api/projects.ts') ? readFileSync('src/api/projects.ts', 'utf8') : '' if (!projectsSource.includes("'/api/v1/projects'")) failures.push('project API must use /api/v1/projects') if (/['"`]\/api\/(?!v1\/)/.test(apiSource)) failures.push('src/api paths must use the /api/v1 prefix') for (const match of apiSource.matchAll(/export type (\w+DTO)\s*=\s*\{([\s\S]*?)\n\}/g)) { const [, name, body] = match if (/^\s*\w+\s*\?\s*:/m.test(body)) failures.push(`${name} fields must be required`) if (/^\s*[A-Z]\w*\s*:/m.test(body)) failures.push(`${name} fields must use camelCase`) if (/^\s*(?:id|\w+Id)\s*:\s*number\b/im.test(body)) failures.push(`${name} identities must be strings`) } const searchSource = existsSync('src/api/search.ts') ? readFileSync('src/api/search.ts', 'utf8') : '' if (searchSource && !searchSource.includes("'/api/v1/search'")) failures.push('search API must use /api/v1/search') const inboxSource = existsSync('src/api/inbox.ts') ? readFileSync('src/api/inbox.ts', 'utf8') : '' for (const path of ['/api/v1/projects/', '/api/v1/inbox/']) { if (inboxSource && !inboxSource.includes(path)) failures.push(`inbox API must use ${path}`) } const html = readFileSync('index.html', 'utf8') if (!html.includes('')) failures.push('index language must be zh-CN') if (!html.includes('