# G1-NEIGHBOURS — реестры пакетов как данные в покое

[p01] Второй независимый разбор по теме **G1-NEIGHBOURS**. Предмет — наш каталог
пакетов (JSON-файлы в git-репозитории, которые читают чужие инструменты) на
фоне выживших реестров-соседей. Это задача на суждение, не на постройку: ни одна
строка кода не правилась. Всё, что проверено чтением нашего дерева, помечено
`[ИЗ ДЕРЕВА]` с `файл:строка`; всё из `FINDINGS-DIGEST.md` — `[ИЗ СВОДА]`;
всё из моих знаний о чужих системах — `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` (без
выдуманных версий, дат и номеров issue, доступа в сеть нет).

## 0. Что я перепроверил в дереве и где свод неточен

[p02] Прежде чем отвечать на вопросы — фиксирую расхождения со сводом, потому что на
них держат часть дальнейшего.

[p03] **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`.

[p04] **2. «15 мест `deny_unknown_fields`» — верно.** Пересчитал в
`crates/vibe-index/src/types/`: `repomd` (1) + `relations` (6) + `entry/mod`
(1) + `content` (5) + `aggregate` (2) = **15** `[ИЗ ДЕРЕВА]`. Совпало.

[p05] **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`). Это не критично,
но число в своде не воспроизводится по его же декомпозиции.

[p06] **4. Спека противоречит не только коду, но и себе — и это сильнее, чем в своде.**
Свод `[ИЗ СВОДА]` поймал трещину: код имеет `deny_unknown_fields`, а спека
обещает «читатели спокойно игнорируют незнакомые поля». Я нашёл, что в самой
`PROP-005` **два факта взаимоисключающи**:

- [p07] `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`.
- `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`.

[p08] Это одно и то же событие (старый читатель видит запись новой версии) с двумя
противоположными предписаниями. Код `[ИЗ ДЕРЕВА: entry/mod.rs:38]` делает
**третье**: жёсткая ошибка разбора (ни «игнор», ни «аккуратное сообщение об
апгрейде»). И главное — поле `schema_version` `[ИЗ ДЕРЕВА: entry/mod.rs:44]`
**нигде не сравнивается** (только пишется как константа `= 1`, `mod.rs:124`),
поэтому детектор «higher schema» из `NEVER-SILENT-SCHEMA` в принципе не может
сработать. Свод увидел одну трещину; их три — и одна внутри самого документа.

[p09] **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.

## 1. Один принцип, из которого следуют приёмы (вопрос 1)

[p10] Свод перечисляет приёмы поодиночке: тегировать объединения, писать пустое
явно, держать catch-all у словарей, версионный контракт, «новый путь, не
новая версия», зарезервированные `_`-ключи, схема-вместе-с-данными (Avro).
За ними — **один принцип**:

> [p11] **Долговечность формата — это дисциплина адресованного неведения: каждой
> форме незнания читателя (неизвестное поле, значение, версия, форма, схема,
> наличие) отводится определённое место, где оно может приземлиться, а не
> становиться ошибкой разбора.**

[p12] Приёмы выводятся из него как «где живёт данная форма неведения»:

[p13]
| Форма неведения | Место, которое даёт выживший формат | Следствие для нас |
| --- | --- | --- |
| Неизвестное **поле** | «проигнорировано» (tolerant reader, без `deny`) | у нас — ошибка `[ИЗ ДЕРЕВА: entry/mod.rs:38]` |
| Неизвестное **значение** словаря | catch-all `Other` / свободная строка | у нас — ошибка, `#[serde(other)]` нет нигде `[ИЗ ДЕРЕВА]` |
| Неизвестная **версия** | правило: major→отказ, minor→warn, нет→1 `[ИЗ СВОДА, PEP 629]` | у нас — ярлык без сравнения `[ИЗ ДЕРЕВА: entry/mod.rs:44,124]` |
| Неизвестная **форма** | новый путь, старый остаётся валидным `[ИЗ СВОДА]` | не заложено |
| Чужое **расширение** | зарезервированный неймспейс (`_`) `[ИЗ СВОДА]` | нет вовсе (`deny_unknown_fields` убивает любое чужое поле) |
| «Не записано» vs «известно-пусто» | различие absence/empty `[ИЗ СВОДА]` | у нас слитo (`skip_serializing_if = is_empty` `[ИЗ ДЕРЕВА: entry/mod.rs:70-79]`) |
| Неизвестная **схема писателя** | приложенная схема (Avro) `[ИЗ СВОДА]` | см. §3 — не переносится |

