# Frontend Design Audit: ss-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+ страниц.*
