<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title>G2-MECHANICS — механика эволюции формата: второй взгляд</title>
  <p p="1">Это второй независимый разбор свода `FINDINGS-DIGEST.md`. Предмет — как наш
формат меняется во времени, не ломая читателя. Самый ценный результат здесь —
**обоснованное несогласие**; где я согласен, я добавляю того, чего в своде нет.</p>
  <p p="2">**Правило разметки источников.** У меня нет интернета, поэтому:
`[ИЗ СВОДА]` — из `FINDINGS-DIGEST.md`; `[ИЗ ДЕРЕВА]` — проверено мной чтением
кода, с `файл:строка`; `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — мои знания о чужих
системах, без выдуманных версий/дат/номеров.</p>
  <section title="TL;DR — где я не согласен">
    <list ordered="true" p="3">
      <item>**Avro для нас — дороже, а не «почти бесплатно».** Предпосылка свода
   «схема и данные в одной истории git» неверна: каталог лежит в реестре,
   схема (Rust-типы) — в исходном репозитории vibevm. Это две разные истории.
   Внешний читатель без репозитория и скопированная наружу запись ломают
   модель Avro целиком — остаётся терпимый читатель, который у нас уже есть.</item>
      <item>**Объединение НЕ «угадывает по набору ключей».** В текущем дереве вариант
   `Directory` уже несёт позитивный дискриминант `kind:"directory"`
   (`crates/vibe-index/src/types/repomd.rs:44-50`). Настоящий дефект —
   `#[serde(untagged)]`, и он устраняется сменой одного атрибута.</item>
      <item>**«14 полей-коллекций» — заниженный периметр.** С `skip_serializing_if =
   "...is_empty"` в дереве **21** поле, не 14. Решение А6 принималось по числу
   14; для языка схемы важны все 21.</item>
      <item>**Невосстановление присутствия у списков в protobuf — осознанное, а не
   «незавершённость».** И нам для каталога оно не нужно: машинные поля не
   бывают legitimately «неизвестными».</item>
      <item>**«Мы наследуем проблемы JSON-кодировки protobuf без их выхода» — неверно
   по рамке.** Наш выход — убрать `deny_unknown_fields` (один атрибут), а не
   двоичный формат. Скалярного коллапса proto3 у нас в JSON нет вовсе.</item>
      <item>**Идентичность поля именем — компенсируется.** Свод предложил список
   отставленных имён; этого мало для rename-безопасности. Настоящий механизм —
   `#[serde(alias = …)]`, и он бесплатен в JSON и TOML.</item>
    </list>
  </section>
  <section title="0. Что я перепроверил по дереву (доверие своду — с поправками)">
    <p p="4">Свод верен в конструкции, проверенной мной `[ИЗ ДЕРЕВА]`:</p>
    <list ordered="false" p="5">
      <item>`deny_unknown_fields` в каталоге — ровно **15** в `crates/vibe-index/src`
  (`repomd.rs:1`, `entry/relations.rs:6`, `entry/mod.rs:1`, `entry/content.rs:5`,
  `entry/aggregate.rs:2`). Совпало со сводом.</item>
      <item>`#[serde(other)]` — **нигде** в дереве (подтверждает и
  `campaigns/packages-2026-09/harvest/a6-wire-format-census.md:359-360`).
  Совпало со сводом.</item>
      <item>**Версия каталога — ярлык, а не переключатель.** Единственное сравнение
  версии во всех `.rs`: `crates/vibe-core/src/manifest/lockfile.rs:430`
  `if lockfile.meta.schema_version != CURRENT_SCHEMA_VERSION` — это **lockfile**.
  Каталог читается через голый `serde_json::from_slice`
  (`crates/vibe-index/src/index/repomd.rs:32-39`), версия не сравнивается ни
  разу. Совпало со сводом.</item>
      <item>**Клиент терпим, себе строги.** `crates/vibe-registry/src/index_client/wire.rs`
  — узкие view-структуры (`VersionEntryView` читает только `version`,
  `wire.rs:30-33`), `#[serde(default)]`, без `deny_unknown_fields`; комментарий
  `wire.rs:37-39` прямо говорит: «Extra fields on the wire … are tolerated
  silently — kept simple so a server-side envelope addition does not force a
  client bump». Совпало со сводом.</item>
    </list>
    <p p="6">Поправки к своду `[ИЗ ДЕРЕВА]`:</p>
    <list ordered="false" p="7">
      <item>**Объединение описано неточно.** Свод: «Общего поля-признака нет; читатель
  угадывает по набору ключей». В дереве:</item>
    </list>
    <fence lang="rust" p="8">  // crates/vibe-index/src/types/repomd.rs:41-55
  #[serde(untagged)]
  pub enum RepomdFileEntry {
      Directory {
          /// Always the literal string `"directory"`. Carrying this as
          /// a tag inside the directory variant lets serde's `untagged`
          /// matcher distinguish unambiguously.
          kind: DirectoryTag,
          entries: u32,
      },
      File { size: u64, sha256: String },
  }</fence>
    <p p="9">У `Directory` есть позитивный тег; «по набору ключей» угадывается только
  `File`. Буква «общего поля нет» верна, но вывод «угадывание» — преуменьшение.</p>
    <list ordered="false" p="10">
      <item>**«14» против «21».** Полей с `skip_serializing_if = "...is_empty"` в
  `crates/vibe-index/src` — **21**:</item>
      <item>`Vec::is_empty` — 12 (`relations.rs:18,31,44,46,65,78`; `entry/mod.rs:72,78,96,108`; `content.rs:68,75`);</item>
      <item>`BTreeMap::is_empty` — 2 (`content.rs:39,41`);</item>
      <item>`*Entry::is_empty` (структурные) — 7 (`entry/mod.rs:87,90,93,99,102,105,111`).
  Свод считает только сырые коллекции (12+2 = **14**); но структурные поля
  вроде `provides: ProvidesEntry` коллапсируют «пусто≡отсутствует» **точно так
  же**: когда структура пуста, поле пропускается (`entry/mod.rs:90`,
  `ProvidesEntry::is_empty` в `relations.rs:35-39`). Для языка схемы релевантны
  все 21. Периметр «сырые коллекции» оправдан, но число, на котором решали А6
  (14), занижено относительно того, что схеме предстоит выразить.</item>
    </list>
    <p p="11">Не перепроверял и не считаю нагрузко-несущим для своих выводов: «23 типа / 18
