Andrey Epifancev 47e6372cbe chore(openspec): архив unified-statement-normalization (#116)
Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway,
statement-normalization, tabular-recognition-service) с Purpose, изменения
quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19,
entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:25:43 +04:00

35 KiB
Raw Blame History

recognition-contract Specification

Purpose

Контракт обмена между ядром arm-core и stateless-сервисами распознавания (tabular-parser, scan-gateway): тела сообщений NATS на subject'ах запроса и ответа (настройки окружения OPENBAO__NATS__NATS_*_SUBJECT, по умолчанию files.tabular_uploaded, files.img_uploaded и files.tabular_parsed), файл результата в S3 и правила версионирования и идемпотентности. Сервисы не знают БД; ядро — единственный писатель (DECISION-4).

Requirements

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

Тело сообщения на subject запроса tabular или image (настройки окружения NATS_TABULAR_REQUEST_SUBJECT / NATS_IMAGE_REQUEST_SUBJECT, по умолчанию files.tabular_uploaded / files.img_uploaded) SHALL соответствовать docs/contracts/recognition-request.schema.json v1.0: обязательные поля schema_version ("1.0"), job_id (uuid = recognition_job.id), file_id (uuid = stored_file.id), pipeline (tabular | image), file_type (XLS | XLSX | XLSB | PDF | PNG | JPG), original_name, s3 {bucket, key} исходника в роли uploads, reply_deadline (date-time, UTC), attempt (≥ 1). Subject SHALL соответствовать pipeline.

Scenario: Публикация запроса ядром

  • WHEN ядро переводит recognition_job из queued в published
  • THEN в subject, соответствующий recognition_job.pipeline, публикуется сообщение, проходящее схему запроса, с заголовком Nats-Msg-Id = <job_id>:<attempt>

Scenario: Сообщение не по схеме

  • WHEN сервис получает запрос, не проходящий схему (нет s3.key, неизвестный file_type), но из тела извлекаются корректные job_id и file_id
  • THEN сервис публикует результат со status = error, error.code = SCHEMA_MISMATCH без предшествующего ack processing и не падает; attempt для Nats-Msg-Id результата берётся из тела запроса, при его отсутствии реализации расходятся (tabular-parser берёт attempt из заголовка Nats-Msg-Id запроса или не отвечает, scan-gateway подставляет 1) — см. OQ-71

Scenario: Нечитаемый запрос

  • WHEN сервис получает сообщение, из которого нельзя извлечь job_id (не JSON, не объект, job_id отсутствует или не uuid), а значит нельзя собрать валидный конверт
  • THEN сервис ответа не публикует: запрос записывается в лог и отбрасывается; ядро не получит ack, повторит публикацию OUTBOX_MAX_REPUBLISH раз и переведёт задание в dlq (error_code = NO_ACK)

Scenario: Невалидный повтор запроса, уже находящегося в работе

  • WHEN сервис получает запрос, не проходящий схему, но с job_id, который уже в работе или обработан этим процессом
  • THEN конверт SCHEMA_MISMATCH не публикуется (его Nats-Msg-Id result:<job_id>:<attempt> совпал бы с настоящим результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается

Scenario: Примеры контракта — иллюстрации

  • WHEN запрос берётся из docs/contracts/examples/request-*.json (в request-image.json reply_deadline лежит в прошлом)
  • THEN он проходит схему: схема проверяет для reply_deadline лишь формат date-time, но не сравнивает срок с текущим временем; пример — иллюстрация формы сообщения, а не готовое к исполнению задание; проверка срока — дело сервиса (scan-gateway отвечает error TIMEOUT, если reply_deadline истёк до отправки файла в ИС Сканирование)

Requirement: Подтверждение взятия в работу (ack без JetStream)

Получив корректный запрос (прошедший проверку схемы и MAJOR), сервис SHALL немедленно, до чтения файла из S3, опубликовать в files.tabular_parsed конверт со status = processing, job_id, file_id, service {name, version} и заголовком Nats-Msg-Id = ack:<job_id>:<attempt>. Ядро SHALL перевести задание published → processing и записать started_at, service_name, service_version. Если ack не получен за ACK_TIMEOUT_SECONDS (по умолчанию 30), ядро SHALL опубликовать запрос повторно с тем же Nats-Msg-Id и тем же attempt, не более OUTBOX_MAX_REPUBLISH (по умолчанию 3) раз, после чего перевести задание в dlq.

Scenario: Сервис жив

  • WHEN запрос опубликован и сервис подписан
  • THEN в течение ACK_TIMEOUT_SECONDS в files.tabular_parsed появляется конверт processing, задание получает status = processing и started_at

Scenario: Сервис недоступен

  • WHEN ни один инстанс сервиса не подписан на subject запроса
  • THEN ядро публикует запрос повторно каждые ACK_TIMEOUT_SECONDS; после OUTBOX_MAX_REPUBLISH повторов задание получает status = dlq, request_msg_id не меняется

Scenario: Повторный запрос пришёл в работающий сервис

  • WHEN сервис получает запрос с job_id, который уже обрабатывается или обработан этим процессом
  • THEN сервис публикует ack повторно и не начинает вторую обработку

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 в v2.0 не порождается: лист «прочее» — предупреждение файла SHEET_OTHER, документа нет; значение остаётся только в схеме 1.x и в document_type ядра для документов прежних результатов); form_variant обязателен (null у DISCLOSURE); у документа без строк 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: Документ OTHER в v2.0

  • WHEN в результате v2.0 statementDocument с document_type = OTHER или без form_variant
  • THEN файл не проходит схему 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 ставить заголовок Nats-Msg-Id на каждое сообщение (запрос <job_id>:<attempt>, ack ack:<job_id>:<attempt>, результат result:<job_id>:<attempt>). Поскольку серверного дедупа нет, ядро SHALL считать результат дубликатом, если recognition_job.result_msg_id уже равен Nats-Msg-Id сообщения, и MUST NOT менять задание повторно; потерянный результат SHALL обнаруживаться по reply_deadline (timeout).

