aidance-tools

Правила ESLint, команды проверок для CI и сторожевые проверки для проектов на TypeScript и Nuxt
181
Install
npm install aidance-tools
Latest Version:0.1.0
License:MIT
Last Updated:Sep 20, 2026
Links: npm
Maintainer: alex7annet

aidance-tools

Инструменты проверок для проектов на TypeScript и Nuxt: правила ESLint, команды для CI (aidance <команда>) и библиотека сторожевых проверок конфигурации. Ничего из этого не знает про aidance: соглашения (имя типа Result, тег @throws, список composable, формат TODO, область покрытия) вынесены в опции, поэтому пакет ставится в любой проект.

Подключение

// eslint.config.ts
import tseslint from 'typescript-eslint'
import aidanceTools from 'aidance-tools'

export default tseslint.config(
  {
    files: ['**/*.ts', '**/*.vue'],
    languageOptions: {
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
  },
  aidanceTools.configs.recommended,
  aidanceTools.configs.typed,
)

recommended работает без типов. typed требует parserOptions.projectService (в Nuxt с @nuxt/eslint уже включено). Правило unstable-key разбирает шаблон через vue-eslint-parser, в файлах с другим парсером оно молчит.

Правила

Все правила в пресетах включены на уровне error.

unstable-key (recommended)

Ключ v-for по переменной индекса цикла. vue/require-v-for-key требует наличия ключа, но не его стабильности; индекс ломает переиспользование узлов при вставке и удалении. Имя сверяется с реальной переменной индекса окружающих v-for, включая <template v-for> и вложенные циклы. Известное ложное срабатывание: обход объекта (value, key) in obj, где вторая переменная — стабильный ключ.

missing-spec (recommended)

Модуль из каталога с логикой без спеки spec/<имя>.spec.<ext> рядом.

Опция По умолчанию Смысл
dirs ['utils', 'composables'] сегменты пути, чьи модули обязаны иметь спеку
specDir 'spec' имя подкаталога со спеками

Правило смотрит на файл, а не на дифф: в проекте с историей оно даст находки на каждом старом модуле. Включайте его на файлах изменения или гасите подавлениями.

no-cross-layer-import (recommended)

Слои Nuxt (layers/*) не зависят друг от друга, корневой общий код не зависит от слоёв. Проверяются четыре канала связи: явные импорты и реэкспорты, включая import type и import(); строковые пути в vi.mock-подобных вызовах; автоимпортируемые имена чужого слоя по .nuxt/imports.d.ts; компоненты чужого слоя в шаблоне .vue по .nuxt/components.d.ts. Без каталога .nuxt два последних канала пропускаются молча: запустите nuxt prepare.

Опция По умолчанию Смысл
rootDir рабочий каталог ESLint корень проекта
layersDir 'layers' каталог слоёв от корня
nuxtDir '.nuxt' каталог сгенерированных объявлений
aliases ~, @, ~~, @@ в корень, #layers в layers префикс → каталог от корня
allow {} слой → что ему можно: имя слоя целиком или путь внутри layers/
rootDirs [] корневые каталоги, которым запрещено импортировать слои
mockCalls vi.mock, vi.doMock, vi.importActual, vi.importMock вызовы с путём модуля первым аргументом
'aidance-tools/no-cross-layer-import': ['error', {
  rootDirs: ['components', 'composables', 'utils', 'stores'],
  allow: {
    tariffs: ['zone-picker/utils/zone-view.ts'],
    'courier-adjustments': ['adjustments'],
  },
}]

pages/ и server/ в rootDirs не включайте: страницы и BFF собирают слои воедино, это их назначение.

thin-component (recommended)

В функции компонента Vue логика, которой место в composable или чистой функции: try/catch, ветвление кроме защитного if (…) return, цикл, switch, таймер, цепочка await или тело длиннее лимита. Компонент — обвязка: объявления props/emits/slots/expose, вызовы composables, привязка результата к шаблону, обработчики из одного-двух вызовов. Тогда логика живёт в .ts, владеется по символам и тестируется без монтирования. Проверяются все функции скрипта, включая колбэки watch и computed; вложенные функции считаются отдельно. Файлы не .vue правило не трогает, нужен vue-eslint-parser.

Опция По умолчанию Смысл
maxStatements 2 инструкций в теле функции
maxAwaits 1 await в одной функции
timers setTimeout, setInterval, requestAnimationFrame вызовы, которых в компоненте не должно быть

todo-format (recommended)

Комментарий со словом TODO не соответствует шаблону.

Опция По умолчанию Смысл
pattern '^TODO: \\S' регулярное выражение для текста комментария

Для проектов с ключом задачи: { pattern: '^TODO: [A-Z]+-\\d+ ' }.

no-discarded-result (typed)

Значение типа Result или Promise<Result> выброшено: await fn() без присваивания, fn() без await, void fn(). Аналог #[must_use] в Rust. Тип считается Result, если это псевдоним с именем из опции либо объединение, у каждого члена которого есть свойство ok.

Опция По умолчанию Смысл
resultTypeName 'Result' имя псевдонима типа результата

Полноту обработки правило не проверяет: if (r.ok) { … } без ветки ошибки проходит. Для этого есть @typescript-eslint/switch-exhaustiveness-check.

no-nuxt-instance-in-fn (typed)

Внутри функции формы Fn вызван composable, которому нужен инстанс Nuxt. После первого await на SSR инстанса нет, вызов падает с «nuxt instance unavailable». Значение читается в setup и передаётся через input. Функция считается Fn, если её объявленный тип (аннотация переменной или явный тип возврата) — Promise<Result<…>>; тело, переданное обёртке (const fn: Contract = catchFailure(async (input) => …)), считается телом Fn. Функции без аннотаций не проверяются. Колбэки внутри Fn проверяются тоже.

Опция По умолчанию Смысл
banned useNuxtApp, useRoute, useRouter, useRuntimeConfig, useState, useCookie, useRequestHeaders, useRequestEvent запрещённые имена
resultTypeName 'Result' имя псевдонима типа результата

throws-declared (typed)

Реализация const fn: Contract = … поднимает ровно те ошибки, что объявлены контрактом. Два режима по форме контракта.

Режим Fn: контракт возвращает Promise<Result<…, Err>>, объявленное — теги _tag объединения Err. Тело может быть передано обёртке, проверяется первый функциональный аргумент.

type CancelOrder = Fn<Input, Order, OrderNotFound | OrderAlreadyShipped>

const cancelOrder: CancelOrder = catchFailure(async (input) => {
  const order = throwFailure(await fetchOrder({ orderId: input.orderId })) // NotFound
  if (order.shipped) throw new OrderAlreadyShipped()
  return api.cancel(order.id)
})

Считаются: throw x по типу x (тип без _tag — дефект, не считается), throwFailure(result) и return result на верхнем уровне тела по типу ошибки Result (в том числе return fail(…)), вызовы функций с @throws.

Режим JSDoc: контракт другой формы, объявленное — теги @throws на псевдониме Contract или над самой переменной. Считаются throw new X() и вызовы функций с собственными @throws.

/**
 * @throws {OrderNotFound} заказ не найден
 * @throws {OrderAlreadyShipped} заказ уже отправлен
 */
export type CancelOrder = (orderId: string) => Promise<Order>

В обоих режимах вне try; не считаются броски внутри try с обработчиком, который не перебрасывает пойманное, и throw error в catch.

Сообщение Где Смысл
undeclaredErr на броске, throwFailure, return или вызове поднимается X, но в Err его нет
unusedErr на имени переменной Err содержит X, но он не поднимается
undeclared на броске или вызове бросается X, но в @throws его нет
unused на имени переменной @throws X объявлен, но не бросается
Опция По умолчанию Смысл
tagName 'throws' имя тега JSDoc
requireDeclaration false проверять и аннотированные функции без единого тега (режим Fn проверяется всегда)
resultTypeName 'Result' имя псевдонима типа результата
throwFailureName 'throwFailure' функция, которая распаковывает Result и бросает его ошибку

Слепые пятна приняты осознанно: throw внутри колбэка, переданного другой функции, засчитывается текущей функции; catch, глотающий всё, скрывает и то, что глотать не следовало.

Конфиг проекта

Файл .aidance/tools.ts (или .mjs) в каталоге инструментария проекта, для старых проектов aidance.config.ts (или .mjs/.js) в корне — единый источник того, что у каждого проекта своё. Его читают команды lint, format, lint-fix-rule и precheck, поэтому «проверено линтером», «отформатировано» и «попало в precheck» означают один и тот же набор файлов. Пока список каталогов лежит в трёх местах, отчёт precheck не может честно ответить, был ли файл проверен вообще.

// aidance.config.ts
import { defineConfig } from 'aidance-tools/config'

export default defineConfig({
  scope: {
    // Группы — для параллельных job'ов линта в CI; вся область — их объединение.
    groups: {
      layers: ['layers'],
      rest: ['components', 'composables', 'pages', 'server', 'utils'],
    },
  },
  format: { command: 'oxfmt' },
  precheck: { docsDir: 'docs' },
})

.ts загружается Node 22.6+ (снятие типов включено по умолчанию с Node 23), для более старых версий подходит .mjs. Без файла команды работают на умолчаниях: область — текущий каталог, форматтер неизвестен. Любая команда принимает --config <путь>, чтобы указать файл явно.

Команды CI

Бинарник aidance ставится вместе с пакетом: npx aidance <команда> [флаги]. Коды возврата у всех команд общие: 0 — проверка пройдена, 1 — найдены нарушения, 2 — сломан сам запуск (негодный аргумент, нет входного файла, битый JSON). Результат идёт в stdout, диагностика — в stderr.

lint

Обёртка ESLint по области из конфига. --group <имя> сужает область до одной группы scope.groups, остальные флаги уходят в eslint как есть.

npx aidance lint                      # вся область
npx aidance lint --group layers --fix # одна группа, автофикс
npx aidance lint --format gitlab      # отчёт code quality для CI

--max-warnings 0 передаётся всегда: на предупреждениях ESLint возвращает 0, и без флага гейт пропускал бы их молча. --pass-on-unpruned-suppressions тоже: исчезнувший долг из eslint-suppressions.json — хорошая новость, а не повод покраснеть. В CI две группы гоняются параллельными job'ами (eslint-layers, eslint-rest), и отчёты code quality GitLab мержит сам.

format

Та же область, что у lint, команда форматтера из format.command. Флаги уходят форматтеру как есть.

npx aidance format          # отформатировать область
npx aidance format --check  # гейт в CI

coverage-baseline

Гейт покрытия: сравнивает coverage/coverage-summary.json (репортер json-summary Vitest) с зафиксированным coverage-baseline.json. Два гейта: пофайлово число непокрытых строк, веток и функций не растёт; общий процент строк не падает. Новый файл сам по себе гейт не валит.

npx aidance coverage-baseline --check                  # в CI
npx aidance coverage-baseline --update                 # зафиксировать улучшение
npx aidance coverage-baseline --update --allow-decrease # осознанно понизить
Флаг По умолчанию Смысл
--summary <путь> coverage/coverage-summary.json отчёт Vitest
--baseline <путь> coverage-baseline.json файл baseline в репозитории
--include <глоб> всё область покрытия, флаг повторяемый
--exclude <глоб> ничего исключения, перекрывают include

--include/--exclude дублируют coverage.include/coverage.exclude Vitest намеренно: гейт не должен зависеть от того, что фактически посчитал репортер. Понижение baseline без --allow-decrease ничего не пишет и возвращает 2.

bundle-size

Гейт размера клиентского JS: обходит каталог сборки, суммирует несжатый размер и сравнивает с bundle-size-budget.json. --update перезаписывает только факт (measured), порог budgetKib меняет человек отдельной строкой диффа. Если факт ниже порога больше чем на десятую часть, команда предупреждает о протухшем пороге. Отсутствие сборки и пустой замер — ошибки запуска, а не нулевой размер.

npx aidance bundle-size --check --output .output/public --budget bundle-size-budget.json
npx aidance bundle-size --update --source "STAFF-1234"

npm-audit

Обёртка npm audit --json: пишет полный отчёт файлом до разбора (нечитаемый вывод тоже доезжает артефактом) и различает «есть уязвимости» и «реестр недоступен» по форме JSON, а не по коду возврата npm, который в обоих случаях равен единице.

npx aidance npm-audit --audit-level high --json audit.json

precheck

Машинный отчёт по изменённым файлам мерж-реквеста для агентов ревью: всё, что проверила машина, агент не перепроверяет. Главное свойство отчёта — каждая секция различает «проверено, замечаний нет» (ok: true) и «проверка не отработала» (ok: false плюс причина в skipped). Пустая секция без записи в skipped не бывает: непроверено ≠ чисто.

npx aidance precheck --files-from changed-files.txt --diff-from changed.diff --json precheck.json  # в CI
npx aidance precheck --base main                                                                  # локально
npx aidance precheck --mr 123 --only eslint,detectors
Флаг По умолчанию Смысл
--files-from <путь> — готовый список <статус>\t<путь> (вывод git diff --name-status); git не нужен
--diff-from <путь> — готовый дифф git diff --unified=0 для детекторов и графа влияния
--base <ref> main база для локального расчёта: git merge-base + git diff
--mr <iid> — вместо --base: ветка refs/merge-requests/<iid>/head
--git-dir <путь> cwd каталог git-дерева, если он не совпадает с рабочим
--only <секции> все через запятую: eslint, format, tests, detectors, impact, openapi
--json <путь> stdout куда писать отчёт
--config <путь> .aidance/tools.{ts,mjs}, иначе aidance.config.{ts,mjs,js} конфиг проекта
--timeout <мс> 300000 таймаут одного внешнего процесса

Код возврата не зависит от находок: 0, если отчёт собран, 2 — негодные аргументы. Иначе вызывающий не отличит «нашлись замечания» от «precheck сломан».

Секции отчёта (version: 1): files — изменённые файлы со статусом и признаком области; eslint — eslint --format json по файлам области, подавленные baseline'ом сообщения считаются отдельно; format — <форматтер> --list-different; typecheck — всегда в skipped (типы проверяет отдельный шаг пайплайна); tests — vitest related --run по всем изменённым файлам, ноль связанных тестов — не «прошло»; detectors — grep-проверки добавленных строк диффа; impact — изменённые экспорты и до десяти мест вызова через git grep, с пометкой авто-импортов Nuxt по .nuxt/imports.d.ts; openapi — записи .aidance/openapi.lock.json, скачанные с фича-ветки бекенда, а не с ветки по умолчанию (уровень error: такая спецификация в main уезжать не должна, перед мержем aidance openapi fetch <проект>); без lock-файла секция чистая. Верхнеуровневый skipped сводит записи всех секций.

Область и форматтер precheck берёт из верхнеуровневых scope и format конфига (см. «Конфиг проекта»), своё — из секции precheck. Без файла область — весь проект, форматтер отключён с записью в skipped, детекторы — встроенные.

import { defineConfig } from 'aidance-tools/config'
import { BUILT_IN_DETECTORS, createTodoFormatDetector, hit } from 'aidance-tools/precheck'

export default defineConfig({
  scope: { groups: { app: ['components', 'composables', 'layers', 'pages', 'server', 'utils'] } },
  format: {
    command: 'oxfmt',
    ignore: [{ pattern: '*.config.*', test: (path) => /\.config\.[^/]+$/.test(path) }],
  },
  precheck: {
    tests: {
      resolveProjects: (files) => (files.some((file) => file.startsWith('scripts/')) ? ['tooling'] : []),
    },
    detectors: [
      ...BUILT_IN_DETECTORS.filter((detector) => detector.name !== 'todo-format'),
      createTodoFormatDetector({ pattern: /\/\/\s*TODO\b.*\b[A-Z]+-\d+\b/, expected: '// TODO ABC-123 <текст>' }),
      {
        name: 'endpoint-literal',
        why: 'конфиг ESLint ловит вызовы $fetch, но не URL-литералы',
        run: (input) =>
          input.addedLines
            .filter((line) => /https?:\/\//.test(line.text))
            .map((line) => hit({ detector: 'endpoint-literal', input, line: line.line, snippet: line.text, message: 'адрес литералом' })),
      },
    ],
  },
})
Поле По умолчанию Смысл
scope.groups (верхний уровень) весь проект каталоги области линта и форматтера; precheck.scopeDirs переопределяет
scope.extensions js, ts, vue расширения области
format.command, format.listDifferentArgs, format.ignore (верхний уровень) — / --list-different / [] форматтер; без format секция уходит в skipped
precheck.eslintCommand eslint команда линтера
tests.command, tests.args, tests.resolveProjects vitest / related --run --reporter=json / все прогонщик и выбор проектов по файлам
detectors BUILT_IN_DETECTORS полный список: чтобы отключить встроенный, его не включают
docsDir docs каталог *.md для undocumented-config
nuxtDir .nuxt где искать imports.d.ts для графа влияния

Встроенные детекторы: undocumented-config (ключ process.env.X или runtimeConfig.x не упомянут в документации), missing-spec (новый модуль в utils/, composables/ или тех же каталогах слоя без spec/<имя>.spec.ts, настраивается через createMissingSpecDetector), todo-format (пометка не по соглашению, по умолчанию // TODO: <текст>). Свой детектор — объект Detector { name, why, run(input, context) }: run получает файл, его содержимое и добавленные строки диффа и возвращает находки через hit. Детекторы дублировать правилами ESLint нельзя: одно замечание приезжало бы дважды.

lint-fix-rule

Автофикс ESLint только по перечисленным правилам. Сплошной eslint --fix по области чинит заодно подавленные ошибки из eslint-suppressions.json и переписывает сотни файлов, а флаг --rule лишь добавляет правило поверх конфига. Команда использует предикат fix Node API ESLint: нарушения собираются как обычно, фиксы применяются только к сообщениям целевых правил.

npx aidance lint-fix-rule curly --scope layers --scope utils
npx aidance lint-fix-rule vue/attributes-order unocss/order -- layers/tariffs
npx aidance lint-fix-rule curly --from-suppressions --prune --dry-run
Флаг Смысл
--scope <каталог> область прогона, повторяемый; по умолчанию текущий каталог
-- <путь> … явные пути вместо области
--suppressions <путь> файл подавлений, по умолчанию eslint-suppressions.json
--from-suppressions область — файлы, где эти правила подавлены; несовместим с путями
--prune после фикса точечно снять из файла подавлений записи, которых больше нет; несовместим с путями
--dry-run ничего не писать, только посчитать

Node API ESLint не применяет файл подавлений, поэтому «сколько осталось» команда считает сама после прогона. Штатный eslint --prune-suppressions при частичной области перезаписал бы файл целиком и потерял записи файлов вне прогона; --prune трогает только пары «файл + правило» из своей области и пишет файл в формате ESLint, так что дифф ложится ровно на снятые строки.

test-audit

Инструментальный срез тестовой базы: по каждой спеке считает метрики риска (тесты «только факт вызова», слабые утверждения toBeDefined/toBeTruthy, проверки порядка вызовов, ожидания реального времени, дубли имён тестов, отношение объёма спеки к исходнику) и печатает топы. Ни один вердикт не выносится автоматически: срез сужает область чтения, решение за человеком. Парный исходник ищется по раскладке <каталог>/spec/<имя>.spec.ts ↔ <каталог>/<имя>.{ts,vue,js}, непокрытые строки — из coverage-baseline.json.

npx aidance test-audit --top 10
npx aidance test-audit --group layers/tariffs
npx aidance test-audit --json > test-audit.json

OpenAPI: спецификации бекендов и клиенты

npx aidance openapi <подкоманда> скачивает спецификации бекендов из GitLab, нормализует их, держит имена операций и генерирует клиенты через orval. Всё настраивается в .aidance/openapi.ts; каталог .aidance/ можно удалить в любой момент — проект от него не зависит: спецификации с именами операций, сгенерированные клиенты, orval.config.mjs и файл алиасов лежат в самом проекте.

// .aidance/openapi.ts
export default {
  specsDir: 'api-server/openapi',        // спецификации в репозитории
  outDir: 'api-server/generated',        // сгенерированные клиенты, по каталогу на проект
  // orvalConfig: 'orval.config.mjs', formatter: 'oxfmt', httpClient: 'axios' | 'fetch', aliasPrefix: '#api',
  // gitlab: { url: 'https://gitlab.platform.corp', insecure: true },
  mutator: { path: 'api-server/fetch-instance.ts', name: 'customInstance' },
  projects: {
    tariffs: {
      gitlab: { projectId: '1738', specPath: 'docs/openapi/swagger.yaml' }, // defaultRef: ветка, иначе ветка репозитория
      outputs: ['types', 'tanstack', 'zod'],                                // types | zod | fetch | tanstack | mock
      domains: ['tariffs', 'zone-picker'],                                  // домены docs, которые ходят в сервис; для проверки бекенда в harness
    },
  },
} satisfies import('aidance-tools/openapi').OpenApiConfig

Токен GitLab: переменная GITLAB_TOKEN, флаг --env-file <.env> или .env в корне проекта. Общий флаг --root <каталог> задаёт корень проекта.

Подкоманда Что делает
init [--legacy <путь>] [--outputs …] [--mutator <путь>:<имя>] создаёт .aidance/openapi.ts; проекты берёт из прежнего api-server/openApiConfig.js, если он есть
fetch [проекты…] [--ref <ветка>] [--strict] скачивает спецификации, нормализует, переносит имена операций из прежнего файла, пишет openapi.lock.json; печатает переезды, новые и удалённые операции, ломающие изменения; удалённые операции проверяет на использование в проекте (код 1, если используются); --strict — код 1 и на ломающие изменения
generate [проекты…] [--clean] [--dry-run] применяет патчи в памяти, запускает orval по временному конфигу, приводит файлы к стабильным именам index.ts, types.ts, zod.ts, mock.ts; пишет постоянный orval.config.mjs в корень; --clean удаляет прежние файлы orval из каталога проекта
update [проекты…] [--ref] [--strict] fetch, затем generate, если замечаний нет
diff [проекты…] [--ref <ветка>] что изменилось у бекенда относительно файла в репозитории, без записи
project add <имя> --project-id <id> --spec-path <путь> [--outputs …] [--ref] [--spec-name] [--domains a,b] добавляет проект в конфиг текстовой правкой блока projects, после правки конфиг перечитывается
project remove <имя> убирает проект из конфига; файлы не трогает
patches [проекты…] состояние патчей: применяется, устарел, владелец починил
status откуда взята каждая спецификация; ветка, отличная от ветки репозитория, даёт код 1
aliases пишет <outDir>/aliases.mjs с алиасом #api и проверяет, подключён ли он в nuxt.config, vitest.config
migrate-imports [--dry-run] переписывает импорты и моки (vi.mock, require) с путей ~/api-server/generated/<проект>/<файл> на #api/<проект>, #api/<проект>/types, /zod, /mock

Что делает библиотека под командами:

  • Нормализация. Файл записывается заново одним эмиттером: двойные кавычки, ключи по алфавиту (кроме детей properties, их порядок задаёт порядок полей в типах), методы в порядке HTTP, массивы как есть, расширения x-* (кроме имён членов enum x-enum-varnames, по ним orval называет константы) и info.version сняты. Переформатирование у владельца сервиса не даёт diff.
  • Имена операций. operationId принадлежит фронту и хранится в спецификации в репозитории. При обновлении имена переносятся по ключу METHOD:/path; исчезнувшая и появившаяся операции с одинаковым телом или описанием считаются переездом пути, имя сохраняется. Новой операции имя генерируется по пути (POST:/api/v2/campaigns/{id}/confirm → CampaignsIdConfirmV2), при совпадении добавляется метод.
  • Ломающие изменения. Свой детектор: исчезнувшие операции, параметры и свойства ответов, смена типов, новые обязательные поля запроса, ушедшие значения enum; схемы components сравниваются один раз с перечнем операций-потребителей. Если в PATH или OASDIFF есть бинарник oasdiff, его отчёт печатается рядом.
  • Патчи. .aidance/openapi-patches/<проект>.ts, применяются при генерации к копии спецификации; файл в репозитории остаётся как у владельца.
// .aidance/openapi-patches/tariffs.ts
export default [
  {
    id: 'finish-at-nullable',
    reason: 'сервис принимает null в finish_at, спека объявляет string',
    task: 'STAFF-3105',
    target: '/components/schemas/FinishTariffRequest/properties/finish_at', // JSON Pointer
    expect: { type: 'string' },   // не совпало — патч устарел, генерация остановлена
    reject: { nullable: true },   // совпало — владелец починил, патч пора удалить
    op: 'merge',                  // merge | set | remove | add | rename (value — новое имя ключа)
    value: { nullable: true },
  },
  { id: 'custom', reason: '…', apply: (spec) => spec },                   // запасной выход: функция
] satisfies import('aidance-tools/openapi').OpenApiPatch[]

Переход со скриптов api-server/scripts: init → fetch → generate --clean → aliases и подключение openapiAliases в nuxt.config.ts и vitest.config.ts → migrate-imports. Патчи из openapi-patches.mjs переписываются в декларативную форму (см. openapi-plan.md).

Сторожевые проверки

aidance-tools/guards — чистые функции над разобранными package.json, package-lock.json, конфигом Vitest и .gitlab-ci.yml. Каждая возвращает список нарушений, пустой список — проверка пройдена. Спека проекта читает файлы, вызывает проверку и ожидает пустой список: текст нарушения тогда виден прямо в отчёте Vitest.

// spec/dependencies.spec.ts
import { describe, expect, it } from 'vitest'
import {
  findDuplicatedDependencies,
  findDuplicatedPackages,
  findPinLockMismatches,
  findUnpinnedDependencies,
  readJsonFile,
} from 'aidance-tools/guards'
import type { PackageJsonFile, PackageLockFile } from 'aidance-tools/guards'

const pkg = readJsonFile<PackageJsonFile>('package.json')
const lock = readJsonFile<PackageLockFile>('package-lock.json')
const exceptions = { 'better-sqlite3': 'диапазон закреплён отдельной спекой' }

describe('зависимости', () => {
  it('прод-зависимости прибиты точной формой =X.Y.Z', () => {
    expect(findUnpinnedDependencies(pkg, { exceptions })).toEqual([])
    expect(findPinLockMismatches(pkg, lock, { exceptions })).toEqual([])
  })

  it('нет дублей dev/prod и вторых экземпляров в дереве', () => {
    expect(findDuplicatedDependencies(pkg)).toEqual([])
    expect(
      findDuplicatedPackages(lock, ['vite', '@nuxt/kit', 'undici'], {
        ignorePrefixes: ['node_modules/@sentry/cli/'],
      }),
    ).toEqual([])
  })
})
Функция Что ловит
findUnpinnedDependencies(pkg, { section, exceptions }) диапазон вместо =X.Y.Z
findStalePinExceptions(pkg, { exceptions }) исключение, которое пора убрать
findPinOverrides(pkg) overrides/resolutions в обход пинов
findPinLockMismatches(pkg, lock) установленная версия не равна пину
findDuplicatedDependencies(pkg) пакет и в dependencies, и в devDependencies
findRootLockMismatches(pkg, lock) корневой узел лока классифицирует иначе
findDevFlaggedDependencies(lock, names) прод-узел с dev/devOptional/optional
findDuplicatedPackages(lock, names, { ignorePrefixes }) не ровно один экземпляр в дереве
findNestedResolutions(lock, name) потребитель видит не корневую версию
findExtraneousPackages(lock) узлы extraneous
findProjectsMissingExclude(config, pattern) проект Vitest без exclude, например **/.worktrees/**
partitionSpecs(config, specPaths) спеки без проекта, в двух проектах, пустые проекты
loadGitlabCi(text), needsJobNames, ruleChanges, commentBlockAbove разбор .gitlab-ci.yml с тегом !reference
collectSourceFiles(dir, { match, relativeTo }), stripComments обход исходников для собственных детекторов

Мини-разбор semver (isExactPin, compareVersions, satisfiesRange) тоже экспортируется: satisfiesRange на неизвестной форме диапазона отвечает null, а не true, чтобы проверка не проходила молча.

Граф использований

aidance-tools/graph строит граф «кто какие символы каких файлов использует» по исходникам проекта: статические и динамические импорты, реэкспорты, а для Nuxt ещё авто-импорты и авто-компоненты по .nuxt/imports.d.ts и .nuxt/components.d.ts. Импорты разбираются регулярными выражениями, парсер не нужен. Harness строит по нему связи Изменений (доноры и потребители), но функция самостоятельна: по ней же можно отвечать на «что задето правкой».

import { buildUsesGraph, consumersOfFile } from 'aidance-tools/graph'

const graph = buildUsesGraph({ root: process.cwd(), nuxtDir: '.nuxt' })
// graph.edges: [{ from: 'pages/p.vue', file: 'utils/b.ts', symbols: ['other'] }, …]
// graph.unresolved: относительные и алиасные импорты, которых нет на диске

consumersOfFile(graph, 'utils/b.ts', ['other']) // ['pages/p.vue']

Ребро без symbols означает модуль целиком: import * as ns, побочный импорт, export *, import(). Импорт по умолчанию даёт символ default. Пакеты и #app рёбрами не становятся, это граница графа. Каталоги из EXCLUDED_DIR_NAMES (node_modules, .nuxt, .worktrees, .claude, …) не обходятся и не бывают целью ребра. Алиасы по умолчанию как у Nuxt (~, @, ~~, @@ в корень), свои передаются через aliases. Суффиксы бандлера вроде ?raw отбрасываются.

Индекс кода

aidance code <подкоманда> [--root <корень>] отвечает на четыре вопроса о коде, ради которых иначе читают файлы целиком. Модель проектирования в harness получает те же четыре инструмента по MCP (aidance code serve, сервер на stdio без зависимостей), человек — текстом в терминале.

Подкоманда Что
exports <файл> экспорты файла: имя, вид, строка, первая строка объявления; для .vue — компонент и его defineProps/defineEmits/defineExpose/defineSlots/defineModel
definition <файл> <имя> код одного экспорта целиком с ведущим JSDoc (до 150 строк); default для экспорта по умолчанию, props/emits/expose/slots/model:<имя> для макросов компонента
consumers <файл> [символ ...] кто импортирует файл или перечисленные символы по графу использований (потребитель модуля целиком считается потребителем любого символа)
imports <файл> что файл импортирует из проекта; пакеты npm не входят
serve сервер MCP: initialize, ping, tools/list, tools/call, по сообщению JSON-RPC на строку

Экспорты разбираются по AST TypeScript без проверки типов, граф — тот же aidance-tools/graph, строится при первом обращении и живёт, пока живёт процесс. Пути файлов относительно корня, абсолютные допустимы внутри него. Коды возврата: 0, 1 — нет файла или экспорта, 2 — негодный запуск. Библиотечно то же доступно как createCodeIndex({ root }) из aidance-tools/code.

Общие типы Fn (aidance-tools/fn)

Блок типов, по которому работают no-discarded-result, throws-declared и no-nuxt-instance-in-fn: Failure<'Tag'>, Result, Fn<In, Out, Err>, извлекатели InputOf/OutputOf/ErrorOf, Handlers, ok, fail, assertNever, catchFailure, throwFailure. Проект импортирует его из пакета, копировать в types/ не нужно:

import { Failure, catchFailure, throwFailure, type Fn } from 'aidance-tools/fn'

class OrderNotFound extends Failure<'OrderNotFound'> {}
type CancelOrder = Fn<{ orderId: string }, Order, OrderNotFound>

export const cancelOrder: CancelOrder = catchFailure(async (input) => {
  const order = throwFailure(await fetchOrder({ orderId: input.orderId }))
  return api.cancel(order.id)
})

catchFailure превращает брошенный Failure в fail, остальное (дефекты) летит дальше. Модуль без зависимостей и без побочных эффектов, безопасен в клиентской сборке; в проекте пакет должен стоять в dependencies.

Рядом с пакетом

Пресеты не трогают правила typescript-eslint. В проекте стоит включить @typescript-eslint/ban-ts-comment с описанием, switch-exhaustiveness-check с considerDefaultExhaustiveForUnions: false, no-floating-promises и only-throw-error.

Разработка

npm run typecheck
npm test
npm run build

Правило лежит в src/rules/<имя>.ts, спека на RuleTester в src/rules/spec/<имя>.spec.ts. Правила с типами тестируются через src/spec/typed-rule-tester.ts: каждый кейс — самодостаточный файл проекта из src/rules/spec/fixtures/tsconfig.json.

Команды CI лежат в src/cli/<команда>/index.ts и экспортируют run(argv) без побочных эффектов при импорте; диспетчер — src/cli/main.ts, бинарник — src/cli/bin.ts. Сторожевые проверки — src/guards/, precheck — src/precheck/, конфиг проекта — src/config/, граф использований — src/graph/. Спеки везде в spec/ рядом с модулем.

Related Packages

eslint-plugin-pino-logger

ESLint rule to enforce structured logging with Pino in Nuxt, Next, and general J...

5,000 0
eslint-plugin-nuxt-layers

ESLint rules to enforce Nuxt 4 layer architecture with configurable imports

6,201 14
@diphyx/eslint-plugin

Shared ESLint plugin and flat config for DiPhyx Nuxt/Vue + Harlemify projects

1,247 1
@cloudflight/eslint-plugin-vue

Cloudflight eslint-plugin & eslint-config for vue

38,060
eslint-plugin-vue-storefront

ESLint rules for Vue Storefront

39,292

Version History

Version Released License
0.1.0 MIT