каталожных / 86 полей». Принял из свода как есть.</p>
  </section>
  <section title="1. Avro — насколько хороша для нас на самом деле? (ДЕШЕВЛЕ ИЛИ ДОРОЖЕ ТЕРПИМОГО ЧИТАТЕЛЯ)">
    <section title="Что должен сделать читатель, чтобы модель Avro сработала">
      <p p="12">Модель Avro `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`: каждая запись пишется против схемы
писателя; читатель обязан добыть **именно ту схему писателя**, по которой
записано, и согласовать её со своей схемой читателя. Обычно — по отпечатку
(fingerprint), который лежит в данных, через реестр схем.</p>
      <p p="13">Чтобы это работало у нас, читатель должен:</p>
      <list ordered="true" p="14">
        <item>Извлечь из данных идентификатор схемы писателя. У нас в `repomd.json` —
   `schema_version: u32` (`repomd.rs:21`), **не** отпечаток схемы. То есть
   читатель знает «версия 1», но не знает, какой именно текст схемы
   соответствует версии 1.</item>
        <item>Где-то этот текст найти. У нас его для каталога **нет как артефакта** —
   `[ИЗ ДЕРЕВА]`: в `crates/vibe-index/` нет `.json`-схемы; схема существует
   только как Rust-типы (`types/`) и спека-проза (`PROP-005`).</item>
        <item>Запустить согласование схем.</item>
      </list>
    </section>
    <section title="Предпосылка свода неверна: это не «одна история git»">
      <p p="15">Свод `[ИЗ СВОДА]`: «в git-репозитории это стоит указателя, и история схемы
