fix: правки финального ревью — README, design R-7, spec severity, docstring'и, тесты (#46)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
64dab79290
commit
e6d55a396f
@ -274,7 +274,7 @@ MinIO и NATS); те же значения подхватываются из `EN
|
||||
|---|---|---|
|
||||
| `SCHEMA_MISMATCH` | тело не JSON / не по схеме / MAJOR версии ≠ 1 / `pipeline` ≠ `image`. Конверт публикуется, только если в теле есть корректные `job_id` и `file_id`, иначе запрос пишется в лог и отбрасывается | нет |
|
||||
| `FILE_READ_ERROR` | объект S3 не прочитан; `s3.bucket` не совпадает с бакетом сервиса | да |
|
||||
| `RECOGNITION_FAILED` | у `original_name` нет допустимого расширения (файл в ИС не передаётся; `<job_id>.<ext>`, 1–16 букв/цифр ASCII); не записан файл во «Входящие» (горячая папка недоступна); «Исходящие» не читались ни разу до дедлайна; **ни одной таблицы не разобрано** (успешные файлы набора не дали ни одного документа; сообщение «ни одной таблицы не разобрано», `raw_s3_key` заполнен, причины — в `metrics.warnings`: `TABLE_PARSE_ERROR`, `TABLE_LOST`, `JSON_FAILED`) | да |
|
||||
| `RECOGNITION_FAILED` | у `original_name` нет допустимого расширения (файл в ИС не передаётся; `<job_id>.<ext>`, 1–16 букв/цифр ASCII); не записан файл во «Входящие» (горячая папка недоступна); «Исходящие» не читались ни разу до дедлайна; **ни одной таблицы не разобрано** (успешные файлы набора не дали ни одного документа; сообщение «ни одной таблицы не разобрано», `raw_s3_key` заполнен, причины — в `metrics.warnings`: `TABLE_PARSE_ERROR`, `TABLE_LOST`, `TABLE_SKIPPED`, `JSON_FAILED`) | да |
|
||||
| `TIMEOUT` | ИС не ответила до `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)`; `reply_deadline` истёк до отправки файла | да |
|
||||
| `INVALID_FORMAT` | все файлы ответа ошибочны, и у первого ошибочного файла не разобрался JSON (сырой ответ сохранён, `raw_s3_key` указан) или корень ответа не объект; склейка нарушена или пуста | да |
|
||||
| код ошибки ИС | **все** файлы ответа ошибочны (`error.code` непустой): код первого ошибочного файла по имени пробрасывается, если он входит в `ErrorCode` (например, `INVALID_DOCUMENT`); каждый ошибочный файл — предупреждение `JSON_FAILED` в `metrics.warnings` | да |
|
||||
@ -398,7 +398,7 @@ make golden-update # пересобрать эталоны нормализ
|
||||
вне числа — 0 (P5-3).
|
||||
- Успешный json ИС без таблиц — `JSON_FAILED` (`error`), рядом с нормальными json конверт будет `needs_check` (P5-7, Р-32).
|
||||
- Нарушенная структура одной таблицы ИС теряет всю таблицу (`TABLE_PARSE_ERROR`, P5-2); остальные таблицы разбираются.
|
||||
- Расшифровки без кода (OQ-36) и склейка одной формы из нескольких таблиц (OQ-77) не обрабатываются: каждая таблица —
|
||||
- Склейка одной формы из нескольких таблиц (OQ-77) не обрабатываются: каждая таблица —
|
||||
свой документ.
|
||||
|
||||
Транспорт и эмулятор:
|
||||
|
||||
@ -699,7 +699,9 @@ def build_result( # noqa: PLR0913 - идентификаторы + исход +
|
||||
|
||||
|
||||
def _limit_warnings(warnings: list[FileWarning]) -> list[FileWarning]:
|
||||
"""Не больше `MAX_WARNINGS`: все `error`, затем `info` по порядку; итоговый порядок — исходный (P5-10)."""
|
||||
"""Не больше `MAX_WARNINGS`: все `error`, затем `info` по порядку; итоговый порядок — исходный (P5-10).
|
||||
|
||||
Если `error` больше `MAX_WARNINGS`, остаются первые `MAX_WARNINGS` из `error`, а `info` отбрасываются полностью."""
|
||||
if len(warnings) <= MAX_WARNINGS:
|
||||
return warnings
|
||||
errors = [i for i, w in enumerate(warnings) if w.severity is Severity.ERROR]
|
||||
|
||||
@ -51,7 +51,8 @@ class Normalizer(Protocol):
|
||||
|
||||
Структурные ошибки входа (не объект, нет `files`) возвращаются как `status = error`, а не исключением;
|
||||
`NullNormalizer` не бросает ни на каком входе. `OcrNormalizer` изолирует только ошибки разбора таблицы (одна
|
||||
таблица — одно предупреждение `BACKEND_PARSING_ERROR`, файл и набор живут дальше); исключения вне разбора таблиц
|
||||
таблица — одно предупреждение `TABLE_PARSE_ERROR`, `BACKEND_PARSING_ERROR` — префикс `message`, файл и набор
|
||||
живут дальше); исключения вне разбора таблиц
|
||||
поднимаются сознательно (D-4): воркер превращает их в `UNKNOWN_ERROR`."""
|
||||
|
||||
def normalize(self, raw: object) -> NormalizeOutcome: ...
|
||||
|
||||
@ -91,6 +91,8 @@ def test_confidence_of(raw: object, expected: float) -> None:
|
||||
table("ББ", row(0, ("1", "a", 100.0))),
|
||||
table("ББ", row(0, (-1, "a", 100.0))),
|
||||
table("ББ", row(0, (1, "a", 100.0), (1, "b", 100.0))),
|
||||
table("ББ", row(0, (True, "a", 100.0))),
|
||||
table("ББ", {"row_index": 0, "cells": ["не объект"]}),
|
||||
],
|
||||
)
|
||||
def test_broken_structure_raises(broken: object) -> None:
|
||||
|
||||
@ -6,6 +6,7 @@ import pytest
|
||||
import src.normalizer.ocr.answer_set as answer_set_module
|
||||
from arm_statements import DocStatus, DocumentType, EnvelopeStatus, PeriodKind, Severity, default_rules
|
||||
from src.contract import ErrorCode
|
||||
from src.normalizer.files import BAD_ENTRY_MESSAGE
|
||||
from src.normalizer.ocr.answer_set import NO_TABLES_MESSAGE, normalize_answer_set
|
||||
from tests.unit.normalizer.ocr_builders import BAD, BALANCE, HEAD, INCOME, OK, merged, ocr_table, response
|
||||
|
||||
@ -157,3 +158,31 @@ def test_one_failed_one_successful_without_documents_is_recognition_failed(secon
|
||||
assert res.status is EnvelopeStatus.ERROR and res.documents == ()
|
||||
assert (res.error.code, res.error.message) == (ErrorCode.RECOGNITION_FAILED, NO_TABLES_MESSAGE)
|
||||
assert ("JSON_FAILED", "file:j.json") in [(w.code, w.locator) for w in res.file_warnings]
|
||||
|
||||
|
||||
def test_table_exception_text_truncated_to_300(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def boom(candidate: Any, rules: Any = None) -> Any:
|
||||
raise RuntimeError("я" * 500)
|
||||
|
||||
monkeypatch.setattr(answer_set_module, "normalize_candidate", boom)
|
||||
res = normalize_answer_set(merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE)))), RULES)
|
||||
[w] = res.file_warnings
|
||||
assert w.message == "BACKEND_PARSING_ERROR: RuntimeError: " + "я" * 300 # R-3: усекается текст исключения
|
||||
|
||||
|
||||
def test_bad_entry_and_parse_error_through_answer_set() -> None:
|
||||
res = normalize_answer_set({"job_id": "j", "files": ["не объект"]}, RULES)
|
||||
assert res.status is EnvelopeStatus.ERROR
|
||||
assert res.error is not None
|
||||
assert (res.error.code, res.error.message) == (ErrorCode.INVALID_FORMAT, BAD_ENTRY_MESSAGE)
|
||||
[w] = res.file_warnings
|
||||
assert (w.code, w.locator) == ("JSON_FAILED", "file:files[0]")
|
||||
|
||||
res = normalize_answer_set(
|
||||
{"job_id": "j", "files": [{"name": "j.json", "response": None, "parse_error": "Expecting value"}]}, RULES
|
||||
)
|
||||
assert res.error is not None
|
||||
assert res.error.code is ErrorCode.INVALID_FORMAT
|
||||
assert "Expecting value" in res.error.message
|
||||
[w] = res.file_warnings
|
||||
assert w.locator == "file:j.json"
|
||||
|
||||
@ -570,6 +570,24 @@ CASES: list[tuple[str, str, Callable[[dict[str, Any]], None], bool]] = [
|
||||
_drop("service.statements_version"),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"result/statement-detail-without-parent_ref",
|
||||
"result-statement-minimal.json",
|
||||
lambda p: p["documents"][0]["lines"][0].update(line_status="detail", line_code=None, parent_ref=None),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"result/statement-balance-lines-without-checks",
|
||||
"result-statement-minimal.json",
|
||||
_drop("documents.0.checks"),
|
||||
False,
|
||||
),
|
||||
(
|
||||
"result/statement-needs_review_fields-duplicate",
|
||||
"result-statement-minimal.json",
|
||||
_set("documents.0.header.needs_review_fields", ["inn", "inn"]),
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -1047,7 +1047,7 @@ class TestSlice3a:
|
||||
body = env.request()
|
||||
async with emulator(env):
|
||||
result = await env.run(body)
|
||||
assert result["status"] == "needs_check" # SUCCESS_ANSWER: confidence < 100 в шапке
|
||||
assert result["status"] == "needs_check" # SUCCESS_ANSWER: needs_check из-за CODE_COLUMN_NOT_FOUND
|
||||
key = result["result_s3"]["key"]
|
||||
stored = json.loads(env.storage.objects[(BUCKET, key)])
|
||||
assert stored["documents"][0]["document_type"] == "BALANCE"
|
||||
|
||||
@ -29,10 +29,10 @@
|
||||
`form_name` сам не ставит и пару «кандидат → документ» не хранит.
|
||||
- **P5-7.** Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` — `JSON_FAILED` (`error`):
|
||||
«ничего не выбрасывать молча» (§11, принцип 2). Цена: если ИС вернёт успешный пустой json рядом с нормальными, конверт
|
||||
будет `needs_check` (вопрос владельцу — Q-1).
|
||||
будет `needs_check`. Q-1 закрыт решением владельца Р-32 (подтверждает выбор плана).
|
||||
- **P5-8.** Все json набора ошибочны — конверт `error` с кодом первого, а `metrics.warnings` всё равно перечисляют
|
||||
`JSON_FAILED` каждого json (как строки Среза 0): иначе коды остальных json теряются. Цена: спека называет
|
||||
`JSON_FAILED` «при успешном соседе»; на `error`-конверт ядро `RECOGNIZED` не считает, вреда нет (Q-2).
|
||||
`JSON_FAILED` «при успешном соседе»; на `error`-конверт ядро `RECOGNIZED` не считает, вреда нет. Q-2 закрыт решением владельца Р-33 (подтверждает выбор плана).
|
||||
- **P5-9.** `TABLE_PARSE_ERROR.message` = `BACKEND_PARSING_ERROR: <тип>: <текст>` (текст до 300 символов) — код старого
|
||||
§11.1 п. 9 сохранён в тексте для поиска по логам и golden. Цена: нет.
|
||||
- **P5-10.** Предел `metrics.warnings` (100 структур, `message` до 1000 символов) сохранён; при переполнении остаются все
|
||||
@ -78,3 +78,7 @@
|
||||
ошибки: нет, эквивалентно.
|
||||
- **R-6 (T4).** Сравнение с эталонами в `test_golden` временно стояло под `pytest.skip` до задачи 6 (эталоны fixtures
|
||||
были старого формата); в задаче 6 skip снят. Цена ошибки: окно без golden-проверки на ветке между задачами 4 и 6.
|
||||
- **R-7.** Набор «ошибочный json + успешный json без таблиц / без документов» даёт `RECOGNITION_FAILED` / «ни одной
|
||||
таблицы не разобрано», а код первого ошибочного — только если успешных json нет (чтение Р-32 + Р-33; исправление
|
||||
T3.I-1, commit 3b9e1f6). Цена ошибки: при другом чтении код ошибки конверта в смешанном случае будет другим; ядро на
|
||||
`error`-конверт `RECOGNIZED` не считает.
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
проверкой «`error` не `null`». Конверт (включая ack `processing`) и файл результата несут `schema_version = "2.0"`, `result_schema_version = "2.0"`; `service.statements_version` (содержимое `api/arm_statements/VERSION`) SHALL быть в файле результата, в конверте `service` — `{name, version}`. Если ошибочны **все** файлы — `status = error`, `error.code` и `error.message` берутся у
|
||||
первого ошибочного файла по имени (для `parse_error` и не-объекта — `INVALID_FORMAT`); если код ИС этого файла не
|
||||
входит в перечень кодов контракта, `error.code = UNKNOWN_ERROR`, а исходный код SHALL быть указан в `error.message`.
|
||||
Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС в `statementDocument`», а статус определяется по требованию «Статус результата нормализации». `metrics.warnings` SHALL быть списком структур предупреждений (`code`, `level`, `message`, `locator`, …) по схеме v2.0 — одним и тем же в конверте и в файле результата (предел 100 структур, `message` до 1000 символов; при переполнении остаются все `error`, затем `info`). Каждый ошибочный json набора SHALL давать предупреждение `JSON_FAILED` (`error`, `locator = file:<имя json>`, `message = "<код ИС>: <сообщение ИС>"`; для `parse_error` и не-объекта — код `INVALID_FORMAT`), в том числе когда ошибочны все json; исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:<i>`, `message = "BACKEND_PARSING_ERROR: <тип>: <текст>"`). Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` SHALL давать `JSON_FAILED`. Если предупреждений нет, поле `warnings` SHALL быть опущено.
|
||||
Если есть хотя бы один успешный файл — документы собираются из успешных файлов по требованию «Нормализация ответа ИС в `statementDocument`», а статус определяется по требованию «Статус результата нормализации». `metrics.warnings` SHALL быть списком структур предупреждений (`code`, `severity`, `message`, `locator`, …) по схеме v2.0 — одним и тем же в конверте и в файле результата (предел 100 структур, `message` до 1000 символов; при переполнении остаются все `error`, затем `info`). Каждый ошибочный json набора SHALL давать предупреждение `JSON_FAILED` (`error`, `locator = file:<имя json>`, `message = "<код ИС>: <сообщение ИС>"`; для `parse_error` и не-объекта — код `INVALID_FORMAT`), в том числе когда ошибочны все json; исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:<i>`, `message = "BACKEND_PARSING_ERROR: <тип>: <текст>"`). Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` SHALL давать `JSON_FAILED`. Если предупреждений нет, поле `warnings` SHALL быть опущено.
|
||||
|
||||
#### Scenario: Успешный ответ ИС в Срезе 0
|
||||
- **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (по образцу `aep_6.json`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user