T27.AI

Блог

Ноль из шестисот сорока

2026-08-12 · 9 мин чтения

Пакет объявлял 640 тестов и не выполнил ни одного, а код возврата был 0. Механизм самый обыкновенный, починка — одна строка на импорт, а под ней обнаружилась физическая константа, которую не даёт ни один вход.

verificationzigvacuity

Главная строка всего текста состоит из четырёх слов, и напечатал её компилятор:

$ zig test src/root.zig
All 0 tests passed.

В том репозитории 640 тестовых блоков в 87 файлах. Не выполнился ни один. Код возврата — 0, шаг сборки зелёный, и каждый гейт, читавший этот код, пропустил его совершенно правильно.

Как набор тестов не выполняет ничего

Zig анализирует объявление верхнего уровня только тогда, когда на него кто-то ссылается. У каждого физического домена в пакете есть корневой файл, и в каждом написано ровно это:

pub const formulas = @import("formulas.zig");

На `formulas` не ссылался никто. Значит, объявление не анализировалось, значит, `formulas.zig` не входил в компиляцию, значит, его тестовых блоков не существовало — не пропущены, не упали, а отсутствуют. То же верно и для всего, что эти файлы импортируют дальше, до самого низа.

Починка — одна ссылка на импорт:

test {
    _ = formulas;
}

Понадобилась она в девяти файлах. Измерение сдвинулось с «All 0 tests passed» до 253 прошедших, 1 пропущенного, 254 всего. Тот, что пройти не мог, — предмет последнего раздела.

Почему никто не спросил

За эту неделю я обследовал пять пакетов. Ни у одного не было воркфлоу, запускающего `zig build`. Это и есть всё объяснение, и оно скучнее любой альтернативы: ни хитрого бага, ни гонки, ни расхождения версий. Компилятору просто не задали вопрос — он и не дал ответа.

ПакетЧто остановилоГде остановилось
zig-halfШесть дефектов; корень модуля написан синтаксисом RustНе компилировался никогда
zig-physicsМанифест на JSON, а не на ZONСтрока 1, столбец 7
trinity-trainingЗависимость закреплена на 0.2.0, апстрим — 2.1.0До компиляции, на загрузке
zig-knowledge-graphНет сборочного скрипта; зависимость без хешаЗапускать нечего
zig-hdcПуть импорта, уходящий выше корня модуляТолько когда наконец экспортировали

Пять пакетов, пять непригодных к использованию кем бы то ни было, и причина каждый раз одна и та же. Ошибка манифеста в zig-physics — самая наглядная: `{.name: "zig-physics", version: "0.1.0"}` — это JSON. ZON — не JSON. `zig build` остановился на первой строке и не дошёл ни до одного из 87 файлов за ней.

Один токен в основании всего

Последний пакет занял больше всего времени, потому что его отказ был уровнем выше. Файл `vsa_jit.zig` не удавалось экспортировать, а от него зависели пять корней в другом репозитории. Виновником оказался единственный импорт:

const arm64 = @import("../../jit_arm64.zig");

Из `src/vm/` этот путь выходит за корень модуля. Он не может разрешиться — ни у меня, ни в CI, ни где-либо ещё, и ни в один момент истории файла. `jit_arm64.zig` всё это время лежал в той же папке. Починка — удалить пять символов.

Он уцелел потому, что содержащий его файл никто не экспортировал. Вот что стоит унести с собой: **добавление экспорта — это акт верификации.** Оно переводит код из «присутствует» в «досягаем», а досягаемое — единственная категория, которую компилятор проверяет. Экспорт в том PR — долговечная половина правки; починка пути без него сгнила бы точно так же.

Пять копий, и компилировалась не та

Этот файл существует пять раз в трёх репозиториях — дважды в апстриме и по разу у двух потребителей. Я ожидал, что авторитетной окажется та копия, которая компилируется. Она оказалась отставшей.

Отставшая копия вызывала двухаргументный `dotProduct` и читала обычное поле. Копия, которая не компилировалась, вызывала трёхаргументную форму и разворачивала optional — потому что её обновили под новую зависимость, а два нужных ей файла при переносе оставили позади. Она не собиралась нигде — и была новее.

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

Что тесты нашли, когда смогли выполниться

Один тест упал в ту же секунду, как стал досягаем: предсказание параметра Барберо–Иммирци — той связи, что фиксирует спектр площади в петлевой квантовой гравитации. Утверждение — что γ лежит между 0.1 и 0.5, и это верная физика: согласование энтропии чёрных дыр даёт около 0.2375.

Проекция считает γ из двух координат корня E8 как |c₄| + |c₅|, умноженное на φ⁻¹. Но каждый корень E8 — это перестановка (±1, ±1, 0⁶) либо (±½)⁸ с чётным числом минусов, поэтому сумма равна 0, 1 или 2 и ничему больше. Область значений функции — три числа:

|c₄| + |c₅|полученная γвнутри (0.1, 0.5)?
00.436992 — аварийная ветвьда
10.618034нет
21.236068нет

Цели 0.2375 среди них нет и быть не может. Единственное допустимое значение — запасная ветвь, срабатывающая, когда обе координаты нулевые: ответ физичен ровно тогда, когда проекции нечего проецировать.

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

Я завёл issue и пометил тест пропущенным со ссылкой — не удалил и не расширил границу. Утверждение верное, менять надо проекцию. Написать замену формулы означало бы подогнать её под то утверждение, которому она обязана удовлетворять, — получилась бы модель, согласная с собственным тестом по построению, а именно от этого вся услуга и отказывается.

Прибор соврал в обе стороны

Трижды за одну сессию локальный тулчейн сообщил об этих репозиториях неправду, и направление стоит назвать точно, потому что только одно из трёх — та ошибка, которой ждут.

Дважды это была ложная тревога: `testing.refAllDeclsRecursive` и `std.time.timestamp` существуют в 0.15.2, на которую эти пакеты нацелены, и удалены позже, — локальный 0.16 сообщал об отказах, которых для цели нет. Один раз — ложное спокойствие в обратную сторону: код, тянущийся к C-аллокатору, проходит на macOS, где libc линкуется всегда, и падает на Linux с «C allocator is only available when linking against libc». Это поймал прогон CI; локально не поймало бы ничто.

Две ложные тревоги и одно ложное спокойствие, от одного прибора, за один вечер. Отсюда следует не «доверяйте CI», а то, что измерение неинтерпретируемо без названного прибора и что отличающийся прибор ошибается в обе стороны, а не консервативно в одну.

Что это меняет в бесплатной проверке

Структурный прогон на этом сайте уже отказывается считать пустой результат прохождением: пустой счёт триггеров — отказ, а не ноль. На этой неделе к пакетам, за которыми он теперь следит, добавилась более острая версия того же правила: шаг падает, если выполнилось меньше 200 тестов, — потому что набор, не выполнивший ничего, тоже завершается нулём, и никакой вердикт этих двух случаев не различает.

Такова общая форма. Вердикт — это один бит, и собственную пустоту он в себе не несёт. Везде, где проверка может пройти, не состоявшись, счётчик состоявшегося приходится мерить отдельно и на нём же ставить гейт, — а если вы никогда не видели, как ваш гейт падает, вы ещё не знаете, что он умеет.

Чего это не решает

Пруфы

Каждая цифра выше измерена, и рядом с ней названы её пределы.