Как это работает

Ниже изложена методика оценки качества кода через бенчмарк PRISM. После генерации бенчмарк берёт код и по-настоящему его запускает и отвечает не только «решено ли», но и чем именно плохо и во сколько это обойдётся. Ниже — как именно, с критериями и цифрами.

Четыре оси качества (SMOP)

SMOP — первые буквы четырёх осей (Syntax · Meaning · Optimization · Platform). Как призма раскладывает свет на спектр, PRISM раскладывает расплывчатое «качество кода» на четыре независимых измерения. У каждого — свой оракул (чем меряем), своя шкала 0–10 и свой критерий (за что балл). Одно «не прошло» превращается в диагноз: что именно сломалось.

осьвопросоракулбалл выше, когда
S Синтаксискод компилируется?BSL LS · компилятор 1Сменьше ошибок сборки
M Смыслкод решает задачу?скрытые тесты на исполнениибольше тестов пройдено
O Оптимальностькод не будет тормозить?codestat · техжурнал 1Скласс роста ближе к оптимальному
P Платформаверно обращается к объектам 1С?прогон против синтетической базыменьше обращений к выдуманным метаданным

Дальше — каждая ось подробно, с таблицей критерия.

S — Синтаксис

Код вообще компилируется — или там ошибки? Самый базовый уровень: машина пробует собрать модуль. Вердикт «собирается или нет» даёт тот движок, который код потом и исполняет: в алгоритмике (A) это OneScript, в платформенных задачах (B) компилятор 1С (DESIGNER /CheckModules). BSL Language Server в категории A тоже работает, но отвечает за другое. Он парсер, а не компилятор, и снисходителен: его дело не вердикт, а тяжесть. Балл считается по числу корневых причин ошибок (соседние ошибки одного места объединяются в одну):

состояние модулякорневых причинбалл
собирается, замечаний нет010
собирается, но анализатор придирается1+8
не собирается1–36
не собирается4–64
не собираетсябольше 62

Граница шкалы проходит между 8 и 6: балл выше шестёрки означает, что модуль собирается как есть. Одна опечатка, из-за которой модуль не компилируется, даёт шестёрку, а не восьмёрку: балл отвечает на вопрос «собирается ли», а не «сколько до этого правок».

Обрезанную генерацию ловит отдельная проверка

Если нарушен баланс парных конструкций (Функция/КонецФункции, Если, Цикл, Попытка) — балл сразу 0, минуя шкалу. Так отсекается генерация, оборванная на полуслове, которую парсер иначе молча проглотил бы.

Синтаксис модуля ≠ синтаксис запроса

S смотрит на компиляцию модуля. Текст запроса (внутри кавычек) для компилятора — просто строка: кривой запрос или обращение к несуществующему полю он пропускает. Такая ошибка вылезет уже на исполнении (Запрос.Выполнить()) и уйдёт в M или P, а не в S. Поэтому нормально видеть S = 10 и «синтаксическую ошибку» в логе одновременно — это синтаксис разных вещей.

M — Смысл

Код решает поставленную задачу? Машина запускает решение и прогоняет скрытые тесты — заготовленные пары «на таком входе должен быть такой результат», которых модель не видела (иначе подогнала бы ответ под них). Оракул — OneScript для A и настоящая 1С против синтетической базы для B. Балл — доля пройденных тестов:

доля пройденных тестовбалл
100 %10
≥ 85 %8
≥ 60 %6
≥ 50 %4
больше 02
0 % или не исполнился0

На лидерборде M плавная (доля × 10), без мёртвых зон грубой шкалы: 4 теста из 5 дают ровно 8.0, а не округляются вниз. Не скомпилировался или не запустился — 0: подтверждённого смысла нет. Код может прекрасно компилироваться (высокая S) и при этом давать неверный ответ (низкая M) — это разные вещи, и PRISM их разделяет.

O — Оптимальность

Код написан грамотно — или будет тормозить? Решать задачу можно правильно, но ужасно медленно. Ключевой приём здесь: оптимальность мы меряем не «по виду кода», а исполнением — гоняем решение на растущем входе и смотрим, как быстро растёт работа.

  • в алгоритмике (A) счётчик — число операций (OneScript -codestat): подаём всё больший вход и смотрим на класс роста;
  • в платформенных задачах (B) счётчик — число обращений к СУБД (техжурнал 1С) на растущей базе: кто берёт данные набором (одним запросом) — обращений почти не прибавляет; кто лезет в базу в цикле — прибавляет на каждую строку.

