openspec: change statements-v2 — адаптер OCR → arm_statements, контракт 2.0 (#46, arm-ka-docs#116)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-18 19:54:19 +04:00
parent ec9f765c27
commit 43f74fbd50
4 changed files with 270 additions and 0 deletions

View File

@ -0,0 +1,71 @@
## Context
Решение принято в docs change `unified-statement-normalization` (RSHB/arm-ka-docs#116); подробный дизайн — его
`design.md` (`docs/openspec/changes/unified-statement-normalization/design.md`, до архивации; после — в архиве docs).
План реализации — `plans/2026-10-18-plan-5-scan-gateway.md` того же change'а.
## Decisions made without discussion
Дата всех решений — 18.10.2026. Маркеры: `P5-n` — решения автора плана, `R-n` — рулинги контроллера SDD. Формат пунктов P5-n:
решение, обоснование, цена ошибки — как в плане (без сокращений).
- **P5-1.** Сетка OCR — одна строка сетки на строку ИС по возрастанию `row_index`, пропуски `row_index` пустыми строками
не заполняются; ширина строк таблицы — `max(column_index) + 1`. «Плотная по `row_index`» прочитано как «по порядку
`row_index`», `ref` всё равно несёт номер ИС. Цена: шаг 2 не видит «дыры» между строками (в реальных json их нет).
- **P5-2.** Нарушенная структура таблицы ИС (повтор `row_index`, повтор `column_index` в строке, нецелые индексы, `rows`
не список) — исключение адаптера → `TABLE_PARSE_ERROR` всей таблицы. Сегодня такие ячейки молча пропускались или
перезаписывались; повтор `row_index` сломал бы уникальность `ref`. Цена: одна битая ячейка теряет таблицу (видно —
`error`).
- **P5-3.** `value` ячейки: `None` → `""`, не строка → `str(value)`, без `strip`; `confidence`: число (не `bool`) →
`[0, 100]`, иначе `0` (как `clamp_confidence`, §11.1 «NaN → 0»). Цена: числовой `value` ИС (не встречался) даст текст
`"1234.0"`.
- **P5-4.** Ячейка ИС с пустым текстом — `Cell("", …)`, а не `None`: ИС отдаёт ячейку на каждую позицию строки, `None` —
только там, где ячейки нет. Цена: нет.
- **P5-5.** Шапка: поле ИС отсутствует, не объект или `form_name` пустой → `None` (а не `Cell("", 0)`); `ref` полей шапки —
`data_ocr.<поле ИС>` (пакет `ref` шапки в выход не переносит — у `HeaderField` его нет, план 1, раздел API);
`data_ocr.label` (заголовок комплекта) не используется. Цена: `<поле>_text` пустого поля — `null`, а не `""`.
- **P5-6.** `evidence.title = Cell(label.form_name, …, ref="table:<i>.label")`; `header.form_name` контракта —
`document.header.form_name` пакета (= `evidence.title.text` как в источнике, пустой — `null`, план 1 P1-29); шлюз
`form_name` сам не ставит и пару «кандидат → документ» не хранит.
- **P5-7.** Успешный json без `data_ocr`, без `table_structure` или с пустым `table_structure` — `JSON_FAILED` (`error`):
«ничего не выбрасывать молча» (§11, принцип 2). Цена: если ИС вернёт успешный пустой json рядом с нормальными, конверт
будет `needs_check` (вопрос владельцу — Q-1).
- **P5-8.** Все json набора ошибочны — конверт `error` с кодом первого, а `metrics.warnings` всё равно перечисляют
`JSON_FAILED` каждого json (как строки Среза 0): иначе коды остальных json теряются. Цена: спека называет
`JSON_FAILED` «при успешном соседе»; на `error`-конверт ядро `RECOGNIZED` не считает, вреда нет (Q-2).
- **P5-9.** `TABLE_PARSE_ERROR.message` = `BACKEND_PARSING_ERROR: <тип>: <текст>` (текст до 300 символов) — код старого
§11.1 п. 9 сохранён в тексте для поиска по логам и golden. Цена: нет.
- **P5-10.** Предел `metrics.warnings` (100 структур, `message` до 1000 символов) сохранён; при переполнении остаются все
`error`, затем `info`; строки-сводки нет — новый код предупреждения вводится только словарём пакета. Список в конверте
и в файле результата одинаков (спека). Цена: при > 100 предупреждениях часть `info` не видна ни там, ни там (сырой
ответ — в `raw/`).
- **P5-11.** `statements_version` читается при создании `OcrNormalizer`; нет `VERSION` — сервис не стартует. Версия едет в
`NormalizeOutcome.statements_version` и в `service.statements_version` файла результата; в конверт не пишется (в
схеме конверта поля нет). Цена: dev-checkout без `VERSION` не стартует (копию всегда раскладывает скрипт).
- **P5-12.** `api/rules/` удалён целиком, `SCAN_RULES_DIR` убран из настроек; словари — `default_rules()` пакета. Цена:
переопределить словарь без релиза нельзя (и не нужно — словари меняются через `arm-statements`).
- **P5-13.** `CONTRACT_VERSION = "2.0"` — для файла результата и всех конвертов, включая ack; MAJOR запроса —
отдельная константа (`SUPPORTED_MAJOR = 1`).
- **P5-14.** Golden v2 — `{schema_version, status, error, metrics, documents}` файла результата без `job_id`, `file_id`,
`service`, `raw_s3_key` (`statements_version` меняется при каждой раскладке копии); успешный golden дополнительно
проходит схему v2.0. Цена: golden не ловит регресс поля `service`.
- **P5-15.** Перевод в контракт — `to_contract` / `warning_to_contract` пакета (план 1, P1-26): у предупреждений
документа нет `locator` и пустых необязательных ключей (схема не допускает `null` у `column_index` и `field`), у
нарушений §7 нет `column_key`, у предупреждений файла `locator` есть; шлюз только валидирует `dict` своими моделями.
Цена: правка правил перевода — в `arm-statements`, а не здесь.
- **P5-16.** Модули: `OcrNormalizer` остаётся в `src/normalizer/ocr/__init__.py` (меньше правок `app.py` и тестов),
адаптер — `ocr/adapter.py`, обход набора — `ocr/answer_set.py`, ошибки json — `normalizer/files.py` (снимает цикл
импортов `src.normalizer` ↔ `src.normalizer.ocr`); своего конвертера в контракт (`contract_map.py`) нет.
- **P5-17.** Review Focus 3 утверждает `done` для обоих документов «БО 1 кв 2025» и «Бухгалтерская отчетность 1 кв.25»
(proposal, Р-23). Падение при верном кандидате — находка для пакета, а не правка шлюза; ослабить утверждение может
только контроллер рулингом.
- **P5-18.** `metrics.rows` — число строк `lines` всех документов (как в v1.2), теперь включая строки без кода.
- **P5-19.** `NullNormalizer` сохраняется для тестов воркера и выдаёт `JSON_FAILED` через `make_warning` пакета.
- **P5-20.** Обход набора зовёт `normalize_candidate` на каждую таблицу, а не `normalize`: исключение одной таблицы —
`TABLE_PARSE_ERROR` с `locator` таблицы, остальные таблицы разбираются (план 1: `normalize` исключения кандидата не
перехватывает). `AnswerSetResult.documents` — документы пакета как есть (заголовок — в `header.form_name`).
- **R-1.** Коммиты ветки несут трейлеры `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` (бриф владельца) и
`Claude-Session: …` (план) — совпадает с обоими источниками. Цена ошибки: нет.
- **R-2 (Р-36).** Задача 4 копирует из `docs/contracts/examples` только существующие файлы верхнего уровня как есть, новых
примеров контракта верхнего уровня не добавляет (решение владельца Р-36). Цена ошибки: нет.

View File

@ -0,0 +1,38 @@
## Why
Единая нормализация ББ/ОФР (docs change `unified-statement-normalization`, RSHB/arm-ka-docs#116): шаги 2–8 разбора
вынесены в общий пакет `arm_statements`, контракт результата распознавания поднят до 2.0. Шлюз оставляет у себя обмен с ИС
Сканирование и адаптер «таблица json ИС → `FormCandidate`», а собственный нормализатор и справочники удаляет. Требования —
спека docs `specs/scan-gateway/spec.md` (требования «Адаптер json ИС шага 1», «Ошибки набора json», «Что уходит из
шлюза») и `specs/recognition-contract/spec.md`.
## What Changes
- Адаптер `normalizer/ocr/adapter.py`: каждая таблица каждого успешного json — один `FormCandidate` (шапка json копируется
в шапку каждой таблицы, `label` — заголовок в `evidence`, плотная сетка, `Cell.ref = row:<row_index>`, `locator =
file:<имя json>;table:<i>`).
- Обход набора json (`normalizer/ocr/answer_set.py`, `normalizer/files.py`): предупреждения файла `JSON_FAILED`,
`TABLE_PARSE_ERROR`; `TABLE_LOST` / `TABLE_SKIPPED` — от пакета; статус — `envelope_status` пакета.
- Контракт результата 2.0: `schema_version = "2.0"` у файла и конверта (включая ack), структурные `metrics.warnings`,
`service.statements_version` в файле результата; перевод в контракт — `to_contract` / `warning_to_contract` пакета.
- Удаление собственного нормализатора шагов 2–8 (`normalizer/ocr/text|dates|amounts|layout|rows|header|document`),
`normalizer/rules.py`, `api/rules/*.yaml` и `SCAN_RULES_DIR`; словари — `default_rules()` пакета.
- Golden `fixtures/ocr/expected_normalize/*` пересобираются (отдельная ветка и PR в fixtures).
## Порядок выкладки
Шаг 5 выкладки «сначала ядро» (docs `proposal.md`): слияние PR шлюза — только по команде человека и только после
выкладки на контур arm-core, принимающего `2.0`. Golden в fixtures сливается первым, PR scan-gateway — сразу за ним
(окно, в котором golden в `main` красный, держать минимальным). Запрос `recognition-request` остаётся `1.x`.
## Non-goals
- Горячие папки, опрос, settle, склейка `raw/{job_id}.json`, очистка, таймауты, эмулятор не меняются.
- Копия `api/arm_statements/` не правится (её раскладывает `sync-statements`); тест идентичности контракта не меняется.
## Impact
- `api/src/normalizer/**`, `api/src/contract/**`, `api/src/core/worker`, настройки (`SCAN_RULES_DIR` удалён), `api/rules/`
(удалён), `api/contracts/` (копия схем 2.0), тесты, `CLAUDE.md`/README.
- Issue: RSHB/arm-scan-gateway#46; план —
`docs/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-5-scan-gateway.md`.

View File

@ -0,0 +1,134 @@
## MODIFIED Requirements
### Requirement: Результат по контракту
Сервис SHALL публиковать в `files.tabular_parsed` конверт по `recognition-result-message.schema.json` с
`service.name = "scan-gateway"`, `Nats-Msg-Id = result:<job_id>:<attempt>`; при `status ∈ {done, needs_check}` — файл
результата (`recognition-result.schema.json`, `schema_version = "2.0"` — `CONTRACT_VERSION` сервиса; запрос `recognition-request` остаётся `1.x`, запрос `2.0` → `SCHEMA_MISMATCH`) в `results/{file_id}/{job_id}/result.json` и `result_s3`;
при неудаче публикации SHALL повторить её до трёх раз. Нормализатор получает склеенный ответ и SHALL считать файл
успешным, если `error.code == ""` (поле `error` реального успешного ответа — `{"code": "", "message": ""}`, а не `null`;
отсутствующий или `null` `error` ошибкой не считается), и ошибочным, если `error.code` непуст, `parse_error` не `null`
либо корень ответа файла не является объектом JSON (ошибка этого файла — `INVALID_FORMAT`); успех MUST NOT определяться
проверкой «`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 быть опущено.
#### Scenario: Успешный ответ ИС в Срезе 0
- **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (по образцу `aep_6.json`)
- **THEN** начиная со Среза 3a конверт больше не `NOT_IMPLEMENTED`: поведение — по сценарию «Успешный ответ ИС» ниже
#### Scenario: Успешный ответ ИС
- **WHEN** ответ из одного файла с `error = {"code": "", "message": ""}` и непустым `data_ocr` (`aep_6.json`)
- **THEN** конверт `status = needs_check`, `raw_s3_key = raw/<job_id>.json`, `result_s3` указывает на файл результата с двумя документами (`BALANCE`, `INCOME`)
#### Scenario: Ошибка ИС
- **WHEN** единственный файл ответа `<job_id>.json` содержит `error.code = INVALID_DOCUMENT`
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен, `metrics.warnings` содержит одну структуру `JSON_FAILED` (`error`, `locator = file:<job_id>.json`, `message = INVALID_DOCUMENT: <сообщение>`)
#### Scenario: Ошибка одного из документов
- **WHEN** ответ состоит из `<job_id>.json` (успех, `aep_3.json`) и `<job_id>_ofr.json` (`INVALID_DOCUMENT`, `aep_3_ofr.json`)
- **THEN** конверт `status = needs_check`, документы собраны из `<job_id>.json`, `metrics.warnings` содержит `JSON_FAILED` (`error`, `locator = file:<job_id>_ofr.json`, `message = INVALID_DOCUMENT: <сообщение>`) и предупреждения пакета; ядро не автоподтверждает документы файла
#### Scenario: Все документы ошибочны
- **WHEN** оба файла ответа ошибочны (первый по имени — `INVALID_DOCUMENT`, второй — не JSON)
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код первого ошибочного файла по имени), `metrics.warnings` содержит два `JSON_FAILED` (по одному на json); у файла не JSON сообщение начинается с `INVALID_FORMAT`
#### Scenario: Все файлы не JSON
- **WHEN** единственный файл ответа не разбирается как JSON
- **THEN** конверт `status = error`, `error.code = INVALID_FORMAT`, `metrics.warnings` содержит один `JSON_FAILED` с `message = INVALID_FORMAT: <сообщение разбора>`
#### Scenario: Корень ответа не объект
- **WHEN** файл ответа — корректный JSON, но корень — массив
- **THEN** этот файл ошибочен с кодом `INVALID_FORMAT`; остальные файлы оцениваются по общим правилам
#### Scenario: Код ошибки ИС вне перечня контракта
- **WHEN** единственный файл ответа содержит `error.code = SOME_NEW_CODE`
- **THEN** конверт `status = error`, `error.code = UNKNOWN_ERROR`, `error.message` содержит `SOME_NEW_CODE`, `JSON_FAILED` в `metrics.warnings` с `message = SOME_NEW_CODE: <сообщение>`
#### Scenario: Все json набора ошибочны
- **WHEN** оба json набора ошибочны (первый по имени — `INVALID_DOCUMENT`)
- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT` (код и сообщение первого ошибочного json), `metrics.warnings` содержит `JSON_FAILED` каждого json
#### Scenario: Успешный json без таблиц
- **WHEN** успешный json не содержит `data_ocr.table_structure` или `table_structure: []`
- **THEN** этот json даёт `JSON_FAILED`; если успешных json с таблицами нет — конверт `status = error`
### Requirement: Нормализация ответа ИС в `statementDocument`
Шлюз SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в `raw/{job_id}.json` до нормализации, очистку папки) и адаптер «таблица `table_structure[i]` json → `FormCandidate`»; шаги 2–8 SHALL выполняться копией пакета `arm_statements` (`api/arm_statements/`, правке не подлежит). Адаптер SHALL: создавать кандидата на каждую таблицу каждого успешного json; копировать шапку json (`organization`, `inn`, `units`, `date`) в шапку каждой своей таблицы как `Cell` (`text = form_name`, `confidence` ИС); передавать `label.form_name` таблицы как заголовок в `evidence` (тип — общим правилом шага 1 по словарю заголовков пакета, `type_hint(okud_text, title_text, rules)`, у OCR `okud_text = None`); строить плотную сетку по `row_index` и `column_index` ИС (пустые позиции — `null`), `Cell.ref = row:<row_index>`; `locator = file:<имя json>;table:<i>`. `value` у OCR-ячеек всегда `null`, `source = OCR`. Нарушенная структура таблицы ИС SHALL давать исключение адаптера и `TABLE_PARSE_ERROR` всей таблицы, остальные таблицы разбираются. Статус конверта SHALL определяться только `envelope_status` пакета; перевод документов и предупреждений в контракт — только `to_contract` / `warning_to_contract` пакета.
#### Scenario: Один json — ББ и ОФР
- **WHEN** json `aep_3.json` содержит две таблицы — «Бухгалтерский баланс» и «Отчет о финансовых результатах» — под общей шапкой
- **THEN** два кандидата с `locator` `file:aep_3.json;table:0` и `…;table:1`, у каждого копия шапки json
#### Scenario: Таблица упала при разборе
- **WHEN** разбор `table:1` json `aep_3.json` бросил исключение, `table:0` разобрана
- **THEN** документ `BALANCE` из `table:0`; в `metrics.warnings` — `TABLE_PARSE_ERROR` уровня `error` с `locator = "file:aep_3.json;table:1"`; конверт `needs_check`
#### Scenario: Таблица ИС только с текстом
- **WHEN** в json есть таблица без столбца кодов и без распознанного типа, в ячейках только текст подписей
- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` уровня `info` с `locator` таблицы; конверт из-за неё не `needs_check`
#### Scenario: Чистый скан с шапкой (`aep_6.json`)
- **WHEN** нормализуется `aep_6.json` (две таблицы: «Бухгалтерский баланс» и «Отчет о финансовых результатах»)
- **THEN** два кандидата, из них два документа `BALANCE` и `INCOME` по шагам 2–8 пакета; поля, колонки и значения — по golden `fixtures/ocr/expected_normalize/`; `header.units = "тыс. руб."`; статус определяет `envelope_status` пакета
#### Scenario: Зашумлённый скан без шапки (`aep_3.json`)
- **WHEN** нормализуется `aep_3.json` (таблица «Бухгалтерский баланс» без строки-шапки, коды с мусором)
- **THEN** разбор столбца кода, синтез шапки, предупреждения и статус документа — правилами пакета (шаги 2–8), шлюз их не дублирует; `header.inn` и `report_date` — по правилам очистки пакета; строки без кода сохраняются со статусом строки
#### Scenario: Суммы в скобках и с пробелами (`aep_3.json`, ОФР)
- **WHEN** ячейка значения содержит `(7744536}` с `confidence 88.9`, другая — `(6032409)`, третья — `13 070`
- **THEN** разбор сумм и `is_recognized` — общие правила пакета (шаг 4); шлюз передаёт текст ячейки как есть (`value = None`), без собственного разбора
#### Scenario: Нечисловая ячейка значения
- **WHEN** ячейка значения содержит `не ****` с `confidence 5.6`
- **THEN** `amount = null`, `string_value = "не ****"`, `is_recognized = false` (правило шага 4 пакета)
#### Scenario: Повтор кода строки
- **WHEN** в таблице баланса две строки с кодом `1150`
- **THEN** поведение определяет шаг пакета для повтора кода; шлюз ничего не отбрасывает до шагов 2–8, предупреждение приходит от пакета структурой `metrics.warnings`
#### Scenario: Тип по кодам при зашумлённом label
- **WHEN** `label.form_name` таблицы — `Бvxгалтерский баланс` (не сопоставлен словарём заголовков), коды строк `1110…1700`
- **THEN** тип определяется общим правилом шага 1 пакета (по кодам), предупреждение — структурой пакета
#### Scenario: Label противоречит единодушным кодам
- **WHEN** `label.form_name` таблицы — `Бухгалтерский баланс`, все коды строк данных `2110…2910`
- **THEN** тип и предупреждение о расхождении — по правилам пакета; `header.form_name` — текст `label` как распознан; статус `needs_check`
#### Scenario: Label противоречит смешанным кодам
- **WHEN** `label.form_name` таблицы — `Отчет о финансовых результатах`, коды строк данных `1150`, `1100` и `2110`
- **THEN** тип и предупреждение о расхождении — по правилам пакета; статус `needs_check`
#### Scenario: Таблица без столбца кода
- **WHEN** в таблице нет строки с ячейкой «Код» и ни в одном столбце нет кодов
- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` с `locator` таблицы; остальные таблицы файла разобраны
### Requirement: Статус результата нормализации
Статус конверта SHALL определяться функцией `envelope_status` пакета `arm_statements` по документам и предупреждениям. Если из успешных файлов не собран ни один документ — `status = error`, `error.code = RECOGNITION_FAILED`, `error.message = "ни одной таблицы не разобрано"`, причины по таблицам — в `metrics.warnings` (`TABLE_PARSE_ERROR`, `TABLE_SKIPPED`, `TABLE_LOST`); если все json ошибочны — код и сообщение первого ошибочного json. Если документы есть — `done` или `needs_check` по `envelope_status`; потерянный json набора (`JSON_FAILED`) или таблица (`TABLE_PARSE_ERROR`, `TABLE_LOST`) исключают `done`. `metrics.rows` — число строк `lines` всех документов, включая строки без кода.
#### Scenario: Ни одной таблицы
- **WHEN** единственный успешный файл содержит `table_structure: []`
- **THEN** json даёт `JSON_FAILED`, конверт `status = error`, `raw_s3_key` заполнен
#### Scenario: Все таблицы упали при разборе
- **WHEN** в успешном json разбор каждой таблицы бросил исключение
- **THEN** конверт `status = error`, `error.code = RECOGNITION_FAILED`, `error.message = "ни одной таблицы не разобрано"`, `metrics.warnings` содержит `TABLE_PARSE_ERROR` каждой таблицы
#### Scenario: Потерянный документ набора
- **WHEN** `<job_id>.json` успешен, `<job_id>_ofr.json` ошибочен
- **THEN** статус не `done`, а `needs_check`
#### Scenario: Всё распознано
- **WHEN** синтетический ответ, где все ячейки распознаны уверенно, шапка и даты разобраны, предупреждений нет
- **THEN** `status = done`, поле `metrics.warnings` отсутствует
#### Scenario: Исключение в одной таблице
- **WHEN** разбор `table_structure[1]` бросает исключение, `table_structure[0]` разбирается
- **THEN** один документ, `TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:1`, `message = BACKEND_PARSING_ERROR: <класс>: <сообщение>`), статус `needs_check`
## REMOVED Requirements
### Requirement: Справочники нормализации — данные с редакциями форм
**Reason**: справочник строк (`statement_lines.yaml`) и выбор редакции удалены; двойники (`ocr_lookalikes.yaml`) и заголовки форм (`statement_labels.yaml`) — словари пакета `arm_statements` (`default_rules()`).
**Migration**: `api/rules/` и настройка `SCAN_RULES_DIR` удалены; словари меняются через репозиторий `arm-statements` и `sync-statements`.

View File

@ -0,0 +1,27 @@
## 1. Подготовка хоста
- [ ] 1.1 Задача 1: подготовка хоста по плану 2 (копия `arm_statements`, `statements-check`, линтеры, `testpaths`)
## 2. Адаптер
- [ ] 2.1 Задача 2: адаптер — таблица json ИС → `FormCandidate`
## 3. Обход набора json
- [ ] 3.1 Задача 3: `normalize`, предупреждения файла, статус и код ошибки
## 4. Контракт 2.0
- [ ] 4.1 Задача 4: контракт v2.0 и переключение нормализатора
## 5. Удаление старого
- [ ] 5.1 Задача 5: удаление старого нормализатора, справочников и `SCAN_RULES_DIR`
## 6. Golden
- [ ] 6.1 Задача 6: golden v2 и Review Focus на реальных ответах ИС
## 7. Завершение
- [ ] 7.1 Задача 7: интеграционные тесты, документация, `make check-full`, PR