лежит в той же истории, что и данные». Но:</p>
      <list ordered="false" p="16">
        <item>Каталог (`repomd.json`, `primary.jsonl`, `by-name/*.json`) — это
  **публикованный артефакт реестра**; поле `registry`/`registry_url`
  (`repomd.rs:22-23`) указывает на репозиторий-источник. Схема (Rust-типы)
  живёт в **исходном репозитории vibevm**. Это **два разных репозитория с
  двумя разными историями**. История каталога не содержит текста схемы —
   значит, «одна история» ложно.</item>
        <item>«Схема едет с данными» — тоже ложно сегодня: рядом с `repomd.json` нет файла
  схемы. (Контрпример из дерева: для `vibe tree --json` схема **есть** —
  `crates/vibe-cli/resources/package-tree.schema.v1.json` с `$id` и
  `schema_version:{const:1}`, плюс золотой тест `crates/vibe-cli/tests/tree_json.rs`.
  Способность публиковать артефакт схемы в доме **есть** — просто не применена к
  каталогу. Это и есть настоящий Avro-смежный пробел.)</item>
      </list>
    </section>
    <section title="Читатель без репозитория / запись, скопированная наружу">
      <list ordered="false" p="17">
        <item>Внешний инструмент читает каталог из реестра; он **не клонирует** исходный
  репозиторий vibevm с Rust-типами. У него нет ни схемы писателя, ни пути к
  ней. Механика согласования Avro не запускается — он возвращается к
  «читать JSON как умею», то есть к **терпимому читателю**. Avro ему ничего не
  дал.</item>
        <item>Запись, скопированная из репозитория наружу (строка `primary.jsonl` в
  баг-репорте, зеркало метаданных пакета): нет ни версии, ни контекста — чистый
  JSON. Avro невозможен; терпимое чтение — единственный вариант. А это самый
  реальный способ, которым нашими данными будут пользоваться.</item>
      </list>
    </section>
    <section title="Вердикт: ДОРОЖЕ">
      <p p="18">Avro требует: публиковать машинно-читаемый артефакт схемы с данными (или
реестр), учить читателей его доставать и согласовывать, версионировать этот
артефакт, держать дисциплину отпечатков. Это большая машинерия — ради выгоды,
которую у нас некому потреблять (внешних читателей ноль `[ИЗ СВОДА]`, а своим
согласование схем не нужно: писатель и читатель — один бинарник).</p>
      <p p="19">Терпимый читатель требует: читать узкими view, игнорировать незнакомые поля,
отсутствующее трактовать как default. **Это уже написано** (`wire.rs`).
Маржинальная стоимость — около нуля.</p>
      <p p="20">**Ответ на вопрос: Avro для нас дороже терпимого читателя, заметно и по
машинерии, и по предпосылкам.** Настоящий (и полезный) урок Avro — не «возить
схему с данными», а «сделать схему машинно-читаемым артефактом вообще». Этого
артефакта у каталога сегодня нет; фикс — опубликовать JSON Schema / JTD (паттерн
уже есть в `vibe-cli/resources/`), а не внедрять разрешение схем Avro.</p>
    </section>
  </section>
  <section title="2. «Пусто против отсутствует» — нужна ли нам различимость списков">
    <section title="Невосстановление у protobuf — осознанный отказ, а не недоделка">
      <p p="21">Свод `[ИЗ СВОДА]`: для скаляров присутствие вернули через ~5 лет, для списков и
словарей — «так и не вернули … слиты навсегда», и подаёт это как незавершённость.</p>
      <p p="22">Я утверждаю, что это **осознанный отказ**, и вот цена различимости для списка
`[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`:</p>
      <list ordered="false" p="23">
        <item>каждое поле-список получает третье состояние (нет / пусто / заполнено);</item>
        <item>сгенерированный код несёт `Option&lt;Vec&gt;` или `has`-бит на каждое такое поле;</item>
        <item>каждый читатель обязан решить, что **семантически** значит «нет» против
  «пусто» — а это решение не универсально;</item>
        <item>любой инструмент, не понимающий присутствия, молча сколлапсирует их обратно
  (round-trip-несоответствие).</item>
      </list>
      <p p="24">Для proto3 цена особенно высока: их двоичный формат не выделяет присутствие для
repeated-полей без явного бокового маркера, и реставрация ломала бы компактность
и обратную совместимость с существующими данными. Отказ рационален.</p>
    </section>
    <section title="Главный аргумент: для машинных данных «отсутствует» — не настоящее состояние">
      <p p="25">Ключевое различие, которое свод не провёл: **присутствие нужно
