# План: линтеры, тесты и качество кодовой базы > Статус: **archived** Архивирован: 2026-09-26 Удалить после: 2026-10-26 > Причина: все этапы Q0–Q8 выполнены или закрыты решениями; рабочие части > перенесены в `docs/quality-gates.md`, `docs/testing-strategy.md`, > `docs/agent-workflow.md`, `AGENTS.md`, `web/eslint-plugins/README.md` и > `docs/backlog.md`. Использование: только историческая справка > > Выполнены Q0–Q5, Q6a–Q6f.5, Q7a–Q7e и Q8.1–Q8.3; открытых шагов не осталось. > > Источник: `docs/backlog.md`, аудит конфигурации `web/`, e2e-тестов и > инфраструктурных скриптов. Q0–Q5, Q6a–Q6f.5, Q7a–Q7e и Q8.1–Q8.3 выполнены. > Решения, важные за пределами плана, живут в `docs/quality-gates.md`, > `docs/testing-strategy.md` и `AGENTS.md`. > > Цель: сделать качество кодовой базы воспроизводимым, убрать ложную «зелёность» > проверок, стабилизировать тестовые контракты и только после этого расширять > автоматические правила. ## 1. Границы и принципы 1. Сначала stabilизировать существующие проверки и тестовые контракты, а не добавлять новые сложные lint-правила. 2. Старый UI удалён: production работает на корневых маршрутах, а `/v1/**` и v1-specific код больше не являются частью приложения. 3. Не добавлять новые v1-фичи, не чинить v1-баги и не расширять v1-тесты. 4. `core/` и `theme/` — общие модули. Их изменения проверяются unit-тестами и smoke-сценариями production-потребителей. 5. Изменения делаются небольшими атомарными частями, в соответствии с `AGENTS.md`. 6. Каждый новый lint-rule сначала появляется в report-only/warn-only режиме, получает fixtures и тесты, и только после очистки baseline становится blocking. 7. Детали реализации правил остаются в `web/eslint-plugins/README.md`; `AGENTS.md` содержит краткие обязательные политики, понятные агенту без дополнительных поисков, и ссылки на подробные документы. ### Политика удаления legacy v1 - Q3 — отдельная атомарная cleanup-задача; поддержка v1 не возвращается. - Старые маршруты и UI-файлы удаляются без redirects и без совместимого maintenance-режима. - В рабочей кодовой базе не оставлять UI-v1-пути, исключения или документацию, которые выглядят как действующий источник совместимости. - Исторические упоминания допустимы только в `docs/archive/`. - Будущий API-контракт `POST /v1/process` из `docs/plan-platform.md` не связан с удалённым UI-v1 и остаётся в планах. ## 2. Текущее состояние quality-контура - `pnpm verify` объединяет typecheck, unit, rule-тесты, lint, форматирование, docs и production build; deploy выполняет его перед сборкой. - `i18n/dict-consistency` даёт 0 warnings на текущем baseline; после Q7 вычищены 4 неиспользуемых design token, token audit тоже чист. - E2E-контракт использует semantic locators и общий source-aware schema resolver; активных known-issue `test.fixme` нет. - `ToolError` хранится как key/vars и переводится только при выводе; смена локали обновляет уже видимый alert. - E2E гоняется в четырёх Playwright-проектах: полный Chromium, Firefox и WebKit для browser-critical спецификаций и mobile Chromium 390×844. Job остаётся non-blocking, число воркеров ограничено из-за памяти. ## 3. Этапы выполнения ### Q0. Зафиксировать baseline и синхронизировать документацию **Статус:** готов к ревью. **Оценка:** S. **Зависимости:** нет. - Проверить фактические команды и зафиксировать их в quality-gate документации. - Вынести расширенный workflow агента в `docs/agent-workflow.md`, оставив в `AGENTS.md` только обязательные инварианты и ссылки. - Обновить `docs/backlog.md`: - отметить dictionary linter как реализованный, но не доведённый до zero-warning; - убрать противоречие по кнопке Generate; - синхронизировать числа инструментов и тестовых сценариев. - Восстановить или исправить ссылку на раздел G в `docs/checklist-manual-testing.md`. - Убрать устаревшее утверждение про hardcoded debt в `kit` из `AGENTS.md`, если оно больше не соответствует коду. - Перенести явно устаревшие `docs/analysis.md` и `docs/sorting-backend.md` в `docs/archive/`, добавить даты архивирования и удаления через 30 дней. - Создать `docs/README.md` как индекс активных документов и карту источников истины; подробные правила не дублировать в корневых файлах. **Готово, когда:** один документ описывает текущие команды, актуальный baseline и все известные `test.fixme` имеют ссылки на зарегистрированный баг. ### Q1. Единый quality gate и CI **Статус:** готов к ревью. **Проверено:** `pnpm verify` проходит; E2E оставлен отдельным non-blocking job. **Оценка:** S–M. **Зависимости:** Q0. Добавить read-only `verify`-команду, которая запускает: ```text pnpm --dir web check pnpm --dir web test pnpm --dir web test:rules pnpm --dir web lint:all pnpm --dir web exec prettier --check . pnpm check:docs pnpm --dir web build ``` E2E оставить отдельным тяжёлым гейтом: ```text pnpm --dir web test:e2e ``` - Не дублировать вручную аргументы ESLint/Stylelint в нескольких скриптах; оркестратор должен вызывать package scripts или единый общий модуль. - Описать фактические команды и semantics ошибок/warnings в `docs/quality-gates.md`; здесь нужна точная процедура, а не длинный дубль в `AGENTS.md`. - На первом этапе явно разделить `errors` и `warnings`. - После очистки i18n baseline добавить строгий режим `--max-warnings=0`. - Добавить отдельный workflow для pull request: typecheck, unit, rule-тесты, lint, format и build. - E2E сначала запускать отдельным job; сделать его blocking после закрытия известных ошибок и стабилизации baseline. Условия перехода и текущая политика описаны в `docs/quality-gates.md`, раздел «Политика e2e в CI». **Готово, когда:** одна команда воспроизводит все быстрые проверки, а deploy не может пройти при type/unit/lint/build-ошибке. ### Q2. Политика i18n и zero-warning baseline **Статус:** готов к ревью. **Проверено:** i18n lint даёт 0 warnings, rule-тесты и полный `pnpm verify` проходят. **Оценка:** M. **Зависимости:** Q0, Q1. - `BASE_LOCALE` — единственный источник registry-English fallback для `tools.*.title` и `tools.*.description`; остальные локали требуют физические ключи. - Временно неиспользуемые `tools.*.params` исключены pattern-based policy до отдельной очистки мёртвого словаря; пустые значения и placeholders не подавляются. - Исправлены три реальных RU option-label gaps и удалены неиспользуемые option-группы из `ru.ts`. - Исправлен tie-break placeholder parity: при равенстве голосов выбирается `BASE_LOCALE`. - `lint-all` запускает ESLint с `--max-warnings=0`; unused-token сообщение оставалось неблокирующим и закрыто в Q7. - Plural-формы остаются отдельной задачей после Q2 и не смешиваются с этим этапом. ### Q3. Удалить UI-v1 (cleanup завершён) **Статус:** выполнен. **Проверено:** `pnpm verify` и production E2E smoke проходят; после проверки обновлены конфиги и активные документы. **Оценка:** M. **Зависимости:** Q0, Q1, Q2. - Удалены `web/src/routes/v1/**`, `web/src/lib/v1/**`, `app_v1.css`, v1-specific i18n/tests и fixtures. - Удалены isolation plugin, его тест и все v1-исключения из lint-конфигураций. - Убраны v1-ссылки из production-кода и активных документов; исторические упоминания остаются в `docs/archive/`. - Проверены production build, verify и отдельный E2E smoke production-маршрутов. **Результат:** `/v1/**`, UI-v1-файлы и legacy-конфигурация отсутствуют; следующий этап — Q5. ### Q4. Тестовый контракт e2e **Статус:** выполнен. **Проверено:** `pnpm verify` и полный Playwright smoke проходят; 105 тестов passed, 4 known-issue теста skipped. **Оценка:** M. **Зависимости:** Q0–Q3. - Добавлен `docs/testing-strategy.md` с locator, fixture, i18n и `test.fixme` policy. - В production markup добавлены только разрешённые hooks для source/result/ verdict/empty-state; текстовые элементы получили accessible labels, фильтры — `aria-pressed`, а каталог — семантические heading/status landmarks. - E2E переведён на `role`, label и stable `href`; удалены `TOTAL`/`GROUPS`, exact fuzzy counts, internal CSS selectors, `alt`-locators и полные verdict strings. Точные verdict-переводы перенесены в unit-тесты. - Helpers централизованы в `web/e2e/helpers/page.ts`; исправлены stale theme/source/locale checks. Theme fixme снят. - Q5-фиксы resize/crop, generator, Reset и error-сценария не смешивались с этим этапом. **Результат:** изменение формулировки, класса или порядка элементов не ломает поведенческий тест без изменения пользовательского контракта; следующий этап — Q6. ### Q5. Закрыть известные preview-баги **Статус:** выполнен. **Проверено:** unit, Q5 E2E smoke и полный `pnpm verify` проходят; активных known-issue `test.fixme` нет. **Оценка:** M–L. **Зависимости:** Q4. - Добавлен общий opt-in `defaultFromSource` resolver в `registry-schema`: он принимает source dimensions, обновляет только source-aware поля при upload и используется в UI, executor и worker. `resize-png` и `crop-png` получают размеры текущего source; static/generator/text schemas не затронуты. - Reset сохраняет source/text, восстанавливает source-aware defaults и запускает deferred auto-run через debounce. - Генераторы проверены в auto-run без кнопки Generate; кнопка не добавляется. - `ToolError` хранится как key/vars, переводится derived-значением при выводе и обновляется при смене локали. E2E использует независимый invalid pixel-count сценарий. - Resize/crop, generator, Reset и localization fixme сняты; Q6a расширяет покрытие, не смешиваясь с исправлением этих контрактов. **Результат:** Q5 known issues закрыты и отражены в `docs/backlog.md` и `docs/checklist-manual-testing.md`; Q6a выполнен, следующий этап — Q6b. ### Q6. Расширение unit и browser-покрытия **Оценка:** M–L. **Зависимости:** Q4, Q5. #### Q6a. Executor protocol, fallback и debounce **Статус:** выполнен. Unit-тесты покрывают debounce, worker image/string/ verdict/files protocol, `ToolError` transport, constructor/postMessage/onerror/ malformed fallback, source-aware sanitization и DOM metadata. Browser smoke проверяет rapid auto-run и no-worker fallback; deferred transport fallback покрыт unit-тестами. - `execute(tool, ctx)` сохраняет публичный API; `createExecutor` и protocol handler являются внутренними test seams. - `ToolError` не запускает direct retry повторно; transport/protocol failures отключают worker и переходят в direct fallback. - DOM-зависимые registry entries помечены `domOnly`, поэтому зелёный smoke не скрывает нормальный worker fallback. - `debounce` покрыт fake timers; E2E подтверждает последние значения при быстрых последовательных изменениях генератора. #### Q6b. FileResult → ZIP → download **Статус:** выполнен. Q6a закрыл worker-транспорт; Q6b добавил проверяемый ZIP seam, unit round-trip для `FileResult`, защиту пустого результата и error/busy контракт download. Отдельный browser-сценарий скачивает архив и проверяет имена частей и PNG-сигнатуры. - `buildZipEntries` не зависит от DOM; `downloadZip` остаётся UI-обёрткой. - `SchemaToolView` переиспользует `downloadBlob`, ловит ошибки кодирования и блокирует повторный download на время сборки. - E2E использует `downloadResultFile` и `unzipSync`, не добавляя новые `data-testid`; files-инструменты не проходят через image-smoke. #### Q6c. Output MIME/quality **Статус:** выполнен. Q6c покрывает текущий фиксированный output-контракт JPG/WebP/BMP, не превращаясь в UX-райз кнопки Download. Registry проверяет согласованность MIME/extension/quality-поля, unit — границы quality, browser — реальные сигнатуры файлов и разницу размера при изменении quality. - `ToolEntry.output` остаётся декларативным описанием формата; новый селектор форматов не добавляется. - `validateOutputQuality` отделяет проверку 0..1 от DOM-рендеринга; JPEG/WebP используют quality `1..100` из schema, BMP не имеет quality-параметра. - E2E проверяет JPEG `FF D8 FF`, WebP `RIFF/WEBP`, BMP `BM` и PNG-фолбэк. - Ошибки кодирования и unsupported MIME используют существующие i18n-ключи. #### Q6d. Malformed и special PNG fixtures **Статус:** выполнен. Runtime-fixtures покрывают CRC, truncated, palette и 16-bit без бинарных файлов в git. E2E фиксирует браузерный контракт: critical CRC/truncated отклоняются без падения, ancillary CRC и хвост после `IEND` допускаются, palette/16-bit декодируются. Unit фиксирует контракт анализаторов для RGBA-данных, которые браузер отдаёт после нормализации. - `web/e2e/helpers/fixtures.ts` собирает indexed, 16-bit, CRC-mutation, truncation и tail-варианты из уже существующих `crc32`/`chunk`. - Отклонение битых файлов — свойство декодера движка, а не единый продуктовый контракт: Q6f.3 зафиксировал, что Firefox и WebKit декодируют `idat-cut` и `truncated-no-iend`, а WebKit ещё и `crc-bad-idat`. Это ожидаемое поведение, которое закреплено per-engine в `web/e2e/png-fixtures.spec.ts`. - Обязательное требование при любом исходе: файл не роняет страницу, ошибка показывается alert-ом, а не «пустым» результатом. - Поведение `createImageBitmap` является частью browser-контракта, а не самостоятельного PNG-парсера product-кода. - Сырое сообщение браузера при decode-ошибке не локализуется в Q6d; это отдельная задача в backlog. #### Q6e. Registry-wide smoke **Статус:** выполнен. Smoke расширен до всех 122 production tools из registry: image→image, image→text, text→image, generator input→image и verdict. FileResult и malformed/special PNG не дублируются — они уже покрыты Q6b/Q6d. - `tools-smoke.spec.ts` расширен image/text-output кейсами, включая все незакрытые color/geometry инструменты. - `generators.spec.ts` использует единый data-harness для всех генераторов; `single-color-png` сохраняет отдельную проверку auto-run/размеров. - Text-input сценарии используют общий `renderText` helper, включая `data-uri-to-png`. - Component-тесты не добавлены: Vitest работает в node без DOM/Testing Library, а текущий e2e-контракт уже покрывает поведение preview. Выбор test seam или DOM-окружения перенесён в Q6f. #### Q6f.1. Layout resolver seam **Статус:** выполнен. Чистая логика группировки полей вынесена из `SchemaFields.svelte` в `resolveLayoutGroups` и покрыта unit-тестами на дедупликацию, неизвестные поля, пустые группы, `cols` и default-хвост. - Компонент использует общий resolver, поэтому unit-тесты блокируют изменения layout-логики без DOM/Testing Library. - `SchemaPreview` view-model и DOM-компоненты не менялись; их seam остаётся отдельным решением Q6f.2. - Новые зависимости, vitest-конфиг и browser matrix не добавлялись. #### Q6f.2. SchemaPreview model seam **Статус:** выполнен. Вычисления `sourceValue`, `resultValue`, `formatValue` и `hasResult` вынесены в `buildSchemaPreviewModel`; Svelte-компонент использует модель, а unit-тесты проверяют все input/result-ветки без DOM. - Translator передаётся в модель явно, поэтому unit-тесты не зависят от locale state и проверяют только view-model. - image/text/verdict/files и пустые результаты покрыты в `pnpm verify`. - Новые зависимости, vitest-конфиг и browser matrix не добавлялись. #### Q6f.3. Browser matrix и mobile viewport **Статус:** выполнен. Playwright запускает четыре проекта: полный Chromium, Firefox и WebKit для browser-critical спецификаций и mobile Chromium 390×844 с `hasTouch` для навигации, каталога, генераторов и pipeline. - `web/playwright.config.ts`: `browserContractTests` — `pipeline.spec.ts`, `png-fixtures.spec.ts`, `text-and-verdicts.spec.ts`; `mobileTests` — `navigation.spec.ts`, `catalog.spec.ts`, `generators.spec.ts`, `pipeline.spec.ts`. - `workers` — 1 локально и 2 в CI. Browser matrix ограничена памятью: при 12 воркерах на машине с 16 GB RAM браузеры не успевают поднять worker, и тесты падают без ошибок (проверено и в Chromium). Локально matrix поэтому медленная, но почти не нагружает компьютер. Per-project лимита воркеров в Playwright нет. - Per-project `expect.timeout` не задаётся: Firefox и WebKit отдают результат так же быстро, а расхождения объясняются памятью, а не скоростью движка. - Декодеры движков расходятся, и это зафиксировано как ожидаемое поведение: `png-fixtures.spec.ts` хранит список движков, отклоняющих каждый битый файл, и не пропускает проверки. В любом браузере файл не должен ронять страницу, а отказ декодера показывается alert-ом. - CI устанавливает `chromium firefox webkit`; job остаётся non-blocking. - Локальный прогон: 276 passed, 0 skipped, 0 failed. Visual snapshots и coverage thresholds не добавлялись — это Q6f.4. #### Q6f.4. Coverage thresholds **Статус:** выполнен для чистой логики. Vitest считает покрытие только по `src/lib`, с явным списком исключений, и `pnpm verify` падает при падении ниже порога. - `web/vitest.config.ts`: provider `v8`, `reporter: text-summary`, пороги statements 90, branches 82, functions 72, lines 91. - Пороги поставлены на 1.5–2.5 п.п. ниже измеренного baseline (91.77 / 83.3 / 74.31 / 92.53), поэтому gate ловит регрессию, а не текущий долг. - Scope — логика, которую node-Vitest вообще может исполнить. Исключены браузерные модули `core/domText.ts` и `core/io.ts` (canvas, `createImageBitmap`), data-модули `i18n/en.ts`, `i18n/ru.ts`, `tool-icons.ts`, rune-файлы `*.svelte.ts`, entry worker-а и type-only `components/define.ts`. Причина каждого исключения зафиксирована в конфиге и в `docs/testing-strategy.md`. - `registry/*` (47–81% statements, 25–47% functions) в scope остаётся: это обычная непокрытая логика, а не ограничение окружения. Покрывается e2e-smoke Q6e, но unit-порог её не скрывает. - `verify` теперь выполняет `test:coverage` вместо `test`: набор тестов тот же, двойного прогона нет. - Порог functions самый слабый (74.31% против 91.77%): добавление инструмента с непокрытым `run` быстрее всего опускает именно его. Это ожидаемо, порог держит запас. - DOM-тесты `SchemaToolView`/controls сознательно не начаты: node-Vitest не имеет DOM, а e2e уже покрывает поведение. Задача живёт в `docs/backlog.md`. Не применять один одинаковый smoke-сценарий к image-, text-, generator- и file-result-инструментам. #### Q6f.5. Visual snapshots **Статус:** решено не делать. Обоснование, чем закрыта потребность сейчас и при каком условии стоит вернуться — в `docs/testing-strategy.md`, раздел 8. ### Q7. Укрепление lint-инфраструктуры **Оценка:** M. **Зависимости:** Q1, Q2, Q3. #### Q7a. Очистка неиспользуемых design tokens **Статус:** выполнен. Token audit чист: `All app.css tokens are used somewhere`. - Удалены `--color-text-accent-muted` (light и dark, иначе ломается parity), `--space-text-s: 0`, `--size-content-max: 880px` и `--duration-m: 0.3s`. - Проверено, что мёртвые токены — это пыль, а не признак хардкода: дублирующих значений (`max-width`, `0.3s`, muted-цвет) в `src` нет. Потребители `space-text-m/xl/2xl`, `size-content-max-wide` и `size-workspace-min` используются, поэтому шкалы не тронуты. - Шкалы остались неполными намеренно (`duration-s/l` без `m`, `space-text` без `s`): мёртвый элемент шкалы хуже, чем «дырявая» шкала, а вернуть его можно вместе с потребителем. - Режим unused-аудита решён в Q7d: он остаётся предупреждением. #### Q7b. Тесты token audit и PostCSS HCT-плагина **Статус:** выполнен. У обоих не было ни одного теста, хотя HCT-плагин (`web/scripts/postcss-hct.mjs`, 430 строк) пересчитывает всю палитру. - `web/scripts/__tests__/postcss-hct.test.mjs` — 17 тестов: literal и производный `hct()`, seed из `:root` и из hex, цепочка hct-токенов, refs `h/c/t`, `calc/min/max/clamp`, обёртка hue, клампы tone/chroma, несколько вызовов в одном объявлении, а также warn-and-keep для неизвестного seed, циклической ссылки и битого тела `hct()`. - `web/scripts/__tests__/token-audit.test.mjs` — 15 тестов на parity (light-only, dark-only, derived, не-цвет), authorship (hct, color-mix, seed-токены, хардкод с локацией, игнор не-цветов и обычных свойств), unused и предикаты `isColorValue`/`isDerived`. - Ожидания не зашивают вывод плагина, а сравнивают его с эквивалентной литеральной записью: `hct(from var(--seed) h c calc(t + 10))` должен совпасть с `hct(120 40 60)`, если seed — `hct(120 40 50)`. Иначе тест просто зафиксировал бы текущее поведение. - Единственный seam: `checkUnused(root, used = collectUsedTokens())` — зависимость от чтения `src` стала инъецируемой, production-вызов не изменился. - `vitest.config.ts` подхватывает `scripts/**/*.test.mjs`, поэтому скрипты покрываются обычным прогоном и `verify`; `test:rules` не трогали. - Проверка мутацией выявила дыру: первая версия тестов не ловила кламп tone в ветке `from` (смена `clamp(t, 0, 100)` на `0, 90` проходила зелёной). Добавлен отдельный тест на производный кламп; после этого мутация падает. #### Q7c. Базовый JS recommended для инфраструктуры линтинга **Статус:** выполнен. `eslint-plugins/**` и `scripts/**` больше не вне линта. - `web/eslint.config.js`: `toolingFiles = ["**/eslint-plugins/**/*.js", "**/scripts/**/*.mjs"]` получают `js.configs.recommended` плюс `languageOptions` с `globals.node` (иначе `no-undef` ругается на `console`, `process` и `URL` в Node-скриптах). - Полные TS/svelte-наборы туда не подключаются: это `.mjs` и конфиги, а не продуктовый код. - Масштаб оказался небольшим: 28 проблем, из них 26 — `no-undef` на Node-глобалах и только 2 настоящие: мёртвые импорты `existsSync` и `resolve` в `scripts/serve-static.mjs`. Линтер сразу нашёл пыль в инфраструктуре, для которой его раньше не было. - `eslint-plugins/**` дал ноль проблем, а новые тесты `scripts/__tests__` тоже попали под правило. - Проверено пробой: после включения неиспользуемая переменная и вызов неопределённого глобала в `scripts/` дают ошибки, то есть правило применилось, а не молча пропустило файлы. #### Q7d. Решения по оставшимся вопросам **Статус:** закрыт тремя решениями. 1. **Обычный CSS под те же токен-правила, что Svelte styles — узко.** В `src` всего один CSS-файл, и он же словарь токенов, поэтому переносить туда `no-hardcoded-in-svelte` и подобные нечего: stylelint уже проверяет `src/**/*.css` (префиксы, запрет `!important`, цвет только у brand-токенов), а `check-tokens.mjs` дублирует проверку цвета. Реальный пробел был один — `var()` может ссылаться на несуществующий токен, — его закрыл новый модуль `web/scripts/token-audit/refs.mjs` (см. Q7e). Широкую унификацию механизмов решено не делать: это лишний слой поверх дублирующихся проверок. 2. **Конфиги под рекомендации — только JS.** `*.config.js` (`eslint`/`postcss`/`stylelint`) добавлены в `toolingFiles`; по измерению они дают 0 проблем, поэтому это бесплатно. TS-конфиги (`vite`, `vitest`, `playwright`) не подключены: им потребовался бы `tseslint`-парсер и запись `vite.config.ts` в `allowDefaultProject`, а выигрыш близок к нулю. Попутно удалена мёртвая запись `svelte.config.js` в `allowDefaultProject` — такого файла нет. 3. **Unused-аудит остаётся предупреждением.** Правило «новый токен — вместе с потребителем» закреплено в `AGENTS.md`. Причина: `verify` входит в deploy-workflow, поэтому стилевой долг блокировал бы деплой сайта; кроме того, чтение токена из JS (`getPropertyValue`) аудит не видит и дал бы ложное срабатывание. Baseline чистый, пыль вычищена в Q7a. #### Q7e. Проверка резолва `var()` в `app.css` **Статус:** выполнен. Аудит падает, если `var(--x)` в `app.css` указывает на токен, которого нет в этом же файле. - `web/scripts/token-audit/refs.mjs` + вызов в `check-tokens.mjs`; режим — ошибка, а не предупреждение, потому что это дефект (объявление молча отбрасывается), а не стилистический долг. - `var(--x, fallback)` не считается ошибкой: fallback легален без определения токена. - 7 тестов в `scripts/__tests__/token-audit.test.mjs`: резолв в обеих темах, опечатка с локацией, fallback, дедуп повторов, ссылка внутри определения токена, `var()` в обычном свойстве. - Проверено на живом файле: временная опечатка `var(--color-bordr)` дала `Unresolved var() references` и код выхода 1; `app.css` восстановлен байт в байт. ### Q8. Новые автоматические правила **Оценка:** L. **Зависимости:** Q4, Q7. **Разбивка:** три атомарных шага, каждый заканчивается либо работающим warn-правилом с нулевым baseline, либо измерением с записью вывода. Механически важно: `lint-all` запускает ESLint с `--max-warnings=0`, поэтому warn-правило обязано приземлиться без срабатываний на текущем коде. #### Q8.1. Пользовательские тексты вне i18n **Статус:** выполнен. Пилот `i18n/no-hardcoded-user-text` (warn) с нулевым baseline; 2 реальных нарушения исправлены. - Измерение до правила: 42 строковых атрибута пользовательского текста, все — в `/kit`; вне кита текстовые узлы дали 2 находки. Итог после исключений: 2 настоящих бага и 2 бренда, которые план требовал исключить. - `web/eslint-plugins/i18n/no-hardcoded-user-text.js`: узлы `SvelteText` плюс строковые литералы пользовательских атрибутов; `