[p14] Королларий, отличающий выживших: они **назначают границу сознательно и пишут
её** (двухуровневый словарь NuGet, нормативный контракт PEP 629 — оба `[ИЗ
СВОДА]`). Homebrew `[ИЗ СВОДА]` провалился не оттого, что не знал приёма, а
оттого, что никогда не провёл границу. Наш случай (§0.4) — тот же грех в
острой форме: граница есть в спеке, но две разные и обе «impl/done».

[p15] Из принципа сразу следует проверка любого решения: **«куда приземлится
незнакомое?»** Если ответ «никуда, ошибка» — формат хрупок в будущем. Вся
наша каталожная машина сегодня отвечает «ошибка» на пять строк таблицы из
семи.

## 2. Чем наш случай отличен по существу (вопрос 2) — 4 отличия

[p16] Свод сравнивает приёмы; я сравниваю **структурные условия**, потому что именно
они делают чужой приём неприменимым.

[p17] **Отличие 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, но даёт ей **причину**, которой у свода нет.

[p18] **Отличие 2 — На критическом пути чтения нет живого сервера.** Потребитель
берёт **статический файл** по raw-URL или git clone
(`[ИЗ ДЕРЕВА: PROP-005-package-index.xml:66]` — raw-URL fetchable без clone;
`:598` — резолвер делает `HTTP GET <index_url>/repomd.json`; сервер
`vibe-index` опционален). PyPI/Maven/npm `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — живые
реестры: они могут серверно торговаться (`Accept: vnd…v2`, deprecation-заголовки,
shadow-serve, переводят старое в новое адаптером на лету). У нас
**посредника нет** — совместимость обязана жить **в самом файле**. Это
ключевой факт: он обесценивает весь класс «сервер-versioning» и делает
правильным рычагом именно §5 («новый путь»). Свод этот факт упоминает мимоходом;
по мне он — ось всей темы.

[p19] **Отличие 3 — Читателей сегодня ноль, и они перечислимы (кривая стоимости
перевёрнута).** Сам свод `[ИЗ СВОДА]` фиксирует: внешних потребителей
пока ноль, и что переименование поля в PyPI обошлось дёшево **именно потому,
что читателей было трое и их обзвонили**. Зрелые соседи (заморозка навсегда,
новый-путь-без-переиспользования, зарезервированный неймспейс) — это ответы на
условие «я **не могу** достучаться до своих читателей». Мы — **можем**: мы в
окне раннего-PyPI, не зрелого. Значит, оптимальная стратегия сейчас —
**потратить это окно** (мигрировать, править, даже ломать-и-переименовывать,
пока это бесплатно), а аппарат терпимости строить **готовым к заморозке**, но
не замораживать. Преждевременная заморозка — единственное преимущество,
которое у нас есть перед зрелыми соседями, мы про́дадим задаром. Это моё
самое сильное расхождение с позицией свода (см. §7).

[p20] **Отличие 4 — Каталог и описываемый им артефакт делят одну git-историю.**
В RPM/Debian `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` индекс — производный взгляд над
пулом бинарных артефактов; пул и индекс разделены, можно оставить старый пул
и добавить новый индекс. У нас «артефакт» — это git-репозиторий пакета
(`source_url`/`source_ref`/`content_hash` ссылаются на него,
`[ИЗ ДЕРЕВА: entry/mod.rs:55-57]`), а метаданные и репозиторий движутся
**вместе** в одной истории. Это меняет экономику §5: нельзя «заморозить
старый пул артефактов и выпустить новый индекс» — форма и описываемое
сцеплены. Зато git даёт бесплатную неизменяемость старого пути через
branch/tag-policy (см. §5), чего у живого сервера нет.

## 3. Где свод натянул аналогию (вопрос 3)

[p21] **Аналогия Avro-в-git внутренне противоречива самому тезису свода.** Свод
`[ИЗ СВОДА]`: Avro дорог в сети тем, что кладёт схему писателя в каждое
сообщение, но «в git-репозитории это стоит указателя». Звучит — но Avro
**работает** не транспортом схемы, а **согласованием** схемы писателя со
схемой читателя в момент чтения (promotions, разрешение псевдонимов) `[ПО
ПАМЯТИ, НЕ ПРОВЕРЕНО]` — а это **отношение** между двумя схемами, разрешаемое
живым читателем по правилам. И ровно **отношения**, по собственному тезису
свода (`Часть 4`: «Файл — это ответ, у которого не было вопроса»; всё, что
свойство *отношений*, «умирает при соприкосновении с файлом в git» `[ИЗ
СВОДА]`), на диске не выживают. Указатель в git дёшево доставляет схему
писателя — согласен, — но **примирение** схем всё равно требует читателя,
знающего правила, что схлопывается обратно до «tolerant reader» (который нам
и так нужен) плюс «схема лежит в git» (что у нас уже есть — это спека).
Avro добавляет **ничего** поверх «файл + толерантный читатель»; притягательность
аналогии (дешёвый транспорт схемы) реальна, а её **плата** (автоматическая
совместимость) не переносится. Растянуто ровно в том, что mattered.

[p22] **Бонус — Cloudflare как «ближайший аналог».`[ИЗ СВОДА]` называет разбор
аварии Cloudflare «ближайшим аналогом нашего случая». Но (а) урок аварии —
«ужесточить приём собственных сгенерированных файлов как пользовательский
ввод» `[ИЗ СВОДА]` — толкает к **большей строгости** на сгенерированных
файлах, что в напряжении с тезисом о tolerant-reader, который свод строит;
(б) отказ Cloudflare — молчаливый mis-score у развёрнутого бинарного парсера с
лимитом размера `[ИЗ СВОДА]`; наш же аналог (`deny_unknown_fields`) даёт
**жёсткую**, а не молчаливую ошибку — противоположный режим отказа. Авария
поддерживает «статическая проверка в сборке» (что свод тоже держит), но не
«толерантный читатель», и звать её «ближайшим аналогом» — преувеличение
посадки.

## 4. Чего не хватает в списке соседей (вопрос 4)

[p23] В теме соседи перечислены, но **анализ** в `Части 2` свода дан лишь для
PyPI/PEP, NuGet и Homebrew. Самый близкий нам структурный двойник назван в
теме, но **не разобран**:

- [p24] **Индекс crates.io** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — это тоже JSON-in-git
  (исторически git-репозиторий индекса, который клиент клонировал; позже —
  sparse-HTTP). Один инструмент пишет (publish), один известный клиент читает
  (cargo); формат индекса версионируется, и сопровождавшим приходилось
  проводить миграции формата индекса (переход git-clone→sparse — отдельная
  глава). Это наш случай почти один-в-один: «машинный индекс в git/HTTP,
  читаемый чужим инструментом, с эволюцией формата». Он ближе, чем PyPI
  (открытая человеческая подача) или NuGet (живой сервер). Деталей механики
  версионирования я по памяти цитировать не могу — но утверждаю, что **именно
  этот сосед должен был быть главным зеркалом**, а не PyPI. PyPI ценен как
  *авария* (PEP 714), crates.io — как *рабочая миграция формата* того же
  класса, что и наш.

[p25] Два дополнительных, под наши подслючаи:

- [p26] **Helm `index.yaml`** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — сгенерированный индекс
  chart-репозитория, статический файл, читаемый Helm; несёт поле `apiVersion`
  — прямой аналог нашего вопроса «версия как контракт». Ближе к каталогу, чем
  реестры с живым сервером.
- **Nix `flake.lock`** `[ПО ПАМЯТИ, НЕ ПРОВЕРЕНО]` — машинный lockfile в git с
  версиионированной схемой (`version`/`nodes`); хорошая параллель к нашему
  `vibe.lock` (который, в отличие от каталога, версию **проверяет и отвергает**
  `[ИЗ ДЕРЕВА: crates/vibe-core/src/manifest/lockfile.rs:430]`,
  `CURRENT_SCHEMA_VERSION = 5` `:50`).

## 5. «Ломающее изменение — новый путь» — конкретно для git (вопрос 5)

[p27] «Путь» в нашем случае — это **путь файла в git-репозитории индекса**, он же
raw-URL: `repomd.json`, `primary.jsonl`, `by-name/<name>.json`
(`[ИЗ ДЕРЕВА: PROP-005-package-index.xml:66, 118-205]`). «Новый путь» для
ломающего изменения = публиковать каталог v2 по **другому пути**, оставив v1
замороженным на старом:

- [p28] вариант пути: подкаталог `v2/repomd.json` (или суффикс `repomd.v2.json`, или
  ветка/тег `index-v2`);
- старый потребитель идёт по `repomd.json` — там по-прежнему валидный v1;
- новый потребитель идёт по `v2/repomd.json`.

[p29] **Что это стоит.** (1) **Двойная запись** на каждом reindex — писать приходится
оба дерева; `by-name`/`primary` фан-out зеркалится под `v2/`. (2)
**Обнаружимость** — как новый клиент узнаёт, что есть v2? Указателем в v1
(например, поле `successor` в `repomd.json`) — но это поле само должно быть
forward-совместимым в v1, что создаёт *проблему самозагрузки* (первое такое
поле нельзя ввести «новым путём», его надо ввести как **добавление поля** — а
это требует толерантного читателя v1, которого у нас пока нет, §0.5). (3)
**Дилемма стороны записи**: v1 перестаёт принимать новые пакеты → старые клиенты
видят честно-устаревший, но валидный снимок; или v1 продолжает зеркалить →
вы поддерживаете двух писателей. Это и есть настоящая цена.

[p30] **Что происходит с тем, кто ходит по старому пути через год.** Он получает
**замороженный v1 навсегда** — работает, но перестаёт видеть пакеты,
опубликованные после разреза. Это приемлемо тогда и только тогда, когда v1
честно помечен «frozen, superseded». Молчаливая остановка обновлений (старый
путь отвечает, но данные не растут) — это тихое устаревание, ровно то, что
RFC 9413 `[ИЗ СВОДА]` клеймит как закрепление дефекта.

[p31] **Почему нам это дешевле, чем PyPI/NuGet** (отличие 2 + 4 из §2): у живого
сервера «заморозить старый путь» = держать работающий adapter бесконечно;
у нас «заморозить старый путь» = **branch policy / тег** git'а по нулевой
маржинальной цене (raw-URL, приколотый к тегу, неизменяем даром). Наше
структурное преимущество — git хранит каждую версию навсегда бесплатно, — надо
эксплуатировать явно: «новый путь» = «новый тег/ветка», а старая замораживается
policy, не кодом сервера.

## 6. Двухуровневый словарь NuGet — на наших настоящих именах (вопрос 6)

[p32] NuGet `[ИЗ СВОДА]`: документированные ресурсы вечны, недокументированные
удаляются свободно, и **это написано**. У нас аналогичная граница — не
«документировано/нет» (расширительного неймспейса у нас ещё нет), а
**«ответвляется ли потребитель на этом значении»**:

- [p33] **Верхний уровень (вечные): значение, на котором потребитель ВЕТВИТСЯ —
  маршрутизация, материализация, boot-linking. Должны быть закрытыми enum'ами,
  версионируемыми, без переиспользования имён, с catch-all `unknown`.**
- `kind: PackageKind` `[ИЗ ДЕРЕВА: kinds.rs:21-34; entry/mod.rs:46]` —
    flow/feat/stack/tool/mcp/lang; в PROP-008 §2.3 это «метаданные», но
    реально ведёт именование и маршрутизацию.
- `delivery: DeliveryMode` `[ИЗ ДЕРЕВА: content.rs:53-57]` —
    eager/lazy-push/lazy-pull; определяет, как материализуются subskills.
- `naming: NamingConvention` `[ИЗ ДЕРЕВА: kinds.rs:88-112; repomd.rs:24]` —
    fqdn/kind-name/name/kind-name; определяет path-mapping.
- `format: PackageFormat` `[ИЗ ДЕРЕВА: crates/vibe-core/src/manifest/package.rs:155]`
    — simple/normal; определяет boot-linking.

- [p34] **Нижний уровень (свободные к эволюции/удалению, несут «неизвестное»):
  значение, которое потребитель только показывает или использует как подсказку.**
- `boot_snippet.category` `[ИЗ ДЕРЕВА: content.rs:99-100]` — это **уже**
    `Option<String>`, а не enum, хотя морально это тот же маленький закрытый
    набор («foundation / flow / stack / user-override», комментарий в
    `content.rs:96-98`). Проект **уже** применил двухуровневую интуицию здесь —
    ad hoc, не назвав: `category` оставлен строкой именно потому, что это
    hint-порядка, а не идентичность.
- `description`, `keywords`, `homepage` `[ИЗ ДЕРЕВА: entry/mod.rs:75-79]` —
    display-only; кандидаты в нижний уровень / на удаление без шума.
- `BindingSite` `[ИЗ ДЕРЕВА: index_client/wire.rs:94]` — клиентский enum
    package/subskill; может быть нижним.

[p35] **Пробел, который вскрывает упражнение.** У NuGet нижний уровень — это **место**
(ресурс, который можно удалить). У нас **места нет**: `deny_unknown_fields`
значит, что чужому/расширительному ключу **негде жить**. Ближе всего в
экосистеме — `_`-заповедник PyPI/NuGet `[ИЗ СВОДА]`, которого у нас нет.
Принять двухуровневую модель для нас буквально означает: (а) назвать, какие
поля верхние, какие нижние; (б) **дать нижнему уровню дом** — зарезервированный
`_`-namespace либо явный `#[serde(other)]`/catch-all на верхних enum'ах —
чтобы у неведения был адрес. Это и есть замыкание на принцип §1.

