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

G4-MANIFEST — манифест и единая политика на три формата

01Это отчёт-размышление, не постройка. Ни одной строки кода не правлено. Все утверждения о чужих системах несут метку источника; утверждения о нашем дереве несут файл:строка. Метки:

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

03Связанные файлы: lockfile.rs = vibe.lock; vibe-index/src/types/** = каталог; manifest/document.rs = vibe.toml. Все три живут в crates/.

0. Главный рефрейм (ставлю первым — он перекрашивает вопросы 2–5)

04Тезис свода и владельца: «применить те же выводы к парсеру vibe.toml». Это предполагает, что vibe.toml — внешняя поверхность чтения, та же, что и каталог. Предположение ложное в нашей текущей архитектуре, и это меняет ответы на половину вопросов.

05Что я нашёл в дереве:

  • 06vibe.toml читается тем же бинарником, который бежит. Manifest::read (crates/vibe-core/src/manifest/document.rs:289) и Manifest::parse_str (:298) — единственные двери; их зовёт и vibe-check (crates/vibe-check/src/checks/manifest_validity.rs:36), и индексер из «извлечённого дерева пакета» (комментарий :296-297). Версия читателя и версия писателя — одна и та же (один vibe, один запуск). Это режим lockstep, не cross-version.
  • Каталог — и есть та самая «чужая поверхность». by-name/<name>.json, primary.jsonl, repomd.json — статические файлы, которые PROP-005 §2.4/§2.10 объявляет внешним контрактом (crates/vibe-registry/src/index_client/wire.rs:3-6). Клиент УЖЕ читает их терпимо — view-структурами без deny_unknown_fields, вытаскивая только нужные поля (wire.rs:17-33).
  • Проект уже выбрал двухколейную политику и реализовал её кодогенерацией: crates/vibe-wire/src/lib.rs:1-44 описывает JTD-конвейер (schemas/cargo xtask codegen, контроль дрейфа cargo xtask check-codegen в CI), где сгенерированные типы намеренно терпимы (генератор физически не умеет эммитить deny_unknown_fields; решение владельца 2026-08-06: для формата, что «приходит извне, мягкость = forward compatibility»), а рукописные типы остаются строгими («house style, ~63 места»; моя пересчётка дала 60 deny_unknown_fields в crates/, excl. тесты/комментарии — та же величина).

07Вывод рефрейма. Правильное разделение труда уже лежит на поверхности:

08
поверхность кто пишет кто читает правильная строгость
vibe.lock наш же инструмент наш же инструмент (тот же запуск) строгая (ловим порчу/перекос)
vibe.toml проекта человек, сейчас тот же vibe (lockstep) строгая (ловим опечатку)
каталог (primary.jsonl и т.д.) индексер (машина) чужой/будущий инструмент терпимая
vibe.toml внутри опубликованного пакета человек (автор) см. ниже

09Внешняя поверхность — каталог. На него и нужно класть forward-compat (версия, терпимость, заповедник). Переносить тот же механизм на парсер vibe.toml — значит тащить внутренний, lockstep-читаемый формат на «терпимую» колею, где ему делать нечего. Если владелец настаивает, что чужой инструмент будет читать именно рукописный vibe.toml пакета напрямую (а не каталог) — это проектное решение, которое нужно принимать сознательно, и оно противоречит уже сделанному выбору каталога как внешней поверхности. Ниже я отвечаю на все вопросы и в постановке владельца, и в постановке рефрейма.

1. Перепроверка таблицы трёх форматов — расхождения названы

10Таблица из свода (Часть 1). Я проверил каждую ячейку по дереву.

11
версия в данных ветвится по ней незнакомый ключ
vibe.lock есть (5) да, отвергает отвергается
каталог индекса есть (1) нет отвергается
vibe.toml нет вовсе отвергается

12Ячейки сошлись:

  • 13vibe.lock: CURRENT_SCHEMA_VERSION: u32 = 5 (crates/vibe-core/src/manifest/lockfile.rs:50); ветвление и отказ — lockfile.rs:430-435 (if lockfile.meta.schema_version != CURRENT_SCHEMA_VERSION { return Err(UnsupportedLockfile {…}) }); история версий 1→5 в комментарии :46-49. ✓
  • каталог: Repomd::SCHEMA_VERSION: u32 = 1 (crates/vibe-index/src/types/repomd.rs:38) и VersionEntry::SCHEMA_VERSION: u32 = 1 (crates/vibe-index/src/types/entry/mod.rs:124). ✓
  • каталог «никто не ветвится»: единственное сравнение schema_version во всём дереве — в lockfile.rs:430. По каталогу сравнений ноль: поле только пишется (memory.rs, primary.rs:118, by_name.rs:141, from_github.rs:434 и др.), никогда не читается как условие. ✓
  • vibe.toml: поля версии нет в Manifest (crates/vibe-core/src/manifest/document.rs:67-184 — весь список полей); deny_unknown_fields стоит (:66). ✓

14Расхождения/уточнения к своде (это находки):

  1. 15«15 мест deny_unknown_fields» — точно, но только для каталога. Моя пересчётка по crates/vibe-index/src/types/**: repomd.rs:15 (1), relations.rs (6), entry/mod.rs:38 (1), content.rs (5), aggregate.rs (2) = ровно 15 [ИЗ ДЕРЕВА]. Свод прав для каталога. Но в манифесте deny_unknown_fields стоит ещё в ~40 местах (manifest/**/*.rs), а всего в crates/ — ~60. «Дом-стиль строгости» гораздо шире, чем каталожные 15, и свод мог бы это подчеркнуть: проблема не каталожная, а общая.
  1. 16«1 объединение без тега… общего поля-признака нет» — неточно. RepomdFileEntry помечен #[serde(untagged)] (crates/vibe-index/src/types/repomd.rs:42), но вариант Directory несёт явное поле kind: DirectoryTag (:46-48), где DirectoryTag — одновариантный enum, сериализующийся в "directory" (:73-77). Автор в комментарии прямо пишет: «Carrying this as a tag inside the directory variant lets serde's untagged matcher distinguish unambiguously». Значит, это полутегированное объединение: тег есть, но только на одном плече; у File тега нет ({size, sha256}). Рекомендация «каждое объединение — тегированное» здесь ближе к «добавить симметричный тег на второе плечо», а не «построить тегизацию с нуля».
  1. 17Аналогия с PyPI PEP 714 натянута структурно. [ИЗ СВОДА] авария PyPI — это «одно поле, которое то bool, то dict» (одно имя ключа — два типа). У нас плечи объединения имеют непересекающиеся наборы ключей ({kind,entries} vs {size,sha256}), и никто не переиспользует имя с другим типом. Класс отказа «опечатка в типе значения на том же ключе» сюда не ложится. Опасность untagged-объединения реальна, но иная: при добавлении третьего плеча старый читатель молча сопоставит первому подошедшему. Для наших двух плеч с непересекающимися ключами риск сегодня мал; для будующего третьего плеча — да, нужен тег.
  1. 18«5 закрытых словарей» — я насчитал иначе. Мультизначных fieldless-enum в типах каталога я нашёл три: PackageKind (kinds.rs:21), NamingConvention (kinds.rs:88), DeliveryMode (content.rs:53). Плюс вырожденный одновариантный DirectoryTag и само объединение. Возможно, свод считает иначе (например, включая enum'ы из relations/wire или считая каждое wire-значение). Я не могу воспроизвести 5 строгим чтением; отмечаю расхождение и не asserts свою тройку как эталон — определение «словаря» требует оговорки. Существенно лишь общее: нигде в дереве нет #[serde(other)] (подтверждено grep'ом по crates/ — совпадения только в докуменации), значит любое неизвестное значение в этих enum'ах = ошибка разбора. Это и есть суть, и она верна.
  1. 19Свод упускает, что машина «схема → кодогенерация» уже есть. Часть 0 свода («владелец решил: описать формат схемами и генерировать код») описывает это как вперёд-стоящую работу. Но vibe-wire (crates/vibe-wire/src/lib.rs:1-44) — это УЖЕ работающий JTD→Rust конвейер с контролем дрейфа в CI, с уже принятым владельцем решением о терпимости для внешних форматов (2026-08-06). Значит, вопрос не «построить машинерию», а «в какие типы её распространить» — см. §0 и §7.

