feat: complete published registry contracts and gated SRO ingestion
All checks were successful
Mostovik Backend CI/CD / Tests and lint (push) Successful in 3m55s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Successful in 3m43s
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Successful in 1m45s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-14 17:01:02 +02:00
parent 49cbfd265c
commit 18971d33ec
76 changed files with 33156 additions and 311 deletions

View File

@@ -0,0 +1,527 @@
# Source records — Backend Improvements
Назначение: единый handoff для backend по трём источникам, которые пока работают через
frontend-моки:
- «Реестр участников бюджетного процесса»;
- «Реестр субъектов МСП — получателей поддержки»;
- «Проверка членства в СРО».
Документ составлен после генерации клиента командой `bun run apigen` из актуального
`openapi.json`. Он фиксирует не только наличие URL, но и весь контракт, необходимый, чтобы
frontend мог выключить list/detail-моки, использовать сгенерированный Orval-клиент и сохранить
таблицы, фильтры, карточки источников, обновление и выгрузку.
Важно:
- универсальные URL `organization-source-records` уже существуют, но сами по себе не являются
достаточной реализацией источника;
- все перечисленные изменения должны быть отражены одновременно в runtime backend, `openapi.json`
и contract tests backend;
- после изменения спецификации frontend запускает `bun run apigen`; вручную править сгенерированные
файлы нельзя;
- до поставки соответствующего runtime-контракта моки не удаляются.
### Граница этого handoff
Это самостоятельное техническое задание на доработку текущего backend и OpenAPI. Детальные
исходные правила нормализации, upstream mapping и расширенные fixture-примеры остаются в
source-first спецификациях; они обязательны и не могут противоречить этому документу.
| Источник | Полная source-first спецификация |
| ----------------- | -------------------------------------------------------------------------------- |
| Бюджетный процесс | `docs/source-integration/sources/budget-process-registry/backend-api.md` |
| Поддержка МСП | `docs/source-integration/sources/sme-support-recipients-registry/backend-api.md` |
| Членство в СРО | `docs/source-integration/sources/sro-membership-check/backend-api.md` |
Если backend-реализация выбирает между кратким описанием ниже и обязательным полем/инвариантом
в source-first спецификации, применяется source-first спецификация. Изменение контракта требует
синхронно изменить оба документа, runtime API и `openapi.json`.
## Идентификаторы источников
| Источник | Source card slug | `source_group` | `source` / parser key | `record_type` | Export group |
| ------------------------------------- | --------------------------------- | ------------------------- | ---------------------------- | ------------------------------ | ------------------------- |
| Реестр участников бюджетного процесса | `budget-process-registry` | `budget_process_registry` | `budget_ubpandnubp` | `budget_registry_organization` | `budget_process_registry` |
| Реестр получателей поддержки МСП | `sme-support-recipients-registry` | `government_support` | `fns_sme_support_recipients` | `sme_support_measure` | `government_support` |
| Проверка членства в СРО | `sro-membership-check` | `sro_membership` | `sro_membership_check` | `sro_membership` | `sro_membership` |
Идентификаторы — канонические. Frontend может понимать aliases маршрута, но никогда не передаёт
их в API вместо значений из таблицы.
## Доработки существующих endpoint'ов
### `organization-source-records`
Затрагиваемые endpoint'ы:
- `GET /api/v2/organization-source-records/`
- `GET /api/v2/organization-source-records/{uid}/`
- `POST /api/v2/organization-source-records/export-ticket/`
- `POST /api/v2/organization-source-records/export-download/`
Проблема:
- актуальная OpenAPI описывает общий list/read, но `source_group` содержит только
`budget_process_registry` и `government_support`; `sro_membership` отсутствует;
- list endpoint описывает только базовые query-параметры. Параметры фильтров, которыми обладают
экраны и моки трёх источников, не попадают в generated TypeScript client;
- `payload` остаётся смешанным универсальным объектом: бюджетные вложенные блоки сужены до
`Record<string, string>`, а для СРО нет полей вообще. Это не позволяет заменить временные DTO
сгенерированными безопасными типами;
- `meta` list response свободный object, хотя frontend рассчитывает на
`meta.pagination.total_count` для серверной пагинации;
- export request не принимает `sro_membership`.
Нужно:
- сохранить общий URL, но описать ответ как discriminated union по паре
`source_group` + `record_type`;
- вынести в named schemas общую организацию, общую строку, pagination и отдельные list/detail
payload каждого из трёх источников;
- сделать обязательными для published record: `uid`, `source_group`, `source`, `record_type`,
`external_id`, `title`, `status`, `created_at`, `updated_at`, `organization.uid`,
`organization.name`, `organization.inn`, `organization.ogrn`, `organization.okpo`;
- сохранять nullable именно как JSON `null`, а не как `""`, `"—"` или отсутствующее поле;
- добавить `sro_membership` во все применимые enum: list `source_group`, export `sources`,
source extension, карточка организации и другие фильтры, использующие группу источника;
- описать `meta.pagination` отдельной схемой с required полями `page`, `page_size`,
`total_count`, `total_pages`, `has_next`, `has_previous`;
- фильтровать и сортировать до pagination; стабильно разрешать ties по `uid`, null — последними;
- поддерживать `GET /{uid}/` для каждого `uid`, возвращённого соответствующим list endpoint;
- возвращать `400` с машиночитаемым кодом для неизвестного filter/order, `404` для отсутствующей
published записи, а не заменять их пустым успешным ответом.
Минимальный общий envelope списка:
```json
{
"success": true,
"data": [],
"errors": null,
"meta": {
"pagination": {
"page": 1,
"page_size": 50,
"total_count": 0,
"total_pages": 0,
"has_next": false,
"has_previous": false
}
}
}
```
Форма detail response должна быть единой для всех групп. Рекомендуемый и совместимый с текущим
frontend вариант — plain source record без envelope:
```json
{
"uid": "2e5db996-e17d-4f5e-a898-bb29566927e4",
"source_group": "<canonical-source-group>",
"source": "<canonical-source>",
"record_type": "<canonical-record-type>",
"payload": {},
"organization": {}
}
```
Запрещено возвращать для одной группы plain record, а для другой `{ "data": record }`:
generated функция и Zod schema должны иметь одну форму ответа. В list payload допускаются только
поля, необходимые таблице; detail дополняет, но не изменяет значения общих/list полей.
#### Реестр участников бюджетного процесса
Текущий запрос таблицы:
```text
GET /api/v2/organization-source-records/?source_group=budget_process_registry&source=budget_ubpandnubp&record_type=budget_registry_organization&page=1&page_size=50&ordering=extension__organization__name
```
Нужно добавить в OpenAPI и runtime list endpoint следующие параметры.
| Параметр | Тип | Правило |
| ---------------------------- | -------------------- | -------------------------------------------------------- |
| `source_group` | enum | `budget_process_registry` |
| `source` | enum/string | `budget_ubpandnubp` |
| `record_type` | enum/string | `budget_registry_organization` |
| `status` | enum | `active`, `inactive`, `special`, `unknown` |
| `organization` | UUID | Точный `organization.uid` |
| `search` | string, max 200 | Наименование, ИНН, КПП, ОГРН, ОКПО, код/реестровый номер |
| `date_from`, `date_to` | date | Включительно по `record_date` |
| `region_code` | string | `payload.address.region.code` |
| `organization_type` | string | Код типа организации |
| `establishment_kind` | string | Код вида учреждения |
| `budget_level` | string | Код уровня бюджета |
| `is_branch` | boolean | Признак обособленного подразделения |
| `has_procurement_permission` | boolean | `payload.summary.has_procurement_permission` |
| `ordering` | enum/comma-separated | Allowlist ниже; каждый ключ допускает префикс `-` |
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
Allowlist `ordering`:
```text
extension__organization__name, status,
payload__classification__organization_type__name,
payload__classification__establishment_kind__name,
payload__budget__level__name, payload__address__region__name,
updated_at, payload__is_separate_division
```
`BudgetRegistryRecordListPayload` должен включать:
```ts
interface CodeName {
code: string
name: string
}
interface BudgetRegistryRecordListPayload {
registry: {
code: string
registration_number: string | null
record_number: string | null
}
classification: {
organization_type: CodeName | null
establishment_kind: CodeName | null
}
budget: {
level: CodeName | null
code: string | null
name: string | null
}
address: { region: CodeName | null }
is_separate_division: boolean | null
summary: {
activities_count: number
authorities_count: number
permissions_count: number
accounts_count: number
successions_count: number
has_procurement_permission: boolean
}
}
```
`GET /{uid}/` обязан возвращать `BudgetRegistryRecordDetailPayload`, расширяющий list payload.
Это не произвольный `object`: все блоки и коллекции ниже должны быть named OpenAPI schemas.
| Блок detail payload | Обязательная структура |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registry` | `code`, `registration_number`, `record_number`, `guid`, `parent_record_number`, `status_code`, `status_name`, `registration_date`, `inclusion_date`, `exclusion_date`, `start_date`, `end_date`, `updated_at` |
| `legal` | `firm_name`, `name_in_documents`, `legal_form`, `ownership_form`, `legal_person_kind` |
| `classification` | list-поля, `government_body`, `flags` с признаками и `contour_type_code` |
| `budget` | list-поля, `public_legal_entity`, `budget_chapter`, `authorized_organization`, `treasury_body` |
| `address` | `full`, `postal_code`, `country`, `region`, `district`, `city`, `locality`, `street`, `house`, `building`, `apartment`, `oktmo`, `okato`, `kladr_code` |
| `hierarchy` | `founder_kind`, `founder_place`, `creator_kind`, `creator_place`, `parent_organization`, `division_parent` |
| `reorganization` | `code`, `name`, `document`, `document_number`, `document_date`, `start_date`, `end_date` |
| `upstream_audit` | `source_version`, `load_date`, first/last registration GUID, last registration number, control/bid/update number и reason |
| `heads[]` | `full_name`, `position`, `is_primary`, `document_name`, `document_number`, `document_date` |
| `contacts[]` | `phone`, `email`, `website` |
| `activities[]` | `code`, `name`, `kind` |
| `authorities[]` | `code`, `name`, `permissions[]: { code, name }` |
| `permissions` | named arrays `participant`, `non_participant`, `procurement`, `accepted`, `transferred`, `budget_participant`, `budget_institution` |
| `accounts` | named arrays `personal`, `financial_authority`, `treasury` |
| `successions[]`, `contracts[]`, `attachments[]` | отдельные named item schemas; fields не сериализуются в одну display-строку |
| `unclassified_blocks` | `Record<string, JsonObject[]>` только для непустого неизвестного upstream block, с raw lineage и schema-drift metric |
Коллекции всегда массивы, даже если пусты. Неизвестный непустой upstream block не выбрасывается:
он сохраняется в `unclassified_blocks`, маркируется schema-drift metric и получает named schema
после появления репрезентативного sample. Frontend не должен реконструировать коллекции из текста
или повторно обращаться к Budget.gov.
Инварианты:
- без `status` list возвращает все published статусы;
- `records_count` карточки источника равен `total_count` нефильтрованного list на одном snapshot;
- `organizations_count` — distinct `organization.uid`, а покрытие ОПК считается отдельно;
- следующий failed import не заменяет последний успешный published snapshot;
- поля-идентификаторы остаются строками, включая ведущие нули.
#### Реестр субъектов МСП — получателей поддержки
Текущий запрос таблицы:
```text
GET /api/v2/organization-source-records/?source_group=government_support&source=fns_sme_support_recipients&record_type=sme_support_measure&page=1&page_size=50&ordering=-record_date,-external_id
```
Нужно добавить в OpenAPI и runtime list endpoint параметры:
| Параметр | Тип | Правило |
| ------------------------------------------ | -------------------- | ----------------------------------------------------------------- |
| `source_group` | enum | `government_support` |
| `source` | enum/string | `fns_sme_support_recipients` |
| `record_type` | enum/string | `sme_support_measure` |
| `organization` | UUID | Точный `organization.uid` |
| `search` | string, max 200 | Организация, ИНН/ОГРН/ОКПО, номер поддержки, поставщик, форма/вид |
| `sme_category` | enum/string | Категория МСП |
| `region_code` | string | Код региона обогащённой организации |
| `support_form_code` | string | Точный upstream code формы |
| `support_kind_code` | string | Точный upstream code вида |
| `provider_inn` | string | Ровно 10 цифр |
| `has_violation` | boolean | Наличие нарушения |
| `support_unit` | enum | `RUB`, `square_meter`, `hour`, `percent`, `unit` |
| `amount_from`, `amount_to` | decimal string | Только RUB, включительно |
| `registry_entry_from`, `registry_entry_to` | date | Дата внесения в XML |
| `support_until_from`, `support_until_to` | date | Срок поддержки |
| `termination_from`, `termination_to` | date | Дата прекращения |
| `ordering` | enum/comma-separated | Allowlist ниже, `-` допускается |
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
Allowlist `ordering`:
```text
record_date, external_id, extension__organization__name,
extension__organization__inn, extension__organization__ogrn,
extension__organization__okpo, payload__support_form__name,
payload__support_kind__name, payload__support_until,
payload__provider__name, payload__sme_category__name,
payload__has_violation, amount
```
Нужны named schemas `SmeSupportRecordListPayload` и `SmeSupportRecordDetailPayload`.
List payload должен содержать `support_registry_number`, `recipient_type`, `sme_category`,
`region`, `support_form`, `support_kind`, даты решения/срока/прекращения/внесения,
`support_sizes[]`, `provider`, `has_violation`, количество нарушений и документов, дату snapshot.
Ниже — обязательные правила типов, без которых типизированный mapper заменить моки не сможет.
| Поле | Тип и инвариант |
| ----------------- | --------------------------------------------------------------------------------------------------------------- | -------------- | ------ | --------- | -------------------------------- |
| Dictionary | `{ code: string, name: string }`; не локализованная одиночная строка |
| `support_sizes[]` | непустой массив `{ unit_code: string, unit: 'RUB' | 'square_meter' | 'hour' | 'percent' | 'unit', value: decimal-string }` |
| `amount` | сумма только RUB-size либо `null`; часы, проценты и единицы не конвертируются в деньги |
| `region` | dictionary с nullable provenance; источник значения сохраняется |
| `provider` | `name`, `inn`, nullable `organization_uid`, `okpo`, `region`, `oktmo`; enrichment не заменяет upstream name/ИНН |
| Даты | ISO `YYYY-MM-DD`; `termination_date` и `source_updated_date` nullable |
| Счётчики | `violation_count` и `regulatory_documents_count` — integer, ноль не заменяется `null` |
Нельзя передавать отформатированную валюту или склеенные documents/violations вместо typed arrays.
Detail payload дополнительно обязан включать `source_document_id`, полный массив
`regulatory_documents`, полный массив `violations` и provenance snapshot. В объекте нарушения
нужны `type`, `recognized_date`, `remedy_deadline`, `remedied_date`; пустые значения — `null`.
`regulatory_documents` и `violations` всегда массивы. Enrichment поставщика nullable и не
подменяет исходные `provider.name` и `provider.inn`.
Кроме перечисленных list-параметров backend должен принять временные совместимые aliases
`support_form` и `support_kind`, если они ещё используются активным frontend callsite. Канонические
параметры, отражённые в OpenAPI, — `support_form_code` и `support_kind_code`; aliases не должны
появляться в новых frontend-вызовах и должны быть удалены только в согласованном breaking change.
#### Проверка членства в СРО
Проблема:
- в текущем `openapi.json` нет ни одного идентификатора источника СРО: `sro_membership`,
`sro_membership_check`, `sro-membership-check`;
- `source_group` list/export не принимает `sro_membership`, а payload list/detail не содержит
status членства, СРО, регион или дату допуска;
- следовательно, общий URL нельзя безопасно вызвать с generated DTO для данного экрана и
переключение off моков невозможно.
Целевой запрос:
```text
GET /api/v2/organization-source-records/?source_group=sro_membership&source=sro_membership_check&record_type=sro_membership&page=1&page_size=50&ordering=organization__name,payload__sro_name,payload__admission_date
```
Нужно добавить параметры:
| Параметр | Тип | Правило |
| ------------------------------------------ | -------------------- | --------------------------------------------- |
| `source_group` | enum | `sro_membership` |
| `source` | enum/string | `sro_membership_check` |
| `record_type` | enum/string | `sro_membership` |
| `organization` | UUID | Точный `organization.uid` |
| `search` | string, max 200 | name/full name, ИНН, ОГРН, ОКПО, название СРО |
| `membership_status` | enum | `active`, `excluded` |
| `region` | string | Точный нормализованный регион |
| `sro_id` | string | Точный resolved ID СРО |
| `admission_date_from`, `admission_date_to` | date | Включительно |
| `ordering` | enum/comma-separated | Allowlist ниже, `-` допускается |
| `page`, `page_size` | integer | Default 1/50, `page_size <= 100` |
Allowlist `ordering`:
```text
organization__name, organization__full_name, organization__inn,
organization__ogrn, organization__okpo, payload__membership_status,
payload__region, payload__sro_name, payload__admission_date, updated_at
```
Нужны named schemas:
```ts
interface SroMembershipListPayload {
membership_status: 'active' | 'excluded'
membership_status_raw: string
region: string
sro_id: string
sro_name: string
sro_url: string
admission_date: string | null
source_registry_date: string | null
}
interface SroMembershipDetailPayload extends SroMembershipListPayload {
source_record_url: string
retrieved_at: string
admission_date_missing_reason: 'not_found' | 'sro_page_unresolved' | 'parse_error' | null
lineage: {
lookup_key: 'inn' | 'ogrn'
lookup_value: string
sro_resolution: 'row_link' | 'same_site_id' | 'same_site_sitemap'
}
}
```
Одна строка — одно членство одной организации в одной СРО. Организация с несколькими СРО
возвращается несколькими стабильными `uid`. `sro_url` и `source_record_url` должны быть HTTPS
URL только в allowlist `*.reestr-sro.ru`; backend не должен передавать произвольный upstream URL.
Required/nullable инварианты detail:
- `organization.name`, `organization.full_name`, ИНН, ОГРН, ОКПО, `membership_status`,
`membership_status_raw`, `region`, `sro_id`, `sro_name`, `sro_url` обязательны;
- `admission_date`, `source_registry_date` и `admission_date_missing_reason` nullable;
- если `admission_date` задана, `admission_date_missing_reason` строго `null`;
- если `admission_date = null`, причина отсутствия строго non-null;
- `record_date` равна дате допуска при её наличии; `status` согласован с нормализованным
`membership_status`;
- unsafe/missing URL, конфликт ИНН/ОГРН, неизвестный membership status и отсутствие ОКПО не
публикуются как valid record: они попадают в quarantine с наблюдаемым кодом ошибки.
### `sources`
Затрагиваемые endpoint'ы:
- `GET /api/v1/sources/`
- `GET /api/v1/sources/{slug}/`
- `POST /api/v1/sources/{slug}/refresh/`
Нужно зарегистрировать все три source cards и отдавать их из списка и detail endpoint с
согласованными slug, counts, `source_items`, `latest_load`, `latest_success_load` и
`active_tasks`. Source item обязан содержать точный parser/source key из таблицы идентификаторов.
Для каждой карточки:
- `records_count` — количество published source records текущего snapshot;
- `organizations_count` — distinct `organization.uid` текущего snapshot;
- `last_updated_at` и latest load — дата именно последней успешной публикации, а не запуска;
- detail и list не расходятся по статусу, counts и активной task;
- `404` на неизвестный slug, а не пустая успешная карточка.
Refresh принимает `{}` или `{ "params": {} }`, ставит задачу в очередь и отвечает `202`:
```json
{
"status": "queued",
"task_ids": ["cce43750-3c50-48b7-aade-22cf4eb6cf87"]
}
```
Повторный активный запуск той же карточки возвращает typed `409 refresh_already_running` и не
создаёт competing snapshot. Для СРО до получения разрешения владельца upstream допустим
`409 upstream_access_not_approved`; это отдельная бизнес-ошибка, не пустой успешный refresh.
### `parsers`, `jobs` и `system logs`
Затрагиваемые endpoint'ы:
- `GET /api/v1/parsers/dashboard/`
- `POST /api/v1/parsers/run/{source_key}/`
- `GET /api/v1/jobs/{task_id}/`
- `GET /api/v1/system/logs/`, `GET /api/v1/system/logs/{id}/`,
`GET /api/v1/system/logs/export/`
Нужно:
- добавить три parser source key в dashboard, parser run и history enum/filter:
`budget_ubpandnubp`, `fns_sme_support_recipients`, `sro_membership_check`;
- для parser run не требовать внешние URL, pagination или upload-параметры: настройки источника
принадлежат backend;
- документировать response parser run (`task_id`, `status`, `source`) и 409 duplicate run;
- в job detail возвращать typed `queued`, `running`, `retrying`, `success`, `failed`,
`cancelled`, `skipped`, целочисленный `progress`, сообщение и source-specific result;
- отображать counts, registry/snapshot date и errors одного published snapshot в dashboard и
history;
- добавить source label и canonical parser source в list/detail/export логов.
### `export-ticket` и `export-download`
Текущий OpenAPI уже содержит ticket/download URL, но export enum не принимает
`sro_membership`.
Нужно:
- добавить все значения из колонки Export group в `OrganizationSourceRecordExportRequest.sources`;
- сохранить асинхронный ticket flow: `POST export-ticket``201` с `ticket`, `expires_in`,
`file_name`; затем form POST на `export-download`;
- сделать ticket одноразовым и короткоживущим; после истечения/повторного применения возвращать
typed `410 ticket_expired`;
- выгружать именно тот published snapshot, который соответствует source records, не raw/imported
данные;
- поддержать JSON, CSV и XLSX в соответствии с declared capability источника; значения ИНН,
ОГРН, ОКПО, IDs и номера реестра оставлять строками.
## Регрессия после `bun run apigen`: тип ordering
Проблема:
- генерация актуальной схемы успешно завершается, но `bun run type-check` завершается ошибкой;
- из generated `endpoints.schemas.ts` исчез экспорт
`V2OrganizationSourceRecordsListOrdering`;
- его импортируют `useMassMediaSourceTable.ts` и `useRopkSanctionsSourceTable.ts`.
Причина:
- параметр `ordering` у `GET /api/v2/organization-source-records/` стал просто `string`;
- Orval генерирует отдельный alias `V2OrganizationSourceRecordsListOrdering` только для OpenAPI
enum. Поэтому эта регрессия относится к контракту, а не к настройке Orval.
Нужно:
- вернуть `ordering` как named enum/schema в OpenAPI list operation;
- включить в него все уже поддерживаемые ключи СМИ и санкций, а также ключи трёх источников из
этого документа;
- поддержать на runtime все значения enum, включая обратную сортировку с `-` и несколько полей
через запятую, либо явно заменить frontend-контракт отдельным согласованным string type. Первый
вариант предпочтителен: он сохраняет совместимость generated API;
- после изменения выполнить `bun run apigen` и `bun run type-check` в backend/frontend CI.
Минимальный набор существующих ключей, который нельзя потерять:
```text
record_date, external_id, extension__organization__name,
payload__news_source, payload__sentiment,
payload__united_states, payload__united_states_sectoral,
payload__uk_hm_treasury, payload__uk_uksl,
payload__european_union, payload__switzerland, payload__ukraine
```
## Приёмка и последовательность замены моков
Backend готов к передаче frontend только после выполнения всех пунктов ниже.
- [ ] Runtime API и `openapi.json` содержат одинаковые identifiers трёх источников.
- [ ] List и detail records имеют named/discriminated schemas для budget, SME support и SRO.
- [ ] Все фильтры и ordering allowlist из этого документа доступны в runtime и видны Orval после
`bun run apigen`.
- [ ] `sro_membership` присутствует во всех требуемых enum, включая records и export.
- [ ] Pagination является typed и возвращает корректный `total_count` после фильтрации.
- [ ] Source cards, dashboard, list, detail, jobs и logs считают один published snapshot.
- [ ] Refresh/parser run возвращают typed task response, duplicate run — typed `409`.
- [ ] Export ticket/download поддерживает три source group и выдаёт published записи.
- [ ] Есть backend contract fixtures: zero, один и many records; nullable dates; multiple SRO на
одну организацию; partial и large detail бюджетного реестра; поддержка с несколькими
размерами/документами/нарушениями.
- [ ] `bun run apigen` проходит без ручных изменений generated файлов.
- [ ] `bun run type-check` проходит, включая СМИ и зарубежные санкции.
После этого frontend выполняет: отключает `VITE_BUDGET_PROCESS_REGISTRY_MOCK`,
`VITE_SME_SUPPORT_RECIPIENTS_MOCK`, `VITE_SRO_MEMBERSHIP_MOCK`; заменяет временные DTO на
generated schemas/adapters; добавляет runtime contract tests и удаляет mock fixtures только после
зелёных интеграционных проверок.