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