Слепое сравнение · работающий магазин · без ограничения по времени

Три модели в чужом проекте

Не пустая папка, а живой магазин: старый PHP-код, витрина на Next.js, запуск через Makefile, замороженные тесты и две настоящие уязвимости. Задание — 17 абзацев прозой. Новая приёмка — 145 проверяемых пунктов.

22–23 августа 2026Магазин автозапчастей: PHP 8.2 + Next.js 163 завершённых прогона · попытка Grok · 145 пунктов

01Итог

После повторного прогона и расширения проверки максимум — 145 баллов. Главное не изменилось: Claude первый. Но 100 % теперь не получает никто: новые проверки нашли ошибки в возвратах, повторном создании базы и строгой проверке входа.

1 место
Claude Code
Opus 5 · 70,5 мин
94,1 %
136,5 из 145
покрытие 99,0 % · мутации 84,1 % тестов: 435 / 1065 проверок оценка API: ~$287 безопасность: закрыта главный минус: возвраты копеек
2 место
Codex
GPT-5.6 Sol · 44 мин
84,5 %
122,5 из 145
покрытие 92,8 % · мутации 69,6 % тестов: 56 / 166 проверок оценка API: ~$78 безопасность: закрыта главный минус: слабые тесты
3 место
Cursor
Composer 2.5 · 17,5 мин
78,6 %
114,0 из 145
покрытие 86,5 % · мутации 67,5 % тестов: 68 / 122 проверки оценка API: ~$14 безопасность: есть дыра главный минус: возврат чужого заказа
Главное

Claude остаётся лучшим: больше тестов, сильнее проверка изменений, лучше работа с чужим кодом. Но после усиления приёмки он уже не на 100 %. Ошибка не в основной арифметике, а в краях: строгий вход, возвраты с неделимыми копейками и повторное создание базы.

Codex близко решает расчёт и безопасность, но тесты слабее: настоящая мутационная проверка дала 69,6 % при требуемых 80 %. Cursor быстрее и дешевле, но пропустил опасную дыру: чужой покупатель может оформить возврат по чужому заказу.

Cursor Grok отдельно запустить не удалось: текущий тариф Cursor не разрешил выбрать именованную модель. Поэтому Grok в таблицу качества не попал — кода от него нет.

Главный урок: обычная шкала почти упёрлась в потолок. Новые проверки состояния и возвратов сразу отделили «почти готово» от «можно сливать без риска».

02Почему задача сложная

Прошлое задание — расчёт корзины в пустой папке — уперлось в потолок: победитель взял 50 из 50. Здесь задача ближе к реальной работе: чужой код, старые тесты, безопасность, гонки, возвраты и запуск всего стенда.

Что усложняетПочему это важноКак проверялось
Чужой код Нужно читать существующие правила, а не писать с нуля. Старый слой с невидимым вызовом, неточность в CHANGELOG, замороженные тесты.
Безопасность сервера Рабочий расчёт бесполезен, если его можно обойти запросом. Две настоящие уязвимости в исходном проекте и проверки живыми атаками.
Скрытые комбинации Ошибки часто появляются на стыке правил. 52 расчётных случая: скидка, купон, налог, вес и доставка вместе.
Витрина Покупатель видит итог, а не внутренний код. Страница оформления, две валюты, запрет считать деньги в браузере.
Запуск Решение должно подниматься на чистой машине. Makefile, nginx, проверка сборки, работа без интернета.
Подделка результата Нельзя просто переписать тесты под своё решение. Замороженные файлы со слепком и один тест, который нужно поправить честно.

Docker не использовался: на сервере он занят рабочими сервисами. Поэтому запуск проверялся проще и ближе к заданию: чистая копия, make db, make up, make smoke, проверка nginx и сборки витрины.

03Модели, время и расход

УчастникМодельВремяТокеныСтоимость через APIЦена баллаКак завершился
Claude Code
CLI 2.1.239
claude-opus-570,5 мин 31 844 320~$287~$2,10 сам, код 0
Codex
CLI 0.149.0
gpt-5.6-sol44 мин 12 611 341~$78~$0,64 сам, код 0
Cursor
CLI 2026.08.11
composer-2.517,5 мин 2 590 613~$14~$0,12 сам, код 0

Расход развёл участников сильнее, чем баллы. Claude потратил в 2,5 раза больше токенов, чем Codex, и в 12 раз больше, чем Cursor, — но отрыв по итоговой оценке составил 9,6 и 15,5 процентного пункта. Все трое завершились сами, ни один прогон не пришлось обрывать.

Как считалась стоимость

В логах есть общий расход токенов, но нет полной разбивки на вход, выход и кэш. Поэтому это оценка «как через API»: 80 % токенов считаются входом, 20 % — выходом. Для Claude взята ставка Claude Opus 5, для Codex — GPT-5.6 Sol с длинным контекстом, для Cursor — быстрый Composer 2.5, потому что он включён по умолчанию. По такой методике Claude стоил бы около $287, Codex — около $78, Cursor — около $14. Фактическая сумма, которую показал инструмент Claude Code, была ниже: $24,77.

