# Находки на 2026-08-09 — материал для размышления

[p01] Это **опциональный вход**. Ты не обязан соглашаться. Найденное несогласие с
любым пунктом ниже — самый ценный результат твоей работы, если оно
обосновано.

[p02] Всё, что помечено «ИЗМЕРЕНО», получено чтением нашего дерева. Всё остальное —
из веб-исследования с дословными цитатами; ты веб не видишь, поэтому проверить
эти пункты не можешь и **не должен делать вид, что можешь**.

## Часть 0. Задача

[p03] vibevm публикует **каталог пакетов**: набор JSON-файлов в git-репозитории,
который читают ЧУЖИЕ инструменты. Данные В ПОКОЕ, не сетевой протокол.
Внешних потребителей **пока ноль** — значит ломающее изменение сейчас
бесплатно и потом никогда не будет.

[p04] Владелец решил: описать формат схемами и генерировать код из них. Замер перед
постройкой нашёл, что цена другая, чем в таблице, по которой решали.

[p05] **Новое (важно):** владелец хочет применить те же выводы к парсеру
**`vibe.toml`** — манифеста, который пишут РУКАМИ и который едет внутри
каждого опубликованного пакета.

## Часть 1. ИЗМЕРЕНО — наше дерево

[p06] Три долговечных формата, и ни один не решён одинаково:

[p07]
|  | версия в данных | кто-то ветвится по ней | незнакомый ключ | заповедник для чужих ключей |
| --- | --- | --- | --- | --- |
| `vibe.lock` | есть (5) | **да, отвергает** | отвергается | нет |
| каталог индекса | есть (1) | **нет, никто** | отвергается | нет |
| `vibe.toml` | **НЕТ ВОВСЕ** | — | отвергается | нет |

[p08] Каталог, детально (`crates/vibe-index/`):

- [p09] **23 типа на проводе**; из них 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. Из исследования — наши соседи (данные в покое)

[p10] **PyPI, авария PEP 714 — прямое попадание в нашу форму.** Поле, которое бывает
«либо булево, либо словарь» — то есть объединение без тега. `pip` падал с
`AttributeError: 'dict' object has no attribute 'partition'`. Баг прожил
8 месяцев, попал в дистрибутивы и образы. Ключевое:

> [p11] «сломанная таким образом версия pip не может установить с PyPI вообще ничего
> — включая новую, починенную версию pip»

[p12] Починка уничтожила бы путь к починке. Пришлось менять спецификацию и
переименовывать поле. Оценка альтернативы «подождать, пока вымоется» —
**5+ лет**.

[p13] **«Поле, которое ты отдаёшь восемь месяцев, ты отдаёшь навсегда.»** PyPI до сих
пор, три года спустя, отдаёт опечатанное имя ключа. Переименование обошлось
дёшево ровно потому, что читателей было **трое и их можно было обзвонить**.

[p14] **Контракт версии (PEP 629), нормативный:**

- [p15] старшая версия выше известной → **ОБЯЗАН** отказаться с внятной ошибкой;
- младшая выше известной → **СЛЕДУЕТ** предупредить и продолжить;
- версии нет → **ОБЯЗАН** считать 1.0.

[p16] **Решение из их спора:** поднимать младшую версию имеет смысл, **только если
поля, которые она вводит, обязательны на этой версии**. Иначе клиенту незачем
её проверять.

[p17] **Терпимость нормативна и асимметрична:** писателю «можно добавлять», читателю
«**ОБЯЗАН** игнорировать незнакомые ключи». Режима отказа нет вообще.

[p18] **Три состояния различены намеренно:** «если ключа нет — файл метаданных может
существовать, а может и нет; если значение истинно — существует; если ложно —
нет». Отсутствие = НЕИЗВЕСТНО, присутствующее ложное = известно-что-нет.

[p19] **Пустая коллекция пишется, а не опускается:** словарь хэшей «ОБЯЗАН
присутствовать, даже если хэшей нет».

