# G3-CLIENTS — словари, версии и то, что не переносится

[p01] Второй независимый взгляд на `FINDINGS-DIGEST.md`. Предмет — как чужой
инструмент, написанный год назад, переживёт встречу с нашими сегодняшними
данными, и насколько индустриальный «мобильный» опыт к нам применим.

[p02] **Разметка источников.** У меня нет интернета. Каждое утверждение о внешнем
мире несёт метку:

- [p03] `[ИЗ СВОДА]` — из `FINDINGS-DIGEST.md`;
- `[ИЗ ДЕРЕВА]` — проверено мной чтением нашего кода, с `файл:строка`;
- `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — мои знания о чужих системах; без выдуманных
  версий/дат/номеров issue.

[p04] Я не запускал git и cargo — историю словарей читаю по amendment-логу в спеке
и док-комментариям, а не по `git log`.

## 0. Сначала — что я перепроверил по дереву и где свод неточен

[p05] Несколько утверждений свода я проверил дословно. Они в основном верны, но в
двух местах свод сам себе противоречит по сравнению с первоисточником
(harvest `a6-wire-format-census.md`) и кодом. Это не придирки — это та же
болезнь, о которой весь документ: документация дрейфует от кода.

[p06] **Верно и подтверждено:**

- [p07] **5 закрытых словарей, неизвестное значение = ошибка разбора.**
  `PackageKind` (6 значений), `NamingConvention` (4), `DeliveryMode` (3),
  `DirectoryTag` (1), `BindingSite` (2) — все fieldless enum с производным
  `Deserialize` `[ИЗ ДЕРЕВА kinds.rs:21,87; content.rs:53; repomd.rs:75;
  inverted.rs:89]`. `#[serde(other)]` в дереве нет нигде — мой grep нашёл его
  только в `FINDINGS-DIGEST.md` и в harvest-файле `[ИЗ ДЕРЕВА]`.
- **Версия каталога — ярлык, а не переключатель.** `load_from` кладёт
  `schema_version: manifest.schema_version` в память без единого сравнения
  `[ИЗ ДЕРЕВА memory.rs:262-280]`. `Repomd::SCHEMA_VERSION = 1`
  `[ИЗ ДЕРЕВА repomd.rs:38]`.
- **`deny_unknown_fields` на каталог-записях делает неизвестное поле
  ломающим.** Подтверждено harvest 3.7; ключевой файл — `repomd.rs:15`
  (`#[serde(deny_unknown_fields)]` на `Repomd`) `[ИЗ ДЕРЕВА]`.

[p08] **Где свод неточен (пункт войдёт в «С чем я не согласен»):**

- [p09] Свод: объединение `RepomdFileEntry` — «общего поля-признака нет; читатель
  угадывает по набору ключей» `[ИЗ СВОДА]`. Код и harvest говорят другое:
  внутри варианта `Directory` есть поле `kind: DirectoryTag` (всегда
  `"directory"`), а наборы ключей вариантов `{kind,entries}` и
  `{size,sha256}` **не пересекаются** — разбор сегодня **однозначен**
  `[ИЗ ДЕРЕВА repomd.rs:43-55]`. Опасность у объединения есть, но она в
  *другом* месте (см. §5), не в неоднозначности.

## 1. Критерий переносимости — проверка на прочность

[p10] Свод формулирует `[ИЗ СВОДА]`: *переносится то, что свойство артефакта или
своей дисциплины; свойство отношений (переговоры, наблюдение, принуждение,
трансляция) умирает при соприкосновении с файлом в git.*

[p11] Проверяю двумя попытками.

### 1а. Практика, которую критерий пропускает («проходит»), но у нас не работает

[p12] **Avro «клади схему писателя рядом с данными»** — свод сам относит её к
свойствам артефакта и замечает, что в git-репозитории это «стоит указателя»
`[ИЗ СВОДА]`. По критерию она должна переноситься. Но для нашего случая она
ломается на самом интересном формате — **`vibe.toml`**, манифесте, который
пишут руками `[ИЗ СВОДА часть 0]`.

[p13] Avro-рецепт самосогласовывает *машинную* схему писателя с *машинной* схемой
читателя `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`. Перенесённый в hand-authored TOML, он
становится циркулярным: автор должен вписать в свой манифест схему, которая
валидирует его же писание. Схема перестаёт быть внешним авторитетом и
становится самоотчётом. Для каталога-JSON (пишется машиной) это ещё
разумно; для `vibe.toml` — нет. **Проходит критерий, не работает у нас** —
на руках-пишущем формате.

