From 1ca2fd18ff07e3567e83c45a5fb46c31975f0734 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 13:53:54 +0400 Subject: [PATCH 1/9] =?UTF-8?q?docs(dev):=20=D0=B8=D0=BD=D1=81=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=BA=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D1=84?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4-=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B0=D1=83=D1=82=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20arm-core=20(?= =?UTF-8?q?#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Модель (JWT без сессий, права как данные, порядок проверок право → видимость → статус), режимы dev/прод, POST /dev/token и GET /me с семантикой 401/403/404, сводка прав по маршрутам, каталог прав и ролей, policy-слой, эталонный клиент web/src/{api,auth}, чек-лист, проверка руками, открытые вопросы (OQ-54). Ссылка добавлена в dev/README.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/README.md | 2 + dev/frontend-auth-guide.md | 241 +++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 dev/frontend-auth-guide.md diff --git a/dev/README.md b/dev/README.md index 5211e3f..a1cc0d7 100644 --- a/dev/README.md +++ b/dev/README.md @@ -6,6 +6,8 @@ поднимается только инфраструктура, а сервисы можно запускать из 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). ## Предварительные требования diff --git a/dev/frontend-auth-guide.md b/dev/frontend-auth-guide.md new file mode 100644 index 0000000..f33f496 --- /dev/null +++ b/dev/frontend-auth-guide.md @@ -0,0 +1,241 @@ +# Аутентификация и авторизация 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 `; сервер проверяет + подпись и срок, берёт из токена 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": "", "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`), по одному на роль: + +| 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`. + +## 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 аутентификации. Фронт для этого +ничего не делает. From 0d9531c47f1856628fb369d9670fc33302bbbada Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 15:31:38 +0400 Subject: [PATCH 2/9] =?UTF-8?q?docs(dev):=20=D0=B3=D0=B0=D0=B9=D0=B4=D1=8B?= =?UTF-8?q?=20QA=20=D0=B8=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BC=D0=B5=D1=80=D0=B6=D0=B5=D0=B9?= =?UTF-8?q?=2006.10.2026=20=E2=80=94=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D1=8B=20=D0=B2=D1=85=D0=BE=D0=B4=D0=BE=D0=B2,=20=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D1=8C=D0=B7=D1=8F=D1=89=D0=B8=D0=B9=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=BE=D0=B4,=20=D0=98=D0=9D=D0=9D=20=D0=B2?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B5,=20=D0=BF=D0=BB=D0=B0?= =?UTF-8?q?=D0=BD=20=D1=81=D1=87=D0=B5=D1=82=D0=BE=D0=B2=201=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qa-guide: §1 (план счетов 488, парсер после пакета ДКБ), §6.5 N5–N8 (422 на границы, 404 при гонке удаления алиаса), §6.7 P3/P6/P7/P9/P11 (скользящий период, две TOTAL, ИНН из названия и строкой выше, «Итого» ↔ Σ уровня 0, шапка без меток), манифест ДКБ и golden 113, синтетика s17–s19, «не баги» по OQ-131/132; §8 ссылки на follow-up arm-core #28, tabular-parser #18. api-contract-slice-4: §2 границы MembersIn/StatusIn/q и формат 422, §4 п. 16–18. Closes #30 Co-Authored-By: Claude Fable 5.1 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- dev/api-contract-slice-4.md | 17 ++++++++++++--- dev/qa-guide.md | 42 +++++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/dev/api-contract-slice-4.md b/dev/api-contract-slice-4.md index f466a21..80a5ca3 100644 --- a/dev/api-contract-slice-4.md +++ b/dev/api-contract-slice-4.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 /legal-entities/{id}/aliases/{alias_id}`) во время переноса этого же алиаса ручным сопоставлением + (`move_alias: true`) теперь ждёт блокировку и отвечает 404 `NOT_FOUND`, если алиас уже переехал (arm-core #26, T11.3) — + это не ошибка клиента, перечитайте список алиасов. ## 5. Сквозные сценарии экранов diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 28ece19..df0ec79 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -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). **Нет (это не баги):** @@ -253,6 +259,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 /legal-entities/{id}/aliases/{alias_id}` для уже перенесённого алиаса | 404 `NOT_FOUND`, записи аудита об удалении нет | ### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c) @@ -337,22 +347,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…s16` с описанием каждого файла и +вариант 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. **Как делать свои файлы.** Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист: @@ -373,8 +387,11 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x только «Сальдо на начало». - Заголовок с незнакомым синонимом («Нач. остаток») даёт `needs_check`, суммы уходят в `extra`. Это не ошибка кода, а словарь: присылайте файл, синоним добавляется строкой в `rules/osv_headers.yaml`. -- ИНН внутри текста контрагента («Полиграф, 3528315710») и ИНН в ячейке над названием сейчас не извлекаются — задача - RSHB/arm-tabular-parser#9 (OQ-100, OQ-130). До неё строка уходит в ядро с именем целиком, `counterparty_inn` пуст. +- ИНН в колонке и ИНН в хвосте названия различаются — берётся колонка, предупреждения нет; строка-ИНН над счётом или договором + (не контрагентом) склеивается, но её ИНН теряется молча (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`. @@ -556,9 +573,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. Сброс стенда From 183b4d02bbfe00dbb98a2307a640307b2069f004 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 15:43:39 +0400 Subject: [PATCH 3/9] =?UTF-8?q?docs(dev):=20Authorize=20=D0=B2=20Swagger?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=8F=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F?= =?UTF-8?q?=20=D1=81=20arm-core=20#29;=20=D0=BF=D1=83=D1=82=D1=8C=20=D1=80?= =?UTF-8?q?=D1=83=D1=87=D0=BA=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B0=D0=BB=D0=B8=D0=B0=D1=81=D0=B0=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=84=D0=B8=D0=BA=D1=81=D0=BE=D0=BC=20/direc?= =?UTF-8?q?tories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frontend-auth-guide §8 и qa-guide §4.5: кнопка Authorize есть только со схемой bearerAuth (arm-core #29), публичные ручки без замка; на стенде старше — curl. Исправлен путь DELETE /directories/legal-entities/{id}/aliases/{alias_id} в api-contract-slice-4 §4 п. 18 и qa-guide N8 (неточность PR #31). Closes #32 Co-Authored-By: Claude Fable 5.1 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- dev/api-contract-slice-4.md | 2 +- dev/frontend-auth-guide.md | 5 ++++- dev/qa-guide.md | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dev/api-contract-slice-4.md b/dev/api-contract-slice-4.md index 80a5ca3..56055ba 100644 --- a/dev/api-contract-slice-4.md +++ b/dev/api-contract-slice-4.md @@ -143,7 +143,7 @@ alias_moved_from (uuid | null)}`. `period_year` = год конца диапазона, а `period_quarter` и `is_cumulative` — `null`. Такой документ не подходит ни одной версии (привязка идёт по кварталу), в чек-листе и своде не участвует, подтверждать его можно. В списке документов показывайте год и пометку «период не определён», не подставляйте квартал сами. Границы периода появятся в контракте v1.2 (Срез 2b). -18. **Удаление алиаса** (`DELETE /legal-entities/{id}/aliases/{alias_id}`) во время переноса этого же алиаса ручным сопоставлением +18. **Удаление алиаса** (`DELETE /directories/legal-entities/{id}/aliases/{alias_id}`) во время переноса этого же алиаса ручным сопоставлением (`move_alias: true`) теперь ждёт блокировку и отвечает 404 `NOT_FOUND`, если алиас уже переехал (arm-core #26, T11.3) — это не ошибка клиента, перечитайте список алиасов. diff --git a/dev/frontend-auth-guide.md b/dev/frontend-auth-guide.md index f33f496..4d4c675 100644 --- a/dev/frontend-auth-guide.md +++ b/dev/frontend-auth-guide.md @@ -225,7 +225,10 @@ curl -s localhost:8000/api/v1/admin/users -H "Authorization: Bearer $TOKEN" curl -s localhost:8000/api/v1/no-such-route -H "Authorization: Bearer $TOKEN" ``` -В Swagger (`/docs`, только при `DEBUG=true`): **Authorize** → вставить токен без слова `Bearer`. +В Swagger (`/docs`, только при `DEBUG=true`): **Authorize** (замок справа вверху) → вставить токен без слова `Bearer` → все +защищённые ручки получают заголовок. Кнопка есть с arm-core #29 (схема `bearerAuth` в `openapi.json`, +`components.securitySchemes`); на стенде старше этого PR кнопки нет — проверяйте curl. Публичные ручки без замка: `POST /dev/token`, +`GET /healthcheck`. ## 9. Открытые вопросы, которые затронут фронт diff --git a/dev/qa-guide.md b/dev/qa-guide.md index df0ec79..bc4c5da 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -142,6 +142,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"])') @@ -262,7 +265,7 @@ docker compose -f ../docs/dev/docker-compose.yml exec postgres \ | 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 /legal-entities/{id}/aliases/{alias_id}` для уже перенесённого алиаса | 404 `NOT_FOUND`, записи аудита об удалении нет | +| N8 | `DELETE /directories/legal-entities/{id}/aliases/{alias_id}` для уже перенесённого алиаса | 404 `NOT_FOUND`, записи аудита об удалении нет | ### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c) From 43eefb917bb5fc95891c88ace6ed9d2baa32143c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 15:59:40 +0400 Subject: [PATCH 4/9] =?UTF-8?q?docs(dev):=20=D0=B3=D0=B0=D0=B9=D0=B4=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B0=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=83=D1=80=20=D0=B1=D0=B0=D0=BD=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=81=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Решения владельца 06.10.2026: в контур едут arm-core, tabular-parser, scan-gateway снимками без истории; docs и fixtures не переносятся (разработка снаружи); вырезаются .claude/, CLAUDE.md, docs/superpowers/, Dockerfile; openspec остаётся. Скрипт scripts/prepare-contour-snapshot.sh: git archive, вырезание, Makefile без uv и docker-build (venv через python -m venv + pip с Nexus), замена ссылок на внешний Gitea, SNAPSHOT.md, проверки. Снимки трёх сервисов проходят make check без docs/fixtures (1084/1087/726 passed, пропуски только fixtures/contract). Closes #34 Co-Authored-By: Claude Fable 5.1 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- dev/README.md | 2 + dev/contour-transfer-guide.md | 178 ++++++++++++++++++++++++++++ scripts/prepare-contour-snapshot.sh | 159 +++++++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 dev/contour-transfer-guide.md create mode 100755 scripts/prepare-contour-snapshot.sh diff --git a/dev/README.md b/dev/README.md index a1cc0d7..75aa927 100644 --- a/dev/README.md +++ b/dev/README.md @@ -8,6 +8,8 @@ Для ручного тестирования стенда (установка ПО, запуск, чек-лист сценариев по ролям) — [`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). ## Предварительные требования diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md new file mode 100644 index 0000000..ca4db64 --- /dev/null +++ b/dev/contour-transfer-guide.md @@ -0,0 +1,178 @@ +# Перенос сервисов АРМ КА в контур банка: гайд для разработчика + +Разработка ведётся **снаружи** (workspace `awp-ca`, Gitea проекта). В контур банка (Gitea платформы РАИСА и её CI/CD) +уезжают только три сервисных репозитория — `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/`, `openspec/config.yaml` | да | спецификация системы и журнал решений (`design.md`); без них код в контуре нечем объяснять | +| `.claude/`, `CLAUDE.md` | **нет** | инструкции и skills AI-агента, инструментарий внешней разработки | +| `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` — инструментарий процесса; в снимке только их следы в + `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 ` → `/tmp/contour//` — дерево коммита без `.git` и без игнорируемых файлов. +2. Удаляет `.claude/`, `CLAUDE.md`, `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/#`. +5. Пишет `SNAPSHOT.md` (источник, хеш, дата, что вырезано). +6. Проверяет: нет `.claude`/`CLAUDE.md`/планов/`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. Как положить снимок в контур + +Репозитории контура — по одному на сервис, имена как снаружи. Первый перенос и каждый следующий делаются одинаково: + +```bash +git clone /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 # дальше PR/мерж по регламенту контура +``` + +`--delete` обязателен: иначе файлы, удалённые или переименованные снаружи, останутся в контуре. Если в контуре делали +локальные правки (раздел 7), сначала вернуть их наружу, иначе `rsync --delete` их перезапишет. + +Порядок сервисов при одной синхронизации: `arm-core` → `tabular-parser` → `scan-gateway` (ядро — потребитель контракта, +его копия схемы должна быть не старше копий сервисов). Если снаружи поднималась версия контракта (`api/contracts/*.json`, +`CONTRACT_VERSION` в `api/src/contract/common.py`), переносить все три сервиса одним заходом. + +## 5. Проверка в контуре перед PR + +```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: @ (<дата>)`, `make venv`, + `make check` (пропуски — только из раздела 5), для arm-core `make web-check`, PR по регламенту контура. +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, в контуре они никуда не ведут. +- `openspec/config.yaml` содержит контекст для AI-агента (упоминает `../docs`, фикстуры, стек); на сборку не влияет. +- Тесты, которые снаружи пропускаются без `fixtures`, в контуре не выполняются никогда. Поэтому перед каждым снимком снаружи + обязателен `make check-full` с доступными `docs` и `fixtures` (`dev/checks.md`, раздел «Порядок перед PR»). diff --git a/scripts/prepare-contour-snapshot.sh b/scripts/prepare-contour-snapshot.sh new file mode 100755 index 0000000..a64e86e --- /dev/null +++ b/scripts/prepare-contour-snapshot.sh @@ -0,0 +1,159 @@ +#!/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 [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 — инструкции и skills AI-агента; docs/superpowers/ — планы и ledger (в них локальные пути); +# Dockerfile/.dockerignore — локальная сборка образа, платформа собирает образ своим CI. openspec/ остаётся: +# это спецификация проекта и решения (design.md). +rm -rf "$DEST/.claude" "$DEST/CLAUDE.md" "$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//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" <&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 ' ')" From 6a3da7c376164ae365eb08db9c97f72ce6d67ca9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 16:01:05 +0400 Subject: [PATCH 5/9] =?UTF-8?q?docs(dev):=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83?= =?UTF-8?q?=D1=80=20=E2=80=94=20GitLab=20=D0=BF=D0=BB=D0=B0=D1=82=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D1=8B,=20merge=20request=20=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #34 Co-Authored-By: Claude Fable 5.1 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- dev/contour-transfer-guide.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index ca4db64..69027ae 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -1,6 +1,7 @@ # Перенос сервисов АРМ КА в контур банка: гайд для разработчика -Разработка ведётся **снаружи** (workspace `awp-ca`, Gitea проекта). В контур банка (Gitea платформы РАИСА и её CI/CD) +Разработка ведётся **снаружи** (workspace `awp-ca`, Gitea проекта). В контур банка (GitLab платформы РАИСА и его CI/CD; +Gitea в контуре нет) уезжают только три сервисных репозитория — `arm-core`, `tabular-parser`, `scan-gateway` — **снимками дерева, без истории**. Репозитории `docs` и `fixtures` в контур не переносятся: решение владельца 06.10.2026. Этот документ — что именно уезжает, чего в платформе нет, как подготовить снимок скриптом и как его положить в контур. @@ -78,7 +79,8 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour ## 4. Как положить снимок в контур -Репозитории контура — по одному на сервис, имена как снаружи. Первый перенос и каждый следующий делаются одинаково: +Репозитории контура — проекты GitLab, по одному на сервис, имена как снаружи. Первый перенос и каждый следующий делаются +одинаково (вместо PR в GitLab — merge request, в остальном git тот же): ```bash git clone /arm-core contour-arm-core @@ -87,7 +89,7 @@ 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 # дальше PR/мерж по регламенту контура +git push -u origin sync/arm-core-1d7f2c8 # дальше merge request и мерж по регламенту GitLab контура ``` `--delete` обязателен: иначе файлы, удалённые или переименованные снаружи, останутся в контуре. Если в контуре делали @@ -97,7 +99,7 @@ git push -u origin sync/arm-core-1d7f2c8 # дальш его копия схемы должна быть не старше копий сервисов). Если снаружи поднималась версия контракта (`api/contracts/*.json`, `CONTRACT_VERSION` в `api/src/contract/common.py`), переносить все три сервиса одним заходом. -## 5. Проверка в контуре перед PR +## 5. Проверка в контуре перед merge request ```bash make venv # python3.11 -m venv + pip через Nexus (вместо setup/setup.sh; тот ставит только прод-зависимости) @@ -163,7 +165,7 @@ git diff <коммит sync> HEAD -- . ':!SNAPSHOT.md' > /tmp/contour-arm-core.p 2. Правки из контура (раздел 7) возвращены наружу и слиты. 3. `prepare-contour-snapshot.sh` по каждому сервису; скрипт завершился словами «снимок готов». 4. В контуре: клон, ветка, `rsync --delete`, просмотр `git status`, коммит `sync: @ (<дата>)`, `make venv`, - `make check` (пропуски — только из раздела 5), для arm-core `make web-check`, PR по регламенту контура. + `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 синхронизации отметить, какие хеши перенесены. From 6edb62c592d64e3f2949aaa50e94bb6e3662ad1f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 16:52:00 +0400 Subject: [PATCH 6/9] =?UTF-8?q?docs(dev):=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B2=D1=8B=D1=80=D0=B5=D0=B7=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=20openspec/config.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вопрос разработчика при переносе в контур: .claude/ и CLAUDE.md вырезаются, а openspec/config.yaml с контекстом агента остаётся. Решение владельца 06.10.2026: вырезать, как CLAUDE.md. Блоки context/rules — инструкции агенту с путями ../docs и ../fixtures, которые в контуре никуда не ведут; openspec CLI в контуре нет, specs/ и design.md читаются без него. - prepare-contour-snapshot.sh: rm, проверка раздела 5, SNAPSHOT.md - contour-transfer-guide.md: таблица раздела 1, разделы 2, 3, 9 Проверено прогоном скрипта на tabular-parser@3b32462. Closes #36 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- dev/contour-transfer-guide.md | 13 ++++++------- scripts/prepare-contour-snapshot.sh | 15 ++++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index 69027ae..7d6940c 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -14,8 +14,8 @@ Gitea в контуре нет) | Что | В контур | Почему | |---|---|---| | `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/`, `openspec/config.yaml` | да | спецификация системы и журнал решений (`design.md`); без них код в контуре нечем объяснять | -| `.claude/`, `CLAUDE.md` | **нет** | инструкции и skills AI-агента, инструментарий внешней разработки | +| `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 | @@ -35,8 +35,8 @@ Gitea в контуре нет) нет. Цели `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` — инструментарий процесса; в снимке только их следы в - `openspec/config.yaml` (контекст для агента, безвреден). +- `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, в контуре их нет). @@ -64,13 +64,13 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour Что делает скрипт (репозиторий-источник не меняется): 1. `git archive ` → `/tmp/contour//` — дерево коммита без `.git` и без игнорируемых файлов. -2. Удаляет `.claude/`, `CLAUDE.md`, `docs/superpowers/`, `Dockerfile`, `.dockerignore`. +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/#`. 5. Пишет `SNAPSHOT.md` (источник, хеш, дата, что вырезано). -6. Проверяет: нет `.claude`/`CLAUDE.md`/планов/`Dockerfile`, нет локальных путей (`/Users/…`, `/home/…`), нет `uv` и +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 и список. @@ -175,6 +175,5 @@ git diff <коммит sync> HEAD -- . ':!SNAPSHOT.md' > /tmp/contour-arm-core.p скрипт их не переписывает. Ориентир для контура — этот документ и `SNAPSHOT.md`. - В `openspec/changes/archive/*/` остаются относительные пути вида `../docs/openspec/...` и `../fixtures/...`: это ссылки на внешний workspace, в контуре они никуда не ведут. -- `openspec/config.yaml` содержит контекст для AI-агента (упоминает `../docs`, фикстуры, стек); на сборку не влияет. - Тесты, которые снаружи пропускаются без `fixtures`, в контуре не выполняются никогда. Поэтому перед каждым снимком снаружи обязателен `make check-full` с доступными `docs` и `fixtures` (`dev/checks.md`, раздел «Порядок перед PR»). diff --git a/scripts/prepare-contour-snapshot.sh b/scripts/prepare-contour-snapshot.sh index a64e86e..e4285e9 100755 --- a/scripts/prepare-contour-snapshot.sh +++ b/scripts/prepare-contour-snapshot.sh @@ -40,10 +40,11 @@ mkdir -p "$DEST" git -C "$REPO" archive "$REF" | tar -x -C "$DEST" # --- 1. Вырезать то, чего в контуре нет ------------------------------------------------------------------------ -# .claude/, CLAUDE.md — инструкции и skills AI-агента; docs/superpowers/ — планы и ledger (в них локальные пути); -# Dockerfile/.dockerignore — локальная сборка образа, платформа собирает образ своим CI. openspec/ остаётся: -# это спецификация проекта и решения (design.md). -rm -rf "$DEST/.claude" "$DEST/CLAUDE.md" "$DEST/docs/superpowers" "$DEST/Dockerfile" "$DEST/.dockerignore" +# .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 -------------------------------- @@ -131,8 +132,8 @@ cat > "$DEST/SNAPSHOT.md" <&2; fail=1; fi done if grep -rIn "/Users/\|/home/[a-z]" "$DEST" >/dev/null 2>&1; then From b65ca31dcaa5a92a5db13e55939c1aa144a6cb6e Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 17:07:20 +0400 Subject: [PATCH 7/9] =?UTF-8?q?docs(dev):=20dev-=D0=93=D0=9A=20=D0=B8?= =?UTF-8?q?=D0=B7=20make=20seed-dev=20=D0=B2=20QA-=20=D0=B8=20=D1=84=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D1=82-=D0=B3=D0=B0=D0=B9=D0=B4=D0=B0=D1=85;=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20=D0=95=D0=A6=D0=9F=20?= =?UTF-8?q?=E2=80=94=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=B5=D0=BD=D0=B4=D0=B5?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20dev-=D0=93=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #38 Co-Authored-By: Claude Fable 5.1 Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b --- dev/frontend-auth-guide.md | 4 +++- dev/qa-guide.md | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/dev/frontend-auth-guide.md b/dev/frontend-auth-guide.md index 4d4c675..947dbe1 100644 --- a/dev/frontend-auth-guide.md +++ b/dev/frontend-auth-guide.md @@ -56,7 +56,9 @@ email, как именно SPA получит токен (в старом АРМ | 422 массив | пустой email | | 404 `{"detail":"Not Found"}` | ручка выключена (`DEBUG=false` или `AUTH_DEV_ISSUER=false`) — так выглядит прод | -Dev-пользователи (`make seed-dev`), по одному на роль: +Dev-пользователи (`make seed-dev`), по одному на роль (тот же `make seed-dev` создаёт и dev-ГК с ЮЛ под документы из +`fixtures` — ГК «Русский Бисквит» и три ГК пакета ДКБ, см. `qa-guide.md` раздел 4.2; экраны справочников и консолидации не +пустые сразу после сида): | Email | Роль | Филиал | Видимость | |---|---|---|---| diff --git a/dev/qa-guide.md b/dev/qa-guide.md index bc4c5da..a2c6cdb 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -92,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 ``` @@ -124,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)». Пароля нет. Это единственная форма @@ -219,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). | № | Шаги | Ожидание | |---|---|---| @@ -274,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) From f46a34169cfbac91b2ccc8f95043c60480f7fde9 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 17:21:59 +0400 Subject: [PATCH 8/9] =?UTF-8?q?docs(context):=20NATS=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D1=82=D1=84=D0=BE=D1=80=D0=BC=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=20armka=5Ffile=5Fworker,=20=D0=BE=D0=B4=D0=B8=D0=BD=20JW?= =?UTF-8?q?T,=20=D1=80=D0=B0=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=D1=8F=D1=87=D0=B8=D1=85=20=D0=BF=D0=B0=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 03-architecture: core NATS без JetStream подтверждён кодом armka_file_worker (работает в контуре); один JWT на все сервисы с правами по subject'ам; старое семейство subject'ов files.*; соглашение ROOT_PATH платформы Aurora; FileWorker не реализует DECISION-4 — только инфраструктурный референс. - 03-architecture: описание scan-gateway — эталон раскладки горячих папок ИС Сканирование как у FileWorker (Input/., *.tmp рядом, Output//*.json, очистка); расхождение кода — arm-scan-gateway #6. - 07-open-questions: OQ-85 дополнен, новый OQ-137 (раскладка папок) закрыт владельцем 06.10.2026. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/03-architecture.md | 35 +++++++++++++++++++++++++++++++---- context/07-open-questions.md | 3 ++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/context/03-architecture.md b/context/03-architecture.md index f779d63..1de3586 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -30,9 +30,13 @@ пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Контракт результата — `docs/contracts/recognition-result.schema.json` (контракт v1.0). Правила распознавания заголовков/группировок — `rules/*.yaml`. -- **scan-gateway** — адаптер ИС Сканирование: переименовать файл `UUID_имя.pdf`, положить в «Входящие», опросить - «Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), нормализовать - в тот же контракт результата, опубликовать `recognition.result`. Таймаут 5 мин → `timeout`. +- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/.` (временный + `<имя>..tmp` рядом, затем атомарный rename; ИС игнорирует `*.tmp`), дождаться батча `Output//*.json` + (появляется целиком и больше не меняется), сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), + очистить `Output//` и входящий файл, нормализовать в тот же контракт результата, опубликовать + `recognition.result`. Таймаут 5 мин → `timeout`. Раскладка папок — как у проверенного в контуре PDFWorker старого АРМ + (`RSHB/armka_file_worker`, решение владельца 06.10.2026, OQ-137); текущий код шлюза именует файл `_` + и пишет временный файл в `Tmp/` — приводится к эталону в arm-scan-gateway #6. - **Frontend** — React + TS по шаблону команды; экраны по макету `dkb_24.09.26` (справочник ГК, консолидации, первичные документы, проверка распознавания, сводная ОСВ, шахматка, корректировки, гудвилл, КФО, ЕХ, журнал, роли). @@ -56,7 +60,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`, не более @@ -277,6 +282,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`. @@ -357,3 +365,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-` (у + FileWorker — `fastapi-dkb-armka-pdf-worker`); наши сервисы держат `ROOT_PATH = /` по умолчанию и получают префикс из + окружения. Healthcheck `GET /healthcheck` отвечает `503`, если NATS отключён или воркер остановлен — тот же принцип, + что у нас. +- Горячие папки ИС Сканирование: эталон — раскладка FileWorker (`Input/.`, `*.tmp` рядом, + `Output//*.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`). diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 714db22..fc38978 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -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` | открыт: arm-tabular-parser#11 п. 4 | | 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/_` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/.` через временный `<назначение>..tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output//*.json` и удаляет `Output//` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `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»** — имя `.`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output//` и входящего файла после чтения; `Output//*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6 | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. From 087109c9a4fe086d3a54a11bd1bd8cd80c54bffc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 17:33:40 +0400 Subject: [PATCH 9/9] =?UTF-8?q?docs(context):=20=C2=A711.1=20=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20OCR-=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=B0=20=E2=80=94=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B8=D0=B7=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D0=BE=D0=B3=D0=BE=20=D1=8F=D0=B4=D1=80?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20scan-gateway?= =?UTF-8?q?=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 05-domain-rules §11.1: буквы-двойники, `|` как разделитель, разбор дат из OCR, раскладка без шапки, классы строк, тип таблицы по кодам, очистка кода/наименования, суммы и критерий is_recognized, шапка документа, ошибка разбора одного документа. Перенос кода ofr_result_conversion — единственное исключение для reference-old-arm (решение владельца 06.10.2026); обвязка записи в БД не переносится. - 09-delivery-plan: Срез 3 — нормализатор по §11 и §11.1. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- context/05-domain-rules.md | 58 +++++++++++++++++++++++++++++++++++++ context/09-delivery-plan.md | 4 ++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 03c3618..9160c07 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -730,6 +730,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, второй тип документа) diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 72f318a..122b8f6 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -160,7 +160,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.