20Итог по таблице: ячейки верны; к деталям под каталогом — три уточнения и одно упущенное (vibe-wire уже есть).

2. Один вопрос или три? (довод ПРОТИВ единой политики, потом суждение)

21Сильнейший довод ПРОТИВ единой политики. Три формата различаются по трём независимым осям, и правильная строгость — функция от клетки в этом пространстве:

  • 22Автор: машина (vibe.lock, каталог) vs человек (vibe.toml).
  • Аудитория: только свой инструмент (vibe.lock) → свой + чужой клиент (каталог) → чужой/будущий инструмент (манифест пакета, если считать его внешним).
  • Время жизни / регенерация: пересоздаётся каждый vibe install (vibe.lock) → пересоздаётся при reindex (каталог) → замораживается навсегда в публикации (манифест пакета).

23Любая «единая политика» обязана выбрать одну точку и натянуть на неё все три формата. Но оптимальная строгость зависит от всех трёх осей сразу, поэтому единая политика гарантированно субоптимальна минимум для двух. Конкретно: бит «deny_unknown_fields» нельзя поставить в одно положение для всех. «Терпеть» — верно для каталога (чужой читатель, аддитивное будущее) и неверно для vibe.lock (неизвестное поле там = почти наверняка порча или перекос версий — должен падать громко, что он и делает). «Отвергать» — верно для vibe.lock и для ловли опечатки в рукописном vibe.toml, но неверно для опубликованного манифеста, который через год читает будущий инструмент.

