Срез 3a: горячие папки как у armka_file_worker, нормализатор OCR → statementDocument v1.2 #8

Merged
aep merged 44 commits from feat/6-slice-3a-ocr-normalizer into main 2026-10-07 04:05:00 +03:00
Owner

Closes #6

OpenSpec change: openspec/changes/slice-3a-ocr-normalizer/ (proposal, design D-1…D-9 + таблица решений №1–35, спеки image-recognition-gateway и scan-emulator). План: docs/superpowers/plans/2026-10-06-slice-3a-ocr-normalizer.md.

Связанные PR: fixtures RSHB/arm-ka-fixtures#13 (эталоны ocr/expected_normalize/), docs RSHB/arm-ka-docs#47 (§11.1, OQ-144…152, compose, qa-guide). Порядок мержа: fixtures #13 и docs #47 — раньше или вместе с этим PR (без fixtures-PR test_golden в основном checkout красный). docs #27/#44, scan-gateway #5 (копия схемы v1.2) и arm-core #27 уже слиты.

Что сделано

  • Нормализатор OCR (api/src/normalizer/ocr/*, rules.py, api/rules/*.yaml): OcrNormalizer строит statementDocument v1.2 по таблицам data_ocr (тип по label/кодам, раскладка колонок по шапке или по данным, значения с is_recognized, редакции форм 66n/fsbu4, статусы done/needs_check/error RECOGNITION_FAILED, предупреждения). Функции очистки старого ядра перенесены дословно (§11.1) с зафиксированными отличиями (№13, №15, №16, №22, №30, №31).
  • Горячие папки как у armka_file_worker: Input/<job_id>.<ext>, .part в Tmp, запись без замены (local: os.link; smb: rename), cleanup под SCAN_CLEANUP (по умолчанию выключен).
  • Воркер: очистка после сохранения raw/, RECOGNITION_FAILED без расширения, OcrNormalizer(load_rules(SCAN_RULES_DIR)) в lifespan.
  • Эмулятор: пара PDF+JSON по sha256 содержимого, SCAN_EMULATOR_DEFAULT, индекс не пересчитывается на каждом проходе.
  • Golden на реальных ответах ИС (make golden-update, эталоны в fixtures), интеграционный roundtrip (aep_3, aep_6, ok_doc, cleanup) с валидацией по JSON-схеме, README/CLAUDE.md.
  • Проверки: make check-full (lint, 1019 unit, 11 integration, docker build) — зелёный; openspec validate slice-3a-ocr-normalizer --strict — valid.
  • CONTRACT_VERSION остаётся 1.0 (решение №14).

Процесс

SDD: 17 задач, на каждую — исполнитель + ревью задачи (spec + quality) + при необходимости раунды исправлений; затем финальное ревью всей ветки (opus), единая правка и повторное ревью. Все решения без обсуждения — в design.md, раздел «Decisions made without discussion» (№17–35).

Rulings I made

(решение — маркер/номер в design.md; цена ошибки — в design.md)

  • os.link+unlink вместо rename для записи без замены, fallback os.replace — №17.
  • R-1 (№18): тест плана value_idxs == (2, 4) вместо ошибочного 5 in value_idxs; алгоритм не менялся.
  • R-2 (№19): line_code_confidence капится 99 для кода вне выбранной редакции (спека, не код плана).
  • №20 (O-3): прочерк «—»/«–» при confidence 100 даёт is_recognized=false (дословный parse_amount).
  • R-3 (№21): values по позиции колонки; расхождение числа колонок после повторной шапки — одно предупреждение.
  • R-4 (№22): лу��алайки только для дат (§11.1 п. 1); спека поправлена.
  • R-5 (№23): для пустой редакции нет шума «код N из редакции X».
  • №24: header.form_name не передаётся при пустом label (контракт запрещает null).
  • R-6 (№25): таблица без колонок значений — предупреждение, needs_check.
  • R-8 (№26): .part, ставший целью публикации, снимается самой записью, независимо от SCAN_CLEANUP.
  • R-9 (№27): при SCAN_CLEANUP=false сироты .part не логируются.
  • R-10 (№28): ошибочный файл-сосед при ≥1 успешном — статус не done.
  • R-11 (№29): предупреждения о пропущенных строках (нечисловой код / пустой код с суммами).
  • R-12 (№30): сумма с >2 знаками → amount=null, string_value, is_recognized=false; confidence в [0,100], NaN → 0.
  • R-13 (№31): тексты дат >200 символов не разбираются (защита от катастрофического backtracking).
  • R-14/R-17 (№32/№35): .part создаётся заново (O_EXCL); при существующем назначении put ничего не пишет.
  • R-15 (№33): yaml-справочники отклоняют повторяющиеся ключи.
  • R-16 (№34): №15 (повтор кода → первая строка) не меняется; вопрос «первая с суммой» — владельцу (OQ-152).
  • Pre-flight: порядок/хелперы плана сверены, конфликты плана со спекой разрешены в пользу спеки (R-1, R-2, R-3).

Находки (маркер — суть — статус)

Критичные/важные:

Маркер Суть Статус
C-1 сумма с >2 знаками (1,234) валила всю таблицу; NaN/вне диапазона confidence fixed c1288d0
I-1 пропуск строк «Выручка»/«БАЛАНС» (пустой код) без предупреждения fixed c1288d0
I-2 повтор кода 2410 в aep_6 ОФР теряет сумму (649) deferred (№34, OQ-152)
I-3 кубический backtracking в датах, O(n²) в fix_lookalikes fixed c1288d0
I-4 golden после правок fixed (fixtures 5dcfaf8)
T1.1, T1.2 опечатка кода 1220 и тест данных 66n fixed ecdf9a4
T2.1 потеряна замена NBSP в fix_lookalikes fixed 746d993
T8.1 значения не по числу колонок после повторной шапки fixed 8ebf572
T8.2 лукалайки для кодов/сумм: спека ≠ §11.1 fixed (спека 37b3604)
T10.1 молчаливая потеря строк с мусорным кодом fixed c1288d0
T11.1, T11.2, T11.3 пустой тест; незащищённый unlink .part; D-2 vs снятие .part fixed 5c2d89b / спека
T14.1, T14.2 индекс эмулятора пересчитывался; тесты не ловили fixed 31e0595…13bd288
T16.1–T16.3 docs: таблица OQ, устаревшие описания, locator fixed (docs a2811eb)

Minor (fixed): T1.3 (c1288d0), T2.4, T4.2, T4.4 (c1288d0), T5.3 (c1288d0), T7.1/T8.9 (спека), T8.3–T8.7 (8ebf572/37b3604), T8.8 (№24), T9.1 (c1288d0), T9.3 (→№28), T9.4 (c1288d0), T10.2, T11.4 (5c2d89b), T11.6, T12.1–T12.3, T12.5 (a65ff8c), T13.1 (a65ff8c), T13.3, T13.4 (c1288d0), T13.7 (№27), T14.3–T14.5, T15.1–T15.5, T15.8 (576ce2c/140ec5b), M-1 (b956d57, cb8c71f), M-2 (c1288d0), M-5 README (fixtures 5dcfaf8), RR-2 (1c39ca1).

Rejected: T3.2 (косметика ruff-format), T4.5, T5.4, T6.4 (з��щитный код), T7.5, T10.8 (нет влияния) — шум отчётов/нет влияния на код.

Deferred (пойдут в follow-up issue при закрытии): I-2; T1.4–T1.8, T2.2, T2.3, T3.1, T3.3–T3.5, T4.1, T4.3, T5.1, T5.2, T5.5, T6.1–T6.3, T7.2–T7.4, T8.10, T8.11, T9.2, T9.5, T10.3–T10.7, T11.5, T12.4, T12.6, T12.7, T13.2, T13.5, T13.6, T14.6 (в т.ч. OQ-151: хеш-совпадение с PDF без JSON-набора не уходит на SCAN_EMULATOR_DEFAULT), T15.6, T15.7 (источник «Приложения 3–4 ФСБУ 4/2023» не проверен), M-3, M-4, M-5 (naming aep_8_form4), RR-1 (yaml merge keys <<: ломают загрузку правил), RR-3 (symlink-.part в Tmp обходит nlink-проверку; нужен посторонний актор), RR-4 (тесты caller-side лимита дат не различающие), RR-5 (режим .part 0o644 вместо 0o666&umask), docs M1–M4.

Наблюдения (O-n): O-1, O-2, O-6, O-7, O-8 — неточные отчёты исполнителей-haiku (ревью ловило); O-3 — длинное тире (№20); O-4 — ошибочный файл-сосед (решено №28); O-5 — в части коммитов trailer Co-Authored-By: Claude Sonnet 5.5 (фактическая модель) вместо Opus 5.5 из задания, история не переписывалась; O-8 — исполнитель закоммитил игнорируемый файл отчёта, коммит снят до push.

Что осталось владельцу

  • Смержить fixtures #13 и docs #47, затем этот PR (порядок выше).
  • Заполнить редакцию fsbu4 в api/rules/statement_lines.yaml (Приложения ФСБУ 4/2023); процедура и список тестов, фиксирующих пустую редакцию, — в README.
  • Положить в fixtures пары PDF+JSON для остальных сканов (aep_4/7/9/30–34 без JSON-набора, эмулятор отвечает INVALID_DOCUMENT).
  • Решить: OQ-144 (права на удаление на SMB-шаре; пока SCAN_CLEANUP=false), OQ-149 (восстанавливать ли строки с пустым/мусорным кодом), OQ-152 (повтор кода — «первая с суммой»), OQ-148 (длинное тире).
  • «закрывай» после мержа: архив change, follow-up issue из deferred.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Closes #6 OpenSpec change: `openspec/changes/slice-3a-ocr-normalizer/` (proposal, design D-1…D-9 + таблица решений №1–35, спеки `image-recognition-gateway` и `scan-emulator`). План: `docs/superpowers/plans/2026-10-06-slice-3a-ocr-normalizer.md`. Связанные PR: fixtures RSHB/arm-ka-fixtures#13 (эталоны `ocr/expected_normalize/`), docs RSHB/arm-ka-docs#47 (§11.1, OQ-144…152, compose, qa-guide). **Порядок мержа: fixtures #13 и docs #47 — раньше или вместе с этим PR** (без fixtures-PR `test_golden` в основном checkout красный). docs #27/#44, scan-gateway #5 (копия схемы v1.2) и arm-core #27 уже слиты. ## Что сделано - **Нормализатор OCR** (`api/src/normalizer/ocr/*`, `rules.py`, `api/rules/*.yaml`): `OcrNormalizer` строит `statementDocument` v1.2 по таблицам `data_ocr` (тип по label/кодам, раскладка колонок по шапке или по данным, значения с `is_recognized`, редакции форм `66n`/`fsbu4`, статусы `done`/`needs_check`/`error RECOGNITION_FAILED`, предупреждения). Функции очистки старого ядра перенесены дословно (§11.1) с зафиксированными отличиями (№13, №15, №16, №22, №30, №31). - **Горячие папки как у armka_file_worker**: `Input/<job_id>.<ext>`, `.part` в `Tmp`, запись без замены (local: `os.link`; smb: `rename`), `cleanup` под `SCAN_CLEANUP` (по умолчанию выключен). - **Воркер**: очистка после сохранения `raw/`, `RECOGNITION_FAILED` без расширения, `OcrNormalizer(load_rules(SCAN_RULES_DIR))` в lifespan. - **Эмулятор**: пара PDF+JSON по sha256 содержимого, `SCAN_EMULATOR_DEFAULT`, индекс не пересчитывается на каждом проходе. - Golden на реальных ответах ИС (`make golden-update`, эталоны в fixtures), интеграционный roundtrip (aep_3, aep_6, ok_doc, cleanup) с валидацией по JSON-схеме, README/CLAUDE.md. - Проверки: `make check-full` (lint, 1019 unit, 11 integration, docker build) — зелёный; `openspec validate slice-3a-ocr-normalizer --strict` — valid. - `CONTRACT_VERSION` остаётся `1.0` (решение №14). ## Процесс SDD: 17 задач, на каждую — исполнитель + ревью задачи (spec + quality) + при необходимости раунды исправлений; затем финальное ревью всей ветки (opus), единая правка и повторное ревью. Все решения без обсуждения — в `design.md`, раздел «Decisions made without discussion» (№17–35). ## Rulings I made (решение — маркер/номер в design.md; цена ошибки — в design.md) - `os.link`+unlink вместо rename для записи без замены, fallback `os.replace` — №17. - R-1 (№18): тест плана `value_idxs == (2, 4)` вместо ошибочного `5 in value_idxs`; алгоритм не менялся. - R-2 (№19): `line_code_confidence` капится 99 для кода вне выбранной редакции (спека, не код плана). - №20 (O-3): прочерк «—»/«–» при confidence 100 даёт `is_recognized=false` (дословный `parse_amount`). - R-3 (№21): `values` по позиции колонки; расхождение числа колонок после повторной шапки — одно предупреждение. - R-4 (№22): лу��алайки только для дат (§11.1 п. 1); спека поправлена. - R-5 (№23): для пустой редакции нет шума «код N из редакции X». - №24: `header.form_name` не передаётся при пустом label (контракт запрещает null). - R-6 (№25): таблица без колонок значений — предупреждение, `needs_check`. - R-8 (№26): `.part`, ставший целью публикации, снимается самой записью, независимо от `SCAN_CLEANUP`. - R-9 (№27): при `SCAN_CLEANUP=false` сироты `.part` не логируются. - R-10 (№28): ошибочный файл-сосед при ≥1 успешном — статус не `done`. - R-11 (№29): предупреждения о пропущенных строках (нечисловой код / пустой код с суммами). - R-12 (№30): сумма с >2 знаками → `amount=null`, `string_value`, `is_recognized=false`; confidence в [0,100], NaN → 0. - R-13 (№31): тексты дат >200 символов не разбираются (защита от катастрофического backtracking). - R-14/R-17 (№32/№35): `.part` создаётся заново (`O_EXCL`); при существующем назначении `put` ничего не пишет. - R-15 (№33): yaml-справочники отклоняют повторяющиеся ключи. - R-16 (№34): №15 (повтор кода → первая строка) не меняется; вопрос «первая с суммой» — владельцу (OQ-152). - Pre-flight: порядок/хелперы плана сверены, конфликты плана со спекой разрешены в пользу спеки (R-1, R-2, R-3). ## Находки (маркер — суть — статус) Критичные/важные: | Маркер | Суть | Статус | |---|---|---| | C-1 | сумма с >2 знаками (`1,234`) валила всю таблицу; NaN/вне диапазона confidence | fixed c1288d0 | | I-1 | пропуск строк «Выручка»/«БАЛАНС» (пустой код) без предупреждения | fixed c1288d0 | | I-2 | повтор кода 2410 в aep_6 ОФР теряет сумму `(649)` | deferred (№34, OQ-152) | | I-3 | кубический backtracking в датах, O(n²) в `fix_lookalikes` | fixed c1288d0 | | I-4 | golden после правок | fixed (fixtures 5dcfaf8) | | T1.1, T1.2 | опечатка кода 1220 и тест данных 66n | fixed ecdf9a4 | | T2.1 | потеряна замена NBSP в `fix_lookalikes` | fixed 746d993 | | T8.1 | значения не по числу колонок после повторной шапки | fixed 8ebf572 | | T8.2 | лукалайки для кодов/сумм: спека ≠ §11.1 | fixed (спека 37b3604) | | T10.1 | молчаливая потеря строк с мусорным кодом | fixed c1288d0 | | T11.1, T11.2, T11.3 | пустой тест; незащищённый unlink `.part`; D-2 vs снятие `.part` | fixed 5c2d89b / спека | | T14.1, T14.2 | индекс эмулятора пересчитывался; тесты не ловили | fixed 31e0595…13bd288 | | T16.1–T16.3 | docs: таблица OQ, устаревшие описания, locator | fixed (docs a2811eb) | Minor (fixed): T1.3 (c1288d0), T2.4, T4.2, T4.4 (c1288d0), T5.3 (c1288d0), T7.1/T8.9 (спека), T8.3–T8.7 (8ebf572/37b3604), T8.8 (№24), T9.1 (c1288d0), T9.3 (→№28), T9.4 (c1288d0), T10.2, T11.4 (5c2d89b), T11.6, T12.1–T12.3, T12.5 (a65ff8c), T13.1 (a65ff8c), T13.3, T13.4 (c1288d0), T13.7 (№27), T14.3–T14.5, T15.1–T15.5, T15.8 (576ce2c/140ec5b), M-1 (b956d57, cb8c71f), M-2 (c1288d0), M-5 README (fixtures 5dcfaf8), RR-2 (1c39ca1). Rejected: T3.2 (косметика ruff-format), T4.5, T5.4, T6.4 (з��щитный код), T7.5, T10.8 (нет влияния) — шум отчётов/нет влияния на код. Deferred (пойдут в follow-up issue при закрытии): I-2; T1.4–T1.8, T2.2, T2.3, T3.1, T3.3–T3.5, T4.1, T4.3, T5.1, T5.2, T5.5, T6.1–T6.3, T7.2–T7.4, T8.10, T8.11, T9.2, T9.5, T10.3–T10.7, T11.5, T12.4, T12.6, T12.7, T13.2, T13.5, T13.6, T14.6 (в т.ч. OQ-151: хеш-совпадение с PDF без JSON-набора не уходит на `SCAN_EMULATOR_DEFAULT`), T15.6, T15.7 (источник «Приложения 3–4 ФСБУ 4/2023» не проверен), M-3, M-4, M-5 (naming `aep_8_form4`), RR-1 (yaml merge keys `<<:` ломают загрузку правил), RR-3 (symlink-`.part` в `Tmp` обходит nlink-проверку; нужен посторонний актор), RR-4 (тесты caller-side лимита дат не различающие), RR-5 (режим `.part` 0o644 вместо `0o666&umask`), docs M1–M4. Наблюдения (O-n): O-1, O-2, O-6, O-7, O-8 — неточные отчёты исполнителей-haiku (ревью ловило); O-3 — длинное тире (№20); O-4 — ошибочный файл-сосед (решено №28); O-5 — в части коммитов trailer `Co-Authored-By: Claude Sonnet 5.5` (фактическая модель) вместо `Opus 5.5` из задания, история не переписывалась; O-8 — исполнитель закоммитил игнорируемый файл отчёта, коммит снят до push. ## Что осталось владельцу - Смержить fixtures #13 и docs #47, затем этот PR (порядок выше). - Заполнить редакцию `fsbu4` в `api/rules/statement_lines.yaml` (Приложения ФСБУ 4/2023); процедура и список тестов, фиксирующих пустую редакцию, — в README. - Положить в fixtures пары PDF+JSON для остальных сканов (`aep_4/7/9/30–34` без JSON-набора, эмулятор отвечает `INVALID_DOCUMENT`). - Решить: OQ-144 (права на удаление на SMB-шаре; пока `SCAN_CLEANUP=false`), OQ-149 (восстанавливать ли строки с пустым/мусорным кодом), OQ-152 (повтор кода — «первая с суммой»), OQ-148 (длинное тире). - «закрывай» после мержа: архив change, follow-up issue из deferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 44 commits 2026-10-07 03:07:10 +03:00
proposal, design (D-1…D-9, решения без обсуждения №1–12), дельты спек image-recognition-gateway
(папки, очистка под флагом, нормализация, справочники с редакциями, статусы) и scan-emulator (пара по sha256,
набор по умолчанию). tasks.md заполняется планом.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Справочники нормализатора OCR:
- api/rules/statement_lines.yaml: редакции форм ББ/ОФР (66n до 2024-12-31, fsbu4 с 2025-01-01)
- api/rules/ocr_lookalikes.yaml: замены букв на цифры (из date_utils._LOOKALIKES)
- api/rules/statement_labels.yaml: подстроки label → тип документа (BALANCE/INCOME)

Модуль rules.py:
- Edition: редакция с интервалом дат и таблицей кодов → наименований
- OcrRules: все справочники + методы edition_for(), edition_by_codes(), known_codes(), editions_with()
- load_rules(): загрузчик с валидацией (коды 4–5 цифр, двойники 1→1, файлы обязательны)

Настройки:
- SCAN_CLEANUP: удаление файлов из горячей папки (по умолчанию false)
- SCAN_RULES_DIR: путь к справочникам (по умолчанию 'rules')

TDD: 10 тестов (TestRealRules, TestEditionChoice, TestValidation, test_settings_defaults)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
I-1: Исправлена опечатка в api/rules/statement_lines.yaml код 1220:
- было: 'ценностями' (неправильный падеж)
- стало: 'ценностям' (правильный падеж, соответствует брифу)

I-2: Добавлен тест test_66n_edition_data_integrity() в TestRealRules:
- проверяет точное количество кодов в редакции 66n (63)
- валидирует специфические коды из брифа: 1220 (опечатка), 1105, 2411, 2412
- тест фиксирует данные редакции 66n, предотвращает проскальзывание опечаток

Результат: 760 тестов пройдено (749 + 11 новых), lint чист.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Исправлена строка 26 в fix_lookalikes: текст.replace("\xa0", " ") для
корректной замены неразрывного пробела (U+00A0) на обычный пробел (U+0020),
как в источнике date_utils._normalize.

Добавлен тест test_nbsp_replacement для проверки замены NBSP с таблицей
двойников и без неё.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Портирован модуль date_utils.py из DKB_ARMKA с перечисленными в §11.1 правками:
- удалены _LOOKALIKES, _LOOKALIKE_CHARS, _NEAR_DIGIT, _normalize (перенесены в text.fix_lookalikes)
- переименована _parse_date в parse_date с новой сигнатурой (value: object -> date | None)
- обновлены аннотации типов: Optional[...] -> ... | None
- добавлена аннотация для _PATTERNS: list[tuple[re.Pattern[str], Callable[[re.Match[str]], date | None]]]
- добавлены noqa/type: ignore комментарии для прохода линтеров

Тесты: 35 тестов пройдены (парсинг дат в текстовых и числовых формах, отклонение названий организаций).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Переносим функции парсинга сумм и проверки пустоты из старого ядра:
замена запятой, удаление пробелов, извлечение круглых скобок (отрицательное),
множественные точки как разделители тысяч. Отличие: verified вычисляется
ДО постановки минуса (решение №13 design.md), чтобы не отмечать суммы
в скобках как неверифицированные.
Перенос `_build_layout`, `_infer_layout`, `_cells_by_index`, `_cell_value` из старого ядра.
- Cell dataclass (frozen): значение и уверенность из ячейки OCR
- build_layout: раскладка из строки с ячейкой «Код…» (заголовок)
- infer_layout: раскладка из строк данных по кодам (когда заголовка нет)
- Справочник кодов приходит параметром, ячейки — Cell вместо словарей

R-1 (design #18): тест test_aep3_like_rows_without_header исправлен:
- столбец 5 содержит только мусор (пусто, ':', '+ :') → не входит в value_idxs
- столбец 4 содержит суммы ('3', '5 678') не реже мусора ('--') → входит
- замена assertion на layout.value_idxs == (2, 4) вместо ошибочного условия

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Перенос _classify_row, _parse_table_rows, _synthetic_header_cells, _resolve_table_type
из reference-old-arm с адаптацией к новой архитектуре:
- Cell dataclass вместо dict
- ParsedTable вместо list возврата
- Fallback для row_index если не int
- Безопасная обработка пустых кодов в table_type_by_codes

14 тестов пройдено (2 на реальных данных fixtures).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- inbox_name: возвращает <job_id>.<ext> (суффикс в нижнем регистре, 1–16 символов [0-9a-z])
- put: атомарная запись через os.link (без замены), запасной путь os.replace при EPERM/EXDEV/ENOTSUP
- cleanup: удаляет ответ, входящий файл, временные файлы при флаге cleanup_enabled
- LocalHotFolder: параметр cleanup: bool = False, атрибут cleanup_enabled
- SmbHotFolder: параметр cleanup: bool = False (заглушка, логика в Task 12)
- create_hotfolder: передаёт cleanup=settings.SCAN_CLEANUP обоим конструкторам
- SpyHotFolder: метод cleanup для шпионажа за очисткой
- Удалены: MAX_PART_NAME_BYTES, _fit_name, _MAX_EXTENSION_CHARS, _utf8_len (имя всегда короткое)
- Тесты: TestInboxName с новым форматом, TestPutWithoutReplace (без замены),
  TestCleanup (включена/выключена), cleanup_enabled в фабрике
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T11.1: добавлен test_keeps_part_file_on_failure_when_cleanup_disabled (cleanup=False);
  переименован test_cleanup_failure_does_not_mask_original_error (cleanup=True).
- T11.2 (R-8): завершающий part.unlink после успешного link обёрнут в contextlib.suppress(OSError);
  файл уже доставлен, ошибка удаления .part её не отменяет.
  Добавлен test_successful_put_despite_final_unlink_failure: успешный link,
  затем PermissionError в unlink → put НЕ падает, целевой файл на месте.
- T11.4: cleanup вызывает validate_name(name) перед удалениями (защита от выхода за пределы).
  Добавлен test_rejects_unsafe_names в TestCleanup.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- Добавлена FixtureIndex с индексированием по sha256 содержимого файлов (не-JSON)
- build_index() строит индекс один раз, refresh() обновляет только при изменении списка файлов
- resolve_stem() находит основу имени по хешу, при промахе возвращает default или (None, "none")
- process_once() принимает параметр index и default, работает с файлами <uuid>.<ext>
- Пропускаются dot-файлы, *.part, *.tmp, *.done (Review Focus 5)
- Сообщение об ошибке: "{sha256[:12]}: нет пары по содержимому в фикстурах"
- EmulatorSettings.default = "" для SCAN_EMULATOR_DEFAULT
- Логирование источника пары: hash / default / none
- Все 909 тестов зелёные, включая 54 новых и переписанных тестов эмулятора

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
T14.1: run() ВСЕГДА строит индекс один раз (даже если fixtures_dir отсутствует → пустой индекс), обновляет через refresh() на каждом проходе. process_once() больше не пересчитывает при None, берёт переданный индекс.

T14.3: OSError при чтении входящего файла логируется одной строкой, проход продолжается со следующего файла.

T14.4: Лог сообщений об ошибке использует "по умолчанию" вместо `default`; исправлена опечатка "фиксутрах" → "фикстурах"; удалено "(Review Focus 5)" из docstring модуля.

Новые тесты: test_upload_file_read_error_continues (T14.3), test_uppercase_extension_accepted (M-4), test_ignores_part_tmp_done_robust (T14.2c).

Все 912 тестов зелёные.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
T14.2(a): Добавлен тест с шпионом на build_index, проверяющий что индекс не пересчитывается повторно на каждом проходе при неизменных фикстурах. 5 проходов — ровно 1 вызов build_index (при инициализации run), остальные используют refresh.

T14.2(b): Добавлен тест корректной обработки появления каталога фикстур после стартапа — run не выбрасывает исключение и может использовать фикстуры после их создания.

T14.1 remainder (double refresh): Исправлено поведение process_once: если index=None, то build_index один раз; если index передан, то только refresh. Обновлена docstring процесса.

T14.2(c): Добавлен мутационный тест test_skip_suffixes_not_logged_as_regex_mismatch, проверяющий что файлы с _SKIP_SUFFIXES пропускаются молча и НЕ логируются как не совпадающие с шаблоном.

T14.4: Переписан test_uppercase_extension_accepted — теперь пишет файл напрямую .PDF и проверяет нормализацию в .pdf.done, вместо использования _drop которая нормализует расширение.

Дополнительно:
- Обновлена docstring run() — точное описание: один refresh в конце каждого прохода
- Удалено "(Review Focus 5)" из docstring test_ignores_part_tmp_done
- Добавлен @pytest.mark.skipif для chmod теста при запуске от root

915 тестов пройдено, 10 деселектировано (10 избранных тестов требуют fixtures).
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T14.1: процесс_once() без refresh если индекс передан; run() refresh перед вызовом, не после
- T14.2a: спай на build_index — не вызывается повторно при неизменных фикстурах
- T14.2b: реальный тест файла, появляющегося на проходе 2: ровно 2 build_index всего
- T14.2c: добавлен dot-файл `.{uuid}.pdf.part` в skip-тест
- Мутационные тесты падают при удалении проверок (не коммичены)
- Typo: «фиксутрах» → «фикстурах»
- 915 тестов проходят, make check зелёный

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- C-1 (№30): сумма с более чем двумя знаками после запятой не принимается (amount=null, string_value, is_recognized=false);
  confidence ячеек и шапки приводится к [0, 100], NaN и бесконечность дают 0
- F-A (№28): ошибочный файл набора при успешном понижает статус до needs_check
- F-B: «ИНН не распознан» только когда ИНН не извлечён, иначе «ИНН распознан неуверенно»
- №29: предупреждения «строк с нечисловым кодом пропущено: N» и «строк без кода с суммами пропущено: N»
- I-3 (№31): тексты дат длиннее 200 символов не разбираются и не проходят fix_lookalikes
- T1.3 (№33): загрузчик yaml отклоняет повторяющиеся ключи; тавтологичный тест дублей заменён настоящим
- гигиена: докстринг amounts.py описывает фактическое поведение (все точки удаляются), мёртвые noqa в ocr/__init__.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Старый .part, оставшийся жёсткой ссылкой на опубликованный файл, больше не перезаписывается на месте; ошибка записи
поднимается как есть, а не как FileNotFoundError от os.replace; EOPNOTSUPP добавлен к списку ФС без жёстких ссылок.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- F-D: фейк rename бросает SMBOSError(STATUS_OBJECT_NAME_COLLISION) (и обычный FileExistsError), rmdir — STATUS_DIRECTORY_NOT_EMPTY;
  тесты validate_name, сирот .part/.done, access-denied scandir, подкаталога в Output, .part при существующем назначении,
  учётных данных во всех вызовах очистки; докстринг SmbHotFolder.put больше не противоречит сам себе (T12.5)
- F-E: тест ошибки очистки проверяет вызов очистки и ровно одну строку WARNING
- T9.4: докстринг протокола Normalizer; T13.4: устаревшие ссылки на «Task 14»; мёртвый noqa BLE001 в worker.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Статус при ошибочном файле набора (№28), новые предупреждения (№29, формулировка ИНН), правило Money (№30), длина дат (№31),
дубли yaml (№33), очистка удаляет всё содержимое Output/<job_id>/, процедура заполнения fsbu4 со списком тестов с пустой
редакцией; SCAN_CLEANUP и SCAN_RULES_DIR в .env.example.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
LocalHotFolder.put сначала проверяет назначение: если файл во входящем каталоге уже есть, он считается доставленным,
ничего не пишется, а .part снимается только при включённой очистке. Иначе удаление старого .part подавляется (остаток
прерванной записи не роняет задание); если он остался, усечение разрешено только при st_nlink == 1, ссылка на чужой файл
не трогается и исключение поднимается как есть.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep merged commit 477816225b into main 2026-10-07 04:05:00 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-scan-gateway#8
No description provided.