[p20] **Ломающее изменение — новый ПУТЬ, а не новая старшая версия в том же
документе.** К этому независимо пришли PyPI и NuGet.

[p21] **Заповедник для чужих ключей:** ключи с подчёркиванием зарезервированы, «ни
один будущий стандарт не назначит им смысла».

[p22] **NuGet — двухуровневый словарь:** документированные ресурсы вечны,
недокументированные удаляются по желанию, и это **написано**. Плюс их
собственный диагноз: версионирование внутри строки-тега выглядит как развязка
сервера и клиента, а на деле связывает их сильнее.

[p23] **NuGet — тихий отказ на шесть лет:** фильтр по типу пакета «молча
игнорировался всеми совместимыми источниками столько, сколько это свойство
существует публично», и починка 2026 года сама стала ломающей.

[p24] **Homebrew — как не надо:** схемы нет, версии в данных нет, обещание
стабильности — одна фраза в чужом документе, две попытки завести проверку
схемы в сборке закрыты нереализованными.

## Часть 3. Из исследования — механика эволюции

[p25] **У протокол-буферов две кодировки, и политика противоположна.** Двоичная
терпима и сохраняет незнакомое. JSON-овая: «разборщик должен по умолчанию
отвергать незнакомые поля». На жалобы ответ: «используйте двоичную». **Мы
наследуем весь набор проблем их JSON-кодировки и не имеем их выхода.**

[p26] **Развороты, оплаченные чужой болью:**

- [p27] `required` убрали совсем: «никогда не знаешь, сколько проживёт тип и не
  придётся ли кому-то через четыре года заполнять твоё обязательное поле
  пустой строкой».
- Возможность отличить «поля нет» от «поле по умолчанию» убрали и **вернули
  через ~5 лет** «в ответ на отзывы пользователей». Для списков и словарей
  **так и не вернули** — там «пусто» и «нет» слиты навсегда.
- Сохранение незнакомых полей убрали и вернули; в обсуждении 19 месяцев спустя
  выяснилось, что исходное обоснование было слабее, чем писала документация.
- **Словари переключили с закрытых на открытые** «именно из-за неожиданного
  поведения, которое вызывают закрытые». Этот разворот прижился, потому что
  был оправдан **демонстрируемой порчей данных**, а не удобством.

[p28] **Avro — другая модель:** кладёт **схему писателя вместе с данными**, читатель
согласует свою схему с писательской. В сети это дорого; **в git-репозитории
это стоит указателя, и история схемы лежит в той же истории, что и данные.**

[p29] **Словарь совместимости (его половина индустрии путает):**

- [p30] **BACKWARD** — новый читатель читает СТАРЫЕ данные;
- **FORWARD** — старый читатель читает НОВЫЕ данные.

[p31] Наш случай — **строго FORWARD**. Добавление поля безопасно назад и враждебно
вперёд; удаление зеркально. **Терпимый читатель — то, что превращает каждое в
полную совместимость.**

[p32] **Принятая по умолчанию проверка совместимости НЕтранзитивна**, потому что
предполагает, что старые сообщения вымываются. **Для репозитория это ложно.**

[p33] **RFC 9413 (2023) отзывает принцип «будь либерален»:** терпимость «входит в
патологический цикл обратной связи», «дефект закрепляется как стандарт
де-факто». **Оговорка, которую теряют почти все цитирующие:** он бьёт по
терпимости к НЕСООТВЕТСТВУЮЩЕМУ входу, а не к ОБЪЯВЛЕННЫМ точкам расширения.

## Часть 4. Из исследования — клиенты и переносимость

[p34] **Критерий переносимости:** практика переносится тогда и только тогда, когда
она — свойство **артефакта** или **твоей собственной дисциплины**. Всё, что
свойство **отношений** (переговоры, наблюдение, принуждение, трансляция),
умирает при соприкосновении с файлом в git.

[p35] **НЕ переносится, и это самая часто ошибочно переносимая практика:**
«бесверсионная аддитивная эволюция». Ей нужен запрос. **Файл — это ответ, у
которого не было вопроса.**

