← Назад до блогу

9 помилок першого production cycle Engineering Radar

Дев’ять production incidents проходять три review stages і стають п’ятьма architecture contracts

English output changed tags or recommended actions.

Це повідомлення зупинило генерацію англомовного Digest. Модель повернула текст правильної форми, але змінила поля, які мали залишитися спільними для UK та EN. Система помітила розбіжність і відхилила результат.

Найпростіший fix міг би звучати так: попросити модель ще раз і сподіватися, що наступна відповідь буде точнішою. Ми зробили інакше. Tags і Recommended Actions перестали бути вільним model output. English artifact почав примусово успадковувати їх із погодженого Ukrainian Digest.

Ця зміна добре описує весь перший production cycle Engineering Radar. Проблема була не лише в тому, що AI іноді помиляється. Production показав місця, де система ще покладалася на неявне очікування замість контракту. Кожний такий випадок ми намагалися перетворити на invariant, validation, test або окремий workflow.

У цій статті «ми» — це я та AI coding agent. Я визначав business rules, editorial authority і acceptance gates. Агент допомагав аналізувати failures, змінювати implementation і перевіряти fixes. Ми працювали за MAE-підходом: не маскували проблеми повторним prompt або ручним обходом, а повертали спостереження у discovery, contract, implementation і verification.

MAE не зробив перший production cycle безпомилковим. Його цінність була в іншому: помилка не залишалася випадковою історією з робочого чату. Вона ставала наступною перевірюваною версією системної поведінки.

Дев’ять incidents у трьох актах

Для кожного випадку ми використовували однаковий розбір:

text
симптом
  → причина
  → виправлення
  → новий контракт
  → залишковий ризик

Це важливіше за каталог повідомлень про помилки. Fix без нового contract легко зникає під час наступного refactoring. Contract без residual risk створює хибне відчуття, що один тест назавжди закрив цілий клас проблем.

Водночас усі дев’ять випадків не можна назвати «помилками AI». Лише частина почалася з model output. У каталозі є щонайменше чотири різні класи:

  • model behavior — відповідь валідна за формою, але змінює identity або має слабку editorial quality;
  • implementation gap — код читає stale state або перевіряє не той downstream limit;
  • platform/runtime behavior — Obsidian process не підхопив нову plugin instance;
  • delivery coordination — Git branch побудований від state, який уже змінився після іншого merge.

Це розділення впливає на fix. Новий prompt не виправить застарілий runtime process. Додаткова Zod schema не розв’яже Git conflict. Human review не повинен компенсувати partial filesystem write. Якщо неправильно класифікувати failure, система отримує ще один workaround замість потрібної межі відповідальності.

Тому в кожному mini-case ми запитували не «хто винен?», а «який компонент мав володіти цим рішенням і який доказ його поведінки можна повторити?». Саме так operational observation стає архітектурним input.

Акт 1. Obsidian і runtime state

1. Команда існувала в коді, але не з’явилася в Obsidian

Після встановлення чергового plugin increment очікувана команда була відсутня в Command Palette. Source files і зібраний main.js уже містили її, але активний Obsidian process продовжував працювати зі старою plugin instance.

Симптом виглядав як implementation failure: «команду не створено». Причина була operational — hot reload не гарантував, що запущений process справді підхопив новий manifest і runtime bundle.

Ми перезавантажили plugin та повторили live smoke через сам Command Palette. Відтоді наявність command registration у source або tests не вважається доказом того, що користувач уже може виконати команду.

Новий контракт розділяє три стани:

text
source updated
  ≠ plugin installed
  ≠ active Obsidian process reloaded

Після інсталяції increment потрібний runtime smoke: команда видима, запускається для правильного active file і повертає очікуваний notice. Це platform-specific operational gate, а не універсальний defect Obsidian. Залишковий ризик теж залишається platform-specific: desktop process може застаріти після зовнішньої заміни bundle, тому інструкція користувача має явно містити reload step.

2. Approval записався, а наступна команда прочитала старий state

Під час Revision flow UK Digest атомарно перейшов із draft-uk у approved state. Одразу після цього EN generation іноді бачила старе frontmatter і відповідала: відкрийте погоджений або експортований Ukrainian Digest.

