diff --git a/context/03-architecture.md b/context/03-architecture.md index 86d6f8c..fed41dc 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -31,8 +31,9 @@ лимиты, чтение, дедлайн, вид каждого листа): листы ОСВ идут в конвейер `osv.py`, листы форм — в конвейер форм `statements/` (блоки форм, колонки, шапка, строки, соотношения §7, склейка продолжений); документы результата — сначала ОСВ, затем формы. Контракт результата — - `docs/contracts/recognition-result.schema.json` (контракт v1.2: `DISCLOSURE` в `document_type`, `statementDocument.checks`, - границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания + `docs/contracts/recognition-result.schema.json` (контракт v2.0 — статусы строк, `needs_review`, `document.status` и + структурные предупреждения, change `unified-statement-normalization`; до перехода сервис пишет v1.2: `DISCLOSURE` в + `document_type`, `statementDocument.checks`, границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания заголовков/группировок — `rules/*.yaml`. - **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/.` (расширение — суффикс `original_name` в нижнем регистре; файл пишется сразу в `Input` с перезаписью существующего, без временного @@ -163,6 +164,18 @@ advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскак - **Нагрузка и транзакции.** Переход версии после загрузки выполняется в отдельной короткой транзакции (`session_transaction`), сбой не отменяет принятые файлы (R-10). Чтение (`GET …/osv`, сводка, экспорт) блокировок не берёт. +### Общий код нормализации ББ/ОФР — `arm-statements` (change `unified-statement-normalization`, целевое) + +Шаги 2–8 нормализации форм (раскладка, статусы строк, значения, код и наименование, сборка документа, проверки §7, +`needs_review`, статус и предупреждения) и их словари живут в отдельном репозитории `arm-statements` пакетом +`arm_statements/` (Р-1: платформа не собирает Python-пакеты). В tabular-parser, scan-gateway и arm-core пакет лежит +**копией** с тестами, импортируется по имени `arm_statements`; копию раскладывает и проверяет `sync-statements` в docs +(`--check`, код 20 при прямой правке; вызывается из `make check-full`, не из `make check`). Сервисы держат только адаптер +шага 1 (json ИС / лист Excel → `FormCandidate`); ядро считает тем же кодом после правок аналитика и не держит своих +копий словаря §7. Словарь «код → наименование» — только в ядре. Правила — `05-domain-rules.md` §11; маркер версии +копии — `service.statements_version` результата → `recognition_job.statements_version`. Порядок выкладки — «сначала +ядро» (ядро принимает 1.x и 2.0). Пункты ниже описывают поведение до перехода (Срез 3b). + ### Формы ББ/ОФР, проверки §7 и API экрана проверки (Срез 3b) Формы (`statementDocument` контракта v1.2) приходят от обоих сервисов распознавания в одном формате; ядро — единственное diff --git a/contracts/examples/result-error.json b/contracts/examples/result-error.json index 54af7cb..b043599 100644 --- a/contracts/examples/result-error.json +++ b/contracts/examples/result-error.json @@ -1,8 +1,11 @@ { - "schema_version": "1.0", + "schema_version": "2.0", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", - "service": { "name": "tabular-parser", "version": "0.1.0" }, + "service": { + "name": "tabular-parser", + "version": "0.1.0" + }, "status": "error", "error": { "code": "NOT_IMPLEMENTED", diff --git a/contracts/examples/result-message-done.json b/contracts/examples/result-message-done.json index 6d56f17..4e47bd1 100644 --- a/contracts/examples/result-message-done.json +++ b/contracts/examples/result-message-done.json @@ -1,14 +1,17 @@ { - "schema_version": "1.0", + "schema_version": "2.0", "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", "status": "done", - "service": { "name": "scan-gateway", "version": "0.1.0" }, + "service": { + "name": "scan-gateway", + "version": "0.1.0" + }, "result_s3": { "bucket": "arm-ka", "key": "results/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/result.json" }, - "result_schema_version": "1.0", + "result_schema_version": "2.0", "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", "metrics": { "duration_ms": 18400, diff --git a/contracts/examples/result-message-error.json b/contracts/examples/result-message-error.json index f5ec77a..6da980c 100644 --- a/contracts/examples/result-message-error.json +++ b/contracts/examples/result-message-error.json @@ -1,9 +1,12 @@ { - "schema_version": "1.0", + "schema_version": "2.0", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", "status": "error", - "service": { "name": "tabular-parser", "version": "0.1.0" }, + "service": { + "name": "tabular-parser", + "version": "0.1.0" + }, "error": { "code": "NOT_IMPLEMENTED", "message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet" diff --git a/contracts/examples/result-message-processing.json b/contracts/examples/result-message-processing.json index 2087150..9136790 100644 --- a/contracts/examples/result-message-processing.json +++ b/contracts/examples/result-message-processing.json @@ -1,7 +1,10 @@ { - "schema_version": "1.0", + "schema_version": "2.0", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", "status": "processing", - "service": { "name": "tabular-parser", "version": "0.1.0" } + "service": { + "name": "tabular-parser", + "version": "0.1.0" + } } diff --git a/contracts/examples/result-osv-minimal.json b/contracts/examples/result-osv-minimal.json index b0a9e97..f54525f 100644 --- a/contracts/examples/result-osv-minimal.json +++ b/contracts/examples/result-osv-minimal.json @@ -1,8 +1,11 @@ { - "schema_version": "1.2", + "schema_version": "2.0", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", - "service": { "name": "tabular-parser", "version": "0.1.0" }, + "service": { + "name": "tabular-parser", + "version": "0.1.0" + }, "status": "done", "documents": [ { @@ -24,7 +27,11 @@ "header_variant": 1 }, "group_schema": [ - { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } + { + "type": "COUNTERPARTY", + "name": "Контрагенты", + "has_inn": true + } ], "columns": { "opening_debit": "C", @@ -70,14 +77,18 @@ "turnover_credit": "200.50", "closing_debit": "0.00", "closing_credit": "700.50", - "extra": { "КПП": "352801001" } + "extra": { + "КПП": "352801001" + } } ], "checks": { "total_row_matches_root": true, "balance_identity_violations": [], "parent_sum_violations": [] - } + }, + "status": "done", + "warnings": [] } ] } diff --git a/contracts/examples/result-osv-needs-check.json b/contracts/examples/result-osv-needs-check.json index 81d64a8..6827c89 100644 --- a/contracts/examples/result-osv-needs-check.json +++ b/contracts/examples/result-osv-needs-check.json @@ -1,15 +1,15 @@ { - "schema_version": "1.1", + "schema_version": "2.0", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", - "service": { "name": "tabular-parser", "version": "1.0.0" }, + "service": { + "name": "tabular-parser", + "version": "1.0.0" + }, "status": "needs_check", "metrics": { "rows": 3, - "warnings": [ - "sheet=Лист_1 hierarchy=outline", - "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3" - ] + "warnings": [] }, "documents": [ { @@ -28,8 +28,15 @@ "header_variant": 1 }, "group_schema": [ - { "type": "ACCOUNT", "name": "Счет" }, - { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } + { + "type": "ACCOUNT", + "name": "Счет" + }, + { + "type": "COUNTERPARTY", + "name": "Контрагенты", + "has_inn": true + } ], "columns": { "opening_debit": "D", @@ -76,7 +83,9 @@ "turnover_credit": "200.50", "closing_debit": "0.00", "closing_credit": "700.50", - "extra": { "КПП": "352801001" } + "extra": { + "КПП": "352801001" + } }, { "row_number": 3, @@ -117,9 +126,24 @@ ], "checks": { "total_row_matches_root": true, - "balance_identity_violations": [3], + "balance_identity_violations": [ + 3 + ], "parent_sum_violations": [] - } + }, + "status": "needs_check", + "warnings": [ + { + "code": "OSV_HIERARCHY_BY_OUTLINE", + "severity": "info", + "message": "иерархия строк — по группировке листа (outline)" + }, + { + "code": "OSV_BALANCE_IDENTITY", + "severity": "info", + "message": "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3" + } + ] } ] } diff --git a/contracts/examples/result-statement-excel.json b/contracts/examples/result-statement-excel.json index 260528b..0f08d5f 100644 --- a/contracts/examples/result-statement-excel.json +++ b/contracts/examples/result-statement-excel.json @@ -1,44 +1,98 @@ { - "schema_version": "1.2", + "schema_version": "2.0", "job_id": "3f1c7a2e-5b64-4d0a-9c1e-8a7b6d5e4f30", "file_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", - "service": { "name": "tabular-parser", "version": "0.3.0" }, - "status": "done", - "metrics": { "duration_ms": 412, "rows": 7, "warnings": ["sheet=Бухгалтерский баланс. Лист 1 значений с дробной частью при единице тыс. руб.: 3"] }, + "service": { "name": "tabular-parser", "version": "0.4.0", "statements_version": "abc1234" }, + "status": "needs_check", + "metrics": { "duration_ms": 412, "rows": 12, "warnings": [] }, "documents": [ { "document_type": "BALANCE", "locator": "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2", - "header": { "form_name": "Бухгалтерский баланс", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "inn": "7700000000", "inn_confidence": 100, "report_date_text": "на 30 сентября 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710001", "okpo": "12345678" }, + "status": "done", + "form_variant": "full", + "warnings": [ + { "code": "FRACTIONAL_IN_THOUSANDS", "severity": "info", "message": "значений с дробной частью при единице тыс. руб.: 6 из 14" } + ], + "header": { + "form_name": "Бухгалтерский баланс", + "organization_name": "ООО \"Пример\"", "organization_name_text": "ООО \"Пример\"", "organization_name_confidence": 100, + "inn": "7700000000", "inn_text": "7700000000", "inn_confidence": 100, + "report_date": "2025-09-30", "report_date_text": "на 30 сентября 2025 г.", "report_date_confidence": 100, + "units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100, + "okud": "0710001", "okud_text": "0710001", "okud_confidence": 100, + "okpo": "12345678", "okpo_text": "12345678", "okpo_confidence": 100, + "needs_review_fields": [] + }, "columns": [ - { "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" }, - { "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" } + { "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }, + { "column_index": 1, "label": "На 31 декабря 2024 г.", "label_confidence": 100, "period_date": "2024-12-31", "period_kind": "absolute" } ], "lines": [ - { "line_code": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] }, - { "line_code": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "900000.00", "confidence": 100, "is_recognized": true } ] }, - { "line_code": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] }, - { "line_code": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": null, "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": null, "confidence": 100, "is_recognized": true } ] }, - { "line_code": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] } + { "row_index": 0, "ref": "Бухгалтерский баланс. Лист 1!C14", "line_status": "ok", "line_code": "1110", "line_code_text": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 1, "ref": "Бухгалтерский баланс. Лист 1!C15", "line_status": "ok", "line_code": "11101", "line_code_text": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "900000.00", "text": "900 000,00", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 2, "ref": "Бухгалтерский баланс. Лист 1!C16", "line_status": "ok", "line_code": "1100", "line_code_text": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 3, "ref": "Бухгалтерский баланс. Лист 1!C17", "line_status": "ok", "line_code": "1200", "line_code_text": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": null, "text": "-", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": null, "text": "", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 4, "ref": "Бухгалтерский баланс. Лист 1!C18", "line_status": "ok", "line_code": "1600", "line_code_text": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 5, "ref": "Бухгалтерский баланс. Лист 2!C5", "line_status": "ok", "line_code": "1370", "line_code_text": "1370", "line_code_confidence": 100, "line_name": "Нераспределенная прибыль (непокрытый убыток)", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 6, "ref": "Бухгалтерский баланс. Лист 2!C6", "line_status": "ok", "line_code": "1300", "line_code_text": "1300", "line_code_confidence": 100, "line_name": "Итого по разделу III", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 7, "ref": "Бухгалтерский баланс. Лист 2!C7", "line_status": "ok", "line_code": "1700", "line_code_text": "1700", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] } ], - "checks": { "relations_failed": [], "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "expected": "905786.99", "actual": "900000.00" } ], "duplicate_line_codes": [] } + "checks": { + "relations_failed": [], + "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ] + } }, { "document_type": "INCOME", "locator": "sheet:Отчет о финансовых результатах", - "header": { "form_name": "Отчет о финансовых результатах", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "report_date_text": "за январь - сентябрь 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710002" }, - "columns": [ { "column_index": 0, "label": "За январь - сентябрь 2025 г.", "period_date": "2025-09-30" } ], - "lines": [ - { "line_code": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "1000.00", "confidence": 100, "is_recognized": true } ] }, - { "line_code": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "-600.00", "confidence": 100, "is_recognized": true } ] }, - { "line_code": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "400.00", "confidence": 100, "is_recognized": true } ] } + "status": "needs_check", + "form_variant": "full", + "warnings": [ + { "code": "INN_NOT_FOUND", "severity": "info", "message": "в шапке не найден ИНН", "field": "inn" } ], - "checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] } + "header": { + "form_name": "Отчет о финансовых результатах", + "organization_name": "ООО \"Пример\"", "organization_name_text": "ООО \"Пример\"", "organization_name_confidence": 100, + "inn": null, "inn_text": null, "inn_confidence": null, + "report_date": "2025-09-30", "report_date_text": "за январь - сентябрь 2025 г.", "report_date_confidence": 100, + "units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100, + "okud": "0710002", "okud_text": "0710002", "okud_confidence": 100, + "needs_review_fields": [] + }, + "columns": [ + { "column_index": 0, "label": "За январь - сентябрь 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" } + ], + "lines": [ + { "row_index": 0, "ref": "Отчет о финансовых результатах!C12", "line_status": "ok", "line_code": "2110", "line_code_text": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "1000.00", "text": "1 000", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 1, "ref": "Отчет о финансовых результатах!C13", "line_status": "detail", "line_code": null, "line_code_text": "", "line_code_confidence": 100, "line_name": "в том числе от продажи продукции", "line_name_confidence": 100, "parent_ref": "Отчет о финансовых результатах!C12", "needs_review": false, + "values": [ { "column_index": 0, "amount": "800.00", "text": "800", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 2, "ref": "Отчет о финансовых результатах!C14", "line_status": "ok", "line_code": "2120", "line_code_text": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "-600.00", "text": "(600)", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 3, "ref": "Отчет о финансовых результатах!C15", "line_status": "ok", "line_code": "2100", "line_code_text": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "400.00", "text": "400", "confidence": 100, "is_recognized": true, "needs_review": true } ] } + ], + "checks": { + "relations_failed": [ { "column_index": 0, "relation": "2200 = 2100 - 2210 - 2220", "codes": ["2200", "2100", "2210", "2220"], "expected": null, "actual": "400.00" } ], + "soft_relations_failed": [] + } }, { "document_type": "DISCLOSURE", "locator": "sheet:Пояснения", - "header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах" }, + "status": "done", + "form_variant": null, + "warnings": [], + "header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах", "needs_review_fields": [] }, "columns": [], "lines": [] } diff --git a/contracts/examples/result-statement-minimal.json b/contracts/examples/result-statement-minimal.json index 913cfc5..1c9f6e2 100644 --- a/contracts/examples/result-statement-minimal.json +++ b/contracts/examples/result-statement-minimal.json @@ -1,50 +1,51 @@ { - "schema_version": "1.2", + "schema_version": "2.0", "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", - "service": { "name": "scan-gateway", "version": "0.1.0" }, - "status": "done", + "service": { "name": "scan-gateway", "version": "0.2.0", "statements_version": "abc1234" }, + "status": "needs_check", "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", + "metrics": { "duration_ms": 18400, "pages": 2, "warnings": [] }, "documents": [ { "document_type": "BALANCE", - "locator": "pages:1-2", + "locator": "file:8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json;table:0", + "status": "needs_check", + "form_variant": "full", + "warnings": [ + { "code": "ORGANIZATION_UNCERTAIN", "severity": "info", "message": "организация распознана неуверенно", "field": "organization_name" }, + { "code": "ROW_SPLIT", "severity": "info", "message": "строка с несколькими кодами разлеплена: 1370, 1300", "line_refs": ["row:14#1", "row:14#2"] } + ], "header": { "form_name": "Бухгалтерский баланс", - "organization_name": "ООО Ромашка", - "organization_name_confidence": 97.5, - "inn": "7700000000", - "inn_confidence": 99, - "report_date_text": "30 сентября 2025 г.", - "report_date": "2025-09-30", - "units": "тыс. руб." + "organization_name": "ООО Ромашка", "organization_name_text": "ООО Ромашка", "organization_name_confidence": 97.5, + "inn": "7700000000", "inn_text": "7700000000", "inn_confidence": 100, + "report_date": "2025-09-30", "report_date_text": "30 сентября 2025 г.", "report_date_confidence": 100, + "units": "тыс. руб.", "units_text": "тыс. руб.", "units_confidence": 100, + "needs_review_fields": [] }, "columns": [ - { "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" }, - { "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" } + { "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" } ], "lines": [ - { - "line_code": "1100", - "line_code_confidence": 99, - "line_name": "Итого по разделу I", - "line_name_confidence": 96, - "values": [ - { "column_index": 0, "amount": "1234.50", "confidence": 95, "is_recognized": true }, - { "column_index": 1, "amount": "1100", "confidence": 93, "is_recognized": true } - ] - }, - { - "line_code": "1600", - "line_code_confidence": 99, - "line_name": "БАЛАНС (актив)", - "line_name_confidence": 98, - "values": [ - { "column_index": 0, "amount": "5678.00", "confidence": 94.5, "is_recognized": true }, - { "column_index": 1, "amount": "-12.30", "confidence": 90, "is_recognized": true } - ] - } - ] + { "row_index": 0, "ref": "row:1", "line_status": "ok", "line_code": "1210", "line_code_text": "1210", "line_code_confidence": 100, "line_name": "Запасы", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "100", "text": "100", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 1, "ref": "row:2", "line_status": "ok", "line_code": "1230", "line_code_text": "1230", "line_code_confidence": 100, "line_name": "Дебиторская задолжен-ность", "line_name_confidence": 80, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428311", "string_value": null, "text": "42831 1", "confidence": 100, "is_recognized": false, "needs_review": false } ] }, + { "row_index": 2, "ref": "row:3", "line_status": "unreadable", "line_code": null, "line_code_text": "ИЧ,250", "line_code_confidence": 25, "line_name": "Финансовые вложения", "line_name_confidence": 67, "needs_review": false, + "values": [ { "column_index": 0, "amount": "50", "text": "50", "confidence": 67, "is_recognized": true, "needs_review": true } ] }, + { "row_index": 3, "ref": "row:4", "line_status": "ok", "line_code": "1200", "line_code_text": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 4, "ref": "row:5", "line_status": "ok", "line_code": "1600", "line_code_text": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] }, + { "row_index": 5, "ref": "row:14#1", "line_status": "ok", "line_code": "1370", "line_code_text": "1370", "line_code_confidence": 100, "line_name": "Нераспределенная прибыль (непокрытый убыток)", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] }, + { "row_index": 6, "ref": "row:14#2", "line_status": "ok", "line_code": "1300", "line_code_text": "1300", "line_code_confidence": 100, "line_name": "Итого по разделу III", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] }, + { "row_index": 7, "ref": "row:15", "line_status": "ok", "line_code": "1700", "line_code_text": "1700", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false, + "values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] } + ], + "checks": { "relations_failed": [], "soft_relations_failed": [] } } ] } diff --git a/contracts/recognition-result-message.schema.json b/contracts/recognition-result-message.schema.json index 4d9f401..692b812 100644 --- a/contracts/recognition-result-message.schema.json +++ b/contracts/recognition-result-message.schema.json @@ -1,13 +1,13 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "arm-ka/recognition-result-message/v1", - "title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v1.0", + "$id": "arm-ka/recognition-result-message/v2", + "title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v2.0", "description": "Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed). Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:: — для status = processing; result:: — для done, needs_check и error. Форма запроса : описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).", - "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.", + "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения; новый MAJOR — «сначала ядро»: ядро на время перехода принимает конверты (включая ack processing) и прежнего, и нового MAJOR. Changelog: 1.0 — исходный контракт; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — metrics.warnings[] стали структурами {code, severity, message, line_refs?, column_index?, field?, locator?} (как metrics.warnings файла результата), производитель пишет в конверт тот же MAJOR, что и в файл результата.", "type": "object", "required": ["schema_version", "job_id", "file_id", "status", "service"], "properties": { - "schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" }, + "schema_version": { "type": "string", "pattern": "^2\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v2.0 пишет \"2.0\"" }, "job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" }, "file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" }, "status": { "enum": ["processing", "done", "needs_check", "error"] }, @@ -47,7 +47,22 @@ "fields_total": { "type": "integer" }, "fields_low_confidence": { "type": "integer" }, "fields_unrecognized": { "type": "integer" }, - "warnings": { "type": "array", "items": { "type": "string" } } + "warnings": { "type": "array", "items": { "$ref": "#/$defs/fileWarning" }, "description": "предупреждения уровня файла — те же, что metrics.warnings файла результата" } + } + } + }, + "$defs": { + "fileWarning": { + "type": "object", + "required": ["code", "severity", "message"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" }, + "severity": { "enum": ["error", "info"] }, + "message": { "type": "string" }, + "line_refs": { "type": "array", "items": { "type": "string" } }, + "column_index": { "type": "integer" }, + "field": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "address"] }, + "locator": { "type": "string" } } } }, diff --git a/contracts/recognition-result.schema.json b/contracts/recognition-result.schema.json index 43781b3..00c8154 100644 --- a/contracts/recognition-result.schema.json +++ b/contracts/recognition-result.schema.json @@ -1,13 +1,13 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "arm-ka/recognition-result/v1", - "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.2", - "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.", - "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1, 1.2, …) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения.", + "$id": "arm-ka/recognition-result/v2", + "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0", + "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.", + "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, смысл is_recognized у OCR отвязан от confidence. Схема 1.2 — в истории git (коммит 43b4b80 репозитория arm-ka-docs) и в моделях маппера 1.x ядра.", "type": "object", "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "properties": { - "schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.2 пишет \"1.2\"" }, + "schema_version": { "type": "string", "pattern": "^2\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v2.0 пишет \"2.0\"" }, "job_id": { "type": "string", "format": "uuid" }, "file_id": { "type": "string", "format": "uuid" }, "service": { @@ -15,10 +15,11 @@ "required": ["name", "version"], "properties": { "name": { "enum": ["tabular-parser", "scan-gateway"] }, - "version": { "type": "string" } + "version": { "type": "string" }, + "statements_version": { "type": "string", "minLength": 1, "description": "коммит репозитория arm-statements, из которого разложена копия общего кода; обязателен, если в documents есть statementDocument" } } }, - "status": { "enum": ["done", "needs_check", "error"] }, + "status": { "enum": ["done", "needs_check", "error"], "description": "done ⇔ все документы done и нет предупреждений файла уровня error; error — документов нет" }, "error": { "type": ["object", "null"], "properties": { @@ -35,7 +36,7 @@ "fields_total": { "type": "integer" }, "fields_low_confidence": { "type": "integer" }, "fields_unrecognized": { "type": "integer" }, - "warnings": { "type": "array", "items": { "type": "string" } } + "warnings": { "type": "array", "items": { "$ref": "#/$defs/fileWarning" }, "description": "предупреждения уровня файла (ошибочный json набора ИС, потерянная таблица, формат файла)" } } }, "raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" }, @@ -44,31 +45,68 @@ "items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] } } }, - "if": { - "properties": { "status": { "enum": ["done", "needs_check"] } }, - "required": ["status"] - }, - "then": { - "properties": { "documents": { "minItems": 1 } } - }, + "allOf": [ + { + "if": { + "properties": { "status": { "enum": ["done", "needs_check"] } }, + "required": ["status"] + }, + "then": { + "properties": { "documents": { "minItems": 1 } } + } + }, + { + "if": { + "properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] } }, "required": ["document_type"] } } }, + "required": ["documents"] + }, + "then": { + "properties": { "service": { "required": ["name", "version", "statements_version"] } } + } + } + ], "$defs": { "money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" }, + "confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, + "documentStatus": { "enum": ["done", "needs_check"] }, + "headerField": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "address"] }, + "warning": { + "type": "object", + "required": ["code", "severity", "message"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$", "examples": ["COLUMN_WITHOUT_DATE", "ROW_SPLIT"], "description": "код по таблице 05-domain-rules.md §11; не перечисление — новый код не меняет MAJOR" }, + "severity": { "enum": ["error", "info"], "description": "error → документ needs_check; info на статус не влияет" }, + "message": { "type": "string" }, + "line_refs": { "type": "array", "items": { "type": "string" }, "description": "ref строк документа (statementDocument.lines[].ref)" }, + "column_index": { "type": "integer" }, + "field": { "$ref": "#/$defs/headerField" } + } + }, + "fileWarning": { + "allOf": [ { "$ref": "#/$defs/warning" } ], + "properties": { + "locator": { "type": "string", "examples": ["file:aep_3_ofr.json", "sheet:Лист3"], "description": "место в файле, к которому относится предупреждение" } + } + }, "failedRelation": { "type": "object", - "required": ["column_index", "relation", "expected", "actual"], + "required": ["column_index", "relation", "codes", "expected", "actual"], "properties": { "column_index": { "type": "integer" }, "relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] }, - "expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения как в файле" }, - "actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам" } + "codes": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^\\d{4,5}$" }, "description": "коды цели и членов соотношения: нарушение подсвечивает все строки этих кодов в колонке" }, + "expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения (цель) по строкам ok; null — цели нет" }, + "actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам ok" } } }, "osvDocument": { "type": "object", - "required": ["document_type", "locator", "header", "group_schema", "rows"], + "required": ["document_type", "locator", "header", "group_schema", "rows", "status", "warnings"], "properties": { "document_type": { "const": "OSV" }, "locator": { "type": "string", "examples": ["sheet:Лист_1"] }, + "status": { "$ref": "#/$defs/documentStatus", "description": "done — ведомость разобрана (§1.6)" }, + "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" }, "description": "предупреждения листа ОСВ; все уровня info, код с префиксом OSV_" }, "header": { "type": "object", "required": ["organization_name", "account_code_raw", "period_text"], @@ -142,79 +180,128 @@ } } }, + "statementValue": { + "type": "object", + "required": ["column_index", "amount", "text", "confidence", "is_recognized", "needs_review"], + "properties": { + "column_index": { "type": "integer" }, + "amount": { "$ref": "#/$defs/money" }, + "string_value": { "type": ["string", "null"], "description": "текст ячейки, только когда amount = null и текст непуст и не прочерк" }, + "text": { "type": ["string", "null"], "description": "сырой текст источника всегда (у Excel — как показывает числовой формат); null — позиции нет" }, + "confidence": { "$ref": "#/$defs/confidence", "description": "уверенность источника (у Excel 100; позиции нет — 100)" }, + "is_recognized": { "type": "boolean", "description": "текст разобран как число без выброшенных и исправленных символов; от confidence не зависит" }, + "needs_review": { "type": "boolean", "description": "сводный признак: нуждается в покрытии §7 и не покрыто, не подтверждён знак, или в несошедшемся жёстком соотношении (§11)" } + } + }, + "statementLine": { + "type": "object", + "required": ["row_index", "ref", "line_status", "line_code", "line_code_text", "line_name", "needs_review", "values"], + "properties": { + "row_index": { "type": "integer", "minimum": 0, "description": "порядковый номер строки в документе с 0, по порядку сетки после разлепления" }, + "ref": { "type": "string", "minLength": 1, "examples": ["row:8", "row:14#2", "Баланс!C27"], "description": "положение в источнике; уникален в документе" }, + "line_status": { "enum": ["ok", "unreadable", "missing", "detail"] }, + "line_code": { "type": ["string", "null"], "pattern": "^\\d{4,5}$", "description": "разобранный код; null ⇔ line_status ≠ ok" }, + "line_code_text": { "type": ["string", "null"], "description": "сырой текст ячейки кода" }, + "line_code_confidence": { "$ref": "#/$defs/confidence", "description": "от источника; у кода с заменой двойника — не выше 99" }, + "line_name": { "type": ["string", "null"], "description": "распознанное наименование как в источнике (не из справочника)" }, + "line_name_confidence": { "$ref": "#/$defs/confidence" }, + "parent_ref": { "type": ["string", "null"], "description": "ref строки-родителя у detail" }, + "needs_review": { "type": "boolean", "description": "неуверенный код не подтверждён структурой или повтор кода с неуверенной копией" }, + "values": { "type": "array", "items": { "$ref": "#/$defs/statementValue" } } + }, + "allOf": [ + { + "if": { "properties": { "line_status": { "const": "ok" } }, "required": ["line_status"] }, + "then": { "properties": { "line_code": { "type": "string" } } }, + "else": { "properties": { "line_code": { "type": "null" } } } + }, + { + "if": { "properties": { "line_status": { "const": "detail" } }, "required": ["line_status"] }, + "then": { "required": ["parent_ref"], "properties": { "parent_ref": { "type": "string" } } } + } + ] + }, "statementDocument": { "type": "object", - "required": ["document_type", "locator", "header", "columns", "lines"], + "required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"], "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] }, - "locator": { "type": "string", "examples": ["pages:1-2"] }, + "locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] }, + "status": { "$ref": "#/$defs/documentStatus" }, + "form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов (§11); null — DISCLOSURE/OTHER" }, + "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } }, "header": { "type": "object", "properties": { - "form_name": { "type": "string" }, + "form_name": { "type": ["string", "null"], "description": "заголовок формы как в источнике" }, "organization_name": { "type": ["string", "null"] }, - "organization_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, - "inn": { "type": ["string", "null"] }, - "inn_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, - "report_date_text": { "type": ["string", "null"] }, + "organization_name_text": { "type": ["string", "null"] }, + "organization_name_confidence": { "$ref": "#/$defs/confidence" }, + "inn": { "type": ["string", "null"], "pattern": "^(\\d{10}|\\d{12})$" }, + "inn_text": { "type": ["string", "null"] }, + "inn_confidence": { "$ref": "#/$defs/confidence" }, "report_date": { "type": ["string", "null"], "format": "date" }, + "report_date_text": { "type": ["string", "null"] }, + "report_date_confidence": { "$ref": "#/$defs/confidence" }, "units": { "type": ["string", "null"], "examples": ["тыс. руб."] }, + "units_text": { "type": ["string", "null"] }, + "units_confidence": { "$ref": "#/$defs/confidence" }, "okud": { "type": ["string", "null"] }, + "okud_text": { "type": ["string", "null"] }, + "okud_confidence": { "$ref": "#/$defs/confidence" }, "okpo": { "type": ["string", "null"] }, + "okpo_text": { "type": ["string", "null"] }, + "okpo_confidence": { "$ref": "#/$defs/confidence" }, "okopf": { "type": ["string", "null"] }, + "okopf_text": { "type": ["string", "null"] }, + "okopf_confidence": { "$ref": "#/$defs/confidence" }, "okfs": { "type": ["string", "null"] }, - "address": { "type": ["string", "null"] } + "okfs_text": { "type": ["string", "null"] }, + "okfs_confidence": { "$ref": "#/$defs/confidence" }, + "address": { "type": ["string", "null"] }, + "address_text": { "type": ["string", "null"] }, + "address_confidence": { "$ref": "#/$defs/confidence" }, + "needs_review_fields": { "type": "array", "items": { "$ref": "#/$defs/headerField" }, "uniqueItems": true, "description": "поля шапки с needs_review = true (сейчас — только report_date)" } } }, "columns": { "type": "array", "items": { "type": "object", - "required": ["column_index", "label"], + "required": ["column_index", "label", "period_kind"], "properties": { "column_index": { "type": "integer" }, - "label": { "type": "string", "examples": ["На 30 сентября 2025 г."] }, - "period_date": { "type": ["string", "null"], "format": "date" } - } - } - }, - "lines": { - "type": "array", - "items": { - "type": "object", - "required": ["line_code", "line_name", "values"], - "properties": { - "line_code": { "type": "string" }, - "line_code_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, - "line_name": { "type": "string" }, - "line_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, - "values": { - "type": "array", - "items": { - "type": "object", - "required": ["column_index"], - "properties": { - "column_index": { "type": "integer" }, - "amount": { "$ref": "#/$defs/money" }, - "string_value": { "type": ["string", "null"] }, - "confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 }, - "is_recognized": { "type": "boolean" } - } - } - } + "label": { "type": "string", "examples": ["На 30 сентября 2025 г.", "На отчетную дату"] }, + "label_confidence": { "$ref": "#/$defs/confidence", "description": "у Excel 100; подписи нет — null" }, + "period_date": { "type": ["string", "null"], "format": "date", "description": "конец периода колонки (period_end); у относительной подписи — от эффективной даты документа" }, + "period_kind": { "enum": ["absolute", "relative:current", "relative:prev_year_end", "relative:prev_prev_year_end", "relative:prev_period", "none"] } } } }, + "lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" } }, "checks": { "type": "object", - "description": "Результаты самопроверки формы по соотношениям §7 (tabular-parser). Отсутствие объекта — «не проверялось» (OCR-путь); пустые массивы — «проверено, сошлось».", + "required": ["relations_failed", "soft_relations_failed"], + "description": "Самопроверка формы по соотношениям §7 общим кодом (оба конвейера). Есть у BALANCE/INCOME со строками, нет у документа без строк. Пустые массивы — «проверено, сошлось».", "properties": { - "relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения жёстких соотношений: документ needs_check" }, - "soft_relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения мягких соотношений (OQ-12, подстроки «в том числе») и жёстких в пределах округления итогов (OQ-138): только предупреждение" }, - "duplicate_line_codes": { "type": "array", "items": { "type": "string" }, "description": "коды строк, встретившиеся в документе больше одного раза (осталась первая строка)" } + "relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "жёсткие соотношения, не сошедшиеся вне допуска ±1 и округления 0,001 %: документ needs_check" }, + "soft_relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "мягкие соотношения (OQ-12, подстроки) и жёсткие в пределах округления (OQ-138): info" } } } - } + }, + "allOf": [ + { + "if": { + "properties": { "document_type": { "enum": ["BALANCE", "INCOME"] }, "lines": { "minItems": 1 } }, + "required": ["document_type", "lines"] + }, + "then": { "required": ["checks"] } + }, + { + "if": { "properties": { "lines": { "maxItems": 0 } }, "required": ["lines"] }, + "then": { "not": { "required": ["checks"] } } + } + ] } } } 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 7bac561..463679a 100644 --- a/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md +++ b/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md @@ -257,7 +257,7 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat До разбора кода SHALL выполняться разлепление: ячейка кода содержит N ≥ 2 частей через перевод строки, и каждая непустая ячейка значений — ровно N частей; разлепление делается, если хотя бы одна часть кода разобралась как код диапазона столбца. Получается N строк с `ref` `row:#`; нечитаемая часть кода — строка `unreadable`; наименование — -k-я из непустых частей, если их тоже N, иначе целиком у каждой. Значения разлепленных строк получают +k-я из непустых частей, если их тоже N, иначе целиком у каждой; `line_code_text` и `text` значений — k-е части. Значения разлепленных строк получают `is_recognized = false`; разлепление — предупреждение `ROW_SPLIT` (`info`). Число частей не совпало или ни одна часть не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`.