arm-tabular-parser/docs/superpowers/plans/2026-10-19-osv-flat-form.md
Andrey Epifancev 7cd71677da docs(osv-flat-form): рулинги R-17…R-19, актуализация design, README, спеки (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 13:20:00 +04:00

35 KiB
Raw Permalink Blame History

ОСВ 1С в плоской форме (AURORA-1661, структура) Implementation Plan

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: ОСВ 1С по одному счёту в плоской форме («по счету: X», колонка «Субконто», без строки счёта, «Итого развернутое» перед «Итого») разбирается без INVALID_FORMAT, со счётом и организацией из шапки, а контрагенты расчётных счетов без названия вида получают COUNTERPARTY.

Architecture: Изменения в конвейере листа ОСВ tabular-parser: шапка (header.py) — код счёта с двоеточием, строка вида субконто, «План счетов»; итоговый блок (classify.collect_body_rows, checks._total); новая стратегия каскада header_account (hierarchy.py) и счёт шапки у корня ветки (classify_rows); правило расчётных счетов — данные osv_groups.yaml. Контракт результата не меняется.

Tech Stack: Python 3.11, pydantic v2, openpyxl, pytest; OpenSpec 1.13.

Spec: openspec/changes/osv-flat-form/ (proposal.md, design.md, specs/osv-parsing/spec.md)

Ветка: feat/59-osv-flat-form во всех трёх worktree: /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/{tabular-parser,fixtures,docs}. Issue — RSHB/arm-tabular-parser#59. Jira — AURORA-1661 (структурная часть).

Global Constraints

  • Каждая shell-команда начинается с cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/<repo> &&; git — только git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/<repo>; в основные checkout'ы (/Users/andrey.epifancev/projects/awp-ca/tabular-parser и т. п., ветка main) не заходить и не писать. Хук отказал — стоп, вернуться к контроллеру с текстом отказа.
  • Код Python правится только Edit/Write, не sed -i/heredoc через Bash.
  • Нет координат в src/parser/** (AST-линтер tests/unit/parser/test_no_coordinates.py): ни букв колонок, ни номеров строк, ни имён листов. Новые синонимы и списки — данные api/rules/*.yaml, не if в коде.
  • Деньги — Decimal, float нет. needs_check — только по контрольным суммам; структуру под сходимость не подгонять.
  • Контракт (api/contracts/, src/contract/) не меняется; CONTRACT_VERSION прежний.
  • Синтетика этого плана — s40–s44 (номера s33–s39 — у параллельного трека A, не занимать).
  • Golden (../fixtures/golden/expected_osv_parse/) правится только make golden-update с просмотром diff, коммит — в репозиторий fixtures отдельно от кода, сообщение с (tabular-parser#59). Изменение снимка реального файла, не объяснённое задачей, — стоп и вопрос контроллеру.
  • Реальные файлы контура (/Users/andrey.epifancev/projects/awp-ca/result_dir.zip, /Users/andrey.epifancev/projects/awp-ca/ОСВ сч. 60 за 2 кв. 2026.xlsx) в git не кладутся: только распаковка во временный каталог вне репозиториев для сверки.
  • Трек A (.wt/osv-indicators/, AURORA-1613 + валюта 1661) владеет путём data_type (rules/data_types.yaml, rules.RowDataType, classify.row_data_type, checks._INDICATOR_DATA_TYPES, колонка «Показатели», умолчание BU) — эти места не трогать.
  • Коммиты — по-русски, с (#59) в теме и строкой Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
  • Проверка задачи: cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser && make check зелёный (≈95 с; включает fixtures-тесты).

Review Focus

  1. «по счету:» без кода после двоеточия (код в соседней ячейке или нет вовсе) — account_code_raw пуст, работает фолбэк OQ-134 из строки таблицы с прежним предупреждением; тест в задаче 2.
  2. Название и период в одной строке («…по счету 60 за 1 квартал 2025 г.») — строки вида субконто нет, организация по прежнему правилу (первая не-метка); тест в задаче 2.
  3. На листе только «Итого развернутое», без обычной «Итого» — она единственная итоговая и сверяется как «Итого»; тест в задаче 3.
  4. Счёт в шапке, строк-счетов нет, но есть outline (контрагент → договоры) — побеждает outline, header_account не используется, строки всё равно наследуют счёт шапки; тест в задаче 4.
  5. Счёт ветки «760» или «600» при записи 76/60 в counterparty_accounts — не расчётный, строки OTHER; тест в задаче 5.

Task 1: Синтетика s40–s44 и снимки текущего поведения

Files:

  • Modify: api/tests/synthetic/generate.py (функции _s40…_s44, записи реестра FILES, docstring main — «s01…s44»)
  • Create: ../fixtures/osv/synthetic/s40_flat_header_account.xlsx, s41_flat_chart_of_accounts.xlsx, s42_subaccounts_totals_reversed.xlsx, s43_subkonto_unnamed_60.xlsx, s44_subkonto_unnamed_10.xlsx
  • Create: снимки ../fixtures/golden/expected_osv_parse/synthetic/s40…s44.json (make golden-update)

Interfaces:

  • Produces: пять файлов синтетики; ожидания по ним задачи 2–5 проверяют разбором через OsvParser/get_parser().

Содержимое листов (все — один лист, колонка A — текст, B–G — суммы «Сальдо на начало / Обороты за период / Сальдо на конец периода» × «Дебет | Кредит»; заголовок групп — объединения B:C, D:E, F:G; числа — Decimal; пусто — нет ячейки):

  • s40: r2 «Оборотно-сальдовая ведомость по счету: 76.Н.1», r3 «Контрагенты», r4 «за 2025 г.», r5 «ООО "Товарное хозяйство"», r7 «Единица измерения: рубль (код по ОКЕИ 383)», r9 «Субконто» + группы, r10 подзаголовки; данные без outline и отступов: «Август - Агро ООО» 1000.00 | — | 500.00 | 300.00 | 1200.00 | —; «АВИС ООО» — | 200.00 | 100.00 | 400.00 | — | 500.00; «Агроаспект ООО» — | — | 50.00 | 50.00 | — | —; «Итого развернутое» 1000.00 | 200.00 | — | — | 1200.00 | 500.00; «Итого» 800.00 | — | 650.00 | 750.00 | 700.00 | —.

  • s41: r2 «Оборотно-сальдовая ведомость по счету: 60», r3 «Организации», r4 «за 2 Квартал 2026 г. План счетов ООО Хреновская Нива», r5 «Субконто» + группы, r6 подзаголовки; «Автодор-Платные Дороги ООО» 820.21 | — | 5000.00 | 4991.00 | 829.21 | —; «АГРОПРОФИ ООО» — | 2763.79 | — | — | — | 2763.79; «Итого развернутое» 820.21 | 2763.79 | — | — | 829.21 | 2763.79; «Итого» — | 1943.58 | 5000.00 | 4991.00 | — | 1934.58.

  • s42: r2 «Оборотно-сальдовая ведомость по счету: 76», r3 «за 2025 г.», r4 «ООО "Товарное хозяйство"», r6 «Единица измерения: рубль (код по ОКЕИ 383)», r8 «Субконто» + группы, r9 подзаголовки; «76.2» 1000.00 | — | — | 1000.00 | — | —; «76.5» — | 300.00 | 700.00 | 600.00 | — | 200.00; «76.Н» — | — | 5000.00 | 5000.00 | — | —; «76.Н.1» — | — | 5000.00 | 5000.00 | — | —; «Итого развернутое» 1000.00 | 300.00 | — | — | — | 200.00; «Итого» 700.00 | — | 5700.00 | 6600.00 | — | 200.00.

  • s43: r1 «ООО "Октябрьское"», r2 «Оборотно-сальдовая ведомость по счету 60», r3 «Период: 9 месяцев 2024 г.», r5 «Субконто» + группы, r6 подзаголовки; «60.01» 34618000.00 | — | 4236500.00 | 38854500.00 | — | —; отступ 1: «ИП Иванов А.А.» 34618000.00 | — | — | 34618000.00 | — | —; отступ 1: «ООО "ЭСТЕТ ОМСК"» — | — | 4236500.00 | 4236500.00 | — | —; «Итого» и затем «Итого развернутое» — обе 34618000.00 | — | 4236500.00 | 38854500.00 | — | —.

  • s44: как s43, но счёт 10: r2 «…по счету 10», «10.01» и строки «Мука пшеничная», «Сахар-песок» с теми же суммами.

  • Step 1: Добавить _s40…_s44 и записи реестра, по образцу _s31/_s32 (листы через SheetSpec/put, отступ — CellSpec(..., indent=1)).

  • Step 2: Сгенерировать файлы

Run: cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser/api && .venv/bin/python -m tests.synthetic.generate ../../fixtures/osv/synthetic (если .venv нет — cd .. && make venv). Expected: в выводе пути s40…s44; git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures status — пять новых файлов, прежние файлы синтетики без изменений (генератор детерминирован).

  • Step 3: Снимки текущего поведения

Run: cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser && make golden-update Expected: новые снимки только synthetic/s40…s44.json; у s40, s41 — status: "error", INVALID_FORMAT «иерархия строк не определена»; s42 — needs_check, «Итого развернутое» — OTHER; s43, s44 — контрагенты/материалы OTHER. Других изменений снимков нет.

  • Step 4: make check зелёный

  • Step 5: Commit (два репозитория)

git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures add osv/synthetic golden/expected_osv_parse/synthetic
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures commit -m "Синтетика s40–s44: плоская форма ОСВ 1С, снимки текущего поведения (tabular-parser#59)"
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser add api/tests/synthetic/generate.py
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser commit -m "test: синтетика s40–s44 — плоская форма ОСВ 1С (#59)"

Task 2: Шапка — код счёта с двоеточием, строка вида субконто, «План счетов»

Files:

  • Modify: api/rules/osv_headers.yaml (header_markers.chart_of_accounts: ['\bплан\s+счетов\b'])
  • Modify: api/src/parser/rules.py (HeaderMarker + "chart_of_accounts"; OsvGroupRules.known)
  • Modify: api/src/parser/header.py (_account_code, _organization, новый _kind_row_cells, _variant1_groups/build_group_levels)
  • Test: api/tests/unit/parser/test_header.py, api/tests/unit/parser/test_rules.py

Interfaces:

  • Produces: OsvGroupRules.known(text: str) -> GroupType | None — тип по явной записи mapping, None для default; header._kind_row_cells(cells: list[_HeadCell], rules: Rules) -> list[_HeadCell] — ячейки строго между строкой ячейки с меткой account и строкой периода (_find_period), если первая выше; иначе [].

  • Step 1: Тесты (падают) — в test_header.py через sheet_from_rows:

    • test_account_code_after_colon: «Оборотно-сальдовая ведомость по счету: 76.Н.1» → account_code_raw == "76.Н.1", ACCOUNT_NOT_FOUND_WARNING нет в warnings.
    • test_colon_without_code_falls_back_to_table_row (Review Focus 1): «…по счету:» без кода → account_code_raw == "" и ACCOUNT_NOT_FOUND_WARNING в warnings (фолбэк делает osv._with_table_account).
    • test_kind_row_between_title_and_period_is_not_organization: строки «…по счету: 76», «Контрагенты», «за 2025 г.», «ООО "Товарное хозяйство"» → organization_name == 'ООО "Товарное хозяйство"'.
    • test_organization_from_chart_of_accounts_tail: «…по счету: 60», «Организации», «за 2 Квартал 2026 г. План счетов ООО Хреновская Нива» → organization_name == "ООО Хреновская Нива", period_quarter == "Q2", period_year == 2026.
    • test_title_and_period_in_one_row_keep_old_organization_rule (Review Focus 2): существующая раскладка _variant1 (org выше названия с периодом) → organization_name == 'АО "Ромашка"'.
    • test_known_kind_row_extends_single_account_schema: шапка как у s40, заголовок «Субконто» → build_group_schema(...)[0] == [ACCOUNT("Субконто"), COUNTERPARTY("Контрагенты")].
    • test_unknown_kind_row_is_not_added_to_schema: строка «Материалы» → схема [ACCOUNT("Субконто")].
    • test_organizations_kind_row_is_counterparty (R-10): строка «Организации» → схема [ACCOUNT("Субконто"), COUNTERPARTY("Организации")]; в osv_groups.yaml добавить ['^организаци', COUNTERPARTY] после ^контрагент.
    • в test_rules.py: test_known_group_is_none_for_default — rules.groups.known("Контрагенты") == "COUNTERPARTY", known("Материалы") is None, known("Организации") == "COUNTERPARTY", known("Номенклатура") == "OTHER".
  • Step 2: Запуск — cd …/tabular-parser/api && .venv/bin/pytest tests/unit/parser/test_header.py tests/unit/parser/test_rules.py -q → новые тесты FAIL.

  • Step 3: Реализация.

    • _account_code: хвост _tail(...) очистить от ведущих : и пробелов перед _ACCOUNT_CODE.match.
    • _organization: сначала хвост метки chart_of_accounts в любой ячейке шапки (текст после совпадения, strip(" ,;:"), непустой); затем первая не-метка, исключая ячейки _kind_row_cells; затем _title_tail_organization. Ячейка периода, содержащая «План счетов», остаётся периодом (_cut_period режет на «г.»).
    • _is_structural_marker: метка chart_of_accounts — структурная.
    • Схема: если _variant1_groups дал ровно одну ACCOUNT, добавить по элементу текста _kind_row_cells (через запятую, strip), для которого rules.groups.known(item) не None: _group(known, item); вариант 2 не меняется.
  • Step 4: Тесты PASS, затем make golden-update: ожидаемо меняются только synthetic/s40 и s41 (если они ещё error — без изменений) и s42 (account_code_raw «76» вместо «76.2», без предупреждения «взят из первой строки-счёта»). Изменение реального снимка — стоп (Global Constraints).

  • Step 5: make check зелёный; commit — tabular-parser: feat(osv): код счёта «по счету: X», строка вида субконто, «План счетов» в шапке (#59); fixtures (если снимки изменились): Снимки s40–s42: шапка плоской формы (tabular-parser#59).


Task 3: «Итого развернутое» перед «Итого»

Files:

  • Modify: api/src/parser/classify.py (collect_body_rows, новый _leading_totals)
  • Modify: api/src/parser/checks.py (_total, total_matches_root, run_checks)
  • Modify: api/src/parser/osv.py (передать rules.headers.total_row_secondary в run_checks)
  • Test: api/tests/unit/parser/test_classify.py, api/tests/unit/parser/test_checks.py

Interfaces:

  • Produces: collect_body_rows(...) — тот же кортеж (body, total_row, warnings), но total_row — первая строка итогового блока (вторые итоговые строки непосредственно перед основной «Итого» входят в блок); run_checks(rows, exempt=None, *, secondary_totals: Sequence[Pattern] = ()) -> tuple[OsvChecks, list[OsvNote]]; total_matches_root(rows, secondary_totals: Sequence[Pattern] = ()) -> bool.

  • Step 1: Тесты (падают)

    • test_secondary_total_before_main_total_is_in_total_block (test_classify.py): лист как s42 → total_row = номер строки «Итого развернутое»; после classify_rows обе строки — TOTAL, level == 0, parent is None.
    • test_only_secondary_total_is_the_total (Review Focus 3): на листе только «Итого развернутое» → она TOTAL, total_row — её номер.
    • test_total_check_uses_main_total_when_secondary_comes_first (test_checks.py): строки TOTAL в порядке «Итого развернутое», «Итого»; суммы «Итого» = корень, «Итого развернутое» — нет → total_matches_root(rows, secondary) is True; без secondary (прежний вызов) — False.
    • Разбор s42 через get_parser(): status == "done", checks.parent_sum_violations == [], checks.balance_identity_violations == [].
  • Step 2: Запуск — FAIL.

  • Step 3: Реализация. _leading_totals(sheet, header, rows, total, rules) -> int — идти от индекса основной «Итого» назад, пока текст строки (колонка текста) матчит total_row_secondary; вернуть номер самой верхней (или total). В collect_body_rows вернуть его как total_row; хвост — по-прежнему от последней из вторых строк после основной. checks._total(rows, secondary) — первая TOTAL, чей body.text не матчит ни один паттерн secondary, иначе первая TOTAL.

  • Step 4: PASS; make golden-update — ожидаемо synthetic/s42.json (обе итоговые строки TOTAL, status: done); реальные снимки (пакет ДКБ с «Итого развернутое» после «Итого») не меняются.

  • Step 5: make check; commit — fix(osv): «Итого развернутое» перед «Итого» — итоговая строка, сверка по основной «Итого» (#59); fixtures — Снимок s42: «Итого развернутое» перед «Итого» (tabular-parser#59).


Task 4: Плоский список под счётом шапки

Files:

  • Modify: api/src/parser/hierarchy.py (strategy_header_account, resolve_levels)
  • Modify: api/src/parser/osv_warnings.py (HIERARCHY_TEXTS["header_account"] = "плоский список под счётом шапки (header_account)")
  • Modify: api/src/parser/classify.py (classify_rows(..., header_account: str | None = None))
  • Modify: api/src/parser/checks.py (total_matches_root без корня ACCOUNT)
  • Modify: api/src/parser/osv.py (передать header_account в resolve_levels и classify_rows)
  • Test: api/tests/unit/parser/test_hierarchy.py, api/tests/unit/parser/test_checks.py, Create api/tests/unit/parser/test_osv_flat_form.py

Interfaces:

  • Consumes: collect_body_rows и run_checks(..., secondary_totals=...) задачи 3.

  • Produces: strategy_header_account(rows: Sequence[BodyRow], deadline: Deadline | None = None) -> dict[int, int] | None — {row: 0} для всех строк, None, если хоть одна is_account; resolve_levels(rows, total_row, deadline, *, header_account: bool = False) -> Levels — header_account пробуется после account_pattern и перед sums, только при header_account=True; classify_rows(..., *, inns=None, unpaired=frozenset(), header_account: str | None = None) — корневая ветка _Branch(header_account, 0).

  • Step 1: Тесты (падают)

    • test_header_account_strategy_flat_rows (test_hierarchy.py): три строки без outline/отступа/счёта, header_account=True → strategy == "header_account", все уровни 0, родители None.
    • test_header_account_not_used_without_header_code: те же строки, header_account=False → InvalidFormatError (прежнее поведение).
    • test_header_account_not_used_with_account_rows: строки «60.01», «ООО А» без оформления, header_account=True → account_pattern.
    • test_total_without_account_root_matches_level_zero_sum (test_checks.py): три корня OTHER/COUNTERPARTY без ACCOUNT, «Итого» = их сумма (сальдо «Итого» свёрнуто нетто) → True; одна сумма испорчена на 1 → False; только «Итого» с нулями → True (R-13 прежний).
    • test_osv_flat_form.py::test_flat_list_s40 (разбор s40 через get_parser() или OsvParser.parse_sheet на листе из sheet_from_rows): status == "done", предупреждение OSV_HIERARCHY_BY_HEADER_ACCOUNT, у каждой строки данных level == 0, parent_row_number is None, account_code_raw == "76.Н.1", organization_name == 'ООО "Товарное хозяйство"'.
    • test_flat_list_s41: status == "done", account_code_raw строк "60".
    • test_outline_wins_over_header_account_and_rows_inherit_header_code (Review Focus 4): счёт в шапке «60», строк-счетов нет, outline: «ООО А» (0) → «Договор 1» (1) → strategy == "outline", у обеих строк account_code_raw == "60".
  • Step 2: Запуск — FAIL.

  • Step 3: Реализация по Interfaces; в osv.py: resolve_levels(body, total_row, deadline, header_account=bool(doc.account_code_raw)), classify_rows(..., header_account=doc.account_code_raw or None). total_matches_root: нет корня ACCOUNT — если _level_zero(rows) не пуст, _sums_match(_values(total), _sum_of(roots)), иначе прежняя проверка нулей.

  • Step 4: PASS; make golden-update — ожидаемо s40, s41 (done, header_account); реальные снимки — только если у файла есть строки до первой строки-счёта (получат account_code_raw шапки, R-4) — каждое такое изменение перечислить в отчёте; иные изменения — стоп.

  • Step 5: make check; commit — feat(osv): стратегия header_account — плоский список под счётом шапки (#59); fixtures — Снимки: плоский список под счётом шапки (tabular-parser#59).


Task 5: Контрагенты расчётных счетов без названия вида

Перед задачей: если трек A (feat/* в .wt/osv-indicators/tabular-parser, AURORA-1613) уже слит в main — git -C …/tabular-parser fetch origin && git -C …/tabular-parser rebase origin/main, make golden-update, разрешить конфликты в classify._classify_row (трек A меняет определение data_type в той же функции; сохранить обе правки). Не слит — работать как есть.

Files:

  • Modify: api/rules/osv_groups.yaml (counterparty_accounts: ['58', '60', '62', '66', '67', '75', '76', '80'] с комментарием про AURORA-1661, OQ-158 и R-9 (71/73 исключены))
  • Modify: api/src/parser/rules.py (OsvGroupRules.counterparty_accounts: list[str] = [], проверка кодов как у OsvCheckRules, is_counterparty_account; общий хелпер сопоставления «код или его субсчёт» для обоих классов)
  • Modify: api/src/parser/classify.py (_classify_row, _Kind, ClassifiedRow.counterparty_by_account: bool = False)
  • Modify: api/src/parser/osv.py (заметка OSV_ROWS)
  • Test: api/tests/unit/parser/test_classify.py, api/tests/unit/parser/test_rules.py, api/tests/unit/parser/test_osv_flat_form.py

Interfaces:

  • Consumes: classify_rows(..., header_account=...) задачи 4.

  • Produces: OsvGroupRules.is_counterparty_account(account_code: str | None) -> bool ("76" → 76, 76.2, 76.Н.1; не 760); ClassifiedRow.counterparty_by_account — строка получила COUNTERPARTY по плану счетов.

  • Step 1: Тесты (падают)

    • test_rules.py::test_counterparty_accounts_match_code_and_subaccounts: is_counterparty_account("76.Н.1"), ("60.01") — True; ("760"), ("600"), ("10.01"), (None) — False (Review Focus 5).
    • test_classify.py::test_unnamed_first_level_on_settlement_account_is_counterparty: схема [ACCOUNT("Субконто")], «60.01» → «ИП Иванов А.А.» (уровень 1) → COUNTERPARTY, group_name is None, counterparty_name == "ИП Иванов А.А.", counterparty_by_account is True.
    • test_unnamed_second_level_stays_other: под таким контрагентом строка уровня 2 → OTHER.
    • test_named_other_group_is_not_reclassified: схема [ACCOUNT, OTHER("Работники организаций")] на 71 → OTHER.
    • test_non_settlement_account_stays_other: счёт «10.01» → OTHER, counterparty_by_account is False.
    • test_osv_flat_form.py::test_s43_counterparties_with_warning: разбор s43 → строки «ИП Иванов А.А.», «ООО "ЭСТЕТ ОМСК"» — COUNTERPARTY; в warnings ровно одно OSV_ROWS с текстом «вид субконто в шапке не назван: строки аналитики счёта 60.01 приняты за контрагентов по плану счетов — проверьте»; status == "done".
    • test_s44_materials_stay_other: разбор s44 → строки OTHER, предупреждения «вид субконто…» нет.
    • test_s41_flat_counterparties: разбор s41 → строки данных COUNTERPARTY (по схеме с видом «Организации», R-10), account_code_raw == "60", предупреждения «вид субконто…» нет.
  • Step 2: Запуск — FAIL.

  • Step 3: Реализация. В _classify_row после _schema_group: если position == 1, position >= len(schema) и rules.groups.is_counterparty_account(code) — COUNTERPARTY без имени, ИНН — по общей ветке COUNTERPARTY (колонка, строка ИНН, хвост названия); флаг counterparty_by_account. В osv.py — по одной заметке OsvNote(OSV_ROWS, …) на каждый различный account_code_raw строк с флагом, в порядке листа.

  • Step 4: PASS; make golden-update — ожидаемо s41, s43; реальные снимки, где схема — одна ACCOUNT на расчётном счёте с не-счётными строками (пакет ДКБ osv-dkb/**), — OTHER → COUNTERPARTY и новое OSV_ROWS: перечислить файлы в отчёте; статусы снимков не должны меняться (если меняются — стоп).

  • Step 5: make check; commit — feat(osv): контрагенты расчётных счетов без названия вида — по плану счетов (#59); fixtures — Снимки: контрагенты расчётных счетов по плану счетов (tabular-parser#59).


Task 6: Правила, открытые вопросы, README, приёмка на реальных файлах

Files:

  • Modify: /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/docs/context/05-domain-rules.md (§1.1, §1.3, §1.4)

  • Modify: /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/docs/context/07-open-questions.md

  • Modify: README.md (корень tabular-parser, раздел «Парсер ОСВ»: каскад стратегий + header_account), CLAUDE.md tabular-parser (строка каскада в «Модули» — outline → indent → account_pattern → header_account → sums)

  • Modify: openspec/changes/osv-flat-form/tasks.md (отметить пункты), design.md (раздел «Decisions made without discussion» — дописать рулинги исполнения, если были)

  • Step 1: §1 правил (docs). §1.1: код счёта после «по счету» — и через двоеточие; счёт шапки важнее строки таблицы; строка вида субконто между названием и периодом — не организация, известные виды — группировки после ACCOUNT; организация из «План счетов <орг>»; строка таблицы «Нормализация значений шапки» — без изменений. §1.3: стратегия header_account (условия, место в каскаде); счёт шапки у строк до первой строки-счёта; правило расчётных счетов (список — данные osv_groups.yaml, позиция 1, без имени, предупреждение; риск OQ-158). §1.4: «Итого развернутое» в любом порядке; «Итого» без корневой ACCOUNT = сумма уровня 0. Ссылки — AURORA-1661, tabular-parser#59, решения владельца 16.10/19.10.2026.

  • Step 2: Открытые вопросы (docs) — следующие свободные номера OQ на момент правки (трек A может занять номера раньше — сверить с origin/main docs): «Учредители» на 80 (именованная OTHER, гудвилл §4 п. 2); сузить список расчётных счетов (71, 73); «Организации» в строке вида субконто — что это за вид в 1С. Каждый — «открыт», с допущением принятого поведения (R-8 остаётся OTHER, R-9 71/73 исключены, R-10 синоним COUNTERPARTY).

  • Step 3: Приёмка на реальных файлах контура (без коммита данных).

Run:

T=$(mktemp -d) && cd "$T" && unzip -q /Users/andrey.epifancev/projects/awp-ca/result_dir.zip 'files/26e4c90d*' 'files/638ecb3d*' 'files/f2e0e04e*' 'files/7e49f8b5*' 'files/45e06f7d*' 'files/cb7e6eb0*' 'files/270b41da*' 'files/738bfcf5*' 'files/8c7e95d9*' && cp "/Users/andrey.epifancev/projects/awp-ca/ОСВ сч. 60 за 2 кв. 2026.xlsx" files/ && echo "$T"
cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser/api && for f in "$T"/files/*; do ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "$f"; done

Expected (записать таблицей в отчёт задачи): ни одного INVALID_FORMAT; у плоских файлов (26e4c90d, 638ecb3d, f2e0e04e, 7e49f8b5, 45e06f7d, cb7e6eb0, письмо) — OSV_HIERARCHY_BY_HEADER_ACCOUNT, account_code_raw из шапки («76.Н.1», «76.АВ», «76.2», «60»), организация «ООО "Товарное хозяйство"» / «ООО Хреновская Нива», строки — COUNTERPARTY; 270b41da/738bfcf5 — account_code_raw = "76", «Итого развернутое» — TOTAL, нарушение остаётся только у валютного «76.55» (трек A); 8c7e95d9 лист «60 сч.» — контрагенты COUNTERPARTY с предупреждением. Письмо — COUNTERPARTY по схеме «Организации», без предупреждения «вид субконто…». Отклонение — описать, не чинить молча. После — rm -rf "$T".

  • Step 4: make check-full в tabular-parser (нужен стек compose на портах .env.test; если порты заняты демо-стендом — стоп и вопрос контроллеру); make check в docs.
  • Step 5: openspec validate osv-flat-form --strict (tabular-parser) — без ошибок.
  • Step 6: Commit — docs: §1 правил ОСВ: плоская форма 1С — счёт из шапки, «Итого развернутое», header_account, контрагенты по плану счетов (tabular-parser#59, AURORA-1661); tabular-parser: docs: README, CLAUDE.md и change osv-flat-form (#59).