24Суд. Довод бьёт по соломенному чучелу — свод его и не предлагает. Перечитав рекомендацию №4: «Строгость не один рычаг… Разделять пространством имён, а не уровнем строгости». Свод уже предлагает per-format-строгость, а не один бит. Значит, реальная развилка не «одна политика или три», а «один СПЕК контракта с тремя экземплярами, или три независимых спека». И вот тут ответ однозначный: один спек, три экземпляра. Доказательство — в самом дереве: словарь видов пакета PackageKind записан в четырёх местах (crates/vibe-core/src/package_ref/kind.rs:31, crates/vibe-index/src/types/kinds.rs:21, crates/vibe-wire/src/generated/registry_sync_report/mod.rs:25, crates/vibe-wire/src/generated/list_report/mod.rs:76), и хотя значения синхронны (parity-тест + кодогенерация), проза разошлась вживую: kind.rs:16 говорит «One of the four installable package kinds», :18 — «adding a fifth kind», а kinds.rs:1 — «duplicate of the four-kind enum»; при этом enum содержит шесть вариантов (Flow, Feat, Stack, Tool, Mcp, Lang), ALL: [PackageKind; 6] (kind.rs:68). Три независимых спецификации гарантируют именно такой дрейф. Один спек + один чекер (§7) — единственный способ его удержать.

25Итог: один фреймворк (семантика версии, заповедник расширений, правило тегированных объединений, запрет переиспользования имён, список отставленных ключей), три параметризации. Свод прав по сути; формулировка «один вопрос или три» немного промахивается мимо того, что свод на самом деле предлагает.

3. Строгость по автору + третий ответ + «один формат или два» (вопросы 2 и 2b)

3.1. Манифест внутри опубликованного пакета — рукописный или машинный?

26Ни тот ни другой в чистом виде. Он написан человеком (автор пакета), но читается чужим инструментом через год. Значит, он наследует оба риска: опечатку от ручного авторства и forward-compat-давление от замороженного артефакта. Бинарный критерий свода «строго, если рука; терпимо, если машина» его не классифицирует — он одновременно и то, и другое. Это и есть причина, по которой нужен третий ответ.

3.2. Третий ответ между «отвергать» и «игнорировать»

27Строгость не должна зависеть от автора файла — читатель не может узнать автора из файла (см. 3.4). Она должна зависеть от пространства имён ключа:

  • 28Ключ в известном пространстве vibe и не распознан → отвергнуть (это опечатка: [[regitry]] вместо [[registry]], и deny_unknown_fields именно это ловит сегодня, см. тест crates/vibe-core/src/global_registry.rs:517).
  • Ключ в зарезервированном пространстве расширенийпропустить и сохранить (это будущее/чужое; см. §4).
  • Ключ вне обоих → отвергнуть (не даём молчаливо плодить мусор в не-зарезервированной зоне).

29Это разделяет «опечатка в словаре vibe» (отвергаем) и «расширение чужого инструмента» (терпим), не спрашивая, кто написал файл. Тот же приём свод упоминает («разделить пространством имён») — я лишь делаю его единственным критерием и убираю «автора» как сигнал.

3.3. Заповедник и третий ответ — конкретно (вопрос 3)

