- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files - Rename git bundle file ss-tools.bundle → superset-tools.bundle - Update .gitignore pattern accordingly - Preserve variable names (hasSsTools etc.) and code identifiers
46 KiB
Frontend Design Audit: superset-tools
Дата: 2026-06-02 (ревизия 3 — PR 0-4 выполнены) Цель: Аудит фронтенд кода на предмет "плавающего" дизайна, выработка AI-native правил Объём: ~120 файлов, 9 UI-атомов (+EmptyState), 55+ компонентов, 10 моделей (+DashboardHubModel), 9 сторов, все страницы, tailwind.config.js, guardrail-скрипт
Содержание
- Методология
- Архитектурный ландшафт
- Найденные проблемы
- 3.1 Критические
- 3.2 Средние
- 3.3 Малые
- Причины плавающего дизайна
- AI-Native Design Rules
- Приоритетный план исправлений
- Статус исправлений
- Следующие шаги для AI-First
- Приложение: Карта файлов
1. Методология
Сканированные области
frontend/
├── tailwind.config.js ✅ конфиг темы
├── src/
│ ├── app.css ✅ 3 строки (tailwind directives)
│ ├── routes/ ✅ все 35+ страниц
│ ├── lib/
│ │ ├── ui/ ✅ 8 атомарных компонентов
│ │ ├── components/ ✅ 30+ доменных компонентов
│ │ ├── stores/ ✅ 9 сторов (8 legacy + 1 runes)
│ │ ├── models/ ✅ 9 моделей
│ │ ├── api/ ✅ клиентский слой
│ │ └── utils/ ✅ утилиты
│ └── components/ ✅ 30+ компонентов старого стиля
Критерии оценки
Каждый файл проверялся по 7 осям:
- Design tokens — использует ли семантические цвета из
tailwind.config.jsили сырые классы - Component reuse — использует ли
$lib/uiатомы или дублирует стили - State architecture — есть ли Screen Model для сложных страниц
- Svelte 5 compliance — runes-only или legacy паттерны
- TypeScript — типизирован или JSDoc/any
- UX contracts — полнота семантических аннотаций
- Module size — не превышает ли 400 строк (INV_7)
2. Архитектурный ландшафт
2.1 Tailwind Config (tailwind.config.js)
Определены семантические цвета, которые должны быть единым источником истины:
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#2563eb', // blue-600
hover: '#1d4ed8', // blue-700
ring: '#3b82f6', // blue-500
light: '#eff6ff', // blue-50
},
secondary: {
DEFAULT: '#f3f4f6', // gray-100
hover: '#e5e7eb', // gray-200
text: '#111827', // gray-900
ring: '#6b7280', // gray-500
},
destructive: {
DEFAULT: '#dc2626', // red-600
hover: '#b91c1c', // red-700
ring: '#ef4444', // red-500
light: '#fef2f2', // red-50
},
ghost: {
hover: '#f3f4f6', // gray-100
text: '#374151', // gray-700
ring: '#6b7280', // gray-500
},
terminal: { /* dark palette for log viewer */ },
log: { /* debug, info, warning, error */ },
source: { /* plugin, api, git, system */ },
},
width: {
sidebar: '240px',
'sidebar-collapsed': '64px',
},
fontFamily: {
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
},
},
}
2.2 UI-атомы (src/lib/ui/)
| Компонент | Описание | Использует семантические токены |
|---|---|---|
Button.svelte |
primary/secondary/danger/ghost variants | ✅ bg-primary, bg-destructive |
Card.svelte |
Container with title, padding variants | ❌ border-gray-200 bg-white |
Input.svelte |
Text input with label + error | ❌ border-gray-300, focus:ring-primary-ring |
Select.svelte |
Dropdown with label | ❌ border-gray-300 |
PageHeader.svelte |
Title + subtitle + actions slot | ❌ text-gray-900 |
Icon.svelte |
SVG icon set | ❌ цвета не использует (currentColor) |
HelpTooltip.svelte |
Tooltip helper | ❌ |
LanguageSwitcher.svelte |
Lang toggle | ❌ |
Вывод: Только Button системно использует семантические токены. Остальные атомы используют сырые Tailwind-классы. Это источник "плавания" — если поменять primary цвет в конфиге, Button изменится, а Card/Input/Select — нет.
2.3 Состояние: 3 параллельных подхода
┌────────────────────────────────────────────────────────────────┐
│ STATE MANAGEMENT MAP │
├────────────────────────────────────────────────────────────────┤
│ │
│ LEGACY STORES (svelte/store writable) │
│ ├── sidebar.ts — sidebar state + localStorage │
│ ├── taskDrawer.ts — task drawer visibility │
│ ├── datasetReviewSession.ts — dataset review session │
│ ├── environmentContext.ts — env context + selection │
│ ├── health.ts — health check state │
│ ├── activity.ts — derived activity │
│ ├── assistantChat.ts — assistant chat session │
│ └── translationRun.ts — translation run tracking │
│ │
│ MODERN STORES (.svelte.ts with $state) │
│ ├── maintenance.svelte.ts — maintenance + WebSocket │
│ │
│ SCREEN MODELS (.svelte.ts class with $state) │
│ ├── MigrationModel.svelte.ts — migration wizard state │
│ ├── GitManagerModel.svelte.ts — git manager state │
│ ├── GitStatusModel.svelte.ts — git status per dashboard │
│ ├── GitConfigModel.svelte.ts — git configuration │
│ ├── BranchModel.svelte.ts — branch management │
│ ├── DeploymentModel.svelte.ts — deployment state │
│ ├── CommitModel.svelte.ts — commit history state │
│ ├── MigrationSettingsModel.svelte.ts — migration settings │
│ └── MappingsModel.svelte.ts — db mapping state │
│ │
└────────────────────────────────────────────────────────────────┘
Проблема: AI-агент видит 3 разных паттерна и не знает, какой выбрать для нового кода.
2.4 Структура компонентов: двойная иерархия
src/lib/ui/ # Атомарные UI-компоненты (8 файлов)
├── Button.svelte
├── Card.svelte
├── Input.svelte
├── Select.svelte
├── PageHeader.svelte
├── Icon.svelte
├── HelpTooltip.svelte
└── LanguageSwitcher.svelte
src/lib/components/ # Доменные компоненты (30+ файлов)
├── layout/
├── translate/
├── reports/
├── dataset-review/
├── ui/ # SearchableMultiSelect, MultiSelect
├── health/
├── llm/
└── assistant/
src/components/ # Старые компоненты (30+ файлов)
├── auth/
├── git/
├── tasks/
├── tools/
├── backups/
├── storage/
├── llm/
├── EnvSelector.svelte
├── DashboardGrid.svelte
├── MappingTable.svelte
├── TaskRunner.svelte
├── TaskHistory.svelte
├── Toast.svelte
├── Navbar.svelte
├── Footer.svelte
├── StartupEnvironmentWizard.svelte
└── ...
Проблема: src/components/ — наследие, но непонятно, почему часть компонентов там, а часть в lib/components/. Для AI-агента это создаёт неопределённость.
2.5 Карта страниц и их состояния
| Страница | Строк | $state атомов | Model? | Использует $lib/ui? | Типизация |
|---|---|---|---|---|---|
dashboards/+page.svelte |
2765 | ~30+ | ❌ нет | ❌ частично | lang="ts" |
datasets/+page.svelte |
472 | ~15+ | ❌ нет | ❌ нет | lang="ts" |
migration/+page.svelte |
637 | ~5 (в модели) | ✅ MigrationModel | ✅ Button, Card, PageHeader | lang="ts" |
settings/+page.svelte |
290 | ~5 | ❌ нет | ❌ нет | lang="ts" |
validation-tasks/+page.svelte |
519 | ~10 | ❌ нет | ❌ нет | ❌ JSDoc |
reports/+page.svelte |
204 | ~6 | ❌ нет | ✅ PageHeader | ❌ без типов |
dashboards/[id]/+page.svelte |
582 | ~15 | частично (GitStatusModel) | ❌ нет | lang="ts" |
datasets/review/+page.svelte |
— | — | ❌ | — | — |
translate/+page.svelte |
— | — | ❌ | — | — |
3. Найденные проблемы
3.1 🔴 Критические
🔴 Проблема 1: Трёхголовая архитектура состояния
Описание: В проекте одновременно существуют 3 несвязанных подхода к управлению состоянием.
Доказательство:
// ПАТТЕРН 1: Legacy writable store (src/lib/stores/sidebar.ts)
import { writable, type Writable } from 'svelte/store';
export const sidebarStore: Writable<SidebarState> = writable(initialState);
// ПАТТЕРН 2: Rune-based store (src/lib/stores/maintenance.svelte.ts)
export function createMaintenanceStore(): MaintenanceStore {
let activeEvents = $state<unknown[]>([]);
return { get activeEvents() { return activeEvents; }, ... };
}
// ПАТТЕРН 3: Screen Model class (src/lib/models/MigrationModel.svelte.ts)
export class MigrationModel {
currentStep: number = $state(1);
}
Последствия:
- AI-агент не может определить, какой паттерн использовать
- Разный DX для тестирования: Model тестируется без DOM, Store — через
get()
🔴 Проблема 2: Нет Screen Model для сложных страниц
Описание: Страницы с 10+ $state атомами и cross-widget инвариантами не выделены в Model.
Доказательство — dashboards/+page.svelte (2765 строк):
<!-- ❌ ВСЁ В ОДНОМ ФАЙЛЕ: ~30 атомов, ~50 функций -->
<script lang="ts">
let allDashboards = $state([]);
let dashboards = $state([]);
let isLoading = $state(true);
let error = $state(null);
let currentPage = $state(1);
let searchQuery = $state("");
// ... ещё 20+ атомов, 50+ функций, Git-операции, всё инлайн
</script>
Последствия:
- Невозможно протестировать unit-тестами без DOM
- AI-агент теряет контекст к 20-й функции
- Нарушение INV_7 (>400 строк)
🔴 Проблема 3: Мёртвые ссылки — навигация на несуществующие роуты
Описание: Ссылки формировались как сырые строки в 40+ местах. Если роут переименован или не создан — 404 в рантайме.
Найденные битые роуты:
| Ссылка | Откуда | Статус |
|---|---|---|
/dashboards/{id}/validation?env_id= |
dashboards/[id]/+page.svelte, dashboards/health/+page.svelte |
❌ → ✅ СОЗДАН |
/validation/{taskId} |
ScheduleAtAGlance.svelte |
❌ → ✅ ИСПРАВЛЕН |
Исправление (2026-06-02):
- Создан
$lib/routes.ts— централизованный реестр с type-safe builder-функциями - 47 raw goto/href мигрированы на
ROUTES.*()(21 файл) - Добавлен link-integrity тест (46 тестов + сканирование raw-строк)
- Создан роут
/dashboards/{id}/validation/ - Починена ссылка
/validation/{taskId}→/validation-tasks/{taskId}
// Было (размазано): goto(`/dashboards/${id}/validation?env_id=${envId}`);
// Стало (SSOT): goto(ROUTES.dashboards.validation(id, envId));
Результат: 698 тестов, 0 raw goto/href, 0 битых роутов.
Новые AI-Native правила (Rules 9-10, см. раздел 5.2):
- Rule 9: Все навигационные ссылки — только через
ROUTES.*() - Rule 10: Link-integrity тест обязателен в CI
🔴 Проблема 4: Размытие дизайн-токенов
Описание: Семантические цвета из tailwind.config.js используются непоследовательно. Рядом с bg-primary соседствуют bg-indigo-50, bg-gradient-to-br from-slate-50 via-white to-sky-50, bg-red-100.
Доказательство — сравнение паттернов в dashboards/+page.svelte:
<!-- ✅ Семантический токен (редко) -->
<button class="bg-primary text-white hover:bg-primary-hover">
<!-- ❌ Дрейф: indigo вместо primary -->
<button class="inline-flex items-center justify-center rounded-lg border border-indigo-300 bg-indigo-50 px-4 py-2 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-100">
<!-- ❌ Дрейф: сырой red вместо destructive -->
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 flex items-center justify-between">
<!-- ❌ Рядом destructive (правильно) и сырой red (неправильно) -->
<button class="px-4 py-2 bg-destructive text-white rounded hover:bg-destructive-hover transition-colors">
<!-- ❌ sky-50, gradient — нет в токенах -->
<div class="relative overflow-hidden rounded-3xl border border-slate-200 bg-gradient-to-br from-slate-50 via-white to-sky-50 p-8 shadow-sm">
Примеры из других файлов:
<!-- validation-tasks/+page.svelte — сырые цвета -->
<select class="rounded-md border border-slate-300 px-2 py-1.5 text-sm">
<!-- settings/+page.svelte — сырые цвета -->
<div class="border-b border-gray-200">
<!-- reports/+page.svelte — сырой Card вместо компонента Card -->
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<!-- settings/+page.svelte — 3 разных стиля кнопок в одном файле -->
<button class="bg-blue-600 text-white px-3 py-1.5 text-sm rounded hover:bg-blue-700">
<button class="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-100">
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Последствия:
- Изменение
primaryв tailwind.config.js не синхронизируется с indigo/sky-кнопками - AI-агент не может определить "правильный" цвет — каждый раз гадает
- Визуальная несогласованность: кнопки на одной странице выглядят по-разному
- 5 разных стилей для одного и того же UI-паттерна
3.2 🟡 Средние
🟡 Проблема 5: Button-компонент не стандартизирован
Описание: Только migration/+page.svelte системно использует $lib/ui/Button. 7 разных стилей ручных <button> в проекте.
🟡 Проблема 6: Дублирование каталогов компонентов
Описание: 3 корневые директории (lib/ui/, lib/components/, components/) без чётких правил. src/components/ — замороженное наследие.
🟡 Проблема 7: 33 any в TypeScript
Описание: 33 вхождения any в исходниках — функции getRunLink(run: any), dashboard: any, catch (err: any).
Ключевые цели:
function getRunLink(run: any)→ конкретный интерфейсRundashboard: any→Dashboardcatch (err: any)→catch (err: unknown)
Последствие: AI-агент вынужден угадывать форму данных. any — главный источник галлюцинаций.
🟡 Проблема 8: JSDoc вместо TypeScript
Описание: validation-tasks/+page.svelte и другие страницы используют JSDoc вместо <script lang="ts">.
🟡 Проблема 9: 8 легаси-сторов блокируют миграцию на runes
Описание: Все сторы, кроме maintenance.svelte.ts, используют Svelte 4 writable().
🟡 Проблема 10: Неравномерное покрытие UX-контрактами
@UX_REACTIVITY отсутствует везде, кроме MigrationPage. См. таблицу ниже.
🟡 Проблема 11: Нет Component Catalog — SSOT для UI-компонентов
Описание: AI-агент не может за 1 чтение узнать, какие компоненты существуют и какие у них пропсы. Масштаб: 8 атомов + 30+ доменных компонентов — все с разными интерфейсами пропсов.
🟡 Проблема 12: Нет API Data Contracts
Описание: api.ts содержит 30+ endpoint-методов, но ни один не документирует входные/выходные DTO.
AI-агент не знает форму API-ответов без чтения бэкенда.
3.3 🟢 Малые
🟢 Проблема 13: cn() utility не строго типизирован
🟢 Проблема 14: Дублирование debounce
🟢 Проблема 15: Zero-state не переиспользуется
🟢 Проблема 16: Border radius плавает
4. Причины плавающего дизайна
4.1 Отсутствие единого Design Language
У проекта есть tailwind.config.js с семантическими токенами, но нет документированных правил:
- Когда использовать
primaryvsblue-600vsindigo-600? - Какой border radius для чего?
- Какой набор компонентов обязателен к использованию?
4.2 Две эпохи Svelte
Проект начинался на Svelte 4 (writable stores, on:event, export let), потом мигрировал на Svelte 5 (runes, $state). Миграция не завершена:
- 8 stores остались на writable
- 30+ компонентов в
src/components/— наследие - Новые страницы пишутся на runes, но без model-first
4.3 AI-агенты без правил
Каждый AI-агент (человек или LLM) при создании новой страницы:
- Не знает, какой цвет использовать — выбирает "похожий"
- Не знает, какой компонент переиспользовать — пишет с нуля
- Не знает, куда положить файл — кладёт в
routes/илиcomponents/ - Не знает паттерн состояния — пишет
$state+ функции инлайн
4.4 Нет "шлюза" для нового кода
Нет code review checklist или pre-commit хука, который проверял бы:
- Использует ли страница
<Button>вместо ручного<button>? - Использует ли страница семантические токены?
- Есть ли Screen Model для страницы с >5 атомами?
5. AI-Native Design Rules
Эти правила предназначены для включения в промпт AI-агента (Svelte-кодера) и для CI-валидации.
Rule 1: Semantic Color Tokens Over Raw Colors
// ✅ CORRECT
<div class="bg-primary text-white hover:bg-primary-hover">
<div class="bg-destructive text-white">
<div class="bg-secondary text-secondary-text">
// ❌ WRONG
<div class="bg-blue-600 text-white">
<div class="bg-indigo-50 text-indigo-700">
<div class="bg-red-100 text-red-700">
<div class="bg-gradient-to-br from-slate-50 via-white to-sky-50">
Исключение: Только в tailwind.config.js при определении новых токенов.
Rule 2: UI Atoms Are Mandatory For Pages
// ✅ CORRECT
import { Button, Card, Input, Select, PageHeader } from "$lib/ui";
<Button variant="primary" onclick={handleSave}>
<Card>
<PageHeader title="Settings" subtitle={() => <p>Sub</p>} />
// ❌ WRONG
<button class="px-3 py-1 bg-primary rounded text-white">
<div class="bg-white rounded-xl border shadow-sm p-6">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold">Settings</h1>
Исключение: layout-компоненты и сами атомы в $lib/ui/.
Rule 3: Screen Model For Any Page With >5 $state Atoms
// ✅ CORRECT: DashboardHubModel.svelte.ts
export class DashboardHubModel {
dashboards: Dashboard[] = $state([]);
page: number = $state(1);
searchQuery: string = $state("");
// ...
async loadDashboards(): Promise<void> { ... }
async deleteDashboard(id: string): Promise<void> { ... }
}
// ✅ CORRECT: migration/+page.svelte (thin render layer)
const model = new MigrationModel();
onMount(() => model.loadEnvironments());
// ❌ WRONG: dashboards/+page.svelte (2765 строк, ~30 $state атомов инлайн)
Trigger: Если в <script> страницы больше 5 $state(...) вызовов → создай Model.
Rule 4: Runes Only For New State Code
// ✅ CORRECT (.svelte.ts)
let count = $state(0);
const doubled = $derived(count * 2);
// ❌ WRONG (.ts)
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
Исключение: Только рефакторинг существующих writable-сторов, у которых есть подписчики.
Rule 5: One Component Directory — lib/components/
// ✅ CORRECT
src/lib/components/git/GitManager.svelte
src/lib/components/git/BranchSelector.svelte
// ❌ WRONG
src/components/git/GitManager.svelte
src/components/git/BranchSelector.svelte
Trigger: Новый доменный компонент → src/lib/components/<domain>/. src/components/ — заморожено.
Rule 6: TypeScript Required On Pages
// ✅ CORRECT
<script lang="ts">
let { data }: { data: PageData } = $props();
let items: Item[] = $state([]);
// ❌ WRONG
<script>
/** @type {Array<any>} */ let items = $state([]);
Rule 7: Consistent Border Radius Hierarchy
rounded-md → inputs, buttons, selects (h-10)
rounded-lg → cards, containers, modals
rounded-xl → panels, drawers (опционально)
rounded-3xl → zero-state / empty-state только
Rule 8: Page Max 400 Lines
>400 lines → Model + Component decomposition
>10 cyclomatic complexity → helper functions
Rule 9: Route Registry For Navigation (NEW 2026-06-02)
// ✅ CORRECT
import { ROUTES } from '$lib/routes.js';
<a href={ROUTES.dashboards.validation(id, envId)}>History</a>
goto(ROUTES.dashboards.detail(id, envId));
// ❌ WRONG — raw string, неверифицируемо, риск 404
<a href={`/dashboards/${id}/validation?env_id=${envId}`}>History</a>
goto(`/dashboards/${id}?env_id=${envId}`);
Trigger: Любая навигация (goto, href, window.location.href) → только через ROUTES.*().
Исключение: Внешние URL (/api/auth/login/adfs).
Rule 10: Link Integrity Test Required (NEW 2026-06-02)
Перед каждым CI-run:
1. vitest run src/lib/__tests__/routes-link-integrity.test.ts
2. Падать при любых raw goto/href строках (кроме ROUTES)
3. Падать, если ROUTES builder не匹配 ни один файловый роут
6. Приоритетный план исправлений
✅ Выполнено (PR 0-4, 2026-06-02)
| # | Задача | Статус |
|---|---|---|
| ✅ | PR 0 — Contract alignment: semantics-svelte skill + svelte-coder prompt — raw Tailwind заменён на semantic tokens | 3 файла |
| ✅ | PR 1 — Token expansion: tailwind.config.js (+surface/border/text/success/warning/info), атомы переведены на tokens |
8 файлов |
| ✅ | PR 1 — Button: destructive variant, danger как deprecated alias |
1 файл |
| ✅ | PR 1 — Icon: className → class: className |
1 файл |
| ✅ | PR 1 — Input/Select: Math.random() → детерминированный счётчик для ID |
2 файла |
| ✅ | PR 1 — svelte.config.js: алиас $components помечен @DEPRECATED |
1 файл |
| ✅ | PR 2 — Guardrail: scripts/audit-frontend-style.mjs — CI-ready аудит |
1 файл |
| ✅ | PR 3 — Pilot page: migration/+page.svelte — 0 raw-color violations, все кнопки → <Button> |
1 файл |
| ✅ | PR 3 — EmptyState: новый атом + barrel export | 2 файла |
| ✅ | PR 3 — cn() utility: тип расширен до clsx-совместимого | 1 файл |
| ✅ | PR 4 — DashboardHubModel: 350 строк, 35 атомов + 35 методов | 1 файл |
| ✅ | PR 4 — Helpers: dashboard-helpers.ts — 10 чистых функций |
1 файл |
| ✅ | PR 4 — ColumnFilterPopover: 5 инстансов вместо 5×35 строк | 1 файл |
| ✅ | PR 4 — DashboardRow: компонент строки таблицы (с semantic tokens) | 1 файл |
| ✅ | PR 4 — Dashboards page: 2765→1340 строк (-51%), 244→0 raw-color violations | 1 файл |
| ✅ | PR 4 — Tests fix: this-context bugs, миграционные тесты |
2 файла |
P0 — Следующая итерация
| # | Задача | Файлы | Ожидаемый результат |
|---|---|---|---|
| 0.1 | Modal extraction: вынос migration/backup модалок из dashboard page | dashboards/+page.svelte |
1340 → <400 строк |
| 0.2 | Фикс migration теста (1 remaining integration timing) | тест | 100% green |
P1 — Важно (5-6 часов)
| # | Задача | Файлы | Ожидаемый результат |
|---|---|---|---|
| 1.1 | Выделить DatasetHubModel + ValidationTasksModel + ReportsModel |
3 страницы | model-first для оставшихся сложных страниц |
| 1.2 | Заменить raw colors на semantic tokens на ВСЕХ страницах | ~70 файлов | 3892 violations → <100 |
| 1.3 | Заменить ручные <button> на <Button> на ВСЕХ страницах |
~50 файлов | Единый стиль кнопок |
| 1.4 | Миграция 2 сторов на .svelte.ts |
taskDrawer.ts, sidebar.ts |
Runes-only stores |
P2 — Долгосрочно
| # | Задача | Файлы | Ожидаемый результат |
|---|---|---|---|
| 2.1 | Консолидировать src/components/ в lib/components/ |
40 файлов | Одна иерархия |
| 2.2 | Мигрировать оставшиеся 6 writable-сторов | 6 файлов | Runes-only |
| 2.3 | Подключить DashboardRow в grid шаблон | dashboards/+page.svelte |
Ещё -80 строк |
7. Статус исправлений (2026-06-02, ревизия 3)
PR 0: Contract Alignment
| Изменение | Файл(ы) | Результат |
|---|---|---|
semantics-svelte §VI — канонический шаблон на semantic tokens + <Button> |
SKILL.md |
Агенты копируют правильный шаблон |
semantics-svelte §VII — Design Token Canon с таблицей ✅/❌ примеров |
SKILL.md |
Источник истины для цветов |
semantics-svelte §I — 2 новых @INVARIANT (UI reuse + legacy freeze) |
SKILL.md |
Правила встроены в протокол |
svelte-coder.md — Visual system переписан, добавлены UI reuse rules |
agent prompt |
Агент знает $lib/ui mandatory |
svelte-coder.md — Frozen zones секция |
agent prompt |
src/components/ объявлен legacy |
svelte.config.js — $components alias помечен @DEPRECATED |
config |
Агент предупреждён |
PR 1: Design Tokens + UI Atoms
| Изменение | Файл(ы) | Результат |
|---|---|---|
Расширены токены: surface (page/card/muted), border (DEFAULT/strong), text (DEFAULT/muted/subtle/inverse), success, warning, info |
tailwind.config.js |
20+ новых токенов |
Card.svelte: border-gray-200 bg-white → border-border bg-surface-card text-text |
1 файл | ✅ |
Input.svelte: border-gray-300 → border-border-strong bg-surface-card text-text |
1 файл | ✅ |
Select.svelte: то же + детерминированный ID |
1 файл | ✅ |
PageHeader.svelte: text-gray-900 → text-text |
1 файл | ✅ |
Button.svelte: destructive variant (канонический), danger deprecated |
1 файл | ✅ |
Icon.svelte: className → class: className |
1 файл | ✅ |
EmptyState.svelte: новый атом + barrel export |
2 файла | ✅ |
PR 2: Guardrail
| Изменение | Файл(ы) | Результат |
|---|---|---|
scripts/audit-frontend-style.mjs — сканирует raw-цвета, oversized pages, model-first violations |
1 файл | 3892 violations каталогизированы, CI-ready |
| Результат: 70+ файлов с raw цветами, 11 oversized pages, 27 model-first warnings | — | Карта долга готова |
PR 3: Pilot — Migration Page
| Изменение | Файл(ы) | Результат |
|---|---|---|
| Все raw цвета → semantic tokens | migration/+page.svelte |
0 raw-color violations |
Все <button> → <Button variant="..."> |
то же | ✅ |
cn() utility: тип расширен до clsx-совместимого (ClassValue) |
lib/utils.ts |
✅ |
| Миграционный тест: селектор обновлён (class → semantic) | тест | 4/4 pass |
PR 4: DashboardHub Decomposition
Dashboard page metrics:
| Метрика | До | После |
|---|---|---|
| Строк | 2765 | 1340 (-51%) |
| Script строк | ~1100 | ~65 (-94%) |
| Raw-color violations | 244 | 0 |
| $state атомов в page | ~35 | 3 (environments deriveds) |
| Функций в page | ~55 | 5 (document click, search, env created, $effects) |
| Model | ❌ нет | ✅ DashboardHubModel (350 строк, класс) |
| Column filter popovers | 5 копий inline | ✅ ColumnFilterPopover (1 компонент) |
| Grid row компонент | ❌ нет | ✅ DashboardRow.svelte (готов) |
| Helpers | inline | ✅ dashboard-helpers.ts (10 функций) |
Dashboard page violations history:
2765 строк, 244 violations → (script extraction) → 2585 строк
→ (filter popover component) → 2437 строк, 212 violations
→ (Model extraction) → 1340 строк, 208 violations
→ (this-context fixes) → 1340 строк
→ (batch semantic tokens) → 1340 строк, 28 violations
→ (edge case tokens) → 1340 строк, 8 violations
→ (final fixes) → 1340 строк, 0 violations ✅
Метрики качества (глобальные)
| Метрика | До | После |
|---|---|---|
| Raw color violations (всего) | 3892 | 3892 (каталог готов, миграция начата) |
| Dashboards page violations | 244 | 0 |
| Migration page violations | ~90 | 0 |
| UI atoms на семантических токенах | 1/8 | 8/8 |
| Model-first страниц | 1 (migration) | 2 (migration + dashboards) |
| Guardrail скрипт | ❌ нет | ✅ CI-ready |
| Oversized pages (>400) | 11 | 11 (dashboards: 2765→1340, осталось 10) |
| Build | ✅ | ✅ |
| Tests (pass/total) | 642/651 | 642/651 (pre-existing failures) |
$lib/ui атомов |
8 | 9 (+EmptyState) |
| Моделей | 9 | 10 (+DashboardHubModel) |
src/components/ legacy files |
40 | 40 (заморожено, миграция — P2) |
8. Следующие шаги для AI-First
На основе аудита и выполненной работы, следующие улучшения для AI-First (в порядке приоритета):
8.1 Component Catalog ($lib/components.ts)
Почему: AI-агент тратит ~5-10 чтений на поиск подходящего компонента. Component catalog сокращает это до 1.
Формат:
export const COMPONENTS = {
Button: {
path: "$lib/ui/Button.svelte",
props: {} as ButtonProps,
variants: ["primary", "secondary", "destructive", "ghost"] as const,
},
Card: { path: "$lib/ui/Card.svelte", props: {} as CardProps },
PageHeader: { path: "$lib/ui/PageHeader.svelte", props: {} as PageHeaderProps },
// ... все 30+ компонентов
} as const;
Трудоёмкость: 1 файл, ~50 строк.
8.2 API Data Contracts
Почему: Каждый fetchApi в api.ts — чёрный ящик для AI-агента. Без DTO агент галлюцинирует поля.
Формат:
// @BRIEF Fetch paginated dashboards for an environment
// @DATA_CONTRACT params -> { env_id, page, page_size, search?, filters? }
// @DATA_CONTRACT response -> { dashboards: Dashboard[], total: number, page: number }
getDashboards: <T = DashboardListResponse>(envId: string, opts?: DashboardQueryOptions) => ...
Трудоёмкость: 30+ методов, ~2-3 строки документации на каждый.
8.3 Типизация any (33 вхождения)
Почему: any — главный источник AI-галлюцинаций. Агент вынужден угадывать структуру.
Ключевые цели:
function getRunLink(run: any)→ конкретный интерфейсRundashboard: any→Dashboardcatch (err: any)→catch (err: unknown)
Трудоёмкость: ~15-20 файлов, механическая работа.
8.4 Store Registry ($lib/stores/index.ts)
Почему: Агент видит все глобальные состояния в одном месте.
Трудоёмкость: 1 файл, ~30 строк.
8.5 Semantic Index Maintenance
- Перестроить индекс с DuckDB для fuzzy-search
- Исправить 103 unresolved relations (информационный шум)
Трудоёмкость: 1 команда + фикс таргетов @RELATION.
9. Приложение: Карта файлов
9.1 UI-атомы (src/lib/ui/)
| Файл | Строк | Использует токены | Типы | Статус |
|---|---|---|---|---|
Button.svelte |
77 | ✅ bg-primary, bg-destructive + destructive variant |
lang="ts" |
✅ PR 1 |
Card.svelte |
58 | ✅ border-border bg-surface-card text-text |
lang="ts" |
✅ PR 1 |
Input.svelte |
68 | ✅ border-border-strong bg-surface-card text-text |
lang="ts" |
✅ PR 1 |
Select.svelte |
60 | ✅ border-border-strong bg-surface-card text-text |
lang="ts" |
✅ PR 1 |
PageHeader.svelte |
45 | ✅ text-text |
lang="ts" |
✅ PR 1 |
Icon.svelte |
111 | ✅ currentColor, class: className |
lang="ts" |
✅ PR 1 |
EmptyState.svelte |
50 | ✅ text-text, text-text-muted, text-text-subtle |
lang="ts" |
✅ PR 3 |
HelpTooltip.svelte |
— | — | — | P2 |
LanguageSwitcher.svelte |
— | — | — | P2 |
9.2 Модели (src/lib/models/)
| Файл | Строк | Семантика | Статус |
|---|---|---|---|
MigrationModel.svelte.ts |
472 | ✅ model-first — state architecture reference | — |
DashboardHubModel.svelte.ts |
350 | ✅ model-first — 35 атомов, 35 методов, 10 инвариантов | ✅ PR 4 |
GitManagerModel.svelte.ts |
— | ✅ | — |
GitStatusModel.svelte.ts |
— | ✅ | — |
GitConfigModel.svelte.ts |
— | ✅ | — |
BranchModel.svelte.ts |
— | ✅ | — |
DeploymentModel.svelte.ts |
— | ✅ | — |
CommitModel.svelte.ts |
— | ✅ | — |
MigrationSettingsModel.svelte.ts |
— | ✅ | — |
MappingsModel.svelte.ts |
— | ✅ | — |
7.3 Сторы (src/lib/stores/)
| Файл | Строк | Паттерн | Семантические токены |
|---|---|---|---|
sidebar.ts |
79 | writable ❌ legacy |
✅ аннотации |
taskDrawer.ts |
112 | writable ❌ legacy |
✅ |
environmentContext.ts |
— | writable ❌ legacy |
✅ |
datasetReviewSession.ts |
87 | writable ❌ legacy |
✅ |
assistantChat.ts |
— | writable ❌ legacy |
✅ |
health.ts |
— | writable ❌ legacy |
✅ |
activity.ts |
— | writable ❌ legacy |
✅ |
translationRun.ts |
— | writable ❌ legacy |
❌ |
maintenance.svelte.ts |
203 | $state ✅ modern |
✅ |
9.4 Страницы (src/routes/)
| Файл | Строк | Model? | $lib/ui? | Типы | UX контракты | Raw violations |
|---|---|---|---|---|---|---|
dashboards/+page.svelte |
1340 | ✅ DashboardHubModel | ✅ Button | ts | ✅ 5/5 | 0 ✅ |
dashboards/[id]/+page.svelte |
582 | частично | ❌ | ts | ✅ 4/5 | ~90 |
datasets/+page.svelte |
472 | ❌ | ❌ | ts | ❌ 1/5 | ~45 |
datasets/review/+page.svelte |
— | ❌ | — | — | — | — |
migration/+page.svelte |
640 | ✅ MigrationModel | ✅ Button,Card,PageHeader | ts | ✅ 5/5 | 0 ✅ |
settings/+page.svelte |
290 | ❌ | ❌ | ts | ✅ 4/5 | ~40 |
validation-tasks/+page.svelte |
519 | ❌ | ❌ | ❌ JSDoc | ✅ 4/5 | ~90 |
reports/+page.svelte |
204 | ❌ | ✅ PageHeader | ❌ any | ✅ 3/5 | ~25 |
translate/+page.svelte |
— | ❌ | — | — | — | — |
+layout.svelte |
112 | — | — | ts | ✅ | — |
7.5 Старые компоненты (src/components/)
src/components/
├── auth/
│ └── ProtectedRoute.svelte
├── git/
│ ├── GitManager.svelte
│ ├── BranchSelector.svelte
│ ├── CommitHistory.svelte
│ ├── CommitModal.svelte
│ ├── ConflictResolver.svelte
│ ├── DeploymentModal.svelte
│ ├── GitInitPanel.svelte
│ ├── GitOperationsPanel.svelte
│ ├── GitReleasePanel.svelte
│ └── GitWorkspacePanel.svelte
├── tasks/
│ ├── TaskResultPanel.svelte
│ └── TaskLogPanel.svelte
│ └── LogFilterBar.svelte
│ └── LogEntryRow.svelte
├── tools/
│ ├── MapperTool.svelte
│ └── DebugTool.svelte
├── backups/
│ ├── BackupManager.svelte
│ └── BackupList.svelte
├── storage/
│ ├── FileUpload.svelte
│ └── FileList.svelte
├── llm/
│ ├── ValidationReport.svelte
│ ├── ProviderConfig.svelte
│ └── DocPreview.svelte
├── DashboardGrid.svelte
├── EnvSelector.svelte
├── Footer.svelte
├── MappingTable.svelte
├── MissingMappingModal.svelte
├── Navbar.svelte
├── PasswordPrompt.svelte
├── RepositoryDashboardGrid.svelte
├── StartupEnvironmentWizard.svelte
├── TaskHistory.svelte
├── TaskList.svelte
├── TaskLogViewer.svelte
├── TaskRunner.svelte
├── Toast.svelte
├── DynamicForm.svelte
└── RepositoryDashboardGrid.svelte
10. Ключевые уроки (PR 0-4)
Что работает
- Model-first на Svelte 5 runes — класс с
$stateатомами + методами. Тестируется без DOM (new Model()→ прямое тестирование инвариантов за ~10ms). - Семантические токены — замена
bg-indigo-600 text-white→bg-primary text-whiteделает дизайн централизованно управляемым. Batch-замена regex на странице заняла 5 минут, дала 87% сокращение violations. - Guardrail-скрипт —
scripts/audit-frontend-style.mjsдаёт CI-проверку за секунды, каталогизирует весь долг. - Компонент-экстракция (ColumnFilterPopover) — 5×35 строк inline → 1×83 строки + 5×10 строк вызова. Экономия ~85 строк на компонент.
⚠️ Риск: Model как "god object"
Статус: DashboardHubModel сейчас 350 строк, 35 методов — допустимо. Но нельзя просто докидывать методы при добавлении фич.
Правило (зафиксировано в модели и semantics-svelte):
| Порог | Действие |
|---|---|
| Model > 400 строк | Декомпозиция — вынести helpers или разбить на submodels |
| Model > 40 public методов | Split: FiltersModel + SelectionModel + GitActionsModel |
План split для DashboardHubModel (когда потребуется):
DashboardFiltersModel— search, column filters, sort, visible optionsDashboardSelectionModel— checkbox, select all/visible, bulk actionsDashboardGitActionsModel— git init, sync, commit, pull, push, status batch- Основной
DashboardHubModel— композиция submodels + core (pagination, load, environments)
Что ломалось
this-контекст — при переносе методов в класс Model, всеonclick={m.method}теряютthis. Нужноonclick={() => m.method()}.- Legacy сторы в Model —
$derived(store.field)в.svelte.tsне работает (store — объект, не значение). Нужно оставлять deriveds в.svelteфайле. - Double
m.prefix —replaceAllпривёл кm.m.openFilterColumnв одном месте. Проверка grepm\.m\.обязательна после batch-замены. - Source-scanning тесты — тесты, ищущие
function handleXв исходниках, ломаются при рефакторинге. Нужно переписывать на поведенческие.
Рекомендации для следующих PR
- Перед model-extraction: сначала вынести чистые helpers, потом компоненты, потом Model.
- После замены
$state→Model: grep\$\state(в page для проверки, что все ушли. - После batch-замены цветов:
npm run build && node scripts/audit-frontend-style.mjs. - Тесты на source-scanning заменить на
render+screen.findByTextпроверки.
Аудит выполнен 2026-06-01. Ревизия 3: 2026-06-02 (PR 0-4 выполнены). Полный контекст: ~120 файлов, 9 UI-атомов, 55+ компонентов, 10 моделей, 9 сторов, 35+ страниц.