<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title>G1-NEIGHBOURS — реестры пакетов как данные в покое</title>
  <p p="1">Второй независимый разбор по теме **G1-NEIGHBOURS**. Предмет — наш каталог
пакетов (JSON-файлы в git-репозитории, которые читают чужие инструменты) на
фоне выживших реестров-соседей. Это задача на суждение, не на постройку: ни одна
строка кода не правилась. Всё, что проверено чтением нашего дерева, помечено
`[ИЗ ДЕРЕВА]` с `файл:строка`; всё из `FINDINGS-DIGEST.md` — `[ИЗ СВОДА]`;
всё из моих знаний о чужих системах — `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` (без
выдуманных версий, дат и номеров issue, доступа в сеть нет).</p>
  <section title="0. Что я перепроверил в дереве и где свод неточен">
    <p p="2">Прежде чем отвечать на вопросы — фиксирую расхождения со сводом, потому что на
них держат часть дальнейшего.</p>
    <p p="3">**1. Объединение `RepomdFileEntry` — не «без признака».** Свод `[ИЗ СВОДА]`
утверждает: «Общего поля-признака нет; читатель угадывает по набору ключей.»
Код `[ИЗ ДЕРЕВА: crates/vibe-index/src/types/repomd.rs:42-55]` показывает
другое: enum и правда `#[serde(untagged)]`, но вариант `Directory` несёт
**явный дискриминатор** `kind: DirectoryTag`, а комментарий в строках 46–48
прямо говорит, что этот тег добавлен *намеренно*, «чтобы матчер `untagged`
различал однозначно». То есть «угадывания по ключам» здесь нет — автор
встроил тег в более тяжёлый вариант. Реальная хрупкость иная и тоньше: у
варианта `File { size, sha256 }` тега **нет**, поэтому добавление третьего
варианта без `kind` может попасть под тень формы `{size, sha256}`. Свод
описал симптом, но неверно назвал причину — а рекомендация «тегировать каждое
объединение» (п. 1 рекомендации свода) для этого места на 90 % уже выполнена,
не хватает тега только в варианте `File`.</p>
    <p p="4">**2. «15 мест `deny_unknown_fields`» — верно.** Пересчитал в
`crates/vibe-index/src/types/`: `repomd` (1) + `relations` (6) + `entry/mod`
(1) + `content` (5) + `aggregate` (2) = **15** `[ИЗ ДЕРЕВА]`. Совпало.</p>
    <p p="5">**3. «5 закрытых словарей» — я насчитал 4.** В типах каталога строковых
enum-словарей ровно четыре: `PackageKind`
(`[ИЗ ДЕРЕВА: crates/vibe-index/src/types/kinds.rs:21-34]`),
`NamingConvention` (`kinds.rs:88-112`), `DeliveryMode`
(`content.rs:53-57`), `DirectoryTag` (`repomd.rs:75-77`). `relations.rs`
содержит только структуры. Пятого в `vibe-index/src/types/` я не нашёл и
выдумывать не буду — возможно, свод считал по более широкому срезу или считал
дубликат `DeliveryMode` из `vibe-core` (`subskill.rs:120`). Это не критично,
но число в своде не воспроизводится по его же декомпозиции.</p>
    <p p="6">**4. Спека противоречит не только коду, но и себе — и это сильнее, чем в своде.**
Свод `[ИЗ СВОДА]` поймал трещину: код имеет `deny_unknown_fields`, а спека
обещает «читатели спокойно игнорируют незнакомые поля». Я нашёл, что в самой
`PROP-005` **два факта взаимоисключающи**:</p>
    <list ordered="false" p="7">
      <item>`FORWARD-COMPAT` `[ИЗ ДЕРЕВА: vibevm/vibespecs/modules/vibe-index/PROP-005-package-index.xml:302]`:
  «readers of v2 written by an old vibevm gracefully ignore unknown fields»,
  статус `impl/done`.</item>
      <item>`NEVER-SILENT-SCHEMA` `[ИЗ ДЕРЕВА: PROP-005-package-index.xml:624]`:
  «Old consumers parsing a higher schema must surface a "please upgrade"
  message — **not** silently parse the subset they understand», статус
  `impl/done`.</item>
    </list>
    <p p="8">Это одно и то же событие (старый читатель видит запись новой версии) с двумя
