contracts(#116): OTHER убран из v2.0, form_variant обязателен; проверка числа повторов перечня кодов (L-22, L-28)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-18 11:36:12 +04:00
parent ad1cd307a1
commit 51d0f935ff
3 changed files with 27 additions and 11 deletions

View File

@ -3,7 +3,7 @@
"$id": "arm-ka/recognition-result/v2", "$id": "arm-ka/recognition-result/v2",
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0", "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0",
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.", "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", "type": "object",
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
"properties": { "properties": {
@ -57,7 +57,7 @@
}, },
{ {
"if": { "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"] "required": ["documents"]
}, },
"then": { "then": {
@ -223,12 +223,12 @@
}, },
"statementDocument": { "statementDocument": {
"type": "object", "type": "object",
"required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"], "required": ["document_type", "form_variant", "locator", "header", "columns", "lines", "status", "warnings"],
"properties": { "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)"] }, "locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] },
"status": { "$ref": "#/$defs/documentStatus" }, "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" } }, "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
"header": { "header": {
"type": "object", "type": "object",

View File

@ -22,7 +22,8 @@
version: 1 version: 1
# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в # Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в
# перечне больше одного раза. # перечне больше одного раза: в варианте full код стоит ровно 1 + (число его вхождений в этот список) раз, в simplified —
# не чаще (сейчас повторов в simplified нет; validate_contracts.py проверяет оба правила).
repeats: repeats:
66n: 66n:
# раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих # раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих

View File

@ -12,6 +12,7 @@ import datetime
import json import json
import re import re
import sys import sys
from collections import Counter
from pathlib import Path from pathlib import Path
from jsonschema import Draft202012Validator, FormatChecker from jsonschema import Draft202012Validator, FormatChecker
@ -32,7 +33,7 @@ SCHEMAS = {
EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")] EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")]
CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml" 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-*. # Порядок важен: result-message-* проверяется раньше result-*.
PATTERNS = [ PATTERNS = [
@ -150,10 +151,24 @@ def code_order_errors(data):
for code in codes: for code in codes:
if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first): if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first):
errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы") errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы")
repeats = {c for i, c in enumerate(codes) if c in codes[:i]} errors.extend(repeat_errors(where, codes, data.get("repeats", {}).get(edition, {}).get(form, []), variant))
allowed = set(data.get("repeats", {}).get(edition, {}).get(form, [])) return errors
if repeats - allowed:
errors.append(f"{where}: повтор кода вне перечня repeats: {sorted(repeats - allowed)}")
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 return errors