arm-tabular-parser/docs/superpowers/plans/2026-10-28-osv-other-systems.md
Andrey Epifancev bb9eb29659 plan: ОСВ других учётных систем (#81)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:07:03 +04:00

263 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ОСВ других учётных систем — план реализации
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** листы ОСВ SAP, Navision, выгрузок КТЗ и отчётов 1С с другим названием распознаются как ОСВ: признаки листа по
названию, запасной признак по полному заголовку таблицы, написания групп и сторон, итог «ВСЕГО».
**Architecture:** новые написания — только записи `api/rules/osv_headers.yaml` + синтетика на каждую (R-22). Код меняется
в одном месте: запасной признак `has_osv_table` в `detect.py`, который `plan_sheets` вызывает для листа, попавшего бы в
«прочее». Регрессия — прогон банка первички до и после, сравнение по документам.
**Tech Stack:** Python 3.11, pytest, ruff, mypy; openpyxl (генератор синтетики); OpenSpec.
**Spec:** `tabular-parser/openspec/changes/osv-other-systems/` (proposal.md, design.md, specs/osv-parsing/spec.md).
Правило — `docs/context/05-domain-rules.md` §1.1–§1.2 (меняется в Task 5). Issue RSHB/arm-tabular-parser#81. Jira
AURORA-1717, AURORA-1719, AURORA-1732. Ветка `feat/81-osv-other-systems` в трёх worktree: `.wt/osv-dict/tabular-parser`,
`.wt/osv-dict/fixtures`, `.wt/osv-dict/docs` (корень `W = /Users/andrey.epifancev/projects/awp-ca/.wt/osv-dict`).
## Global Constraints
- Никаких `if` под файл, организацию, систему-источник или имя листа: новое написание = запись YAML + синтетика (CLAUDE.md
tabular-parser, «Правила расширения»).
- Каждая новая запись словаря срабатывает хотя бы на одной синтетике (`tests/fixtures/test_rules_coverage.py`, R-22).
- Паттерны пишутся по нормализованному тексту: нижний регистр, «ё» → «е», дефисы/тире/переносы/nbsp → пробел, пробелы
схлопнуты (шапка `osv_headers.yaml`).
- Новые `sheet_markers` — с якорем `^` (R-5); якорь снимается для названия, только если файлы списка 1717 без этого не
находятся (записать в design.md, раздел решений).
- Запасной признак: только лист, который `plan_sheets` иначе отнёс бы к «прочее»; строка групп в первых
`SHEET_MARKER_ROWS` (30) строках; все три группы; у каждой — и Дебет, и Кредит; свёрнутое сальдо не годится (D1, R-2,
R-3, R-6).
- Пакет `api/arm_statements/` не трогать. Контракт не меняется.
- Код — только Edit/Write, не sed/скрипты через Bash (правило владельца 18.10.2026). cwd оболочки держать в
`W/tabular-parser`; fixtures и docs править по абсолютным путям, git — `git -C <абсолютный путь>`.
- Файлы банка первички (`/Users/andrey.epifancev/projects/awp-ca/doc-bank/`) — только чтение, в git не кладутся.
- Golden правится только `make golden-update` с просмотром diff, отдельным коммитом в fixtures.
- Комментарии, сообщения коммитов — по-русски; коммиты заканчиваются строкой
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
## Review Focus
1. Расшифровка ББ, которую пакет не узнал как форму («Остаток на начало | Поступило | Выбыло | Остаток на конец»),
не становится ОСВ: в ней нет оборотов → `has_osv_table` ложь (тест в Task 3).
2. Преамбула шапки «Остаток на начало: … | Остаток на конец: …» над таблицей не принимается за строку групп: в ней нет
«оборотов» (тест в Task 2 на синтетике s49 — преамбула выше таблицы).
3. Сторона без границы слова («ДТВВ») не ловится в ячейках, где нет группы: «Дт» внутри текста контрагента
(«ООО Дтвв-Сервис») не даёт стороны (тест в Task 2).
4. Лист «прочее» после листа формы, который сейчас идёт как слабое продолжение (`is_weak_continuation`), остаётся
продолжением — запасной признак проверяется только в ветке «прочее» (тест в Task 3).
5. Книга с формами и без единого листа ОСВ: на листах форм `has_osv_table` не вызывается (тест в Task 3 — счётчик
вызовов на книге s14).
---
### Task 1: Окружение и базовый прогон банка первички
**Files:**
- Create: `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/regress.py`
- Create: `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/headers.md`
- Create (вывод): `.../osv-other-systems-regress/base.csv`
**Interfaces:**
- Produces: `regress.py run <out.csv>` и `regress.py diff <base.csv> <new.csv>`; `headers.md` — точные тексты шапок и
заголовков реальных образцов для синтетики Task 2–3.
- [ ] **Step 1: venv.** `cd W/tabular-parser && make venv` → `api/.venv/bin/python` есть; `make check` зелёный на
исходной ветке (golden и покрытие читают `../fixtures` — это worktree fixtures на той же базе `f1f7f83`).
- [ ] **Step 2: `regress.py run`.** Обходит `doc-bank/index/index.csv`, строки с `route == "парсер"`; разбирает файл
в процессе тем же путём, что `src.parser.cli` (`get_parser(Settings())`, тип файла — по расширению), запуск из
`W/tabular-parser/api` с `ENV_FILE=.env.test` и `PYTHONPATH=.`. Пишет CSV, по строке на документ (на файл без
документов — одна строка): `sha256, name, locator, kind (osv|BALANCE|INCOME|…|none), status, error_code,
sheet_other_count`. Исключение парсера — строка с `error_code = EXCEPTION:<класс>`, прогон не прерывается.
- [ ] **Step 3: `regress.py diff`.** Сопоставляет по `(sha256, locator)`; печатает таблицу переходов `(kind, status) →
(kind, status)` с числом файлов и список ухудшений по D3: `done → needs_check|error`, `needs_check → error`, любой
`kind` формы → `osv`, `none/other → osv`. Переходы в `osv` помечает «проверить вручную» с именем файла.
- [ ] **Step 4: базовый прогон.** `regress.py run base.csv` на текущем HEAD ветки (код = main `a8c9605`). Ожидаемо по
индексу: ~1681 файл; среди ошибок — `INVALID_DOCUMENT` у файлов из списка 1717.
- [ ] **Step 5: образцы заголовков.** Из `doc-bank` найти по `index.csv` (`parse_bug` содержит `AURORA-1717`/`1719`)
по одному файлу на каждое написание из таблиц AURORA-1717 и 1719 (+ «ВСЕГО» из 1732) и выписать в `headers.md`
дословно: строку названия, строку(и) групп и подзаголовков (ячейка за ячейкой), подпись итога. Особо: как у Navision
подписан оборот («ОборотДт»? «Оборот Дт»?), как у SAP НМЖК — «Оборот» со стороной.
Коммита нет (каталог `admin/` не репозиторий).
---
### Task 2: Словари и синтетика написаний (1717 названия, 1719 группы и стороны, 1732 «ВСЕГО»)
**Files:**
- Modify: `api/rules/osv_headers.yaml` (`sheet_markers`, `groups.opening`, `groups.closing`, `groups.turnover` — если
нужно для «ОборотДт», `debit`, `credit`, `total_row_exact`)
- Modify: `api/tests/synthetic/generate.py` (`OsvSpec`, `_header_v1`, `_header_rows`, `build_osv`, новые `_s48…_s50`,
`FILE_NAMES`, docstring модуля)
- Modify: `api/tests/synthetic/test_synthetic_files.py` (`EXPECTED`, список файлов, новые тесты)
- Modify: `api/tests/unit/parser/test_detect.py`
- Create (генерация): `W/fixtures/osv/synthetic/s48_*.xlsx`, `s49_*.xlsx`, `s50_*.xlsx`; Modify:
`W/fixtures/osv/synthetic/README.md`
**Interfaces:**
- Consumes: `headers.md` из Task 1.
- Produces: поля `OsvSpec` (используются в Task 3):
- `title: str | None = None` — строка названия вместо «Оборотно-сальдовая ведомость по счету {account} за {period}»
(`None` — как сейчас);
- `untitled: bool = False` — без строк организации, названия, параметров: заголовок таблицы с первой строки;
- `split_groups: dict[str, tuple[str, str]] = {}` — группы, у которых вместо слитого названия + «Дебет/Кредит» две
ячейки в строке групп со своими текстами (сторона в ячейке группы); если разделены все три группы, строки
подзаголовков нет;
- `total_text: str = TOTAL_TEXT`.
- [ ] **Step 1: падающие тесты синтетики.** Тексты брать из `headers.md`; ниже — ориентир:
- `s48_other_system_titles.xlsx` — пять листов `build_osv` с `title`: «Оборотная ведомость по счету 60 за 1 квартал
2025 г.», «ОСВ по сч. 60 за 1 квартал 2025 г.», «Оборотный баланс …», «Фин. Обороты Листок», «Отчет по Главной
книге …»; у последнего листа `total_text="ВСЕГО"`. Тест `test_s48_other_system_titles_are_osv`: 5 документов
`osvDocument` с локаторами листов, `SHEET_OTHER` нет; у листа «ВСЕГО» строка итога `TOTAL`, предупреждения
«строка «Итого» не найдена» нет.
- `s49_balance_and_turnover_by_side.xlsx` — `group_titles` «Остаток на начало периода» / «Остаток на конец периода»,
`split_groups={"turnover": ("Оборот по дебету", "Оборот по кредиту")}`; над таблицей преамбула «Остаток на
начало: 0 | Остаток на конец: 0» (Review Focus 2). Тест: шесть колонок сумм, суммы `base_tree()` верны, статус
`done`.
- `s50_sap_in_out_glued_side.xlsx` — два листа: SAP «Сальдо вх Дт | Сальдо вх Кт | Оборот Дт | Оборот Кт | Сальдо
исх Дт | Сальдо исх Кт» (все три `split_groups`), SAP НМЖК «Сальдо НачальноеДТВВ | Сальдо НачальноеКТВВ | … |
Сальдо КонечноеКТВВ»; у контрагента имя «ООО Дтвв-Сервис» (Review Focus 3). Тест: оба листа — шесть колонок с
верными сторонами, суммы как у дерева, «ООО Дтвв-Сервис» — строка контрагента.
- `test_detect.py::test_side_without_word_boundary_only_in_group_cell` — сетка `sheet_from_rows`: заголовок «Сальдо
НачальноеДТВВ | …», ниже строка с текстом «Дтвв» в колонке сумм → `find_table_header` берёт колонки только из
строки групп.
Добавить имена в `EXPECTED` (s48 — `documents=5`), в список файлов и `FILE_NAMES`.
- [ ] **Step 2: генерация и прогон — тесты падают.** `cd api && .venv/bin/python -m tests.synthetic.generate
W/fixtures/osv/synthetic` → `.venv/bin/pytest tests/synthetic tests/unit/parser/test_detect.py -k "s48 or s49 or s50
or word_boundary"` — FAIL (`INVALID_DOCUMENT`/`INVALID_FORMAT`).
- [ ] **Step 3: записи словаря.** Ориентир (уточнить по `headers.md`; ловушки: `\w*` жадно съест приклеенную сторону —
«сальдо\s+начальн\w*» оставит пустой остаток вместо «дтвв»; `\bоборот(ы)?\b` не матчит «оборотдт»):
- `sheet_markers`: `^оборотн\w*\s+ведомост`, `^осв\b`, `^оборотн\w*\s+баланс`, `^фин\.?\s*обороты`,
`^отчет\s+по\s+главной\s+книге`;
- `groups.opening`: `остат\w*\s+на\s+начало`, `сальдо\s+начальное`, `сальдо\s+вх\b`, `^начало(?=\s*(дт|кт))`;
- `groups.closing`: `остат\w*\s+на\s+конец`, `сальдо\s+конечное`, `сальдо\s+исх\b`, `^конец(?=\s*(дт|кт))`;
- `groups.turnover` (если `headers.md` подтверждает «ОборотДт» без пробела): `^оборот(?=\s*(дт|кт))`;
- `debit`: `^по\s+дебету\b`, `^дт\s*вв\b`; `credit`: `^по\s+кредиту\b`, `^кт\s*вв\b`;
- `total_row_exact`: `^всего$` (комментарий: ГК КТЗ, сч. 009, AURORA-1732).
Каждой записи — комментарий с источником (AURORA-…, система). Если сторону без границы слова нельзя выразить
записью (остаток ячейки не начинается со стороны), правка `_side`/`_group_cells` общая, без упоминания системы
(design D2), и тест на неё.
- [ ] **Step 4: тесты зелёные.** Тот же `pytest` → PASS. `tests/fixtures/test_golden.py` требует снимок на каждый
синтетический файл: `make golden-update`, в diff — только новые снимки s48–s50 (изменился старый — стоп, разобрать).
Затем `make check` → PASS (покрытие словарей видит новые записи на s48–s50).
- [ ] **Step 5: коммиты.** tabular-parser: `rules/osv_headers.yaml`, `tests/synthetic/*`, `tests/unit/parser/test_detect.py`
— «ОСВ: названия ведомостей, группы и стороны других систем, «ВСЕГО» (#81)». fixtures (`git -C W/fixtures`):
`osv/synthetic/s48…s50`, README — «Синтетика s48–s50 под tp#81»; снимки `golden/expected_osv_parse/s48…s50` —
отдельным коммитом «Golden синтетики s48–s50 под tp#81».
---
### Task 3: Запасной признак листа ОСВ по заголовку таблицы
**Files:**
- Modify: `api/src/parser/detect.py` (новая `has_osv_table`, `classify_sheet`, `__all__`)
- Modify: `api/src/parser/statements/adapter.py:56-83` (`plan_sheets`)
- Modify: `api/src/parser/book.py:146`
- Modify: `api/tests/synthetic/generate.py` (`_s51`), `api/tests/synthetic/test_synthetic_files.py`
- Test: `api/tests/unit/parser/test_detect.py`, `api/tests/unit/parser/statements/test_adapter.py` (тесты `plan_sheets`), `api/tests/unit/parser/test_book_parser.py`
**Interfaces:**
- Consumes: поля `OsvSpec` из Task 2 (`untitled`, `split_groups`).
- Produces:
- `detect.has_osv_table(sheet: Sheet, rules: Rules) -> bool` — в первых `SHEET_MARKER_ROWS` строках есть строка, где
`_group_cells` даёт все три группы, и `_columns_of_group` для каждой возвращает `debit` и `credit` (не `collapsed`);
`InvalidFormatError` из `_columns_of_group` → `False`. Без дедлайна (≤ 30 строк).
- `plan_sheets(sheets, is_osv, rules, deadline, osv_by_table: Callable[[Sheet], bool] | None = None) -> SheetPlan` —
в ветке `else` (лист пошёл бы в `other`): если `osv_by_table` задан и истинен — лист в `osv`, `last_type = None`;
иначе `other`, как сейчас.
- `classify_sheet`: если пакет дал «прочее» и `has_osv_table` — `SheetKind.OSV` (согласованность с `plan_sheets`).
- `book.py`: `plan_sheets(..., osv_by_table=lambda sheet: has_osv_table(sheet, self._rules))`.
- [ ] **Step 1: падающие тесты.**
- `test_detect.py`:
- `test_has_osv_table_navision_split_sides` — `sheet_from_rows`: первая строка «ФинСчет | НачалоДт | НачалоКт |
ОборотДт | ОборотКт | КонецДт | КонецКт» (тексты — из `headers.md`), строки данных → `True`;
- `test_has_osv_table_requires_opening` — то же без «НачалоДт/Кт» → `False`;
- `test_has_osv_table_rejects_collapsed` — «Сальдо на начало | Обороты (Дебет/Кредит) | Сальдо на конец» с одной
колонкой у начала → `False`;
- `test_has_osv_table_rejects_disclosure` — «Наименование | Остаток на начало | Поступило | Выбыло | Остаток на
конец» → `False` (Review Focus 1);
- `test_has_osv_table_only_first_rows` — полный заголовок в строке 31 → `False`;
- `test_classify_sheet_falls_back_to_table` — Navision-сетка → `SheetKind.OSV`.
- тест `plan_sheets`:
- `test_plan_sheets_table_fallback_only_for_other` — лист формы, затем Navision-лист, затем «Справка» → `osv`
содержит только Navision-лист, «Справка» в `other`; `osv_by_table` вызван только для двух последних листов
(счётчик вызовов, Review Focus 5);
- `test_plan_sheets_weak_continuation_not_taken` — лист формы, затем лист из 1–2 кодов (слабое продолжение,
образец — существующие тесты `is_weak_continuation`) → остаётся `STATEMENT_CONTINUATION`, `osv_by_table` не
вызван (Review Focus 4).
- синтетика `s51_untitled_navision.xlsx` (`build_osv` с `untitled=True`, `split_groups` для трёх групп по
`headers.md`) и `test_s51_untitled_sheet_is_osv_by_table`: один `osvDocument`, `SHEET_OTHER` нет, суммы дерева
верны; `EXPECTED` — статус по факту разбора шапки без периода (ожидаемо `needs_check` из-за ненайденного
периода/организации; если иначе — зафиксировать фактический и объяснить в комментарии).
- `test_book_parser.py::test_statement_book_sheets_unchanged_by_fallback` — книга s14 (формы + ОСВ): виды листов те же,
что без запасного признака.
- [ ] **Step 2: тесты падают.** `make venv` уже есть; `cd api && .venv/bin/python -m tests.synthetic.generate
W/fixtures/osv/synthetic && .venv/bin/pytest tests/unit/parser tests/synthetic -k "osv_table or plan_sheets or
s51 or fallback"` → FAIL (`ImportError: has_osv_table`, `INVALID_DOCUMENT`).
- [ ] **Step 3: реализация** по Interfaces. `has_osv_table` переиспользует `_group_cells` и `_columns_of_group` (R-6),
отдельного разбора сторон нет.
- [ ] **Step 4: тесты зелёные.** Тот же `pytest` → PASS; `make golden-update` — в diff только новый снимок s51;
`make check` → PASS.
- [ ] **Step 5: коммиты.** tabular-parser: «ОСВ: запасной признак листа по заголовку таблицы (#81, AURORA-1717)».
fixtures: `osv/synthetic/s51…`, README — «Синтетика s51 под tp#81»; снимок s51 — отдельным коммитом.
---
### Task 4: Golden, регрессия банка, check-full
**Files:**
- Modify (генерация): `W/fixtures/golden/expected_osv_parse/*.json`
- Create (вывод): `/Users/andrey.epifancev/projects/awp-ca/admin/osv-other-systems-regress/new.csv`, `report.md`
- [ ] **Step 1: golden.** `make golden-check` → код 0; `git -C W/fixtures diff --stat f1f7f83 -- golden/` — только
добавленные снимки s48–s51, ни одного изменённого (изменённый — стоп, разобрать с объяснением в design.md).
- [ ] **Step 2: прогон ветки.** `regress.py run new.csv`; `regress.py diff base.csv new.csv > report.md`.
- [ ] **Step 3: разбор.** Каждое ухудшение и каждый переход в `osv` из формы/«прочее» — открыть файл, решить: ведомость
(ожидаемо: «Оборотный баланс» 1718 и файлы списков 1717/1719) или ложное срабатывание. Ложное — правка записи словаря
или якоря, повтор Task 2 Step 4 и этого шага. Не устраняется — стоп, вопрос владельцу с файлом и тем, что видно.
В `report.md` — таблица переходов, список ручных проверок с вердиктом, файлы списков 1717/1719, оставшиеся с
ошибкой, с причиной (1720–1723, «Обороты счета», безымянный лист, ушедший в форму).
- [ ] **Step 4: `make check-full`** (поднимает compose для интеграционных тестов) → PASS. Также
`.venv/bin/pytest tests/fixtures/test_osv_dkb.py` → PASS.
---
### Task 5: Правило 05-domain-rules, документация сервиса
**Files:**
- Modify: `W/docs/context/05-domain-rules.md` §1.1 (поиск листа), §1.2 (написания групп и сторон); строка «Итого» —
где перечислены итоговые подписи (§1.3 или §1.4)
- Modify: `W/tabular-parser/README.md` (раздел «Парсер ОСВ»: признаки листа, запасной признак), `W/tabular-parser/CLAUDE.md`
(перечень синтетики s48–s51 в абзаце golden), docstring `generate.py` (s48–s51)
- Modify: `W/tabular-parser/openspec/changes/osv-other-systems/design.md` — решения, принятые по ходу (R-9…), и
сводка регрессии из `report.md`
- [ ] **Step 1: §1.1** по design D4: признак — название ведомости по `sheet_markers` (перечислить написания) или,
если лист не опознан как форма или продолжение, полный заголовок таблицы ОСВ в первых 30 строках: три группы, у каждой
Дебет и Кредит, без свёрнутого сальдо. §1.2 — написания групп и сторон из Task 2 (сторона в ячейке группы, «по
дебету», приклеенная «ДТВВ»). «ВСЕГО» — рядом с «ИТОГ».
- [ ] **Step 2: проверки.** `openspec validate osv-other-systems --strict` → valid; `make check` → PASS (docstring и
README не ломают линтеры).
- [ ] **Step 3: коммиты.** docs (`git -C W/docs`): «05 §1.1–§1.2: ОСВ других учётных систем (tp#81)»; tabular-parser:
«Документация: признаки листа ОСВ, синтетика s48–s51 (#81)».