[p14] Зеркальный пример, тоже «артефакт/дисциплина»: **«разборщик должен по
умолчанию отвергать незнакомые поля»** (JSON-кодировка protobuf, по своду)
`[ИЗ СВОДА]`. Это дисциплина-свойство — критерий пропускает. Но у нас она
**вредна**: мы сами строгий читатель собственного каталога, и именно эта
строгость (через `deny_unknown_fields`) ломает forward-совместимость, которую
спека обещает `[ИЗ СВОДА]`. Критерий говорит «переносится», практика у нас —
антипаттерн.

[p15] **Вывод 1а:** критерий различает «переносится по форме», но не «полезно по
существу». Артефакт-свойство может перенестись и при этом навредить.

### 1б. Практика, которую критерий бракует («отношения»), но у нас работает

[p16] Возьмём **наш собственный parity-тест** между двумя копиями `PackageKind` —
в `vibe-core` (`kind.rs:31`) и в `vibe-index` (`kinds.rs:21`). Дубликат
сделан сознательно «standalone redistribution beats workspace re-use», и
тест-на-расхождение ловит их в CI `[ИЗ ДЕРЕВА kinds.rs:1-5]`.

[p17] Это **отношение** между двумя точками кода, удерживаемое не свойством ни
одного из артефактов, а *тестом* — то есть наблюдением/принуждением в CI. По
букве критерия — «свойство отношений», должен умирать. **Он работает.** И
работает именно потому, что обе стороны отношения — *наши*, в одном репозитории.

[p18] Это и есть уточнение. Действительная ось критерия — не «артефакт против
отношений», а **«контролируешь ли ты контрагента»**. «Отношения умирают» —
это прокси для «внешнего контрагента не согнёшь», и прокси ошибается на
внутренних отношениях (parity-тест, согласование с нашим собственным
клиентом `index_client`). Тот же прокси ошибается в обратную сторону на
нуле внешних читателей: PyPI-рецепт «обзвони троих читателей перед
переименованием» `[ИЗ СВОДА]` — отношение, «умирает» по критерию, а у нас
при нуле читателей он **тривиально дёшев** (звонить некому). Окно, где он
работает, — ровно то, в котором мы сейчас стоим, и ровно то, которое надо
закрыть, пока оно открыто `[ИЗ СВОДА «поле, которое отдаёшь восемь месяцев…»]`.

[p19] **Вывод 1б (усиление критерия).** Я не нашёл практики, которая *по существу*
опровергала бы критерий — честно об этом сообщаю. Но нашёл, что его
дискриминирующая ось сформулирована неточно. Переформулировка:

> [p20] Переносится то, чей контрагент тебе подконтролен. Артефакт и собственная
> дисциплина — частные случаи «контрагент — ты сам». Отношения гибнут, когда
> контрагент внешний и неподконтрольный; выживают, когда внутренний.

[p21] При этой формулировке «passes-but-broken» из §1а объясняется иначе: Avro и
protobuf-отвержение переносятся как *форма*, но их полезность зависит от
контрагента-читателя, которого у нашего `vibe.toml` пока нет, а у каталога
есть — и он мы же.

## 2. Словари — что такое «запасное значение» на уровне формы данных

[p22] Это самый острый пункт. Свод утверждает `[ИЗ СВОДА]`: незнакомое значение
обязано быть положено в типизированную ячейку, перечисляющую только
известное, значит запасное значение надо заводить **заранее**, иначе
никогда; «ни одну нельзя внедрить в разошедшихся читателей». Механическая
причина верна. Вопрос — **что именно** класть, и во что это нам обойдётся
в коде, который сегодня по словарям ветвится.

[p23] Сначала фиксирую поверхность ветвления (это нужно для оценки форм):

- [p24] `PackageKind` ветвит код через `Display` (путь на диске
  `vibedeps/<kind>-<name>/<version>`, **равномерный по видам**, без
  per-вариантного match) `[ИЗ ДЕРЕВА vibedeps.rs:40-42]`; через
  исчерпывающие `match` в `as_str`/`from_str`/`ALL`
  `[ИЗ ДЕРЕВА kind.rs:57-98]`; через сравнение на равенство в фильтрах
  поиска (`--kind`, grep по дереву показывает equality-употребления, а не
  match).
