VibeVM
Contents
On this page
en
Publisher
org.vibevm.core
Version
1.0.0latest
Audiences
Reading time
16 min
Rendered
Read aloud
never

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

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

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

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

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

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

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

06Верно и подтверждено:

  • 075 закрытых словарей, неизвестное значение = ошибка разбора. 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) [ИЗ ДЕРЕВА].

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

20

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

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

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

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

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

  • 24PackageKind ветвит код через 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].

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

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

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

27Что делает с нашим кодом:

  • 28as_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 — поле-строка с отдельным списком известных (открытый словарь)

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

30Что делает с нашим кодом:

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

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

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

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

  • 34LinkedIn-рецепт предполагает терпимого читателя. У нашего каталога читатель строгий: 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.rskinds.rs) придётся плодить и для нового поля: стоимость дублирования умножается на каждое новое перечисление [ИЗ ДЕРЕВА kinds.rs:1-5].
  • Каждое чтение kind (а их много: VersionEntry.kind, CompatibilityEntry.requires_kinds[], CapabilityRow.kind, PurlRow.kind, SearchHit.kind, путь в vibedeps) обязано научиться kind_v2.or(kind) — permanently.

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

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

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

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

37У нас шесть видов (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.

38Что пришлось бы сделать по LinkedIn:

  1. 39Оставить 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. Поддерживать обе оси жанра бессрочно [ИЗ СВОДА].

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

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

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

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

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

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

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

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

  • 47Исходно — четыре вида (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].

48Итого — два расширения за жизнь проекта, третье планируется. Сам спек формулирует дисциплину жёстче любого внешнего бюджета: «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].

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. 72Свод переоценивает хрупкость единственного объединения. «Читатель угадывает по набору ключей» [ИЗ СВОДА] — но наборы ключей {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. Куда бы я поставил, если бы строил (один абзац, без кода)

73Для 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 перестал быть «тихой дырой» и стал осознанным решением.

For an agent

This page has a machine mirror. The citation carries the version rather than latest, so what an agent quotes does not move under it.

spec://org.vibevm.core/vibevm@1.0.0/research/schema-evolution-2026-08/08-glm-clients-and-vocabularies

.md.xmlllms.txt