- копия схем и примеров контракта v2.0 (запрос остаётся v1.0), модели result/common/message/request; ResultWarning/FileWarning, статус и предупреждения у документов, строки forms v2.0, service.statements_version - statements/pipeline.py: кандидаты адаптера -> normalize_candidate -> to_contract / warning_to_contract - BookParser: статус книги через envelope_status, SHEET_OTHER / FILE_TYPE_MISMATCH / XLS_NO_CODEPAGE как предупреждения файла, OSV_* и OSV_XLSB_NO_OUTLINE в документах ОСВ, тексты иерархии по спеке - воркер, app и CLI получают statements_version из require_statements_version() - старые модели документа формы вынесены в statements/legacy_models.py до удаления старого конвейера (задача 9) - OpenSpec: MODIFIED «Парсер — заменяемый интерфейс» в дельте tabular-recognition-service Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
311 lines
25 KiB
JSON
311 lines
25 KiB
JSON
{
|
||
"$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:<row_index ИС>[#k]; Excel — <лист>!<A1> по правилу 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"] } }
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|