Merge origin/main в feat/26-slice-2b-statement-parser
Refs #26 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
commit
e93b28e20f
@ -34,9 +34,13 @@
|
||||
`docs/contracts/recognition-result.schema.json` (контракт v1.2: `DISCLOSURE` в `document_type`, `statementDocument.checks`,
|
||||
границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания
|
||||
заголовков/группировок — `rules/*.yaml`.
|
||||
- **scan-gateway** — адаптер ИС Сканирование: переименовать файл `UUID_имя.pdf`, положить в «Входящие», опросить
|
||||
«Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), нормализовать
|
||||
в тот же контракт результата, опубликовать `recognition.result`. Таймаут 5 мин → `timeout`.
|
||||
- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (временный
|
||||
`<имя>.<uuid>.tmp` рядом, затем атомарный rename; ИС игнорирует `*.tmp`), дождаться батча `Output/<job_id>/*.json`
|
||||
(появляется целиком и больше не меняется), сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется),
|
||||
очистить `Output/<job_id>/` и входящий файл, нормализовать в тот же контракт результата, опубликовать
|
||||
`recognition.result`. Таймаут 5 мин → `timeout`. Раскладка папок — как у проверенного в контуре PDFWorker старого АРМ
|
||||
(`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137); текущий код шлюза именует файл `<job_id>_<original_name>`
|
||||
и пишет временный файл в `Tmp/` — приводится к эталону в arm-scan-gateway #6.
|
||||
- **Frontend** — React + TS по шаблону команды; экраны по макету `dkb_24.09.26` (справочник ГК, консолидации,
|
||||
первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли).
|
||||
|
||||
@ -60,7 +64,8 @@
|
||||
|
||||
`group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге.
|
||||
|
||||
**На платформе NATS без JetStream** (ответ 03.10.2026): нет durable consumer'ов, серверного дедупа и redelivery.
|
||||
**На платформе NATS без JetStream** (ответ 03.10.2026; подтверждено 06.10.2026 кодом `RSHB/armka_file_worker`, который
|
||||
работает в контуре на `subscribe`/`publish` + `flush` без `jetstream()`): нет durable consumer'ов, серверного дедупа и redelivery.
|
||||
Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`),
|
||||
ядро — queue group `arm-core` на `recognition.result`; сервис сразу по получении корректного запроса публикует ack `processing`;
|
||||
ядро, не получив ack за `ACK_TIMEOUT_SECONDS` (30), публикует запрос повторно с тем же `Nats-Msg-Id`, не более
|
||||
@ -281,6 +286,9 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
|
||||
- **NATS** — `nats-py`; аутентификация nkeys-seed + JWT (`NATS_SEED`, `NATS_JWT`) через `user_jwt_cb`/`signature_cb`;
|
||||
соединение и подписки создаются в lifespan и живут в `app.state.nats` / `app.state.nats_subs`. Core NATS без
|
||||
JetStream: подписки с queue group, заголовки `Nats-Msg-Id` ставим сами, ack и повторы — на уровне приложения.
|
||||
Учётные данные одни на все сервисы АРМ: один JWT с правами по subject'ам (ответ владельца 06.10.2026), поэтому новые
|
||||
subject'ы (`recognition.*`) должны быть внесены в права этого JWT платформой; старый АРМ и `armka_file_worker`
|
||||
работают на семействе `files.*` (`files.tabular_uploaded`, `files.tabular_parsed`, `files.img_parsed`).
|
||||
- **SMB** (горячие папки ИС Сканирование) — `smbprotocol`/`smbclient`, сессия регистрируется в lifespan; нужен только `scan-gateway`.
|
||||
- **Внешняя аутентификация** — пакет `raisa_fastapi_protected_api` из внутреннего Nexus (`AuthorizationMiddleware`,
|
||||
`ProtectedSettings`, …), импортируется только при `DEBUG=false`; фронтовая пара — `raisa-fastapi-protected-web`.
|
||||
@ -361,3 +369,22 @@ JetStream** (3 subject `recognition.*`; аутентификация nkeys/JWT,
|
||||
префиксы-роли + lifecycle по префиксам), Python 3.11 в базовом образе, Nexus (pip/npm, пакеты `raisa_*`) — только внутри
|
||||
банка, CI/CD — пайплайн платформы (мы отдаём команды `make check` / `make check-full`). Секреты — через переменные
|
||||
окружения (`OPENBAO__*`). Открыто: версия Node, параметры JWT РАИСА.
|
||||
|
||||
Дополнено 06.10.2026 по репозиторию `RSHB/armka_file_worker` (PDFWorker старого АРМ, проверен в контуре банка) и ответам
|
||||
владельца:
|
||||
- NATS: core NATS без JetStream подтверждён кодом, работающим в контуре (`nats-py[nkeys] 2.14`, `nats.connect` с
|
||||
`user_jwt_cb`/`signature_cb`, `max_reconnect_attempts=-1`; учётные данные опциональны — оба пустых означают анонимное
|
||||
подключение). Один JWT на все сервисы АРМ, права по subject'ам заданы в JWT. Можно ли включить JetStream — по-прежнему
|
||||
OQ-85.
|
||||
- Переменные окружения те же, что в шаблоне: `OPENBAO__NATS__{NATS_URL, NATS_JWT, NATS_SEED}`, `OPENBAO__MINIO__*`,
|
||||
`OPENBAO__SMB__*`, вложенность через `__`.
|
||||
- Приложения на платформе Aurora живут под префиксом `ROOT_PATH = /aurora/apps/fastapi-dkb-armka-<service>` (у
|
||||
FileWorker — `fastapi-dkb-armka-pdf-worker`); наши сервисы держат `ROOT_PATH = /` по умолчанию и получают префикс из
|
||||
окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип,
|
||||
что у нас.
|
||||
- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/<job_id>.<ext>`, `*.tmp` рядом,
|
||||
`Output/<job_id>/*.json`, очистка после чтения), см. описание scan-gateway выше и OQ-137.
|
||||
- FileWorker **не** реализует наш контракт DECISION-4: subject'ы `files.*`, конверт `task_id`/`file_id`/`source_s3_key`/
|
||||
`expires_at` → `status ready|timeout|error` + `error_code`, без ack `processing`, без `Nats-Msg-Id`, без queue group
|
||||
(один экземпляр), дедуп только в памяти процесса — доставка at-most-once. Это параллельный сервис старого АРМ, а не
|
||||
замена scan-gateway; из него берём только инфраструктурные решения (как и из `reference-old-arm`).
|
||||
|
||||
@ -764,6 +764,64 @@ OQ-92 (пустые «Организация»/«Группа компаний»
|
||||
только строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
|
||||
Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`.
|
||||
|
||||
### §11.1 Очистка OCR-текста (перенос из старого ядра, Срез 3)
|
||||
|
||||
Источник — модуль старого ядра `DKB_ARMKA` `api/src/services/ofr_result_conversion/` (`utils.py`, `date_utils.py`,
|
||||
`consts.py`; в локальном `reference-old-arm` тот же код лежит в `api/src/core/PDFWorker/`). Решение владельца
|
||||
06.10.2026: модуль **проверен на реальном стенде ИС Сканирование, его код можно переносить** в нормализатор `scan-gateway`.
|
||||
Это **единственное исключение** из правила «доменный код из `reference-old-arm` не переносить» (CLAUDE.md workspace).
|
||||
Переносятся чистые функции очистки и разбора; обвязка старого ядра (`json_to_db_rows`, dataclass'ы `Meta/DbRow/DbValue`,
|
||||
прямая запись в `financial_statement_*`) не переносится — результат ложится в `statementDocument` контракта v1.2
|
||||
(`string_value`, `confidence`, `is_recognized`, `period_date`, `period_text`, `line_code_confidence`). Перенос — с тестами
|
||||
на `fixtures/ocr/aep_*.json`; изменения правил ниже — только через этот параграф.
|
||||
|
||||
1. **Буквы-двойники рядом с цифрами** (`date_utils._normalize`, `_LOOKALIKES`): `О/о/O/o → 0`, `З/з → 3`,
|
||||
`l/I/|/і → 1`, `б → 6`, `S/s → 5`, `Ч → 4` — заменяются **только если символ стоит вплотную к цифре**, в несколько
|
||||
проходов («2ОО5» → «2005»); слова («Общество») не портятся. Если первая строка значения состоит только из цифр,
|
||||
пробелов, `|`, точек и двоеточий — `|` считается разделителем клеток формы («2|0|2|4|» → «2 0 2 4»), а не единицей.
|
||||
2. **Даты из OCR-текста** (`date_utils._parse_date`): сначала текстовые формы по всей строке в порядке приоритета —
|
||||
диапазон «с 01.01.2025 по 30.09.2025» (берётся конец), «30 сентября 2025 г./r./года» (латинская `r` вместо `г`),
|
||||
«I квартал 2026» / «1-й кв.» / «первый квартал», «1 полугодие» / «1 п/г», «9 месяцев 2025», «январь – сентябрь 2025»
|
||||
(последний месяц), «за 2025 г.» / «2025 год» (31.12); затем числовые — три группы цифр («30.09.2025», «2025 03 31»,
|
||||
«30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год
|
||||
(31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/
|
||||
N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4
|
||||
(Excel), там на вход приходят чистые строки.
|
||||
3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после
|
||||
нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая
|
||||
заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на
|
||||
«на » / «за ». Повторная шапка внутри таблицы **перезаписывает** раскладку. **Без шапки** раскладка выводится по данным:
|
||||
код — столбец с наибольшим числом значений из справочника кодов строк; наименование — столбец левее кода с наибольшим
|
||||
числом текстовых ячеек; значения — столбцы правее кода, где суммы не реже мусора (полоса скана) или столбец пуст
|
||||
(период без данных); шапка синтезируется (`Код` / `Наименование показателя`, тексты периодов пустые). Строки до первой
|
||||
настоящей шапки разбираются по выведенной раскладке. Ни шапки, ни столбца кода — таблица пропускается с warning.
|
||||
**Рулинг 06.10.2026 (вместо прежнего «без шапки → колонки без дат, `NEEDS_CHECK`»):** раскладку выводить, как в старом
|
||||
ядре, но документ без шапки всё равно отдавать `needs_check` с причиной в `metrics.warnings` — колонки без `period_date`
|
||||
аналитик подтверждает руками.
|
||||
4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные);
|
||||
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с кодом
|
||||
(как в §11 выше); расшифровки без кода — OQ-36.
|
||||
5. **Тип таблицы** (`_resolve_table_type`): по первой цифре кодов строк данных — больше `1xxx` → BALANCE, больше `2xxx` →
|
||||
INCOME; это запасной путь, когда `label.form_name` зашумлён; при расхождении с `label` — `needs_check`.
|
||||
6. **Код и наименование строки**: код очищается до букв, цифр, пробела и точки (`_clear_value`: переносы строк и
|
||||
табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`,
|
||||
у нас — `rules/statement_lines.yaml` из §12 п. 2), а из OCR — только если код не найден в справочнике (`line_code_confidence
|
||||
< 100`). Код считается распознанным (`line_code_recognized`), если `confidence == 100`, очищенный текст равен сырому и код
|
||||
есть в справочнике.
|
||||
7. **Суммы** (`_parse_amount`): запятая → точка, пробелы убираются; содержимое скобок — отрицательное; из строки
|
||||
удаляется всё, кроме цифр, `+`, `-`, `.`; несколько точек — разделители тысяч, остаётся последняя; `Decimal`, иначе
|
||||
`None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — очищенный текст (`_clear_value`).
|
||||
**`is_recognized`** значения = `confidence == 100` **и** разбор суммы не потребовал выбрасывать символы (сырой текст без
|
||||
скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата
|
||||
разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой
|
||||
заголовка нет — пустое значение с `is_recognized = false`, чтобы не сдвигать столбцы.
|
||||
8. **Шапка документа**: ИНН — только цифры из `inn.form_name`, «распознан», если `confidence == 100` и в сыром тексте не
|
||||
было ничего, кроме цифр; единицы — `_clear_value`, тот же критерий; дата отчёта — п. 2, «распознана», если
|
||||
`confidence == 100` и дата разобралась. Нераспознанное → `needs_check` (как в §11).
|
||||
9. **Ошибка разбора одного документа** (исключение в разборе `data_ocr`) не роняет файл: документ помечается ошибкой
|
||||
(`BACKEND_PARSING_ERROR` + текст исключения), остальные документы файла обрабатываются; хотя бы один документ есть —
|
||||
конверт `needs_check`, ни одного — `error` (в терминах `recognition-result-message`).
|
||||
|
||||
---
|
||||
|
||||
## §12. ББ и ОФР из Excel (tabular-parser, второй тип документа)
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
| OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | **закрыт реализацией (arm-core PR #10, техдолг Среза 1, 05.10.2026):** `.dockerignore` исключает `api/.env.*` (включая `.env.test` и `.env.example`); настройки контейнера — только из окружения |
|
||||
| OQ-83 | Контейнеры профиля `services` подписаны на те же subject и queue group общего NATS, что и экземпляры в интеграционных тестах сервисов (`recognition.result` / `arm-core`, `*.request`): NATS может отдать сообщение теста контейнеру и наоборот; интеграционные тесты `scan-gateway` и `arm-core` одновременно тоже делят NATS. Сделать queue group и subject-префикс настраиваемыми для тестов (например `NATS_TEST_PREFIX`) или поднимать отдельный NATS на тесты? | scan-gateway: п. 11 таблицы предложений (T14.1, T11.8); docs: O-11 (задача 3 фазы 2; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно |
|
||||
| OQ-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнут `scan-gateway` (`OPENBAO__DATABASE__*`) и `tabular-parser` (`OPENBAO__DATABASE__*`, `OPENBAO__SMB__*` — `extra_forbidden`), в нём нет `OPENBAO__MINIO__MINIO_SECURE=false` (по умолчанию `true` — https к http-MinIO). Прогнать сценарий, зафиксировать набор переменных и портов (`SERVER__PORT`, `SCAN_HOTFOLDER_PATH` для `scan-gateway`), решить, нужен ли отдельный `.env.example` на сервис, и снять пометку «черновик» с раздела `dev/README.md`? | spec `dev-environment`, сценарий «Запуск из venv против compose без Nexus»; docs: design #43, `dev/README.md` («Запуск сервисов из venv без Nexus»); финальное ревью фазы 2: I-1 | срез 0 (процесс, окружение разработки); кандидат в follow-up issue | вопрос: допущение Среза 0 — раздел README остаётся черновиком, сценарий не подтверждён (проверка требовала `.venv` внутри сервисных репозиториев, в фазе 2 доступных только для чтения) |
|
||||
| OQ-85 | **Возможен ли JetStream на платформе** (серверный дедуп по `Nats-Msg-Id`, durable consumer'ы, redelivery вместо ack и повторной публикации на уровне приложения)? Сейчас NATS без JetStream (ответ платформы 03.10.2026); Срез 0 строит доставку на ack `processing`, повторной публикации ядром и дедупе по `recognition_job.result_msg_id`. Если JetStream подключить можно — меняются только publisher/subscriber (`core/nats.py` в трёх сервисах), outbox/inbox и контракт остаются; исчезают потеря результата при простое ядра и риск повторной обработки. | решение человека 04.10.2026: «узнать у платформы и проверить позже»; `design.md` slice-0-skeleton (D-3, Open Questions п. 4); OQ-72, OQ-73 | инфраструктура NATS (платформа); `core/nats.py`, воркеры сервисов | **вопрос к платформе (поставить напоминание, проверить после ревью Среза 0 / до Среза 3)**: допущение — без JetStream |
|
||||
| OQ-85 | **Возможен ли JetStream на платформе** (серверный дедуп по `Nats-Msg-Id`, durable consumer'ы, redelivery вместо ack и повторной публикации на уровне приложения)? Сейчас NATS без JetStream (ответ платформы 03.10.2026); Срез 0 строит доставку на ack `processing`, повторной публикации ядром и дедупе по `recognition_job.result_msg_id`. Если JetStream подключить можно — меняются только publisher/subscriber (`core/nats.py` в трёх сервисах), outbox/inbox и контракт остаются; исчезают потеря результата при простое ядра и риск повторной обработки. | решение человека 04.10.2026: «узнать у платформы и проверить позже»; `design.md` slice-0-skeleton (D-3, Open Questions п. 4); OQ-72, OQ-73 | инфраструктура NATS (платформа); `core/nats.py`, воркеры сервисов | **вопрос к платформе (поставить напоминание, проверить после ревью Среза 0 / до Среза 3)**: допущение — без JetStream. 06.10.2026: `RSHB/armka_file_worker` (PDFWorker старого АРМ, проверен в контуре) тоже работает на core NATS (`subscribe`/`publish` + `flush`, без `jetstream()`), один JWT на все сервисы АРМ с правами по subject'ам; возможность включить JetStream по-прежнему не выяснена |
|
||||
| OQ-86 | Фильтр видимости по ЗО (`business_segment`): сегмент есть только у `app_user`, у ГК/ЮЛ поля нет (DBML v5.14). Откуда брать сегмент ГК (из ЕЦП CRM? из РФ?), нужен ли вообще фильтр по ЗО в первой очереди? | §10.1 правил; DBML v5.14 | срез 1 (policy), DBML v5.15 | допущение: в Срезе 1 ЗО не фильтрует; видимость — только по филиалу (ГО видит всё) |
|
||||
| OQ-87 | Выгрузка ЕЦП содержит типы `нерез.` (354), `ФЛ` (180), `нет инн` (2); enum `entity_type` — только ЮЛ/ИП. Нужны ли нерезиденты и физлица в справочнике (контрагенты ОСВ, поручители)? | `docs/source/biq/ecp-export-lagal-entity.xlsx`; §13.2 | срез 1 (импорт), DBML | допущение: строки пропускаются с кодом `UNSUPPORTED_TYPE`, enum не расширяется |
|
||||
| OQ-88 | Проверять ли контрольные разряды ИНН при импорте и вводе (сейчас только длина 10/12); в выгрузке 279 нечисловых значений (`LV…`, `РНН…`, `нерез.`) | §13.2 | срез 1 | допущение: только длина; нечисловые → `INVALID_INN` |
|
||||
@ -142,6 +142,7 @@
|
||||
| OQ-134 | Шапка ОСВ без строки организации и с кодом/названием счёта в двух колонках («Счет: Код / Наименование», строка «Бухгалтерский \| 9 месяцев 2025 г.»; Новоросметалл ОСВ 63): парсер не находит организацию, счёт, период, тип данных. Новый вариант заголовка — словарь или код | пакет ДКБ 06.10.2026 | tabular-parser `detect.py`/`header.py`, `osv_headers.yaml` | **реализовано** (tabular-parser PR #13, arm-tabular-parser#11 п. 4): шапка без метки организации (организация — хвост названия ведомости), период и тип данных отдельными ячейками (`periods.yaml: standalone`, `data_types.yaml: standalone_header`), подписи «Код»/«Наименование» не группировки (`osv_headers.yaml: column_captions`), код счёта — из первой строки-счёта (с предупреждением); `05-domain-rules.md` §1.1; тест `test_header_without_labels_and_code_name_columns_are_found` |
|
||||
| OQ-135 | Отчёт 1С «Анализ счета» с отбором по контрагентам группы (`fixtures/analysis-reports/`, в именах файлов «ОСВ_…ПоКомпаниямГруппы»): не ОСВ, но содержит ВГО по корреспонденции счетов. Нужен ли отдельный тип документа для Среза 5, или ВГО считаются только из ОСВ | пакет ДКБ 06.10.2026 | `document_type`, Срез 5 | открыт: допущение — не разбирать; парсер отвечает `INVALID_DOCUMENT` |
|
||||
| OQ-136 | ИНН организаций пакета ДКБ (Цимлянский, НКЗ, Новоросметалл, Приманыческий, Агрокомплекс) в шапках ОСВ отсутствуют; для теста консолидации по этим группам нужны ИНН и состав ГК/ГСК (выгрузка ЕЦП или список от ДКБ) | пакет ДКБ 06.10.2026 | seed/фикстуры справочников, тесты Среза 4–5 на новых группах | открыт |
|
||||
| OQ-137 | Раскладка горячих папок ИС Сканирование: scan-gateway кладёт `Input/<job_id>_<original_name>` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/<task_id>.<ext>` через временный `<назначение>.<uuid>.tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output/<task_id>/*.json` и удаляет `Output/<task_id>/` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `armka_file_worker` (README «Файлы», `clients.py`) и `scan-gateway/gateway/hotfolder.py`, `smb.py`; OQ-78, OQ-79 | scan-gateway (спека `image-recognition-gateway`, эмулятор, тесты) | **закрыт владельцем 06.10.2026: «правильно как в FileWorker»** — имя `<job_id>.<ext>`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output/<job_id>/` и входящего файла после чтения; `Output/<job_id>/*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6 |
|
||||
| OQ-137 | Единицы «тыс. руб.» в шапке против значений в рублях с копейками в выгрузках 1С (Унагранде Компани/Групп, НПЗ) и тысяч с дробями (Межениновская): кто и где приводит единицы. Примеры: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`, `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 руб.; ОФР НПЗ и ОФР Унагранде (стр. 1) — `needs_check` по 2100/2200/2300 по той же причине; полные формы по факту сходятся только у Межениновской | `fixtures/statements-xlsx/README.md`, tabular-parser design 2b D-6, D-7, решения №22–24 (R-11, R-13, R-14) | Срез 3 (маппинг форм), §8 (КФО в тыс. руб.) | допущение принято (решение владельца 06.10.2026, вариант А): парсер отдаёт значения и `units` как в файле, при «тыс.» и ≥ 10 % значений с дробной частью — предупреждение; допуск ±1 не меняется, не нормализуем; приведение единиц — ядро/аналитик (Срез 3) |
|
||||
| OQ-138 | Форма без колонки кодов: сопоставление по именам строк из шаблонов 66н/ФСБУ (словарь `statement_lines.yaml`, `line_code_confidence < 100`)? Сейчас документ без строк и `needs_check` («колонка кодов не найдена») | решение владельца 06.10.2026 (вопрос 6), tabular-parser design 2b D-4 | §12 п. 2, Срез 3 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам |
|
||||
| OQ-139 | Страница-продолжение в отдельном файле (1С печатает ББ/ОФР постранично, один файл = страница): склейка в ядре по ЮЛ + период + тип формы; признак `is_continuation` / `form_page` в контракте (MINOR, две необязательных поля) | решение владельца 06.10.2026 (вопрос 2), design 2b D-8 | §12 п. 9, Срез 3, контракт v1.x | допущение принято: страницы разных файлов не склеиваются; страница 2 без шапки — документ с пустой шапкой и `needs_check`, «загрузите форму одним файлом» |
|
||||
|
||||
@ -171,7 +171,9 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14,
|
||||
## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа
|
||||
|
||||
- `scan-gateway`: горячие папки (по FNR02) с заглушкой-эмулятором ИС Сканирование для dev (возвращает `aep_*.json`
|
||||
по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата; таймаут 5 мин.
|
||||
по имени файла); сырой ответ → S3 `raw/`; нормализация в контракт результата по §11 и **§11.1** (очистка OCR-текста:
|
||||
перенос проверенного на стенде модуля `ofr_result_conversion` старого ядра — единственное разрешённое исключение для
|
||||
`reference-old-arm`, решение владельца 06.10.2026); таймаут 5 мин.
|
||||
- Маппинг: `financial_statement_meta/column/line/value` из JSON; числа, скобки, confidence; ИНН/имя сверка.
|
||||
- Логические проверки §7 (чистые функции `domain/checks.py`), акцепт КА, «Подтвердить данные» → `CONFIRMED`,
|
||||
`RECOGNIZED_DATA_EDIT` с before/after.
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
поднимается только инфраструктура, а сервисы можно запускать из venv (см. «Запуск сервисов из venv»).
|
||||
|
||||
Для ручного тестирования стенда (установка ПО, запуск, чек-лист сценариев по ролям) — [`qa-guide.md`](qa-guide.md).
|
||||
Разработчику фронта: как устроены вход и права `arm-core`, какие ручки когда дёргать, семантика 401/403/404 —
|
||||
[`frontend-auth-guide.md`](frontend-auth-guide.md); контракт API консолидации — [`api-contract-slice-4.md`](api-contract-slice-4.md).
|
||||
Перенос сервисов в контур банка (снимки без истории, что вырезается, скрипт подготовки, проверки в контуре) —
|
||||
[`contour-transfer-guide.md`](contour-transfer-guide.md).
|
||||
|
||||
## Предварительные требования
|
||||
|
||||
|
||||
@ -37,10 +37,10 @@
|
||||
| `GET /consolidations/versions/{version_id}` | VIEW | — | `VersionCard` | 404 `NOT_FOUND` | [34] панель «Организации», [36], [37] |
|
||||
| `POST /consolidations/versions/{version_id}/lock` | EDIT | тело необязательно: `LockIn {takeover_reason?}` (≤ 1000) | 201 `LockOut` (новая блокировка или перехват) / 200 `LockOut` (уже ваша — идемпотентно) | 404 `NOT_FOUND`, 409 `VERSION_NOT_EDITABLE` (версия `CANCELLED`), 409 `LOCKED_BY_OTHER`, 422 (pydantic-массив) при `takeover_reason` длиннее 1000 символов | [35] |
|
||||
| `DELETE /consolidations/versions/{version_id}/lock` | EDIT | — | 204 (в т. ч. если активной блокировки нет) | 404 `NOT_FOUND`, 409 `NOT_LOCK_OWNER` | [34] |
|
||||
| `POST /consolidations/versions/{version_id}/status` | EDIT | `StatusIn {to_status, comment?}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER` / `TRANSITION_NOT_ALLOWED`, 422 `COMMENT_REQUIRED` | [33-01], меню «…» [34] |
|
||||
| `PUT /consolidations/versions/{version_id}/members` | EDIT | `MembersIn {legal_entity_ids[]}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`), 422 `PERIMETER_TOO_SMALL` | [36] |
|
||||
| `POST /consolidations/versions/{version_id}/status` | EDIT | `StatusIn {to_status, comment? (≤ 1000)}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER` / `TRANSITION_NOT_ALLOWED`, 422 `COMMENT_REQUIRED`, 422 (pydantic-массив) при `comment` длиннее 1000 символов | [33-01], меню «…» [34] |
|
||||
| `PUT /consolidations/versions/{version_id}/members` | EDIT | `MembersIn {legal_entity_ids[] (≤ 500)}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`), 422 `PERIMETER_TOO_SMALL`, 422 (pydantic-массив) при более чем 500 `legal_entity_ids` | [36] |
|
||||
| `GET /consolidations/versions/{version_id}/checklist` | VIEW | — | `ChecklistOut {rules[], rows[]}` | 404 | [37] |
|
||||
| `GET /consolidations/versions/{version_id}/osv` | VIEW | query: `limit`, `offset`, `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped` (bool), `match_kind` (повторяемый), `q` | 200 `Page[ConsolidatedRowOut]` | 404 | [30] |
|
||||
| `GET /consolidations/versions/{version_id}/osv` | VIEW | query: `limit`, `offset`, `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped` (bool), `match_kind` (повторяемый), `q` (≤ 500) | 200 `Page[ConsolidatedRowOut]` | 404, 422 (pydantic-массив) при `q` длиннее 500 символов | [30] |
|
||||
| `GET /consolidations/versions/{version_id}/osv/summary` | VIEW | — | `OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}` | 404 | [30], баннер «Осталось: N», [31-00] |
|
||||
| `GET /consolidations/versions/{version_id}/osv/export` | VIEW | те же фильтры, что у `…/osv`, без `limit`/`offset` | файл xlsx (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) | 404 | «Экспорт» [30] |
|
||||
| `GET /consolidations/versions/{version_id}/counterparty-candidates` | VIEW | query: `q` (≤ 500), `scope` (`group` по умолчанию / `all`), `entry_id` | `CandidateOut[]` (до 100) | 404, 422 `QUERY_TOO_SHORT` (`scope=all` и `q` короче 3 символов после обрезки) | [32-osv-map-counterparty] |
|
||||
@ -135,6 +135,17 @@ alias_moved_from (uuid | null)}`.
|
||||
через чек-лист версии, а не через поле документа (OQ-120).
|
||||
15. **Создание не ставит блокировку.** После `POST /consolidations` версия свободна; «Взять в работу» — отдельное действие [35].
|
||||
ГСК в запросе не сохраняется (`related_group_id` в версии пока пуст); члены берутся из текущего состава ГСК на момент создания.
|
||||
16. **Границы входов (arm-core #26).** `MembersIn.legal_entity_ids` ≤ 500 элементов, `StatusIn.comment` ≤ 1000 символов,
|
||||
`q` в `GET …/osv` и `…/osv/export` ≤ 500 символов (у `counterparty-candidates` так было и раньше). Превышение — 422 **в формате
|
||||
pydantic** (`detail[] {loc, msg, type}`), без доменного `code`: показывайте как ошибку валидации формы, не ищите `code`.
|
||||
Ограничивайте ввод на клиенте теми же числами.
|
||||
17. **ОСВ со скользящим периодом** («Октябрь 2024 г. – Сентябрь 2025 г.», arm-tabular-parser #13, OQ-131): у документа
|
||||
`period_year` = год конца диапазона, а `period_quarter` и `is_cumulative` — `null`. Такой документ не подходит ни одной версии
|
||||
(привязка идёт по кварталу), в чек-листе и своде не участвует, подтверждать его можно. В списке документов показывайте год и
|
||||
пометку «период не определён», не подставляйте квартал сами. Границы периода появятся в контракте v1.2 (Срез 2b).
|
||||
18. **Удаление алиаса** (`DELETE /directories/legal-entities/{id}/aliases/{alias_id}`) во время переноса этого же алиаса ручным сопоставлением
|
||||
(`move_alias: true`) теперь ждёт блокировку и отвечает 404 `NOT_FOUND`, если алиас уже переехал (arm-core #26, T11.3) —
|
||||
это не ошибка клиента, перечитайте список алиасов.
|
||||
|
||||
## 5. Сквозные сценарии экранов
|
||||
|
||||
|
||||
179
dev/contour-transfer-guide.md
Normal file
179
dev/contour-transfer-guide.md
Normal file
@ -0,0 +1,179 @@
|
||||
# Перенос сервисов АРМ КА в контур банка: гайд для разработчика
|
||||
|
||||
Разработка ведётся **снаружи** (workspace `awp-ca`, Gitea проекта). В контур банка (GitLab платформы РАИСА и его CI/CD;
|
||||
Gitea в контуре нет)
|
||||
уезжают только три сервисных репозитория — `arm-core`, `tabular-parser`, `scan-gateway` — **снимками дерева, без
|
||||
истории**. Репозитории `docs` и `fixtures` в контур не переносятся: решение владельца 06.10.2026. Этот документ — что
|
||||
именно уезжает, чего в платформе нет, как подготовить снимок скриптом и как его положить в контур.
|
||||
|
||||
Соглашения платформы, от которых всё отталкивается, — `context/03-architecture.md`, разделы «Соглашения шаблона команды»
|
||||
и «Что ожидаем от платформы». Проверки качества — `dev/checks.md`.
|
||||
|
||||
## 1. Что переносится и что нет
|
||||
|
||||
| Что | В контур | Почему |
|
||||
|---|---|---|
|
||||
| `api/` (код, тесты, `contracts/`, `rules/`, `alembic/`, `scripts/`), `web/` (arm-core), `README.md`, `Makefile`, `setup/`, `raisa_config.yml`, `api/.env.example`, `api/.env.test` | да | это и есть сервис по шаблону платформы |
|
||||
| `openspec/specs/`, `openspec/changes/archive/` | да | спецификация системы и журнал решений (`design.md`); без них код в контуре нечем объяснять |
|
||||
| `.claude/`, `CLAUDE.md`, `openspec/config.yaml` | **нет** | инструкции и контекст AI-агента (в `config.yaml` — блоки `context`/`rules` с путями `../docs`, `../fixtures`), инструментарий внешней разработки; `openspec` CLI в контуре нет, `specs/` и `design.md` читаются без него |
|
||||
| `docs/superpowers/` (планы, ledger) | **нет** | рабочие артефакты процесса, содержат локальные пути разработчика |
|
||||
| `Dockerfile`, `.dockerignore` | **нет** | образ для локального `make check-full`; в контуре образ собирает платформа своим CI (шаблон держит `Dockerfile` в `.gitignore`) |
|
||||
| История коммитов | **нет** | снимок одним коммитом; в истории снаружи — трейлеры `Co-Authored-By: Claude …`, `Claude-Session`, ссылки на внешний Gitea |
|
||||
| Репозиторий `docs` | **нет** | контекст, модель, контракты, экспорт ТЗ — остаются снаружи; копия контракта лежит в `api/contracts/` каждого сервиса |
|
||||
| Репозиторий `fixtures` | **нет** | реальная отчётность для регрессии парсера; тесты с маркером `fixtures` в контуре пропускаются |
|
||||
| `reference-old-arm` | **нет** | старый репозиторий банка, читался только для справки |
|
||||
|
||||
Снимок получает файл `SNAPSHOT.md` с хешем коммита-источника и датой: по нему потом сверяется, что уже перенесено.
|
||||
|
||||
## 2. Чего в платформе нет и что есть
|
||||
|
||||
**Нет в контуре (всё это скрипт вырезает или заменяет):**
|
||||
|
||||
- `uv` — `make venv` снаружи ставит окружение через `uv venv --python 3.11` и `uv pip`. В снимке цель `venv` переписана
|
||||
на `python3.11 -m venv` + `pip` с `setup/pip.conf` (Nexus), как делает `setup/setup.sh` шаблона.
|
||||
- Docker и `docker compose` — локальная инфраструктура (`docs/dev/docker-compose.yml`) живёт в `docs`, которого в контуре
|
||||
нет. Цели `make up`, `test-integration`, `check-full` в контуре не запускаются; `docker-build` удалена из `Makefile`.
|
||||
- `docs` и `fixtures` рядом с сервисом — тесты идентичности контракта и тесты на реальных данных пропускаются
|
||||
(раздел 5), сверка ORM с DBML (`make dbml-check` в arm-core) не выполняется.
|
||||
- `tea`, `codegraph`, `openspec` (CLI), хуки `.claude/hooks` — инструментарий процесса; его конфигурация
|
||||
(`.claude/`, `CLAUDE.md`, `openspec/config.yaml`) вырезается.
|
||||
- Внешний Gitea `git.aepif.ru` — ссылки вида `https://git.aepif.ru/RSHB/arm-core/pulls/19` в архивах change'ей
|
||||
скрипт превращает в `RSHB/arm-core#19` (номера относятся к внешнему Gitea, в контуре их нет).
|
||||
|
||||
**Есть только в контуре (снаружи заменено заглушками, в снимке остаётся как есть):**
|
||||
|
||||
- Nexus `nexus.dev.raisa.go.rshbank.ru` (`setup/pip.conf`, `setup/.npmrc`) и пакеты `raisa_fastapi_protected_api`
|
||||
(`api/requirements.txt` arm-core), `raisa-fastapi-protected-web`, `raisa-ui` (`web/package.json`, `optionalDependencies`).
|
||||
Снаружи `make venv` их пропускает (`grep -v '^raisa_'`), импорт `raisa_fastapi_protected_api` условный — только при
|
||||
`DEBUG=false`. В контуре всё ставится штатно.
|
||||
- Запуск платформой: `api/run_server.py`, `SERVER__HOST`/`SERVER__PORT`, ресурсы `raisa_config.yml`, секреты `OPENBAO__*`
|
||||
из OpenBao, Python 3.11 в базовом образе.
|
||||
- PostgreSQL 16 (схема `dkb`), NATS без JetStream (nkeys/JWT), S3-совместимое хранилище, JWT РАИСА для входа.
|
||||
|
||||
## 3. Подготовка снимка: `scripts/prepare-contour-snapshot.sh`
|
||||
|
||||
Запуск из workspace, по одному репозиторию:
|
||||
|
||||
```bash
|
||||
docs/scripts/prepare-contour-snapshot.sh arm-core /tmp/contour
|
||||
docs/scripts/prepare-contour-snapshot.sh tabular-parser /tmp/contour
|
||||
docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour
|
||||
# третий аргумент — ref; по умолчанию origin/main (снимок всегда с сервера, не из рабочего дерева)
|
||||
```
|
||||
|
||||
Что делает скрипт (репозиторий-источник не меняется):
|
||||
|
||||
1. `git archive <ref>` → `/tmp/contour/<repo>/` — дерево коммита без `.git` и без игнорируемых файлов.
|
||||
2. Удаляет `.claude/`, `CLAUDE.md`, `openspec/config.yaml`, `docs/superpowers/`, `Dockerfile`, `.dockerignore`.
|
||||
3. Правит `Makefile`: цель `venv` → `python3.11 -m venv` + `pip install -r requirements-dev.txt` с `setup/pip.conf`
|
||||
(переменная `PYTHON`, по умолчанию `python3.11`); удаляет цель `docker-build` и её упоминания в `check-full`, `.PHONY`,
|
||||
`help`.
|
||||
4. Заменяет ссылки на внешний Gitea на `RSHB/<repo>#<n>`.
|
||||
5. Пишет `SNAPSHOT.md` (источник, хеш, дата, что вырезано).
|
||||
6. Проверяет: нет `.claude`/`CLAUDE.md`/`openspec/config.yaml`/планов/`Dockerfile`, нет локальных путей (`/Users/…`, `/home/…`), нет `uv` и
|
||||
`git.aepif.ru`, на месте платформенные файлы (`raisa_config.yml`, `setup/`, `run_server.py`, `requirements.txt`,
|
||||
`.env.test`, `contracts/`). Любая проблема — код возврата 1 и список.
|
||||
|
||||
Проверено 06.10.2026: снимки arm-core@1d7f2c8, tabular-parser@3b32462, scan-gateway@dffc18a проходят `make check` без
|
||||
`docs` и `fixtures` (1084 / 1087 / 726 passed, пропуски только по причинам из раздела 5).
|
||||
|
||||
## 4. Как положить снимок в контур
|
||||
|
||||
Репозитории контура — проекты GitLab, по одному на сервис, имена как снаружи. Первый перенос и каждый следующий делаются
|
||||
одинаково (вместо PR в GitLab — merge request, в остальном git тот же):
|
||||
|
||||
```bash
|
||||
git clone <url контура>/arm-core contour-arm-core
|
||||
cd contour-arm-core && git switch -c sync/arm-core-1d7f2c8 # ветка по правилам контура
|
||||
rsync -a --delete --exclude .git /tmp/contour/arm-core/ ./ # дерево = снимок, удалённые снаружи файлы исчезают
|
||||
git status --short # прочитать diff: он должен совпадать с изменениями снаружи
|
||||
git add -A
|
||||
git commit -m "sync: arm-core@1d7f2c8 (2026-10-06)" # хеш и дата — из SNAPSHOT.md
|
||||
git push -u origin sync/arm-core-1d7f2c8 # дальше merge request и мерж по регламенту GitLab контура
|
||||
```
|
||||
|
||||
`--delete` обязателен: иначе файлы, удалённые или переименованные снаружи, останутся в контуре. Если в контуре делали
|
||||
локальные правки (раздел 7), сначала вернуть их наружу, иначе `rsync --delete` их перезапишет.
|
||||
|
||||
Порядок сервисов при одной синхронизации: `arm-core` → `tabular-parser` → `scan-gateway` (ядро — потребитель контракта,
|
||||
его копия схемы должна быть не старше копий сервисов). Если снаружи поднималась версия контракта (`api/contracts/*.json`,
|
||||
`CONTRACT_VERSION` в `api/src/contract/common.py`), переносить все три сервиса одним заходом.
|
||||
|
||||
## 5. Проверка в контуре перед merge request
|
||||
|
||||
```bash
|
||||
make venv # python3.11 -m venv + pip через Nexus (вместо setup/setup.sh; тот ставит только прод-зависимости)
|
||||
make check # ruff, mypy, pytest без интеграционных тестов
|
||||
```
|
||||
|
||||
Ожидаемые пропуски (`pytest -rs` покажет причины), это **не** ошибки:
|
||||
|
||||
| Сервис | Пропускается | Причина |
|
||||
|---|---|---|
|
||||
| все | тест идентичности `api/contracts/` с `docs/contracts/` | `docs` нет; идентичность проверена снаружи перед снимком |
|
||||
| все | тесты с маркером `fixtures` (golden-снимки, property-тесты на реальных ОСВ/формах, покрытие словарей) | `fixtures` нет; регрессия на реальных данных — снаружи |
|
||||
| arm-core | сверка ORM с DBML в тестах | DBML в `docs` |
|
||||
| scan-gateway | тесты с реальными ответами ИС Сканирование (`fixtures/ocr`) | то же |
|
||||
|
||||
Не запускать в контуре: `make up`, `make test-integration`, `make check-full`, `make dbml-check`, `make e2e` — им нужны
|
||||
compose из `docs` либо Docker. Интеграционные тесты против PostgreSQL/NATS/MinIO прогоняются снаружи перед каждым снимком;
|
||||
пайплайну платформы отдаётся `make check`.
|
||||
|
||||
Фронт arm-core: `make web-check` (`npm install` через `.npmrc` Nexus, biome, tsc, `npm run build`). В контуре
|
||||
`raisa-fastapi-protected-web` и `raisa-ui` ставятся штатно, снаружи они в `optionalDependencies` и пропускаются.
|
||||
`web/package-lock.json` снаружи не коммитится (OQ-63) — в контуре lock-файл появится при первом `npm install`, решение о его
|
||||
коммите за контуром.
|
||||
|
||||
## 6. Что нужно от платформы для запуска
|
||||
|
||||
Переменные окружения — `api/.env.example` каждого сервиса (имена и смысл), секреты под префиксом `OPENBAO__*`.
|
||||
Минимум для arm-core:
|
||||
|
||||
- `OPENBAO__DATABASE__DATABASE_URL` (asyncpg, схема `dkb`), `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN,
|
||||
MINIO_ADMIN_PASSWORD, MINIO_BUCKET}` (`MINIO_SECURE=true` по умолчанию), `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}`.
|
||||
- `DEBUG=false` (включает middleware РАИСА, выключает `/docs` и dev-токены), `CORS_ORIGINS` (явный список; пусто —
|
||||
кросс-доменные запросы запрещены), `FORWARDED_ALLOW_IPS` (адреса прокси платформы, иначе IP в аудите — адрес прокси),
|
||||
`AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` + `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` — параметры JWT РАИСА (открытый
|
||||
вопрос OQ в `context/07-open-questions.md`). `AUTH_DEV_ISSUER`/`AUTH_DEV_SECRET` в проде не задавать.
|
||||
- Миграции и seed при деплое: из `api/` — `alembic upgrade head`, затем `python -m scripts.seed` (справочники: план счетов,
|
||||
правила элиминации, роли и права; `--dev` только для стенда разработки). Те же команды — цели `make migrate` и `make seed` (`README.md` arm-core,
|
||||
«Быстрый старт», шаг 4 «Миграции и справочники»).
|
||||
|
||||
`tabular-parser` и `scan-gateway`: только `OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `DEBUG`, `NATS_ENABLED=true`; у парсера
|
||||
`PARSE_MAX_BYTES`, `PARSE_TIMEOUT_SECONDS`; у шлюза `SCAN_HOTFOLDER_MODE=smb` и `OPENBAO__SMB__{SMB_HOST, SMB_PATH,
|
||||
SMB_LOGIN, SMB_PASSWORD}` (режим `local` и эмулятор `make emulator` — только для разработки). Subjects NATS, схема ключей S3
|
||||
и ресурсы — `context/03-architecture.md`.
|
||||
|
||||
## 7. Правки внутри контура и обратный поток
|
||||
|
||||
Правило: источник истины — репозитории снаружи. Любая правка в контуре (чаще всего под пайплайн платформы: пути, переменные,
|
||||
`raisa_config.yml`) возвращается наружу до следующей синхронизации:
|
||||
|
||||
```bash
|
||||
cd contour-arm-core
|
||||
git diff <коммит sync> HEAD -- . ':!SNAPSHOT.md' > /tmp/contour-arm-core.patch # база — коммит последнего sync
|
||||
```
|
||||
|
||||
Патч применяется снаружи в ветку и проходит обычный PR; после его мержа следующий снимок уже содержит правку, и
|
||||
`rsync --delete` ничего не потеряет. Если правка касается только контура (например, секретов платформы), её место — вне
|
||||
репозитория: переменные окружения OpenBao, а не код.
|
||||
|
||||
## 8. Чек-лист одной синхронизации
|
||||
|
||||
1. Снаружи: нужные PR слиты в `main` трёх сервисов; `make check-full` зелёный в каждом; контракт в `docs/contracts` и копии
|
||||
`api/contracts` идентичны.
|
||||
2. Правки из контура (раздел 7) возвращены наружу и слиты.
|
||||
3. `prepare-contour-snapshot.sh` по каждому сервису; скрипт завершился словами «снимок готов».
|
||||
4. В контуре: клон, ветка, `rsync --delete`, просмотр `git status`, коммит `sync: <repo>@<hash> (<дата>)`, `make venv`,
|
||||
`make check` (пропуски — только из раздела 5), для arm-core `make web-check`, merge request по регламенту GitLab контура.
|
||||
5. Порядок мержа при смене версии контракта: arm-core → tabular-parser → scan-gateway.
|
||||
6. Снаружи в `context/09-delivery-plan.md` или в issue синхронизации отметить, какие хеши перенесены.
|
||||
|
||||
## 9. Известные неточности снимка
|
||||
|
||||
- `README.md` сервисов описывают `make venv` через `uv` и ссылаются на `../docs`, `../fixtures`, `docs/dev/qa-guide.md`;
|
||||
скрипт их не переписывает. Ориентир для контура — этот документ и `SNAPSHOT.md`.
|
||||
- В `openspec/changes/archive/*/` остаются относительные пути вида `../docs/openspec/...` и `../fixtures/...`: это
|
||||
ссылки на внешний workspace, в контуре они никуда не ведут.
|
||||
- Тесты, которые снаружи пропускаются без `fixtures`, в контуре не выполняются никогда. Поэтому перед каждым снимком снаружи
|
||||
обязателен `make check-full` с доступными `docs` и `fixtures` (`dev/checks.md`, раздел «Порядок перед PR»).
|
||||
246
dev/frontend-auth-guide.md
Normal file
246
dev/frontend-auth-guide.md
Normal file
@ -0,0 +1,246 @@
|
||||
# Аутентификация и авторизация arm-core: инструкция для разработчика фронта
|
||||
|
||||
Источник — код `arm-core` на `main` (06.10.2026): `api/src/core/auth.py`, `api/src/api/dependencies/auth.py`,
|
||||
`api/src/api/endpoints/{me,dev_auth}.py`, `api/src/domain/policy.py`, эталонный клиент `web/src/{api,auth}/`; спека —
|
||||
`arm-core/openspec/specs/authn-authz/spec.md`; роли и права — `context/05-domain-rules.md` §10. Здесь — только то,
|
||||
что нужно, чтобы писать клиент. Правила предметной области и контракты экранов — в `api-contract-slice-4.md` и
|
||||
`context/10-user-journey.md`.
|
||||
|
||||
## 1. Модель в трёх предложениях
|
||||
|
||||
1. **Сессий на сервере нет.** Каждый запрос к `/api/v1/*` несёт `Authorization: Bearer <JWT>`; сервер проверяет
|
||||
подпись и срок, берёт из токена email, находит пользователя в `app_user` и его права. Нет cookie, нет refresh,
|
||||
нет logout-ручки: «выйти» = забыть токен на клиенте.
|
||||
2. **Права — данные, роли — нет.** Что можно пользователю, решает множество кодов прав из `GET /me → permissions`
|
||||
(объединение прав всех его ролей). Код роли (`KA_RF`, `ADMIN`…) в бизнес-логике сервера **не проверяется** и
|
||||
фронтом использоваться для ветвления не должен: матрица «роль → право» — черновик, администратор правит её в UI.
|
||||
3. **Клиент только прячет, сервер решает.** Скрывать пункты меню и кнопки по `permissions` можно и нужно, но любой
|
||||
запрос может вернуть 401/403/404/409 — клиент обязан их обрабатывать, а не считать, что «кнопка была видна, значит
|
||||
можно».
|
||||
|
||||
Порядок проверок на любом защищённом маршруте: **право (403) → видимость объекта (404) → статус/блокировка (409) →
|
||||
операция**. Первые два — предмет этой инструкции.
|
||||
|
||||
## 2. Два режима получения токена
|
||||
|
||||
| | Разработка (`DEBUG=true`, `AUTH_DEV_ISSUER=true`) | Прод (`DEBUG=false`) |
|
||||
|---|---|---|
|
||||
| Откуда токен | `POST /api/v1/dev/token` по email, без пароля | SSO РАИСА (Keycloak); SPA получает JWT от SSO и подставляет в заголовок |
|
||||
| Подпись | HS256, общий секрет `AUTH_DEV_SECRET` | RS256/ES256 по `AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` |
|
||||
| Срок | 8 часов, продления нет | задаёт РАИСА |
|
||||
| Что в токене | `sub` = email, `email` = email, `iat`, `exp` | `sub`, `exp`, email в claim `AUTH_EMAIL_CLAIM` (по умолчанию `email`) |
|
||||
| Ручка `/dev/token` | есть | **не существует** (404 `{"detail":"Not Found"}`), dev-токены отвергаются 401 |
|
||||
| CORS | `*` без credentials | только источники из `CORS_ORIGINS`; токен в заголовке, cookie не нужны |
|
||||
| `/docs`, `/openapi.json` | есть | 404 |
|
||||
|
||||
**Параметры JWT РАИСА неизвестны** (OQ-54 в `context/07-open-questions.md`): URL JWKS, `iss`, `aud`, имя claim с
|
||||
email, как именно SPA получит токен (в старом АРМ — Keycloak через `REACT_APP_KEYCLOAK_URL/REALM/CLIENT_ID`, в
|
||||
`web/package.json` подключён `raisa-fastapi-protected-web` как optionalDependency). Поэтому слой «получить токен» на
|
||||
фронте держите отдельно от слоя «использовать токен»: второй уже не изменится, первый в проде заменится целиком.
|
||||
|
||||
## 3. Ручки
|
||||
|
||||
Префикс везде `/api/v1`. Форматы тела ошибки `{"detail": …}` — три: **строка** (401/403/404 «Not Found»/405/413),
|
||||
**объект** `{code, message, …}` (доменные ошибки, в том числе 404 `NOT_FOUND` и 409), **массив** pydantic
|
||||
(`[{loc, msg, type}]`, 422 валидации тела). Эталонный разбор — `web/src/api/client.ts:parseErrorBody`.
|
||||
|
||||
### 3.1 `POST /dev/token` — dev-токен (только разработка)
|
||||
|
||||
Без авторизации. Тело `{"email": "ka_rf@armka.local"}` (регистр и пробелы по краям не важны).
|
||||
|
||||
| Ответ | Когда |
|
||||
|---|---|
|
||||
| 200 `{"access_token": "<JWT>", "token_type": "bearer"}` | активный пользователь с таким email есть; в журнал пишется `LOGIN` |
|
||||
| 404 строка `пользователь не найден или отключён` | email не найден или `is_active = false` |
|
||||
| 409 строка `неоднозначная учётная запись` | в БД несколько пользователей с email, различающимся регистром |
|
||||
| 422 массив | пустой email |
|
||||
| 404 `{"detail":"Not Found"}` | ручка выключена (`DEBUG=false` или `AUTH_DEV_ISSUER=false`) — так выглядит прод |
|
||||
|
||||
Dev-пользователи (`make seed-dev`), по одному на роль (тот же `make seed-dev` создаёт и dev-ГК с ЮЛ под документы из
|
||||
`fixtures` — ГК «Русский Бисквит» и три ГК пакета ДКБ, см. `qa-guide.md` раздел 4.2; экраны справочников и консолидации не
|
||||
пустые сразу после сида):
|
||||
|
||||
| Email | Роль | Филиал | Видимость |
|
||||
|---|---|---|---|
|
||||
| `admin@armka.local` | ADMIN | Головной офис | всё; единственный с `USERS_ADMIN` и `DIRECTORY_ADMIN` |
|
||||
| `ka_go@armka.local` | KA_GO | Головной офис | всё, правки |
|
||||
| `curator_go@armka.local` | CURATOR_GO | Головной офис | всё, правки справочников |
|
||||
| `expert@armka.local` | EXPERT | Головной офис | всё, только чтение |
|
||||
| `ka_rf@armka.local` | KA_RF | Ростовский РФ | свой филиал, правки |
|
||||
| `rka@armka.local` | RKA | Ростовский РФ | свой филиал |
|
||||
| `client_manager@armka.local` | CLIENT_MANAGER | Краснодарский РФ | свой филиал, чтение + загрузка документов |
|
||||
|
||||
Для проверок разными ролями берите несколько токенов — они независимы, живут в разных вкладках.
|
||||
|
||||
### 3.2 `GET /me` — кто я и что мне можно
|
||||
|
||||
Нужен любой действующий токен; прав не требует. Единственная ручка, которую вызывают «просто с токеном».
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"email": "ka_rf@armka.local",
|
||||
"full_name": "Dev: Аналитик РФ",
|
||||
"regional_office": { "id": "uuid", "name": "Ростовский региональный филиал", "is_head_office": false },
|
||||
"business_segment": "DKB",
|
||||
"roles": ["KA_RF"],
|
||||
"permissions": ["CONSOLIDATION_EDIT", "CONSOLIDATION_VIEW", "DIRECTORY_EDIT", "…"]
|
||||
}
|
||||
```
|
||||
|
||||
`roles` и `permissions` — коды, отсортированы по алфавиту. `business_segment` может быть `null`.
|
||||
`regional_office.is_head_office` — единственное, что фронту полезно знать о видимости: пользователь ГО видит всё,
|
||||
пользователь филиала — только ГК своего филиала (деталей клиент не вычисляет, см. §5).
|
||||
|
||||
**Когда дёргать.**
|
||||
|
||||
- Сразу после получения токена (dev-форма или SSO) — чтобы построить меню и контекст пользователя.
|
||||
- При старте приложения, если токен уже сохранён (перезагрузка страницы): ответ 200 — пользователь «вошёл», 401 —
|
||||
токен протух, показать вход.
|
||||
- **Не** перечитывается автоматически после того, как администратор поменял роли или права: пользователь увидит
|
||||
изменения после перезагрузки страницы. Это известное ограничение, не баг клиента.
|
||||
|
||||
**Ответы `/me` и что они значат.**
|
||||
|
||||
| Ответ | Смысл | Что делать клиенту |
|
||||
|---|---|---|
|
||||
| 200 | вошёл | сохранить профиль в контексте, строить UI по `permissions` |
|
||||
| 401 строка (есть `WWW-Authenticate: Bearer`) | токена нет, он просрочен, подпись не прошла, «аутентификация не настроена» на сервере | сбросить токен, показать вход |
|
||||
| 403 строка `пользователь не зарегистрирован в АРМ` / `учётная запись отключена` / `неоднозначная учётная запись` | **токен валиден, но этого человека нет в АРМ** | показать «нет доступа в АРМ, обратитесь к администратору»; повторный вход не поможет, токен сбрасывать не обязательно |
|
||||
|
||||
### 3.3 Всё остальное — защищённые маршруты
|
||||
|
||||
Каждый маршрут объявляет одно право. Полные таблицы «метод и путь → право» — `arm-core/README.md` (разделы
|
||||
«API справочников», «API файлов и документов», «Консолидация»), для Среза 4 — `api-contract-slice-4.md` §2. Сводка:
|
||||
|
||||
| Область | Чтение | Правки | Прочее |
|
||||
|---|---|---|---|
|
||||
| `/directories/regional-offices`, `/company-groups`, `/related-groups`, `/legal-entities` | `DIRECTORY_VIEW` | `DIRECTORY_EDIT` | удаление ГСК/алиаса — `DIRECTORY_DELETE`; `POST /directories/import` — `DIRECTORY_ADMIN` |
|
||||
| `/legal-entities/{id}/files`, `/files/{id}`, `/documents/{id}` | `DOCUMENT_VIEW` | `DOCUMENT_EDIT` (загрузка, удаление, подтверждение) | |
|
||||
| `/consolidations`, `/company-groups/{id}/consolidations`, `/consolidations/versions/{id}/*` | `CONSOLIDATION_VIEW` | `CONSOLIDATION_EDIT` (создание, блокировка, статус, периметр, сопоставление) | экспорт xlsx — `CONSOLIDATION_VIEW` |
|
||||
| `/admin/users`, `/admin/roles`, `/admin/permissions` | `USERS_ADMIN` | `USERS_ADMIN` | проверка права — до разбора тела: без права 403 всегда |
|
||||
| `/me` | любой пользователь | — | |
|
||||
| `/healthcheck` (без префикса `/api/v1`) | без токена | — | живость сервера, не признак «вошёл» |
|
||||
|
||||
Права `KFO_*`, `EX_VIEW`, `EXPORT`, `AUDIT_VIEW` в каталоге есть, но маршрутов под них пока нет (Срезы 5–7).
|
||||
|
||||
Ответы, общие для всех защищённых маршрутов:
|
||||
|
||||
| Ответ | Смысл | Что делать клиенту |
|
||||
|---|---|---|
|
||||
| 401 строка | как у `/me` | сбросить токен, экран входа — **глобально**, одним обработчиком в HTTP-клиенте |
|
||||
| 403 строка `недостаточно прав: требуется <КОД>` | права нет | показать «нет прав»; обычно кнопка не должна была показываться — проверить `permissions` |
|
||||
| 403 строка про «не зарегистрирован / отключена / неоднозначная» | пользователя удалили или отключили, пока токен жив | как у `/me`: «нет доступа в АРМ» |
|
||||
| 404 объект `{"code": "NOT_FOUND", …}` | объекта нет **или он вне видимости пользователя** — ответы неразличимы по статусу и коду | показать «не найдено»; не ветвиться по тексту `message` |
|
||||
| 405 строка, заголовок `Allow` | неверный метод на существующий маршрут | ошибка клиента |
|
||||
| 404 `{"detail":"Not Found"}` строкой | маршрута нет (в том числе `/dev/token` в проде) | ошибка клиента или выключенный флаг |
|
||||
| 409 / 422 объект | доменная ошибка (`LOCK_REQUIRED`, `LOCKED_BY_OTHER`, `VERSION_NOT_EDITABLE`, `LAST_ADMIN`, …) | ветвиться по `code`; каталог — README «Ошибки API» |
|
||||
|
||||
Как отличить два вида 403 программно: оба — строки без кода. Надёжный признак — **контекст запроса**: 403 на `/me`
|
||||
бывает только аутентификационным; на остальных маршрутах 403 почти всегда «недостаточно прав». Если нужно различать
|
||||
точнее, при 403 с текстом не про право повторите `GET /me`: он вернёт тот же 403 аутентификации.
|
||||
|
||||
## 4. Права и роли: что есть сейчас
|
||||
|
||||
Каталог прав (16 кодов, `api/seed/core/permissions.yaml`; формат `<ОБЪЕКТ>_<ДЕЙСТВИЕ>`):
|
||||
|
||||
| Объект | Коды |
|
||||
|---|---|
|
||||
| DIRECTORY | `DIRECTORY_VIEW`, `DIRECTORY_EDIT`, `DIRECTORY_DELETE`, `DIRECTORY_ADMIN` (импорт) |
|
||||
| DOCUMENT | `DOCUMENT_VIEW`, `DOCUMENT_EDIT` |
|
||||
| CONSOLIDATION | `CONSOLIDATION_VIEW`, `CONSOLIDATION_EDIT` |
|
||||
| KFO | `KFO_VIEW`, `KFO_EXPORT`, `KFO_APPROVE`, `KFO_VERIFY` |
|
||||
| EX | `EX_VIEW` |
|
||||
| EXPORT | `EXPORT` |
|
||||
| USERS | `USERS_ADMIN` |
|
||||
| AUDIT | `AUDIT_VIEW` |
|
||||
|
||||
Роли (7 системных, `roles.yaml`): `KA_RF`, `KA_GO`, `RKA`, `CLIENT_MANAGER`, `CURATOR_GO`, `EXPERT`, `ADMIN`.
|
||||
Стартовая матрица «роль → права» — `role_permissions.yaml` и таблица §10 `05-domain-rules.md`. Новых прав и ролей
|
||||
через API не создать (422 `UNKNOWN_PERMISSION` / `UNKNOWN_ROLE`); права роли меняет `PUT /admin/roles/{code}/permissions`,
|
||||
роли пользователя — `PUT /admin/users/{id}/roles`.
|
||||
|
||||
Правила для фронта:
|
||||
|
||||
- Константы кодов прав — в одном модуле (`web/src/auth/permissions.ts`); завести код в клиенте можно только если он есть
|
||||
в `permissions.yaml`. Типы `PermissionObject`/`PermissionAction` — `web/src/api/types.ts`.
|
||||
- Элемент UI привязывается к **праву**, а не к роли: «кнопка «Взять в работу» видна при `CONSOLIDATION_EDIT`», а не
|
||||
«видна КА». Пользователь может иметь несколько ролей; `permissions` — уже объединение.
|
||||
- `roles` из `/me` — только для отображения («Роль: Кредитный аналитик РФ») и экрана администратора.
|
||||
|
||||
## 5. Чего клиент не вычисляет: policy-слой
|
||||
|
||||
Поверх прав сервер применяет правила видимости и редактируемости (`domain/policy.py`), которые фронт **не дублирует**:
|
||||
|
||||
- **Видимость по филиалу.** Пользователь ГО (`regional_office.is_head_office = true`) видит все ГК, ЮЛ и версии;
|
||||
пользователь филиала — ГК, закреплённые за его филиалом, их ЮЛ и ГСК, монозаёмщиков своего филиала. Списки
|
||||
уже отфильтрованы сервером; чужой объект по id — 404 `NOT_FOUND`, как несуществующий.
|
||||
- **Редактируемость версии консолидации.** Правки — только в статусах «в работе» (`INITIATED`,
|
||||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`) и при своей активной блокировке; иначе 409 `VERSION_NOT_EDITABLE`,
|
||||
`LOCK_REQUIRED`, `LOCKED_BY_OTHER`. Для UI признаки есть в карточке версии (`VersionCard.is_in_work`, `lock`),
|
||||
см. `api-contract-slice-4.md`.
|
||||
- **Зона обслуживания** (`business_segment`) пока не фильтрует (OQ-86).
|
||||
|
||||
## 6. Как это сделано в эталонном клиенте `web/`
|
||||
|
||||
Повторять не обязательно, но контракт с бэкендом здесь уже отработан.
|
||||
|
||||
| Файл | Что делает |
|
||||
|---|---|
|
||||
| `src/api/client.ts` | `apiFetch`: база `${REACT_APP_API_URL}/api/v1`, ставит `Authorization: Bearer` из `token.ts`; на **401 сбрасывает токен и шлёт событие `armka:unauthorized` на `window`**; парсит три формы `detail` в `ApiError {status, code, message}`; сетевую ошибку отдаёт как `ApiError(0, 'NETWORK')`; 204 и пустое тело → `undefined` |
|
||||
| `src/auth/token.ts` | токен в `sessionStorage` (ключ `armka.token`) с запасом в памяти, если хранилище недоступно. Вкладка закрылась — токен пропал; новая вкладка — новый вход (решение D-9) |
|
||||
| `src/auth/useMe.ts` | загружает `/me`, если токен есть; слушает `armka:unauthorized` и сбрасывает профиль с текстом «Сессия истекла» |
|
||||
| `src/auth/AuthGate.tsx` | корневая обёртка: `loading` → спиннер; нет профиля → экран входа; есть → `MeContext.Provider` |
|
||||
| `src/auth/MeContext.ts` | `useCurrentUser()` — профиль из контекста (без повторных запросов `/me`); вне `AuthGate` бросает |
|
||||
| `src/auth/DevLogin.tsx` | форма «Вход для разработки» → `POST /dev/token` → `setToken` → `reload`. В production-сборке **вырезана** по `process.env.NODE_ENV`; вместо неё заглушка «Вход выполняется через РАИСА» с кнопкой «Повторить» |
|
||||
| `src/auth/permissions.ts` | константы кодов прав для UI |
|
||||
| `src/widgets/Sidebar/Sidebar.tsx` | пункты меню с полем `permission` фильтруются по `me.permissions` |
|
||||
| `src/layouts/Admin.tsx` | без `USERS_ADMIN` показывает «Нет доступа» и **не делает запросов** к `/admin/*` |
|
||||
|
||||
Окружение фронта: `REACT_APP_API_URL` (адрес бэкенда, в dev `http://localhost:8000`) и `REACT_APP_ROOT_PATH`
|
||||
(= `ROOT_PATH` бэкенда, в dev `/`); обе обязательны при `npm run start`. Для проверки входа используйте
|
||||
`npm run start`, а не `npm run build`: в production-сборке dev-формы нет.
|
||||
|
||||
## 7. Чек-лист для нового экрана или клиента
|
||||
|
||||
1. Все запросы — через единый HTTP-клиент, который сам подставляет `Bearer` и глобально обрабатывает 401.
|
||||
2. После получения токена и при старте с сохранённым токеном — один `GET /me`; профиль — в контексте, не в каждом экране.
|
||||
3. Видимость элементов — по `me.permissions`, константы — из одного модуля; роли не проверять.
|
||||
4. 403 — показать причину из `detail`; 404 `NOT_FOUND` — «не найдено», без ветвления по тексту; 409/422 — по `code`.
|
||||
5. Деньги в ответах — строки `"100.00"`, не парсить во `float` (общее правило контрактов).
|
||||
6. Токен не писать в URL, `localStorage` и логи; на сервере он тоже не логируется.
|
||||
7. Слой получения токена (dev-форма сейчас, SSO потом) изолирован от слоя использования.
|
||||
|
||||
## 8. Проверка руками
|
||||
|
||||
```bash
|
||||
# токен
|
||||
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
|
||||
-d '{"email":"ka_rf@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
# профиль
|
||||
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
|
||||
# 401: без заголовка
|
||||
curl -s -i localhost:8000/api/v1/me | head -5
|
||||
# 403 по праву: KA_RF без USERS_ADMIN
|
||||
curl -s localhost:8000/api/v1/admin/users -H "Authorization: Bearer $TOKEN"
|
||||
# 404 dev-ручки в проде — так же выглядит и несуществующий путь
|
||||
curl -s localhost:8000/api/v1/no-such-route -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
В Swagger (`/docs`, только при `DEBUG=true`): **Authorize** (замок справа вверху) → вставить токен без слова `Bearer` → все
|
||||
защищённые ручки получают заголовок. Кнопка есть с arm-core #29 (схема `bearerAuth` в `openapi.json`,
|
||||
`components.securitySchemes`); на стенде старше этого PR кнопки нет — проверяйте curl. Публичные ручки без замка: `POST /dev/token`,
|
||||
`GET /healthcheck`.
|
||||
|
||||
## 9. Открытые вопросы, которые затронут фронт
|
||||
|
||||
| # | Вопрос | Что менять на фронте, когда решится |
|
||||
|---|---|---|
|
||||
| OQ-54 | Параметры JWT РАИСА и способ получения токена SPA (Keycloak? `raisa-fastapi-protected-web`?) | только слой получения токена: заменить `DevLogin`/`ProdNotice` на реальный SSO-поток; редирект на вход при 401 |
|
||||
| — | Обновление токена (refresh) и его срок в проде | если SSO даст refresh — обновлять в HTTP-клиенте перед 401 |
|
||||
| — | Выход (`LOGOUT` в аудит не пишется, сессий нет) | кнопка «Выйти» = `clearToken()` + редирект на SSO logout, если он будет |
|
||||
| — | Перечитывание `/me` после смены ролей администратором | сейчас — перезагрузка страницы |
|
||||
|
||||
Аудит входа (OQ-64, закрыт): сервер пишет `LOGIN` при выдаче dev-токена и при первом успешном запросе пользователя
|
||||
после старта процесса, `LOGIN_FAILED` — при 401 с предъявленным токеном и при 403 аутентификации. Фронт для этого
|
||||
ничего не делает.
|
||||
@ -21,6 +21,12 @@ Node 24, uv 0.10): от пустого каталога до экрана адм
|
||||
- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых
|
||||
документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и
|
||||
контрольные цифры — 7.1–7.3). UI консолидации — другой исполнитель.
|
||||
- План счетов — полная выгрузка 1С заказчика, 488 строк (arm-core #25): субсчета реальных ОСВ (66.01, 76.07.x, забалансовые,
|
||||
буквенные `01.К`) резолвятся в `account_id`. Границы входов консолидации: периметр ≤ 500 ЮЛ, комментарий статуса ≤ 1000, поиск
|
||||
`q` ≤ 500 символов (arm-core #26, раздел 6.5).
|
||||
- Парсер ОСВ после пакета ДКБ (arm-tabular-parser #13, #17): скользящие периоды, несколько корней без строки счёта, «Итого
|
||||
развернутое», шапка без меток, ИНН в тексте названия и строкой выше (#15); тесты на 25 файлах `osv-dkb/**` по манифесту
|
||||
ожиданий, golden — 113 файлов (раздел 6.7).
|
||||
|
||||
**Нет (это не баги):**
|
||||
|
||||
@ -86,7 +92,7 @@ docker compose -f ../docs/dev/docker-compose.yml ps
|
||||
make venv # api/.venv на Python 3.11, зависимости из PyPI
|
||||
cp api/.env.example api/.env # dev-настройки: DEBUG=true, AUTH_DEV_ISSUER=true
|
||||
make migrate # схема БД
|
||||
make seed-dev # справочники + 7 dev-пользователей
|
||||
make seed-dev # справочники + 7 dev-пользователей + dev-ГК под документы fixtures (см. ниже)
|
||||
cd api && .venv/bin/python run_server.py
|
||||
```
|
||||
|
||||
@ -118,6 +124,13 @@ REACT_APP_API_URL=http://localhost:8000 REACT_APP_ROOT_PATH=/ npm run start
|
||||
Настройки из `api/.env.example` (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `CORS_ORIGINS=`, `FORWARDED_ALLOW_IPS=127.0.0.1`)
|
||||
для локального стенда менять не нужно: `DEBUG=true` включает документацию API на `/docs`, dev-вход и разрешает `make seed-dev`.
|
||||
|
||||
`make seed-dev` (arm-core #31) создаёт, кроме пользователей, **dev-ГК под документы из `fixtures`**: ГК «Русский Бисквит» ровно как в
|
||||
выгрузке ЕЦП CRM (20 ЮЛ/ИП, филиал «Санкт-Петербургский РФ», ГСК из АО «Русский бисквит» `3528052718` и ООО «Пекарня
|
||||
Счастливочная» `3528104476`, алиасы под имена контрагентов из ОСВ) и ГК владельцев форм пакета ДКБ («Унагранде», «Новороссийский
|
||||
прокатный завод», «Агрофирма Межениновская»). Поэтому ОСВ и формы из `../fixtures` загружаются сразу на «свои» ЮЛ (6.6, 7.1), а
|
||||
импорт выгрузки ЕЦП (6.3) на таком стенде отвечает `GK_EXISTS`/`LE_EXISTS`. Стенд без dev-ГК (пустые справочники, как в тестах
|
||||
ядра): `cd api && .venv/bin/python -m scripts.seed --dev --no-dev-groups`.
|
||||
|
||||
### 4.4 Вход
|
||||
|
||||
В форме введите **email** dev-пользователя (не имя и не логин) и нажмите «Войти (dev)». Пароля нет. Это единственная форма
|
||||
@ -136,6 +149,9 @@ REACT_APP_API_URL=http://localhost:8000 REACT_APP_ROOT_PATH=/ npm run start
|
||||
|
||||
### 4.5 Токен для Swagger и curl
|
||||
|
||||
В Swagger (`/docs`) токен подставляется кнопкой **Authorize** (замок справа вверху): вставьте значение `access_token` без слова
|
||||
`Bearer`, дальше все защищённые ручки отправляют заголовок сами. Кнопка появилась с arm-core #29; если её нет — бэкенд старше, используйте curl:
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
|
||||
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
@ -210,8 +226,9 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger)
|
||||
|
||||
Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на свежем стенде или после
|
||||
сброса (раздел 9), иначе счётчики будут другими.
|
||||
Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на стенде **без dev-ГК**: после сброса
|
||||
(раздел 9) засейте `python -m scripts.seed --dev --no-dev-groups` вместо `make seed-dev`, иначе ГК «Русский Бисквит» и её ЮЛ уже
|
||||
есть и счётчики будут другими (I2 даст `GK_EXISTS`/`LE_EXISTS`, `offices_created` 0).
|
||||
|
||||
| № | Шаги | Ожидание |
|
||||
|---|---|---|
|
||||
@ -253,6 +270,10 @@ docker compose -f ../docs/dev/docker-compose.yml exec postgres \
|
||||
| N2 | токен для несуществующего email (`nobody@armka.local`) через `POST /dev/token` | 404 `пользователь не найден или отключён`, токен не выдан |
|
||||
| N3 | тело `PATCH` ГК с неизвестным полем | 422 (поля вне схемы запрещены) |
|
||||
| N4 | импорт файла больше 20 МБ | 413 |
|
||||
| N5 | `PUT /consolidations/versions/{id}/members` с 501 `legal_entity_ids` (можно повторить один id) | 422, тело — массив pydantic `detail[] {loc: ["body", "legal_entity_ids"], …}`, без `code` |
|
||||
| N6 | `POST …/status` с `comment` длиннее 1000 символов | 422, массив pydantic с `loc: ["body", "comment"]` |
|
||||
| N7 | `GET …/osv?q=<501 символ>` и `GET …/osv/export?q=<501 символ>` | 422, массив pydantic с `loc: ["query", "q"]`; 500 символов — 200 |
|
||||
| N8 | `DELETE /directories/legal-entities/{id}/aliases/{alias_id}` для уже перенесённого алиаса | 404 `NOT_FOUND`, записи аудита об удалении нет |
|
||||
|
||||
### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)
|
||||
|
||||
@ -261,14 +282,16 @@ docker compose -f ../docs/dev/docker-compose.yml exec postgres \
|
||||
`PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды).
|
||||
|
||||
Подготовка: пользователь `ka_go` (права `DOCUMENT_EDIT`, `DOCUMENT_VIEW`, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ —
|
||||
`АО "Русский бисквит"`, ИНН `3528052718` (если вы делали импорт I2 — найдите его в `GET /directories/legal-entities?q=бисквит`
|
||||
и используйте существующее, иначе создайте как в D9). Для проверки «чужого филиала» понадобится ГК с филиалом.
|
||||
`АО "Русский бисквит"`, ИНН `3528052718`. После `make seed-dev` оно уже есть («РУССКИЙ БИСКВИТ, АО» с алиасом под шапку):
|
||||
`GET /directories/legal-entities?q=бисквит` — берите его `id`; создавать как в D9 нужно только на стенде без dev-ГК.
|
||||
Для проверки «чужого филиала» понадобится ГК с филиалом (dev-ГК «Унагранде» — Головной офис, «Агрофирма Межениновская» —
|
||||
Ростовский РФ).
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
|
||||
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
H="Authorization: Bearer $TOKEN"
|
||||
# ЮЛ (пропустите, если уже есть)
|
||||
# ЮЛ — только на стенде без dev-ГК (после make seed-dev оно есть: возьмите id из GET …/legal-entities?q=бисквит)
|
||||
LE=$(curl -s -X POST localhost:8000/api/v1/directories/legal-entities -H "$H" -H 'Content-Type: application/json' \
|
||||
-d '{"inn":"3528052718","entity_type":"ЮЛ","name":"АО \"Русский бисквит\""}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
|
||||
# загрузка (U1)
|
||||
@ -337,22 +360,26 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
|
||||
|---|---|---|
|
||||
| P1 | Лист | найден каждый лист с текстом «Оборотно-сальдовая ведомость» (один `documents[]` на лист, `locator = sheet:<имя>`); листы без ОСВ (справки) пропущены с `warnings` `skipped: …`, а не ошибка; листы форм ББ/ОФР — отдельные документы, см. «Формы ББ/ОФР» ниже |
|
||||
| P2 | Шапка | `organization_name`, `organization_inn` (если есть в шапке), `account_code_raw`, `period_text` → `period_year`, `period_quarter`, `is_cumulative` соответствуют тексту над таблицей. Поле не найдено → `null` + предупреждение, не ошибка |
|
||||
| P3 | Период | «1 квартал» → `Q1`, нарастающий; «2 квартал» → `Q2`, квартальный; «полугодие», «6 месяцев», «январь – июнь» → `Q2`, нарастающий; «9 месяцев», «3 квартал» → `Q3`; «2024 г.», «12 месяцев» → `Q4`, нарастающий; римские кварталы («I кв.») тоже |
|
||||
| P3 | Период | «1 квартал» → `Q1`, нарастающий; «2 квартал» → `Q2`, квартальный; «полугодие», «6 месяцев», «январь – июнь» → `Q2`, нарастающий; «9 месяцев», «3 квартал» → `Q3`; «2024 г.», «12 месяцев» → `Q4`, нарастающий; римские кварталы («I кв.») тоже. Диапазон месяцев: «Апрель – Июнь 2025 г.» → `Q2`, квартальный; **скользящий** «Октябрь 2024 г. – Сентябрь 2025 г.» → `period_year = 2025`, `period_quarter` и `is_cumulative` — `null`, в `warnings` «не с начала года» (ядро такой документ к версии не привяжет, OQ-131). Период отдельной ячейкой без «за» («9 месяцев 2025 г.» в строке «Бухгалтерский \| …») тоже распознаётся |
|
||||
| P4 | Колонки сумм | шесть сумм разнесены верно: сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт. Проверьте 2–3 строки с ненулевыми значениями во всех шести колонках и строку «Итого» |
|
||||
| P5 | Иерархия | уровни строк (`level`, `parent_row_number`) повторяют дерево в Excel: счёт → субсчёт → контрагент → договор. Стратегия — в `warnings` `hierarchy=outline|indent|account_pattern|sums`; если `sums` — дерево восстановлено по равенству сумм, посмотрите внимательнее |
|
||||
| P6 | Типы строк | `group_type`: `ACCOUNT` (код счёта), `COUNTERPARTY`, `CONTRACT`, `DEPARTMENT`, `OTHER`, `TOTAL` (строка «Итого», одна на лист). Строки «Вал.», «Кол.», «USD» → `data_type` `CURRENCY`/`QUANTITY`; «НУ» → `NU` |
|
||||
| P7 | ИНН | если в таблице есть колонка ИНН (по заголовку или по содержимому, когда ≥ 80 % значений — 10/12 цифр), `counterparty_inn` заполнен у контрагентов и **не** заполнен у счетов и договоров; КПП и номера договоров за ИНН не принимаются |
|
||||
| P6 | Типы строк | `group_type`: `ACCOUNT` (код счёта), `COUNTERPARTY`, `CONTRACT`, `DEPARTMENT`, `OTHER`, `TOTAL` (строка «Итого»; в 1С бывает вторая — «Итого развернутое», обе `TOTAL` уровня 0, первая — основная). Строки «Вал.», «Кол.», «USD» → `data_type` `CURRENCY`/`QUANTITY`; «НУ» → `NU` |
|
||||
| P7 | ИНН | если в таблице есть колонка ИНН (по заголовку или по содержимому, когда ≥ 80 % значений — 10/12 цифр), `counterparty_inn` заполнен у контрагентов и **не** заполнен у счетов и договоров; КПП и номера договоров за ИНН не принимаются. Без колонки ИНН: ИНН берётся из **хвоста названия** («Полиграф, 3528315710», «(ИНН 7700000000)», «ИНН: …») — `counterparty_name` без ИНН и разделителя, `row_value` как в файле; **строка только с ИНН над названием** склеивается со следующей строкой того же уровня и из `rows` исчезает (нумерация `row_number` с пропуском — это нормально); ячейка «ИНН/КПП» даёт ИНН. Приоритет: колонка → строка выше → хвост названия |
|
||||
| P8 | Числа | суммы — строки с двумя знаками (`"1234567.89"`); текстовые числа «1 234 567,89», «(1 234,00)», «—» разобраны как число, отрицательное, `null`; пустая ячейка — `null`, не `0` |
|
||||
| P9 | Контрольные суммы | `status = done`: сальдо строки сходится (начало + обороты = конец), «сумма детей = родитель», «Итого» = корень. `needs_check` при расхождении, в `checks` — какие строки; «подгонки» под сходимость быть не должно |
|
||||
| P9 | Контрольные суммы | `status = done`: сальдо строки сходится (начало + обороты = конец), «сумма детей = родитель», «Итого» = корню **или** сумме строк уровня 0 (несколько корней 58.01/58.03 без строки «58» — вариант 2, Новоросметалл/Агрокомплекс). `needs_check` при расхождении, в `checks` — какие строки; «подгонки» под сходимость быть не должно |
|
||||
| P10 | Лишние колонки | «КПП», «Код», «Количество» и прочие попали в `extra` строки, суммы не сдвинулись |
|
||||
| P11 | Шапка без меток | ОСВ без строки «по счету …» и с колонками «Счет: Код / Наименование» (ОСВ 63 Новоросметалл): счёт берётся из первой строки-счёта таблицы (в `warnings` об этом сказано), период и тип данных («Бухгалтерский») — из отдельных ячеек; строка сумм без подписи принимается за «Итого» с предупреждением |
|
||||
|
||||
**Реальные нестандартные файлы** — `../fixtures/osv-dkb/` (пакет ДКБ 06.10.2026: шесть организаций, многолистовые книги,
|
||||
вариант 2 «сумма», скользящий период, 50 тыс. строк; ожидаемые статусы и находки — `osv-dkb/README.md`) и
|
||||
`../fixtures/analysis-reports/` («Анализ счета», ожидается `INVALID_DOCUMENT`). **Готовые кейсы синтетики** — синтетика `../fixtures/osv/synthetic/s01…s19` с описанием каждого файла и
|
||||
вариант 2 «сумма», скользящий период, 50 тыс. строк). Ожидаемый статус каждого листа — манифест
|
||||
`tabular-parser/api/tests/osv_dkb_expectations.yaml` (данные с причиной: чего там нет, должно быть `done`); golden-снимки есть у 22 файлов
|
||||
до 1 МиБ (`golden/expected_osv_parse/osv-dkb/`), три книги Агрокомплекса по 9–14 МБ проверяются только инвариантами.
|
||||
`../fixtures/analysis-reports/` («Анализ счета») — ожидается `INVALID_DOCUMENT`. **Готовые кейсы синтетики** — `../fixtures/osv/synthetic/s01…s19` с описанием каждого файла и
|
||||
ожиданием в `../fixtures/osv/synthetic/README.md`: шапка варианта 2 («Период: …», «Субконто»), уровни без outline (только
|
||||
отступами, только по кодам счетов), строки валюты и количества, числа текстом, два листа ОСВ и посторонний лист, нет
|
||||
сальдо на начало и свёрнутое сальдо на конец, колонка КПП, названия субсчетов, сломанная сумма (`needs_check`), повтор
|
||||
заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков. Прогоните их способом Б и сверьте с README.
|
||||
заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков, ИНН в названии / строкой выше / «ИНН/КПП»
|
||||
(s17–s19). Прогоните их способом Б и сверьте с README.
|
||||
|
||||
**Формы ББ/ОФР (Срез 2b).** Та же команда CLI (способ Б) разбирает и формы: в `documents[]` лист формы — отдельный документ
|
||||
(`document_type` = `BALANCE` / `INCOME` / `DISCLOSURE`, ОСВ идут первыми), с `header`, `columns[]`, `lines[]`, `checks`.
|
||||
@ -386,6 +413,11 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
|
||||
словарь: присылайте файл, синоним добавляется строкой в `rules/osv_headers.yaml`.
|
||||
- ИНН внутри текста контрагента («Полиграф, 3528315710»), в ячейке над названием и склеенный «ИНН/КПП» парсер извлекает
|
||||
(OQ-100, OQ-130, arm-tabular-parser#9): `counterparty_inn` заполнен, `counterparty_name` без ИНН, исходный текст — в `row_value`.
|
||||
- ИНН в колонке и ИНН в хвосте названия различаются — берётся колонка, предупреждения нет; строка-ИНН над счётом или договором
|
||||
(не контрагентом) склеивается, но её ИНН теряется молча (arm-tabular-parser#18, A9-2/A9-3).
|
||||
- Счёт 91 (и, вероятно, 90/99): строки аналитики несут только обороты, сальдо закрыто на 91.09 → `needs_check` на каждой ОСВ 91
|
||||
до решения OQ-132. Аналитика без сальдо на конец (магазины Агрокомплекса в 62/76) — тоже `needs_check` по данным файла.
|
||||
- Скользящий период даёт `period_quarter = null` (см. P3) — это не ошибка парсера; что с ним делать, решает ядро (OQ-131).
|
||||
- xlsb читается без outline level и форматирования: уровни только по отступам пробелами и кодам счетов. Настоящего xlsb из
|
||||
Excel в фикстурах нет (OQ-102) — если у вас есть Excel, сохраните `s02_no_outline_indent.xlsx` как xlsb и прогоните.
|
||||
- Строка «Итого» нужна: без неё `needs_check`.
|
||||
@ -567,9 +599,12 @@ Q3-only — `missing_quarters = ["Q1","Q2"]`.
|
||||
- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только
|
||||
при точном совпадении нормализованного имени (OQ-127).
|
||||
|
||||
- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (ИНН в тексте строки — arm-tabular-parser#9).
|
||||
- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131).
|
||||
- Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано.
|
||||
- Код `60.АВ` (синтетика s09) и буквенные забалансовые вида `РВ.1` в план счетов не резолвятся (arm-core #28, C23-1/C23-2).
|
||||
|
||||
Полный список отложенных находок — Gitea `RSHB/arm-core`, issues «Follow-ups from #5» и «Follow-ups from #18» (#21).
|
||||
Полный список отложенных находок — Gitea: `RSHB/arm-core` issues «Follow-ups from #5», «Follow-ups from #18» (#21), #28;
|
||||
`RSHB/arm-tabular-parser` #18.
|
||||
|
||||
## 9. Сброс стенда
|
||||
|
||||
|
||||
160
scripts/prepare-contour-snapshot.sh
Executable file
160
scripts/prepare-contour-snapshot.sh
Executable file
@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
# Снимок сервисного репозитория АРМ КА для переноса в контур банка (dev/contour-transfer-guide.md).
|
||||
#
|
||||
# Что делает: берёт дерево коммита (`git archive`, без истории), вырезает то, чего в контуре нет или что ему не нужно
|
||||
# (AI-инструментарий, планы, Dockerfile для локальной сборки), переводит `make venv` с uv на `python -m venv` + pip через
|
||||
# Nexus (как `setup/setup.sh` шаблона), убирает цель docker-build, заменяет ссылки на внешний Gitea, пишет SNAPSHOT.md с
|
||||
# провенансом и проверяет результат. Репозиторий-источник не меняется.
|
||||
#
|
||||
# Запуск из workspace: docs/scripts/prepare-contour-snapshot.sh <каталог репозитория> <каталог вывода> [ref]
|
||||
# ref по умолчанию — origin/main (снимок всегда с сервера, не с рабочего дерева).
|
||||
# Пример: docs/scripts/prepare-contour-snapshot.sh tabular-parser /tmp/contour
|
||||
# Результат: <каталог вывода>/<имя репозитория>/ — готовое дерево для rsync в клон репозитория контура.
|
||||
set -euo pipefail
|
||||
|
||||
usage() { echo "usage: $0 <repo-dir> <out-dir> [ref]" >&2; exit 2; }
|
||||
[ $# -ge 2 ] || usage
|
||||
|
||||
REPO=$(cd "$1" && pwd)
|
||||
mkdir -p "$2"
|
||||
OUT=$(cd "$2" && pwd)
|
||||
REF=${3:-origin/main}
|
||||
NAME=$(basename "$REPO")
|
||||
DEST="$OUT/$NAME"
|
||||
|
||||
case "$NAME" in
|
||||
arm-core|tabular-parser|scan-gateway) ;;
|
||||
*) echo "ошибка: $NAME — не сервисный репозиторий (ожидаются arm-core, tabular-parser, scan-gateway); docs и fixtures в контур не переносятся" >&2; exit 2 ;;
|
||||
esac
|
||||
if [ -e "$DEST" ]; then
|
||||
echo "ошибка: $DEST уже существует — удалите или укажите другой каталог вывода" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
git -C "$REPO" fetch -q origin
|
||||
HASH=$(git -C "$REPO" rev-parse "$REF")
|
||||
DATE=$(git -C "$REPO" show -s --format=%cs "$REF")
|
||||
SUBJECT=$(git -C "$REPO" show -s --format=%s "$REF")
|
||||
|
||||
mkdir -p "$DEST"
|
||||
git -C "$REPO" archive "$REF" | tar -x -C "$DEST"
|
||||
|
||||
# --- 1. Вырезать то, чего в контуре нет ------------------------------------------------------------------------
|
||||
# .claude/, CLAUDE.md, openspec/config.yaml — инструкции и контекст AI-агента (config.yaml: блоки context/rules с путями
|
||||
# ../docs и ../fixtures; openspec CLI в контуре нет, specs/ и design.md читаются без него); docs/superpowers/ — планы и
|
||||
# ledger (в них локальные пути); Dockerfile/.dockerignore — локальная сборка образа, платформа собирает образ своим CI.
|
||||
# openspec/specs/ и openspec/changes/archive/ остаются: это спецификация проекта и решения (design.md).
|
||||
rm -rf "$DEST/.claude" "$DEST/CLAUDE.md" "$DEST/openspec/config.yaml" "$DEST/docs/superpowers" "$DEST/Dockerfile" "$DEST/.dockerignore"
|
||||
rmdir "$DEST/docs" 2>/dev/null || true
|
||||
|
||||
# --- 2. Makefile: venv через python -m venv + pip с Nexus, без uv; без docker-build --------------------------------
|
||||
python3 -I - "$DEST/Makefile" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
venv_patched = False
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
# рецепт venv: все строки с uv заменяются одним рецептом на pip
|
||||
if line.startswith("venv:"):
|
||||
comment_start = len(out)
|
||||
while comment_start > 0 and out[comment_start - 1].startswith("#"):
|
||||
comment_start -= 1
|
||||
del out[comment_start:]
|
||||
out.append("# Контур банка: Python 3.11 платформы, pip через Nexus (setup/pip.conf), как в setup/setup.sh; dev-зависимости")
|
||||
out.append("# тянут requirements.txt. PYTHON переопределяется: make venv PYTHON=/usr/bin/python3.11")
|
||||
out.append("PYTHON ?= python3.11")
|
||||
out.append("venv:")
|
||||
out.append("\tcd api && $(PYTHON) -m venv .venv")
|
||||
out.append("\tcp setup/pip.conf api/.venv/pip.conf")
|
||||
out.append("\tcd api && .venv/bin/pip install -r requirements-dev.txt")
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].startswith("\t"):
|
||||
i += 1
|
||||
venv_patched = True
|
||||
continue
|
||||
# цель docker-build и комментарий над ней удаляются
|
||||
if line.startswith("docker-build:"):
|
||||
comment_start = len(out)
|
||||
while comment_start > 0 and out[comment_start - 1].startswith("#"):
|
||||
comment_start -= 1
|
||||
del out[comment_start:]
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].startswith("\t"):
|
||||
i += 1
|
||||
if i < len(lines) and lines[i] == "" and out and out[-1] == "":
|
||||
i += 1
|
||||
continue
|
||||
if line.startswith("check-full:"):
|
||||
line = re.sub(r"\s+docker-build\b", "", line)
|
||||
if line.startswith("\t$(MAKE) docker-build"):
|
||||
i += 1
|
||||
continue
|
||||
if line.startswith(".PHONY:"):
|
||||
line = re.sub(r"\s+docker-build\b", "", line)
|
||||
if line.startswith('\t@echo "docker-build'):
|
||||
i += 1
|
||||
continue
|
||||
line = line.replace(" + docker-build", "")
|
||||
out.append(line)
|
||||
i += 1
|
||||
if not venv_patched:
|
||||
sys.exit("Makefile: цель venv не найдена — скрипт устарел относительно репозитория")
|
||||
path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
# --- 3. Ссылки на внешний Gitea → имя репозитория и номер ---------------------------------------------------------
|
||||
# В openspec-архивах и README встречаются ссылки вида https://git.aepif.ru/RSHB/<repo>/issues/5 и pulls/7.
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] || continue
|
||||
python3 -I - "$file" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text(encoding="utf-8")
|
||||
text = re.sub(r"https?://git\.aepif\.ru/(RSHB/[\w.-]+)/(issues|pulls)/(\d+)", r"\1#\3", text)
|
||||
text = re.sub(r"https?://git\.aepif\.ru/?", "внешний Gitea проекта", text)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
PY
|
||||
done < <(grep -rIl "git.aepif.ru" "$DEST" 2>/dev/null || true)
|
||||
|
||||
# --- 4. Провенанс ---------------------------------------------------------------------------------------------------
|
||||
cat > "$DEST/SNAPSHOT.md" <<EOF
|
||||
# Снимок $NAME для контура
|
||||
|
||||
Источник: внешний репозиторий проекта \`RSHB/$NAME\`, коммит \`$HASH\` ($DATE, «${SUBJECT}»).
|
||||
Снимок без истории; подготовлен \`prepare-contour-snapshot.sh\` ($(date +%Y-%m-%d)).
|
||||
|
||||
Чего здесь нет по сравнению с источником: \`.claude/\`, \`CLAUDE.md\`, \`openspec/config.yaml\`, \`docs/superpowers/\`
|
||||
(инструментарий, контекст агента и планы разработки снаружи), \`Dockerfile\`/\`.dockerignore\` (образ собирает платформа). Соседних репозиториев \`docs\` и
|
||||
\`fixtures\` в контуре нет: тесты идентичности контракта и тесты с маркером \`fixtures\` пропускаются — это ожидаемо.
|
||||
Копия контракта — \`api/contracts/\`, спецификация — \`openspec/specs/\`, решения — \`openspec/changes/archive/*/design.md\`.
|
||||
|
||||
Разработка ведётся снаружи; правки в контуре возвращаются в источник вручную (см. dev/contour-transfer-guide.md в docs).
|
||||
EOF
|
||||
|
||||
# --- 5. Проверки ----------------------------------------------------------------------------------------------------
|
||||
fail=0
|
||||
for p in .claude CLAUDE.md openspec/config.yaml docs/superpowers Dockerfile .dockerignore; do
|
||||
if [ -e "$DEST/$p" ]; then echo "ошибка: в снимке остался $p" >&2; fail=1; fi
|
||||
done
|
||||
if grep -rIn "/Users/\|/home/[a-z]" "$DEST" >/dev/null 2>&1; then
|
||||
echo "ошибка: в снимке есть локальные пути:" >&2; grep -rIn "/Users/\|/home/[a-z]" "$DEST" >&2; fail=1
|
||||
fi
|
||||
if grep -rIn "git.aepif.ru\|uv venv\|uv pip" "$DEST" >/dev/null 2>&1; then
|
||||
echo "ошибка: остались ссылки на внешний Gitea или uv:" >&2; grep -rIn "git.aepif.ru\|uv venv\|uv pip" "$DEST" >&2; fail=1
|
||||
fi
|
||||
for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test api/contracts; do
|
||||
if [ ! -e "$DEST/$p" ]; then echo "ошибка: платформенный файл $p отсутствует" >&2; fail=1; fi
|
||||
done
|
||||
[ "$fail" = 0 ] || exit 1
|
||||
|
||||
echo "снимок готов: $DEST"
|
||||
echo "источник: $NAME@${HASH:0:7} ($DATE); файлов: $(find "$DEST" -type f | wc -l | tr -d ' ')"
|
||||
Loading…
x
Reference in New Issue
Block a user