{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$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, header.okei (+ _text, _confidence), смысл is_recognized у OCR отвязан от confidence, statementDocument.form_variant обязателен, OTHER удалён из statementDocument.document_type. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.", "type": "object", "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "properties": { "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": { "type": "object", "required": ["name", "version"], "properties": { "name": { "enum": ["tabular-parser", "scan-gateway"] }, "version": { "type": "string" }, "statements_version": { "type": "string", "minLength": 1, "description": "коммит репозитория arm-statements, из которого разложена копия общего кода; обязателен, если в documents есть statementDocument" } } }, "status": { "enum": ["done", "needs_check", "error"], "description": "done ⇔ все документы done и нет предупреждений файла уровня error; error — документов нет" }, "error": { "type": ["object", "null"], "properties": { "code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] }, "message": { "type": "string" } } }, "metrics": { "type": "object", "properties": { "duration_ms": { "type": "integer" }, "pages": { "type": "integer" }, "rows": { "type": "integer" }, "fields_total": { "type": "integer" }, "fields_low_confidence": { "type": "integer" }, "fields_unrecognized": { "type": "integer" }, "warnings": { "type": "array", "items": { "$ref": "#/$defs/fileWarning" }, "description": "предупреждения уровня файла (ошибочный json набора ИС, потерянная таблица, формат файла)" } } }, "raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" }, "documents": { "type": "array", "items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] } } }, "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"] } }, "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", "okei", "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[]; строки ролей title / header / numbering в line_refs не попадают (о них — message)" }, "column_index": { "type": "integer", "description": "столбец сетки кандидата (как columns[].column_index); у COLUMN_SKIPPED и других предупреждений о столбце вне columns[] — номер этого столбца" }, "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", "codes", "expected", "actual"], "properties": { "column_index": { "type": "integer" }, "relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] }, "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", "status", "warnings"], "properties": { "document_type": { "const": "OSV" }, "locator": { "type": "string", "examples": ["sheet:Лист_1"] }, "status": { "$ref": "#/$defs/documentStatus", "description": "done — контрольные суммы §1.4 листа сошлись, иначе needs_check (05-domain-rules.md §1.6)" }, "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" }, "description": "предупреждения листа ОСВ; все уровня info, код с префиксом OSV_" }, "header": { "type": "object", "required": ["organization_name", "account_code_raw", "period_text"], "properties": { "organization_name": { "type": "string", "description": "как в r1 файла" }, "organization_inn": { "type": ["string", "null"] }, "account_code_raw": { "type": "string", "examples": ["60", "58.03"] }, "period_text": { "type": "string", "examples": ["1 квартал 2025 г.", "2024 г."] }, "period_year": { "type": ["integer", "null"] }, "period_quarter": { "enum": ["Q1", "Q2", "Q3", "Q4", null] }, "is_cumulative": { "type": ["boolean", "null"], "description": "по тексту; ядро может переопределить по данным (OQ-02)" }, "period_start": { "type": ["string", "null"], "format": "date", "description": "начало периода по шапке: первое число квартала/года либо начало диапазона месяцев (OQ-131)" }, "period_end": { "type": ["string", "null"], "format": "date", "description": "конец периода по шапке" }, "is_from_year_start": { "type": ["boolean", "null"], "description": "period_start — 1 января; is_cumulative не переопределяет" }, "data_type": { "enum": ["BU", "NU", "SUM", "CURRENCY", "QUANTITY", null] }, "currency": { "type": ["string", "null"], "examples": ["RUB", "руб."] }, "header_variant": { "enum": [1, 2] } } }, "group_schema": { "type": "array", "description": "порядок группировок из шапки, после первого ACCOUNT", "items": { "type": "object", "required": ["type"], "properties": { "type": { "enum": ["ACCOUNT", "DEPARTMENT", "COUNTERPARTY", "CONTRACT", "OTHER"] }, "name": { "type": "string", "description": "исходный текст из шапки" }, "has_inn": { "type": "boolean" } } } }, "columns": { "type": "object", "description": "диагностика: какие колонки листа были приняты за шесть сумм и ИНН", "additionalProperties": { "type": "string" } }, "rows": { "type": "array", "items": { "type": "object", "required": ["row_number", "level", "row_value", "group_type"], "properties": { "row_number": { "type": "integer" }, "level": { "type": "integer" }, "parent_row_number": { "type": ["integer", "null"] }, "row_value": { "type": "string" }, "group_type": { "enum": ["ACCOUNT", "DEPARTMENT", "COUNTERPARTY", "CONTRACT", "OTHER", "TOTAL"] }, "group_name": { "type": ["string", "null"] }, "account_code_raw": { "type": ["string", "null"], "description": "код текущей ветки, как в файле (60.1)" }, "counterparty_name": { "type": ["string", "null"] }, "counterparty_inn": { "type": ["string", "null"] }, "data_type": { "enum": ["BU", "NU", "SUM", "CURRENCY", "QUANTITY", null] }, "opening_debit": { "$ref": "#/$defs/money" }, "opening_credit": { "$ref": "#/$defs/money" }, "turnover_debit": { "$ref": "#/$defs/money" }, "turnover_credit": { "$ref": "#/$defs/money" }, "closing_debit": { "$ref": "#/$defs/money" }, "closing_credit": { "$ref": "#/$defs/money" }, "extra": { "type": ["object", "null"], "description": "нераспознанные колонки внутри таблицы: заголовок колонки → текст ячейки (§1.2)", "additionalProperties": { "type": ["string", "null"] } } } } }, "checks": { "type": "object", "properties": { "total_row_matches_root": { "type": "boolean" }, "balance_identity_violations": { "type": "array", "items": { "type": "integer" }, "description": "row_number строк, где opening−turnover≠closing" }, "parent_sum_violations": { "type": "array", "items": { "type": "integer" } } } } } }, "statementValue": { "type": "object", "required": ["column_index", "amount", "text", "confidence", "is_recognized", "needs_review"], "properties": { "column_index": { "type": "integer", "description": "колонка значений — columns[].column_index" }, "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", "'Бухгалтерский баланс. Лист 1'!C14"], "description": "положение в источнике; уникален в документе. OCR — row:[#k]; Excel — <лист>! по правилу Excel: имя листа в одинарных кавычках, если в нём есть что-то кроме букв, цифр и _ или оно начинается с цифры; апостроф внутри имени удваивается" }, "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", "form_variant", "locator", "header", "columns", "lines", "status", "warnings"], "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"], "description": "OTHER в v2.0 не порождается: лист «прочее» — предупреждение файла SHEET_OTHER, документа нет (значение OTHER — только в схеме 1.x)" }, "locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] }, "status": { "$ref": "#/$defs/documentStatus" }, "form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов строк ok (§11); null — DISCLOSURE; обязателен" }, "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } }, "header": { "type": "object", "properties": { "form_name": { "type": ["string", "null"], "description": "заголовок формы как в источнике" }, "organization_name": { "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"] }, "okfs_text": { "type": ["string", "null"] }, "okfs_confidence": { "$ref": "#/$defs/confidence" }, "okei": { "type": ["string", "null"], "examples": ["384"], "description": "код ОКЕИ единицы (384 — тыс. руб., 383 — руб., 385 — млн руб.); запасной источник units (§11.2)" }, "okei_text": { "type": ["string", "null"] }, "okei_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_check (сейчас — только неуверенная дата отчёта, не подтверждённая датами колонок); сомнения в ИНН и организации — предупреждения info с field, сюда не входят (§11.9)" } } }, "columns": { "type": "array", "items": { "type": "object", "required": ["column_index", "label", "period_kind"], "properties": { "column_index": { "type": "integer", "minimum": 0, "description": "номер столбца в сетке кандидата шага 1 (у OCR — column_index ИС, не обязательно подряд; у Excel — номер столбца листа первого сегмента, с 0); уникален в документе; колонки сегментов без шапки сопоставляются с колонками первого сегмента по порядку и несут их номер" }, "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" }, "description": "у документа без строк (DISCLOSURE, форма без столбца кодов) — пустой массив" }, "checks": { "type": "object", "required": ["relations_failed", "soft_relations_failed"], "description": "Самопроверка формы по соотношениям §7 общим кодом (оба конвейера). Есть у BALANCE/INCOME со строками, нет у документа без строк. Пустые массивы — «проверено, сошлось».", "properties": { "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"] } } } ] } } }