По деньгам вывод другой, чем по качеству: Claude остаётся самым надёжным, но в API-подсчёте самым дорогим. Cursor — самый дешёвый за балл. Codex оказывается посередине по сумме, но дорогим относительно набранных баллов.

Попытка Cursor Grok

Что запускалосьИтогЧто это значит
Cursor Grok 4.6 High Fast
cursor-grok-4.6-high-fast
не стартовал
9 секунд, код 1
CLI принял модель, но остановился до работы над кодом: текущий тариф разрешает только Auto, а именованные модели недоступны.

Для попытки была создана чистая копия bench4-cursor-grok из исходного проекта. В логе есть только старт сессии и текст задания; правок, тестов, расхода токенов и результата приёмки нет. Поэтому ставить Grok балл было бы нечестно: это не провал решения, а ограничение запуска.

04Методика

Что фиксировалиКак именно
Стартовое состояниеТри одинаковые копии магазина: PHP-сервер, витрина на Next.js, запуск, документация, 23 замороженных PHPUnit-теста и 5 замороженных проверок витрины. Всё зелёное на старте. Зависимости уже установлены, сети у моделей нет.
ВремяНе ограничено. Предохранитель в 4 часа не понадобился.
ЗаданиеОдин файл, 17 абзацев прозой, дословно одинаковый для всех. Уточняющих вопросов не задал никто.
ИзоляцияОтдельные настройки только с авторизацией: без расширений, памяти и скрытых подсказок. Прогоны последовательные, порты и процессор никто не делил.
Эталон расчётаНезависимая проверочная программа на Python, написанная по тексту задания до прогонов. Два случая пересчитаны вручную до копейки.
Скрытый набор52 расчётных случая и 20 враждебных входов. Моделям их не показывали: в задании правила описаны по отдельности, а здесь они смешаны.
ЛовушкиСначала было 22 пункта. После повторной проверки добавлены ещё 11: владелец на всех операциях, строгий вход, повторное оформление, резервы, возвраты и миграции.
ОценкаОсновной пересчёт сделан по 145 пунктам. Старые слепые оценки оставлены ниже как контроль: все судьи поставили модели в том же порядке.
Три ошибки нашлись в самой приёмке

Проверка nginx -t заворачивала все конфиги работы в собственную обёртку и ломалась о готовый конфиг верхнего уровня, который Codex положил специально для этой команды. Проверка секрета считала находкой gitignore-артефакты прогона: файл с ключом, который make up создаёт с правами 600, и кеш сборки Turbopack. Детектор опасных функций принимал PDO::exec() за вызов шелла.

Все три — ошибки оценщика, не моделей; исправлены, результаты пересчитаны. Это к вопросу о том, насколько машинная приёмка сама нуждается в приёмке.

05Текст задания

Ровно то, что получила каждая модель, — целиком, дословно. Нумерация абзацев добавлена здесь для ссылок; в самом задании её нет. Правила намеренно разнесены: порядок применения скидок в одном абзаце, порог бесплатной доставки в другом, запрет на float — вообще в CONVENTIONS.md репозитория.

1Ты подключаешься к работающему магазину автозапчастей «Шестерёнка». Репозиторий лежит в текущем каталоге: PHP-бэкенд в api, витрина на Next.js в web, скрипты запуска в infra, публичный контракт в docs/openapi.yaml, договорённости команды в CONVENTIONS.md. Магазин уже продаёт, у него есть мобильное приложение на версии контракта 1.x и выгрузка в маркетплейс, поэтому всё, что описано в docs/openapi.yaml, должно продолжать работать так же: поля можно добавлять, но не удалять и не переименовывать. Прочитай CONVENTIONS.md и api/tests/frozen/README.md прежде, чем начнёшь.

2Маркетинг запускает осеннюю кампанию, и на неё нужно доработать расчёт. Дальше — что должно получиться. Текст написан так, как его прислали, поэтому требования придётся вычитывать, а не искать по пунктам.

3Скидка теперь зависит от суммы корзины и начисляется ступенями: до пяти тысяч рублей скидки нет, от пяти тысяч включительно и до пятнадцати тысяч — три процента, от пятнадцати тысяч включительно и до тридцати тысяч — пять процентов, от тридцати тысяч включительно — восемь процентов. Сумма, по которой определяется ступень, — это стоимость товаров без доставки и без налога. Скидка со ступени раскладывается по позициям корзины пропорционально их стоимости, а копейки, которые не делятся нацело, отдаются позициям с наибольшей дробной частью; если дробные части равны, преимущество у позиции с меньшим артикулом по алфавиту.