- `NamingConvention` ветвит через **исчерпывающий** `match` в `repo_name`,
  производящий разные имена репозиториев `[ИЗ ДЕРЕВА kinds.rs:128-135]`.
- `DeliveryMode` ветвит через `requires_description` — но это **`matches!`,
  не исчерпывающий match**: `matches!(self, LazyPush | LazyPull)`
  `[ИЗ ДЕРЕВА subskill.rs:144-146]`.

[p25] Это смешанная поверхность — и она решает всё ниже.

### Форма A — вариант-«неизвестное» через `#[serde(other)]` с потерей исходной строки

[p26] Добавить fieldless-вариант `Unknown` с `#[serde(other)]` `[ПО ПАМЯТИ, НЕ
ПРОВЕРЕНО: serde так маршрутизирует любое нераспознанное строковое значение
в один конкретный unit-вариант]`. Что видит читатель, встретивший
`"app"`: разбирается успешно, попадает в `Unknown`.

[p27] **Что делает с нашим кодом:**

- [p28] `as_str`/`repo_name` — исчерпывающие `match`: **откажет компилятор**, пока
  не решишь, во что `Unknown` рендерится и какой путь для него строить.
  Это *хорошо* — Rust водит за руку.
- `requires_description`-тип (`matches!`) и фильтры-на-равенство: **молча**
  провалятся. `Unknown`-вид не будет находиться фильтром `--kind` (пакет
  просто станет «невидимым» для фильтра); `Unknown`-доставка молча получит
  «описание не требуется». Компилятор не пикнет.
- **Смертельное для нас:** `#[serde(other)]` — deserialize-only и не хранит
  исходную строку `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`. А наш каталог **читает сам
  себя и переписывает** (harvest 3.6: 6 путей `write_to` поверх
  существующего каталога — `add`, `remove`, `reindex`, серверные
  upsert/delete `[ИЗ ДЕРЕВА]`). После одного цикла `add` исходное `"app"`
  превратится в `"unknown"` и **истина потеряна навсегда**. Форма А
  разрушительна именно для само-перезаписывающегося каталога.

### Форма B — поле-строка с отдельным списком известных (открытый словарь)

[p29] Заменить `kind: PackageKind` на `kind: String` (или newtype `KindStr`), а
`KNOWN_KINDS` держать отдельным `const`-списком для UI/валидации. Что видит
читатель: разбирается **всё**, исходная строка сохранена, round-trip
идеален.

[p30] **Что делает с нашим кодом:**

- [p31] Путь по виду через `Display` — остаётся рабочим (строка интерполируется).
- `repo_name` теряет **исчерпывающий match** — вместо него строковый
  `match`/`if` по четырём значениям `NamingConvention`: типобезопасность
  уходит в рантайм-проверки, разбросанные по коду, опечатка в строке —
  не ошибка компиляции, а тихой неправильный путь.
- Фильтр `--kind` начинает работать «по совпадению строк» — включая
  опечатки и регистр.
- Вся гарантирующая сила «через каталог течёт только валидный вид»
  переезжает из системы типов в дисциплину. Это именно тот разворот,
  который индустрия проделала с закрытых на открытые словари `[ИЗ СВОДА]`,
  но мы при этом **выбрасываем самый дешёвый инструмент корректности**,
  который у нас есть, — exhaustiveness.

### Форма C — рецепт LinkedIn: новое поле с новым перечислением, старое вечно

[p32] Оставить `kind: PackageKind` закрытым (6 значений), добавить *новое*
необязательное поле (`kind_v2: PackageKindV2`, начиная с `app`), а
потребителей, не знающих нового поля, держать на старом символе «бессрочно»
`[ИЗ СВОДА]`. Что видит читатель: старый читатель новое поле не видит
(если он терпим).

[p33] **Что делает с нашим кодом — и здесь спотыкается о неявную предпосылку:**

- [p34] LinkedIn-рецепт **предполагает терпимого читателя**. У нашего каталога
  читатель строгий: `VersionEntry` несёт `deny_unknown_fields`
  `[ИЗ ДЕРЕВА, harvest 3.7]`. Новое поле на записи сделает так, что
  **старый строгий читатель не прочитает каталог вообще** — не «не заметит»,
  а упадёт. Значит, Форма C у нас **имеет前置-условие**: сначала убрать
  `deny_unknown_fields` (или перевести собственное чтение на tolerant
  view-структуры, как уже сделано в клиенте `[ИЗ ДЕРЕВА wire.rs:14-16]`), и
  только потом заводить второе поле. Рецепт этого не упоминает.
