JavaRush /Курси /Claude code /Налагодження й аналіз першопричини

Налагодження й аналіз першопричини

Claude code
Рівень 18 , Лекція 1
Відкрита

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).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