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Сначала фиксирую поверхность ветвления (это нужно для оценки форм):
- 24
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].
25Это смешанная поверхность — и она решает всё ниже.
Форма A — вариант-«неизвестное» через #[serde(other)] с потерей исходной строки
26Добавить fieldless-вариант Unknown с #[serde(other)] [ПО ПАМЯТИ, НЕ
ПРОВЕРЕНО: serde так маршрутизирует любое нераспознанное строковое значение
в один конкретный unit-вариант]. Что видит читатель, встретивший
"app": разбирается успешно, попадает в Unknown.
27Что делает с нашим кодом:
- 28
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 — поле-строка с отдельным списком известных (открытый словарь)
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.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.
Синтез по формам
- 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:
- 39Оставить
kind(6 значений) навсегда; ввестиkind_v2: Option<PackageKindV2>с перечислением из одногоApp. - Предварительно сделать каталог терпимым к новым полям (убрать
deny_unknown_fieldsсVersionEntryили перевести собственное чтение на view-структуры) — иначе старый строгий читатель не поднимется на каталоге с новым полем[ИЗ ДЕРЕВА, harvest 3.7]. - Заполнить старое
kindдляapp-пакетов «ближайшим» символом — которого нет по смыслу; выбрать ложное значение и задокументировать ложь. - Научить каждое употребление
kindсмотреть вkind_v2при наличии. - Продублировать parity-механизм на второе перечисление
[ИЗ ДЕРЕВА kinds.rs:1-5]. - Поддерживать обе оси жанра бессрочно
[ИЗ СВОДА].
40Что даёт одношаговое расширение закрытого enum'а:
- 41Добавить
AppвPackageKindв двух местах (с parity-тестом, который уже есть и сам поймает рассинхрон)[ИЗ ДЕРЕВА kinds.rs:1-5]. - Исчерпывающие match'и (
as_str,repo_name, scanner-маппинг) откажут компилироваться, пока не обработаешьAppвезде — Rust водит за руку. - Поднять
schema_versionкаталога и поставить gate (см. §6). - Новые строгие читатели, собранные со знанием
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. С чем я не согласен — списком
- 72Свод переоценивает хрупкость единственного объединения. «Читатель
угадывает по набору ключей»
[ИЗ СВОДА]— но наборы ключей{kind, entries}и{size, sha256}не пересекаются и внутриDirectoryесть тегkind; разбор сегодня однозначен[ИЗ ДЕРЕВА repomd.rs:43-55]. Опасность реальна, но локализована не в неоднозначности, а в отсутствииdeny_unknown_fields(молчаливое глотание аддитивных полей, §5.1). Та же мера (тег/strict), другой — более живучий — аргумент. - «Клиент терпим → наружу мы правильны» — полуправда. Клиент терпим к
незнакомым полям
[ИЗ ДЕРЕВА wire.rs:14-16,37-39], но читает значения словарей теми же закрытыми enum'ами (PackageKind,BindingSite)[ИЗ ДЕРЕВА wire.rs:57,85,88]. Неизвестное значение вида ломает и клиент. Уют свода на словарный случай не распространяется. - Критерий переносимости верен, но неточен по оси. Реальная ось —
«контролируешь ли контрагента», а не «артефакт против отношений». Наш
parity-тест
PackageKind— отношение, но работает, потому что обе стороны наши[ИЗ ДЕРЕВА kinds.rs:1-5]. См. §1б. - Бюджет Google, как он посажен в контекст свода, тянет в неверную для
нас сторону. Применённый к нашим фактическим темпам (реже раза в год,
+amendment-дисциплина спека
[ИЗ ДЕРЕВА VIBEVM-SPEC.md:178]), он защищает закрытые enum'ы, а не требует строк. См. §4. - Рекомендация «внедрить первым статическую проверку совместимости в
сборке» имеет скрытое前置-условие. У каталога нет схемы
(harvest 3.8: контракт — «только код»). Статик-чеку не на что
опираться, пока схема не написана, — а её написание и есть весь спор.
Свод подаёт рекомендацию как бесплатную
[ИЗ СВОДА]; у нас она обусловлена. - Согласие с добавлением. Свод прав, что версия должна стать контрактом
[ИЗ СВОДА №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 перестал
быть «тихой дырой» и стал осознанным решением.