docs: финальное ревью Среза 2a — §1 (лимиты чтения, ИНН, ACCOUNT_RE, риски R-21), OQ-106…OQ-109

- 05-domain-rules §1.0 п. 1: признак ОСВ в первых 30 строках листа; п. 4, §1.3: ИНН по значениям без дополнения
  нулём, дополнение при извлечении (R-24); ACCOUNT_RE с re.ASCII | re.DOTALL (M-1); §1.4: остаточные риски R-21
  (RR-1…RR-4, слепые зоны); §1.6: лимиты чтения (R-23), логирование.
- 06-test-data §3 п. 17: ИНН в тексте строки — ОСВ 76 Счастливочной за 1–3 кв. 2024 (6 + 7 + 7 строк).
- 07-open-questions: OQ-106 (Итого = первый корень), OQ-107 (hierarchy=sums), OQ-108 (defusedxml, лимиты чтения),
  OQ-109 (размер объекта S3 до скачивания); OQ-105 ссылается на риски §1.4.
- OQ-81 «частично закрыт» в 09-delivery-plan, tasks.md и proposal.md change.
- contracts/examples/result-osv-needs-check.json — как пишет парсер (копия tabular-parser байт в байт).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-05 16:32:52 +04:00
parent baeb9189f6
commit c2005a4699
7 changed files with 47 additions and 21 deletions

View File

