diff --git a/openspec/changes/unified-statement-normalization/design.md b/openspec/changes/unified-statement-normalization/design.md index 7ddf620..ba2b969 100644 --- a/openspec/changes/unified-statement-normalization/design.md +++ b/openspec/changes/unified-statement-normalization/design.md @@ -5,8 +5,9 @@ Issue: arm-ka-docs#116. Статус: **финальная версия** — ц `review-2026-10-17-closure.md`, `review-2026-10-17-blind.md`, `review-2026-10-17-closure-2.md`, `review-2026-10-17-blind-2.md`). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в `05-domain-rules.md`, DBML v5.19) написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (`review-2026-10-18-spec.md`, находки K-1…K-25) и -доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже). Где текст дизайна выше расходится с -решениями Р-20…Р-22 и S-n, действуют они и спека. +доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже), прошла повторное ревью вслепую +(`review-2026-10-18-spec-2.md`, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения +S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека. ## Цель @@ -775,7 +776,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо `07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени. -### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-22 — 18.10.2026) +### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-26 — 18.10.2026) - **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и @@ -875,6 +876,28 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо Excel-`ref`; S-17 — доопределить по K-23; S-19 — `codes` и `column_index` у `document_issue`; S-21 — по K-2 / K-21. Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже). +- **Р-23** (18.10.2026; повторное ревью спеки L-1) — значения строк `detail` в `needs_review` и в статус документа не + входят: у них `needs_review = false`, функция статуса их не учитывает; подсказка на экране по `text` и `confidence` + остаётся. Следствие: на реальных сканах РБ и Счастливочной за 1 кв. 2025 формы могут стать `done` (по прогону + ревьюера держали их только ячейки расшифровок). В спеке: `statement-normalization` («Шаг 8 — `needs_review` значения», + функция статуса, сценарий на РБ), `arm-core` (пересчёт после правки, согласованность), `arm-web`, §11.9, proposal; S-51 + пересмотрено. +- **Р-24** (18.10.2026; L-13 и упрощение S-36) — перед выкладкой ядра база очищается от форм ББ/ОФР (документы + `BALANCE` / `INCOME`, их `financial_statement_*`, комментарии, привязки к версиям «в работе»; файлы, где кроме форм + ничего нет, — целиком с заданиями; ОСВ, консолидации, справочники не трогаются; детали и отказы — S-73). Поэтому + миграция v5.19 только добавляет колонки, индексы и значения по умолчанию; §7 для существующих форм не пересчитывается, + импорта копии `arm_statements` в ревизии Alembic нет. Миграция обязана проходить и на непустой базе (колонки по + умолчанию, `file_ordinal` по порядку, `document_issue` пуст), но корректность нарушений у старых форм не + гарантируется — сказано явно. Для приёма 1.x после выкладки (окно перехода) §7 по-прежнему считается ядром копией + пакета при приёме (остаётся из S-36). В спеке: proposal (порядок выкладки), `arm-core` («Миграция DBML v5.19», + «Очистка форм перед выкладкой», «Формы результата 1.x — §7 при приёме»), §7 «Ядро», §9.5 п. 2, DBML v5.19 (шапка). +- **Р-25** (18.10.2026) — утверждены владельцем решения писателя S-26, S-34, S-36 (в редакции Р-24), S-37, S-41, S-47 + (подтверждение без правки не сбрасывает акцепт), S-48 (в редакции L-12: ядро вызывает общую функцию статуса). Отмечены + ниже. +- **Р-26** (18.10.2026; OQ-168) — коды без образца в fixtures (2421 / 2430 / 2450 в 66н, 1120 и другие в ФСБУ 4) в + перечень кодов не входят до появления образцов; неуверенный такой код на скане — `needs_review` строки. Временное + правило, принятое владельцем; OQ-168 остаётся открытым до образцов. + ## Decisions made without discussion Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все @@ -1031,6 +1054,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24. + Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). **Утверждено владельцем 18.10.2026 (Р-25).** 50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs @@ -1039,7 +1063,8 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо 51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО (1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024). - Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166. Все 12 + Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166 (временное + правило утверждено владельцем — Р-26). Все 12 последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`. 52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии, @@ -1047,7 +1072,8 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо `$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs. Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния - docs; без него их `make check` красный на окне. + docs; без него их `make check` красный на окне. **Заменено S-53** (повторное ревью L-2: файла `.v1` у запроса нет, + примеры и ядро не были покрыты). 53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в `testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`: `0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы @@ -1059,7 +1085,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо `column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их - позициями. + позициями. Дополнено S-56 (колонки сегментов со своей шапкой). 55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет; `TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший разбор таблицы держит весь файл на ручном подтверждении. @@ -1070,7 +1096,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо 57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`, `organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает `NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки - снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. + снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. **Утверждено владельцем 18.10.2026 (Р-25).** 58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` → `RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая. 59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией @@ -1081,11 +1107,13 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2). + **Утверждено владельцем 18.10.2026 (Р-25) в редакции Р-24:** миграционного расчёта нет — база перед выкладкой + очищается от форм; расчёт при приёме 1.x остаётся (вариант формы — по структуре кодов, S-59). 60. **S-37. Признак строки правки — `row_index IS NULL`** (K-1): маппер 1.x и 2.0 всегда пишет `row_index`; миграция ставит `NULL` строкам с `line_code_original_value IS NULL`; `line_code_original_value` — код из источника, правками не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок — - только в аудите. + только в аудите. **Утверждено владельцем 18.10.2026 (Р-25).** 61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра — есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или @@ -1099,7 +1127,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась. 64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы - в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора. + в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора. **Утверждено владельцем 18.10.2026 (Р-25).** 65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из `needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке. @@ -1119,10 +1147,12 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если - владелец считает подтверждение правкой — одна строка спеки. + владелец считает подтверждение правкой — одна строка спеки. **Утверждено владельцем 18.10.2026 (Р-25):** + подтверждение без правки акцепт не сбрасывает. 71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением, `needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT` - `info`. Цена: проверка на каждом маппинге — линейная по строкам. + `info`. Цена: проверка на каждом маппинге — линейная по строкам. **Утверждено владельцем 18.10.2026 (Р-25) в + редакции L-12:** согласованность — вызов общей функции статуса по входу из результата (не список признаков). 72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на экране — первое имя кода, файловое — рядом. @@ -1131,5 +1161,119 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая. 74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая. + **Пересмотрено Р-23:** значения `detail` статус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут стать + `done`; держат `needs_check` aep_3 и aep_6. 75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие `1600 = 1700` (страницы 2 баланса ×3), и записи, чьи статусы `done` меняться не должны. Цена: нулевая. + +### Решения при доработке спеки по повторному ревью L-1…L-30 (18.10.2026) + +Решения агента при закрытии находок `review-2026-10-18-spec-2.md` там, где ни ревьюер, ни владелец (Р-23…Р-26) не задали +значение, или где агент отступил от предложения ревьюера. Маркер `S-n` продолжает нумерацию; все обратимы правкой спеки, +словаря или DBML до реализации. + +76. **S-53. Правило теста идентичности копии контракта** (L-2; заменяет S-29): схема копии с `$id` `…/v` сверяется с + `<имя>.v.schema.json` docs, **если такой файл есть**, иначе с основным; пример — с `examples/v/<имя>.json`, если + он есть, иначе с `examples/<имя>.json` (`M` — `MAJOR` копии схемы вида примера, вид — по префиксу имени); прочие файлы + копии (`.v`, `examples/v/` у ядра) — с одноимёнными; файлы docs другого `MAJOR` сервис не сверяет. Правило + вводится в шаг 0 у **всех трёх** потребителей, включая ядро. Основание: существующие тесты (`arm-core/api/tests/ + test_contract.py`, `tabular-parser` и `scan-gateway` `api/tests/unit/test_contract.py`) сравнивают весь набор + `*.json` через `rglob`, включая `examples/`; правило «файл, если есть» проходит до слияния docs (файлов `.v1` нет), на + окне и после, у запроса без `.v1`. Цена: три маленьких PR до слияния docs; удаление `MAJOR` 1 — в порядке «сначала + ядро убирает копии `.v1`, затем docs». +77. **S-54. `file_ordinal` у всех документов** (L-3): индекс документа в `documents[]` результата при любом `MAJOR`, у + ОСВ, форм, `DISCLOSURE` и `OTHER`; несозданный документ (второй лист ОСВ с тем же счётом) номер не занимает и других + не сдвигает — номера с пропусками. Цена: номер не «порядковый без дыр»; экран его не показывает. +78. **S-55. Предупреждения файла и ОСВ в ядре** (L-4): `recognition_job.metrics.warnings` и `JobBrief.warnings` — + структуры `fileWarning`; строки старых заданий конвертируются при чтении в `LEGACY_WARNING` (данные не + переписываются); `JobBrief.mapping_warnings` — строки, как были. Предупреждения листа ОСВ 2.0 — `document_issue` + документа ОСВ, отдаются в новом `DocumentOut.issues[]`. `osvDocument.status` используется: ОСВ 2.0 `RECOGNIZED` ⇔ + `status = done` ∧ `checks` чисты ∧ нет `error` файла ∧ контрагент не `MISMATCH` ∧ нет предупреждений маппинга листа; + статус конверта не входит (как Р-19 для форм). Отступление от гайда «поля только добавляются»: тип `JobBrief.warnings` + меняется — записано в гайде фронта. Цена: фронт обязан перейти на объекты в той же выкладке, что и ядро; ОСВ в книге + с сомнительной ОФР теперь автоподтверждается (раньше держалась статусом задания). +79. **S-56. Колонки сегмента со своей шапкой** (L-6): `columns[]` задаёт первый сегмент; колонки следующего сегмента со + своей шапкой сопоставляются с ними по `period_date` (если у каждой колонки сегмента дата есть и совпадает ровно с + одной), иначе по порядку; при сопоставлении по порядку другое число колонок — `SEGMENT_COLUMNS_MISMATCH`, лишние + столбцы сегмента в `columns[]` не попадают, их номера — в `message`. Цена: сегмент с переставленными колонками без дат + сопоставится по порядку неверно — §7 это поймает; НПЗ (лист 4 ББ со своей шапкой) остаётся `done`. +80. **S-57. Явное правило мусора в `parse_amount`** (L-7): «ядро числа» от первого «цифра, `(`, знак» до последнего + «цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор, выбрасывается с `is_recognized = false`; в ядре — только цифры, + пробелы, `,`, `.`, знак в начале и пара скобок вокруг числа, иначе `amount = null` + `string_value` (буква в ядре, + непарная скобка, символ между цифрами); крайние `|` удаляются до замены двойников; `{` и `[` открывающей скобкой не + считаются. Так `211(1 |` и `(Ю4)` дают `null`, `: : 2856983 = ■` — 2856983, `{38378)` — `null` (сценарий «знак вне + конвенции» переписан на значение без знака). Отступление от сегодняшнего `amounts.py` (выбрасывал всё, кроме цифр): + осознанно. Цена: «12:34» даёт `null` и требует внимания аналитика; «1 234 руб.» даёт 1234 с `is_recognized = false` + (буквы после последней цифры — краевой мусор, значение нуждается в покрытии §7). +81. **S-58. Дрейф перечня кодов — код 10, не 20** (L-8): `sync-statements --check` в хосте сверяет копию перечня с + docs, расхождение — `10` (предупреждение), а не `20`; `20` — только прямая правка копии, чужая версия, недостающая + зависимость пакета и (в ядре) код словаря имён вне перечня **копии**. Порядок правки перечня: docs → `arm-statements` → + хосты. Цена: копия с устаревшим перечнем проходит `check-full` с предупреждением; видна в описании PR. +82. **S-59. Точка входа пакета для пересчёта** (L-10): `StoredForm` (без сетки) → `{form_variant, edition, checks, + needs_review строк и значений, родители detail, status}`; признаки `code_fixed` / `value_fixed`, строки + `row_index = null` участвуют в §7, `needs_review_fields` — вход (пересчёт шапку не переоценивает). Ядро вызывает её + после PATCH и при приёме 1.x; вариант формы при приёме 1.x — **по структуре кодов**, а не `null` (уточняет S-36: иначе + упрощённая форма 1.x получила бы нарушения полного набора до первой правки, а после правки — другой набор). Цена: + у формы 1.x `form_variant` может отличаться от «полного по умолчанию»; образца упрощённой формы нет (OQ-166). +83. **S-60. «Итого по разделу» — сравнение номеров раздела** (L-11): номер — начальные буквы `i`/`v`/`x` первого слова + после фразы (хвост OCR «V1», «V /» отбрасывается), `strip` явно; номера равны ⇔ совпало; номер не выделен — равенство + строк. Отступление от S-49 («только равенство»): ломалось на «Итого по разделу V1» Счастливочной. Цена: «II», + прочитанное как «I1», даст I ≠ II — ложное `NAME_MISMATCH` (безопасная сторона). +84. **S-61. «изо :» — пустой код, правило не меняется** (L-14): двойники цифрами в ячейке кода без цифр не считаются + (иначе слова, затёкшие в столбец кодов, стали бы `unreadable` и блокировали подтверждение); пример в §11.4 + исправлен. Цена: потерянная строка 1130 aep_3 («изо :», `'3'` с уверенностью 0) уходит в `detail` и скрыта; её сумма + не входит в 1100, и `1100 = …` подсвечивает раздел, если сумма больше допуска ±1. +85. **S-62. Родитель `detail`** (L-15): ближайшая строка `ok` выше (`unreadable` / `missing` пропускаются; нет `ok` выше — + строка `missing`); после каждого PATCH ядро пересчитывает родителей **всех** строк `detail`; нет `ok` выше — родитель + `null`, статус остаётся `detail`. Цена: строка `detail` без родителя на экране — в начале раздела. +86. **S-63. Дата колонки руками** (L-16, L-17): ручная дата ставит `period_kind = absolute`; дата, равная текущей, у + колонки с `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` — подтверждение (аналог S-42), снимает предупреждение и + акцепт не сбрасывает. Цена: после ручной даты относительная колонка не следует за правкой даты отчёта — так и нужно. +87. **S-64. Подтверждение дат только уверенными источниками** (L-18): для неуверенной подписи колонки эффективная дата — + дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с подписью уверенности 100; неуверенная + дата отчёта подтверждается только датами уверенных подписей. Для периода и относительных подписей эффективная дата — + по всем источникам. Цена: скан, где и дата отчёта, и все подписи неуверенны, — всегда `needs_check` с подтверждением + даты руками. +88. **S-65. Структурная проверка кода — уточнения** (L-19): «лучшая» подпоследовательность — сначала по числу уверенных + кодов (якоря), затем по длине; коды вне перечня редакции в её последовательность не входят; противоречащие якоря — + входит наибольшее согласованное их множество; неуверенный код подтверждён, если входит во все лучшие; вариант формы — + по всем строкам `ok` и пересчитывается ядром после правки кода. Цена: при перепутанных уверенных соседях неуверенный + код между ними может остаться неподтверждённым — `needs_review`, безопасная сторона. +89. **S-66. Форма знака** (L-20): по результату разбора (скобки, включая `}` / `]`; минус; без знака); сама ячейка в + голосование не входит; нужно строгое большинство, равенство — не подтверждён. Цена: колонка с двумя голосующими + разной формы не подтверждает знак ни одной сомнительной ячейки. +90. **S-67. Редакция для проверок ядра** (L-21): `CODE_NOT_IN_EDITION` и `CODE_NOT_IN_DICTIONARY` — по редакции точки + входа (лучшая подпоследовательность по всем строкам `ok`, ничья — по дате); `edition` ответа API и шаблонные строки — + по дате, как сейчас. Цена: на бланке 66н, сданном в 2025 году, шаблонные строки на экране — по `fsbu4`, а проверки — по + `66n`; сведение — отдельной задачей при необходимости (O-1 отчёта). +91. **S-68. `OTHER` удалён из v2.0, `form_variant` обязателен** (L-22): в схеме v2.0 `statementDocument.document_type` — + `BALANCE` | `INCOME` | `DISCLOSURE`, `form_variant` — в `required` (`null` у `DISCLOSURE`); `OTHER` остаётся в схеме + 1.x и в `document_type` ядра. Цена: нулевая — производителей `OTHER` в 2.0 нет. +92. **S-69. Откат миграции — точная формулировка** (L-24; уточняет S-44): `downgrade` останавливается только на данных, + которые v5.18 не может представить без потери первичных данных и ручного ввода (повтор кода, строка без кода, две + формы одного типа в файле, непустой `crm_id` групп, правленое наименование организации); производные и сигнальные + колонки v5.19 и `document_issue` удаляет с записью числа удалённых значений в журнал миграции (подтверждения — в + аудите). Цена: откат после работы аналитиков теряет подтверждения ячеек и кодов как признаки (история — в аудите). +93. **S-70. Механика копии пакета — уточнения** (L-25): `VERSION` из сравнения исключён (в источнике его нет); зависимости + пакета из его `pyproject` обязаны быть в зависимостях хоста (иначе `--check` = 20); пакет проходит самый строгий + конфиг `ruff` / `mypy` среди хостов; тесты пакета со своим `conftest`, `autouse`-фикстуры хоста ограничены его + каталогом `tests/`. Цена: пакет живёт по самым строгим правилам всех хостов. +94. **S-71. Содержимое таблицы ИС и `TABLE_SKIPPED`** (L-27): содержимое — хотя бы одна ячейка с цифрой или «цифрой под + видом буквы»; таблица только из текста без столбца кодов и типа — новое предупреждение файла `TABLE_SKIPPED` (`info`), + а не `TABLE_LOST` (`error`). Цена: текстовая таблица ИС не блокирует автоподтверждение; если в ней была форма без + единой цифры — такой формы не бывает. +95. **S-72. «Цифра под видом буквы» — содержимое строки** (L-30): ячейка значений, текст которой после замены двойников + без условия соседства содержит цифру и не содержит других букв (`'I'`, `'| О'`), делает строку строкой данных; текст + из настоящих букв при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев», «(расшифровка подписи)» у + Межениновской) — нет: иначе каждая книга получала бы мусорные строки `detail`. Признано осознанным исключением из + «ничего не выбрасывать молча». Цена: текстовая строка без цифр с уверенностью 100 по-прежнему теряется молча. +96. **S-73. Очистка форм перед выкладкой — детали** (Р-24): скрипт ядра `scripts/purge_statements.py`, `--dry-run` по + умолчанию, `--apply` одной транзакцией; отказ без изменений, если форма упомянута в `ex_snapshot_item` или привязана к + версии не «в работе»; файлы только с формами удаляются целиком (как `DELETE /files/{id}`, без запрета + `FILE_HAS_CONFIRMED_DOCUMENTS`, задания — тоже); в смешанных файлах «ОСВ + формы» удаляются только формы, файл + остаётся — повторно его не загрузить (дедуп `sha256`), поэтому `--dry-run` перечисляет их и владелец решает про + `--include-mixed`; системный аудит `FILE_DELETE`. Цена: подтверждённые формы и их привязки к версиям «в работе» + исчезают до повторной загрузки; КФО этих версий пересчитывается. +97. **S-74. `needs_review` значений после выхода строки из `detail`** (Р-23): строка, которой назначили код, получает + `needs_review` значений по общему правилу (покрытие §7); строка, отмеченная `detail`, — `false`; значения + `unreadable` / `missing` считаются по общему правилу (документ и так `needs_check` по статусу строки). Цена: нулевая. diff --git a/openspec/changes/unified-statement-normalization/proposal.md b/openspec/changes/unified-statement-normalization/proposal.md index 1e22693..5114c5e 100644 --- a/openspec/changes/unified-statement-normalization/proposal.md +++ b/openspec/changes/unified-statement-normalization/proposal.md @@ -31,7 +31,8 @@ - **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json` v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser меняется только в части структуры предупреждений и `document.status`. На окне перехода схемы 1.x лежат рядом - (`*.v1.schema.json`, примеры `examples/v1/`), тест идентичности сервиса сверяет копию со схемой своего MAJOR. + (`*.v1.schema.json`, примеры `examples/v1/`); тест идентичности копии у ядра и сервисов сверяет файл копии с файлом + docs своего MAJOR, если он есть, иначе с основным (схемы и примеры; правило — спека `recognition-contract`). - **Упорядоченные перечни кодов форм** — данные `contracts/statement-code-orders.yaml` (66н / ФСБУ 4, полная / упрощённая, законные повторы кода — Р-20); в пакет `arm_statements` — байтовой копией. - **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`, @@ -56,23 +57,37 @@ ### Порядок выкладки — «сначала ядро» (обязателен) -0. tabular-parser и scan-gateway: тест идентичности копии контракта выбирает файл docs по MAJOR копии (`$id` `…/v1` → - `*.v1.schema.json`) — до слияния шага 2, иначе их `make check` краснеет на окне перехода. +0. arm-core, tabular-parser и scan-gateway: тест идентичности копии контракта по новому правилу — схема копии + сверяется с `<имя>.v.schema.json` своего MAJOR, если такой файл есть в docs, иначе с основным; пример — с + `examples/v/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы другого MAJOR не сверяются. Правило + проходит и до слияния шага 2 (файлов `.v1` ещё нет), и после; без него `make check` всех трёх краснеет между шагами 2 + и 3 (ядро) или 2 и 5 (сервисы). 1. `arm-statements` — пакет с тестами; `sync-statements` в docs. 2. docs: контракт v2.0, DBML v5.19, правила (этот change). -3. arm-core: миграция v5.19, маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля - API. До этого шага ни один сервис не шлёт 2.0: первый же `line_code = null` дал бы файлу `ERROR`. +3. arm-core: **сначала очистка форм ББ/ОФР на контуре** (Р-24: владелец или платформенная команда по его заявке, + после резервной копии БД — скрипт ядра `scripts/purge_statements.py`: `--dry-run` с отчётом, затем `--apply`; удаляются + документы `BALANCE` / `INCOME`, их `financial_statement_*`, комментарии и привязки к версиям «в работе», а файлы, где + кроме форм нет ОСВ, — целиком с заданиями; смешанные файлы «ОСВ + формы» — по решению владельца по отчёту; при + формах в верифицированных версиях скрипт отказывает; ОСВ, консолидации, справочники не трогаются — спека `arm-core`, + «Очистка форм перед выкладкой»), затем миграция v5.19 (только колонки, индексы и значения по умолчанию; §7 для + старых форм не считается), маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля + API. Формы пересоздаются повторной загрузкой файлов. До этого шага ни один сервис не шлёт 2.0: первый же + `line_code = null` дал бы файлу `ERROR`. 4. arm-web: экран по новым полям (поля 1.x-документов заполнены по умолчанию — экран работает и на них). 5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга. 6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change). Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs -сверяются с `*.v1.schema.json` (шаг 0); каждый сервис обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих -MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удаляются вместе с маппером 1.x (шаг 6). +сверяются с `*.v1.schema.json` и `examples/v1/` (правило шага 0; у запроса файла `.v1` нет — с основным); каждый сервис +обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих MAJOR с PR шага 3. Схемы `*.v1.schema.json` и +`examples/v1/` удаляются вместе с маппером 1.x (шаг 6): сначала PR ядра убирает свои копии `.v1`, затем docs. -**Чего ждать на реальных сканах.** На шести OCR-документах `fixtures/ocr` после перехода большинство форм останутся -`needs_check` (строки без кода с суммами, неуверенные значения вне соотношений, неуверенная дата); законные повторы кода -и мягкие строки ОФР перестают держать документ (Р-20, Р-21). Доля `done` фиксируется в PR scan-gateway. +**Чего ждать на реальных сканах.** Значения строк `detail` в `needs_review` и в статус не входят (Р-23), законные +повторы кода и мягкие строки ОФР документ не держат (Р-20, Р-21). Поэтому ББ и ОФР РБ («Бухгалтерская отчетность 1 +кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: по прогону ревью все их неуверенные значения строк с кодом +покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали их только ячейки +расшифровок. aep_3 и aep_6 остаются `needs_check` (строки `unreadable` / `missing` с суммами, несошедшиеся +соотношения). Фактическая доля `done` фиксируется в PR scan-gateway. ## Non-goals @@ -122,8 +137,10 @@ MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удал - scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами 2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в словари пакета. -- arm-core: миграция v5.19 (`financial_statement_*`, `document`, `document_issue`, `recognition_job`, - `related_group`; `document_issue` существующих форм — §7 копией пакета), `result_mapping`, `statement_mapping`, +- arm-core: очистка форм ББ/ОФР перед выкладкой (`scripts/purge_statements.py`, Р-24); миграция v5.19 + (`financial_statement_*`, `document`, `document_issue`, `recognition_job`, `related_group`; только колонки, индексы и + значения по умолчанию, §7 по старым формам не считается — на неочищенной базе их нарушения до первой правки не + показываются); `JobBrief.warnings` — структуры, `DocumentOut.issues` (в том числе ОСВ); `result_mapping`, `statement_mapping`, `statements`, `documents`, `auto_confirm`, схемы API формы; признак строки правки — `row_index IS NULL`; `api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён (несколько имён на код, загрузчик повтор кода не запрещает); PATCH шапки принимает `organization_name`. diff --git a/openspec/changes/unified-statement-normalization/specs/arm-core/spec.md b/openspec/changes/unified-statement-normalization/specs/arm-core/spec.md index 32c03f3..8c796db 100644 --- a/openspec/changes/unified-statement-normalization/specs/arm-core/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/arm-core/spec.md @@ -11,9 +11,20 @@ false`); каждая пара «строка × колонка» — одна ` `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` с -`file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится (Р-10); ОСВ по -счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл. +где `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_ordinal` 0 и 1; уникальность `(stored_file_id, file_ordinal)` не нарушена, файл не `ERROR` + +#### Scenario: PDF с ББ и ОФР по 1.x +- **WHEN** результат `1.2` содержит `BALANCE` и `INCOME` +- **THEN** документы с `file_ordinal` 0 и 1 #### Scenario: Повтор кода сохраняется - **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`) @@ -24,10 +35,15 @@ false`); каждая пара «строка × колонка» — одна ` - **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`, `line_code_text = "ИЧ,250"` и `row_index` из результата -#### Scenario: Два баланса в одном файле -- **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать) +#### 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` — по порядку строк результата с @@ -75,17 +91,22 @@ units_text`; `organization_name_original_value = organization_name`, `inn_confid период — конец квартала (`period_year` и `period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом (`line_code_confidence < 100`) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того же типа и периода (та же пара `period_year`, `period_quarter`; формы без квартала не считаются); иначе `NEEDS_CHECK`. -**Согласованность:** `done` противоречит содержимому, если есть строка `unreadable` / `missing` со значением (`amount` или -`string_value` не `null`), строка, значение или поле шапки с `needs_review`, `checks.relations_failed` или предупреждение -документа уровня `error`; тогда ядро считает документ `needs_check` и пишет `document_issue` -`RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Статус конверта и `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 пересчитываться. **Совпадение наименований:** обе строки приводятся к нижнему регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел, -пробелы схлопываются; совпало, если строки равны, или более короткая (не короче 5 символов) входит в более длинную, или -отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** справочным именем кода (у кода может быть -несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря ядра. Если хотя бы одно из имён начинается с -«итого по разделу», совпадение — только равенство. Кода нет в словаре имён или наименование пусто — не совпало. +пробелы схлопываются, крайние пробелы обрезаются (`strip`); совпало, если строки равны, или более короткая (не короче 5 +символов) входит в более длинную, или отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** +справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря +ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера раздела**: номер — +начальная часть первого слова после фразы из букв `i`, `v`, `x` (латиница; кириллические «і», «х» приводятся к ним), +остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени — +совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало. Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка). `RECOGNIZED` SHALL, как и прежде, автоподтверждаться (OQ-157). @@ -110,8 +131,17 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре #### Scenario: Подмена итога раздела - **WHEN** строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I» -- **THEN** наименование не совпало со справочным «Итого по разделу II» (для «итого по разделу» — только равенство), - `NAME_MISMATCH` +- **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` с суммой @@ -130,8 +160,13 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре 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`. При перезагрузке файла записи документа +`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`; правка @@ -148,24 +183,36 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко - **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: Документы без `checks` — результаты 1.x и формы до миграции -Для формы без `checks` контракта 2.0 — принятой по результату `1.x` и существовавшей до миграции v5.19 — ядро SHALL -**один раз** посчитать §7 копией `arm_statements` по текущим суммам (`form_variant = null` — полный набор) и записать -нарушения в `document_issue` (источник `core`, виды `relation` / `soft`, как в требовании о `document_issue`): у `1.x` — -при приёме результата, у существующих форм — в миграции v5.19. Условие D-6 «проверки сошлись» для `1.x` SHALL браться из -этого расчёта. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue` +### 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` статус не держат. +форма пересчитывается общим кодом, как форма 2.0; `MAPPING_WARNING` и `LEGACY_WARNING` статус не держат. Формы, +существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед +выкладкой»). -#### Scenario: Мигрированная форма с нарушением -- **WHEN** до миграции форма `NEEDS_CHECK` с несошедшимся `1600 = 1700` без акцепта; миграция v5.19 выполнена -- **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]`; экран показывает - нарушение; `POST …/confirm` — 409 `CHECKS_NOT_PASSED` +#### 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_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]` (источник `core`), + документ `NEEDS_CHECK`; `POST …/confirm` без акцепта — 409 `CHECKS_NOT_PASSED` #### Scenario: 1.x-документ после PATCH - **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится @@ -173,14 +220,18 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет) ### Requirement: Пересчёт после правки -После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review` и статус тем же общим кодом -`arm_statements` по сохранённым данным (конвенция знака — по `source_text`; `form_variant = null` — полный набор), с -правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed` -строки ⇒ `needs_review` строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование -знака не входят. Отличие `statements_version` задания от версии копии ядра — `document_issue` +После каждого 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 сбрасывать. +колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки +тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47). #### Scenario: Подтверждение ячейки без правки - **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы @@ -191,6 +242,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко - **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`, @@ -201,8 +257,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко - `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения значения или снять подтверждение; - `columns[] {column_id, period_date}` — дата колонке (обязательная дата; `null` — 422 ошибка валидации; пересчитывает - `is_current` и период документа по правилу §9.5 п. 2); правка `header.report_date` (в том числе `null`) пересчитывает - `period_date` колонок с `period_kind = relative:*` общим кодом (`null` даёт `null`); + `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}` — подтвердить код или снять подтверждение. @@ -211,8 +270,9 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко `unreadable` / `missing` нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` принимает только `"detail"`; `"ok"` — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`, -`needs_review` строки = `false`; `line_code_original_value` не меняется; у `detail` `parent_row_index` пересчитывается -как ближайшая строка `ok` выше по `row_index`. Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с +`needs_review` строки = `false`; `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`), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном @@ -250,6 +310,14 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко - **WHEN** `report_date` в `needs_review_fields`, аналитик отправляет PATCH с тем же `header.report_date` - **THEN** `needs_review_fields` пуст, `REPORT_DATE_UNCERTAIN` снят, статус пересчитан, аудит +#### Scenario: Ручная дата относительной колонки +- **WHEN** колонке `relative:prev_year_end` аналитик ставит `period_date` 31.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` @@ -280,7 +348,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды словаря имён есть в -перечне. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из +перечне. В 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 @@ -292,6 +364,10 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко аренде» - **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`) и `organization_name_modified`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔ @@ -310,22 +386,41 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает ### Requirement: Миграция DBML v5.19 -Одна миграция Alembic SHALL приводить схему к DBML v5.19; заполнение существующих данных: `row_index` — `NULL` у строк с -`line_code_original_value IS NULL` (созданных правкой), у остальных — по возрастанию `line_code` внутри формы с 0; -`line_status = ok`, `needs_review` строк — `false`, `needs_review` значений — по прежнему `values_ok`, `source_text = +Одна миграция 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` каждой формы — нарушения §7, посчитанные копией `arm_statements` (требование «Документы без `checks`»). -Откат (`downgrade`) SHALL быть гарантирован на пустой БД и на данных, совместимых с v5.18 (нет двух строк с одним кодом в -форме, строк с `line_code = null`, двух форм одного типа в файле); на несовместимых данных `downgrade` SHALL -останавливаться с ошибкой, называющей причину, и MUST NOT удалять данные молча. Сверка `check_dbml_vs_orm.py` — против -`data-model-v5.19.dbml`. +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 @@ -334,6 +429,82 @@ organization_name`, `document.file_ordinal` — по `(created_at, id)` внут - **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`, одной транзакцией: +1. **Отбор:** документы `BALANCE` / `INCOME` (у файлов любого статуса, включая `SUPERSEDED`). +2. **Отказ без изменений**, если хотя бы один отобранный документ упомянут в `ex_snapshot_item` или привязан + (`consolidation_document`) к версии консолидации не «в работе» (`VERIFIED` и далее): отчёт называет версии, решение — + за владельцем. +3. **Файлы только с формами** — у файла нет документов `OSV`: удаляется файл целиком, как `DELETE /files/{id}` (§9.5 п. 5), + но без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`: привязки документов к версиям «в работе» снимаются (статус версий не + меняется), удаляются документы всех типов файла (`BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`) с + `financial_statement_*` и комментариями документов, задания `recognition_job` файла и `stored_file`; объектам S3 + ставится `expires_at = now()`; ссылка `superseded_by_file_id` других файлов на удаляемый обнуляется, а заменённые им + файлы остаются `SUPERSEDED` (повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ). +4. **Смешанные файлы** — у файла есть и ОСВ, и формы: удаляются только документы `BALANCE` / `INCOME` с их + `financial_statement_*`, комментариями и привязками; файл, ОСВ, `DISCLOSURE` / `OTHER` и задания остаются. Повторно + загрузить такой файл нельзя (дедуп по `sha256`), поэтому отчёт `--dry-run` перечисляет смешанные файлы отдельно, и + владелец решает до `--apply`, удалять ли их целиком (ключ `--include-mixed`). +5. **Аудит:** на каждый удалённый файл — системное `FILE_DELETE` (`user_id = NULL`, описание «очистка форм перед v5.19», + карточка файла в `before_value`); на документ смешанного файла — то же событие по файлу с перечнем удалённых + документов. +6. **Не трогаются:** ОСВ (`osv_entry`), консолидации и их версии, корректировки, гудвилл, справочники, пользователи, + журнал аудита. Результаты КФО версий «в работе», посчитанные с удалёнными формами, пересчитываются, как после удаления + файла. + +#### Scenario: Отчёт перед очисткой +- **WHEN** владелец запускает скрипт без ключей +- **THEN** скрипт ничего не меняет и печатает число файлов только с формами, смешанных файлов, документов форм, + подтверждённых документов и привязок к версиям «в работе» + +#### Scenario: Форма в верифицированной версии +- **WHEN** документ ББ привязан к версии `VERIFIED` +- **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.0` scan-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 diff --git a/openspec/changes/unified-statement-normalization/specs/arm-web/spec.md b/openspec/changes/unified-statement-normalization/specs/arm-web/spec.md index 3fec489..2ce2387 100644 --- a/openspec/changes/unified-statement-normalization/specs/arm-web/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/arm-web/spec.md @@ -23,7 +23,8 @@ уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек (`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` — столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized = -false` напрямую нет. +false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их +сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть. #### Scenario: Догадка подтверждена проверкой - **WHEN** значение `is_recognized = false`, `needs_review = false` @@ -33,7 +34,7 @@ false` напрямую нет. В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника (код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у -колонки без даты (`period_kind = none`) или с предупреждением о дате; снятие подтверждения ячейки и кода (`confirmed: +колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed: false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409 @@ -48,3 +49,13 @@ SHALL быть отключена с подсказкой «Разберите #### Scenario: Подтверждение заблокировано - **WHEN** у формы две строки `missing` с суммами - **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой + +### Requirement: Предупреждения файла и документа в карточке файла +Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}` +(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать +`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`. +`job.mapping_warnings` остаётся списком строк. + +#### Scenario: Потерянный ОФР в карточке +- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]` +- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки diff --git a/openspec/changes/unified-statement-normalization/specs/quality-checks/spec.md b/openspec/changes/unified-statement-normalization/specs/quality-checks/spec.md index 125c363..5a3564d 100644 --- a/openspec/changes/unified-statement-normalization/specs/quality-checks/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/quality-checks/spec.md @@ -4,8 +4,9 @@ Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`, `ruff format --check .`, `mypy src` (в сервисе с копией общего кода `arm_statements` — `mypy src arm_statements`, в `arm-core` — ещё `scripts`), `pytest -m "not integration"` (включая тесты копии `arm_statements/tests/`), тест -идентичности вендоренного контракта с `../docs/contracts` (на окне перехода между `MAJOR` — со схемой того `MAJOR`, -который сервис пишет). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать +идентичности вендоренного контракта с `../docs/contracts` (схема копии — с `<имя>.v.schema.json` своего `MAJOR`, +если такой файл есть в docs, иначе с основным; примеры — с `examples/v/`, если там есть одноимённый файл, иначе с +`examples/`; правило — спека `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать запущенного Docker, PostgreSQL или NATS. Свежесть копии `arm_statements` относительно репозитория `arm-statements` в `make check` MUST NOT проверяться (соседний репозиторий в `make check` не читается). diff --git a/openspec/changes/unified-statement-normalization/specs/recognition-contract/spec.md b/openspec/changes/unified-statement-normalization/specs/recognition-contract/spec.md index ea10b00..9480d2e 100644 --- a/openspec/changes/unified-statement-normalization/specs/recognition-contract/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/recognition-contract/spec.md @@ -54,7 +54,9 @@ v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `f `detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`, `is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected, -actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`; у документа +actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` (`OTHER` в v2.0 не +порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в +`document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе. #### Scenario: Результат валидируется примерами @@ -83,6 +85,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | ` - **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с `status` и `warnings`, файл проходит схему v2.0 +#### Scenario: Документ OTHER в v2.0 +- **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant` +- **THEN** файл не проходит схему v2.0 + #### Scenario: Строка без кода с кодом в поле - **WHEN** у строки `line_status = unreadable` передан непустой `line_code` - **THEN** файл не проходит схему v2.0 @@ -115,10 +121,25 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | ` ### Requirement: Копия контракта в сервисе идентична источнику Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check` их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением). -На окне перехода между `MAJOR` тест идентичности SHALL сверять копию со схемой того `MAJOR`, который сервис пишет (или, -у ядра, читает): копия с `$id`, оканчивающимся на `/v1`, — с `docs/contracts/<имя>.v1.schema.json`, иначе — с основным -файлом `docs/contracts/<имя>.schema.json`; ядро держит копии обоих `MAJOR`. Правило теста вводится в `tabular-parser` и -`scan-gateway` до слияния контракта v2.0 в docs, иначе их `make check` упадёт на окне перехода. +Файл копии SHALL сверяться с файлом docs по правилу, одинаковому у ядра и сервисов и верному до, во время и после окна +перехода между `MAJOR`: +- **схема** `<имя>.schema.json` копии: `M` — номер из её `$id` (`…/v`); пара — `docs/contracts/<имя>.v.schema.json`, + **если такой файл есть**, иначе основной `docs/contracts/<имя>.schema.json` (у `recognition-request` файла `.v1` нет + — его копия `…/v1` сверяется с основным файлом; до слияния v2.0 в docs файлов `.v1` нет вовсе — все копии сверяются с + основными); +- **пример** `examples/<имя>.json` копии: вид — по префиксу имени (`request-` — запрос, `result-message-` — конверт, + `result-` — файл результата), `M` — `MAJOR` копии схемы этого вида; пара — `docs/contracts/examples/v/<имя>.json`, + если такой файл есть, иначе `docs/contracts/examples/<имя>.json`; +- **прочие файлы копии** (`<имя>.v.schema.json`, `examples/v/*.json` — их держит ядро на окне) — с одноимённым + файлом docs; нет его в docs — расхождение; +- **набор:** каждой схеме и каждому примеру верхнего уровня docs (`contracts/*.schema.json` без `.v`, + `contracts/examples/*.json`) SHALL соответствовать файл копии; файлы docs `*.v.schema.json` и `examples/v/` + требуются в копии, только если копия держит этот `MAJOR` (ядро на окне); файлы другого `MAJOR` сервисом не + сверяются. + +Правило вводится в тест идентичности **всех трёх** потребителей (`arm-core`, `tabular-parser`, `scan-gateway`) до +слияния контракта v2.0 в docs (шаг 0 выкладки); иначе их `make check` упадёт на окне перехода. Ядро держит копии обоих +`MAJOR` с PR шага 3; при удалении `MAJOR` 1 (шаг 6) ядро сначала удаляет свои копии `.v1`, затем docs — файлы `.v1`. #### Scenario: Копия устарела - **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет @@ -130,8 +151,28 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | ` окне перехода не краснеет #### Scenario: Сервис перешёл на 2.0 -- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`) -- **THEN** тест сверяет её с основным файлом docs +- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`) и примеры до v2.0 +- **THEN** тест сверяет схему с основным файлом docs (файла `.v2` нет), примеры `result-*` — с `examples/*.json`; + `examples/v1/` и `*.v1.schema.json` docs не сверяются + +#### Scenario: Запрос v1.0 после слияния v2.0 +- **WHEN** в docs уже есть `recognition-result.v1.schema.json`, а `recognition-request.v1.schema.json` нет; копия + запроса в сервисе — v1.0 (`$id` `…/v1`), примеры `request-image.json`, `request-tabular.json` +- **THEN** копия запроса сверяется с основным `recognition-request.schema.json`, примеры запроса — с + `examples/request-*.json` (в `examples/v1/` их нет); тест проходит + +#### Scenario: Примеры прежнего MAJOR +- **WHEN** копия `tabular-parser` — v1.2, в ней `examples/result-statement-excel.json` версии 1.2, а в docs + `examples/result-statement-excel.json` уже 2.0, `examples/v1/result-statement-excel.json` — 1.2 +- **THEN** пример копии сверяется с `examples/v1/result-statement-excel.json` и проходит + +#### Scenario: Шаг 0 до слияния docs +- **WHEN** тест с новым правилом слит в `arm-core`, `tabular-parser` и `scan-gateway`, а docs ещё на v1.2 без файлов `.v1` +- **THEN** все копии сверяются с основными файлами и примерами верхнего уровня, тест проходит, как прежде + +#### Scenario: Ядро на окне перехода +- **WHEN** копия ядра после шага 3 держит основные схемы v2.0, `*.v1.schema.json` и `examples/v1/` +- **THEN** основные сверяются с основными docs, `.v1` и `examples/v1/` — с одноимёнными файлами docs; набор совпадает ## ADDED Requirements @@ -140,9 +181,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | ` `2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`, а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref = -null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения — по прежнему `values_ok` -(`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при -`period_date`, иначе `none`, `form_variant = null`; строки `metrics.warnings` — в `recognition_job.metrics.warnings` +null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения = **НЕ** `values_ok` +(`values_ok` — `is_recognized` не `false` и `confidence` 100 или пуста: чистое значение — `needs_review = false`), +`needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant` — по +структуре кодов (точка входа пакета для пересчёта, спека `arm-core`), `file_ordinal` — индекс в `documents[]`; строки `metrics.warnings` — в `recognition_job.metrics.warnings` структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу — `document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0` только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих @@ -150,8 +192,13 @@ null`, `line_code_original_value = line_code`, `text = string_value`, `needs_rev #### Scenario: Сервис ещё на v1.2 - **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes` -- **THEN** ядро принимает его маппером `1.x`, обе строки кода — по прежнему правилу «остаётся первая» с - `MAPPING_WARNING`, нарушения §7 — в `document_issue` от ядра, статус документа — по D-6, файл не `ERROR` +- **THEN** ядро принимает его маппером `1.x`: из двух строк с одним кодом хранится первая (прежнее правило маппера + `1.x`), вторая не хранится, у документа `MAPPING_WARNING` о повторе; нарушения §7 — в `document_issue` от ядра, статус + документа — по D-6, файл не `ERROR` + +#### Scenario: Чистое значение 1.x +- **WHEN** в результате `1.2` значение с `confidence = 100` и `is_recognized = true` +- **THEN** `needs_review = false`; значение с `confidence = 60` — `needs_review = true` #### Scenario: Ack нового сервиса - **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"` diff --git a/openspec/changes/unified-statement-normalization/specs/scan-gateway/spec.md b/openspec/changes/unified-statement-normalization/specs/scan-gateway/spec.md index b261738..9a65887 100644 --- a/openspec/changes/unified-statement-normalization/specs/scan-gateway/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/scan-gateway/spec.md @@ -43,4 +43,10 @@ document` — в шаги 2–6 пакета; `ocr_lookalikes.yaml` — в сл #### Scenario: Строки без кода в aep_3 - **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |` - **THEN** строки попадают в результат со статусами (`detail` под строкой с кодом, `missing` в начале таблицы) и сырым - текстом; счётчиков «строк без кода с суммами пропущено» нет + текстом; значение `211(1 |` — `amount = null`, `string_value = "211(1 |"`, `is_recognized = false` (скобка внутри + цифр, правило шага 4), а не 2111; счётчиков «строк без кода с суммами пропущено» нет + +#### Scenario: Таблица ИС только с текстом +- **WHEN** в json есть таблица без столбца кодов и без распознанного типа, в ячейках только текст подписей +- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` уровня `info` с `locator` таблицы; конверт из-за неё + не `needs_check` diff --git a/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md b/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md index 7f63941..044eb6e 100644 --- a/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md @@ -33,13 +33,24 @@ NOT делаться. Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source `, ссылка — `origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir `. Без `--check` скрипт раскладывает копию коммита -`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). `--check` -SHALL возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`; `10` — копия -равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала); `20` — копия отличается от дерева -коммита из `VERSION`, `VERSION` нет в истории источника, копия перечня кодов в пакете отличается от -`docs/contracts/statement-code-orders.yaml` или (только у `arm-core`) код словаря имён `api/rules/statement_lines.yaml` -отсутствует в перечне своей редакции; `2` — нет checkout источника или хоста, неизвестный аргумент. Файлы и коды -расхождения SHALL печататься. +`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). Копия +сравнивается с деревом `arm_statements/` источника без файла `VERSION` (его в источнике нет). `--check` SHALL +возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов +в пакете равна `docs/contracts/statement-code-orders.yaml`; `10` — копия равна дереву коммита из `VERSION`, но в +`--ref` есть более новый (копия отстала), или копия перечня кодов отличается от файла docs (перечень в docs правился +раньше `arm-statements` или пересинхронизации хоста — предупреждение называет файл); `20` — копия отличается от дерева +коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список `dependencies` +его `pyproject`) отсутствуют в зависимостях хоста или (только у `arm-core`) код словаря имён +`api/rules/statement_lines.yaml` отсутствует в перечне своей редакции **копии пакета**; `2` — нет checkout источника +или хоста, неизвестный аргумент. Файлы и коды расхождения SHALL печататься. Порядок правки перечня кодов: PR в docs → +PR в `arm-statements` (байтовая копия yaml) → `sync-statements` в хостах «сначала ядро»; до конца этой цепочки хосты +получают `10`, а не `20`. + +Код пакета SHALL проходить самый строгий конфиг `ruff` и `mypy` среди хостов (пакет проверяется им в своём +репозитории): конфиг хоста применяется к копии, а правка копии под хост запрещена. Тесты пакета SHALL не зависеть от +фикстур и `conftest` хоста: в `api/arm_statements/tests/` свой `conftest.py`, а `conftest` хоста MUST NOT содержать +`autouse`-фикстур без ограничения каталогом `tests/` хоста. Зависимости пакета — только те, что есть в зеркале банка +(сейчас PyYAML); хост держит их в своих зависимостях с версией зеркала. Цель `statements-check` хоста (`sync-statements.sh --check --host-dir .`, docs — `../docs` или `ARM_KA_DOCS`) SHALL входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR; @@ -58,6 +69,11 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat - **WHEN** рядом нет `arm-statements` и `--source` не задан - **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный +#### Scenario: Перечень кодов в docs новее копии +- **WHEN** в docs в `statement-code-orders.yaml` добавлен код, а `arm-statements` и копии в хостах ещё не обновлены +- **THEN** `--check` в каждом хосте завершается с кодом 10 и называет файл перечня; `make check-full` зелёный с + предупреждением, посторонние PR в сервисы не краснеют + #### Scenario: Код словаря ядра вне перечня - **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет - **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию @@ -138,7 +154,8 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat - **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false` ### Requirement: Шаг 1 — многостраничная форма -Одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру +Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная +шапка внутри таблицы ИС — сегмент шага 2). У Excel одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT` (`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и @@ -163,8 +180,8 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца. #### Scenario: Год и сумма — не код -- **WHEN** в ячейке «2024» или четырёхзначная сумма «3450» -- **THEN** кодом это не считается (вне отрезков) +- **WHEN** в ячейке «2024» или четырёхзначная сумма «1050» или «7450» +- **THEN** кодом это не считается (вне отрезков); «3450» — код (отрезок 3100–3699 отчёта об изменениях капитала) #### Scenario: Краевая пунктуация и черта - **WHEN** ячейка кода — `"1220 ,"` или `"| 140"` @@ -191,10 +208,35 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона -столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с -кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение -`SEGMENT_COLUMNS_MISMATCH` (`error`) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение -`SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, статусы по шагу 3). +столбца первого сегмента, минимум один. Колонки документа (`columns[]`) SHALL задаваться первым сегментом; колонки +любого следующего сегмента новых `columns[]` не создают, а сопоставляются с колонками первого сегмента: +- сегмент со своей шапкой (повторная шапка OCR, лист-продолжение Excel со своей строкой заголовка) — по `period_date`, + если у каждой колонки сегмента дата есть и совпадает с датой ровно одной колонки первого сегмента; иначе по порядку; +- сегмент без шапки — колонки значений — столбцы правее кода, где в строках с кодами есть числа, сопоставление по + порядку. + +Значения сегмента несут `column_index` сопоставленной колонки первого сегмента. При сопоставлении по порядку число +колонок сегмента не совпало с первым — предупреждение `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции в +пределах меньшего числа колонок, +лишние столбцы сегмента в `columns[]` не попадают, их номера в сетке перечислены в `message`. Столбец кодов не найден ни +так, ни так — предупреждение `SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, +статусы по шагу 3). + +#### Scenario: Лист-продолжение НПЗ со своей шапкой +- **WHEN** у ББ НПЗ («Бухгалтерский баланс. Лист 4») своя строка заголовка, колонки периодов — в столбцах 8–10, у первого + листа — в столбцах 16, 21, 24, даты подписей те же (30.09.2025, 31.12.2024, 31.12.2023) +- **THEN** новых колонок нет: значения листа 4 получают `column_index` 16, 21, 24 по совпадению `period_date`; + предупреждения нет, §7 ББ считается по одной колонке на дату, документ остаётся `done` + +#### Scenario: Повторная шапка с другими подписями +- **WHEN** повторная шапка второй страницы скана содержит две колонки без разобранной даты, у первого сегмента — две + колонки с датами +- **THEN** сопоставление по порядку, значения несут номера колонок первого сегмента, предупреждения нет + +#### Scenario: Сегмент шире первого +- **WHEN** у сегмента со своей шапкой четыре колонки периодов, у первого — три +- **THEN** `SEGMENT_COLUMNS_MISMATCH` уровня `error`, значения первых трёх — по позиции, номер четвёртого столбца — в + `message` #### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке - **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом @@ -263,12 +305,25 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`, `column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`). +Подтверждение SHALL опираться только на уверенные источники, чтобы неуверенное не подтверждало само себя: для +неуверенной подписи эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с +уверенной подписью (`label_confidence = 100`); такой нет — подпись не подтверждена. Неуверенная дата отчёта (шаг 8, поля +шапки) подтверждается только самой поздней абсолютной датой колонок с уверенной подписью. Для периода документа и +относительных подписей эффективная дата считается, как выше, по всем источникам. + #### Scenario: Бланк 66н без впечатанных дат - **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», дата отчёта шапки 30.09.2025 - **THEN** `period_date` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`, `relative:prev_year_end`, `relative:prev_prev_year_end`; предупреждений о дате колонок нет +#### Scenario: Неуверенная подпись не подтверждает сама себя +- **WHEN** даты отчёта в шапке нет, единственная колонка с абсолютной датой — подпись OCR «На 30.09.2О25» с уверенностью + 60, остальные колонки без подписи +- **THEN** эффективная дата для подтверждения строится без этой подписи — уверенных источников нет, подпись не + подтверждена: `COLUMN_DATE_UNCERTAIN` уровня `error`; период документа (`period_known`) по-прежнему определяется этой + датой + #### Scenario: Мусорная подпись над числами - **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы - **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня @@ -306,16 +361,20 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat ### Requirement: Шаг 3 — строки данных и их статусы Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или -непуста с уверенностью ниже 100. Строка только с текстом наименования — не строка данных. У строки — `row_index` (порядковый номер в документе с 0, +непуста с уверенностью ниже 100, или её текст — «цифра под видом буквы»: после замены двойников без условия «вплотную к +цифре» в нём есть цифра и нет других букв (`'I'`, `'| О'`). Строка только с текстом наименования — не строка данных; +текст из настоящих букв в колонке значений при уверенности 100 (подписи «Руководитель … Косарев», «(расшифровка +подписи)» под формой Excel) содержимым не считается — осознанное исключение из «ничего не выбрасывать молча». У строки — `row_index` (порядковый номер в документе с 0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:`, разлепленные `row:#` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!` по правилу шага 1, без столбца кодов — первой непустой ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`. Статусы: - `ok` — код разобран; -- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой; -- `detail` — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа); - `parent_ref` — `ref` ближайшей строки с кодом выше; -- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки роли `title` +- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр (в том числе только из + букв-двойников вне соседства с цифрой: «изо :», «}», «:») считается пустой; +- `detail` — строка без кода с содержимым, над которой (в том же или в предыдущих сегментах документа) есть строка `ok`; + `parent_ref` — `ref` ближайшей строки `ok` выше (строки `unreadable` / `missing` между ними пропускаются); +- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok`; строки роли `title` сюда не попадают. Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не @@ -334,6 +393,20 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat - **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой - **THEN** строка `detail`, `parent_ref` = `ref` строки 1150 +#### Scenario: Расшифровка под строкой-мусором +- **WHEN** порядок строк: 1150 (`ok`), «ИЧ,250» (`unreadable`), строка без кода с суммой +- **THEN** последняя — `detail` с `parent_ref` строки 1150 (ближайшая строка `ok`, `unreadable` пропускается) + +#### Scenario: Код из букв-двойников без цифр (aep_3) +- **WHEN** ячейка кода — `"изо :"`, в колонке значений `'3'` с уверенностью 0, выше есть строки `ok` +- **THEN** цифр в ячейке кода нет — она пустая, строка `detail`; если это потерянная строка 1130, её сумма не войдёт в + 1100, и соотношение `1100 = …` подсветит раздел (цена правила, S-61) + +#### Scenario: Цифра под видом буквы (aep_6) +- **WHEN** в строке без кода под строкой `ok` единственная непустая ячейка значений — `'I'` с уверенностью 100 +- **THEN** строка данных `detail` (раньше терялась молча); значение `amount = null`, `string_value = "I"`, + `is_recognized = false` + #### Scenario: Потерянный код в начале таблицы - **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет - **THEN** строка `missing` @@ -365,14 +438,25 @@ k-я из непустых частей, если их тоже N, иначе ц - `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` = текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`); - `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`; -- `value` нет → `parse_amount(text, source)`: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, - скобки → минус, `}` и `]` — закрывающая скобка; прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`, - `is_recognized = true`; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка — +- `value` нет → `parse_amount(text, source)` по стадиям: + 1. пусто или прочерк (`-`, `–`, `—`, `−` без цифр) → `amount = null`, `is_recognized = true`; многострочное значение + в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`; + 2. крайние `|` (след границы таблицы) удаляются до замены двойников, а не становятся единицей (`"|122477"` → 122477); + затем буквы-двойники вплотную к цифре → цифры (Р-11); и то и другое — исправление (`is_recognized = false`); + 3. **ядро числа** — участок от первого из символов «цифра, `(`, `-`, `−`, `+`» до последнего из «цифра, `)`, `}`, + `]`»; всё вне ядра — **краевой мусор** (`|`, `:`, `=`, `■`, `•`, кавычки, точки, `{`, `[` и любые другие символы): + выбрасывается, а если среди выброшенного есть что-то кроме пробелов — `is_recognized = false`; + 4. в ядре допустимы только цифры, пробелы (в том числе NBSP и тонкие), `,`, `.`, знак (`-`, `−` как минус, `+`) в + начале и одна пара скобок вокруг всего числа: `(` первым символом и `)`, `}` или `]` последним (`}` / `]` — + исправление); `{` и `[` открывающей скобкой не считаются. Иначе — буква в ядре (после стадии 2), скобка без пары + или не на краю ядра, знак не в начале, любой другой символ между цифрами (`:`, `/`, `=`, `|`) — `amount = null`, + `string_value` = текст, `is_recognized = false`; + 5. цифр нет вовсе → `amount = null`, `string_value` = текст, `is_recognized = false`; + 6. скобки → минус; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка — тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`, `string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе - `null`) и `is_recognized = false`; многострочное значение в строке с однострочным кодом → `amount = null`, - `string_value` = текст, `is_recognized = false`; + `null`) и `is_recognized = false`; - `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или `]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит; - позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` @@ -389,6 +473,24 @@ k-я из непустых частей, если их тоже N, иначе ц - **WHEN** ячейка OCR — `"(7744536}"` - **THEN** `amount = -7744536`, `is_recognized = false` +#### Scenario: Скобка внутри цифр (aep_3) +- **WHEN** ячейка OCR — `"211(1 |"` +- **THEN** крайняя `|` — краевой мусор; в ядре `211(1` скобка без пары и не на краю → `amount = null`, `string_value = + "211(1 |"`, `is_recognized = false` + +#### Scenario: Буква в ядре числа (aep_6) +- **WHEN** ячейка OCR — `"(Ю4)"` +- **THEN** «Ю» — не двойник и стоит в ядре → `amount = null`, `string_value = "(Ю4)"`, `is_recognized = false` (не −4) + +#### Scenario: Краевой мусор с двух сторон +- **WHEN** ячейка OCR — `": : 2856983 = ■"` или `"| : 122477"` +- **THEN** `amount = 2856983` и `amount = 122477`, `is_recognized = false` (выброшен мусор вне ядра) + +#### Scenario: Фигурная скобка не открывает +- **WHEN** ячейка OCR — `"{38378)"` +- **THEN** `{` — краевой мусор, в ядре `38378)` закрывающая скобка без пары → `amount = null`, `string_value = "{38378)"`, + `is_recognized = false` + #### Scenario: Запятая по источнику - **WHEN** текст `"4,200"` - **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20` @@ -428,11 +530,14 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code` при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND` уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`, -`locator` таблицы), у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); -документа в обоих случаях нет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7 +`locator` таблицы), таблица ИС без содержимого — предупреждение **файла** `TABLE_SKIPPED` (`info`, `locator` таблицы); +у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех +этих случаях нет. Содержимое таблицы ИС — хотя бы одна ячейка с цифрой или с «цифрой под видом буквы» (шаг 3); таблица +только из текста (подписи, пояснительный текст) содержимого не имеет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7 не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`. -`form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; +`form_variant` SHALL определяться только по структуре кодов всех строк `ok` документа (независимо от уверенности +кода; у ядра — заново при каждом пересчёте после правки, точка входа ниже): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле, `confidence` → `<поле>_confidence`. @@ -500,16 +605,21 @@ actual}`, где `codes` — коды цели и членов соотноше - **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет ### Requirement: Шаг 8 — `needs_review` значения -Сервис SHALL вычислять `needs_review` у каждого значения: +Сервис SHALL вычислять `needs_review` у каждого значения строк `ok`, `unreadable` и `missing`. У значений строк +`detail` `needs_review = false` всегда (Р-23): у них нет кода, покрыть их соотношением нельзя, в проверки §7, агрегаты и +статус документа они не входят; сомнение остаётся видно в подсказке ячейки по `text` и `confidence`. Для остальных +значений: - значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не нуждаются; - значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без «округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких соотношений нет); -- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус, - без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`, - `is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён; +- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака совпадает с формой + строгого большинства остальных членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без + правки аналитика (сама ячейка в голосование не входит); форма знака — по результату разбора `parse_amount`: «скобки» + (пара скобок, в том числе с `}` / `]`), «минус», «без знака»; голосующих нет или равенство голосов — знак не + подтверждён; - `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности; - иначе `needs_review = false`. @@ -524,9 +634,22 @@ actual}`, где `codes` — коды цели и членов соотноше - **THEN** у 2120 знак подтверждён, `needs_review = false` #### Scenario: Знак вне конвенции -- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"{38378)"` без минуса после разбора +- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"38378"` с уверенностью 60 (форма «без знака»), и `2100 = 2110 − + 2120` сходится по модулю - **THEN** `needs_review = true` у 2120 независимо от соотношения +#### Scenario: Скобка, которая не открывает +- **WHEN** 2120 — `"{38378)"` +- **THEN** разбор даёт `amount = null` (шаг 4), значение не покрыто, `needs_review = true` + +#### Scenario: Расшифровки не держат документ (Р-23, РБ 1 кв. 2025) +- **WHEN** в ББ и ОФР РБ «Бухгалтерская отчетность 1 кв.25» все неуверенные значения строк с кодом покрыты сошедшимися + соотношениями, неуверенные коды подтверждены перечнем `fsbu4`, а неуверенные значения остались только у строк `detail` + (в ОФР — `'1 425 130'` с уверенностью 78, `'134 796'` с уверенностью 71, `'(15 567)'` с уверенностью 75) +- **THEN** у значений `detail` `needs_review = false`, они статус не держат; документ получает `done`, если выполнены + остальные условия функции статуса (шапка, период, предупреждения); подсказка у ячеек `detail` показывает `text` и + уверенность + #### Scenario: Справочная строка без проверки - **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении) - **THEN** `needs_review = true` @@ -545,11 +668,15 @@ actual}`, где `codes` — коды цели и членов соотноше неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`, `fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО). -Четырёхзначные коды строк `ok` документа образуют последовательность; для **обеих** редакций берётся наибольшая -подпоследовательность, согласованная с порядком перечня варианта формы (итоги после слагаемых; код с повтором может -войти столько раз, сколько он стоит в перечне); редакция — с более длинной подпоследовательностью, при равенстве — по -эффективной дате (`fsbu4` после 2024-12-31); уверенные коды участвуют как подтверждённые якоря; при нескольких -наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён, если +Четырёхзначные коды строк `ok` документа образуют последовательность; коды, которых нет в перечне редакции, в +последовательность этой редакции не входят. Для **обеих** редакций берётся лучшая подпоследовательность, согласованная +с порядком перечня варианта формы (итоги после слагаемых; код с повтором может войти столько раз, сколько он стоит в +перечне); «лучшая» — лексикографически: сначала наибольшее число уверенных кодов (уверенные коды — якоря: включаются +все, какие можно включить вместе), затем наибольшая длина. Якоря, противоречащие друг другу по порядку бланка, все +вместе войти не могут — входит наибольшее согласованное их множество; неуверенный код проверяется против выбранного. +Редакция — с лучшей подпоследовательностью (то же сравнение), при равенстве — по эффективной дате (`fsbu4` после +2024-12-31). Неуверенный код подтверждён, только если он входит во **все** лучшие подпоследовательности выбранной +редакции. Вариант формы для перечня — `form_variant` документа (шаг 6, по всем строкам `ok`). Пятизначный код подтверждён, если подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review = @@ -563,6 +690,11 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по - **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240 - **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check` +#### Scenario: Якоря противоречат друг другу +- **WHEN** порядок кодов 1210 (уверенный), 1240 (уверенный), 1230 (уверенный), 1250 (уверенность 50) +- **THEN** 1240 и 1230 вместе войти не могут; лучшие подпоследовательности — 1210, 1240, 1250 и 1210, 1230, 1250; код 1250 + входит в обе и подтверждён + #### Scenario: Законный повтор 1220 (Р-20, AURORA-1431) - **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность 100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230 @@ -581,7 +713,9 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по - **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра ### Requirement: Шаг 8 — поля шапки -Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе +Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок с +уверенной подписью (`label_confidence = 100`; неуверенная подпись дату отчёта не подтверждает, требование «Шаг 2 — +подпись и дата колонки»); иначе поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field = report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH @@ -590,9 +724,15 @@ report_date`). У бланка только с относительными по (подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют. #### Scenario: Неуверенная, но верная дата -- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025 +- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок с уверенной подписью 30.09.2025 - **THEN** `needs_review_fields` пуст, предупреждения о дате нет +#### Scenario: Неуверенные дата и подпись не подтверждают друг друга +- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, единственная абсолютная подпись колонки «На 30.09.2025» — с + уверенностью 60 +- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` и `COLUMN_DATE_UNCERTAIN` уровня `error`; + снимаются подтверждением даты отчёта и даты колонки аналитиком + #### Scenario: Неуверенный год - **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024 - **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check` @@ -616,12 +756,13 @@ report_date`). У бланка только с относительными по |---|---|---| | `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе | | `TABLE_PARSE_ERROR` | error (файл) | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`); документа нет | -| `TABLE_LOST` | error (файл) | таблица ИС с содержимым без колонки кодов и без `type_hint` | +| `TABLE_LOST` | error (файл) | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без колонки кодов и без `type_hint` | +| `TABLE_SKIPPED` | info (файл) | таблица ИС без содержимого (только текст), без колонки кодов и без `type_hint`; документа нет | | `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов | | `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) | | `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | | `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | -| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым | +| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента при сопоставлении по порядку не совпали по числу с первым | | `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи | | `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты | | `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента | @@ -667,9 +808,10 @@ report_date`). У бланка только с относительными по проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`, `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника, `row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения -`{needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни предупреждений документа (у ядра — -записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки и подтверждения аналитика -(правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не учитываются. Статус ОСВ — +`{line_status строки, needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни +предупреждений документа (у ядра — записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки +и подтверждения аналитика (правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не +учитываются. Значения строк `detail` функция MUST NOT учитывать, даже если на входе у них `needs_review = true` (Р-23). Статус ОСВ — `done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований @@ -691,3 +833,44 @@ report_date`). У бланка только с относительными по #### Scenario: Соседний документ не тянет файл - **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет - **THEN** конверт `needs_check`, у ББ `document.status = done` + +#### Scenario: Сомнительная ячейка расшифровки +- **WHEN** у формы все строки `ok` или `detail`, нарушений и `error` нет, единственное значение с уверенностью ниже 100 — + в строке `detail` +- **THEN** документ `done` + +### Requirement: Точка входа пакета для пересчёта по сохранённым данным +Пакет SHALL предоставлять функцию пересчёта формы по сохранённым данным без сетки и `FormCandidate` — ею ядро +пересчитывает форму после PATCH и при приёме результата `1.x`, сервисы ею не пользуются. Вход — тип пакета +`StoredForm`, собираемый хостом из своих данных: +- `document_type`; `effective_date` (дата отчёта, иначе самая поздняя абсолютная дата колонок — у ядра + `document.reporting_date`); `has_organization_or_inn`; `period_known`; `needs_review_fields` (как хранит ядро: поле + уходит из него только правкой или подтверждением даты отчёта — сам пересчёт поля шапки не переоценивает); `accepted`; + `warnings` — предупреждения документа `{code, severity}` (у ядра — записи `document_issue` вида `warning`); +- `columns[] {key, column_index, period_date, period_kind}` (`key` — идентификатор хоста, у ядра `column_id`); +- `lines[] {key, row_index | null, line_status, line_code, line_code_confidence, line_name, code_fixed, parent_key, + values[] {column_key, amount, text, confidence, is_recognized, value_fixed}}`: `row_index = null` — строка, созданная + правкой (участвует в §7, как строка `ok`, пока у неё есть сумма); `code_fixed` — код назначен, сменён или подтверждён + аналитиком (`line_code_modified` или `is_confirmed` строки); `value_fixed` — значение правлено или подтверждено + (`is_modified` или `is_confirmed`); `text` — сырой текст источника (`source_text`), по нему считается форма знака; + у значения, созданного правкой, `text = null`. + +Выход — `{form_variant, edition, checks {relations_failed, soft_relations_failed}, line_needs_review {key → bool}, +value_needs_review {key → bool}, detail_parents {key → parent_key | null}, status}`. Правила — те же шаги 6–8, что у +сервисов, с поправками: `form_variant` определяется заново по кодам строк `ok` (код мог смениться правкой); `edition` — +редакция с лучшей подпоследовательностью кодов документа по всем строкам `ok` (как в структурной проверке, при +равенстве — по `effective_date`; кодов нет — по `effective_date`); `value_fixed` ⇒ `needs_review = false`, такое значение +в голосование знака не входит; `code_fixed` ⇒ `needs_review` строки = `false`; значения строк `detail` — `false`; +`accepted` ⇒ жёсткие нарушения статус не держат; `detail_parents` — ближайшая строка `ok` выше по `row_index` (строки +`row_index = null` родителями не бывают), нет такой — `null`. Пересчёт не меняет `line_status`, суммы и коды — только +выводит признаки. + +#### Scenario: Пересчёт после правки по строкам БД +- **WHEN** ядро передаёт форму: ячейка 1230 правлена (`value_fixed`), код 1250 назначен строке бывшей `unreadable` + (`code_fixed`), шаблонная строка 1260 с суммой (`row_index = null`), строка `detail` с уверенностью 60 +- **THEN** `checks` посчитаны с 1250 и 1260 в `1200 = …`; у 1230 и у значения `detail` `needs_review = false`, у строки + 1250 `needs_review = false`; `status` — по общей функции статуса; `form_variant` и `edition` — по кодам после правки + +#### Scenario: Смена кода меняет вариант +- **WHEN** у упрощённого по структуре ББ (есть 1600, нет 1100 и 1200) аналитик назначает строке код 1100 +- **THEN** точка входа возвращает `form_variant = full`, нарушения считаются полным набором