Andrey Epifancev 4e42a93f54 chore(openspec): архив unified-normalization-core-a и -core-b (#126, #131)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:26:03 +04:00

18 KiB
Raw Blame History

data-model Specification

Purpose

Схема базы данных ядра совпадает с текущей моделью данных DBML (v5.18) один в один: таблицы, колонки, типы, enum'ы, ключи и индексы; миграции применяются к пустой базе и полностью откатываются; расхождение между ORM и DBML невозможно пропустить.

Requirements

Requirement: Схема БД равна DBML v5.19

ORM-метаданные SHALL содержать ровно 44 таблицы и 33 enum'а из ../docs/model/data-model-v5.19.dbml в схеме PostgreSQL из настройки DB_SCHEMA (по умолчанию dkb; в контуре — OPENBAO__APP__DB_SCHEMA; простой идентификатор [a-z_][a-z0-9_]{0,62}, не начинающийся с pg_, иначе приложение не стартует); сверка с DBML идёт в эталонной схеме dkb; имена таблиц, колонок, enum'ов и их значений SHALL совпадать с DBML буквально; nullability, PK, UNIQUE, FK, значения по умолчанию и индексы SHALL соответствовать DBML, включая частичные уникальные индексы (related_group_member (related_group_id, legal_entity_id) и (legal_entity_id, kind) WHERE is_current, consolidation_lock WHERE released_at IS NULL) и NULLS NOT DISTINCT на document (stored_file_id, document_type, osv_account_code). related_group_member.kind SHALL быть NOT NULL и равняться related_group.kind группы (проверяет ядро при вставке). audit_action SHALL содержать LEGAL_ENTITY_CHANGE и USER_CHANGE; osv_data_type SHALL содержать NU (после BU). Enum counterparty_match_kind SHALL содержать INN, NAME, ALIAS, MANUAL, PROPAGATED; колонка osv_entry.counterparty_match_kind (nullable) SHALL быть NULL тогда и только тогда, когда counterparty_id NULL (проверяет ядро при записи). app_user.email SHALL храниться в нижнем регистре. Миграция 0004_dbml_v5_16 SHALL создавать enum и колонку и заполнять её по уже смапленным строкам (ИНН строки без пробелов = legal_entity.inn → INN; trim(counterparty_name) = legal_entity.name → NAME; остальные с counterparty_id → ALIAS); откат SHALL удалять колонку и enum. Колонка kpp (varchar, nullable, без индексов и уникальности; уникален только inn) SHALL быть последней в legal_entity и legal_entity_history; формат и запрет КПП у ИП проверяет ядро. Миграция 0005_dbml_v5_17 SHALL добавлять обе колонки (существующие записи — kpp IS NULL), откат — удалять. Колонка stored_file.consolidation_id (uuid, nullable, FK → consolidation.id; v5.18) SHALL быть последней в stored_file; индекс ix_stored_file_legal_entity_id_consolidation_id (legal_entity_id, consolidation_id). Миграция 0006_dbml_v5_18 SHALL добавлять колонку, FK и индекс и заполнять колонку по consolidation_document: файлу с документом, привязанным к версии, — консолидация этой версии; при привязках к версиям разных консолидаций — самый ранний период (год, затем квартал); файлы без привязок остаются NULL; откат SHALL удалять индекс и колонку. Поля, отсутствующие в DBML, MUST NOT существовать. Схема v5.19 (RSHB/arm-ka-docs#116) SHALL добавлять: таблицу document_issue и enum'ы statement_line_status, statement_form_variant, document_issue_kind и прочие enum'ы DBML v5.19; колонки document.file_ordinal (уникальна в паре (stored_file_id, file_ordinal)), recognition_job.statements_version, related_group.crm_id (varchar, nullable, уникальна), в financial_statement_meta — form_variant, needs_review_fields, header_source, organization_name_original_value, organization_name_modified; в financial_statement_column — period_kind, label_confidence; в financial_statement_line — row_index, source_ref, line_status, line_code_text, parent_row_index, needs_review, is_confirmed; в financial_statement_value — source_text, needs_review, is_confirmed (с confirmed_by/at). Уникальность (meta_id, line_code) снимается, добавляется частичная уникальность (meta_id, source_ref) WHERE source_ref IS NOT NULL; уникальность документа (stored_file_id, document_type, osv_account_code) становится частичной (только для типов, кроме BALANCE / INCOME). Состав и порядок колонок — буквально по DBML v5.19.

Scenario: Сверка ORM с DBML

  • WHEN выполняется python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.19.dbml (make dbml-check)
  • THEN вывод пуст и код возврата 0

Scenario: Лишняя колонка в ORM

  • WHEN в ORM добавлена колонка, которой нет в DBML (или удалена существующая)
  • THEN скрипт печатает таблица.колонка: отсутствует в DBML (или отсутствует в ORM) и возвращает код 1

Scenario: Расхождение типа или nullability

  • WHEN колонка osv_entry.opening_debit объявлена в ORM как Numeric(18, 4) или NOT NULL
  • THEN скрипт сообщает ожидалось numeric(18,2) NULL / фактически numeric(18,4) NOT NULL и возвращает код 1

Scenario: ЮЛ одновременно в ГСК и ГСЗ

  • WHEN для одного legal_entity_id вставлены две текущие строки related_group_member с kind = gsk и kind = gsz
  • THEN обе вставки проходят; третья текущая строка с kind = gsk нарушает уникальный индекс

Scenario: Строка налогового учёта

  • WHEN в osv_entry вставляется строка с data_type = 'NU'
  • THEN вставка проходит

Scenario: Backfill при миграции

  • WHEN до миграции 0004 есть строки с counterparty_id, найденным по ИНН, по имени и по алиасу, и строка без counterparty_id
  • THEN после alembic upgrade head у них counterparty_match_kind = INN, NAME, ALIAS и NULL соответственно; alembic downgrade 0003 удаляет колонку и enum

Scenario: Backfill консолидации файла при миграции 0006

  • WHEN до миграции 0006 есть файл A с документом, привязанным к версии консолидации 2025 Q2 и к версии консолидации 2025 Q1, и файл B без привязок
  • THEN после alembic upgrade head у A consolidation_id = консолидация 2025 Q1, у B — NULL; колонка последняя в stored_file; alembic downgrade 0005_dbml_v5_17 удаляет индекс и колонку, повторный upgrade их возвращает

Scenario: Сверка с DBML v5.19

  • WHEN выполняется make dbml-check по ../docs/model/data-model-v5.19.dbml
  • THEN таблицы, колонки, enum'ы, ключи и индексы ORM совпадают с DBML без расхождений

Requirement: Миграции применяются с нуля и откатываются

Цепочка ревизий 0001_dbml_v5_13 → 0002_dbml_v5_14 → 0003_dbml_v5_15 → 0004_dbml_v5_16 → 0005_dbml_v5_17 → 0006_dbml_v5_18 SHALL создавать схему из DB_SCHEMA (если нет), все enum'ы, таблицы, ограничения, индексы и последовательности кодов (company_group_code_seq, related_group_code_seq) на пустой базе PostgreSQL 16 (требуется ≥ 15); 0003 SHALL добавлять NU в osv_data_type, приводить app_user.email к нижнему регистру и создавать индексы ix_osv_entry_parent_entry_id, ix_osv_entry_recognition_job_id, ix_stored_file_current_recognition_job_id (DBML v5.15 п. 4); downgrade до base SHALL удалять всё созданное (новые значения enum — через пересоздание типа), оставляя пустую схему; повторный upgrade после downgrade SHALL проходить без ошибок.

Scenario: Полный цикл

  • WHEN на пустой БД выполняются alembic upgrade head, alembic downgrade base, alembic upgrade head
  • THEN все три команды завершаются успешно, после последней в information_schema.tables схемы DB_SCHEMA 43 таблицы модели (+ служебная alembic_version) и в pg_type 28 enum-типов

Scenario: Откат до v5.14

  • WHEN после upgrade head выполняется alembic downgrade 0002_dbml_v5_14
  • THEN osv_data_type не содержит NU, индексов v5.15 п. 4 нет; регистр app_user.email не возвращается (данные)

Scenario: КПП добавляется и откатывается

  • WHEN на базе с ЮЛ выполняется alembic upgrade head, затем alembic downgrade 0004_dbml_v5_16
  • THEN после upgrade у legal_entity и legal_entity_history есть последняя колонка kpp varchar NULL, у существующих ЮЛ kpp IS NULL; после downgrade колонок нет, повторный upgrade их возвращает

Scenario: Нестандартная схема

  • WHEN DB_SCHEMA = dkb_alt и на пустой БД выполняется alembic upgrade head
  • THEN таблицы и enum'ы модели создаются в схеме dkb_alt, приложение работает с ней; схема dkb не создаётся

Scenario: Автогенерация ничего не находит

  • WHEN после upgrade head выполняется alembic revision --autogenerate на той же БД
  • THEN сгенерированная ревизия не содержит операций (пустые upgrade/downgrade)

Requirement: Версия DBML зафиксирована в коде

Пакет моделей SHALL объявлять константу версии DBML (DBML_VERSION = "5.19"); скрипт сверки SHALL сравнивать её с версией в шапке файла DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.

Scenario: Новая версия DBML без обновления ORM

  • WHEN в docs/model/ появляется data-model-v5.20.dbml, а константа в ORM — 5.19
  • THEN сверка против нового файла падает с сообщением о версии до сравнения таблиц

Requirement: Деньги и проценты — точные типы

Все денежные колонки SHALL быть numeric(18,2), проценты — numeric(7,4)/numeric(9,4), уверенность — numeric(5,2); ORM SHALL отдавать их как Decimal, float в моделях MUST NOT встречаться.

Scenario: Чтение денежной колонки

  • WHEN из osv_entry читается строка со closing_debit = 106311116.46
  • THEN значение имеет тип Decimal и равно Decimal("106311116.46")

Requirement: Миграция DBML v5.19

Одна миграция Alembic SHALL приводить схему к DBML v5.19 и только добавлять колонки, таблицу, enum'ы и индексы и заполнять значения по умолчанию (Р-24); §7 по существующим формам миграция MUST NOT считать, импорта копии arm_statements в ревизии нет. Перед выкладкой база очищается от форм ББ/ОФР (требование «Очистка форм перед выкладкой»), но миграция SHALL проходить и на непустой базе (стенды разработчиков, база, очищенная не полностью): row_index — NULL у строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию line_code внутри формы с 0; line_status = ok, needs_review строк — false, needs_review значений — НЕ values_ok (values_ok — правлено, или is_recognized не false и confidence равна 100 или пуста), source_text = string_value, is_confirmed = false, period_kind — absolute при period_date, иначе none, form_variant = null, header_source — из *_original_value и inn_confidence, organization_name_original_value = organization_name, document.file_ordinal — по (created_at, id) внутри файла с 0 (у всех документов), related_group.crm_id — null; document_issue — пуст. Корректность нарушений у форм, оставшихся с до-миграционных времён, не гарантируется: у них нет записей document_issue, поэтому до первой правки экран не показывает нарушений §7, а POST …/confirm не отвечает CHECKS_NOT_PASSED; первый PATCH пересчитывает форму общим кодом. Это цена Р-24; на контуре её снимает очистка. Сверка check_dbml_vs_orm.py — против data-model-v5.19.dbml.

Откат (downgrade) SHALL быть гарантирован на пустой БД и на данных, которые схема v5.18 может представить без потери первичных данных и ручного ввода. downgrade SHALL останавливаться с ошибкой, называющей причину и число записей, и ничего не менять, если есть: две строки с одним кодом в форме, строка с line_code = null, две формы одного типа в файле, непустой related_group.crm_id, organization_name_modified = true (исходное наименование хранится только в новой колонке). Остальные новые колонки и таблица document_issue — производные и сигнальные данные v5.19 (needs_review, source_text, header_source, is_confirmed с confirmed_by/at, period_kind, label_confidence, form_variant, needs_review_fields, file_ordinal, statements_version, document_issue): downgrade удаляет их и пишет в журнал миграции, сколько записей с непустыми значениями удалено (подтверждения аналитика остаются в audit_event). Это не «молчаливое удаление»: потеря названа в журнале и в этом требовании.

Scenario: Цикл миграций

  • WHEN alembic upgrade head → downgrade base → upgrade head на чистой БД
  • THEN все три шага проходят, сверка ORM с DBML v5.19 — без расхождений

Scenario: Миграция на непустой базе

  • WHEN на стенде есть форма ББ, загруженная до v5.19, с несошедшимся 1600 = 1700, и выполнена миграция
  • THEN миграция проходит: у строк row_index по коду, line_status = ok, у документа file_ordinal, записей document_issue нет — нарушение до первой правки не показано (корректность не гарантируется); после любого PATCH форма пересчитана, и запись RELATION_FAILED появляется

Scenario: Строка правки до миграции

  • WHEN до миграции у формы есть строка 1250, созданная правкой (line_code_original_value = null), с суммой
  • THEN после миграции у неё row_index = null, origin = template; строки источника пронумерованы с 0

Scenario: Откат на данных v2.0

  • WHEN в БД есть форма с двумя строками 2410, выполняется downgrade ниже v5.19
  • THEN откат останавливается с ошибкой о повторе кода; данные не изменены

Scenario: Откат после подтверждений

  • WHEN в БД нет несовместимых данных, но у ячеек есть is_confirmed = true и записи document_issue
  • THEN downgrade проходит, удаляет колонки и таблицу и пишет в журнал число удалённых подтверждений и записей