@ -16,8 +16,8 @@
бухгалтером. **Ни одна буква колонки, ни один номер строки, ни одно имя листа не являются контрактом.** Фикстуры в
`fixtures/osv/` — это примеры двух организаций, а не спецификация формата. Парсер обязан:
1. **Найти лист(ы) с ОСВ** по содержимому: любая ячейка, матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра,
«ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`.
1. **Найти лист(ы) с ОСВ** по содержимому: ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости всегда
над таблицей), матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра, «ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`.
Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать.
2. **Найти заголовок таблицы** по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…»,
«Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит»,
@ -29,7 +29,8 @@
«начала» (только обороты и конец), с одной свёрнутой колонкой сальдо (знак = сторона), с дополнительными
колонками («Количество», «Валюта», «ИНН», «КПП», «Код»). Неизвестные колонки — сохранять в `extra` строки, не терять.
4. **Определить колонку текста строки** — первая непустая слева от сумм (обычно A, но не обязательно); ИНН — колонка с
заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — 10/12 цифр.
заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — ровно 10/12 цифр (число из 9/11 цифр при этом выборе
нулём не дополняется, иначе числовой КПП сошёл бы за ИНН — R-24).
5. **Определить иерархию строк** по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) —
по отступу (ведущие пробелы / `alignment.indent`), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по
факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings` как
@ -97,7 +98,8 @@ r11 Договоры ←
Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5),
`row_value` (текст первой колонки), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат
только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; ИНН внутри текста строки контрагента
только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; число из 9/11 цифр — ИНН региона 01–09
с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН внутри текста строки контрагента
(«Полиграф, 3528315710») парсер не извлекает: `counterparty_name` = весь текст строки, **[открыто]** OQ-100),
`data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки).
@ -107,7 +109,8 @@ r11 Договоры ←
ветки; их суммы остаются в шести полях, но в проверки родителя не входят (§1.4).
Классификация строки (п. 6–10 «Парсинг ОСВ»):
1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (регистр учитывается;
1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (флаги `re.ASCII | re.DOTALL`:
цифры только 0–9, название после запятой может быть многострочным; регистр учитывается;
сегмент субсчёта — 1–2 цифры **или 1–3 заглавные буквы**: буквенные субсчета 1С «76.АВ», «76.ВА», «76.ОТ» встречаются в
8 реальных файлах; после запятой — опциональное название, пробел необязателен: «60.01, Расчеты с поставщиками»)
→ ACCOUNT, `account_code_raw` — код как в файле без названия, `account_code` (нормализованный) строит ядро, все
@ -134,9 +137,20 @@ r11 Договоры ←
(начало, конец) — по колонкам либо, **если у родителя сальдо свёрнуто** (не больше одной ненулевой стороны), по нетто
`дебет − кредит` (R-21): 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета и контрагентов —
развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт). Без этого правила 32 из 66 реальных файлов не сходились бы
по шести колонкам только из-за способа показа. Остаточный риск: перестановка сторон при совпавшем нетто родителя
этой проверкой не ловится — её ловят тождество строки и «Итого = корень». **[открыто]** OQ-105: подтвердить, что
нетто-свёртка родителя — штатное поведение 1С для всех версий выгрузки.
по шести колонкам только из-за способа показа. **[открыто]** OQ-105: подтвердить, что нетто-свёртка родителя —
штатное поведение 1С для всех версий выгрузки. Остаточные риски (проверками §1.4 не ловятся):
- RR-1 — у свёрнутого родителя сверяется только нетто: распределение сальдо детей между Дт и Кт (перестановка сторон
при совпавшем нетто) не проверяется;
- RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя с потерянной в файле стороной
неотличимо от нетто-показа 1С и проходит при совпавшем нетто;
- RR-3 — нетто допускается на любом уровне дерева (не только «счёт → субсчета»): ошибка распределения сальдо между
договорами под свёрнутым контрагентом не ловится;
- RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто
строки, ловится лишь сверкой с развёрнутым родителем по колонкам;
- слепая зона: обе стороны сальдо развёрнутого ребёнка увеличены на одно X, а все предки до корня свёрнуты — не
ловится ничем;
- родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом;
- вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16).
- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (**пустая ОСВ**: заголовок и
сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ
штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не
@ -180,10 +194,10 @@ result-osv-needs-check.json`.
| `needs_check` | нарушена любая проверка §1.4 хотя бы на одном листе (или «Итого» не найдена); причины — `metrics.warnings`, номера строк — `checks` |
| `error` / `INVALID_DOCUMENT` | в книге нет ни одного листа ОСВ; в сообщении — число просмотренных листов |
| `error` / `INVALID_FORMAT` | лист ОСВ есть, но нет заголовка таблицы, обязательной группы сумм (§1.0 п. 2) или иерархию определить не удалось; в сообщении — лист и шаг |
| `error` / `UNREADABLE` | книга не открывается |
| `error` / `UNSUPPORTED_FORMAT` | сигнатура файла не XLS/XLSX/XLSB либо размер больше `PARSE_MAX_BYTES` |
| `error` / `UNREADABLE` | книга не открывается (лог WARNING) |
| `error` / `UNSUPPORTED_FORMAT` | сигнатура файла не XLS/XLSX/XLSB, размер больше `PARSE_MAX_BYTES` либо превышен лимит чтения (распакованный zip, площадь объединённых ячеек) |
| `error` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` |
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение` |
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение`, стек — в лог ERROR |
Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по
`file_type` запроса; несовпадение — предупреждение. Листы форм ББ/ОФР (§12 п. 1) и листы без признаков ОСВ пропускаются с
@ -193,8 +207,10 @@ result-osv-needs-check.json`.
«xlsb: outline level недоступен»).
Лимиты: `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с; кооперативный — проверка между листами,
стратегиями и блоками строк, зависание внутри библиотеки чтения не прерывается). Любой реальный файл разбирается быстрее
5 с (максимум ≈ 0,6 с).
в циклах по листу и между стратегиями; чтение книги библиотекой он не прерывает). Поэтому до библиотеки чтения действуют
лимиты чтения (R-23) → `UNSUPPORTED_FORMAT`: распакованный zip (xlsx/xlsb) ≤ 10 × `PARSE_MAX_BYTES`, суммарная площадь
объединённых ячеек книги ≤ 200 000 (реальный максимум 10 090). Защиты от XML-бомб (`defusedxml`) нет — **[открыто]**
OQ-108. Любой реальный файл разбирается быстрее 5 с (максимум ≈ 0,6 с).
Словари: `osv_headers.yaml` (признаки листов, группы сумм, «Дебет»/«Кредит», колонка ИНН, «Итого», метки шапки),
`osv_groups.yaml` (группировка → `group_type`), `periods.yaml` (период → квартал и нарастающий итог), `data_types.yaml`

View File

@ -110,7 +110,7 @@
16. **Сальдо родителя нетто, детей развёрнуто**: 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета —
развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт); без учёта этого 32 из 66 файлов не сходились бы по
колонкам (§1.4 правил, R-21).
17. **ИНН внутри текста строки**: у Счастливочной в ОСВ 76 за 2024 г. встречаются контрагенты вида «Полиграф, 3528315710»
17. **ИНН внутри текста строки**: у Счастливочной в ОСВ 76 за 1–3 кв. 2024 встречаются контрагенты вида «Полиграф, 3528315710»
(20 строк) без колонки ИНН; парсер ИНН из текста не извлекает (OQ-100).
## 4. ББ / ОФР (PDF) и ответы ИС Сканирование