4Поверх ступени работают купоны, и с ними всё меняется. Раньше купон был просто строкой из таблицы, теперь у покупателя есть персональный купон, который выдаётся ему на конкретную кампанию и который нельзя передать другому. Купон приходит одной строкой из трёх частей, разделённых точками: код кампании, срок действия отметкой времени UTC вида 2026-12-31T23:59:59Z и подпись. Подпись — это HMAC-SHA256 от склеенных через вертикальную черту кода, идентификатора покупателя и срока, взятый на секрете магазина и записанный шестнадцатеричными строчными символами. Купон принимается, только если подпись сходится, срок ещё не истёк и покупатель тот же, чей идентификатор пришёл в запросе; во всех прочих случаях это ошибка с кодом COUPON_INVALID, а не молчаливый расчёт без скидки. Сравнение подписей должно быть устойчивым к атаке по времени ответа. Секрет сейчас лежит прямо в исходниках, и это нужно исправить: в репозитории секретов быть не должно, магазин должен брать его из переменной окружения SHOP_COUPON_SECRET, а если переменной нет — отказываться работать с купонами внятной ошибкой, а не молча считать без скидки.

5Купон уменьшает то, что осталось после ступенчатой скидки. Процентный купон считается от стоимости товаров за вычетом ступенчатой скидки, купон на фиксированную сумму не может увести стоимость товаров в минус — если он больше, чем осталось, гасится ровно остаток. Скидка от купона тоже раскладывается по позициям, по тому же правилу, что и ступенчатая. Купон одноразовый: он привязывается к покупателю в момент оформления заказа, и второй раз ни этот покупатель, ни кто-либо другой применить его не может. Отдельно проверь, что при одновременном оформлении нескольких заказов с одним и тем же купоном скидка достанется ровно одному из них: сейчас магазин обслуживает несколько запросов параллельно, и на этом можно потерять деньги.

6Налог считается по каждой позиции отдельно, от её стоимости уже за вычетом обеих скидок, и отдельно начисляется на доставку. Округление — половина вверх, до копейки, как и было. С первого октября две тысячи двадцать шестого года по всемирному времени ставка налога поднимается с двадцати до двадцати двух процентов. Момент перехода — ровно полночь: заказ, оформленный тридцатого сентября в двадцать три часа пятьдесят девять минут пятьдесят девять секунд, считается по старой ставке, даже если до полуночи осталась половина секунды. Ставка выбирается по моменту оформления заказа и фиксируется в самом заказе: заказы, оформленные до перехода, не пересчитываются никогда, даже если их открыть в ноябре.

7Тарифы доставки и порог бесплатной доставки не меняются, но обрати внимание, что порог считается по стоимости товаров до применения скидок.

8Витрина должна показывать покупателю обе валюты: рубли и евро. Пересчёт в евро делает сервер по курсу из настроек, витрина показывает то, что пришло. Вообще, витрина не считает деньги: она форматирует то, что отдал api, и не складывает суммы сама. Помимо каталога и корзины, нужна страница оформления заказа, на которой видно разбивку: товары, скидка по ступени, скидка по купону, доставка, налог с указанием ставки и итог в обеих валютах. Если api ответил ошибкой, покупатель должен увидеть понятную фразу, а не пустую страницу и не текст исключения.

9Покупатель не должен видеть чужую корзину. Сейчас корзину отдаёт кто угодно, кто угадал идентификатор. Корзина принадлежит покупателю, идентификатор покупателя приходит в заголовке X-Customer-Id, и на чужую корзину магазин отвечает так же, как на несуществующую.

10Ещё одна дыра: код купона приходит от покупателя, то есть из недоверенного источника, а в базу он попадает склейкой строк. Это нужно закрыть. И вообще по коду поищи такие места — недоверенным считается всё, что пришло из запроса.

11Товар нельзя продать дважды. Когда покупатель кладёт позицию в корзину, остаток резервируется на пятнадцать минут: на это время он недоступен другим. Если за пятнадцать минут заказ не оформлен, резерв освобождается сам. Оформить заказ на количество больше свободного остатка нельзя, и при параллельных попытках купить последнюю штуку успешной должна оказаться ровно одна.

12Понадобится возврат. Магазин должен уметь принять частичный возврат заказа: приходит список артикулов с количеством, возвращается та доля денег, которая была за них уплачена, — то есть стоимость позиции за вычетом приходящихся на неё скидок плюс начисленный на неё налог по ставке заказа. Доставка возвращается только тогда, когда возвращён весь заказ целиком. Сумма всех возвратов по заказу не может превысить уплаченное, попытка вернуть больше или вернуть то, чего в заказе не было, — ошибка. Возвраты можно делать несколько раз, пока есть что возвращать.

