# Общий индекс команды (издание Pro)

Документ описывает возможность `shared-index`: что она делает, как проверяется и
где её границы. Числа и факты здесь — измеренные 25.10.2026 на ревизии, названной
в `index_status`; при расхождении верить прогону, а не этому тексту.

## 1. Задача

Индекс RAGRAF живёт на одной машине. Пока он отвечает только локально, знание
нельзя разделить: у каждого участника свой корпус, своя ревизия и своё «свежо», а
один и тот же вопрос «кто вызывает эту функцию» задаётся заново на каждой машине.

Возможность решает ровно это: **владелец индекса публикует его для чтения**, а
допущенные клиенты спрашивают по сети и получают тот же конверт ответа, что
локально, вместе с честной свежестью удалённого индекса.

## 2. Два режима

| режим | команда | что делает |
|---|---|---|
| обслуживать | `python -m ragraf.shared_index serve --port 9511` | HTTP-сервер, отдаёт операции ЧТЕНИЯ допущенным клиентам |
| спрашивать | `python -m ragraf.shared_index query search "..." --url http://127.0.0.1:9511` | клиент: тот же конверт, что локально, плюс `freshness` |

Запись (индексация) по сети **не публикуется** — это граница этапа, а не
недоделка: клиент не должен уметь дёрнуть чужую переиндексацию.

## 3. Белый список операций

```
search, search_multi, symbol, code, callers, impact, freshness, index_status
```

Всё остальное получает отказ с причиной и перечнем разрешённого. Операции записи
(`ensure`, `reindex`, `watch`, `note`, `drift_ack`, `db`, …) называются записью, а
операции хранилища секретов — отдельным классом отказа: слова «нет в белом списке»
и «значения секретов наружу не уходят» отвечают на разные вопросы владельца.

Параметры операции проверяются по подписи метода фасада. Параметр, которого
операция не объявляет, — отказ с названным списком допустимых, а не тихий пропуск:
иначе «белый список» перестал бы быть списком.

## 4. Права: токены из `access.py` и отдельный допуск

Аутентификация — именованные токены и области (`read`/`write`/`admin`) из
`src/ragraf/access.py`. Секрет не хранится: в `catalog/access_tokens.json` лежит
только его SHA-256, а показывается он один раз при создании.

Допуск — отдельное решение. Действующий токен сам по себе доступа не даёт: нужна
строка в `pro_share_acl`. Пустой список значит «никого», а не «всех»: иначе
публикация индекса и выдача прав были бы одним действием, и его нельзя было бы
отменить отдельно.

```powershell
# установленный пакет:
python -m ragraf.shared_index status
# дерево разработки: тот же модуль, но путь к src задан явно (вендоренный Python
# работает с sys.flags.safe_path, поэтому ни cwd, ни PYTHONPATH в sys.path не попадают):
python -c "import sys; sys.path.insert(0, 'src'); import runpy; sys.argv[0]='ragraf.shared_index'; runpy.run_module('ragraf.shared_index', run_name='__main__')" status
```

Команды ниже показаны в форме установленного пакета (`python -m`).

```powershell
python -m ragraf.shared_index token --label "клиент-A"        # создать токен чтения и допустить метку
python -m ragraf.shared_index open  --port 9511               # открыть корпус для чтения
python -m ragraf.shared_index status                          # кто допущен и на каком порту
python -m ragraf.shared_index close                           # снять публикацию
python -m ragraf.shared_index revoke --label "клиент-A"        # отозвать допуск
```

Клиент передаёт токен переменной окружения `RAGRAF_SHARE_TOKEN` (или
`--token-stdin`): аргумент командной строки виден в списке процессов, и секрет в
нём оказался бы там же, где его ищут сборщики мусора.

## 5. Свежесть удалённого индекса

В каждом ответе есть три поля:

```json
"freshness": {
  "changed_since_index": false,
  "checked_at": "2026-10-25T18:02:11+03:00",
  "reason": "индекс совпадает с кодом: контрольная сумма корпуса не изменилась"
}
```

`changed_since_index` может быть `null` — это значит «проверить не удалось», и
тогда `reason` называет причину словами. Молчание вместо ответа запрещено: именно
так прошлое состояние кода выдаётся за настоящее.