человеко-написанным данным, где «я не заполнил» реально, и почти не нужно
машинно-сгенерированным, где каждое поле детерминированно вычислено.**</p>
      <p p="26">Каталог пишет индексатор. Он всегда знает значение каждого поля — читает его из
`vibe.toml` пакета. Поэтому «отсутствует» для каталожного поля — не легитимное
«неизвестно», а всегда «вычислено как пустое». Различимость пусто/нет **ничего
не покупает**, потому что состояние «нет» никогда не наступает законно.</p>
      <p p="27">Конкретно по нашим полям `[ИЗ ДЕРЕВА]`: `authors`, `keywords`, `requires.packages`,
`provides.capabilities`, `features`, `i18n.available` — все вычисляются
индексатором из манифеста; «нет» у них не бывает. Даже `requires_any`
(`entry/mod.rs:96`) — «зависимость any-of не задана» — это ограничение, а не
присутствие.</p>
    </section>
    <section title="Где различимость всё-таки нужна — и это не каталог">
      <p p="28">У манифеста `vibe.toml`, который пишет человек `[ИЗ СВОДА]`, «я не указал» —
реальное состояние. Там `authors = []` (явно: авторов нет) против отсутствующего
`authors` (не записано) — **осмысленное** различие. И TOML здесь помогает, а не
мешает: отсутствие null в TOML — это **фича**, она принуждает выражать
трёхзначность через «нет ключа» против `[]`, без null-сигнала, который можно
перетолковать. JSON и TOML соглашаются в механизме.</p>
    </section>
    <section title="Вердикт">
      <p p="29">**Различимость пусто/нет для списков нам для каталога НЕ нужна.** 21 поле со
`skip_serializing_if = "...is_empty"` безопасны для машинных данных. Присутствие
стоит сохранять только там, где «неизвестно» легитимно — то есть в
человеко-написанном манифесте. И свод сам к этому приходит в рекомендации №2
(`null не нужен: в TOML его нет`) — но он не развёл, что для каталога это
безразлично, а для манифеста — обязательно, и фиксы там разные.</p>
    </section>
  </section>
  <section title="3. Тегированные объединения — что делать с `RepomdFileEntry`">
    <p p="30">Текущая форма `[ИЗ ДЕРЕВА]` (`repomd.rs:41-77`): `#[serde(untagged)]`,
`Directory { kind: DirectoryTag, entries }`, `File { size, sha256 }`. На проводе
`{"kind":"directory","entries":N}` против `{"size":N,"sha256":"..."}`. Дефект не
в «отсутствии тега», а в `untagged`: плохие ошибки («did not match any variant»),
асимметричная разметка (один вариант мечен, второй угадывается), и — главное для
А6 — `untagged` трудно/невозможно выразить в обычном языке схемы.</p>
    <p p="31">Ниже — пять конкретных форм с ценой. A, B, D — рабочие; C — ценовой baseline
(текущее направление); E — отбраковывается.</p>
    <section title="Форма A — явное поле-дискриминатор (стандарт)">
      <fence lang="rust" p="32">#[serde(tag = "kind", rename_all = "lowercase")]
pub enum RepomdFileEntry {
    Directory { entries: u32 },
    File { size: u64, sha256: String },
}</fence>
      <fence lang="json" p="33">{ "kind": "directory", "entries": 42 }
{ "kind": "file", "size": 184522, "sha256": "..." }</fence>
      <list ordered="false" p="34">
        <item>**Цена:** каждое файловое действие дополнительно несёт `"kind":"file"` (байты);
  ломающее изменение файлового варианта (сегодня у файла нет `kind`) — но
  внешних потребителей ноль `[ИЗ СВОДА]`, сейчас это даром.</item>
        <item>**Чем платим / получаем:** чистые ошибки serde, оба варианта симметричны,
  форма выражается в JTD, JSON Schema (`discriminator`/`oneOf`), protobuf
  (`oneof`), Avro (union). Это ровно то, что **разрешает блокер А6** — тип
  становится выразимым в языке схемы в тот момент, когда мы перестаём брать
  `untagged`. По сути — один атрибут от нынешнего состояния.</item>
      </list>
    </section>
    <section title="Форма B — разные ключи верхнего уровня (разделённые карты)">
      <fence lang="json" p="35">"files":       { "primary.jsonl": { "size": 184522, "sha256": "..." } },
