diff --git a/openspec/changes/statements-v2/design.md b/openspec/changes/statements-v2/design.md new file mode 100644 index 0000000..bb87da5 --- /dev/null +++ b/openspec/changes/statements-v2/design.md @@ -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:.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 ` (бриф владельца) и + `Claude-Session: …` (план) — совпадает с обоими источниками. Цена ошибки: нет. +- **R-2 (Р-36).** Задача 4 копирует из `docs/contracts/examples` только существующие файлы верхнего уровня как есть, новых + примеров контракта верхнего уровня не добавляет (решение владельца Р-36). Цена ошибки: нет. diff --git a/openspec/changes/statements-v2/proposal.md b/openspec/changes/statements-v2/proposal.md new file mode 100644 index 0000000..60ba24e --- /dev/null +++ b/openspec/changes/statements-v2/proposal.md @@ -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:`, `locator = + file:<имя json>;table:`). +- Обход набора 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`. diff --git a/openspec/changes/statements-v2/specs/image-recognition-gateway/spec.md b/openspec/changes/statements-v2/specs/image-recognition-gateway/spec.md new file mode 100644 index 0000000..d6fe401 --- /dev/null +++ b/openspec/changes/statements-v2/specs/image-recognition-gateway/spec.md @@ -0,0 +1,134 @@ +## MODIFIED Requirements + +### Requirement: Результат по контракту +Сервис SHALL публиковать в `files.tabular_parsed` конверт по `recognition-result-message.schema.json` с +`service.name = "scan-gateway"`, `Nats-Msg-Id = result::`; при `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:`, `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/.json`, `result_s3` указывает на файл результата с двумя документами (`BALANCE`, `INCOME`) + +#### Scenario: Ошибка ИС +- **WHEN** единственный файл ответа `.json` содержит `error.code = INVALID_DOCUMENT` +- **THEN** конверт `status = error`, `error.code = INVALID_DOCUMENT`, `error.message` из ответа, `raw_s3_key` заполнен, `metrics.warnings` содержит одну структуру `JSON_FAILED` (`error`, `locator = file:.json`, `message = INVALID_DOCUMENT: <сообщение>`) + +#### Scenario: Ошибка одного из документов +- **WHEN** ответ состоит из `.json` (успех, `aep_3.json`) и `_ofr.json` (`INVALID_DOCUMENT`, `aep_3_ofr.json`) +- **THEN** конверт `status = needs_check`, документы собраны из `.json`, `metrics.warnings` содержит `JSON_FAILED` (`error`, `locator = file:_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:`; `locator = file:<имя json>;table:`. `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** `.json` успешен, `_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`. diff --git a/openspec/changes/statements-v2/tasks.md b/openspec/changes/statements-v2/tasks.md new file mode 100644 index 0000000..26ca753 --- /dev/null +++ b/openspec/changes/statements-v2/tasks.md @@ -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