Находки на 2026-08-09 — материал для размышления
01Это опциональный вход. Ты не обязан соглашаться. Найденное несогласие с любым пунктом ниже — самый ценный результат твоей работы, если оно обосновано.
02Всё, что помечено «ИЗМЕРЕНО», получено чтением нашего дерева. Всё остальное — из веб-исследования с дословными цитатами; ты веб не видишь, поэтому проверить эти пункты не можешь и не должен делать вид, что можешь.
Часть 0. Задача
03vibevm публикует каталог пакетов: набор JSON-файлов в git-репозитории, который читают ЧУЖИЕ инструменты. Данные В ПОКОЕ, не сетевой протокол. Внешних потребителей пока ноль — значит ломающее изменение сейчас бесплатно и потом никогда не будет.
04Владелец решил: описать формат схемами и генерировать код из них. Замер перед постройкой нашёл, что цена другая, чем в таблице, по которой решали.
05Новое (важно): владелец хочет применить те же выводы к парсеру
vibe.toml — манифеста, который пишут РУКАМИ и который едет внутри
каждого опубликованного пакета.
Часть 1. ИЗМЕРЕНО — наше дерево
06Три долговечных формата, и ни один не решён одинаково:
| версия в данных | кто-то ветвится по ней | незнакомый ключ | заповедник для чужих ключей | |
|---|---|---|---|---|
vibe.lock |
есть (5) | да, отвергает | отвергается | нет |
| каталог индекса | есть (1) | нет, никто | отвергается | нет |
vibe.toml |
НЕТ ВОВСЕ | — | отвергается | нет |
08Каталог, детально (crates/vibe-index/):
- 0923 типа на проводе; из них 18 — файлы каталога (17 структур + 1 объединение).
- 86 полей.
- 14 полей-коллекций, где пустое неотличимо от отсутствующего —
skip_serializing_if = "…is_empty". - 1 объединение без тега, и оно единственное:
RepomdFileEntryвtypes/repomd.rs— либо{"kind":"directory","entries":N}, либо{"size":N,"sha256":"…"}. Общего поля-признака нет; читатель угадывает по набору ключей. Это в манифесте каталога — файле, который читатель открывает ПЕРВЫМ. deny_unknown_fieldsв 15 местах. Каталог перечитывается ради перезаписи на 6 путях. Следствие: незнакомое поле не теряет данные — оно не даёт прочитать каталог вообще; сервер грузит каталог на старте, значит старый сервер не запустится на каталоге, записанном новым.- 5 закрытых словарей, у всех неизвестное значение = ошибка разбора.
#[serde(other)]нет нигде в дереве. - Версия каталога — ярлык, а не переключатель: ни одного сравнения.
- Спека ПРЯМО ОБЕЩАЕТ «читатели старой версии спокойно игнорируют незнакомые поля». Код делает противоположное. Обещание помечено «реализовано».
- Клиент (
vibe-registry/src/index_client/wire.rs) уже терпим — читает своими view-структурами. То есть наружу мы правильны, а сами себе строги.
Часть 2. Из исследования — наши соседи (данные в покое)
10PyPI, авария PEP 714 — прямое попадание в нашу форму. Поле, которое бывает
«либо булево, либо словарь» — то есть объединение без тега. pip падал с
AttributeError: 'dict' object has no attribute 'partition'. Баг прожил
8 месяцев, попал в дистрибутивы и образы. Ключевое:
11«сломанная таким образом версия pip не может установить с PyPI вообще ничего — включая новую, починенную версию pip»
12Починка уничтожила бы путь к починке. Пришлось менять спецификацию и переименовывать поле. Оценка альтернативы «подождать, пока вымоется» — 5+ лет.
13«Поле, которое ты отдаёшь восемь месяцев, ты отдаёшь навсегда.» PyPI до сих пор, три года спустя, отдаёт опечатанное имя ключа. Переименование обошлось дёшево ровно потому, что читателей было трое и их можно было обзвонить.
14Контракт версии (PEP 629), нормативный:
- 15старшая версия выше известной → ОБЯЗАН отказаться с внятной ошибкой;
- младшая выше известной → СЛЕДУЕТ предупредить и продолжить;
- версии нет → ОБЯЗАН считать 1.0.
16Решение из их спора: поднимать младшую версию имеет смысл, только если поля, которые она вводит, обязательны на этой версии. Иначе клиенту незачем её проверять.
17Терпимость нормативна и асимметрична: писателю «можно добавлять», читателю «ОБЯЗАН игнорировать незнакомые ключи». Режима отказа нет вообще.
18Три состояния различены намеренно: «если ключа нет — файл метаданных может существовать, а может и нет; если значение истинно — существует; если ложно — нет». Отсутствие = НЕИЗВЕСТНО, присутствующее ложное = известно-что-нет.
19Пустая коллекция пишется, а не опускается: словарь хэшей «ОБЯЗАН присутствовать, даже если хэшей нет».
20Ломающее изменение — новый ПУТЬ, а не новая старшая версия в том же документе. К этому независимо пришли PyPI и NuGet.
21Заповедник для чужих ключей: ключи с подчёркиванием зарезервированы, «ни один будущий стандарт не назначит им смысла».
22NuGet — двухуровневый словарь: документированные ресурсы вечны, недокументированные удаляются по желанию, и это написано. Плюс их собственный диагноз: версионирование внутри строки-тега выглядит как развязка сервера и клиента, а на деле связывает их сильнее.
23NuGet — тихий отказ на шесть лет: фильтр по типу пакета «молча игнорировался всеми совместимыми источниками столько, сколько это свойство существует публично», и починка 2026 года сама стала ломающей.
24Homebrew — как не надо: схемы нет, версии в данных нет, обещание стабильности — одна фраза в чужом документе, две попытки завести проверку схемы в сборке закрыты нереализованными.
Часть 3. Из исследования — механика эволюции
25У протокол-буферов две кодировки, и политика противоположна. Двоичная терпима и сохраняет незнакомое. JSON-овая: «разборщик должен по умолчанию отвергать незнакомые поля». На жалобы ответ: «используйте двоичную». Мы наследуем весь набор проблем их JSON-кодировки и не имеем их выхода.
26Развороты, оплаченные чужой болью:
- 27
requiredубрали совсем: «никогда не знаешь, сколько проживёт тип и не придётся ли кому-то через четыре года заполнять твоё обязательное поле пустой строкой». - Возможность отличить «поля нет» от «поле по умолчанию» убрали и вернули через ~5 лет «в ответ на отзывы пользователей». Для списков и словарей так и не вернули — там «пусто» и «нет» слиты навсегда.
- Сохранение незнакомых полей убрали и вернули; в обсуждении 19 месяцев спустя выяснилось, что исходное обоснование было слабее, чем писала документация.
- Словари переключили с закрытых на открытые «именно из-за неожиданного поведения, которое вызывают закрытые». Этот разворот прижился, потому что был оправдан демонстрируемой порчей данных, а не удобством.
28Avro — другая модель: кладёт схему писателя вместе с данными, читатель согласует свою схему с писательской. В сети это дорого; в git-репозитории это стоит указателя, и история схемы лежит в той же истории, что и данные.
29Словарь совместимости (его половина индустрии путает):
- 30BACKWARD — новый читатель читает СТАРЫЕ данные;
- FORWARD — старый читатель читает НОВЫЕ данные.
31Наш случай — строго FORWARD. Добавление поля безопасно назад и враждебно вперёд; удаление зеркально. Терпимый читатель — то, что превращает каждое в полную совместимость.
32Принятая по умолчанию проверка совместимости НЕтранзитивна, потому что предполагает, что старые сообщения вымываются. Для репозитория это ложно.
33RFC 9413 (2023) отзывает принцип «будь либерален»: терпимость «входит в патологический цикл обратной связи», «дефект закрепляется как стандарт де-факто». Оговорка, которую теряют почти все цитирующие: он бьёт по терпимости к НЕСООТВЕТСТВУЮЩЕМУ входу, а не к ОБЪЯВЛЕННЫМ точкам расширения.
Часть 4. Из исследования — клиенты и переносимость
34Критерий переносимости: практика переносится тогда и только тогда, когда она — свойство артефакта или твоей собственной дисциплины. Всё, что свойство отношений (переговоры, наблюдение, принуждение, трансляция), умирает при соприкосновении с файлом в git.
35НЕ переносится, и это самая часто ошибочно переносимая практика: «бесверсионная аддитивная эволюция». Ей нужен запрос. Файл — это ответ, у которого не было вопроса.
36Когда эти компании сталкиваются с потребителями, которых не контролируют, они версионируют. Meta держит бесверсионный внутренний интерфейс и явно версионированный внешний с двухлетним сроком. LinkedIn сделал так же и опубликовал, что бесверсионная публичная модель провалилась.
37Про словари консенсуса НЕТ. Google: добавление значения — не ломающее. Kubernetes (вышел из Google): добавление значения — не совместимое. LinkedIn: считайте обратно-несовместимым. Четыре организации независимо изобрели третью категорию — «опасное изменение».
38Механическая причина, по которой чинить надо заранее: незнакомое ПОЛЕ можно пропустить, у него есть адрес. Незнакомое ЗНАЧЕНИЕ словаря обязано быть положено в типизированную ячейку, перечисляющую только известное. Любая починка расширяет ячейку заранее. Ни одну нельзя внедрить в читателей, которые уже разошлись.
39Все распространённые разборщики по умолчанию бросают исключение. Две терпимые по умолчанию библиотеки — оба случая, когда издатель схемы сам писал генератор кода читателя.
40Единственный опубликованный рецепт для НЕКОНТРОЛИРУЕМЫХ потребителей (LinkedIn): не расширять словарь, а добавить новое необязательное поле с новым перечислением и поддерживать старых клиентов со старыми символами бессрочно. И отдельно: этот приём «не работает для данных, сохранённых в Avro» — то есть в покое становится хуже.
41Разбор аварии Cloudflare (ноябрь 2025) — ближайший аналог нашего случая. Безобидное изменение прав добавило строк в сгенерированный файл конфигурации, файл вырос вдвое, у развёрнутых потребителей был зашит предел → глобальная авария. Версионный перекос изменил вид отказа: новые прокси возвращали ошибки, а старые ошибок не видели и считали неверно — «всему трафику выставлялся ботовый балл ноль». Старый читатель работал молча, неправильно и уверенно. Их вывод в починку: ужесточить приём собственных сгенерированных файлов так же, как для пользовательского ввода.
42Переносится (внедрять первым): статическая проверка совместимости в сборке — ей не нужен ни сервер, ни телеметрия.
43Аргумент за ОБЯЗАТЕЛЬНУЮ версию: у Discord бесверсионный маршрут по умолчанию годами заморожен на устаревшей версии — они навсегда пожертвовали возможностью сдвинуть собственное умолчание. Бесверсионный формат не эволюционирует, он только нарастает.
44Защита Google — не правило, а БЮДЖЕТ: «перечисления должны получать новые значения нечасто… не чаще раза в год. Для часто меняющихся использовать строку».
Часть 5. Моя текущая рекомендация — оспорь её
- 45Каждое объединение — тегированное.
- Отсутствие означает «не записано»; известное пустое пишется явно пустым
списком. (
nullне нужен: в TOML его нет, и правило, выживающее в обоих форматах, скорее верное.) - У каждого закрытого словаря — запасное значение «неизвестное».
- Строгость не один рычаг: она зависит от того, кто написал файл.
Проверяй строго на входной двери (
vibe.toml, руками, — ловим опечатку), неси терпимо дальше (каталог, машиной, — переживаем будущее). Разделять пространством имён, а не уровнем строгости. - Версия формата несёт контракт: старшая — отказ, младшая — предупреждение, отсутствие — считать первой.
- Имена полей не переиспользуются; есть список отставленных.
- Порядок работ: каталог — полигон (ломать даром, здесь строится машинерия), манифест — боевое применение (дорого, но машинерия уже обкатана).