Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway, statement-normalization, tabular-recognition-service) с Purpose, изменения quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19, entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 KiB
arm-core Specification
Purpose
Требования к ядру arm-core со стороны единой нормализации ББ/ОФР: хранение результата формы контракта 2.0 (DBML v5.19), приём результатов 1.x и 2.0 в окне перехода, пересчёт формы после правки общим кодом arm_statements, правка строк, кодов и дат колонок, блокировки подтверждения и очистка форм перед выкладкой.
Requirements
Requirement: Хранение результата формы v2.0 (DBML v5.19)
Маппер результата 2.0 SHALL писать документ формы как пришёл, без повторной дедупликации колонок, кодов и значений:
каждая строка результата — financial_statement_line (row_index, source_ref = ref, line_status, line_code,
line_code_original_value = line_code результата, line_code_text, line_code_confidence, line_name,
line_name_confidence, parent_row_index — row_index строки с ref = parent_ref, needs_review, is_confirmed = false); каждая пара «строка × колонка» — одна financial_statement_value (amount, string_value, source_text =
values[].text, confidence, is_recognized, needs_review; значения, которого нет в результате, — пустая запись с
needs_review = false); колонки — financial_statement_column с period_kind, label_confidence; шапка —
financial_statement_meta с form_variant, needs_review_fields и header_source (раздел «Шапка формы»);
предупреждения и нарушения документа — document_issue (источник service); версия общего кода —
recognition_job.statements_version. Уникальность (meta_id, line_code) снята; (meta_id, source_ref) уникален там,
где source_ref не null. Каждый BALANCE / INCOME результата SHALL становиться отдельным document; вторая
форма того же типа в файле хранится (Р-10); ОСВ по счёту, DISCLOSURE и OTHER — по-прежнему один документ на тип (и
счёт) на файл. Каждый создаваемый document (ОСВ, формы, DISCLOSURE, OTHER) при приёме результата любого
MAJOR (мапперы 1.x и 2.0) SHALL получать file_ordinal = индекс документа в documents[] результата (с 0);
документ, который не создаётся (второй лист ОСВ с тем же счётом, второй DISCLOSURE), номер не занимает, но и не
сдвигает остальные — номера уникальны в файле и могут идти с пропусками.
Scenario: Книга из двух ОСВ
- WHEN результат содержит две ОСВ по счетам 60 и 62 (
documents[0],documents[1]), принятый маппером1.xили2.0 - THEN два документа с
file_ordinal0 и 1; уникальность(stored_file_id, file_ordinal)не нарушена, файл неERROR
Scenario: PDF с ББ и ОФР по 1.x
- WHEN результат
1.2содержитBALANCEиINCOME - THEN документы с
file_ordinal0 и 1
Scenario: Повтор кода сохраняется
- WHEN в результате две строки
okс кодом 2410 (пустая и с(649)) - THEN в БД две строки 2410 с разными
row_indexиsource_ref, обе со своими значениями
Scenario: Строка без кода
- WHEN в результате строка
unreadableсline_code_text = "ИЧ,250"и суммой - THEN строка хранится с
line_code = null,line_code_original_value = null,line_status = unreadable,line_code_text = "ИЧ,250"иrow_indexиз результата
Scenario: Два баланса одного периода в одном файле
- WHEN результат содержит два
BALANCEс одной паройperiod_year,period_quarter(лист-продолжение другого типа или двойная печать) - THEN создаются два документа с разными
file_ordinal; ни один не автоподтверждается (Р-18)
Scenario: Два баланса разных периодов в одном файле
- WHEN результат содержит
BALANCEна 30.09.2025 иBALANCEна 31.12.2024 - THEN два документа с разными
file_ordinal; каждый получает статус по своим условиям, правило Р-18 их не касается
Requirement: Признак строки, созданной правкой
Строка формы SHALL считаться созданной правкой аналитика (шаблонной, origin = template) тогда и только тогда, когда
row_index IS NULL. Маппер 1.x и маппер 2.0 SHALL всегда писать row_index (у 1.x — по порядку строк результата с
0), поэтому у строки из источника он не null. line_code_original_value SHALL хранить код строки из результата как
пришёл (null у unreadable / missing / detail и у строк, созданных правкой) и правками кода MUST NOT меняться.
Признак line_code_original_value IS NULL для origin MUST NOT использоваться.
Scenario: Строка источника без кода — не шаблонная
- WHEN у строки
unreadableиз результатаline_code_original_value = nullиrow_index = 7 - THEN
origin = source; правила шаблонных строк (удаление без сумм, 409) к ней не применяются
Scenario: Назначение одного кода двум строкам источника
- WHEN аналитик назначил код 1250 строке
unreadable(row_index = 7, без сумм), а затем тот же код — строкеmissing(row_index = 2) с суммой - THEN обе строки
okс кодом 1250, ни одна не удалена, 409 нет (повтор кода законен, Р-4)
Requirement: Шапка формы — сырой текст и уверенность
Маппер 2.0 SHALL писать в financial_statement_meta.header_source объект {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для всех полей шапки результата (organization_name, inn, report_date, units,
okud, okpo, okopf, okfs, okei, address); разобранные значения — в одноимённые колонки; *_original_value —
как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; organization_name_original_value = organization_name, inn_confidence = inn_confidence.
header_source и *_original_value правками MUST NOT меняться. Маппер 1.x заполняет header_source из полей шапки
1.x. StatementOut.header.source SHALL отдавать header_source.
Scenario: Подсказка у поля шапки
- WHEN в результате
organization_name_text = "ООО «Ромашкa»",organization_name_confidence = 67 - THEN
header_source.organization_name = {"text": "ООО «Ромашкa»", "confidence": 67}; экран показывает их в подсказке у поля
Requirement: Агрегат формы по коду
Все чтения формы «по коду» SHALL идти через одну чистую функцию «агрегат формы по коду» без хранения (DECISION-5):
для кода и колонки — сумма amount строк ok с этим кодом (null, если ни одной непустой), список line_id,
справочное имя. На неё переводятся межформенная проверка 1370 / 2400, суммы формы для консолидации и выгрузок,
parent_code подстрок, раскладка строк на экране, правка ячеек по line_code, метрики fields_*. Строки detail,
unreadable, missing в агрегат, КФО и снимок ЕХ MUST NOT попадать.
Scenario: Межформенная проверка по агрегату
- WHEN в ББ две строки 1370 (сумма 500 и 300 на отчётную дату)
- THEN межформенная проверка берёт 1370 = 800
Requirement: Условие RECOGNIZED при приёме 2.0
При приёме 2.0 статус документа формы SHALL быть RECOGNIZED ⇔ document.status = done и он согласован с
содержимым документа ∧ в metrics.warnings файла нет предупреждений уровня error ∧ counterparty_check ≠ MISMATCH ∧
период — конец квартала (period_year и period_quarter определены) ∧ у каждой строки ok с неуверенным кодом
(line_code_confidence < 100) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того
же типа и периода (та же пара period_year, period_quarter; формы без квартала не считаются); иначе NEEDS_CHECK.
Согласованность: ядро SHALL вызывать общую функцию статуса пакета (statement-normalization, «Шаг 8 — статус
документа и конверта») по входу, собранному из результата: layout_found — lines[] непуст, has_organization_or_inn
и period_known — по шапке и колонкам результата, строки и значения с их needs_review (значения detail функция не
учитывает), число checks.relations_failed, accepted = false, уровни warnings[] документа. done согласован, если
функция тоже даёт done; иначе ядро считает документ needs_check и пишет document_issue
RESULT_STATUS_INCONSISTENT (info, источник core). Это вызов той же функции, а не пересчёт §7 и needs_review. Статус конверта и needs_check соседнего документа в условие
MUST NOT входить (Р-19). Проверки §7, needs_review и предупреждения сервиса при загрузке MUST NOT пересчитываться.
Совпадение наименований: обе строки приводятся к нижнему регистру, ё → е, всё, кроме букв и цифр, — пробел,
пробелы схлопываются, крайние пробелы обрезаются (strip); совпало, если строки равны, или более короткая (не короче 5
символов) входит в более длинную, или отношение сходства difflib.SequenceMatcher ≥ 0,8; сравнение — с любым
справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря
ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются номера раздела: номер —
начальная часть первого слова после фразы из букв i, v, x (латиница; кириллические «і», «х» приводятся к ним),
остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени —
совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало.
Несовпадение — document_issue NAME_MISMATCH (info, источник core, строка). RECOGNIZED SHALL, как и прежде,
автоподтверждаться (OQ-157).
Scenario: Ячейка соседнего документа
- WHEN ББ
done, ОФР того же файлаneeds_check, предупреждений файла уровняerrorнет, ИНН совпал, период — 30.09.2025 - THEN ББ
RECOGNIZEDи автоподтверждается, ОФР —NEEDS_CHECK
Scenario: Потерянный json
- WHEN все документы
done, но вmetrics.warningsфайлаJSON_FAILED(илиTABLE_PARSE_ERROR) уровняerror - THEN все документы формы файла
NEEDS_CHECK
Scenario: Подмена неуверенного кода на соседа
- WHEN строка с кодом 1120 (уверенность 50) и наименованием «Нематериальные активы»
- THEN наименование не совпало со справочным «Результаты исследований и разработок», документ
NEEDS_CHECK,NAME_MISMATCHу строки
Scenario: Законный повтор с другим именем
- WHEN строка 1220 (уверенность 50) с наименованием «НДС, начисленный по отгрузке», а в словаре имён
fsbu4у 1220 есть это имя вторым - THEN наименование совпало,
NAME_MISMATCHнет
Scenario: Подмена итога раздела
- WHEN строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
- THEN номер раздела I не равен II у справочного «Итого по разделу II»,
NAME_MISMATCH
Scenario: Хвост OCR после номера раздела (Счастливочная, РБ)
- WHEN строка 1500 (уверенность 56, разлепленная) с наименованием «Итого по разделу V1», или строка с наименованием «Итого по разделу V /»
- THEN номер раздела V равен номеру справочного «Итого по разделу V», наименование совпало,
NAME_MISMATCHнет
Scenario: Сервис прислал done при неразобранной строке без значений
- WHEN документ пришёл
done, но в нём строкаunreadableбез значений - THEN функция статуса даёт
needs_check(не все строкиok/detail), документNEEDS_CHECK,RESULT_STATUS_INCONSISTENT; автоподтверждения нет
Scenario: Сервис ошибся со статусом
- WHEN документ пришёл
done, но в нём строкаunreadableс суммой - THEN документ
NEEDS_CHECK,RESULT_STATUS_INCONSISTENTу документа; автоподтверждения нет
Scenario: Период не конец квартала
- WHEN документ
doneс датой отчёта 15.08.2025 - THEN квартал не определён, документ
NEEDS_CHECK(как в D-6) и не автоподтверждается; ручное подтверждение — 409PERIOD_REQUIRED, пока аналитик не поправит дату отчёта
Requirement: Предупреждения и нарушения документа — document_issue
Ядро SHALL хранить предупреждения и нарушения документа в document_issue: вид (warning | relation | soft), код,
уровень (error | info), сообщение, список line_id, column_id (колонка формы) или column_index (столбец
предупреждения, которого нет среди колонок формы), поле шапки, источник (service | core); для нарушений — codes
(коды цели и членов), текст соотношения, expected, actual. Нарушение из checks.relations_failed — kind = relation, code = RELATION_FAILED, severity = error; из soft_relations_failed — kind = soft, code = RELATION_SOFT, severity = info; line_ids — все строки ok кодов codes в документе. При загрузке пишутся
предупреждения и checks результата (источник service) и проверки ядра уровня info (источник core):
CODE_NOT_IN_DICTIONARY (кода нет в словаре имён редакции документа), CODE_NOT_IN_EDITION (код не из перечня
редакции документа и его варианта — по копии перечня arm_statements), NAME_MISMATCH, RESULT_STATUS_INCONSISTENT.
Редакция документа для этих проверок — edition точки входа пакета для пересчёта (statement-normalization):
редакция с лучшей подпоследовательностью кодов строк ok документа, при равенстве — по эффективной дате; так бланк 66н,
сданный в 2025 году, проверяется по 66n, а не по дате. Поле edition ответа GET …/statement и выбор шаблонных
строк по-прежнему — по дате периода (§7 «Полный состав формы»). document_issue ведётся у любого документа файла: у
ОСВ — предупреждения osvDocument.warnings результата 2.0 (вид warning, источник service, OSV_*, info). При перезагрузке файла записи документа
удаляются вместе с ним. При пересчёте после правки записи relation и soft заменяются целиком независимо от источника;
записи warning источника service сохраняются, кроме снимаемых правкой: правка header.report_date (в том числе
подтверждение тем же значением) снимает PERIOD_UNKNOWN, REPORT_DATE_UNPARSED, REPORT_DATE_UNCERTAIN; правка
header.inn или header.organization_name — NO_ORGANIZATION_NO_INN; правка columns[].period_date —
COLUMN_WITHOUT_DATE и COLUMN_DATE_UNCERTAIN этой колонки; прочие error сервиса снимаются только перезагрузкой
файла. При удалении шаблонной строки её id SHALL убираться из line_ids всех записей документа. GET …/statement и
POST …/confirm SHALL читать нарушения §7 только из document_issue.
Scenario: Дата колонке
- WHEN у колонки предупреждение
COLUMN_WITHOUT_DATE, аналитик ставит ейperiod_dateчерез PATCH - THEN предупреждение удаляется, статус пересчитывается
Scenario: Неснимаемое предупреждение
- WHEN у документа
SEGMENT_COLUMNS_MISMATCH, аналитик правит значения - THEN предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
Scenario: Бланк 66н в 2025 году без ложного info
- WHEN форма Межениновской за 3 кв. 2025 на бланке 66н с кодом 1120 (в перечне
fsbu4его нет) - THEN редакция документа для проверок ядра —
66n(подпоследовательность длиннее),CODE_NOT_IN_EDITIONу 1120 нет
Scenario: Пропущенная колонка
- WHEN в результате предупреждение
COLUMN_SKIPPEDсcolumn_index = 5, колонки 5 средиcolumns[]нет - THEN запись
document_issueсcolumn_index = 5иcolumn_id = null; экран показывает её у документа
Requirement: Формы результата 1.x — §7 при приёме
Для формы, принятой по результату 1.x (окно перехода после выкладки ядра; checks контракта 2.0 у неё нет), ядро
SHALL один раз при приёме посчитать §7 точкой входа пакета для пересчёта (statement-normalization; вариант формы —
по структуре кодов, как у 2.0) по суммам результата и записать нарушения в document_issue (источник core, виды
relation / soft, как в требовании о document_issue) и form_variant в financial_statement_meta. Условие D-6
«проверки сошлись» для 1.x SHALL браться из этого расчёта; needs_review значения при приёме 1.x = НЕ
values_ok (values_ok — is_recognized не false и confidence равна 100 или пуста), needs_review строки —
false. Предупреждения маппера 1.x по документу (повтор кода, значение вне колонок) — document_issue
MAPPING_WARNING (info, источник core); строки metrics.warnings результата 1.x — в
recognition_job.metrics.warnings структурами {code: "LEGACY_WARNING", severity: "info", message}. После правки такая
форма пересчитывается общим кодом, как форма 2.0; MAPPING_WARNING и LEGACY_WARNING статус не держат. Формы,
существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед
выкладкой»).
Scenario: Полярность needs_review при приёме 1.x
- WHEN в результате
1.2у значенияconfidence = 100,is_recognized = true, у другого —confidence = 80 - THEN у первого
needs_review = false, у второго —true
Scenario: Форма 1.x с нарушением
- WHEN результат
1.2содержит ББ с несошедшимся1600 = 1700 - THEN у формы запись
document_issuerelationRELATION_FAILEDсcodes = ["1600", "1700"](источникcore), документNEEDS_CHECK;POST …/confirmбез акцепта — 409CHECKS_NOT_PASSED
Scenario: 1.x-документ после PATCH
- WHEN форма принята по результату
1.2, аналитик правит сумму 1230 так, что1200 = 1210 + … + 1260сходится - THEN ядро пересчитывает §7 и статус общим кодом, запись
relationпо 1200 заменена (её больше нет), статус пересчитан;STATEMENTS_VERSION_MISMATCHне пишется (у задания 1.x версии нет)
Requirement: Пересчёт после правки
После каждого PATCH формы ядро SHALL пересчитывать checks, needs_review, form_variant, родителей строк detail
и статус точкой входа пакета для пересчёта по сохранённым данным (statement-normalization; конвенция знака — по
source_text), с правилами: is_modified или is_confirmed значения ⇒ needs_review = false; line_code_modified или
is_confirmed строки ⇒ needs_review строки = false; значения строк detail — needs_review = false (Р-23), у
строки, выведенной из detail назначением кода, needs_review её значений считается по общему правилу; акцепт ⇒
жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят. Результат пишется: needs_review
строк и значений, form_variant, parent_row_index всех строк detail (ближайшая строка ok выше по row_index;
нет такой — null, строка остаётся detail), записи document_issue видов relation / soft, статус. Отличие statements_version задания от версии копии ядра — document_issue
STATEMENTS_VERSION_MISMATCH (info); у задания без версии (1.x) запись не пишется. Один PATCH — один пересчёт и одна
запись аудита RECOGNIZED_DATA_EDIT. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
колонки, поля шапки; подтверждение или снятие подтверждения (confirmed) и подтверждение даты отчёта или даты колонки
тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47).
Scenario: Подтверждение ячейки без правки
- WHEN аналитик подтверждает ячейку с
needs_review = trueбез изменения суммы - THEN
is_confirmed = true,confirmed_by/at,is_modifiedне ставится,needs_review = false, аудит; акцепт проверок, если был, сохраняется
Scenario: Снятие подтверждения
- WHEN аналитик снимает подтверждение с ячейки (
confirmed: false), которая не покрыта соотношением - THEN
is_confirmed = false,needs_review = trueпосле пересчёта, аудит
Scenario: Родитель уходит в расшифровки
- WHEN у строки 1150 две расшифровки (
detail,parent_row_index= 1150), аналитик отмечает саму 1150 расшифровкой - THEN после пересчёта родитель всех трёх строк
detail— ближайшая строкаokвыше 1150; строк над ними нет —null(на экране — в начале раздела)
Requirement: PATCH формы — строки, колонки, подтверждение, наименование организации
PATCH /documents/{id}/statement SHALL принимать помимо header {inn, report_date, units} и values (сумма):
header.organization_name— непустая строка до 1000 символов:organization_name_modified = true,organization_name_original_valueне меняется, пересчитываетсяcounterparty_check, аудитbefore/after;header.report_date, равный текущему, у поля изneeds_review_fields— подтверждение даты: поле уходит изneeds_review_fields, снимаетсяREPORT_DATE_UNCERTAIN, аудит; иначе запрос без изменений — 200 без эффектов, как прежде;values[] {line_id, column_id, confirmed: true | false}— подтвердить ячейку без изменения значения или снять подтверждение;columns[] {column_id, period_date}— дата колонке (обязательная дата;null— 422 ошибка валидации; пересчитываетis_currentи период документа по правилу §9.5 п. 2); ручная дата ставит колонкеperiod_kind = absolute, чтобы следующая правка даты отчёта её не перезаписала; дата, равная текущей, у колонки сCOLUMN_DATE_UNCERTAINилиCOLUMN_WITHOUT_DATE— подтверждение: предупреждение снимается,period_kind = absolute, аудит (иначе запрос без изменений — 200 без эффектов); правкаheader.report_date(в том числеnull) пересчитываетperiod_dateколонок сperiod_kind = relative:*общим кодом (nullдаётnull);lines[] {line_id, line_code}— назначить или сменить код;lines[] {line_id, line_status: "detail"}— отметить расшифровкой;lines[] {line_id, confirmed: true | false}— подтвердить код или снять подтверждение.
Переходы: unreadable / missing / detail → ok назначением кода; unreadable / missing / ok → detail
отметкой; ok → ok сменой кода; ok с неуверенным кодом — подтверждением. Обратного перехода detail →
unreadable / missing нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла.
line_status принимает только "detail"; "ok" — 422 LINE_CODE_REQUIRED (в ok строку переводит только
line_code). Назначенный код: line_code_confidence = 100, line_code_modified = true, needs_review строки =
false. Подтверждённый код (confirmed: true): is_confirmed = true, needs_review строки = false,
line_code_confidence остаётся как пришло, line_code_modified не ставится; снятие подтверждения (confirmed: false)
возвращает needs_review по прежнему правилу (Р-34). line_code_original_value не меняется; после любого изменения статусов или кодов
parent_row_index всех строк detail документа пересчитывается как ближайшая строка ok выше по row_index
(требование «Пересчёт после правки»). Код — 4–5 цифр (иначе 422 INVALID_LINE_CODE), может совпадать с
существующим; незнакомый словарю — info. Строка с тем же кодом, созданная правкой (row_index IS NULL), с суммами —
409 LINE_CODE_TAKEN (сначала очистить её суммы или выбрать другой код); без сумм — удаляется (её id убирается из
document_issue.line_ids), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном
элементе больше одного действия или ни одного — 422 INVALID_LINE_PATCH; строка не этого документа — 422
UNKNOWN_LINE; колонка не этого документа — 422 UNKNOWN_COLUMN. Ошибки элемента lines[] (LINE_CODE_TAKEN,
LINE_CODE_REQUIRED, INVALID_LINE_CODE, INVALID_LINE_PATCH, UNKNOWN_LINE) SHALL нести в detail line_id и
line_code первого неверного элемента (line_code — код элемента или null). Правка суммы по line_code (для достроенной строки)
при наличии строки ok с этим кодом — 422 UNKNOWN_CELL (передайте line_id). Удаления строк источника нет. Строки,
созданные правкой: row_index = null, source_ref = null, line_status = ok, line_code_original_value = null. Аудит
RECOGNIZED_DATA_EDIT с before/after по коду, статусу, родителю, подтверждениям и наименованию организации.
Ограничения состояния — как прежде (DOCUMENT_READONLY, FILE_SUPERSEDED, DOCUMENT_NOT_STATEMENT).
Scenario: Назначение кода строке-мусору (AURORA-1462)
- WHEN аналитик назначает строке
unreadable«ИЧ,250» (row_index = 2) код 1250 - THEN строка
ok,line_code = 1250,line_code_confidence = 100,line_code_modified = true,line_code_original_valueостаётсяnull,row_indexостаётся 2 (origin = source); §7 и статус пересчитаны
Scenario: Отметка расшифровкой
- WHEN аналитик отмечает строку
missingс суммой как расшифровку - THEN
line_status = detail, строка выходит из сумм и не блокирует подтверждение
Scenario: Код занят шаблонной строкой с суммами
- WHEN аналитик назначает строке
unreadableкод 1250, а строка 1250 создана правкой аналитика (row_index = null) и имеет сумму - THEN 409
LINE_CODE_TAKEN, ничего не изменено
Scenario: Код занят пустой шаблонной строкой
- WHEN аналитик назначает строке
unreadableкод 1250, а строка 1250 создана правкой и сумм не имеет - THEN шаблонная строка удалена, её
idубран изdocument_issue.line_ids; строка источника сталаokс кодом 1250
Scenario: Правка наименования организации
- WHEN у формы
NO_ORGANIZATION_NO_INN, аналитик вводитheader.organization_name - THEN наименование сохранено,
organization_name_modified = true, исходное — вorganization_name_original_value,counterparty_checkпересчитан, предупреждение снято, аудит
Scenario: Подтверждение неуверенной даты отчёта
- WHEN
report_dateвneeds_review_fields, аналитик отправляет PATCH с тем жеheader.report_date - THEN
needs_review_fieldsпуст,REPORT_DATE_UNCERTAINснят, статус пересчитан, аудит
Scenario: Ручная дата относительной колонки
- WHEN колонке
relative:prev_year_endаналитик ставитperiod_date31.12.2024, затем правит дату отчёта - THEN после первой правки
period_kind = absolute; правка даты отчёта её дату не меняет
Scenario: Подтверждение неуверенной даты колонки
- WHEN у колонки
COLUMN_DATE_UNCERTAIN, аналитик отправляетcolumns: [{column_id, period_date}]с той же датой - THEN предупреждение снято,
period_kind = absolute, статус пересчитан, аудит
Scenario: Статус ok без кода
- WHEN приходит
lines: [{line_id, line_status: "ok"}] - THEN 422
LINE_CODE_REQUIRED
Requirement: Подтверждение формы — порядок 409
POST /documents/{id}/confirm для формы ББ/ОФР SHALL проверять в порядке: FILE_SUPERSEDED → ALREADY_CONFIRMED →
DOCUMENT_NOT_CONFIRMABLE → PERIOD_REQUIRED → LINES_UNRESOLVED (есть строки unreadable / missing, у которых хотя
бы в одной ячейке amount или string_value не null; detail.lines[] {line_id, row_index, line_code_text}; строки без
значений не мешают) → DUPLICATE_FORM (в файле уже подтверждена другая форма того же типа, period_year и
period_quarter; detail.document_id) → CHECKS_NOT_PASSED (записи document_issue вида relation без акцепта).
Между файлами повтор формы решает автозамена §9.5 п. 3.
Scenario: Неразобранная строка с суммой
- WHEN у формы строка
missingс суммой и одновременно жёсткое нарушение - THEN 409
LINES_UNRESOLVEDс этой строкой вdetail.lines
Scenario: Неразобранная строка только с текстом
- WHEN у формы строка
unreadableбез сумм, но с мусором211(1 |в ячейке значения (string_value) - THEN 409
LINES_UNRESOLVED— потерянная сумма тоже требует разбора
Scenario: Вторая форма того же периода
- WHEN в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй
- THEN 409
DUPLICATE_FORMсо ссылкой на первый
Requirement: Словари ядра и достройка шаблонных строк
Копия словаря §7 ядра (api/rules/statement_checks.yaml) SHALL удаляться: проверки ядро считает копией пакета
arm_statements. api/rules/statement_lines.yaml SHALL оставаться только словарём имён (код → наименования по
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
состав редакций — из копии перечня кодов пакета; sync-statements --check сверяет, что все коды словаря имён есть в
перечне. В PR перехода ядра на v2.0 словарь имён SHALL быть дополнен: в редакции fsbu4 — вторые имена законных
повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощенной системе
налогообложения» (Р-20) и коды печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные поисковые активы», 1140
«Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по fixtures/ocr и шаблонам
банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают NAME_MISMATCH. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка ok из
файла, шаблоном MUST NOT достраиваться; строки unreadable / missing не заменяются шаблонной строкой молча.
Scenario: Код есть только у строки unreadable
- WHEN у формы нет строки
okс кодом 1250, но есть строкаunreadable«ИЧ,250» - THEN шаблонная строка 1250 достраивается пустой, строка
unreadableпоказывается отдельно со статусом
Scenario: Два имени у кода
- WHEN в редакции
fsbu4словаря имён код 1520 записан дважды — «Кредиторская задолженность» и «Обязательство по аренде» - THEN словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
Scenario: Гудвил с заголовком раздела в имени
- WHEN строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил»
- THEN справочное «Гудвил» (≥ 5 символов) входит в него — совпало,
NAME_MISMATCHнет
Requirement: Ответ GET …/statement по v2.0
StatementOut SHALL отдавать: у документа form_variant; у шапки needs_review_fields, source (header_source;
confidence — строкой Decimal, как прочие уверенности API), organization_name_modified и okei; у колонок period_kind, label_confidence; у строк id, origin (template ⇔
row_index = null), row_index, source_ref, line_status, line_code, line_code_text, line_code_confidence,
line_name (из файла), reference_name (справочное имя по коду — первое имя кода в словаре — или null),
parent_row_index, parent_code, needs_review, is_confirmed, line_code_modified; у значений text
(source_text), needs_review, is_confirmed и прежние поля; issues[] — записи document_issue с line_ids,
column_id, column_index, field, codes; checks — нарушения из document_issue с codes и line_ids;
edition_lines[] {code, name} — справочник строк для подсказки кода: коды словаря имён формы в редакции ответа по одному
(имя — первое), у формы без редакции — коды всех редакций по одному. DocumentBrief (FileOut.documents[]) и
DocumentOut SHALL отдавать issue_counts {error, info} — число записей document_issue документа по уровню. Порядок
lines — по возрастанию кода, как сейчас (schemas/statements.py), одинаковые коды — по row_index; строки
unreadable / missing — после ближайшей предыдущей по row_index строки, размещённой по коду, без такой — в начале;
detail — после родителя. Разделы и размещение итогов на экране («как на бумаге», Р-14) строит экран.
Scenario: Документ, принятый по 1.x
- WHEN форма загружена до перехода сервиса (результат 1.2)
- THEN у строк
line_status = ok,row_indexпо порядку,source_ref = null,origin = source, у значенийtext = string_value,issuesсодержат нарушения §7, посчитанные ядром, иMAPPING_WARNING; экран работает
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проходит, удаляет колонки и таблицу и пишет в журнал число удалённых подтверждений и записей
Requirement: Очистка форм перед выкладкой
Перед выкладкой ядра с миграцией v5.19 на контур (шаг 3 порядка выкладки) база SHALL очищаться от форм ББ/ОФР (Р-24):
формы пересоздаются повторной загрузкой файлов уже по новым правилам. Очистку выполняет владелец или платформенная
команда по его заявке, на контуре — после резервной копии БД и до alembic upgrade v5.19 (на стендах разработчиков — по
желанию). Инструмент — скрипт ядра scripts/purge_statements.py (пишется в PR шага 3; описание здесь, кода нет) с
режимами --dry-run (по умолчанию: только отчёт) и --apply, одной транзакцией:
- Отбор: документы
BALANCE/INCOME(у файлов любого статуса, включаяSUPERSEDED). - Отказ без изменений, если хотя бы один отобранный документ упомянут в
ex_snapshot_itemили привязан (consolidation_document) к версии консолидации в статусе «Верифицирована» или дальше —VERIFIEDилиARCHIVEDверифицированной версии (естьex_snapshotсsource_version_idверсии): такая версия read-only и заморожена в ЕХ. Отчёт называет версии, решение — за владельцем. Версии в работе, на согласовании, отклонённые (REJECTED) и отменённые (CANCELLED, в том числе архивированные) очистке не мешают: привязки к ним снимаются, как к версиям «в работе» (п. 3, 4), статус версий не меняется (Р-35). - Файлы только с формами — у файла нет документов
OSV: удаляется файл целиком, какDELETE /files/{id}(§9.5 п. 5), но без запретаFILE_HAS_CONFIRMED_DOCUMENTS: привязки документов к неверифицированным версиям (п. 2) снимаются (статус версий не меняется), удаляются документы всех типов файла (BALANCE,INCOME,DISCLOSURE,OTHER) сfinancial_statement_*и комментариями документов, заданияrecognition_jobфайла иstored_file; объектам S3 ставитсяexpires_at = now(); ссылкаsuperseded_by_file_idдругих файлов на удаляемый обнуляется, а заменённые им файлы остаютсяSUPERSEDED(повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ). - Смешанные файлы — у файла есть и ОСВ, и формы: удаляются только документы
BALANCE/INCOMEс ихfinancial_statement_*, комментариями и привязками; файл, ОСВ,DISCLOSURE/OTHERи задания остаются. Повторно загрузить такой файл нельзя (дедуп поsha256), поэтому отчёт--dry-runперечисляет смешанные файлы отдельно, и владелец решает до--apply, удалять ли их целиком (ключ--include-mixed). - Аудит: на каждый удалённый файл — системное
FILE_DELETE(user_id = NULL, описание «очистка форм перед v5.19», карточка файла вbefore_value); на документ смешанного файла — то же событие по файлу с перечнем удалённых документов. - Не трогаются: ОСВ (
osv_entry), консолидации и их версии, корректировки, гудвилл, справочники, пользователи, журнал аудита. Результаты КФО неверифицированных версий, посчитанные с удалёнными формами, пересчитываются, как после удаления файла.
Scenario: Отчёт перед очисткой
- WHEN владелец запускает скрипт без ключей
- THEN скрипт ничего не меняет и печатает число файлов только с формами, смешанных файлов, документов форм, подтверждённых документов и привязок к неверифицированным версиям
Scenario: Форма в верифицированной версии
- WHEN документ ББ привязан к версии
VERIFIED - THEN
--applyотказывает с перечнем версий, база не изменена
Scenario: Форма в версии на согласовании или отклонённой
- WHEN документ ББ привязан к версии
VERIFICATION_RF, а ОФР — к версииREJECTED; верифицированных версий нет - THEN
--applyвыполняется: привязки сняты, статусы версий не изменились
Scenario: Очистка и повторная загрузка
- WHEN очистка выполнена, ядро v5.19 выложено, аналитик загружает тот же PDF с ББ и ОФР
- THEN дедуп не мешает (файла больше нет), формы создаются по мапперу 1.x или 2.0 — смотря что шлёт сервис
Requirement: Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0
recognition_job.metrics.warnings SHALL хранить предупреждения уровня файла структурами {code, severity, message, locator?} (fileWarning контракта): у 2.0 — как пришли, у 1.x — строки, обёрнутые в LEGACY_WARNING info.
JobBrief.warnings карточки файла (FileOut.job) SHALL отдавать список этих структур, а не строк; строки заданий,
сохранённых до выкладки ядра v5.19, при чтении конвертируются в {code: "LEGACY_WARNING", severity: "info", message: <строка>, locator: null} (данные в БД не переписываются). JobBrief.mapping_warnings (предупреждения маппинга ядра)
остаётся списком строк. Это несовместимое изменение типа поля для фронта — фиксируется в гайде фронта.
Предупреждения листа ОСВ результата 2.0 (osvDocument.warnings, коды OSV_*, info) SHALL храниться в
document_issue документа ОСВ (вид warning, источник service) и отдаваться в DocumentOut.issues[] (те же поля,
что issues формы) вместе с предупреждениями маппинга ядра этого листа; DocumentOut.warnings (строки маппинга файла)
сохраняется на окно перехода. osvDocument.status SHALL использоваться: при приёме 2.0 документ ОСВ RECOGNIZED ⇔
osvDocument.status = done ∧ checks листа без нарушений ∧ в metrics.warnings файла нет предупреждений уровня
error ∧ counterparty_check ≠ MISMATCH ∧ нет предупреждений маппинга ядра по этому листу (повтор счёта в файле, строки
без родителя, нарастающая по данным Q1); статус конверта в условие не входит (как Р-19 для форм). При приёме 1.x — как
прежде (§9.3).
Scenario: Карточка файла задания до выкладки
- WHEN у задания, сохранённого до v5.19,
metrics.warnings = ["sheet=ОСВ 60 строки без родителя"] - THEN
JobBrief.warnings = [{code: "LEGACY_WARNING", severity: "info", message: "sheet=ОСВ 60 строки без родителя", locator: null}]
Scenario: Потерянный json в карточке файла
- WHEN результат
2.0scan-gateway несётmetrics.warningsсJSON_FAILED - THEN
JobBrief.warningsсодержит{code: "JSON_FAILED", severity: "error", message, locator}; предупреждение не теряется
Scenario: ОСВ в файле с ОФР, требующим проверки
- WHEN книга из ОСВ (
status = done, контрольные суммы сошлись) и ОФР (needs_check), конвертneeds_check, предупреждений файлаerrorнет - THEN ОСВ
RECOGNIZEDи автоподтверждается; её предупрежденияOSV_*— вDocumentOut.issues
Requirement: crm_id ГСК/ГСЗ
related_group.crm_id (varchar, nullable, уникален) SHALL сохраняться в POST …/related-groups и
PATCH /directories/related-groups/{id} и отдаваться в RelatedGroupOut / RelatedGroupDetail; повтор — 409
CRM_ID_EXISTS; изменение — аудит GROUP_CHANGE с before/after (arm-core#112). Импорт ЕЦП crm_id групп не
заполняет (OQ-91).
Scenario: Занятый crm_id
- WHEN создаётся ГСЗ с
crm_id, который уже есть у другой ГСК или ГСЗ - THEN 409
CRM_ID_EXISTS, ничего не создано