Якість і безпека
Adversarial review: як спробувати спростувати правильність зміни
Після глави 50 про огляд коду корисно зробити крок у бік: не тільки шукати помилку, а й перевіряти, чи є підстава вважати помилкою саме те, що підозрює…
Після глави 50 про огляд коду корисно зробити крок у бік: не тільки шукати помилку, а й перевіряти, чи є підстава вважати помилкою саме те, що підозрює рецензент. Що мало б статися за підтримуваних умов, щоб ця зміна виявилася неправильною? Який доказ відрізнить підтверджений дефект від припущення?
Критична перевірка на спростування, або adversarial review, — це уважне випробування важливого твердження про зміну контрприкладами. Вона не вимагає вороже ставитися до автора, перевіряти всі мислимі комбінації чи обов’язково знайти зауваження. Її межа — погоджені вимоги, власний або явно дозволений навчальний приклад і конкретний ризик. Цей розділ не надає дозволу перевіряти чужі системи.
51.1. Перевіряйте твердження, а не людину
Зміна може містити хорошу назву функції, зелені перевірки й переконливий опис, але все ще порушувати правило продукту. І навпаки: складний на вигляд шлях може бути безпечним завдяки обмеженню в базі чи контракту, якого рецензент спочатку не помітив.
Практичні настанови Google з огляду коду радять перевіряти поведінку на крайніх випадках і паралельному виконанні, оцінювати технічні факти, а не особисті вподобання, і не затримувати зміну заради досконалості чи стилю. Це настанови конкретної інженерної організації, а не універсальна політика для всіх команд; вимоги конкретного проєкту й ризик зміни визначають межу огляду. (Google Engineering Practices: The Standard of Code Review; What to Look For)
Перед пошуком контрприкладу запишіть твердження, яке збираєтеся перевірити. Наприклад: «одне місце в розкладі може мати не більше одного активного бронювання» або «користувач може змінювати лише власне бронювання». Таке правило, яке має залишатися істинним після зміни, називатимемо інваріантом.
Далі зафіксуйте три речі:
- Умови. Хто виконує дію, який стан був до неї, яка версія коду й які домовленості про дані чинні?
- Незалежний критерій очікування. Що визначає правильну поведінку: вимога, контракт, офіційна гарантія інструмента чи інваріант даних?
- Можливий контрприклад. Яка найкоротша допустима послідовність дій порушить правило, якщо підозра правильна?
Наприклад, щоб перевірити авторизацію, фрази «користувач не увійшов» недостатньо. Важливіша пара: «користувач увійшов, але намагається змінити запис іншої людини». Вхід до продукту доводить, хто звернувся; він сам собою не доводить, що цій людині дозволено змінити конкретний об’єкт.
Якщо незрозуміло, чи належить сценарій до підтримуваних умов, це може бути прогалиною вимоги. Не оголошуйте його дефектом, доки не встановите, чи продукт має його підтримувати і який ризик створює невизначеність.
51.2. Визначте межі перед тим, як ускладнювати сценарій
Список крайніх випадків без контексту роздувається нескінченно. У різних продуктах можуть бути інші дозволені ролі, правила скасування, гарантії бази, часові межі й способи повторити запит. Зафіксуйте межі до того, як додавати сценарій до звіту:
| Частина перевірки | Запитання |
|---|---|
| Зміна | Яка саме зміна й базова ревізія перевіряються? |
| Правило | Яке користувацьке або технічне твердження має лишатися істинним? |
| Передумови | Які ролі, дані, порядок дій, версії клієнта й відмови дозволені? |
| Очікуваний результат | Яке джерело визначає його незалежно від реалізації? |
| Наслідок | Що побачить користувач або що зміниться в збережених даних? |
| Відсутні відомості | Який факт треба уточнити, перш ніж робити висновок? |
Поганий сценарій звучить так: «десь можлива гонка». Кращий формулює конкретне чергування: «два запити перевірили, що місце вільне, перш ніж будь-який із них записав бронювання; після цього обидва зберегли окремий запис для одного місця». Тут уже зрозуміло, які передумови потрібні й який інваріант треба перевірити.
Окремо перевірте, чи тест має незалежний критерій очікуваного результату. Якщо код і тест спираються на ту саму неправильну передумову, зелений тест підтверджує тільки узгодженість цієї пари. Наприклад, тест, що лише перевіряє відповідь «не авторизовано» для людини без входу, нічого не каже про людину з чинною сесією, яка просить чужий запис.
51.3. Повний розбір: перевірка входу не дорівнює праву на запис
Розгляньмо вигаданий навчальний програмний інтерфейс застосунку (Application Programming Interface, API) для бронювань. За вимогою, користувач може змінити лише власне бронювання у своїй організації. Це приклад для пояснення, його код і тести не запускалися.
Олена й Марко належать до організації «Північ»; Олена має бронювання 418, а Марко — 527. Обоє увійшли. Марко надсилає запит на зміну запису 418. Поточний обробник спершу перевіряє, що запитувач має чинний вхід, а потім шукає бронювання за його номером у всій таблиці. Він не перевіряє, кому належить знайдений запис.
Мінімальний контрприклад:
- У сховищі є бронювання 418 з власницею Оленою.
- Марко має чинний обліковий запис, але не є власником 418.
- Марко змінює поле контакту в запиті до запису 418.
- Система зберігає новий контакт і повідомляє про успіх.
Це порушує правило навіть тоді, коли маршрут правильно відхиляє неавторизованих відвідувачів. Вхід підтверджено; право змінювати запис не підтверджено.
Open Worldwide Application Security Project (OWASP) у рекомендації API1:2023 пояснює цю різницю як авторизацію на рівні об’єкта: коли запит містить ідентифікатор об’єкта й виконує над ним дію, перевіряйте, чи має саме поточний користувач право на саме цю дію. (OWASP API1:2023) Це незалежний критерій для нашої навчальної вимоги, а не доказ того, що будь-яка система має ідентичну модель ролей.
Мінімальне виправлення має обмежити пошук поточним користувачем і його організацією або виконати рівноцінні перевірки до зміни. Тест має підтвердити, що запит Марка відхилено, запис Олени не змінився і відповідь не витекла зайвими даними; позитивна перевірка має дозволити Олені змінити свій запис. Якщо користувач може належати до кількох організацій, перевірте також запит у неправильному організаційному контексті. Перевірка без входу покриває інше правило.
Якщо продукт навмисно приховує існування чужого запису, відмова може виглядати як «не знайдено», а не «заборонено». Тоді критерій включає не тільки відсутність зміни, а й те, що відповідь не розкриває зайвих даних. Не вимагайте конкретний код відповіді без опори на контракт.
51.4. Добирайте класи дій за ризиком
Коли головний інваріант відомий, виберіть небагато різних способів його порушити. Почніть із наслідків, а не з кількості сценаріїв:
- Межа значень: порожній, мінімальний, максимальний, неправильний або відсутній ідентифікатор.
- Інший учасник: інший власник, клієнт, роль або організація.
- Повтор: клієнт повторив запит після тайм-ауту й не знає, чи перша спроба спрацювала.
- Інший порядок: скасування, оновлення чи завершення відбуваються не в тому порядку, який припускає щасливий шлях.
- Одночасність: кілька дозволених запитів читають той самий стан і намагаються його змінити.
- Часткова відмова: одна частина операції збереглася, а наступний запис, повідомлення або підтвердження не відбувся.
- Застарілий контекст: старіший клієнт, роль або запис діє під час зміни контракту чи правил доступу.
Вибраний сценарій має відповідати фактичному шляху програми. Не припускайте, що клієнт може передати будь-яке значення, якщо контракт це забороняє; водночас перевірте, чи сервер не покладається лише на неперевірене обмеження інтерфейсу. Не додавайте зовнішню мережу, зловмисника, спеціальні привілеї й екзотичні відмови лише для того, щоб зробити зауваження драматичнішим.
51.5. Повний розбір: два запити забирають одне місце
Припустімо, розклад має наперед створені місця з окремими ідентифікаторами. Вимога — не більше одного активного бронювання на одне місце. Поточний код спочатку запитує, чи місце вільне, а потім створює бронювання. Обидві дії відбуваються окремо.
Це вигаданий навчальний приклад; наведений код і згадані перевірки не запускалися. Приклад запиту записано мовою структурованих запитів (Structured Query Language, SQL).
Можлива послідовність:
| Крок | Запит A | Запит B |
|---|---|---|
| 1 | читає: місце 815 вільне | |
| 2 | читає: місце 815 вільне | |
| 3 | створює бронювання A для 815 | |
| 4 | створює бронювання B для 815 |
Кожен запит мав правдоподібну відповідь на власну перевірку. Транзакція забезпечує все або нічого для змін усередині неї, але сама по собі не робить послідовність «перевірити, потім записати» безпечною від гонки. За рівня Read Committed два клієнти все ще можуть побачити вільне місце. Щоб зберегти інваріант, потрібен захист від одночасного запису — наприклад, обмеження унікальності, явне блокування рядка або серіалізована транзакція з обробкою помилки й повтором. Тут атомарність транзакції означає лише те, що її власні зміни фіксуються разом або відкочуються разом.
У PostgreSQL за замовчуванням використовується рівень ізоляції Read Committed: окремий запит бачить дані, зафіксовані до його початку. Два послідовні читання можуть бачити різний стан, якщо між ними зафіксувалася паралельна зміна. Отже, два незалежні запити на перевірку можуть обидва прочитати «вільно», перш ніж хтось збереже бронювання. (PostgreSQL 18, Transaction Isolation)
Якщо таблиця зберігає тільки активні бронювання для фіксованих місць, одним із можливих захистів є обмеження унікальності за ідентифікатором місця: база відхиляє другий рядок. У PostgreSQL вставка може використати ON CONFLICT DO NOTHING — при конфлікті не створювати другий запис:
INSERT INTO active_reservations (slot_id, customer_id)
VALUES (815, 29)
ON CONFLICT (slot_id) DO NOTHING
RETURNING id;
У цьому фрагменті поле slot_id має бути NOT NULL і мати обмеження UNIQUE. Якщо вставка повернула ідентифікатор, запит зарезервував місце. Якщо вставка не повернула рядок, місце вже зайняте: цей сценарій повідомляє, що бронювання не вдалося. Повтор того самого запиту обробляйте за окремим правилом ідентифікації запиту. Обробник не має мовчки повідомляти про успіх, коли рядок не створено. PostgreSQL документує обмеження унікальності як правило для всіх рядків, а ON CONFLICT — як спосіб визначити дію при конфлікті. (PostgreSQL 18, Constraints; PostgreSQL 18, INSERT)
Це рішення відповідає лише описаній моделі. Воно передбачає одне бронювання на фіксоване місце з окремим стабільним ідентифікатором, а не довільні інтервали часу. Якщо система зберігає скасовані рядки назавжди, просте UNIQUE за slot_id заблокує повторне бронювання після скасування: знадобиться окремий активний запис, індекс, що застосовує унікальність лише до активних рядків, або інша узгоджена модель. Якщо час можна бронювати довільними інтервалами, потрібна гарантія від перетинів інтервалів, а не лише однакових ідентифікаторів.
Змістовний регресійний тест має змоделювати два конкурентні запити на тому самому ізольованому сховищі, а не лише підмінити його макетом. Обидва запити доходять до спроби запису. Очікування: зафіксовано рівно одне активне бронювання, другий запит отримав коректну відмову, клієнти не бачать хибного подвійного успіху. Бар’єр у тесті допомагає перевірити конкретний порядок дій; випадкове очікування кілька мілісекунд саме собою не гарантує відтворюваного чергування.
Якщо використовувати вищий рівень ізоляції, це також потребує окремої перевірки. Серіалізований рівень PostgreSQL може відхилити одну транзакцію помилкою серіалізації; застосунок має бути готовий повторити всю транзакцію. Сама назва рівня Serializable не означає, що помилка зникне або що повтор уже реалізовано. У багатьох моделях простіше й точніше виразити інваріант обмеженням даних.
51.6. Знімайте хибну підозру так само доказово, як підтверджуйте дефект
Уявімо, що рецензент стверджує: «повторна доставка того самого повідомлення створить другу бронь». Це правдоподібна гіпотеза, але слово «повтор» ще не є доказом.
Це також вигаданий навчальний приклад; запит і пов’язані перевірки не запускалися. Обробка повідомлення починається зі вставки квитанції з ключем організації, джерела й ідентифікатора повідомлення. Усі три поля обов’язкові, а комбінація має унікальне обмеження. Запис квитанції та створення бронювання виконуються в одній транзакції:
INSERT INTO processed_events (tenant_id, source_id, event_id)
VALUES (12, 'calendar-provider', 'evt-731')
ON CONFLICT (tenant_id, source_id, event_id) DO NOTHING
RETURNING event_id;
Лише якщо вставка повернула рядок, ця транзакція створює бронювання. Якщо інша доставка вже створила квитанцію, вставка нічого не повертає, тож обробник не створює ще одне бронювання. Цей приклад доводить лише відсутність дубльованого збереженого бронювання за вказаних умов; щоб повернути тотожну відповідь на повтор, модель мала б окремо зберігати результат або ідентифікатор бронювання. Припустімо також, що перевірено всі шляхи створення бронювання: жоден не обходить цю квитанцію; ключ не допускає NULL; обидва записи фіксуються або відкочуються разом.
За цих умов висновок «повтор цього повідомлення створив другу бронь» спростовано для збережених бронювань. Добра відповідь рецензента: «Я спершу пропустив унікальне обмеження й спільну транзакцію, тому знімаю зауваження про другий рядок. Я не перевіряв, чи повторюється зовнішнє сповіщення; якщо воно входить у вимогу, це окремий шлях і окрема перевірка».
Обмеження бази не доводить, що двічі не буде надіслано листа, списано кошти у зовнішнього постачальника або повторено дію поза транзакцією. Для зовнішнього ефекту можуть знадобитися власний ключ повтору постачальника, журнал відправлення або таблиця вихідних повідомлень (outbox) — записів про події, які треба доставити; окремий процес читає їх і надсилає події з повторними спробами. Рецензент, який вказує на такий інший наслідок, може мати рацію. Треба чітко назвати, яке саме твердження спростовано.
Цю дисципліну добре ілюструє давній публічний звіт про Ruby on Rails. Автор звернення про помилку № 13234 навів відтворення, де два паралельні запити проходять перевірку унікальності, перш ніж будь-який запише результат. Сценарій із дублікатом реальний; початкова теза, що це проблема безпеки потоків усередині методу save, не описує точну причину. Поточна документація Rails прямо попереджає, що перевірка унікальності на рівні застосунку не гарантує відсутності дублікатів і радить обмеження унікальності в базі. Правильний висновок не «нічого не ламається» і не «весь дефект у Rails», а «окремі читання й запис мають гонку; інваріант треба забезпечити на рівні, який бачить обидва конкурентні записи». (Rails, звернення № 13234; Rails API: uniqueness validation, concurrency and integrity)
51.7. Реальний розбір: повідомлена помилка підтвердилася
Публічна історія може показати, як гіпотеза стає підтвердженим дефектом. У SQL порядок ключових слів має відповідати граматиці конкретної бази. У 2021 році автор звернення про помилку № 41480 у Ruby on Rails повідомив, що міграція з одночасними параметрами if_not_exists і algorithm: concurrently створює некоректний запит до PostgreSQL. До звіту він додав короткий застосунок для відтворення, версії Rails 6.1.2.1, pg 1.2.3 і PostgreSQL 13.1, а також фактичне повідомлення про синтаксичну помилку.
Звіт можна перевірити незалежним критерієм — офіційною граматикою PostgreSQL для CREATE INDEX. Вона ставить CONCURRENTLY перед IF NOT EXISTS. У повідомленні про помилку Rails згенерував зворотний порядок: IF NOT EXISTS CONCURRENTLY. Тут є зв’язок від передумови до результату: задана комбінація параметрів → неправильний рядок запиту → помилка парсера.
У пов’язаному запиті на включення змін (pull request, PR) № 41490 автор виправлення змінив порядок і додав регресійний тест, який має виявляти попередній результат. Його об’єднали 4 березня 2021 року. Це сильніший доказ, ніж голосування чи впевненість автора: є відтворення, правильний синтаксис від постачальника бази, виправлення та тест. (Rails, звернення № 41480; Rails, запит на включення змін № 41490; PostgreSQL 18, CREATE INDEX)
Водночас це історичний дефект конкретної версії й конкретної комбінації параметрів. З нього не випливає, що актуальний Rails досі генерує ту саму помилку. Точна межа твердження зберігає корисний урок, не перетворюючи його на узагальнення про нинішній продукт.
51.8. Доручайте рецензенту-агенту пошук доказів, а не винесення вироку
Агент штучного інтелекту (Artificial Intelligence, AI) може швидко запропонувати підозрілі шляхи, пояснити послідовність подій або написати тест. Попросіть його назвати:
- точний файл, рядок і ревізію;
- правило, яке, на його думку, порушено;
- передумови й найкоротше відтворення;
- очікувану та фактичну поведінку;
- джерело незалежного критерію;
- який факт спростував би знахідку.
Після цього перевірте самі, чи існує такий шлях виконання, чи стосується джерело потрібної версії і чи відтворює тест реальну межу системи. Якщо агент повідомляє, що запускав команду, зіставте це з доступним журналом виконання. Не записуйте у висновок перевірку, якої ніхто не запускав.
Офіційні матеріали OpenAI станом на 24 вересня 2026 року описують правила огляду Codex у файлі AGENTS.md та радять перевіряти їх на прикладі, який має спрацювати, безпечному контрприкладі й нерелевантній зміні. Та сама публікація уточнює: тести, захист гілок і обов’язкові схвалення залишаються механізмами, які забезпечують виконання вимог. У Claude Code довідка на цю дату описує керований Code Review як попередній дослідницький режим (research preview): він публікує зауваження, але не схвалює і не блокує запит на включення змін. Наявність багатьох агентів або перевірки й відсіювання помилкових спрацювань у продукті не гарантує, що конкретна знахідка правильна чи що вона перевірила потрібну версію. Доступність і назви функцій змінюються; перевіряйте актуальну документацію, а не переносіть можливості між продуктами. (OpenAI: Custom Code Review Rules for Codex, 20 липня 2026 року; Anthropic: Claude Code Review)
51.9. Коли завершувати перевірку
Мета — дати команді достатньо доказів для рішення, а не гарантувати відсутність усіх майбутніх дефектів. Для кожного ризику сформулюйте один із висновків:
- Підтверджено: допустимий сценарій порушує правило; покажіть відтворення, наслідок і найменший належний спосіб виправлення.
- Не підтверджено: потрібні передумови не відповідають підтримуваному контракту або незалежна гарантія зберігає інваріант; зафіксуйте, що саме перевірено.
- Недостатньо даних: бракує вимоги, версії, конфігурації чи доступного середовища; назвіть конкретне свідчення, яке змінило б рішення.
Зупиніть пошук, коли критичні для зміни твердження мають відповідні перевірки, найважливіші допустимі шляхи не залишили відкритого блокера, а наступні сценарії лише повторюють уже досліджені механізми. Запишіть межі впевненості й перейдіть до наступного ризику. Якщо тестове середовище не дозволяє перевірити конкурентність, не підміняйте відсутній доказ впевненим тоном.
У книжці Accelerate автори пояснюють, що менші пакети змін скорочують цикл і пришвидшують зворотний зв’язок; у додатку вони також наводять результати на рівні організацій, де команди без процесу погодження або з взаємним рев’ю повідомляли про вищу ефективність постачання. Як редакційний висновок для цієї глави, ці спостереження підтримують раннє, зосереджене рев’ю та чітку межу завершення, але не доводять, що окремий запит слід пропускати повз обов’язкові перевірки чи захист гілки. (Nicole Forsgren, Jez Humble, Gene Kim, Accelerate, авторизований уривок)
Точний звіт часто має одне підтверджене зауваження, одну зняту підозру й одну невідому передумову. Кількість пунктів не вимірює якість рецензії. Огляд завершено не тоді, коли зникли всі сумніви, а коли команда може пояснити, що перевірено, що спростовано, що лишається невідомим і чому цього достатньо для наступного рішення.
Короткий підсумок
Критична перевірка починається з одного важливого твердження, його умов і незалежного критерію правильності. Потім рецензент будує найменше дозволене відтворення й відокремлює факт від гіпотези. Перевірки доступу мають розрізняти чинний вхід і право на конкретний запис; конкурентні інваріанти мають спиратися на гарантію сховища, а не лише на послідовність у застосунку. Знайдену помилку треба довести, хибну підозру — зняти, а нестачу доказів — назвати прямо.
Джерела та подальше читання
Матеріали перевірено 10 жовтня 2026 року. Документація PostgreSQL і продуктові довідки можуть змінюватися; за конкретного впровадження звіряйте версію й конфігурацію.
- PostgreSQL 18, 5.5 Constraints — обмеження UNIQUE, первинні ключі, виключні обмеження та поведінка NULL; підтримує пояснення, які гарантії належать схемі даних.
- PostgreSQL 18, 13.2 Transaction Isolation — рівні ізоляції, знімок читання за замовчуванням, повторна перевірка умови UPDATE після конкурентної зміни та потреба обробляти помилки серіалізації.
- PostgreSQL 18, INSERT і CREATE INDEX — обробка конфліктів вставки та граматика індексу, використані в навчальному і реальному прикладах.
- Open Worldwide Application Security Project, API1:2023 Broken Object Level Authorization — перевірка повноважень поточного користувача над кожним об’єктом, до якого звертається API.
- Ruby on Rails, звернення № 41480, від 17 лютого 2021 року — мінімальне відтворення помилкового порядку SQL-ключових слів з указаними версіями Rails, адаптера PostgreSQL і сервера бази.
- Ruby on Rails, PR #41490, змерджений 4 березня 2021 року — порядок генерації виправлено, додано регресійний тест.
- Ruby on Rails, ActiveRecord uniqueness validation, документація API — описує гонку між перевіркою унікальності на рівні застосунку та записом; радить відповідне обмеження в базі. Перевірено 24 вересня 2026 року.
- Ruby on Rails, звернення № 13234, від 8 грудня 2013 року — публічний приклад, у якому спостережуване дублювання є реальним, але пояснення про дефект безпеки потоків треба відрізняти від гонки між читанням і записом.
- Документація GitHub, Pull request reviews і Status checks — огляди й автоматичні перевірки мають різні ролі; необхідні схвалення та перевірки залежать від правил конкретної гілки.
- Google Engineering Practices, The Standard of Code Review і What to Look For — рекомендації щодо технічних ризиків, крайніх випадків, паралельності, підтримуваності та невикористання особистих стильових уподобань як блокера.
- Nicole Forsgren, Jez Humble, Gene Kim, Accelerate, авторизований уривок — дослідження швидкого зворотного зв’язку й малих пакетів змін; наведені організаційні результати не є правилом для схвалення окремого запиту.
- OpenAI, Custom Code Review Rules for Codex, 20 липня 2026 року — інструкції репозиторію можуть уточнювати огляд; публікація прямо відділяє його від тестів, захисту гілок і обов’язкових схвалень.
- Anthropic, Claude Code Review — чинна на дату перевірки документація про режими огляду, продуктову доступність, статус попереднього дослідницького режиму та те, чи блокує керований огляд запит на включення змін.