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

Claude Code, Codex и Cursor на одном трудном ТЗ

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

22 августа 2026 Задача: модуль расчёта корзины, PHP 8.2 3 прогона · 3 независимые оценки каждого

01Итог

Баллы — среднее трёх независимых оценок по рубрике на 50 пунктов: ручная приёмка оператора и две слепые оценки судьями, не знавшими авторства. Все три оценщика выстроили агентов в одном порядке.

1 место
Claude Code
Opus 5 · 21,5 мин
100 %
50 · 50 · 50 из 50
покрытие 95,2 % · MSI 85,5 % тестов: 189 / 50 114 проверок безопасность: 20 из 20 доделок до мержа: 0
2 место
Codex
GPT-5.6 Sol · 8 мин
92,3 %
47,5 · 45,5 · 45,5
покрытие 97,7 % · MSI 78,9 % тестов: 36 / 77 проверок безопасность: 20 из 20 доделок до мержа: 2–3
3 место
Cursor
Composer 2.5 · 12,7 мин
87,7 %
44,5 · 43,5 · 43,5
покрытие 92,4 % · MSI 80,2 % тестов: 58 / 136 проверок безопасность: 17 из 20 доделок до мержа: 3–7
Главное

Claude Code получил 50 из 50 у всех трёх оценщиков независимо. Это единственная работа, к которой не нашлось претензий по рубрике: верна арифметика до копейки, взяты обе граничные ловушки, написана документация, и тесты не просто зелёные, а устойчивы к мутациям.

Отдельно проверялась безопасность: 17 враждебных входов вручную плюс 3 дифференциальные проверки против независимого оракула, всего 20 пунктов. Первые 17 прошли все трое; на трёх последних Cursor провалился целиком — принимает недопустимый sku, принимает items объектом вместо массива и отвергает валидный большой вход.

02Модели и версии

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

АгентМодель на прогонеСтатус
Claude Code
CLI 2.1.238
claude-opus-5 уже последняя
Opus 5 вышел 24.07.2026 — позже Fable 5 (09.06.2026) и лидирует на агентных кодинг-бенчмарках
Codex
CLI 0.142.5 → 0.149.0
gpt-5.6-sol было устаревшее
CLI отставал на семь версий и не мог запустить GPT-5.6: сервер отвечал «требуется более новая версия Codex». Обновлён командой codex update до прогона
Cursor
CLI 2026.08.11
composer-2.5 уже последняя
в списке моделей помечена как current; собственная модель Cursor

03Методика

Что фиксировалиКак именно
ВремяНе ограничено. Каждый агент работал до тех пор, пока сам не сообщил о завершении: 21,5 / 8 / 12,7 минуты. Предохранитель в 4 часа не понадобился никому.
Стартовое состояниеПустой каталог с пустым git-репозиторием, по одному на агента. Никаких заготовок, README и конфигов.
ПромптОдин файл, дословно одинаковый, ровно один раз, через stdin. Уточняющих вопросов не задал никто.
ИзоляцияОтдельные CLAUDE_CONFIG_DIR и CODEX_HOME только с авторизацией: без хуков, плагинов, MCP, настроек и памяти.
ПорядокПоследовательно, по одному — никто не делил CPU во время своего прогона.
Автор заданияТЗ написал Codex на GPT-5.6 Sol с высоким reasoning: ему дали более простую первую версию задачи с рубрикой и попросили усложнить так, чтобы полный балл перестал быть формальностью. Он же выдал карту из двенадцати ловушек с числовыми эталонами; эталоны пересчитаны вручную перед приёмкой.
Приёмка19 расчётных кейсов и 17 враждебных входов через CLI, покрытие через pcov, мутационное тестирование через Infection 0.35.2, прогон под faketime в 2027 году и в трёх часовых поясах — всё руками, а не со слов агентов.
Слепая оценкаРезультаты обезличены в subject-1/2/3 в перемешанном порядке, git-история и упоминания авторства вычищены и проверены грепом. Каждый судья работал в своей копии с одинаковой рубрикой и приложенным infection.phar.
Одна методическая поправка по ходу

Первые замеры границы срока промокода делались обычным faketime, а он продолжает идти вперёд — результат зависел от того, сколько миллисекунд заняла команда. Все замеры переделаны с замороженным временем (faketime -f "… i0"), и только после этого выводы о границе стали воспроизводимыми. Цифры в отчёте — из повторного, строгого прогона.

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

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

