Issue-to-PR: спочатку зрозуміти, потім змінювати

У реальній команді завдання приходить не акуратною постановкою, а сирим issue з трекера, де факти, очікування і чуже готове рішення змішані в одному абзаці. Спокуса велика: відкрити вказаний у тікеті файл і одразу поправити - але саме так з'являються акуратні fix не там, де треба.

Тому сьогодні ми підемо іншим шляхом: розберемо тікет на факти й гіпотези, простежимо шлях даних у plan mode, зберемо план, який можна перевірити, і розріжемо його на невеликі reviewable slices - не торкнувшись жодного рядка вихідного коду.

Результат вебінару - не готовий PR, а шлях, яким його можна зробити без здогадок і зайвого scope.

Тікет виглядає яснішим, ніж він є

В issue #731 вже написано, який файл треба виправити. Це звучить зручно, але тікет підтверджує симптом, а не місце помилки.

# Issue #731

Після скасування доставка зникає з Active,
але після refresh з'являється знову.
Сховайте cancelled delivery в ActiveDeliveries.tsx.
Скриншот додано.

Після refresh дані приходять заново. Frontend-фільтр помилку API тільки сховає - не полагодить.

Запропоноване рішення автора - корисна гіпотеза, але не acceptance criterion.

Код розташований посередині маршруту

Якщо почати з implementation, можна швидко зробити акуратний fix не там, де треба. Спочатку перетворюємо issue на шлях, який можна перевірити.

flowchart LR I["Issue"] --> N["Intake"] N --> X["Investigation"] X --> P["Approved plan"] P --> S["Slices"] S --> C["Implementation"] C --> V["Checks + diff"] V --> R["PR + review"]

На цьому вебінарі докладний маршрут закінчується на Slices. Implementation починається тільки після перевірки evidence, scope і ризиків. Далі - implementation, commits і review: це тема наступного рівня.

Один і той самий loop підходить для bug fix, feature, refactoring, migration і docs update. Змінюються acceptance criteria і risk profile, а не маршрут.

Факт, очікування, гіпотеза, прогалина

Одне речення тікета часто виглядає як вимога. Розкладіть його на частини - і стане видно, що вже доведено, а що поки лише здається правдою.

Частина issue #731Статус
Після refresh доставка знову видимаспостережуваний факт
Cancelled не повинна бути в Activeочікувана поведінка
Потрібен frontend-фільтргіпотеза автора
Який endpoint повертає списокunknown
Не змінювати всю status modelмежа завдання

Evidence відповідає "що ми знаємо", а hypothesis - "що варто перевірити". Не міняйте ці ролі місцями.


Issue Intake задає вхід для дослідження

Короткий Intake прибирає готовий fix з вимог і показує, що досліджувати далі.

# Issue Intake #731

Problem: після refresh скасована доставка знову в Active.
Requirement: Active не містить cancelled після перезавантаження.
Evidence: скриншот + відтворювана послідовність refresh.
Hypothesis: фільтр у frontend - правильний fix.
Unknown: який API віддає список і хто ним ще користується?
Non-goal: не переглядати модель статусів доставки.
Risk: frontend-фікс залишить поведінку API неправильною.

Перед investigation (дослідження завдання по коду) перевірте п'ять речей:

Intake може жити в issue comment або всередині issue-plan.md. Новий файл потрібен лише тоді, коли він допомагає review.

Спочатку досліджуємо без правок

Plan mode розділяє читання і дію: Claude досліджує codebase і пропонує підхід ще до редагування вихідних файлів. Він потрібен, коли неправильний перший fix обійдеться дорожче за коротке дослідження.

/plan investigate issue #731

Do not edit source files.
Trace the Active deliveries data path.
Return files, entry points, tests,
evidence, unknowns and risks.

Межа режиму проста: source files не змінюються до review плану.

Ultraplan /ultraplan - опціональний research preview у Claude Code on the web. Він потребує GitHub repo і ділить загальний rate limit акаунта. Локальний plan mode залишається основним шляхом.

Не картографуємо весь repo

Широкий запит "поясни весь проєкт" принесе багато контексту і мало рішення. а issue #731 потребує тільки шляху Active deliveries.

  1. знайти UI action і network request;
  2. простежити route і controller;
  3. перевірити service і repository;
  4. знайти tests та інших consumers;
  5. подивитися error і edge paths.
rg "status=active|ActiveDeliveries" src test
# src/ui/ActiveDeliveries.tsx
# src/api/deliveries.ts

rg "findAll|findByStatus" src/server test
# src/server/DeliveryService.ts: return repo.findAll()
# test/api/deliveries.test.ts

Пошук ще не доводить fix. Він показує перший call path і місце, де query може губитися.


Evidence змінює напрям рішення

Investigation корисне не тоді, коли підтверджує першу здогадку, а коли допомагає вчасно від неї відмовитися.

# Investigation #731

Evidence
- UI рендерить відповідь API без локального кешу.
- GET ?status=active доходить до DeliveryController.
- DeliveryService ігнорує status і кличе findAll().
- Mobile використовує той самий endpoint.
- API test не перевіряє фільтрацію за status.