- Чем заполнять *старое* `kind` для пакета, который по сути `app`?
  Ближайшего старого символа нет (это новый жанр) `[ИЗ ДЕРЕVA
  VIBEVM-SPEC.md:178 — app это отдельный жанр]`. Придётся либо лгать
  (`tool`?), либо вводить сигнальное значение в закрытый словарь — то есть
  расширять тот самый словарь, который рецепт обещал не трогать.
- Дубликат-по-паритет (`kind.rs` ↔ `kinds.rs`) придётся плодить и для
  нового поля: стоимость дублирования **умножается** на каждое новое
  перечисление `[ИЗ ДЕРЕВА kinds.rs:1-5]`.
- Каждое чтение `kind` (а их много: `VersionEntry.kind`,
  `CompatibilityEntry.requires_kinds[]`, `CapabilityRow.kind`,
  `PurlRow.kind`, `SearchHit.kind`, путь в `vibedeps`) обязано научиться
  `kind_v2.or(kind)` — permanently.

### Синтез по формам

- [p35] **Форма А** дёшева при десериализации, но **разрушает данные при
  перезаписи** и молча ломает `matches!`/equality-ветви. Для нашего
  само-перезаписывающегося каталога — неприемлема.
- **Форма В** round-trip-безупречна, но **сдаёт exhaustiveness** — наш
  главный козырь, который индустрия как раз не имела и оттого тянулась к
  открытым словарям.
- **Форма С** сохраняет типы, но **требует терпимости, которой у каталога
  нет**, заставляет вечно лгать в старом поле и удваивает дубликаты.

[p36] **Что из этого вытекает для нас, а не для индустрии вообще:** ни одна форма
не дёшева, потому что у нас есть два свойства, которых у «среднего» случая
нет — (1) каталог перечитывает и переписывает сам себя (убивает Форму А),
(2) наш код ветвит словари исчерпывающими match'ами (делает Форму В
болезненной, а добавление варианта — compile-driven, см. ниже). Самый дешёвый
для **`PackageKind`** путь — не любая из трёх «форм запаса», а **одношаговое
расширение закрытого enum'а** (добавить `App`) плюс настоящая версия-контракт,
потому что exhaustiveness сам проведёт по всем ветвям. Подробнее — §3 и §6.

## 3. Рецепт LinkedIn, посчитанный на нашем случае (6 → 7)

[p37] У нас шесть видов (`flow`, `feat`, `stack`, `tool`, `mcp`, `lang`)
`[ИЗ ДЕРЕВА kind.rs:31-54]`, и по записи планируется седьмой — **`app`**,
«anticipated as a future kind and deliberately not yet specified»
`[ИЗ ДЕРЕВА VIBEVM-SPEC.md:178]`. Свод предлагает LinkedIn-рецепт для
неконтролируемых потребителей `[ИЗ СВОДА]`. Считаем его на нашем переходе
6→7.

[p38] **Что пришлось бы сделать по LinkedIn:**

1. [p39] Оставить `kind` (6 значений) навсегда; ввести `kind_v2: Option<PackageKindV2>`
   с перечислением из одного `App`.
2. **Предварительно** сделать каталог терпимым к новым полям (убрать
   `deny_unknown_fields` с `VersionEntry` или перевести собственное чтение
   на view-структуры) — иначе старый строгий читатель не поднимется на
   каталоге с новым полем `[ИЗ ДЕРЕВА, harvest 3.7]`.
3. Заполнить старое `kind` для `app`-пакетов «ближайшим» символом — которого
   нет по смыслу; выбрать ложное значение и задокументировать ложь.
4. Научить **каждое** употребление `kind` смотреть в `kind_v2` при наличии.
5. Продублировать parity-механизм на второе перечисление `[ИЗ ДЕРЕВА
   kinds.rs:1-5]`.
6. Поддерживать обе оси жанра **бессрочно** `[ИЗ СВОДА]`.

[p40] **Что даёт одношаговое расширение закрытого enum'а:**

1. [p41] Добавить `App` в `PackageKind` в двух местах (с parity-тестом, который
   уже есть и сам поймает рассинхрон) `[ИЗ ДЕРЕВА kinds.rs:1-5]`.
2. Исчерпывающие match'и (`as_str`, `repo_name`, scanner-маппинг) откажут
   компилироваться, пока не обработаешь `App` везде — **Rust водит за руку**.