"directories": { "by-name": { "entries": 42 } }</fence>
      <list ordered="false" p="36">
        <item>**Цена:** одна логическая коллекция (`files: BTreeMap&lt;…&gt;`, `repomd.rs:34`)
  дробится на две; читатель обязан проверять обе; теряется единый
  детерминированный порядок обхода путей; путь не может мигрировать
  файл↔каталог без переноса ключа.</item>
        <item>**Чем платим:** фрагментация API и двойная бухгалтерия. Зато каждый кусок
  однороден и строго типизирован. Урок PyPI `[ИЗ СВОДА]` («поле, которое бывает
  bool или dict → крах») аргументирует за явную структуру — разделение на две
  типизированные карты структурно явно, но ломает инвариант «один путь → одна
  запись».</item>
      </list>
    </section>
    <section title="Форма D — вложение в именованный объект (обёртка) — нет в своде">
      <p p="37">Соседне-тегированная форма, родная для serde:</p>
      <fence lang="rust" p="38">#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
pub enum RepomdFileEntry {
    Directory { entries: u32 },
    File { size: u64, sha256: String },
}</fence>
      <fence lang="json" p="39">{ "kind": "directory", "value": { "entries": 42 } }
{ "kind": "file",      "value": { "size": 184522, "sha256": "..." } }</fence>
      <p p="40">(Одно-ключевая форма `{"directory":{…}}` / `{"file":{…}}` тоже возможна, но
требует ручной ser/deser — serde не даёт её из коробки; adjacent-тег —
серде-нативный её аналог.)</p>
      <list ordered="false" p="41">
        <item>**Цена:** лишний уровень вложенности и `value`-обёртка → больше байтов и
  глубины.</item>
        <item>**Чем платим / получаем:** максимальная самодокументируемость (читатель видит
  единственный ключ и знает вариант, не сканируя поля) и чистая
  прямая-совместимость: третий вариант `{"kind":"symlink","value":{…}}`
  добавляется без двусмысленности. Самая «будущестойкая» форма; переплата байтами
  оправдана, если вариантов со временем будет больше двух.</item>
      </list>
    </section>
    <section title="Форма C — вывод вида по обязательному полю (baseline = текущее направление)">
      <fence lang="json" p="42">{ "entries": 42 }
{ "size": 184522, "sha256": "..." }</fence>
      <list ordered="false" p="43">
        <item>**Цена:** это худшая форма — ровно то нетегированное угадывание, которому
  свод (справедливо) не доверяет, и ровно форма PyPI «bool или dict», которая
  роняла `pip` `[ИЗ СВОДА]`. Плохие ошибки, двусмысленность при совпадении имён
  полей в будущих вариантах.</item>
        <item>**Чем платим:** включена сюда только как ценовой baseline, чтобы оценить
  остальные. По сути это нынешний дизайн минус тег у `Directory`.</item>
      </list>
    </section>
    <section title="Форма E — тег в ключе карты — отбраковывается">
      <fence lang="json" p="44">"dir:by-name": { "entries": 42 }
"primary.jsonl": { "size": 184522, "sha256": "..." }</fence>
      <list ordered="false" p="45">
        <item>**Цена:** загрязняет пространство имён путей (путь — данные, не тег типа);
  путь, содержащий двоеточие, ломает схему; читатель парсит строки. Сильно не
  рекомендуется: путь — это идентичность и должен оставаться чистыми данными.</item>
      </list>
    </section>
    <section title="Рекомендация">
      <p p="46">**Форма A.** Один атрибут от нынешнего состояния, стандарт во всех языках схем,
дешёвая, и — критически — устраняет блокер А6 («тип невыразим в языке схемы»):
он невыразим **только** пока мы держим `untagged`. Если ожидается рост числа
вариантов — Форма D.</p>
    </section>
  </section>
  <section title="4. Асимметричная строгость — граница: терпимо к чему именно">
    <p p="47">Свод (рекомендация №4) делит строгость по поверхности: строго на входной двери