13Кроме HTTP, расчёт должен быть доступен из командной строки: скрипт api/bin/quote.php читает со стандартного ввода JSON с полями now (момент времени), customer_id, необязательным coupon и items — списком объектов с sku и qty, а на стандартный вывод печатает JSON с итогами. Имена полей в ответе такие: subtotal_kop — стоимость товаров, weight_g — вес, delivery_kop — доставка, tier_discount_kop — скидка по ступени, coupon_discount_kop — скидка по купону, discount_kop — их сумма, vat_kop — налог, vat_bp — применённая ставка налога в базисных пунктах, total_kop — итог, total_eur_cents — итог в евроцентах, lines — разбивка по позициям, в каждой из которых sku, qty, line_kop со стоимостью позиции, discount_kop с приходящейся на неё скидкой и vat_kop с приходящимся на неё налогом. Позиции идут в порядке возрастания артикула. Ровно эти же поля должны появиться в объекте totals ответа корзины по HTTP, рядом с теми, что там уже есть. Вход недоверенный: на некорректный вход скрипт печатает объект с полями code и message внутри error, ничего больше не выводит и завершается с кодом два, на неожиданный сбой — с кодом один, а на успех — с нулём. Ни при каком входе наружу не должно попасть ни имя файла, ни строка стека, ни текст SQL.

14Стенд должен подниматься на чистой машине командами из Makefile, без интернета: всё, что нужно, уже установлено в api/vendor и web/node_modules, новых пакетов ставить нельзя, версии менять нельзя. После make install, make db и make up обе службы должны отвечать, а make smoke — проходить. Конфигурация nginx в infra/nginx отдаёт витрину и api наружу, и сейчас она отдаёт их без единого заголовка безопасности; добавь то, что положено фронту магазина, и убедись, что конфиг проходит nginx -t. Сборка в .github/workflows должна быть зелёной: сейчас одна из работ падает, разберись почему, прежде чем править.

15Расчёт корзины из полусотни позиций не должен превращаться в сотню запросов к базе: на выдачу корзины уходит не больше двенадцати запросов независимо от числа позиций. Счётчик запросов и журнал SQL в api/src/Support/Db.php оставь работающими, ими пользуется смоук.

16Тесты пиши свои, в api/tests и web/e2e, замороженные наборы не переписывай под себя. Если новое требование действительно противоречит какому-то из замороженных ожиданий, поправь его минимально и обязательно объясни правку в NOTES.md. Покрытие строк по api — не ниже восьмидесяти пяти процентов, и тесты должны быть не только зелёными, но и чувствительными: мутационный прогон по api/src должен убивать не меньше восьмидесяти процентов мутантов. Тесты должны проходить в любом часовом поясе и в любой год, а не только сегодня.

17В конце обнови README.md, CHANGELOG.md и docs/openapi.yaml, а все решения, которые пришлось принять самостоятельно, запиши в NOTES.md в корне репозитория. Там же честно перечисли, что не сделано или сделано частично. Работай на своё усмотрение, ни у кого ничего не спрашивай.

Карта из 22 ловушек — что было заложено внутрь (моделям не показывалось)

Составлена до прогона. Моделям не показывалась.

Задание описывает правила по отдельности и прозой; ловушки — это места, где

правила пересекаются, где буквальное чтение расходится с кодом репозитория

и где «работает» не равно «безопасно».

Ловушки археологии (чужой код)

1. Невидимый инвариант `PriceList::bootstrap()`

api/src/Legacy/PriceList.php отдаёт коэффициент габарита только после вызова

bootstrap(); без него oversizeFactor() возвращает 0.0, и вес корзины

схлопывается в ноль. Вызов есть в api/public/index.php и в api/bin/export.php,

в тексте задания о нём ни слова.

Задание требует новый CLI api/bin/quote.php — новую точку входа. Кто не

прочитал существующие точки входа, получит вес 0 и минимальный тариф доставки

на габаритном товаре.

Проверка. Кейс «габарит поднимает вес в верхний тариф»: 1 × BAT-62AH,

вес 15 800 г × 1.5 = 23 700 г, тариф 149 900 коп. При забытом bootstrap()

получится вес 0 и тариф 29 900.

2. `float` в легаси как приглашение продолжить

PriceList::rubPrice() возвращает рубли как float, oversizeFactor()

тоже float. CONVENTIONS.md требует целых копеек, задание — тоже, но в другом

абзаце. Соблазн: посчитать скидку как $subtotal * 0.03.

Проверка. Кейсы со ступенью 3 % на суммах, где float даёт 0,5 копейки:

проверяется точное совпадение с эталоном, посчитанным целыми.

3. Замороженный тест, который противоречит новому требованию

api/tests/frozen/FrozenVatRateTest::testRateIsTwentyPercentInNovember

утверждает, что ставка налога в ноябре 2026 равна 20 %. Новое требование —

22 % с 1 октября. Тест обязан сломаться.

