tabular-parser: столбец кодов и колонки сумм — code_column / amount_columns пакета, метки — normalize_label / normalize_caption, маркер — is_continuation, контракт — to_contract / warning_to_contract (statements/contract.py не пишется), XLS_NO_CODEPAGE — предупреждение файла вместо OSV_XLS_NO_CODEPAGE; scan-gateway: contract_map.py не пишется, form_name из пакета, WarningCode в make_warning, FailedRelation.expected/actual — Money | None. Закрыты O-1…O-7, O-9 плана 4 и O-2…O-7 плана 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
1134 lines
100 KiB
Markdown
1134 lines
100 KiB
Markdown
# План 4 — tabular-parser: адаптер Excel шага 1, общий код `arm_statements`, контракт v2.0. План реализации
|
||
|
||
> **Для исполнителей-агентов:** обязательный навык — superpowers:subagent-driven-development (в плане 10 задач: по
|
||
> `~/.claude/CLAUDE.md` — SDD, контроллер — фоновый сабагент на модели sonnet; модели исполнителей указаны в каждой
|
||
> задаче; финальное ревью ветки — opus). Шаги размечены чекбоксами (`- [ ]`).
|
||
|
||
**Goal:** tabular-parser разбирает формы ББ/ОФР из Excel так: свой код читает книгу, классифицирует листы, выделяет блоки
|
||
форм и склеивает листы-продолжения в `FormCandidate` (адаптер шага 1), а шаги 2–8 выполняет копия пакета
|
||
`arm_statements`; результат и конверт — контракт v2.0 (у ОСВ — `status` и структурные предупреждения `OSV_*`); собственный
|
||
код шагов 2–8 форм и словари форм удалены; манифест реальных форм и golden обновлены.
|
||
|
||
**Architecture:** новые модули адаптера пишутся рядом со старыми (`statements/cells.py`, `forms.py`, `header_cells.py`,
|
||
`adapter.py`, `pipeline.py`); переключение книги на пакет и на контракт v2.0 — одной задачей (модели,
|
||
копия схем, сборка книги, путь ОСВ, воркер), сразу за ней — golden и манифест, затем удаление старого кода форм. Пакет
|
||
вызывается только через публичный API (`from arm_statements import …`); ячейка Excel → `Cell` (текст по числовому формату,
|
||
`value`, `ref = <лист>!<A1>`) — в адаптере; разбор ОСВ не меняется, меняется только вид его предупреждений и статус листа.
|
||
|
||
**Tech Stack:** Python 3.11, pydantic v2, openpyxl / xlrd / pyxlsb (читатели как есть), PyYAML, pytest, ruff, mypy;
|
||
копия пакета `api/arm_statements/` (stdlib + PyYAML); `tea` (Gitea), OpenSpec, codegraph.
|
||
|
||
**Spec:** `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/`
|
||
(ветка `feat/116-unified-normalization` репозитория arm-ka-docs): главное — `specs/tabular-recognition-service/spec.md`;
|
||
шаг 1 для Excel — `specs/statement-normalization/spec.md` («Шаг 1 — кандидат в форму», «Шаг 1 — шапка кандидата», «Шаг 1 —
|
||
многостраничная форма», «Шаг 8 — предупреждения», «Шаг 8 — статус документа и конверта», «Шаг 4 — единицы и дробные
|
||
значения»); контракт — `specs/recognition-contract/spec.md` и `contracts/*.schema.json` v2.0, `contracts/examples/*.json`;
|
||
правила — `context/05-domain-rules.md` §1.6, §11.2, §11.9; порядок выкладки — `proposal.md` (сервисы переходят на 2.0
|
||
после ядра, шаг 5). API пакета — раздел «Публичный API пакета» плана 1 (`plans/2026-10-18-plan-1-arm-statements.md`):
|
||
только эти имена. `design.md` — справка (Р-n, S-n); спека важнее.
|
||
|
||
---
|
||
|
||
## Место в работе и условия старта
|
||
|
||
| План | Что | Связь с этим планом |
|
||
|---|---|---|
|
||
| 0 | тест идентичности копий контракта по MAJOR (в т. ч. tabular-parser) | слит в tabular-parser **до** задачи 7: без него копия схем v2.0 не сверяется с docs |
|
||
| 1 | пакет `arm_statements` (репозиторий `arm-statements`) | API — источник имён этого плана; копия пакета приходит в tabular-parser через план 2 |
|
||
| 2 | `sync-statements`: копия в `api/arm_statements/`, подготовка хоста (его задача 8) | PR задачи 8 плана 2 в tabular-parser слит **до старта**; задача 2 этого плана только сверяет и при нужде обновляет копию `--in-place` |
|
||
| 3 | arm-core части А и Б: приём 2.0 | PR этого плана **сливается** только после выкладки ядра, принимающего 2.0 (`proposal.md`, шаг 5) |
|
||
| docs PR #117 | контракт v2.0, правила | до слияния — отсоединённый worktree docs на `origin/feat/116-unified-normalization`; после — на `origin/main` |
|
||
|
||
Разработка этого плана может идти параллельно с планом 3; **слияние** PR tabular-parser и PR fixtures — только по команде
|
||
человека после выкладки ядра (иначе первый же результат 2.0 даст файлу `ERROR` у ядра 1.x).
|
||
|
||
## Предусловия (для контроллера, до задачи 1)
|
||
|
||
- **П-1.** Issue в RSHB/arm-tabular-parser: `tea issues create --login home --repo RSHB/arm-tabular-parser --title
|
||
"Формы ББ/ОФР через общий код arm_statements, контракт результата v2.0 (arm-ka-docs#116)" --description "<цель, ссылка на
|
||
этот план>"`. Issue в RSHB/arm-ka-fixtures (golden этого PR): `--title "Golden форм и ОСВ под контракт v2.0
|
||
(arm-tabular-parser#<issue>)"`.
|
||
- **П-2.** Зеркальный workspace `$WS/.wt/normalization-tp/` (`$WS` = `/Users/andrey.epifancev/projects/awp-ca`;
|
||
`00-workspace-layout.md`, «Worktree вместо веток»): worktree `tabular-parser/` (ветка фичи), worktree `fixtures/`
|
||
(ветка фичи fixtures — golden пишется сюда, поэтому **не** симлинк), отсоединённый worktree `docs/` (в нём не
|
||
коммитится), симлинки `reference-old-arm`, `CLAUDE.md`, `.claude` (источник `arm-statements` скрипт плана 2 находит
|
||
сам — `$WS/arm-statements`):
|
||
|
||
```bash
|
||
WS=/Users/andrey.epifancev/projects/awp-ca; M=$WS/.wt/normalization-tp
|
||
mkdir -p $M
|
||
git -C $WS/tabular-parser fetch origin
|
||
git -C $WS/tabular-parser worktree add -b tmp/statements-v2 $M/tabular-parser origin/main
|
||
git -C $WS/fixtures fetch origin
|
||
git -C $WS/fixtures worktree add -b tmp/statements-v2-golden $M/fixtures origin/main
|
||
git -C $WS/docs fetch origin
|
||
git -C $WS/docs worktree add --detach $M/docs origin/feat/116-unified-normalization
|
||
for l in reference-old-arm CLAUDE.md .claude; do ln -s $WS/$l $M/$l; done
|
||
git -C $M/tabular-parser branch -m feat/<issue>-statements-v2
|
||
git -C $M/fixtures branch -m feat/<fx-issue>-statements-v2-golden
|
||
cd $M/tabular-parser && codegraph init && make venv
|
||
```
|
||
|
||
- **П-3.** В тексте плана `$T` = `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-tp/tabular-parser`,
|
||
`$F` = `…/normalization-tp/fixtures`, `$D` = `…/normalization-tp/docs`. Команды `make` — из `$T`, backend — из `$T/api`,
|
||
git — `git -C <путь>`. Перед правкой исполнитель вызывает `codegraph_explore` с `projectPath` = `$T`.
|
||
- **П-4.** Проверить: в `$T` есть `api/arm_statements/__init__.py` и `api/arm_statements/VERSION` (PR задачи 8 плана 2
|
||
слит) и `api/tests/contract_identity.py` (план 0). Нет копии пакета — стоп и вопрос владельцу (подключение копии —
|
||
PR плана 2, не этого плана; руками копию не раскладывать). Нет плана 0 — задачи 1–6 можно вести, задача 7 ждёт.
|
||
- **П-5.** После слияния docs PR #117: `git -C $D checkout --detach origin/main`.
|
||
|
||
## Global Constraints
|
||
|
||
- Пакет — только через публичный API `from arm_statements import …` (имена раздела «Публичный API пакета» плана 1; в т. ч.
|
||
`Rules`, `default_rules`, `Cell`, `Evidence`, `HeaderCells`, `FormCandidate`, `Source`, `DocumentType`,
|
||
`StatementWarning`, `WarningCode`, `normalize_candidate`, `envelope_status`, `make_warning`, `type_hint`,
|
||
`is_continuation`, `continuation_type`, `sheet_joins`, `code_column`, `amount_columns`, `normalize_label`,
|
||
`normalize_caption`, `parse_code`, `parse_date`, `parse_header`, `to_contract`, `warning_to_contract`, `to_jsonable`,
|
||
`statements_version`). Импорт внутренних модулей пакета (`arm_statements.codes` и т. п.) в `src/` запрещён. Файлы
|
||
`api/arm_statements/**` не правятся (прямая правка — код 20 `statements-check`); нехватка в API — находка `O-n`, не
|
||
правка копии. Правило шага 2 (столбец кодов, колонки сумм) и нормализация меток в адаптере не повторяются — только
|
||
функции пакета (P4-6, P4-7).
|
||
- Шаг 1 для Excel (спека): `source = excel`; `locator = sheet:<A>[,<B>…]`; у ячейки `confidence = 100`, `text` — как
|
||
показывает числовой формат, `value` — число по формату (`Decimal`), дата или `bool`; `ref = <лист>!<A1>`, имя листа в
|
||
одинарных кавычках, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры, апостроф внутри имени
|
||
удваивается; одна форма — одна сетка, граница листа — номер строки сетки в `segment_breaks`.
|
||
- Координат в `src/parser/**` нет (кроме `grid.py`, `readers/`) — AST-линтер `tests/unit/parser/test_no_coordinates.py`;
|
||
буквы колонок и адрес ячейки строит только `grid.py`. Слов форм в коде нет — только словари пакета (`Rules.headers.*`).
|
||
- Контракт: файл результата и конверт (включая ack `processing`) — `schema_version = "2.0"`; запрос остаётся `1.0`
|
||
(поддерживаемый MAJOR запроса — `"1"`, `SCHEMA_MISMATCH` по-прежнему). `service.statements_version` — содержимое
|
||
`api/arm_statements/VERSION`, обязателен при `statementDocument`.
|
||
- Предупреждения форм и файла — только через `make_warning` с членом `WarningCode` (код и уровень — словарь пакета); у
|
||
ОСВ — код с префиксом `OSV_`, уровень `info`. Строк вида `sheet=<имя> <текст>` в `metrics.warnings` нет. Перевод
|
||
документов форм и предупреждений в контракт — только `to_contract` / `warning_to_contract` пакета.
|
||
- Статус конверта — `envelope_status` пакета по статусам всех документов (ОСВ — `done`/`needs_check` листа) и
|
||
предупреждениям файла; документов нет — `error INVALID_DOCUMENT`. Порядок документов: ОСВ в порядке листов, затем формы.
|
||
- Разбор ОСВ не меняется (`classify.py`, `to_money`, `hierarchy.py`, `checks.py` по существу, словари §1): golden ОСВ
|
||
после перехода отличается только ключами `schema_version`, `metrics.warnings`, `documents[].status`,
|
||
`documents[].warnings`.
|
||
- Деньги — `Decimal`, без `float`; первичка не мутируется.
|
||
- Golden правится только `make golden-update` с просмотром diff и отдельным коммитом в fixtures.
|
||
- `make check` не требует Docker и сети; `statements-check` — только в `make check-full`.
|
||
- Язык комментариев, docstring, сообщений, коммитов, issue и PR — русский. Коммиты заканчиваются строками
|
||
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` и
|
||
`Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W`; PR — строкой
|
||
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`. Слияние — только по команде человека.
|
||
- Находки и решения несут маркеры (`T<n>.<k>`, `C-n`/`I-n`/`M-n`, `R-n`, `O-n`); решения этого плана — `P4-n` (раздел в
|
||
конце), контроллер переносит их в `design.md` docs (`## Decisions made without discussion`).
|
||
|
||
## Review Focus
|
||
|
||
Реальные книги, где адаптер шага 1 встречает то, что сценарии спеки описывают по одному. Каждый пункт закреплён тестом
|
||
задачи-владельца:
|
||
|
||
1. **Титульный блок «Коды» выгрузки 1С** (Унагранде «Б.Б Лист 1», «ББ. Лист 1 .xls», «ББ. Лист 1 3кв.25», «ОФР. Лист 1» ×2):
|
||
справа «Коды» с ОКУД, датой, ОКПО, ИНН, ОКОПФ 12267, ОКФС, ОКЕИ. Ожидание: столбец под «Коды» не столбец кодов, шапка
|
||
берёт `okopf = "12267"`, строки с кодом или текстом кода «12267» нет, ББ `needs_check` только из-за `1600 = 1700`, ОФР
|
||
`done`. Тесты: `test_code_column_ignores_title_codes_block` (задача 4), `test_unagrande_title_codes_block` (задача 8).
|
||
2. **НПЗ — листы разной геометрии**: ББ на 5 листах, у «Лист 4» своя строка заголовка и колонки в других столбцах; ОФР на 3
|
||
листах, последний — одна строка 2400 с кодом в другой колонке. Ожидание: два кандидата с 5 и 3 листами, `segment_breaks`
|
||
на каждой границе, значения листов-продолжений под `column_index` первого листа, нет `SHEET_NOT_JOINED`,
|
||
`SEGMENT_*`, `HEADER_ROW_NOT_FOUND`; строка «Реализация иного имущества…» — `detail` с `parent_ref` строки 2340; оба
|
||
`done`. Тесты: `test_npz_candidates` (задача 5), `test_npz_documents` (задача 8).
|
||
3. **Межениновская — нумерация и подписи под формой**: строка «1 | 2 | 3 | 4 | 5» с «2» в столбце кодов, ниже таблицы —
|
||
подписи руководителя и дата. Ожидание: строки с `line_code_text = "2"` нет, строк `unreadable` / `missing` нет, ИНН
|
||
`7016006635` из шапки, ББ и ОФР `done`. Тест: `test_mezheninovskaya_numbering_and_signatures` (задача 8). Если тест
|
||
падает на стороне шагов 2–3 (роли строк) — это находка для пакета (план 1), а не повод править сетку в адаптере.
|
||
4. **xlsb без числовых форматов**: у каждой формы книги xlsb — `XLSB_NO_FORMATS` (`info`); при «тыс. руб.» и ≥ 10 %
|
||
дробных значений — `XLSB_SCALE_SUSPECT` (`error`), документ `needs_check`; без дробных — `done`. Тест:
|
||
`test_xlsb_form_scale_suspect` (задача 7, синтетика `write_xlsb`).
|
||
5. **«Пояснения»**: книга только из пояснений (лист с ОКУД 0710003 и лист «Пояснения к бухгалтерскому балансу» без кодов)
|
||
— документы `DISCLOSURE` `done`, конверт `done`; лист «Пояснения к бухгалтерскому балансу» с кодами 1110…1700 —
|
||
`BALANCE` и `TYPE_CONFLICT` (`error`). Тесты: `test_disclosures_only_book_is_done`,
|
||
`test_disclosure_title_with_balance_codes` (задача 7).
|
||
|
||
---
|
||
|
||
## Структура изменений
|
||
|
||
```
|
||
tabular-parser/
|
||
├── openspec/changes/unified-normalization-tp/ # задача 1: proposal, tasks, дельты трёх спек
|
||
├── api/arm_statements/** # копия пакета (план 2); задача 2 — только --in-place при нужде
|
||
├── api/contracts/** # задача 7: копия docs/contracts v2.0 (без .v1 и examples/v1)
|
||
├── api/src/contract/{common,request,result,__init__}.py # задача 7: модели v2.0
|
||
├── api/src/parser/
|
||
│ ├── grid.py # задача 3: cell_ref()
|
||
│ ├── osv_warnings.py # задача 6: OsvNote и коды OSV_*
|
||
│ ├── checks.py, osv.py # задача 6: заметки с кодами; задача 7: status/warnings в документе
|
||
│ ├── detect.py # задача 5: is_osv_sheet(); задача 7: classify_sheet на словарях пакета
|
||
│ ├── readers/__init__.py # задача 7: BookRead.mismatch
|
||
│ ├── book.py, __init__.py # задача 7: сборка книги через пакет, require_statements_version()
|
||
│ └── statements/
|
||
│ ├── number_format.py # задача 3: текст по формату (остаётся)
|
||
│ ├── cells.py # задача 3: ячейка Excel → Cell, сетка
|
||
│ ├── forms.py # задача 4: блоки форм, вид листа, столбец кодов, колонки сумм
|
||
│ ├── header_cells.py # задача 4: шапка блока → HeaderCells, Evidence
|
||
│ ├── adapter.py # задача 5: план листов книги, склейка → FormCandidate
|
||
│ ├── pipeline.py # задача 7: StatementPipeline (адаптер → normalize_candidate → контракт)
|
||
│ └── {checks,columns,detect,header,lines,merge,statement}.py # задача 9: удаляются
|
||
├── api/rules/statement_{headers,periods,checks}.yaml # задача 9: удаляются (словари в пакете)
|
||
├── api/src/core/worker.py, api/src/app.py, api/src/parser/cli.py # задача 7: statements_version, версии
|
||
└── api/tests/… # по задачам; statements_manifest.yaml — задача 8
|
||
fixtures/golden/expected_{osv,statement}_parse/** # задача 8 (отдельный PR fixtures)
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1: OpenSpec change `unified-normalization-tp` в tabular-parser
|
||
|
||
Модель исполнителя: sonnet (перенос требований спеки в дельты).
|
||
|
||
**Files:**
|
||
- Create: `$T/openspec/changes/unified-normalization-tp/proposal.md`, `tasks.md`,
|
||
`specs/statement-parsing/spec.md`, `specs/tabular-recognition-service/spec.md`, `specs/osv-parsing/spec.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: текущие `$T/openspec/specs/{statement-parsing,tabular-recognition-service,osv-parsing}/spec.md`; спека docs
|
||
(`specs/tabular-recognition-service/spec.md`, требования шага 1 Excel в `specs/statement-normalization/spec.md`).
|
||
- Produces: change, на который ссылаются коммиты и PR; при закрытии — `openspec archive unified-normalization-tp`.
|
||
|
||
Состав дельт (текст требований — из спеки docs, приведённый к языку спек сервиса; сценарии — дословно из docs):
|
||
- `statement-parsing`: **MODIFIED** «Листы форм и их продолжения находятся по содержимому» (классификация: ОСВ → форма →
|
||
продолжение → прочее, слабое продолжение только сразу после листа формы, `SHEET_OTHER`), «Шапка документа» (поля как
|
||
`Cell` с `confidence = 100` в границах блока, разбор — общим кодом), «Склейка листов-продолжений и статус документа»
|
||
(до раскладки, `segment_breaks`, `CONTINUATION_TYPE_CONFLICT`, `SHEET_NOT_JOINED`; статус — функцией пакета), «Словари
|
||
форм, координаты, golden» (словари форм — в пакете; координат нет; golden и манифест); **REMOVED** «Заголовок таблицы,
|
||
колонка кодов и колонки-периоды определяются по тексту», «Строки с кодом и значения», «Соотношения §7 — данные,
|
||
проверка по колонкам» (Reason: шаги 2–8 выполняет копия `arm_statements`; Migration: требования —
|
||
`statement-normalization` docs); **ADDED** «Адаптер Excel шага 1», «Ячейка Excel», «Строки без кода и неизвестные
|
||
колонки в Excel», «Манифест ожиданий на реальных формах», «Статус книги» (из docs `tabular-recognition-service`).
|
||
- `tabular-recognition-service`: **MODIFIED** «Результат по контракту» (v2.0, `service.statements_version`, структурные
|
||
`metrics.warnings`, ack `processing` — 2.0, запрос 1.0).
|
||
- `osv-parsing`: **MODIFIED** «Контрольные суммы определяют статус» (`status` листа и `warnings[]` `OSV_*` `info` в
|
||
документе, строк `sheet=` нет) и «Форматы, лимиты, время» (`FILE_TYPE_MISMATCH` — предупреждение файла `info`).
|
||
|
||
- [ ] **Step 1: Прочитать три текущие спеки сервиса и требования docs; написать proposal.md** (Why — ссылка на docs
|
||
change и этот план; What Changes — по списку выше; Impact — модули из «Структуры изменений»).
|
||
- [ ] **Step 2: Написать дельты** — MODIFIED с полным новым текстом требования и всеми его сценариями.
|
||
- [ ] **Step 3: `tasks.md`** — задачи 1–10 этого плана одной строкой каждая.
|
||
- [ ] **Step 4: Проверка**
|
||
|
||
Run: `cd $T && openspec validate unified-normalization-tp --strict`
|
||
Expected: `Change 'unified-normalization-tp' is valid`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git -C $T add openspec/changes/unified-normalization-tp
|
||
git -C $T commit -m "openspec: change unified-normalization-tp — адаптер Excel, общий код, контракт v2.0 (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Подготовка хоста — сверка с планом 2 и свежесть копии
|
||
|
||
Модель исполнителя: haiku (проверки по списку).
|
||
|
||
Подготовку хоста делает план 2 (`plans/2026-10-18-plan-2-sync-statements.md`, задача 8 «tabular-parser — подключение
|
||
копии»; раздел «Что должны включить планы 3, 4, 5»), и её PR слит до старта (П-4). Эта задача только сверяет `main`
|
||
с планом 2 и, если пакету нужна версия новее, обновляет копию скриптом. Недостающее из списка ниже — **не** доделывать
|
||
здесь, а остановиться и сообщить контроллеру (находка `O-n`: правка подключения — PR плана 2).
|
||
|
||
**Files:**
|
||
- Modify (только если нужна новая версия пакета): `$T/api/arm_statements/**` — скриптом, руками не править
|
||
|
||
**Interfaces:**
|
||
- Consumes (из плана 2): `make lint` → `mypy src arm_statements`; `api/pytest.ini` `testpaths` с `arm_statements/tests`;
|
||
`known-first-party = ["src", "arm_statements"]`; строгий блок `[[tool.mypy.overrides]]` для `arm_statements.*`;
|
||
пины `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906`; цель `statements-check` в `check-full`, не в
|
||
`check`; `.dockerignore` и `Dockerfile` не менялись (словари копии попадают в образ через `COPY api/ ./`).
|
||
- Produces: копия, в API которой есть все имена раздела Global Constraints.
|
||
|
||
- [ ] **Step 1: Сверка подключения** — каждый пункт Consumes есть в `$T` (`grep` по `Makefile`, `api/pytest.ini`,
|
||
`api/pyproject.toml`, `api/requirements-dev.txt`); `api/arm_statements/VERSION` — 40 hex-символов.
|
||
- [ ] **Step 2: Свежесть и API копии**
|
||
|
||
Run: `cd $T && make statements-check; cd api && .venv/bin/python -c "import arm_statements as a; print(a.statements_version()); [getattr(a, n) for n in ('Rules','default_rules','Cell','Evidence','HeaderCells','FormCandidate','Source','DocumentType','StatementWarning','WarningCode','normalize_candidate','envelope_status','make_warning','type_hint','is_continuation','continuation_type','sheet_joins','code_column','amount_columns','normalize_label','normalize_caption','parse_code','parse_date','parse_amount','parse_header','to_contract','warning_to_contract','to_jsonable','statements_version')]"`
|
||
Expected: `statements-check` — `0` или `10`; хэш печатается, `AttributeError` нет.
|
||
|
||
- [ ] **Step 3 (только при `10` «копия отстала» или нехватке имени):** обновить копию в worktree фичи
|
||
|
||
Run: `cd $T && ../docs/scripts/sync-statements.sh --in-place --host-dir . && make check`
|
||
Expected: `statements-check tabular-parser: 0`; `make check` зелёный.
|
||
|
||
```bash
|
||
git -C $T add api/arm_statements
|
||
git -C $T commit -m "chore(arm_statements): копия пакета arm-statements@<v7> (#<issue>)"
|
||
```
|
||
|
||
- [ ] **Step 4: Базовая проверка** — `cd $T && make check` зелёный, в выводе pytest есть `arm_statements/tests/…`.
|
||
|
||
---
|
||
|
||
### Task 3: Ячейка Excel → `Cell`: ссылка, текст по формату, сетка
|
||
|
||
Модель исполнителя: sonnet.
|
||
|
||
**Files:**
|
||
- Modify: `$T/api/src/parser/grid.py` (`cell_ref`), `$T/api/src/parser/statements/number_format.py` (текст)
|
||
- Create: `$T/api/src/parser/statements/cells.py`
|
||
- Test: `$T/api/tests/unit/parser/test_grid.py`, `$T/api/tests/unit/parser/statements/test_number_format.py`,
|
||
`$T/api/tests/unit/parser/statements/test_cells.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `Sheet`, `Cell` (grid), `display_amount`, `arm_statements.Cell`.
|
||
- Produces:
|
||
- `grid.cell_ref(sheet_name: str, row: int, col: int) -> str` — 1-based; правило кавычек — Global Constraints
|
||
(«буква» — `str.isalpha()`, цифра — `str.isdigit()`).
|
||
- `number_format.FormatResult(kind, value: Decimal | None = None, text: str | None = None)` — `text` заполнен у
|
||
`kind == "value"`; `display_amount(raw: Decimal, fmt: str) -> FormatResult`; `general_text(raw: Decimal) -> str`.
|
||
- `cells.excel_cell(sheet: Sheet, row: int, col: int, unparsed_formats: set[str]) -> arm_statements.Cell | None`
|
||
- `cells.grid_rows(sheet: Sheet, first_row: int, last_row: int, width: int, unparsed_formats: set[str]) ->
|
||
list[tuple[arm_statements.Cell | None, ...]]` — строки `first_row..last_row` включительно, позиция = `col - 1`, длина
|
||
каждой строки = `width`.
|
||
|
||
Правила текста и значения (P4-3, P4-4, P4-5):
|
||
- числовая ячейка с форматом, разобранным в цифровую секцию: `value` — показываемое число (знак — от сырого, R-3),
|
||
**без** проверки диапазона `numeric(18,2)` (больше 16 целых цифр — `VALUE_OUT_OF_RANGE` выдаёт пакет); `text` —
|
||
литералы секции до и после плейсхолдеров + модуль числа с группами по три через пробел (если в секции есть
|
||
разделитель групп) и `decimals` знаками после **запятой**; «-» впереди — только если сырое отрицательно и в формате одна
|
||
секция без условия;
|
||
- секция-литерал (`[=0]"-"`, `"(647)"`): `text` — литерал, `value` — как разбирает `to_money` литерал (прочерк — `None`);
|
||
- `General`, пустой формат, формат не разобран, xlsb (формата нет): `value` — сырое `Decimal`, `text` —
|
||
`general_text(raw)`: целое — без дробной части, иначе без хвостовых нулей, разделитель — запятая, без групп и
|
||
экспоненты; неразобранный формат кладётся в `unparsed_formats`;
|
||
- дата/дата-время: `value` — `date`, `text` — `dd.mm.yyyy`; логическое — `value` bool, `text` «ИСТИНА»/«ЛОЖЬ»; текст —
|
||
`value = None`, `text = Cell.text`; пустая ячейка и не-левая-верхняя позиция объединённого диапазона — `None`;
|
||
- `confidence = 100.0`, `ref = cell_ref(sheet.name, row, col)` (у объединённого — адрес левой верхней).
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
```python
|
||
# tests/unit/parser/test_grid.py
|
||
def test_cell_ref_quotes_sheet_name_by_excel_rule() -> None:
|
||
assert cell_ref("Баланс", 27, 3) == "Баланс!C27"
|
||
assert cell_ref("Лист_1", 2, 2) == "Лист_1!B2"
|
||
assert cell_ref("Бухгалтерский баланс. Лист 1", 14, 3) == "'Бухгалтерский баланс. Лист 1'!C14"
|
||
assert cell_ref("2024", 1, 1) == "'2024'!A1"
|
||
assert cell_ref("Отчёт д'Артаньяна", 1, 28) == "'Отчёт д''Артаньяна'!AB1"
|
||
|
||
# tests/unit/parser/statements/test_number_format.py
|
||
@pytest.mark.parametrize(
|
||
("raw", "fmt", "value", "text"),
|
||
[
|
||
("106311116.46", "#,##0,", "106311", "106 311"),
|
||
("-646532.4", "#,##0;(#,##0)", "-646532", "(646 532)"),
|
||
("-1500", "#,##0", "-1500", "-1 500"),
|
||
("-1500", "#,##0;#,##0", "-1500", "1 500"),
|
||
("1.5", "0.00", "1.50", "1,50"),
|
||
("0", '#,##0;-#,##0;"-"', None, "-"),
|
||
("12345678901234567", "0", "12345678901234567", "12345678901234567"),
|
||
],
|
||
)
|
||
def test_display_text(raw: str, fmt: str, value: str | None, text: str) -> None:
|
||
shown = display_amount(D(raw), fmt)
|
||
assert (shown.kind, shown.value, shown.text) == ("value", None if value is None else D(value), text)
|
||
|
||
def test_general_text() -> None:
|
||
assert [general_text(D(x)) for x in ("7016006635.0", "106311116.460", "-0.5")] == ["7016006635", "106311116,46", "-0,5"]
|
||
|
||
# tests/unit/parser/statements/test_cells.py — листы собирать gridbuilder.sheet_from_rows / with_formats
|
||
def test_excel_cell_by_number_format() -> None: # спека: «Excel-ячейка с числовым форматом»
|
||
cell = excel_cell(with_formats(balance_sheet_with(row=27, col=4, value=D("106311116.46")), {(27, 4): "#,##0,"}),
|
||
27, 4, set())
|
||
assert cell == Cell(text="106 311", value=D("106311"), confidence=100.0, ref="Баланс!D27")
|
||
|
||
def test_unparsed_format_keeps_raw_and_is_recorded() -> None:
|
||
formats: set[str] = set()
|
||
cell = excel_cell(with_formats(sheet_one(D("0.15")), {(1, 1): "0.0%"}), 1, 1, formats)
|
||
assert (cell.value, cell.text, formats) == (D("0.15"), "0,15", {"0.0%"})
|
||
|
||
def test_date_text_bool_and_merged() -> None: ...
|
||
# дата 2025-09-30 → text "30.09.2025", value date; текст «Итого» → value None; позиция внутри merged (не origin) → None
|
||
|
||
def test_grid_rows_dense_width() -> None: ...
|
||
# 3 строки × width=6: каждая строка — кортеж длины 6, пустые позиции None, cell в позиции col-1
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests to verify they fail**
|
||
|
||
Run: `cd $T/api && .venv/bin/pytest tests/unit/parser/test_grid.py tests/unit/parser/statements/test_number_format.py tests/unit/parser/statements/test_cells.py -v`
|
||
Expected: FAIL — `cell_ref`, `general_text`, `cells` не определены, у `FormatResult` нет `text`.
|
||
|
||
- [ ] **Step 3: Implement** `cell_ref` (через `column_letter`), текст в `number_format` (в `_Section` добавить
|
||
`grouping: bool`, `prefix: str`, `suffix: str`; `_scaled` больше не вызывает `quantize_money` — квантование и диапазон
|
||
в пакете), `cells.py`. Старые потребители `display_amount` (`statements/lines.py`) продолжают работать:
|
||
`kind`/`value` для чисел в пределах диапазона не меняются.
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `cd $T && make check`
|
||
Expected: зелёный (старый конвейер форм и golden не изменились: старые тесты `test_number_format` по `value` проходят;
|
||
если старый тест ждал `unparsed` для числа вне диапазона — переписать его на новое правило, это P4-4).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git -C $T add api/src/parser/grid.py api/src/parser/statements/number_format.py api/src/parser/statements/cells.py api/tests/unit
|
||
git -C $T commit -m "feat(statements): ячейка Excel → Cell: текст по числовому формату, value, ref <лист>!<A1> (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Блоки форм, вид листа и шапка на словарях пакета
|
||
|
||
Модель исполнителя: sonnet.
|
||
|
||
**Files:**
|
||
- Create: `$T/api/src/parser/statements/forms.py`, `$T/api/src/parser/statements/header_cells.py`
|
||
- Test: `$T/api/tests/unit/parser/statements/test_forms.py`, `$T/api/tests/unit/parser/statements/test_header_cells.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `arm_statements.Rules` (`headers.form_titles`, `headers.okud`, `headers.okud_code`, `headers.code_caption`,
|
||
`headers.labels`, `headers.auditor`, `headers.legal_form`, `periods.relative`), `type_hint`, `is_continuation`,
|
||
`continuation_type`, `code_column`, `amount_columns`, `normalize_label`, `normalize_caption`, `parse_code`,
|
||
`parse_date`, `DocumentType`, `grid_rows` (задача 3). Нормализация текста под шаблон — таблица «Словари» раздела API
|
||
плана 1 (P4-7): метки шапки, заголовки, ОКУД, `auditor`, `legal_form` — `normalize_label`; подпись «Код» и
|
||
относительные периоды — `normalize_caption`. `normalize_text` из `src.parser.rules` здесь не используется.
|
||
- Produces (`forms.py`; `StatementRules` = `arm_statements.Rules`):
|
||
- `matches(patterns: Iterable[re.Pattern[str]], text: str) -> bool` — `search` по `normalize_label(text)`
|
||
- `caption_matches(patterns: Iterable[re.Pattern[str]], text: str) -> bool` — `search` по `normalize_caption(text)`
|
||
- `sheet_grid(sheet: Sheet, first_row: int, last_row: int) -> list[tuple[Cell | None, ...]]` — `grid_rows(sheet,
|
||
first_row, last_row, <ширина листа>, set())` (неразобранные форматы здесь не собираются — их собирает
|
||
`build_candidates`)
|
||
- `sheet_code_column(sheet: Sheet, rules: StatementRules, first_row: int, last_row: int, *, relaxed: DocumentType |
|
||
None = None) -> int | None` — столбец листа (1-based) = `code_column(sheet_grid(...), rules, relaxed=relaxed) + 1` (`None` → `None`):
|
||
правило (а)/(б) шага 2 пакета (с защитой от блока «Коды»); `relaxed` — ослабленный поиск в отрезке формы, минимум
|
||
`Rules.code_ranges.min_codes_continuation`
|
||
- `FormBlock(type_hint: DocumentType, title: tuple[int, int] | None, okud: tuple[int, int] | None, first_row: int,
|
||
last_row: int)` — `title`/`okud` — `(row, col)` ячеек-признаков
|
||
- `find_blocks(sheet: Sheet, rules: StatementRules) -> list[FormBlock]` — строки с заголовком (`type_hint(None, text)`
|
||
не `None`) или ОКУД (`type_hint(text, None)`; ячейка, для которой `is_continuation(text)`, ОКУДом блока не
|
||
считается) — кандидаты; между соседними без строки с кодом (`code_rows` по `sheet_code_column` листа) — один блок;
|
||
первый блок начинается с первой строки листа, каждый — до строки перед следующим; `type_hint` блока —
|
||
`type_hint(okud_text, title_text, rules)`
|
||
- `code_rows(sheet: Sheet, rules: StatementRules, col: int, first_row: int, last_row: int) -> list[int]` — строки, где
|
||
`parse_code(text).code` ячейки столбца `col` не `None`
|
||
- `codes_of(sheet: Sheet, rules: StatementRules, col: int, first_row: int, last_row: int) -> list[str]`
|
||
- `sheet_amount_columns(sheet: Sheet, rules: StatementRules, code_col: int, first_row: int, last_row: int) -> int` —
|
||
`len(amount_columns(sheet_grid(...), code_col - 1, rules))`: столбцы правее столбца кодов, где в строке с кодом есть
|
||
число (то же правило, что колонки сегмента без заголовка в шаге 2 пакета)
|
||
- `continuation_marker(sheet: Sheet, rules: StatementRules) -> Cell | None` — первая ячейка, для которой
|
||
`is_continuation(text, rules)` (маркер с ОКУД вне словаря — тоже маркер; тип — `continuation_type(cell.text, rules)`,
|
||
может быть `None`)
|
||
- `StatementSheetKind = Literal["statement", "continuation", "other"]`;
|
||
`classify_statement_sheet(sheet: Sheet, rules: StatementRules) -> StatementSheetKind` — есть блок → `statement`;
|
||
маркер, или ячейка с `caption_matches(headers.code_caption)` и подписью-периодом правее (`parse_date` не `None` или
|
||
`caption_matches(periods.relative)`), или `sheet_code_column(...)` не `None` → `continuation`; иначе `other`
|
||
- `is_weak_continuation(sheet: Sheet, rules: StatementRules, form_type: DocumentType) -> bool` —
|
||
`sheet_code_column(…, relaxed=form_type)` не `None`; `form_type` не `BALANCE`/`INCOME` → `False`
|
||
- Produces (`header_cells.py`):
|
||
- `header_cells(sheet: Sheet, rules: StatementRules, block: FormBlock, last_row: int, unparsed_formats: set[str]) ->
|
||
tuple[HeaderCells, Evidence]` — поиск в строках `block.first_row..last_row`; `last_row` задаёт адаптер (P4-6)
|
||
|
||
Правила шапки (как сейчас в `statements/header.py`, но результат — `Cell` ячейки-значения через `excel_cell`):
|
||
организация — значение правее метки `organization` или в следующей строке; пусто, число или метка ОКПО — ближайшая
|
||
строка выше с признаком `legal_form`; ИНН — первая ячейка правее метки `inn`, в тексте которой 10 или 12 цифр (сам разбор
|
||
— пакет); строки ниже первой метки `auditor` для организации, ИНН и адреса не смотрятся; `okpo`, `okfs`, `okopf`, `okei`,
|
||
`address` — значение у своей метки; пара `okopf_okfs` — два числа правее в той же строке или строкой выше; `units` —
|
||
первая ячейка правее метки `units`, кроме метки ОКЕИ и чистых цифр; `report_date` — ячейка строки сразу под заголовком
|
||
формы, у которой `parse_date(text)` не `None`, иначе тройка «число | месяц | год» правее метки `date` →
|
||
`Cell(text="<д>.<мм>.<гггг>" для числового месяца или "<д> <месяц> <гггг>", value=None, confidence=100.0, ref=<ref
|
||
ячейки числа>)`; `okud` — ячейка ОКУД блока. `Evidence(okud=<ячейка ОКУД>, title=<ячейка заголовка>)`.
|
||
|
||
- [ ] **Step 1: Write the failing tests** (листы — `sheet_from_rows`; правила — `default_rules()`)
|
||
|
||
```python
|
||
def test_title_and_okud_without_codes_between_form_one_block(rules: StatementRules) -> None: ...
|
||
# «Бухгалтерский баланс» (стр. 3), «Форма по ОКУД | 0710001» (стр. 5), таблица ниже → один блок BALANCE,
|
||
# title=(3, c), okud=(5, c2), first_row=1
|
||
|
||
def test_second_title_after_codes_opens_new_block(rules: StatementRules) -> None: ...
|
||
# ББ с кодами 1110…1600, ниже «Отчет о финансовых результатах» и 2110… → блоки BALANCE и INCOME, граница — строка заголовка ОФР
|
||
|
||
def test_okud_wins_over_title(rules: StatementRules) -> None:
|
||
blocks = find_blocks(sheet_from_rows([["Бухгалтерский баланс"], ["по ОКУД", "0710002"]]), rules)
|
||
assert [block.type_hint for block in blocks] == [DocumentType.INCOME]
|
||
|
||
def test_report_title_alone_is_not_a_form(rules: StatementRules) -> None:
|
||
assert find_blocks(sheet_from_rows([["Бухгалтерская отчетность"]]), rules) == []
|
||
|
||
def test_continuation_marker_is_not_a_block(rules: StatementRules) -> None:
|
||
sheet = sheet_from_rows([["Форма 0710001 с.2"], ["Пассив", "Код", "На 30.09.2025"], ["Капитал", "1310", 10]])
|
||
assert find_blocks(sheet, rules) == []
|
||
assert classify_statement_sheet(sheet, rules) == "continuation"
|
||
|
||
def test_code_column_ignores_title_codes_block(rules: StatementRules) -> None: # Review Focus 1
|
||
# столбец F: «Коды», 0710001, 30, 09, 2025, 12345678, 7700000000, 12267, 16, 384 (один код из девяти);
|
||
# столбец C: 1110, 1150, 1170, 1100, 1210, 1230 → sheet_code_column(...) == 3
|
||
|
||
def test_amount_columns_counts_numeric_columns_right_of_code(rules: StatementRules) -> None: ...
|
||
# код в C, числа в D, E, F в строках с кодами, текст «Изменение» в G без чисел → sheet_amount_columns(...) == 3
|
||
|
||
def test_weak_and_other_sheets(rules: StatementRules) -> None: ...
|
||
# лист с единственной строкой «Чистая прибыль | 2400 | 10 | 8» → classify «other»,
|
||
# is_weak_continuation(…, DocumentType.INCOME) True, (…, DocumentType.BALANCE) False;
|
||
# лист «Примечания» с текстом → «other», is_weak_continuation(…, INCOME) False
|
||
|
||
def test_marker_with_unknown_okud_is_continuation(rules: StatementRules) -> None: ...
|
||
# лист «Форма 0710000 с.2» + таблица с кодами → find_blocks == [], classify «continuation»,
|
||
# continuation_marker(...).text == "Форма 0710000 с.2", continuation_type(...) is None
|
||
|
||
def test_labels_with_punctuation_match(rules: StatementRules) -> None:
|
||
assert matches(rules.headers.labels["address"], "Местонахождение (адрес)")
|
||
assert matches(rules.headers.labels["okopf_okfs"], "по ОКОПФ / ОКФС")
|
||
assert matches(rules.headers.legal_form, "АО «Пример»")
|
||
|
||
def test_header_cells_of_1c_block(rules: StatementRules) -> None: ...
|
||
# «Организация | АО «Пример»», «ИНН | 7700000000», «по ОКПО | 12345678», «по ОКОПФ / ОКФС | 12267 | 16» строкой выше
|
||
# метки, «Единица измерения: | в тыс. рублей | по ОКЕИ | 384», подзаголовок «на 30 сентября 2025 г.»:
|
||
# cells.organization_name.text == "АО «Пример»", .inn.ref == "Баланс!C5", (.okopf.text, .okfs.text) == ("12267", "16"),
|
||
# .report_date.text == "на 30 сентября 2025 г."; parse_header(cells, rules) → units "тыс. руб.", report_date 2025-09-30
|
||
|
||
def test_auditor_inn_is_not_taken(rules: StatementRules) -> None: ...
|
||
def test_organization_by_legal_form_above_label(rules: StatementRules) -> None: ... # НПЗ
|
||
def test_report_date_from_triple(rules: StatementRules) -> None: ...
|
||
# «Дата (число, месяц, год) | 30 | 09 | 2025» → text "30.09.2025"; «30 | сентября | 2025» → "30 сентября 2025";
|
||
# parse_header даёт 2025-09-30 в обоих случаях
|
||
|
||
def test_units_only_by_okei(rules: StatementRules) -> None: # спека: «Единицы только кодом ОКЕИ»
|
||
# «по ОКЕИ | 384», метки «Единица измерения» нет → cells.units is None, cells.okei.text == "384";
|
||
# head = parse_header(cells, rules): (head.units.value, head.okei.value) == ("тыс. руб.", "384")
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests to verify they fail**
|
||
|
||
Run: `cd $T/api && .venv/bin/pytest tests/unit/parser/statements/test_forms.py tests/unit/parser/statements/test_header_cells.py -v`
|
||
Expected: FAIL — модулей нет.
|
||
|
||
- [ ] **Step 3: Implement `forms.py`, `header_cells.py`** по Interfaces (донор логики — `statements/detect.py` и
|
||
`statements/header.py`; сами модули-доноры не правятся, их удаляет задача 9). Если метка шапки не находится словарём
|
||
пакета, а словарём tabular-parser находится — это расхождение словарей: находка `O-n`, словарь пакета не правится.
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `cd $T && make check`
|
||
Expected: зелёный; `test_no_coordinates.py` проходит (новые модули без координат).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git -C $T add api/src/parser/statements/forms.py api/src/parser/statements/header_cells.py api/tests/unit/parser/statements
|
||
git -C $T commit -m "feat(statements): блоки форм, вид листа и шапка-ячейки на словарях arm_statements (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Адаптер книги → `FormCandidate`
|
||
|
||
Модель исполнителя: sonnet.
|
||
|
||
**Files:**
|
||
- Create: `$T/api/src/parser/statements/adapter.py`
|
||
- Modify: `$T/api/src/parser/detect.py` (вынести `is_osv_sheet`, поведение `classify_sheet` не меняется)
|
||
- Test: `$T/api/tests/unit/parser/statements/test_adapter.py`,
|
||
`$T/api/tests/fixtures/test_statements_adapter_real.py` (маркер `fixtures`)
|
||
|
||
**Interfaces:**
|
||
- Consumes: задачи 3–4; `FormCandidate`, `Evidence`, `HeaderCells`, `Source`, `DocumentType`, `make_warning`,
|
||
`WarningCode`, `sheet_joins`, `continuation_type`, `Deadline`, `SheetKind`.
|
||
- Produces:
|
||
- `detect.is_osv_sheet(sheet: Sheet, rules: Rules) -> bool` — признак ведомости в первых `SHEET_MARKER_ROWS` строках
|
||
- `adapter.SheetPlan(osv: tuple[Sheet, ...], statements: tuple[tuple[Sheet, SheetKind], ...], other: tuple[Sheet, ...])`
|
||
- `adapter.plan_sheets(sheets: Sequence[Sheet], is_osv: Callable[[Sheet], bool], rules: StatementRules) -> SheetPlan` —
|
||
по порядку: ОСВ; `statement` → `SheetKind.STATEMENT`; `continuation` → `STATEMENT_CONTINUATION`; `other` сразу после
|
||
листа формы или продолжения и `is_weak_continuation(лист, rules, <type_hint последнего блока последнего листа
|
||
формы>)` → `STATEMENT_CONTINUATION`; иначе `other`. Лист ОСВ или «прочее» прерывает «сразу после».
|
||
- `adapter.build_candidates(sheets: Sequence[tuple[Sheet, SheetKind]], rules: StatementRules, deadline: Deadline, *,
|
||
xlsb: bool) -> list[FormCandidate]`
|
||
|
||
Алгоритм `build_candidates` (спека «Шаг 1 — многостраничная форма», §11.2 п. 2–4; P4-6, P4-8, P4-10):
|
||
1. Лист `STATEMENT`: для каждого блока — новый кандидат: `source=EXCEL`, `locator="sheet:<имя>"`, `type_hint` блока,
|
||
`evidence` и `header` — `header_cells(…, last_row=<строка перед первой строкой с кодом столбца кодов блока>)`
|
||
(столбец — `sheet_code_column` блока, строки — `code_rows`; кодов нет — `block.last_row`), `rows = grid_rows(sheet,
|
||
block.first_row, block.last_row, …)`.
|
||
2. Лист `STATEMENT_CONTINUATION`: открытая форма — последний кандидат с `type_hint` `BALANCE`/`INCOME`. Маркер есть и его
|
||
тип не `None` и не равен типу формы → отдельный кандидат (`type_hint` = тип маркера) + `CONTINUATION_TYPE_CONFLICT`.
|
||
Маркера нет → `sheet_joins(type, codes, sheet_amount_columns(лист), sheet_amount_columns(первый лист формы в её
|
||
блоке), rules)`, где столбец кодов листа — `sheet_code_column(лист, …)`, а если его нет —
|
||
`sheet_code_column(лист, …, relaxed=type)` (как сегмент шага 2 пакета), `codes` — `codes_of` по нему;
|
||
`False` → отдельный кандидат (`type_hint=None`) + `SHEET_NOT_JOINED`. Открытой формы нет → отдельный кандидат
|
||
(`type_hint` = тип маркера или `None`) без предупреждения. У отдельного кандидата `header = HeaderCells()`,
|
||
`evidence = Evidence()`, `rows` — все строки листа. Иначе — присоединение: `segment_breaks` += текущее число строк
|
||
сетки, строки листа (все строки листа) дописываются, `locator` += `,<имя>`.
|
||
3. Ширина: строки всех листов кандидата выравниваются по наибольшей ширине (дописываются `None`).
|
||
4. Предупреждения кандидата (`make_warning` с `WarningCode`, документа): `xlsb` → `XLSB_NO_FORMATS` у каждого
|
||
кандидата; непустой набор
|
||
неразобранных форматов всех листов кандидата → одно `NUMBER_FORMAT_UNPARSED` с перечнем (`«fmt»` через запятую,
|
||
длинные — до 30 символов с «…»).
|
||
5. `deadline.check(f"лист «{name}»: форма")` перед каждым листом.
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
```python
|
||
def test_balance_with_marked_continuation_and_income(rules: StatementRules) -> None: # спека: «Книга ББ + ОФР»
|
||
candidates = build_candidates([(balance, STATEMENT), (balance_2_marked, STATEMENT_CONTINUATION), (ofr, STATEMENT)],
|
||
rules, NO_DEADLINE, xlsb=False)
|
||
assert [(c.type_hint, c.locator) for c in candidates] == [
|
||
(DocumentType.BALANCE, "sheet:Баланс,Баланс (2)"), (DocumentType.INCOME, "sheet:ОФР")]
|
||
assert candidates[0].segment_breaks == (balance.n_rows,)
|
||
assert len({len(row) for row in candidates[0].rows}) == 1
|
||
assert candidates[0].source is Source.EXCEL
|
||
|
||
def test_refs_of_cells(rules: StatementRules) -> None: # спека: «Ссылки ячеек»
|
||
# код 1110 в C27 листа «Баланс» → ячейка сетки с text "1110" имеет ref "Баланс!C27";
|
||
# у листа «Бухгалтерский баланс. Лист 1» — "'Бухгалтерский баланс. Лист 1'!C27"
|
||
|
||
def test_marker_type_conflict_is_separate_candidate(rules: StatementRules) -> None: ...
|
||
# ББ, затем лист с «Форма 0710002 с.2» → 2 кандидата; у второго type_hint INCOME и
|
||
# [(w.code, w.severity) for w in c.warnings] == [("CONTINUATION_TYPE_CONFLICT", Severity.ERROR)]
|
||
|
||
def test_single_line_continuation_joins(rules: StatementRules) -> None: # спека: «Лист-продолжение с одной строкой»
|
||
# ОФР (3 колонки сумм), лист без шапки с единственной строкой 2400 → 1 кандидат, segment_breaks == (n_rows ОФР,)
|
||
|
||
def test_wider_sheet_with_three_codes_is_not_joined(rules: StatementRules) -> None: # «Лист другой ширины с тремя кодами»
|
||
# ББ (3 колонки сумм), лист без маркера и шапки с 1210, 1230, 1250 и 4 колонками → 2 кандидата,
|
||
# у второго SHEET_NOT_JOINED (error), type_hint None
|
||
|
||
def test_lonely_continuation_has_no_warning(rules: StatementRules) -> None: ...
|
||
def test_two_forms_on_one_sheet_share_locator(rules: StatementRules) -> None: ...
|
||
def test_xlsb_and_unparsed_formats_warnings(rules: StatementRules) -> None: ...
|
||
# xlsb=True → XLSB_NO_FORMATS у каждого кандидата; форматы «0.0%» на двух листах формы → одно NUMBER_FORMAT_UNPARSED
|
||
|
||
def test_plan_sheets_weak_continuation_only_after_form(rules: StatementRules) -> None: ...
|
||
# [ОФР, лист «2400»] → второй — STATEMENT_CONTINUATION; [ОСВ, лист «2400»] → other; [ОФР, «Примечания», «2400»] → other ×2
|
||
|
||
# tests/fixtures/test_statements_adapter_real.py
|
||
def test_npz_candidates(fixtures_dir: Path) -> None: # Review Focus 2
|
||
# read_book(НПЗ) → plan_sheets → build_candidates: [(type_hint, locator.count(",") + 1, len(segment_breaks))] ==
|
||
# [(BALANCE, 5, 4), (INCOME, 3, 2)]; ни у одного кандидата нет SHEET_NOT_JOINED / CONTINUATION_TYPE_CONFLICT
|
||
|
||
def test_unagrande_title_block_header(fixtures_dir: Path) -> None: # Review Focus 1
|
||
# «unagrande-group/2025-Q3/ББ. Лист 1 3кв.25.xls» → header.okopf.text == "12267", header.inn.text содержит "7728757141"
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests to verify they fail**
|
||
|
||
Run: `cd $T/api && .venv/bin/pytest tests/unit/parser/statements/test_adapter.py tests/fixtures/test_statements_adapter_real.py -v`
|
||
Expected: FAIL — `adapter`, `is_osv_sheet` не определены.
|
||
|
||
- [ ] **Step 3: Implement** `is_osv_sheet` (`classify_sheet` вызывает его, поведение то же) и `adapter.py`.
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `cd $T && make check`
|
||
Expected: зелёный; golden и манифест не меняются (книга ещё идёт старым конвейером).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git -C $T add api/src/parser/detect.py api/src/parser/statements/adapter.py api/tests
|
||
git -C $T commit -m "feat(statements): адаптер шага 1 — книга → FormCandidate, склейка листов, segment_breaks (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: Предупреждения ОСВ с кодами `OSV_*` (вывод пока прежний)
|
||
|
||
Модель исполнителя: sonnet.
|
||
|
||
**Files:**
|
||
- Create: `$T/api/src/parser/osv_warnings.py`
|
||
- Modify: `$T/api/src/parser/checks.py` (`run_checks`), `$T/api/src/parser/osv.py` (`parse_sheet`), `$T/api/src/parser/book.py`
|
||
(вызов), тесты `test_checks.py`, `test_book_parser.py` (только места вызова)
|
||
- Test: `$T/api/tests/unit/parser/test_osv_warnings.py`
|
||
|
||
**Interfaces:**
|
||
- Produces (`osv_warnings.py`):
|
||
- `OsvNote(code: str, message: str)` — frozen dataclass
|
||
- коды (P4-12): `OSV_HIERARCHY_BY_OUTLINE|INDENT|ACCOUNT_PATTERN|SUMS|TRIVIAL` (`hierarchy_note(strategy: str) ->
|
||
OsvNote`), `OSV_HEADER` (предупреждения шапки `doc.warnings`), `OSV_ROWS` (предупреждения строк `collect_body_rows`),
|
||
`OSV_INN_ROWS` (`glue.warnings`), `OSV_NO_DATA_ROWS`, `OSV_BALANCE_IDENTITY`, `OSV_PARENT_SUM`, `OSV_TOTAL` (итог не
|
||
найден или не равен корню), `OSV_XLSB_NO_OUTLINE` (заметка читателя, задача 7; «xls без кодовой страницы» — не
|
||
код ОСВ, а предупреждение файла `XLS_NO_CODEPAGE` пакета, P4-13)
|
||
- `legacy_line(sheet_name: str, note: OsvNote) -> str` — `f"sheet={sheet_name} {note.message}"` (вывод 1.x до задачи 7)
|
||
- Produces (`checks.py`): `run_checks(rows, exempt=None) -> tuple[OsvChecks, list[OsvNote]]` — тексты прежние, коды по
|
||
виду проверки.
|
||
- Produces (`osv.py`): `SheetResult(document: OsvDocument, notes: tuple[OsvNote, ...], ok: bool)`;
|
||
`OsvParser.parse_sheet(sheet, deadline) -> SheetResult`; сообщение `hierarchy_note` пока `hierarchy=<стратегия>`
|
||
(текст меняет задача 7).
|
||
|
||
- [ ] **Step 1: Write the failing tests**
|
||
|
||
```python
|
||
def test_check_notes_have_codes() -> None:
|
||
_, notes = run_checks(rows_with_identity_violation_and_parent_mismatch())
|
||
assert [note.code for note in notes] == ["OSV_BALANCE_IDENTITY", "OSV_PARENT_SUM"]
|
||
|
||
def test_parse_sheet_notes_start_with_hierarchy(osv_sheet: Sheet) -> None:
|
||
result = OsvParser(load_rules()).parse_sheet(osv_sheet, NO_DEADLINE)
|
||
assert result.notes[0].code.startswith("OSV_HIERARCHY_BY_")
|
||
assert all(note.code.startswith("OSV_") for note in result.notes)
|
||
|
||
def test_legacy_lines_are_unchanged(osv_sheet: Sheet) -> None:
|
||
# book.parse(...).metrics.warnings на синтетике ОСВ — те же строки, что до задачи (сравнить с литералом из текущего теста)
|
||
```
|
||
|
||
- [ ] **Step 2: Run tests to verify they fail**
|
||
|
||
Run: `cd $T/api && .venv/bin/pytest tests/unit/parser/test_osv_warnings.py -v`
|
||
Expected: FAIL — модуля нет.
|
||
|
||
- [ ] **Step 3: Implement** по Interfaces; `book.py` рендерит заметки `legacy_line`, порядок прежний.
|
||
- [ ] **Step 4: Run tests**
|
||
|
||
Run: `cd $T && make check && make golden-check`
|
||
Expected: зелёный; `golden-check` — код 0 (снимки ОСВ и форм не изменились).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git -C $T add api/src/parser api/tests
|
||
git -C $T commit -m "refactor(osv): предупреждения листа ОСВ с кодами OSV_* (вывод 1.x прежний) (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: Переключение на общий код и контракт v2.0
|
||
|
||
Модель исполнителя: sonnet (интеграция). Самая крупная задача: модели и копия схем, сборка книги, путь ОСВ и воркер
|
||
меняются вместе, иначе `make check` красный (P4-1). Golden и манифест — задача 8; до неё проверка **без фикстур**.
|
||
|
||
**Files:**
|
||
- Modify: `$T/api/contracts/**` (копия `$D/contracts/*.schema.json` верхнего уровня и `$D/contracts/examples/*.json`; файлы
|
||
`*.v1.schema.json` и `examples/v1/` не копируются), `$T/api/src/contract/{common,request,result,message,__init__}.py`,
|
||
`$T/api/src/parser/{__init__,book,detect,osv,cli}.py`, `$T/api/src/parser/readers/__init__.py`,
|
||
`$T/api/src/core/worker.py`, `$T/api/src/app.py`
|
||
- Create: `$T/api/src/parser/statements/pipeline.py`
|
||
- Test: `$T/api/tests/result_contract.py`, `$T/api/tests/golden_helpers.py` (`VOLATILE`),
|
||
`$T/api/tests/unit/test_contract.py`, `$T/api/tests/unit/test_contract_v12.py` → `test_contract_v20.py`,
|
||
`$T/api/tests/unit/parser/test_book_parser.py`, `$T/api/tests/unit/parser/statements/test_pipeline.py` (новый),
|
||
`$T/api/tests/unit/test_worker.py`, `$T/api/tests/unit/parser/test_cli.py`, `$T/api/tests/unit/parser/test_readers.py`,
|
||
`$T/api/tests/synthetic/test_statement_synthetic_files.py`, `$T/api/tests/synthetic/test_synthetic_files.py`,
|
||
`$T/api/tests/integration/test_roundtrip.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: задачи 3–6; `normalize_candidate`, `envelope_status`, `make_warning`, `WarningCode`, `to_contract`,
|
||
`warning_to_contract`, `default_rules`, `statements_version`, `DocStatus`, `RulesError`.
|
||
- Produces (контракт, `src/contract/`):
|
||
- `CONTRACT_VERSION = "2.0"` (результат и конверт); `SCHEMA_VERSION_PATTERN = r"^2\.\d+$"`;
|
||
`REQUEST_SCHEMA_VERSION_PATTERN = r"^1\.\d+$"`, `RequestSchemaVersion` — у `RecognitionRequest`
|
||
- `ResultWarning(code: str (pattern ^[A-Z][A-Z0-9_]*$), severity: Literal["error", "info"], message: str,
|
||
line_refs: list[str] | None, column_index: int | None, field: HeaderFieldName | None)`; `FileWarning(ResultWarning)` +
|
||
`locator: str | None`; `Metrics.warnings: list[FileWarning] | None`
|
||
- `ServiceInfo.statements_version: str | None` (`min_length=1`); `RecognitionResult` — валидатор: при наличии
|
||
`StatementDocument` поле обязательно (как `allOf` схемы)
|
||
- `OsvDocument` + `status: DocumentStatus`, `warnings: list[ResultWarning]`
|
||
- `StatementHeader` — поля схемы v2.0 (`form_name`, по каждому полю `<поле>`, `<поле>_text`, `<поле>_confidence`,
|
||
`needs_review_fields`); `StatementColumn` (+ `label_confidence`, `period_kind`); `StatementValue` (+ `text`,
|
||
`needs_review`); `StatementLine` (`row_index`, `ref`, `line_status`, `line_code: str | None`, `line_code_text`,
|
||
`line_code_confidence`, `line_name: str | None`, `line_name_confidence`, `parent_ref`, `needs_review`, `values`;
|
||
валидатор: `line_code` не `None` ⇔ `line_status == "ok"`); `FailedRelation` (+ `codes`, `expected`/`actual` —
|
||
`Money | None`: `null` — цель или все члены пусты, план 1 P1-21); `StatementChecks` без `duplicate_line_codes`; `StatementDocument` (`document_type: Literal["BALANCE",
|
||
"INCOME", "DISCLOSURE"]`, `form_variant: Literal["full", "simplified"] | None` обязателен, `status`, `warnings`;
|
||
валидатор `checks` по `allOf` схемы)
|
||
- Перевод в контракт — пакетом (план 1, задача 19), своего `statements/contract.py` нет: документ формы —
|
||
`StatementDocument.model_validate(to_contract(document))` (шапка плоская, `form_name` из `document.header.form_name`,
|
||
без `column_key`, `checks` нет ⇒ ключа нет); предупреждение файла — `FileWarning.model_validate(warning_to_contract(w,
|
||
file_level=True))`; предупреждение листа ОСВ (`OsvNote` → `ResultWarning(code, severity="info", message)`) строится
|
||
моделью напрямую (коды `OSV_*` не в словаре пакета)
|
||
- Produces (`statements/pipeline.py`):
|
||
- `StatementsOutcome(documents: tuple[StatementDocument, ...], file_warnings: tuple[FileWarning, ...])`
|
||
- `StatementPipeline(rules: StatementRules)` с методом `parse(sheets: Sequence[tuple[Sheet, SheetKind]], deadline:
|
||
Deadline, *, xlsb: bool) -> StatementsOutcome` — `build_candidates` → для каждого кандидата `deadline.check` и
|
||
`normalize_candidate` (P4-11) → `to_contract` / `warning_to_contract` (заголовок формы — уже в
|
||
`document.header.form_name`)
|
||
- Produces (книга): `BookParser(rules, *, osv: OsvParser, statements: StatementPipeline, statement_rules:
|
||
StatementRules, max_bytes, timeout_seconds, clock)`; `ParseOutcome` прежний (документы — модели v2.0,
|
||
`metrics.warnings` — `FileWarning`); `readers.BookRead(sheets, warnings, detected, mismatch: str | None)` — строка
|
||
несовпадения формата больше не в `warnings`; `detect.classify_sheet(sheet, rules, statement_rules: StatementRules |
|
||
None = None)` — ветка форм через `forms.classify_statement_sheet` (`None` → `default_rules()`)
|
||
- Produces (`src/parser/__init__.py`): `require_statements_version(path: Path | None = None) -> str` —
|
||
`statements_version(path)`, `None` → `RuntimeError("копия arm_statements без VERSION: разложите её sync-statements")`
|
||
(P4-16); `get_parser` грузит `default_rules()` (`RulesError` → `RuntimeError` с текстом ошибки) и собирает
|
||
`StatementPipeline`
|
||
- Produces (воркер): `RecognitionWorker(..., statements_version: str)` → `ServiceInfo(name, version,
|
||
statements_version)` в конверте и файле результата; `app.py` передаёт `require_statements_version()`; CLI — тоже.
|
||
|
||
Сборка книги (`BookParser.parse`; спека «Статус книги», «Путь ОСВ в контракте v2.0»):
|
||
1. лимиты и чтение — как сейчас; `read.mismatch` → `FILE_TYPE_MISMATCH` (файл, `info`, сообщение — прежний текст
|
||
«file_type=…, но содержимое …: прочитано как …»);
|
||
2. `plan_sheets(read.sheets, is_osv_sheet, statement_rules)`; листы ОСВ — по порядку `parse_sheet`: документ получает
|
||
`status = done if ok else needs_check` и `warnings` из заметок (`severity = info`), плюс заметка читателя
|
||
`OSV_XLSB_NO_OUTLINE` в каждый документ ОСВ книги (P4-13); заметка «xls без кодовой страницы» — предупреждение
|
||
файла `make_warning(WarningCode.XLS_NO_CODEPAGE, …)` (`info`, без `locator`) в `metrics.warnings`, в любой книге xls
|
||
(с ОСВ, формами или и тем, и другим); сообщение иерархии — «иерархия строк
|
||
— по группировке листа (outline)» / «по отступам (indent)» / «по кодам счетов (account_pattern)» / «по суммам (sums)» /
|
||
«плоский список (trivial)»;
|
||
3. листы `other` → `SHEET_OTHER` (файл, `info`, `locator = sheet:<имя>`, сообщение «лист без признаков ОСВ и формы»);
|
||
4. формы — `StatementPipeline.parse(plan.statements, deadline, xlsb=read.detected == "XLSB")`;
|
||
5. документов нет → `INVALID_DOCUMENT` (сообщение прежнее); иначе `status = envelope_status(статусы ОСВ и форм как
|
||
DocStatus, предупреждения файла)`; `metrics.warnings` — все предупреждения файла по порядку листов; `rows` — как сейчас
|
||
(строки данных ОСВ + `len(lines)`);
|
||
6. ошибка листа ОСВ (`ParserError`) — исход `error` как сейчас; предупреждение «форма не разобрана: книга завершилась
|
||
ошибкой листа ОСВ» больше не пишется (P4-14), `metrics.warnings` ошибки — собранные до неё предупреждения файла.
|
||
|
||
- [ ] **Step 1: Копия контракта**
|
||
|
||
```bash
|
||
cp $D/contracts/recognition-request.schema.json $D/contracts/recognition-result.schema.json \
|
||
$D/contracts/recognition-result-message.schema.json $T/api/contracts/
|
||
cp $D/contracts/examples/*.json $T/api/contracts/examples/
|
||
```
|
||
|
||
- [ ] **Step 2: Write the failing tests** (выборка; остальные тесты файлов из списка правятся под v2.0 по смыслу)
|
||
|
||
```python
|
||
# tests/unit/test_contract_v20.py
|
||
def test_docs_examples_validate_with_models() -> None:
|
||
for name in ("result-osv-minimal", "result-osv-needs-check", "result-statement-excel", "result-statement-minimal",
|
||
"result-error"):
|
||
payload = json.loads((EXAMPLES_DIR / f"{name}.json").read_text(encoding="utf-8"))
|
||
assert RecognitionResult.model_validate(payload).to_wire() == payload
|
||
|
||
def test_statement_without_statements_version_is_rejected() -> None: ...
|
||
def test_unreadable_line_with_code_is_rejected() -> None: ... # спека: «Строка без кода с кодом в поле»
|
||
def test_request_stays_v1_and_result_is_v2() -> None:
|
||
assert CONTRACT_VERSION == "2.0"
|
||
RecognitionRequest.model_validate(load_example("request-tabular")) # schema_version "1.0" проходит
|
||
|
||
# tests/unit/parser/statements/test_pipeline.py
|
||
def test_document_converts_to_contract(statement_rules: StatementRules) -> None:
|
||
outcome = StatementPipeline(statement_rules).parse([(balance_sheet, SheetKind.STATEMENT)], NO_DEADLINE, xlsb=False)
|
||
document = outcome.documents[0]
|
||
assert document.header.form_name == "Бухгалтерский баланс"
|
||
assert (document.header.inn, document.header.inn_text, document.header.inn_confidence) == ("7700000000", "7700000000", 100)
|
||
assert document.lines[0].ref == "Баланс!C10"
|
||
assert_valid_result(ParseOutcome(status=ResultStatus.DONE, documents=outcome.documents))
|
||
|
||
# tests/unit/parser/test_book_parser.py
|
||
def test_osv_document_has_status_and_osv_warnings() -> None: # спека: «Предупреждение ОСВ»
|
||
outcome = parse_book(osv_with_identity_violation)
|
||
doc = outcome.documents[0]
|
||
assert doc.status == "needs_check"
|
||
assert all(w.severity == "info" and w.code.startswith("OSV_") for w in doc.warnings)
|
||
assert "OSV_BALANCE_IDENTITY" in {w.code for w in doc.warnings}
|
||
assert not any(w.message.startswith("sheet=") for w in outcome.metrics.warnings)
|
||
|
||
def test_other_sheet_is_file_warning() -> None: # спека: «Лист «прочее»»
|
||
# книга [ББ, «Примечания»] → документов 1, metrics.warnings == [SHEET_OTHER info, locator "sheet:Примечания"]
|
||
|
||
def test_file_type_mismatch_is_info_file_warning() -> None: ...
|
||
def test_xls_without_codepage_is_file_warning() -> None: ...
|
||
# книга xls (ОСВ + ББ) с заметкой читателя «нет кодовой страницы» → metrics.warnings содержит
|
||
# {code: "XLS_NO_CODEPAGE", severity: "info"}; у документов ОСВ нет OSV_XLS_NO_CODEPAGE
|
||
def test_disclosures_only_book_is_done() -> None: # спека: «Книга только из пояснений»; Review Focus 5
|
||
# листы «0710003» (ОКУД, без кодов) и «Пояснения к бухгалтерскому балансу» (без кодов) →
|
||
# [(d.document_type, d.status) for d in docs] == [("DISCLOSURE", "done")] * 2, outcome.status is DONE
|
||
|
||
def test_disclosure_title_with_balance_codes() -> None: # Review Focus 5
|
||
# «Пояснения к бухгалтерскому балансу» + коды 1110…1700 → BALANCE и TYPE_CONFLICT (error)
|
||
|
||
def test_xlsb_form_scale_suspect() -> None: # Review Focus 4
|
||
# write_xlsb({"Баланс": форма ББ, «тыс. руб.», 40 % дробных}) → warnings ⊇ {XLSB_NO_FORMATS info,
|
||
# XLSB_SCALE_SUSPECT error}, status needs_check; без дробных → только XLSB_NO_FORMATS, status done
|
||
|
||
def test_book_without_osv_and_forms_is_invalid_document() -> None: ...
|
||
def test_documents_order_osv_then_forms() -> None: ...
|
||
|
||
# tests/unit/test_worker.py
|
||
def test_result_carries_statements_version() -> None: # спека: «Версия общего кода в результате»
|
||
# RecognitionWorker(..., statements_version="abc1234") → result.service.statements_version == "abc1234";
|
||
# конверт и ack processing — schema_version "2.0"; запрос "1.0" принимается, "2.0" → SCHEMA_MISMATCH без ack
|
||
|
||
def test_require_statements_version_fails_without_file(tmp_path: Path) -> None:
|
||
with pytest.raises(RuntimeError, match="VERSION"):
|
||
require_statements_version(tmp_path / "VERSION")
|
||
```
|
||
|
||
- [ ] **Step 3: Run tests to verify they fail**
|
||
|
||
Run: `cd $T/api && ARM_KA_FIXTURES=/nonexistent .venv/bin/pytest tests/unit tests/synthetic -m "not integration" -x -q`
|
||
Expected: FAIL (модели 1.2, конвейер старый).
|
||
|
||
- [ ] **Step 4: Implement** по Interfaces и «Сборке книги». `tests/result_contract.py`: `service` с
|
||
`"statements_version": "test"`; `golden_helpers.VOLATILE` += `("service", "statements_version")`; `render` и CLI
|
||
получают версию через `require_statements_version()`. Старые модули форм (`statement.py` и др.) больше не вызываются
|
||
книгой, но остаются до задачи 9 вместе со своими тестами.
|
||
- [ ] **Step 5: Run tests**
|
||
|
||
Run: `cd $T && ARM_KA_FIXTURES=/nonexistent make check`
|
||
Expected: зелёный (тесты на реальных фикстурах пропущены — их чинит задача 8; тест идентичности копии контракта против
|
||
`$D` — PASSED, не SKIPPED: `cd api && .venv/bin/pytest -rs -k vendored_contracts`).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git -C $T add api
|
||
git -C $T commit -m "feat: формы через arm_statements, контракт результата и конверта v2.0, статус и OSV_* у ОСВ (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: Golden и манифест реальных форм
|
||
|
||
Модель исполнителя: sonnet (сверка на реальных данных).
|
||
|
||
**Files:**
|
||
- Modify: `$F/golden/expected_osv_parse/**`, `$F/golden/expected_statement_parse/**` (только `make golden-update`)
|
||
- Modify: `$T/api/tests/statements_manifest.yaml`, `$T/api/tests/statement_helpers.py`,
|
||
`$T/api/tests/fixtures/test_statements_property.py`, `$T/api/tests/fixtures/test_statements_number_format.py`,
|
||
прочие `tests/fixtures/*` по факту падений
|
||
- Create: `$T/api/tests/fixtures/test_statements_real.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: книга задачи 7.
|
||
- Produces: `DocumentExpectation` + `error_warnings: list[str] | None` (P4-18: точное множество кодов предупреждений
|
||
документа уровня `error`; `None` — не проверяется).
|
||
|
||
- [ ] **Step 1: Обновить golden**
|
||
|
||
Run: `cd $T && make golden-update`
|
||
Expected: перезаписаны снимки ОСВ и форм, в выводе unified diff.
|
||
|
||
- [ ] **Step 2: Проверить, что ОСВ изменилась только по ключам v2.0**
|
||
|
||
```bash
|
||
cd $F && git diff --name-only -- golden/expected_osv_parse | while read -r f; do
|
||
git show "HEAD:$f" > /tmp/old.json
|
||
python3 - /tmp/old.json "$f" <<'EOF'
|
||
import json, sys
|
||
old, new = (json.load(open(p, encoding="utf-8")) for p in sys.argv[1:])
|
||
assert old["status"] == new["status"], (sys.argv[2], old["status"], new["status"])
|
||
for d in (old, new):
|
||
d.pop("schema_version", None); d.get("metrics", {}).pop("warnings", None)
|
||
for doc in d.get("documents", []): doc.pop("status", None); doc.pop("warnings", None)
|
||
assert old == new, sys.argv[2]
|
||
EOF
|
||
done; echo "osv ok"
|
||
```
|
||
Expected: `osv ok` без `AssertionError` (временный файл — в scratchpad исполнителя, не `/tmp`, если окружение требует).
|
||
|
||
- [ ] **Step 3: Просмотреть снимки форм** по каждому файлу `expected_statement_parse/**`: статусы документов против
|
||
манифеста, предупреждения `error` с объяснением, строки `detail`/`unreadable`/`missing` — глазами против Review Focus
|
||
1–3. Каждое изменение статуса документа против манифеста — с причиной в отчёте задачи (пойдёт в описание PR).
|
||
- [ ] **Step 4: Манифест** (спека «Манифест ожиданий на реальных формах»): `failed_relations: ["1600 = 1700"]` у
|
||
`unagrande-co/2024-FY/Б.Б. Лист 2.xls`, `unagrande-group/2024-FY/ББ. Лист 2 (2).xls`,
|
||
`unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` (было `[]`, причина «страница 2 без шапки» остаётся); страницы 1 «только
|
||
актив» сохраняют `["1600 = 1700"]`; статусы `done` (Межениновская ×2, НПЗ ×2, ОФР «Лист 1» Унагранде ×2) не меняются;
|
||
`error_warnings` — у каждого документа по прогону (например, страницы 2 — `["NO_ORGANIZATION_NO_INN"]` или как покажет
|
||
прогон, с объяснением); остальные записи — по прогону, каждое изменение — строкой в отчёт.
|
||
- [ ] **Step 5: Property-тест под v2.0** (`test_statements_property.py`): статус — `document.status`; удалить проверку
|
||
уникальности кодов (повторы законны); `value.column_index ∈ {c.column_index for c in document.columns}`;
|
||
`needs_check` объясним: есть предупреждение `error`, или `checks.relations_failed`, или `needs_review` у строки/значения,
|
||
или `header.needs_review_fields`, или строка `unreadable`/`missing`; `ref` уникальны в документе; `error_warnings`
|
||
манифеста сверяются точным множеством.
|
||
- [ ] **Step 6: Тесты Review Focus на реальных книгах** (`tests/fixtures/test_statements_real.py`, маркер `fixtures`)
|
||
|
||
```python
|
||
def test_unagrande_title_codes_block(fixtures_dir: Path) -> None: # Review Focus 1
|
||
for key in TITLE_BLOCK_FILES: # пять файлов «Лист 1» Унагранде
|
||
for doc in statement_docs(parse(fixtures_dir, key)):
|
||
assert doc.header.okopf == "12267"
|
||
assert not any("12267" in (line.line_code_text or "") for line in doc.lines)
|
||
|
||
def test_npz_documents(fixtures_dir: Path) -> None: # Review Focus 2; спека «НПЗ «Реализация иного имущества»»
|
||
balance, income = statement_docs(parse(fixtures_dir, NPZ))
|
||
assert (balance.status, income.status) == ("done", "done")
|
||
bad = {"SHEET_NOT_JOINED", "SEGMENT_COLUMNS_MISMATCH", "SEGMENT_WITHOUT_CODES", "HEADER_ROW_NOT_FOUND"}
|
||
assert not bad & {w.code for d in (balance, income) for w in d.warnings}
|
||
cols = {c.column_index for c in balance.columns}
|
||
assert all(v.column_index in cols for line in balance.lines for v in line.values)
|
||
by_ref = {line.ref: line for line in income.lines}
|
||
other = next(l for l in income.lines if l.line_name and l.line_name.startswith("Реализация иного имущества"))
|
||
assert other.line_status == "detail" and by_ref[other.parent_ref].line_code == "2340"
|
||
assert any(l.line_code == "2400" and l.ref.startswith("'Отчет о финансовых результата_2'") for l in income.lines)
|
||
|
||
def test_mezheninovskaya_numbering_and_signatures(fixtures_dir: Path) -> None: # Review Focus 3
|
||
for key in MEZHENINOVSKAYA:
|
||
(doc,) = statement_docs(parse(fixtures_dir, key))
|
||
assert doc.status == "done" and doc.header.inn == "7016006635"
|
||
assert not any(l.line_code_text == "2" for l in doc.lines)
|
||
assert not any(l.line_status in ("unreadable", "missing") for l in doc.lines)
|
||
```
|
||
(точное имя листа ОФР НПЗ — по golden; если тест 2 или 3 падает на ролях строк — стоп, находка для плана 1.)
|
||
|
||
- [ ] **Step 7: Run tests**
|
||
|
||
Run: `cd $T && make check && make golden-check`
|
||
Expected: зелёный с фикстурами; `golden-check` — 0.
|
||
|
||
- [ ] **Step 8: Commit** (два репозитория, отдельные коммиты)
|
||
|
||
```bash
|
||
git -C $F add golden
|
||
git -C $F commit -m "golden: снимки ОСВ и форм под контракт v2.0 (arm-tabular-parser#<issue>)"
|
||
git -C $T add api/tests
|
||
git -C $T commit -m "test: манифест реальных форм и Review Focus под общий код и v2.0 (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: Удаление собственного кода шагов 2–8 и словарей форм
|
||
|
||
Модель исполнителя: haiku (удаление по списку, без нового кода).
|
||
|
||
**Files:**
|
||
- Delete: `$T/api/src/parser/statements/{checks,columns,detect,header,lines,merge,statement}.py`,
|
||
`$T/api/rules/statement_{headers,periods,checks}.yaml`,
|
||
`$T/api/tests/unit/parser/statements/{test_checks,test_columns,test_detect,test_header,test_lines,test_statement}.py`,
|
||
`$T/api/tests/unit/parser/test_statement_rules.py`
|
||
- Modify: `$T/api/src/parser/rules.py` (убрать `StatementHeaderRules`, `StatementPeriodRules`, модели §7, `FormType`,
|
||
`StatementLabel`, поля `Rules.statement_*` и их загрузку), `$T/api/tests/rules_coverage.py` (`DICTIONARIES`),
|
||
`$T/api/tests/unit/parser/test_rules.py`, `$T/api/tests/fixtures/test_rules_coverage.py` (без `StatementParser`),
|
||
`$T/api/src/parser/numbers.py` (удалить функции без вызывающих после удаления — только по `grep`), `$T/api/rules/README.md`,
|
||
`$T/README.md` (раздел «Парсер форм»), `$T/CLAUDE.md` (раздел Среза 2b → «Формы через arm_statements»: модули адаптера,
|
||
словари форм в пакете, контракт 2.0, OSV_*, правило «копию не править»), `$T/api/tests/synthetic/generate_statements.py`
|
||
(импорты)
|
||
|
||
- [ ] **Step 1: Удалить файлы и ссылки**; затем
|
||
|
||
Run: `cd $T/api && grep -rnE "statement_(headers|periods|checks)|statements\.(checks|columns|detect|header|lines|merge|statement)\b|StatementParser|duplicate_line_codes" src tests rules scripts`
|
||
Expected: пусто (кроме `tests/synthetic/generate_statements.py`, если он ссылается на имена файлов фикстур — тогда не пусто
|
||
только там и это не импорт).
|
||
|
||
- [ ] **Step 2: Run tests**
|
||
|
||
Run: `cd $T && make check && make golden-check`
|
||
Expected: зелёный; `golden-check` — 0 (удаление не меняет вывод); `test_no_coordinates.py` и покрытие словарей ОСВ проходят.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git -C $T add -A api README.md CLAUDE.md
|
||
git -C $T commit -m "refactor: удалён собственный конвейер форм и словари форм — шаги 2–8 в arm_statements (#<issue>)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: Полная проверка, PR tabular-parser и fixtures
|
||
|
||
Модель исполнителя: sonnet. Перед задачей — финальное ревью ветки (opus) по SDD.
|
||
|
||
- [ ] **Step 1: Полная проверка**
|
||
|
||
Run: `cd $T && make check && make check-full && make statements-check; echo "exit=$?"; cd $T && openspec validate unified-normalization-tp --strict`
|
||
Expected: всё зелёное; `statements-check` — `0` или `10` («копия отстала» — вписать в описание PR); change валиден.
|
||
|
||
- [ ] **Step 2: Push и PR** (две ветки, два PR, взаимные ссылки)
|
||
|
||
```bash
|
||
git -C $F push -u origin feat/<fx-issue>-statements-v2-golden
|
||
tea pulls create --login home --repo RSHB/arm-ka-fixtures --head feat/<fx-issue>-statements-v2-golden --base main \
|
||
--title "Golden ОСВ и форм под контракт v2.0" --description "<closes #<fx-issue>; парный PR arm-tabular-parser>"
|
||
git -C $T push -u origin feat/<issue>-statements-v2
|
||
tea pulls create --login home --repo RSHB/arm-tabular-parser --head feat/<issue>-statements-v2 --base main \
|
||
--title "Формы через arm_statements, контракт v2.0" --description "<см. ниже>"
|
||
```
|
||
|
||
Описание PR tabular-parser: цель; ссылки на arm-ka-docs#116, этот план, парный PR fixtures; **условие слияния** — ядро с
|
||
приёмом 2.0 выложено (`proposal.md`, шаг 5), docs #117 слит, PR fixtures сливается сразу вслед; изменения манифеста с
|
||
причинами (задача 8); доля `done` на реальных формах до/после; «Rulings I made» (P4-n и R-n контроллера); таблица
|
||
находок с маркерами и статусами; `deferred` — отдельным списком.
|
||
- [ ] **Step 3: Решения в design.md** — контроллер переносит P4-n и свои R-n в
|
||
`openspec/changes/unified-statement-normalization/design.md` docs (`## Decisions made without discussion`, дата) — в
|
||
ветку `feat/116-unified-normalization`, если PR #117 не слит, иначе отдельным PR docs.
|
||
|
||
---
|
||
|
||
## Решения автора плана без обсуждения (P4-n)
|
||
|
||
Контроллер переносит их в `design.md` docs, раздел `## Decisions made without discussion` (дата 18.10.2026).
|
||
|
||
- **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` строк различаются).
|
||
|
||
## Чего не хватает в API пакета для адаптера Excel (O-n)
|
||
|
||
Пункты O-1…O-7 и O-9 закрыты сведением с планом 1 (18.10.2026, его P1-21…P1-31); маркеры сохранены.
|
||
|
||
- **O-1.** Закрыт: `code_column(rows, rules, *, relaxed)` и `amount_columns(rows, code_col, rules, *, source) ->
|
||
tuple[int, ...]` в API (P1-28); адаптер зовёт их через `sheet_code_column` / `sheet_amount_columns` (задача 4), своего
|
||
правила (б) нет.
|
||
- **O-2.** Закрыт: `normalize_label` и `normalize_caption` в API, таблица «ключ словаря → нормализация» (P1-22); P4-7.
|
||
- **O-3.** Закрыт: `StatementHeader.form_name` в пакете (P1-29); адаптер `form_name` не ставит.
|
||
- **O-4.** Закрыт: `is_continuation(text, rules) -> bool` в API; `continuation_marker` на нём (задача 4).
|
||
- **O-5.** Закрыт: код `XLS_NO_CODEPAGE` (`info`, файл) в словаре пакета, таблице спеки и §11.9 (P1-23); P4-13.
|
||
- **O-6.** Закрыт: ключи `Rules.headers.labels` перечислены в API плана 1 (`LABEL_KEYS`), загрузчик пакета их проверяет.
|
||
- **O-7.** Закрыт: описка «плана 3» в плане 1 исправлена на «плана 4».
|
||
- **O-8.** Покрытие словарей форм реальными файлами теряется (P4-19): в пакете тесты только на синтетике. Предложение:
|
||
тест покрытия `Rules.headers.*` в tabular-parser на `statements-xlsx/**` (как покрытие словарей ОСВ) — follow-up.
|
||
- **O-9.** Закрыт: `min_codes_continuation: 1` — в `statement_code_ranges.yaml` пакета (`Rules.code_ranges`), им
|
||
пользуются ослабленный поиск `code_column` и `sheet_joins` (P1-28).
|
||
- **O-10.** Сверено с планом 2 (появился в ветке во время написания): подготовку хоста делает его задача 8, задача 2
|
||
этого плана сведена к сверке и `--in-place`; `.dockerignore` не меняется (план 2: «`api/arm_statements` не добавлять в
|
||
`.dockerignore`» — тесты копии попадают в образ, это его решение). Пин `pytest` план 2 не вводит — здесь тоже нет.
|
||
|
||
## Открытые вопросы владельцу
|
||
|
||
- **В-1.** Гранулярность кодов `OSV_*` (P4-12): хватает ли кодов по источнику, или экрану нужны отдельные коды для
|
||
случаев внутри `OSV_HEADER` (организация, счёт, период, валюта) и `OSV_ROWS`? Ответ меняет только `osv_warnings.py` и
|
||
golden.
|
||
- **В-2.** Межениновская и НПЗ должны остаться `done` (спека манифеста). Если прогон задачи 8 покажет, что подписи под
|
||
формой Межениновской или лист 4 НПЗ дают `missing`/`unreadable` или `SEGMENT_*` по правилам шагов 2–3 пакета — это
|
||
противоречие спеки и реального файла: решение (правка пакета или манифеста) — за владельцем, адаптер под файл не
|
||
подгоняется.
|
||
- **В-3.** Слияние: PR tabular-parser и PR fixtures — после выкладки ядра с приёмом 2.0 (части А и Б плана 3) и слияния
|
||
docs #117; порядок относительно scan-gateway свободный. Подтвердить, что до этого PR остаётся открытым, а не
|
||
сливается с флагом.
|