<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title>G4-MANIFEST — манифест и единая политика на три формата</title>
  <p p="1">Это отчёт-размышление, не постройка. Ни одной строки кода не правлено. Все
утверждения о чужих системах несут метку источника; утверждения о нашем дереве
несут `файл:строка`. Метки:</p>
  <list ordered="false" p="2">
    <item>`[ИЗ СВОДА]` — из `FINDINGS-DIGEST.md`; веб я не вижу и проверить не могу.</item>
    <item>`[ИЗ ДЕРЕВА]` — проверено мной чтением кода.</item>
    <item>`[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — моё знание чужих систем, без выдуманных версий/дат.</item>
  </list>
  <p p="3">Связанные файлы: `lockfile.rs` = `vibe.lock`; `vibe-index/src/types/**` = каталог;
`manifest/document.rs` = `vibe.toml`. Все три живут в `crates/`.</p>
  <section title="0. Главный рефрейм (ставлю первым — он перекрашивает вопросы 2–5)">
    <p p="4">Тезис свода и владельца: «применить те же выводы к парсеру `vibe.toml`». Это
предполагает, что `vibe.toml` — внешняя поверхность чтения, та же, что и каталог.
**Предположение ложное в нашей текущей архитектуре**, и это меняет ответы на
половину вопросов.</p>
    <p p="5">Что я нашёл в дереве:</p>
    <list ordered="false" p="6">
      <item>`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.</item>
      <item>Каталог — и есть та самая «чужая поверхность». `by-name/&lt;name&gt;.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`).</item>
      <item>Проект **уже выбрал двухколейную политику** и реализовал её кодогенерацией:
  `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. тесты/комментарии — та же величина).</item>
    </list>
    <p p="7">**Вывод рефрейма.** Правильное разделение труда уже лежит на поверхности:</p>
    <table p="8">
      <tr>
        <td>поверхность</td>
        <td>кто пишет</td>
        <td>кто читает</td>
        <td>правильная строгость</td>
      </tr>
      <tr>
        <td>`vibe.lock`</td>
        <td>наш же инструмент</td>
        <td>наш же инструмент (тот же запуск)</td>
        <td>строгая (ловим порчу/перекос)</td>
      </tr>
      <tr>
        <td>`vibe.toml` проекта</td>
        <td>человек, сейчас</td>
        <td>тот же `vibe` (lockstep)</td>
        <td>строгая (ловим опечатку)</td>
      </tr>
      <tr>
        <td>каталог (`primary.jsonl` и т.д.)</td>
        <td>индексер (машина)</td>
        <td>чужой/будущий инструмент</td>
        <td>терпимая</td>
      </tr>
      <tr>
        <td>`vibe.toml` внутри опубликованного пакета</td>
        <td>человек (автор)</td>
        <td>см. ниже</td>
      </tr>
    </table>
    <p p="9">Внешняя поверхность — каталог. На него и нужно класть forward-compat
(версия, терпимость, заповедник). Переносить тот же механизм на парсер `vibe.toml`
— значит тащить внутренний, lockstep-читаемый формат на «терпимую» колею, где ему
делать нечего. **Если владелец настаивает, что чужой инструмент будет читать
именно рукописный `vibe.toml` пакета напрямую (а не каталог) — это проектное
решение, которое нужно принимать сознательно, и оно противоречит уже сделанному
выбору каталога как внешней поверхности.** Ниже я отвечаю на все вопросы и в
постановке владельца, и в постановке рефрейма.</p>
  </section>
  <section title="1. Перепроверка таблицы трёх форматов — расхождения названы">
    <p p="10">Таблица из свода (Часть 1). Я проверил каждую ячейку по дереву.</p>
    <table p="11">
      <tr>
        <td></td>
        <td>версия в данных</td>
        <td>ветвится по ней</td>
        <td>незнакомый ключ</td>
      </tr>
      <tr>
        <td>`vibe.lock`</td>
        <td>есть (5)</td>
        <td>**да, отвергает**</td>
        <td>отвергается</td>
      </tr>
      <tr>
        <td>каталог индекса</td>
        <td>есть (1)</td>
        <td>**нет**</td>
        <td>отвергается</td>
      </tr>
      <tr>
        <td>`vibe.toml`</td>
        <td>**нет вовсе**</td>
        <td>—</td>
        <td>отвергается</td>
      </tr>
    </table>
    <p p="12">**Ячейки сошлись:**</p>
    <list ordered="false" p="13">
      <item>`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`. ✓</item>
      <item>каталог: `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`). ✓</item>
      <item>каталог «никто не ветвится»: единственное сравнение `schema_version` во всём
  дереве — в `lockfile.rs:430`. По каталогу сравнений ноль: поле только пишется
  (`memory.rs`, `primary.rs:118`, `by_name.rs:141`, `from_github.rs:434` и др.),
  никогда не читается как условие. ✓</item>
      <item>`vibe.toml`: поля версии нет в `Manifest`
  (`crates/vibe-core/src/manifest/document.rs:67-184` — весь список полей);
  `deny_unknown_fields` стоит (`:66`). ✓</item>
    </list>
    <p p="14">**Расхождения/уточнения к своде (это находки):**</p>
    <list ordered="true" p="15">
      <item>**«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, и
   свод мог бы это подчеркнуть: проблема не каталожная, а общая.</item>
    </list>
    <list ordered="true" p="16">
      <item>**«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}`). Рекомендация «каждое объединение — тегированное»
   здесь ближе к «добавить симметричный тег на второе плечо», а не «построить
   тегизацию с нуля».</item>
    </list>
    <list ordered="true" p="17">
      <item>**Аналогия с PyPI PEP 714 натянута структурно.** `[ИЗ СВОДА]` авария PyPI —
   это «одно поле, которое то bool, то dict» (одно имя ключа — два типа). У нас
   плечи объединения имеют **непересекающиеся** наборы ключей (`{kind,entries}`
   vs `{size,sha256}`), и никто не переиспользует имя с другим типом. Класс
   отказа «опечатка в типе значения на том же ключе» сюда **не ложится**. Опасность
   untagged-объединения реальна, но иная: при добавлении третьего плеча старый
   читатель молча сопоставит первому подошедшему. Для наших двух плеч с
   непересекающимися ключами риск сегодня мал; для будующего третьего плеча —
   да, нужен тег.</item>
    </list>
    <list ordered="true" p="18">
      <item>**«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'ах = ошибка разбора. Это и есть
   суть, и она верна.</item>
    </list>
    <list ordered="true" p="19">
      <item>**Свод упускает, что машина «схема → кодогенерация» уже есть.** Часть 0 свода
   («владелец решил: описать формат схемами и генерировать код») описывает это как
   вперёд-стоящую работу. Но `vibe-wire` (`crates/vibe-wire/src/lib.rs:1-44`) —
   это УЖЕ работающий JTD→Rust конвейер с контролем дрейфа в CI, с уже принятым
   владельцем решением о терпимости для внешних форматов (2026-08-06). Значит,
   вопрос не «построить машинерию», а «в какие типы её распространить» — см. §0 и
   §7.</item>
    </list>
    <p p="20">Итог по таблице: **ячейки верны; к деталям под каталогом — три уточнения и одно
упущенное (vibe-wire уже есть).**</p>
  </section>
  <section title="2. Один вопрос или три? (довод ПРОТИВ единой политики, потом суждение)">
    <p p="21">**Сильнейший довод ПРОТИВ единой политики.** Три формата различаются по **трём
независимым осям**, и правильная строгость — функция от клетки в этом
пространстве:</p>
    <list ordered="false" p="22">
      <item>**Автор:** машина (`vibe.lock`, каталог) vs человек (`vibe.toml`).</item>
      <item>**Аудитория:** только свой инструмент (`vibe.lock`) → свой + чужой клиент
  (каталог) → чужой/будущий инструмент (манифест пакета, если считать его
  внешним).</item>
      <item>**Время жизни / регенерация:** пересоздаётся каждый `vibe install` (`vibe.lock`)
  → пересоздаётся при reindex (каталог) → замораживается навсегда в публикации
  (манифест пакета).</item>
    </list>
    <p p="23">Любая «единая политика» обязана выбрать одну точку и натянуть на неё все три
формата. Но оптимальная строгость зависит от всех трёх осей сразу, поэтому
единая политика гарантированно субоптимальна минимум для двух. Конкретно: бит
«`deny_unknown_fields`» нельзя поставить в одно положение для всех. «Терпеть» —
верно для каталога (чужой читатель, аддитивное будущее) и **неверно** для
`vibe.lock` (неизвестное поле там = почти наверняка порча или перекос версий —
должен падать громко, что он и делает). «Отвергать» — верно для `vibe.lock` и для
ловли опечатки в рукописном `vibe.toml`, но неверно для опубликованного
манифеста, который через год читает будущий инструмент.</p>
    <p p="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) — единственный способ
его удержать.</p>
    <p p="25">**Итог: один фреймворк (семантика версии, заповедник расширений, правило
тегированных объединений, запрет переиспользования имён, список отставленных
ключей), три параметризации. Свод прав по сути; формулировка «один вопрос или
три» немного промахивается мимо того, что свод на самом деле предлагает.**</p>
  </section>
  <section title="3. Строгость по автору + третий ответ + «один формат или два» (вопросы 2 и 2b)">
    <section title="3.1. Манифест внутри опубликованного пакета — рукописный или машинный?">
      <p p="26">Ни тот ни другой в чистом виде. Он **написан человеком** (автор пакета), но
**читается чужим инструментом через год**. Значит, он наследует **оба** риска:
опечатку от ручного авторства **и** forward-compat-давление от замороженного
артефакта. Бинарный критерий свода «строго, если рука; терпимо, если машина» его
не классифицирует — он одновременно и то, и другое. Это и есть причина, по которой
нужен третий ответ.</p>
    </section>
    <section title="3.2. Третий ответ между «отвергать» и «игнорировать»">
      <p p="27">Строгость не должна зависеть от автора файла — **читатель не может узнать автора
из файла** (см. 3.4). Она должна зависеть от **пространства имён ключа**:</p>
      <list ordered="false" p="28">
        <item>Ключ в **известном пространстве `vibe`** и не распознан → **отвергнуть** (это
  опечатка: `[[regitry]]` вместо `[[registry]]`, и `deny_unknown_fields` именно
  это ловит сегодня, см. тест `crates/vibe-core/src/global_registry.rs:517`).</item>
        <item>Ключ в **зарезервированном пространстве расширений** → **пропустить и
  сохранить** (это будущее/чужое; см. §4).</item>
        <item>Ключ **вне обоих** → отвергнуть (не даём молчаливо плодить мусор в
  не-зарезервированной зоне).</item>
      </list>
      <p p="29">Это разделяет «опечатка в словаре vibe» (отвергаем) и «расширение чужого
инструмента» (терпим), **не спрашивая, кто написал файл**. Тот же приём свод
упоминает («разделить пространством имён») — я лишь делаю его единственным
критерием и убираю «автора» как сигнал.</p>
    </section>
    <section title="3.3. Заповедник и третий ответ — конкретно (вопрос 3)">
      <p p="30">В TOML естественная форма — **зарезервированная таблица**, не префикс ключа.
Предложение:</p>
      <list ordered="false" p="31">
        <item>Корневая таблица `[tool.&lt;name&gt;]` (по примеру Cargo `[package.metadata]`/`[tool.*]`
  `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`) — постоянный заповедник. `vibe` никогда не
  назначает смысла ничему под `[tool.*]`; внешний инструмент пишет туда своё.</item>
        <item>Дополнительно можно зарезервировать `x_*`/`_`-префиксные ключи внутри таблиц
  (как «ключи с подчёркиванием зарезервированы» `[ИЗ СВОДА]`), но таблица
  `[tool.*]` структурно чище для вложенных данных.</item>
      </list>
      <p p="32">**Кто пишет:** чужой инструмент (свои экспериментальные поля, сторонние
расширения); опционально — сам `vibe` для неотстоявшихся полей под флагом.</p>
      <p p="33">**Что делает `vibe`, встретив `[tool.&lt;x&gt;]`:** пропускает, **сохраняет verbatim**
через цикл read→write. Механизм уже частично есть:
`merge_preserving_comments` (`crates/vibe-core/src/manifest/mod.rs:116-189`)
сохраняет структуру и комментарии при перезаписи; нужно добавить гарантированное
сохранение именно зарезервированных таблиц (сейчас `deny_unknown_fields` на
`Manifest` (`document.rs:66`) **отвергнет** `[tool.x]` целиком — это первый затык,
который надо убрать).</p>
      <p p="34">**Серьёзный технический затык (из `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` знания serde):**
`deny_unknown_fields` и «пропустить произвольные ключи в известную карту» в serde
**несовместимы на одном типе** — `deny_unknown_fields` срабатывает до того, как
serde разложит ключи в поле. Значит, заповедник нельзя получить, просто оставив
`deny_unknown_fields`; нужно **явное поле** в `Manifest`, например
`#[serde(default, rename = "tool")] pub tool: BTreeMap&lt;String, toml::Value&gt;`
(или `toml::Value`/`serde_json::Value`), и тогда `deny_unknown_fields` на
остальном продолжает ловить опечатки. Это конкретное, машинно-реализуемое правило,
а не пожелание.</p>
    </section>
    <section title="3.4. Как читатель вообще узнаёт автора файла?">
      <p p="35">**Никак, надёжно.** Файл не несёт поля «я рукописный» vs «я машинный». Поле
`[origin].generated_by` (`document.rs:249`) маркирует только факт «копия
сгенерирована `vibe workspace publish`», а не намерение автора. Значит, критерий
«строгость по автору» **нереализуем как сформулирован** — и свод, и я должны
пересесть на единственно реализуемый: **строгость по пространству имён** (3.2).</p>
    </section>
    <section title="3.5. Вопрос 2b — манифест пакета и манифест проекта: один формат или два?">
      <p p="36">**Сегодня — один.** `Manifest` — единая структура для всех ролей
(`crates/vibe-core/src/manifest/document.rs:6-26`): роль задаётся тем, **какие
секции присутствуют** (`[project]` ⊕ `[package]`, плюс `[workspace]`, `[origin]`),
по модели cargo. Опубликованная копия пакета — та же структура с добавленной
таблицей `[origin]` (`:219-252`). Один парсер, один `deny_unknown_fields`, одна
отсутствующая версия. `[ИЗ ДЕРЕВА]`</p>
      <p p="37">**Цена «один формат» (оставить как есть):**</p>
      <list ordered="false" p="38">
        <item>Нельзя сделать опубликованную копию терпимой, не сделав терпимой и копию
  проекта — **та же структура**.</item>
        <item>Нельзя версионировать их независимо.</item>
        <item>Опубликованный манифест несёт потребительские секции (`[requires]`,
  `[[registry]]`, `[i18n]`, `[boot]`), бессмысленные в артефакте, но допустимые
  схемой.</item>
        <item>Future-`vibe` читает old-опубликованный манифест той же строгой дверью →
  упирается в `deny_unknown_fields` на новом поле.</item>
      </list>
      <p p="39">**Цена «два формата» (разделить):**</p>
      <list ordered="false" p="40">
        <item>Большой рефакторинг: ~44 исходных манифеста (см. §5), весь модуль `manifest/`,
  каждый звонок `Manifest::read`/`parse_str`.</item>
        <item>Но даёт: опубликованной копии — свою версию, терпимое чтение, провенанс;
  копии проекта — строгость и ловлю опечатки.</item>
      </list>
      <p p="41">**Прецедент cargo `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]`:** у cargo **один** рукописный
`Cargo.toml`, но **другой**, машинно-сгенерированный, версионированный
индекс-формат (записи crates.io index несут поле версии схемы, которое
переезжало). Т.е. cargo по сути отвечает «две поверхности»: рукописная — для
своего инструмента, сгенерированная — для чужого.</p>
      <p p="42">**Мой суд (и он совпадает с рефреймом §0):** «две поверхности, один источник».
Не дробить `Manifest` на два типа, а **вообще не делать рукописный `vibe.toml`
внешней поверхностью**. Внешняя поверхность — каталог (`VersionEntry` уже несёт
`schema_version = 1`, уже читается клиентом терпимо). Рукописный `vibe.toml`
остаётся строгим и lockstep-читаемым; если чужому инструменту нужны данные пакета,
он идёт в каталог, а не парсит рукописный файл. Это растворяет вопросы 2/2b/3/4
для манифеста целиком. **Если же владелец непременно хочет, чтобы чужой инструмент
читал именно `vibe.toml` пакета напрямую**, тогда честный ответ — «два формата»,
и Published-вариант надо версионировать и сделать терпимым (лучше — сгенерировать
его, как vibe-wire).</p>
    </section>
  </section>
  <section title="4. Цена версии в манифесте (вопрос 4)">
    <p p="43">**Сначала счёт существующих манифестов `[ИЗ ДЕРЕВА]`** (всего 172 `vibe.toml`
в дереве, разбивка по верхнему каталогу):</p>
    <table p="44">
      <tr>
        <td>корзина</td>
        <td>число</td>
        <td>статус при вводе версии</td>
      </tr>
      <tr>
        <td>`packages/**` без `vibedeps` (исходники, авторские)</td>
        <td>**44**</td>
        <td>настоящая миграция</td>
      </tr>
      <tr>
        <td>`vibedeps/**` (регенерируемые копии зависимостей)</td>
        <td>~116</td>
        <td>регенерируются → не бремя, но замороженные checked-in копии сломаются</td>
      </tr>
      <tr>
        <td>`fixtures/**` + `crates/**/fixtures` (тестовые данные)</td>
        <td>~9</td>
        <td>**ломают тесты**, если поле обязательное</td>
      </tr>
      <tr>
        <td>`research/**`, корень и прочее</td>
        <td>~3</td>
        <td>проверять индивидуально</td>
      </tr>
    </table>
    <p p="45">(Числа — моя пересчётка `find … -name vibe.toml` по корзинам; «~» отмечает
группировку, полная сумма = 172.)</p>
    <p p="46">**Что делает читатель, встретив манифест без версии:**</p>
    <list ordered="false" p="47">
      <item>Если поле **необязательное** (отсутствие = 1, как предлагает свод, рек. №5):
  все 44 парсятся; поле остаётся пустым; фикстуры не трогаются; **но поле
  становится декоративным** — авторы его не填ят, и для чужого читателя оно не
  несёт информации.</item>
      <item>Если поле **обязательное** (как у `vibe.lock` и каталога): все 44 надо
  снабдить `schema_version = 1`; ~9 фикстур ломают тесты (в них Asserted точная
  форма); frozen `vibedeps`-копии ломаются, пока не перегенерены. Реальная цена.</item>
    </list>
    <p p="48">**Почему «считать первой версией» тут может быть НЕВЕРНО (ключевой момент
вопроса 4).** Правило «отсутствие → 1.0» свод берёт из PEP 629 `[ИЗ СВОДА]`. Оно
работает для PyPI, потому что их JSON-метаданные **машинно-сгенерированы** и поле
присутствует почти всегда. Для **рукописного** TOML то же правило
**саморазрушительно**:</p>
    <list ordered="true" p="49">
      <item>Необязательное поле + отсутствие=1 → авторы не заполняют → поле пустое у
   большинства из 44 → для чужого читателя оно бесполезно именно тогда, когда
   нужнее всего. Мы вводим версию, чтобы чужой читатель мог договориться о
   контракте, и тут же делаем её факультативной.</item>
      <item>Для **опубликованного артефакта** «нет версии» становится неоднозначным: «старый,
   до-версионный» или «опубликован инструментом, который поле выкинул»? В момент,
   когда чужому читателю важнее всего отличить старое от чужого/битого,
   отсутствие=1 это различие **стирает**.</item>
      <item>Эталон — наш же `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.</item>
    </list>
    <p p="50">**Но (и это главный выход):** если `vibe.toml` — НЕ внешняя поверхность
(рефрейм §0), то версия в манифесте **не нужна вовсе**, и весь вопрос отпадает.
Версия принадлежит каталогу (где она уже есть, =1). Так что «absence=1 неверно
для манифеста» — верно, но при правильной архитектуре вопрос не возникает:
манифест не версионируется, каталог — да.</p>
  </section>
  <section title="5. Порядок работ: каталог-полигон vs манифест-боевое (вопрос 5)">
    <p p="51">**Свод:** каталог — полигон (ломать даром), манифест — боевое.</p>
    <p p="52">**Сильнейший довод за ОБРАТНЫЙ порядок (манифест первым):** манифест —
**единственный** из трёх, что ездит внутри опубликованного артефакта и читается
чужим будущим инструментом; его forward-compat-часы тикают с момента первой
публикации. Каталог же сегодня читает только наш терпимый клиент; его часы
тикают лишь с появлением чужого потребителя (ноль сегодня). Значит, проектировать
forward-compat сначала на каталоге — строить его для формата с **наименее
срочной** потребностью.</p>
    <p p="53">**Суд — каталог первым, но по более сильной причине, чем «ломать даром».**
Довод «обратный порядок» бьёт по приоритету срочности, но упускает, что
манифест **социально дорог** (44 файла, живой потребитель, единственный автор —
он же владелец), а каталог — **структурно богат** и **социально дёшев**: в
каталоге 23 типа / ~86 полей `[ИЗ СВОДА]`, объединение, закрытые словари —
наибольшее структурное разнообразие, на котором машинерию (схема → кодоген →
чекер → версия-контракт) можно реально **обкатать**. Манифест структурно проще
(один документ, плоские таблицы), значит, на нём машинерия **менее
испытана**, а риск **выше**. Решение: доказать машинерию на
сложном-но-дешёвом (каталог), потом применить **доказанное** к
простому-но-дорогому (манифест). Обратный порядок испытывает меньше и рискует
больше — он хуже.</p>
    <p p="54">**Уточнение к «ломать даром»:** каталог не вполне бесплатен. Свод сам отмечает:
сервер не стартует на каталоге, записанном новым инструментом, потому что
обратное чтение идёт строгими типами (`primary.rs:92` → `VersionEntry` с
`deny_unknown_fields`). Значит, «бесплатно» = «без **внешних** последствий», а не
«без последствий вообще»; внутренний откат (рестарт сервера) реален, но
самоизлечим reindex'ом. Эту оговорку свод теряет.</p>
    <p p="55">**Итог: порядок свода верен; основание — «богатый полигон + дешёвые внешние
последствия», а не только «дёшево ломать».**</p>
  </section>
  <section title="6. Один чекер на три формата (вопрос 6)">
    <p p="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).</p>
    <list ordered="false" p="57">
      <item>**R1. Один источник для каждого словаря; все копии идентичны.** Для каждого
  закрытого enum'а (`PackageKind`, `NamingConvention`, `DeliveryMode`) ровно одно
  определение; остальные (`vibe-index`, `vibe-wire×2`) генерируются или
  parity-проверяются. Сегодня parity-тест ловит **только варианты**; **расширить
  до wire-строк** и — главное — **удалить машинно-непроверяемую прозу о
  мощности** («four», «fifth»): именно она разошлась (`kind.rs:16,18`,
  `kinds.rs:1`). Машинная проверка: сравнить множества `(вариант, wire-строка)`
  по всем четырём определениям; несовпадение → fail.</item>
      <item>**R2. Уникальность wire-строк и список отставленных.** Ни у какого enum'а два
  варианта не сериализуются в одну wire-строку; ни одна wire-строка не
  переиспользуется после удаления (ведётся явный список retired-ключей).
  Проверка: множество wire-строк текущих ∩ множество retired = ∅.</item>
      <item>**R3. Наличие поля версии соответствует спеку.** Для каждого формата, который
  спек объявляет «версионным», верхнеуровневая структура несёт поле версии И
  проставляет его при записи. Сегодня: `vibe.lock` и каталог — да; `vibe.toml` —
  нет. Если спек говорит «манифест не версионный» (рефрейм §0), отсутствие —
  корректно; если говорит «версионный» — fail. Проверка читает specmark-анкор и
  сравнивает с наличием поля.</item>
      <item>**R4. Семантика версии соответствует контракту.** Там, где поле есть,
  поведение читателя = (major→отказ с цитатой; minor→warn-continue). `vibe.lock`
  — единственный, кто сравнивает, и он отказывает по `!=` (`lockfile.rs:430`):
  проверить, что это совпадает с его заявленным контрактом «ровно одна
  версия, pre-release». Каталог объявил версию, но **не сравнивает нигде** — если
  спек обещает «старый читатель игнорирует новые поля», чекер должен это
  противоречие поймать (см. R5).</item>
      <item>**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`); чекер читает
  контракт по анкору и диффает с атрибутом.</item>
      <item>**R6. Заповедник расширений реально работает.** Если объявлен зарезервированный
  namespace (`[tool.*]`), чекер Asserts, что либо на верхнем уровне нет
  `deny_unknown_fields`, либо заповедник смоделирован **явным полем** (см. §3.3),
  иначе `[tool.x]` молча отвергается. Защита от «объявили заповедник, а он не
  работает».</item>
      <item>**R7. Round-trip сохраняет неизвестное-в-заповеднике.** write→read→write
  байт-идентичен (по модулю комментариев) для каждого формата; в частности,
  секция `[tool.*]` переживает цикл. Ловит класс «терпим, но молча выкидываем».</item>
    </list>
    <p p="58">**Что объединяет три формата (чтобы не разошлись):** R1 + R5 применяются ко всем
трём с **одним источником истины — спеком**. specmark-анкор — та самая «одна
точка», чекер диффает каждый форматный код против неё. Так дрейф
`PackageKind`-типа удерживается: вариант проверяется кодогеном/parity (R1),
соответствие строгости — против спека (R5), а машинно-непроверяемая проза о
мощности просто удаляется. R5 — высшей ценности: он поймал бы сегодняшнее
противоречие каталога до того, как его нащёл свод.</p>
  </section>
  <section title="7. С чем я НЕ согласен (списком)">
    <list ordered="true" p="59">
      <item>**С посылом «применить те же выводы к парсеру `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`)</item>
      <item>**С «строгость зависит от автора файла».** Автор не виден из файла
   (`[origin].generated_by` — про провенанс публикации, не про намерение);
   критерий нереализуем. Единственный реализуемый — **по пространству имён**
   (§3.2), и свод сам к нему склоняется, но оставляет «автора» как сигнал.</item>
      <item>**С характеристикой объединения как «без тега, читатель угадывает».** Оно
   полутегированное: `Directory` несёт явный `kind`-тег (`repomd.rs:46`); угадывание
   только в том, какого плеча тег нет. Аналогия с PyPI PEP 714 структурно
   натянута — плечи с непересекающимися ключами, не «одно имя → два типа».</item>
      <item>**С «absence = считать первой версией» для манифеста.** Для рукописного формата
   это саморазрушительно (поле становится декоративным; стирает «старый vs
   чужой»). Эталон — наш же `vibe.lock`, который делает версию обязательной и
   отвергает отсутствие. (Впрочем, при рефрейме §0 версии в манифесте нет вовсе.)</item>
      <item>**С числом «5 закрытых словарей».** Строгим чтением нахожу 3 мультизначных
   fieldless-enum в типах каталога; отмечаю расхождение и не asserts свою цифру —
   зависит от определения «словаря». Существенно (и верно): `#[serde(other)]` нет
   нигде, неизвестное значение = ошибка разбора.</item>
      <item>**С формулировкой «один вопрос или три».** Свод не предлагает один бит на все
   три; он предлагает один фреймворк с per-format-строгостью. Реальная развилка —
   «один спек контракта с тремя экземплярами vs три независимых спека» — и ответ
   «один», потому что три гарантируют дрейф (живой пример — `PackageKind` в 4
   экземплярах, проза разошлась: «four»/«fifth» при 6 вариантах).</item>
    </list>
    <p p="60">**С чем согласен и добавляю:** тегировать объединения; писать пустую коллекцию
явно; держать запасное значение у закрытых словарей; не переиспользовать имена +
список отставленных; каталог первым. **Добавление свода:** машина
«схема→кодогенерация» уже существует (`vibe-wire`, JTD, решение владельца
2026-08-06 о терпимости для внешних форматов) — значит, работа не «построить», а
«распространить на каталог и связать чекером R5».</p>
  </section>
  <section title="8. Сводная рекомендация (одним абзацем)">
    <p p="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` обещанию спека) — высшей ценности, он поймал бы сегодняшнее
противоречие автоматически.</p>
  </section>
</spec>