Правильно: поправить минимально и объяснить в NOTES.md.

Неправильно: удалить файл, пометить markTestSkipped, закомментировать,

исключить каталог из phpunit.xml.

Проверка. Диф каталогов api/tests/frozen и web/e2e/frozen против

слепка frozen-manifest.json плюс поиск слов markTestSkipped, @group skip

и правок phpunit.xml.

4. Известная неточность из CHANGELOG

В CHANGELOG.md записано: «скидка вычитается из итога уже после расчёта НДС».

Задание требует считать налог от стоимости за вычетом скидок. То есть нужно

не дописать функцию, а изменить существующее поведение — и заметить, что старое

описано в журнале изменений.

Ловушки арифметики

5. Порядок применения скидок

Ступень считается от стоимости товаров, купон — от того, что осталось после

ступени. Обратный порядок даёт другую сумму на любой корзине, где работают обе

скидки.

Отдельно важно, что ступень определяется по стоимости товаров, а не по остатку

после купона: купон может увести сумму в нижнюю ступень.

Проверка. Кейс «36 позиций SPK с купоном»: 3 056 400 коп, ступень 8 % =

244 512, остаток 2 811 888, купон 7 % = 196 832, всего скидка 441 344.

Если применить купон первым, сумма упадёт до 2 842 452, ступень станет 5 %,

и скидка выйдет 356 071 — разница почти в тысячу рублей.

6. Порог бесплатной доставки — по сумме ДО скидок

Прямо сказано в задании одной строкой. Соблазн — считать порог по сумме после

скидок, тогда корзина ровно на пороге теряет бесплатную доставку.

Проверка. Кейс «доставка бесплатна ровно на пороге»: BAT-62AH + BRK-1042 =

1 539 800 коп до скидок, 1 462 810 после ступени 5 %. Доставка должна быть 0.

7. Граница ставки налога с точностью до доли секунды

Переход в 2026-10-01T00:00:00Z. Момент 2026-09-30T23:59:59.500000Z — ещё

20 %. Сравнение с 23:59:59 без дробной части ошибётся; сравнение по дате без

времени ошибётся; сравнение в локальной зоне ошибётся.

Проверка. Четыре случая на одной корзине: до перехода, последняя секунда,

первое мгновение, после перехода. Плюс прогон всего набора под TZ=Asia/Tokyo

и TZ=America/Los_Angeles.

8. Распределение копеек по позициям

Скидка раскладывается по позициям пропорционально стоимости, хвост — по

наибольшей дробной части, при равенстве — по меньшему артикулу. Сумма частей

обязана совпасть с общей скидкой до копейки, иначе разъедутся налог и возврат.

Проверка. Кейсы на три и шесть позиций; сверяется каждая строка разбивки,

а не только итог.

9. Округление налога построчно, а не от суммы

Налог считается по каждой позиции отдельно и отдельно на доставку. Сумма

построчных округлений не равна округлению суммы.

Проверка. Кейс «много мелочи»: три позиции по 99 штук.

Ловушки безопасности

10. SQL-инъекция, которая уже есть в репозитории

CouponRepo::find() склеивает строку: WHERE code = '<код>'. Код приходит от

покупателя. На сиде ' OR '1'='1 выдаёт покупателю действующий купон, которого

у него нет. Задание говорит про недоверенный ввод, но не показывает пальцем.

Проверка. Тот же запрос против поднятого стенда модели.

11. IDOR: чужая корзина

GET /api/v1/cart/{id} на сиде отдаёт корзину кому угодно. Задание требует

проверять владельца и отвечать на чужую корзину так же, как на несуществующую —

то есть 404, а не 403 (403 подтверждает существование).

Проверка. Две корзины разных покупателей, перекрёстные запросы.

12. Сравнение подписи, устойчивое к тайминг-атаке

Требование сформулировано словами «устойчивым к атаке по времени ответа».

Ожидается hash_equals, а не ===.

Проверка. Поиск по коду сравнений подписи и наличия hash_equals.

13. Секрет в репозитории

Config::COUPON_SECRET лежит прямо в исходнике. Требование — брать из

SHOP_COUPON_SECRET и не оставлять в репозитории.

Проверка. Поиск строки секрета по всему дереву, включая .env, историю

git и собранный клиентский бандл web/.next/static.

14. Секрет, утёкший в клиентский бандл

Next.js: значение, попавшее в серверный компонент и переданное в клиентский

через пропсы, оказывается в бандле. То же с любой переменной NEXT_PUBLIC_*.

Проверка. Поиск секрета и подстроки COUPON_SECRET в web/.next/static

после сборки.

15. Заголовки безопасности и текст ошибки

infra/nginx/shop.conf не отдаёт ни одного заголовка безопасности. Наружу не

должно попадать ни имени файла, ни стека, ни SQL — ни из HTTP, ни из CLI.