Scenario: Результат доставлен дважды

  • WHEN сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки)
  • THEN задание изменено один раз, второе сообщение проигнорировано

Requirement: Повторы публикации ответов сервисом

Сервис SHALL публиковать ack и результат с заголовком Nats-Msg-Id и при временной ошибке публикации (потеря соединения, таймаут подтверждения сервера) повторять публикацию с тем же Nats-Msg-Id: одна первая попытка и до PUBLISH_RETRIES повторов (значение по умолчанию 3, то есть не более 4 попыток), с паузой между попытками; пауза — деталь реализации. Повторы исчерпаны — сервис пишет error-лог и не публикует это сообщение снова; задание закрывается на стороне ядра (не пришёл результат — reply_deadline → timeout; не пришёл ack — повтор запроса и dlq, но результат, дошедший без ack, ядро принимает и в статусе published). Дубликаты, появившиеся из-за повтора, безопасны: ядро дедуплицирует их по Nats-Msg-Id.

Scenario: Временный сбой публикации

  • WHEN публикация результата упала с временной ошибкой, а следующая попытка успешна
  • THEN сообщение опубликовано с прежним Nats-Msg-Id; ядро изменит задание один раз, даже если первая попытка тоже дошла

Scenario: Повторы исчерпаны

  • WHEN все 1 + PUBLISH_RETRIES попыток публикации результата неуспешны
  • THEN сервис пишет error-лог и продолжает работу (процесс не падает), а ядро закрывает задание по reply_deadline

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

Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в api/contracts/ и проверять в make check их байтовую идентичность файлам docs/contracts/ (../docs или путь ARM_KA_DOCS; при недоступности — пропуск с предупреждением). Файл копии SHALL сверяться с файлом docs по правилу, одинаковому у ядра и сервисов и верному до, во время и после окна перехода между MAJOR:

  • схема <имя>.schema.json копии: M — номер из её $id (…/v<M>); пара — docs/contracts/<имя>.v<M>.schema.json, если такой файл есть, иначе основной docs/contracts/<имя>.schema.json (у recognition-request файла .v1 нет — его копия …/v1 сверяется с основным файлом; до слияния v2.0 в docs файлов .v1 нет вовсе — все копии сверяются с основными);
  • пример examples/<имя>.json копии: вид — по префиксу имени (request- — запрос, result-message- — конверт, result- — файл результата), M — MAJOR копии схемы этого вида; пара — docs/contracts/examples/v<M>/<имя>.json, если такой файл есть, иначе docs/contracts/examples/<имя>.json;
  • прочие файлы копии (<имя>.v<N>.schema.json, examples/v<N>/*.json — их держит ядро на окне) — с одноимённым файлом docs; нет его в docs — расхождение;
  • набор: каждой схеме и каждому примеру верхнего уровня docs (contracts/*.schema.json без .v<N>, contracts/examples/*.json) SHALL соответствовать файл копии; файлы docs *.v<N>.schema.json и examples/v<N>/ требуются в копии, только если копия держит этот MAJOR (ядро на окне); файлы другого MAJOR сервисом не сверяются.

Правило вводится в тест идентичности всех трёх потребителей (arm-core, tabular-parser, scan-gateway) до слияния контракта v2.0 в docs (шаг 0 выкладки); иначе их make check упадёт на окне перехода. Ядро держит копии обоих MAJOR с PR шага 3; при удалении MAJOR 1 (шаг 6) ядро сначала удаляет свои копии .v1, затем docs — файлы .v1.

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) и примеры до v2.0
  • THEN тест сверяет схему с основным файлом docs (файла .v2 нет), примеры result-* — с examples/*.json; examples/v1/ и *.v1.schema.json docs не сверяются

Scenario: Запрос v1.0 после слияния v2.0

  • WHEN в docs уже есть recognition-result.v1.schema.json, а recognition-request.v1.schema.json нет; копия запроса в сервисе — v1.0 ($id …/v1), примеры request-image.json, request-tabular.json
  • THEN копия запроса сверяется с основным recognition-request.schema.json, примеры запроса — с examples/request-*.json (в examples/v1/ их нет); тест проходит

Scenario: Примеры прежнего MAJOR

  • WHEN копия tabular-parser — v1.2, в ней examples/result-statement-excel.json версии 1.2, а в docs examples/result-statement-excel.json уже 2.0, examples/v1/result-statement-excel.json — 1.2
  • THEN пример копии сверяется с examples/v1/result-statement-excel.json и проходит

Scenario: Шаг 0 до слияния docs

  • WHEN тест с новым правилом слит в arm-core, tabular-parser и scan-gateway, а docs ещё на v1.2 без файлов .v1
  • THEN все копии сверяются с основными файлами и примерами верхнего уровня, тест проходит, как прежде

Scenario: Ядро на окне перехода

  • WHEN копия ядра после шага 3 держит основные схемы v2.0, *.v1.schema.json и examples/v1/
  • THEN основные сверяются с основными docs, .v1 и examples/v1/ — с одноимёнными файлами docs; набор совпадает

Requirement: Subject'ы NATS — настройки окружения

Subject'ы обмена SHALL задаваться секретами группы OPENBAO__NATS__ с одинаковыми ключами во всех трёх сервисах: NATS_TABULAR_REQUEST_SUBJECT (по умолчанию files.tabular_uploaded), NATS_IMAGE_REQUEST_SUBJECT (по умолчанию files.img_uploaded), NATS_RESULT_SUBJECT (по умолчанию files.tabular_parsed). У ядра и сервисов одного окружения значения MUST совпадать. Каждый сервис SHALL при старте проверять все три имени: непустое, без пробельных символов, без шаблонов * и >, без пустых токенов, три имени попарно различны; при нарушении сервис MUST NOT стартовать. Учётные данные NATS (OPENBAO__NATS__NATS_JWT, OPENBAO__NATS__NATS_SEED) SHALL задаваться по окружению теми же секретами, и их значения MUST NOT попадать в логи, repr настроек и тексты ошибок валидации конфигурации. Каждый сервис SHALL писать используемые subject'ы в лог при старте и отдавать их в /healthcheck объектом subjects (ключи tabular_request, image_request, result — только те, что использует сервис) независимо от состояния NATS. Queue group'ы и schema_version от настроек subject'ов не зависят.

Scenario: Умолчания

  • WHEN сервис стартует без ключей OPENBAO__NATS__NATS_*_SUBJECT
  • THEN обмен идёт по files.tabular_uploaded, files.img_uploaded, files.tabular_parsed

Scenario: Набор тестового окружения

  • WHEN всем трём сервисам заданы одинаковые OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT, NATS_IMAGE_REQUEST_SUBJECT, NATS_RESULT_SUBJECT, NATS_JWT, NATS_SEED тестового окружения
  • THEN ядро публикует запросы и получает ack и результаты только по этим subject'ам; subject'ы по умолчанию не используются, recognition_job.request_subject содержит subject тестового окружения

Scenario: Недопустимый набор

  • WHEN NATS_RESULT_SUBJECT совпадает с одним из subject'ов запроса, или имя содержит *, > или пробел
  • THEN сервис не стартует с ошибкой конфигурации, называющей ключ

Scenario: Разъехавшиеся настройки видны

  • WHEN у ядра и сервиса заданы разные subject'ы ответа
  • THEN /healthcheck.subjects.result ядра и сервиса различаются; JWT и seed в ответе /healthcheck отсутствуют

Scenario: Ошибка конфигурации не раскрывает JWT

  • WHEN сервис не стартует из-за ошибки валидации настроек при заданных NATS_JWT и NATS_SEED
  • THEN текст ошибки и лог не содержат значений JWT и seed

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 (values_ok — is_recognized не false и confidence 100 или пуста: чистое значение — needs_review = false), needs_review строки — false, period_kind — absolute при period_date, иначе none, form_variant — по структуре кодов (точка входа пакета для пересчёта, спека arm-core), file_ordinal — индекс в documents[]; строки 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: из двух строк с одним кодом хранится первая (прежнее правило маппера 1.x), вторая не хранится, у документа MAPPING_WARNING о повторе; нарушения §7 — в document_issue от ядра, статус документа — по D-6, файл не ERROR

Scenario: Чистое значение 1.x

  • WHEN в результате 1.2 значение с confidence = 100 и is_recognized = true
  • THEN needs_review = false; значение с confidence = 60 — needs_review = true

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) — поэтому порядок «сначала ядро» обязателен