- Добавлена 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
- 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
Перенос _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
Перенос `_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
Переносим функции парсинга сумм и проверки пустоты из старого ядра:
замена запятой, удаление пробелов, извлечение круглых скобок (отрицательное),
множественные точки как разделители тысяч. Отличие: verified вычисляется
ДО постановки минуса (решение №13 design.md), чтобы не отмечать суммы
в скобках как неверифицированные.
Исправлена строка 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
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
- README: верхняя граница задержки settle, шаг 5 «Полного круга»; критерии ошибочного файла (корень не объект)
- CLAUDE.md: по одному json на каждый распознанный документ; критерии ошибочного файла
- design.md: D-5 — оговорка про нестроковый error.code отдельным предложением; №79 — источник указания, SQL уборки в порядке FK
- open-questions-proposal.md п.20: замены ../docs/dev/README.md (185–187, 224–226, 252–253) и комментарий docker-compose.yml:184
- план: Task 6 — пометка про замену down -v (R-27)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Профиль services (arm-core, scan-gateway из этого worktree, scan-emulator): aep_3.pdf → error NOT_IMPLEMENTED
с metrics.warnings про <job_id>_ofr.json, aep_6.pdf → error NOT_IMPLEMENTED без metrics, aep_5.PDF →
error INVALID_DOCUMENT; raw/<job_id>.json — склейка набора, тела совпадают с фикстурами; Tmp пуст, .part нет.
Compose правки не требует (умолчания Tmp/settle 2 с). Устаревшие места ../docs/dev/README.md — точными
заменами в open-questions-proposal.md, пункт 20. Уборка — rm -sfv своих сервисов и том hotfolder вместо
down -v (R-27). Отметки: tasks 7.5, план Task 6.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Раскладка Output/<job_id>/*.json (склеиваются все файлы), SCAN_TMP_DIR, SCAN_SETTLE_SECONDS, структура raw/{job_id}.json,
успех = error.code == "", ошибочные файлы в metrics.warnings, правило набора эмулятора. D-5: не строковый error.code
тоже ошибка (UNKNOWN_ERROR, R-32). Решения R-36 (design #76). Тест TestAnswerSet на реальных фикстурах помечен fixtures.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Для `<uuid>_<name>.<ext>` эмулятор собирает все `<name>.json` и `<name>_*.json` из SCAN_FIXTURES_DIR (не префикс
`<name>*`) и кладёт их в `Output/<uuid>/` как `<uuid>.json` / `<uuid>_<suffix>.json` с `file_uid = <uuid>`. Набор
пишется в `<root>/<SCAN_TMP_DIR>/.<uuid>.answer` и переименовывается в каталог ответа целиком (R-26). Ответ «нет пары» —
`data_ocr` объектом с пустыми полями (R-28). Решения исполнителя — design №75 (R-35).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- spec «Ответ из одного файла»: набор передаётся не позднее SCAN_SETTLE_SECONDS +
SCAN_POLL_SECONDS после первого опроса, увидевшего окончательный набор (R-29).
- design D-5: сигнатура NormalizeOutcome с warnings; воркер дописывает их в metrics.warnings.
- Decisions made without discussion: №67 R-29, №68 решения исполнителя Task 2, №69–72
R-30..R-33 (предел warnings, склейка в src/gateway/merge.py, крайние случаи NullNormalizer,
слияние warnings с metrics), №73 T2.1/T2.2/T2.4, №74 R-34 (адаптация test_roundtrip.py).
- Отметки: Task 3 в плане, 7.2 в tasks.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Исправлена критическая ошибка: NullNormalizer считал ответ ошибкой при `error is not None`,
а у реального успеха ИС error = {"code": "", "message": ""} — aep_3.json и aep_6.json давали
UNKNOWN_ERROR. Успех файла теперь — error.code == "" (отсутствующий/null/не-объектный error
и error без кода ошибкой не считаются); регрессия — на реальных фикстурах (маркер fixtures).
- src/gateway/merge.py: merge_answers(job_id, files) -> MergedResponse
{"job_id", "files": [{"name", "response", "parse_error"}]} по возрастанию имени; не JSON —
parse_error файла; dump_merged — UTF-8 (с ASCII-экранированием при одиночном суррогате).
- Воркер: опрос отдаёт готовый набор → склейка → raw/{job_id}.json (application/json) до
нормализации → Normalizer.normalize(склейка). Перечитывание «не JSON» убрано (R-24).
- NullNormalizer: все файлы ошибочны — код/сообщение первого по имени (parse_error и
не-объект — INVALID_FORMAT, код вне контракта — UNKNOWN_ERROR с исходным кодом); есть
успешный — NOT_IMPLEMENTED; ошибочные файлы — строки `<имя>: <код ИС>: <сообщение>`.
- NormalizeOutcome.warnings (R-25) → metrics.warnings конверта и файла результата; пустые
опускаются; не больше 100 строк по 1000 символов, лишние — сводной строкой (R-30).
- Тесты: test_merge.py, test_normalizer.py переписан под склейку, test_worker.py — склейка в
raw, ошибка одного файла, parse_error в warnings, все ошибочны, реальные aep_3+aep_3_ofr и
aep_6, settle и таймаут ненастоявшегося набора; SUCCESS_ANSWER — в форме реального успеха.
Интеграционный test_roundtrip.py минимально переведён на формат склейки (прогон — Task 5).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- T2.1: после чтения набора каталог ответа листается заново (Local и SMB); если снимок (имена и
размеры) отличается от настоявшегося — None, набор настаивается заново.
- T2.2: SmbHotFolder — ENOTDIR из листинга Output/<job_id> (на месте каталога файл) — None,
как у LocalHotFolder.
- T2.4: тест SMB — ENOENT / SHARING_VIOLATION / ENOTDIR во время настаивания сбрасывают
накопленное состояние settle (pending_jobs() == 0).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- HotFolder.poll(job_id) -> list[AnswerFile] | None: все *.json каталога <output>/<job_id>/ по возрастанию
имени, когда набор не пуст и имена/размеры не менялись SCAN_SETTLE_SECONDS (SettleTracker: состояние по
job_id, внедряемые часы, сброс при выдаче/опустении, предел 1024 задания, потокобезопасно);
- put: временный .<name>.part в <корень>/<SCAN_TMP_DIR>/ (создаётся), rename в Input (Local и SMB);
- настройки SCAN_TMP_DIR (Tmp) и SCAN_SETTLE_SECONDS (2, >= 0); в .env.test settle = 0 (R-23);
- воркер: переходная прослойка — берётся первый файл набора (склейка всех — Task 3, R-22).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>