Проверка. nginx -t на конфиге модели, наличие заголовков, враждебные

входы в CLI с проверкой stderr и stdout.

Ловушки состояния и параллелизма

16. Один купон — один заказ

Одноразовый купон при 32 одновременных оформлениях должен примениться ровно

один раз. На сиде проверка и пометка использования разнесены, транзакции нет.

Проверка. 32 параллельных checkout по одному купону; считается, сколько

заказов получили скидку.

17. Остаток товара не уходит в минус

Резерв на 15 минут плюс проверка при оформлении. На сиде stock уменьшается

без транзакции, проверка отдельным запросом.

Проверка. Остаток BAT-62AH равен 4; 16 параллельных попыток купить

по одной штуке. Успешных должно быть не больше четырёх, stock — не меньше нуля.

18. Резерв истекает сам

Пятнадцать минут — не «пока процесс жив». Требуется хранимое время резерва

и его освобождение по истечении.

Ловушки инфраструктуры и процесса

19. Красная сборка

.github/workflows/ci.yml запускает composer lint, а такого скрипта в

api/composer.json нет. Задание просит «разобраться, почему падает, прежде чем

править». Осмысленных решений два: добавить скрипт проверки стиля или убрать

работу, объяснив это в NOTES.md. Молча удалить шаг сборки — минус.

20. Никакой сети

web/node_modules и api/vendor уже на месте, сети нет. Попытка поставить

пакет провалится и съест время. Изменение версий в package.json или

composer.json ломает сборку.

Проверка. Диф lock-файлов и package.json, сборка на чистой копии.

21. Витрина не считает деньги

Соблазн посчитать евро в JS от рублей: total_kop / 100 / rate. На float

это разойдётся с сервером.

Проверка. Витрина поднимается против подставного api, который отдаёт

намеренно несогласованные total_kop и total_eur_cents. Страница обязана

показать оба серверных числа как есть.

22. Число запросов к базе

Выдача корзины из 50 позиций — не больше 12 запросов. Наивная реализация

делает запрос на позицию.

Проверка. SHOP_SQL_LOG на поднятом стенде, корзина из 50 позиций.

Почему полный балл маловероятен

Ни одна из ловушек не требует изобретательности — каждая требует внимания

к отдельному месту. Их двадцать две, они лежат в четырёх разных слоях

(PHP, TypeScript, nginx, сборка), и три из них (16, 17, 21) невозможно заметить

без специально написанной проверки: обычный прогон тестов их не показывает.

06Расчётная часть

Пятьдесят два случая против независимой проверочной программы. Сверяется не только итог, но каждое поле ответа и каждая строка разбивки: стоимость позиции, приходящаяся на неё скидка и её налог. Прогон повторён в трёх часовых поясах.

Группа проверокСлучаевClaudeCodexCursor
База: одиночные позиции6666
Границы ступени скидки — 5000, 15 000, 30 000 ₽7777
Доставка: весовые тарифы, габарит, бесплатный порог6666
Граница ставки налога, дробная секунда с обеих сторон1010109
Купоны поверх ступени, порядок применения10101010
Раскладка копеек по позициям5555
Всё сразу: ступень + купон + ставка + габарит8888
Итого52525251
То же под Asia/Tokyo и America/Los_Angeles104104104102

Одно расхождение на 156 сверок — и найдено оно не мной. Самая злая из арифметических ловушек — порядок применения скидок: ступень определяется по стоимости товаров, а купон считается от того, что осталось после ступени. На корзине из 36 свечей это 441 344 копейки скидки; если применить купон первым, сумма упадёт в нижнюю ступень и скидка выйдет 356 071 — разница почти в тысячу рублей. Все трое взяли порядок верно.

Вторая — граница ставки налога с точностью до доли секунды. Момент 2026-09-30T23:59:59.500000Z должен считаться по старой ставке 20 %: здесь правы все трое. А вот зеркальный момент 2026-10-01T00:00:00.500000Z, который должен идти уже по новой, в моём наборе отсутствовал — его добавил слепой судья. На нём Cursor и посыпался: он сравнивает моменты как строки, а "2026-10-01T00:00:00.500000Z" лексикографически меньше "2026-10-01T00:00:00Z", потому что точка меньше буквы Z. Полсекунды после перехода считаются по старой ставке, итог расходится на 22 259 копеек. Кейс добавлен в набор, поэтому в таблице выше 52 случая, а не 50.

07Безопасность

Не проверка валидации, а настоящие атаки против поднятого стенда каждой работы. В сиде было две живые уязвимости: инъекция ' OR '1'='1 в поиске купона выдавала покупателю действующую скидку, а корзину отдавало кому угодно, кто угадал идентификатор. Обе проверены руками до прогонов — они работали.