3. Поднять `schema_version` каталога и поставить gate (см. §6).
4. Новые строгие читатели, собранные со знанием `App`, работают; старые —
   см. ниже.

[p42] **Сравнение стоимости.** LinkedIn-рецепт существует для ситуации «у тебя
неконтролируемые потребители, которых нельзя согнуть» `[ИЗ СВОДА]`. У нас
**внешних потребителей ноль** `[ИЗ СВОДА часть 0]`. Платить permanent
dual-field + вечную ложь в старом поле + удвоенный parity +前置-работу по
терпимости — за переход **из шести в семь**, при пустом поле внешних
читателей, — это **лекарство хуже болезни**. Рецепт решает не нашу задачу.

[p43] **Где рецепт всё-таки оседлает нас.** Свод честно отмечает: приём «не работает
для данных, сохранённых в Avro» — в покое становится хуже `[ИЗ СВОДА]`. У
нас «в покое» — это ровно режим каталога. Так что даже если бы мы захотели
LinkedIn-рецепт «на будущее», данные в репозитории зафиксируют его стоимость
навсегда — ровно тот эффект, которого рецепт стремится избежать для
неконтролируемых читателей, только обращённый на нас самих.

[p44] **Вердикт.** Для `PackageKind` 6→7 при нуле внешних потребителей —
одношаговое расширение закрытого enum'а много дешевле и не несёт постоянных
издержек. LinkedIn-рецепт отложить до момента, когда появится
неконтролируемый читатель, — и тогда вместе с ним внедрять терпимость
(убирать `deny_unknown_fields`), а не второе поле.

## 4. Бюджет вместо правила — применимо ли к нашему словарю видов

[p45] Защита Google, по своду: перечисления должны получать новые значения не
чаще раза в год; для чаще меняющегося — использовать строку `[ИЗ СВОДА]`.

[p46] **Как часто менялся наш словарь видов — из того, что читаемо без git:**

- [p47] Исходно — четыре вида (`flow`, `feat`, `stack`, `tool`); об этом ещё
  помнят якорь спека `{#four-installable-kinds}` `[ИЗ ДЕРЕВА
  VIBEVM-SPEC.md:176]` и док-комментарий «One of the four installable
  package kinds… adding a fifth kind is a spec change» `[ИЗ ДЕРЕВА
  kind.rs:16,18-19]`.
- Пятое — `mcp`, через PROP-027 `[ИЗ ДЕРЕВА kind.rs:36-40]`.
- Шестое — `lang`, «owner ruling of 2026-08-06… The register was five until
  that date» `[ИЗ ДЕРЕВА VIBEVM-SPEC.md:180; kind.rs:46-53]`.
- Седьмое — `app`, «anticipated», в enum ещё не внесён `[ИЗ ДЕРЕВА
  VIBEVM-SPEC.md:178]`.

[p48] Итого — два расширения за жизнь проекта, третье планируется. Сам спек
формулирует дисциплину жёстче любого внешнего бюджета: «kind set is a
closed register that grows only by an owner-sanctioned amendment to this
section — it is terminology discipline, not an architectural ceiling»
`[ИЗ ДЕРЕВА VIBEVM-SPEC.md:178]`.

[p49] **Применение бюджета Google:** наш темп **заметно реже раза в год** и по
амендмент-логу, и по замыслу спека. Бюджет Google в этом случае говорит
обратное тому, что легко предположить при беглом чтении свода: **не «уводи
в строку», а «оставайся enum'ом»**. То же верно для остальных словарей —
`BindingSite` (2), `DirectoryTag` (1), `DeliveryMode` (3), `NamingConvention`
(4) `[ИЗ ДЕРЕВА]`: все меняются редко, все проходят порог «раз в год».

