Files
ss-tools/frontend/ai-native-design-audit.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- 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
2026-06-16 11:15:19 +03:00

989 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-скрипт
---
## Содержание
1. [Методология](#1-методология)
2. [Архитектурный ландшафт](#2-архитектурный-ландшафт)
3. [Найденные проблемы](#3-найденные-проблемы)
- 3.1 [Критические](#31-критические)
- 3.2 [Средние](#32-средние)
- 3.3 [Малые](#33-малые)
4. [Причины плавающего дизайна](#4-причины-плавающего-дизайна)
5. [AI-Native Design Rules](#5-ai-native-design-rules)
6. [Приоритетный план исправлений](#6-приоритетный-план-исправлений)
7. [Статус исправлений](#7-статус-исправлений)
8. [Следующие шаги для AI-First](#8-следующие-шаги-для-ai-first)
9. [Приложение: Карта файлов](#9-приложение-карта-файлов)
---
## 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 осям:
1. **Design tokens** — использует ли семантические цвета из `tailwind.config.js` или сырые классы
2. **Component reuse** — использует ли `$lib/ui` атомы или дублирует стили
3. **State architecture** — есть ли Screen Model для сложных страниц
4. **Svelte 5 compliance** — runes-only или legacy паттерны
5. **TypeScript** — типизирован или JSDoc/any
6. **UX contracts** — полнота семантических аннотаций
7. **Module size** — не превышает ли 400 строк (INV_7)
---
## 2. Архитектурный ландшафт
### 2.1 Tailwind Config (`tailwind.config.js`)
Определены семантические цвета, которые должны быть единым источником истины:
```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 несвязанных подхода к управлению состоянием.
**Доказательство:**
```typescript
// ПАТТЕРН 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 строк):**
```svelte
<!-- ❌ ВСЁ В ОДНОМ ФАЙЛЕ: ~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}`
```typescript
// Было (размазано): 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:**
```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">
```
**Примеры из других файлов:**
```svelte
<!-- 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)` → конкретный интерфейс `Run`
- `dashboard: any``Dashboard`
- `catch (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` с семантическими токенами, но нет документированных правил:
- Когда использовать `primary` vs `blue-600` vs `indigo-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) при создании новой страницы:
1. Не знает, какой цвет использовать — выбирает "похожий"
2. Не знает, какой компонент переиспользовать — пишет с нуля
3. Не знает, куда положить файл — кладёт в `routes/` или `components/`
4. Не знает паттерн состояния — пишет `$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
```svelte
// ✅ 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
```svelte
// ✅ 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
```typescript
// ✅ 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
```typescript
// ✅ 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
```svelte
// ✅ 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)
```svelte
// ✅ 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.
**Формат:**
```typescript
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 агент галлюцинирует поля.
**Формат:**
```typescript
// @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)` → конкретный интерфейс `Run`
- `dashboard: any``Dashboard`
- `catch (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)
### Что работает
1. **Model-first на Svelte 5 runes** — класс с `$state` атомами + методами. Тестируется без DOM (`new Model()` → прямое тестирование инвариантов за ~10ms).
2. **Семантические токены** — замена `bg-indigo-600 text-white``bg-primary text-white` делает дизайн централизованно управляемым. Batch-замена regex на странице заняла 5 минут, дала 87% сокращение violations.
3. **Guardrail-скрипт**`scripts/audit-frontend-style.mjs` даёт CI-проверку за секунды, каталогизирует весь долг.
4. **Компонент-экстракция** (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 options
- `DashboardSelectionModel` — checkbox, select all/visible, bulk actions
- `DashboardGitActionsModel` — git init, sync, commit, pull, push, status batch
- Основной `DashboardHubModel` — композиция submodels + core (pagination, load, environments)
### Что ломалось
1. **`this`-контекст** — при переносе методов в класс Model, все `onclick={m.method}` теряют `this`. Нужно `onclick={() => m.method()}`.
2. **Legacy сторы в Model**`$derived(store.field)` в `.svelte.ts` не работает (store — объект, не значение). Нужно оставлять deriveds в `.svelte` файле.
3. **Double `m.` prefix**`replaceAll` привёл к `m.m.openFilterColumn` в одном месте. Проверка grep `m\.m\.` обязательна после batch-замены.
4. **Source-scanning тесты** — тесты, ищущие `function handleX` в исходниках, ломаются при рефакторинге. Нужно переписывать на поведенческие.
### Рекомендации для следующих PR
1. Перед model-extraction: сначала вынести чистые helpers, потом компоненты, потом Model.
2. После замены `$state`→Model: grep `\$\state(` в page для проверки, что все ушли.
3. После batch-замены цветов: `npm run build && node scripts/audit-frontend-style.mjs`.
4. Тесты на source-scanning заменить на `render` + `screen.findByText` проверки.
---
*Аудит выполнен 2026-06-01. Ревизия 3: 2026-06-02 (PR 0-4 выполнены). Полный контекст: ~120 файлов, 9 UI-атомов, 55+ компонентов, 10 моделей, 9 сторов, 35+ страниц.*