Amiga
Оглавление курса · урок 11 из 24

Модуль 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 · tests/Architecture/LayerBoundariesTest.php
<?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;
    }
}

Обратите внимание на третий элемент каждой строки отчёта: тест сообщает не только «что сломано», но и «что сделать». Для человека это вежливость, для агента — следующий шаг.

Сообщение линтера как инструкция

Сравните два сообщения об одной ошибке:

text
partials/hero.htm: inline style found
text
partials/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 и формально пройдёт проверку. Второе задаёт безопасный путь и ссылку на систему записи. Хорошее сообщение отвечает на четыре вопроса: где, какое правило, как исправить, где прочитать подробнее.

Простой линтер под такое правило — десяток строк:

bash · scripts/lint/inline-assets.sh
#!/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 мин на рабочем месте
  1. Откройте последние десять MR своего проекта и выпишите замечания ревьюеров, которые повторялись. Выберите одно, которое проверяется механически.
  2. Попросите Codex превратить его в структурный тест или линтер: «Напиши тест в tests/Architecture, который ловит это нарушение. Сообщение об ошибке должно содержать файл, строку, правило и конкретный способ исправления. Прогони тест на текущем коде и покажи нарушения».
  3. Если нарушений много, попросите сделать baseline-файл и проверку только новых случаев, затем добавьте строку о тесте в AGENTS.md.

Коротко

  • Проверяйте инварианты, а не детали реализации: «данные валидируются на границе», а не «используем библиотеку X».
  • Слои и разрешённые зависимости — самый дешёвый способ не дать агенту размножить неудачный шаблон.
  • Структурный тест на PHPUnit пишется за полчаса и не требует новых зависимостей.
  • Сообщение об ошибке — это промпт: где, какое правило, как исправить, где читать.
  • Повторяющееся замечание ревью переносите в проверку; для легаси — baseline и только новые нарушения.
Отметка хранится только в вашем браузере
Хотите связаться с владельцем
компании напрямую?
Дмитрий Тарасов
Дмитрий Тарасов
СЕО

НАПИСАТЬ

Написать в MAX