[p36] **Когда эти компании сталкиваются с потребителями, которых не контролируют,
они версионируют.** Meta держит бесверсионный внутренний интерфейс и явно
версионированный внешний с двухлетним сроком. LinkedIn сделал так же и
опубликовал, что бесверсионная публичная модель провалилась.

[p37] **Про словари консенсуса НЕТ.** Google: добавление значения — не ломающее.
Kubernetes (вышел из Google): добавление значения — **не** совместимое.
LinkedIn: считайте обратно-несовместимым. Четыре организации независимо
изобрели **третью категорию** — «опасное изменение».

[p38] **Механическая причина, по которой чинить надо заранее:** незнакомое ПОЛЕ
можно пропустить, у него есть адрес. Незнакомое ЗНАЧЕНИЕ словаря обязано быть
положено в типизированную ячейку, перечисляющую только известное. Любая
починка расширяет ячейку **заранее**. **Ни одну нельзя внедрить в читателей,
которые уже разошлись.**

[p39] **Все распространённые разборщики по умолчанию бросают исключение.** Две
терпимые по умолчанию библиотеки — оба случая, когда издатель схемы сам писал
генератор кода читателя.

[p40] **Единственный опубликованный рецепт для НЕКОНТРОЛИРУЕМЫХ потребителей**
(LinkedIn): не расширять словарь, а **добавить новое необязательное поле с
новым перечислением** и поддерживать старых клиентов со старыми символами
**бессрочно**. И отдельно: этот приём «не работает для данных, сохранённых
в Avro» — то есть в покое становится хуже.

[p41] **Разбор аварии Cloudflare (ноябрь 2025) — ближайший аналог нашего случая.**
Безобидное изменение прав добавило строк в **сгенерированный файл
конфигурации**, файл вырос вдвое, у развёрнутых потребителей был зашит предел →
глобальная авария. **Версионный перекос изменил вид отказа:** новые прокси
возвращали ошибки, а **старые ошибок не видели и считали неверно** — «всему
трафику выставлялся ботовый балл ноль». Старый читатель работал молча,
неправильно и уверенно. Их вывод в починку: **ужесточить приём собственных
сгенерированных файлов так же, как для пользовательского ввода.**

[p42] **Переносится (внедрять первым):** статическая проверка совместимости в
сборке — ей не нужен ни сервер, ни телеметрия.

[p43] **Аргумент за ОБЯЗАТЕЛЬНУЮ версию:** у Discord бесверсионный маршрут по
умолчанию годами заморожен на устаревшей версии — они навсегда пожертвовали
возможностью сдвинуть собственное умолчание. **Бесверсионный формат не
эволюционирует, он только нарастает.**

[p44] **Защита Google — не правило, а БЮДЖЕТ:** «перечисления должны получать новые
значения нечасто… не чаще раза в год. Для часто меняющихся использовать
строку».

## Часть 5. Моя текущая рекомендация — оспорь её

1. [p45] Каждое объединение — тегированное.
2. Отсутствие означает «не записано»; известное пустое пишется явно пустым
   списком. (`null` не нужен: **в TOML его нет**, и правило, выживающее в обоих
   форматах, скорее верное.)
3. У каждого закрытого словаря — запасное значение «неизвестное».
4. Строгость **не один рычаг**: она зависит от того, кто написал файл.
   Проверяй строго на входной двери (`vibe.toml`, руками, — ловим опечатку),
   неси терпимо дальше (каталог, машиной, — переживаем будущее).
   Разделять **пространством имён**, а не уровнем строгости.
5. Версия формата несёт контракт: старшая — отказ, младшая — предупреждение,
   отсутствие — считать первой.
6. Имена полей не переиспользуются; есть список отставленных.
7. Порядок работ: **каталог — полигон** (ломать даром, здесь строится
   машинерия), **манифест — боевое применение** (дорого, но машинерия уже
   обкатана).

