Как делать12 октября 2026 г.8 мин чтения
Вайбкодинг: от прототипа к продуктуШаг 6 из 7

Архитектурные границы и миграции в AI-проекте: правила для агента

Коротко

Запишите границы модулей и правила работы со схемой в файл инструкций проекта, но не полагайтесь на него: для агента это контекст, а не запрет. Каждое правило дублируйте проверкой, которая падает: импорты — правилом import-x/no-restricted-paths с TypeScript-резолвером, схему — prisma migrate diff --exit-code в CI, неизменность применённых миграций — сравнением с основной веткой и хуком, который не даёт агенту их редактировать. Миграцию от агента читают как код и прогоняют на копии данных до слияния.

РуководствоУровень: СреднийВайбкодинг: от прототипа к продуктуПрактика: ~1–2 ч

После материала вы сможете

  • описать границы модулей и правила работы со схемой в 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 г.

Содержание
  1. 01Что размывает агент и почему файла инструкций мало
  2. 02Границы в файле инструкций
  3. 03Проверка границ линтером: что работает на самом деле
  4. 04Схема — только через миграции
  5. 05Применённые миграции не правят
  6. 06Как ревьюить миграцию агента
  7. 07Ограничения и упражнения

Что размывает агент и почему файла инструкций мало

Ассистент решает задачу кратчайшим путём, который видит. Нужна статистика заказов — обработчик сам открывает соединение с базой. Нужна ставка НДС — импортируется константа из внутренностей чужого модуля. Нужна новая колонка — схема подгоняется командой prisma db push, а если что-то не сошлось, правится уже применённая миграция. Каждая такая правка работает локально и проходит тесты. Через месяц модули сцеплены так, что любое изменение задевает всё, а история миграций на разных базах расходится.

Первое желание — дописать запреты в CLAUDE.md или AGENTS.md. Это нужно, но недостаточно: документация Claude Code прямо говорит, что эти файлы — контекст, а не обязательная конфигурация, а чтобы заблокировать действие независимо от решения модели, нужен хук PreToolUse. Поэтому правила строятся в три слоя.

  1. 01Файл инструкцийAGENTS.md: какие слои есть, кто кого может импортировать, как менять схему. Агент читает и старается следовать
  2. 02Линтер и CIimport-x/no-restricted-paths, prisma migrate diff --exit-code, скрипт проверки миграций. Падают у человека и у агента одинаково
  3. 03Ограничения инструментов агентаХук PreToolUse и правила deny: агент не может отредактировать применённую миграцию или запустить db push
  4. 04Ревью человекомSQL каждой новой миграции читают и прогоняют на копии данных до слияния
Каждое правило из файла инструкций должно иметь проверку, которая падает

Учебный проект — магазин на TypeScript и Prisma 7.10 с PostgreSQL 17 в Docker: обработчики HTTP, два модуля и слой доступа к базе.

bash
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/
Структура учебного проекта: зависимости направлены сверху вниз — app → modules → db
bash
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
# 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`.
Разделы об архитектуре и базе данных. Проверен markdownlint-cli2 0.23.3

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
Как инструменты читают файлы инструкций — по их документации на 12.10.2026

Проверка границ линтером: что работает на самом деле

Вот обработчик, который мог бы написать агент, чтобы «сэкономить время»: он идёт в базу напрямую и берёт константу из внутренней папки модуля оплаты.

src/app/orders/stats/route.ts
// 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 });
}
Два нарушения границ: прямой доступ к базе и импорт из internal/

