BetaДокументация для beta‑теста, возможны ошибки и неточности.
Перейти к содержимому

Проверка границ в ежедневной работе ​

Правила линтера работают, пока их запускают. Эта статья о том, как встроить проверку архитектуры в сборку и разработку так, чтобы её нельзя было забыть, и как не дать проекту разрастись: примеры взяты из Projector, где FEOD обязателен во всех корнях.

Только правила FEOD, отдельно от остального линта ​

В проекте уже есть обычный линт и форматтер, а архитектурная проверка нужна как отдельный шаг: быстрый, с понятным результатом и без правил стиля. Projector запускает Oxlint, в который включены только правила FEOD:

js
// scripts/check-feod.mjs (сокращённо)
import { createConfig } from "@o-feod/oxlint-structure-plugin/configs";
import feodConfig from "../feod.config.mjs";

export function checkFeod({ rootDir, config = feodConfig } = {}) {
  // собирает временную конфигурацию Oxlint из feod.config.mjs,
  // запускает Oxlint только с правилами FEOD и возвращает диагностики
}

Политика лежит в одном файле feod.config.mjs, а скрипт её читает. Благодаря этому правила одинаковы в терминале, в сборке и в редакторе.

Команда подключена в три места:

ГдеПоведение при нарушении
buildблокирует сборку
testблокирует прогон тестов
общий checkпадает вместе с типами и тестами

Отдельный запуск (architecture) нужен для быстрой проверки перед коммитом. Результат в успешном случае короткий: FEOD: boundaries, entries, cycles and size limits checked.

Проверка рядом с dev-сервером ​

В режиме разработки блокирующая проверка мешает: сервер должен подниматься, даже если сломан один импорт. Projector запускает её асинхронно, с таймаутом и без права остановить сервер:

js
// фрагмент идеи: вызывается на старте и при перезапуске dev-сервера
checkFeodAsync({ timeoutMs: 60_000 })
  .then((diagnostics) => report(diagnostics))
  .catch((error) => log.warn(error))

Принципы:

  • Нарушение или сбой Oxlint попадают в лог. Сервер продолжает работать: медленная или упавшая проверка стоит строки в логе, а не остановки разработки.
  • Не на каждое HMR-изменение. Проверка идёт на старте и при перезапуске, иначе она съедала бы время на каждом сохранении. Постоянную подсветку даёт редактор.
  • Блокирующая версия остаётся в сборке. Там нарушение должно останавливать процесс.

Если политика зависит от окружения (например, часть корней отключена в CI), конфигурацию собирают тем же скриптом, а не копируют.

Что исключать из проверки ​

Файлы вне заданных корней (tests, scripts, docs, конфигурации инструментов) не проверяются ни одним правилом: их не нужно перечислять в ignore (файлы вне корней). Список ignore нужен для сгенерированных файлов внутри корня:

js
rootDefault: {
  ignore: ["dist/**", "public/**", "**/*.config.*", "packages/vio/tests/**"],
}

Тесты, которым нужна приватная реализация (например, повторный импорт файла с query-суффиксом для проверки перезагрузки), живут вне корней. Исключений для таких случаев в боевом коде нет: так граница остаётся честной.

Реестр больших файлов ​

Размер файла хорошо сигнализирует о том, что в нём смешано несколько ответственностей. Правило large-files не просто ограничивает размер, а превращает долг в явный список:

js
extra: {
  largeFiles: {
    thresholds: { default: 400, ".vue": 500 },
    files: {
      "server/modules/agent/jobs.ts": { ceiling: 620, plan: "Вынести хранилище задач" },
    },
  },
}

Правило работает в обе стороны. Два сообщения ниже получены на пробном проекте с порогом 25 строк (путь и план в записи реестра — условные):

text
server/modules/agent/big.ts   has 30 lines, over the 25 limit. Split it, or register it in extra.largeFiles.files with a ceiling and a split plan
server/modules/agent/mid.ts   is now 20 lines, within the 25 limit. Remove it from extra.largeFiles.files
СитуацияЧто говорит правило
Файл вырос выше порога, записи нетразбейте файл или внесите его в реестр с потолком и планом
Файл в реестре вырос выше потолкапотолок поднимать нельзя: файл нужно сократить
Файл сократился относительно потолкапотолок нужно снизить
Файл вернулся в порогзапись нужно удалить
Файл удалёнзапись нужно удалить

Остальные строки таблицы описаны в документации Projector и на пробном проекте не воспроизводились. Реестр работает как «храповик»: долг можно держать, но нельзя увеличивать, а уменьшение закрепляется. Таблицу для документации можно генерировать тем же счётчиком (largeFilesRows, renderLargeFilesMarkdown), чтобы список файлов и планов не расходился с конфигурацией (реестр больших файлов).

Считаются строки, которые видит линтер: для .vue это блок <script>.

Настройки, которые окупаются на практике ​

В Projector включено почти всё, что есть в конфигурации. Вот что и зачем:

НастройкаЗачем
imports.computed: "forbid", imports.unresolved: "forbid"нет невидимых для линтера зависимостей и опечаток в путях
cycles: { modules: true, types: "include" }циклы ловятся и в типах, а не только в значениях
modules.publicEntries: ["index.ts"], requirePublicEntry: trueу каждого модуля один явный вход
modules.forbiddenNames: ["common", "shared", "utils"]не появляются модули-свалки с размытым смыслом
submodules.visibility: "parent-only"подмодуль виден только родителю
submodules.childImportsParent: "implementation"подмодуль может прочитать типы родителя без ложного цикла parent → child → parent
globals: { allowedFiles: ["**/*.d.ts"], localImports: "forbid" }глобальный слой остаётся набором деклараций без зависимостей
inventory.uncovered: trueфайлы вне слоёв находятся, даже если их никто не открывал
transitModules по корнямсквозные контейнеры объявлены там, где они есть

Параметр childImportsParent решает типичную проблему: подмодулю нужны типы родителя, но с cycles.types: "include" такой импорт выглядел бы циклом. Политика разрешает ребёнку читать реализацию непосредственного родителя и не считает это циклом, а зависимости значений и циклы между модулями по-прежнему проверяются.

Как не дать проверке стать формальностью ​

  • Одна команда на всё. Команда check запускает архитектуру, тесты, типы и сборку. Если архитектура отдельно, её пропустят.
  • Документ рядом с политикой. Описание корней и ограничений (docs/architecture.md) хранится в репозитории, а в AGENTS.md есть строка «FEOD обязателен во всех корнях». Новый участник или ИИ-агент видит правило раньше, чем нарушает его.
  • Нарушение объясняет себя. Поле message в confine превращает «import is confined» в понятную фразу: «Linux-specific operations live in the Linux adapter».
  • Исключения видны. Всё, что не проходит проверку, либо внесено в ignore, либо в реестр больших файлов, либо в allowedIn. Это три места, где можно просмотреть накопленный долг.

См. также: одно ядро для нескольких приложений, связанные модули, FEOD для бэкенда.