Файл на диску вже був правильним. Застарілим був state, отриманий із активного Obsidian object після попередньої операції. Послідовність команд виявила невидиму різницю між «плагін щойно записав файл» і «поточний in-memory representation уже оновився».

Fix у Weekly Digest 0.3.1 перестав покладатися на cached active file для цього переходу. EN generation читає поточний Digest безпосередньо через Vault adapter і повторно перевіряє frontmatter.

ts
const current = await vault.read(activeDigestPath);
const digest = parseAndValidate(current);
 
assertState(digest, ["approved-uk", "exported-uk"]);

Новий контракт: state-changing command не гарантує синхронне оновлення всіх UI representations; наступна критична операція має прочитати durable state з Vault. Це не означає, що UI cache завжди неправильний. Воно означає, що approval, export і provenance не можна прив’язувати до припущення про його freshness.

Residual risk — concurrent manual edit між read і write. Його закривають approval hashes і guarded atomic replacement, але не сам direct read.

Акт 2. AI output та editorial contracts

3. Модель змінила tags і Recommended Actions

Opening incident виник під час English generation. Strict schema підтвердила, що tags і actions мають правильні типи, але не могла довести, що вони ідентичні погодженому UK state.

Початкова implementation просила модель повернути ці поля й після цього порівнювала результат. Це перетворювало deterministic identity на probabilistic task. Навіть якісна мовна адаптація могла змінити порядок tag, нормалізувати написання або запропонувати action, який здавався моделі доречнішим.

У Weekly Digest 0.2.1 responsibility змінилася. Модель відповідає за локалізований текст. Identity fields примусово беруться з UK source:

ts
const english = {
  ...validatedModelText,
  tags: approvedUk.tags,
  recommendedActions: approvedUk.recommendedActions
};

Signal membership, order, URLs і action кожного Signal все одно проходять strict checks. Але модель більше не має права «покращувати» спільну identity.

Новий контракт простий: якщо значення можна успадкувати deterministic, його не потрібно делегувати LLM. Залишковий ризик переходить у source artifact: якщо UK reviewer погодив неправильний action, EN точно успадкує цю помилку. Тому human-in-the-loop залишається upstream authority, а inheritance захищає parity, не істину.

4. Excerpt перевищив 300 символів

Інша відповідь моделі була змістовною, але не пройшла Digest schema: excerpt перевищив 300 символів. Повторний prompt міг би дати коротшу версію, але зробив би pipeline менш передбачуваним і витратив би ще один request на механічне обмеження.

У Weekly Digest 0.3.2 з’явилася deterministic normalization перед schema validation. Надлишковий excerpt скорочується на безпечній межі, а не відправляється на повторну генерацію.

ts
function normalizeExcerpt(value: string, limit = 300) {
  if ([...value].length <= limit) return value;
  return shortenAtWordBoundary(value, limit);
}

Важлива межа: normalization дозволена для presentation field. Вона не застосовується до Signal membership, URLs, Recommended Actions або factual content. Скоротити опис — прийнятна deterministic operation; мовчки відкинути факт чи Signal — ні.

Новий контракт розділив mechanically repairable output і semantic output. Residual risk: автоматично скорочений excerpt може стати менш вдалим або втратити важливий nuance. Тому він усе одно проходить editorial review.

5. Один випадковий символ був schema-valid excerpt

Наступний incident показав протилежну проблему. Excerpt не був задовгим. Він складався з випадкового символу й формально задовольняв мінімальну string schema.

Це не defect типізації. Схема коректно відповіла на своє питання: поле існує, має тип string і вкладається у limit. Але вона не могла відповісти, чи цей текст придатний для читача, SEO і social preview.

Ми не намагалися перетворити Zod schema на універсального редактора. Можна додати minimum length, заборонити деякі patterns або вимагати sentence-like structure, але кожна така евристика має false positives і все одно не доводить змістовність.

Новий контракт: schema validation контролює shape і bounded constraints; editorial approval контролює meaning і quality. Людина перевіряє title, excerpt, introduction, Signal cards, actions і links до export.

Residual risk — reviewer fatigue. Human gate не магічний: якщо інтерфейс приховує поле або review перетворюється на механічне натискання кнопки, випадковий символ пройде далі. Тому Digest має залишатися читабельним Markdown artifact, а не opaque approval dialog.