[p50] **Несогласие с импликацией свода.** Свод приводит бюджет Google нейтрально,
но в контексте «про словари консенсуса нет… четыре организации независимо
изобрели третью категорию "опасное изменение"» `[ИЗ СВОДА]` он читается как
давление в сторону строк. Применённый к нашим **фактическим** темпам, он —
защита статус-кво (закрытых enum'ов), а не аргумент за открытие. Цитата
верна `[ИЗ СВОДА]`; направление, в которое её тянет контекст, — к нам
неприменимо.

[p51] **Где бюджет всё же бьёт.** Бюджет — про *частоту*, а наш риск — про
*механику*. Даже редкое расширение сегодня **ломает каталог**, потому что
(а) закрытый enum без `#[serde(other)]` даёт ошибку разбора на новом
значении `[ИЗ ДЕРЕВА]`, (б) `deny_unknown_fields` не даст пережить даже
сопровождающие новые поля `[ИЗ ДЕРЕВА]`. То есть проблема не в том, что мы
расширяемся слишком часто (мы — нет), а в том, что **одно** расширение при
нынешней строгости обходится аварией. Бюджет Google здесь ни при чём;
помогает версия-контракт (§6) и терпимость по происхождению (§6, переклика с
рекомендацией свода №4 `[ИЗ СВОДА]`).

## 5. Урок Cloudflare — где наш старый читатель разобрал бы успешно и понял неверно

[p52] Это пункт, ради которого задача существует. Cloudflare-сценарий `[ИЗ СВОДА]`:
версионный перекос → новые узлы ошибаются, **старые ошибок не видят и считают
неверно**; старый читатель работает молча, неправильно и уверенно. Ищу в
нашем каталоге места, где добавление (безопасное по замыслу) приведёт к
«разобрал успешно, понял неверно». Нашёл четыре, в порядке опасности.

### 5.1. Объединение `RepomdFileEntry` молча глотает аддитивные поля

[p53] `[ИЗ ДЕРЕВА repomd.rs:42-55]` — `#[serde(untagged)]`, без
`deny_unknown_fields`. Это **первый файл, который читатель открывает**
(`repomd.json` — манифест каталога `[ИЗ ДЕРЕВА repomd.rs:1-3]`).

[p54] Механика: serde `untagged` перебирает варианты по порядку и без
`deny_unknown_fields` **лишние ключи не мешают матчу** (подтверждено harvest
3.3: «лишний ключ в файловой записи… всё ровно сматчится с File»). Будущий
`File`-вариант с дополнительным полем (условный `executable` или
`compression`) **старым читателем разбирается как `File`**, а новое поле
молча теряется. Читатель «успешен», модель — неполна/неверна. Это точный
аналог Cloudflare: версия ушла вперёд, старый узел «работает», смысл утерян
молча.

### 5.2. `schema_version` читается, но нигде не сравнивается — инерция делает любой перекос невидимым

[p55] `[ИЗ ДЕРЕВА memory.rs:262-280]` — `load_from` копирует
`schema_version: manifest.schema_version` в память **без единого `==`/`match`**
(подтверждено harvest 3.5: «Ни одного сравнения»). Поле существует, но
инертно.

[p56] Cloudflare-параллель прямая: «версионный перекос изменил вид отказа». У нас
поле версии **не способно выполнить свою единственную работу** — обнаружить
рассогласование. Если будущая старшая версия переопределит смысл
существующего поля (не добавит, а переопределит), старый читатель прочитает
без ошибки и истолкует по-старому. Это и есть «разобрал успешно, понял
неверно» — и ускорено именно тем, что版本-поле мертво.

### 5.3. Inverted-ряды без `deny_unknown_fields` — тихая потеря

[p57] `[ИЗ ДЕРЕВА inverted.rs:66,75]` — `CapabilityRow` и `PurlRow` единственные
каталог-типы **без** `deny_unknown_fields` (harvest 3.7/3.6). На пути
«прочитал → в память → переписал» аддитивное поле здесь **молча
выбрасывается**. Меньше «понял неверно», чем «тихо обеднил»: если будущее
поле несёт семантику (условный `scope` у capability), старый читатель будет
обслуживать индекс ограниченных возможностей, не зная об этом.

### 5.4. `DeliveryMode::requires_description` — Cloudflare внутри нашего же кода

[p58] `[ИЗ ДЕРЕВА subskill.rs:144-146]` — `matches!(self, LazyPush | LazyPull)`, не
исчерпывающий match. Добавь четвёртый режим доставки и забудь обновить
руку — новый режим **молча** получит «описание не требуется», без ошибки
компиляции. Это та же механика на микроуровне: схема выросла, читатель
продолжил «работать», поведение тихо неверное. Не внешний старый читатель —
наш собственный код про наш собственный словарь.

### Что с этим делать (Cloudflare-вывод в починку)

[p59] Сам Cloudflare-вывод, по своду: «ужесточить приём собственных сгенерированных
файлов так же, как для пользовательского ввода» `[ИЗ СВОДА]`. На нашем языке
это значит: **тот, кто пишет каталог, и тот, кто его читает, — один процесс,
и визировать свой же вывод надо так же строго, как чужой**. Конкретно:

- [p60] 5.1 → дать объединению внешний тег (рекомендация свода №1 `[ИЗ СВОДА]`)
  **или** хотя бы `deny_unknown_fields` на вариантах, чтобы аддитивное поле
  не глоталось молча, а отказывало внятно;
- 5.2 → версия обязана стать переключателем (§6), иначе она бесполезна;
- 5.3 → либо `deny_unknown_fields` на inverted-рядах, либо осознанное решение
  «эти ряды всегда перегенерируются, им не нужна round-trip переносимость»
  (harvest 3.6: их и не перечитывают `[ИЗ ДЕРЕВА]`) — это **легальный**
  выход, если зафиксировать его явно;
- 5.4 → превратить `matches!` по словарным значениям в исчерпывающий `match`
  везде, где ветвление семантично.

[p61] Замечу: для 5.1 я не согласен с тем, *как* свод мотивирует тегирование. Свод
тегирует объединения ради «однозначности разбора» `[ИЗ СВОДА]`, но наш
случай уже однозначен по наборам ключей `[ИЗ ДЕРЕВА repomd.rs:43-55]`.
Подлинная причина тегировать (или ставить `deny_unknown_fields`) у нас —
**не неоднозначность, а тихое глотание аддитивных полей**. Та же мера, другой
аргумент — и этот аргумент сильнее, потому что он переживает будущие
варианты.

## 6. Обязательная версия против версии по умолчанию

[p62] Свод приводит Discord как довод за обязательную: бесверсионный маршрут по
умолчанию годами заморожен на устаревшей версии; «бесверсионный формат не
эволюционирует, он только нарастает» `[ИЗ СВОДА]`. Разбираю обратную сторону.

[p63] **Что мы ломаем, сделав версию обязательной, и что делать с записями без
неё — отдельно для каталога и для `vibe.toml`:**

- [p64] **Каталог.** `Repomd.schema_version` и `VersionEntry.schema_version`
  **уже всегда присутствуют** на проводе (harvest 3.2: «всегда»)
  `[ИЗ ДЕРЕВА]`. Сделать их «обязательными» — **ничего не стоит**: они уже
  обязательны de facto. Обратная сторона здесь отсутствует.
- **`vibe.toml` (манифест, пишется руками).** Версии нет вовсе `[ИЗ СВОДА
  таблица часть 1]`. Обязательная версия здесь — **налог на каждого автора и
  отказ разбора для каждого уже существующего манифеста**, включая все наши
  собственные пакеты. Это и есть то, что ломается.

[p65] **С записями без версии — два режима, и они не враги:**

- [p66] **Обязательность на записи/вперёд** (writer-side): новые манифесты обязаны
  нести `schema_version`. Это довод Discord'а, и он верен для *будущих*
  записей `[ИЗ СВОДА]`.
- **Умолчание при чтении/назад** (reader-side): запись без версии читается
  как `1` (PEP 629: «версии нет → считать 1.0» `[ИЗ СВОДА]`). Это уже
  рекомендация свода №5 `[ИЗ СВОДА]`.

[p67] Эти два режима — две стороны **асимметричного контракта** (строгий писатель,
терпимый читатель), который свод сам проповедует для полей `[ИЗ СВОДА]`.
Ошибка — не «обязательная против умолчания», а **сделать версию обязательной
на чтении**: тогда все существующие манифесты упадут. Правильная сборка —
обязательно при записи, умолчание-в-единицу при чтении.

[p68] **Обратная сторона, которую свод упускает — стоимость для автора.**
Обязательная версия на *пишущемся-руками* формате имеет скрытую цену, которой
нет на *пишущемся-машиной* каталоге: смысл версии (что именно изменилось)
живёт в **нашем** спеке, а не в голове автора. Рука, пишущая
`schema_version = 1`, культивирует число, которое она не может проверить.
Поэтому обязательная версия на `vibe.toml` — **частично театр**, если к ней
не приложен валидатор авторского времени, который скажет автору, что номер
неверен. Для каталога (версию знает пишущая машина) этой цены нет. Сводный
довод Discord'а `[ИЗ СВОДА]` прав для каталога без оговорок и прав для
`vibe.toml` — только в паре с валидатором.

[p69] **Куда деть «версию как контракт» (PEP 629, по своду `[ИЗ СВОДА]`):**

- [p70] старшая выше известной → **отказ** с внятной ошибкой;
- младшая выше известной → **предупреждение**, продолжить;
- версии нет → считать первой.

[p71] Сейчас же (§5.2) версия — инертный ярлык: контракт провозглашён полем, но не
исполняется ни в одной точке `[ИЗ ДЕРЕВА memory.rs:273]`. Первое, что надо
сделать с версией, — **начать её сравнивать**. Без этого «обязательная
версия» лишь прибавит автору работу, ничего не дав взамен.

## 7. С чем я не согласен — списком

1. [p72] **Свод переоценивает хрупкость единственного объединения.** «Читатель
   угадывает по набору ключей» `[ИЗ СВОДА]` — но наборы ключей `{kind,
   entries}` и `{size, sha256}` не пересекаются и внутри `Directory` есть
   тег `kind`; разбор сегодня однозначен `[ИЗ ДЕРЕВА repomd.rs:43-55]`.
   Опасность реальна, но локализована не в неоднозначности, а в отсутствии
   `deny_unknown_fields` (молчаливое глотание аддитивных полей, §5.1). Та же
   мера (тег/strict), другой — более живучий — аргумент.
2. **«Клиент терпим → наружу мы правильны» — полуправда.** Клиент терпим к
   незнакомым **полям** `[ИЗ ДЕРЕВА wire.rs:14-16,37-39]`, но читает
   значения словарей теми же **закрытыми** enum'ами (`PackageKind`,
   `BindingSite`) `[ИЗ ДЕРЕВА wire.rs:57,85,88]`. Неизвестное *значение*
   вида ломает и клиент. Уют свода на словарный случай не распространяется.