Показатель роста p сравнивается с оптимальным для задачи p_opt, сигнал — отклонение d = p − p_opt:

отклонение класса роста dбалл
на уровне оптимума (≤ 0.2)10
чуть хуже (≤ 0.5)8
заметно хуже (≤ 0.8)6
на класс хуже, n → n² (≤ 1.2)4
ещё хуже2

С таймаутом в двух категориях поступают по-разному. В алгоритмике не дождались — значит, и правда слишком медленно: это сигнал, балл 2. В платформенных задачах таймаут говорит лишь о том, что замер не состоялся, — тогда ось остаётся неизмеренной (N/A), а не наказывается баллом.

«Запрос в цикле» на цифрах

На одной платформенной задаче эталон читает регистр дважды, и с ростом базы это число не меняется. Решение с запросом внутри цикла на той же базе делает уже 62 обращения — и чем больше база, тем больше. Оба варианта проходят тесты (M одинаковая), но по оси O они расходятся: один держит нагрузку, другой ляжет на реальных данных. Статический анализ увидел бы это «по виду кода» не всегда — а исполнение ловит по эффекту.

Гейт по исполнению: не запустился — O = N/A

O ставится только исполнившемуся коду. Не скомпилировался или упал — балл N/A (ось выпадает из оценки), а не высокий. Иначе анализ «по виду кода» льстил бы неисполнимому решению: в категории B встречалось O = 10 при M = 0 — красивый на вид код, который вообще не работает.

Замер есть не у каждой задачи

Гонять решение на растущем входе можно там, где к задаче написан профиль нагрузки (perf.yaml). Для остальных B-задач, где нет нагрузочных тестов оптимальность пока оценивается по-старому — разбором самого кода: BSL Language Server ищет типовые антипаттерны (запрос в цикле, виртуальная таблица без параметров и подобное), их суммарный вес и даёт балл — по своей шкале, не по таблице выше. Это запасной путь, а не основной: где профиль есть, статику не спрашивают вовсе. И тот же гейт по исполнению действует и здесь — неисполнимый код получит N/A, а не высокий балл за красивый вид.

Насколько удачно выбран сам подход к решению — этого машина не судит; это уже к эксперту (ниже).

P — Платформа

Код верно обращается к объектам 1С — справочникам, регистрам, реквизитам? Нейросети часто выдумывают несуществующие поля и объекты: со стороны код правдоподобен, но падает на первом же реальном обращении к базе. PRISM запускает код против синтетической базы и смотрит, пережил ли он контакт с метаданными. Сигнал берётся из того же прогона, что и M — один запуск, два измерения. Балл — доля тестов, отработавших без платформенной ошибки:

доля тестов без платформенной ошибкибалл
все обращения отработали (100 %)10
большинство верны (≥ 50 %)6
хоть один тест пережил базу4
ни одного0

Ось применима только к категории B: в алгоритмике обращений к метаданным нет, там P = N/A.

Если решение до базы не добралось (модуль не собрался, нужной функции нет, текст запроса не разобрался), ось честно отдаёт «не измерено», а не ноль. Ноль означал бы, что обращения к метаданным были и оказались неверными, тогда как их никто не проверял: модуль с одной опечаткой мог ссылаться на метаданные безупречно. Провал кандидата наказывается там, где он измерен: Синтаксис за несобираемость, Семантика за непройденные тесты. А сколько осей у записи вообще измерено, показывает охват рядом с общим баллом.

Есть случай потруднее. Модуль собрался, тесты запустились, но до базы дело так и не дошло: код упал раньше, на обычном BSL. По логу это неотличимо от «сходил и всё сделал верно». Поэтому на время тестов включается технологический журнал 1С, и ось спрашивает у него ровно один факт: обращался ли код кандидата к данным. Если не обращался, а претензий по метаданным нет, проверять было нечего, и балл снова «не измерено».

Зачем разделять M и P

По задумке методики нельзя сваливать любой провал в одно «не прошло». PRISM различает две болезни: «выдумал несуществующее поле» (P) и «поля верные, но логика неверна» (M). Для разработчика это разные диагнозы с разным лечением — и PRISM показывает, какой именно.

Точные пороги — в протоколе метрики

Таблицы выше даны для наглядности. Канонический источник порогов, весов и правил — metrics/smop_l1_auto.yaml; скореры читают их оттуда, а не из этого текста. Так числа живут в одном месте и не расходятся.

Два уровня: машина и эксперт

Оценку ставят на двух уровнях, и у каждого своя роль.