[p36] **Свидетельство, что границу не проводили сознательно.** `delivery` — строгий
3-вариантный enum (`content.rs:53`), а `boot_snippet.category` — тот же класс
маленького закрытого набора, но свободная строка (`content.rs:100`). Одно и то
же правило не применяется; значит, разделительная линия никогда не была
решением. Это `homebrew`-симптом в нашем коде — не отсутствие приёма, а
отсутствие проведённой границы.

## 7. Сводка несогласий (консолидированно)

1. [p37] **`RepomdFileEntry` — не «без признака»**: тег `kind` вшит в вариант
   Directory намеренно (`repomd.rs:44-49`); свод перепутал симптом и причину.
2. **Закрытых словарей 4, не 5** — по декомпозиции самого свода число не бьётся.
3. **Спека противоречит себе** (`FORWARD-COMPAT` :302 vs `NEVER-SILENT-SCHEMA`
   :624), оба `impl/done`; код делает третье; `schema_version` не сравнивается
   нигде — свод увидел одну трещину из трёх.
4. **Толерантность клиента неполна** (ломается на новом *значении* `kind`/
   `BindingSite`, а не на поле) и **случайна** (codegen не умеет `deny`,
   `vibe-wire/lib.rs:26-43`; покрывает отчёты, не каталог).