противоположными предписаниями. Код `[ИЗ ДЕРЕВА: entry/mod.rs:38]` делает
**третье**: жёсткая ошибка разбора (ни «игнор», ни «аккуратное сообщение об
апгрейде»). И главное — поле `schema_version` `[ИЗ ДЕРЕВА: entry/mod.rs:44]`
**нигде не сравнивается** (только пишется как константа `= 1`, `mod.rs:124`),
поэтому детектор «higher schema» из `NEVER-SILENT-SCHEMA` в принципе не может
сработать. Свод увидел одну трещину; их три — и одна внутри самого документа.</p>
    <p p="9">**5. Толерантность клиента — не «уже правильны», а неполна и притом случайна.**
Свод `[ИЗ СВОДА]` выставляет клиентский путь как имеющийся плюс. По коду:
view-структуры в `index_client/wire.rs` терпимы к **полям** (`NameEntryView`
читает только `version`, остальное игнорирует; комментарий
`[ИЗ ДЕРЕВА: crates/vibe-registry/src/index_client/wire.rs:14-16, 36-39]`).
Но те же структуры декодируют `kind: PackageKind` (`wire.rs:57,85`) и
`BindingSite` (`wire.rs:94`) как **закрытые enum без `#[serde(other)]`** —
значит, они ломаются на *новом значении* словаря. Это именно та неудача,
которую сам свод (`Часть 3/4`) называет более медленной и дорогой: «поле
можно пропустить, значение словаря обязано лечь в типизированную ячейку»
`[ИЗ СВОДА]`. Мы «починили поля и оставили Values открытыми». Дополнительно:
терпимость `vibe-wire` — **не решение, а неспособность генератора выдать
строгость** (`[ИЗ ДЕРЕВА: crates/vibe-wire/src/lib.rs:26-43]` — «the generator
cannot emit it … until 2026-08-06 nobody had chosen either»), задним числом
ратифицированная решением владельца. И покрывает она сейчас только **отчёты
команд** (`[ИЗ ДЕРЕВА: schemas/*.jtd.json]` — 7 схем `*_report`/`*_plan`, ни
одной для каталога), а не сам формат каталога — `VersionEntry`/`Repomd` всё
ещё hand-written.</p>
  </section>
  <section title="1. Один принцип, из которого следуют приёмы (вопрос 1)">
    <p p="10">Свод перечисляет приёмы поодиночке: тегировать объединения, писать пустое
явно, держать catch-all у словарей, версионный контракт, «новый путь, не
новая версия», зарезервированные `_`-ключи, схема-вместе-с-данными (Avro).
За ними — **один принцип**:</p>
    <quote p="11">**Долговечность формата — это дисциплина адресованного неведения: каждой
форме незнания читателя (неизвестное поле, значение, версия, форма, схема,
наличие) отводится определённое место, где оно может приземлиться, а не
становиться ошибкой разбора.**</quote>
    <p p="12">Приёмы выводятся из него как «где живёт данная форма неведения»:</p>
    <table p="13">
      <tr>
        <td>Форма неведения</td>
        <td>Место, которое даёт выживший формат</td>
        <td>Следствие для нас</td>
      </tr>
      <tr>
        <td>Неизвестное **поле**</td>
        <td>«проигнорировано» (tolerant reader, без `deny`)</td>
        <td>у нас — ошибка `[ИЗ ДЕРЕВА: entry/mod.rs:38]`</td>
      </tr>
      <tr>
        <td>Неизвестное **значение** словаря</td>
        <td>catch-all `Other` / свободная строка</td>
        <td>у нас — ошибка, `#[serde(other)]` нет нигде `[ИЗ ДЕРЕВА]`</td>
      </tr>
      <tr>
        <td>Неизвестная **версия**</td>
        <td>правило: major→отказ, minor→warn, нет→1 `[ИЗ СВОДА, PEP 629]`</td>
        <td>у нас — ярлык без сравнения `[ИЗ ДЕРЕВА: entry/mod.rs:44,124]`</td>
      </tr>
      <tr>
        <td>Неизвестная **форма**</td>
        <td>новый путь, старый остаётся валидным `[ИЗ СВОДА]`</td>
        <td>не заложено</td>
      </tr>
      <tr>
        <td>Чужое **расширение**</td>
        <td>зарезервированный неймспейс (`_`) `[ИЗ СВОДА]`</td>
        <td>нет вовсе (`deny_unknown_fields` убивает любое чужое поле)</td>
      </tr>
      <tr>
        <td>«Не записано» vs «известно-пусто»</td>
        <td>различие absence/empty `[ИЗ СВОДА]`</td>
        <td>у нас слитo (`skip_serializing_if = is_empty` `[ИЗ ДЕРЕВА: entry/mod.rs:70-79]`)</td>
      </tr>
      <tr>
        <td>Неизвестная **схема писателя**</td>
        <td>приложенная схема (Avro) `[ИЗ СВОДА]`</td>
        <td>см. §3 — не переносится</td>
      </tr>
    </table>
    <p p="14">Королларий, отличающий выживших: они **назначают границу сознательно и пишут