Уровень 1 — машина. Берёт код и запускает: компилятор, тесты, реальная база, техжурнал. Работает сама, на тысячах ответов. Именно она строит лидерборд и отвечает на вопрос «какая нейросеть лучше». Балл каждой оси выводится из одной таблицы — единого источника правды и для скорера, и для человека.

Уровень 2 — эксперт (пока в планах; прототип платформы — genlab-1c-web). Оценивать руками каждый ответ нельзя: задач десятки, а ответов — тысячи, людей не хватит. Поэтому роль эксперта другая:

  • изредка сверять машину — сравнить её оценки со своими на небольшой выборке (как поверяют весы эталонной гирькой, а не перевешивают каждый товар);
  • оценить то, что машине не под силу — например, удачно ли выбран сам подход к решению. У этого нет единственно верного ответа, тут нужен человек.

Если коротко: машина измеряет код, а эксперт измеряет машину. Оба мерят одно и то же по одной конституции (metrics/smop.yaml) — различаются лишь инструментом и дробностью балла. Их согласие — каппа Коэна между авто- и экспертной оценкой — и есть главный научно-исследовательский результат PRISM.

Знак «проверено» (Verified)

Совпадение машины с экспертом превращает «машина так посчитала» в «этой оценке можно верить». Проверенная экспертом часть бенчмарка получит отметку Verified — слой доверия поверх автоматического лидерборда. Пока такого слоя нет (Уровень 2 в планах), но методика и шкала под него уже заложены.

Шкала оценок

Эксперт ставит баллы «через одну»: 0 · 2 · 4 · 6 · 8 · 10 — без серединок, чтобы человек не отлынивал нейтральной оценкой, а определялся. Машина же считает точно и хранит плавный балл: «4 теста из 5» — это 80 %, и на лидерборде так и показывается — 8.0, в полном разрешении. В грубые шесть ступеней точную оценку проецируют только в один момент — при сверке с экспертом (у него другой шкалы нет). То есть точное число — настоящая оценка для лидерборда, а ступень — общий язык для сравнения с человеком, а не само измерение.

Что такое «задача»

Задача в PRISM — это всегда условие + скрытая проверка + эталон:

  • условие — текст задания, который получает нейросеть;
  • скрытая проверка — тесты, которые модель не видит (иначе подгоняла бы ответ);
  • эталон — образцовое решение, которое само обязано проходить эти тесты на 100 % (не проходит свои тесты — значит, сломаны тесты или эталон, и задача в банк не идёт).

Задачи бывают двух категорий:

  • Категория A — алгоритмика на чистом языке 1С: сортировки, разбор строк, коллекции, даты. База не нужна — оценивается сам алгоритм и его класс роста. Исполняется в OneScript, бесплатном движке языка 1С. Оси: S · M · O.
  • Категория B — платформенные задачи: остатки на складе, списание по FIFO, расчёт себестоимости. Им нужна настоящая 1С с базой, поэтому исполняются в реальной платформе (1С в Docker) против синтетической базы, которую PRISM собирает из описания самой задачи. Добавляется ось P. Оси: S · M · O · P.

Категория B даёт модели инструменты (function calling)

Чтобы написать платформенный код, модель должна знать схему базы — и собирает её сама, вызовами инструментов: спрашивает, какие есть регистры и поля, и пишет код, уже зная схему. Модель без поддержки инструментов схему не видит → выдумывает метаданные → низкий балл по оси P. Это часть замысла: грамотная работа с метаданными 1С — как раз тот навык, что мы и меряем.

Где живёт качество бенчмарка

Машина оценивает ровно настолько хорошо, насколько хороши скрытые тесты к задаче. Поэтому самая ценная работа человека — не проверять оценки, а придумывать сильные задания с сильными тестами. Хотите помочь проекту — начните с этого (Как участвовать).

Общая оценка Q

Иногда хочется одну цифру «на глаз» — для этого есть Q, среднее по осям. С одной важной оговоркой: усредняются только измеренные оси. Применимых к категории A три (S, M, O), к категории B четыре (плюс P), но ось, которую измерить не удалось, в среднее не входит и нулём не подменяется. Знаменатель у записей получается разный, поэтому два балла Q сравнимы только вместе с охватом: сколько осей из применимых реально измерено. Без него запись с двумя проверками выглядела бы наравне с записью, за которой стоят четыре, а выпадение оси Q ещё и поднимает. Отсюда правило: охват идёт рядом с Q везде, где Q публикуется.

И даже с охватом Q остаётся вторичной величиной: одна цифра прячет, где именно модель ошиблась. Главный результат PRISM — всегда вектор из четырёх осей, а не Q.

Дальше