View File

@ -110,7 +110,11 @@
| OQ-102 | Нужен **реальный xlsb из Excel**: единственный `fixtures/osv/synthetic/s13_no_outline_indent.xlsb` собран тестовым писателем BIFF12 и не доказывает чтение файлов, сохранённых Excel (стили, другая раскладка записей). Получить xlsb у аналитиков или пересохранить `s02` в Excel и заменить `s13` по инструкции в README fixtures | tabular-parser: R-9, O-7 | срез 2a (достоверность xlsb) | допущение принято: читаем `pyxlsb` без outline level и форматирования; проверка — на первом настоящем xlsb |
| OQ-103 | Счета с буквой в **первом** сегменте кода (забалансовые «МЦ.04», «ГТД», «НЕ.01.1») не распознаются как ACCOUNT: регулярка требует цифр в первом сегменте, строка станет контрагентом/группировкой по позиции. Нужны ли такие ОСВ, как их кодировать в `chart_of_account` | tabular-parser: R-16 (design №26), `classify.py` | срез 2c (план счетов), парсер | допущение принято: не распознаются; в 66 реальных файлах не встречаются |
| OQ-104 | Группировка «Валюта» в строке детализации (шапка варианта 2 «Детализация по субсчетам: …, Валюта») совпадает с меткой валюты шапки и порождает ложное предупреждение «валюта «» не распознана» (`currency = null` в обоих случаях). Править парсер (различать область шапки и строку детализации)? | tabular-parser: O-8, T12.1 (синтетика s16) | срез 2a+ (`header.py`) | допущение принято: дефект малозначим, предупреждение зафиксировано в golden-снимке s16 |
| OQ-105 | Сверка «сумма детей = родитель» по нетто (R-21): у родителя сальдо свёрнуто (счёт — одна сторона), у детей — развёрнуто. Подтвердить аналитиками, что нетто-свёртка родителя — штатное поведение 1С для **всех** версий выгрузки, а не артефакт двух организаций; остаточный риск — перестановка сторон при совпавшем нетто не ловится этой проверкой (ловят тождество строки и «Итого = корень») | tabular-parser: R-21 (design №31), `05-domain-rules.md` §1.4 | срез 2a (контрольные суммы), срез 2c | допущение принято: нетто допустимо, если у родителя не более одной ненулевой стороны сальдо; 32 из 66 реальных файлов иначе не сходились |
| OQ-105 | Сверка «сумма детей = родитель» по нетто (R-21): у родителя сальдо свёрнуто (счёт — одна сторона), у детей — развёрнуто. Подтвердить аналитиками, что нетто-свёртка родителя — штатное поведение 1С для **всех** версий выгрузки, а не артефакт двух организаций; остаточные риски RR-1…RR-4 и слепые зоны (перестановка сторон и одинаковое увеличение обеих сторон сальдо ребёнка под свёрнутыми предками не ловятся ничем) перечислены в §1.4 | tabular-parser: R-21 (design №31), `05-domain-rules.md` §1.4 | срез 2a (контрольные суммы), срез 2c | допущение принято: нетто допустимо, если у родителя не более одной ненулевой стороны сальдо; 32 из 66 реальных файлов иначе не сходились |
| OQ-106 | «Итого = корень» сверяется с **первой** корневой строкой ACCOUNT (уровень 0). Лист с несколькими корнями уровня 0 (несколько счетов в одной ОСВ; ОСВ по субконто без строк-счетов) получает `needs_check`, хотя «Итого» может быть равна Σ корней. Сверять «Итого» с суммой корней? | tabular-parser: финальное ревью Среза 2a, M-4 | срез 2a+ (контрольные суммы `checks.py`), чек-лист документов | допущение принято: сверка с первым корнем; многокорневой лист → `needs_check` (аналитик проверяет вручную); в фикстурах таких листов нет |
| OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) |
| OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23 | tabular-parser (readers), платформа (зависимости образа) | допущение принято: действуют лимиты R-23 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000; XML-бомбы не отсекаются (README «Известные ограничения») |
| OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | открыт: правка воркера — follow-up; лимит размера сейчас срабатывает после скачивания (память под файл выделяется) |
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

