Инвентарь отладочного API 1C:EDT, сделанный 11.09.2026 по распакованным бандлам целевой платформы, зафиксировал дефект, который не подал ни одного признака: measureResults, measureCoverage и measureCallers на EDT 2024.2 возвращали пустой результат с success: true. Ошибки не было — не было ни одной строки в карте выполнения там, где замер шёл и строки существовали. Причина помещалась в один вызов: время строки читалось только через getTimeForDebugTarget(type) и getPureTimeForDebugTarget(type), а эти методы появились в 2025.x. На 2024.2 их нет, строка отбрасывалась целиком, и цикл добавлял в результат ноль записей. Разрыв виден был и в самих бандлах: бандл com._1c.g5.v8.dt.profiling.core в 2024.2 имеет версию 4.0.800, в 2025.2 — уже 5.0.0.
Пустой ответ под флагом успеха — самый дорогой вид отказа: читающая его модель-оператор не имеет ни одного основания усомниться. Разбираться пришлось не с замером, а со связкой, которая со стороны выглядит небрежностью: закреплённая версия сборки, необязательные импорты и отражение, которое никто не чистит.
Якорь — нижняя граница, а не последняя версия
Один и тот же бинарник обязан грузиться на EDT 2024.2, 2025.1 и 2026.x: стенд и рабочие установки живут на разных версиях, и сборка под новую на старой просто не запустится. ADR-002 от августа 2026 выводит из этого неочевидное: привязывать надо не к актуальной версии, а к самой старой. В комментарии к цели сборки так и записано — пин на самую старую поддерживаемую версию; диапазоны версий в MANIFEST.MF берутся те, что удовлетворяет 2024.2.
Смысл якоря в том, что компилятор обязан видеть разрывы API снизу, а не сверху. Собранная против 2026.x правка, зовущая метод, которого в 2024.2 нет, пройдёт сборку незаметно — и всплывёт пустым ответом, как в истории с замером. Следствие ADR-002 формулирует запрет прямо: поднимать цель сборки, чтобы красные импорты перестали гореть, нельзя; возможности, которым нужна среда новее, отказывают в момент вызова — так живёт configuration_update, которому нужен EDT 2026.1 +.
Почему reflection и ловушка по Throwable остаются
Проектное правило из «инвариантов» формулирует причину короче всего:
NoClassDefFoundError — это Error, а не Exception. Сузив catch или заменив Class.forName на прямой импорт, мы роняем целую версию EDT: класса, которого нет в 2024.2, достаточно, чтобы бандл не загрузился. Импорты com._1c.g5.* остаются resolution:=optional по той же причине.
Это не страховка на всякий случай, а состояние сборки: 1385 вхождений catch (Throwable) в 370 файлах, 815 обращений Class.forName и 91 необязательный импорт в манифесте бандла (подсчёт по исходникам, 20.09.2026). Цифры выглядят как долг, который пора закрыть, и именно это предлагают ревью при каждом удобном случае. Закрыть его нельзя: ошибка вставала бы на разрешении класса, то есть до всякого вызова метода. Одного прямого импорта отсутствующего класса достаточно, чтобы OSGi не отдал бандл целиком — вместе с инструментами, которые к этой версии отношения не имеют.
Ответственность за такое молчание закрыта с двух сторон. Негрузящийся адаптер платформенного шва попадает в стартовую диагностику: поле startupIssues в /health появляется только тогда, когда сбои есть, а прогрев швов идёт фоном после выбора рабочей области (#736, #757) и не задерживает подъём сервера. Отказ получает код EDT_API_INCOMPATIBLE с источником needsHuman: отсутствующего в этой редакции класса повтор вызова не лечит, поэтому совет «повтори через несколько секунд» для него запрещён.
Как версионный шов выглядит в коде
Правка замера оставила два пути чтения времени строки, и порядок их выбора — это и есть шов:
private static double measuredTimeMs(Object lineObject, String durationMethod,
String durabilityMethod, Object targetType, double performanceFrequency) {
Duration duration = optionalDuration(lineObject, durationMethod, targetType);
if (duration != null) {
return duration.toNanos() / 1_000_000.0;
}
try {
Object durability = ProfilingServiceAccess.invokeMethod(lineObject, durabilityMethod);
if (performanceFrequency <= 0.0) {
return 0.0;
}
return ((Number)durability).longValue() / performanceFrequency * 1000.0;
} catch (Exception ignored) {
return 0.0;
}
}
Отражательный помощник опирается на подбор перегрузки: если метода getTimeForDebugTarget в среде нет, летит NoSuchMethodException, optionalDuration отдаёт null, и время считается из сырого счётчика getDurability() и частоты замера. Недоступна и частота — строка остаётся в выдаче с нулевым временем: честный ноль лучше выдуманного числа. Та же логика в подборе перегрузок: перебираются все getMethods() с нужным именем, берётся самая широкая совместимая, среди равных по ширине — самая конкретная. Порядок getMethods() контрактом не считается: прежний выбор «кто первый попался» ломался на перегрузках с равным числом параметров (#734, #756).
Обратный случай закрыт так же. Метода getInterfaceCompatibilityMode в закреплённой 2024.2 нет, и генераторы форм берут запасной режим TAXI, а в журнал среды пишут предупреждение — подмена не должна выглядеть необъяснимой. Волна 11.09 показала, что дефект ходит в обе стороны: form_edit создавал форму на EDT 2025.2 и падал на стенде 2024.2 именно из-за этого метода (#549); десять кейсов form_edit до сих пор помечены в списке ожидаемых отказов e2e. Проверка «сверху» этот класс дефектов не ловит — ловит младшая версия.
Развилка выбирается по фактическому наличию API, а не по строке с версией. UpdateEngineCapabilities грузит классы службы сравнения и проверяет нужные методы; отсутствующее пополняет список, и путь выключается целиком — частично работающее сравнение опаснее честного отказа. На выходе три состояния: штатный механизм EDT 2026.1 +, совместимый 2024.2–2025.x и «недоступно» с названной причиной. Прямых сравнений строки версии с номером среды в исходниках бандла я не нашёл: версия EDT нужна инструментам только для того, чтобы назвать себя в отказе и в ответе edt_version.
Дыру закрывает офлайн-контракт, а не второй стенд
ADR-017 от 12.09.2026 сознательно отказался от второго стенда на 2025.x. Живой автоматической проверкой остаётся одна среда — EDT 2024.2, то есть нижняя граница совместимости, где такой дефект и проявляется. Разрыв записан без округления в удобную сторону: 2025.1 и 2026.x живой проверкой не покрыты, 2025.2 в CI-приёмку не входит, а 2025.1 не проверяется нигде. Аргумент против второго стенда бытовой: кеш EDT — около 1 ГБ загрузки и 2–3 ГБ на диске, прогон стенда — 6–10 минут, и на машине с одним раннером приёмка волны удлинилась бы вдвое.
Вместо живой второй среды работает заглушка, которая притворяется средой. Контракт ProfilingLinesContract (#552) подкладывает читателю результатов три объекта: строку 2024.2 без Duration-методов, строку 2025.x, которая Duration отдаёт, и строку с недоступной частотой. Строка 2024.2 обязана доехать до карты и отдать 500 мс из счётчика и частоты.
$ python3 tools/profiling_lines_contract.py
profiling lines contract passed (13 проверок)
Важно не число проверок, а направление: на заглушках прогоняется ровно та ветка, что раньше молча съедала строки, и обе версии остаются в выдаче без собранной среды EDT.
Отражение при этом не отпущено без присмотра. Гейт инвентаря отражённых вызовов (#578, #594) разбирает исходники и сверяет имена классов и методов с настоящими бандлами стендов, читая подписи javap. Прогон 20.09.2026: загрузок классов 1186, getMethod — 2638, invoke — 519, вызовов через помощников, прячущих отражение, — 629, пар «класс + метод» проверено 430, известных красных записей — 6, законных версионных развилок — 3, итог: нового красного нет. Вызов, которого нет ни в одной версии, — это дефект, а не развилка, и он краснеет; список известных красных живёт с номером задачи и датой появления и краснеет сам, если запись старше 14 дней. На машине без стендов гейт честно помечается пропущенным, а не зеленеет.
Обещать версию можно только после прогона
Поддерживаемой объявляется версия, на которой прошла живая приёмка, и публичные обещания формулируются по доказанному. «Загружается» и «поддерживается» — разные слова.
Это пункт 1 ADR-032 от 15.09.2026 (#709) — второй половины честности не хватало. Состояние на тот день: живьём автоматически проверяется только 2024.2 на серверном стенде, загрузка на 2025.1 и 2026.x обеспечена конструктивно, но прогоном не доказана ни на одной из них. Запрос на два живых стенда — 2024.2 и 2026.1 — заведён (#694), работа идёт, приёмки на двух стенда ещё нет. ADR-017 при этом не отменяется самим фактом правки: он пересматривается, когда появится вторая машина или раннер либо когда #694 докажет сосуществование стендов.
Формулировка переехала в публичный changelog выпуска 1.12.1 от 19.09.2026: версии 1C:EDT обещаются по доказанному прогону, 2024.2 проверена на тестовом стенде, более новые выпуски загружаются, а статус их проверки — в матрице тестирования. Отвечать за слово «поддерживается» прогоном — и есть та дисциплина, которая удерживает всю конструкцию от расползания.
Связка, похожая снаружи на неряшливость, держится на трёх опорах: якорь сборки на нижней границе версий, отражение вместо прямого импорта и обещание версии только по доказанному прогону. Убрав любую из трёх, получаешь либо бандл, который не грузится на 2024.2, либо пустой ответ с success: true. Цена известна: узкое окно поддержки и регрессия, которая дойдёт до рабочей установки раньше, чем до гейта. Она записана в ADR-017 и ADR-032 как осознанный разрыв, а не как забытая проверка.