6. Portable title limit не збігся з production SEO gate

MDX exporter мав portable validation, але site loader застосовував жорсткіший production constraint. Title, який проходив локальний contract, блокував native site validation через SEO limit у 60 символів.

Це був класичний integration gap: обидві системи мали validation, але різні правила для того самого поля. Додаткова перевірка не допомагає, якщо вона доводить інший contract.

Obsidian повідомляє про failure native site validation та rollback MDX export

У MDX Export 0.3.1 portable limit синхронізували з production: frontmatter title має не більше 60 символів. Повний editorial heading при цьому не потрібно штучно обрізати — він може залишитися у body як H1 або бути адаптований під site rendering contract.

text
editorial heading: повний змістовний title
frontmatter title: SEO-safe, ≤60 characters

Новий контракт: downstream native validation є частиною export gate, а portable schema має відповідати його публічним constraints. Residual risk — production contract може змінитися. Тому loader запускається під час кожного export, а не лише копіюється в документацію один раз.

Акт 3. Bilingual transaction, Revision і Git delivery

7. Два валідні MDX ще не утворюють валідну bilingual pair

Перший English export мав оновити uk.mdx до locales: [uk, en] і створити сусідній en.mdx. Якщо перший write завершився, а другий упав на validation, repository міг залишитися у стані, якого ніколи не погоджував reviewer.

Окремий atomic write захищає один файл, але не multi-file invariant. Тому exporter отримав transaction boundary: temporary files, backups, native validation, commit усієї операції або rollback у reverse order.

ts
try {
  const writes = await prepareAll([ukTarget, enTarget]);
  await validateNativePair();
  await commitAll(writes);
} catch (error) {
  await rollbackCompletedInReverseOrder();
  throw error;
}

Новий контракт: bilingual identity перевіряється як одна операція. Не можна залишити EN із новим content і UK зі старими locales або навпаки.

Residual risk — filesystem transaction не є database transaction і не публікує сайт. Process crash, permissions або external file watcher залишаються failure modes, тому backups зберігаються до validation, а Git, deployment і production smoke існують як окремі gates.

8. Опублікований Digest не можна було просто доповнити

Після першої W33 публікації ми знайшли ще п’ять Signals, які належали до того самого періоду. Початковий Digest уже був approved, exported і опублікований. Просте редагування файла знищило б зв’язок із попереднім approval hash і приховало б, який склад Signals бачив reviewer.

Happy path завершував Weekly Cycle й видаляв Temporary Content. Він не мав легального переходу назад. Це не була помилка однієї кнопки — у domain model бракувало окремого lifecycle для контрольованого доповнення історичного artifact.

Так з’явився Revision flow:

text
W33 approved Digest
  └─ R2
      ├─ base: 1 Signal + base approval hash
      ├─ added: 5 Signals
      ├─ revised UK + new approval
      ├─ revised EN + new approval
      └─ atomic replacement + controlled closure

Закритий Cycle не відкривався повторно, а видалений context не відновлювався. R2 зберіг ancestry, окремі current hashes і cleanup evidence. Фінальний Digest містив шість Signals.

Новий контракт: історію не переписуємо; зміна published period отримує Revision identity. Новий тематично пов’язаний Signal зазвичай іде в наступний weekly cycle та може створити confirmed Before → After, а не автоматично відкриває стару публікацію.

Residual risk — revision lifecycle додає complexity і може стати звичним способом компенсувати слабкий selection review. Перший R2 доводить працездатність flow, але не встановлює нормальну monthly revision rate.

9. R2 branch конфліктував із уже merged English Digest

Revision була готова локально, але main уже змінився після merge English Digest у PR #26. PR #27 для R2 торкався тих самих uk.mdx і en.mdx, тому remote branch більше не можна було просунути як просте продовження старої base.

Це не defect Radar domain model. Atomic export успішно сформував правильну пару на диску. Failure виник у delivery coordination: локально валідний artifact був побудований від застарілого Git state.

Branch rebased на актуальний main, conflicts були розв’язані як нова цілісна bilingual pair, після чого gates запустили повторно. Remote branch оновили через --force-with-lease, а не unrestricted force push.

