<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title>РЕВЬЮ И ВЕРДИКТ</title>
  <p p="1">_Финальный разбор мегаотчёта и девяти исходных файлов. Всё, что сказано про наш
код, проверено чтением дерева `C:\Users\olegc\git\v\vibevm` с `файл:строка`.
Всё, что сказано про чужие системы, взято **либо** из вкопированного в наше же
дерево первоисточника (`refs/**`), **либо** помечено как непроверенное._</p>
  <section title="0. Итог одним абзацем">
    <p p="2">Отчёт добротный, числа сходятся, но он ставит не тот диагноз и потому
прописывает лекарство, которое в одном месте опаснее болезни. Главная ошибка:
предложено «сделать читателя каталога терпимым», при том что каталог **читают,
чтобы переписать** — на шести путях. Терпимый читатель без **сохранения**
незнакомого превращает громкий отказ в тихое удаление чужих данных: то есть
ровно в тот отказ, который сам же отчёт называет худшим. Второе: половина
предложения выведена из противоречия «шлагбаум против декларации», которого
нет — это не две роли, а один целый `u32`, в который вторая роль физически не
влезает; а утверждение «crates.io добавляет поля без поднятия версии» **ложно**,
и опровержение лежит в нашем же дереве, в `refs/src/cargo/` — там версия 2
введена **именно ради добавленного поля**. Третье, и самое дорогое: восемь
участников обсуждали эволюцию **схемы** и ни разу — эволюцию **смысла**: у нас
нет ни отметки об отзыве версии, ни места под подпись, ни идентичности алгоритма
у `content_hash`, который спека объявляет ключом связи каталога с локом. Вот эти
три вещи и есть настоящие «механизмы, окно которых закрывается», а не
терпимость, которую можно включить когда угодно. Практический вывод: из восьми
пунктов принять как есть — один, изменить — пять, отвергнуть — два; и начать не
с них, а с четырёх мелких необратимых вещей, которые никто не назвал.</p>
  </section>
  <section title="1. Проверка семи разрешений">
    <section title="1.1. Объединение полутегированное (5.1) — **верно, но неполно**">
      <p p="3">Факт подтверждён дословно: `crates/vibe-index/src/types/repomd.rs:42` —
`#[serde(untagged)]`; `:44-50` — вариант `Directory` несёт `kind: DirectoryTag`
и доккомментарий прямо объясняет, что тег стоит там намеренно; `:51-54` —
вариант `File` тега не несёт. Разрешение фактически право.</p>
      <p p="4">Неполнота в трёх местах.</p>
      <p p="5">**Первое.** Отчёт сохранил слабый довод и выбросил сильный. Он тегирует ради
«однозначности разбора» — а разбор и так однозначен, наборы ключей не
пересекаются. Настоящая причина названа в файле 08 и **потеряна** при сведении:
`RepomdFileEntry` — единственный тип каталога, который **молча глотает
добавленные поля**. Это первый пункт из «четырёх мест, где старый читатель
разберёт и поймёт неверно», которые тот же отчёт объявляет самой ценной находкой
серии. Разрешение 5.1 и находка №1 стоят в одном документе и не разговаривают
друг с другом.</p>
      <p p="6">**Второе, механическое и решающее.** Починка «поставить хотя бы
`deny_unknown_fields` на плечи» (файл 08, §5.1) **невозможна**:
`deny_unknown_fields` — атрибут контейнера, и serde не даёт его ни на
`untagged`-перечислении, ни на отдельных его плечах. Значит, единственный способ
прекратить молчаливое глотание — **перестать быть untagged**. Это делает
рекомендацию №1 не вопросом вкуса, а единственным выходом.</p>
      <p p="7">**Третье.** Не назван побочный эффект: тест `repomd.rs:132` утверждает
`assert!(!json.contains("kind"))` для файлового плеча. Переход на явный тег
ломает его — мелочь, но она показывает, что «один атрибут» на деле «один атрибут
плюс золотой тест плюс перезапись всех существующих `repomd.json`».</p>
    </section>
    <section title="1.2. «14 против 21» (5.2) — **верно, и я это воспроизвела; но решение шире, чем 21**">
      <p p="8">Своя перепись по `crates/vibe-index/src/types/`: 35 вхождений
`skip_serializing_if`, гистограмма — `Option::is_none` ×14, `Vec::is_empty` ×12,
`BTreeMap::is_empty` ×2, `&lt;Тип&gt;::is_empty` ×7. Значит:</p>
      <fence p="9">коллекций           12 Vec + 2 BTreeMap            = 14   ← число свода
+ вложенных структур  7 (&lt;Тип&gt;::is_empty)                  
                                                   = 21   ← число замера ✓
+ Option&lt;T&gt;          14 (absent ⟺ None, схлопывания нет)
                                                   = 35   ← вся поверхность</fence>
      <p p="10">Оба числа воспроизводятся, разрешение верно. **Но вывод «решение принимается по
21» неверен.** Решение принимается по **35**, потому что предложенное правило
(«отсутствие = не записано; известное пустое пишем явно») обязано что-то сказать
про все 35 полей — а для 14 `Option`-полей оно сказать ничего не может, поскольку
`null` в том же предложении отвергнут. Правило, которое покрывает 21 из 35 и
объявляет себя единым, — не единое правило.</p>
      <p p="11">И ещё одна деталь, которой нет ни у кого: 4 поля-коллекции **уже сегодня** пишутся
всегда, без пропуска-когда-пусто — `Repomd.files` (`repomd.rs:34`),
`RequiresAnyEntry.one_of` (`relations.rs:59`), `PackageEntry.versions`
(`aggregate.rs:32`), `NameEntry.packages` (`aggregate.rs:75`). То есть обе
конвенции в формате уже сосуществуют, несогласованно, и никто этого не заметил.</p>
    </section>
    <section title="1.3. «5 против 4 против 3» (5.3) — **верно, и я могу это доказать арифметикой, которой не привёл никто**">
      <p p="12">Числа в отчёте связаны и сходятся только при пяти словарях:</p>
      <fence p="13">15 структур в types/        (Repomd, VersionEntry, 6× relations,
                             5× content, PackageEntry, NameEntry)
+ 2 структуры в index/inverted.rs   (CapabilityRow :66, PurlRow :75)
= 17 структур
+ 1 объединение             (RepomdFileEntry :43)
= 18 «файлов каталога»                         ← совпало ✓
+ 5 словарей                (PackageKind, NamingConvention,
                             DeliveryMode, DirectoryTag, BindingSite)
= 23 типа на проводе                           ← совпало ✓</fence>
      <p p="14">Пятый — `BindingSite` в `crates/vibe-index/src/index/inverted.rs:89`. Кто считал
только `types/`, получил 4; кто выкинул одновариантный `DirectoryTag` — 3.
Разрешение «расхождение периметра, не факта» верно.</p>
      <p p="15">**Но оно похоронило живой дефект.** Именно пятый словарь — единственный, который
записан **дважды по разные стороны границы публикатор/потребитель, с разными
правилами сериализации и без теста на расхождение**:</p>
      <fence p="16">пишет   crates/vibe-index/src/index/inverted.rs:88   rename_all = "kebab-case"
читает  crates/vibe-registry/src/index_client/wire.rs:93  rename_all = "lowercase"</fence>
      <p p="17">Сегодня они совпадают **по случайности**: оба варианта — однословные