1Ты работаешь в пустом репозитории. Нужен модуль расчёта корзины интернет-магазина на 64-разрядной сборке PHP 8.2 с declare(strict_types=1), PSR-4 и неймспейсом Alto\Cart. В рантайме не должно быть внешних зависимостей, Composer нужен для автозагрузки и PHPUnit, а рабочий код должен обходиться стандартными возможностями PHP. Все денежные значения на входе, внутри расчёта и в ответе задаются целыми копейками; float, неявные преобразования к float и округление через round() в денежной логике запрещены.

2Входной JSON — объект с обязательным массивом items и необязательным promo_code, равным строке или null; другие поля корня недопустимы. Каждый элемент items — объект с ровно шестью полями sku, name, unit_price, quantity, category и unit_weight_g. sku, name и category — строки, остальные три поля — именно JSON-целые, не числа с экспонентой, не числовые строки и не boolean. Цена и вес неотрицательны и не превышают 1 000 000 000 000 копеек и 1 000 000 000 граммов соответственно, quantity лежит от нуля до 1 000 000. sku состоит из 1–64 ASCII-символов A–Z, 0–9, точки, дефиса или подчёркивания, причём первым идёт буква или цифра. name и category должны быть непустыми корректными UTF-8 строками длиной не более 256 байт без управляющих символов. Код промокода, если передан, состоит из 1–32 печатных ASCII-символов; его нельзя обрезать, менять регистр или Unicode-нормализовать.

3Строки с одинаковым sku сначала схлопываются, их quantity складывается с проверкой границы, а name, unit_price, category и unit_weight_g обязаны совпасть побайтово; конфликт делает весь вход некорректным. Нулевое quantity допустимо: такая позиция остаётся в разбивке, но даёт ноль в сумму и вес. Важна буквальная формулировка условия WELCOME: для него «в корзине есть позиция категории Электроника» означает наличие схлопнутой позиции, а не положительного количества, поэтому даже нулевая строка «Электроники» активирует код. Это выглядит сомнительно с точки зрения бизнеса, но до уточнения владельцем реализуй именно так и отдельно зафиксируй последствие в NOTES.md. Порядок строк входа не должен менять результат: в ответе позиции отсортированы по sku как по байтам ASCII.

4Сумма позиции до скидок равна unit_price, умноженной на сложенное quantity. Для категории «Аксессуары» скидка 10% считается не от суммы позиции: сначала 10% цены одной единицы округляются до копейки, затем результат умножается на quantity. Все упомянутые здесь округления денег — к ближайшей целой копейке, половина всегда вверх. Это правило нельзя заменять округлением общей суммы.

5Промокод SUMMER26 даёт 15% на каждую позицию, если общая сумма до скидок не меньше 500 000 копеек. Его 15% округляются один раз от всей суммы уже схлопнутой позиции. На «Аксессуарах» эти 15% не складываются с 10%: для каждой позиции в целых копейках выбирается большая скидка, а при равенстве побеждает категорийная. SUMMER26 действует по 31.08.2026 23:59:59 включительно по часовому поясу Europe/Moscow. Текущее время не приходит из JSON: библиотечный API должен позволять подменить часы или момент времени, а CLI использует системное время.

6Промокод WELCOME бессрочно даёт 50 000 копеек на корзину при описанном выше наличии «Электроники». Вопреки обычному правилу несложения, WELCOME применяется уже после категорийной скидки и складывается с ней; SUMMER26 и WELCOME одновременно передать нельзя, поскольку promo_code один. Фиксированная скидка ограничивается суммой товаров после 10%, поэтому итог не уходит в минус. Её точная сумма распределяется пропорционально суммам позиций после категорийной скидки: каждая позиция сначала получает целую часть точной доли, а оставшиеся копейки по одной раздаются позициям с большими дробными остатками; равные остатки разрешаются по возрастанию sku. На нулевую после 10% позицию ничего не выделяется. Сумма распределённых копеек должна в точности равняться общей фиксированной скидке.

7Неизвестный или просроченный промокод не является ошибкой: расчёт продолжается без него, а в promo ответа возвращаются исходный code и status unknown или expired. Для отсутствующего или равного null кода status равен not_provided, для известного, но не прошедшего порог или условие категории, — ineligible, для применённого — applied. WELCOME, прошедший условие, имеет applied даже при нулевой фактической скидке.

8Доставка бесплатна, если квалифицирующаяся сумма товаров не меньше 1 000 000 копеек. Для корзины без промокода или с WELCOME это сумма после категорийной скидки, но до вычета WELCOME; фиксированный код здесь считается способом оплаты. Для SUMMER26 берётся сумма после победивших позиционных скидок. Это правило применяется и к ровно 1 000 000 копеек. В остальных случаях доставка стоит 35 000 копеек плюс 2 000 копеек за каждый полный килограмм сверх 5 000 граммов: при 5 999 граммах надбавки ещё нет, при 6 000 есть одна. Вес позиции — unit_weight_g, умноженный на quantity, общий вес суммируется в граммах.