text
fetch current main
  → rebase publication branch
  → resolve UK/EN as one pair
  → rerun gates
  → push --force-with-lease

Новий контракт: export evidence не переноситься автоматично через rebase. Після зміни base потрібна повторна validation. --force-with-lease захищає від перезапису невідомого remote update, але не доводить правильність conflict resolution.

Residual risk — дві незалежні delivery branches усе ще можуть редагувати ту саму publication pair. Довгостроково це можна зменшити serial publication ownership або automation, але для v0.1 контрольована Git discipline була свідомо достатньою.

Evidence snapshot, а не vanity metric

Після закриття v0.1 evidence snapshot містив:

  • Інтеграційні incidents першого cycle: 9.
  • Signals у W33 R2: 6 — 1 base + 5 added.
  • Plugin tests: 98 у 6 окремих suites.
  • Site tests: 445 у 80 files.
  • Production build: 169 pages.

98 plugin tests і 445 site tests не складаються в одну «кількість тестів системи». Вони перевіряють різні codebases і були зафіксовані в конкретному acceptance snapshot. Ранні spike documents містять менші counts для попередніх plugin versions — це історія розвитку, а не суперечність, яку потрібно приховати.

Так само ми не заявляємо, що Digest стабільно готується за 30 хвилин. W33 не мав active-work instrumentation, pause events і точних preparation boundaries. KPI залишається not measured, навіть якщо окремі elapsed intervals виглядають коротшими.

Що ці incidents змінили насправді

Дев’ять fixes можна стиснути до п’яти архітектурних правил.

  1. Durable state важливіший за UI state. Critical command перечитує Vault, а не довіряє freshness активного object.
  2. Deterministic identity не делегується AI. Модель локалізує текст; UK approval володіє membership, tags і actions.
  3. Schema контролює форму, reviewer — значення. Обидва gates потрібні й відповідають на різні питання.
  4. Пов’язаний набір файлів потребує transaction boundary. Atomic write одного файла недостатній для UK/EN pair.
  5. Publication — це ланцюг доказів. Export, Git, merge, deployment і production smoke не є синонімами.

Ці правила не були повністю відсутні на початку. Частина існувала як intention у BRD або spike. Production змусив перетворити intention на executable behavior і негативні тести.

Саме це я вважаю практичною цінністю MAE. Методологія не обіцяє вгадати всі integration failures до першого запуску. Вона дає спосіб не витрачати failure двічі: спостереження повертається в artifact, contract, implementation, test і user instruction.

Що залишається свідомим post-MVP debt

Не кожний residual risk потрібно закривати ще одним plugin increment у v0.1.

  • Five official Sources залишаються завершеним scope; розширення до 20 потребує окремих adapter і legal contracts.
  • Active-time telemetry потрібна до того, як оцінювати KPI підготовки Digest.
  • Monthly bilingual ratio, publication cadence, missed Critical Signals і revision rate потребують повного періоду спостереження.
  • CLI control залишається третьою фазою; v0.1 орієнтована на Obsidian UI.
  • Full plugin source не публікується до завершення місячної експлуатаційної перевірки.

Це не список прихованих незавершених defects. Це розділення між функціонально завершеним v0.1 scope, monitored outcomes і наступними product phases.

Місяць експлуатації перед відкриттям коду

Наступний доказ має з’явитися не з нового prompt, а з повторюваної роботи системи. Протягом місяця потрібно перевірити:

  • чи п’ять Sources стабільно проходять scheduled collection і recovery;
  • чи Critical candidates та breaking changes не губляться в selection;
  • скільки active editorial time займає UK/EN cycle;
  • чи Recommended Actions залишаються актуальними після related Signals;
  • скільки Digest і revisions реально публікується;
  • чи backup та isolated restore продовжують проходити на зміненому Vault.

Після цього можна порівняти architecture contracts із operational behavior, опублікувати feedback і вирішити, які plugin sources безпечно відкрити.

А в наступній частині ми зробимо інший крок: зберемо відтворюваний tutorial для власного Obsidian — від контрольованого RSS fixture до bilingual MDX. Без повного приватного implementation, але з достатньою структурою, командами, state transitions і troubleshooting, щоб повторити сам підхід.


← Назад до блогу