mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
import { Glob } from 'bun';
|
|
import { existsSync } from 'node:fs';
|
|
import { relative, resolve } from 'node:path';
|
|
|
|
type Violation = {
|
|
file: string;
|
|
line: number;
|
|
reason: string;
|
|
snippet: string;
|
|
};
|
|
|
|
type Rule = {
|
|
reason: string;
|
|
regex: RegExp;
|
|
};
|
|
|
|
type Requirement = {
|
|
file: string;
|
|
reason: string;
|
|
needles: string[];
|
|
};
|
|
|
|
const rules: Rule[] = [
|
|
{ reason: 'hardcoded zinc utility', regex: /\b(?:bg|text|border)-zinc-\d{2,3}\b/ },
|
|
{ reason: 'hardcoded white-alpha border', regex: /\bborder-white\/\d+\b/ },
|
|
{ reason: 'hardcoded black overlay', regex: /\bbg-black\/\d+\b/ },
|
|
{ reason: 'inline rgba color literal', regex: /rgba\(/ },
|
|
{ reason: 'inline rgb color literal', regex: /rgb\(/ },
|
|
{ reason: 'hex color literal', regex: /#[0-9a-fA-F]{3,8}\b/ },
|
|
{ reason: 'legacy red semantic marker', regex: /text-red-500/ },
|
|
{ reason: 'primary-tinted hover on non-primary surfaces', regex: /\b(?:hover:bg-primary\/(?:[1-8]0)|group-hover:bg-primary\/(?:[1-8]0))\b/ },
|
|
];
|
|
|
|
const requirements: Requirement[] = [
|
|
{
|
|
file: 'src/index.css',
|
|
reason: 'theme token and utility baseline',
|
|
needles: [
|
|
'--success:',
|
|
'--warning:',
|
|
'--danger:',
|
|
'--info:',
|
|
'--surface-muted:',
|
|
'--surface-elevated:',
|
|
'--surface-overlay:',
|
|
'.theme-glow-primary',
|
|
'.theme-surface-overlay',
|
|
'.theme-sticky-bar',
|
|
'.theme-page-frame',
|
|
'.theme-page-actions',
|
|
'.theme-page-content',
|
|
'.theme-card-shell',
|
|
'.theme-card-header',
|
|
'.theme-card-content',
|
|
'.theme-error-panel',
|
|
'.theme-dialog-panel',
|
|
'.theme-dialog-header',
|
|
'.theme-dialog-body',
|
|
'.theme-dialog-footer',
|
|
],
|
|
},
|
|
{
|
|
file: 'tailwind.config.js',
|
|
reason: 'semantic color mapping in Tailwind',
|
|
needles: ['danger:', 'success:', 'warning:', 'info:'],
|
|
},
|
|
{
|
|
file: 'src/components/llm/ProviderDialog.tsx',
|
|
reason: 'modal overlay must use semantic utility',
|
|
needles: ['theme-surface-overlay', 'theme-dialog-panel', 'theme-dialog-header', 'theme-dialog-body', 'theme-dialog-footer'],
|
|
},
|
|
{
|
|
file: 'src/components/llm/TestResultDialog.tsx',
|
|
reason: 'modal overlay must use semantic utility',
|
|
needles: ['theme-surface-overlay', 'theme-dialog-panel', 'theme-dialog-header', 'theme-dialog-body', 'theme-dialog-footer'],
|
|
},
|
|
{
|
|
file: '../docs/design/ui-theme-language.md',
|
|
reason: 'design language documentation baseline',
|
|
needles: ['强制规则', '页面级统一约束', 'destructive 与 danger 的约定', 'theme-surface-overlay'],
|
|
},
|
|
{
|
|
file: 'src/components/ConfigManager.tsx',
|
|
reason: 'system config page should use unified page shell utilities',
|
|
needles: ['theme-page-frame', 'theme-page-actions', 'theme-page-content', 'theme-error-panel'],
|
|
},
|
|
{
|
|
file: 'src/components/ReviewConfigPage.tsx',
|
|
reason: 'review config page should use unified page shell utilities',
|
|
needles: ['theme-page-frame', 'theme-page-actions', 'theme-page-content', 'theme-card-shell', 'theme-error-panel'],
|
|
},
|
|
];
|
|
|
|
const violations: Violation[] = [];
|
|
|
|
const addViolation = (file: string, line: number, reason: string, snippet: string) => {
|
|
violations.push({ file: relative(process.cwd(), file), line, reason, snippet });
|
|
};
|
|
|
|
const scanSourceFiles = async () => {
|
|
const glob = new Glob('src/**/*.tsx');
|
|
|
|
for await (const file of glob.scan('.')) {
|
|
const absoluteFile = resolve(process.cwd(), file);
|
|
const content = await Bun.file(absoluteFile).text();
|
|
const lines = content.split(/\r?\n/);
|
|
|
|
lines.forEach((lineContent, index) => {
|
|
rules.forEach((rule) => {
|
|
if (rule.regex.test(lineContent)) {
|
|
addViolation(absoluteFile, index + 1, rule.reason, lineContent.trim());
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
const verifyRequirements = async () => {
|
|
for (const requirement of requirements) {
|
|
const absoluteFile = resolve(process.cwd(), requirement.file);
|
|
|
|
if (!existsSync(absoluteFile)) {
|
|
addViolation(absoluteFile, 1, requirement.reason, 'required file missing');
|
|
continue;
|
|
}
|
|
|
|
const content = await Bun.file(absoluteFile).text();
|
|
requirement.needles.forEach((needle) => {
|
|
if (!content.includes(needle)) {
|
|
addViolation(absoluteFile, 1, requirement.reason, `missing token/pattern: ${needle}`);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const printResult = () => {
|
|
if (violations.length === 0) {
|
|
console.log('✅ UI regression guard passed');
|
|
return;
|
|
}
|
|
|
|
console.error(`❌ UI regression guard failed with ${violations.length} issue(s):`);
|
|
violations.forEach((violation) => {
|
|
console.error(`- ${violation.file}:${violation.line} [${violation.reason}] ${violation.snippet}`);
|
|
});
|
|
process.exit(1);
|
|
};
|
|
|
|
await scanSourceFiles();
|
|
await verifyRequirements();
|
|
printResult();
|