Одна задача, три агента, каждому — сколько времени понадобится. ТЗ написано прозой, требования в нём нужно извлечь. Приёмка ручная, плюс две слепые оценки судьями, не знавшими авторства.
Баллы — среднее трёх независимых оценок по рубрике на 50 пунктов: ручная приёмка оператора и две слепые оценки судьями, не знавшими авторства. Все три оценщика выстроили агентов в одном порядке.
Claude Code получил 50 из 50 у всех трёх оценщиков независимо. Это единственная работа, к которой не нашлось претензий по рубрике: верна арифметика до копейки, взяты обе граничные ловушки, написана документация, и тесты не просто зелёные, а устойчивы к мутациям.
Отдельно проверялась безопасность: 17 враждебных входов вручную плюс
3 дифференциальные проверки против независимого оракула, всего 20 пунктов. Первые 17 прошли все
трое; на трёх последних Cursor провалился целиком — принимает недопустимый sku,
принимает items объектом вместо массива и отвергает валидный большой вход.
Перед прогоном проверено, что у каждого агента стоит последняя доступная модель. Одно обновление потребовалось.
| Агент | Модель на прогоне | Статус |
|---|---|---|
| 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 |
| Что фиксировали | Как именно |
|---|---|
| Время | Не ограничено. Каждый агент работал до тех пор, пока сам не сообщил о завершении: 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"), и только после этого
выводы о границе стали воспроизводимыми. Цифры в отчёте — из повторного, строгого прогона.
Ровно то, что получил каждый агент, — целиком, дословно, без единой правки. Отправлялось один раз, вместе с пустым репозиторием и больше ничем. Нумерация абзацев добавлена здесь для ссылок в разборе; в самом задании её нет — это сплошная проза, из которой требования нужно извлечь.
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, а не запрет больших чисел |
19 кейсов с эталонными числами, посчитанными вручную по тексту задания до открытия кода. Все суммы в копейках. Ниже — самые показательные.
| Кейс | эталон | Claude | Codex | Cursor |
|---|---|---|---|---|
| Поштучное округление аксессуар 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, который проверяет реализацию,
а не требование.
Задание объявляет вход недоверенным. 17 враждебных входов поданы каждому решению вручную; проверялись код возврата, пустота stdout и отсутствие утечки внутренностей в stderr.
| Проверка | Claude | Codex | Cursor |
|---|---|---|---|
| stdin больше 1 МиБ отвергается до декодирования | код 2 | код 2 | код 2 |
| Глубина JSON больше 32 | код 2 | код 2 | код 2 |
| Больше 1 000 позиций до схлопывания | код 2 | код 2 | код 2 |
| Лишние поля в корне и в позиции | код 2 | код 2 | код 2 |
Ложные типы чисел: 1e3, 1.0, "1000", true, за пределом int | 5 из 5 | 5 из 5 | 5 из 5 |
| Некорректный UTF-8, 257 байт, управляющий символ | 3 из 3 | 3 из 3 | 3 из 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, тогда как двое других — ни одной.
Количество тестов ничего не говорит о том, ловят ли они дефекты. Поэтому кроме покрытия строк замерялась мутационная устойчивость.
MSI — Mutation Score Indicator, показатель мутационного покрытия. Infection берёт готовый
код и вносит в него мелкие искусственные ошибки: меняет > на >=,
+ на −, true на false, выбрасывает строку.
Каждая такая правка — «мутант». После каждой мутации прогоняются тесты автора. Если хоть один
тест покраснел, мутант убит — тесты заметили подмену. Если все остались зелёными, мутант
выжил: строка кода выполнялась, но её поведение никто не проверял.
MSI = убитые мутанты ÷ все мутанты. Покрытие строк отвечает на вопрос «этот код вообще запускался?», MSI — на вопрос «а если он начнёт врать, тесты это заметят?». Разница видна на Codex: покрытие 97,7 % при MSI 78,9 % означает, что код исполняется почти весь, но 68 внесённых ошибок его тесты пропустили.
| Метрика | Claude | Codex | Cursor |
|---|---|---|---|
| Тест-методов / тестов PHPUnit | 68 / 189 | 26 / 36 | 58 / 58 |
| Проверок (assertions) | 50 114 | 77 | 136 |
| Покрытие строк | 95,24 % | 97,70 % | 92,40 % |
| Мутантов сгенерировано | 366 | 322 | 298 |
| Убито / выжило | 310 / 53 | 254 / 68 | 238 / 59 |
| MSI (мутационное покрытие) | 85,52 % | 78,88 % | 80,20 % |
| Сценариев безопасности покрыто тестами | 6 из 6 | 5 из 6 | 4 из 6 |
Тест на внутреннюю ошибку CLI (INTERNAL_ERROR, код 1) | есть | нет | нет |
| Генеративные (fuzz) тесты инвариантов | есть | нет | нет |
Три независимые оценки каждой работы: ручная приёмка оператора и двое слепых судей. Ранжирование совпало полностью — включая случаи, когда судья, сам того не зная, оценивал работу собственного CLI.
| Судья | Claude | Codex | Cursor | Порядок |
|---|---|---|---|---|
| Ручная приёмка (оператор) | 50,0 | 47,5 | 44,5 | Claude → Codex → Cursor |
| Судья Claude Code (Opus 5) | 50,0 | 45,5 | 43,5 | Claude → Codex → Cursor |
| Судья Codex (GPT-5.6 Sol) | 50,0 | 45,5 | 43,5 | Claude → Codex → Cursor |
| Среднее из 50 | 50,00 100 % | 46,17 92,3 % | 43,83 87,7 % | полное совпадение |
items как JSON-объект, а не только как массив,
и sku со строчными буквами. Обе находки воспроизведены и учтены в оценке.
calc_mismatches
и security_failures у обоих пусты.
Throwable с проверкой кода INTERNAL_ERROR и возврата 1: остальные это реализовали, но не проверили.23:59:59.000000, и заказ, оформленный в 23:59:59,5, считается просроченным.Throwable: обработчик написан, но не проверен.sku в нижнем регистре, приём items как JSON-объекта вместо массива, отказ на валидном большом входе.| Когда | Кого брать | Почему |
|---|---|---|
| Сложное ТЗ, цена ошибки высока | 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 %.