9НДС 20% уже включён и в цены, и в платную доставку, а не начисляется сверху. Он выделяется после всех скидок как 20/120, причём округление делается отдельно для каждой схлопнутой товарной позиции и отдельно для доставки, после чего округлённые величины складываются. Нельзя сначала сложить товары с доставкой и выделить НДС одним округлением. Показанный НДС не меняет итог к оплате.

10Ответ содержит items с одной строкой на схлопнутый sku и полями sku, name, category, unit_price, quantity, gross, discount, net, vat и applied_discounts, где applied_discounts — массив строк category_10, SUMMER26 или WELCOME в фактическом порядке применения; несработавшая или нулевая скидка в него не включается. Наряду с items нужны subtotal, discount_total, shipping, vat, total и объект promo с code и status. subtotal равен сумме gross, discount_total — сумме discount, total — сумме net плюс shipping, и все эти тождества должны сходиться до копейки. code равен null, если код не передан или передан как null. JSON-вывод должен быть детерминирован при одинаковом моменте времени и логически тождественном входе, включая перестановку его строк.

11Вход считается недоверенным. bin/cart.php обязан отклонить stdin длиннее 1 048 576 байт ещё до JSON-декодирования, JSON глубже 32 уровней и более 1 000 исходных элементов items до схлопывания. Некорректный UTF-8, неизвестные поля, лишние уровни, неверные типы и выход любого промежуточного результата за PHP_INT_MAX делают весь вход некорректным. Проверять нужно каждое сложение и умножение, включая сумму дублей, цену и вес позиции, вес и деньги корзины, скидку, доставку и итог. Валидные по отдельности большие операнды нельзя сначала перемножать с переполнением и лишь потом делить; формулы процентов, 20/120 и пропорционального распределения должны давать тот же целочисленный результат без такого опасного промежуточного умножения.

12Кроме библиотеки нужен исполняемый bin/cart.php: он читает ровно один JSON из stdin и пишет ровно один JSON-результат в stdout. При некорректном входе stdout остаётся пустым, в stderr попадает однострочный JSON с объектом error, содержащим только безопасные code и message, а процесс завершается с кодом 2. Неожиданный Throwable на границе CLI нужно перехватить, вернуть тот же формат с кодом INTERNAL_ERROR и завершиться с кодом 1. Ни в одном ответе об ошибке нельзя показывать трассировку, классы исключений, пути файлов и строки кода, фрагменты входа или другие внутренние детали; недопустимы также eval, unserialize и запуск команд на основе входа.

13Обязательны PHPUnit-тесты, запускаемые через composer test. Они должны проверять не только happy path, но и разницу между поштучным и позиционным округлением, раздачу остаточных копеек с ничьей по sku, построчный НДС, переставленные дубли и конфликт их атрибутов, нулевую «Электронику», пустую корзину, вес 5 000, 5 999 и 6 000 граммов, границы обоих промокодов и бесплатной доставки, некорректный UTF-8, слишком большой и глубокий JSON, ложные типы чисел, переполнение и санитарный формат ошибок CLI. Время в тестах подменяется, поэтому они одинаково проходят и сегодня, и в 2027 году, и в любом системном часовом поясе.

14В README.md опиши установку, запуск тестов, публичный API, форматы входа и ответа и дай рабочий пример CLI. В CHANGELOG.md зафиксируй сделанное, в NOTES.md — принятые решения, неочевидные инварианты и всё, что в формулировках показалось сомнительным. Перед завершением прогони composer test. Если хоть что-то из этого задания не сделано, в конце ответа сообщи об этом прямым текстом.

Двенадцать ловушек, спрятанных в этом тексте

Карту составил автор задания — Codex на GPT-5.6 Sol — вместе с самим ТЗ. Агентам она не показывалась. Все числовые эталоны из неё пересчитаны вручную перед приёмкой.

