docs: дельты OpenSpec, design.md, README и CLAUDE.md под контракт 2.0 (#54)
I-3: дельты osv-parsing (листы ОСВ, иерархия, golden, XLS_NO_CODEPAGE) и перечень словарей api/rules; пункт об архивации (Purpose спек) в tasks.md. I-4: README, CLAUDE.md и docstring hierarchy.py совпадают с кодом, пример вывода CLI переснят. M-6: tasks.md, proposal.md, design.md с решениями P4/R. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
0d82b5cf5e
commit
4d30718689
13
CLAUDE.md
13
CLAUDE.md
@ -47,7 +47,7 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote
|
|||||||
**Добавлено** (в скобках — задача `openspec/changes/slice-0-skeleton/tasks.md`):
|
**Добавлено** (в скобках — задача `openspec/changes/slice-0-skeleton/tasks.md`):
|
||||||
- `api/src/parser/` — интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 — Срез 2a (см. ниже), форм §12 —
|
- `api/src/parser/` — интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 — Срез 2a (см. ниже), форм §12 —
|
||||||
Срез 2b;
|
Срез 2b;
|
||||||
- `api/src/contract/` — pydantic-модели контракта v1.1 (запрос, результат, конверт); `api/contracts/` — копии схем (идентичность по правилу MAJOR (S-53),
|
- `api/src/contract/` — pydantic-модели контракта v2.0 (запрос, результат, конверт); `api/contracts/` — копии схем (идентичность по правилу MAJOR (S-53),
|
||||||
`tests/contract_identity.py`, с `../docs/contracts` проверяется в `make check`) — 3.1;
|
`tests/contract_identity.py`, с `../docs/contracts` проверяется в `make check`) — 3.1;
|
||||||
- `api/rules/` — README о назначении; сами словари `*.yaml` (в документах `rules/*.yaml` относительно `api/`)
|
- `api/rules/` — README о назначении; сами словари `*.yaml` (в документах `rules/*.yaml` относительно `api/`)
|
||||||
появились в Срезе 2a — 4.1;
|
появились в Срезе 2a — 4.1;
|
||||||
@ -93,8 +93,8 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote
|
|||||||
кооперативный, → `TIMEOUT`) — `core/config.py`. Фазу чтения таймаут не прерывает: до библиотеки чтения действуют
|
кооперативный, → `TIMEOUT`) — `core/config.py`. Фазу чтения таймаут не прерывает: до библиотеки чтения действуют
|
||||||
лимиты `readers/_limits.py` (распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек ≤ 200 000; R-23).
|
лимиты `readers/_limits.py` (распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек ≤ 200 000; R-23).
|
||||||
|
|
||||||
**Контракт v1.1:** `rows[].extra` (нераспознанные колонки таблицы по заголовку); `CONTRACT_VERSION = "1.1"`; копия схемы
|
**Контракт:** `rows[].extra` (нераспознанные колонки таблицы по заголовку; появилось в v1.1); текущая версия — 2.0
|
||||||
в `api/contracts/` идентична `../docs/contracts/` по правилу MAJOR (S-53, `tests/contract_identity.py`; проверяется в `make check`).
|
(`CONTRACT_VERSION`, раздел «Формы через arm_statements»); копия схемы в `api/contracts/` идентична `../docs/contracts/` по правилу MAJOR (S-53, `tests/contract_identity.py`; проверяется в `make check`).
|
||||||
|
|
||||||
**Тесты:** `make check` ≈ 95 с, потому что `make test` (`-m "not integration"`) включает тесты маркера `fixtures` (golden,
|
**Тесты:** `make check` ≈ 95 с, потому что `make test` (`-m "not integration"`) включает тесты маркера `fixtures` (golden,
|
||||||
property, покрытие словарей); без каталога `../fixtures` они пропускаются. Полный набор — `make check-full` (+ интеграция
|
property, покрытие словарей); без каталога `../fixtures` они пропускаются. Полный набор — `make check-full` (+ интеграция
|
||||||
@ -106,7 +106,7 @@ property, покрытие словарей); без каталога `../fixtur
|
|||||||
|
|
||||||
Конвейер форм ББ/ОФР (шаги 2–8, словари форм, перевод в контракт) — в пакете `api/arm_statements/` (копия
|
Конвейер форм ББ/ОФР (шаги 2–8, словари форм, перевод в контракт) — в пакете `api/arm_statements/` (копия
|
||||||
`arm-statements`, версия — `arm_statements/VERSION`). Собственного конвейера форм и словарей форм в сервисе нет: удалены
|
`arm-statements`, версия — `arm_statements/VERSION`). Собственного конвейера форм и словарей форм в сервисе нет: удалены
|
||||||
`api/src/parser/statements/{checks,columns,detect,header,lines,merge,statement,legacy_models}.py` и
|
`api/src/parser/statements/{checks,columns,detect,header,lines,merge,statement}.py` и
|
||||||
`api/rules/statement_*.yaml`.
|
`api/rules/statement_*.yaml`.
|
||||||
|
|
||||||
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
|
**Модули адаптера** (`api/src/parser/statements/`): `forms.py` (блоки форм и вид листа по словарям пакета), `cells.py`
|
||||||
@ -136,7 +136,6 @@ property, покрытие словарей); без каталога `../fixtur
|
|||||||
в `../fixtures/golden/`: `expected_osv_parse` и `expected_statement_parse`; правятся только `make golden-update` с
|
в `../fixtures/golden/`: `expected_osv_parse` и `expected_statement_parse`; правятся только `make golden-update` с
|
||||||
просмотром diff и отдельным коммитом в `fixtures`.
|
просмотром diff и отдельным коммитом в `fixtures`.
|
||||||
|
|
||||||
**Контракт v1.2:** `CONTRACT_VERSION = "1.2"`; `documents[]` получает `BALANCE`/`INCOME`/`DISCLOSURE` (`statementDocument`:
|
**Значения форм:** значения ББ/ОФР из Excel — как их показывает числовой формат ячейки (#42, `statements/number_format.py`;
|
||||||
`header`, `lines`, `checks`); копия `api/contracts/` идентична `../docs/contracts/`. Значения ББ/ОФР из Excel — как их показывает
|
xlsb — как в файле + предупреждение `XLSB_NO_FORMATS`); ОСВ формат не применяет.
|
||||||
числовой формат ячейки (#42, `statements/number_format.py`; xlsb — как в файле + предупреждение); ОСВ формат не применяет.
|
|
||||||
Книга НПЗ в манифесте — `done` (итоги расходятся в пределах округления, OQ-138).
|
Книга НПЗ в манифесте — `done` (итоги расходятся в пределах округления, OQ-138).
|
||||||
|
|||||||
58
README.md
58
README.md
@ -6,7 +6,7 @@ Stateless-сервис распознавания табличных файло
|
|||||||
возвращает структуру файла и ничего не знает про организации, периметры и план счетов — нормализацией занимается
|
возвращает структуру файла и ничего не знает про организации, периметры и план счетов — нормализацией занимается
|
||||||
`arm-core`.
|
`arm-core`.
|
||||||
|
|
||||||
Обмен с `arm-core` идёт по контракту **v1.1**: схемы — [`../docs/contracts/`](../docs/contracts/), копия — в
|
Обмен с `arm-core` идёт по контракту **v2.0**: схемы — [`../docs/contracts/`](../docs/contracts/), копия — в
|
||||||
`api/contracts/` (идентичность с источником проверяется в `make check`).
|
`api/contracts/` (идентичность с источником проверяется в `make check`).
|
||||||
|
|
||||||
**Срез 0 — скелет.** Транспорт работает полностью (подписка, ack, чтение S3, публикация результата, ошибки транспорта);
|
**Срез 0 — скелет.** Транспорт работает полностью (подписка, ack, чтение S3, публикация результата, ошибки транспорта);
|
||||||
@ -58,7 +58,7 @@ api/ бэкенд (все команды разработки
|
|||||||
распознавания), logger.py, exception_handlers.py
|
распознавания), logger.py, exception_handlers.py
|
||||||
src/parser/ парсер ОСВ (Срез 2a): интерфейс Parser, OsvParser, читатели, детекторы, иерархия, классификация,
|
src/parser/ парсер ОСВ (Срез 2a): интерфейс Parser, OsvParser, читатели, детекторы, иерархия, классификация,
|
||||||
контрольные суммы, CLI; заглушка NullParser — для тестов воркера (модули — ниже)
|
контрольные суммы, CLI; заглушка NullParser — для тестов воркера (модули — ниже)
|
||||||
src/contract/ pydantic-модели контракта v1.1
|
src/contract/ pydantic-модели контракта v2.0
|
||||||
contracts/ копии JSON-схем контракта и примеры
|
contracts/ копии JSON-схем контракта и примеры
|
||||||
rules/ словари синонимов *.yaml (osv_headers, osv_groups, periods, data_types) и README
|
rules/ словари синонимов *.yaml (osv_headers, osv_groups, periods, data_types) и README
|
||||||
scripts/ golden_update.py — обновление golden-снимков
|
scripts/ golden_update.py — обновление golden-снимков
|
||||||
@ -117,7 +117,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
|||||||
| `rules.py` | загрузчик словарей `rules/*.yaml` в frozen-модели (pydantic), `normalize_text`, `Pattern`; ошибка словаря — `RuntimeError` с файлом и ключом при старте |
|
| `rules.py` | загрузчик словарей `rules/*.yaml` в frozen-модели (pydantic), `normalize_text`, `Pattern`; ошибка словаря — `RuntimeError` с файлом и ключом при старте |
|
||||||
| `detect.py` | вид листа (`classify_sheet`: ОСВ / форма ББ-ОФР / прочее), блок заголовка таблицы (`find_table_header`: 1–4 строки, шесть колонок сумм, колонка текста, ИНН, дополнительные колонки, правая граница, повтор заголовка внутри данных) |
|
| `detect.py` | вид листа (`classify_sheet`: ОСВ / форма ББ-ОФР / прочее), блок заголовка таблицы (`find_table_header`: 1–4 строки, шесть колонок сумм, колонка текста, ИНН, дополнительные колонки, правая граница, повтор заголовка внутри данных) |
|
||||||
| `header.py` | шапка документа (организация, счёт, период, тип данных, валюта) и схема группировок `group_schema` (варианты шапки 1 и 2) |
|
| `header.py` | шапка документа (организация, счёт, период, тип данных, валюта) и схема группировок `group_schema` (варианты шапки 1 и 2) |
|
||||||
| `hierarchy.py` | каскад стратегий уровней строк: `outline` → `indent` → `account_pattern` → `sums`; `trivial` — меньше двух строк данных; выбранная стратегия пишется в `metrics.warnings` как `hierarchy=<имя>` |
|
| `hierarchy.py` | каскад стратегий уровней строк: `outline` → `indent` → `account_pattern` → `sums`; `trivial` — меньше двух строк данных; выбранная стратегия пишется в `warnings[]` документа листа как `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`info`) |
|
||||||
| `numbers.py` | суммы в `Decimal` с двумя знаками (`to_money`), ИНН из текста ячейки (`inn_digits`); `float` в домене нет |
|
| `numbers.py` | суммы в `Decimal` с двумя знаками (`to_money`), ИНН из текста ячейки (`inn_digits`); `float` в домене нет |
|
||||||
| `inn_rows.py` | строки ИНН рядом с контрагентом (восемь раскладок, контрольная сумма без подсказки шапки, противоречие подсказке, OQ-163); выполняется над деревом до классификации |
|
| `inn_rows.py` | строки ИНН рядом с контрагентом (восемь раскладок, контрольная сумма без подсказки шапки, противоречие подсказке, OQ-163); выполняется над деревом до классификации |
|
||||||
| `classify.py` | сбор строк таблицы (`collect_body_rows`: «Итого», свёрнутое сальдо) и тип группировки, счёт, контрагент, `data_type`, `extra` (`classify_rows`) |
|
| `classify.py` | сбор строк таблицы (`collect_body_rows`: «Итого», свёрнутое сальдо) и тип группировки, счёт, контрагент, `data_type`, `extra` (`classify_rows`) |
|
||||||
@ -139,7 +139,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
|||||||
| Исход | Когда |
|
| Исход | Когда |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `done` | контрольные суммы §1.4 сошлись на всех листах |
|
| `done` | контрольные суммы §1.4 сошлись на всех листах |
|
||||||
| `needs_check` | нарушены контрольные суммы хотя бы на одном листе (`checks`: `total_row_matches_root`, `balance_identity_violations`, `parent_sum_violations`; причины и номера строк — в `metrics.warnings`); структуру под сходимость парсер не подгоняет. Строка «Итого» не найдена — тоже `needs_check` |
|
| `needs_check` | нарушены контрольные суммы хотя бы на одном листе (`checks`: `total_row_matches_root`, `balance_identity_violations`, `parent_sum_violations`; причины и номера строк — в `warnings[]` документа листа, коды `OSV_*`); структуру под сходимость парсер не подгоняет. Строка «Итого» не найдена — тоже `needs_check` |
|
||||||
| `error` / `INVALID_DOCUMENT` | в книге нет листов ОСВ и форм (в сообщении — число просмотренных листов) |
|
| `error` / `INVALID_DOCUMENT` | в книге нет листов ОСВ и форм (в сообщении — число просмотренных листов) |
|
||||||
| `error` / `INVALID_FORMAT` | лист ОСВ найден, но нет заголовка таблицы, обязательной группы сумм («обороты», «сальдо на конец» — отсутствовать может только «начало») или не удалось определить иерархию; в сообщении — лист и шаг |
|
| `error` / `INVALID_FORMAT` | лист ОСВ найден, но нет заголовка таблицы, обязательной группы сумм («обороты», «сальдо на конец» — отсутствовать может только «начало») или не удалось определить иерархию; в сообщении — лист и шаг |
|
||||||
| `error` / `UNREADABLE` | книга не открывается (повреждённый zip/BIFF); пишется в лог уровня WARNING |
|
| `error` / `UNREADABLE` | книга не открывается (повреждённый zip/BIFF); пишется в лог уровня WARNING |
|
||||||
@ -147,9 +147,11 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
|||||||
| `error` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` |
|
| `error` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` |
|
||||||
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение, одной строкой `Тип: сообщение`; стек — в лог уровня ERROR («Непредвиденная ошибка разбора: …») |
|
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение, одной строкой `Тип: сообщение`; стек — в лог уровня ERROR («Непредвиденная ошибка разбора: …») |
|
||||||
|
|
||||||
В `metrics.warnings` — диагностика, не влияющая на статус: `sheet=<имя> hierarchy=<стратегия>`, пропущенные листы
|
Диагностика, не влияющая на статус, — структурные предупреждения `{code, severity, message, locator?}`. Предупреждения
|
||||||
(`skipped: нет признаков ОСВ или формы`), несовпадение `file_type` с содержимым, свёрнутое
|
файла в `metrics.warnings`: пропущенный лист — `SHEET_OTHER` (`info`, `locator = sheet:<имя>`), несовпадение `file_type`
|
||||||
сальдо, нераспознанные числа с номером строки, непрочитанные поля шапки, «нет строк данных» (пустая ОСВ).
|
с содержимым — `FILE_TYPE_MISMATCH`, xls без кодовой страницы — `XLS_NO_CODEPAGE`. Предупреждения листа ОСВ — в
|
||||||
|
`warnings[]` его документа (`OSV_*`, `info`): стратегия иерархии (`OSV_HIERARCHY_BY_<СТРАТЕГИЯ>`), свёрнутое сальдо,
|
||||||
|
нераспознанные числа с номером строки, непрочитанные поля шапки, «нет строк данных» (пустая ОСВ).
|
||||||
|
|
||||||
### Контрольные суммы (§1.4)
|
### Контрольные суммы (§1.4)
|
||||||
|
|
||||||
@ -167,7 +169,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
|||||||
|
|
||||||
**ИНН контрагента** (OQ-100, OQ-130): `counterparty_inn` берётся из колонки ИНН (в том числе склеенного «ИНН/КПП»), затем из
|
**ИНН контрагента** (OQ-100, OQ-130): `counterparty_inn` берётся из колонки ИНН (в том числе склеенного «ИНН/КПП»), затем из
|
||||||
строки ИНН рядом с наименованием (любая из восьми раскладок, `inn_rows.py`, OQ-163: такая строка в `rows` не попадает, в
|
строки ИНН рядом с наименованием (любая из восьми раскладок, `inn_rows.py`, OQ-163: такая строка в `rows` не попадает, в
|
||||||
`metrics.warnings` — «строка N: ИНН … отнесён к контрагенту в строке M»; правило OQ-130 «строка без сумм над названием»
|
`warnings[]` документа (`OSV_INN_ROWS`) — «строка N: ИНН … отнесён к контрагенту в строке M»; правило OQ-130 «строка без сумм над названием»
|
||||||
заменено), затем из конца текста названия («Полиграф, 3528315710»,
|
заменено), затем из конца текста названия («Полиграф, 3528315710»,
|
||||||
«ООО Альфа (ИНН 7701234567)»; 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»). Из текста ИНН
|
«ООО Альфа (ИНН 7701234567)»; 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»). Из текста ИНН
|
||||||
отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст. Только для строк
|
отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст. Только для строк
|
||||||
@ -308,15 +310,19 @@ detail`). Статус книги и критерий `ok` документа в
|
|||||||
(`[=0]"-"`, `[=-646532]"(647)"`) дают прочерк или текст-литерал («(647)» -> -647). Формат `General` — сырое значение.
|
(`[=0]"-"`, `[=-646532]"(647)"`) дают прочерк или текст-литерал («(647)» -> -647). Формат `General` — сырое значение.
|
||||||
Формат, который не удалось разобрать (`%`, даты, дроби, экспонента), — сырое значение и одно предупреждение на
|
Формат, который не удалось разобрать (`%`, даты, дроби, экспонента), — сырое значение и одно предупреждение на
|
||||||
документ: `формат чисел не разобран, значения как в ячейках: «…»`. В xlsb числовые форматы не читаются (pyxlsb): значения
|
документ: `формат чисел не разобран, значения как в ячейках: «…»`. В xlsb числовые форматы не читаются (pyxlsb): значения
|
||||||
как в файле и одно предупреждение на документ `формат чисел xlsb не читается: значения как в файле`. Разбор ОСВ
|
как в файле и одно предупреждение на документ `формат чисел xlsb не читается: значения как в ячейках` (`XLSB_NO_FORMATS`). Разбор ОСВ
|
||||||
формат ячейки не применяет. При «тыс. руб.» и доле значений (по показываемым) с дробной частью не меньше
|
формат ячейки не применяет. При «тыс. руб.» и доле значений (по показываемым) с дробной частью не меньше
|
||||||
`fractional_share_percent` выдаётся одно предупреждение на документ; само оно на статус не влияет — `needs_check`
|
`fractional_share_percent` выдаётся одно предупреждение на документ: `FRACTIONAL_IN_THOUSANDS` в xlsx/xls — на статус
|
||||||
задают нарушенные соотношения (расхождение до `rounding_tolerance_percent` — мягкое нарушение, OQ-138).
|
не влияет, `XLSB_SCALE_SUSPECT` в xlsb (формат чисел не читается) — уровня `error`, документ `needs_check`; кроме того,
|
||||||
|
`needs_check` задают нарушенные соотношения (расхождение до `rounding_tolerance_percent` — мягкое нарушение, OQ-138).
|
||||||
- Страницы одной формы в разных файлах не склеиваются: каждая страница-продолжение без первой страницы остаётся
|
- Страницы одной формы в разных файлах не склеиваются: каждая страница-продолжение без первой страницы остаётся
|
||||||
одиноким продолжением и даёт `needs_check`.
|
одиноким продолжением и даёт `needs_check`.
|
||||||
- Лист с 1–2 кодами форм (меньше `min_codes`) формой не считается, если идёт первым; после листа формы он берётся
|
- Лист с 1–2 кодами форм (меньше `min_codes`) формой не считается, если идёт первым; после листа формы он берётся
|
||||||
как продолжение (`min_codes_continuation` словаря) и присоединяется, если совпало число колонок сумм и тип формы;
|
как продолжение (`min_codes_continuation` словаря) и присоединяется, если все его коды принадлежат типу открытой формы
|
||||||
иначе не присоединён, книга `needs_check`, предупреждение «лист с кодами форм (N < min_codes) не присоединён: …».
|
(число колонок сумм при этом не сверяется); лист с `min_codes` и более кодами без маркера присоединяется, только если
|
||||||
|
совпало и число колонок сумм (`sheet_joins` пакета). Иначе лист остаётся отдельным кандидатом с предупреждением
|
||||||
|
`SHEET_NOT_JOINED` (`error`): «лист «X» не присоединён к форме «Y»: коды или число колонок сумм не подходят»; книга
|
||||||
|
`needs_check`.
|
||||||
|
|
||||||
### Реальные формы: файл → статус
|
### Реальные формы: файл → статус
|
||||||
|
|
||||||
@ -344,7 +350,7 @@ detail`). Статус книги и критерий `ok` документа в
|
|||||||
| `templates/ОФР_после 01.01.2025.xlsx` | INCOME `needs_check` | шаблон |
|
| `templates/ОФР_после 01.01.2025.xlsx` | INCOME `needs_check` | шаблон |
|
||||||
|
|
||||||
Запуск на форме: `ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "<путь к файлу>"` из `api/` (см. «Запуск на файле»);
|
Запуск на форме: `ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "<путь к файлу>"` из `api/` (см. «Запуск на файле»);
|
||||||
в stdout — файл результата v1.2, у форм в `documents[]` — `header`, `lines[]`, `checks`.
|
в stdout — файл результата v2.0, у форм в `documents[]` — `header`, `lines[]`, `checks`.
|
||||||
|
|
||||||
## Тесты на реальных данных и golden-снимки
|
## Тесты на реальных данных и golden-снимки
|
||||||
|
|
||||||
@ -419,7 +425,7 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli <файл> [--file-type XL
|
|||||||
читает настройки сервиса (`OPENBAO__…`); для разбора они не используются, подойдёт и `api/.env` (см. выше).
|
читает настройки сервиса (`OPENBAO__…`); для разбора они не используются, подойдёт и `api/.env` (см. выше).
|
||||||
Симптом без окружения — `ValidationError: OPENBAO … Field required` при запуске: задайте `ENV_FILE=.env.test`.
|
Симптом без окружения — `ValidationError: OPENBAO … Field required` при запуске: задайте `ENV_FILE=.env.test`.
|
||||||
|
|
||||||
В stdout печатается JSON файла результата контракта v1.2 — тот же, что воркер кладёт в S3 под
|
В stdout печатается JSON файла результата контракта v2.0 — тот же, что воркер кладёт в S3 под
|
||||||
`results/{file_id}/{job_id}/result.json` (`job_id` и `file_id` — нулевые заглушки). Формат вывода: UTF-8 без
|
`results/{file_id}/{job_id}/result.json` (`job_id` и `file_id` — нулевые заглушки). Формат вывода: UTF-8 без
|
||||||
экранирования кириллицы, отступ 2 пробела, порядок полей — как в модели контракта, суммы — строки. `duration_ms` —
|
экранирования кириллицы, отступ 2 пробела, порядок полей — как в модели контракта, суммы — строки. `duration_ms` —
|
||||||
фактическая, поэтому меняется от запуска к запуску. При `status = error` печатается `{"status": "error", "error": {...}}`,
|
фактическая, поэтому меняется от запуска к запуску. При `status = error` печатается `{"status": "error", "error": {...}}`,
|
||||||
@ -436,25 +442,32 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli <файл> [--file-type XL
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema_version": "1.1",
|
"schema_version": "2.0",
|
||||||
"job_id": "00000000-0000-0000-0000-000000000000",
|
"job_id": "00000000-0000-0000-0000-000000000000",
|
||||||
"file_id": "00000000-0000-0000-0000-000000000000",
|
"file_id": "00000000-0000-0000-0000-000000000000",
|
||||||
"service": {
|
"service": {
|
||||||
"name": "tabular-parser",
|
"name": "tabular-parser",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0",
|
||||||
|
"statements_version": "3390f92daddbfd4919d7f799ad2959cc35bfbd92"
|
||||||
},
|
},
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"duration_ms": 2,
|
"duration_ms": 3,
|
||||||
"rows": 9,
|
"rows": 9,
|
||||||
"warnings": [
|
"warnings": []
|
||||||
"sheet=Лист_1 hierarchy=outline"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"documents": [
|
"documents": [
|
||||||
{
|
{
|
||||||
"document_type": "OSV",
|
"document_type": "OSV",
|
||||||
"locator": "sheet:Лист_1",
|
"locator": "sheet:Лист_1",
|
||||||
|
"status": "done",
|
||||||
|
"warnings": [
|
||||||
|
{
|
||||||
|
"code": "OSV_HIERARCHY_BY_OUTLINE",
|
||||||
|
"severity": "info",
|
||||||
|
"message": "иерархия строк — по группировке листа (outline)"
|
||||||
|
}
|
||||||
|
],
|
||||||
"header": {
|
"header": {
|
||||||
"organization_name": "АО \"Русский бисквит\"",
|
"organization_name": "АО \"Русский бисквит\"",
|
||||||
"organization_inn": null,
|
"organization_inn": null,
|
||||||
@ -463,6 +476,9 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli <файл> [--file-type XL
|
|||||||
"period_year": 2024,
|
"period_year": 2024,
|
||||||
"period_quarter": "Q4",
|
"period_quarter": "Q4",
|
||||||
"is_cumulative": true,
|
"is_cumulative": true,
|
||||||
|
"period_start": "2024-01-01",
|
||||||
|
"period_end": "2024-12-31",
|
||||||
|
"is_from_year_start": true,
|
||||||
"data_type": "BU",
|
"data_type": "BU",
|
||||||
"currency": "RUB",
|
"currency": "RUB",
|
||||||
"header_variant": 1
|
"header_variant": 1
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Стратегии — чистые функции над `BodyRow` (строки данных без листа): `outline` → `indent` → `account_pattern` →
|
Стратегии — чистые функции над `BodyRow` (строки данных без листа): `outline` → `indent` → `account_pattern` →
|
||||||
`sums`. Каждая возвращает уровни `{row: level}` или `None`, если неприменима; первая, давшая непротиворечивое дерево
|
`sums`. Каждая возвращает уровни `{row: level}` или `None`, если неприменима; первая, давшая непротиворечивое дерево
|
||||||
(`is_consistent`), побеждает, её имя попадает в `Levels.strategy` (`hierarchy=<имя>` в `metrics.warnings`).
|
(`is_consistent`), побеждает, её имя попадает в `Levels.strategy` (`OSV_HIERARCHY_BY_<ИМЯ>` в `warnings[]` документа).
|
||||||
|
|
||||||
Уровни и родители считаются по порядку строк и уровням, а не по номерам и текстам: два одноимённых контрагента под
|
Уровни и родители считаются по порядку строк и уровням, а не по номерам и текстам: два одноимённых контрагента под
|
||||||
разными субсчетами получают разных родителей (Review Focus 3). Строка «Итого» в стратегии не передаётся: она —
|
разными субсчетами получают разных родителей (Review Focus 3). Строка «Итого» в стратегии не передаётся: она —
|
||||||
|
|||||||
122
openspec/changes/unified-normalization-tp/design.md
Normal file
122
openspec/changes/unified-normalization-tp/design.md
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# Design: unified-normalization-tp
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Change переносит в tabular-parser требования общей нормализации ББ/ОФР: шаг 1 (кандидат в форму из книги Excel)
|
||||||
|
пишет адаптер сервиса, шаги 2–8 выполняет копия пакета `arm_statements`, результат и конверт — контракт v2.0.
|
||||||
|
Дизайн и решения Р-1…Р-24 — change `unified-statement-normalization` в arm-ka-docs (issue RSHB/arm-ka-docs#116);
|
||||||
|
реализация — план 4 (`plans/2026-10-18-plan-4-tabular-parser.md` в docs), issue RSHB/arm-tabular-parser#54, ветка
|
||||||
|
`feat/54-statements-v2`.
|
||||||
|
|
||||||
|
Номера P4-n — решения автора плана, R-n — рулинги контроллера при исполнении (нумерация сквозная, R-3 не присваивался).
|
||||||
|
Дата всех решений ниже — 18.10.2026.
|
||||||
|
|
||||||
|
## Decisions made without discussion
|
||||||
|
|
||||||
|
### Решения автора плана (P4-n)
|
||||||
|
|
||||||
|
- **P4-1.** Переход на общий код и на контракт v2.0 — одна задача (7): модели, копия схем, сборка книги, путь ОСВ и
|
||||||
|
воркер меняются вместе; «контракт v2.0 в копии» отдельной последней задачей не вынесен — модели 2.0 без копии 2.0 (или
|
||||||
|
наоборот) валят `result_contract` и тест идентичности. Golden и манифест — сразу за переключением (задача 8), до
|
||||||
|
удаления старого кода; между задачами 7 и 8 проверка идёт с `ARM_KA_FIXTURES=/nonexistent`. Цена: одна крупная задача и
|
||||||
|
окно в одну задачу без проверки на реальных файлах.
|
||||||
|
- **P4-2.** Адаптер пишется новыми модулями рядом со старыми (`cells`, `forms`, `header_cells`, `adapter`, `contract`,
|
||||||
|
`pipeline`); старые `statements/*` не правятся и удаляются целиком задачей 9. Цена: временное дублирование логики
|
||||||
|
поиска блоков и шапки.
|
||||||
|
- **P4-3.** `Cell.text` числа Excel — как в русской локали: группы по три через пробел, если в формате есть разделитель
|
||||||
|
групп; десятичная запятая; литералы секции (скобки) сохраняются; «-» добавляется только у односекционного формата;
|
||||||
|
`General`/неразобранный формат/xlsb — число без групп и экспоненты, целое без дробной части; дата — `dd.mm.yyyy`;
|
||||||
|
логическое — «ИСТИНА»/«ЛОЖЬ». Цена: у экзотических форматов текст может отличаться от экрана Excel; `value` и статус
|
||||||
|
от этого не зависят (у Excel значение берётся из `value`). Совпадает с примером docs
|
||||||
|
`result-statement-excel.json` (`"586 260,26"`).
|
||||||
|
- **P4-4.** Показываемое число вне `numeric(18,2)` больше не делает формат «неразобранным»: адаптер отдаёт `Decimal`, а
|
||||||
|
`VALUE_OUT_OF_RANGE` и `amount = null` выдаёт пакет (спека «Шаг 4»). Цена: правка поведения #42 для чисел больше 16
|
||||||
|
целых цифр (на реальных файлах их нет).
|
||||||
|
- **P4-5.** Сетка кандидата: строки блока целиком, включая пустые; объединённая ячейка — только в позиции левой верхней,
|
||||||
|
прочие позиции `None`; ширина — наибольшая среди листов кандидата. Цена: подпись, объединённая на два столбца, видна
|
||||||
|
пакету только в левом.
|
||||||
|
- **P4-6.** Шапка ищется в строках блока выше первой строки с кодом его столбца кодов (столбец — `code_column` пакета:
|
||||||
|
то же правило (а)/(б) шага 2 с защитой от блока «Коды», своего правила у адаптера нет), без кодов — во всём блоке: так титульный блок «Коды» (ОКОПФ 12267) не мешает, а подписи под формой (ИНН и ФИО в блоке
|
||||||
|
подписей) не попадают в шапку. Цена: метка шапки ниже начала таблицы не найдётся.
|
||||||
|
- **P4-7.** Шаблоны словаря пакета адаптер применяет к тексту, нормализованному функцией пакета по таблице «Словари»
|
||||||
|
раздела API плана 1: метки шапки, заголовки, ОКУД, `auditor`, `legal_form` — `normalize_label` (= прежний
|
||||||
|
`normalize_text` tabular-parser, P1-22); подпись «Код» и относительные периоды — `normalize_caption`; маркер
|
||||||
|
продолжения — `is_continuation`. Цена: нет — смена нормализации в пакете меняет и `type_hint`, и адаптер.
|
||||||
|
- **P4-8.** Склейка: открытая форма — последний кандидат с `type_hint` `BALANCE`/`INCOME`; лист с маркером сверяется только
|
||||||
|
по типу; лист без маркера (сильное продолжение — столбец кодов по правилу шага 2 или своя строка заголовка; слабое —
|
||||||
|
столбец кодов ослабленным поиском `code_column(…, relaxed=<тип формы>)`, минимум `min_codes_continuation` кодов,
|
||||||
|
сразу после листа формы) — `sheet_joins` с числом колонок сумм `len(amount_columns(...))` пакета («столбцы правее
|
||||||
|
столбца кодов, где в строках с кодами есть число»); открытой формы нет — отдельный кандидат без предупреждения (пакет даст `NO_ORGANIZATION_NO_INN`).
|
||||||
|
Цена: «Изменение, %» с числами считается колонкой сумм с обеих сторон сравнения; лист с иной шапкой, но тем же числом
|
||||||
|
колонок, присоединится и будет сопоставлен пакетом по датам.
|
||||||
|
- **P4-9.** `SHEET_OTHER` для листов «прочее» выдаёт книга сама (документа и кандидата нет), а не пакет по кандидату без
|
||||||
|
`type_hint`. Цена: два места порождения одного кода (у пакета — для кандидатов, не нашедших ни типа, ни кодов).
|
||||||
|
- **P4-10.** `XLSB_NO_FORMATS` — у каждого кандидата книги xlsb (P1-5); `NUMBER_FORMAT_UNPARSED` — одно на кандидата с
|
||||||
|
перечнем форматов всех его листов. Цена: у пояснений xlsb тоже есть `XLSB_NO_FORMATS` (`info`).
|
||||||
|
- **P4-11.** Книга вызывает `normalize_candidate` по одному кандидату, а не `normalize`: так проверяется дедлайн между
|
||||||
|
кандидатами (`header.form_name` пакет ставит сам из `evidence.title`, пара «кандидат → документ» не нужна).
|
||||||
|
Исключение пакета на кандидате — `RECOGNITION_FAILED` всей книги, как сейчас (`TABLE_PARSE_ERROR` — код таблицы ИС).
|
||||||
|
Цена: одна сломанная форма роняет книгу с ОСВ.
|
||||||
|
- **P4-12.** Коды ОСВ — по источнику предупреждения, не по тексту: `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>`, `OSV_HEADER`,
|
||||||
|
`OSV_ROWS`, `OSV_INN_ROWS`, `OSV_NO_DATA_ROWS`, `OSV_BALANCE_IDENTITY`, `OSV_PARENT_SUM`, `OSV_TOTAL`,
|
||||||
|
`OSV_XLSB_NO_OUTLINE`; тексты сообщений прежние, кроме иерархии. Цена: внутри `OSV_HEADER` и
|
||||||
|
`OSV_ROWS` разные случаи различаются только текстом.
|
||||||
|
- **P4-13.** Заметки читателя: несовпадение формата — `FILE_TYPE_MISMATCH` файла; «xls без кодовой страницы» —
|
||||||
|
`XLS_NO_CODEPAGE` файла (`info`, код пакета, P1-23) в любой книге xls — касается и форм, и ОСВ; «xlsb без outline» — в
|
||||||
|
каждый документ ОСВ книги (`OSV_XLSB_NO_OUTLINE`), в книге без ОСВ только логируется (иерархия строк — только у ОСВ).
|
||||||
|
Цена: предупреждение о кодировке видно в карточке файла, а не у листа ОСВ.
|
||||||
|
- **P4-14.** Предупреждение «форма не разобрана: книга завершилась ошибкой листа ОСВ» снято: в v2.0 для него нет кода, а
|
||||||
|
текст ошибки и так называет лист ОСВ. Цена: аналитик не видит списка листов форм, оставшихся неразобранными.
|
||||||
|
- **P4-15.** Статус документа ОСВ — прежние условия листа: контрольные суммы §1.4 сошлись и строки ИНН без нарушений
|
||||||
|
(`glue.ok`, #35). Цена: формулировка спеки «когда контрольные суммы сошлись» читается шире.
|
||||||
|
- **P4-16.** Нет `api/arm_statements/VERSION` — сервис не стартует (`RuntimeError`), как при ошибке словаря; в golden
|
||||||
|
`service.statements_version` — volatile-поле. Цена: копия без `VERSION` не запустится даже локально.
|
||||||
|
- **P4-17.** Новые синтетические случаи (форма xlsb, книга из пояснений, конфликты продолжений) — в памяти в юнит-тестах
|
||||||
|
(`sheet_from_rows`, `write_xlsb`), новых файлов в `fixtures/statements-xlsx/synthetic` нет; PR fixtures — только golden.
|
||||||
|
- **P4-18.** Манифест получает необязательное поле `error_warnings` (точное множество кодов `error` документа);
|
||||||
|
проверка уникальности кодов строк снята (повторы законны, Р-4). Цена: при правке пакета, меняющей набор
|
||||||
|
предупреждений, манифест правится вместе с golden.
|
||||||
|
- **P4-19.** Тест покрытия словарей tabular-parser больше не охватывает словари форм — они в пакете. Цена: «мёртвый»
|
||||||
|
синоним заголовка или метки шапки на реальных файлах не ловится (O-8).
|
||||||
|
- **P4-20.** В tabular-parser — свой change `unified-normalization-tp` с дельтами `statement-parsing`,
|
||||||
|
`tabular-recognition-service`, `osv-parsing` (по образцу P0-5); требование «Копия контракта идентична источнику» правит
|
||||||
|
план 0, здесь не трогается.
|
||||||
|
- **P4-21.** Golden — отдельные issue, ветка и PR в RSHB/arm-ka-fixtures (один PR на репозиторий); сливается сразу за PR
|
||||||
|
tabular-parser. Цена: короткое окно, когда `make check` `main` одного из них красный на golden.
|
||||||
|
- **P4-22.** Две формы на одном листе дают два документа с одинаковым `locator` (спека уникальности `locator` не требует;
|
||||||
|
`ref` строк различаются).
|
||||||
|
|
||||||
|
### Рулинги контроллера при исполнении (R-n)
|
||||||
|
|
||||||
|
- **R-1.** docs-worktree отсоединён на origin/main (docs #117 слит), а не на feat/116 (П-1/П-5 плана). Цена ошибки: нет.
|
||||||
|
- **R-2.** Предупреждение tp #47 «xls без кодовой страницы…» — `XLS_NO_CODEPAGE` пакета (info, файл, без locator); нового
|
||||||
|
`OSV_*`-кода нет; текст сообщения reader'а tp #47 сохранён; передаётся флагом `BookRead.no_codepage`. Цена: правка
|
||||||
|
`book.py` и golden.
|
||||||
|
- **R-4.** Задача 1 (markdown-дельты) без отдельного task-ревью — проверена `openspec validate --strict` и финальным
|
||||||
|
ревью ветки. Цена: неточность формулировки дельты.
|
||||||
|
- **R-5.** Проверка диапазона numeric(18,2) в `display_amount` не возвращается (P4-4); диапазон и `VALUE_OUT_OF_RANGE`
|
||||||
|
выдаёт пакет; старый конвейер удалён задачей 9. Цена: нет после удаления.
|
||||||
|
- **R-6.** Для |raw| ≥ 1e15 текст/значение числа без округления до 15 значащих цифр (иначе число из 17 цифр теряет
|
||||||
|
цифры); пробел групп — обычный U+0020 как в примере docs `result-statement-excel.json`. Цена: расхождение с NBSP
|
||||||
|
русского Excel, на value/статус не влияет.
|
||||||
|
- **R-7.** Ячейки из одних цифр не считаются подзаголовком даты `report_date` (иначе год из тройки «число|месяц|год»
|
||||||
|
принимается за подзаголовок), как в прежнем `header.py`. Цена: подзаголовок из голого числа «2025» не распознаётся.
|
||||||
|
- **R-8.** Ячейка в строке-шапке таблицы (в строке есть подпись «Код» по словарю пакета `code_caption`) не заголовок
|
||||||
|
формы: подпись колонки «Пояснения» (`'^пояснени'` в `form_titles` пакета) делала лист-продолжение НПЗ ещё одной формой
|
||||||
|
DISCLOSURE. Дефект адаптера (спека называет «Пояснения» подписью колонки), пакет не правится. Решение владельца:
|
||||||
|
оставить, посмотреть позже. Цена: компактная шапка, где заголовок формы и «Код» в одной строке, потеряет заголовок.
|
||||||
|
- **R-9.** Предупреждения адаптера при кандидате без документа — предупреждения файла со своим `locator`; порядок
|
||||||
|
`metrics.warnings` — сначала reader, затем по номеру первого листа; `XLS_NO_CODEPAGE` и `OSV_XLSB_NO_OUTLINE` — флаги
|
||||||
|
`BookRead` (`no_codepage`, `no_outline`). Цена: правка `book.py` и golden.
|
||||||
|
- **R-10.** НПЗ, ОФР, строки «Реализация иного имущества, прав требования, прочих работ и услуг» (F34, F44): значения 0
|
||||||
|
с форматом `[=0]"-"` — только прочерки, суммы нет; по спеке «строка без кода и без содержимого — не строка данных»,
|
||||||
|
пакет её не выдаёт; значение прочерка не превращаем в 0 (сломало бы различие null/0 и соотношения §7).
|
||||||
|
`test_npz_documents` не требует detail/parent_ref 2340. Вопрос владельцу O-8.2: сценарий спеки «Реализация иного
|
||||||
|
имущества… с суммами» не соответствует файлу. Цена: строки из прочерков не видны аналитику.
|
||||||
|
- **R-11.** Задача 9 (удаление по списку) без отдельного task-ревью — покрыта финальным ревью ветки.
|
||||||
|
- **R-12.** Остановка Р-31 по Межениновской (посторонние формулы аналитика в столбцах U–W) снята владельцем 18.10.2026:
|
||||||
|
правило в пакете (docs #123, Р-44; arm-statements #6, 3390f92) — столбец без подписи правее последней подписанной
|
||||||
|
колонки с числами не колонка сумм, предупреждение `NUMBERS_OUTSIDE_TABLE` (info). Копия пакета обновлена коммитом
|
||||||
|
f5d071f. Цена: нет.
|
||||||
|
- **R-13.** Ложная склейка/дедлайн: `plan_sheets` получает `deadline` (финальное ревью I-1, регрессия таймаута против
|
||||||
|
main). Цена: нет.
|
||||||
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
- Код: `api/src/parser/statements/{cells,forms,header_cells,adapter,pipeline}.py` (новые), `grid.py`, `detect.py`,
|
- Код: `api/src/parser/statements/{cells,forms,header_cells,adapter,pipeline}.py` (новые), `grid.py`, `detect.py`,
|
||||||
`book.py`, `readers/`, `osv.py`, `checks.py`, `osv_warnings.py`; `api/src/contract/**` (модели v2.0), `api/contracts/**`
|
`book.py`, `readers/`, `osv.py`, `checks.py`, `osv_warnings.py`; `api/src/contract/**` (модели v2.0), `api/contracts/**`
|
||||||
(копия схем v2.0), `api/src/core/{worker,config}.py`, `api/src/app.py`; удаляются `statements/{checks,columns,detect,header,lines,merge,statement}.py`
|
(копия схем v2.0), `api/src/core/worker.py`, `api/src/app.py`; удаляются `statements/{checks,columns,detect,header,lines,merge,statement}.py`
|
||||||
и `api/rules/statement_{headers,periods,checks}.yaml`.
|
и `api/rules/statement_{headers,periods,checks}.yaml`.
|
||||||
- Копия пакета `api/arm_statements/` (раскладывает `sync-statements`) — не правится.
|
- Копия пакета `api/arm_statements/` (раскладывает `sync-statements`) — не правится.
|
||||||
- Тесты: манифест `api/tests/statements_manifest.yaml`; golden `fixtures/golden/expected_{osv,statement}_parse/**` (отдельный PR fixtures, arm-tabular-parser#54).
|
- Тесты: манифест `api/tests/statements_manifest.yaml`; golden `fixtures/golden/expected_{osv,statement}_parse/**` (отдельный PR fixtures, arm-tabular-parser#54).
|
||||||
|
|||||||
@ -108,6 +108,11 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
|||||||
- **WHEN** запрос говорит `XLSX`, а байты — BIFF8
|
- **WHEN** запрос говорит `XLSX`, а байты — BIFF8
|
||||||
- **THEN** файл прочитан как xls, в `metrics.warnings` — `FILE_TYPE_MISMATCH` (`info`), статус по содержимому
|
- **THEN** файл прочитан как xls, в `metrics.warnings` — `FILE_TYPE_MISMATCH` (`info`), статус по содержимому
|
||||||
|
|
||||||
|
#### Scenario: xls без кодовой страницы
|
||||||
|
- **WHEN** разбирается xls без записи CODEPAGE (синтетика, `codepage=None`)
|
||||||
|
- **THEN** строки прочитаны как cp1251, в `metrics.warnings` — предупреждение файла `XLS_NO_CODEPAGE` (`info`, без
|
||||||
|
`locator`; код пакета, `OSV_`-кода для него нет); статус по содержимому
|
||||||
|
|
||||||
#### Scenario: Превышен лимит
|
#### Scenario: Превышен лимит
|
||||||
- **WHEN** передан файл в 51 МиБ
|
- **WHEN** передан файл в 51 МиБ
|
||||||
- **THEN** `error UNSUPPORTED_FORMAT`, книга не открывается
|
- **THEN** `error UNSUPPORTED_FORMAT`, книга не открывается
|
||||||
@ -130,3 +135,66 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
|||||||
#### Scenario: Распакованный zip больше лимита
|
#### Scenario: Распакованный zip больше лимита
|
||||||
- **WHEN** zip книги распаковывается больше чем в 10 × `PARSE_MAX_BYTES`
|
- **WHEN** zip книги распаковывается больше чем в 10 × `PARSE_MAX_BYTES`
|
||||||
- **THEN** `error UNSUPPORTED_FORMAT` («распакованный размер … больше лимита»), книга не открывается
|
- **THEN** `error UNSUPPORTED_FORMAT` («распакованный размер … больше лимита»), книга не открывается
|
||||||
|
|
||||||
|
### Requirement: Листы ОСВ находятся по содержимому
|
||||||
|
Парсер SHALL считать листом ОСВ любой лист, где ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости
|
||||||
|
всегда над таблицей) матчит признак ОСВ из `rules/osv_headers.yaml` (без учёта регистра, «ё/е», переносов строк и
|
||||||
|
дефисов). Лист с признаками формы ББ/ОФР или её продолжения уходит в конвейер форм (спека `statement-parsing`); прочие листы
|
||||||
|
SHALL пропускаться с предупреждением файла `SHEET_OTHER` (`severity = info`, `locator = sheet:<имя>`) в
|
||||||
|
`metrics.warnings`. Книга без единого листа ОСВ и без форм → `status = error`, `error.code = INVALID_DOCUMENT`
|
||||||
|
(«в книге нет листов ОСВ или форм»). Несколько листов ОСВ → несколько `documents` с `locator = sheet:<имя>`.
|
||||||
|
Имя листа MUST NOT использоваться для поиска.
|
||||||
|
|
||||||
|
#### Scenario: Один лист — одна ОСВ (все реальные фикстуры)
|
||||||
|
- **WHEN** разбирается любой файл из `fixtures/osv/**`
|
||||||
|
- **THEN** `documents` содержит ровно один `osvDocument` с `locator = "sheet:Лист_1"`, статус `done`
|
||||||
|
|
||||||
|
#### Scenario: Два листа ОСВ и посторонний лист
|
||||||
|
- **WHEN** разбирается синтетический файл с листами «ОСВ 60», «Справка», «ОСВ 62»
|
||||||
|
- **THEN** `documents` содержит два документа в порядке листов, в `metrics.warnings` есть `SHEET_OTHER` с `locator = "sheet:Справка"`
|
||||||
|
|
||||||
|
#### Scenario: Нет ОСВ
|
||||||
|
- **WHEN** разбирается книга с одним листом произвольной таблицы
|
||||||
|
- **THEN** `status = error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
|
||||||
|
|
||||||
|
### Requirement: Иерархия строк — каскад стратегий
|
||||||
|
Парсер SHALL определять `level` строк данных первой подходящей стратегией из списка: outline level Excel; отступ ячейки
|
||||||
|
или ведущие пробелы; шаблон кода счёта и жирность; равенство «сумма детей = родитель». Стратегия подходит, если даёт дерево,
|
||||||
|
где у каждой строки есть предок с меньшим уровнем (кроме уровня 0). Строка «Итого» стратегиям MUST NOT передаваться:
|
||||||
|
она всегда имеет уровень 0 и не имеет родителя (R-14). Если строк данных (кроме «Итого») меньше двух (пустая ОСВ или одна
|
||||||
|
строка) — стратегия `trivial`: дерево из корней (R-13). Выбранная стратегия SHALL быть записана в `warnings[]` документа листа предупреждением
|
||||||
|
`OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`severity = info`); в `metrics.warnings` строк `sheet=<имя> hierarchy=…` нет. `parent_row_number` SHALL указывать на ближайшую
|
||||||
|
предыдущую строку с меньшим уровнем.
|
||||||
|
|
||||||
|
#### Scenario: Outline level (реальные файлы)
|
||||||
|
- **WHEN** разбирается `rb/2024-FY/ОСВ_66 за 2024.xls` (и любой другой реальный файл со строками данных)
|
||||||
|
- **THEN** предупреждение документа `OSV_HIERARCHY_BY_OUTLINE`, строки `66` (0) → `66.01` (1) → контрагент (2); у «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ»
|
||||||
|
`parent_row_number` — строка `66.03`
|
||||||
|
|
||||||
|
#### Scenario: Без outline level
|
||||||
|
- **WHEN** разбирается синтетическая ОСВ без группировок Excel, с отступами ячеек (`s02`)
|
||||||
|
- **THEN** `OSV_HIERARCHY_BY_INDENT`, дерево 0/1/2 и суммы совпадают с деревом той же ведомости, где уровни видны только по
|
||||||
|
кодам счетов и жирности (`s03`)
|
||||||
|
|
||||||
|
#### Scenario: Без outline и отступов
|
||||||
|
- **WHEN** разбирается синтетическая ОСВ, где уровни видны только по кодам счетов и жирности (`s03`)
|
||||||
|
- **THEN** `OSV_HIERARCHY_BY_ACCOUNT_PATTERN`, инварианты §1.4 выполнены, «Итого» не вложена в последний счёт
|
||||||
|
|
||||||
|
#### Scenario: Пустая ОСВ — тривиальное дерево
|
||||||
|
- **WHEN** в ОСВ нет строк данных кроме «Итого»
|
||||||
|
- **THEN** `OSV_HIERARCHY_BY_TRIVIAL`, «Итого» — уровень 0 без родителя
|
||||||
|
|
||||||
|
### Requirement: Золотые снимки и property-тесты
|
||||||
|
Для каждого файла `fixtures/osv/**` SHALL существовать снимок `fixtures/golden/expected_osv_parse/<зеркальный путь>.json`
|
||||||
|
(= файл результата без `job_id`, `file_id`, `service.version`, `service.statements_version`, `metrics.duration_ms`); тест сравнивает разбор со снимком,
|
||||||
|
обновление — только явной командой (`make golden-update`) с просмотром diff. Property-тест по всем файлам SHALL проверять
|
||||||
|
инварианты независимо от снимков (статус ≠ `error`, контрольные суммы, корень = счёт шапки, период распознан). Без
|
||||||
|
каталога fixtures оба теста пропускаются с причиной (`ARM_KA_FIXTURES`).
|
||||||
|
|
||||||
|
#### Scenario: Регрессия
|
||||||
|
- **WHEN** правка парсера меняет `level` одной строки в любом реальном файле
|
||||||
|
- **THEN** golden-тест падает с diff по этому файлу
|
||||||
|
|
||||||
|
#### Scenario: Новый файл без снимка
|
||||||
|
- **WHEN** в `fixtures/osv/` добавлен файл без снимка
|
||||||
|
- **THEN** property-тест его разбирает, golden-тест сообщает «нет снимка: выполните make golden-update»
|
||||||
|
|||||||
@ -46,7 +46,7 @@ NATS и S3; воркер MUST NOT зависеть от конкретной р
|
|||||||
(`error NOT_IMPLEMENTED`) SHALL использоваться только в тестах воркера и MUST NOT быть реализацией по умолчанию.
|
(`error NOT_IMPLEMENTED`) SHALL использоваться только в тестах воркера и MUST NOT быть реализацией по умолчанию.
|
||||||
Реализация по умолчанию SHALL создаваться один раз при старте приложения (словари ОСВ читаются и проверяются там же,
|
Реализация по умолчанию SHALL создаваться один раз при старте приложения (словари ОСВ читаются и проверяются там же,
|
||||||
словари форм — `default_rules()` пакета). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`, `osv_groups`,
|
словари форм — `default_rules()` пакета). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`, `osv_groups`,
|
||||||
`periods`, `data_types`) и README; словарей форм ББ/ОФР в сервисе нет — они часть копии `arm_statements`.
|
`periods`, `data_types`, `osv_checks`) и README; словарей форм ББ/ОФР в сервисе нет — они часть копии `arm_statements`.
|
||||||
|
|
||||||
#### Scenario: Подмена реализации в тесте
|
#### Scenario: Подмена реализации в тесте
|
||||||
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
|
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
|
||||||
|
|||||||
@ -1,21 +1,27 @@
|
|||||||
## 1. Change и подготовка
|
## 1. Change и подготовка
|
||||||
|
|
||||||
- [ ] 1.1 OpenSpec change `unified-normalization-tp`: proposal, tasks, дельты `statement-parsing`, `tabular-recognition-service`, `osv-parsing` (задача 1)
|
- [x] 1.1 OpenSpec change `unified-normalization-tp`: proposal, tasks, дельты `statement-parsing`, `tabular-recognition-service`, `osv-parsing` (задача 1)
|
||||||
- [ ] 1.2 Подготовка хоста: сверка копии `arm_statements` с планом 2 и её свежесть (`make statements-check`) (задача 2)
|
- [x] 1.2 Подготовка хоста: сверка копии `arm_statements` с планом 2 и её свежесть (`make statements-check`) (задача 2)
|
||||||
|
|
||||||
## 2. Адаптер Excel шага 1
|
## 2. Адаптер Excel шага 1
|
||||||
|
|
||||||
- [ ] 2.1 Ячейка Excel → `Cell`: `ref = <лист>!<A1>`, текст по числовому формату, плотная сетка (задача 3)
|
- [x] 2.1 Ячейка Excel → `Cell`: `ref = <лист>!<A1>`, текст по числовому формату, плотная сетка (задача 3)
|
||||||
- [ ] 2.2 Блоки форм на листе, вид листа, шапка блока на словарях пакета (задача 4)
|
- [x] 2.2 Блоки форм на листе, вид листа, шапка блока на словарях пакета (задача 4)
|
||||||
- [ ] 2.3 Адаптер книги → `FormCandidate`: классификация листов, склейка продолжений, `segment_breaks`, предупреждения (задача 5)
|
- [x] 2.3 Адаптер книги → `FormCandidate`: классификация листов, склейка продолжений, `segment_breaks`, предупреждения (задача 5)
|
||||||
|
|
||||||
## 3. ОСВ и переход на контракт v2.0
|
## 3. ОСВ и переход на контракт v2.0
|
||||||
|
|
||||||
- [ ] 3.1 Предупреждения ОСВ с кодами `OSV_*` (вывод пока прежний) (задача 6)
|
- [x] 3.1 Предупреждения ОСВ с кодами `OSV_*` (вывод пока прежний) (задача 6)
|
||||||
- [ ] 3.2 Переключение на общий код и контракт v2.0: модели, копия схем, сборка книги, путь ОСВ, воркер, `statements_version` (задача 7)
|
- [x] 3.2 Переключение на общий код и контракт v2.0: модели, копия схем, сборка книги, путь ОСВ, воркер, `statements_version` (задача 7)
|
||||||
|
|
||||||
## 4. Регрессия и очистка
|
## 4. Регрессия и очистка
|
||||||
|
|
||||||
- [ ] 4.1 Golden и манифест реальных форм (задача 8)
|
- [x] 4.1 Golden и манифест реальных форм (задача 8)
|
||||||
- [ ] 4.2 Удаление собственного кода шагов 2–8 и словарей форм (задача 9)
|
- [x] 4.2 Удаление собственного кода шагов 2–8 и словарей форм (задача 9)
|
||||||
- [ ] 4.3 Полная проверка `make check-full`, PR tabular-parser и PR fixtures (задача 10)
|
- [ ] 4.3 Полная проверка `make check-full`, PR tabular-parser и PR fixtures (задача 10)
|
||||||
|
|
||||||
|
## 5. При архивации
|
||||||
|
|
||||||
|
- [ ] 5.1 После `openspec archive` вручную поправить Purpose главных спек: `statement-parsing` — «контракт v1.2,
|
||||||
|
`rules/statement_*.yaml`» → контракт v2.0, словари форм в пакете `arm_statements`; `osv-parsing` — «`osvDocument`
|
||||||
|
контракта v1.1» → v2.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user