(`Package`, `Subskill`), а для однословных kebab и lowercase дают одно и то же.
Добавьте вариант из двух слов — писатель выдаст `boot-snippet`, клиент будет
ждать `bootsnippet`, и разбор упадёт у потребителя, а не у нас. Паритет-тесты в
дереве есть ровно для двух вещей: `content_hash`
(`crates/vibe-index/tests/content_hash_parity.rs`) и `PackageKind`
(`types/kinds.rs`, `types/mod.rs`). Для `BindingSite` — нет.</p>
      <p p="18">Отчёт использует расхождение словарей как **аргумент** за общий чекер (п. 8) и
иллюстрирует его прозой («four kinds» при шести вариантах). Настоящая
иллюстрация — вот эта, и она на проводе.</p>
    </section>
    <section title="1.4. Роль версии: шлагбаум против декларации (5.4) — **неполно, и в одном месте неверно**">
      <p p="19">Это разрешение просили проверить строже всего. Проверила. Оно не выдерживает в
трёх точках.</p>
      <p p="20">**(а) Это не две роли, а один целый `u32`.** Контракт, который отчёт сам цитирует
(старшая — отказ, младшая — предупреждение, нет — считать первой), **уже
совмещает обе роли в одном номере**: старшая часть — шлагбаум, младшая —
декларация возможностей. Значит, «одним целым числом обе роли не сыграть» —
верно, но это утверждение не про роли, а про **число компонент**. У нас
`schema_version: u32` (`repomd.rs:21`, `entry/mod.rs:44`) — одна компонента,
поэтому декларации некуда лечь. Настоящий вопрос:</p>
      <fence p="21">Сколько компонент у версии?
├── одна (как сейчас)  → только шлагбаум. Это не выбор роли, это арифметика.
└── две (major.minor)  → обе роли сразу, как в цитируемом же контракте.</fence>
      <p p="22">**И этот выбор необратим ровно в том смысле, о котором вся глава 5.6:** `1` можно
потом прочитать как `1.0`, а `2` уже нельзя превратить в `1.1`. То есть решение о
числе компонент — механизм с закрывающимся окном, и его в списке механизмов нет.
Никто не предложил разделить версию.</p>
      <p p="23">**(б) Фактическая посылка ложна, и опровержение лежит в нашем дереве.**
Утверждение «crates.io добавляет поля **без** поднятия версии» неверно. В
`refs/src/cargo/crates/cargo-util-schemas/src/index.rs:70-73` написано прямым
текстом: `Version 2 schema adds the features2 field. Version 3 schema adds
artifact, bindep_targes, and lib`. То есть версия у ближайшего двойника
поднималась **именно ради добавленных полей** — дважды.</p>
      <p p="24">Почему? Потому что `features2` (там же, `:22-28`) отделён от `features` не для
красоты: «versions older than 1.19 will fail to load due to not being able to
parse the new syntax». Ничего существующего не изменилось — но читатель, который
**проигнорирует** новое поле, разрешит фичи неправильно и молча.</p>
      <p p="25">Отсюда правило, которого нет ни у отчёта, ни у одного из воркеров:</p>
      <quote p="26">**Версию поднимают тогда, когда документ перестаёт быть самодостаточным для
читателя, отбрасывающего непонятое.** Не «когда меняется смысл существующих
данных» (это уже, оказывается, слишком узко) и не «на каждое добавление» (это
слишком широко).</quote>
      <p p="27">Проверьте на наших случаях: добавить `homepage` — документ остаётся
самодостаточным, не поднимаем. Добавить седьмой вид пакета — читатель, который
его не знает, **не сможет даже категоризировать запись**, поднимаем. Разница
операциональна, чего нельзя сказать ни про одну из двух формулировок в отчёте.</p>
      <p p="28">**(в) Есть третья роль, и это та, которую версия играет у нас сегодня.**
Отчёт пишет: «версия — ярлык, а не переключатель: ни одного сравнения». Это
занижено. Смотрим код:</p>
      <list ordered="false" p="29">
        <item>`memory.rs:270-273` — `load_from` кладёт `schema_version: manifest.schema_version`
  в память и больше нигде к нему не обращается;</item>
        <item>`memory.rs:242` — `write_to` штампует `schema_version: Repomd::SCHEMA_VERSION`,
  то есть **собственную константу писателя**, а прочитанное значение
  выбрасывает.</item>
      </list>
      <p p="30">Значит, поле не инертно — оно **перезаписывается**. Сегодня оно не описывает
данные, оно описывает, **кто последним трогал файл**. Это третья, никем не
названная роль: **версия как штамп писателя / провенанс**. Для полностью
пересоздаваемого производного каталога это не дефект, а осмысленный режим — «этот
каталог собран более старым мной, пересобери». Прежде чем менять роль, стоит
понять, что нынешняя — не пустое место.</p>
      <p p="31">**(г) Рекомендация («шлагбаум на уровне записи») тем не менее правильная** — и у
двойника она реализована буквально: `index.rs:87` — `pub v: Option&lt;u32&gt;`,
доккомментарий `:66-68`: «If this is None, it defaults to version 1. **Entries
with unknown versions are ignored**». Но там же названа цена, которой в отчёте
нет: «This is honored as of 1.51, so unfortunately older versions will ignore it,
and potentially **misinterpret** version 2 and newer entries». Первая версия
шлагбаума не защищает никого — это надо написать в решении, а не узнать потом.</p>
      <p p="32">**Итог по 5.4:** вывод сохраняется, обоснование надо переписать целиком.</p>
    </section>
    <section title="1.5. Отзыв Avro-в-git (5.5) — **верно; но выброшен полезный остаток**">
      <p p="33">Посылка «схема и данные в одной истории git» действительно ложна, и это
проверяемо: в дереве **нет ни одного** `repomd.json`
(`find . -name repomd.json` — пусто вне `target/`), каталог живёт в отдельном
репозитории (публикационная цель — `https://github.com/vibespecs`,
`crates/vibe-core/src/manifest/project.rs:410`), а машинно-читаемой схемы для
каталога нет вовсе: в `schemas/` лежат семь JTD-схем, и все семь — отчёты команд
(`init_report`, `install_plan`, `install_report`, `list_report`,
`registry_publish_report`, `registry_sync_report`, `uninstall_report`).</p>
      <p p="34">Но вместе с Avro выброшен единственный переносимый его остаток, который назвал
файл 07: **сделать схему машинно-читаемым артефактом и опубликовать её рядом с
данными**. Способность в доме есть и работает —
`crates/vibe-cli/resources/package-tree.schema.v1.json` с золотым тестом
`crates/vibe-cli/tests/tree_json.rs`. В предложении §6 такого пункта нет вообще.
Разрешение верное, сведение потеряло вывод.</p>
    </section>
    <section title="1.6. Механизмы против обязательств (5.6) — **граница проведена не там**">
      <p p="35">Второе несущее разрешение. Тест сформулирован правильно — «закрывается ли окно
вместе с публикацией» — но список заполнен по интуиции, а не этим тестом.
Применяю тест к каждой строке.</p>
      <fence p="36">ЗАЯВЛЕНО КАК МЕХАНИЗМ            применяю тест            куда на самом деле