5. **Avro-в-git растянут** и противоречит собственному тезису свода о смерти
   «свойств отношений» на диске.
6. **Главное: стратегический шаг сведён к «построй аппарат терпимости сейчас».**
   Я считаю правильным обратное: при читателях=0 сегодня максимально дёшево
   **ломать и переименовывать**, а аппарат строить **готовым к заморозке**, но
   не замораживать. Окно (как у раннего PyPI с его тремя читателями) закроется,
   и тогда вступят техники зрелых соседей — но не сейчас.

## 8. Чего я бы добавил к рекомендации свода

- [p38] **JTD-codegen уже частично построен** (`vibe-wire`, `schemas/`, `tools/jtd-codegen`)
  `[ИЗ ДЕРЕВА]`, но покрывает **отчёты команд**, а не каталог. Раз владельцу
  хочется «схемы → код», первый честный шаг — **описать каталог JTD-схемой** и
  прогнать через тот же генератор. Это немедленно выставит вопрос «что делает
  генератор с неизвестными полями» — и ответ сегодня «терпит, потому что не
  умеет строго» (`lib.rs:26-43`). То есть решение о tolerant-vs-strict для
  каталога **невозможно отложить**: сам переход на codegen его навязывает.
- **`deny_unknown_fields` совместим с forward-compat только при наличии
  зарезервированного неймспейса.** Минимальная обратимо-совместимая правка —
  не снимать `deny`, а добавить **один** одобренный escape-хэтч (`x-*` или
  `_`-ключи), как у PyPI/NuGet `[ИЗ СВОДА]`. Тогда строгость (для опечаток в
  `vibe.toml`) и forward-compat (для каталога) не конфликтуют — они разнесены
  по пространству имён, что совпадает с рекомендацией свода №4 («разделять
  пространством имён»), но доведённой до конкретного механизма.
- **Заморозить список отставленных имён полей сейчас**, пока переименование
  бесплатно (рекомендация №6 свода верна, но её надо делать **до** первого
  внешнего читателя — то есть сейчас, а не «когда понадобится»).
- **Контракт версии — внедрить как проверку в сборке**, а не как runtime-ветку:
  статически гарантировать, что minor-бамп вводит только необязательные поля,
  major-бамп — только по новому пути (§5). Переносимая практика из свода
  (`Часть 4`: статическая проверка совместимости в сборке `[ИЗ СВОДА]`), и ей
  не нужен ни сервер, ни телеметрия.