её** (двухуровневый словарь NuGet, нормативный контракт PEP 629 — оба `[ИЗ
СВОДА]`). Homebrew `[ИЗ СВОДА]` провалился не оттого, что не знал приёма, а
оттого, что никогда не провёл границу. Наш случай (§0.4) — тот же грех в
острой форме: граница есть в спеке, но две разные и обе «impl/done».</p>
    <p p="15">Из принципа сразу следует проверка любого решения: **«куда приземлится
незнакомое?»** Если ответ «никуда, ошибка» — формат хрупок в будущем. Вся
наша каталожная машина сегодня отвечает «ошибка» на пять строк таблицы из
семи.</p>
  </section>
  <section title="2. Чем наш случай отличен по существу (вопрос 2) — 4 отличия">
    <p p="16">Свод сравнивает приёмы; я сравниваю **структурные условия**, потому что именно
они делают чужой приём неприменимым.</p>
    <p p="17">**Отличие 1 — Автор один, и это инструмент, а не открытая человеческая подача.**
PyPI/Debian/npm `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` принимают манифесты от тысяч
независимых людей, пишущих任意 инструментами произвольный по качеству
метаданные; терпимость их реестров в первую очередь держит **неправильный
человеческий ввод**. Наш каталог пишет **одна машина** — `vibe-index add` /
сканер (`[ИЗ ДЕРЕВА: crates/vibe-index/src/scanner/org_walk.rs:203,
scanner/from_github.rs, cli/add.rs:85]`); hand-authoring исключён. Значит,
главное давление, породившее терпимость PyPI, у нас **отсутствует**:
tolerant-reader покупает нам меньше страховки, чем им. Строгость на входной
двери (для opечаток) имеет смысл для `vibe.toml` (пишут руки), но для каталога
(пишет машина) выгода strict-режима резко падает. Это подпирает рекомендацию
свода №4, но даёт ей **причину**, которой у свода нет.</p>
    <p p="18">**Отличие 2 — На критическом пути чтения нет живого сервера.** Потребитель