30В TOML естественная форма — зарезервированная таблица, не префикс ключа. Предложение:

  • 31Корневая таблица [tool.<name>] (по примеру Cargo [package.metadata]/[tool.*] [ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]) — постоянный заповедник. vibe никогда не назначает смысла ничему под [tool.*]; внешний инструмент пишет туда своё.
  • Дополнительно можно зарезервировать x_*/_-префиксные ключи внутри таблиц (как «ключи с подчёркиванием зарезервированы» [ИЗ СВОДА]), но таблица [tool.*] структурно чище для вложенных данных.

32Кто пишет: чужой инструмент (свои экспериментальные поля, сторонние расширения); опционально — сам vibe для неотстоявшихся полей под флагом.

33Что делает vibe, встретив [tool.<x>]: пропускает, сохраняет verbatim через цикл read→write. Механизм уже частично есть: merge_preserving_comments (crates/vibe-core/src/manifest/mod.rs:116-189) сохраняет структуру и комментарии при перезаписи; нужно добавить гарантированное сохранение именно зарезервированных таблиц (сейчас deny_unknown_fields на Manifest (document.rs:66) отвергнет [tool.x] целиком — это первый затык, который надо убрать).

34Серьёзный технический затык (из [ПО ПАМЯТИ, НЕ ПРОВЕРЕНО] знания serde): deny_unknown_fields и «пропустить произвольные ключи в известную карту» в serde несовместимы на одном типеdeny_unknown_fields срабатывает до того, как serde разложит ключи в поле. Значит, заповедник нельзя получить, просто оставив deny_unknown_fields; нужно явное поле в Manifest, например #[serde(default, rename = "tool")] pub tool: BTreeMap<String, toml::Value> (или toml::Value/serde_json::Value), и тогда deny_unknown_fields на остальном продолжает ловить опечатки. Это конкретное, машинно-реализуемое правило, а не пожелание.

3.4. Как читатель вообще узнаёт автора файла?

35Никак, надёжно. Файл не несёт поля «я рукописный» vs «я машинный». Поле [origin].generated_by (document.rs:249) маркирует только факт «копия сгенерирована vibe workspace publish», а не намерение автора. Значит, критерий «строгость по автору» нереализуем как сформулирован — и свод, и я должны пересесть на единственно реализуемый: строгость по пространству имён (3.2).

3.5. Вопрос 2b — манифест пакета и манифест проекта: один формат или два?

36Сегодня — один. Manifest — единая структура для всех ролей (crates/vibe-core/src/manifest/document.rs:6-26): роль задаётся тем, какие секции присутствуют ([project][package], плюс [workspace], [origin]), по модели cargo. Опубликованная копия пакета — та же структура с добавленной таблицей [origin] (:219-252). Один парсер, один deny_unknown_fields, одна отсутствующая версия. [ИЗ ДЕРЕВА]

37Цена «один формат» (оставить как есть):

  • 38Нельзя сделать опубликованную копию терпимой, не сделав терпимой и копию проекта — та же структура.
  • Нельзя версионировать их независимо.
  • Опубликованный манифест несёт потребительские секции ([requires], [[registry]], [i18n], [boot]), бессмысленные в артефакте, но допустимые схемой.
  • Future-vibe читает old-опубликованный манифест той же строгой дверью → упирается в deny_unknown_fields на новом поле.

39Цена «два формата» (разделить):

  • 40Большой рефакторинг: ~44 исходных манифеста (см. §5), весь модуль manifest/, каждый звонок Manifest::read/parse_str.
  • Но даёт: опубликованной копии — свою версию, терпимое чтение, провенанс; копии проекта — строгость и ловлю опечатки.

41Прецедент cargo [ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]: у cargo один рукописный Cargo.toml, но другой, машинно-сгенерированный, версионированный индекс-формат (записи crates.io index несут поле версии схемы, которое переезжало). Т.е. cargo по сути отвечает «две поверхности»: рукописная — для своего инструмента, сгенерированная — для чужого.

42Мой суд (и он совпадает с рефреймом §0): «две поверхности, один источник». Не дробить Manifest на два типа, а вообще не делать рукописный vibe.toml внешней поверхностью. Внешняя поверхность — каталог (VersionEntry уже несёт schema_version = 1, уже читается клиентом терпимо). Рукописный vibe.toml остаётся строгим и lockstep-читаемым; если чужому инструменту нужны данные пакета, он идёт в каталог, а не парсит рукописный файл. Это растворяет вопросы 2/2b/3/4 для манифеста целиком. Если же владелец непременно хочет, чтобы чужой инструмент читал именно vibe.toml пакета напрямую, тогда честный ответ — «два формата», и Published-вариант надо версионировать и сделать терпимым (лучше — сгенерировать его, как vibe-wire).

