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

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

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

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

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

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

[p05] Что я нашёл в дереве:

- [p06] `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 места»; моя пересчётка дала 60
  `deny_unknown_fields` в `crates/`, excl. тесты/комментарии — та же величина).

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

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

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

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

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

[p11]
|  | версия в данных | ветвится по ней | незнакомый ключ |
| --- | --- | --- | --- |
| `vibe.lock` | есть (5) | **да, отвергает** | отвергается |
| каталог индекса | есть (1) | **нет** | отвергается |
| `vibe.toml` | **нет вовсе** | — | отвергается |

[p12] **Ячейки сошлись:**

- [p13] `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`). ✓

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

1. [p15] **«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. [p16] **«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. [p17] **Аналогия с PyPI PEP 714 натянута структурно.** `[ИЗ СВОДА]` авария PyPI —
   это «одно поле, которое то bool, то dict» (одно имя ключа — два типа). У нас
   плечи объединения имеют **непересекающиеся** наборы ключей (`{kind,entries}`
   vs `{size,sha256}`), и никто не переиспользует имя с другим типом. Класс
   отказа «опечатка в типе значения на том же ключе» сюда **не ложится**. Опасность
   untagged-объединения реальна, но иная: при добавлении третьего плеча старый
   читатель молча сопоставит первому подошедшему. Для наших двух плеч с
   непересекающимися ключами риск сегодня мал; для будующего третьего плеча —
   да, нужен тег.

1. [p18] **«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. [p19] **Свод упускает, что машина «схема → кодогенерация» уже есть.** Часть 0 свода
   («владелец решил: описать формат схемами и генерировать код») описывает это как
   вперёд-стоящую работу. Но `vibe-wire` (`crates/vibe-wire/src/lib.rs:1-44`) —
   это УЖЕ работающий JTD→Rust конвейер с контролем дрейфа в CI, с уже принятым
   владельцем решением о терпимости для внешних форматов (2026-08-06). Значит,
   вопрос не «построить машинерию», а «в какие типы её распространить» — см. §0 и
   §7.

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

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

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

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

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

[p24] **Суд.** Довод бьёт по соломенному чучелу — свод его и не предлагает. Перечитав
рекомендацию №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) — единственный способ
его удержать.

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

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

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

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

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

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

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

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

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

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

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

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

[p33] **Что делает `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]` целиком — это первый затык,
который надо убрать).

[p34] **Серьёзный технический затык (из `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` знания 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. Как читатель вообще узнаёт автора файла?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1. [p49] Необязательное поле + отсутствие=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.

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

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

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

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

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

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

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

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

[p56] У нас **уже есть** машина чекеров: `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).

- [p57] **R1. Один источник для каждого словаря; все копии идентичны.** Для каждого
  закрытого 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.*]` переживает цикл. Ловит класс «терпим, но молча выкидываем».

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

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

1. [p59] **С посылом «применить те же выводы к парсеру `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 вариантах).

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

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

[p61] Один **контракт** (семантика версии 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` обещанию спека) — высшей ценности, он поймал бы сегодняшнее
противоречие автоматически.