Проверка свежести **не запускает переиндексацию**: `watch.check(..., auto_reindex=False)`.
Порядок сверки двухступенчатый: сначала отпечаток входа (пути, размеры, время
правки — десятки миллисекунд), и только если доказать не удалось — полная сверка
контрольных сумм, минуя кэш `watch` (окно кэша 15 секунд иначе сообщило бы
«изменений нет» через секунду после правки).

## 6. Границы: что не уходит наружу

* **Значения секретов.** Операций хранилища секретов нет ни в белом списке, ни в
  коде модуля. Ответ на такую попытку называет её отдельным классом отказа.
* **Пути этой машины.** Абсолютный путь вне корня корпуса заменяется пометкой
  `<скрыто: вне разрешённого корня>`; путь внутри корпуса отдаётся относительным.
  Строка вида `/api/users` маршрутом не считается и не маскируется — это путь
  приложения, а не файл машины.
* **Содержимое файлов.** Отдают только `code` и `search`, и только то, что попал
  в выдачу операции. Это держится белым списком, а не фильтром текста.
* **Сеть.** По умолчанию сервер слушает только loopback. Привязка к внешнему
  адресу требует явного `--allow-network`; без него сервер не поднимается.
* **Запись.** Индексация, сторож и любая запись по сети недоступны.

## 7. Совместимость версий

Клиент называет свою версию; сервер сверяет `major.minor`. Расхождение — отказ с
причиной («конверт ответа меняется вместе с возможностями»), а не ответ не той
формы. Клиент, не назвавший версию, тоже получает отказ: отвечать наугад нельзя.

## 8. Хранилище состояния

Состояние публикации живёт в `control.sqlite` корпуса, в таблицах с префиксом
`pro_`:

| таблица | что в ней |
|---|---|
| `pro_share` | корпус, состояние (`opened`/`closed`), адрес привязки, порт, кто и когда открыл |
| `pro_share_acl` | метка допущенного клиента (`*` — любой действующий токен), области, кто выдал, когда отозван |

Ядро об этих таблицах не знает и при переиндексации их не удаляет: проверка
`python tests/test_shared_index.py` переиндексирует корпус и требует, чтобы строки
и `capability` остались прежними.

## 9. Издание: закрытый модуль

Модуль `src/ragraf/shared_index.py` — **закрытая часть коммерческой поставки**
(уровень лицензии Pro; в Enterprise работает по той же лицензии). В поставке
Community его нет: граница поставки — отсутствие кода, а не проверка права в
рантайме. Объявление одно — `pro_only` в `tools/release/manifest-enterprise.toml`;
оно и публикует файл в закрытой поставке, и требует его отсутствия в Community:

```powershell
python tools\release\build_release.py --out $env:TEMP\community
python tools\release\build_release.py --manifest tools\release\manifest-enterprise.toml --out $env:TEMP\ent
python tools\release\verify_release.py --tree $env:TEMP\community
    -> файла src\ragraf\shared_index.py в сборке нет (правило excluded его отсекает)
python tools\release\verify_release.py --tree $env:TEMP\ent --manifest tools\release\manifest-enterprise.toml
    -> файл есть, правило excluded не применяется по объявлению pro_only
```

Операции фасада `shared_index_status`, `shared_index_publish`, `shared_index_query`
объявлены в базовой линии изданий как возможность `shared-index` издания pro. В
сборке без модуля они отвечают отказом `FeatureUnavailable` с причиной «модуля
`shared_index.py` нет в этой сборке», а не трассировкой.

## 10. Проверки

```powershell
python tests\test_shared_index.py        # 69 свойств: сеть, права, свежесть, pro_*, поставка
python tests\test_editions.py            # слой изданий и гейт операций
python tools\release\check_pro_delivery.py
pwsh -NoLogo -NoProfile -NonInteractive -File tools\Invoke-ProjectDoctrine.ps1
```

## 11. Чего в этом этапе нет

* брокера секретов команды (следующий этап): выдачи по запросу, срока, плановой смены;
* TLS и обратного прокси: канал — loopback или доверенная сеть, и это сказано в
  `--help`, а не подразумевается;
* публикации индексации (записи) по сети;
* управления парком установок и коннекторов к внешним системам.
