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Что я нашёл в дереве:
- 06
vibe.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 места»; моя пересчётка дала 60deny_unknown_fieldsвcrates/, excl. тесты/комментарии — та же величина).
07Вывод рефрейма. Правильное разделение труда уже лежит на поверхности:
| поверхность | кто пишет | кто читает | правильная строгость |
|---|---|---|---|
vibe.lock |
наш же инструмент | наш же инструмент (тот же запуск) | строгая (ловим порчу/перекос) |
vibe.toml проекта |
человек, сейчас | тот же vibe (lockstep) |
строгая (ловим опечатку) |
каталог (primary.jsonl и т.д.) |
индексер (машина) | чужой/будущий инструмент | терпимая |
vibe.toml внутри опубликованного пакета |
человек (автор) | см. ниже |
09Внешняя поверхность — каталог. На него и нужно класть forward-compat
(версия, терпимость, заповедник). Переносить тот же механизм на парсер vibe.toml
— значит тащить внутренний, lockstep-читаемый формат на «терпимую» колею, где ему
делать нечего. Если владелец настаивает, что чужой инструмент будет читать
именно рукописный vibe.toml пакета напрямую (а не каталог) — это проектное
решение, которое нужно принимать сознательно, и оно противоречит уже сделанному
выбору каталога как внешней поверхности. Ниже я отвечаю на все вопросы и в
постановке владельца, и в постановке рефрейма.
1. Перепроверка таблицы трёх форматов — расхождения названы
10Таблица из свода (Часть 1). Я проверил каждую ячейку по дереву.
| версия в данных | ветвится по ней | незнакомый ключ | |
|---|---|---|---|
vibe.lock |
есть (5) | да, отвергает | отвергается |
| каталог индекса | есть (1) | нет | отвергается |
vibe.toml |
нет вовсе | — | отвергается |
12Ячейки сошлись:
- 13
vibe.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Расхождения/уточнения к своде (это находки):
- 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, и свод мог бы это подчеркнуть: проблема не каталожная, а общая.
- 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}). Рекомендация «каждое объединение — тегированное» здесь ближе к «добавить симметричный тег на второе плечо», а не «построить тегизацию с нуля».
- 17Аналогия с PyPI PEP 714 натянута структурно.
[ИЗ СВОДА]авария PyPI — это «одно поле, которое то bool, то dict» (одно имя ключа — два типа). У нас плечи объединения имеют непересекающиеся наборы ключей ({kind,entries}vs{size,sha256}), и никто не переиспользует имя с другим типом. Класс отказа «опечатка в типе значения на том же ключе» сюда не ложится. Опасность untagged-объединения реальна, но иная: при добавлении третьего плеча старый читатель молча сопоставит первому подошедшему. Для наших двух плеч с непересекающимися ключами риск сегодня мал; для будующего третьего плеча — да, нужен тег.
- 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'ах = ошибка разбора. Это и есть суть, и она верна.
- 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
в дереве, разбивка по верхнему каталогу):
| корзина | число | статус при вводе версии |
|---|---|---|
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 точная форма); frozenvibedeps-копии ломаются, пока не перегенерены. Реальная цена.
48Почему «считать первой версией» тут может быть НЕВЕРНО (ключевой момент
вопроса 4). Правило «отсутствие → 1.0» свод берёт из PEP 629 [ИЗ СВОДА]. Оно
работает для PyPI, потому что их JSON-метаданные машинно-сгенерированы и поле
присутствует почти всегда. Для рукописного TOML то же правило
саморазрушительно:
- 49Необязательное поле + отсутствие=1 → авторы не заполняют → поле пустое у большинства из 44 → для чужого читателя оно бесполезно именно тогда, когда нужнее всего. Мы вводим версию, чтобы чужой читатель мог договориться о контракте, и тут же делаем её факультативной.
- Для опубликованного артефакта «нет версии» становится неоднозначным: «старый, до-версионный» или «опубликован инструментом, который поле выкинул»? В момент, когда чужому читателю важнее всего отличить старое от чужого/битого, отсутствие=1 это различие стирает.
- Эталон — наш же
vibe.lock: он делаетschema_versionобязательным и отвергает отсутствие (lockfile.rs:108-111, «avibe.lockwithout 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:92 → VersionEntry с
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. С чем я НЕ согласен (списком)
- 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) - С «строгость зависит от автора файла». Автор не виден из файла
(
[origin].generated_by— про провенанс публикации, не про намерение); критерий нереализуем. Единственный реализуемый — по пространству имён (§3.2), и свод сам к нему склоняется, но оставляет «автора» как сигнал. - С характеристикой объединения как «без тега, читатель угадывает». Оно
полутегированное:
Directoryнесёт явныйkind-тег (repomd.rs:46); угадывание только в том, какого плеча тег нет. Аналогия с PyPI PEP 714 структурно натянута — плечи с непересекающимися ключами, не «одно имя → два типа». - С «absence = считать первой версией» для манифеста. Для рукописного формата
это саморазрушительно (поле становится декоративным; стирает «старый vs
чужой»). Эталон — наш же
vibe.lock, который делает версию обязательной и отвергает отсутствие. (Впрочем, при рефрейме §0 версии в манифесте нет вовсе.) - С числом «5 закрытых словарей». Строгим чтением нахожу 3 мультизначных
fieldless-enum в типах каталога; отмечаю расхождение и не asserts свою цифру —
зависит от определения «словаря». Существенно (и верно):
#[serde(other)]нет нигде, неизвестное значение = ошибка разбора. - С формулировкой «один вопрос или три». Свод не предлагает один бит на все
три; он предлагает один фреймворк с 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 обещанию спека) — высшей ценности, он поймал бы сегодняшнее
противоречие автоматически.