aidance-tools
| Install | |
|---|---|
npm install aidance-tools |
|
| Latest Version: | 0.1.0 |
| License: | MIT |
| Last Updated: | Sep 20, 2026 |
| Links: | npm |
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-*(кроме имён членов enumx-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 rule to enforce structured logging with Pino in Nuxt, Next, and general J...
ESLint rules to enforce Nuxt 4 layer architecture with configurable imports
Shared ESLint plugin and flat config for DiPhyx Nuxt/Vue + Harlemify projects
Version History
| Version | Released | License |
|---|---|---|
| 0.1.0 | MIT |