docs: финальное ревью — пример needs_check, спека (лимиты чтения, ИНН, ACCOUNT_RE, риски R-21), OQ-81
- T13.1: contracts/examples/result-osv-needs-check.json — предупреждения как пишет парсер («sheet=Лист_1 hierarchy=outline», текст нарушения из checks.py), organization_inn = null, версия 1.0.0; копия в docs байт в байт. - T13.2: лист ОСВ — признак в первых 30 строках (SHEET_MARKER_ROWS); T13.3: сломанный code span. - T13.4: остаточные риски R-21 списком (RR-1…RR-4, слепая зона «+X к обеим сторонам», (0,0) при (X, X), «12.ООО»). - I-1/I-2/M-1/M-3 в тексте спеки: лимиты чтения (R-23) и сценарии, лог стека, флаги ACCOUNT_RE, R-24. - T13.6: OQ-81 — «частично закрыт» в design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
9b8b8ea727
commit
4651513820
@ -2,16 +2,22 @@
|
|||||||
"schema_version": "1.1",
|
"schema_version": "1.1",
|
||||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
"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",
|
"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": [
|
"documents": [
|
||||||
{
|
{
|
||||||
"document_type": "OSV",
|
"document_type": "OSV",
|
||||||
"locator": "sheet:Лист_1",
|
"locator": "sheet:Лист_1",
|
||||||
"header": {
|
"header": {
|
||||||
"organization_name": "ООО Ромашка",
|
"organization_name": "ООО Ромашка",
|
||||||
"organization_inn": "7700000000",
|
"organization_inn": null,
|
||||||
"account_code_raw": "60",
|
"account_code_raw": "60",
|
||||||
"period_text": "1 квартал 2025 г.",
|
"period_text": "1 квартал 2025 г.",
|
||||||
"period_year": 2025,
|
"period_year": 2025,
|
||||||
|
|||||||
@ -127,7 +127,7 @@ xlsx в `../fixtures/osv/synthetic/`; xlsb — один файл `s13`, собр
|
|||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
Не блокируют: OQ-02 (нарастающие по данным — ядро), OQ-45 (`NU` в ядре), OQ-01 (хранение «Итого»), OQ-73 (дедуп по
|
Не блокируют: OQ-02 (нарастающие по данным — ядро), OQ-45 (`NU` в ядре), OQ-01 (хранение «Итого»), OQ-73 (дедуп по
|
||||||
attempt), OQ-81 (закрыт хелпером фикстур tabular-parser). Новые вопросы внесены в `07-open-questions.md` задачей 13
|
attempt), OQ-81 (частично закрыт: в tabular-parser `ARM_KA_FIXTURES` поддержан хелпером фикстур, `scan-gateway` — follow-up). Новые вопросы внесены в `07-open-questions.md` задачей 13
|
||||||
(OQ-98…OQ-105: коды валют CURRENCY-строк, несколько ОСВ одного счёта в книге, ИНН внутри текста строки и «ИНН/КПП»,
|
(OQ-98…OQ-105: коды валют CURRENCY-строк, несколько ОСВ одного счёта в книге, ИНН внутри текста строки и «ИНН/КПП»,
|
||||||
числа с «+»/«.5»/«5.», xlsb из Excel, буквенный первый сегмент счёта, «Валюта» в строке детализации, подтверждение
|
числа с «+»/«.5»/«5.», xlsb из Excel, буквенный первый сегмент счёта, «Валюта» в строке детализации, подтверждение
|
||||||
сверки по нетто).
|
сверки по нетто).
|
||||||
|
|||||||
@ -8,8 +8,9 @@
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: Листы ОСВ находятся по содержимому
|
### Requirement: Листы ОСВ находятся по содержимому
|
||||||
Парсер SHALL считать листом ОСВ любой лист, где хотя бы одна ячейка матчит признак ОСВ из `rules/osv_headers.yaml`
|
Парсер SHALL считать листом ОСВ любой лист, где ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости
|
||||||
(без учёта регистра, «ё/е», переносов строк и дефисов). Листы без признаков ОСВ SHALL пропускаться с предупреждением
|
всегда над таблицей) матчит признак ОСВ из `rules/osv_headers.yaml` (без учёта регистра, «ё/е», переносов строк и
|
||||||
|
дефисов). Листы без признаков ОСВ SHALL пропускаться с предупреждением
|
||||||
`sheet=<имя> skipped: нет признаков ОСВ` в `metrics.warnings`; лист с признаками ББ/ОФР (§12 п. 1) — с предупреждением
|
`sheet=<имя> skipped: нет признаков ОСВ` в `metrics.warnings`; лист с признаками ББ/ОФР (§12 п. 1) — с предупреждением
|
||||||
`sheet=<имя> skipped: statement (slice 2b)`. Книга без единого листа ОСВ →
|
`sheet=<имя> skipped: statement (slice 2b)`. Книга без единого листа ОСВ →
|
||||||
`status = error`, `error.code = INVALID_DOCUMENT`. Несколько листов ОСВ → несколько `documents` с `locator = sheet:<имя>`.
|
`status = error`, `error.code = INVALID_DOCUMENT`. Несколько листов ОСВ → несколько `documents` с `locator = sheet:<имя>`.
|
||||||
@ -34,7 +35,8 @@
|
|||||||
считается (R-12). Допустимы: отсутствие группы «начало» (сальдо на начало `null`), одна колонка в группе без
|
считается (R-12). Допустимы: отсутствие группы «начало» (сальдо на начало `null`), одна колонка в группе без
|
||||||
Дебет/Кредит (свёрнутое сальдо: положительное — дебет, отрицательное — кредит, предупреждение), дополнительные колонки
|
Дебет/Кредит (свёрнутое сальдо: положительное — дебет, отрицательное — кредит, предупреждение), дополнительные колонки
|
||||||
(«Количество», «Валюта», «ИНН», «КПП», «Код»). Колонка текста строки — первая непустая слева от сумм по строкам данных;
|
(«Количество», «Валюта», «ИНН», «КПП», «Код»). Колонка текста строки — первая непустая слева от сумм по строкам данных;
|
||||||
колонка ИНН — по заголовку `ИНН` или колонка, где ≥ 80 % непустых значений — 10 или 12 цифр. Правая граница таблицы —
|
колонка ИНН — по заголовку `ИНН` или колонка, где ≥ 80 % непустых значений — ровно 10 или 12 цифр (число из 9/11 цифр
|
||||||
|
при этом выборе MUST NOT дополняться нулём: иначе числовая колонка КПП сошла бы за колонку ИНН, R-24). Правая граница таблицы —
|
||||||
последняя колонка «Кредит»; всё правее MUST NOT попадать в суммы. Колонки внутри таблицы, не сопоставленные с шестью
|
последняя колонка «Кредит»; всё правее MUST NOT попадать в суммы. Колонки внутри таблицы, не сопоставленные с шестью
|
||||||
суммами и ИНН, SHALL попадать в `rows[].extra` по заголовку. Заголовок не найден или в найденной строке групп нет «оборотов» либо «сальдо
|
суммами и ИНН, SHALL попадать в `rows[].extra` по заголовку. Заголовок не найден или в найденной строке групп нет «оборотов» либо «сальдо
|
||||||
на конец» → `status = error`, `error.code = INVALID_FORMAT` с названием ненайденной группы, листом и шагом. В `columns` SHALL быть записана диагностика (буква
|
на конец» → `status = error`, `error.code = INVALID_FORMAT` с названием ненайденной группы, листом и шагом. В `columns` SHALL быть записана диагностика (буква
|
||||||
@ -115,11 +117,13 @@
|
|||||||
Каждая строка данных SHALL получить `group_type`: ACCOUNT — текст матчит `ACCOUNT_RE` (регистр учитывается, IGNORECASE
|
Каждая строка данных SHALL получить `group_type`: ACCOUNT — текст матчит `ACCOUNT_RE` (регистр учитывается, IGNORECASE
|
||||||
нет): две-три цифры и до двух сегментов субсчёта через точку, где сегмент — `\d{1,2}` **или 1–3 заглавные буквы**
|
нет): две-три цифры и до двух сегментов субсчёта через точку, где сегмент — `\d{1,2}` **или 1–3 заглавные буквы**
|
||||||
кириллицы/латиницы («76.АВ», «76.ВА», «76.ОТ», R-16), далее опционально запятая и название: `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$`
|
кириллицы/латиницы («76.АВ», «76.ВА», «76.ОТ», R-16), далее опционально запятая и название: `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$`
|
||||||
(после запятой допустим любой пробел или его отсутствие, R-18); `account_code_raw` — код как в файле, без названия после
|
с флагами `re.ASCII | re.DOTALL` (после запятой допустим любой пробел или его отсутствие, R-18; цифры — только 0–9;
|
||||||
|
название после запятой может занимать несколько строк ячейки — M-1); `account_code_raw` — код как в файле, без названия после
|
||||||
запятой. Не-счёт — группировка из `group_schema` по позиции (число не-счётных предков после последнего ACCOUNT + 1), при
|
запятой. Не-счёт — группировка из `group_schema` по позиции (число не-счётных предков после последнего ACCOUNT + 1), при
|
||||||
выходе за схему — OTHER; последняя строка уровня 0 с текстом из словаря «Итого» — TOTAL. Для COUNTERPARTY
|
выходе за схему — OTHER; последняя строка уровня 0 с текстом из словаря «Итого» — TOTAL. Для COUNTERPARTY
|
||||||
`counterparty_name = row_value`, `counterparty_inn` — **только** из колонки ИНН: цифры текстовой ячейки колонки («ИНН
|
`counterparty_name = row_value`, `counterparty_inn` — **только** из колонки ИНН: цифры текстовой ячейки колонки («ИНН
|
||||||
352819549855» → `352819549855», R-15), результат только при 10 или 12 цифрах, иначе `null`; ИНН внутри текста строки
|
352819549855» → «352819549855», R-15), результат только при 10 или 12 цифрах, иначе `null`; число из 9/11 цифр
|
||||||
|
(ИНН регионов 01–09 с потерянным ведущим нулём) при извлечении дополняется нулём слева (R-24); ИНН внутри текста строки
|
||||||
(«Полиграф, 3528315710») MUST NOT извлекаться (R-17). Строки «Вал.», «Кол.», трёхбуквенного кода валюты SHALL получать
|
(«Полиграф, 3528315710») MUST NOT извлекаться (R-17). Строки «Вал.», «Кол.», трёхбуквенного кода валюты SHALL получать
|
||||||
`group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST NOT занимать позицию в `group_schema` и MUST NOT наследовать
|
`group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST NOT занимать позицию в `group_schema` и MUST NOT наследовать
|
||||||
`counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки, R-19); их суммы остаются в шести полях.
|
`counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки, R-19); их суммы остаются в шести полях.
|
||||||
@ -159,14 +163,29 @@
|
|||||||
turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по каждому родителю суммы = Σ детей с тем же
|
turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по каждому родителю суммы = Σ детей с тем же
|
||||||
`data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у
|
`data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у
|
||||||
родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто,
|
родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто,
|
||||||
а субсчета и контрагентов — развёрнуто; остаточные риски: перестановка сторон при совпавшем нетто родителя этой проверкой
|
а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки CURRENCY/QUANTITY в суммирование не входят и сами
|
||||||
не ловится, строгими остаются тождество строки и «Итого = корень»); строки CURRENCY/QUANTITY в суммирование не входят и сами
|
|
||||||
родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (пустая ОСВ,
|
родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (пустая ОСВ,
|
||||||
R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в
|
R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в
|
||||||
`warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root,
|
`warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root,
|
||||||
balance_identity_violations, parent_sum_violations}`. Любое нарушение → `status = needs_check` всего результата и запись
|
balance_identity_violations, parent_sum_violations}`. Любое нарушение → `status = needs_check` всего результата и запись
|
||||||
причины в `metrics.warnings`; парсер MUST NOT подгонять структуру под сходимость.
|
причины в `metrics.warnings`; парсер MUST NOT подгонять структуру под сходимость.
|
||||||
|
|
||||||
|
Остаточные риски (известные слепые зоны, не ловятся проверками §1.4):
|
||||||
|
- RR-1 — у свёрнутого родителя сверяется только нетто сальдо: распределение сальдо детей между дебетом и кредитом
|
||||||
|
(перестановка сторон при совпавшем нетто) не проверяется.
|
||||||
|
- RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя, у которого в файле потеряна одна
|
||||||
|
сторона, неотличимо от нетто-показа 1С и проходит, если нетто совпало.
|
||||||
|
- RR-3 — нетто допускается на любом уровне дерева (счёт, субсчёт, контрагент, договор), а не только на уровне «счёт →
|
||||||
|
субсчета», где его показывает 1С: ошибка распределения сальдо между договорами под свёрнутым контрагентом не ловится.
|
||||||
|
- RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто
|
||||||
|
строки, ловится лишь сверкой с родителем по колонкам, а под свёрнутым родителем — ничем (см. слепую зону ниже).
|
||||||
|
- Слепая зона: обе стороны сальдо (на начало или на конец) развёрнутого ребёнка увеличены на одно и то же X, а все его
|
||||||
|
предки до корня свёрнуты — не ловится ничем (тождество строки, нетто родителей и «Итого = корень» сохраняются).
|
||||||
|
- Родитель с сальдо (0, 0) при детях (X Дт, X Кт) принимается: нулевое сальдо — свёрнутое, нетто детей X − X = 0
|
||||||
|
(закреплено тестом как осознанное поведение R-21).
|
||||||
|
- Вне R-21: текст вида «12.ООО» (2–3 цифры, точка, до трёх заглавных букв) принимается за счёт (R-16) и меняет
|
||||||
|
`group_type` строк ветки; суммы и проверки это не затрагивает.
|
||||||
|
|
||||||
#### Scenario: Все реальные файлы сходятся
|
#### Scenario: Все реальные файлы сходятся
|
||||||
- **WHEN** разбирается каждый файл `fixtures/osv/**`
|
- **WHEN** разбирается каждый файл `fixtures/osv/**`
|
||||||
- **THEN** `status = done`, `checks.total_row_matches_root = true`, оба списка нарушений пусты
|
- **THEN** `status = done`, `checks.total_row_matches_root = true`, оба списка нарушений пусты
|
||||||
@ -195,9 +214,15 @@ balance_identity_violations, parent_sum_violations}`. Любое нарушен
|
|||||||
### Requirement: Форматы, лимиты, время
|
### Requirement: Форматы, лимиты, время
|
||||||
Парсер SHALL читать XLSX (openpyxl, полное чтение), XLS (xlrd, `formatting_info`), XLSB (pyxlsb; outline level недоступен —
|
Парсер SHALL читать XLSX (openpyxl, полное чтение), XLS (xlrd, `formatting_info`), XLSB (pyxlsb; outline level недоступен —
|
||||||
каскад начинается с отступа). Несоответствие `file_type` сигнатуре файла → попытка по сигнатуре и предупреждение.
|
каскад начинается с отступа). Несоответствие `file_type` сигнатуре файла → попытка по сигнатуре и предупреждение.
|
||||||
Файл больше `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) → `error UNSUPPORTED_FORMAT` с текстом про лимит; книга не
|
Файл больше `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) → `error UNSUPPORTED_FORMAT` с текстом про лимит. До вызова
|
||||||
открывается → `UNREADABLE`; превышение `PARSE_TIMEOUT_SECONDS` (по умолчанию 120, проверка между листами, стратегиями и
|
библиотеки чтения SHALL действовать лимиты чтения (R-23) → `error UNSUPPORTED_FORMAT` с текстом про лимит: сумма
|
||||||
блоками строк) → `TIMEOUT`; непредвиденное исключение → `RECOGNITION_FAILED` с одной строкой причины. Любой файл
|
объявленных несжатых размеров записей zip (xlsx/xlsb) не больше `ZIP_UNCOMPRESSED_LIMIT_FACTOR × PARSE_MAX_BYTES` (10 ×);
|
||||||
|
суммарная площадь объединённых ячеек книги не больше `MERGED_AREA_LIMIT` (200 000 ячеек; для xlsx — потоковым сканом
|
||||||
|
`<mergeCell ref>` до openpyxl, для любого формата — после чтения). Книга не открывается → `UNREADABLE` (лог WARNING);
|
||||||
|
превышение `PARSE_TIMEOUT_SECONDS` (по умолчанию 120, проверка между листами, в циклах по всему листу — поиск заголовка
|
||||||
|
таблицы, дополнительные колонки, строки, стратегии иерархии; чтение книги библиотекой таймаут не прерывает) → `TIMEOUT`;
|
||||||
|
непредвиденное исключение → `RECOGNITION_FAILED` с одной строкой причины, стек — в лог уровня ERROR. Защиты от XML-бомб
|
||||||
|
(`defusedxml`) нет — новая зависимость требует решения платформы. Любой файл
|
||||||
`fixtures/osv/**` SHALL разбираться быстрее 5 с.
|
`fixtures/osv/**` SHALL разбираться быстрее 5 с.
|
||||||
|
|
||||||
#### Scenario: XLSB
|
#### Scenario: XLSB
|
||||||
@ -213,6 +238,14 @@ balance_identity_violations, parent_sum_violations}`. Любое нарушен
|
|||||||
- **WHEN** передан файл в 51 МиБ
|
- **WHEN** передан файл в 51 МиБ
|
||||||
- **THEN** `error UNSUPPORTED_FORMAT`, книга не открывается
|
- **THEN** `error UNSUPPORTED_FORMAT`, книга не открывается
|
||||||
|
|
||||||
|
#### Scenario: Бомба объединённых ячеек
|
||||||
|
- **WHEN** передан xlsx в несколько килобайт с `<mergeCell ref="A1:Z200000"/>` (5,2 млн ячеек)
|
||||||
|
- **THEN** `error UNSUPPORTED_FORMAT` («лимит объединённых ячеек») быстрее 1 с, openpyxl не вызывается
|
||||||
|
|
||||||
|
#### Scenario: Распакованный zip больше лимита
|
||||||
|
- **WHEN** zip книги распаковывается больше чем в 10 × `PARSE_MAX_BYTES`
|
||||||
|
- **THEN** `error UNSUPPORTED_FORMAT` («распакованный размер … больше лимита»), книга не открывается
|
||||||
|
|
||||||
### Requirement: Словари — данные, не код
|
### Requirement: Словари — данные, не код
|
||||||
Признаки листов, синонимы групп колонок и подзаголовков, колонки ИНН, строки «Итого», маркеров шапки (`rules/osv_headers.yaml`:
|
Признаки листов, синонимы групп колонок и подзаголовков, колонки ИНН, строки «Итого», маркеров шапки (`rules/osv_headers.yaml`:
|
||||||
`sheet_markers`, `statement_markers`, `groups`, `debit`, `credit`, `service_columns`, `total_row`, `header_markers`),
|
`sheet_markers`, `statement_markers`, `groups`, `debit`, `credit`, `service_columns`, `total_row`, `header_markers`),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user