Andrey Epifancev 90ea429642 docs(#116): proposal и дельты спек единой нормализации ББ/ОФР
- proposal: зачем, что меняется, порядок выкладки «сначала ядро», non-goals
- statement-normalization: шаги 1–8 общего кода arm_statements (кандидат, раскладка, статусы строк,
  значения, §7, needs_review, структурная проверка кода, уровни предупреждений, статус)
- recognition-contract: файл результата и конверт v2.0, переход 1.x → 2.0
- tabular-recognition-service, scan-gateway: адаптеры шага 1 и что уходит из сервисов
- arm-core: хранение v2.0, агрегат по коду, document_issue, RECOGNIZED, PATCH lines/columns/confirmed,
  порядок 409, миграция v5.19, crm_id ГСК/ГСЗ
- arm-web: экран проверки формы по новым полям
- design.md: автозамена между файлами — §9.5 п. 3, а не §12 п. 3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 00:40:50 +04:00

12 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Сообщение результата распознавания

Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed) SHALL соответствовать docs/contracts/recognition-result-message.schema.json v2.0: обязательные поля schema_version ("2.0"), job_id, file_id, status (processing | done | needs_check | error), service {name, version}; при status ∈ {done, needs_check} обязательны result_s3 {bucket, key} и result_schema_version; при status = error обязателен error {code, message}; опциональны raw_s3_key (только scan-gateway) и metrics. metrics.warnings[] — структуры warning (как в файле результата), не строки. Поле group_schema в конверт NOT включается — источник recognition_job.group_schema — файл результата. Производитель v2.0 SHALL писать в конверт тот же MAJOR, что и в файл результата.

Scenario: Успешный результат

  • WHEN сервис завершил обработку файла
  • THEN файл результата записан в S3 по ключу results/{file_id}/{job_id}/result.json, а затем опубликован конверт со status = done | needs_check, result_s3 указывает на этот файл, result_schema_version = "2.0", заголовок Nats-Msg-Id = result:<job_id>:<attempt>

Scenario: Ошибка обработки

  • WHEN сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается)
  • THEN публикуется конверт со status = error и error.code из перечня схемы (UNSUPPORTED_FORMAT, UNREADABLE, TIMEOUT, SCHEMA_MISMATCH, NOT_IMPLEMENTED, FILE_READ_ERROR, INVALID_FORMAT, INVALID_DOCUMENT, RECOGNITION_FAILED, UNKNOWN_ERROR); result_s3 может отсутствовать

Scenario: Предупреждения в конверте

  • WHEN у файла есть предупреждение уровня файла (ошибочный json набора ИС)
  • THEN metrics.warnings конверта содержит объект {code: "JSON_FAILED", severity: "error", message, locator}

Requirement: Файл результата

Файл результата SHALL соответствовать docs/contracts/recognition-result.schema.json v2.0: один файл (stored_file) → один job_id → массив documents из osvDocument и/или statementDocument; денежные значения — строки ^-?\d+(\.\d{1,2})?$; confidence — 0–100. Пустой массив documents допустим только при status = error. service.statements_version — коммит копии общего кода arm_statements, которым посчитан результат (обязателен при наличии statementDocument). metrics.warnings[] — предупреждения уровня файла (структура warning с locator).

Каждый документ (и osvDocument, и statementDocument) SHALL иметь status (done | needs_check) и warnings[] — структуры {code, severity, message, line_refs?, column_index?, field?}; у osvDocument все предупреждения — info, status = done, когда ведомость разобрана. Остальное в osvDocument — как в v1.2 (extra, period_start, period_end, is_from_year_start).

statementDocument SHALL иметь form_variant (full | simplified | null); header — поля organization_name, inn, report_date, units, okud, okpo, okopf, okfs, address и для каждого <поле>_text и <поле>_confidence, плюс needs_review_fields[]; columns[] — column_index, label, label_confidence, period_date, period_kind (absolute | relative:current | relative:prev_year_end | relative:prev_prev_year_end | relative:prev_period | none); lines[] — row_index, ref, line_status (ok | unreadable | missing | detail), line_code (строка 4–5 цифр или null; null ⇔ статус не ok), line_code_text, line_code_confidence, line_name (строка или null), line_name_confidence, parent_ref (у detail), needs_review, values[] — column_index, amount, string_value, text, confidence, is_recognized, needs_review. checks SHALL присутствовать у BALANCE / INCOME со строками и отсутствовать у документа без строк; relations_failed и soft_relations_failed — {column_index, relation, codes[], expected, actual}; duplicate_line_codes удалён. document_type — BALANCE | INCOME | DISCLOSURE | OTHER. ref уникален в документе.

Scenario: Результат валидируется примерами

  • WHEN CI сервиса запускает тесты контракта
  • THEN все файлы docs/contracts/examples/result-*.json проходят схему файла результата v2.0, а все request-*.json / result-message-*.json — схемы сообщений

