Files
ss-tools/frontend/ai-native-design-audit.md

46 KiB
Raw Blame History

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. Методология
  2. Архитектурный ландшафт
  3. Найденные проблемы
  4. Причины плавающего дизайна
  5. AI-Native Design Rules
  6. Приоритетный план исправлений
  7. Статус исправлений
  8. Следующие шаги для AI-First
  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)

Определены семантические цвета, которые должны быть единым источником истины:

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         │
│  ├── 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/
├── 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) → конкретный интерфейс 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

// ✅ 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).


Перед каждым 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) → конкретный интерфейс 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+ страниц.