1. Де закінчується реалізація і починається налагодження
На практиці налагодження рідко починається урочисто. Зазвичай ви щойно чесно виконали крок approved plan, запустили targeted check і отримали червоний вивід, stack trace та дуже людське бажання написати Claude «ну полагодь це якось». У цей момент найважливіше — не прискорюватися, а пригальмувати.
Момент перемикання задають ваші stop rules. Один упалий targeted check — ще не драма. Але якщо Claude зробив одну-дві правки, помилка не зникла, а diff почав рости — implementation loop завершився. У ньому ви змінюєте код; у debugging спочатку пояснюєте собі, чому код ламається.
Уявіть ситуацію з Commerce OS. Ви шлете checkout-запит із порожнім кошиком і замість валідаційної відповіді отримуєте серверну помилку:
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{ "items": [] }'
# HTTP/1.1 500 Internal Server Error
Якщо в цей момент одразу попросити «додай перевірку порожнього кошика» — ви, можливо, отримаєте менш шумну помилку, але зовсім не факт, що зрозумієте причину. Баг, який полагодили без розуміння, любить повертатися пізніше в іншому місці, інколи навіть із друзями.
Корисно тримати в голові просте правило: щойно перевірка падає, наступне завдання — не фікс, а діагностичний запис: що відтворюється, за якого входу, який фактичний результат, який очікувався, що змінювалося в останніх diff.
2. Симптом, гіпотеза і коренева причина — три різні речі
Новачки в налагодженні часто змішують три різні речі в одну: що видно ззовні, що ви припускаєте всередині і що доведено. Звідси ілюзія, ніби «помилка на 41-й рядку» і є root cause. Але рядок падіння — це місце, де система скрикнула, а не де народилася проблема.
Зручно розрізняти три рівні ось так:
| Рівень | Головне питання | Приклад у Commerce OS |
|---|---|---|
| Симптом | Що ми спостерігаємо? | POST /api/orders повертає 500 для { "items": [] } |
| Гіпотеза | Що могло це викликати? | валідація порожнього кошика не спрацювала; OrderController пропускає порожній список далі; сервіс очікує хоча б один товар |
| Коренева причина | Що підтверджено доказами? | OrderController пропускає порожній список товарів у CheckoutService, і далі checkout-потік ламається на розрахунку, який передбачає непорожній кошик |
Ця різниця може здаватися занудною, але вона економить години: гіпотеза — хай переконлива — усе ще не факт, а коренева причина підтверджена кодом, логом, reproduction step або точковою перевіркою.
Візьмімо маленький фрагмент із checkout-сервісу:
import java.math.BigDecimal;
import java.util.List;
public OrderResult checkout(List<OrderItem> items) {
BigDecimal total = pricingService.calculate(items.get(0), items);
paymentService.reserve(total);
return OrderResult.created(total);
}
Якщо items порожній, падіння станеться на items.get(0). Але root cause — не IndexOutOfBoundsException на цьому рядку: це симптом усередині коду. Причина глибша й корисніша — checkout-потік допускає розрахунок ціни без ранньої перевірки, що в кошику взагалі є товари.
Це дуже схоже на медицину, і аналогія тут доречна. Температура — симптом, підозра на інфекцію — гіпотеза, підтверджений аналіз — коренева причина. Таблетка «від температури» не завжди лікує те, через що температура піднялася. Із багами та сама історія — тільки без білого халата й із більшою кількістю логів.
3. Зберіть докази до першого запиту до Claude
Найчастіша помилка в налагодженні з AI звучить так: «Ось у мене щось не працює, розберися». Без добрих доказів Claude будує здогадки з повітря. А повітря в розробці — дуже дорогий ресурс, особливо коли Claude вже редагує файли.
Тому до першого діагностичного запиту зберіть компактний пакет доказів: reproduction steps, фактичний результат, очікуваний результат, релевантний фрагмент stack trace, останній diff, список підозрілих файлів і одну-дві команди, що стабільно відтворюють проблему.
Подивіться на приклад розумного фрагмента логу:
ERROR c.a.orders.OrderController - Failed to create order
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at com.acme.orders.CheckoutService.checkout(CheckoutService.java:41)
at com.acme.orders.OrderController.checkout(OrderController.java:27)
Для початківця stack trace іноді виглядає як давній сувій проклять, але читати його можна дуже практично. Спочатку шукайте перший рядок зі своїм кодом, а не з нутрощами фреймворку — тут CheckoutService.java:41. Потім дивіться, хто його викликав — OrderController.java:27. Видно шлях помилки: запит потрапив у контролер, той пропустив його далі, сервіс не впорався.
Дуже корисно одразу зафіксувати ці докази в короткому EVIDENCE_LOG.md:
## Симптом
`POST /api/orders` з `{"items":[]}` повертає HTTP 500.
## Фактичний результат
`IndexOutOfBoundsException` у `CheckoutService.java:41`.
## Очікуваний результат
Клієнт має отримати валідаційну помилку, а не серверний 500.
## Останній diff
Зміни були в `OrderController` навколо checkout-обробки.
Зверніть увагу: про фікс тут ані слова. І це добре — поки ви збираєте докази, ви як інженер працюєте найкраще. Почнете лікувати раніше діагнозу — зросте шанс зафарбувати лампочку замість того, щоб усунути причину пожежі.
4. Просіть діагноз, а не «магічний фікс»
Дуже багато в налагодженні з AI вирішує перша формулювання. Напишете «полагодь checkout» — Claude майже напевно сприйме це як дозвіл редагувати код. Якщо вам потрібна діагностика, а не правка, це потрібно сказати явно і достатньо жорстко.
Гарний debugging-запит майже завжди містить чотири речі: заборону на правки, список вхідних доказів, прохання повернути гіпотези за ймовірністю і вимогу запропонувати мінімальну підтверджуючу перевірку. Немає пункту — Claude фантазує або занадто швидко пише код.
Ось робочий формат запиту:
Розберіть падіння checkout.
Код поки не змінюйте.
На основі stack trace, reproduction step і останнього diff поверніть:
1) гіпотези за ймовірністю;
2) докази для кожної гіпотези;
3) мінімальну перевірку для гіпотези №1;
4) що поки лишається невідомим.
У цій формулі важливе все. «Код поки не змінюйте» знімає спокусу затягнути вас у patch loop; «що лишається невідомим» дисциплінує і вас. Гарний debugging — чесна робота з невизначеністю, а не ілюзія повної впевненості.
Нижче — схема здорового циклу діагностики:
flowchart TD
A[Перевірка впала] --> B[Зібрали пакет доказів]
B --> C[Запросили Claude без edits]
C --> D[Отримали гіпотези й докази]
D --> E[Вибрали гіпотезу №1]
E --> F[Зробили мінімальну підтверджуючу перевірку]
F -->|Так, гіпотезу підтверджено| G[Записали кореневу причину]
F -->|Ні, не підтверджено| D
Зверніть увагу: кроку «одразу змінити три файли й подивитися, чи стало краще» тут немає. І це не випадковість — діагностична сесія нудніша за сесію реалізації, і нудьга тут ваш друг: не дає злетіти в красиву, але недоведу історію.
5. Мінімальна підтверджуюча перевірка
Коли Claude видав вам список гіпотез, наступне завдання — не запускати весь проєкт, усі тести й місячний календар разом. Потрібна smallest confirming check — мінімальна перевірка, яка підтверджує головну гіпотезу або відправляє до другої.
Припустімо, головна гіпотеза така: OrderController пропускає checkout навіть із порожнім списком. Мінімальна перевірка — не прогін усіх orders-тестів, а одна точкова репродукція плюс коротке звіряння з кодовим шляхом:
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{ "items": [] }'
# HTTP/1.1 500 Internal Server Error
А тепер дивимося, куди цей запит іде до фіксу:
@PostMapping
public OrderResponse checkout(@RequestBody CheckoutRequest request) {
return checkoutService.checkout(request.items()); // порожній список іде далі без guard
}
Так, перевірка майже смішно маленька — але саме такі працюють найкраще. Один запит стабільно відтворює 500, а в контролері видно: порожній список без валідації йде в сервіс. Два факти зійшлися — головна гіпотеза стала дуже сильною. Прогін усього test suite навпаки маскує картину: багато зеленого шуму й один червоний сигнал.
Тут є важливий педагогічний момент: мінімальна підтверджуюча перевірка — ще не regression test для PR. Баг на майбутнє ви не закриваєте — ви підтверджуєте, що правильно розумієте, звідки він береться.
6. Розпізнати patch loop і повернути сесію
Patch loop — це той неприємний режим, коли зовні ви все ще «виправляєте баг», а фактично сиплете нові правки без зростання розуміння. З AI він особливо підступний: Claude вміє дуже переконливо пропонувати локально правдоподібні зміни. Кожна окрема правка звучить розумно — а загальна траєкторія при цьому їде в канаву.
Корисно порівняти здорову діагностику і patch loop напряму:
| Здорова діагностика | Patch loop |
|---|---|
| одна провідна гіпотеза | багато випадкових виправлень поспіль |
| diff майже не росте | diff розповзається сусідніми файлами |
| є відтворення | «нібито має допомогти» |
| є мінімальна перевірка | перевіряється все підряд або майже нічого |
| можна сформулювати причину | можна лише сказати «тепер помилка інша» |
На практиці patch loop зазвичай упізнається за відчуттям: «лагодимо вже хвилин двадцять, а причину одним реченням усе ще не сформулюємо». Це дуже надійний сигнал зупинитися: повернути останні спекулятивні правки, перечитати diff і відкрити свіжу сесію з компактним пакетом доказів. Ви вже бачили: перевантажений контекст рідко розумнішає після ще двох повідомлень.
Якщо потрібно, можна зробити дуже коротку технічну паузу:
git diff --stat
git restore src/orders/OrderController.java
Сенс тут не в самому git restore, а в дисципліні: ви відмовляєтеся нарощувати хаос поверх хаосу. Це дуже доросла звичка, хоч і не виглядає героїчно, — зате потім не доводиться розгрібати diff, схожий на сліди боротьби ведмедя з роутером.
Гарна фраза для себе і для Claude в такий момент звучить приблизно так: «Стоп. Ми в patch loop. Повернімося до симптому, доказів і однієї провідної гіпотези». Проста, а сесію чудово протвережує.
7. Root cause statement як робочий артефакт
Коли ви підтвердили провідну гіпотезу, одразу хочеться виправляти. Але один маленький крок неймовірно підвищує якість подальшої роботи: записати root cause statement явно, одним коротким абзацом — не в стилі роману про страждання сервісу, а чітко й перевірювано.
Гарний root cause statement відповідає на три питання: що ламається, чому і де це доведено. Для нашого прикладу він може виглядати так:
## Коренева причина
`OrderController` пропускає checkout-запит із порожнім списком товарів далі в `CheckoutService`.
Нижче по ланцюжку сервіс розрахунку ціни працює так, ніби в кошику є хоча б один товар,
через що запит завершується серверною помилкою замість клієнтського `400`.
## Підтвердження
- баг стабільно відтворюється через `POST /api/orders` з `{"items":[]}`
- stack trace веде шляхом `OrderController.java:27 -> CheckoutService.java:41`
- у поточному `OrderController` до фіксу немає ранньої перевірки `request.items() == null || request.items().isEmpty()`
Зверніть увагу, наскільки тут мало магії і наскільки багато конкретики. Такий фрагмент кладеться в EVIDENCE_LOG.md, передається іншій сесії, показується колезі. Відволіклися на годину й повернулися — не згадуєте заново, «що ми там начебто зрозуміли».
І лише після такого запису налагодження справді можна вважати завершеним. Не тому, що баг виправлено, а тому, що ви перетворили хаос симптомів на інженерне знання. А з ним працювати набагато приємніше, ніж із надією на черговий вдалий if (x != null).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