Unknown
- Невідомий status має повернути 400 чи порожній список?
Знайдений рядок ще не робить DeliveryService фінальним місцем зміни. Спочатку перевірте repository pattern і контракт query.

Невідомість отримує наступний крок

Вимагати повного знання до плану так само шкідливо, як ховати невідомість. Розділіть unknown за впливом на рішення.

UnknownOwnerNext stepBlocks?
відповідь на невідомий statusAPI ownerуточнити контракттак
ім'я test fixtureimplementerвзяти наявний патернні

Зупиніться, якщо змінюється public API, зачеплена shared status model або product rule не підтверджене. Ім'я helper і вибір fixture можна залишити implementer.

Хороший план не ховає unknown і не робить з кожного питання блокер. Він фіксує owner, наступний крок і чесний статус.

Plan фіксує рішення, а не історію пошуку

Якщо вставити в plan весь transcript, implementer знову шукатиме рішення всередині історії. Залиште тільки те, що спрямовує зміну - і все.

Для issue #731 обраний шлях іде через наявний backend query, зберігає endpoint contract і додає regression evidence.

Якщо issue state або base commit змінилися, зупиніться і повторно перевірте evidence та affected files.

Reviewable plan на одному екрані

Ті самі поля, але вже заповнені. Такий план читається за хвилину, і за ним можна ухвалити рішення, не відкриваючи transcript.

# issue-731-plan.md

Goal: Active не показує cancelled після refresh.
Scope/approach: передати status із service в repository query; без frontend filter.
Files: DeliveryService, DeliveryRepository, API test.
Steps: відтворити в тесті -> полагодити query -> перевірити consumers.
Assumption: cancelled не входить в Active.
Risks: mobile використовує той самий endpoint; особливе значення active.
Verification: targeted API test + наявні тести зелені.
Stop: змінюється response shape або assumption не підтверджується.
Based on: issue #731 у стані Open, commit a1b2c3d.

Зауважте: план не переказує investigation. Кожен рядок або спрямовує дію, або задає межу.


Ризик без реакції не допомагає

Risk list потрібен не заради повноти документа. Він пов'язує можливу проблему з check, mitigation або stop condition.

РизикСигналРеакція
Mobile залежить від endpointзнайдено client usageперевірити contract test
active має особливий meaningdocs і code розходятьсяstop і спитати owner
Fix тягне redesignaffected files ростутьвинести окреме завдання
Happy path змінюєтьсяexisting tests падаютьповернути plan на revision

Для звичайного завдання досить 3-5 реальних ризиків. Рядок "Claude розбереться на ходу" не вважається реакцією.


Verification пишеться до реалізації

Перевірка до коду швидко показує слабкий plan: спостережуваний результат доводиться формулювати одразу, відкрутитися не вийде.

targeted API test passes
existing delivery API tests pass
diff stays inside approved files

Machine-check доводить технічну поведінку. Product owner відповідає за product rule і окремо підтверджує, що cancelled справді не входить в Active.


Як повернути plan на revision

Чернетка плану звучить упевнено, але рішення залишається за людиною. Виходів три:

РішенняКоли обрати
approveevidence, scope і checks достатні
reviseнапрям правильний, але plan занадто широкий
stopproduct rule не підтверджене або scope змінився

Revise - це конкретні межі, а не "зроби краще":

Revise the plan.
Do not add a frontend filter.
Keep the change in the existing status query path.
Add the mobile consumer risk and the targeted API check.
Stop if the public response shape must change.

Результат: frontend filter виключений з підходу, у плані з'явилися mobile risk і targeted check, stop condition став ширшим.

Approval - не ввічливе "ок", а рішення прийняти scope і risk до першої правки.

Одна ідея, один зрозумілий check

План, реалізований одним шматком, приходить на review одним великим diff - чесно перевірити його вже ніхто не може. PR slice - мінімальна логічна частина approved plan, яку можна зрозуміти, перевірити і за потреби відкотити окремо.

Погано: "полагодити API, підчистити status model і оновити dependencies".

Краще: "закріпити filtering contract для status=active в API test; production code поки не чіпати".

Slice може стати commit, частиною PR або окремим PR. Упаковка залежить від команди, логічна межа залишається.

Кожен slice рухає criterion або знімає ризик

Approved plan перетворюється на послідовність, де кожен крок має один intent і вихід, який можна перевірити.

flowchart LR A["Approved plan"] --> S1["1. Reproduce in API test"] S1 --> S2["2. Fix status query path"] S2 --> S3["3. Verify consumers"] S3 --> D["Ready for implementation"]

Перший slice закріплює симптом, другий змінює мінімальну production-зону, третій закриває consumer risk.

SliceCheckStop
Відтворити баг фільтраціїtest падає до fixsymptom не відтворюється
Виправити наявний query pathtargeted test проходитьзмінюється форма response
Перевірити consumersweb і mobile отримують одну семантикупотрібне нове product rule
Failing test - корисний перший slice, але не догма. Якщо відповідного harness немає, використайте вузький request або smoke script.