docs(#116): дизайн единой нормализации ББ/ОФР (scan-gateway + tabular-parser) #117
@ -3,7 +3,7 @@
|
||||
"$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. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.",
|
||||
"$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": {
|
||||
@ -57,7 +57,7 @@
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] } }, "required": ["document_type"] } } },
|
||||
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"] } }, "required": ["document_type"] } } },
|
||||
"required": ["documents"]
|
||||
},
|
||||
"then": {
|
||||
@ -223,12 +223,12 @@
|
||||
},
|
||||
"statementDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"],
|
||||
"required": ["document_type", "form_variant", "locator", "header", "columns", "lines", "status", "warnings"],
|
||||
"properties": {
|
||||
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
|
||||
"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": "по структуре кодов (§11); null — DISCLOSURE/OTHER" },
|
||||
"form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов строк ok (§11); null — DISCLOSURE; обязателен" },
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
|
||||
"header": {
|
||||
"type": "object",
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
version: 1
|
||||
|
||||
# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в
|
||||
# перечне больше одного раза.
|
||||
# перечне больше одного раза: в варианте full код стоит ровно 1 + (число его вхождений в этот список) раз, в simplified —
|
||||
# не чаще (сейчас повторов в simplified нет; validate_contracts.py проверяет оба правила).
|
||||
repeats:
|
||||
66n:
|
||||
# раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих
|
||||
|
||||
@ -12,6 +12,7 @@ import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
@ -32,7 +33,7 @@ SCHEMAS = {
|
||||
EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")]
|
||||
|
||||
CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml"
|
||||
CODE_RE = re.compile(r"^\d{4,5}$")
|
||||
CODE_RE = re.compile(r"^\d{4}$") # в перечне только четырёхзначные коды (пятизначные — через родителя)
|
||||
|
||||
# Порядок важен: result-message-* проверяется раньше result-*.
|
||||
PATTERNS = [
|
||||
@ -150,10 +151,24 @@ def code_order_errors(data):
|
||||
for code in codes:
|
||||
if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first):
|
||||
errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы")
|
||||
repeats = {c for i, c in enumerate(codes) if c in codes[:i]}
|
||||
allowed = set(data.get("repeats", {}).get(edition, {}).get(form, []))
|
||||
if repeats - allowed:
|
||||
errors.append(f"{where}: повтор кода вне перечня repeats: {sorted(repeats - allowed)}")
|
||||
errors.extend(repeat_errors(where, codes, data.get("repeats", {}).get(edition, {}).get(form, []), variant))
|
||||
return errors
|
||||
|
||||
|
||||
def repeat_errors(where, codes, repeats, variant):
|
||||
"""Законные повторы (Р-20): в варианте full код встречается ровно 1 + (число его вхождений в repeats) раз;
|
||||
в simplified повторы допустимы только из repeats и не чаще, чем в full; код из repeats, которого нет в full, — ошибка."""
|
||||
errors = []
|
||||
counts = Counter(c for c in codes if isinstance(c, str))
|
||||
allowed = Counter(repeats)
|
||||
for code in sorted(set(counts) | set(allowed)):
|
||||
expected = 1 + allowed[code]
|
||||
actual = counts[code]
|
||||
if variant == "full":
|
||||
if actual != expected:
|
||||
errors.append(f"{where}: код {code} встречается {actual} раз, а по repeats должен {expected}")
|
||||
elif actual > 1 and actual > expected:
|
||||
errors.append(f"{where}: повтор кода {code} ({actual} раз) вне перечня repeats")
|
||||
return errors
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user