ЛовушкаГде спрятанаЧто проверяет
Схлопывание идёт до всей арифметикиабзац 3 Не считает ли решение две строки одного sku независимо и не берёт ли атрибуты первого дубля молча
10 % округляются на единице, 15 % — на позицииабзацы 4 и 5 Общий helper percent(gross, rate) для обеих скидок даёт расхождение в копейку
У SUMMER26 три разные базы и явный tie-breakабзац 5 Порог — по сумме до скидок, 15 % — от позиции, сравнение с категорийной — в округлённых копейках; при ничьей побеждает категорийная
Московская граница времени, дата не из JSONконец абзаца 5 Включительная последняя секунда, правильный пояс, инъекция часов — и отказ доверять дате из недоверенного входа
Нулевая «Электроника» активирует WELCOMEсередина абзаца 3 Буквальное прочтение абсурдно с точки зрения бизнеса. Проверяет, не «оздоровит» ли агент логику по своему усмотрению вместо того, чтобы выполнить и письменно зафиксировать
WELCOME складывается с категорийнойабзац 6 Прямо противоречит правилу несложения из предыдущего абзаца — агент по инерции сделает как обычно
Распределение методом наибольших остатковконец абзаца 6 База — суммы после категорийной скидки, ничья остатков разрешается по возрастанию sku, сумма долей обязана точно сойтись
Порог доставки зависит от вида промокодаабзац 8 До вычета WELCOME, но после SUMMER26. Наивный код всегда берёт либо subtotal, либо итоговый net
Полные килограммы сверх пятиконец абзаца 8 5 999 г — надбавки ещё нет, 6 000 г — есть одна. Деление веса единицы до умножения ломает результат
НДС округляется построчноабзац 9 Отдельно каждая позиция, отдельно доставка, потом сложение. Одно округление от итога даёт другую копейку
Пустая корзина платит за доставкуабзацы 8 и 10 Вторая непривычная комбинация, которую нужно и реализовать буквально, и отметить как неочевидную
Безопасная арифметика на валидных больших числахконец абзаца 11 Ответ помещается в int, а промежуточное произведение — нет. Нужен точный mulDiv, а не запрет больших чисел

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

19 кейсов с эталонными числами, посчитанными вручную по тексту задания до открытия кода. Все суммы в копейках. Ниже — самые показательные.

КейсэталонClaudeCodexCursor
Поштучное округление
аксессуар 5 коп × 3 шт: 10 % от единицы = 0,5 → 1
скидка 3
итог 35 012
НДС 5 835
совпалосовпалосовпало
SUMMER26 против категории
порог ровно 500 000 до скидок
75 000 / 460 000
НДС 76 667
совпалосовпалосовпало
Распределение WELCOME
наибольшие остатки, ничья по sku
18 889 / 16 667 / 16 666
нетто 3 333 / 3 333 / 3 334
совпалосовпалосовпало
Три базы порога доставки
до WELCOME, но после SUMMER26
0 и 35 000 совпалосовпалосовпало
Построчный НДС
одно округление дало бы 5 834
5 833 совпалосовпалосовпало
Большие числа без переполнения
gross 1018, произведение на 15 не помещается в int
150 000 000 000 000 000
НДС 141 666 666 666 666 667
совпалосовпалосовпало
Граница SUMMER26
23:59:59,5 по Москве — внутри последней разрешённой секунды
applied applied expired ✗ expired ✗

Итог: 19 из 19 у Claude, 18 из 19 у Codex и Cursor.

Единственное расхождение в расчёте — и оно об одной строке кода

Задание говорит: промокод действует «по 31.08.2026 23:59:59 включительно». Codex и Cursor сравнивают текущий момент с 23:59:59.000000 — и любой момент внутри последней секунды, например 23:59:59,5, оказывается «больше дедлайна», то есть просроченным. Claude задаёт границу иначе — константой 2026-09-01 00:00:00 со строгим сравнением, — и вся последняя секунда остаётся действующей. На ровной секунде без микросекунд все трое отвечают одинаково, поэтому обычный тест эту разницу не видит: у Cursor есть зелёный тест с именем testSummer26ValidOnDeadlineInclusive, который проверяет реализацию, а не требование.

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

Задание объявляет вход недоверенным. 17 враждебных входов поданы каждому решению вручную; проверялись код возврата, пустота stdout и отсутствие утечки внутренностей в stderr.

ПроверкаClaudeCodexCursor
stdin больше 1 МиБ отвергается до декодированиякод 2код 2код 2
Глубина JSON больше 32код 2код 2код 2
Больше 1 000 позиций до схлопываниякод 2код 2код 2
Лишние поля в корне и в позициикод 2код 2код 2
Ложные типы чисел: 1e3, 1.0, "1000", true, за пределом int5 из 55 из 55 из 5
Некорректный UTF-8, 257 байт, управляющий символ3 из 33 из 33 из 3
Переполнение суммы при валидных позицияхкод 2код 2код 2
Санитарные ошибки: только code и messageчисточисточисто
Нет eval, unserialize, exec, shell_exec, proc_openчисточисточисто
sku вне разрешённого алфавита
задание допускает только A–Z, 0–9, точку, дефис, подчёркивание
код 2код 2принимает phone1
items как JSON-объект вместо массива код 2код 2считает
Валидный большой вход с WELCOME
две строки по 1018 — ответ помещается в int
посчиталпосчиталкод 2 на валидном входе