Scenario: Лишняя колонка сохранена

  • WHEN в ОСВ между ИНН и суммами есть колонка «КПП»
  • THEN у строк контрагентов extra = {"КПП": "<значение>"}, файл проходит схему v2.0

Scenario: Форма из Excel с проверками

  • WHEN tabular-parser разобрал книгу с балансом на двух листах, ОФР со строкой без кода под 2110 и пояснениями (examples/result-statement-excel.json)
  • THEN файл проходит схему v2.0: BALANCE с locator из двух листов, status, checks и мягким нарушением; INCOME со строкой detail (line_code = null, parent_ref); DISCLOSURE без columns/lines и без checks

Scenario: Форма из OCR с сомнительными значениями

  • WHEN scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка unreadable (examples/result-statement-minimal.json)
  • THEN файл проходит схему v2.0; у покрытого значения is_recognized = false, needs_review = false, text — сырой; у строки unreadable line_code = null, line_code_text — сырой текст; документ needs_check

Scenario: Границы периода ОСВ

  • WHEN в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.»
  • THEN period_start = "2024-10-01", period_end = "2025-09-30", is_from_year_start = false; документ ОСВ с status и warnings, файл проходит схему v2.0

Scenario: Строка без кода с кодом в поле

  • WHEN у строки line_status = unreadable передан непустой line_code
  • THEN файл не проходит схему v2.0

Requirement: Версионирование контракта

Поле schema_version SHALL быть строкой MAJOR.MINOR. Добавление необязательных полей или значений enum — MINOR (обратно совместимо); удаление/переименование полей, смена типов или семантики — MAJOR. Сервис SHALL отвергать запрос с неизвестным MAJOR (error.code = SCHEMA_MISMATCH); ядро SHALL принимать результат с известным MAJOR и любым известным или новым MINOR. Запрос recognition-request остаётся v1.0; файл результата и конверт — v2.0 (MAJOR поднят: line_code стал nullable, metrics.warnings и document.warnings — структуры, удалён checks.duplicate_line_codes, изменён смысл is_recognized у OCR).

Scenario: Несовместимая версия запроса

  • WHEN сервис, поддерживающий запрос 1.x, получает запрос schema_version = "2.0"
  • THEN публикуется результат status = error, error.code = SCHEMA_MISMATCH без ack processing: ответом на запрос служит сам финальный конверт, ядро закрывает им задание (published → error), и повторной доставки запроса не будет. Конверт публикуется только если из тела извлекаются корректные job_id и file_id; иначе — как для нечитаемого запроса (ответа нет, ядро закроет задание по NO_ACK / dlq). attempt для Nats-Msg-Id берётся из тела запроса; при его отсутствии реализации расходятся (tabular-parser берёт attempt из заголовка Nats-Msg-Id запроса или не отвечает, scan-gateway подставляет 1) — см. OQ-71

Scenario: Ядро получает MAJOR, которого не знает

  • WHEN ядро, знающее 1.x и 2.x, получает конверт или файл результата schema_version = "3.0"
  • THEN результат не проходит проверку схемы, файл получает ERROR с причиной в metrics.mapping_error, повтор не выполняется (как для любого результата не по схеме)

ADDED Requirements

Requirement: Переход на контракт v2.0 — «сначала ядро»

Ядро SHALL до перехода любого сервиса принимать конверты (включая ack processing) и файлы результата и 1.x, и 2.0, с двумя мапперами файла результата. При приёме 1.x статус документа формы SHALL считаться по прежнему правилу D-6, а поля v2.0 заполняться по умолчанию: line_status = ok, row_index — по порядку строк в результате, ref = null, text = string_value, needs_review значения — по прежнему values_ok (is_recognized не false и confidence 100 или пуста), needs_review строки — false, period_kind — absolute при period_date, иначе none, form_variant = null, предупреждения-строки — warning {code: "LEGACY_WARNING", severity: "info", message}. Сервисы SHALL переходить на 2.0 только после выкладки ядра, принимающего 2.0. Маппер 1.x удаляется после перехода обоих сервисов отдельным изменением.

Scenario: Сервис ещё на v1.2

  • WHEN после выкладки ядра tabular-parser присылает результат 1.2 с повтором кода и checks.duplicate_line_codes
  • THEN ядро принимает его маппером 1.x, статус документа — по D-6, файл не ERROR

Scenario: Ack нового сервиса

  • WHEN scan-gateway после перехода публикует ack processing со schema_version = "2.0"
  • THEN ядро переводит задание в processing, как для ack 1.0

Scenario: Порядок нарушен

  • WHEN сервис прислал результат 2.0 с line_code = null ядру, знающему только 1.x
  • THEN результат не проходит проверку схемы, файл получает ERROR (metrics.mapping_error) — поэтому порядок «сначала ядро» обязателен