берёт **статический файл** по raw-URL или git clone
(`[ИЗ ДЕРЕВА: PROP-005-package-index.xml:66]` — raw-URL fetchable без clone;
`:598` — резолвер делает `HTTP GET &lt;index_url&gt;/repomd.json`; сервер
`vibe-index` опционален). PyPI/Maven/npm `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — живые
реестры: они могут серверно торговаться (`Accept: vnd…v2`, deprecation-заголовки,
shadow-serve, переводят старое в новое адаптером на лету). У нас
**посредника нет** — совместимость обязана жить **в самом файле**. Это
ключевой факт: он обесценивает весь класс «сервер-versioning» и делает
правильным рычагом именно §5 («новый путь»). Свод этот факт упоминает мимоходом;
по мне он — ось всей темы.</p>
    <p p="19">**Отличие 3 — Читателей сегодня ноль, и они перечислимы (кривая стоимости
перевёрнута).** Сам свод `[ИЗ СВОДА]` фиксирует: внешних потребителей
пока ноль, и что переименование поля в PyPI обошлось дёшево **именно потому,
что читателей было трое и их обзвонили**. Зрелые соседи (заморозка навсегда,
новый-путь-без-переиспользования, зарезервированный неймспейс) — это ответы на
условие «я **не могу** достучаться до своих читателей». Мы — **можем**: мы в
окне раннего-PyPI, не зрелого. Значит, оптимальная стратегия сейчас —
**потратить это окно** (мигрировать, править, даже ломать-и-переименовывать,
пока это бесплатно), а аппарат терпимости строить **готовым к заморозке**, но
не замораживать. Преждевременная заморозка — единственное преимущество,
которое у нас есть перед зрелыми соседями, мы про́дадим задаром. Это моё
самое сильное расхождение с позицией свода (см. §7).</p>
    <p p="20">**Отличие 4 — Каталог и описываемый им артефакт делят одну git-историю.**
В RPM/Debian `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` индекс — производный взгляд над
пулом бинарных артефактов; пул и индекс разделены, можно оставить старый пул
и добавить новый индекс. У нас «артефакт» — это git-репозиторий пакета
(`source_url`/`source_ref`/`content_hash` ссылаются на него,
`[ИЗ ДЕРЕВА: entry/mod.rs:55-57]`), а метаданные и репозиторий движутся
**вместе** в одной истории. Это меняет экономику §5: нельзя «заморозить
старый пул артефактов и выпустить новый индекс» — форма и описываемое
сцеплены. Зато git даёт бесплатную неизменяемость старого пути через
branch/tag-policy (см. §5), чего у живого сервера нет.</p>
  </section>
  <section title="3. Где свод натянул аналогию (вопрос 3)">
    <p p="21">**Аналогия Avro-в-git внутренне противоречива самому тезису свода.** Свод
`[ИЗ СВОДА]`: Avro дорог в сети тем, что кладёт схему писателя в каждое
сообщение, но «в git-репозитории это стоит указателя». Звучит — но Avro
**работает** не транспортом схемы, а **согласованием** схемы писателя со
схемой читателя в момент чтения (promotions, разрешение псевдонимов) `[ПО
ПАМЯТИ, НЕ ПРОВЕРЕНО]` — а это **отношение** между двумя схемами, разрешаемое
живым читателем по правилам. И ровно **отношения**, по собственному тезису
свода (`Часть 4`: «Файл — это ответ, у которого не было вопроса»; всё, что
свойство *отношений*, «умирает при соприкосновении с файлом в git» `[ИЗ
СВОДА]`), на диске не выживают. Указатель в git дёшево доставляет схему
писателя — согласен, — но **примирение** схем всё равно требует читателя,
знающего правила, что схлопывается обратно до «tolerant reader» (который нам
и так нужен) плюс «схема лежит в git» (что у нас уже есть — это спека).
Avro добавляет **ничего** поверх «файл + толерантный читатель»; притягательность
аналогии (дешёвый транспорт схемы) реальна, а её **плата** (автоматическая
совместимость) не переносится. Растянуто ровно в том, что mattered.</p>
    <p p="22">**Бонус — Cloudflare как «ближайший аналог».`[ИЗ СВОДА]` называет разбор
аварии Cloudflare «ближайшим аналогом нашего случая». Но (а) урок аварии —
«ужесточить приём собственных сгенерированных файлов как пользовательский
ввод» `[ИЗ СВОДА]` — толкает к **большей строгости** на сгенерированных
файлах, что в напряжении с тезисом о tolerant-reader, который свод строит;
(б) отказ Cloudflare — молчаливый mis-score у развёрнутого бинарного парсера с
лимитом размера `[ИЗ СВОДА]`; наш же аналог (`deny_unknown_fields`) даёт
**жёсткую**, а не молчаливую ошибку — противоположный режим отказа. Авария
поддерживает «статическая проверка в сборке» (что свод тоже держит), но не
«толерантный читатель», и звать её «ближайшим аналогом» — преувеличение
посадки.</p>
  </section>
  <section title="4. Чего не хватает в списке соседей (вопрос 4)">
    <p p="23">В теме соседи перечислены, но **анализ** в `Части 2` свода дан лишь для
PyPI/PEP, NuGet и Homebrew. Самый близкий нам структурный двойник назван в
теме, но **не разобран**:</p>
    <list ordered="false" p="24">
      <item>**Индекс crates.io** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — это тоже JSON-in-git
  (исторически git-репозиторий индекса, который клиент клонировал; позже —
  sparse-HTTP). Один инструмент пишет (publish), один известный клиент читает
  (cargo); формат индекса версионируется, и сопровождавшим приходилось
  проводить миграции формата индекса (переход git-clone→sparse — отдельная
  глава). Это наш случай почти один-в-один: «машинный индекс в git/HTTP,
  читаемый чужим инструментом, с эволюцией формата». Он ближе, чем PyPI
  (открытая человеческая подача) или NuGet (живой сервер). Деталей механики
  версионирования я по памяти цитировать не могу — но утверждаю, что **именно
  этот сосед должен был быть главным зеркалом**, а не PyPI. PyPI ценен как
  *авария* (PEP 714), crates.io — как *рабочая миграция формата* того же
  класса, что и наш.</item>
    </list>
    <p p="25">Два дополнительных, под наши подслючаи:</p>
    <list ordered="false" p="26">
      <item>**Helm `index.yaml`** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — сгенерированный индекс
  chart-репозитория, статический файл, читаемый Helm; несёт поле `apiVersion`
  — прямой аналог нашего вопроса «версия как контракт». Ближе к каталогу, чем
  реестры с живым сервером.</item>
      <item>**Nix `flake.lock`** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — машинный lockfile в git с
  версиионированной схемой (`version`/`nodes`); хорошая параллель к нашему
  `vibe.lock` (который, в отличие от каталога, версию **проверяет и отвергает**
  `[ИЗ ДЕРЕВА: crates/vibe-core/src/manifest/lockfile.rs:430]`,
  `CURRENT_SCHEMA_VERSION = 5` `:50`).</item>
    </list>
  </section>
  <section title="5. «Ломающее изменение — новый путь» — конкретно для git (вопрос 5)">
    <p p="27">«Путь» в нашем случае — это **путь файла в git-репозитории индекса**, он же
raw-URL: `repomd.json`, `primary.jsonl`, `by-name/&lt;name&gt;.json`
(`[ИЗ ДЕРЕВА: PROP-005-package-index.xml:66, 118-205]`). «Новый путь» для
ломающего изменения = публиковать каталог v2 по **другому пути**, оставив v1
замороженным на старом:</p>
    <list ordered="false" p="28">
      <item>вариант пути: подкаталог `v2/repomd.json` (или суффикс `repomd.v2.json`, или
  ветка/тег `index-v2`);</item>
      <item>старый потребитель идёт по `repomd.json` — там по-прежнему валидный v1;</item>
      <item>новый потребитель идёт по `v2/repomd.json`.</item>
    </list>
    <p p="29">**Что это стоит.** (1) **Двойная запись** на каждом reindex — писать приходится
оба дерева; `by-name`/`primary` фан-out зеркалится под `v2/`. (2)
**Обнаружимость** — как новый клиент узнаёт, что есть v2? Указателем в v1
(например, поле `successor` в `repomd.json`) — но это поле само должно быть
forward-совместимым в v1, что создаёт *проблему самозагрузки* (первое такое
поле нельзя ввести «новым путём», его надо ввести как **добавление поля** — а
это требует толерантного читателя v1, которого у нас пока нет, §0.5). (3)
**Дилемма стороны записи**: v1 перестаёт принимать новые пакеты → старые клиенты
видят честно-устаревший, но валидный снимок; или v1 продолжает зеркалить →
вы поддерживаете двух писателей. Это и есть настоящая цена.</p>
    <p p="30">**Что происходит с тем, кто ходит по старому пути через год.** Он получает
**замороженный v1 навсегда** — работает, но перестаёт видеть пакеты,
опубликованные после разреза. Это приемлемо тогда и только тогда, когда v1
честно помечен «frozen, superseded». Молчаливая остановка обновлений (старый
путь отвечает, но данные не растут) — это тихое устаревание, ровно то, что
RFC 9413 `[ИЗ СВОДА]` клеймит как закрепление дефекта.</p>
    <p p="31">**Почему нам это дешевле, чем PyPI/NuGet** (отличие 2 + 4 из §2): у живого
сервера «заморозить старый путь» = держать работающий adapter бесконечно;
у нас «заморозить старый путь» = **branch policy / тег** git'а по нулевой
маржинальной цене (raw-URL, приколотый к тегу, неизменяем даром). Наше
структурное преимущество — git хранит каждую версию навсегда бесплатно, — надо
эксплуатировать явно: «новый путь» = «новый тег/ветка», а старая замораживается
policy, не кодом сервера.</p>
  </section>
  <section title="6. Двухуровневый словарь NuGet — на наших настоящих именах (вопрос 6)">
    <p p="32">NuGet `[ИЗ СВОДА]`: документированные ресурсы вечны, недокументированные
удаляются свободно, и **это написано**. У нас аналогичная граница — не
«документировано/нет» (расширительного неймспейса у нас ещё нет), а
**«ответвляется ли потребитель на этом значении»**:</p>
    <list ordered="false" p="33">
      <item>**Верхний уровень (вечные): значение, на котором потребитель ВЕТВИТСЯ —
  маршрутизация, материализация, boot-linking. Должны быть закрытыми enum'ами,
  версионируемыми, без переиспользования имён, с catch-all `unknown`.**</item>
      <item>`kind: PackageKind` `[ИЗ ДЕРЕВА: kinds.rs:21-34; entry/mod.rs:46]` —
    flow/feat/stack/tool/mcp/lang; в PROP-008 §2.3 это «метаданные», но
    реально ведёт именование и маршрутизацию.</item>
      <item>`delivery: DeliveryMode` `[ИЗ ДЕРЕВА: content.rs:53-57]` —
    eager/lazy-push/lazy-pull; определяет, как материализуются subskills.</item>
      <item>`naming: NamingConvention` `[ИЗ ДЕРЕВА: kinds.rs:88-112; repomd.rs:24]` —
    fqdn/kind-name/name/kind-name; определяет path-mapping.</item>
      <item>`format: PackageFormat` `[ИЗ ДЕРЕВА: crates/vibe-core/src/manifest/package.rs:155]`
    — simple/normal; определяет boot-linking.</item>
    </list>
    <list ordered="false" p="34">
      <item>**Нижний уровень (свободные к эволюции/удалению, несут «неизвестное»):
  значение, которое потребитель только показывает или использует как подсказку.**</item>
      <item>`boot_snippet.category` `[ИЗ ДЕРЕВА: content.rs:99-100]` — это **уже**
    `Option&lt;String&gt;`, а не enum, хотя морально это тот же маленький закрытый
    набор («foundation / flow / stack / user-override», комментарий в
    `content.rs:96-98`). Проект **уже** применил двухуровневую интуицию здесь —
    ad hoc, не назвав: `category` оставлен строкой именно потому, что это
    hint-порядка, а не идентичность.</item>
      <item>`description`, `keywords`, `homepage` `[ИЗ ДЕРЕВА: entry/mod.rs:75-79]` —
    display-only; кандидаты в нижний уровень / на удаление без шума.</item>
      <item>`BindingSite` `[ИЗ ДЕРЕВА: index_client/wire.rs:94]` — клиентский enum
    package/subskill; может быть нижним.</item>
    </list>
    <p p="35">**Пробел, который вскрывает упражнение.** У NuGet нижний уровень — это **место**
(ресурс, который можно удалить). У нас **места нет**: `deny_unknown_fields`
значит, что чужому/расширительному ключу **негде жить**. Ближе всего в
экосистеме — `_`-заповедник PyPI/NuGet `[ИЗ СВОДА]`, которого у нас нет.
Принять двухуровневую модель для нас буквально означает: (а) назвать, какие
поля верхние, какие нижние; (б) **дать нижнему уровню дом** — зарезервированный
`_`-namespace либо явный `#[serde(other)]`/catch-all на верхних enum'ах —
чтобы у неведения был адрес. Это и есть замыкание на принцип §1.</p>
    <p p="36">**Свидетельство, что границу не проводили сознательно.** `delivery` — строгий
