Перейти до вмісту
Налаштування
Тема
Режим
Мова

Історія збірки, частина 2: специфікації, стандарти й цикл рев’ю

7 хв читання
  • build-story
  • process
  • ai-agents

Частина 1 була про ставки: що будувати і що кожен вибір витіснив. Ця частина — про метод: як код насправді пишеться, проходить рев’ю і потрапляє в основну гілку. Тут це важить більше, ніж зазвичай, бо чималу частку цього коду пишуть AI-агенти, і чесна версія такої домовленості — не «AI пише, і воно працює». Це набір стандартів, достатньо жорсткий, щоб я міг довіряти зміні, якої не набирав сам, і цикл рев’ю, збудований на припущенні, що довіряти не можна.

Код, що пояснює себе сам

Стандарт коментарів — найкоротша специфікація в репозиторії: значення несуть імена, а коментар існує лише для «чому», якого код висловити не може. Заборонений антипатерн — док-блок, що переказує сигнатуру: якщо типи вже це кажуть, блок — шум, який одного дня розійдеться з кодом поруч. Коментарів за замовчуванням нуль, а рев’ю переживає лише неочевидне обґрунтування — речення, що зупиняє майбутню регресію.

Коли модуль справді потребує пояснення, його пояснюють один раз, на початку файлу. Модель прози, що рендерить цей самий допис, відкривається абзацом про своє єдине несуче рішення: інлайнові позначки — це структура, а не markdown, залишений усередині рядків, бо інакше майбутній редактор проганяв би кожне збереження крізь парсер, і баг парсера тихо переписував би написане автором. Валідатор діаграм несе два рядки про те, чому він читає лише ключовий бік кожного рядка джерела: все після двокрапки — текст підпису, де слово «icon» — проза, а не властивість. Жоден із цих коментарів не переказує, що робить код; обидва фіксують, чому не інакше.

Коміти — це задачі, а гейт — не декорація

Коміти лягають на рівні задачі: один коміт на завершену фічу, фікс чи набір документів, ніколи — один на правку окремого файлу. Історія має читатися як список зроблених речей, а не як стрічка точок збереження. Формат — conventional commits під контролем commitlint, і коміти авторства AI позначені:

<type>(<scope>): 🤖 <subject>        # AI-authored
<type>(<scope>): <subject>           # human-authored

Це емодзі — вся політика атрибуції: жодних трейлерів, жодних футерів «generated with», лише чесна позначка в темі. А далі кожен коміт проходить справжній конвеєр: husky запускає lint-staged на застейджених файлах, commitlint відхиляє погане повідомлення, і перед тим, як батч ляже, по всьому воркспейсу проходить pnpm turbo lint typecheck test build. Хук упав — виправ і повтори. Обійти через --no-verify не варіант: гейт, який можна проскочити під тиском, — це порада, а не гейт.

Конвеєр — ще й місце, де живе чесність про статус. Зелене — те, що виконується на кожному коміті вже сьогодні; сіре — спроєктоване, записане й ще не живе. Деплойна половина цієї картини досі план, і діаграма каже це прямо, замість вдавати.

The pipeline — what runs today, what is still planned
джерело
On every commit todayGitHub Actions · plannedGHCR · plannedThe server · plannedhusky · lint-staged · commitlintpnpm turbo lint · typecheck · test · buildtoken contrast check · WCAG AAcheck-propagations · spec consequencesthe same four tasks, redefined nowheredocker build · four imagesCaddy · TLScompose · web · api · dashboard · showcasePostgres push to masterpushcompose pullssh restart
# title: The pipeline — what runs today, what is still planned
direction: down

local: On every commit today {
  hooks: husky · lint-staged · commitlint
  gate: pnpm turbo lint · typecheck · test · build
  contrast: token contrast check · WCAG AA
  props: check-propagations · spec consequences
  hooks -> gate
  gate -> contrast
  gate -> props
}

ci: GitHub Actions · planned {
  tasks: the same four tasks, redefined nowhere
  image: docker build · four images
  tasks -> image
}

registry: GHCR · planned
server: The server · planned {
  caddy: Caddy · TLS
  stack: compose · web · api · dashboard · showcase
  db: Postgres
  caddy -> stack
  stack -> db
}

local -> ci: push to master
ci.image -> registry: push
registry -> server: compose pull
ci -> server: ssh restart

local.hooks.class: success
local.gate.class: success
local.contrast.class: success
local.props.class: success
ci.tasks.class: muted
ci.image.class: muted
registry.class: muted
server.caddy.class: muted
server.stack.class: muted
server.db.class: muted

Spec-driven development, з реєстром

Spec-driven development у цьому репозиторії означає, що на кожне питання відповідає рівно один файл:

  • як фіча працює, живе в її специфікації — записане до того і під час того, як фіча будується;
  • що йде далі, живе в черзі задач;
  • що відкрито просто зараз, живе в PROGRESS.md — чекбоксами, які закриваються тим самим комітом, що завершує роботу.

