内容
# MCP-сервер структуры конфигураций 1С
[](https://github.com/AzeevAN/mcp-1c/actions/workflows/tests.yml)
[](LICENSE)
[](pyproject.toml)
Справочник по метаданным нескольких конфигураций 1С, по синтаксису платформы и
по языку запросов — для агентов, пишущих код на BSL. Отдаёт минимально
достаточный срез: разрешение человеческой формулировки в точное имя объекта,
поиск готовых процедур в выгрузке кода, их сигнатуры, ограниченные окна тел и
обратные связи из вызовов, метаданных и форм,
структуру объекта на нужном уровне детализации, его связи, описание
методов платформы **с учётом версии конкретной
конфигурации** и конструкции языка запросов.
Не заменяет grep по рабочим исходникам проекта: сервер ищет процедуры только в
загруженной выгрузке конфигурации в файлы и не видит незагруженные правки.
Граница источников зафиксирована в
[docs/data-sources.md](docs/data-sources.md).
## Состояние — на 2026-08-21
| Этап | Статус |
|---|---|
| Обработка выгрузки для 1С | ✅ 20 видов метаданных, 8.3.5 и 8.3.23, XML и JSON |
| Формат выгрузки | ✅ [schema v1](docs/schema-v1.md) |
| Загрузчик, модель, граф связей, рендер | ✅ 5 конфигураций, 20 522 объекта, 322 тыс. рёбер |
| Справка платформы | ✅ слитый индекс трёх версий, 25 691 элемент, границы `since`/`until` |
| Язык запросов | ✅ `shquery_ru.hbk`, 127 страниц, отдельный источник без версий |
| Поиск | ✅ 97,1% справка, 94,7% язык запросов, 90,5% метаданные — см. «Измерено» |
| Виртуальные таблицы регистров | ✅ готовые имена полей запроса (`КоличествоОстаток`) |
| Таблица замен для старых платформ | ✅ недоступное не просто запрещено, а заменено рецептом |
| Реестр источников, сопоставление версий | ✅ |
| MCP-сервер, 10 инструментов | ✅ streamable-http и stdio |
| Docker | ✅ один контейнер, `357MB` по выводу Docker CLI |
| Кэш поисковых индексов | ✅ 12 МБ, поднимается вместо повторного разбора |
| Стенд замеров | ✅ `python -m mcp1c.bench`, P@k, MRR, отрыв, сверка пометок |
| Тесты | ✅ `.venv/bin/python -m pytest`, 1100 |
| Дашборд | ✅ реестр, источники, прогон запросов, граф связей, карточки, словарь |
| Авторизация | ✅ `API_TOKEN` на чтение, `ADMIN_TOKEN` на запись |
| Приём выгрузки конфигурации в файлы | ✅ `data/incoming/`, отбор и учёт источника |
| Индексы кода из выгрузки в файлы | ✅ поиск, карточка процедуры и обратный поиск вызовов |
Размер финального образа проверен 2026-08-21: команда
`docker image ls mcp1c:latest --format '{{.Size}}'` вывела `357MB`.
Это отображаемый Docker CLI размер. Команда
`docker image inspect mcp1c:latest --format '{{.Size}}'` вернула `77950901` байт
внутреннего поля;
это другая метрика, а не второй замер отображаемого размера.
**Ограничений на конфигурацию нет.** Загружается любая — типовая
(бухгалтерия, зарплата, документооборот, розница) и отраслевая: обработка
выгрузки обходит метаданные, а не знает их наизусть. Разбирается то, что
выгрузилось.
Значение имеет **версия платформы**, на которой работает конфигурация: от неё
зависит, что `get_syntax` покажет доступным, а что скроет как более позднее.
Проверено на **8.3.5 и 8.3.23** — это границы того, что встречалось; справки
слиты из трёх версий, 8.3.5, 8.3.23 и 8.3.27. Обработка выгрузки при этом
обязана компилироваться на 8.3.5: нижняя граница задана ею, а не сервером.
## Содержание
1. **[Запуск](#1-запуск)** — Docker, [дашборд](#дашборд),
[граф связей](#связи--граф-объекта), без Docker
2. **[Подключение агента](#2-подключение-агента)** — [как работает MCP](#как-это-работает),
[если не подключается](#если-клиент-не-подключается), токен, конфиги
клиентов: [Claude Code](#claude-code), [Codex CLI](#codex-cli),
[Cursor](#cursor), [VS Code](#vs-code-copilot), [Qwen Code](#qwen-code),
[stdio](#локальный-запуск-через-stdio)
3. **[Инструменты](#3-инструменты)** —
[порядок вызовов](#порядок-вызовов--и-что-теряется-если-его-нарушить),
[источники](#источники-независимы), [язык запросов](#язык-запросов--отдельный-источник),
[версии платформы](#ответ-зависит-от-версии-платформы),
[слияние справок](#справки-нескольких-версий-сливаются-в-один-индекс),
[замены](#замена-вместо-запрета)
4. **[Управление данными](#4-управление-данными)** — источники,
[выгрузка конфигурации в файлы](#выгрузка-конфигурации-в-файлы),
[словарь и поисковые ключи](#словарь-как-говорят-против-того-как-названо),
[CLI](#отладка-без-агента--mcp1ccli),
[стенд замеров](#замер-качества-поиска--mcp1cbench),
[сервер вручную](#сервер-вручную--mcp1cserver),
[откуда брать данные](#откуда-берутся-исходные-данные)
5. **[Как устроено](#5-как-устроено)** — модули,
[замеры](#измерено-на-реальных-данных--2026-08-18), [тесты](#тесты)
6. **[Безопасность](#6-безопасность)** — токены, что открыто без них
7. **[Документы](#7-документы)**
8. **[Лицензия](#8-лицензия)** — Apache 2.0, отношение к фирме «1С»
---
# 1. Запуск
## Docker (основной способ)
```bash
# 1. Положить исходные данные
mkdir -p data/bootstrap
cp ВыгрузкаКонфигурации.zip data/bootstrap/
cp /opt/1cv8/8.3.27.2130/shcntx_ru.hbk data/bootstrap/
# 2. Поднять
docker compose up -d --build
# 3. Проверить
curl http://localhost:5001/health
```
```json
{"status":"ok",
"configurations_total":2,
"syntax_loaded":true,
"query_language_loaded":true,
"configurations":["РозницаДляКазахстана","ОтраслеваяКонфигурация"],
"syntax":["8.3.5.1570","8.3.23.1997","8.3.27"]}
```
Справка платформы и язык запросов — разные источники и разные поля:
`syntax_loaded` относится только к первой, `syntax` перечисляет версии
загруженных справок. Имена конфигураций и версии справок отдаются лишь
запросу, прошедшему проверку на чтение; без токена остаются `status`,
счётчик и два флага.
Всё, что лежит в `data/bootstrap/`, индексируется при старте: `*.zip` — выгрузки
конфигураций, `*.hbk` — справка платформы. Повторно один и тот же файл не
разбирается: сверка идёт по хешу.
Выгрузка конфигурации в файлы, положенная в `bootstrap/` по привычке, не
подходит — сервер узнаёт её по составу архива (код есть, манифеста
`manifest.json` / `manifest.xml` нет) и не пытается разобрать: это гигабайты,
гонять которые при каждом старте нельзя. Вместо ошибки в списке стартовых
сообщений называется её имя с напоминанием, куда класть такие файлы —
`data/incoming/`, раздел [«Выгрузка конфигурации в файлы»](#выгрузка-конфигурации-в-файлы)
ниже.
Каталог `./data` монтируется в контейнер как `/data`. Внутри него сервер держит
исходники, индексы, кэш и `registry.json`; пути в реестре относительные, поэтому
каталог можно переносить между машиной разработчика и контейнером.
**`data/` целиком вне git** — это том, а не часть репозитория. Переносится
копированием каталога. Поэтому после клонирования справку нужно положить
самому: репозиторий её не содержит и содержать не может, это контент фирмы
«1С».
После изменения кода контейнер нужно **пересоздать**, а не перезапустить:
```bash
docker compose up -d --build --force-recreate
```
`restart` поднимет прежний контейнер на прежнем образе, и правки не применятся.
**Про порт.** Наружу сервер отдан на `5001`, внутри контейнера слушает `8000` —
проброс `5001:8000` в `docker-compose.yml`. Все адреса в этом файле внешние,
то есть `5001`. Занят другим сервисом — поменяйте левую часть проброса, правую
не трогайте: на неё завязаны `EXPOSE` и healthcheck образа.
## Дашборд
`http://localhost:5001/` — шесть страниц:
| Страница | Что там |
|---|---|
| Обзор | что загружено: объекты, связи, версия платформы, предупреждения из манифестов |
| Источники | список загруженного, три таблицы покрытия каждого корпуса кода, путь к полному JSON-журналу, загрузка `.zip` и `.hbk` с полосой передачи, удаление, блок «Входящие выгрузки» — файлы из `data/incoming/`, разбор по кнопке |
| Запросы | прогон списка формулировок с оценкой и причиной ранжирования |
| **Связи** | **граф окрестности объекта картинкой** |
| Карточка | состав объекта или описание элемента платформы — то же, что видит агент |
| Словарь | правила с происхождением; завести псевдоним или группу синонимов |
### Загрузка — что видно и когда
Загрузка идёт в два этапа, и показываются они по-разному.
**Передача файла** — полосой на странице «Источники»: проценты, объём,
скорость и оценка остатка. Считает её браузер, и это не прихоть.
Сервер хода передачи **не видит**: `await request.form()` возвращает
управление только когда тело пришло целиком, и задание заводится уже после
этого. Проверено на живом сокете: 40 МБ шли 8,1 с, за это время 32 опроса
`/sources` со второго соединения не увидели ни одного задания. На `localhost`
разницы нет, на удалённом сервере это минуты пустого экрана.
Заодно предел в 500 МБ проверяется до отправки: раньше файл-переросток
заливался целиком и лишь потом получал отказ.
**Разбор** — таблицей «Загрузка» на той же странице: «принимается» →
«разбирается» → «готово» либо «ошибка» с причиной. Пока работа идёт, страница
обновляет себя сама (`meta refresh`, раз в 2 с).
Полоса требует JS, таблица — нет. **С выключенным JS форма работает как
обычная**: файл уходит, задание заводится, состояние видно в таблице — нет
только полосы во время передачи.
### Связи — граф объекта
`/graph` рисует окрестность объекта: цвет по виду, стрелка по направлению
ссылки, подпись ребра по наведению. Клик по узлу строит граф вокруг него,
перетаскивание двигает, колесо масштабирует. Предел соседей выбирается на
странице (15…400), обрезка называется числом — «показано 30 из 102».
Отвечает на «что сломается, если тронуть»: регистр, окружённый оранжевыми
документами, сразу говорит, кто его двигает.
**Глубина всегда один шаг.** На двух шагах от ходового справочника достаётся
тысяча объектов, на трёх — треть конфигурации; дальше связь идёт через общие
механизмы вроде дополнительных реквизитов, которые соединяют почти всё со
всем. Отсечь их порогом по числу связей нельзя: у такого узла их 34, а у
осмысленного `Справочник.Пользователи` — 323. Поэтому раскрывает узлы человек,
а не эвристика — он видит, куда идти не стоит.
**У агента такого инструмента нет намеренно.** На два шага граф разрастается
до тысячи объектов, а отсечь общие механизмы простым порогом нельзя: у
осмысленного узла связей может быть больше, чем у технического. Отдельный
инструмент вернётся только вместе с измеримым правилом ограничения выдачи;
пока точный следующий шаг выбирает человек в интерактивном графе.
Промах лечится не выходя из браузера: на странице запросов у каждой фразы есть
ссылка «не то — завести псевдоним», она ведёт в словарь с уже подставленной
фразой. Правка действует сразу — индексы не пересобираются, перезапуск не нужен.
**Чтение закрывается `API_TOKEN`, запись — `ADMIN_TOKEN`.** Пока `API_TOKEN` не
задан, читать может кто угодно, кто дотянется до адреса, — включая структуру
конфигураций и доработки. Для localhost это приемлемо, для сервера в сети нет.
Токены разделены потому, что токен чтения лежит в конфиге каждого MCP-клиента и
утекает вместе с ним; прав удалять источники у агента быть не должно. Админский
токен годится и за токен чтения — держать два заголовка не надо.
```jsonc
// .mcp.json — как клиент передаёт токен
{"mcpServers": {"1c": {"type": "http", "url": "http://localhost:5001/mcp",
"headers": {"X-Api-Token": "..."}}}}
```
Только ASCII: заголовки HTTP кодируются latin-1, кириллица в них не дойдёт.
`/health` остаётся открытым для healthcheck, но имена конфигураций отдаёт
только по токену.
**Загрузка, удаление и правка словаря требуют `ADMIN_TOKEN`** — того же, что у
`/admin/reload`; без него этих ручек не существует, а не «они закрыты». Токен
вводится один раз в форме, в браузер уходит не он, а идентификатор сессии.
Задаётся через `.env` рядом с `docker-compose.yml` — шаблон со всеми
переменными лежит в [`.env.example`](.env.example):
```bash
cp .env.example .env
python3 -c "import secrets; print(secrets.token_urlsafe(32))" # значение
docker compose up -d --force-recreate
```
Имя в результатах — ссылка на карточку: у объекта это реквизиты с типами,
табличные части и движения, у элемента платформы — сигнатура, параметры,
доступность и версия появления. Тот же текст, что получает агент, с
переключателем `brief / fields / full`. У реквизита своей карточки нет —
ссылка ведёт на объект-владелец.
Страница «Запросы» отвечает на вопрос «почему сервер выдал именно это»: рядом с
каждым попаданием стоит причина — `точное совпадение`, `псевдоним из словаря`,
`все слова запроса`. По ней видно, чем лечить промах — синонимом, псевдонимом
или весом.
Разбор справки занимает несколько секунд: страница ответит после него, но
MCP-клиентов это не задерживает — индексация уходит в отдельный поток.
## Без Docker
Из копии репозитория — так же, как гоняются тесты и CLI:
```bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
PYTHONPATH=src .venv/bin/python -m mcp1c.server --host 0.0.0.0 --port 5001
```
Либо пакетом — тогда `PYTHONPATH` не нужен, а рядом появляются три команды:
```bash
pip install .
mcp1c-server --host 0.0.0.0 --port 5001 # = python -m mcp1c.server
mcp1c reg-list --data data # = python -m mcp1c.cli
mcp1c-bench --help # = python -m mcp1c.bench
```
Модульный запуск (`python -m mcp1c.server`) остаётся рабочим и в этом случае:
команды — те же точки входа под другим именем. Все ключи описаны в разделе
[«Управление данными»](#4-управление-данными).
---
# 2. Подключение агента
Сервер реализует протокол MCP штатными транспортами официального SDK, поэтому
подходит любому MCP-клиенту. Никаких обёрток вокруг HTTP не требуется.
| Транспорт | Когда | Адрес |
|---|---|---|
| **streamable-http** | сервер в Docker или на отдельной машине | `http://адрес:5001/mcp` |
| **stdio** | клиент сам запускает процесс локально | — |
Оба поддерживаемых транспорта проверены официальным клиентом MCP: рукопожатие
`initialize`, `tools/list`, `tools/call`, протокол `2025-11-25`.
Устаревший транспорт HTTP+SSE не поддерживается: `--transport sse`
отклоняется как неверное значение до чтения данных и запуска сервера. Старому
клиенту нужен режим streamable-http либо локальный запуск через stdio.
## Как это работает
Полезно понимать до того, как что-то не подключится. **Адрес один — `/mcp`,**
ручки по инструменту не бывает; какой инструмент вызывается, написано в теле
запроса, а не в пути.
Дальше — две разные механики, и путать их не стоит:
| | Описания инструментов | Данные |
|---|---|---|
| Когда | один раз, при подключении | по каждому вызову |
| Кто начинает | клиент, сам, без участия модели | модель, по решению |
| Метод | `POST initialize`, затем `POST tools/list` | `POST tools/call` |
| Куда попадает | системный промпт модели | тело разговора |
| Цена | разово, лежит весь сеанс | за каждый вызов |
При подключении клиент делает `POST initialize` — сервер отвечает названием,
версией и текстом `instructions`, а в заголовке возвращает `mcp-session-id`.
Затем `POST tools/list` отдаёт инструменты разом: имя, описание, JSON-схему
параметров. Всё это кладётся в контекст модели до того, как человек напечатал
первое слово. Модель не ходит за описанием, когда оно понадобится, — оно уже
у неё.
Отсюда следствие, важное при правке описаний: **они занимают место в окне весь
сеанс, независимо от того, вызовет модель хоть один инструмент или ни одного.**
**Инструментов всегда десять, независимо от того, что загружено.** Набор — это
контракт, а не переменная: инструменты связаны между собой, и на рабочем
сервере источники конфигурации, кода, справки платформы и языка запросов могут
быть загружены независимо. Размер контракта `tools/list` плюс `instructions`
не зависит от состояния реестра.
Отсюда прямое следствие, которое надо знать заранее: **если источник не
загружен, вы всё равно платите за его инструменты.** Без справки платформы
`search_syntax` и `get_syntax` лежат в контексте и стоят 1 185 токенов, отвечая
«справка не подключена»; `compare_configurations` при одной конфигурации — 262
токена ради ответа «нужно минимум две». Лечится это загрузкой источника, а не
отбором инструментов: динамический набор пробовали 2026-08-19 и отменили,
потому что независимые источники должны сохранять единый контракт сессии.
Описания поэтому пишутся плотно, а подробности уезжают в выдачу самого
инструмента: за неё платят только когда она нужна.
`GET /mcp` — не «неправильный POST», а третий метод на том же адресе: он
открывает поток сообщений **от сервера к клиенту** и требует уже полученный
`mcp-session-id`. `DELETE /mcp` закрывает сессию.
## Если клиент не подключается
Код ответа в логе (`docker logs -f mcp1c`) называет причину:
| Код | Что не так |
|---|---|
| `406` | клиент не шлёт `Accept: application/json, text/event-stream` — нужны оба типа |
| `400 Missing session ID` | клиент не вернул заголовок `mcp-session-id`, полученный при `initialize` |
| `400` на первом же `GET /mcp` | клиент начал рукопожатие с `GET` — он говорит старым транспортом HTTP+SSE, а на этом адресе streamable-http |
| `404` на `/sse` | то же самое: старый транспорт полностью отключён |
| `401` | не передан `X-Api-Token` при заданном `API_TOKEN` |
| в логе пусто | клиент не отправил запрос вовсе — дело в его конфиге, до сервера не дошло |
Живой случай: **Qwen Code** не подключался, потому что в его конфиге стоял
ключ `url` — в семействе Gemini CLI (Qwen наследует формат) он означает старый
SSE-транспорт, и клиент начинал с `GET`, получая `400`. С `httpUrl` — то есть
streamable-http — подключение проходит сразу.
## Токен: что добавить в настройки клиента
**Если на сервере задан `API_TOKEN`, каждый клиент обязан слать его заголовком.
Без заголовка `/mcp` отвечает `401`, и агент просто не увидит инструментов.**
Годится любой из двух заголовков — сервер принимает оба:
```
X-Api-Token: <токен>
Authorization: Bearer <токен>
```
Три вещи, о которые спотыкаются:
- **Только ASCII.** Заголовки HTTP кодируются latin-1, кириллический токен
через них не дойдёт. Генерировать так:
`python3 -c "import secrets; print(secrets.token_urlsafe(32))"`.
- **В клиент кладётся `API_TOKEN`, а не `ADMIN_TOKEN`.** Админский тоже
примут, но конфиг клиента уезжает в git и в бэкапы: утёкший токен чтения
даёт просмотр, утёкший админский — право снести источники.
- **`stdio` токена не требует вовсе.** Там клиент сам запускает процесс, сеть
не участвует и проверять нечего. Если клиент не умеет задавать заголовки —
это рабочий обходной путь.
Проверить, что сервер видит токен, до всякой настройки клиента:
```bash
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'x-api-token: ВАШ_ТОКЕН' \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
http://localhost:5001/mcp
```
`200` — токен принят. `401` — не тот токен или заголовок не дошёл.
### Как не закоммитить секрет
`.mcp.json` и подобные файлы обычно лежат в репозитории. Варианты:
1. **Подстановка переменной** — если клиент это умеет (Claude Code умеет):
`"X-Api-Token": "${MCP1C_API_TOKEN}"`, а сама переменная в `~/.zshrc`.
В git уезжает имя переменной, не значение.
2. **Вынести файл из-под git:**
`git rm --cached .mcp.json && echo ".mcp.json" >> .gitignore`.
3. **Держать настройку не в проекте, а в пользовательском конфиге клиента** —
тогда репозиторий вообще ни при чём.
## Claude Code
Файл `.mcp.json` в корне проекта:
```json
{
"mcpServers": {
"1c": {
"type": "http",
"url": "http://localhost:5001/mcp",
"headers": { "X-Api-Token": "${MCP1C_API_TOKEN}" }
}
}
}
```
Блок `headers` нужен, только если на сервере задан `API_TOKEN`. Значение взято
из переменной окружения, чтобы файл можно было держать в репозитории:
```bash
echo 'export MCP1C_API_TOKEN=ваш_токен' >> ~/.zshrc && source ~/.zshrc
```
Либо командой:
```bash
claude mcp add --transport http 1c http://localhost:5001/mcp \
--header "X-Api-Token: $MCP1C_API_TOKEN"
```
## Codex CLI
`~/.codex/config.toml` или `.codex/config.toml` в проекте:
```toml
[mcp_servers.mcp1c]
url = "http://localhost:5001/mcp"
# Только если задан API_TOKEN. Имя ключа для заголовков у Codex менялось между
# версиями — сверьтесь со своей (`codex --help`, раздел MCP). Не подхватилось —
# используйте stdio, там токен не нужен вовсе.
[mcp_servers.mcp1c.http_headers]
X-Api-Token = "ваш_токен"
```
## Cursor
`.cursor/mcp.json`:
```json
{
"mcpServers": {
"1c": {
"type": "streamable-http",
"url": "http://localhost:5001/mcp",
"headers": { "X-Api-Token": "ваш_токен" }
}
}
}
```
## VS Code (Copilot)
`.vscode/mcp.json` — здесь ключ называется `servers`:
```json
{
"servers": {
"1c": {
"type": "http",
"url": "http://localhost:5001/mcp",
"headers": { "X-Api-Token": "ваш_токен" }
}
}
}
```
## Qwen Code
Формат от Gemini CLI, и **ключ выбирает транспорт** — это единственная
тонкость:
```json
{
"mcpServers": {
"1c": {
"httpUrl": "http://localhost:5001/mcp",
"headers": { "X-Api-Token": "ваш_токен" }
}
}
}
```
`httpUrl` — streamable-http, наш случай. `url` в этом формате означает старый
SSE-транспорт: клиент начнёт рукопожатие с `GET /mcp`, получит
`400 Missing session ID` и не подключится.
## Прочие клиенты
Windsurf, Antigravity, Cline, Roo Code, консольные агенты — форма записи та же:
тип транспорта, URL и, если задан `API_TOKEN`, блок заголовков. Различия только
в имени файла и в ключе верхнего уровня (`mcpServers` или `servers`) —
сверьтесь с документацией конкретного клиента.
Клиент не умеет задавать заголовки — не тупик: подключайтесь через `stdio`,
там токен не нужен, потому что нет сети.
## Локальный запуск через stdio
Когда клиент должен поднимать сервер сам. **Токен здесь не нужен**: процесс
запускается клиентом, общение идёт через каналы процесса, а не через сеть —
проверять нечего и не от кого защищаться.
```json
{
"mcpServers": {
"1c": {
"command": "python3",
"args": ["-m", "mcp1c.server", "--transport", "stdio", "--data", "/путь/к/data"],
"env": { "PYTHONPATH": "/путь/к/проекту/src" }
}
}
}
```
---
# 3. Инструменты
Набор фиксирован и намеренно мал: каждый инструмент постоянно висит в контексте
агента. Новый инструмент появляется только для отдельной пользовательской
задачи, а не для каждого внутреннего индекса.
| Инструмент | Назначение |
|---|---|
| `list_configurations` | что загружено, какие провайдеры доступны по каждой конфигурации |
| `search_objects(query, config, kind, limit)` | человеческая формулировка → точное имя объекта |
| `search_procedures(query, config, extension, scope, limit)` | точное имя, слова или фраза о типовом событии → процедуры в коде конфигурации либо одного расширения |
| `get_procedure(address, config, extension, start_line, lines)` | оглавление модуля или сигнатура, контекст компиляции и окно тела процедуры |
| `get_callers(address, config, extension, limit)` | подтверждённые места вызова, привязки метаданных и обработчики формы |
| `get_object(full_name, config, detail)` | состав объекта; `detail`: `brief` / `fields` / `full` |
| `get_related(full_name, config)` | движения, ссылки, зависимости — **только прямые** |
| `compare_configurations(full_name, configs)` | один объект в двух конфигурациях |
| `search_syntax(query, config, kind, limit)` | поиск по справке платформы **и по языку запросов** |
| `get_syntax(name, config, detail)` | сигнатура, параметры, доступность, версия, замена для старой платформы |
`config` обязателен, когда загружено больше одной конфигурации: сервер
намеренно не подставляет её молча — иначе агент напишет код по чужой базе, и
об этом никто не узнает.
`list_configurations` показывает фактическое состояние индекса кода основной
конфигурации и каждого расширения: готов, строится с этапом и прогрессом,
ошибка либо код не загружен. Для готового расширения выводится число
перекрытий по четырём аннотациям, количество затронутых модулей и собственных
процедур, а также готовый параметр `extension` для обращения к его коду.
Готовый корпус с неизвестным или повреждённым вариантом называется состоянием
**«готов с ограничениями»**. Все три оболочки получают точные агрегаты одного
поколения, но показывают их по своей задаче. `list_configurations` и `reg-list`
сохраняют подробную диагностику: категории, первые 20 проблем в стабильном
порядке и точное число оставшихся; для неадресуемого кандидата выводится только
порядковый номер, без физического имени и локального пути. `/sources` вместо
ленты адресов показывает три сверяемые таблицы с количеством, процентом и
явным знаменателем: модули и процедуры, структуры форм, модули форм.
Полный список каждого корпуса атомарно хранится в одном актуальном файле
`data/logs/code-<sha256 source_id>.json`. Основная конфигурация и каждое
расширение имеют разные журналы; повторная загрузка заменяет только свой файл,
удаление источника удаляет его. Битый, чужой по поколению или содержательно
расходящийся с текущими индексами журнал не показывается как актуальный и
пересобирается из готовых индексов. Структуры форм отдельно показывают,
сколько полностью, частично и не прочитаны.
Контракт и инварианты schema v1 описаны ниже. Предупреждение стоит выше
неполного ответа и прямо говорит: нулевой счётчик не доказывает отсутствие
скрытых данных.
### Журнал покрытия кода — schema v1
Один файл принадлежит одному независимо удаляемому источнику
`<Конфигурация>:modules` или `<Конфигурация>:ext:<Расширение>`. Исходное имя
не включается в путь: файл называется по полному sha256 `source_id`.
```json
{
"schema_version": 1,
"kind": "module_coverage",
"source": {
"id": "Отраслевая конфигурация:modules",
"kind": "modules",
"sha256": "...",
"loaded_at": "2026-08-24T12:00:00+00:00",
"selection_version": 4,
"locator_generation": 3,
"code_version": "1.0.0"
},
"identity": {
"source_id": "Отраслевая конфигурация:modules",
"source_sha256": "...",
"generation": 3
},
"coverage": {
"modules": {
"total": 2,
"source_available": 1,
"empty": 0,
"partial": 0,
"unreadable": 0,
"conflict": 0,
"compiled_without_source": 1
},
"procedures": {"total": 1, "full": 1, "partial": 0},
"form_structures": {
"total": 1, "full": 0, "partial": 1, "unavailable": 0
},
"form_modules": {
"total": 1, "read": 1, "empty": 0, "missing": 0, "unreadable": 0
},
"limitations": {
"categories": {"compiled_without_source": 1, "unknown_marker": 1},
"occurrences_total": 2,
"problem_rows_total": 2,
"unknown_markers": 1,
"known_markers_incomplete": 0,
"unsupported_addresses": 0,
"broken_containers": 0,
"unreadable_bodies": 0,
"budget_exceeded": 0,
"body_conflicts": 0,
"compiled_without_source": 1
}
},
"problems": [
{
"category": "unknown_marker",
"address": "ОбщаяФорма.Пример",
"ordinal": 0,
"reason": "маркер form не поддержан",
"marker": 99
},
{
"category": "compiled_without_source",
"address": "ОбщийМодуль.Пример",
"ordinal": 0,
"reason": "скомпилированный модуль без исходника",
"marker": null
}
]
}
```
Категории `modules`, `procedures`, `form_structures` и `form_modules` в сумме
обязаны давать свой `total`; обе таблицы форм имеют один знаменатель;
`problem_rows_total` равен длине полного `problems`. `address` равен `null`,
если канонический адрес доказать нельзя, тогда `ordinal` — детерминированный
номер кандидата. `marker` хранит прочитанный маркер структуры `form` либо
`null`, если проблема к нему не относится. `identity` связывает журнал с
sha256 и поколением каталога локаторов.
В файл не попадают абсолютные пути, сырые исключения, traceback, токены, тела
BSL и сырая запись `form`. Каталог открывается без перехода по символической
ссылке, временный файл создаётся внутри него и атомарно заменяет итоговый.
Отказ записи не отменяет готовность корпуса.
`get_object` добавляет сведения о коде в зависимости от `detail`: на `brief`
к провайдеру кода не обращается, на `fields` даёт одну строку со счётчиками,
на `full` перечисляет модули и все формы объекта без их содержимого. Форма
остаётся видимой по любому доказательству: самостоятельному XML-дескриптору,
`Form.xml`, `Form.bin`, плоскому `.Form` или одному модулю формы.
Если индекс строится, завершился ошибкой или не загружен, метаданные объекта
всё равно возвращаются вместе с честным состоянием кода. `get_related` отмечает
связанные объекты, чьи процедуры действительно перекрыты любым загруженным
расширением; собственная процедура расширения такой пометки не создаёт.
На `fields` и `full` карточка объекта перечисляет без общего лимита причины,
относящиеся к его формам, поэтому непрочитанная форма не превращается в ложное
«форм нет». Инструменты процедур и связей также ставят предупреждение выше
частичного результата; смена поколения повторяет весь ответ вместе со
счётчиками и проблемами.
`search_procedures` объединяет два уровня. Точное совпадение имени находит
любую процедуру, включая неэкспортную; поиск по словам, склонениям и
комментарию-шапке работает только по экспортным. На каждом уровне `limit`
задаётся отдельно и должен быть целым от 1 до 50. В ответе приходят адрес
`Модуль::Имя`, сигнатура с диска, экспортность, строка, число подтверждённых
мест вызова и отдельный счётчик одноимённых мест, цель которых разрешить не
удалось. Булевый индекс аннотации расширения называется нейтрально: он не
выдаёт `&После` или `&Перед` за полное перекрытие исходной процедуры.
Сигнатуры и тела не хранятся в оперативной памяти.
Иерархические `.bsl`, плоские `.txt` и запись `module` исходных контейнеров
`Form.bin`/`.Form` проходят один и тот же лексер; поиск и последующая карточка
используют локатор одного поколения, а не восстанавливают путь из адреса.
Для 12 стандартных событий есть отдельный осторожный слой типовых намерений.
Фраза вроде «что выполняется при записи объекта» переводится в точное имя
`ПриЗаписи`, но без `scope` инструмент показывает только имя и число
реализаций — случайный адрес не выбирается. При нескольких реализациях внутри
объекта нужен точный модуль или форма; нераспознанная фраза остаётся в обычном
поиске. Список событий, правила расширения каталога, границы и команда замера —
в документе [«Типовые намерения поиска процедур»](docs/standard-procedure-intents.md).
Без `scope` поиск глобальный. Явный `scope="Документ.ЧекККМ"` поднимает все
модули объекта, а `scope="ОбщийМодуль.ОбщегоНазначения"` или другой точный
адрес — один модуль; остальная выдача остаётся ниже. Область не угадывается
из `query`. `extension` выбирает ровно одно загруженное расширение; без него
поиск идёт только по основной конфигурации.
`get_procedure` принимает точный адрес из поиска. Адрес модуля без `::`
возвращает оглавление без тел; `Модуль::Имя` — сигнатуру, контекст компиляции и
тело. `start_line` начинается с 0, `lines` должен быть целым от 1 до 200;
длинный ответ содержит готовый вызов следующего окна без пропуска строк. Для
выбранного расширения `&После`, `&Перед` и `&Вместо` показывают его тело;
`&ИзменениеИКонтроль` — тело основной конфигурации и только дословные блоки
`#Вставка`/`#Удаление`. О чужих загруженных расширениях инструмент предупреждает
над телом, но их код в ответ не подмешивает. Сигнатуры и тела читаются с диска
для одного ответа и не остаются в оперативной памяти.
Тот же локатор переживает кэш и рестарт: если источник заменён во время
чтения, весь ответ повторяется по новому поколению, а части двух поколений не
смешиваются.
`get_callers` принимает только точный адрес `Модуль::Имя`. Сначала он
группирует подтверждённые места вызова по модулю и называет процедуру-владельца,
затем показывает подписки на событие и регламентные задания из графа
метаданных, после них — элемент и событие формы из `Form.xml`. `limit` должен
быть целым от 1 до 50; после усечения остаются точные числа мест и модулей.
Дескриптор или контейнерная запись сами по себе не доказывают отсутствие
обработчика: при недоказанной позиционной семантике инструмент сообщает о
частичном покрытии. Статус `Form.xml` хранится отдельно, поэтому исправные
XML-привязки остаются доступны даже при дефекте соседнего контейнера.
Вызовы одноимённой процедуры без разрешённого модуля выводятся отдельно и не
приписываются запрошенному адресу. Если две процедуры лежат на одной физической
строке, владелец не угадывается. Тексты модулей инструмент не читает. Пустой
ответ напоминает, что строковые вызовы через `Выполнить`,
`ОписаниеОповещения` и `ПодключитьОбработчикОжидания` не индексируются.
## Чего провайдер не знает
- Поиск словами идёт только по экспортным процедурам. Неэкспортную можно
найти по точному имени или через оглавление известного модуля.
- Динамические вызовы, где имя передаётся строкой в `Выполнить`,
`ОписаниеОповещения` или `ПодключитьОбработчикОжидания`, не индексируются.
- У скомпилированного общего модуля нет доступного исходника: виден сам адрес,
но не процедуры, вызовы, сигнатуры и тела внутри него.
- В контейнерной структуре `Form.bin`/`.Form`, включая плоскую выгрузку, пока
доказаны скобочная
грамматика и корневые маркеры `19`, `20`, `23`, `25`, `26`, `27`, но не
назначение позиционных полей. Поэтому такая форма видна как частично
прочитанная, а реквизиты, элементы и события из неё не угадываются.
Полный состав и привязки по-прежнему доступны только из `Form.xml`;
XML-дескриптор отдельно даёт UUID, имя, синоним и тип формы.
- Одноимённый вызов, который не разрешён до точного модуля, не
приписывается запрошенной процедуре и показывается отдельно.
Одно имя может жить в двух доменах сразу: `СтрНайти` есть и в платформе
(с 8.3.6), и в языке запросов. Тогда `get_syntax` перечисляет одноимённые с
готовым адресом каждого, и повторить вызов можно строкой из выдачи:
```
get_syntax("СтрНайти") → Одноимённых элементов: 2
- `Глобальный контекст.СтрНайти` — Метод, с 8.3.6
- `Запрос.СтрНайти` — Функция запроса
get_syntax("Запрос.СтрНайти") → карточка функции языка запросов
```
Квалификатор `Запрос.` нужен потому, что у элемента языка запросов нет
владельца: назвать его через `Объект.Член`, как платформенный, невозможно.
## Порядок вызовов — и что теряется, если его нарушить
```
list_configurations
├─ search_objects → get_object → get_related
├─ search_procedures → get_procedure → get_callers
└─ search_syntax → get_syntax
```
В цепочке кода каждый шаг добавляет сведения, которых нет у предыдущего.
Без `search_procedures` нужно заранее знать точный адрес и теряется поиск по
назначению и области. Без `get_procedure` не показаны сигнатура, контекст
компиляции, тело процедуры и точная семантика расширения. Без `get_callers`
нет проверки прямых
последствий правки: подтверждённых мест вызова, привязок метаданных и событий
формы.
**Шаг `get_object` пропускать нельзя.** Поиск отдаёт только имена и счётчики;
всё, от чего зависит код, живёт в карточке объекта:
- **вид и периодичность регистра.** `СрезПоследних` есть только у
периодического регистра сведений, а непериодических **566 из 603**;
- **готовые имена полей виртуальных таблиц.** В запросе ресурс `Количество`
называется `КоличествоОстаток`, `КоличествоОборот`, `КоличествоПриход` — в
конфигураторе таких имён не видно нигде, их порождает платформа;
- **предел субконто, корреспонденция, ресурсы графика** — без них поля вида
`СубконтоДт1` назвать нечем;
- **строки неограниченной длины.** Помечены прямо в типе:
`Строка (неогр. — только через ПОДСТРОКА)` против `Строка(200)`. Такое поле
нельзя ставить в запрос как есть — платформа не даст его ни сравнить, ни
сгруппировать, ни упорядочить. Таких полей от 23% до 38% строковых в живых
конфигурациях, поэтому на карточках, где они встречаются (2 474 из 20 522),
**перед списком полей** печатается оговорка с рецептом.
Запрос, написанный сразу после `search_objects`, выглядит правильным и падает
на «поле не найдено». Пример того, что приходит только из `get_object`:
```
## Таблицы запроса
- `РегистрНакопления.ТоварыНаСкладах.Остатки`
измерения: Склад, Номенклатура, Характеристика
ресурсы: КоличествоОстаток, РезервОстаток
```
Второй такой же — и он найден живым промахом 2026-08-18. Агент сгруппировал
запрос по строке без ограничения длины; данные мы отдали верные, но разница
читалась только по отсутствию числа в скобках:
```
> **Строки неограниченной длины** помечены `(неогр.)`. Платформа не даёт их
> сравнивать, группировать и упорядочивать и не пускает в РАЗЛИЧНЫЕ,
> ОБЪЕДИНИТЬ и агрегатные КОЛИЧЕСТВО, МИНИМУМ, МАКСИМУМ. Ограничивайте
> длину — одинаково в списке выборки и в группировке:
>
> ПОДСТРОКА(КодСкидки, 1, 100) КАК КодСкидки
>
> Длину подбирайте по смыслу поля: 100 — не универсальное число.
## Реквизиты
- `КодСкидки` — Строка (неогр. — только через ПОДСТРОКА) // Код скидки
- `КодМаркировки` — Строка(200) // Код маркировки
```
**Рецепт стоит и в оговорке, и в самой строке поля, и это не избыточность.**
Первая редакция печатала оговорку последним абзацем карточки. Живой агент
2026-08-18 вызвал `get_object` с `detail=fields`, получил её целиком — и всё
равно сгруппировал по такому полю. Оговорка стояла на 721 токен позже строки
поля, а решение принимается там, где имя копируют. Тот же урок уже был записан
на описаниях инструментов: правило работает там, где его читают, а не там, где
его аккуратнее положить.
Каждый запрет проверен: агрегатные — цитата из справки, остальные пять —
прогоны на живой базе с записанными текстами ошибок. Справка знает об
ограничении только в трёх агрегатных функциях из шести и молчит про
группировку, упорядочивание, `РАЗЛИЧНЫЕ`, `ОБЪЕДИНИТЬ` и сравнение — то есть
агент, честно её прочитавший, узнать об этом не мог. Разбивка по
происхождению — в [docs/data-sources.md](docs/data-sources.md), раздел
«Оговорки в карточке».
**Перед вызовом функции платформы на старой конфигурации — `get_syntax`.**
Недоступное помечается, и там же лежит рецепт замены, если он записан.
## Источники независимы
Их пять, и каждый подключается отдельно:
| Источник | Файл | Что даёт | Без него |
|---|---|---|---|
| Метаданные конфигурации | `СтруктураКонфигурации_*.zip` | объекты, реквизиты, связи, движения | `search_objects` и `get_object` не отвечают |
| Код конфигурации | архив выгрузки конфигурации в файлы | процедуры основной конфигурации, сигнатуры, тела, контекст компиляции, места вызовов, события форм | без кода конфигурации `search_procedures`, `get_procedure` и `get_callers` называют недостающий источник |
| Код расширения | архив выгрузки расширения в файлы | собственные и изменённые процедуры выбранного расширения, их тела и места вызовов | без кода расширения основная конфигурация доступна, но `search_procedures`, `get_procedure` и `get_callers` не видят код и перекрытия этого расширения |
| Справка платформы | `shcntx_ru.hbk` | методы, свойства, сигнатуры, доступность, версии | `search_syntax` говорит «источник не подключён» |
| Язык запросов | `shquery_ru.hbk` | `ВЫБРАТЬ`, `ЛЕВОЕ СОЕДИНЕНИЕ`, `ИТОГИ ПО`, `РАЗНОСТЬДАТ` | конструкции языка запросов не находятся |
| Что загружено | Что работает |
|---|---|
| Все необходимые источники | всё |
| Только конфигурация | метаданные; синтаксис отвечает «источник не подключён» |
| **Только справка** | синтаксис без фильтрации по версии, `config` не нужен |
| Ничего | `list_configurations` объясняет, что загрузить |
## Язык запросов — отдельный источник
`shquery_ru.hbk` из того же каталога установки платформы. 127 страниц:
52 функции, 67 ключевых слов, 8 статей. Загружается как обычный источник и
попадает в **тот же поисковый индекс**, что справка платформы, — искать
отдельным инструментом не нужно, `search_syntax` находит и то и другое.
**Версий в самом файле нет** — проверено на всех 129 страницах: ноль
упоминаний «8.3.x» и «начиная с версии». Но язык запросов **меняется**: релиз
8.3.20 добавил 25 функций, среди них `СтрНайти`, `Лев`, `Прав`, `ВРег`, `НРег`,
`СтрЗаменить`, `Окр`, `Цел` и вся тригонометрия.
Взять версию неоткуда: справка платформы функций языка запросов не описывает
вовсе (`ПОДСТРОКА` — ноль совпадений на 25 511 элементов). Поэтому версии
задаёт курируемая таблица `query_versions.py` — по списку 1С «Функции,
добавленные в язык запросов начиная с релиза 8.3.20». Остальным 27 функциям
версия не приписывается: они были всегда.
Дальше работает обычный фильтр: конфигурация на 8.3.5 этих функций не увидит,
на 8.3.23 увидит.
Таблица проверяется данными — сравнением двух справок разных платформ. Чего нет
в старой и есть в новой, то появилось между ними, и у этого обязана стоять
версия:
```bash
python3 tools/lab/compare_query_help.py <старая.hbk> <новая.hbk>
```
Прогон 2026-08-19, 8.3.5.1570 против текущей: появилось 29, покрыто 29, ложных
срабатываний 0. Ложное срабатывание — худшая из ошибок: элемент, который был
уже в старой справке, но помечен версией, спрячется от конфигурации, где он
есть.
Экземпляр один на сервер: повторная загрузка заменяет прежний.
**Таблицы страниц показываются, но не ищутся.** В этой справке ячейки таблиц
размечены абзацами внутри `<TD>`, и без отдельного разбора карточка печатала
таблицу столбцом значений: «Товар / Количество / Номер / Сантехника / 104 / …»
два десятка строк подряд. Теперь таблицы разбираются отдельным полем — 51
таблица на 31 странице из 127 — и печатаются на своих местах в тексте:
страница с двумя примерами показывает каждый результат под своим примером.
В поисковый индекс содержимое таблиц не попадает.
Таблицы в этой справке двух разных природ, и разбираются они по-разному:
| Что | Сколько | Как выглядит в карточке |
|---|---|---|
| таблица данных — результат запроса-примера | 51 на 31 странице | markdown-таблицей |
| рисованная синтаксическая диаграмма — грамматика конструкции | 21 на 17 страницах | лесенкой с отступом по уровню ветвления |
Различаются по разметке, а не по классу CSS: `class=SimplyTable` стоит не на
всех — 7 настоящих таблиц идут без него. Признак — геометрия: у таблицы данных
все строки одной ширины, у диаграммы ширины рваные и есть ячейки из одной
вертикальной черты (это нарисованная линия, а не значение).
**Испорченная разметка называется вслух.** Страница с незакрытой `<TABLE>`
разбирается без таблиц, но не теряется, и её имя попадает в предупреждения
источника: строкой в выводе загрузки (`mcp1c.cli reg-add`) и
отдельной строкой на странице «Источники» дашборда. Молча отдать карточку
беднее обычного нельзя: от справки, в которой этого просто нет, такое
неотличимо.
Половина имён совпадает с именами платформы (57 из 127) — `ГОД`, `МЕСЯЦ`,
`ПРЕДСТАВЛЕНИЕ` есть и там и там. Чтобы вопрос про запрос не уводил к методу
платформы, обороты вроде «в запросе», «в тексте запроса», «в выборке» дают
элементам языка запросов мягкий подъём. Мягкий намеренно: при уверенном
отрыве платформенный элемент остаётся первым — «как задать параметр в
запросе» может быть и про `Запрос.УстановитьПараметр`.
**Параметр `config` обязателен, если загружено больше одной конфигурации.**
По умолчанию ничего не подставляется: молчаливый выбор приводит к тому, что
агент пишет код по чужой конфигурации, и этого никто не замечает.
## Ответ зависит от версии платформы
Один и тот же вызов, две конфигурации:
```
get_syntax("СтрШаблон", config="Розница") → 8.3.23
# Метод: Глобальный контекст.СтрШаблон
с версии платформы 8.3.6
Доступность: ТонкийКлиент, ВебКлиент, Сервер, ТолстыйКлиент, …
get_syntax("СтрШаблон", config="Отраслевая") → 8.3.5
# `Глобальный контекст.СтрШаблон` недоступен в этой конфигурации
Элемент существует, но появился в 8.3.6, а конфигурация работает на 8.3.5.1570.
Использовать нельзя — код не скомпилируется.
```
Для платформы 8.3.5 из выдачи убрано 6 539 элементов, для 8.3.23 — 874. Не
предупреждением, а фильтрацией: предупреждение агент пропустит, отсутствующий
в выдаче метод — нет.
Поле **Доступность** (сервер / тонкий клиент / веб-клиент / мобильные) читать
обязательно: вызов серверного метода из клиентского контекста не компилируется.
## Справки нескольких версий сливаются в один индекс
Одна свежая справка на старой конфигурации **врёт**. Замерено на 8.3.5:
199 элементов сервер объявил бы несуществующими, 117 отдал бы с чужой
сигнатурой (`ЗаписьXML.ОткрытьФайл` на 8.3.5 берёт два параметра, в 8.3.27 —
три), 410 — с чужой доступностью. Всё это ошибки компиляции, а не неточности.
Поэтому справки разных версий кладутся рядом и сливаются в один индекс с
границами `since` и `until`, а ответ собирается под версию конкретной
конфигурации. **Справок нужно столько, сколько платформ у загруженных
конфигураций** — две крайние промежуточные не заменяют.
Цена измерена и мала: слияние трёх версий даёт 25 691 ключ против 24 777 у
одной, то есть меньше процента. Отдельный контейнер на версию тоже работает и
остаётся аварийным путём, но как основной проигран по цифрам — 300–450 МБ и
свой адрес на каждую версию.
Сервер сам называет, каких справок не хватает и какие лишние, — в выдаче
`list_configurations`.
## Замена вместо запрета
Сказать «функции нет» — половина ответа. Вторая половина — чем её заменить, и
из справки она не выводится: пометка об устаревании стоит на 15 страницах из
25 тысяч.
Поэтому есть таблица замен (`replacements.py`), сейчас 6 записей — строковые
функции, появившиеся в 8.3.6. Вместо запрета `get_syntax` отдаёт рецепт:
```
get_syntax("СтрРазделить", config="Отраслевая") → 8.3.5
# `СтрРазделить` недоступна: появилась в 8.3.6
Замена: РазложитьСтрокуВМассивПодстрок(<Строка>, <Разделитель>)
Оговорка: разделитель у `СтрРазделить` — набор символов, каждый из которых
самостоятельный разделитель; у замены это одна строка целиком.
```
**Оговорка обязательна.** Замена почти никогда не эквивалентна, и молча
подсунуть похожую функцию — хуже, чем не подсказать ничего.
Таблица пополняется по живым случаям, а не вслепую: сочинять обходы для
функций, которых никто не спрашивал, смысла нет.
---
# 4. Управление данными
## Добавить источник
```bash
# в Docker
docker compose exec mcp1c python -m mcp1c.cli reg-add /data/bootstrap/Выгрузка.zip --data /data
docker compose exec mcp1c python -m mcp1c.cli reg-add /data/bootstrap/shcntx_ru.hbk --data /data
# без Docker
PYTHONPATH=src python3 -m mcp1c.cli reg-add Выгрузка.zip
```
Проще: положить файл в `data/bootstrap/` — он подхватится при следующем старте.
## Выгрузка конфигурации в файлы
Второй вид источника — не метаданные (`СтруктураКонфигурации_*.zip`), а сама
выгрузка конфигурации в файлы: код модулей и формы. Сервер строит и кэширует
внутренние индексы процедур, вызовов и форм; `search_procedures` ищет по
готовому комплекту. При промахе кэша индексы строятся фоном: инструменты,
не зависящие от кода, продолжают отвечать, а инструменты кода показывают этап X/4 и
фактически обработанные N из M элементов текущего этапа. Готовый комплект
публикуется целиком, без промежуточного состояния из разных версий.
Фоновая сборка не перезаписывает `registry.json`, пока startup ещё
восстанавливает следующие источники. Если startup аварийно оборвался,
предыдущий полный снимок на диске остаётся целиком; частично восстановленный
список источников поверх него не сохраняется.
Четыре расходных файла кэша сохраняют безопасные локаторы, агрегаты покрытия
и первые 20 обезличенных проблем с точным числом остальных. Тела модулей,
сигнатуры процедур и сырая запись формы туда не попадают. При warm-старте
проверяются sha256 источника, версия отбора, поколение локаторов, внутренние
инварианты каждого индекса и их согласованность с одним каталогом. Поэтому
структурно или семантически повреждённый комплект считается промахом целиком,
а не источником частично старого ответа. Повреждённый или read-only кэш не
роняет источник: сервер выполняет cold-пересборку в памяти и публикует полную
диагностику, даже если новый кэш нельзя записать. Опубликованные cold- и
warm-ответы совпадают по всем счётчикам и категориям причин.
Локальные причины каждой формы хранятся отдельно от ограниченного списка:
благодаря этому
`get_object` после рестарта показывает все проблемы форм выбранного объекта,
не превращая общий `/sources` в безлимитный ответ. Reader не читает файл кэша
больше 256 МиБ и не распаковывает payload больше 512 МиБ; превышение любого
предела — обычный cache miss до `marshal.loads`, а не попытка исчерпать память.
Финальная живая приёмка selection v4 выполнена 2026-08-21 на четырёх
обезличенных источниках кода (три основные выгрузки и одно расширение):
```bash
PYTHONPATH=src .venv/bin/python \
tools/lab/measure_modules_acceptance.py --data data --timeout 120
```
Скрипт печатает один JSON без имён и путей. Warm-подъём занял 3,676 с,
`startup_problems_total = 0`, пик процесса — 856,2 МиБ. Каталог доказал
8 650 иерархических `.bsl`, 828 модулей из `Form.bin`, 1 523 плоских `.txt`,
1 079 модулей из `.Form` и 7 скомпилированных модулей без исходника.
Обнаружено 6 150 форм: 3 331 полностью, 1 330 частично и 1 489 не прочитаны.
Точные категории ограничений: `descriptor_only=671`,
`invalid_syntax=1598`, `known_marker_semantics_incomplete=659`,
`form_structure_missing=561`, `compiled_without_source=7`; неизвестных
маркеров, превышений бюджета, битых контейнеров и конфликтов в этом корпусе
нет. Ноль здесь — результат измерения именно этих источников, а не обещание
поддержки любого будущего формата.
Schema v2 того же скрипта дополнительно измеряет разрешение вызовов и цену
чтения тела формы тем же production-reader, которым пользуется карточка
процедуры. Повторный прогон 2026-08-24 на двух доступных обезличенных корпусах
дал 97 289 разрешённых BSL-рёбер из 178 216 (54,59%). Привязки подписок и
регламентных заданий разрешились в 194 случаях из 203 (95,57%), но из-за
малого числа подняли объединённую долю только до 54,64%. Восемь непривязанных
рёбер ведут в скомпилированные модули без исходника, одно — в модуль без такой
процедуры.
Все 1 079 читаемых записей `module` плоских `.Form` открылись без отказа. В
четырёх последовательных процессах повторное чтение показало медиану
0,052–0,067 мс, p95 0,271–0,346 мс и p99 0,454–0,764 мс; единичный максимум
27,054 мс не меняет распределение. Первый проход не называется cold, потому
что скрипт не управляет файловым кэшем ОС. По этим цифрам извлекать тела форм
в отдельные файлы при приёме не требуется: текущий локатор и чтение по запросу
остаются достаточными.
Предыдущий замер фоновой отзывчивости 2026-08-21 на холодном кэше трёх
источников кода: за 72,12 с
сделано 73 секундных вызова каждого пути, отказов нет. Этот таймер начинается
после готовности `/health`, тогда как контейнерный замер 125 с ниже идёт от
запуска контейнера до `ready` всех источников кода. `/health` отвечал с
медианой 5,76 мс и худшим временем 171,01 мс; настоящий `search_objects` —
8,11 и 172,49 мс соответственно. Контейнер достиг `healthy`, все 12 файлов
кэша опубликованы. Для повтора смонтируйте в отдельный контейнер копию
`data/` без `index/cache/*.modules-*` и запустите:
```bash
.venv/bin/python tools/lab/measure_background_responsiveness.py \
http://127.0.0.1:5002 /путь/к/копии/data
```
Память финального образа повторно измерена 2026-08-21 после selection v4:
четыре источника кода и 16 файлов кэша модульных индексов. Холодный подъём
после удаления только расходного кэша занял 111,216 с, тёплый перезапуск того
же контейнера с неизменными `dev`, `ino`, размером и `mtime_ns` всех 16 файлов
— 11,495 с.
| Состояние | `memory.peak` cgroup | `memory.current` после `ready` | RSS PID 1 | HWM PID 1 | `docker stats` |
|---|---:|---:|---:|---:|---:|
| холодная пересборка | 1 404 649 472 Б (1 339,6 МиБ) | 1 400 582 144 Б (1 335,7 МиБ) | 762 180 КиБ (744,3 МиБ) | 764 228 КиБ (746,3 МиБ) | 728,3 МиБ |
| тёплый старт | 612 851 712 Б (584,5 МиБ) | 599 142 400 Б (571,4 МиБ) | 570 180 КиБ (556,8 МиБ) | 570 180 КиБ (556,8 МиБ) | 533,2 МиБ |
Оба раза контейнер достиг `healthy`; `restart_count = 0`, `oom_killed = false`.
В каждом из двух финальных прогонов было ровно 0 строк с каждым из терминов
`traceback`, `exception`, `critical` и `error`.
Значения cgroup включают кэш страниц ядра, поэтому они закономерно отличаются
от RSS процесса и показания `docker stats`. Это ориентир размера именно этого
корпуса, а не основание назначать `mem_limit`.
Скрипт печатает один JSON только с агрегатами: длительностью, количеством
источников и кэшей, полями cgroup и PID 1, `docker stats`, здоровьем,
перезапусками, OOM и счётчиками терминов журнала. Имена источников, локальные
пути и строки журнала не печатаются. В режиме `cold` он собирает финальный
образ, предварительно останавливает контейнер и удаляет **только точные** файлы
`modules-toc`, `modules-calls`, `modules-forms` и `modules-search`, вычисленные
рабочей функцией имени для текущих источников. Каталог `index/cache` и сами
цели не могут быть символическими ссылками; лишний или смешанный набор имён не
считается готовым. После остановки каталог открывается с `O_NOFOLLOW`, а его
`dev`/`ino` сверяются с первоначально проверенным каталогом. Цели перечисляются
и проверяются через `dir_fd`, удаляются только точные базовые имена через тот
же дескриптор: подмена родителя на символическую ссылку не ведёт наружу, а
жёсткая ссылка теряет только внутреннюю ссылку. Затем скрипт делает
`--force-recreate`. Сам измеритель не удаляет и не заменяет источники,
распакованный код или `registry.json`. Запущенный сервер штатно атомарно обновляет
состояния `registry.json` во время старта и фоновой сборки; замер
учитывает эти записи в маркере поколения. Готовность требует не
только `healthy`, но и неизменившиеся `sha256`, `loaded_at` и привязку самого
источника кода, тот же `sha256` конфигурации-владельца, состояние `ready` и
точный набор четырёх кэшей на источник. При обычном рестарте метаданные
конфигурации разбираются снова и получают новый runtime-`loaded_at`; измеритель
разрешает только этот переход, а затем требует два одинаковых снимка нового
поколения. После чтения
памяти, `docker stats` и журнала финальная проверка читает реестр, затем кэш и
снова реестр; оба маркера должны совпасть с готовым поколением. Маркер включает
также `dev`, `ino`, размер и `mtime_ns` самого `registry.json`, поэтому даже
запись тех же байтов считается новым поколением. Режим `warm` до остановки
запоминает `dev`, `ino`, размер и `mtime_ns` каждого точного файла кэша. Сразу
после `docker stop`, ещё до `docker start`, он повторно открывает каталог с
`O_NOFOLLOW`, сверяет его `dev`/`ino` и весь снимок файлов через удерживаемый
`dir_fd`; тот же снимок требуется при готовности и в финальной проверке.
Пересборка, перезапись или порча файла отменяет весь результат. `--timeout` и
`--poll-interval` — конечные
положительные секунды; единый остаток `timeout` ограничивает каждую
Docker-команду.
```bash
.venv/bin/python tools/lab/measure_container_memory.py --mode cold --data data \
--timeout 300 --poll-interval 0.5
.venv/bin/python tools/lab/measure_container_memory.py --mode warm --data data \
--timeout 300 --poll-interval 0.5
```
**Выгрузка расширения кладётся туда же, в `data/incoming/`, и разбирается той
же кнопкой.** Вид выгрузки сервер определяет сам, по `Configuration.xml`
внутри архива — человек по-прежнему выбирает только конфигурацию, к которой
расширение прикрепляется, а не отдельную кнопку или поле «это расширение».
Расширение заводится **отдельным источником** `<ИмяКонфигурации>:ext:<ИмяРасширения>`
(вид `extension`), а не `<ИмяКонфигурации>:modules`: имя расширения берётся из
тега `Name` его собственной выгрузки, а не придумывается человеком. Код ложится
в свой каталог `data/extensions/<ИмяКонфигурации>/<ИмяРасширения>/` — рядом с
`data/modules/<ИмяКонфигурации>/`, но не внутри него, поэтому вторая выгрузка
не стирает первую. Расширений у одной конфигурации может быть сколько угодно —
модулей конфигурации при этом ровно одна выгрузка, как и раньше.
**Личность расширения — тег `Name` внутри его выгрузки, а не имя файла и не
содержимое целиком.** Человек может переименовать архив как угодно: два файла
с разными именами, но одинаковым `Name`, переразбирают ОДИН и тот же источник
— второй разбор просто обновляет код и `origin` (имя последнего разобранного
файла), ключ и каталог не меняются. Обратная сторона того же правила: если
`Name` у двух РАЗНЫХ расширений после очистки от недопустимых для пути
символов совпадёт (например, `Прайс/Розница` и `Прайс:Розница` дают одно и то
же имя каталога), они тоже схлопнутся в один источник — второй разбор
перепишет код первого. Для обычных имён (без символов вне букв, цифр, дефиса,
подчёркивания, точки и пробела) очистка ничего не меняет и такого совпадения
не бывает.
**Распознавание — положительное правило с обеих сторон, а порядок проверки
важен.** Ошибиться в пользу конфигурации опаснее: значит пойти в ветку
модулей и снести уже разобранный код конфигурации целиком. Поэтому сначала
смотрят сильные признаки расширения (`ObjectBelonging`,
`ConfigurationExtensionPurpose`) — виден хоть один, в ветку модулей хода нет
уже ни при каком `CompatibilityMode`: дальше решают только между
«расширение» (сошлись все четыре условия: оба сильных признака, непустой
`NamePrefix`, отсутствие `CompatibilityMode`) и отказом. И только когда ни
одного сильного признака расширения нет вовсе, решает `CompatibilityMode`:
непустой — конфигурация, пустой или тега нет — отказ. Всё остальное — тоже
отказ с объяснением, ничего не тронув на диске: `Configuration.xml` не
нашёлся (ищется не только в корне архива, но и в единственном каталоге
верхнего уровня — так собирают архив командой `zip -r архив.zip папка`, и
служебный `__MACOSX/` с `.DS_Store` архива Finder на macOS этому не мешают),
не читается (битый CRC, оборванная запись), не разбирается как XML, не
несёт узнаваемой структуры, или заявленный размер файла неправдоподобно
велик.
**Куда класть.** В `data/incoming/`, на том, а не через форму загрузки на
«Источниках» — у неё для этого нет поля. Каталог создаёт сам сервер при
старте, так что он на месте с первого запуска. Дальше на странице «Источники»
(только вошедшему под `ADMIN_TOKEN`) появляется блок «Входящие выгрузки» с
кнопкой «разобрать».
**Сканируется только сам каталог, без вложенных подкаталогов.** Архив,
положенный в `data/incoming/Розница/`, сервер не увидит — и не скажет об этом
ничего, потому что показывать ему будет нечего.
**Пока файл копируется, кнопки нет.** `cp` полутора гигабайт идёт минуты, а
файл виден в каталоге с первой секунды: у архива, изменявшегося меньше пяти
секунд назад, состояние показывается с пометкой «файл ещё копируется», sha256
не считается вовсе (он всё равно устареет), и разбор такого файла отклоняется
с объяснением. Дождитесь конца копирования и обновите страницу.
**Повторный разбор перетирает каталог целиком — но только после успеха.**
У обновлённой выгрузки кнопка подписана «переразобрать»: старое содержимое
`data/modules/<ИмяКонфигурации>/` (или каталога расширения) заменяется новым
целиком, иначе файлы, которых в новой выгрузке нет (удалённый объект,
переименованный модуль), остались бы там навсегда и две выгрузки
смешались бы. Распаковка при этом идёт во временный каталог рядом и не
задевает прежний разбор: если `extract` упадёт на середине (битый CRC у
модуля, кончившееся место, права) или отберёт ноль файлов, до записи в
рабочий каталог дело не доходит вовсе — реестр и диск не расходятся. Замена
— не снос старого на месте, а рокировка («переименовать старое в сторону →
переименовать новое на его место → снести отставленное»): если переезд
нового на место старого не удался, сервер пробует вернуть старое туда, где
оно было. **Если не удаётся и это** (природа отказа у обоих переименований
обычно одна — права, тот же bind-mount), разбор отказывает явно и называет
в тексте оба пути: где физически лежит прежний разбор и где — пустой или
частично распакованный новый, чтобы можно было вернуть каталог руками;
молча показывать «разобрано» по каталогу, которого нет, сервер не станет.
Огрызки от прерванного посередине процесса (`.tmp-*`) убираются перед
следующим разбором той же конфигурации или расширения — сохранённая копия
прежнего разбора (`.old-*`), если до такого дошло, сама не удаляется:
решение о ней за человеком. Права каталога переразбор не меняет — новый
каталог получает права прежнего (или обычные, если каталога ещё не было), а
не урезанные, с которыми создаётся временный.
Одновременные разборы одного источника выполняются последовательно: новая
заявка сразу отменяет прежнее поколение, но ждёт завершения его работы с
временным каталогом. Поэтому один разбор не может удалить чужой активный
`.tmp-*`, а рабочий каталог и кэш получает только последнее поколение.
Отменённая заявка завершается явной ошибкой; ожидающая устаревшая заявка не
начинает распаковку вовсе. Разные конфигурации и расширения с разными ключами
источника друг друга не задерживают.
**«Отбор устарел» — состояние на случай, когда меняется не файл, а
правило.** Каждый разобранный источник помнит версию правила отбора
(внутренняя `SELECTION_VERSION`: что берём из архива и куда кладём). Если
код на диске разобран старой версией правила, источник показывается как
«отбор устарел», а не «разобрано» — хотя архив на `incoming/` тот же самый
файл, с тем же sha256. Кнопка подписана «переразобрать», как у обновлённой
выгрузки: код на диске перетирается заново, уже по нынешнему правилу.
Прежний разбор сам по себе не переписывается — правку правила отбора
человек видит только после нажатия кнопки. Источник, разобранный до
появления этого поля (запись без `selection_version` в старом
`registry.json`), тоже считается устаревшим: версия у него неизвестна, а не
«наверняка свежая» — иначе человек никогда не увидел бы «переразобрать» для
кода, который никогда не проходил через нынешнее правило.
Текущая версия 4 сохраняет XML-дескрипторы форм и иерархические
`Ext/Form.bin`; источник версии 3 поэтому обязательно показывается
устаревшим и получает эти файлы только после явного переразбора.
Версия отбора входит в штамп кэша: старый кэш не принимается даже для того
же неизменившегося ZIP. Перед заменой каталога сервер пишет короткий журнал
рокировки; после аварийной остановки следующий запуск по `registry.json`
либо завершает новое поколение, либо возвращает прежний каталог.
**Мусор архиватора Finder на macOS в код не попадает.** Архив, собранный
командой «Сжать объекты», несёт служебный `__MACOSX/` с копиями ресурсных
вилок каждого файла (`._Имя`, тот же суффикс, что у оригинала) — отбор их
пропускает, как и любое имя, начинающееся на `._`, где бы оно ни лежало.
**Обёртка архива не воспроизводится в пути на диске.** Архив, упакованный
командой `zip -r архив.zip папка` или через Finder («Сжать объекты») —
вся выгрузка лежит внутри одного каталога верхнего уровня, — при раскладке
на диск этот уровень не повторяется: код ложится как
`data/modules/<ИмяКонфигурации>/Catalogs/…`, а не
`data/modules/<ИмяКонфигурации>/папка/Catalogs/…`. Правило распознавания
обёртки общее с поиском `Configuration.xml` внутри неё (то же самое место
абзацем выше, «Распознавание»): служебный `__MACOSX/` и корневой
`.DS_Store` в счёт не идут, а несколько каталогов верхнего уровня обёрткой
не считаются — угадывать, какой из них «тот самый», сервер не станет.
Санитизация путей (отказ членам, ведущим наружу корня) применяется уже
после снятия обёртки, а не вместо неё.
**Удаление источника уносит и код.** Кнопка «удалить» у источника
`<ИмяКонфигурации>:modules` сносит `data/modules/<ИмяКонфигурации>/`, а у
источника расширения `<ИмяКонфигурации>:ext:<ИмяРасширения>` — только его
собственный каталог `data/extensions/<ИмяКонфигурации>/<ИмяРасширения>/`:
модули конфигурации и другие расширения той же конфигурации не задеваются.
Иначе сотни мегабайт остались бы занятыми невидимо: в разделе «Исходные
файлы» они не показываются, там только `data/sources/`.
**Архив, в котором не нашлось ни модулей, ни форм, отклоняется.** Выгрузка
структуры метаданных — тоже `.zip`, и без этой проверки она заводила бы
источник на ноль файлов с состоянием «разобрано». Её подают формой
«Загрузить» на этой же странице.
**Почему не через браузер.** Архив весит гигабайт с лишним — у типовой
конфигурации розничных продаж 2.3.10.5 это 1 380 МБ. Форма загрузки на
«Источниках» построена под метаданные в единицы мегабайт и на таком файле не
работает сразу по трём причинам: предел загрузки 500 МБ; тройное копирование
с пиком около 4 ГБ — буфер приёма тела запроса, временная копия во
`mkdtemp()` и копия на сам том; и сама передача больше гигабайта по HTTP без
возможности продолжить после обрыва. Файл на томе все три ограничения
обходит: копируется обычным `cp`, а сервер читает архив членом за членом, не
разворачивая его целиком ни разу.
**Что остаётся на диске.** Не весь архив — только модули (`.bsl` у
иерархической выгрузки, `.txt` и канонические скомпилированные
`CommonModules/<Имя>.Module` или `CommonModule.<Имя>.Module` у плоской) и
формы: XML-дескриптор, `Form.xml` и `Form.bin` у иерархической,
контейнер `.Form` **целиком** у плоской. `Form.bin` и `.Form` сохраняются
без изменений как источник для провайдера кода; производный `.bsl` при
приёме не создаётся. Текстовые макеты
`*.Template.txt` не принимаются за модули. Контейнер сохраняется как есть,
вместе с двоичной записью формы: код обычной формы лежит внутри него записью
`module`, и разбирать контейнер на приёме означало бы тащить сюда разбор
формата 1С.
После распаковки физическое дерево перечисляется один раз в неизменяемый
каталог канонических адресов и безопасных локаторов. Оглавление, вызовы, формы
и поиск получают один и тот же снимок: обычные `.bsl`/`.txt` читаются как
файлы, код `Form.bin`/`.Form` — как запись `module` исходного контейнера,
скомпилированный `.Module` остаётся адресом без придуманного тела. Одинаковые
тексты одного адреса после общей нормализации дедуплицируются; разные не
получают молчаливого приоритета и исключаются как конфликт.
Код конфигурации ложится в `data/modules/<ИмяКонфигурации>/`,
код расширения — в свой каталог `data/extensions/<ИмяКонфигурации>/<ИмяРасширения>/`;
актуальные журналы покрытия — в `data/logs/`. Все три вида защищены правилами
`**/modules/`, `**/extensions/` и `**/logs/` в `.gitignore`. Замер
`python3 tools/lab/measure_intake.py <архив>...`, прогон 2026-08-19:
| Что | Архив | Развёрнуто | Отбираем |
|---|---|---|---|
| типовая конфигурация розничных продаж 2.3.10.5 | 1 380 МБ | 2 063 МБ, 33 188 файлов | **351,4 МБ, 11 072 файла** |
| расширение к ней | 1 МБ | 8 МБ, 453 файла | 6,9 МБ, 155 файлов |
Живой прогон самого приёма на тех же файлах — не оценка по каталогу zip, а
реальный вызов `intake.planned_size`, `intake.enough_space` и
`intake.extract`, замер `python3 tools/lab/measure_intake_run.py <каталог для
распаковки> <архив>...`, прогон 2026-08-19: оценка требуемого места на архиве
в 1,4 ГБ — **0,15 с** (читается только центральный каталог zip, тело не
трогается вовсе), распаковка — **3,1 с**, пик RSS процесса — **111 МБ**.
Файлов и байт, которые вернул `extract()`, и того, что фактически легло на
диск, — поровну, скрипт сверяет это сам.
**Исходник сервер не удаляет.** Каталог `data/incoming/` серверу удалять
запрещено — файл остаётся на месте и после успешного разбора; повторный
разбор того же архива его не трогает, сверка идёт по sha256 с кэшем на
странице (считать хеш гигабайта при каждом обновлении списка нельзя).
**Источник заводится под ключом `<ИмяКонфигурации>:modules`**, а не именем
конфигурации: тот же ключ, что у метаданных, при загрузке вытеснил бы их из
реестра. Какая конфигурация — хозяйка выгрузки, решает человек. Если в
реестре загружена ровно одна, выбор не нужен: поля рядом с кнопкой на
странице нет, разбор берёт единственную сам. Если загружено две и больше,
рядом с кнопкой появляется выпадающий список загруженных имён — какую
выбрали, к той код и привязывается. Имя из формы сверяется со списком
загруженных: неизвестное отклоняется до начала разбора, с объяснением.
Автоматическая привязка по манифесту выгрузки (без участия человека) пока не
сделана. Платформа источника модулей берётся у конфигурации, к которой он
привязан, — сама выгрузка в файлы точной сборки платформы не содержит,
только режим совместимости, а это другое число. У расширения выбор
конфигурации работает так же (см. выше «Выгрузка расширения кладётся туда
же»); вид выгрузки — модули или расширение — сервер различает сам по
`Configuration.xml`, отдельного поля или кнопки для этого на странице нет.
**Нехватка места — отказ до начала.** После выбора конфигурации и вида
источника реестр сверяет свободное место на томе до распаковки. Требуется
объём безопасно отобранных файлов плюс резерв индекса: 15% с округлением
вверх, но не меньше 25 МБ. Формула одинакова для первой загрузки и
переразбора: занятый старым корнем объём уже исключён из значения свободного
места, повторно прибавлять его нельзя. Не хватает — разбор не начинается, а
причина с обеими цифрами в мегабайтах остаётся на странице «Источники» и
переживает перезапуск. Прямой вызов реестра проходит ту же проверку; при
ошибке или заполнении тома прежний разбор остаётся на месте.
Скомпилированный `*.Module` виден в сводке и в перечне модулей как
`ОбщийМодуль.<Имя>`, но процедур и тела у него нет: `search_procedures`,
`get_procedure` и `get_callers` говорят об этом прямо и не выдают пустой
индекс за доказательство отсутствия процедуры.
**Кнопка «разобрать» требует `ADMIN_TOKEN`** — как любая другая запись: без
токена этого маршрута не существует, а не «он закрыт».
## Применить изменения без перезапуска
Работающий сервер держит реестр в памяти, поэтому после `reg-add` его нужно
подтолкнуть. Либо перезапуск (`docker compose restart mcp1c`, около 2 секунд),
либо админ-ручка:
```bash
# включается переменной ADMIN_TOKEN; без неё маршрут отключён
ADMIN_TOKEN=секрет docker compose up -d
curl -X POST -H "x-admin-token: секрет" http://localhost:5001/admin/reload
```
## Словарь: как говорят против того, как названо
Главная сложность поиска — разрыв между словами человека и именами в
конфигурации. «Заказ клиента» — а объект называется `ЗаказПокупателя`.
Словарь лежит в `data/dictionary.json`, правится без пересборки образа.
Два механизма, и они разные.
**Синонимы слов** — общие для всех конфигураций:
```bash
python3 -m mcp1c.cli dict-synonyms клиент покупатель заказчик
```
**Псевдонимы объектов** — прямое указание «когда я говорю так, я имею в виду
вот эти объекты», вес выше любого текстового совпадения. Два десятка типовых
фраз («файлы», «товары», «клиенты», «сотрудники», «задачи») встроены и
работают сразу; если объекта в конфигурации нет, псевдоним не применяется.
Свои добавляются с привязкой к конфигурации:
```bash
python3 -m mcp1c.cli dict-alias "справочник физлиц" \
Справочник.ФизическиеЛица Справочник.Пользователи \
--config РозницаДляКазахстана
```
```
«справочник физлиц»
Справочник.ФизическиеЛица псевдоним из словаря
Справочник.Пользователи псевдоним из словаря
```
Существование объектов проверяется при добавлении — псевдоним на опечатку
бесполезен. Посмотреть содержимое: `dict-show`, удалить: `dict-alias «фраза»
--remove`.
Изменения применяются перезапуском контейнера или `POST /admin/reload` —
пересобирать образ не нужно. Перезагрузка выполняется вне event loop, поэтому
`/health` и MCP-запросы продолжают обслуживаться во время восстановления.
**Поисковые ключи справки** — третий механизм, и правится он только в
коде (`search_keys.py`) с обычной проверкой изменений. Разрыв тут другой природы: человек не
называет конструкцию чужим словом, а **описывает задачу**. «Количество дней
между двумя датами» против `РАЗНОСТЬДАТ`, «убрать повторы» против
`РАЗЛИЧНЫЕ`, «перебрать строки результата запроса» против `Следующий` — общих
слов с именем мало или нет вовсе, и синоним не поможет, заменять нечего.
Поэтому к 116 страницам языка запросов из 127 и отдельным платформенным
методам приписаны формулировки, которыми их спрашивают, и попадают в поисковый
индекс отдельным полем. Два источника проверяют привязку независимо. В
рантайме ключи не весят ничего.
Живой промах «как перебрать строки в выборке результата запроса» 2026-08-25
перешёл с отсутствия в top-10 на первое место. На том же корпусе изменился
только он:
| Проверка | До | После |
|---|---:|---:|
| Целевой запрос | промах | 1-е место |
| Язык запросов, P@1 | 18/22 | 19/22 |
| Язык запросов, P@10 | 19/22 | 20/22 |
| Метаданные | 17/21 P@1 | 17/21 P@1 |
| Процедуры | 1/3 в top-10 | 1/3 в top-10 |
| Точные имена справки | 49 043/50 508 P@1 | 49 043/50 508 P@1 |
| Одноимённые элементы | 10 299/10 439 P@1 | 10 299/10 439 P@1 |
Оба артефакта снимаются командой из раздела «Замер качества поиска»: первый
раз — с `--save data/bench/2026-08-25-query-row-iteration-before.json`, второй
— с тем же набором ключей и дополнительно
`--baseline data/bench/2026-08-25-query-row-iteration-before.json --save
data/bench/2026-08-25-query-row-iteration-after.json`.
Ключи **сочинены нами, а не выгружены**, и отсюда три ограничения:
- живут отдельным слоем в git, а не приписываются разобранному элементу;
- **в ответ агенту не попадают** — ответ по-прежнему собирается только из
справки, ключи работают исключительно на попадание в нужную статью;
- привязаны к страницам по идентификатору, и если справка даст другой набор
страниц, расхождение называется при загрузке, а не проявляется молча
просевшим поиском.
Правило целиком — в [docs/data-sources.md](docs/data-sources.md), раздел
«Сгенерированные слои поверх источников».
## Посмотреть, что загружено
```bash
docker compose exec mcp1c python -m mcp1c.cli reg-list --data /data
```
```
РозницаДляКазахстана 2.3.10.5 платформа 8.3.23.1997
объектов 5637, связей 44034, загружено 2026-08-18T12:22:16+00:00
метаданные : да
синтаксис : справка 8.3.27, новее конфигурации, скрыто 874
модули : не загружен
язык запросов: подключён, 127 страниц
```
Конфигураций может не быть вовсе — сервер при этом работает, если загружена
хоть одна справка: отвечают `search_syntax` и `get_syntax`, `config` указывать
не нужно. `reg-list` в этом случае перечисляет подключённое и возвращает 0:
```
Конфигурации не загружены. Подключено:
язык запросов, 127 страниц
Работают search_syntax и get_syntax, без фильтра по версии.
```
На полностью пустом реестре — «Ничего не загружено.» и код возврата 1. Любая
команда, которой нужна конфигурация, там же скажет, чего именно не хватает и
чем каждое берётся. `reg-list` не ждёт холодную сборку: для основной
конфигурации и каждого расширения он показывает то же атомарное состояние,
что `/sources` и `list_configurations` — готовые счётчики, этап и прогресс,
ошибку либо отсутствие кода.
## Отладка без агента — `mcp1c.cli`
CLI ходит в тот же реестр и те же функции, что и инструменты MCP. Если он
отвечает верно — дело в настройке клиента, а не в сервере.
Команды делятся на три группы. **По реестру** — то же, что видит агент:
```bash
PYTHONPATH=src python3 -m mcp1c.cli reg-list [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-add Выгрузка.zip [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-add shcntx_ru.hbk [--data data]
PYTHONPATH=src python3 -m mcp1c.cli reg-search "чек ккм" --config РозницаДляКазахстана
PYTHONPATH=src python3 -m mcp1c.cli reg-search "разделить строку" --syntax --limit 5
PYTHONPATH=src python3 -m mcp1c.cli reg-search-procedures "провести документ" \
--config Пример --scope Документ.Чек --limit 10
PYTHONPATH=src python3 -m mcp1c.cli reg-get-procedure \
'Документ.Чек.МодульОбъекта::ОбработкаПроведения' \
--config Пример --start-line 0 --lines 200
PYTHONPATH=src python3 -m mcp1c.cli reg-get-callers \
'Документ.Чек.МодульОбъекта::ОбработкаПроведения' \
--config Пример --limit 20
```
`reg-search` без `--syntax` ищет по метаданным, с ним — по справке и языку
запросов. Три команды кода зеркалят одноимённые MCP-инструменты и печатают
тот же ответ: `reg-search-procedures QUERY` принимает `--config`,
`--extension`, `--scope`, `--limit` (по умолчанию 10);
`reg-get-procedure ADDRESS` — `--config`, `--extension`, `--start-line`
(0) и `--lines` (200); `reg-get-callers ADDRESS` — `--config`, `--extension`
и `--limit` (20). У всех трёх `--data` по умолчанию равен `data`.
При холодном одноразовом запуске эти команды ждут фоновую сборку не больше
90 секунд, чтобы процесс не завершил daemon-потоки до результата и записи
четырёх файлов кэша. Если границы не хватило, команда возвращает явную ошибку;
сервер этим ожиданием не пользуется и продолжает показывать прогресс.
**Прямо по файлу, без реестра** — посмотреть выгрузку до того, как она поедет
в сервер:
```bash
PYTHONPATH=src python3 -m mcp1c.cli info Выгрузка.zip
PYTHONPATH=src python3 -m mcp1c.cli stats Выгрузка.zip
PYTHONPATH=src python3 -m mcp1c.cli show Выгрузка.zip Документ.ЧекККМ --detail full
PYTHONPATH=src python3 -m mcp1c.cli related Выгрузка.zip Документ.ЧекККМ --depth 2
PYTHONPATH=src python3 -m mcp1c.cli find Выгрузка.zip реализация --limit 10
```
Путь — ZIP или распакованный каталог, формат определяется по манифесту.
**Словарь поиска** — синонимы общие, псевдонимы привязаны к конфигурации:
```bash
PYTHONPATH=src python3 -m mcp1c.cli dict-show # правила и их происхождение
PYTHONPATH=src python3 -m mcp1c.cli dict-show --all --config Розница...
PYTHONPATH=src python3 -m mcp1c.cli dict-synonyms чек ккм касса # группа взаимозаменяемых слов
PYTHONPATH=src python3 -m mcp1c.cli dict-synonyms чек ккм --remove
PYTHONPATH=src python3 -m mcp1c.cli dict-alias "справочник физлиц" Справочник.ФизическиеЛица
PYTHONPATH=src python3 -m mcp1c.cli dict-alias "справочник физлиц" --remove
```
`dict-show` показывает происхождение каждого правила — с этого начинается
разбор «почему поиск так себя ведёт».
## Замер качества поиска — `mcp1c.bench`
Отдельный стенд, потому что «стало лучше» без цифр — мнение.
```bash
PYTHONPATH=src .venv/bin/python -m mcp1c.bench \
--data data --config РозницаДляКазахстана \
--auto --sets query-language,roznica-metadata,modules-procedures \
--check-notes
```
| Ключ | Что делает |
|---|---|
| `--data путь` | каталог данных сервера, по умолчанию `data` |
| `--sets имя,имя` | ручные наборы schema v1 из `tests/queries/*.json`, без расширения |
| `--auto` | автоматические наборы по справке: точные имена и одноимённые |
| `--config` | конфигурация; обязательна, если загружено несколько |
| `--extension` | отдельный корпус кода расширения для наборов процедур; без ключа измеряется базовый код |
| `--limit` | глубина выдачи, по умолчанию 10 |
| `--save путь` | записать прогон для сравнения; по уговору `data/bench/ГГГГ-ММ-ДД.json` |
| `--baseline путь` | сравнить с прошлым прогоном — назовёт поимённо, кто сменил место |
| `--check-notes` | сверить пометки в наборе с тем, какое место запрос занял |
Каждый ручной JSON содержит `schema_version: 1`, явный `domain` — `syntax`,
`metadata` или `procedures` — и массив `cases`. Имя файла не выбирает индекс.
В procedure-наборе `expected` хранит bare-имена BSL без имён модулей; стенд
без учёта регистра разворачивает имя во все точные адреса выбранного корпуса.
Это позволяет публиковать воспроизводимые формулировки, не раскрывая структуру
конкретного внедрения. Необязательные `expected_miss: true` и zero-based
`expected_rank` машинно сверяют зафиксированный baseline при `--check-notes`;
это не порог качества pytest. Старый корневой JSON-массив и старые baseline
без версии отклоняются явно: их нужно пересоздать. `--save` сначала атомарно
заменяет файл отчёта и только затем печатает результаты: ошибка записи не
оставляет частичный отчёт ни на диске, ни в stdout.
Печатает P@1/P@3/P@5/P@10, MRR, долю «чужой домен первым» и медианный отрыв
первого результата от второго. Порогов в assert нет намеренно: наборы
запросов — не тесты, проценты ломались бы от каждой правки словаря. Код 1
означает расхождение пометок; код 2 — что весь замер отменён до печати и
сохранения из-за негодного набора, неоднозначного выбора или неготового
индекса. При смене кода во время набора процедур весь набор повторяется с
начала по одному поколению индекса.
Сравнение двух прогонов выглядит так (ухудшения первыми):
```
=== сравнение с прошлым прогоном ===
- «как прибавить месяц к дате в запросе»: 1 -> промах
- «как отсортировать результат запроса»: 1 -> 5
+ «в чем разница между внутренним и левым соединением»: 5 -> 4
```
**Наборы в образ не входят** (`tests/` в `.dockerignore`) — запускать из
рабочей копии, не из контейнера.
## Сервер вручную — `mcp1c.server`
```bash
PYTHONPATH=src python3 -m mcp1c.server --data data # streamable-http на :8000/mcp
PYTHONPATH=src python3 -m mcp1c.server --transport stdio # локальному клиенту
PYTHONPATH=src python3 -m mcp1c.server --host 0.0.0.0 --port 5001
```
Других значений у `--transport` нет. Устаревший HTTP+SSE полностью отключён:
флаг `--transport sse` отклоняется до чтения данных и запуска сервера.
## Откуда берутся исходные данные
**Структура конфигурации** — обработкой из [`exporter-1c/`](exporter-1c/README.md).
Четыре варианта модуля под обычную и управляемую форму, XML и JSON; XML-варианты
совместимы с 8.3.5. Две обработки лежат уже собранными и открываются как есть:
`ВыгрузкаСтруктурыКонфигурации_ОбычнаяФорма_XML.epf` (8.3.5 и выше) и
`ВыгрузкаСтруктурыКонфигурации_УправляемаяФорма_XML_JSON.epf` (8.3.6 и выше,
формат выбирается на форме).
**Справка платформы** — файл `shcntx_ru.hbk` из каталога установки 1С:
```
/opt/1cv8/<версия>/shcntx_ru.hbk
C:\Program Files\1cv8\<версия>\bin\shcntx_ru.hbk
```
**Имя должно совпадать целиком.** В том же каталоге лежат сотни файлов `.hbk`
— 38 разных справок, каждая на два десятка языков. Похожие на нужный:
| Файл | Что это | Почему не подходит |
|---|---|---|
| `shcntx_root.hbk` | та же справка, языконезависимая часть | 25 508 элементов, но **ни одного описания**: только дерево страниц и английские идентификаторы, без версий появления |
| `shlang_ru.hbk` | описание встроенного языка | не контейнер 1С вовсе |
| `shquery_ru.hbk` | язык запросов | то же |
| `config_ru.hbk` | справка конфигуратора | контейнер, но страниц синтакс-помощника внутри нет |
| `1cv8_ru.hbk` | руководство пользователя | не контейнер |
По размеру их не отличить: `shcntx_root.hbk` весит 33 МБ против 39 МБ у
нужного. Суффикс `_ru` — язык, `_root` — общая часть без текстов.
Файл не тот — сервер объясняет, почему именно, и оставляет прежнюю справку на
месте.
Достаточно **одной** справки от самой свежей доступной платформы: каждый элемент
несёт версию появления, и для старых конфигураций лишнее отфильтровывается. Если
версии нет в пути, она выводится из самих данных.
**Справки от старых платформ тоже принимаются** — они размечены иначе (разделы
на `div` вместо `p`), это учтено. Пригодится, если поднимать отдельный сервер
под старые внедрения: справка от 8.3.5 даёт 18 936 элементов и не содержит
`СтрНайти`, `СтрРазделить`, `ЗаписьJSON` — их в 8.3.5 и не было. Но версию
такая справка о себе не сообщает: пометок «начиная с версии» в ней нет, потому
что тогда всё было текущим. Поэтому **версию берут из имени файла или каталога**
— положите её как `8.3.5.1570.hbk` или в `data/hbk/8.3.5.1570/`, иначе
сопоставление с конфигурацией работать не будет.
---
# 5. Как устроено
```
src/mcp1c/
v8container.py контейнер 1С — общий для .hbk, .cf, .epf
syntax_parser.py разбор справки платформы
syntax_model.py модель элемента справки, виды, границы версий
syntax_merge.py слияние справок разных версий в один индекс
query_parser.py разбор справки по языку запросов (shquery_ru.hbk)
replacements.py чем заменить функцию, которой нет в старой платформе
virtual_tables.py таблицы запроса регистров и имена их полей
loader.py чтение выгрузок, XML и JSON в одну модель
model.py модель конфигурации
graph.py граф связей
graph_view.py окрестность объекта для картинки на дашборде
search.py лексический поиск
search_keys.py формулировки, которыми спрашивают язык запросов
synonyms.py встроенный словарь: как говорят против того, как названо
dictionary.py локальный словарь поверх встроенного
index_cache.py кэш поисковых индексов, расходный
modules_index.py оглавление, вызовы и формы для get_callers
store.py чтение и запись разобранных справок
render.py markdown-карточки объектов и элементов
intake.py отбор и распаковка выгрузки в файлы, санитизация имён
incoming.py состояние data/incoming: кэш sha256, причина отказа
registry.py реестр источников, сопоставление версий
tools.py десять инструментов, без зависимости от MCP
server.py протокольный слой (единственная внешняя зависимость)
dashboard.py веб-интерфейс: реестр, запросы, словарь
cli.py отладочный CLI
bench.py стенд замеров качества поиска
```
**Одна модель на два формата.** XML и JSON — разные сериализации одной схемы;
загрузчик приводит оба к одинаковому словарю. Проверено: обе выгрузки дают
одинаковый набор из 30 ключей.
**Граф строит загрузчик, а не 1С.** Рёбра выводятся из типов реквизитов,
движений документов, оснований ввода, владельцев, обработчиков подписок и
методов регламентных заданий. Правила можно менять без перевыгрузки.
**Слабые рёбра.** Реквизиты вроде `ЗначениеДоступа` перечисляют сотни типов и
связывают почти всё со всем. Такие связи помечаются слабыми и по умолчанию
скрыты — иначе полезные в них тонут.
**Уровни детализации.** Полное описание `Документ.ЧекККМ` (50 реквизитов,
17 табличных частей) съедает контекст целиком. `brief` — пара строк, `fields` —
состав, `full` — со связями.
**Без внешних баз.** Боевые индексы остаются в одном процессе: компактные
массивы кода отвечают без отдельного сетевого перехода, а поиск занимает
0,18–1,5 мс. Elasticsearch, векторное хранилище и графовая БД не окупают
отдельный процесс и сопровождение на таком объёме. Векторный поиск дополнительно
требовал бы модель-энкодер (+185–620 МБ к образу) и 16–32 мс только на
кодирование запроса.
## Измерено на реальных данных — 2026-08-18
Что загружено на рабочем сервере:
| Конфигурация | Платформа | Объектов | Рёбер |
|---|---|---|---|
| Бухгалтерия для Казахстана | 8.3.27.1936 | 3 492 | 84 426 |
| Документооборот КОРП | 8.3.27.1936 | 4 596 | 50 554 |
| Зарплата и управление персоналом | 8.3.27.1936 | 5 181 | 100 136 |
| Розница для Казахстана | 8.3.23.1997 | 5 637 | 58 345 |
| Отраслевая конфигурация | 8.3.5.1570 | 1 616 | 29 288 |
| **Итого** | | **20 522** | **322 749** |
Набор случайный — это то, что оказалось под рукой. Существенна не сама
конфигурация, а разброс платформ: 8.3.5, 8.3.23 и 8.3.27 в одном реестре,
и каждая конфигурация отвечает по своей.
Плюс справка платформы — слитый индекс трёх версий (8.3.5, 8.3.23, 8.3.27),
**25 691 элемент**, и язык запросов — **127 страниц** отдельным источником.
Тёплый старт финального образа с 12 файлами кэша модульных индексов — **19 с**
на замеренном 2026-08-21 рабочем корпусе. При холодном старте исходники
разбираются, индексы строятся и складываются в `data/index/cache/`, разобранные
справки — в `data/index/syntax/`. Дальше они поднимаются оттуда.
Кэш производный и расходный: привязан к версии Python, отпечатку кода пакета
и хешу источника. Не сошлось что-нибудь — индексы строятся заново. Каталог
можно удалить в любой момент, он восстановится сам. Если том кэша только
для чтения, невозможность удалить устаревший файл не прерывает снятие
источника с учёта.
Порядок записей в `registry.json` не важен: код восстанавливается только
после своей конфигурации. Если её записи нет, её удалили или заменили во время
запуска, старая запись модулей или расширения не публикуется без владельца.
Постинги поисковых индексов лежат в массивах `numpy`; временное словарное
наполнение освобождается сразу после заморозки. Общий RSS сервера нельзя
получить сложением изолированных слоёв: холодный пик и прогретое состояние
контейнера снимаются отдельно после подъёма всех источников.
### Тексты модулей — инструменты кода подключены
Приём выгрузки конфигурации в файлы реализован — раздел [«Выгрузка
конфигурации в файлы»](#выгрузка-конфигурации-в-файлы) выше: код и формы
ложатся на диск, источник учтён в реестре, четыре внутренних индекса строятся
и кэшируются. `search_procedures` ищет точные имена и экспортные процедуры по
словам, `get_procedure` отдаёт оглавление модуля либо карточку с окном тела,
а `get_callers` объединяет подтверждённые места вызова, привязки метаданных и
обработчики формы без чтения тел.
Текущий производственный срез основной конфигурации на 2026-08-21: **137 116
процедур**, **619 029 мест вызова**, **3 194 формы**, **89 528 элементов
форм** и **24 202 строки привязки событий**.
Воспроизводится на своей выгрузке без печати её имени или пути:
```bash
MODULES_ROOT=/путь/к/выгрузке
.venv/bin/python tools/lab/measure_modules_cache.py "$MODULES_ROOT"
```
Измеритель печатает JSON-`агрегаты` с ключами `procedures`, `calls`,
`forms`, `elements` и `event_rows`. Последний счётчик — сырые строки
`<Event>` до группировки и удаления дублей в публичных привязках.
Исторический снимок прототипа снят 2026-08-20 на обезличенной иерархической
выгрузке в файлы: 7 878 модулей, 137 115 процедур и 260 МБ текста. Ниже
сохранены именно его цифры, а не текущие агрегаты.
| Слой | На диске | В памяти |
|---|---|---|
| оглавление 137 115 процедур | 14,9 МБ | 62 МБ |
| 619 030 разрешённых и неразрешённых мест вызова | 12,0 МБ | 51 МБ |
| поиск по 49 181 экспортной процедуре | 4,27 МБ | 173 МБ |
| формы: 3 194 файла, 69 769 элементов | 5,8 МБ | 44 МБ |
| тексты модулей и сигнатуры | 260 МБ | 0 |
Латентность поиска — 1,2–1,5 мс. Полная холодная сборка четырёх индексов с
записью кэша — около 50 с.
Выгрузок в файлы существует две разных, и вторая мерилась отдельно — отраслевая
конфигурация 10.5.1.3 на 8.3.5: плоская раскладка, модули в `.txt`, код обычных
форм внутри двоичных контейнеров `.Form`. 2 603 модуля, 33 555 процедур, разбор
1,1 с, поиск по всем 94 МБ при медиане 0,2 мс. Структуры формы этот формат не
содержит, а часть общих модулей поставлена скомпилированными — исходника в них
нет вовсе. Такие модули видны по адресу `ОбщийМодуль.<Имя>`, но процедуры и
вызовы внутри них прочитать нельзя. Поэтому сводки, поиск, оглавление обычного
модуля, обратный поиск и сведения о перекрытиях предупреждают над результатом,
что агрегаты приведены только по доступным исходникам и могут быть неполными.
Скрипты замеров лежат в `tools/lab/`; ими воспроизводится каждая цифра выше:
```bash
.venv/bin/python tools/lab/measure_modules_cache.py <каталог выгрузки в файлы>
.venv/bin/python tools/lab/measure_modules.py <каталог выгрузки в файлы>
.venv/bin/python tools/lab/measure_resident.py <каталог> <файл индекса> собрать
.venv/bin/python tools/lab/measure_search.py <файл индекса> экспортные
.venv/bin/python tools/lab/measure_forms.py <каталог>
.venv/bin/python tools/lab/measure_flat.py <каталог плоской выгрузки>
PYTHONPATH=src .venv/bin/python \
tools/lab/measure_standard_procedure_intents.py --data data
.venv/bin/python tools/lab/measure_container_memory.py --mode cold --data data \
--timeout 300 --poll-interval 0.5
.venv/bin/python tools/lab/measure_container_memory.py --mode warm --data data \
--timeout 300 --poll-interval 0.5
```
### Качество поиска
Снимается стендом, воспроизводится одной командой:
```bash
PYTHONPATH=src .venv/bin/python -m mcp1c.bench \
--data data --config РозницаДляКазахстана \
--auto --sets query-language,roznica-metadata,modules-procedures \
--check-notes
```
| Набор | Запросов | P@1 | P@3 | P@5 | P@10 | MRR | Отрыв |
|---|---|---|---|---|---|---|---|
| Язык запросов | 22 | 86,4% | 86,4% | 90,9% | 90,9% | 0,875 | 34,1% |
| Метаданные Розницы | 21 | 81,0% | 90,5% | 90,5% | 95,2% | 0,862 | 93,2% |
| Процедуры модулей | 3 | 0% | 0% | 0% | 33,3% | 0,047619 | 0% |
| Точные имена справки | 50 508 | 97,1% | 98,3% | 98,7% | 98,9% | 0,977 | 91,7% |
| Одноимённые | 10 439 | 98,7% | 99,8% | 99,9% | 100% | 0,992 | 93,9% |
Вся таблица снята 2026-08-25 точной командой выше; команда завершилась с кодом
0. Три ручных набора собраны из живых
формулировок и промахов, два последних строятся из самих данных. У процедур
ранги `[промах, промах, 7]`. Это baseline для улучшений, а не порог приёмки.
Строка процедур впервые зафиксирована 2026-08-21 и повторным прогоном
2026-08-25 подтверждена без изменений.
Строка процедур отдельно воспроизводится быстрее:
```bash
PYTHONPATH=src .venv/bin/python -m mcp1c.bench \
--data data --config РозницаДляКазахстана \
--sets modules-procedures
```
«Отрыв» — насколько первый результат оторвался от второго, медианой. Отвечает
на вопрос «уверенно попали или чудом»: 35% по языку запросов против 91,7% по
справке означает, что эти победы держатся втрое слабее и правка ранжирования
способна их перевернуть, не сдвинув ни одного процента P@1.
Латентность ручных наборов — 0,18–1,29 мс на запрос.
**Наборы запросов в образ не входят** (`tests/` в `.dockerignore`): мерить
надо из рабочей копии, а не из контейнера.
## Тесты
```bash
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest # 1100 тестов (прогон 2026-08-21)
```
От содержимого `data/` не зависят: проприетарных выгрузок в репозитории нет,
всё нужное собирается синтетически в `tests/conftest.py`.
Качество поиска тестами **не проверяется** — оно меряется стендом
(`mcp1c.bench`, см. «Измерено»). Пороги в процентах ломались бы от каждой
правки словаря, поэтому стенд печатает цифры, а решение принимает человек.
`pytest` проверяет наблюдаемое поведение: «индекс не построен заново»,
«выдача совпала», «старт не упал».
---
# 6. Безопасность
Два токена, оба задаются переменными окружения. **Пока токен не задан,
соответствующий доступ открыт всем, кто дотянется до адреса.**
| Переменная | Что закрывает | Не задана |
|---|---|---|
| `API_TOKEN` | чтение: инструменты MCP и страницы дашборда | структура конфигураций открыта всем |
| `ADMIN_TOKEN` | запись: загрузка и удаление источников, разбор входящей выгрузки (`/sources/incoming/parse`), правка словаря, `/admin/reload` | эти маршруты отключены, отвечают 404 |
Разница между «открыто» и «отключено» намеренная. Чтение без токена работает —
на своей машине это удобно и ничем не грозит. Запись без токена **не
работает вовсе**: одна неудачная правка словаря тихо ломает поиск всем, кто
подключён к общему серверу.
Токен передаётся заголовком — либо `X-Api-Token`, либо
`Authorization: Bearer <токен>`. Административный годится и для чтения:
иначе владельцу пришлось бы держать в клиенте два заголовка вместо одного.
**Токен должен быть в латинице.** Заголовки HTTP кодируются latin-1, и
кириллический токен до сервера физически не доходит: через форму входа в
браузере он сработает, через заголовок клиента — нет.
Мимо проверки пропускаются два пути: `/health` (по нему ходит healthcheck
контейнера, и сведений сверх права чтения он не отдаёт) и `/login` — иначе
форма входа оказалась бы за той самой авторизацией, которую она выдаёт.
Ещё два правила, не про токены:
- **MCP-эндпоинт отдаёт структуру конфигураций целиком.** Задавайте
`API_TOKEN` при любом выносе за пределы своей машины, сетевого доступа
недостаточно.
- **Каталог `data/` целиком в `.gitignore`** — и `.hbk` с выгрузками, и
разобранные индексы. Индекс справки — тот же контент фирмы «1С», только
распакованный. Однажды он туда попал и пролежал 20 коммитов; историю
переписывали `git filter-repo`, а правило переформулировали по каталогу,
а не по расширениям: проверять надо не «это `.hbk`?», а «это лежит в
`data/`?».
---
# 7. Документы
| Файл | О чём |
|---|---|
| [CHANGELOG.md](CHANGELOG.md) | что сделано и что выяснено про 1С |
| [docs/schema-v1.md](docs/schema-v1.md) | контракт формата выгрузки |
| [docs/data-sources.md](docs/data-sources.md) | что из какого источника берём |
| [docs/query-language-design.md](docs/query-language-design.md) | устройство источника языка запросов |
| [docs/dashboard-design.md](docs/dashboard-design.md) | устройство дашборда |
| [docs/modules-intake-design.md](docs/modules-intake-design.md) | приём выгрузки конфигурации в файлы — постановка |
| [docs/modules-provider-design.md](docs/modules-provider-design.md) | индексы и инструменты провайдера кода |
| [exporter-1c/README.md](exporter-1c/README.md) | обработка выгрузки для 1С |
| [CONTRIBUTING.md](CONTRIBUTING.md) | как править проект: язык, коммиты, проверки |
| [SECURITY.md](SECURITY.md) | как сообщить об уязвимости |
Публикуются шесть самодостаточных документов-контрактов: формат выгрузки,
граница источников, устройство языка запросов и дашборда, приём выгрузки кода
и провайдер кода. Каждая ссылка из публичного файла должна вести на материал,
который тоже входит в репозиторий.
Внешняя БД, векторы, графовая БД, SSE-транспорт и ленивая загрузка отклонены
замерами текущего корпуса. Вернуться к ним можно только с новыми измерениями,
которые опровергают прежнюю стоимость или показывают новый живой сценарий.
---
# 8. Лицензия
[Apache License 2.0](LICENSE). Отдельно стоит прочитать [NOTICE](NOTICE) — там
две вещи, которые лицензия сама не говорит:
- проект разработан независимо и **не аффилирован с ООО «1С»**; «1С» и
«1С:Предприятие» — товарные знаки ООО «1С», и лицензия прав на них не даёт
(Apache 2.0, раздел 6);
- в репозитории **нет справки платформы и нет выгрузок конфигураций** — ни в
исходном виде, ни разобранных. Это контент фирмы «1С» и данные конкретных
внедрений; каждый берёт их из своей поставки и кладёт в `data/`. Лицензия
распространяется на код, а не на данные, которые вы в него загрузили.
连接信息
猜你喜欢
暂无推荐服务器