Запишите границы модулей и правила работы со схемой в файл инструкций проекта, но не полагайтесь на него: для агента это контекст, а не запрет. Каждое правило дублируйте проверкой, которая падает: импорты — правилом import-x/no-restricted-paths с TypeScript-резолвером, схему — prisma migrate diff --exit-code в CI, неизменность применённых миграций — сравнением с основной веткой и хуком, который не даёт агенту их редактировать. Миграцию от агента читают как код и прогоняют на копии данных до слияния.
После материала вы сможете
- описать границы модулей и правила работы со схемой в
AGENTS.mdтак, чтобы их видели Claude Code и Codex - настроить
import-x/no-restricted-pathsи убедиться, что правило действительно срабатывает, в том числе на относительные пути - поймать в CI изменение схемы без миграции и правку уже применённой миграции
- отревьюить миграцию агента: прочитать SQL, прогнать на копии данных и исправить до слияния
Что знать заранее
Проверено на
Node.js 24 LTS (24.18)Prisma ORM 7.10.0PostgreSQL 17 (postgres:17-alpine)ESLint 10.12, typescript-eslint 8.71, TypeScript 6.0.3eslint-plugin-import-x 4.17.1, eslint-import-resolver-typescript 4.4.5actionlint 1.7.12
Сверено с документацией: 12 октября 2026 г.
Содержание
Что размывает агент и почему файла инструкций мало
Ассистент решает задачу кратчайшим путём, который видит. Нужна статистика заказов — обработчик сам открывает соединение с базой. Нужна ставка НДС — импортируется константа из внутренностей чужого модуля. Нужна новая колонка — схема подгоняется командой prisma db push, а если что-то не сошлось, правится уже применённая миграция. Каждая такая правка работает локально и проходит тесты. Через месяц модули сцеплены так, что любое изменение задевает всё, а история миграций на разных базах расходится.
Первое желание — дописать запреты в CLAUDE.md или AGENTS.md. Это нужно, но недостаточно: документация Claude Code прямо говорит, что эти файлы — контекст, а не обязательная конфигурация, а чтобы заблокировать действие независимо от решения модели, нужен хук PreToolUse. Поэтому правила строятся в три слоя.
- 01Файл инструкцийAGENTS.md: какие слои есть, кто кого может импортировать, как менять схему. Агент читает и старается следовать
- 02Линтер и CIimport-x/no-restricted-paths, prisma migrate diff --exit-code, скрипт проверки миграций. Падают у человека и у агента одинаково
- 03Ограничения инструментов агентаХук PreToolUse и правила deny: агент не может отредактировать применённую миграцию или запустить db push
- 04Ревью человекомSQL каждой новой миграции читают и прогоняют на копии данных до слияния
Учебный проект — магазин на TypeScript и Prisma 7.10 с PostgreSQL 17 в Docker: обработчики HTTP, два модуля и слой доступа к базе.
src/
app/ HTTP handlers: call module APIs only
orders/route.ts
orders/stats/route.ts
modules/
orders/index.ts public API of the module
orders/repo.ts the only place where orders touches the database
billing/index.ts
billing/internal/ private details of billing
db/client.ts Prisma client
generated/prisma/ generated by prisma generate, not in git
prisma/
schema.prisma
migrations/npm install -D eslint@10.12.0 typescript-eslint@8.71.1 typescript@6.0.3 \
eslint-plugin-import-x@4.17.1 eslint-import-resolver-typescript@4.4.5
npm install -D prisma@7.10.0 && npm install @prisma/client@7.10.0 @prisma/adapter-pg@7.10.0 pg@8.23.1Границы в файле инструкций
В файл инструкций попадает то, что агент не выведет из кода сам: какие папки за что отвечают, в каком направлении разрешены зависимости, как менять схему базы и чего не делать никогда. Каждое правило формулируйте так, чтобы его можно было проверить, и сразу называйте команду проверки.
# AGENTS.md
## Architecture
- `src/app/` — HTTP handlers. They call module APIs only:
`import { createOrder } from "@/modules/orders/index.js"`.
- `src/modules/<name>/` — business modules. Other code imports a module
only through its `index.ts`; files in `internal/` are private.
- `src/db/` — the Prisma client. Handlers never import it; modules use it
from their `repo.ts`.
- Dependencies point down: app → modules → db. Never the other way.
- `npm run lint` checks these import rules. If a rule blocks you, stop and
describe the problem; do not disable the rule or add an exception.
## Database
- Change the schema only in `prisma/schema.prisma`, then create a migration:
`npx prisma migrate dev --name <change> --create-only`.
- If `migrate dev` refuses to run non-interactively, write the SQL into
a new folder `prisma/migrations/<timestamp>_<change>/migration.sql` with:
```bash
npx prisma migrate diff --from-migrations prisma/migrations \
--to-schema prisma/schema.prisma --script
```
- Show the generated SQL and wait for review before applying it.
- Never edit or delete a migration that is already committed.
- Never run `prisma db push` or `prisma migrate reset`.Claude Code
- Что читает
CLAUDE.mdпроекта;AGENTS.md, еслиCLAUDE.mdнет, или через импорт@AGENTS.md. Файлы из подпапок подгружаются, когда агент работает с файлами в них..claude/rules/*.mdс полемpaths— только для совпадающих путей- Что из этого следует
- Правило для миграций можно вынести в
.claude/rules/сpaths: ["prisma/**"]
Codex
- Что читает
AGENTS.mdот корня проекта вниз до рабочей папки, по одному файлу на папку; ближний к рабочей папке читается последним. Общий объём по умолчанию — до 32 КиБ- Что из этого следует
- Файл в
prisma/Codex не увидит, если запущен из корня: границы держите в корневомAGENTS.md
Проверка границ линтером: что работает на самом деле
Вот обработчик, который мог бы написать агент, чтобы «сэкономить время»: он идёт в базу напрямую и берёт константу из внутренней папки модуля оплаты.
// Written by the agent "to save time": the handler goes straight to the database
// and borrows a constant from another module's internals.
import { db } from "../../../db/client.js";
import { VAT_PERCENT } from "@/modules/billing/internal/tariffs.js";
export async function GET() {
const orders = await db.order.count();
return Response.json({ orders, vatPercent: VAT_PERCENT });
}Первый кандидат — встроенное правило ESLint no-restricted-imports. Оно сравнивает строку импорта с шаблонами в стиле .gitignore и не разрешает путь до файла. Поэтому шаблон @/db/* пропускает тот же модуль, импортированный относительным путём:
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig(tseslint.configs.recommended, {
files: ["src/app/**/*.ts"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{ group: ["@/db/*", "@/generated/*"], message: "Handlers use module APIs from @/modules/<name>, not the database." },
{ group: ["@/modules/*/internal/*"], message: "Import a module only through its index.ts." },
],
},
],
},
});$ npx eslint -c eslint.restricted-imports.config.js src
src/app/orders/stats/route.ts
4:1 error '@/modules/billing/internal/tariffs.js' import is restricted from being used by a pattern. Import a module only through its index.ts no-restricted-imports
✖ 1 problem (1 error, 0 warnings)Можно расширить шаблоны до **/db/*, но тогда правило срабатывает на всё, где в пути встречается папка db, в том числе на безобидный локальный помощник:
# group: ["@/db/*", "@/generated/*", "**/db/*", "**/generated/*"]
$ npx eslint -c eslint.restricted-imports-v2.config.js src
src/app/orders/stats/route.ts
3:1 error '../../../db/client.js' import is restricted from being used by a pattern ...
4:1 error '@/modules/billing/internal/tariffs.js' import is restricted from being used by a pattern ...
# the same config on src/app/orders/label.ts, which imports a local helper ./db/format.js
src/app/orders/label.ts
1:1 error './db/format.js' import is restricted from being used by a pattern ...Надёжнее правило no-restricted-paths из eslint-plugin-import-x. Оно сначала разрешает импорт до реального файла, а потом проверяет зоны: target — где действует запрет, from — откуда нельзя импортировать, except — исключения относительно from. Псевдоним @/ и импорты .js для файлов .ts разрешает TypeScript-резолвер.
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import { importX } from "eslint-plugin-import-x";
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
export default defineConfig(
{ ignores: ["src/generated/**"] },
tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
plugins: { "import-x": importX },
settings: {
"import-x/resolver-next": [createTypeScriptImportResolver()],
},
rules: {
"import-x/no-unresolved": "error",
"import-x/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/app",
from: ["./src/db", "./src/generated"],
message: "Handlers call module APIs (src/modules/<name>/index.ts), not the database.",
},
{
target: "./src/app",
from: "./src/modules/*/internal/**",
message: "Import a module only through its index.ts.",
},
{
target: "./src/modules/orders",
from: "./src/modules/billing",
except: ["./index.ts"],
message: "orders may use billing only through billing/index.ts.",
},
{
target: ["./src/db", "./src/modules"],
from: "./src/app",
message: "Lower layers must not depend on handlers.",
},
],
},
],
},
},
);$ npx eslint src # plus two more violations: orders → billing/internal, db → app
src/app/orders/stats/route.ts
3:20 error Unexpected path "../../../db/client.js" imported in restricted zone. Handlers call module APIs (src/modules/<name>/index.ts), not the database import-x/no-restricted-paths
4:29 error Unexpected path "@/modules/billing/internal/tariffs.js" imported in restricted zone. Import a module only through its index.ts import-x/no-restricted-paths
src/db/leak.ts
1:22 error Unexpected path "../app/orders/route.js" imported in restricted zone. Lower layers must not depend on handlers import-x/no-restricted-paths
src/modules/orders/vat.ts
1:29 error Unexpected path "../billing/internal/tariffs.js" imported in restricted zone. orders may use billing only through billing/index.ts import-x/no-restricted-paths
✖ 4 problems (4 errors, 0 warnings)$ npx eslint -c eslint.no-resolver.config.js src # same zones, resolver removed
exit code: 0
# with "import-x/no-unresolved": "error" added
$ npx eslint -c eslint.no-resolver.config.js src
src/app/orders/route.ts
1:29 error Unable to resolve path to module '@/modules/orders/index.js' import-x/no-unresolved
src/db/client.ts
3:30 error Unable to resolve path to module '../generated/prisma/client.js' import-x/no-unresolved
...Исправленный обработчик идёт через публичный API модуля, и линтер проходит без ошибок:
import { orderStats } from "@/modules/orders/index.js";
export async function GET() {
return Response.json(await orderStats());
}no-restricted-imports (ядро ESLint)
- Сравнивает строку импорта с шаблоном
- Не видит, куда ведёт относительный путь
- Подходит для запрета пакетов: lodash, @prisma/client в UI
- Ничего не нужно устанавливать
import-x/no-restricted-paths
- Разрешает импорт до файла и проверяет зоны
- Одинаково ловит @/db/client.js и ../../../db/client.js
- Подходит для границ между папками проекта
- Нужен резолвер; без него правило молча пропускает всё
Схема — только через миграции
prisma db push синхронизирует схему с базой без файлов миграций и без записи в _prisma_migrations — документация Prisma называет это инструментом для прототипирования. Агенту он нравится: одна команда, и база «в порядке». Но изменение не попадёт ни в историю, ни на другие окружения, а следующий migrate dev предложит сбросить базу. CI ловит такое состояние одной командой: prisma migrate diff сравнивает результат всех миграций со схемой и с флагом --exit-code возвращает 2, если разница есть.
$ npx prisma db push
Your database is now in sync with your Prisma schema. Done in 2.25s
$ npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --exit-code
[*] Changed the `orders` table
[+] Added column `status`
exit code: 2
$ npx prisma migrate dev --name add_order_status
Drift detected: Your database schema is not in sync with your migration history.
[*] Changed the `orders` table
[+] Added column `status`
We need to reset the "public" schema at "localhost:55019"
You may use prisma migrate reset to drop the development database.
All data will be lost.Чтобы сравнить папку миграций со схемой, Prisma 7 прогоняет миграции во временной теневой базе (shadow database), и её адрес нужно указать в конфигурации. Без него команда сразу останавливается:
$ npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --exit-code
Error: You must set `datasource.shadowDatabaseUrl` in your `prisma.config.ts` if you want to diff a migrations directory.import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: { path: "prisma/migrations" },
datasource: {
url: env("DATABASE_URL"),
// an empty scratch database: migrate diff replays the migrations here
shadowDatabaseUrl: process.env.SHADOW_DATABASE_URL,
},
});Отдельно о сбросе базы. В Prisma 7.10 есть собственная защита: команда migrate reset, запущенная из Claude Code, остановилась и потребовала явного согласия пользователя. Это полезная страховка, но рассчитывать только на неё не стоит: другие инструменты и другие команды она не покрывает.
$ npx prisma migrate reset --force
Error: Prisma Migrate detected that it was invoked by Claude Code.
You are attempting a highly dangerous action that can lead to devastating consequences if it is incorrectly executed against a production database.
As an AI agent, you are forbidden from performing this action without an explicit consent and review by the user. ...
exit code: 1Применённые миграции не правят
Документация Prisma однозначна: применённую миграцию нельзя редактировать или удалять, исправление оформляют новой миграцией. Агент об этом знает, но под задачей «поменяй значение по умолчанию» может просто исправить SQL в существующем файле:
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "orders" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'new';
+ALTER TABLE "orders" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'draft';Что увидят команды Prisma? В нашей проверке на Prisma 7.10 migrate status и migrate deploy на базе, где эта миграция уже применена, отработали без ошибок и без предупреждений, хотя контрольная сумма в _prisma_migrations уже не совпадала с файлом. Расхождение показал только migrate dev на базе разработки — и предложил её сбросить. Документация описывает предупреждение при migrate deploy; в этой версии мы его не получили, поэтому полагаться на него не стоит.
$ DATABASE_URL=<staging> npx prisma migrate status
2 migrations found in prisma/migrations
Database schema is up to date!
exit code: 0
$ DATABASE_URL=<staging> npx prisma migrate deploy
2 migrations found in prisma/migrations
No pending migrations to apply.
exit code: 0
$ docker exec kb-g-migr-pg psql -U postgres -d shop_stage -c "SELECT migration_name, checksum FROM _prisma_migrations"
20261012135246_add_order_status | 40e36b249bea23e9d0ed732a670e80030586385158f409ea236af2ec7a115b24
$ sha256sum prisma/migrations/20261012135246_add_order_status/migration.sql
f660b1bc7a4e71aff9bf229e3cd63a0ee73f24aed73a3700949962e7288344c6
$ npx prisma migrate dev
- The migration `20261012135246_add_order_status` was modified after it was applied.
- Drift detected: Your database schema is not in sync with your migration history.Надёжная проверка — по истории Git: файлы в prisma/migrations, которые уже есть в основной ветке, в pull request могут только добавляться. Скрипт ниже падает на изменённой или удалённой миграции, а для новых подсвечивает рискованные операторы, используя формат аннотаций GitHub Actions.
// CI check for prisma/migrations against the base branch:
// - an applied (already merged) migration must not be modified or deleted;
// - risky statements in new migrations are reported for a human review.
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
const base = process.argv[2] ?? "origin/main";
const diff = execFileSync(
"git",
["diff", "--name-status", "--no-renames", `${base}...HEAD`, "--", "prisma/migrations"],
{ encoding: "utf8" },
);
const changes = diff.split("\n").filter(Boolean).map((line) => line.split("\t"));
const RISKY = [
/\bDROP\s+(TABLE|COLUMN|INDEX|TYPE|CONSTRAINT)\b/i,
/\bALTER\s+COLUMN\s+"?\w+"?\s+(SET\s+DATA\s+)?TYPE\b/i,
/\bSET\s+NOT\s+NULL\b/i,
/\b(TRUNCATE|DELETE\s+FROM|UPDATE)\b/i,
];
let failed = false;
for (const [status, file] of changes) {
if (status !== "A") {
console.log(`::error file=${file}::Applied migration ${status === "D" ? "deleted" : "modified"}. Revert it and create a new migration.`);
failed = true;
continue;
}
if (!file.endsWith(".sql")) continue;
const sql = readFileSync(file, "utf8");
for (const re of RISKY) {
const hit = sql.match(re);
if (hit) console.log(`::warning file=${file}::Risky statement "${hit[0]}": check data loss, locks and rollback before merging.`);
}
}
console.log(`${changes.length} change(s) in prisma/migrations compared with ${base}`);
process.exit(failed ? 1 : 0);$ node scripts/check-migrations.mjs main # branch where the agent edited an applied migration
::error file=prisma/migrations/20261012135246_add_order_status/migration.sql::Applied migration modified. Revert it and create a new migration.
1 change(s) in prisma/migrations compared with main
exit code: 1Ещё раньше ошибку можно остановить на стороне агента. Хук PreToolUse в Claude Code получает JSON с именем инструмента и абсолютным путём файла; код выхода 2 блокирует вызов, а текст из stderr агент получает как объяснение. Хук ниже запрещает редактировать миграции, которые уже есть в Git, и не мешает дописывать только что созданную через --create-only.
// PreToolUse hook for Edit|Write: blocks changes to migrations already committed to git.
// A new migration created with --create-only is still untracked and can be edited.
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
const file = String(input.tool_input?.file_path ?? "").replaceAll("\\", "/");
if (!file.includes("/prisma/migrations/")) process.exit(0);
try {
execFileSync("git", ["ls-files", "--error-unmatch", file], { cwd: input.cwd, stdio: "ignore" });
} catch {
process.exit(0); // not in git yet: a fresh migration, editing is allowed
}
console.error(
`${file} is already committed and may be applied somewhere. ` +
"Do not edit applied migrations: change schema.prisma and run " +
"`npx prisma migrate dev --name <change> --create-only`, then ask for review.",
);
process.exit(2);{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": [
"Bash(npx prisma db push *)",
"Bash(npx prisma migrate reset *)",
"Read(./.env)",
"Read(./.env.*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-migrations.mjs"]
}
]
}
]
}
}$ node cases/hook-test.mjs
Edit .\prisma\migrations\20261012135246_add_order_status\migration.sql
exit code: 2
stderr: C:/…/prisma/migrations/20261012135246_add_order_status/migration.sql is already committed and may be applied somewhere. Do not edit applied migrations: …
Write .\prisma\migrations\20990101000000_draft\migration.sql
exit code: 0
Edit .\src\modules\orders\index.ts
exit code: 0Как ревьюить миграцию агента
Пусть агент переименовал поле name в fullName с колонкой full_name. В неинтерактивном режиме migrate dev --create-only отказался работать, и агент, следуя AGENTS.md, сгенерировал SQL через migrate diff --script. Проверка из CI подсветила рискованный оператор:
$ npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --script
-- AlterTable
ALTER TABLE "customers" DROP COLUMN "name",
ADD COLUMN "full_name" TEXT NOT NULL;
$ node scripts/check-migrations.mjs main
::warning file=prisma/migrations/20261012140000_rename_customer_name/migration.sql::Risky statement "DROP COLUMN": check data loss, locks and rollback before merging.
1 change(s) in prisma/migrations compared with main
exit code: 0Схема и миграции согласованы
prisma migrate diff --exit-codeвозвращает 0. Если 2 — в pull request есть изменение схемы без миграции.Прочитать SQL целиком
Ищите
DROP, смену типа,SET NOT NULL,UPDATEиDELETE. Переименование, которое Prisma сгенерировала как удаление и добавление, — потеря данных.Прогнать на копии данных
Пустая база не покажет проблему. Копия с данными — показывает: ниже миграция упала на копии, созданной через
CREATE DATABASE … TEMPLATE.Исправить до слияния
Пока миграция не в основной ветке, её SQL можно править. Переименование — через
RENAME COLUMN; изменение с заполнением данных — в несколько шагов, как в уроке «ORM и миграции».Решить, как откатываться
Запишите в описании pull request, какой новой миграцией и каким действием с данными изменение отменяется, если что-то пойдёт не так после релиза.
$ docker exec kb-g-migr-pg psql -U postgres -c "CREATE DATABASE shop_copy TEMPLATE shop"
$ DATABASE_URL=<copy> npx prisma migrate deploy
Applying migration `20261012140000_rename_customer_name`
Error: P3018
A migration failed to apply. New migrations cannot be applied before the error is recovered from.
Migration name: 20261012140000_rename_customer_name
Database error code: 23502
ERROR: column "full_name" of relation "customers" contains null valuesPostgreSQL выполнил миграцию в транзакции, поэтому колонка name на копии осталась на месте. Prisma пометила миграцию как неудачную, и до исправления новые миграции на эту базу не применятся. Исправляем SQL, отмечаем неудачную попытку как откатанную и применяем снова:
-- Reviewed: the field was renamed, not replaced. Keep the data.
ALTER TABLE "customers" RENAME COLUMN "name" TO "full_name";$ DATABASE_URL=<copy> npx prisma migrate resolve --rolled-back 20261012140000_rename_customer_name
Migration 20261012140000_rename_customer_name marked as rolled back.
$ DATABASE_URL=<copy> npx prisma migrate deploy
Applying migration `20261012140000_rename_customer_name`
All migrations have been successfully applied.
$ docker exec kb-g-migr-pg psql -U postgres -d shop_copy -c "SELECT id, full_name FROM customers"
id | full_name
----+---------------
1 | Test CustomerВсе проверки собраны в workflow на каждый pull request. Для git diff с основной веткой нужна полная история (fetch-depth: 0), для теневой базы — сервис PostgreSQL.
name: boundaries
on:
pull_request:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx prisma generate
env:
DATABASE_URL: postgresql://unused@localhost:5432/unused
- run: npm run lint
migrations:
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: shadow
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/app
SHADOW_DATABASE_URL: postgresql://postgres:test@localhost:5432/shadow
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- name: Schema changes come with a migration
run: npx prisma migrate diff --from-migrations prisma/migrations --to-schema prisma/schema.prisma --exit-code
- name: Applied migrations are untouched
env:
BASE_REF: ${{ github.base_ref }}
run: node scripts/check-migrations.mjs "origin/$BASE_REF"Ограничения и упражнения
- Линтер проверяет импорты, но не смысл. Если агент скопирует SQL-запрос в обработчик вместо импорта клиента, граница формально соблюдена. Такое находит только ревью.
- Зоны
no-restricted-pathsописывают папки. При переносе модулей конфигурацию нужно обновлять, иначе правило перестанет что-либо проверять — помогает та же проверка намеренной поломкой. - Хук и
denyзащищают только от действий через инструменты агента. Они не заменяют разграничение доступов: у агента и у локальной разработки не должно быть учётных данных продакшен-базы. - Проверка по Git защищает миграции, которые уже в основной ветке. Миграцию, применённую вручную на сервере в обход CI, она не заметит.
- Для крупных проектов есть специализированные инструменты границ, например
eslint-plugin-boundariesили dependency-cruiser; принцип тот же — правило плюс проверка, что оно срабатывает.
Как проверено10 · пройдено3 · частичносверено 12 октября 2026 г.
- запущено, работает
учебный проект: src/app, src/modules, src/db, prisma/schema.prisma, prisma.config.ts
Node.js 24.18, Prisma 7.10.0, PostgreSQL 17 (docker postgres:17-alpine, --rm, kb-g-migr-pg): migrate dev init, prisma generate, tsc --noEmit (TypeScript 6.0.3) без ошибок, tsx cases/smoke.ts создал заказ с НДС (11200) и посчитал заказы
- запущено, работает
eslint.restricted-imports.config.js (no-restricted-imports)
ESLint 10.12: шаблон @/db/* пропустил ../../../db/client.js (log-02); шаблон **/db/* поймал его, но дал ложное срабатывание на ./db/format.js (log-03, log-04)
- запущено, работает
eslint.config.js (import-x/no-restricted-paths + eslint-import-resolver-typescript 4.4.5 + no-unresolved)
eslint-plugin-import-x 4.17.1: 4 нарушения найдены (app→db относительным путём, app→internal через @/, orders→billing/internal, db→app), ложного срабатывания нет (log-06); без резолвера — exit 0 при тех же нарушениях (log-07); с no-unresolved без резолвера — ошибки на каждом импорте (log-09); исправленный обработчик — exit 0 (log-10)
- запущено, работает
prisma db push → migrate diff --exit-code, migrate dev drift
db push добавил колонку, migrate diff вернул код 2, migrate dev — Drift detected (log-13…15); без shadowDatabaseUrl migrate diff --from-migrations останавливается ошибкой (вывод в log-12 до добавления поля)
- запущено, работает
migrate reset из агента
Prisma 7.10 при запуске из Claude Code отказал: «Prisma Migrate detected that it was invoked by Claude Code…» (log-16); согласие не подделывалось, дрейф убран DROP COLUMN во временной базе
- запущено, работает
правка применённой миграции: migrate status / deploy / dev
на shop_stage после правки: migrate status — «Database schema is up to date!», exit 0; migrate deploy — «No pending migrations to apply.», exit 0; checksum в _prisma_migrations ≠ sha256 файла (log-19, 20, 22); migrate dev — «was modified after it was applied» (log-21)
- запущено, работает
scripts/check-migrations.mjs
временный git-репозиторий: ветка с изменённой миграцией — ::error, exit 1 (log-25); ветка с DROP COLUMN — ::warning, exit 0 (log-26); после исправления — без предупреждений (log-31)
- запущено, работает
миграция агента на копии данных, исправление RENAME COLUMN, migrate resolve --rolled-back
CREATE DATABASE shop_copy TEMPLATE shop; migrate deploy — P3018, 23502 (log-28); колонка name сохранилась (log-29); после правки SQL resolve --rolled-back + deploy успешны, данные на месте, migrate diff — No difference (log-30)
- запущено, работает
AGENTS.md
markdownlint-cli2 0.23.3: 0 issues (log-34)
- запущено, работает
упражнение «Новая граница» (зона notifications)
зона из подсказки: импорт notifications из orders — ошибка, из src/app — нет (log-36, cases/exercise-notifications)
- частично
.claude/hooks/protect-migrations.mjs
скрипт запущен с JSON в формате PreToolUse из документации (Windows-пути с обратными слешами): закоммиченная миграция — exit 2 и сообщение в stderr; новая незакоммиченная — exit 0; обычный файл — exit 0 (log-23). В живой сессии Claude Code хук не запускался
- частично
.claude/settings.json
валидирован ajv 8 по схеме json.schemastore.org/claude-code-settings.json (log-24); правила deny в сессии Claude Code не проверялись
- частично
.github/workflows/boundaries.yml
actionlint 1.7.12 (docker, --rm): 0 ошибок (log-33); шаги повторены в чистом git clone: npm ci, prisma generate, eslint, migrate diff --exit-code (0), check-migrations (0) — log-35; на GitHub-раннере не запускался
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.