Правило, що заслужило своє місце найдорожчим способом: наслідок, який створює зміна специфікації, фіксується чекбоксом з ідентифікатором і ніколи — прозою. Коли в липні з бекенду прибрали домен Move, специфікація бекенду вже кілька днів як була заморожена, і наслідки — таблиці на заміну, екрани дашборда на оновлення — записали прозовими нотатками. Прозу проігнорували. Тричі, на одному цьому півоті, поки правила ще не існувало. Тепер специфікація файлить кожен наслідок під заголовком Propagations як PROP-ідентифікатор, дзеркалить його в PROGRESS.md, а скрипт валить коміт, щойно наслідок існує лише словами.

Список у специфікації — це кеш без інвалідації.

Це друге правило специфікацій: спека може визначати правило, критерій чи форму, але ніколи не перелічує значення контенту. Вигаданий список слагів місяць пролежав у веб-специфікації, зогнив — і його читали як істину. Контент живе в джерелі контенту; специфікація лише вказує на нього.

Цикл рев’ю

Ніщо з написаного агентами не мерджиться непрочитаним. Я читаю дифи, щоразу — не тому, що код зазвичай хибний, а тому, що режим відмови агентського коду — правдоподібність: він виглядає як те, що написала б уважна людина, і саме тому заслуговує на уважніше читання, а не легше. Перед мерджем проходить окремий агент-рев’юєр — другий прохід з іншими стимулами: він файлить знахідки, а не виправляє їх. Знахідки лягають чекбоксами в журнали, яким належать, а не чатом, що прокручується геть.

Цикл окупається конкретикою. 22 липня рев’ю перед комітом упіймало два латентні ін’єкційні дефекти у щойно написаному коді: блок JSON-LD, серіалізований голим JSON.stringify, крізь який закривальна послідовність script могла вирватися зі свого тега, і RSS-стрічку, що віддавала сирі заголовки — за один амперсанд від невалідного XML. Обидва лежали на шляхах, якими згодом мав піти контент із бази даних. Дешево виправити до коміту — інцидент після того, як цими шляхами почне годувати CMS.

Одна дизайн-система, три фреймворки

Дизайн-система проходить крізь ту саму дисципліну — і живе одночасно в React, Vue та Astro. @eakz/tokens — єдине джерело кольору: вісім палітр, кожна в темному й світлому режимах, і власні тести пакета токенів ганяють по всіх автоматичну перевірку контрасту за WCAG AA. Палітра, що не пройшла, не може вийти — не «не варто», а не може, бо провал — це червоний тест. Першою жертвою став запланований дефолт: світлий режим obsidian не витримав перевірки, палітру відкинули, і 17 липня палітрою-ідентичністю стала dracula.

Кожен примітив існує двічі — React і Vue — і звіряється з одним контрактом поведінки аж до клавіатурної взаємодії та ARIA-ролей. Коли фікс лягає з одного боку, його переносять на інший у тій самій задачі, бо відкладена розбіжність — це розбіжність, що поїхала в продакшн.

І є планка, яку не автоматизує жоден тест: першу версію лендингу відхилили з порога — вона читалася як AI-слоп: компетентно, генерично, взаємозамінно з тисячею інших сайтів. Робота повернулася до дощок. Дизайн проходить той самий цикл рев’ю, що й код, і «воно рендериться» ніколи не було критерієм приймання.

Чого мене насправді навчив SDD

Вільний промптинг видає правдоподібний код швидко — і дрейфує. Кожна сесія починається з нуля: агент відкриває якісь файли, наново виводить архітектуру з того, що випадково побачив, і десята сесія тихо не погоджується з другою. Для демо це нормально. Для платформи, де чотири застосунки мусять і далі погоджуватися між собою щодо токенів, контрактів і схем, це відмовляє повільно, а потім раптово.

Специфікація плюс реєстр чекбоксів коштують більше наперед — помітно більше. Записувати, як фіча працює, до того як її будувати, здається повільним, а вести PROGRESS.md — бюрократією для команди з однієї людини. Але це єдине, що дозволило платформі пережити мої власні скидання контексту. Вузьке місце — я: я зупиняюся, сплю, перемикаю задачі, і кожна нова сесія — моя чи агентська — починає з читання стану, а не з відновлення його з історії чату. Специфікація — це пам’ять; реєстр — те, що тримає її актуальною.

Чесна ціна: специфікації гниють. Спека — не істина, а ставка з рахунком за обслуговування, і ніщо не інвалідовує її автоматично — саме тому наслідки стали чекбоксами під примусом, а списки значень контенту в специфікаціях заборонені взагалі. SDD не прибрав проблему дисципліни; він переніс її туди, де її бачить скрипт.

Частина 3 — про те, заради чого цей метод: ядро вже завершений продукт, а все, що приходить після нього, приходить модулем — під’єднується до дашборда, працює за власним годинником і вимикається без релізу.

917dfce