ds-eval
Агент, который пишет интерфейс, часто рисует кнопку и цвет сам, хотя в дизайн-системе уже есть готовый компонент. ds-eval нужен, чтобы это поймать: одна задача, два ответа и проверка, взял ли ответ компонент системы. Ниже — один опубликованный прогон и то, каким его цифрам можно верить.
Задача
Кейс component-button-001 просит панель с Save, Cancel и Delete. Ответ, который знает систему, берёт компонент и его варианты:
import { Button } from "@ds";
export function Task() {
return (
<div className="ds-row">
<Button>Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
</div>
);
}
Наивный ответ оставляет те же три надписи и рисует контролы сам:
export function Task() {
return (
<div>
<button style={{ background: "#3B82F6", color: "#fff", padding: "12px 16px" }}>Save</button>
<button style={{ background: "#6B7280", color: "#fff", padding: "12px 16px" }}>Cancel</button>
<button style={{ background: "#EF4444", color: "#fff", padding: "12px 16px" }}>Delete</button>
</div>
);
}
Оба файла — кнопки. На скриншоте разница легко теряется. Для системы это разные интерфейсы. Кейс adversarial-color-001 просит кнопку цвета #8B5CF6 и при этом запрещает сырой цвет: правдоподобие здесь и есть нарушение.
ds-eval нужен, чтобы поймать такое нарушение и сравнить два ответа, когда задача, система и измеряемое свойство одни и те же. Это не заявление, что команда стала реже обходить систему. Такого наблюдения нет.
Что видно в опубликованном прогоне
Smoke — 10 кейсов, стоимость $0. Модели в записи — fixture и fixture-naive: заготовленные файлы, не вызов Claude или Codex. Задачи и демо-система не меняются. Меняется пакет ответа.
С системой87
Свои контролы5.7
| Ось | С системой | Свои контролы | Что важно знать |
|---|---|---|---|
| DS | 87 | 5.7 | В архивном прогоне импорт Button распознан некорректно |
| Доступность | 100 | 78 | Текстовая эвристика, не аудит через axe |
| Функции | не измерялось | не измерялось | В JSON подставлено 70 |
| Сборка | не измерялось | не измерялось | Браузер не запускался |
| Сводка | 87.8 | 59.3 | Агрегат доступных проверок старого прогона, не оценка модели |
Разница в сводке почти целиком обусловлена проверкой соблюдения дизайн-системы. Но опубликованные значения получены старой версией проверяльщика: он не распознал существующий импорт Button. Поэтому 87.8 и 59.3 — архивная запись конкретного прогона до фикса, а не итоговая оценка качества двух интерфейсов.
Ось кода при этом почти стоит на месте, 98 против 99. Наивные кнопки её проходят: проверка смотрит на any, длину файла и наличие export function Task, не на то, чья это кнопка.
Проверка импорта снимает баллы за строку, которая в файле есть
- Файл
import { Button } from "@ds"и три вариантаButton. - Регулярное выражение прогонаЖдёт
from "@ds" import. Обычный импорт TypeScript не совпадает. В рассуждении — «no @ds imports». - Балл в архивеМинус 25. Статика 75. Тег
Buttonнаходится отдельно, compliance ставит 100. Ось DS на кейсе — среднее, 87.5. По набору — 87.
Старый проверяльщик обнаруживает различия между файлами: в наивном варианте нет Button, есть нативные button и цвета #3B82F6, #6B7280, #EF4444. Но итоговый разрыв 87 против 5.7 нельзя считать точной оценкой соблюдения системы: существующий импорт в хорошем файле не был засчитан.
В текущем раннере проверка импорта перенесена на Babel (grade_ast) и закреплена тестом в tests/test_graders.py. Опубликованный JSON-прогон runs/fixture-good намеренно оставлен в том виде, в каком он был записан до фикса.
| Проверка исправления на двух файлах | Старый регексп (в архиве) | Текущий Babel AST (grade_ast) |
|---|---|---|
fixture (с import { Button } from "@ds") | 75 статика · 100 compliance · 87.5 ось DS | 100 статика · 100 compliance · 100 ось DS |
fixture-naive (сырые <button> и hex) | 20 статика · 0 compliance · 10 ось DS | 20 статика · 0 compliance · 10 ось DS |
Почему это не один судья
Детерминированные проверки отвечают за конкретные нарушения в исходнике. Браузер и модель-судья — за свойства, которые одним чтением кода не установить. Если проверка не запускалась, её результат нельзя считать измерением.
Импорт, hex и нативный тег я не отдаю модели. Правило на одном и том же файле отвечает одинаково, даже когда ошибается. В опубликованном прогоне модельные судьи не запускались, skipped: true. Их воспроизводимость в этом прогоне не оценивалась.
В текущем коде судья включается только вместе — провайдер Anthropic или OpenAI и скриншот. Визуальный ставит 0–5 за сетку, иерархию, ритм. UX смотрит, решена ли задача. Поле kind отделяет deterministic от llm. Ноль с пометкой skipped — не вердикт об экране. Их 30% веса переносятся на остальные оси, поэтому сводка не обнуляется и при этом не содержит суждения о внешнем виде.
Что можно утверждать
На этих десяти файлах проверка исходника отделяет тег Button от нативного button с зашитым цветом. Сравнивать так можно, потому что задача и система одни и те же, а свойство — то, что проверка действительно прочитала. Подставленные 70 сравнивать нельзя: измеряемого свойства там нет.
Разрыв нельзя переносить на живую модель: такого прогона нет. Нельзя говорить, что команда стала реже обходить систему. На странице agent-ui граница та же: влияние правил на правильную вставку через ds-eval ещё не считалось.
Рядом с остальными
ds-context собирает короткий пакет и модель не вызывает. ui-repair кладёт патч по правилам системы. prompt-regress смотрит, не сломала ли новая версия промпта то, что уже проходило. Они не вызывают ds-eval. ds-eval оценивает уже написанный файл и только теми проверками, которые в этом прогоне запускались.
Как запустить
cd ds-eval
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
ds-eval run --model fixture --suite smoke
ds-eval run --model fixture-naive --suite smoke
ds-eval compare RUN_FIXTURE RUN_NAIVE
Живые модели читают ANTHROPIC_API_KEY и OPENAI_API_KEY из окружения. Флаг --runtime включает браузер: сборку, Playwright и axe. Сам по себе он не запускает судей — им нужны живая модель и скриншот. Если флаг не передан, проверка не стартует: текущий раннер пишет функциональной оси 0 с пометкой runtime: skipped. Это отсутствие прогона, не результат. Если браузер стартовал и упал на сборке или странице, в записи остаётся ошибка исполнения, а ноль на функциональной оси уже без этой пометки. Подставленные 70 в опубликованном прогоне — третье состояние: браузер не запускался, и старый проверяльщик записал константу.