3-вариантный enum (`content.rs:53`), а `boot_snippet.category` — тот же класс
маленького закрытого набора, но свободная строка (`content.rs:100`). Одно и то
же правило не применяется; значит, разделительная линия никогда не была
решением. Это `homebrew`-симптом в нашем коде — не отсутствие приёма, а
отсутствие проведённой границы.</p>
  </section>
  <section title="7. Сводка несогласий (консолидированно)">
    <list ordered="true" p="37">
      <item>**`RepomdFileEntry` — не «без признака»**: тег `kind` вшит в вариант
   Directory намеренно (`repomd.rs:44-49`); свод перепутал симптом и причину.</item>
      <item>**Закрытых словарей 4, не 5** — по декомпозиции самого свода число не бьётся.</item>
      <item>**Спека противоречит себе** (`FORWARD-COMPAT` :302 vs `NEVER-SILENT-SCHEMA`
   :624), оба `impl/done`; код делает третье; `schema_version` не сравнивается
   нигде — свод увидел одну трещину из трёх.</item>
      <item>**Толерантность клиента неполна** (ломается на новом *значении* `kind`/
   `BindingSite`, а не на поле) и **случайна** (codegen не умеет `deny`,
   `vibe-wire/lib.rs:26-43`; покрывает отчёты, не каталог).</item>
      <item>**Avro-в-git растянут** и противоречит собственному тезису свода о смерти
   «свойств отношений» на диске.</item>
      <item>**Главное: стратегический шаг сведён к «построй аппарат терпимости сейчас».**
   Я считаю правильным обратное: при читателях=0 сегодня максимально дёшево
   **ломать и переименовывать**, а аппарат строить **готовым к заморозке**, но
   не замораживать. Окно (как у раннего PyPI с его тремя читателями) закроется,
   и тогда вступят техники зрелых соседей — но не сейчас.</item>
    </list>
  </section>
  <section title="8. Чего я бы добавил к рекомендации свода">
    <list ordered="false" p="38">
      <item>**JTD-codegen уже частично построен** (`vibe-wire`, `schemas/`, `tools/jtd-codegen`)
  `[ИЗ ДЕРЕВА]`, но покрывает **отчёты команд**, а не каталог. Раз владельцу
  хочется «схемы → код», первый честный шаг — **описать каталог JTD-схемой** и
  прогнать через тот же генератор. Это немедленно выставит вопрос «что делает
  генератор с неизвестными полями» — и ответ сегодня «терпит, потому что не
  умеет строго» (`lib.rs:26-43`). То есть решение о tolerant-vs-strict для
  каталога **невозможно отложить**: сам переход на codegen его навязывает.</item>
      <item>**`deny_unknown_fields` совместим с forward-compat только при наличии
  зарезервированного неймспейса.** Минимальная обратимо-совместимая правка —
  не снимать `deny`, а добавить **один** одобренный escape-хэтч (`x-*` или
  `_`-ключи), как у PyPI/NuGet `[ИЗ СВОДА]`. Тогда строгость (для опечаток в
  `vibe.toml`) и forward-compat (для каталога) не конфликтуют — они разнесены
  по пространству имён, что совпадает с рекомендацией свода №4 («разделять
  пространством имён»), но доведённой до конкретного механизма.</item>
      <item>**Заморозить список отставленных имён полей сейчас**, пока переименование
  бесплатно (рекомендация №6 свода верна, но её надо делать **до** первого
  внешнего читателя — то есть сейчас, а не «когда понадобится»).</item>
      <item>**Контракт версии — внедрить как проверку в сборке**, а не как runtime-ветку:
  статически гарантировать, что minor-бамп вводит только необязательные поля,
  major-бамп — только по новому пути (§5). Переносимая практика из свода
  (`Часть 4`: статическая проверка совместимости в сборке `[ИЗ СВОДА]`), и ей
  не нужен ни сервер, ни телеметрия.</item>
    </list>
  </section>
</spec>
