VibeVM
Contents
On this page
en
Publisher
org.vibevm.core
Version
1.0.0latest
Audiences
Reading time
13 min
Rendered
Read aloud
never

WIRE-CENSUS — каталог пакетов vibevm (формат «на проводе»)

01Замер, а не правка. Цель — исчерпывающий и точный список того, что сегодня выходит на провод в опубликованном каталоге пакетов (repomd.json, primary.jsonl, by-name/*.json, by-cap/*.jsonl, by-purl/*.jsonl) и в живых HTTP-ответах сервера индекса. Никаких рекомендаций.

02Периметр (считано по нему): crates/vibe-index/src/types/**, crates/vibe-index/src/index/**, crates/vibe-index/src/server/**, schemas/**, crates/vibe-registry/src/index_client/**.

03Ключевые числа (итог, подробности ниже):

04
# Метрика Значение
1 Публичных сериализуемых типов в периметре 23 (18 каталог-файловых: 17 структур + RepomdFileEntry; +5 закрытых словарей-enum) + внешний Group из vibe-core на проводе
2 Полей всего 86 (82 именованных поля структур + 4 поля в вариантах RepomdFileEntry)
3 Полей-коллекций, у которых «пусто» СЕЙЧАС неотличимо от «нет поля» 14 (подтверждено; см. 3.2)
4 Объединений без тега на проводе 1 (RepomdFileEntry; второго нет)
5 Мест, где каталог перечитывается и переписывается 6 call-сайтов write_to поверх существующего каталога (3 CLI + 3 серверных обработчика) + init пишет с нуля

3.1 Таблица типов

05Обозначения: «DUF» = deny_unknown_fields стоит. «полей» — число полей (для enum-вариантов — сумма полей по вариантам). Внешний Group приведён отдельно.

Каталог-файловые типы (сериализуются в опубликованные файлы)

06
Тип (файл:строка) Роль (какой файл/часть каталога) DUF полей skip_serializing_if rename/rename_all default
Repomd types/repomd.rs:20 repomd.json — манифест индекса 9 нет (на контейнере) нет нет
RepomdFileEntry types/repomd.rs:43 значения карты Repomd.files (файл/директория) ❌ (untagged) 4 (2+2) нет нет (варианты без переимен.) нет
VersionEntry types/entry/mod.rs:43 одна строка primary.jsonl; элемент versions[] 30 13 полей (см. 3.2) нет на 13 полях
PackageEntry types/entry/aggregate.rs:26 элемент NameEntry.packages[] 5 latest_stable нет latest_stable
NameEntry types/entry/aggregate.rs:69 by-name/<name>.json 3 нет нет нет
WorkspaceOriginEntry types/entry/content.rs:19 VersionEntry.workspace_origin 5 commit нет commit
FeaturesEntry types/entry/content.rs:38 VersionEntry.features 2 features, exclusive нет оба поля
SubskillEntry types/entry/content.rs:61 элемент VersionEntry.subskills[] 5 describes,description,channels нет те же 3
I18nEntry types/entry/content.rs:74 VersionEntry.i18n 2 available,default нет оба
BootSnippetEntry types/entry/content.rs:93 VersionEntry.boot_snippet 2 category нет category
CompatibilityEntry types/entry/relations.rs:15 VersionEntry.compatibility 2 оба поля нет оба
ProvidesEntry types/entry/relations.rs:30 VersionEntry.provides 1 capabilities нет да
RequiresEntry types/entry/relations.rs:43 VersionEntry.requires 2 оба нет оба
RequiresAnyEntry types/entry/relations.rs:58 элемент VersionEntry.requires_any[] 1 нет нет нет
ObsoletesEntry types/entry/relations.rs:64 VersionEntry.obsoletes 1 packages нет да
ConflictsEntry types/entry/relations.rs:77 VersionEntry.conflicts 1 packages нет да
CapabilityRow index/inverted.rs:66 строка by-cap/<slug>.jsonl 5 нет нет нет
PurlRow index/inverted.rs:75 строка by-purl/<slug>.jsonl 6 нет нет нет

Закрытые словари (fieldless enum, используются как типы полей внутри записей)

07
Тип (файл:строка) Где используется rename_all число значений
PackageKind types/kinds.rs:21 VersionEntry.kind, CompatibilityEntry.requires_kinds[], inverted-ряды, HTTP lowercase 6
NamingConvention types/kinds.rs:88 Repomd.naming варианты rename поимённо 4
DeliveryMode types/entry/content.rs:53 SubskillEntry.delivery kebab-case 3
DirectoryTag types/repomd.rs:75 RepomdFileEntry::Directory.kind lowercase 1
BindingSite index/inverted.rs:89 PurlRow.binding_site kebab-case 2

Внешний тип на проводе

08
Тип Где Сериализация
Group (vibe-core/src/package_ref.rs:41, реэкспорт в types/mod.rs:28) VersionEntry.group, PackageEntry.group, inverted-ряды, HTTP newtype поверх String, #[serde(try_from = "String", into = "String")] → на проводе обычная JSON-строка с валидацией при чтении (отклоняет заглавные, пустые сегменты, недопустимые символы)

09Поправка к «около 18»: 18 — это в точности число каталог-файловых типов, если считать 17 структур плюс единственное объединение RepomdFileEntry. Дерево добавляет ещё 5 закрытых словарей-enum (типы полей) и внешний Group. Итого различных публичных сериализуемых типов, которых касается провод каталога: 23 в периметре + 1 внешний. Считал прямым обходом types/** + index/inverted.rs + реэкспортов types/mod.rs.

Прочие типы «на проводе», но НЕ каталог-файловые (для полноты)

10Это ответные конверты живого HTTP-API сервера индекса (crates/vibe-index/src/server/routes/**). Они #[derive(Serialize)] (на стороне сервера — только запись), в файлы каталога не пишутся, и даны здесь отдельным списком, чтобы не смешивать с записями каталога:

  • 11packages.rs: ListResponse, PackageRow, SearchResponse, SearchHit (серверный), PackageVersionsResponse, UpsertResponse, DeleteResponse. Маршрут /v1/packages/{group}/{name}/{version} отдаёт сам VersionEntry целиком — т.е. каталог-запись попадает в HTTP-ответ один-в-один.
  • purls.rs: Response, Hit (binding_site тут — &'static str, не enum).
  • capabilities.rs: Response, Hit.

12Клиент crates/vibe-registry/src/index_client/wire.rs читает это своими толерантными view-структурами: NameEntryView/PackageEntryView/VersionEntryView (для by-name/*.json), SearchResults/SearchHit (клиентский), PurlLookupResults/PurlLookupHit, BindingSite (клиентский). См. 3.7.

3.2 Таблица полей

13Категория: СКАЛЯР (строка/число/bool/версия/Group/закрытый словарь), СПИСОК (Vec), СЛОВАРЬ (BTreeMap), ВЛОЖЕННЫЙ (другая каталог-структура как поле), ОБЪЕДИНЕНИЕ (значение RepomdFileEntry).

14«обяз.?» — обязательно ли поле на проводе: всегда (нет default/skip/Option) / может отсутствовать.

15«различимы пусто и нет поля?» — ключевой столбец:

  • 16ДА — пустое/нулевое значение сериализуется и отличается от отсутствия поля;
  • НЕТ — пустое значение опускается (skip_serializing_if = …::is_empty / is_none), поэтому на проводе «пусто» и «нет поля» неразличимы;
  • всегда — поле не может отсутствовать (состояния «нет поля» не существует).

Repomd (types/repomd.rs:20)

17
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
Repomd.schema_version u32 СКАЛЯР всегда всегда
Repomd.registry String СКАЛЯР всегда всегда
Repomd.registry_url String СКАЛЯР всегда всегда
Repomd.naming NamingConvention СКАЛЯР (закр. словарь) всегда всегда
Repomd.generated_at DateTime<Utc> СКАЛЯР всегда всегда
Repomd.generator String СКАЛЯР всегда всегда
Repomd.package_count u32 СКАЛЯР всегда всегда
Repomd.version_count u32 СКАЛЯР всегда всегда
Repomd.files BTreeMap<String, RepomdFileEntry> СЛОВАРЬ всегда нет skip всегда (даже пустая карта сериализуется как {})

RepomdFileEntry (types/repomd.rs:43, #[serde(untagged)])

18
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
Directory.kind DirectoryTag СКАЛЯР (закр. словарь) всегда* всегда
Directory.entries u32 СКАЛЯР всегда* всегда
File.size u64 СКАЛЯР всегда* всегда
File.sha256 String СКАЛЯР всегда* всегда

19\ Внутри выбранного варианта поля обязательны. Само значение — ОБЪЕДИНЕНИЕ*: на проводе ровно один из двух наборов ключей (см. 3.3).

VersionEntry (types/entry/mod.rs:43)

20
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
VersionEntry.schema_version u32 СКАЛЯР всегда всегда
VersionEntry.kind PackageKind СКАЛЯР (закр. словарь) всегда всегда
VersionEntry.group Group СКАЛЯР (строка) всегда всегда
VersionEntry.name String СКАЛЯР всегда всегда
VersionEntry.version Version СКАЛЯР всегда всегда
VersionEntry.content_hash String СКАЛЯР всегда всегда
VersionEntry.source_url String СКАЛЯР всегда всегда
VersionEntry.source_ref String СКАЛЯР всегда всегда
VersionEntry.resolved_commit Option<String> СКАЛЯР может Option + skip Option::is_none ДА (None→нет поля; Some("")→"")
VersionEntry.registry String СКАЛЯР всегда всегда
VersionEntry.workspace_origin Option<WorkspaceOriginEntry> ВЛОЖЕННЫЙ может Option + skip Option::is_none ДА (None→нет поля; Some(пустая структура)→объект)
VersionEntry.license Option<String> СКАЛЯР может Option + skip is_none ДА
VersionEntry.authors Vec<String> СПИСОК может skip Vec::is_empty НЕТ
VersionEntry.description Option<String> СКАЛЯР может Option + skip is_none ДА
VersionEntry.homepage Option<String> СКАЛЯР может Option + skip is_none ДА
VersionEntry.keywords Vec<String> СПИСОК может skip Vec::is_empty НЕТ
VersionEntry.describes Option<String> СКАЛЯР может Option + skip is_none ДА
VersionEntry.compatibility CompatibilityEntry ВЛОЖЕННЫЙ может default + skip CompatibilityEntry::is_empty НЕТ (пустая структура == нет поля)
VersionEntry.provides ProvidesEntry ВЛОЖЕННЫЙ может default + skip ProvidesEntry::is_empty НЕТ
VersionEntry.requires RequiresEntry ВЛОЖЕННЫЙ может default + skip RequiresEntry::is_empty НЕТ
VersionEntry.requires_any Vec<RequiresAnyEntry> СПИСОК может skip Vec::is_empty НЕТ
VersionEntry.obsoletes ObsoletesEntry ВЛОЖЕННЫЙ может default + skip ObsoletesEntry::is_empty НЕТ
VersionEntry.conflicts ConflictsEntry ВЛОЖЕННЫЙ может default + skip ConflictsEntry::is_empty НЕТ
VersionEntry.features FeaturesEntry ВЛОЖЕННЫЙ может default + skip FeaturesEntry::is_empty НЕТ
VersionEntry.subskills Vec<SubskillEntry> СПИСОК может skip Vec::is_empty НЕТ
VersionEntry.i18n I18nEntry ВЛОЖЕННЫЙ может default + skip I18nEntry::is_empty НЕТ
VersionEntry.boot_snippet Option<BootSnippetEntry> ВЛОЖЕННЫЙ может Option + skip is_none ДА
VersionEntry.files_count u32 СКАЛЯР всегда всегда
VersionEntry.indexed_at DateTime<Utc> СКАЛЯР всегда всегда
VersionEntry.indexed_by String СКАЛЯР всегда всегда

PackageEntry (types/entry/aggregate.rs:26)

21
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
PackageEntry.group Group СКАЛЯР всегда всегда
PackageEntry.name String СКАЛЯР всегда всегда
PackageEntry.indexed_at DateTime<Utc> СКАЛЯР всегда всегда
PackageEntry.latest_stable Option<Version> СКАЛЯР может Option + skip is_none ДА
PackageEntry.versions Vec<VersionEntry> СПИСОК всегда нет skip всегда (даже [] пишется; «нет поля» невозможно)

NameEntry (types/entry/aggregate.rs:69)

22
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
NameEntry.name String СКАЛЯР всегда всегда
NameEntry.indexed_at DateTime<Utc> СКАЛЯР всегда всегда
NameEntry.packages Vec<PackageEntry> СПИСОК всегда нет skip всегда

WorkspaceOriginEntry (types/entry/content.rs:19)

23
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
WorkspaceOriginEntry.upstream String СКАЛЯР всегда всегда
WorkspaceOriginEntry.path String СКАЛЯР всегда всегда
WorkspaceOriginEntry.commit Option<String> СКАЛЯР может Option + skip is_none ДА
WorkspaceOriginEntry.generated_by String СКАЛЯР всегда всегда
WorkspaceOriginEntry.generated_at String СКАЛЯР всегда всегда

FeaturesEntry (types/entry/content.rs:38)

24
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
FeaturesEntry.features BTreeMap<String, Vec<String>> СЛОВАРЬ может default + skip BTreeMap::is_empty НЕТ
FeaturesEntry.exclusive BTreeMap<String, Vec<String>> СЛОВАРЬ может default + skip BTreeMap::is_empty НЕТ

SubskillEntry (types/entry/content.rs:61)

25
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
SubskillEntry.path String СКАЛЯР всегда всегда
SubskillEntry.delivery DeliveryMode СКАЛЯР (закр. словарь) всегда всегда
SubskillEntry.describes Option<String> СКАЛЯР может Option + skip is_none ДА
SubskillEntry.description Option<String> СКАЛЯР может Option + skip is_none ДА
SubskillEntry.channels Vec<String> СПИСОК может skip Vec::is_empty НЕТ

I18nEntry (types/entry/content.rs:74)

26
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
I18nEntry.available Vec<String> СПИСОК может skip Vec::is_empty НЕТ
I18nEntry.default Option<String> СКАЛЯР может Option + skip is_none ДА

BootSnippetEntry (types/entry/content.rs:93)

27
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
BootSnippetEntry.source String СКАЛЯР всегда всегда
BootSnippetEntry.category Option<String> СКАЛЯР может Option + skip is_none ДА

CompatibilityEntry (types/entry/relations.rs:15)

28
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
CompatibilityEntry.min_vibe_version Option<String> СКАЛЯР может Option + skip is_none ДА
CompatibilityEntry.requires_kinds Vec<PackageKind> СПИСОК может skip Vec::is_empty НЕТ

ProvidesEntry (types/entry/relations.rs:30)

29
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
ProvidesEntry.capabilities Vec<String> СПИСОК может skip Vec::is_empty НЕТ

RequiresEntry (types/entry/relations.rs:43)

30
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
RequiresEntry.packages Vec<String> СПИСОК может skip Vec::is_empty НЕТ
RequiresEntry.capabilities Vec<String> СПИСОК может skip Vec::is_empty НЕТ

RequiresAnyEntry (types/entry/relations.rs:58)

31
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
RequiresAnyEntry.one_of Vec<String> СПИСОК всегда нет skip (но вся запись живёт в requires_any[], который опускается если пуст) всегда

ObsoletesEntry (types/entry/relations.rs:64)

32
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
ObsoletesEntry.packages Vec<String> СПИСОК может skip Vec::is_empty НЕТ

ConflictsEntry (types/entry/relations.rs:77)

33
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
ConflictsEntry.packages Vec<String> СПИСОК может skip Vec::is_empty НЕТ

CapabilityRow (index/inverted.rs:66) — нет deny_unknown_fields

34
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
CapabilityRow.kind PackageKind СКАЛЯР (закр. словарь) всегда всегда
CapabilityRow.group Group СКАЛЯР (строка) всегда всегда
CapabilityRow.name String СКАЛЯР всегда всегда
CapabilityRow.version Version СКАЛЯР всегда всегда
CapabilityRow.capability String СКАЛЯР всегда всегда

PurlRow (index/inverted.rs:75) — нет deny_unknown_fields

35
тип.поле Rust-тип кат. обяз.? почему может отсутствовать различимы?
PurlRow.kind PackageKind СКАЛЯР (закр. словарь) всегда всегда
PurlRow.group Group СКАЛЯР (строка) всегда всегда
PurlRow.name String СКАЛЯР всегда всегда
PurlRow.version Version СКАЛЯР всегда всегда
PurlRow.purl String СКАЛЯР всегда всегда
PurlRow.binding_site BindingSite СКАЛЯР (закр. словарь) всегда всегда

Сводка по коллекциям (3.2, итог)

36Всего 18 полей-коллекций (СПИСОК Vec или СЛОВАРЬ BTreeMap) среди каталог-типов. Из них у 14 пустое значение СЕЙЧАС неотличимо от отсутствующего (skip_serializing_if = …::is_empty опускает пустоту; на проводе невозможно отличить []/{} от «ключа нет»):

  1. 37VersionEntry.authorsVec<String>
  2. VersionEntry.keywordsVec<String>
  3. VersionEntry.requires_anyVec<RequiresAnyEntry>
  4. VersionEntry.subskillsVec<SubskillEntry>
  5. FeaturesEntry.featuresBTreeMap<String, Vec<String>>
  6. FeaturesEntry.exclusiveBTreeMap<String, Vec<String>>
  7. SubskillEntry.channelsVec<String>
  8. I18nEntry.availableVec<String>
  9. CompatibilityEntry.requires_kindsVec<PackageKind>
  10. ProvidesEntry.capabilitiesVec<String>
  11. RequiresEntry.packagesVec<String>
  12. RequiresEntry.capabilitiesVec<String>
  13. ObsoletesEntry.packagesVec<String>
  14. ConflictsEntry.packagesVec<String>

38Остальные 4 коллекции всегда присутствуют (нет skip), поэтому «пусто» и «нет поля» для них различимы тривиально (состояния «нет поля» просто нет): PackageEntry.versions, NameEntry.packages, RequiresAnyEntry.one_of, Repomd.files.

39Перепроверка числа «14» из пакета: подтверждено в точности. Считал так: прошёл grep-ом все skip_serializing_if = "…::is_empty" по types/entry/** и types/repomd.rs, отфильтровал поля, чей Rust-тип — Vec или BTreeMap. Получилось ровно 14. Дополнительно отмечу: 7 вложенных структур-полей (VersionEntry.compatibility/provides/requires/obsoletes/conflicts/features/i18n) имеют ту же самую неразличимость через собственные is_empty()-методы — это ВЛОЖЕННЫЕ, не коллекции, но семантически та же потеря различия «пусто/нет».

3.3 Объединения (варианты формы)

40Полный обход serde-атрибутов периметра (grep untagged|tag =|flatten по crates/vibe-index/src и crates/vibe-registry/src/index_client) даёт ровно одно объединение на проводе:

RepomdFileEntry (types/repomd.rs:43) — #[serde(untagged)]

41#[serde(untagged)]
pub enum RepomdFileEntry {
    Directory { kind: DirectoryTag, entries: u32 },
    File { size: u64, sha256: String },
}
  • 42Как читатель отличает варианты СЕГОДНЯ: это untagged enum — внешнего поля-тега на уровне объекта нет. serde перебирает варианты в порядке объявления и пробует десериализовать каждый. Вариант Directory объявлен первым.
  • Поле-тег: явного тега на контейнере нет, но внутри варианта Directory есть поле kind: DirectoryTag со значением всегда "directory" — оно самодокументирует директорию (см. тест directory_serialises_with_kind_tag, repomd.rs:119).
  • Наборы ключей вариантов:
  • Directory{ "kind", "entries" }
  • File{ "size", "sha256" }
  • Пересекаются ли наборы ключей: НЕТ. {kind, entries} ∩ {size, sha256} = ∅. Поэтому разбор однозначен: объект с size/sha256 не совпадёт с Directory (нет kind/entries) и уйдёт в File; объект с kind/entries совпадёт с Directory. Поле kind здесь — запасная страховка поверх уже непересекающихся ключей.

43Замечание о робастности: т.к. enum untagged и без deny_unknown_fields, лишний ключ в файловой записи (напр. {"size":..,"sha256":"..","x":1}) всё ровно сматчится с File. А вот неизвестное значение kind (не "directory") в записи директории завалит вариант Directory, затем не сматчится с File (нет size/sha256) → ошибка разбора.

44Второго объединения нет. Известный кандидат был единственным. Никаких других untagged/tag =/ручных impl Deserialize в периметре не обнаружено (flatten встречается только в crates/vibe-index/src/lockfile.rs:22,37 — это парсинг vibe.lock, к каталогу отношения не имеет). Если бы второе объединение существовало, оно было бы главной находкой; его нет.

3.4 Закрытые словари на проводе (fieldless enum)

45Все закрытые словари — это enum без полей с производным Serialize/Deserialize. Поведение при неизвестном значении для всех одно и то же: производный Deserialize для fieldless enum без #[serde(other)] даёт ошибку разбора (unknown variant), запасного варианта нет. Никакого #[serde(other)] в дереве нет (grep подтвердил).

46
Словарь (файл:строка) Значения на проводе (дословно) Сериализация Что делает читатель с неизвестным значением Вторые копии списка в дереве
PackageKind types/kinds.rs:21 flow, feat, stack, tool, mcp, lang rename_all = "lowercase" → строки (см. as_str, kinds.rs:37) ошибка разбора (derived Deserialize, unknown variant). FromStr тоже ошибается (kinds.rs:76) ДА: (1) преднамеренный дубликат в vibe-core (kinds.rs:1-5, parity-тест); (2) внутри файла список повторён в as_str match, all() (:48), from_str match и в строке сообщения об ошибке :77
NamingConvention types/kinds.rs:88 fqdn, kind-name, name, kind/name поимённый #[serde(rename = …)] на каждом варианте ошибка разбора ДА: зеркалирует vibe-core::manifest::NamingConvention (kinds.rs:84-86); те же строки дублированы в as_str (:114) и в value(name=…)
DeliveryMode types/entry/content.rs:53 eager, lazy-push, lazy-pull rename_all = "kebab-case" → строки ошибка разбора вторая копия не найдена (зеркалирует vibe-core, но локально не дублируется)
DirectoryTag types/repomd.rs:75 directory rename_all = "lowercase" ошибка разбора, но т.к. живёт внутри untagged RepomdFileEntry, неизвестный kind заваливает Directory, затем не матчит File → в итоге ошибка разбора всего значения вторая копия не найдена
BindingSite index/inverted.rs:89 package, subskill rename_all = "kebab-case" (одиночные слова → как lowercase) ошибка разбора ДА — отдельная находка: тот же словарь существует вторым типом в клиенте crates/vibe-registry/src/index_client/wire.rs:94 (rename_all = "lowercase"). Сегодня значения совпадают ("package"/"subskill"), но это два независимых Rust-типа в двух crate'ах — точка потенциального расхождения

3.5 Версия формата

  • 47Где объявлена:
  • Repomd::SCHEMA_VERSION: u32 = 1 (types/repomd.rs:38); поле Repomd.schema_version пишется в repomd.json.
  • VersionEntry::SCHEMA_VERSION: u32 = 1 (types/entry/mod.rs:124); поле VersionEntry.schema_version пишется в каждую строку primary.jsonl.
  • Дубль в памяти: Index SCHEMA_VERSION = 1 (index/memory.rs:28), поле Index.schema_version.
  • Значение: 1 (везде).
  • Ветвится ли по этому значению хоть что-нибудь: НЕТ. Поле только присваивается, сериализуется, читается обратно и хранится. grep по crates показывает: Index::load_from кладёт schema_version: manifest.schema_version в память без проверки (memory.rs:273); cli/dump.rs:57 печатает "schema_version": index.schema_version. Ни одного сравнения (==/!=/match) для версий Repomd/VersionEntry в каталоге нет.
  • Контраст: vibe.lock имеет свою, отдельную версию (CURRENT_SCHEMA_VERSION, vibe-core/src/manifest/lockfile.rs:430if lockfile.meta.schema_version != CURRENT_SCHEMA_VERSION), и там по ней ветвятся/отвергают. Но это vibe.lock (TOML, схема 5), а не каталог индекса. Свои schema_version есть также у побочных файлов (Checkpoint index/checkpoint.rs:21, сканер-кэш scanner/org_cache.rs:50) — это не опубликованный каталог.
  • Вывод: версия каталога сегодня — ярлык, а не переключатель. Любое значение (в т.ч. неизвестное) молча принимается при чтении.

3.6 Кругооборот — читается ли каталог, чтобы переписать его

48Index::write_to (index/memory.rs:161) всегда перегенерирует все файлы каталога из типизированной in-memory модели (by_pkgref: BTreeMap<PkgKey, PackageEntry>), а НЕ из сырого прочитанного JSON. Источник in-memory модели разнится по пути:

49
Путь Читает существующий каталог? Откуда берутся данные для записи
vibe-index init (cli/init.rs:48) нет пустой Index::new, запись с нуля
vibe-index add (cli/add.rs:51,122) ДА (Index::load_from) существующие записи — из прочитанного с диска каталога; одна новая запись — из источника (vibe.toml); затем upsert + write_to
vibe-index remove (cli/remove.rs:35,57) ДА (Index::load_from) все записи — из прочитанного с диска каталога; write_to перегенерирует
vibe-index reindex / rescan-org (cli/reindex.rs:218,281) ДА (грузит existing минимум ради registry/naming) incremental: неизменённые записи берёт из existing.iter_versions() (т.е. из прочитанного каталога, :250), изменённые — из сканирования источника; full: собирает заново из сканирования; оба режима — write_to
сервер: POST /v1/packages upsert (server/routes/packages.rs:256) ДА (косвенно) Index загружен с диска один раз на старте (cli/serve.rs:77 load_from), мутация в памяти, write_to перегенерирует весь каталог
сервер: DELETE …/{version} (packages.rs:304) ДА (косвенно) та же in-memory модель со старта → write_to
сервер: DELETE …/{group}/{name} (packages.rs:333) ДА (косвенно) та же in-memory модель со старта → write_to

50Точный ответ на вопрос 3.6: каталог в нескольких путях перечитывается с диска и переписывается (add, remove, incremental-reindex, а также серверные upsert/delete поверх стартовой загрузки). При этом write_to всегда собирает файлы из типизированной памяти; сырой JSON сквозь не прокачивается. Следствие для решения о смене формата:

  • 51Поля, которых наш код не знает, не выживают при перезаписи — они отсутствуют в типизированных структурах и просто не запишутся обратно.
  • Сильнее того: поскольку у каждой каталог-записи стоит deny_unknown_fields (см. 3.7), неизвестное поле не даст каталог прочитать вообще — load_from упадёт с ошибкой разбора ещё до того, как дойдёт до перезаписи. Исключение — inverted-ряды CapabilityRow/PurlRow, у которых deny_unknown_fields нет (но они и не перечитываются для перезаписи нигде — их всегда перегенерируют из VersionEntry).
  • Полностью с нуля каталог собирают только init и reindex в режиме full (и то поля ограничены тем, что умеют типы).

52Итого call-сайтов write_to, переписывающих уже существующий каталог: 6 (add, remove, reindex/rescan, серверные upsert, delete_version, delete_package), плюс init пишет с нуля.

3.7 Строгость на чтении

53Места десериализации записей каталога (serde_json::from_* над каталог-типами):

54
Место Тип deny_unknown_fields? Лишнее поле → ошибка?
index/primary.rs:92 (parse, serde_json::from_str по строке) VersionEntry ✅ (entry/mod.rs:38) ДА, ошибка разбора (Error::Malformed)
index/by_name.rs:67 (parse, from_slice) NameEntryPackageEntryVersionEntry ✅ все три ДА
index/by_name.rs:94 (внутри read_all) NameEntry ДА
index/repomd.rs:38 (read, from_slice) Repomd ✅ (repomd.rs:15) ДА
index/memory.rs:263-264 (load_fromrepomd::read + by_name::read_all) Repomd, NameEntry ДА (через подчинённые функции)
index/inverted.rs чтения CapabilityRow/PurlRow нет (только запись + подсчёт) DUF отсутствует н/д (никто не парсит)
server/routes/packages.rs:232 (тело POST) VersionEntry (axum Json) ДА → HTTP 400
server/routes/index_files.rs сырые байты файлов отдаются как есть (serve_file) сервер сам не десериализует; строгость — на стороне читателя

55Итог по записям каталога: все типизированные чтения строгие — запись с лишним незнакомым полем падает с ошибкой разбора. Сырые файлы сервер отдаёт байт-в-байт, не разбирая.

Клиент crates/vibe-registry/src/index_client/** — строг или терпим?

56Терпим. Клиент намеренно использует отдельные «view»-структуры с у́зким набором полей, #[serde(default)] и без deny_unknown_fields:

  • 57NameEntryView (wire.rs:18) читает только packages[]; у PackageEntryView (wire.rs:24) — group + versions[]; у VersionEntryView (wire.rs:31) — только version. Комментарий прямым текстом (wire.rs:14-16): «Only the fields the resolver's version selector needs are read; the rest of the on-disk shape is tolerated.»
  • SearchResults/SearchHit (wire.rs:45,56): «Extra fields on the wire (today: command) are tolerated silently» (wire.rs:37-39); тест search_results_decode_minimal_envelope (wire.rs:115) пропускает лишний ключ command.
  • PurlLookupResults/PurlLookupHit — те же #[serde(default)], без DUF.

58Это наш собственный внешний потребитель, и его поведение — образец того, что сделает чужой читатель: он увидит только те поля, которые знает, а всё прочее (включая будущие новые поля) тихо проигнорирует. Следствие: добавить поле в запись не сломает такого читателя; убрать/переименовать известное ему поле — сломает.

3.8 Схемы

59Описывает ли хоть одна схема в дереве типы каталога индекса?НЕТ.

60Формат каталога сегодня задан только Rust-структурами с serde в crates/vibe-index/src/types/**index/inverted.rs). Никакого *.jtd.json/*.schema.json для VersionEntry, Repomd, NameEntry, CapabilityRow, PurlRow и т.п. в дереве нет.

61Присутствующие в дереве schemas/*.jtd.json (JSON Type Definition) описывают совсем другой провод — JSON-отчёты CLI-команд, генерируемые в crates/vibe-wire/src/generated/*:

62
Файл Что описывает
schemas/init_report.jtd.json отчёт vibe init --json
schemas/install_plan.jtd.json план установки (vibe install --json)
schemas/install_report.jtd.json отчёт об установке
schemas/list_report.jtd.json отчёт vibe list --json
schemas/registry_publish_report.jtd.json отчёт vibe registry publish --json
schemas/registry_sync_report.jtd.json отчёт vibe registry sync --json (metadescription прямо указывает source of truth → crates/vibe-wire/src/generated/registry_sync_report.rs)
schemas/uninstall_report.jtd.json отчёт об удалении

63Отдельно: существует crates/vibe-cli/resources/package-tree.schema.v1.json — но это схема vibe load --json (дерево пакетов проекта, schema_version: 1, const), к каталогу индекса отношения не имеет (упомянут здесь, чтобы не оставлять сомнений: это тоже не каталог).

64Вывод: каталог индекса — единственная крупная часть провода vibevm, у которой нет машинной схемы; его «контракт» существует только как код.

For an agent

This page has a machine mirror. The citation carries the version rather than latest, so what an agent quotes does not move under it.

spec://org.vibevm.core/vibevm@1.0.0/research/schema-evolution-2026-08/01-measure-our-wire-format-claudez

.md.xmlllms.txt