(`vibe.toml`, руками — ловим опечатку), терпимо дальше (каталог, машиной —
переживаем будущее). Это верно, но НЕ проработано по **видам «незнакомого»**.
Разберу пять видов.</p>
    <p p="48">**Вид 1 — незнакомое ПОЛЕ (ключ).** Терпимо = игнорировать. Это безопасный,
стандартный, прямо-совместимый выбор `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`. Цена: опечатка
в имени поля молча игнорируется (потеря данных этого поля). Парирование:
строгость на двери авторинга (валидация `vibe.toml`), где опечатки и рождаются.
→ **Терпим дальше.**</p>
    <p p="49">**Вид 2 — незнакомое ЗНАЧЕНИЕ словаря (известное поле, новый член enum,
`kind:"plugin"`).** Спорное `[ИЗ СВОДА]`: Google — не ломающее, K8s/LinkedIn —
ломающее. Механическая причина `[ИЗ СВОДА]`: неизвестное значение обязано
лечь в типизированную ячейку, перечисляющую только известное; у нас без
`#[serde(other)]` разборщик **падает**. Самая опасная терпимость — из аварии
Cloudflare `[ИЗ СВОДА]`: старый читатель молча отображал неизвестное в default и
**уверенно считал неверно» («ботовый балл ноль»). Молчаливый default
хуже отказа. → **Граница: никогда не проглатывать неизвестное значение enum в
default.** Либо отказ (строго), либо явный вариант `Unknown(String)`, который
потребитель обязан обработать. Причём граница **зависит от поля**: `kind`
примыкает к идентичности пакета — неизвестный kind = читатель не может даже
категоризировать, отказ правилен; `delivery` (`content.rs:51-57`,
eager/lazy-push/lazy-pull) — это подсказка, не идентичность, тут `Unknown` +
мягкая деградация оправданы. Рецепт LinkedIn `[ИЗ СВОДА]`: новое необязательное
поле с новым enum, старые символы поддерживаются бессрочно.</p>
    <p p="50">**Вид 3 — незнакомый ТИП значения (строка там, где число, `"size":"184522"`).**
Это **не эволюция, а порча.** Несоответствие типа = фундаментальное расхождение
схем; коэрцция («строка-как-число → число») — в точности патология «будь
либерален» из RFC 9413 `[ИЗ СВОДА]`: дефект закрепляется как стандарт де-факто.
→ **Твёрдая черта: несоответствие типа = отказ.** Терпимость сюда = проглатывание
мусора.</p>
    <p p="51">**Вид 4 — лишний ЭЛЕМЕНТ в списке.** Два подслучая:</p>
    <list ordered="false" p="52">
      <item>(а) Список длиннее, чем ждёт читатель (аппаратный лимит у потребителя). Это
  авария Cloudflare `[ИЗ СВОДА]`: сгенерированный файл вырос вдвое, у
  развёрнутых потребителей был зашит предел → глобальный сбой. Урок: читатели
  **не должны** хардкодить потолок длины открытых списков. → **Терпим длину
  (читаем все).**</item>
      <item>(б) Элемент неожиданного ТИПА в однородном списке (строка в `Vec&lt;u64&gt;`). То
  же, что Вид 3. → **Отказ.**</item>
    </list>
    <p p="53">**Вид 5 — незнакомый КЛЮЧ в закрытой карте.** Для `BTreeMap&lt;String, X&gt;`
незнакомые ключи обычно безопасны (карты открыты по природе). Но если значения
типизированы/enum — к значениям применяется правило Вида 3. Наш
`features: BTreeMap&lt;String, Vec&lt;String&gt;&gt;` (`content.rs:39`) открыт по ключам и
строковый по значениям → полностью терпим.</p>
    <section title="Где терпимость перестаёт быть эволюцией">
      <p p="54">В тот момент, когда она **отображает нераспознанный вход в определённое
значение, на котором потребитель потом действует** (паттерн Cloudflare «score
zero»). Молчаливый default неизвестного значения enum — единственная самая
опасная терпимость. Добавление (новые поля, новая длина, новые ключи карты) —
терпимо; нарушение типа — нет; новые значения enum — не молча в default.</p>
    </section>
    <section title="Уточнение к рекомендациям свода">
      <p p="55">«Строго на двери, терпимо дальше» — верно, но **недоспецифицировано на enum'ах**.