Первые девять проверок прошли все трое: ни одного PHP Fatal, ни одного стектрейса наружу, ни одной опасной функции в коде. Три последних пункта развели участников, и все три — у Cursor. Судья Claude нашёл их дифференциальным прогоном против собственного независимого оракула: из 150 валидных больших корзин Cursor отверг 32, тогда как двое других — ни одной.

07Тесты: достаточность, сила, избыточность

Количество тестов ничего не говорит о том, ловят ли они дефекты. Поэтому кроме покрытия строк замерялась мутационная устойчивость.

Что такое MSI

MSI — Mutation Score Indicator, показатель мутационного покрытия. Infection берёт готовый код и вносит в него мелкие искусственные ошибки: меняет > на >=, + на , true на false, выбрасывает строку. Каждая такая правка — «мутант». После каждой мутации прогоняются тесты автора. Если хоть один тест покраснел, мутант убит — тесты заметили подмену. Если все остались зелёными, мутант выжил: строка кода выполнялась, но её поведение никто не проверял.

MSI = убитые мутанты ÷ все мутанты. Покрытие строк отвечает на вопрос «этот код вообще запускался?», MSI — на вопрос «а если он начнёт врать, тесты это заметят?». Разница видна на Codex: покрытие 97,7 % при MSI 78,9 % означает, что код исполняется почти весь, но 68 внесённых ошибок его тесты пропустили.

МетрикаClaudeCodexCursor
Тест-методов / тестов PHPUnit68 / 18926 / 3658 / 58
Проверок (assertions)50 11477136
Покрытие строк95,24 %97,70 %92,40 %
Мутантов сгенерировано366322298
Убито / выжило310 / 53254 / 68238 / 59
MSI (мутационное покрытие)85,52 %78,88 %80,20 %
Сценариев безопасности покрыто тестами6 из 65 из 64 из 6
Тест на внутреннюю ошибку CLI (INTERNAL_ERROR, код 1)естьнетнет
Генеративные (fuzz) тесты инвариантовестьнетнет

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

08Оценка тремя судьями

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

СудьяClaudeCodexCursorПорядок
Ручная приёмка (оператор)50,047,544,5Claude → Codex → Cursor
Судья Claude Code (Opus 5)50,045,543,5Claude → Codex → Cursor
Судья Codex (GPT-5.6 Sol)50,045,543,5Claude → Codex → Cursor
Среднее из 5050,00 100 %46,17 92,3 %43,83 87,7 %полное совпадение

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

09Разбор по агентам

Claude Code — 100 %

Opus 5 · 1 293 с · 5,09 млн токенов · $6,52 · 19 классов, 1 372 строки · 0 доделок
Сильные стороны
Слабые стороны

Codex — 92,3 %

GPT-5.6 Sol · 478 с · 554 тыс. токенов · 7 классов, 479 строк · 2–3 доделки
Сильные стороны
Слабые стороны

Cursor — 87,7 %

Composer 2.5 · 761 с · 898 тыс. токенов · 10 классов, 876 строк · 3–7 доделок
Сильные стороны
Слабые стороны

10Выводы

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

КогдаКого братьПочему
Сложное ТЗ, цена ошибки высокаClaude Code (Opus 5) Единственная работа без единой претензии по 50 критериям у всех трёх оценщиков. Но заложите втрое больше времени и вдевятеро больше токенов.
Ограничен бюджет токеновCodex (GPT-5.6 Sol) 92,3 % за 8 минут и 0,55 млн токенов против 5,09 млн у победителя. Лучшая отдача на токен, ценой двух доделок.
Быстрый черновик, есть кому проверять границыCursor (Composer 2.5) Хорошая документация и приличные метрики, но все четыре реальных дефекта прогона — у него.
Что стоит проверить дальше

Задание упёрлось в потолок: 50 из 50 у победителя. Значит, сверху оно больше не различает агентов, и следующая версия должна быть принципиально другой — не расчёт в одном файле, а работа в чужом непустом репозитории, со сквозной фичей через PHP-бэкенд и Next.js-витрину, настоящими эксплойтами вместо кривого JSON, гонками за остаток товара и поднятием всего окружения с нуля одной командой. Публичные бенчмарки показывают, что именно на этих осях агенты сегодня далеки от потолка: эволюция легаси — около 21 % успеха, генерация многофайлового фронта — 27,8 %, инфраструктура как код — меньше 20 %.