989 lines
46 KiB
Markdown
989 lines
46 KiB
Markdown
# 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+ страниц.*
|