Я бы уточнил: дверь (`vibe.toml`) отвергает и неизвестные значения enum, и
опечатки; каталог (вниз по потоку) терпит незнакомые **поля**, но всё равно
**не** молча дефолтит неизвестные **значения** enum — несёт их как `Unknown`,
чтобы будущий читатель мог ими воспользоваться. Это более острая граница, чем
просто «терпимо дальше».</p>
    </section>
  </section>
  <section title="5. Что из механики protobuf нам НЕ подходит — и наследуем ли мы проблемы JSON">
    <section title="Что НЕ переносится">
      <list ordered="false" p="56">
        <item>**Числа как идентичность поля** — ядро protobuf. См. §6: у нас имена. Мы **не**
  наследуем их rename-безопасность, но она нам и не нужна при правильной
  дисциплине имён.</item>
        <item>**Двоичный формат** — нерелевантен: мы публикуем JSON для читаемости людьми и
  инструментами, компактность не покупаем.</item>
        <item>**Драма с удалением `required`** `[ИЗ СВОДА]`: protobuf убрал `required`
  из-за долго живущих типов через границы организаций. У нас один писатель и ноль
  внешних потребителей `[ИЗ СВОДА]` — обязательные поля (`VersionEntry` полон
  них, `entry/mod.rs:43-121`) **нормальны и полезны**. Урок «`required` — зло»
  нельзя тащить оптом: он настроен на их развёртывание (много организаций, типы
  на десятилетия). Я бы локализовал: `required` зол лишь когда читатели, которых
  ты не контролируешь, могут быть вынуждены синтезировать значение. Писателя
  контролируем мы. Другой случай.</item>
        <item>**Коллапс присутствия скаляров в proto3** `[ИЗ СВОДА]`: proto3 не различает
  «нет» и default для скаляров. В JSON этого **нет**: отсутствие ключа, `0` и
  `null` — три разных состояния. Так что скалярного коллапса proto3 мы вообще
  **не наследуем**.</item>
      </list>
    </section>
    <section title="Наследуем ли мы «проблемы JSON-кодировки без выхода» — НЕ согласен с рамкой">
      <p p="57">Свод `[ИЗ СВОДА]`: JSON-кодек protobuf по умолчанию отвергает незнакомые поля, а
на жалобы ответ — «используйте двоичную»; мы наследуем весь набор проблем их
JSON без их выхода.</p>
      <p p="58">Я не согласен с рамкой. «JSON-проблема» protobuf в том, что их JSON строг, а
двоичный — терпим, и недовольных отсылают в двоичный. **Нас никто не заставляет
быть строгими в JSON:** serde позволяет выбрать `deny_unknown_fields` или нет.
Сейчас мы **сами выбрали** строгость в 15 местах своего читателя и терпимость —
в клиенте. Так что «выход» (терпимое чтение) тривиально доступен нам убиранием
одного атрибута — двоичный формат для выхода не нужен. Свод подаёт так, будто
двоичный protobuf — единственный выход из строгого JSON; для нас выход —
терпимый JSON, и он бесплатен.</p>
      <p p="59">Что мы реально наследуем от JSON-без-схемы: не «проблемы protobuf», а проще —
**в JSON нет схемы, поэтому дисциплина эволюции живёт целиком в головах +
чекере.** Это и есть урок Cloudflare `[ИЗ СВОДА]`: ужесточить приём собственных
сгенерированных файлов так же, как пользовательский ввод. И — повторю из §1 —
артефакт схемы для каталога у нас отсутствует, хотя способность его публиковать
в доме есть (`vibe-cli/resources/package-tree.schema.v1.json`).</p>
    </section>
  </section>
  <section title="6. Номера полей против имён — что теряем и возмещаемо ли">
    <p p="60">У protobuf идентичность поля — число; у нас — имя (JSON/TOML ключ). Урок
