Andrey Epifancev 248f8b0c74 openspec(#116): спека по ревью K-1…K-25 и решениям владельца Р-20…Р-22; решения S-26…S-52
- design.md: Р-20 (законные повторы кода в перечне, синонимы в словаре имён), Р-21 (мягкое соотношение, сошедшееся
  точно, подтверждает неуверенное значение), Р-22 (S-1…S-25 приняты с оговорками; отмечены утверждённые владельцем);
  решения при доработке S-26…S-52 с ценой ошибки
- statement-normalization: отдельное требование sync-statements (пути, VERSION, коды 0 / 10 / 20 / 2), словарь
  единиц и ОКЕИ, ref Excel с кавычками, нумерация с двойниками и пропуском, номер колонки, содержимое строки, столбец
  кодов у DISCLOSURE, упрощённый 2400 мягкий, Р-20 / Р-21 со сценариями, обе редакции перечня, интерфейс функции
  статуса, TABLE_PARSE_ERROR / SHEET_OTHER уровня файла
- arm-core: признак строки правки row_index IS NULL, header_source, формы без checks (1.x и миграция) — §7 ядром в
  document_issue, codes / column_index, RESULT_STATUS_INCONSISTENT, синонимы и «итого по разделу», PATCH
  organization_name / confirmed: false / подтверждение даты, LINE_CODE_TAKEN, акцепт и подтверждения, LINES_UNRESOLVED
  с текстом, «того же периода», откат миграции
- recognition-contract: MODIFIED «Копия контракта … идентична» — сверка по MAJOR копии, *.v1.schema.json на окне;
  1.x — MAPPING_WARNING / LEGACY_WARNING; okei, column_index, пустые массивы DISCLOSURE, статус ОСВ одной формулировкой
- quality-checks (новая дельта): make check охватывает копию arm_statements, make check-full — statements-check
- tabular-recognition-service: манифест statements_manifest.yaml — failed_relations страниц 2 баланса (K-25)
- scan-gateway, arm-web: TABLE_PARSE_ERROR файла; подсказки шапки, снятие подтверждений, LINE_CODE_TAKEN
- proposal: шаг 0 выкладки, перечни кодов, ожидания на реальных сканах, Impact

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

17 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. Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы, лист «прочее»). 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?} (line_refs — только ref строк из lines[]); у osvDocument все предупреждения — info, status = done, когда контрольные суммы §1.4 листа сошлись, иначе needs_check. Остальное в osvDocument — как в v1.2: у строки rows[] необязательное поле extra — объект «заголовок колонки → строка или null» для колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»); у шапки — 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, okei, address и для каждого <поле>_text и <поле>_confidence, плюс needs_review_fields[]; columns[] — column_index (номер столбца в сетке кандидата: у OCR — column_index ИС, у Excel — номер столбца листа с 0), 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 (у Excel <лист>!<A1>, имя листа в кавычках по правилу Excel), 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; у документа без строк columns и lines — пустые массивы. 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). На окне перехода схемы прежнего MAJOR SHALL лежать в docs/contracts/ рядом с текущими как recognition-result.v1.schema.json и recognition-result-message.v1.schema.json (байтовые копии 1.2 и 1.0), их примеры — в docs/contracts/examples/v1/; scripts/validate_contracts.py проверяет оба набора. Они удаляются тем же изменением, что удаляет маппер 1.x ядра.

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, повтор не выполняется (как для любого результата не по схеме)

Requirement: Копия контракта в сервисе идентична источнику

Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в api/contracts/ и проверять в make check их байтовую идентичность файлам docs/contracts/ (../docs или путь ARM_KA_DOCS; при недоступности — пропуск с предупреждением). На окне перехода между MAJOR тест идентичности SHALL сверять копию со схемой того MAJOR, который сервис пишет (или, у ядра, читает): копия с $id, оканчивающимся на /v1, — с docs/contracts/<имя>.v1.schema.json, иначе — с основным файлом docs/contracts/<имя>.schema.json; ядро держит копии обоих MAJOR. Правило теста вводится в tabular-parser и scan-gateway до слияния контракта v2.0 в docs, иначе их make check упадёт на окне перехода.

Scenario: Копия устарела

  • WHEN в docs/contracts/ изменена схема, а копия в сервисе — нет
  • THEN тест идентичности в make check сервиса падает с указанием имени файла

Scenario: Сервис ещё пишет 1.x после слияния v2.0 в docs

  • WHEN в docs основной recognition-result.schema.json уже v2.0, а копия в tabular-parser — v1.2 ($id …/v1)
  • THEN тест идентичности сверяет копию с recognition-result.v1.schema.json и проходит; посторонний PR в сервис на окне перехода не краснеет

Scenario: Сервис перешёл на 2.0

  • WHEN scan-gateway в PR шага 5 обновил копию до v2.0 ($id …/v2)
  • THEN тест сверяет её с основным файлом docs

ADDED Requirements

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

Ядро SHALL до перехода любого сервиса принимать конверты (включая ack processing) и файлы результата и 1.x, и 2.0, с двумя мапперами файла результата. При приёме 1.x статус документа формы SHALL считаться по прежнему правилу D-6, где «проверки §7 сошлись» — по расчёту ядра копией arm_statements (полный набор), записанному в document_issue, а поля v2.0 заполняться по умолчанию: line_status = ok, row_index — по порядку строк в результате с 0, ref = null, line_code_original_value = line_code, text = string_value, needs_review значения — по прежнему values_ok (is_recognized не false и confidence 100 или пуста), needs_review строки — false, period_kind — absolute при period_date, иначе none, form_variant = null; строки metrics.warnings — в recognition_job.metrics.warnings структурами {code: "LEGACY_WARNING", severity: "info", message}; предупреждения маппера по документу — document_issue MAPPING_WARNING (info); statements_version задания — null. Сервисы SHALL переходить на 2.0 только после выкладки ядра, принимающего 2.0. Маппер 1.x и схемы *.v1.schema.json удаляются после перехода обоих сервисов отдельным изменением.

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

  • WHEN после выкладки ядра tabular-parser присылает результат 1.2 с повтором кода и checks.duplicate_line_codes
  • THEN ядро принимает его маппером 1.x, обе строки кода — по прежнему правилу «остаётся первая» с MAPPING_WARNING, нарушения §7 — в document_issue от ядра, статус документа — по 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) — поэтому порядок «сначала ядро» обязателен