arm-tabular-parser/docs/superpowers/plans/2026-10-28-osv-other-systems.md
Andrey Epifancev 29071bdc7b plan: снимки контрольного набора по шагам (#81)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:14:59 +04:00

31 KiB
Raw Blame History

ОСВ других учётных систем — план реализации

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, control-base.md
  • Modify: api/tests/synthetic/generate.py (OsvSpec, _header_v1, _header_rows, build_osv, table_sheet, _s48…_s57, FILE_NAMES, docstring модуля), api/tests/synthetic/test_synthetic_files.py
  • Create (генерация): W/fixtures/osv/synthetic/s48…s57; Modify: W/fixtures/osv/synthetic/README.md

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 НМЖК — «Оборот» со стороной.

  • Step 6: генератор. В api/tests/synthetic/generate.py поля OsvSpec (см. Interfaces ниже) и их поддержка в _header_v1/_header_rows/build_osv; существующие s01–s47 побайтно не меняются (test_generate.py и повторная генерация в W/fixtures/osv/synthetic — git -C W/fixtures status чистый по старым файлам).

  • Step 7: контрольный набор синтетики — файлы и тесты по таблице ниже, тексты подписей — из headers.md, где есть реальный образец. Набор пишется до правок словаря и кода и должен уметь опровергнуть успех: позитивы проверяют не только «лист — ОСВ», но и точные суммы по сторонам (перепутанные Дебет/Кредит ломают тождество строки и суммы); негативы проверяют, что вид листа и результат не изменились.

    Позитивы (на базе падают, после Task 2–3 проходят):

    Файл Что в нём Тест и проверки
    s48_other_system_titles.xlsx листы build_osv с title: «Оборотная ведомость по счету 60 за 1 квартал 2025 г.», «ОСВ по сч. 60 …», «Оборотный баланс …», «Фин. Обороты Листок» (+ строка «Период: …»), «Отчет по Главной книге …»; ещё два листа — название во второй колонке и «ОБОРОТНАЯ\nВЕДОМОСТЬ по счету 60» (верхний регистр, перенос); у одного листа total_text="ВСЕГО", у другого "Всего " test_s48_other_system_titles_are_osv: 7 osvDocument по листам, SHEET_OTHER нет; у листов «ВСЕГО» итог — TOTAL, предупреждения «строка «Итого» не найдена» нет; суммы base_tree()
    s49_balance_and_turnover_by_side.xlsx «Остаток на начало периода» / «Остаток на конец периода» с «Дебет/Кредит» под ними, split_groups={"turnover": ("Оборот по дебету", "Оборот по кредиту")}; над таблицей преамбула «Остаток на начало: 0 | Остаток на конец: 0» (Review Focus 2) шесть колонок, суммы дерева, статус done
    s50_sap_in_out_glued_side.xlsx лист SAP «Сальдо вх Дт | Сальдо вх Кт | Оборот Дт | Оборот Кт | Сальдо исх Дт | Сальдо исх Кт»; лист НМЖК «Сальдо НачальноеДТВВ | … | Сальдо КонечноеКТВВ»; контрагент «ООО Дтвв-Сервис» (Review Focus 3) оба листа — шесть колонок с верными сторонами, суммы дерева, «ООО Дтвв-Сервис» — строка контрагента, done
    s51_untitled_navision.xlsx untitled=True, три split_groups «НачалоДт/НачалоКт», «Оборот…», «КонецДт/КонецКт» (по headers.md), первая колонка «ФинСчет» один osvDocument, SHEET_OTHER нет, суммы дерева; статус — фактический (без шапки ожидаемо needs_check по периоду/организации), зафиксировать в EXPECTED с комментарием
    s52_mixed_book.xlsx statement_sheet() (форма ББ) → лист как в s51 → reference_sheet() документ формы тот же, что у книги без Navision-листа; Navision — osvDocument; «Справка» — SHEET_OTHER
    s53_balance_by_side.xls, s54_untitled_navision.xlsb s49 через to_xls, s51 через to_xlsb те же проверки, что у s49 / s51 (читатели xls и xlsb)

    Негативы (проходят и на базе, и после — иначе успех опровергнут):

    Файл Что в нём Тест и проверки
    s55_not_osv_titles.xlsx обычная ОСВ 1С (чтобы книга не была ошибкой) + листы с таблицей без групп ОСВ: «Справка: оборотная ведомость прилагается», «Оборотный капитал и его структура», отчёт 1С «Обороты счета 60» с колонками корр. счетов внутри «Оборот Дт/Кт» (R-1) 1 osvDocument; три листа — SHEET_OTHER
    s56_almost_osv_tables.xlsx обычная ОСВ 1С + безымянные листы: (а) «Наименование | Остаток на начало | Поступило | Выбыло | Остаток на конец»; (б) «Оборот Дт | Оборот Кт | Сальдо на конец Дт | Кт» без начала; (в) свёрнутое начало «Сальдо на начало» одной колонкой + обороты и конец с Дт/Кт; (г) полный заголовок Navision с 31-й строки 1 osvDocument; (а)–(г) — SHEET_OTHER (D1, R-2, R-3, Review Focus 1)
    s57_form_mentions_osv.xlsx форма ББ (statement_sheet()) с ячейкой примечания «см. оборотную ведомость по сч. 60» и строкой «Остаток на начало периода» в тексте пояснения; следом лист слабого продолжения формы (1–2 кода, как в существующих тестах is_weak_continuation) документы формы те же, что на базе (вид, locator, число строк); osvDocument нет; продолжение присоединено (Review Focus 4)

    EXPECTED, список файлов, FILE_NAMES — для всех. Негативам — отдельный тест-маркер в имени (test_neg_…), чтобы их прогон читался отдельно.

  • Step 8: прогон набора на базе. Сгенерировать в W/fixtures/osv/synthetic, make golden-update (снимки всех s48–s57; у негативов это регрессия базового поведения, снимки позитивов фиксируют ошибку базы и обновятся в Task 2–3), затем .venv/bin/pytest tests/synthetic -k "s48 or s49 or s50 or s51 or s52 or s53 or s54 or s55 or s56 or s57" -rA. Ожидаемо: все test_neg_… — PASS, все позитивы — FAIL. Иначе стоп: позитив, проходящий на базе, ничего не доказывает (переписать), негатив, падающий на базе, неверно описывает текущее поведение (разобрать). Итог — таблица «тест → результат на базе» в admin/osv-other-systems-regress/control-base.md.

  • Step 9: коммиты. tabular-parser: генератор и тесты контрольного набора — «Контрольный набор синтетики ОСВ других систем (#81)» (позитивы падают — допустимо в ветке до Task 3: пометить их pytest.mark.xfail(strict=True, reason="tp#81: до Task 2–3"), снимать xfail по мере реализации). fixtures: файлы s48–s57 и README — «Синтетика s48–s57 под tp#81»; снимки негативов — отдельным коммитом.

Interfaces (генератор, Step 6):

  • Поля OsvSpec:
    • title: str | None = None — строка названия вместо «Оборотно-сальдовая ведомость по счету {account} за {period}» (None — как сейчас);
    • title_col: int = 1 — колонка строки названия;
    • untitled: bool = False — без строк организации, названия, параметров: заголовок таблицы с первой строки;
    • top_padding: int = 0 — пустых строк над заголовком таблицы при untitled (для s56 (г): 30);
    • split_groups: dict[str, tuple[str, str]] = {} — группы, у которых вместо слитого названия + «Дебет/Кредит» две ячейки в строке групп со своими текстами (сторона в ячейке группы); если разделены все три группы, строки подзаголовков нет;
    • total_text: str = TOTAL_TEXT.
  • Листы-негативы без дерева ОСВ — функцией по образцу reference_sheet/_flat_sheet: table_sheet(name: str, rows: Sequence[Sequence[str | D | None]], *, start_row: int = 1) -> SheetSpec.

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/test_synthetic_files.py (снять xfail у s48–s50, s53)
  • Modify: api/tests/unit/parser/test_detect.py

Interfaces:

  • Consumes: контрольный набор и headers.md из Task 1.

  • Step 1: падающий unit-тест. test_detect.py::test_side_without_word_boundary_only_in_group_cell — сетка sheet_from_rows: заголовок «Сальдо НачальноеДТВВ | …», ниже строка с текстом «Дтвв» в колонке сумм → find_table_header берёт колонки только из строки групп. Снять xfail у тестов s48, s49, s50, s53.

  • Step 2: тесты падают. .venv/bin/pytest tests/synthetic tests/unit/parser/test_detect.py -k "s48 or s49 or s50 or s53 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; все test_neg_… контрольного набора → PASS (негатив упал — запись словаря слишком широкая: сузить, не ослаблять тест). make golden-update: меняются снимки s48–s50, s53; снимки негативов s55–s57 и всех старых файлов не изменились (изменились — стоп, разобрать). make check → PASS (покрытие словарей видит новые записи).

  • Step 5: коммиты. tabular-parser: rules/osv_headers.yaml, tests/synthetic/test_synthetic_files.py, tests/unit/parser/test_detect.py — «ОСВ: названия ведомостей, группы и стороны других систем, «ВСЕГО» (#81)». fixtures (git -C W/fixtures): снимки s48–s50, s53 — «Golden синтетики s48–s50, s53 под 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/test_synthetic_files.py (снять xfail у s51, s52, s54)
  • 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: контрольный набор из Task 1 (s51, s52, s54 — позитивы; s55–s57 — негативы).

  • 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).
    • снять xfail у тестов s51, s52, s54 контрольного набора.
    • test_book_parser.py::test_statement_book_sheets_unchanged_by_fallback — книга s14 (формы + ОСВ): виды листов те же, что без запасного признака.
  • Step 2: тесты падают. cd api && .venv/bin/pytest tests/unit/parser tests/synthetic -k "osv_table or plan_sheets or s51 or s52 or s54 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; все test_neg_… → PASS; make golden-update — в diff меняются только снимки s51, s52, s54, снимки негативов s55–s57 и старых файлов не изменились; make check → PASS.

  • Step 5: коммиты. tabular-parser: «ОСВ: запасной признак листа по заголовку таблицы (#81, AURORA-1717)». fixtures: снимки s51, s52, s54 — отдельным коммитом.


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–s57, ни одного изменённого (изменённый — стоп, разобрать с объяснением в design.md). Контрольный набор целиком: .venv/bin/pytest tests/synthetic -k "s48 or s49 or s50 or s51 or s52 or s53 or s54 or s55 or s56 or s57" -rA → все PASS, xfail не осталось; таблица «тест → база → ветка» дописывается в control-base.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–s57 в абзаце golden), docstring generate.py (s48–s57)

  • 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–s57 (#81)».