Модуль 3 · Датчики и ограничения · урок 11 из 24
Архитектура через инварианты: линтеры и структурные тесты
Агент воспроизводит то, что видит в репозитории, — и удачные решения, и неудачные. Правило «компоненты не ходят в базу напрямую», записанное в AGENTS.md, он выполнит в девяти случаях из десяти. Десятый случай станет образцом для следующих задач. Этот урок о том, как перевести архитектуру из текста в проверки, которые срабатывают всегда, и как сформулировать их ошибки, чтобы агент чинил их сам.
Что говорит источник
Команда OpenAI, описавшая эксперимент в статье Harness engineering, контролирует инварианты, а не детали реализации. Пример из статьи: агент обязан разбирать и проверять форму данных на границе системы, но конкретная библиотека для этого не навязывается.
Внутри каждого домена у них жёсткая слоистая модель: Types → Config → Repo → Service → Runtime → UI. Сквозные аспекты — авторизация, коннекторы, телеметрия, feature flags — входят только через один интерфейс Providers. Всё остальное запрещено и проверяется механически: кастомными линтерами (их тоже написал Codex) и структурными тестами. Авторы замечают, что такую архитектуру обычно откладывают до сотни инженеров, а с агентами она нужна с первого дня.
Ещё два принципа оттуда же. Границы задаются централизованно, а внутри границ у агента локальная свобода. И сообщения об ошибках линтеров пишутся как инструкции по исправлению, потому что попадают прямо в контекст агента.
Наш вывод. В терминах Биргитты Бёкелер, о которых подробно в уроке о петлях обратной связи, правило в AGENTS.md — вероятностная направляющая, а структурный тест — детерминированный датчик. Первое повышает шанс правильного решения, второе гарантирует, что неправильное не пройдёт. Нужны оба: без текста агент узнаёт о правиле только после ошибки, без проверки правило со временем перестают соблюдать.
Слои для OctoberCMS-плагина
Слои OpenAI — это схема для их собственного продукта, копировать её буквально не нужно. Важна идея: у каждого слоя есть разрешённые зависимости, а всё прочее запрещено. Для типичного плагина OctoberCMS это может выглядеть так:
| Слой | Каталог | Можно зависеть от | Нельзя |
|---|---|---|---|
| Модели | models/ |
— | компонентов, контроллеров, HTTP |
| Сервисы | classes/ |
моделей | компонентов, Request, Session |
| Компоненты | components/ |
сервисов | Db::, прямых запросов к моделям |
| Контроллеры бэкенда | controllers/ |
сервисов, моделей | компонентов фронта |
| Тема | themes/*/pages, partials |
переменных компонентов | бизнес-логики, больших inline-стилей |
В мобильном приложении то же самое: data → domain → presentation, и экран не должен импортировать сетевой клиент. Для Kotlin такие правила проверяют библиотеки вроде Konsist или ArchUnit, для PHP — Deptrac или собственные правила PHPStan. Но начать можно без зависимостей — обычным тестом.
Структурный тест
Структурный тест читает исходники как данные и проверяет связи между ними. Он грубее статического анализатора, зато пишется за полчаса, и агент сам может его расширять.
<?php
declare(strict_types=1);
namespace Tests\Architecture;
use PHPUnit\Framework\TestCase;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
final class LayerBoundariesTest extends TestCase
{
private const PLUGIN = __DIR__ . '/../../src/plugins/acme/shop';
/** @var array<string, array<string, string>> слой => [запрещённый шаблон => как исправить] */
private const FORBIDDEN = [
'components' => [
'/\bDb::/' => 'Перенесите запрос в сервис в classes/ и вызовите его из компонента.',
'/^use\s+Acme\\\\Shop\\\\Models\\\\/m' => 'Компонент не читает модели напрямую: добавьте метод в сервис classes/*Service.php.',
],
'classes' => [
'/\bRequest::|\brequest\(\)/' => 'Сервис не знает об HTTP: передайте нужные значения аргументами из компонента.',
],
];
public function testLayersDoNotCrossBoundaries(): void
{
$violations = [];
foreach (self::FORBIDDEN as $layer => $rules) {
foreach ($this->phpFiles(self::PLUGIN . '/' . $layer) as $file) {
$code = (string) file_get_contents($file);
foreach ($rules as $pattern => $fix) {
if (preg_match($pattern, $code, $m, PREG_OFFSET_CAPTURE) === 1) {
$line = substr_count(substr($code, 0, $m[0][1]), "\n") + 1;
$violations[] = sprintf('%s:%d — нарушена граница слоя «%s». %s', $file, $line, $layer, $fix);
}
}
}
}
self::assertSame([], $violations, implode("\n", $violations));
}
/** @return list<string> */
private function phpFiles(string $dir): array
{
if (!is_dir($dir)) {
return [];
}
$files = [];
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)) as $f) {
if ($f->isFile() && $f->getExtension() === 'php') {
$files[] = $f->getPathname();
}
}
return $files;
}
}Обратите внимание на третий элемент каждой строки отчёта: тест сообщает не только «что сломано», но и «что сделать». Для человека это вежливость, для агента — следующий шаг.
Сообщение линтера как инструкция
Сравните два сообщения об одной ошибке:
partials/hero.htm: inline style foundpartials/hero.htm:14 — inline <style> длиннее 10 строк.
Перенесите правила в resources/scss/pages/<slug>.scss, подключите бандл
страницы через vite в layout и пересоберите: cd src/themes/amiga && npm run build.
Правило: docs/CODESTYLE_BACK_FRONT.md, раздел 3.4.Первое агент исправит как придётся — например, переложит стили в атрибут style и формально пройдёт проверку. Второе задаёт безопасный путь и ссылку на систему записи. Хорошее сообщение отвечает на четыре вопроса: где, какое правило, как исправить, где прочитать подробнее.
Простой линтер под такое правило — десяток строк:
#!/usr/bin/env bash
set -euo pipefail
status=0
while IFS= read -r file; do
lines=$(awk '/<style/{f=1} f{n++} /<\/style>/{f=0} END{print n+0}' "$file")
if [ "$lines" -gt 10 ]; then
echo "$file — inline <style> на $lines строк. Перенесите в resources/scss/pages/, см. docs/CODESTYLE_BACK_FRONT.md §3.4"
status=1
fi
done < <(find src/themes -name '*.htm')
exit $statusВкусовые инварианты
OpenAI выделяет отдельную группу — taste invariants: структурированное логирование, правила именования схем и типов, лимиты размера файлов, платформенные требования к надёжности. Это то, что ревьюер раньше писал в комментариях к каждому третьему MR. Для агентства список естественно продолжается: declare(strict_types=1) в каждом PHP-файле, BEM в SCSS, запрет console.log в собранном JS, лимит длины метода, обязательный data-testid у интерактивных элементов, на которые опираются Playwright-тесты.
Критерий отбора простой: если замечание повторяется в ревью третий раз, его место в проверке, а не в памяти ревьюера. Лучшие практики Codex говорят о том же: правила добавлять после повторяющихся ошибок, а не заранее (best practices).
Где это ломается
Ложные срабатывания. Регулярное выражение поймает Db:: в комментарии. Решение — явный список исключений с причиной и сроком, а не «временно отключим тест». Иначе агент научится, что правила можно выключать.
Легаси. На существующем проекте тест сразу упадёт на сотнях мест. Зафиксируйте текущие нарушения в baseline-файле и проверяйте только новые. Уменьшение baseline — хорошая фоновая задача, об этом урок об энтропии.
Слишком жёсткие рамки. Бёкелер называет это harnessability: строгие типы и явные границы модулей облегчают построение обвязки. Обратная сторона — каждое правило сужает пространство решений. Если агент регулярно упирается в инвариант и решает задачу обходным путём, возможно, неправ инвариант. OpenAI формулирует это так: код не обязан совпадать со вкусом людей, если он корректен, поддерживаем и читаем для будущих запусков агента.
Кто пишет правила. В эксперименте OpenAI линтеры писал Codex. Это разумно: формулировать правило — работа человека, кодировать проверку и прогонять её по истории репозитория — работа агента.
Попробуйте сами
10–15 мин на рабочем месте- Откройте последние десять MR своего проекта и выпишите замечания ревьюеров, которые повторялись. Выберите одно, которое проверяется механически.
- Попросите Codex превратить его в структурный тест или линтер: «Напиши тест в tests/Architecture, который ловит это нарушение. Сообщение об ошибке должно содержать файл, строку, правило и конкретный способ исправления. Прогони тест на текущем коде и покажи нарушения».
- Если нарушений много, попросите сделать baseline-файл и проверку только новых случаев, затем добавьте строку о тесте в AGENTS.md.
Коротко
- Проверяйте инварианты, а не детали реализации: «данные валидируются на границе», а не «используем библиотеку X».
- Слои и разрешённые зависимости — самый дешёвый способ не дать агенту размножить неудачный шаблон.
- Структурный тест на PHPUnit пишется за полчаса и не требует новых зависимостей.
- Сообщение об ошибке — это промпт: где, какое правило, как исправить, где читать.
- Повторяющееся замечание ревью переносите в проверку; для легаси — baseline и только новые нарушения.