─────────────────────────────────────────────────────────────────────────────
тег на объединении               окно закрывается ✓        МЕХАНИЗМ
запасное значение в словарях     окно закрывается ✓        МЕХАНИЗМ (но см. 4.2)
решённое значение «отсутствия»   окно закрывается ✓        МЕХАНИЗМ
версия с контрактом              окно закрывается ✓        МЕХАНИЗМ
терпимый читатель                НЕ закрывается ✗          в любой момент</fence>
      <p p="37">**Терпимый читатель стоит не в той колонке.** Ослабление читателя не ломает ни
одних существующих данных и не требует ничьего согласия: его можно включить
сегодня, через год, через пять лет. Он не механизм с окном — он самая обратимая
вещь в списке. А поставлен он туда, где создаёт ощущение срочности, и именно на
этом ощущении держится вся глава 6.</p>
      <p p="38">Симметрично, в колонке обязательств («потом») стоит «никогда не переименовывать»
— а его несущий механизм, `#[serde(alias = "…")]`, тоже бесплатен в любой момент
и в обоих форматах (файл 07, §6). В сведении он потерян целиком.</p>
      <p p="39">**Чего в колонке механизмов не хватает — четыре вещи, у которых окно
действительно закрывается:**</p>
      <list ordered="true" p="40">
        <item>**Место под подпись и признак «обязан понять».** Поиск по
   `crates/vibe-index/src` и `crates/vibe-core/src/manifest` даёт **ноль**
   вхождений `signature`, `signed_by`, `gpg`, `minisign`, `sigstore`. Спека
   честно описывает два слоя целостности (`repomd.json::files[*].sha256` — на
   файлы индекса, `content_hash` — на содержимое пакета,
   `vibevm/vibespecs/modules/vibe-index/PROP-005-package-index.xml:306`), но сам `repomd.json`
   не покрыт ничем: корень доверия — «доверяй гит-хосту». Это допустимое
   решение — но **оно нигде не записано как решение**, а мирроры в дизайне уже
   есть.
   И вот ловушка, которой не увидел никто: **как только читатель становится
   терпимым, будущее поле `signature` можно молча проигнорировать** — а это
   ровно downgrade-атака. Терпимость и будущая аутентичность прямо
   конфликтуют, и примирить их можно только одним способом: **решить заранее,
   какие поля читатель обязан понять**, а какие вправе выбросить. Множества
   «обязан понять» у нас нет и предложение его не заводит.</item>
        <item>**Отметка об отзыве версии (tombstone).** Поиск даёт ноль вхождений `yank`,
   `withdraw`, `revoke`, `tombstone`. У двойника это поле есть **с 2014 года**:
   `refs/src/cargo/crates/cargo-util-schemas/src/index.rs:30-34` —
   `pub yanked: Option&lt;bool&gt;`, «This was added in 2014. Everything in the
   crates.io index has this set now». Механика окна здесь та же, что в
   собственном доводе отчёта про «отсутствие»: как только читатель приучен
   понимать «пакета нет в каталоге» как «его не существует», отличить «отозван»
   от «никогда не индексировался» становится нельзя — отсутствие уже занято.</item>
        <item>**Идентичность алгоритма у `content_hash`.** См. находку 2.6 ниже.</item>
        <item>**Число компонент версии.** См. 1.4(а).</item>
      </list>
    </section>
    <section title="1.7. Строгость по пространству имён, а не по автору (5.7) — **верно, лучшее из семи; две оговорки**">
      <p p="41">Довод «из файла нельзя узнать, кто его написал» правилен и проверяем: единственный
провенанс-маркер — `[origin]` (`crates/vibe-core/src/manifest/document.rs`,
секция `origin`), и он про факт публикации, а не про намерение автора.</p>
      <p p="42">Оговорка первая, механическая: **правило нереализуемо в заявленной форме**.
`deny_unknown_fields` и `#[serde(flatten)]` в serde несовместимы, а
«заповедник» — это по сути flatten-ловушка. Значит, заповедник обязан быть
**явным именованным полем** (`tool: BTreeMap&lt;String, toml::Value&gt;` или подобным),
иначе `deny_unknown_fields` отвергнет `[tool.x]` до того, как до него дойдёт
дело. Файл 09 это заметил для манифеста; при сведении оговорка потерялась, и в
п. 7 предложения заповедник выглядит бесплатным.</p>
      <p p="43">Оговорка вторая, важнее: правило говорит «машинный файл → незнакомое поле
**игнорировать**». Но наш машинный файл читают, **чтобы переписать**.
Игнорировать ≠ сохранить. См. находку 2.1 — это и есть главный пропуск всей
серии.</p>
    </section>
  </section>
  <section title="2. Что пропустили все восемь">
    <section title="2.1. Терпимость без сохранения — это не терпимость, это тихое удаление">
      <p p="44">Самая дорогая находка. Пункт 3 предложения снимает `deny_unknown_fields` с типов
каталога. Проверим, что произойдёт.</p>
      <p p="45">Каталог читается ради перезаписи. Пути (все, вне тестов):</p>
      <fence p="46">add.rs:51      load_from  →  add.rs:122       write_to
remove.rs:35   load_from  →  remove.rs:57     write_to
reindex.rs:218 load_from  →  reindex.rs:281   write_to
serve.rs:77    load_from  →  packages.rs:256  write_to   (upsert)
                          →  packages.rs:304  write_to   (delete_version)
                          →  packages.rs:333  write_to   (delete_package)</fence>
      <p p="47">Шесть. (Седьмой `write_to` — `init.rs:48` — только пишет, не читает; именно
поэтому шесть, а не семь. Число из отчёта подтверждается, но **только с
названным периметром**.)</p>
      <p p="48">Теперь механика. `Index::load_from` (`memory.rs:262`) читает `by-name/*.json` в
типизированную память; `write_to` (`memory.rs:161`) выписывает всё заново из
этой памяти. Сегодня незнакомое поле даёт **отказ разбора** — громко, заметно,
без потери данных на диске. После пункта 3 незнакомое поле будет **прочитано,
проигнорировано и не записано обратно**. То есть следующий же `vibe-index add`
сотрёт всё, что дописал более новый писатель.</p>
      <p p="49">Отчёт называет молчаливую неправоту худшим видом отказа (это его вывод из разбора
аварии Cloudflare и его же «самая ценная находка серии»). Пункт 3 **вводит
именно такой отказ** — своим собственным лекарством.</p>
      <p p="50">Починка существует и стоит одного поля: терпимость обязана идти **в паре с
захватом**, `#[serde(flatten)] extra: Map&lt;String, Value&gt;` на типах
read-modify-write. И — важное следствие — из-за несовместимости flatten с
`deny_unknown_fields` **выбирать приходится потипно**: нельзя быть строгим и
захватывающим одновременно.</p>
      <p p="51">**Почему пропустили все.** Число «6 путей перезаписи» кочевало по всем девяти
файлам как **усилитель тяжести** нынешней строгости («сервер не запустится»), и
ни разу — как **ограничение на лекарство**.</p>
    </section>
    <section title="2.2. Каталог — не один формат, а два, и терпимость нужна только одному">
      <p p="52">`primary::read` (`index/primary.rs:75`) и `primary::parse` (`:84`) имеют **ноль
