From e33a4a45f99081c0171d659167b833cd9b25cf67 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 02:02:41 +0400 Subject: [PATCH] =?UTF-8?q?contracts(#116):=20=D0=BE=D0=BA=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=D0=B0=201.x,=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=87=D0=BD=D0=B8=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC,=20okei,=20=D1=81=D0=BC?= =?UTF-8?q?=D1=8B=D1=81=D0=BB=20column=5Findex=20=D0=B8=20ref=20(K-5,=20K-?= =?UTF-8?q?6,=20K-8,=20K-22,=20S-15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recognition-result.v1.schema.json и recognition-result-message.v1.schema.json — байтовые копии схем 1.2 / 1.0 (до 9b1a4cc), примеры 1.x — examples/v1/; живут до удаления маппера 1.x ядра (K-8) - statement-code-orders.yaml — упорядоченные перечни кодов ББ/ОФР по редакциям 66n / fsbu4 и вариантам full / simplified, с законными повторами (Р-20: 1220, 1520, 2410 у fsbu4; раздел III НКО у 66n) и кодами печатных форм 1С из fixtures (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) (K-5) - validate_contracts.py: схемы v1 и examples/v1/, проверка формы перечня кодов; make venv ставит PyYAML - v2.0: header.okei (+ _text, _confidence) и okei в headerField (K-22); columns[].column_index — столбец сетки кандидата (K-6); line_refs — только строки lines[]; ref Excel — имя листа в кавычках по правилу Excel (S-15); статус ОСВ — по контрольным суммам §1.4 листа (S-22); примеры приведены к этим правилам Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- Makefile | 8 +- .../examples/result-statement-excel.json | 59 ++--- .../examples/result-statement-minimal.json | 18 +- contracts/examples/v1/result-error.json | 12 + .../examples/v1/result-message-done.json | 21 ++ .../examples/v1/result-message-error.json | 11 + .../v1/result-message-processing.json | 7 + contracts/examples/v1/result-osv-minimal.json | 83 +++++++ .../examples/v1/result-osv-needs-check.json | 125 ++++++++++ .../examples/v1/result-statement-excel.json | 46 ++++ .../examples/v1/result-statement-minimal.json | 50 ++++ .../recognition-result-message.schema.json | 4 +- .../recognition-result-message.v1.schema.json | 70 ++++++ contracts/recognition-result.schema.json | 23 +- contracts/recognition-result.v1.schema.json | 220 ++++++++++++++++++ contracts/statement-code-orders.yaml | 193 +++++++++++++++ scripts/validate_contracts.py | 80 ++++++- 17 files changed, 971 insertions(+), 59 deletions(-) create mode 100644 contracts/examples/v1/result-error.json create mode 100644 contracts/examples/v1/result-message-done.json create mode 100644 contracts/examples/v1/result-message-error.json create mode 100644 contracts/examples/v1/result-message-processing.json create mode 100644 contracts/examples/v1/result-osv-minimal.json create mode 100644 contracts/examples/v1/result-osv-needs-check.json create mode 100644 contracts/examples/v1/result-statement-excel.json create mode 100644 contracts/examples/v1/result-statement-minimal.json create mode 100644 contracts/recognition-result-message.v1.schema.json create mode 100644 contracts/recognition-result.v1.schema.json create mode 100644 contracts/statement-code-orders.yaml diff --git a/Makefile b/Makefile index 83fb376..93449fc 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория. # Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md. VENV := .venv -# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema) +# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML) PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) CHANGE ?= slice-0-skeleton # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) @@ -9,12 +9,12 @@ DBML ?= model/data-model-v5.19.dbml .PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams -# Единственная зависимость скриптов проверки контракта — jsonschema +# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml) venv: uv venv --python 3.11 --allow-existing $(VENV) - uv pip install --python $(VENV)/bin/python jsonschema + uv pip install --python $(VENV)/bin/python jsonschema pyyaml -# Примеры contracts/examples/ против трёх схем JSON Schema 2020-12 +# Примеры contracts/examples/ (и examples/v1/ окна перехода) против схем JSON Schema 2020-12 + перечни кодов форм validate-contracts: $(PY) scripts/validate_contracts.py diff --git a/contracts/examples/result-statement-excel.json b/contracts/examples/result-statement-excel.json index 0f08d5f..5e7b311 100644 --- a/contracts/examples/result-statement-excel.json +++ b/contracts/examples/result-statement-excel.json @@ -22,33 +22,34 @@ "units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100, "okud": "0710001", "okud_text": "0710001", "okud_confidence": 100, "okpo": "12345678", "okpo_text": "12345678", "okpo_confidence": 100, + "okei": "384", "okei_text": "384", "okei_confidence": 100, "needs_review_fields": [] }, "columns": [ - { "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" } + { "column_index": 3, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }, + { "column_index": 4, "label": "На 31 декабря 2024 г.", "label_confidence": 100, "period_date": "2024-12-31", "period_kind": "absolute" } ], "lines": [ - { "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 } ] } + { "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": null, "text": "-", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "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", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ] + "soft_relations_failed": [ { "column_index": 4, "relation": "1110 = 11101", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ] } }, { @@ -69,20 +70,20 @@ "needs_review_fields": [] }, "columns": [ - { "column_index": 0, "label": "За январь - сентябрь 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" } + { "column_index": 3, "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 } ] } + { "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": 3, "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": 3, "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": 3, "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": 3, "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" } ], + "relations_failed": [ { "column_index": 3, "relation": "2200 = 2100 - 2210 - 2220", "codes": ["2200", "2100", "2210", "2220"], "expected": null, "actual": "400.00" } ], "soft_relations_failed": [] } }, diff --git a/contracts/examples/result-statement-minimal.json b/contracts/examples/result-statement-minimal.json index 1c9f6e2..9a3eb1d 100644 --- a/contracts/examples/result-statement-minimal.json +++ b/contracts/examples/result-statement-minimal.json @@ -25,25 +25,25 @@ "needs_review_fields": [] }, "columns": [ - { "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" } + { "column_index": 2, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" } ], "lines": [ { "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] }, + "values": [ { "column_index": 2, "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 } ] } + "values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] } ], "checks": { "relations_failed": [], "soft_relations_failed": [] } } diff --git a/contracts/examples/v1/result-error.json b/contracts/examples/v1/result-error.json new file mode 100644 index 0000000..54af7cb --- /dev/null +++ b/contracts/examples/v1/result-error.json @@ -0,0 +1,12 @@ +{ + "schema_version": "1.0", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "service": { "name": "tabular-parser", "version": "0.1.0" }, + "status": "error", + "error": { + "code": "NOT_IMPLEMENTED", + "message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet" + }, + "documents": [] +} diff --git a/contracts/examples/v1/result-message-done.json b/contracts/examples/v1/result-message-done.json new file mode 100644 index 0000000..6d56f17 --- /dev/null +++ b/contracts/examples/v1/result-message-done.json @@ -0,0 +1,21 @@ +{ + "schema_version": "1.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" }, + "result_s3": { + "bucket": "arm-ka", + "key": "results/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/result.json" + }, + "result_schema_version": "1.0", + "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", + "metrics": { + "duration_ms": 18400, + "pages": 2, + "fields_total": 4, + "fields_low_confidence": 0, + "fields_unrecognized": 0, + "warnings": [] + } +} diff --git a/contracts/examples/v1/result-message-error.json b/contracts/examples/v1/result-message-error.json new file mode 100644 index 0000000..f5ec77a --- /dev/null +++ b/contracts/examples/v1/result-message-error.json @@ -0,0 +1,11 @@ +{ + "schema_version": "1.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" }, + "error": { + "code": "NOT_IMPLEMENTED", + "message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet" + } +} diff --git a/contracts/examples/v1/result-message-processing.json b/contracts/examples/v1/result-message-processing.json new file mode 100644 index 0000000..2087150 --- /dev/null +++ b/contracts/examples/v1/result-message-processing.json @@ -0,0 +1,7 @@ +{ + "schema_version": "1.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" } +} diff --git a/contracts/examples/v1/result-osv-minimal.json b/contracts/examples/v1/result-osv-minimal.json new file mode 100644 index 0000000..b0a9e97 --- /dev/null +++ b/contracts/examples/v1/result-osv-minimal.json @@ -0,0 +1,83 @@ +{ + "schema_version": "1.2", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "service": { "name": "tabular-parser", "version": "0.1.0" }, + "status": "done", + "documents": [ + { + "document_type": "OSV", + "locator": "sheet:Лист_1", + "header": { + "organization_name": "ООО Ромашка", + "organization_inn": "7700000000", + "account_code_raw": "60", + "period_text": "1 квартал 2025 г.", + "period_year": 2025, + "period_quarter": "Q1", + "is_cumulative": false, + "period_start": "2025-01-01", + "period_end": "2025-03-31", + "is_from_year_start": true, + "data_type": "BU", + "currency": "RUB", + "header_variant": 1 + }, + "group_schema": [ + { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } + ], + "columns": { + "opening_debit": "C", + "opening_credit": "D", + "turnover_debit": "E", + "turnover_credit": "F", + "closing_debit": "G", + "closing_credit": "H" + }, + "rows": [ + { + "row_number": 1, + "level": 0, + "parent_row_number": null, + "row_value": "60", + "group_type": "ACCOUNT", + "group_name": null, + "account_code_raw": "60", + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" + }, + { + "row_number": 2, + "level": 1, + "parent_row_number": 1, + "row_value": "ООО Лютик, ИНН 7711111111", + "group_type": "COUNTERPARTY", + "group_name": "Контрагенты", + "account_code_raw": "60", + "counterparty_name": "ООО Лютик", + "counterparty_inn": "7711111111", + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50", + "extra": { "КПП": "352801001" } + } + ], + "checks": { + "total_row_matches_root": true, + "balance_identity_violations": [], + "parent_sum_violations": [] + } + } + ] +} diff --git a/contracts/examples/v1/result-osv-needs-check.json b/contracts/examples/v1/result-osv-needs-check.json new file mode 100644 index 0000000..81d64a8 --- /dev/null +++ b/contracts/examples/v1/result-osv-needs-check.json @@ -0,0 +1,125 @@ +{ + "schema_version": "1.1", + "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", + "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", + "service": { "name": "tabular-parser", "version": "1.0.0" }, + "status": "needs_check", + "metrics": { + "rows": 3, + "warnings": [ + "sheet=Лист_1 hierarchy=outline", + "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3" + ] + }, + "documents": [ + { + "document_type": "OSV", + "locator": "sheet:Лист_1", + "header": { + "organization_name": "ООО Ромашка", + "organization_inn": null, + "account_code_raw": "60", + "period_text": "1 квартал 2025 г.", + "period_year": 2025, + "period_quarter": "Q1", + "is_cumulative": false, + "data_type": "BU", + "currency": "RUB", + "header_variant": 1 + }, + "group_schema": [ + { "type": "ACCOUNT", "name": "Счет" }, + { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } + ], + "columns": { + "opening_debit": "D", + "opening_credit": "E", + "turnover_debit": "F", + "turnover_credit": "G", + "closing_debit": "H", + "closing_credit": "I", + "inn": "B" + }, + "rows": [ + { + "row_number": 1, + "level": 0, + "parent_row_number": null, + "row_value": "60", + "group_type": "ACCOUNT", + "group_name": "Счет", + "account_code_raw": "60", + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" + }, + { + "row_number": 2, + "level": 1, + "parent_row_number": 1, + "row_value": "ООО Лютик", + "group_type": "COUNTERPARTY", + "group_name": "Контрагенты", + "account_code_raw": "60", + "counterparty_name": "ООО Лютик", + "counterparty_inn": "7711111111", + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50", + "extra": { "КПП": "352801001" } + }, + { + "row_number": 3, + "level": 2, + "parent_row_number": 2, + "row_value": "Вал.", + "group_type": "OTHER", + "group_name": null, + "account_code_raw": "60", + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "CURRENCY", + "opening_debit": "0.00", + "opening_credit": "400.00", + "turnover_debit": "100.00", + "turnover_credit": "50.00", + "closing_debit": "0.00", + "closing_credit": "400.00" + }, + { + "row_number": 4, + "level": 0, + "parent_row_number": null, + "row_value": "Итого", + "group_type": "TOTAL", + "group_name": null, + "account_code_raw": null, + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" + } + ], + "checks": { + "total_row_matches_root": true, + "balance_identity_violations": [3], + "parent_sum_violations": [] + } + } + ] +} diff --git a/contracts/examples/v1/result-statement-excel.json b/contracts/examples/v1/result-statement-excel.json new file mode 100644 index 0000000..260528b --- /dev/null +++ b/contracts/examples/v1/result-statement-excel.json @@ -0,0 +1,46 @@ +{ + "schema_version": "1.2", + "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"] }, + "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" }, + "columns": [ + { "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" }, + { "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" } + ], + "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 } ] } + ], + "checks": { "relations_failed": [], "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "expected": "905786.99", "actual": "900000.00" } ], "duplicate_line_codes": [] } + }, + { + "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 } ] } + ], + "checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] } + }, + { + "document_type": "DISCLOSURE", + "locator": "sheet:Пояснения", + "header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах" }, + "columns": [], + "lines": [] + } + ] +} diff --git a/contracts/examples/v1/result-statement-minimal.json b/contracts/examples/v1/result-statement-minimal.json new file mode 100644 index 0000000..913cfc5 --- /dev/null +++ b/contracts/examples/v1/result-statement-minimal.json @@ -0,0 +1,50 @@ +{ + "schema_version": "1.2", + "job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03", + "file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04", + "service": { "name": "scan-gateway", "version": "0.1.0" }, + "status": "done", + "raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json", + "documents": [ + { + "document_type": "BALANCE", + "locator": "pages:1-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": "тыс. руб." + }, + "columns": [ + { "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" }, + { "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" } + ], + "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 } + ] + } + ] + } + ] +} diff --git a/contracts/recognition-result-message.schema.json b/contracts/recognition-result-message.schema.json index 692b812..a78afd8 100644 --- a/contracts/recognition-result-message.schema.json +++ b/contracts/recognition-result-message.schema.json @@ -3,7 +3,7 @@ "$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 (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, что и в файл результата.", + "$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, что и в файл результата. Схема 1.0 на окне перехода лежит рядом — recognition-result-message.v1.schema.json (примеры — examples/v1/).", "type": "object", "required": ["schema_version", "job_id", "file_id", "status", "service"], "properties": { @@ -61,7 +61,7 @@ "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"] }, + "field": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "okei", "address"] }, "locator": { "type": "string" } } } diff --git a/contracts/recognition-result-message.v1.schema.json b/contracts/recognition-result-message.v1.schema.json new file mode 100644 index 0000000..4d9f401 --- /dev/null +++ b/contracts/recognition-result-message.v1.schema.json @@ -0,0 +1,70 @@ +{ + "$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", + "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 отвергают незнакомые значения.", + "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\"" }, + "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"] }, + "service": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "enum": ["tabular-parser", "scan-gateway"] }, + "version": { "type": "string" } + } + }, + "result_s3": { + "type": "object", + "description": "файл результата (recognition-result): results/{file_id}/{job_id}/result.json", + "required": ["bucket", "key"], + "properties": { + "bucket": { "type": "string", "minLength": 1 }, + "key": { "type": "string", "minLength": 1 } + } + }, + "result_schema_version": { "type": "string", "pattern": "^\\d+\\.\\d+$", "description": "schema_version файла результата, MAJOR.MINOR" }, + "error": { + "type": "object", + "required": ["code", "message"], + "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" } + } + }, + "raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" }, + "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": { "type": "string" } } + } + } + }, + "allOf": [ + { + "if": { + "properties": { "status": { "enum": ["done", "needs_check"] } }, + "required": ["status"] + }, + "then": { "required": ["result_s3", "result_schema_version"] } + }, + { + "if": { + "properties": { "status": { "const": "error" } }, + "required": ["status"] + }, + "then": { "required": ["error"] } + } + ] +} diff --git a/contracts/recognition-result.schema.json b/contracts/recognition-result.schema.json index 00c8154..85e2103 100644 --- a/contracts/recognition-result.schema.json +++ b/contracts/recognition-result.schema.json @@ -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, смысл is_recognized у OCR отвязан от confidence. Схема 1.2 — в истории git (коммит 43b4b80 репозитория arm-ka-docs) и в моделях маппера 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. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.", "type": "object", "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "properties": { @@ -69,7 +69,7 @@ "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"] }, + "headerField": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "okei", "address"] }, "warning": { "type": "object", "required": ["code", "severity", "message"], @@ -77,8 +77,8 @@ "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" }, + "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" } } }, @@ -105,7 +105,7 @@ "properties": { "document_type": { "const": "OSV" }, "locator": { "type": "string", "examples": ["sheet:Лист_1"] }, - "status": { "$ref": "#/$defs/documentStatus", "description": "done — ведомость разобрана (§1.6)" }, + "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", @@ -184,7 +184,7 @@ "type": "object", "required": ["column_index", "amount", "text", "confidence", "is_recognized", "needs_review"], "properties": { - "column_index": { "type": "integer" }, + "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 — позиции нет" }, @@ -198,7 +198,7 @@ "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": "положение в источнике; уникален в документе" }, + "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": "сырой текст ячейки кода" }, @@ -258,10 +258,13 @@ "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_review = true (сейчас — только report_date)" } + "needs_review_fields": { "type": "array", "items": { "$ref": "#/$defs/headerField" }, "uniqueItems": true, "description": "поля шапки, значение которых требует проверки и держит документ в needs_check (сейчас — только неуверенная дата отчёта, не подтверждённая датами колонок); сомнения в ИНН и организации — предупреждения info с field, сюда не входят (§11.9)" } } }, "columns": { @@ -270,7 +273,7 @@ "type": "object", "required": ["column_index", "label", "period_kind"], "properties": { - "column_index": { "type": "integer" }, + "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); у относительной подписи — от эффективной даты документа" }, @@ -278,7 +281,7 @@ } } }, - "lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" } }, + "lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" }, "description": "у документа без строк (DISCLOSURE, форма без столбца кодов) — пустой массив" }, "checks": { "type": "object", "required": ["relations_failed", "soft_relations_failed"], diff --git a/contracts/recognition-result.v1.schema.json b/contracts/recognition-result.v1.schema.json new file mode 100644 index 0000000..43781b3 --- /dev/null +++ b/contracts/recognition-result.v1.schema.json @@ -0,0 +1,220 @@ +{ + "$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 отвергают незнакомые значения.", + "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\"" }, + "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" } + } + }, + "status": { "enum": ["done", "needs_check", "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": { "type": "string" } } + } + }, + "raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" }, + "documents": { + "type": "array", + "items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] } + } + }, + "if": { + "properties": { "status": { "enum": ["done", "needs_check"] } }, + "required": ["status"] + }, + "then": { + "properties": { "documents": { "minItems": 1 } } + }, + "$defs": { + "money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" }, + "failedRelation": { + "type": "object", + "required": ["column_index", "relation", "expected", "actual"], + "properties": { + "column_index": { "type": "integer" }, + "relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] }, + "expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения как в файле" }, + "actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам" } + } + }, + "osvDocument": { + "type": "object", + "required": ["document_type", "locator", "header", "group_schema", "rows"], + "properties": { + "document_type": { "const": "OSV" }, + "locator": { "type": "string", "examples": ["sheet:Лист_1"] }, + "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" } } + } + } + } + }, + "statementDocument": { + "type": "object", + "required": ["document_type", "locator", "header", "columns", "lines"], + "properties": { + "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] }, + "locator": { "type": "string", "examples": ["pages:1-2"] }, + "header": { + "type": "object", + "properties": { + "form_name": { "type": "string" }, + "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"] }, + "report_date": { "type": ["string", "null"], "format": "date" }, + "units": { "type": ["string", "null"], "examples": ["тыс. руб."] }, + "okud": { "type": ["string", "null"] }, + "okpo": { "type": ["string", "null"] }, + "okopf": { "type": ["string", "null"] }, + "okfs": { "type": ["string", "null"] }, + "address": { "type": ["string", "null"] } + } + }, + "columns": { + "type": "array", + "items": { + "type": "object", + "required": ["column_index", "label"], + "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" } + } + } + } + } + } + }, + "checks": { + "type": "object", + "description": "Результаты самопроверки формы по соотношениям §7 (tabular-parser). Отсутствие объекта — «не проверялось» (OCR-путь); пустые массивы — «проверено, сошлось».", + "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": "коды строк, встретившиеся в документе больше одного раза (осталась первая строка)" } + } + } + } + } + } +} diff --git a/contracts/statement-code-orders.yaml b/contracts/statement-code-orders.yaml new file mode 100644 index 0000000..1e199ff --- /dev/null +++ b/contracts/statement-code-orders.yaml @@ -0,0 +1,193 @@ +# Упорядоченные перечни кодов строк форм ББ/ОФР по редакциям и вариантам — данные для структурного подтверждения +# неуверенного кода (05-domain-rules.md §11.9 «Код строки»; change unified-statement-normalization, K-5, Р-20). +# +# Что это: порядок кодов бланка (итоги после слагаемых: 1110 … 1190, 1100), без наименований (Р-3: «код → наименование» +# знает только ядро, arm-core/api/rules/statement_lines.yaml). Только четырёхзначные коды: пятизначная подстрока +# подтверждается через родителя (§11.9). Перечень — объединение кодов шаблонов банка (словарь имён ядра, +# fixtures/statements-xlsx/templates/) и кодов печатных форм 1С и сканов, найденных в fixtures (источник — комментарий +# у кода). Код без образца в fixtures сюда не добавляется: кандидаты — OQ-168. +# +# Где живёт: этот файл — источник; в пакете arm_statements лежит его байтовая копия (rules/statement_code_orders.yaml), +# идентичность проверяет sync-statements --check (как копии схем контракта в сервисах). Ядро берёт порядок и состав +# редакций из копии пакета; sync-statements --check сверяет, что все коды словаря имён ядра есть в перечне своей +# редакции. Правка перечня — PR сюда, затем в arm-statements, затем sync-statements в потребителях («сначала ядро»). +# +# Как читается (§11.9): четырёхзначные коды строк ok документа — последовательность; для каждой редакции берётся +# наибольшая подпоследовательность, согласованная с порядком перечня варианта формы документа (form_variant); выбирается +# редакция с более длинной (при равенстве — по эффективной дате: fsbu4 после 2024-12-31). Повтор кода допустим только +# там, где он есть в перечне (repeats): тогда структура подтверждает обе копии; повтор вне перечня с неуверенной копией — +# needs_review у неуверенной копии (Р-16, Р-20). +# +# Проверка формы файла — scripts/validate_contracts.py (make validate-contracts). +version: 1 + +# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в +# перечне больше одного раза. +repeats: + 66n: + # раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих + # (fixtures/ocr/aep_3.json, 31.12.2024: 1310 «Паевой фонд», 1320 «Целевой капитал», 1350 «Целевые средства», + # 1360 «Фонд недвижимого и особо ценного движимого имущества», 1370 «Резервный и иные целевые фонды») + BALANCE: ["1310", "1320", "1350", "1360", "1370", "1300"] + INCOME: [] + fsbu4: + # печатная форма 1С по ФСБУ 4/2023 (fixtures/ocr/aep_6.json, «БО 1 кв 2025.json», «Бухгалтерская отчетность + # 1 кв.25.json»): 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде» (AURORA-1431) + BALANCE: ["1220", "1520"] + # 2410 «Налог при упрощенной системе налогообложения» после 2412 (fixtures/ocr/aep_6.json, OQ-152) + INCOME: ["2410"] + +editions: + 66n: + BALANCE: + full: + - "1110" + - "1120" + - "1130" + - "1140" + - "1150" + - "1160" + - "1170" + - "1180" + - "1190" + - "1100" + - "1210" + - "1220" + - "1230" + - "1231" # код клиента: unagrande-group/2024-FY/ББ. Лист 1 .xls + - "1240" + - "1250" + - "1260" + - "1200" + - "1600" + - "1310" + - "1320" + - "1340" + - "1350" + - "1360" + - "1370" + - "1300" + - "1310" # раздел III НКО (aep_3), см. repeats + - "1320" + - "1350" + - "1360" + - "1370" + - "1300" + - "1410" + - "1420" + - "1430" + - "1450" + - "1400" + - "1510" + - "1520" + - "1530" + - "1540" + - "1550" + - "1500" + - "1700" + # упрощённая форма, приказ 66н прил. 5 — по бланку, образца в fixtures нет (OQ-166) + simplified: ["1150", "1170", "1210", "1250", "1230", "1600", "1300", "1350", "1360", "1410", "1450", "1510", + "1520", "1550", "1700"] + INCOME: + full: + - "2110" + - "2120" + - "2100" + - "2210" + - "2220" + - "2200" + - "2310" + - "2320" + - "2330" + - "2340" + - "2350" + - "2300" + - "2410" + - "2411" + - "2412" + - "2460" + - "2400" + - "2510" + - "2520" + - "2530" # печатная форма 1С: unagrande-co/2024-FY/ОФР. Лист 2 .xls, mezheninovskaya/2025-Q3 + - "2500" + - "2900" + - "2910" + - "2930" # печатная форма 1С: fixtures/ocr/aep_3.json (после 2900) + # упрощённая форма, приказ 66н прил. 5 — по бланку, образца нет (OQ-166) + simplified: ["2110", "2120", "2330", "2340", "2350", "2410", "2400"] + fsbu4: + BALANCE: + full: + - "1105" # печатная форма 1С: npz/2025-Q3, fixtures/ocr/aep_6.json, «БО 1 кв 2025.json» + - "1110" + - "1130" # печатная форма 1С: npz/2025-Q3, «БО 1 кв 2025.json», «Бухгалтерская отчетность 1 кв.25.json» + - "1140" # то же и aep_6.json + - "1150" + - "1160" + - "1170" + - "1180" + - "1190" + - "1100" + - "1210" + - "1215" + - "1220" + - "1220" # «НДС, начисленный по отгрузке», см. repeats + - "1230" + - "1240" + - "1250" + - "1260" + - "1200" + - "1600" + - "1310" + - "1320" + - "1340" + - "1350" + - "1360" + - "1370" + - "1300" + - "1410" + - "1420" + - "1430" + - "1450" + - "1400" + - "1510" + - "1520" + - "1520" # «Обязательство по аренде», см. repeats + - "1530" # печатная форма 1С: npz/2025-Q3, aep_6.json, оба json 1 кв. 2025 + - "1540" + - "1550" + - "1500" + - "1700" + # упрощённая форма по ФСБУ 4/2023 — состав принят как у 66н до образца (OQ-166, OQ-168) + simplified: ["1150", "1170", "1210", "1250", "1230", "1600", "1300", "1350", "1360", "1410", "1450", "1510", + "1520", "1550", "1700"] + INCOME: + full: + - "2110" + - "2120" + - "2100" + - "2210" + - "2220" + - "2200" + - "2310" + - "2320" + - "2330" + - "2340" + - "2350" + - "2300" + - "2410" + - "2411" + - "2412" + - "2410" # «Налог при упрощенной системе налогообложения», см. repeats + - "2420" + - "2460" # печатная форма 1С: npz/2025-Q3, aep_6.json, оба json 1 кв. 2025 + - "2400" + - "2510" + - "2520" + - "2530" + - "2500" + - "2900" + - "2910" + # упрощённая форма по ФСБУ 4/2023 — как у 66н до образца (OQ-166, OQ-168) + simplified: ["2110", "2120", "2330", "2340", "2350", "2410", "2400"] diff --git a/scripts/validate_contracts.py b/scripts/validate_contracts.py index a34c2d6..6b3ebbb 100644 --- a/scripts/validate_contracts.py +++ b/scripts/validate_contracts.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 -"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12. +"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12 и упорядоченных перечней кодов форм. Запуск из любого cwd: python scripts/validate_contracts.py -Зависимости: стандартная библиотека + jsonschema. +Зависимости: стандартная библиотека + jsonschema + PyYAML (`make venv`). Код возврата: 0 — всё валидно, 1 — любая ошибка. + +Окно перехода на MAJOR 2 (change unified-statement-normalization): схемы прежнего MAJOR лежат рядом как +`*.v1.schema.json`, их примеры — в `examples/v1/`; удаляются после перехода обоих сервисов (маппер 1.x ядра — тоже). """ import datetime import json @@ -21,8 +24,16 @@ SCHEMAS = { "request": CONTRACTS / "recognition-request.schema.json", "message": CONTRACTS / "recognition-result-message.schema.json", "result": CONTRACTS / "recognition-result.schema.json", + "message.v1": CONTRACTS / "recognition-result-message.v1.schema.json", + "result.v1": CONTRACTS / "recognition-result.v1.schema.json", } +# Каталог примеров → суффикс вида схемы: examples/ — текущий MAJOR, examples/v1/ — прежний (окно перехода). +EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")] + +CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml" +CODE_RE = re.compile(r"^\d{4,5}$") + # Порядок важен: result-message-* проверяется раньше result-*. PATTERNS = [ ("request-", "request"), @@ -75,9 +86,11 @@ def main(): print(f"OK схема {path.name}") counts = {kind: 0 for kind in SCHEMAS} - for example in sorted(EXAMPLES.glob("*.json")): - kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None) - if kind is None: + examples = [(example, suffix) for folder, suffix in EXAMPLE_DIRS for example in sorted(folder.glob("*.json"))] + for example, suffix in examples: + base = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None) + kind = None if base is None else (base if base == "request" else base + suffix) + if kind is None or kind not in SCHEMAS: print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)") failed = True continue @@ -107,10 +120,67 @@ def main(): print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}") failed = True + failed = check_code_orders() or failed total = sum(counts.values()) print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}") return 1 if failed else 0 +def code_order_errors(data): + """Структура перечня: editions[id] → forms[BALANCE|INCOME] → variants[full|simplified] → список кодов.""" + errors = [] + if not isinstance(data, dict) or not isinstance(data.get("editions"), dict): + return ["нет словаря editions"] + for edition in ("66n", "fsbu4"): + forms = data["editions"].get(edition) + if not isinstance(forms, dict): + errors.append(f"нет редакции {edition}") + continue + for form, first in (("BALANCE", "1"), ("INCOME", "2")): + variants = forms.get(form) + if not isinstance(variants, dict): + errors.append(f"{edition}: нет формы {form}") + continue + for variant in ("full", "simplified"): + codes = variants.get(variant) + where = f"{edition}/{form}/{variant}" + if not isinstance(codes, list) or not codes: + errors.append(f"{where}: пустой или не список") + continue + 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)}") + return errors + + +def check_code_orders(): + """Упорядоченные перечни кодов форм (05-domain-rules.md §11.9): yaml разбирается и устроен как описано.""" + if not CODE_ORDERS.is_file(): + print(f"FAIL {CODE_ORDERS.name}: файл не найден") + return True + try: + import yaml + except ImportError: + print(f"FAIL {CODE_ORDERS.name}: нужен PyYAML (make venv)") + return True + try: + data = yaml.safe_load(CODE_ORDERS.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: + print(f"FAIL {CODE_ORDERS.name}: не YAML: {str(exc).splitlines()[0]}") + return True + errors = code_order_errors(data) + if errors: + print(f"FAIL {CODE_ORDERS.name}") + for error in errors: + print(f" {error}") + return True + print(f"OK {CODE_ORDERS.name}") + return False + + if __name__ == "__main__": sys.exit(main())