4. Цена версии в манифесте (вопрос 4)

43Сначала счёт существующих манифестов [ИЗ ДЕРЕВА] (всего 172 vibe.toml в дереве, разбивка по верхнему каталогу):

44
корзина число статус при вводе версии
packages/** без vibedeps (исходники, авторские) 44 настоящая миграция
vibedeps/** (регенерируемые копии зависимостей) ~116 регенерируются → не бремя, но замороженные checked-in копии сломаются
fixtures/** + crates/**/fixtures (тестовые данные) ~9 ломают тесты, если поле обязательное
research/**, корень и прочее ~3 проверять индивидуально

45(Числа — моя пересчётка find … -name vibe.toml по корзинам; «~» отмечает группировку, полная сумма = 172.)

46Что делает читатель, встретив манифест без версии:

  • 47Если поле необязательное (отсутствие = 1, как предлагает свод, рек. №5): все 44 парсятся; поле остаётся пустым; фикстуры не трогаются; но поле становится декоративным — авторы его не填ят, и для чужого читателя оно не несёт информации.
  • Если поле обязательное (как у vibe.lock и каталога): все 44 надо снабдить schema_version = 1; ~9 фикстур ломают тесты (в них Asserted точная форма); frozen vibedeps-копии ломаются, пока не перегенерены. Реальная цена.

48Почему «считать первой версией» тут может быть НЕВЕРНО (ключевой момент вопроса 4). Правило «отсутствие → 1.0» свод берёт из PEP 629 [ИЗ СВОДА]. Оно работает для PyPI, потому что их JSON-метаданные машинно-сгенерированы и поле присутствует почти всегда. Для рукописного TOML то же правило саморазрушительно:

  1. 49Необязательное поле + отсутствие=1 → авторы не заполняют → поле пустое у большинства из 44 → для чужого читателя оно бесполезно именно тогда, когда нужнее всего. Мы вводим версию, чтобы чужой читатель мог договориться о контракте, и тут же делаем её факультативной.
  2. Для опубликованного артефакта «нет версии» становится неоднозначным: «старый, до-версионный» или «опубликован инструментом, который поле выкинул»? В момент, когда чужому читателю важнее всего отличить старое от чужого/битого, отсутствие=1 это различие стирает.
  3. Эталон — наш же vibe.lock: он делает schema_version обязательным и отвергает отсутствие (lockfile.rs:108-111, «a vibe.lock without it… is rejected»). Каталог тоже обязателен (VersionEntry.schema_version без #[serde(default)], entry/mod.rs:44). Манифест должен соответствовать им, а не машинно-генерируемому стандарту PyPI.

50Но (и это главный выход): если vibe.toml — НЕ внешняя поверхность (рефрейм §0), то версия в манифесте не нужна вовсе, и весь вопрос отпадает. Версия принадлежит каталогу (где она уже есть, =1). Так что «absence=1 неверно для манифеста» — верно, но при правильной архитектуре вопрос не возникает: манифест не версионируется, каталог — да.

5. Порядок работ: каталог-полигон vs манифест-боевое (вопрос 5)

51Свод: каталог — полигон (ломать даром), манифест — боевое.

52Сильнейший довод за ОБРАТНЫЙ порядок (манифест первым): манифест — единственный из трёх, что ездит внутри опубликованного артефакта и читается чужим будущим инструментом; его forward-compat-часы тикают с момента первой публикации. Каталог же сегодня читает только наш терпимый клиент; его часы тикают лишь с появлением чужого потребителя (ноль сегодня). Значит, проектировать forward-compat сначала на каталоге — строить его для формата с наименее срочной потребностью.

53Суд — каталог первым, но по более сильной причине, чем «ломать даром». Довод «обратный порядок» бьёт по приоритету срочности, но упускает, что манифест социально дорог (44 файла, живой потребитель, единственный автор — он же владелец), а каталог — структурно богат и социально дёшев: в каталоге 23 типа / ~86 полей [ИЗ СВОДА], объединение, закрытые словари — наибольшее структурное разнообразие, на котором машинерию (схема → кодоген → чекер → версия-контракт) можно реально обкатать. Манифест структурно проще (один документ, плоские таблицы), значит, на нём машинерия менее испытана, а риск выше. Решение: доказать машинерию на сложном-но-дешёвом (каталог), потом применить доказанное к простому-но-дорогому (манифест). Обратный порядок испытывает меньше и рискует больше — он хуже.

54Уточнение к «ломать даром»: каталог не вполне бесплатен. Свод сам отмечает: сервер не стартует на каталоге, записанном новым инструментом, потому что обратное чтение идёт строгими типами (primary.rs:92VersionEntry с deny_unknown_fields). Значит, «бесплатно» = «без внешних последствий», а не «без последствий вообще»; внутренний откат (рестарт сервера) реален, но самоизлечим reindex'ом. Эту оговорку свод теряет.

55Итог: порядок свода верен; основание — «богатый полигон + дешёвые внешние последствия», а не только «дёшево ломать».

6. Один чекер на три формата (вопрос 6)

56У нас уже есть машина чекеров: vibe-check с швом Check, реестром all_checks(), ячейками-per-CheckId и цитированием спек-анкоров в сообщениях (crates/vibe-check/src/checks/mod.rs:1-9, manifest_validity.rs:36-46). Ячейка ManifestValidity уже связывает vibe.toml и vibe.lock в одну проверку. «Один чекер на три формата» = расширить этот шов. Ниже — каждое правило как машинно-проверяемое; purposely чтобы форматы не разошлись, как разошёлся PackageKind (§2).

  • 57R1. Один источник для каждого словаря; все копии идентичны. Для каждого закрытого enum'а (PackageKind, NamingConvention, DeliveryMode) ровно одно определение; остальные (vibe-index, vibe-wire×2) генерируются или parity-проверяются. Сегодня parity-тест ловит только варианты; расширить до wire-строк и — главное — удалить машинно-непроверяемую прозу о мощности («four», «fifth»): именно она разошлась (kind.rs:16,18, kinds.rs:1). Машинная проверка: сравнить множества (вариант, wire-строка) по всем четырём определениям; несовпадение → fail.
  • R2. Уникальность wire-строк и список отставленных. Ни у какого enum'а два варианта не сериализуются в одну wire-строку; ни одна wire-строка не переиспользуется после удаления (ведётся явный список retired-ключей). Проверка: множество wire-строк текущих ∩ множество retired = ∅.
  • R3. Наличие поля версии соответствует спеку. Для каждого формата, который спек объявляет «версионным», верхнеуровневая структура несёт поле версии И проставляет его при записи. Сегодня: vibe.lock и каталог — да; vibe.toml — нет. Если спек говорит «манифест не версионный» (рефрейм §0), отсутствие — корректно; если говорит «версионный» — fail. Проверка читает specmark-анкор и сравнивает с наличием поля.
  • R4. Семантика версии соответствует контракту. Там, где поле есть, поведение читателя = (major→отказ с цитатой; minor→warn-continue). vibe.lock — единственный, кто сравнивает, и он отказывает по != (lockfile.rs:430): проверить, что это совпадает с его заявленным контрактом «ровно одна версия, pre-release». Каталог объявил версию, но не сравнивает нигде — если спек обещает «старый читатель игнорирует новые поля», чекер должен это противоречие поймать (см. R5).
  • R5. deny_unknown_fields ↔ обещание спека (самое ценное). Для каждого (формат, структура) чекер сверяет атрибут deny_unknown_fields с заявленным в спеке контрактом строгости. Это поймало бы заглавную находку свода автоматически: каталог обещает «читатели старой версии спокойно игнорируют незнакомые поля» [ИЗ СВОДА], а код ставит deny_unknown_fields в 15 местах (crates/vibe-index/src/types/**). specmark уже связывает код↔спек (#[spec(implements = "spec://…")], напр. entry/mod.rs:39-42); чекер читает контракт по анкору и диффает с атрибутом.
  • R6. Заповедник расширений реально работает. Если объявлен зарезервированный namespace ([tool.*]), чекер Asserts, что либо на верхнем уровне нет deny_unknown_fields, либо заповедник смоделирован явным полем (см. §3.3), иначе [tool.x] молча отвергается. Защита от «объявили заповедник, а он не работает».
  • R7. Round-trip сохраняет неизвестное-в-заповеднике. write→read→write байт-идентичен (по модулю комментариев) для каждого формата; в частности, секция [tool.*] переживает цикл. Ловит класс «терпим, но молча выкидываем».

58Что объединяет три формата (чтобы не разошлись): R1 + R5 применяются ко всем трём с одним источником истины — спеком. specmark-анкор — та самая «одна точка», чекер диффает каждый форматный код против неё. Так дрейф PackageKind-типа удерживается: вариант проверяется кодогеном/parity (R1), соответствие строгости — против спека (R5), а машинно-непроверяемая проза о мощности просто удаляется. R5 — высшей ценности: он поймал бы сегодняшнее противоречие каталога до того, как его нащёл свод.

7. С чем я НЕ согласен (списком)

  1. 59С посылом «применить те же выводы к парсеру vibe.toml». Он предполагает, что vibe.toml — внешняя поверхность. В нашей архитектуре внешняя поверхность — каталог (уже версионный, уже терпимо читаемый клиентом), а vibe.toml — lockstep-читается тем же бинарником. Перенос forward-compat на него тащит внутренний формат не на ту колею. (crates/vibe-registry/src/index_client/wire.rs, crates/vibe-core/src/manifest/document.rs:289-303)
  2. С «строгость зависит от автора файла». Автор не виден из файла ([origin].generated_by — про провенанс публикации, не про намерение); критерий нереализуем. Единственный реализуемый — по пространству имён (§3.2), и свод сам к нему склоняется, но оставляет «автора» как сигнал.
  3. С характеристикой объединения как «без тега, читатель угадывает». Оно полутегированное: Directory несёт явный kind-тег (repomd.rs:46); угадывание только в том, какого плеча тег нет. Аналогия с PyPI PEP 714 структурно натянута — плечи с непересекающимися ключами, не «одно имя → два типа».
  4. С «absence = считать первой версией» для манифеста. Для рукописного формата это саморазрушительно (поле становится декоративным; стирает «старый vs чужой»). Эталон — наш же vibe.lock, который делает версию обязательной и отвергает отсутствие. (Впрочем, при рефрейме §0 версии в манифесте нет вовсе.)
  5. С числом «5 закрытых словарей». Строгим чтением нахожу 3 мультизначных fieldless-enum в типах каталога; отмечаю расхождение и не asserts свою цифру — зависит от определения «словаря». Существенно (и верно): #[serde(other)] нет нигде, неизвестное значение = ошибка разбора.
  6. С формулировкой «один вопрос или три». Свод не предлагает один бит на все три; он предлагает один фреймворк с per-format-строгостью. Реальная развилка — «один спек контракта с тремя экземплярами vs три независимых спека» — и ответ «один», потому что три гарантируют дрейф (живой пример — PackageKind в 4 экземплярах, проза разошлась: «four»/«fifth» при 6 вариантах).

60С чем согласен и добавляю: тегировать объединения; писать пустую коллекцию явно; держать запасное значение у закрытых словарей; не переиспользовать имена + список отставленных; каталог первым. Добавление свода: машина «схема→кодогенерация» уже существует (vibe-wire, JTD, решение владельца 2026-08-06 о терпимости для внешних форматов) — значит, работа не «построить», а «распространить на каталог и связать чекером R5».

8. Сводная рекомендация (одним абзацем)

61Один контракт (семантика версии major-отказ/minor-warn; заповедник [tool.*] с сохранением через round-trip; тегированные объединения; запрет переиспользования имён + retired-список), три экземпляра: vibe.lock — как есть (строгий, версия обязательна, отказ по !=); каталог — перенести в vibe-wire/JTD (терпимый, версия =1 остаётся, пробел «спек обещает игнор, код отвергает» закрывается через R5); vibe.tomlостаётся строгим рукописным lockstep-форматом без версии, а forward-compat-бремя несёт каталог как единственная внешняя поверхность. Если владелец настаивает на чужом-чтении именно vibe.toml пакета — тогда «два формата»: Published-копия версонируется и делается терпимой (лучше сгенерированной). Порядок: каталог первым (богатый полигон + дёшево внешне). Чекер: расширить vibe-check правилами R1–R7, из них R5 (соответствие deny_unknown_fields обещанию спека) — высшей ценности, он поймал бы сегодняшнее противоречие автоматически.

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/09-glm-manifest-and-unified-policy

.md.xmlllms.txt