protobuf `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`: переименование Rust-поля безопасно (число
остаётся), переименование НЕ является изменением провода.</p>
    <p p="61">**Что мы теряем, используя имена:**</p>
    <list ordered="false" p="62">
      <item>**Rename-безопасность.** У нас переименование поля (`author` → `authors`) —
  это изменение провода: старые читатели ищут «author» и не находят «authors».</item>
      <item>Имена утекают в идентичность: на «более удачное имя» возникает ломающее
  искушение, которого числа бы не дали (обратная сторона: имена
  самодокументируемы).</item>
    </list>
    <p p="63">**Возмещаемо ли в JSON и TOML — да:**</p>
    <list ordered="false" p="64">
      <item>**Список отставленных имён** (рекомендация свода №6) — необходим, но
  **недостаточен**: он предотвращает коллизию, но **не даёт** rename-безопасности,
  лишь делает переименования явно-ломающими-и-отслеживаемыми.</item>
      <item>**Алиасы при десериализации** — настоящий механизм, которого свод **не назвал**:</item>
    </list>
    <fence lang="rust" p="65">  #[serde(alias = "author")]
  pub authors: Vec&lt;String&gt;,</fence>
    <p p="66">читатель принимает **и** «author», **и** «authors», обратно пишет «authors».
  Это и есть rename-безопасность, дёшево, в чистом JSON. Цена: алиас живёт в
  читателе (или пока все старые данные не мигрированы); писатель выпускает только
  новое имя; round-trip нормализует. Для каталога, переписываемого при каждом
  индексировании, миграция автоматична (следующая сборка пишет новые имена).</p>
    <list ordered="false" p="67">
      <item>**В TOML** алиас работает так же — мы читаем через `toml::from_str` (как в
  `crates/vibe-index/src/lockfile.rs:48`), serde-атрибуты едины для форматов.
  Так что компенсация одинакова для JSON и TOML; специфика TOML — не идентичность
  поля (ключи — те же строки), а отсутствие null (см. §2).</item>
    </list>
    <p p="68">**Вердикт:** мы теряем rename-безопасность и непрозрачную идентичность; **полностью
возмещаемо** через `#[serde(alias)]` + список отставленных имён + автоматическое
переиндексирование. Числа нам не нужны. Числа побеждают только в высокочурном
сетевом протоколе со многими независимыми командами; мы — читаемый человеком
формат данных в покое, здесь имена + алиасы лучше.</p>
  </section>
  <section title="С чем я согласен — и что добавляю">
    <list ordered="false" p="69">
      <item>**Терпимый по умолчанию каталог прав** (клиент уже таков). Добавляю: каталог
  прямо-совместим ровно благодаря терпимому читателю — поэтому **собственный
  строгий читатель сервера (15 `deny_unknown_fields`) — аномалия**, и именно она
  делает так, что новый сервер не запустится на каталоге, записанном ещё более
  новым (`repomd.rs:15` + `index/repomd.rs:38`).</item>
      <item>**Строго на двери, терпимо дальше** — согласен; **уточнил границу по enum'ам**
  (§4: reject или `Unknown`, никогда молча в default).</item>
      <item>**Контракт версии (старшая — отказ, младшая — предупреждение, отсутствие —
  1.0)** — согласен. Добавляю: сегодня версия каталога **не сравнивается ни разу**
  (`index/repomd.rs:32-39`), то есть у нас ярлык версии без контракта; пробел — в
  отсутствии самого сравнения, а не в форме контракта.</item>
      <item>**Каждое объединение — тегированное** — согласен (§3, Форма A).</item>
      <item>**Не переиспользовать имена, держать список отставленных** — согласен; **добавил
  алиасы** как недостающий механизм rename-безопасности (§6).</item>
    </list>
  </section>
  <section title="Итоговый вердикт">
    <p p="70">Из шести пунктов: в трёх (Avro, списки-присутствие, JSON-наследство protobuf) я
меняю вывод свода; в одном (объединение) поправляю факт и предлагаю пять форм; в
двух (строгость, номера полей) уточняю и добавляю. Ни одного пункта не принял
без добавления. Центральная конструктивная находка поверх свода: **способность
публиковать машинно-читаемый артефакт схемы и версионировать его по пути уже
существует в этом репозитории** (`package-tree.schema.v1.json` + `jsonschema` +
золотой тест) — просто не применена к каталогу; многие «теоретические» ответы
свода здесь уже имеют рабочий образец для копирования.</p>
  </section>
</spec>