вызовов** во всём дереве. `by-cap`/`by-purl` при каждой записи стираются
подчистую (`memory.rs:176-178`, `inverted.rs:264`). Отсюда:</p>
      <fence p="53">КАТАЛОГ
├── читается-и-переписывается            ← только здесь живут все проблемы
│     by-name/&lt;name&gt;.json   (NameEntry → PackageEntry → VersionEntry)
│     repomd.json           (Repomd)
└── только пишется, назад не читается    ← здесь проблем нет вообще
      primary.jsonl(.gz), by-cap/*.jsonl, by-purl/*.jsonl</fence>
      <p p="54">Следствия, которых нет ни у кого: (1) круг типов, которым нужна терпимость +
захват, — три, а не двадцать три; (2) экспортные типы могут остаться строгими
даром, потому что round-trip у них не бывает; (3) `deny_unknown_fields` на
`VersionEntry` (`entry/mod.rs:38`) кусается **не потому, что `VersionEntry` —
строка `primary.jsonl`**, а потому что он вложен в `by-name`. Все восемь считали
каталог одним монолитом на 23 типа и оценивали работу соответственно.</p>
    </section>
    <section title="2.3. Ближайший двойник лежит в этом же дереве. Непрочитанный">
      <p p="55">Файл 06 (§4) написал: crates.io — «наш случай почти один-в-один… именно этот
сосед должен был быть главным зеркалом», и честно добавил, что механику
процитировать не может, доступа в сеть нет. Механика лежала на диске:</p>
      <fence p="56">refs/src/cargo/crates/cargo-util-schemas/index.schema.json   (7,4 КБ, JSON Schema)
refs/src/cargo/crates/cargo-util-schemas/src/index.rs        (Rust-типы, serde)</fence>
      <p p="57">Четыре веб-исследования и четыре разбора по дереву — и первоисточник
структурного двойника пролежал непрочитанным. Что он решает (всё цитируемо
локально):</p>
      <table p="58">
        <tr>
          <td>вопрос из отчёта</td>
          <td>что делает двойник</td>
          <td>ссылка</td>
        </tr>
        <tr>
          <td>терпимый или строгий читатель</td>
          <td>`IndexPackage` **без** `deny_unknown_fields`</td>
          <td>`index.rs:6-9`</td>
        </tr>
        <tr>
          <td>роль версии</td>
          <td>`v: Option&lt;u32&gt;`; нет → 1; неизвестная → **запись игнорируется**</td>
          <td>`index.rs:87`, `:66-68`</td>
        </tr>
        <tr>
          <td>поднимать ли на добавление</td>
          <td>**да, поднимали дважды, ради добавленных полей**</td>
          <td>`index.rs:70-73`</td>
        </tr>
        <tr>
          <td>закрытый словарь</td>
          <td>вид зависимости — **свободная строка**, не enum: `kind: Option&lt;Cow&lt;str&gt;&gt;`, «"dev", "build", and "normal"»</td>
          <td>`index.rs:113`</td>
        </tr>
        <tr>
          <td>отзыв версии</td>
          <td>`yanked: Option&lt;bool&gt;`, с 2014</td>
          <td>`index.rs:30-34`</td>
        </tr>
        <tr>
          <td>отсутствие = умолчание?</td>
          <td>**нет, потипно**: `features` со значением по умолчанию `{}`, `features2` нуллабельный, `default_features` с умолчанием **true**</td>
          <td>`index.schema.json`, `index.rs`</td>
        </tr>
        <tr>
          <td>стабильность поля во времени</td>
          <td>`pubtime` «should be the original publish time and **not changed** on any status changes»</td>
          <td>`index.rs` (док к `pubtime`)</td>
        </tr>
      </table>
      <p p="59">Отдельно отмечу строку про вид зависимости: файл 08 доказывал, что открытый
словарь-строка «сдаёт exhaustiveness — наш главный козырь». Двойник — на Rust, на
том же serde, с тем же компилятором — **выбрал строку**. Это не значит, что он
прав, а мы нет; это значит, что довод «так никто не делает» неверен, и решать
надо по существу.</p>
    </section>
    <section title="2.4. Формат невоспроизводим, и это решение никто не принимал">
      <p p="60">`memory.rs:249` — при каждой записи `generated_at: Utc::now()`. При этом
`primary.jsonl` сделан **намеренно байт-детерминированным**: `gzip_deterministic`
с `mtime=0` (`primary.rs:61-65`) и два теста на это (`primary.rs:172`, `:200`).
То есть на одном слое за побайтовую воспроизводимость боролись, а слоем выше её
уничтожили — и никто из восьми этого не заметил.</p>
      <p p="61">Три следствия, ни одно не прослежено:</p>
      <list ordered="true" p="62">
        <item>**Каталог нельзя проверить пересборкой.** Самая дешёвая проверка целостности
   и согласованности из всех возможных («собери заново, сравни байты»)
   недоступна, потому что байты гарантированно разные.</item>
        <item>**Гит пухнет без информационного прироста.** JSON-в-гите с полной
   перезаписью означает, что каждый reindex коммитит весь файл. Именно объём
   репозитория, а не эволюция схемы, в своё время выдавил crates.io с
   git-клона на sparse-HTTP — файл 06 упомянул этот переход как «отдельную
   главу» и не связал.</item>
        <item>**Внутренняя рассинхронизация меток времени.** `NameEntry::new(name,
   self.generated_at)` (`memory.rs:200`) штампует файлы `by-name` **прочитанной
   ранее** меткой, тогда как `repomd.json` получает свежий `Utc::now()`. Две
   метки в одном каталоге принадлежат разным моментам **по конструкции**.</item>
      </list>
    </section>
    <section title="2.5. Правило подъёма версии нигде не написано — а без него шлагбаум не сработает никогда">
      <p p="63">Пункт 5 предложения делает версию шлагбаумом. Нигде — ни в предложении, ни в
спеке — не сказано, **когда писатель её увеличивает**. Шлагбаум без правила
подъёма не опускается ни разу.</p>
      <p p="64">Заодно поправлю утверждение, которое отчёт принял от воркера без проверки:
«спека противоречит и самой себе — два взаимоисключающих утверждения». Прочитала
обе строки дословно:</p>
      <list ordered="false" p="65">
        <item>`PROP-005-package-index.xml:302` — «readers of v2 written by an old vibevm
  gracefully ignore unknown fields», `@status:impl/done`;</item>
        <item>`:624` — «Old consumers parsing a higher schema must surface a "your vibevm is
  older than this index; please upgrade" message — not silently parse the subset
  they understand», `@status:impl/done`.</item>
      </list>
      <p p="66">Это **не взаимоисключающие** утверждения, если читать первое как режим
аддитивного изменения при неизменной версии, а второе — как режим поднятой
версии. Сложенные вместе, они дают ровно тот контракт-шлагбаум, к которому отчёт
приходит в главе 6. Проблема не в противоречии, а в том, что **ни одна из строк
не определяет, какое изменение обязывает поднять версию**, — поэтому спор между
ними неразрешим, а детектор «higher schema» из второй строки не может сработать,
так как сравнения нет нигде. Трещин две (спека↔код и мёртвое сравнение), а не
три; зато настоящая дыра — отсутствие правила — не названа никем.</p>
      <p p="67">Правильная формулировка правила выведена выше из `features2` (см. 1.4(б)).</p>
    </section>
    <section title="2.6. Производные поля — вот где живёт «смертельный слом», и туда никто не посмотрел">
      <p p="68">Отчёт (глава 4, п. 9) утверждает: единственный смертельный слом RPM за двадцать
лет — **переопределение смысла существующего тега**. И после этого ни один из
восьми не спросил, где такой тег есть у нас. Он есть, и не один:</p>
      <table p="69">
        <tr>
          <td>поле</td>
          <td>смысл — это вычисление, живущее только в коде</td>
          <td>где</td>
        </tr>
        <tr>
          <td>`content_hash`</td>
          <td>обход дерева + список исключений + нормализация путей</td>
          <td>`crates/vibe-index/src/content_hash.rs:29,40-60`</td>
        </tr>
        <tr>
          <td>`latest_stable`</td>
          <td>«максимальная версия с пустым pre»</td>
          <td>`types/entry/aggregate.rs:47-55`</td>
        </tr>
        <tr>
          <td>`package_count` / `version_count`</td>
          <td>пересчёт</td>
          <td>`memory.rs:247-248`</td>
        </tr>
        <tr>
          <td>`files[*].size` / `sha256` / `entries`</td>
          <td>пересчёт</td>
          <td>`memory.rs:178-240`</td>
        </tr>
        <tr>
          <td>`files_count`</td>
          <td>пересчёт</td>
          <td>`types/entry/mod.rs:117`</td>
        </tr>
      </table>
      <p p="70">Самое опасное — первое. Список исключений зашит константой:</p>
      <fence lang="rust" p="71">// crates/vibe-index/src/content_hash.rs:29
const SHIPPABLE_EXCLUDES: &amp;[&amp;str] =
    &amp;[".git", ".vibe", "target", "node_modules", ".vibeignore"];</fence>
      <p p="72">Добавьте туда `dist` или `.venv` — и **каждый `content_hash` в мире изменит
значение при том же имени, том же типе и той же версии схемы**. А спека объявляет
`content_hash` **ключом связи каталога с локфайлом**
(`PROP-005-package-index.xml:304`: «`content_hash` is the join key between the
index and the lockfile»). Существующий паритет-тест
(`crates/vibe-index/tests/content_hash_parity.rs`) сторожит **совпадение двух
копий алгоритма между собой**, а не **стабильность алгоритма во времени** — это
разные вещи, и вторую никто не сторожит.</p>
      <p p="73">Механизм с закрывающимся окном: идентичность алгоритма должна ехать в самой
строке (`sha256:` → что-то вроде `sha256-v1:`) или в соседнем поле. Задним
числом это не внедряется — старые строки уже без метки, и отличить «посчитано
старым алгоритмом» от «посчитано новым» будет нельзя.</p>
      <p p="74">И общее правило, которого нет ни у кого: **для каждого производного поля должно
быть написано, кто выигрывает при расхождении — записанное значение или
пересчёт.** Сегодня не написано ни для одного, а терпимый читатель делает это
хуже, а не лучше: он с радостью примет протухшее денормализованное значение.</p>
    </section>
    <section title="2.7. «Внешних потребителей ноль» — недоказуемо по конструкции, и на этом стоит вся стратегия">
      <p p="75">Это молчаливая предпосылка всех девяти файлов и обеих несущих глав. Она
непроверяема, и непроверяема **намеренно**.</p>
      <p p="76">Дефолтный реестр, засеваемый на каждой свежей машине, — публичная организация на
GitHub плюс зеркало:</p>
      <fence p="77">crates/vibe-core/src/manifest/project.rs:410  DEFAULT_REGISTRY_URL = "https://github.com/vibespecs"
crates/vibe-core/src/manifest/project.rs:417  DEFAULT_REGISTRY_REF = "main"
crates/vibe-core/src/manifest/project.rs:424  DEFAULT_REGISTRY_GITVERSE_URL = "https://gitverse.ru/vibespecs"
crates/vibe-core/src/global_registry.rs:106-127  засеваются обе, без спроса</fence>
      <p p="78">Статический файл в публичном гит-репозитории, отдаваемый по raw-URL, **не даёт
телеметрии** — и отчёт сам этому радуется (глава 4, п. 12: версию в заголовке
протокола отвергли, чтобы зеркала оставались тупым файловым сервером). Значит,
честная формулировка не «потребителей ноль», а **«ноль потребителей, о которых мы
бы когда-нибудь узнали»**. Цена ошибки при этом асимметрична: если предпосылка
неверна, мы не узнаем об этом никогда, потому что отказ произойдёт молча и на
чужой машине.</p>
      <p p="79">Справедливости ради — в пользу предпосылки: в этом репозитории **один** тег
(`pre-cultural-refactor`), релизных тегов нет; свидетельств выпущенных бинарников
на руках нет. И `repomd.json` в дереве отсутствует, то есть каталог здесь ещё не
лежит. Так что предпосылка правдоподобна. Но план обязан назвать, **сколько
стоит, если она ложна**, — и не называет.</p>
      <p p="80">Ещё одно, чего не сказал никто: в гите публикация **необратима**. «Мы ещё не
опубликовались» перестаёт быть правдой в момент первого пуша в публичный `main`,
и откатить это нельзя — история хранит каждое состояние вечно.</p>
    </section>
    <section title="2.8. Канонизация — решение, которого никто не рассмотрел, и оно снимает половину спора">
      <p p="81">Спор «пусто против отсутствует» ведётся семантически: что означает отсутствие.
Есть чисто механическое правило, которое решает вопрос вообще без семантики:</p>
      <quote p="82">**Ровно одна последовательность байт на одно состояние.** Два каталога,
описывающие одно и то же, обязаны быть побайтово одинаковыми.</quote>
      <p p="83">Что оно даёт: (а) вопрос «пусто или отсутствует» решается указом, в любую
сторону, и перестаёт иметь значение; (б) вынуждает решить вопрос 2.4
(`generated_at`); (в) делает «пересобрал и сравнил» настоящей проверкой; (г)
минимизирует шум в гите; (д) **машинно проверяется** — то есть это куда лучшее
правило для чекера из п. 8, чем большинство предложенных.</p>
      <p p="84">Никто из восьми его не предложил, потому что все искали **смысл** отсутствия, а
не **форму** записи.</p>
    </section>
    <section title="2.9. Три пункта предложения взаимно несовместимы под кодогенерацией">
      <p p="85">Исходный вопрос владельца — описать формат схемами и генерировать код. Наш язык
схем и его свойства зафиксированы в дереве: `crates/vibe-wire/src/lib.rs:17-45`
— JTD, генератор `jtd-codegen 0.4.1`, и прямым текстом: **«the generator cannot
emit it»** про `deny_unknown_fields`, плюс «JTD's own `additionalProperties`
works the *opposite* way». Там же (`lib.rs:49-53`) — генератор переименовывает
snake_case в camelCase и требует ручных `rename`.</p>
      <p p="86">Складываем с предложением:</p>
      <fence p="87">п.1  тегированное объединение   JTD умеет размеченные объединения    → ✓ и это ДЕЙСТВИТЕЛЬНО
                                                                        разблокирует кодоген
п.2  запасное значение в словарях  JTD-enum — закрытое множество строк,
                                    catch-all в нём выразить нечем     → ✗ придётся сделать
                                                                        словарь строкой,
                                                                        то есть сдать
                                                                        exhaustiveness
п.4  «известное пустое пишем явно»  в JTD нет «пропусти когда пусто»;
                                    необязательное вернётся Option&lt;T&gt;  → ✗ решение примет
                                                                        генератор, не мы
п.8  один чекер                    строгость генератор не умеет вовсе  → строгость останется
                                                                        только у рукописных</fence>
      <p p="88">То есть **принять п. 1 + п. 2 + курс на кодогенерацию одновременно означает
отказаться от закрытых Rust-перечислений** — ровно от того, что файл 08 называет
нашим главным козырем. Ни один из восьми не поставил эти пункты в одну комнату.</p>
      <p p="89">_(Свойства JTD я вывожу из заметок в нашем же `vibe-wire` плюс общих знаний;
считать непроверенным до часового спайка против `jtd-codegen 0.4.1`.)_</p>
    </section>
    <section title="2.10. Мелкое, но показательное: внутренний документ уже разошёлся с кодом">
      <p p="90">`campaigns/packages-2026-09/harvest/a6-wire-format-census.md:35` утверждает, что
у `VersionEntry` 13 полей с пропуском-когда-пусто. В коде их **18**
(`types/entry/mod.rs`, строки 59, 67, 70, 72, 74, 76, 78, 84, 87, 90, 93, 96, 99,
102, 105, 108, 111, 114). Остальные числа того же документа сходятся. Это тот
самый дрейф документации от кода, ради которого предлагается п. 8 — и он уже
случился внутри самих материалов расследования.</p>
    </section>
  </section>
  <section title="3. Числа, проверенные по дереву">
    <p p="91">Проверила пять из семи. Все пять сходятся; у трёх обнаружился существенный
периметр, который надо называть вслух.</p>
    <p p="92">**№1. «`deny_unknown_fields` в 15 местах» — СОШЛОСЬ.**
Ровно 15 в `crates/vibe-index/src`, все в `types/`:
`repomd.rs:15`; `entry/mod.rs:38`; `entry/content.rs:18, 37, 60, 73, 92`;
`entry/relations.rs:14, 29, 42, 57, 63, 76`; `entry/aggregate.rs:21, 64`.
По всей первой стороне (`crates/` + `xtask/`) — 63, что совпадает с «~63 места»
из `crates/vibe-wire/src/lib.rs:22`. Не покрыты намеренно: `RepomdFileEntry`
(serde не позволяет под `untagged`), `CapabilityRow` (`inverted.rs:65`),
`PurlRow` (`inverted.rs:74`).</p>
    <p p="93">**№2. «21 поле со схлопыванием» — СОШЛОСЬ.**
35 вхождений `skip_serializing_if` в `types/`, гистограмма: `Option::is_none` 14,
`Vec::is_empty` 12, `BTreeMap::is_empty` 2, `&lt;Тип&gt;::is_empty` 7. Коллекций
12+2 = **14**; плюс 7 вложенных структур = **21**. Оба числа отчёта
воспроизводятся точно. _Периметр:_ есть третье защитимое число — 23, если считать
вложенными структурами ещё и `Option&lt;WorkspaceOriginEntry&gt;` (`:67`) и
`Option&lt;BootSnippetEntry&gt;` (`:114`); но для **этого** вопроса они не считаются,
у `Option` «отсутствует» и «None» — биекция без схлопывания. 21 верно.</p>
    <p p="94">**№3. «6 путей перезаписи» — СОШЛОСЬ, но только с названным периметром.**
Вызовов `write_to` вне тестов **семь**: `add.rs:122`, `init.rs:48`,
`reindex.rs:281`, `remove.rs:57`, `packages.rs:256`, `:304`, `:333`. Из них
`init.rs:48` — только запись, без предварительного чтения. Пути
«прочитал-изменил-переписал» — **шесть**. Полный список с парами
`load_from`→`write_to` — в находке 2.1.</p>
    <p p="95">**№4. «23 типа / 18 файлов каталога / 5 словарей» — СОШЛОСЬ, и три числа
замыкаются только вместе.** Разбор — в разделе 1.3. Пятый словарь —
`BindingSite` (`index/inverted.rs:89`); варианты «4» и «3» получаются
вычитанием периметра.</p>
    <p p="96">**№5. «4 места, где старый читатель разберёт и поймёт неверно» — СОШЛОСЬ, все
четыре на месте.**
(1) `repomd.rs:42-55` — `untagged` без `deny`, лишние ключи глотаются; строгость
здесь **невозможна** в принципе (см. 1.1).
(2) `memory.rs:270-273` — `schema_version` копируется и не сравнивается; сильнее
того, `memory.rs:242` перезаписывает его константой писателя.
(3) `inverted.rs:65-66, 74-75` — `CapabilityRow` и `PurlRow`, единственные типы
каталога без `deny_unknown_fields`.
(4) `crates/vibe-core/src/manifest/subskill.rs:145` — `matches!(self,
DeliveryMode::LazyPush | DeliveryMode::LazyPull)`, не исчерпывающий `match`:
четвёртый режим доставки молча получит «описание не требуется» без ошибки
компиляции.</p>
    <p p="97">**Не проверяла:** «86 полей» целиком (грубый счёт даёт 71 именованное поле в
`types/` + 12 в двух строках `inverted.rs` — сходится по порядку, точную сверку
не делала).</p>
  </section>
  <section title="4. Вердикт по восьми пунктам предложения">
    <p p="98">**1. Тегированное объединение — ПРИНЯТЬ, заменив обоснование.**
Форма A (`#[serde(tag = "kind")]`). Обоснование не «однозначность разбора» (она
и так есть), а два других: тип становится выразим в языке схем — это и был
исходный блокер; и это **единственный** способ прекратить молчаливое глотание
добавленных полей, потому что строгость на `untagged` серде не даёт. Одним
коммитом с правкой золотого теста `repomd.rs:132`.</p>
    <p p="99">**2. Запасное значение в каждом закрытом словаре — ИЗМЕНИТЬ, резко сузив.**
«Каждый» — неверно. Критерий: **ветвится ли на этом значении потребитель, и
переписывается ли из него каталог.**</p>
    <fence p="100">DirectoryTag      один вариант, это и есть тег          → НЕТ, бессмысленно
NamingConvention  исчерпывающий match строит пути;
                  у неизвестного нет безопасного
                  поведения                             → НЕТ, громкий отказ верен
PackageKind       примыкает к идентичности; и
                  #[serde(other)] здесь ЗАПРЕЩЁН —
                  он теряет исходную строку, а
                  by-name переписывается (2.1)          → ОТЛОЖИТЬ до решения
                                                          по кодогену; если делать,
                                                          то Unknown(String)
DeliveryMode      подсказка, не идентичность            → ДА, Unknown + мягкая
                                                          деградация
BindingSite       сначала устранить дублирование (1.3);
                  на стороне писателя это экспорт,
                  запас нужен в клиенте                 → ДА, в клиенте</fence>
    <p p="101">**3. Терпимый читатель каталога — ПРИНЯТЬ ТОЛЬКО В ПАРЕ с сохранением
незнакомого, и только на трёх типах.**
В одиночку пункт превращает громкий отказ в тихое удаление (2.1). Терпимость +
`flatten`-захват на `NameEntry` / `PackageEntry` / `VersionEntry`; экспортные
типы (`CapabilityRow`, `PurlRow`, строки `primary.jsonl`) можно оставить строгими
даром, у них не бывает round-trip (2.2). Строгость и захват на одном типе
несовместимы — решение потипное.</p>
    <p p="102">**4. «Отсутствие = не записано; известное пустое пишем явно» — ОТВЕРГНУТЬ в этой
формулировке; ЗАМЕНИТЬ на канонизацию.**
Правило покрывает 21 поле из 35 и оставляет 14 без ответа, потому что `null` в
том же предложении отвергнут. Заявление «одно правило, выживающее в обоих
форматах» не выполняется. Двойник принимает решения **потипно** (см. таблицу в
2.3), и это не небрежность, а норма. Плюс «писать пустое явно» добавит до 21
ключа в каждую запись и раздует `primary.jsonl`. Взамен — правило из 2.8: одна
байтовая форма на одно состояние. Оно решает тот же спор, машинно проверяемо и
попутно чинит 2.4.</p>
    <p p="103">**5. Версия — шлагбаум на уровне записи — ПРИНЯТЬ, дополнив тремя вещами.**
Направление верное, и двойник его реализует буквально (`index.rs:87`). Не
хватает: **(а)** написанного правила подъёма (формулировка — 1.4(б)); **(б)**
решения перестать затирать прочитанное значение константой писателя
(`memory.rs:242`) — иначе шлагбаум сравнивает поле с самим собой; **(в)**
отдельной константы «максимальная понимаемая версия», чтобы шлагбаум был одним
сравнением, а не рассуждением. И записать вслух, что первая версия шлагбаума не
защищает никого.</p>
    <p p="104">**6. Версия в `vibe.toml`, отсутствие ≠ «считать первой» — ОТВЕРГНУТЬ в текущем
обосновании, ПЕРЕФОРМУЛИРОВАТЬ.**
Эталон выбран неверно. `vibe.lock` — **пересоздаваемый черновик**: его
собственный комментарий говорит «earlier versions are not read… the next `vibe
install` regenerates it» (`crates/vibe-core/src/manifest/lockfile.rs:44-46`), а
шлагбаум там — `!=`, точное равенство (`lockfile.rs:430`). Переносить калитку
точного равенства, спроектированную для файла, который выбрасывается при каждой
установке, на рукописный файл, который обязан жить годами, — категориальная
ошибка. Двойник же для машинно-написанного индекса делает **ровно наоборот**:
`v: Option&lt;u32&gt;`, отсутствие ⇒ 1. Сначала надо ответить, внешняя ли `vibe.toml`
поверхность (см. 5.2); версия — следствие этого ответа, а не наоборот.</p>
    <p p="105">**7. Заповедник для чужих ключей — ПРИНЯТЬ, но как явное поле.**
`deny_unknown_fields` и `flatten` несовместимы; заповедник обязан быть
именованным полем (`tool: BTreeMap&lt;String, toml::Value&gt;`), иначе он объявлен и не
работает. Плюс тест round-trip: `[tool.x]` обязан пережить цикл
чтение→запись. Механизм сохранения комментариев в доме уже есть
(`crates/vibe-core/src/manifest/mod.rs`, `merge_preserving_comments`).</p>
    <p p="106">**8. Один чекер на три формата — ПРИНЯТЬ, изменив состав правил.**
Лучшее правило из всех девяти файлов — сверка `deny_unknown_fields` с тем, что
обещает спека (файл 09, R5): оно поймало бы заглавную находку автоматически.
Взять. Добавить: **канонизация/детерминизм** (2.4, 2.8); **паритет для каждого
продублированного словаря, включая `rename_all`** — сегодня паритет есть только
у `content_hash` и `PackageKind`, а сломан именно `BindingSite` (1.3); **«нет
производного поля без записанного правила пересчёта»** (2.6). Обоснование
«иначе три набора правил разойдутся» оставить, но иллюстрацию сменить с прозы на
`BindingSite` — она на проводе.</p>
  </section>
  <section title="5. Ответы на четыре вопроса владельца">
    <section title="Вопрос 1 — чем служит версия каталога?">
      <p p="107">**Поставлен неверно.** Правильный вопрос: **сколько компонент у нашей версии?**
Ответ «шлагбаум или декларация» не выбирается — он вычисляется из числа
компонент (1.4(а)).</p>
      <p p="108">**Рекомендация.** Оставить одну компоненту и сделать шлагбаум на уровне записи,
как у двойника: нет версии → считать 1; выше понимаемой → **пропустить запись** с
одним сообщением, остальное прочитать; ниже → читать. В том же абзаце написать
правило подъёма: **поднимаем, когда документ перестаёт быть самодостаточным для
читателя, отбрасывающего непонятое.**</p>
      <p p="109">Но решить это надо **сейчас и осознанно**, потому что `1` ещё можно потом
прочитать как `1.0`, а `2` в `1.1` уже не превратить. Если есть хоть какая-то
вероятность, что декларация возможностей понадобится, — заводить две компоненты
надо до первой публикации, а не после.</p>
      <p p="110">И отдельно: перестать затирать прочитанную версию константой писателя. Пока
`write_to` штампует своё, версия описывает писателя, а не данные.</p>
    </section>
    <section title="Вопрос 2 — манифест пакета и манифест проекта: один формат или два?">
      <p p="111">**Поставлен на шаг позже, чем нужно.** Сначала: **является ли рукописный
`vibe.toml` вообще внешней поверхностью чтения?**</p>
      <p p="112">Код говорит, что нет: `Manifest::read` / `Manifest::parse_str`
(`crates/vibe-core/src/manifest/document.rs`) — единственные двери, и их зовёт
тот же бинарник, что и писал; внешняя поверхность — каталог, и клиент уже читает
его терпимо (`crates/vibe-registry/src/index_client/wire.rs:14-16`).</p>
      <p p="113">**Рекомендация.** Оставить каталог единственной внешней поверхностью и
**записать это как решение** — сейчас это случайность реализации, а не выбор.
Тогда вопрос «один формат или два» растворяется, и вместе с ним растворяются
манифестные половины вопросов 1 и 3. Если же владелец хочет, чтобы чужой
инструмент парсил именно `vibe.toml` пакета, — тогда честный ответ «два», и
опубликованная копия должна быть **сгенерированной**, а не рукописной; но это
отдельное проектное решение, и принимать его надо явно, а не через версию поля.</p>
    </section>
    <section title="Вопрос 3 — принимаем ли, что отсутствие означает «не записано»?">
      <p p="114">**Поставлен как вопрос о смысле; полезнее поставить как вопрос о форме.**</p>
      <p p="115">**Рекомендация — три уровня:**</p>
      <list ordered="true" p="116">
        <item>**Глобально принять не смысл, а канонизацию**: одна байтовая форма на одно
   состояние (2.8). Это то единственное, что действительно должно быть общим для
   трёх форматов.</item>
        <item>**Для каталога — оставить как есть** (пусто опускаем). Каталог пишет машина,
   она всегда знает значение каждого поля; состояние «не записано» там законно не
   наступает. Заодно не раздуваем каждую запись 21 ключом. Но привести к одной
   конвенции 4 поля-коллекции, которые сегодня пишутся всегда (1.2), — иначе
   канонизация невозможна.</item>
        <item>**Различимость «не сказано» сохранить только там, где она реальна** — в
   рукописном манифесте. Это разделение сделал файл 07; при сведении его
   схлопнули в одно правило, и правило перестало быть верным для обеих сторон.</item>
      </list>
      <p p="117">Двойник, напомню, принимает эти решения **потипно** и не стесняется.</p>
    </section>
    <section title="Вопрос 4 — с какого момента формат считается опубликованным?">
      <p p="118">**Дата — не то, что здесь решается.**</p>
      <p p="119">**Рекомендация.** Определить публикацию не датой, а **наблюдаемым действием с
именем**: первый пуш каталога в публичную ветку, на которую третья сторона может
приколоться. И принять два честных следствия: (а) по этому определению часы
могут пойти в день, когда дефолтный реестр наполнится, — то есть, возможно,
раньше, чем кажется; (б) сигнала о появлении читателя **не будет никогда**,
архитектура его не предусматривает (2.7).</p>
      <p p="120">Поэтому **не покупать аргумент про свободу по полной цене**. Обязательства
(вечные псевдонимы, список отставленных имён) действительно можно отложить — их
несущий механизм `#[serde(alias)]` бесплатен в любой момент. А четыре
необратимые вещи надо сделать независимо от даты, потому что их закрывает само
отсутствие: место под подпись и множество «обязан понять»; отметка об отзыве
версии; идентичность алгоритма у `content_hash`; число компонент версии.</p>
    </section>
  </section>
  <section title="6. Порядок работ">
    <p p="121">**Первым — дёшево, необратимо, ничьих решений не требует:**</p>
    <list ordered="true" p="122">
      <item>**`BindingSite`: один источник + паритет-тест на пару `(вариант,
   wire-строка)`, включая `rename_all`.** Живой дефект, полчаса работы
   (`inverted.rs:88` ↔ `index_client/wire.rs:93`).</item>
      <item>**`content_hash`: вписать идентичность алгоритма** и добавить золотой тест на
   фиксированное дерево-образец. Окно закрывается; поле — ключ связи с локфайлом.</item>
      <item>**Решить вопрос воспроизводимости** (`generated_at`, `memory.rs:249`): если
   каталог обязан быть воспроизводимым — убрать `Utc::now()` и добавить тест
   «пересобрал → байт в байт»; если нет — записать это как решение, а не оставлять
   как случайность рядом с намеренно детерминированным gzip.</item>
      <item>**Назвать множество «обязан понять»** — до того, как читатель станет терпимым.
   После этого подпись уже не приделать безопасно.</item>
    </list>
    <p p="123">**Вторым — после ответа владельца по вопросу 2 и после часового спайка по JTD:**</p>
    <list ordered="true" p="124">
      <item>Тег на объединении (п. 1), одним коммитом с золотым тестом.</item>
      <item>Терпимость **плюс захват** на `NameEntry`/`PackageEntry`/`VersionEntry`;
   экспортные типы не трогать.</item>
      <item>Версия-шлагбаум + написанное правило подъёма + константа «максимум
   понимаемого» + прекратить затирать прочитанное значение.</item>
      <item>Отметка об отзыве версии — если каталог вообще будет описывать отзыв.</item>
    </list>
    <p p="125">**Третьим:** чекер (п. 8) с правилами из раздела 4 — но только после того, как
хоть одно из правил станет истинным, иначе первый прогон будет красным по всем
строкам.</p>
    <p p="126">**Чего НЕ делать:**</p>
    <list ordered="false" p="127">
      <item>**Не снимать `deny_unknown_fields` раньше, чем появится захват незнакомого.**
  Это превратит громкий отказ в тихое удаление на шести путях.</item>
      <item>**Не ставить `#[serde(other)]` на `PackageKind`.** Он теряет исходную строку, а
  `by-name` перезаписывается — одно `add`, и правда уничтожена.</item>
      <item>**Не вводить версию в `vibe.toml`, пока не решён вопрос внешней поверхности**,
  и не копировать туда калитку `!=` из локфайла.</item>
      <item>**Не раздавать запасное значение всем пяти словарям** — двум оно вредно, одному
  бессмысленно.</item>
      <item>**Не описывать каталог JTD-схемой раньше спайка**: пункты 1, 2 и 4 под
  кодогенерацией конфликтуют между собой (2.9), и узнать это лучше за час, чем
  за спринт.</item>
      <item>**Не считать «полигон» бесплатным.** Каталог дёшев только снаружи; внутри
  строгое обратное чтение означает, что старый сервер не поднимется на каталоге,
  написанном новым.</item>
    </list>
  </section>
  <section title="7. Чего я не проверила">
    <list ordered="false" p="128">
      <item>Не запускала `cargo build` и `cargo test`. Всё сказанное о поведении выведено
  из чтения кода, а не из прогона.</item>
      <item>Поведение serde `untagged` (лишние ключи не мешают матчу) и несовместимость
  `deny_unknown_fields` с `flatten` — из документации и общего знания serde, не из
  эксперимента. Оба утверждения несущие; проверяются пятью строками теста.</item>
      <item>Свойства JTD и `jtd-codegen 0.4.1` (закрытые enum, отсутствие
  «пропусти-когда-пусто», `Option&lt;T&gt;` у необязательных) — из заметок в
  `crates/vibe-wire/src/lib.rs` плюс общих знаний. **Непроверено**, нужен спайк.</item>
      <item>Не проверяла ни одной веб-цитаты из файлов 02, 03, 04 — читала их выборочно и
  в сеть не ходила. Всё про Meta, LinkedIn, Discord, Cloudflare, Homebrew, NuGet,
  Maven, Debian, RPM у меня осталось непроверенным.</item>
      <item>Всё про crates.io/cargo, что я утверждаю, взято **только** из вкопированной в
  наше дерево копии `refs/src/cargo/crates/cargo-util-schemas/`. Насколько эта
  копия свежа относительно верхнего течения — не проверяла.</item>
      <item>Не проверяла, опубликован ли фактически каталог в `github.com/vibespecs` —
  установила только, что это дефолтная публикационная цель в коде и что в этом
  репозитории `repomd.json` нет.</item>
      <item>Число «86 полей» сверила лишь по порядку величины, не поштучно.</item>
      <item>Не проверяла кросс-платформенную стабильность сортировки в `content_hash`
  (`files.sort()` идёт по `PathBuf` **до** нормализации `\`→`/`). Подозрение на
  расхождение Windows/Unix есть, доказательства нет — `Path::cmp` сравнивает
  по компонентам, что скорее спасает; нужен тест, а не рассуждение. Отдельно
  реален `to_string_lossy()` (`content_hash.rs:53`): два разных не-UTF-8 имени
  файла могут дать один хэш.</item>
      <item>Не считала манифесты `vibe.toml` в дереве и не проверяла число 172 из файла 09.</item>
      <item>Не смотрела, что происходит при повторной публикации одной и той же
  `(group, name, version)` с другими байтами — правила неизменяемости
  опубликованной версии я в спеке не искала, а это, возможно, ещё одна дыра того
  же класса, что отметка об отзыве.</item>
    </list>
  </section>
</spec>