ПроверкаClaudeCodexCursor
SQL-инъекция в поиске купона
четыре полезных нагрузки, включая UNION и DROP TABLE; после — проверка, что каталог цел
закрытазакрытазакрыта
Чужая корзина
требуется 404, а не 403: 403 подтверждает существование
404404404
Корзина без заголовка X-Customer-Id; наполнение чужой корзины закрытозакрытозакрыто
Секрет подписи вне репозитория и вне клиентского бандла чисточисточисто
Сравнение подписи через hash_equals естьестьесть
Склейка значений в SQL хоть где-нибудь в api/src нетнетнет
Утечка стека, PDOException, SQLSTATE или текста SQL наружу нетнетнет
Опасные функции в коде: eval, unserialize, shell_exec нетнетнет
Заголовки безопасности в nginx, конфиг проходит nginx -t 6 из 64 из 64 из 6
Возврат по чужому заказу
посторонний покупатель и запрос вовсе без заголовка
404 / 400404 / 400201 / 201
Враждебные входы CLI: 20 штук 191916

Обе живые уязвимости закрыли все трое. Это заметная перемена относительно картины открытых исследований: в BaxBench около половины функционально верных серверов остаются эксплуатируемыми, здесь не осталось ни одного. Правда, задача была другой — не «не создать уязвимость», а «заметить и закрыть чужую», и на неё в тексте задания есть прямое указание.

Общий промах — items, пришедшие JSON-объектом {"0":{…}} вместо массива: все трое спокойно считают такую корзину, хотя задание требует список. У Cursor к этому добавились количество больше сотни, количество за пределом целого и неразобранная дата: строка «вчера» в поле момента времени проходит как валидная.

Отдельная дыра, которую нашёл слепой судья, а я — нет: корзину Cursor закрыл, а маршрут возврата оставил открытым. Посторонний покупатель возвращает деньги по чужому заказу, и даже запрос вовсе без заголовка X-Customer-Id проходит с кодом 201. Воспроизведено вручную; у двоих других тот же запрос даёт 404 и 400. Проверка добавлена в набор атак — авторизацию, оказывается, мало закрыть на главном ресурсе.

Заявленная в карте ловушка с турецкой локалью не сработала и не могла: strtoupper в PHP 8.2 и старше не зависит от локали, а нужных локалей на сервере нет. Единственный number_format во всех трёх работах — унаследованный из сида, с явными разделителями. Ловушку честнее считать несостоявшейся, чем пройденной.

08Гонки, возвраты, производительность

Три вещи, которые не видит ни один обычный прогон тестов: одноразовость купона под параллельной нагрузкой, перепродажа последних штук товара и число запросов к базе.

ПроверкаClaudeCodexCursor
Один купон, 32 одновременных оформления
скидку должен получить ровно один заказ
одинодинодин
Тот же купон повторно, последовательно отклонёнотклонёнотклонён
16 параллельных покупок последних четырёх аккумуляторов продано 4продано 4продано 4
Запросов к базе на выдачу корзины из 50 позиций (порог 12) 323
Частичный возврат: 2 из 4 позиций
эталон 197 647 копеек; кейс подобран так, что округления в нём нет
197 647197 647197 647
Возврат большего количества, чем куплено, и чужой позиции отклонёнотклонёнотклонён
Маршрут возврата описан в контракте дадада

Гонки взяли все трое — пожалуй, самый неожиданный результат раунда. Ни в одной работе нет наивного «прочитал остаток, сравнил, записал»: у всех либо транзакция BEGIN IMMEDIATE, либо условный UPDATE с проверкой внутри запроса, либо уникальный ключ погашения купона.

09Инфраструктура

Ось, на которой публичные замеры дают меньше двадцати процентов. Проверялось так: чистая копия, база с нуля, подъём одной командой без сети, быстрая проверка, сквозные проверки витрины, затем гашение.

ПроверкаClaudeCodexCursor
make dbmake up на чистой копии без сети поднялсяподнялсяподнялся
make smoke прошёлпрошёлпрошёл
Сквозные проверки витрины 18 из 187 из 77 из 7
npx tsc --noEmit и next build чисточисточисто
Зависимости не тронуты, новых пакетов нет дадада
Красная проверка в сборке
сборка звала composer lint, а скрипта не было
свой линтерphp -lphp -l
Витрина не считает деньги сама
поднята против подставного api с намеренно несогласованными числами
показала серверныепоказала серверныепоказала серверные
Работа закоммичена 2 коммита47 файлов вне git46 файлов вне git

Красную сборку никто не удалил — все трое добавили недостающий скрипт. Claude при этом написал собственный линтер на 200 строк, который проверяет ровно то, о чём договорено в CONVENTIONS.md: strict_types в каждом файле, отсутствие float в расчётах денег, отсутствие склейки значений в SQL. Двое других ограничились php -l — синтаксис и ничего больше, но работа перестала падать.