Первый кандидат — встроенное правило ESLint no-restricted-imports. Оно сравнивает строку импорта с шаблонами в стиле .gitignore и не разрешает путь до файла. Поэтому шаблон @/db/* пропускает тот же модуль, импортированный относительным путём:

eslint.restricted-imports.config.js
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." },
        ],
      },
    ],
  },
});
terminal
$ 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/client.js прошёл незамеченным

Можно расширить шаблоны до **/db/*, но тогда правило срабатывает на всё, где в пути встречается папка db, в том числе на безобидный локальный помощник:

terminal
# 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-резолвер.

eslint.config.js
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.",
            },
          ],
        },
      ],
    },
  },
);
Четыре зоны: обработчики не ходят в базу и во internal/, orders видит только billing/index.ts, нижние слои не зависят от обработчиков
terminal
$ 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)
Все четыре нарушения найдены, ложного срабатывания на ./db/format.js нет
terminal
$ 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 модуля, и линтер проходит без ошибок:

src/app/orders/stats/route.ts
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, если разница есть.

terminal
$ 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.
Агент добавил поле и выполнил db push: CI видит схему без миграции, migrate dev требует сброса

Чтобы сравнить папку миграций со схемой, Prisma 7 прогоняет миграции во временной теневой базе (shadow database), и её адрес нужно указать в конфигурации. Без него команда сразу останавливается:

terminal
$ 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.
prisma.config.ts
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,
  },
});
Теневая база — отдельная база, никогда не та же, что в url: Prisma сбрасывает её в начале прогона. process.env вместо env() — чтобы команды, которым теневая база не нужна, работали без этой переменной

Отдельно о сбросе базы. В Prisma 7.10 есть собственная защита: команда migrate reset, запущенная из Claude Code, остановилась и потребовала явного согласия пользователя. Это полезная страховка, но рассчитывать только на неё не стоит: другие инструменты и другие команды она не покрывает.

terminal
$ 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 7.10 при запуске migrate reset агентом (сокращено)

Применённые миграции не правят

Документация Prisma однозначна: применённую миграцию нельзя редактировать или удалять, исправление оформляют новой миграцией. Агент об этом знает, но под задачей «поменяй значение по умолчанию» может просто исправить SQL в существующем файле:

diff
@@ -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; в этой версии мы его не получили, поэтому полагаться на него не стоит.

terminal
$ 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.
Staging молчит, контрольные суммы разные, migrate dev замечает правку

Надёжная проверка — по истории Git: файлы в prisma/migrations, которые уже есть в основной ветке, в pull request могут только добавляться. Скрипт ниже падает на изменённой или удалённой миграции, а для новых подсвечивает рискованные операторы, используя формат аннотаций GitHub Actions.

scripts/check-migrations.mjs
// 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);
terminal
$ 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.

.claude/hooks/protect-migrations.mjs
// 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);
.claude/settings.json
{
  "$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"]
          }
        ]
      }
    ]
  }
}
Хук для Edit и Write, запрет db push, migrate reset и чтения .env. Файл проверен по JSON-схеме settings Claude Code
terminal
$ 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
Хук, запущенный с тем же JSON, который присылает Claude Code в Windows

Как ревьюить миграцию агента

Пусть агент переименовал поле name в fullName с колонкой full_name. В неинтерактивном режиме migrate dev --create-only отказался работать, и агент, следуя AGENTS.md, сгенерировал SQL через migrate diff --script. Проверка из CI подсветила рискованный оператор:

terminal
$ 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
Переименование превратилось в удаление и добавление колонки
  1. Схема и миграции согласованы

    prisma migrate diff --exit-code возвращает 0. Если 2 — в pull request есть изменение схемы без миграции.

  2. Прочитать SQL целиком

    Ищите DROP, смену типа, SET NOT NULL, UPDATE и DELETE. Переименование, которое Prisma сгенерировала как удаление и добавление, — потеря данных.

  3. Прогнать на копии данных

    Пустая база не покажет проблему. Копия с данными — показывает: ниже миграция упала на копии, созданной через CREATE DATABASE … TEMPLATE.

  4. Исправить до слияния

    Пока миграция не в основной ветке, её SQL можно править. Переименование — через RENAME COLUMN; изменение с заполнением данных — в несколько шагов, как в уроке «ORM и миграции».

  5. Решить, как откатываться

    Запишите в описании pull request, какой новой миграцией и каким действием с данными изменение отменяется, если что-то пойдёт не так после релиза.

terminal
$ 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 values
Миграция агента на копии данных: колонка full_name не может быть NOT NULL без значений

PostgreSQL выполнил миграцию в транзакции, поэтому колонка name на копии осталась на месте. Prisma пометила миграцию как неудачную, и до исправления новые миграции на эту базу не применятся. Исправляем SQL, отмечаем неудачную попытку как откатанную и применяем снова:

prisma/migrations/20261012140000_rename_customer_name/migration.sql
-- Reviewed: the field was renamed, not replaced. Keep the data.
ALTER TABLE "customers" RENAME COLUMN "name" TO "full_name";
terminal
$ 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
Данные на месте, migrate diff снова возвращает 0, проверка миграций проходит без предупреждений

Все проверки собраны в workflow на каждый pull request. Для git diff с основной веткой нужна полная история (fetch-depth: 0), для теневой базы — сервис PostgreSQL.

.github/workflows/boundaries.yml
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"
Проверен actionlint 1.7.12; шаги повторены локально в чистом клоне, на GitHub-раннере не запускался

Ограничения и упражнения

  • Линтер проверяет импорты, но не смысл. Если агент скопирует 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.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.