3. **Критерий переносимости верен, но неточен по оси.** Реальная ось —
   «контролируешь ли контрагента», а не «артефакт против отношений». Наш
   parity-тест `PackageKind` — отношение, но работает, потому что обе
   стороны наши `[ИЗ ДЕРЕВА kinds.rs:1-5]`. См. §1б.
4. **Бюджет Google, как он посажен в контекст свода, тянет в неверную для
   нас сторону.** Применённый к нашим фактическим темпам (реже раза в год,
   +amendment-дисциплина спека `[ИЗ ДЕРЕВА VIBEVM-SPEC.md:178]`), он
   защищает закрытые enum'ы, а не требует строк. См. §4.
5. **Рекомендация «внедрить первым статическую проверку совместимости в
   сборке» имеет скрытое前置-условие.** У каталога **нет схемы**
   (harvest 3.8: контракт — «только код»). Статик-чеку не на что
   опираться, пока схема не написана, — а её написание и есть весь спор.
   Свод подаёт рекомендацию как бесплатную `[ИЗ СВОДА]`; у нас она
   обусловлена.
6. **Согласие с добавлением.** Свод прав, что версия должна стать контрактом
   `[ИЗ СВОДА №5]`, и прав в разделении строгости по происхождению
   `[ИЗ СВОДА №4]`. Я добавляю: (а) «обязательная» и «по умолчанию» — не
   антиподы, а writer-strict/reader-default; (б) обязательная версия на
   hand-authored формате — театр без валидатора авторского времени; (в)
   каталог уже de facto обязателен, спор только про `vibe.toml`.

## 8. Куда бы я поставил, если бы строил (один абзац, без кода)

[p73] Для `PackageKind`: ничего не открывать и не дублировать — расширить закрытый
enum (`App`) в обоих местах, доверив проходку по ветвям exhaustiveness'у, и
поднять `schema_version` с инертного ярлыка до контракта (старший → отказ,
младший → предупреждение, нет → 1) `[ИЗ ДЕРЕВА memory.rs:273 — сейчас не
сравнивается]`. Дать объединению `RepomdFileEntry` `deny_unknown_fields` на
вариантах или внешний тег — не ради однозначности (она есть), а чтобы
аддитивное поле не глоталось молча (§5.1). Терпимость сделать свойством
**происхождения**, а не глобальным рычагом: строго на входной двери
(`vibe.toml`, руки — ловим опечатку), терпимо на каталоге (машина,
переживаем будущее) — это рекомендация свода №4 `[ИЗ СВОДА]`, и я с ней
согласен. И — зафиксировать явно, что inverted-ряды всегда
перегенерируются и им round-trip не нужен (harvest 3.6), чтобы §5.3 перестал
быть «тихой дырой» и стал осознанным решением.