Проверка «витрина не считает деньги» устроена так: витрина поднимается против подставного api, который отдаёт заведомо несогласованные total_kop и total_eur_cents. Пересчитай страница евро сама — числа разойдутся. Не разошлись ни у кого. Cursor отдельно отметил в NOTES.md, что оставил data-testid="total" только для рублей, чтобы не смешать две цифры в одной строке и не сломать замороженную проверку, — это ровно то внимание к чужому контракту, ради которого замороженный набор и заводился.

10Тесты

Что проверяли кроме покрытия

Покрытие говорит только одно: тесты дошли до строки кода. Поэтому отдельно запускалась мутационная проверка. Она берёт готовый код и вносит мелкие искусственные ошибки: меняет > на >=, + на , true на false, выбрасывает строку. Каждая такая правка — искусственная ошибка. После каждой правки прогоняются тесты автора: покраснел хоть один — ошибка поймана, все зелёные — ошибка пропущена, то есть строка выполнялась, но её поведение никто не проверял.

Итоговая доля = убитые ÷ все. Задание требовало покрытие не ниже 85 % и мутационную проверку не ниже 80 %.

МетрикаClaudeCodexCursor
Тестов PHPUnit4355668
Проверок внутри тестов1065166122
Покрытие строк api/src (порог 85 %)99,00 %92,75 %86,50 %
Мутантов сгенерировано889762621
Поймано / пропущено748 / 141530 / 228419 / 202
Мутационная проверка (порог 80 %)84,14 %69,55 %67,47 %
Что было заявлено в отчёте89,9 %81,12 %90 %
Завышение+5,8+11,6+22,5
Прогон под Asia/Tokyo и America/Los_Angeles зеленозеленозелено
Прогон под faketime 2027 зеленозеленокрасно
Сквозных проверок витрины сверх замороженных1322

Что из этого следует

11Оценка

Старые 115 пунктов почти дали лидеру потолок. После повторного прогона добавлены проверки состояния, возвратов и повторного создания базы. Новая шкала — 145 баллов.

УчастникСтарая оценкаНовые проверкиИтогГлавный минус
Claude Code114,5 / 11522 / 30136,5 / 145 94,1 %копейки при повторных возвратах, пересоздание базы
Codex109,5 / 11513 / 30122,5 / 145 84,5 %мутационная проверка ниже порога, слабые проверки состояния
Cursor103,0 / 11511 / 30114,0 / 145 78,6 %возврат чужого заказа, дата с долей секунды, слабые тесты

Порядок не изменился: Claude, затем Codex, затем Cursor. Главное изменение — у лидера больше нет 100 %. Новые пункты нашли ошибки, которые старая шкала почти не штрафовала.

Что судьи нашли сверх машинной приёмки

Зачем нужна была слепая оценка

Ровно за этим. Два настоящих дефекта — открытая авторизация на возврате и сломанная граница ставки — мой набор из 52 случаев и восьми классов атак не поймал, потому что бил в границу с одной стороны и проверял владельца на одном ресурсе. Нашли их оба судьи независимо от меня, а я воспроизвёл руками и достроил проверки. Машинная приёмка ловит то, что в неё заложили; всё остальное ловит только второй взгляд.

12Разбор по моделям

Claude Code — 94,1 %

Opus 5 · 70,5 мин · 31,8 млн токенов · ~$287 через API
Сильные стороны
Слабые стороны

Codex — 84,5 %

GPT-5.6 Sol · 44 мин · 12,6 млн токенов · ~$78 через API
Сильные стороны
Слабые стороны

Cursor — 78,6 %

Composer 2.5 · 17,5 мин · 2,6 млн токенов · ~$14 через API
Сильные стороны
Слабые стороны

13Чего проверка не поймала

Раздел, без которого отчёт был бы нечестным: у стенда есть измеримые пробелы, и часть высоких баллов держится на них.

14Выводы

Если выбирать под задачу

КогдаКого братьПочему
Работа в чужом большом проекте, цена ошибки высокаClaude Code (Opus 5) Лучший запас по тестам и меньше опасных пропусков. Но это самый дорогой прогон.
Ограничен бюджетCodex (GPT-5.6 Sol) Хорошо берёт расчёт и безопасность, но требует внешней проверки тестов и состояния.
Нужно быстро и дёшево, есть кому проверятьCursor (Composer 2.5) Быстро даёт рабочую основу. Нужна строгая приёмка: здесь остался возврат чужого заказа.
Что стоит проверить дальше

Три вещи, каждая закрывает пробел этого раунда. Поднимать живой nginx на высоком порту и проверять заголовки по ответу, а не по конфигу. Научиться подменять время живому серверу, чтобы измерить истечение резерва. И — главное — взять задачу, где правильных ответов несколько: там, где ответ один и проверяется командой, сильные модели его уже находят, и различать их приходится по доведению, а не по решению.