diff --git a/openspec/changes/slice-3a-ocr-normalizer/design.md b/openspec/changes/slice-3a-ocr-normalizer/design.md index d501732..a7e04c5 100644 --- a/openspec/changes/slice-3a-ocr-normalizer/design.md +++ b/openspec/changes/slice-3a-ocr-normalizer/design.md @@ -151,7 +151,7 @@ docs: bounded-PR (`03-architecture.md`, OQ-137, §11.1 п. 6 и 9, новые OQ | 9 | 2026-10-06 | Эмулятор пересчитывает индекс хешей при изменении списка файлов каталога, а не на каждый файл | дёшево и достаточно для dev | изменённый, но не переименованный PDF не переиндексируется | | 10 | 2026-10-06 | `label` колонки — сырой текст шапки с `\n` → пробел; `period_date` из `parse_date` | ядро показывает текст как есть | — | | 12 | 2026-10-06 | `parse_amount` переносится дословно: скобка, распознанная как `}` (`(7744536}`), минус не даёт — сумма положительная с `is_recognized = false` | проверенный код; расширение множества скобок — отдельное правило §11.1 после проверки на стенде | знак потерян, аналитик видит `raw/` и подсветку | -| 13 | 2026-10-06 | В переносе `parse_amount` признак `verified` считается до постановки знака минус (в источнике — после, и любая сумма в скобках получала `verified = False`) | иначе каждая отрицательная сумма формы давала `is_recognized = false` и `needs_check` без причины; единственное отличие от источника, зафиксировано тестом `(6032409) → -6032409, verified` | расхождение с поведением старого ядра на стенде: там такие суммы подсвечивались, у нас — нет | +| 13 | 2026-10-06 | В переносе `parse_amount` признак `verified` считается до постановки знака минус (в источнике — после, и любая сумма в скобках получала `verified = False`) | иначе каждая отрицательная сумма формы давала `is_recognized = false` и `needs_check` без причины; единственное отличие от источника, зафиксировано тестом `(6032409) → -6032409, verified` | расхождение с поведением старого ядра на стенде: там такие суммы подсвечивались, у нас — нет. Побочный эффект: скобки без содержимого (`()`, `( )`, `(.)`) дают `(None, verified = true)` (в источнике false) | | 14 | 2026-10-06 | `CONTRACT_VERSION` scan-gateway остаётся `1.0`, хотя копия схемы — v1.2 | документы 3a не используют полей 1.1–1.2 (`DISCLOSURE`, `checks`, границы периода ОСВ); потребитель принимает любой MINOR; меньше изменений в тестах конверта | если ядро начнёт различать MINOR производителя — одна константа и проверка в `worker_helpers` | | 15 | 2026-10-06 | Повтор кода строки в таблице OCR: остаётся первая строка, предупреждение, `needs_check` | DBML: уникальный индекс `financial_statement_line (meta_id, line_code)` — без этого маппинг 3b падает; Excel-путь 2b делает так же (§12 п. 5); старое ядро дубли не убирало | вторая строка с тем же кодом теряется в документе (остаётся в `raw/`) | | 16 | 2026-10-06 | `string_value` заполняется только при `amount = null` и непустом не-прочерке, текстом как распознан (`\n`/`\t` → пробел); у разобранных сумм — `null` | DBML `financial_statement_value.string_value`: «как распознано, если не число»; так же в 2b; первичная редакция спеки и старое ядро писали очищенный текст всегда | аналитик не видит сырой текст суммы на экране (он в `raw/`) | @@ -168,4 +168,9 @@ docs: bounded-PR (`03-architecture.md`, OQ-137, §11.1 п. 6 и 9, новые OQ | 26 | 2026-10-07 | R-8. `.part`, ставший целью успешной публикации (`os.link` → назначение), снимается самой записью независимо от `SCAN_CLEANUP`; ошибка его снятия подавляется. Флаг управляет остатками: осиротевшие `.part`, `.part` при уже существующем назначении, ответы, входящий файл, `.done` | иначе при `SCAN_CLEANUP=false` на каждое задание оставалась бы жёсткая ссылка; ошибка удаления на шаре с неподтверждёнными правами не должна превращать доставленный файл в `RECOGNITION_FAILED` | на ФС без права удаления в `Tmp` у каждого задания останется `.part`-ссылка (место не занимает) | | 27 | 2026-10-07 | R-9. При `SCAN_CLEANUP=false` сервис ничего не удаляет и не логирует «осиротевший `.part`»; сценарий «Очистка выключена» приведён к «все файлы остаются на месте» | в слое горячих папок нет логгера, а поиск сирот потребовал бы обхода `Tmp` на каждое задание; ценность — только диагностическая | сироты в `Tmp` при выключенной очистке накапливаются без заметки в логе; чистятся вручную или включением флага | | 28 | 2026-10-07 | R-10. Ошибочный файл набора при хотя бы одном успешном понижает статус до `needs_check` (`done` невозможен) | спека «Ошибка одного из документов»: потерянный документ (например, ОФР) не должен остаться незамеченным под `done`; строка об ошибке уже в `metrics.warnings` | чаще `needs_check`; в D-6 («только документы») — уточнение | -| 29 | 2026-10-07 | R-11. На таблицу — одно предупреждение `пропущено строк с нечисловым кодом: N` для строк с непустым нечисловым кодом, отброшенных `classify_row`; строки с пустым кодом — без предупреждения | иначе строки данных с мусорным кодом (aep_3: «Выручка» `211(1 |`, `БАЛАНС`) терялись молча; поведение источника (отбрасывание) не меняется | таблица с отброшенными строками всегда `needs_check`; восстановление таких строк — OQ-149 | +| 29 | 2026-10-07 | R-11 (уточнён финальным ревью I-1). На таблицу — предупреждение `строк с нечисловым кодом пропущено: N` (непустой нечисловой код, строка отброшена `classify_row`) и предупреждение `строк без кода с суммами пропущено: N` (пустой код, в колонках значений есть хотя бы одна разобранная сумма — как в Excel-пути 2b, OQ-141); строки с пустым кодом без сумм — без предупреждения | в aep_3 «Выручка» и «БАЛАНС» имеют ПУСТОЙ код (мусор `211(1 \|` — в колонке значения), без предупреждения они терялись молча; поведение источника (отбрасывание) не меняется | таблица с отброшенными строками всегда `needs_check`; восстановление строк — OQ-149 | +| 30 | 2026-10-07 | R-12 (финальное ревью C-1). Сумма с более чем двумя знаками после запятой (`1,234`, `0,125`) → `amount = null`, `string_value` — текст ячейки, `is_recognized = false`; `confidence` ячейки и полей шапки приводится к [0, 100], NaN → 0 | `Money` контракта — `^-?\d+(\.\d{1,2})?$`; иначе один ячейковый артефакт OCR (пробел тысяч как запятая) валил всю таблицу (`BACKEND_PARSING_ERROR`) и документ | значение остаётся в `string_value` и `raw/`, аналитик вводит сумму вручную | +| 31 | 2026-10-07 | R-13 (финальное ревью I-3). Отклонение от дословного переноса: тексты дат длиннее 200 символов не разбираются (`None`), `fix_lookalikes` к ним не применяется | регулярки квартала/полугодия — кубическое backtracking (`"1" + " "*4000 + "b"` — 76 с, GIL держит весь процесс); дата — не более десятков символов | дата в аномально длинном тексте не найдена (не дата и так) | +| 32 | 2026-10-07 | R-14 (финальное ревью). Локальная запись: `.part` создаётся заново (удаление старого + `O_EXCL`), fallback `os.replace` — только вокруг `os.link`, `EOPNOTSUPP` добавлен к списку | `.part`, оставшийся жёсткой ссылкой на уже опубликованный файл (подавленное удаление, OQ-144), перезаписывался бы на месте и портил файл, который читает ИС; EPERM на записи маскировался `FileNotFoundError` | — | +| 33 | 2026-10-07 | R-15 (финальное ревью, T1.3). Загрузчик yaml отклоняет повторяющиеся ключи (`SafeLoader` с проверкой) | спека: «Коды справочника SHALL быть без дублей»; `safe_load` молча перезаписывает дубли, тест `len(set)` тавтологичен; владелец заполняет `fsbu4` вручную | битый справочник не стартует — это и нужно | +| 34 | 2026-10-07 | R-16 (финальное ревью I-2). №15 (повтор кода → остаётся первая строка) НЕ меняется: в aep_6 ОФР код 2410 повторён, и оставленная первая строка пуста, а отброшенная (`(649)`) — со значением; выбор «первая с суммой» отложен владельцу как OQ | паритет с Excel-путём 2b (§12 п. 5); эвристика «первая с суммой» для aep_3 даёт иное, чем для aep_6 — нужна проверка на стенде | сумма отброшенной строки остаётся только в `raw/`; предупреждение «код N повторяется» сигнализирует аналитику | diff --git a/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md b/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md index 28b8b01..4419026 100644 --- a/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md +++ b/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md @@ -173,19 +173,21 @@ - `values` — по одному на колонку документа (по позиции; если раскладка строки после повторной шапки даёт иное число колонок — недостающие значения как «ячейки нет», лишние отбрасываются, одно предупреждение на таблицу «повторная шапка изменила число колонок значений», решение №21; таблица без колонок значений — предупреждение «нет колонок значений», №25): `amount` — `parse_amount` (запятая → точка, скобки → минус, пробелы и прочие символы - отбрасываются, несколько точек — разделители тысяч; прочерк/пусто → `null`); `string_value` — текст ячейки как + отбрасываются, несколько точек — разделители тысяч; прочерк/пусто → `null`; сумма с более чем двумя знаками после запятой (`1,234`) не помещается в `Money` контракта — `amount = null`, + `string_value` — текст ячейки, `is_recognized = false`, решение №30); `string_value` — текст ячейки как распознан (`\n`/`\t` → пробел, края обрезаны) **только если `amount = null`** и текст не прочерк (DBML: «как распознано, - если не число»), иначе `null`; `confidence` — ячейки; `is_recognized = confidence == 100 и разбор не отбрасывал символов`; + если не число»), иначе `null`; `confidence` — ячейки (значения вне 0–100 и NaN приводятся к диапазону, №30); `is_recognized = confidence == 100 и разбор не отбрасывал символов`; ячейки под колонкой нет → `amount = null`, `string_value = null`, `confidence = 0`, `is_recognized = false`. - `header` — `form_name` из `label` таблицы; `organization_name` и `organization_name_confidence` из `data_ocr.organization` (`\n` → пробел); `inn` — только цифры из `data_ocr.inn.form_name` (`null`, если цифр нет), `inn_confidence = confidence`, если сырой текст состоял только из цифр, иначе `min(confidence, 99)`; `units` — `clean_value(units.form_name)`; - `report_date_text` — сырой текст `date.form_name`, `report_date` — `parse_date` после `fix_lookalikes` - (`null`, если текст не дата); `okud`, `okpo`, `okopf`, `okfs`, `address` — `null` (ИС их не отдаёт). + `report_date_text` — текст `date.form_name` с обрезанными краями, `report_date` — `parse_date` после `fix_lookalikes` + (`null`, если текст не дата); `okud`, `okpo`, `okopf`, `okfs`, `address` — не передаются (в проводе отсутствуют, у потребителя `null`; ИС их не отдаёт). - `checks` MUST NOT присутствовать. -Строка с непустым нечисловым кодом (`1150 :`, `211(1 |`) пропускается, на таблицу выдаётся одно предупреждение -`пропущено строк с нечисловым кодом: ` (решение №29); строки с пустым кодом (заголовки групп, «в том числе») — без -предупреждения. +Строка с непустым нечисловым кодом (`1150 :`, `изо :`) пропускается, на таблицу выдаётся одно предупреждение +`строк с нечисловым кодом пропущено: `; строка с пустым кодом и хотя бы одной разобранной суммой в колонках значений +пропускается с одним предупреждением на таблицу `строк без кода с суммами пропущено: ` (как в Excel-пути, OQ-141); +строки с пустым кодом без сумм (заголовки групп, «в том числе») — без предупреждения (решение №29). Двойники букв и цифр (`fix_lookalikes`) SHALL применяться к тексту дат (дата отчёта и подписи колонок) только вплотную к цифрам; `|` SHALL считаться разделителем клеток, если первая строка значения состоит из цифр, пробелов и знаков пунктуации. Коды очищаются `clean_value`, суммы — `parse_amount`, ИНН — выделением цифр (как в проверенном на стенде