diff --git a/docs/superpowers/plans/2026-10-28-osv-other-systems.md b/docs/superpowers/plans/2026-10-28-osv-other-systems.md new file mode 100644 index 0000000..64048db --- /dev/null +++ b/docs/superpowers/plans/2026-10-28-osv-other-systems.md @@ -0,0 +1,262 @@ +# ОСВ других учётных систем — план реализации + +> **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 `. + +## 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 ` и `regress.py diff `; `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)».