View File

@ -102,7 +102,7 @@ Change'и: `tabular-parser/openspec/changes/slice-2a-osv-parser` (issue arm-tabu
- **Допущения парсера, решаются в 2c:** `is_cumulative` только по тексту периода (OQ-02), `data_type = NU` как есть
(OQ-45), «Итого» выдаётся строкой `TOTAL` (OQ-01).
- **Документы:** `05-domain-rules.md` §1 приведён к факту (§1.6 — результат парсера), `06-test-data.md` §7 (синтетика,
golden, рукописный xlsb), OQ-98…OQ-105 открыты, OQ-81 закрыт для `tabular-parser`.
golden, рукописный xlsb), OQ-98…OQ-105 открыты (финальное ревью добавило OQ-106…OQ-109), OQ-81 частично закрыт (в `tabular-parser` `ARM_KA_FIXTURES` поддержан; `scan-gateway` — follow-up).
- **Не сделано / риски:** xlsb проверен только рукописным синтетическим файлом (OQ-102); ИНН из текста строки не
извлекается (OQ-100); счета с буквой в первом сегменте не распознаются (OQ-103).

View File

@ -2,16 +2,22 @@
"schema_version": "1.1",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"service": { "name": "tabular-parser", "version": "0.1.0" },
"service": { "name": "tabular-parser", "version": "1.0.0" },
"status": "needs_check",
"metrics": { "rows": 3, "warnings": ["hierarchy=outline", "balance identity violated at rows [3]"] },
"metrics": {
"rows": 3,
"warnings": [
"sheet=Лист_1 hierarchy=outline",
"§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3"
]
},
"documents": [
{
"document_type": "OSV",
"locator": "sheet:Лист_1",
"header": {
"organization_name": "ООО Ромашка",
"organization_inn": "7700000000",
"organization_inn": null,
"account_code_raw": "60",
"period_text": "1 квартал 2025 г.",
"period_year": 2025,

View File

@ -25,7 +25,7 @@
- **`09-delivery-plan.md`**: Срез 2 разбит на 2a (парсер ОСВ), 2b (ББ/ОФР из Excel), 2c (загрузка и маппинг в ядре),
2d (UI «Первичные документы»); `08-source-docs-index.md`/README без изменений.
- **`07-open-questions.md`**: новые OQ (коды валют для строк CURRENCY; поведение при нескольких ОСВ одного счёта в одной
книге), статус OQ-81 (закрыт хелпером), OQ-45/OQ-01/OQ-02 — пометка «допущение парсера, решение в 2c».
книге), статус OQ-81 (частично закрыт: в tabular-parser `ARM_KA_FIXTURES` поддержан хелпером, scan-gateway — follow-up), OQ-45/OQ-01/OQ-02 — пометка «допущение парсера, решение в 2c».
## Capabilities

View File

@ -4,7 +4,7 @@
`result-osv-minimal.json` (+`extra`, `schema_version 1.1`) и новый `result-osv-needs-check.json`; `openspec validate --strict`;
проверка схемой через `jsonschema` (скрипт в `dev/checks.md`, если есть, иначе вручную в отчёте).
- [x] 1.2 `09-delivery-plan.md`: Срез 2 → подсрезы 2a–2d с составом каждого; `07-open-questions.md`: новые OQ (коды валют
CURRENCY-строк; несколько ОСВ одного счёта в книге — OQ-98…OQ-105), OQ-81 закрыт для tabular-parser, пометки «допущение парсера» у OQ-01/02/45.
CURRENCY-строк; несколько ОСВ одного счёта в книге — OQ-98…OQ-105), OQ-81 частично закрыт (в tabular-parser `ARM_KA_FIXTURES` поддержан; scan-gateway — follow-up), пометки «допущение парсера» у OQ-01/02/45.
- [x] 2.1 После реализации парсера (дочерний change, задачи 3.1–4.3): §1 `05-domain-rules.md` — статусы/коды, `hierarchy=<имя>`
в warnings, TOTAL в rows, лимиты, имена словарей; §1.5 — `ASSUMPTION(OQ-02)`; `06-test-data.md` — синтетика, golden,
xlsb; сверка: сценарии спеки `osv-parsing` ↔ пункты §1 (таблица в отчёте).