Merge pull request 'docs(2b): контракт v1.2, §12 и OQ-138…143, формы ББ/ОФР (Срез 2b)' (#27) from feat/26-slice-2b-statement-parser into main

Reviewed-on: #27
This commit is contained in:
aep 2026-10-06 21:36:53 +03:00
commit b96b5c9da2
16 changed files with 4203 additions and 59 deletions

View File

@ -27,8 +27,12 @@
- **arm-core** — монолитное ядро: справочники, файлы/документы, маппинг результатов распознавания, консолидация
(вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема.
- **tabular-parser** — читает файл из S3 `uploads/`, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) **по содержимому**,
пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Контракт результата —
`docs/contracts/recognition-result.schema.json` (контракт v1.0). Правила распознавания
пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Внутри — `BookParser` (оркестратор книги:
лимиты, чтение, дедлайн, вид каждого листа): листы ОСВ идут в конвейер `osv.py`, листы форм — в конвейер форм
`statements/` (блоки форм, колонки, шапка, строки, соотношения §7, склейка продолжений); документы результата — сначала
ОСВ, затем формы. Контракт результата —
`docs/contracts/recognition-result.schema.json` (контракт v1.2: `DISCLOSURE` в `document_type`, `statementDocument.checks`,
границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания
заголовков/группировок — `rules/*.yaml`.
- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (временный
`<имя>.<uuid>.tmp` рядом, затем атомарный rename; ИС игнорирует `*.tmp`), дождаться батча `Output/<job_id>/*.json`
@ -87,7 +91,7 @@
работе или обработанного, он не публикуется (его `Nats-Msg-Id` совпал бы с настоящим результатом). Нечитаемый запрос
(не JSON, нет `job_id`) остаётся без ответа: сервис пишет лог и отбрасывает его, ядро не получает ack, повторяет
публикацию `OUTBOX_MAX_REPUBLISH` раз и закрывает задание в `dlq` с `error_code = NO_ACK`. Новый MINOR раскатывается сначала на потребителей
(ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают
(ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR, например 1.2), потому что закрытые enum отвергают
незнакомые значения.
- Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв
контрагентов, проверки — в ядре.

View File

@ -86,6 +86,13 @@ r11 Договоры ←
"period_text":"1 квартал 2025 г.","data_type":"BU","groups":[{"type":"ACCOUNT"},{"type":"COUNTERPARTY","has_inn":true},
{"type":"CONTRACT"}]}` и → `document(document_type=OSV, osv_account_code, period_year, period_quarter, is_cumulative, locator)`.
**Шапка без меток и без строки организации (реализовано, PR tabular-parser #13, OQ-134; ОСВ 63 Новоросметалл).** Период
может стоять отдельной ячейкой («Бухгалтерский | 9 месяцев 2025 г.»: `periods.yaml` — `standalone`, `data_types.yaml` —
`standalone_header`), организация — хвостом названия ведомости, а колонки счёта — подписями «Счет: Код / Наименование»
(`osv_headers.yaml` — `column_captions`: «Код» и «Наименование» под группой «Счет» не группировки). Код счёта берётся из
первой строки-счёта таблицы, если «по счету …» в шапке нет, — с предупреждением «код счёта … из строки таблицы». Текст
строки может стоять правее колонки текста: уровень задаёт колонка, в которой он найден.
### 1.2 Колонки сумм
Заголовки сумм объединены (merged) и позиции колонок плавают между файлами даже одной организации. Определять по тексту
@ -99,8 +106,13 @@ r11 Договоры ←
Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5),
`row_value` (текст первой колонки), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат
только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; число из 9/11 цифр — ИНН региона 01–09
с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН внутри текста строки контрагента
(«Полиграф, 3528315710») парсер не извлекает: `counterparty_name` = весь текст строки, **[открыто]** OQ-100),
с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН контрагента — в порядке
приоритета: колонка ИНН (в том числе склеенная «ИНН/КПП»), затем строка без сумм **прямо над** названием на том же уровне
оформления (в `rows` не попадает, предупреждение «ИНН … над строкой … отнесён к ней»), затем конец текста названия
(«Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)» — 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»).
Из текста ИНН отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст; только для строк
`COUNTERPARTY`, договоры и прочее не трогаются; правила — `osv_headers.yaml` (`counterparty_inn_suffix`, `inn_only_row`,
`inn_kpp_cell`); **реализовано**, OQ-100, OQ-130 (tabular-parser #9, PR #15; синтетика `s17`–`s19`),
`data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки).
Строки «Вал.», «Кол.» и трёхбуквенный код валюты («USD») — не уровень группировки, а вид сумм родителя:
@ -159,7 +171,12 @@ r11 Договоры ←
значения родителя и выше не затронуты);
- родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом;
- вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16).
- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (**пустая ОСВ**: заголовок и
- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0); если корней уровня 0 несколько (лист без
строки корневого счёта: верхний уровень — субсчета 58.01, 58.03, OQ-133, PR #13) — «Итого» == **сумме строк уровня 0**
(показатели без суммы и сама «Итого» не входят; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя
выше, R-21); при одном корне сверка прежняя. Вторая итоговая строка сразу за «Итого» («Итого развернутое») — тоже `TOTAL`,
в сверку не входит; строка сумм без подписи в конце таблицы принимается за «Итого» с предупреждением.
Если корня нет (**пустая ОСВ**: заголовок и
сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ
штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не
найдена вовсе — сверка невозможна: `total_row_matches_root = false` и предупреждение «строка «Итого» не найдена»
@ -179,6 +196,15 @@ r11 Договоры ←
| 4 квартал, IV кв. | Q4 | **false** [открыто] |
| год, за 2025 год, 12 месяцев, годовая | Q4 | true |
**Период-диапазон месяцев** («Январь - Май 2025 г.», «Октябрь 2024 г. – Сентябрь 2025 г.»; `periods.yaml`: `months`, `range`;
OQ-131, PR #13 и 2b): парсер заполняет границы `period_start`/`period_end`/`is_from_year_start` (контракт v1.2; для квартала
и года: конец — последний день квартала, начало — 1 января у нарастающего и первый день квартала у квартального;
`is_from_year_start` — `period_start` = 1 января). Диапазон «январь – конец квартала» — нарастающий
(`is_cumulative = true`, квартал по месяцу конца), ровно один квартал — квартальный, остальное (скользящие 12 месяцев,
полгода с апреля, конец не на границе квартала) — `period_year` = год конца, `period_quarter = null`,
`is_cumulative = null`, границы по тексту и предупреждение «не с начала года» / «конец диапазона не совпадает с концом
квартала»: ядро такой документ к версии не привязывает (OQ-131).
`ASSUMPTION(OQ-02)`: парсер определяет `period_quarter` и `is_cumulative` **только по тексту периода**
(`rules/periods.yaml`, правила идут от специфичных к общим): квартальная ведомость Q2–Q4 считается не нарастающей;
сверку по данным (`Q3.opening == Q1.opening`) выполняет ядро и может переопределить признак. `period_year` — четыре
@ -215,8 +241,8 @@ result-osv-needs-check.json`.
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение`, стек — в лог ERROR |
Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по
`file_type` запроса; несовпадение — предупреждение. Листы форм ББ/ОФР (§12 п. 1) и листы без признаков ОСВ пропускаются с
предупреждением (`sheet=<имя> skipped: statement (slice 2b)` / `skipped: нет признаков ОСВ`). Другие предупреждения:
`file_type` запроса; несовпадение — предупреждение. Листы без признаков ОСВ и форм пропускаются с
предупреждением (`sheet=<имя> skipped: нет признаков ОСВ или формы`; листы форм теперь разбираются, §12). Другие предупреждения:
`sheet=<имя> hierarchy=<стратегия>` (всегда), свёрнутое сальдо, нераспознанное число (с номером строки), не найденное
поле шапки, «нет строк данных». xlsb не содержит outline level — каскад начинается с отступа (предупреждение
«xlsb: outline level недоступен»).
@ -420,6 +446,14 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
Межформенная: `1370(отчётная дата) − 1370(начало года) = 2400(ОФР за период)` — при расхождении КА может акцептовать
с комментарием (`financial_statement_meta.checks_accepted_*`).
**Парсер tabular-parser (§12 п. 7)** считает **жёсткими** (нарушение → `needs_check`) соотношения ББ — `1100`, `1200`
(+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700` — и ОФР — `2100`, `2200`,
`2300`; **мягкими** (предупреждение) — `2400 = 2300 − 2410 + 2420` и варианты ФСБУ 4/2023 (`2400 = 2300 + 2410 + 2460`,
`2410 = 2411 + 2412`), `2500 = 2400 + 2510 + 2520 + 2530` и «сумма пятизначных подстрок = родитель»
(`ASSUMPTION(OQ-12)`). Допуск ±1 в единицах формы, расходы — двойной расчёт знаков (`abs_terms`). Межформенная проверка
`1370 − 1370(начало года) = 2400` парсером не выполняется. Это сигнал о структуре и данных файла, а не замена проверок
ядра: ядро (Срез 3) пересчитывает §7 и ведёт акцепт (`financial_statement_meta.checks_accepted_*`).
Когда: после распознавания; после «Сохранить» правок; при «Подтвердить данные». Непройдено → `document.status =
NEEDS_CHECK`; пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`.
Отрицательные значения в формах — в скобках `(2100)` → `-2100`. Значения с `confidence < 100` подсвечиваются.
@ -800,30 +834,67 @@ OQ-92 (пустые «Организация»/«Группа компаний»
что и у `scan-gateway`, чтобы ядро маппило ББ/ОФР одинаково независимо от источника (`confidence = 100`,
`is_recognized = true` для табличного пути).
1. **Классификация листа**: ячейки с «Бухгалтерский баланс» / «0710001» → BALANCE; «Отчет о финансовых результатах» /
«Отчет о прибылях и убытках» / «0710002» → INCOME. На одном листе могут быть обе формы подряд (ББ, затем ОФР) —
разрезать по второму заголовку; в одном файле несколько листов — несколько `document`. Листы ОСВ и форм в одном
файле — допустимо.
2. **Колонка кодов строк**: та, где ≥ 60 % непустых значений — четырёхзначные коды из допустимого набора
(1110–1700 для ББ, 2110–2500 для ОФР, плюс 5xxx для расшифровок). Заголовок «Код» необязателен. Если колонки кодов
нет — попытаться сопоставить по наименованиям показателей (словарь `rules/statement_lines.yaml`: «Основные средства» →
1150) с пометкой `line_code_confidence < 100` и warning; агент-аналитик подтвердит.
3. **Колонка наименований**: ближайшая текстовая слева от кодов; «Пояснения» (1–2 символа/цифры) слева от неё — не
наименование.
4. **Колонки-периоды**: ячейки заголовка справа от кодов, матчащие дату («На 31 декабря 2024 г.», «31.12.2024»,
«2024», «За 9 месяцев 2025 г.», «За январь–сентябрь 2025 г.») → `financial_statement_column` с `period_date`
(для ОФР — дата конца периода) и `is_current` = самая поздняя. Число колонок — от 1 до 3+. Колонки без даты
(«Изменение», «%») — в `extra`.
5. **Значения**: Decimal по правилам §1.2; скобки → минус; «–»/«-»/пусто → NULL. Единица измерения — из шапки
(«тыс. руб.», «млн руб.», ОКЕИ 384/385) → `header.units`; если не найдена — по порядку величины относительно
ОСВ той же организации **не угадывать**, оставить NULL (ядро спросит).
6. **Шапка**: организация, ИНН, дата, ОКПО/ОКОПФ/ОКФС/ОКВЭД — регулярками по области выше таблицы; как в §1.0 п. 6.
7. **Самопроверка**: формулы §7 по каждой колонке; расхождение > 0 (в единицах формы) → `needs_check` с перечнем
непройденных соотношений в `metrics.warnings`. Это не замена проверок ядра, а сигнал, что структура угадана неверно
(например, перепутаны колонки периодов).
8. Расшифровки к строкам (лист `К балансу`, рекомендуемые формы банка) — в первой очереди **не разбирать**; классифицировать
как `DISCLOSURE`/`OTHER` и сохранить `locator`.
**Статус: реализовано (Срез 2b, tabular-parser feat/14, контракт v1.2; решения владельца 06.10.2026).** Модули —
`api/src/parser/statements/`, словари — `api/rules/statement_headers.yaml`, `statement_periods.yaml`,
`statement_checks.yaml`; ожидания по реальным файлам — `api/tests/statements_manifest.yaml` (06-test-data §4б).
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel; экран
«Проверка распознавания» для Excel-источника показывает слева не страницы, а рендер листа (или просто таблицу) —
UI-решение вне парсера.
1. **Классификация листов**: лист ОСВ → форма → продолжение → прочее. Форма: заголовок («Бухгалтерский баланс»,
«Отчет о финансовых результатах», «Отчет о прибылях и убытках») или ОКУД в **любой** строке до таблицы (у НПЗ он стоит
под титулом); титул «Бухгалтерская отчетность» — не тип, тип берёт ближайший следующий заголовок. Тип по ОКУД:
0710001 → `BALANCE`, 0710002 → `INCOME`, прочие 0710xxx (0710003 капитал, 0710004 ДДС) и пояснения («Пояснения к
бухгалтерскому балансу», «Расшифровка») → `DISCLOSURE`; при противоречии ОКУД и заголовка — тип по ОКУД и
предупреждение. Две формы на одном листе — по второму заголовку, в книге несколько листов — несколько `document`.
Продолжение: лист без заголовка формы, но с маркером «Форма … с.2», либо с подписью «Код» и периодами правее, либо с
колонкой кодов. ОСВ и формы в одной книге допустимы.
2. **Колонка кодов**: в блоке ≥ 60 % непустых значений — коды (ББ 1100–1799, ОФР 2100–2999, расшифровки 5xxx,
пятизначные — по первым четырём цифрам), кодов не меньше `min_codes` = 3; заголовок «Код/Коды» необязателен, числа вида
`1110.0` читаются как текст «1110». Колонки нет — документ без строк, `needs_check`, предупреждение «колонка кодов не
найдена»; сопоставление по наименованиям строк **не делается** (решение владельца, OQ-139).
3. **Колонка наименований** — ближайшая левее кодов с преобладанием текста; «Пояснения» (пустые или ≤ 3 символов) не
наименование.
4. **Колонки-периоды**: ячейки заголовка правее кодов с распознанным периодом («На 31 декабря 2024 г.», «31.12.2024»,
«За 9 месяцев 2025 г.», «За январь - сентябрь 2025 г.», «2024»), по порядку слева направо (`column_index` 0, 1, 2…);
`period_date` — дата или конец периода; `is_current` в контракте нет — ядро берёт самую позднюю. Заголовок в две строки
(«На» + «20__ г.», перенос внутри ячейки) склеивается; строка нумерации «1 2 3 4 5» пропускается. Колонки без периода
(«Изменение», «%») **не читаются** (одно предупреждение на документ): у `statementDocument` поля `extra` нет.
5. **Значения**: `Decimal`, скобки → минус, «–»/пусто → `null`, нечисловой текст → `string_value`,
`is_recognized = false`. **Единицы не нормализуются**: `amount` как в ячейке, `header.units` — текст шапки; при «тыс.» и
≥ 10 % значений с дробной частью — предупреждение «значений с дробной частью при единице тыс. руб.: N из M» (OQ-138,
решение владельца). Строки без кода (разделы, «в том числе:», «Реализация иного имущества…» у НПЗ) не отдаются
(`ASSUMPTION(OQ-36)`, OQ-141); повтор кода — остаётся первая строка, код в `checks.duplicate_line_codes`, `needs_check`.
Пятизначные подстроки (11101, 12301…) отдаются как обычные строки.
6. **Шапка**: организация (после метки «Организация», иначе текст с признаком ОПФ строкой выше метки — НПЗ), ИНН 10/12
цифр у метки «ИНН» (**ИНН аудитора не берётся**: строки после метки «аудитор» исключены), ОКПО, ОКОПФ/ОКФС, ОКЕИ, адрес,
единицы, `report_date_text`/`report_date` — из подзаголовка формы или тройки «число | месяц | год» у метки «Дата» (для
ОФР — конец периода). Шапка другого листа не используется: у ОФР НПЗ нет метки «ИНН» → `inn = null`. Найденное поле —
`*_confidence = 100`, не найденное — `null`.
7. **Самопроверка**: соотношения §7 по каждой колонке — данные `statement_checks.yaml`. **Жёсткие** (нарушение →
`needs_check`): ББ `1100`, `1200` (+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`,
`1600 = 1700`; ОФР `2100`, `2200`, `2300`. **Мягкие** (только предупреждение, `ASSUMPTION(OQ-12)`): `2400 = 2300 −
2410 + 2420` и варианты ФСБУ 4/2023, `2500`, «сумма пятизначных подстрок = родитель». Допуск ±1 **в единицах формы**,
отсутствующий код = 0, соотношение проверяется, только если присутствует его цель; правая часть считается дважды — как
записано и с членами `abs_terms` по модулю (1С выгружает расходы со знаком минус, 66н — в скобках). Результат —
`documents[].checks` (`relations_failed`, `soft_relations_failed`, `duplicate_line_codes`) и предупреждения.
**Фактически сходятся полные формы только у Межениновской**: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`: `1600 = 1700`
расходится на 1.94 / 9.02 / 2.48 руб., потому что значения в рублях с копейками при шапке «тыс. руб.»; допуск ±1 остаётся
(решение владельца 06.10.2026, вариант А, OQ-138). ОФР НПЗ и ОФР Унагранде (стр. 1) тоже `needs_check` по 2100/2200/2300
по той же причине; ББ Унагранде (стр. 1) — только актив, `1600 = 1700` не сходится. Это сигнал о данных файла, а не о
неверно угаданной структуре; допуск под файл не подгоняется.
8. **Расшифровки** (0710003/0710004, пояснения, лист «Примечания» шаблона) — `DISCLOSURE` с `locator` и шапкой, если
нашлась, без `columns`/`lines`; на статус не влияет (OQ-23). Книга только из таких листов — `done`.
9. **Склейка**: листы-продолжения внутри книги присоединяются к последнему открытому документу формы, если число колонок
сумм совпадает; строки добавляются в конец, `locator` перечисляет листы через запятую. **Страницы в разных файлах не
склеиваются** (1С печатает форму постранично, один файл = одна страница): страница 2 без первой — документ с пустой шапкой
и `needs_check`, предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, OQ-140).
Продолжение без колонки кодов при непустом результате не теряется молча — `needs_check`.
10. **Статус**: документ `ok` ⇔ жёсткие соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов
∧ есть колонка-период ∧ в шапке есть организация или ИНН ∧ документ не одинокое продолжение. Книга — `done`, если `ok`
у всех документов, иначе `needs_check`; книга без листов ОСВ и форм — `INVALID_DOCUMENT`. Порядок документов: сначала
ОСВ в порядке листов, затем формы. Шаблоны (коды есть, сумм нет, шапки нет) — `needs_check`.
11. **Отложено**: запасной путь по именам строк (OQ-139), нормализация единиц (OQ-138), признак продолжения в контракте и
склейка страниц из разных файлов в ядре (OQ-140, Срез 3), строки без кода (OQ-141), образцы ФСБУ (OQ-142), лист
продолжения с единственной кодовой строкой (OQ-143 реализовано: после листа формы хватает одного кода,
`min_codes_continuation` = 1; первая страница по-прежнему требует `min_codes` = 3).
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel (маппинг форм —
Срез 3); экран «Проверка распознавания» для Excel-источника показывает не страницы, а таблицу — UI-решение вне парсера.

View File

@ -139,7 +139,7 @@ JSON `aep_*.json` — реальные ответы Content AI: структур
|---|---|---|
| ОСВ шести организаций (29 файлов, 34 МБ) | `osv-dkb/<org>/<период>/` | АО «Цимлянский» (7 ОСВ за 9 мес. 2025), ООО «Новороссийский Кислородный завод» (2 книги по 3 листа), ООО «Новоросметалл» (2 книги по 3 листа + ОСВ 63; вариант 2, «сумма»), ООО «Новороссийский прокатный завод» (2 книги; одна за окт. 2024 – сент. 2025), ООО «Приманыческий» (7 ОСВ), АО фирма «Агрокомплекс» им. Ткачева (4 ОСВ по 50 тыс. строк, 9–14 МБ) |
| «Анализ счета» (3 книги) | `analysis-reports/<org>/2025-Q3/` | отчёт 1С по корреспонденции счетов с отбором по контрагентам группы; не ОСВ, парсер отвечает `INVALID_DOCUMENT` |
| ББ/ОФР в Excel (15 файлов) | `statements-xlsx/<org>/<период>/` | АО «Унагранде Ко» и «Унагранде Групп» (постраничные xls из 1С, 2024 г. и 9 мес. 2025), ООО НПЗ (одна книга ФСБУ 4/2023 с подстроками «в том числе»), ООО «Агрофирма Межениновская» (TDSheet, 66н); `templates/` — пустые формы 66н и ФСБУ 4/2023 |
| ББ/ОФР в Excel (18 файлов: 14 форм и 4 шаблона) | `statements-xlsx/<org>/<период>/` | АО «Унагранде Ко» и «Унагранде Групп» (постраничные xls из 1С, 2024 г. и 9 мес. 2025), ООО НПЗ (одна книга ФСБУ 4/2023 с подстроками «в том числе»), ООО «Агрофирма Межениновская» (TDSheet, 66н); `templates/` — пустые формы 66н и ФСБУ 4/2023 |
| План счетов 1С заказчика | `directories/chart-of-accounts-1c.xlsx` | 488 строк: код, наименование, субконто 1–3, вид; в seed 122 кода, все есть в выгрузке; 366 кодов выгрузки в seed нет (OQ-113, arm-core#23) |
`osv-dkb/` намеренно вне `osv/`: fixtures-тесты парсера и ядра требуют один лист `Лист_1`, `done`, период = каталогу и
@ -149,6 +149,41 @@ golden-снимок на каждый файл, а здесь многолист
«Итого развернутое», листы с именем кода счёта, нет валюты в шапке, объём) — `fixtures/osv-dkb/README.md`,
arm-tabular-parser#11, OQ-131…OQ-135. ИНН организаций в шапках ОСВ нет (OQ-136).
## 4б. Формы ББ/ОФР в Excel: манифест, golden, синтетика (Срез 2b)
**Раскладки реальных файлов** (`fixtures/statements-xlsx/`, README там же; 18 файлов: 14 форм и 4 шаблона):
| Раскладка | Где | Особенности |
|---|---|---|
| 1С-xls постранично | `unagrande-co/2024-FY/`, `unagrande-group/{2024-FY,2025-Q3}/` | один файл = одна страница формы; на странице 1 титул (организация, ИНН, ОКПО, единица «в тыс. рублей», ОКЕИ, аудитор со **своим** ИНН), на странице 2 только «Форма 0710001 с.2»; пятизначные подстроки (11101…), значения в рублях с копейками при «тыс. руб.»; страниц 1 ББ в пакете нет (только актив) и ОФР Групп 2024 — только стр. 2 |
| Многолистовая xlsx ФСБУ 4/2023 | `npz/2025-Q3/` | ББ на 5 листах, ОФР на 3 (третий — одна строка 2400, присоединяется как продолжение); заголовок формы после титула; листы 2–5 без заголовка и маркера; строка 1105 «Гудвил», подстроки 12301…, строка без кода с суммами; лист ОФР с одной строкой 2400 (OQ-143) |
| `TDSheet` 66н | `mezheninovskaya/2025-Q3/` | вся форма на одном листе, дата тройкой «30 \| 9 \| 2025» числами, двухстрочные заголовки, строка нумерации колонок, значения — тысячи с дробной частью |
| Шаблоны | `templates/` | 66н и ФСБУ 4/2023: коды есть, сумм и шапки нет; заголовок на две строки; в шаблоне ФСБУ «АКТИВ» и код 1110 в одной строке (OQ-142) |
**Манифест** `tabular-parser/api/tests/statements_manifest.yaml`: для каждого из 18 файлов — организация, ИНН, документы по
порядку (`type`, `status`, `complete`, причина `needs_check`, `failed_relations`). Статусы сверены прогоном: `done` только
у Межениновской (ББ и ОФР); остальные — `needs_check` по данным файла (рубли с копейками при шапке «тыс. руб.», только
актив, страница 2 без шапки, шаблон без сумм), НПЗ — `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 руб. (OQ-138).
Таблица «файл → статус» — README `tabular-parser`, «Парсер форм».
**Golden** `fixtures/golden/expected_statement_parse/<тот же путь без расширения>.json` — 24 снимка (18 реальных файлов +
6 синтетических); регрессия, не оракул; обновление — `make golden-update` (один для ОСВ и форм), просмотр diff, отдельный
коммит в `fixtures`. Оракулы корректности — манифест и property-тест `tests/fixtures/test_statements_property.py` (манифест покрывает
каждый файл; тип, статус и видимая причина `needs_check` каждого документа совпадают с ожиданиями).
**Синтетика** `fixtures/statements-xlsx/synthetic/st01…st07` (генератор `tabular-parser/api/tests/synthetic/generate_statements.py`,
ожидания — `tests/synthetic/test_statement_synthetic_files.py`, описание — README каталога):
| Файл | Что проверяет |
|---|---|
| `st01_both_forms_one_sheet` | ББ и ОФР на одном листе, два документа с одним `locator` (§12 п. 1) |
| `st02_csv_like` | csv-подобный лист: двухстрочный заголовок, коды без подписи «Код», отрицательное значение, единиц нет |
| `st03_no_codes` | форма без колонки кодов → `needs_check`, «колонка кодов не найдена» (OQ-139) |
| `st04_mixed_book` | книга: лист ОСВ + лист баланса, порядок документов ОСВ → форма |
| `st05_continuation_only` | одинокая страница 2 → `needs_check`, предупреждение о продолжении (OQ-140) |
| `st07_single_code_continuation` | ОФР без 2400 на первом листе и лист-продолжение с единственной кодовой строкой 2400 присоединяется (OQ-143) |
| `st06_header_variants` | варианты, которых нет в реальных файлах: старое название ОФР, расшифровки/ОДДС/ОИК, форма только с ОКУД 0710xxx, организация в «», «Адрес», «Пояснения», числовая дата (покрытие словарей `statement_*.yaml`) |
## 5. Прочее
- `Форма расшифровки к отчетности 01 10 24 / 01 04 25 / 01 07 25.xlsx` — расшифровки АО РБ (кредиты и займы, дебиторы и
@ -169,7 +204,7 @@ fixtures/
osv/
rb/ 2024-Q1/ 2024-Q2/ 2024-Q3/ 2024-FY/ 2025-Q1/ 2025-Q2/ 2025-Q3/ # файлы с исходными именами
schastl/ ...
synthetic/ README.md + s01…s16 (генератор в tabular-parser, §7)
synthetic/ README.md + s01…s19 (генератор в tabular-parser, §7)
statements/
rb/ schastl/ gir-bo/ # pdf
statements-xlsx/<org>/<период>/ # ББ/ОФР в Excel (§12, §4а): unagrande-co, unagrande-group, npz,
@ -185,7 +220,8 @@ fixtures/
# буквально)
golden/
Консолидация_Группа РБ.xlsx # эталон КФО по датам
expected_osv_parse/<зеркало osv/ без расширения>.json # golden-снимки парсера ОСВ: 88 файлов (72 реальных + 16 синтетических)
expected_osv_parse/<зеркало osv/ без расширения>.json # golden-снимки парсера ОСВ: 113 файлов (94 реальных: 72 в osv/ + 22 в osv-dkb/; + 19 синтетических)
expected_statement_parse/<зеркало statements-xlsx/ без расширения>.json # golden-снимки парсера форм ББ/ОФР: 24 файла (18 реальных + 6 синтетических), §4б
directories/
ecp-export-russkiy-biskvit.xlsx # срез выгрузки ЕЦП CRM по ГК «Русский Бисквит» (Срез 1)
chart-of-accounts-1c.xlsx # план счетов 1С заказчика, 488 строк (пакет ДКБ, OQ-113)
@ -224,8 +260,8 @@ fixtures/
## 7. Синтетика, golden-снимки и xlsb парсера ОСВ (Срез 2a)
Реальные файлы покрывают один вариант шапки и один формат файла (xls/xlsx из 1С 8.3). Остальное — синтетика
`fixtures/osv/synthetic/` (вымышленные организации и ИНН; суммы согласованы арифметически, поэтому все файлы, кроме
`s10`, дают `done`). Файлы собирает детерминированный генератор `tabular-parser/api/tests/synthetic/generate.py`
`fixtures/osv/synthetic/` (вымышленные организации и ИНН; суммы согласованы арифметически, поэтому листы ОСВ, кроме
`s10`, дают `done`; книги `s12`/`s14` с формами — `needs_check` по формам). Файлы собирает детерминированный генератор `tabular-parser/api/tests/synthetic/generate.py`
(повторный запуск даёт те же байты), ожидания по каждому файлу — в `tests/synthetic/test_synthetic_files.py`, описание —
в `fixtures/osv/synthetic/README.md`.
@ -242,13 +278,14 @@ fixtures/
| `s09_subaccount_names` | названия субсчетов в строке счёта, буквенный субсчёт |
| `s10_broken_sum` | испорченная сумма → `needs_check` с номерами строк |
| `s11_header_repeat` | заголовок таблицы повторяется внутри данных (печать по страницам) |
| `s12_statement_and_osv` | лист ББ и лист ОСВ в одной книге: форма пропускается с предупреждением |
| `s12_statement_and_osv` | лист ББ и лист ОСВ в одной книге: с Среза 2b — два документа (ОСВ, затем ББ без колонки кодов → `needs_check`) |
| `s13_no_outline_indent` (`.xlsb`) | та же ведомость, что `s02`, в xlsb |
| `s14_statement_forms` | три формы ББ/ОФР, каждая опознаётся своим признаком |
| `s14_statement_forms` | три формы ББ/ОФР, каждая опознаётся своим признаком (с 2b — документы `BALANCE`/`INCOME` без колонки кодов, `needs_check`) |
| `s15_group_title_synonyms` | названия групп сумм «Входящее/Начальное/Конечное/Исходящее сальдо» |
| `s16_header_synonyms` | шапка без организации, период «2024», группировки `OTHER`, «Детализация: …» |
`s14`–`s16` добавлены тестом покрытия словарей: каждая запись `rules/*.yaml` обязана сработать на реальных или
`s17`–`s19` — извлечение ИНН (из текста названия, из строки над названием, «ИНН/КПП» в одной ячейке; OQ-100, OQ-130).
Формы ББ/ОФР из Excel — подраздел 4б. `s14`–`s16` добавлены тестом покрытия словарей: каждая запись `rules/*.yaml` обязана сработать на реальных или
синтетических файлах.
**xlsb рукописный.** `s13` собран **не Excel и не LibreOffice** (их нет на машине разработки), а минимальным писателем
@ -257,7 +294,7 @@ BIFF12 `tabular-parser/api/tests/synthetic/xlsb_writer.py`: он пишет то
файл из Excel (OQ-102). Инструкция замены — в README `fixtures/osv/synthetic/`.
**Golden-снимки.** Для каждого файла `fixtures/osv/**` (включая синтетику) в `fixtures/golden/expected_osv_parse/` лежит
результат разбора (88 файлов = 72 реальных + 16 синтетических): файл результата контракта v1.1 без `job_id`, `file_id`,
результат разбора (113 файлов: 94 реальных и 19 синтетических `s01`–`s19`): файл результата контракта v1.2 (v1.1 — до Среза 2b) без `job_id`, `file_id`,
`service.version`, `metrics.duration_ms`. Снимок — **регрессия, не оракул**: он фиксирует, что парсер выдаёт сейчас.
Обновление — только `make golden-update` в `tabular-parser` с просмотром diff и отдельным коммитом в `fixtures`;
`make golden-check` (код возврата 1 при расхождении) — без записи. Оракулы корректности — property-тест

View File

@ -105,7 +105,7 @@
| OQ-97 | Право `DIRECTORY_DELETE` по матрице §10 только у ADMIN: удалять алиасы и пустые ГСК/ГСЗ могут только администраторы, а не КА. Расширить право на KA_RF/KA_GO/CURATOR_GO? | §10; arm-core O-6 | срез 1 (seed) | **закрыт решением владельца 05.10.2026: право `DIRECTORY_DELETE` на КА не расширяется**, удаляют алиасы и пустые ГСК/ГСЗ только ADMIN; матрица §10 без изменений |
| OQ-98 | Единый список кодов валют для строк `data_type = CURRENCY` («USD», «EUR» под контрагентом): достаточно ли ISO 4217-списка из `rules/data_types.yaml` (`currency_codes`: RUB, USD, EUR, CNY, KZT, BYN, GBP, CHF, JPY, TRY, AED)? Нужны ли нестандартные написания и коды, которые используют конфигурации 1С | tabular-parser: design D-4, R-6, R-22 | срез 2c (маппинг CURRENCY-строк), словарь `data_types.yaml` | допущение принято: ISO-перечень, дополняется строкой в YAML вместе с синтетикой; коды не входят в тест покрытия словарей (R-22) |
| OQ-99 | Несколько ОСВ **одного счёта** в одной книге (разные листы: два периода или две организации): парсер выдаёт по одному `document` на лист (`locator = sheet:<имя>`) и ничего не дедуплицирует. Как ядро выбирает/сопоставляет такие документы с периодом и организацией? | tabular-parser: spec `osv-parsing` («Листы ОСВ находятся по содержимому») | срез 2c (маппинг, `document.period_check`) | допущение принято: каждый лист — отдельный документ, выбор — за ядром и аналитиком при проверке |
| OQ-100 | ИНН внутри **текста** строки контрагента («Полиграф, 3528315710», 20 строк в ОСВ 76 Счастливочной) и склеенная ячейка «ИНН/КПП»: извлекать ли ИНН в парсере или в ядре при резолве контрагента? Сейчас `counterparty_inn` берётся только из колонки ИНН (§1.3), `counterparty_name` = весь текст строки | tabular-parser: R-15, R-17 (design №25, №27) | срез 2c (резолв `counterparty_id` по ИНН/имени) | **решено 07.10.2026 (владелец):** извлекать в парсере (ИНН → `counterparty_inn`, имя без ИНН) — RSHB/arm-tabular-parser#9; до реализации — как раньше |
| OQ-100 | ИНН внутри **текста** строки контрагента («Полиграф, 3528315710», 20 строк в ОСВ 76 Счастливочной) и склеенная ячейка «ИНН/КПП»: извлекать ли ИНН в парсере или в ядре при резолве контрагента? Сейчас `counterparty_inn` берётся только из колонки ИНН (§1.3), `counterparty_name` = весь текст строки | tabular-parser: R-15, R-17 (design №25, №27) | срез 2c (резолв `counterparty_id` по ИНН/имени) | **реализовано** (решение владельца 07.10.2026, RSHB/arm-tabular-parser#9, PR #15, слит с #13): ИНН из конца текста названия контрагента → `counterparty_inn`, `counterparty_name` без ИНН (`osv_headers.yaml`: `counterparty_inn_suffix`); тесты — `tests/unit/parser/test_classify.py` (`test_inn_is_extracted_from_counterparty_name`), синтетика `s17_inn_in_name`; описание — `05-domain-rules.md` §1.3 |
| OQ-101 | Числа с ведущим «+», «.5», «5.» парсер отвергает как нечисловой текст (`null` + предупреждение с номером строки). Встречаются ли такие записи в выгрузках других систем; принимать ли их | tabular-parser: T7.3 | срез 2a+, словарь/правила `numbers.py` | допущение принято: отвергаются; в реальных файлах не встречаются |
| 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 реальных файлах не встречаются |
@ -118,7 +118,7 @@
| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | **закрыт для Среза 4 (06.10.2026):** привязываются к версии только `CONFIRMED` документы незаменённых файлов, а файл с `CONFIRMED` документом удалить нельзя (`FILE_HAS_CONFIRMED_DOCUMENTS`) — удаление файла с документом в версии невозможно, расширять `DELETE /files/{id}` не нужно; автозамена снимает привязки документов заменённого файла (`FILE_REPLACE.unbound_version_ids`). Остаток: FK из `financial_statement_meta` и `ex_snapshot_item` — Срезы 3 и 7 |
| OQ-111 | Ретеншн S3-объектов при удалении файла: сейчас `DELETE` лишь ставит `s3_object.expires_at = now()`, объекты `uploads`/`results` физически не стираются (дополняет OQ-22). Кто и когда их удаляет — lifecycle-правило бакета по метаданным, фоновая задача ядра, ручная очистка? Объект, записанный в S3 до сбоя БД, остаётся сиротой | arm-core Срез 2c: design D-10, №36 | платформа (lifecycle), Срез 2 → эксплуатация | допущение принято: бакет растёт; очистка — вне среза |
| OQ-112 | Остаточные объекты e2e-прогона: каждый запуск `make e2e` оставляет в MinIO dev-стенда ~2 объекта (`uploads/…`, `results/…` — следствие OQ-111) | arm-core Срез 2c: Task 11 (O-5) | dev-стенд | допущение принято: чистить бакет `arm-ka` вручную (раздел «Сброс стенда» `dev/qa-guide.md`) |
| OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | **частично решён 06.10.2026:** план счетов 1С заказчика получен (`fixtures/directories/chart-of-accounts-1c.xlsx`, 488 строк, все 122 кода seed в нём есть); перенос в seed — arm-core#23; подтверждение владельца, что это полный план — ждём |
| OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | **закрыт: допущение принято — выгрузка плана счетов 1С заказчика (`fixtures/directories/chart-of-accounts-1c.xlsx`, 488 строк, все 122 кода seed в ней есть) полная (arm-core #25);** ранее: частично решён 06.10.2026, перенос в seed — arm-core#23 |
| OQ-114 | `EntryOut` (API строк ОСВ) не отдаёт `is_modified`/`original_*`: панели правки подсреза 2d нужен признак правки аналитика (инвариант «первичка не мутируется»: правка — рядом, с аудитом). Добавить поля в схему и контракт API при проектировании 2d | arm-core Срез 2c: Task 9 (T9.7) | подсрез 2d | открыт: в 2c строк не правят, `is_modified = false` у всех |
| OQ-115 | Автозамена: если новый файл содержит лишь часть документов прежнего (прежний — ОСВ 60 и 51, новый — только 60), прежний уходит в `SUPERSEDED` **целиком**, документ 51 пропадает из списка файлов; в `FILE_REPLACE.description` о нём ничего нет. Подтвердить «замена файлом целиком» (OQ-06) для этого случая или заменять по документам | arm-core Срез 2c: Task 8 (T8.4) | Срез 2d (список файлов), Срез 4 (комплект документов) | допущение принято: замена файлом целиком, документы заменённого файла сохраняются в БД |
| OQ-116 | ОСВ Q2–Q4, признанная нарастающей по данным Q1 (сальдо на начало корня равно сальдо Q1, §1.5), получает предупреждение маппинга и поэтому остаётся `NEEDS_CHECK`, а не `RECOGNIZED` (буквально D-7/D-9 спеки: `RECOGNIZED` только без предупреждений). Верно ли, что аналитик подтверждает такие документы вручную, или нарастающая по данным — штатный случай без предупреждения (OQ-02, OQ-05) | arm-core Срез 2c: Task 7 (R-17, design №30) | Срез 2d (нагрузка на аналитика), §9.3 | допущение принято: `NEEDS_CHECK`; снять предупреждение — одна правка в `result_mapping.py` и правило статуса |
@ -135,14 +135,20 @@
| OQ-127 | Рекомендация кандидата (`recommended`) — только точное совпадение `normalize_alias` имени строки с нормализованным именем/алиасом ЮЛ. На реальных данных «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» (так записан контрагент в ОСВ РБ) рекомендации не даёт: аналитик находит ЮЛ поиском по `q`. Нужна ли нечёткая рекомендация (токены, порядок слов, префикс «Юг-Финанс /») или достаточно ручного выбора и накопления алиасов | arm-core Срез 4: Task 13 (O-3), design D-11 | `GET …/counterparty-candidates` | допущение принято: рекомендация точная; после первого ручного сопоставления алиас закрепляет имя |
| OQ-128 | Неоднозначность дорезолва на лету при тёзке по имени: после ручного сопоставления создаётся алиас, но `CounterpartyIndex.resolve` на шаге «нормализованное имя/алиас» возвращает контрагента только при единственном кандидате — если ЮЛ-тёзка (по нормализованному `name`) есть рядом с владельцем нового алиаса, кандидатов двое, резолв неоднозначен и строки без сохранённого `counterparty_id` остаются несопоставленными. ТЗ DOC-221278: «алиас однозначен» — решает ли это, или алиас должен иметь приоритет над именем | arm-core Срез 4: Task 11 (T11.9) | дорезолв свода (D-3), `CounterpartyIndex` | открыт: допущение — резолв по Срезу 2c (неоднозначность → `NULL`); спасает автоприменение `PROPAGATED` (сохраняет `counterparty_id`) |
| OQ-129 | Формулировка сценария «сопоставление и автоприменение» (spec consolidated-osv): «`unmapped_total` падает на 1 + propagated» неверна на реальных данных. `propagated` считает записи `osv_entry` (включая пары-пробелы и строки, не дающие строк свода), а свод склеивает кварталы, поэтому `unmapped_total` падает на число **склеенных** строк свода (R-15, design №80). Уточнить текст spec при архивации | arm-core Срез 4: Task 13 (T13.M1, R-15) | spec `consolidated-osv` при архивации change | допущение принято: тест сверяет обе величины отдельно; текст spec уточнён в change, `openspec/specs/` — при архивации |
| OQ-130 | ОСВ произвольного формата: ИНН в ячейке **на строку выше** названия контрагента (строка только с ИНН, без сумм). Сейчас такая строка уходит в OTHER, ИНН для резолва теряется. Решение владельца 07.10.2026: склеивать со следующей строкой контрагента; синтетические фикстуры на этот случай, на ИНН в тексте и на «ИНН/КПП» в одной ячейке | владелец, 07.10.2026 | tabular-parser §1.0 п. 4, §1.3; резолв контрагента в ядре | **решено:** RSHB/arm-tabular-parser#9 (bounded), ждёт реализации |
| OQ-131 | Скользящий период ОСВ «Октябрь 2024 г. – Сентябрь 2025 г.» (НПЗ 60/62/76, Агрокомплекс 76, `osv-dkb/`): парсер берёт первый месяц → `2024 Q4`, нарастающий. Нужен разбор диапазона дат и признак «не с начала года»; как ядру относиться к такому документу при привязке к версии (не нарастающий и не квартальный) | пакет ДКБ 06.10.2026, прогон парсера | tabular-parser `periods.yaml`/`header.py`; ядро `fits` (D-5 Среза 4) | открыт: arm-tabular-parser#11 п. 1; допущение — не привязывать, показывать в чек-листе как «период не соответствует» |
| OQ-130 | ОСВ произвольного формата: ИНН в ячейке **на строку выше** названия контрагента (строка только с ИНН, без сумм). Сейчас такая строка уходит в OTHER, ИНН для резолва теряется. Решение владельца 07.10.2026: склеивать со следующей строкой контрагента; синтетические фикстуры на этот случай, на ИНН в тексте и на «ИНН/КПП» в одной ячейке | владелец, 07.10.2026 | tabular-parser §1.0 п. 4, §1.3; резолв контрагента в ядре | **реализовано** (RSHB/arm-tabular-parser#9, PR #15): строка только с ИНН над названием склеивается со следующей строкой контрагента (`inn_only_row`, предупреждение «ИНН … над строкой … отнесён к ней»), «ИНН/КПП» в одной ячейке (`inn_kpp_cell`); тесты — `test_classify.py` (`test_inn_only_row_above_is_glued_to_next_counterparty`, `test_inn_with_kpp_in_one_column_cell_gives_inn`), синтетика `s18_inn_row_above`, `s19_inn_kpp_cell`; `05-domain-rules.md` §1.3 |
| OQ-131 | Скользящий период ОСВ «Октябрь 2024 г. – Сентябрь 2025 г.» (НПЗ 60/62/76, Агрокомплекс 76, `osv-dkb/`): парсер берёт первый месяц → `2024 Q4`, нарастающий. Нужен разбор диапазона дат и признак «не с начала года»; как ядру относиться к такому документу при привязке к версии (не нарастающий и не квартальный) | пакет ДКБ 06.10.2026, прогон парсера | tabular-parser `periods.yaml`/`header.py`; ядро `fits` (D-5 Среза 4) | **реализовано в парсере** (tabular-parser PR #13 + Срез 2b): диапазон месяцев разбирается (`periods.yaml`: `months`, `range`), границы `period_start`/`period_end`/`is_from_year_start` — в контракте v1.2 (docs change 2b), скользящий период → `period_quarter = null`, `is_cumulative = null` и предупреждение «не с начала года» (`05-domain-rules.md` §1.5). **Открыто для ядра:** привязка скользящего документа к версии (допущение — не привязывать, показывать в чек-листе как «период не соответствует») |
| OQ-132 | Тождество сальдо по строкам (§1.4) на счёте 91 (и, вероятно, 90, 99): строки аналитики доходов/расходов несут только обороты, сальдо закрывается на 91.09 → `needs_check` на каждой ОСВ 91 (Цимлянский, Приманыческий). Исключать ли счета без сальдо по аналитике из проверки тождества строк | пакет ДКБ 06.10.2026 | tabular-parser `checks.py`, статус документа | открыт: допущение — оставить `needs_check`, аналитик подтверждает вручную; arm-tabular-parser#11 п. 2 |
| OQ-133 | ОСВ варианта 2 без строки корневого счёта: верхний уровень — субсчета (58.01, 58.03), «Итого» = их сумме, парсер сверяет «Итого» с первым корнем → `needs_check` на каждом листе (Новоросметалл, Агрокомплекс; уточняет OQ-106). Сверять «Итого» с суммой строк уровня 0 | пакет ДКБ 06.10.2026 | tabular-parser `checks.py` | открыт: arm-tabular-parser#11 п. 3 |
| OQ-134 | Шапка ОСВ без строки организации и с кодом/названием счёта в двух колонках («Счет: Код / Наименование», строка «Бухгалтерский \| 9 месяцев 2025 г.»; Новоросметалл ОСВ 63): парсер не находит организацию, счёт, период, тип данных. Новый вариант заголовка — словарь или код | пакет ДКБ 06.10.2026 | tabular-parser `detect.py`/`header.py`, `osv_headers.yaml` | открыт: arm-tabular-parser#11 п. 4 |
| OQ-133 | ОСВ варианта 2 без строки корневого счёта: верхний уровень — субсчета (58.01, 58.03), «Итого» = их сумме, парсер сверяет «Итого» с первым корнем → `needs_check` на каждом листе (Новоросметалл, Агрокомплекс; уточняет OQ-106). Сверять «Итого» с суммой строк уровня 0 | пакет ДКБ 06.10.2026 | tabular-parser `checks.py` | **реализовано** (tabular-parser PR #13, arm-tabular-parser#11 п. 3): при нескольких корнях уровня 0 «Итого» сверяется с суммой строк уровня 0 (сальдо по колонкам либо по нетто, как у родителя, R-21); `checks.py`, `05-domain-rules.md` §1.4 |
| OQ-134 | Шапка ОСВ без строки организации и с кодом/названием счёта в двух колонках («Счет: Код / Наименование», строка «Бухгалтерский \| 9 месяцев 2025 г.»; Новоросметалл ОСВ 63): парсер не находит организацию, счёт, период, тип данных. Новый вариант заголовка — словарь или код | пакет ДКБ 06.10.2026 | tabular-parser `detect.py`/`header.py`, `osv_headers.yaml` | **реализовано** (tabular-parser PR #13, arm-tabular-parser#11 п. 4): шапка без метки организации (организация — хвост названия ведомости), период и тип данных отдельными ячейками (`periods.yaml: standalone`, `data_types.yaml: standalone_header`), подписи «Код»/«Наименование» не группировки (`osv_headers.yaml: column_captions`), код счёта — из первой строки-счёта (с предупреждением); `05-domain-rules.md` §1.1; тест `test_header_without_labels_and_code_name_columns_are_found` |
| OQ-135 | Отчёт 1С «Анализ счета» с отбором по контрагентам группы (`fixtures/analysis-reports/`, в именах файлов «ОСВ_…ПоКомпаниямГруппы»): не ОСВ, но содержит ВГО по корреспонденции счетов. Нужен ли отдельный тип документа для Среза 5, или ВГО считаются только из ОСВ | пакет ДКБ 06.10.2026 | `document_type`, Срез 5 | открыт: допущение — не разбирать; парсер отвечает `INVALID_DOCUMENT` |
| OQ-136 | ИНН организаций пакета ДКБ (Цимлянский, НКЗ, Новоросметалл, Приманыческий, Агрокомплекс) в шапках ОСВ отсутствуют; для теста консолидации по этим группам нужны ИНН и состав ГК/ГСК (выгрузка ЕЦП или список от ДКБ) | пакет ДКБ 06.10.2026 | seed/фикстуры справочников, тесты Среза 4–5 на новых группах | открыт |
| OQ-137 | Раскладка горячих папок ИС Сканирование: scan-gateway кладёт `Input/<job_id>_<original_name>` через временный `Tmp/.<имя>.part`, ничего не удаляет; PDFWorker старого АРМ (`RSHB/armka_file_worker`, проверен в контуре) кладёт `Input/<task_id>.<ext>` через временный `<назначение>.<uuid>.tmp` рядом (ИС игнорирует `*.tmp`), читает батч `Output/<task_id>/*.json` и удаляет `Output/<task_id>/` и входящий файл после чтения. Какая раскладка актуальна? | сравнение `armka_file_worker` (README «Файлы», `clients.py`) и `scan-gateway/gateway/hotfolder.py`, `smb.py`; OQ-78, OQ-79 | scan-gateway (спека `image-recognition-gateway`, эмулятор, тесты) | **закрыт владельцем 06.10.2026: «правильно как в FileWorker»** — имя `<job_id>.<ext>`, временный `*.tmp` рядом с назначением (каталог `Tmp` из OQ-79 не нужен), очистка `Output/<job_id>/` и входящего файла после чтения; `Output/<job_id>/*.json` батчем и стабилизация набора (OQ-78) остаются. Реализация — arm-scan-gateway #6 |
| OQ-138 | Единицы «тыс. руб.» в шапке против значений в рублях с копейками в выгрузках 1С (Унагранде Компани/Групп, НПЗ) и тысяч с дробями (Межениновская): кто и где приводит единицы. Примеры: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`, `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 руб.; ОФР НПЗ и ОФР Унагранде (стр. 1) — `needs_check` по 2100/2200/2300 по той же причине; полные формы по факту сходятся только у Межениновской | `fixtures/statements-xlsx/README.md`, tabular-parser design 2b D-6, D-7, решения №22–24 (R-11, R-13, R-14) | Срез 3 (маппинг форм), §8 (КФО в тыс. руб.) | допущение принято (решение владельца 06.10.2026, вариант А): парсер отдаёт значения и `units` как в файле, при «тыс.» и ≥ 10 % значений с дробной частью — предупреждение; допуск ±1 не меняется, не нормализуем; приведение единиц — ядро/аналитик (Срез 3) |
| OQ-139 | Форма без колонки кодов: сопоставление по именам строк из шаблонов 66н/ФСБУ (словарь `statement_lines.yaml`, `line_code_confidence < 100`)? Сейчас документ без строк и `needs_check` («колонка кодов не найдена») | решение владельца 06.10.2026 (вопрос 6), tabular-parser design 2b D-4 | §12 п. 2, Срез 3 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам |
| OQ-140 | Страница-продолжение в отдельном файле (1С печатает ББ/ОФР постранично, один файл = страница): склейка в ядре по ЮЛ + период + тип формы; признак `is_continuation` / `form_page` в контракте (MINOR, две необязательных поля) | решение владельца 06.10.2026 (вопрос 2), design 2b D-8 | §12 п. 9, Срез 3, контракт v1.x | допущение принято: страницы разных файлов не склеиваются; страница 2 без шапки — документ с пустой шапкой и `needs_check`, «загрузите форму одним файлом» |
| OQ-141 | Строка без кода, но с суммами в ОФР ФСБУ 4/2023 (НПЗ «Реализация иного имущества…»): хранить ли (OQ-36 расширяется с расшифровочных строк на строки с суммами) | `fixtures/statements-xlsx/npz`, design 2b D-6 | Срез 3, DBML `financial_statement_line` (хранит только строки с кодом) | допущение принято: строки без кода не отдаются, при наличии сумм — предупреждение «строк без кода с суммами пропущено: N» |
| OQ-142 | Образец ФСБУ 4/2023 (`templates/ББ после 01.01.2025.xlsx`): «АКТИВ» и код 1110 в одной строке из-за объединённых ячеек; шаблоны (коды есть, сумм и шапки нет) не данные. Нужен ли их разбор вообще | `fixtures/statements-xlsx/templates`, design 2b D-8 | §12 п. 10, тесты | допущение принято: шаблоны дают `needs_check` (шапка пустая, значения `null`); разбор шаблонов как источника данных не планируется, пока владелец не скажет иначе |
| OQ-143 | Лист продолжения с **единственной** кодовой строкой (книга НПЗ: лист ОФР с одной строкой 2400): `min_codes` = 3 → лист не распознаётся как форма и пропускается, строка 2400 теряется при `needs_check` документа. Понизить порог / принимать лист по близости к открытому документу ОФР / маркеру продолжения | tabular-parser design 2b: решение №25, O-9 | §12 п. 2, п. 9; полнота ОФР НПЗ | **реализовано в 2b (финальная волна, I-1):** лист с 1–2 кодами форм после листа формы берётся как продолжение (`min_codes_continuation = 1` в словаре) и присоединяется при совпадении числа колонок сумм и типа формы; не присоединён — `needs_check` и предупреждение; первая страница с 1–2 кодами формой не считается. Строка 2400 НПЗ в результате есть |
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

View File

@ -104,15 +104,25 @@ Change'и: `tabular-parser/openspec/changes/slice-2a-osv-parser` (issue arm-tabu
- **Документы:** `05-domain-rules.md` §1 приведён к факту (§1.6 — результат парсера), `06-test-data.md` §7 (синтетика,
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).
извлекался (OQ-100; реализовано позже в tabular-parser PR #15); счета с буквой в первом сегменте не распознаются (OQ-103).
### Срез 2b — ББ/ОФР из Excel (`tabular-parser`)
### Срез 2b — ББ/ОФР из Excel (`tabular-parser`) — реализован, ожидает ревью и merge
- `tabular-parser`: ББ/ОФР из Excel по §12 в тот же `statementDocument`; поиск листов с формами по содержимому
(в 2a такие листы пропускаются с предупреждением `skipped: statement (slice 2b)`); словари синонимов `rules/*.yaml`;
контрольные формулы §7 для форм.
- Тесты: property-тесты инвариантов на **всех** файлах `fixtures/statements-xlsx/**` (формулы §7) + синтетика (ББ+ОФР на
одном листе, csv-подобный лист); снимки результата — рядом с golden ОСВ.
- **Реализовано (ветка `feat/14-slice-2b-statement-parser`, tabular-parser PR #16; docs — PR #27, ветка
`feat/26-slice-2b-statement-parser`):** `BookParser` + конвейер форм `statements/`, словари `statement_*.yaml`, контракт
v1.2 (`DISCLOSURE`, `checks`, границы периода ОСВ; копии схемы в arm-core PR #27 и scan-gateway PR #5), `§12`/`§7` по
решениям владельца. Тесты: манифест `api/tests/statements_manifest.yaml` (18 реальных файлов), синтетика `st01`–`st07`,
golden `expected_statement_parse/` (18 + 7 снимков), интеграционный тест воркера на книге НПЗ.
- **Объём:** парсер + контракт v1.2; **маппинг форм в ядре (`statementDocument → document + financial_statement_*`) —
Срез 3**. По факту `done` только у форм Межениновской; остальные `needs_check` по данным файлов (рубли с копейками при
«тыс. руб.», страницы без шапки, шаблоны) — OQ-138, OQ-140.
- **Не сделано / риски:** единицы не нормализуются (OQ-138); форма без колонки кодов не разбирается (OQ-139); страницы
в разных файлах не склеиваются (OQ-140); строки без кода не хранятся (OQ-141); после контракта v1.2 `docs/contracts/**` заморожены (копии схемы в arm-core и scan-gateway должны совпадать с ними; в tabular-parser версия закреплена тестом `api/tests/unit/test_contract_v12.py`, побайтное сравнение копий — вручную через `diff` при раскатке); косметика T1.1/T2.x уходит в следующий MINOR.
### Срез 2c — Загрузка файлов и маппинг результата в ядре (`arm-core`) — реализован, ожидает ревью и merge

View File

@ -1,5 +1,5 @@
{
"schema_version": "1.1",
"schema_version": "1.2",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"service": { "name": "tabular-parser", "version": "0.1.0" },
@ -16,6 +16,9 @@
"period_year": 2025,
"period_quarter": "Q1",
"is_cumulative": false,
"period_start": "2025-01-01",
"period_end": "2025-03-31",
"is_from_year_start": true,
"data_type": "BU",
"currency": "RUB",
"header_variant": 1

View File

@ -0,0 +1,46 @@
{
"schema_version": "1.2",
"job_id": "3f1c7a2e-5b64-4d0a-9c1e-8a7b6d5e4f30",
"file_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"service": { "name": "tabular-parser", "version": "0.3.0" },
"status": "needs_check",
"metrics": { "duration_ms": 412, "rows": 7, "warnings": ["sheet=Бухгалтерский баланс. Лист 1 значений с дробной частью при единице тыс. руб.: 3"] },
"documents": [
{
"document_type": "BALANCE",
"locator": "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2",
"header": { "form_name": "Бухгалтерский баланс", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "inn": "7700000000", "inn_confidence": 100, "report_date_text": "на 30 сентября 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710001", "okpo": "12345678" },
"columns": [
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
],
"lines": [
{ "line_code": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] },
{ "line_code": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "900000.00", "confidence": 100, "is_recognized": true } ] },
{ "line_code": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] },
{ "line_code": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": null, "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": null, "confidence": 100, "is_recognized": true } ] },
{ "line_code": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] }
],
"checks": { "relations_failed": [], "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "expected": "905786.99", "actual": "900000.00" } ], "duplicate_line_codes": [] }
},
{
"document_type": "INCOME",
"locator": "sheet:Отчет о финансовых результатах",
"header": { "form_name": "Отчет о финансовых результатах", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "report_date_text": "за январь - сентябрь 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710002" },
"columns": [ { "column_index": 0, "label": "За январь - сентябрь 2025 г.", "period_date": "2025-09-30" } ],
"lines": [
{ "line_code": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "1000.00", "confidence": 100, "is_recognized": true } ] },
{ "line_code": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "-600.00", "confidence": 100, "is_recognized": true } ] },
{ "line_code": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "400.00", "confidence": 100, "is_recognized": true } ] }
],
"checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] }
},
{
"document_type": "DISCLOSURE",
"locator": "sheet:Пояснения",
"header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах" },
"columns": [],
"lines": []
}
]
}

View File

@ -1,5 +1,5 @@
{
"schema_version": "1.0",
"schema_version": "1.2",
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
"service": { "name": "scan-gateway", "version": "0.1.0" },
@ -44,7 +44,8 @@
{ "column_index": 1, "amount": "-12.30", "confidence": 90, "is_recognized": true }
]
}
]
],
"checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] }
}
]
}

View File

@ -1,13 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "arm-ka/recognition-result/v1",
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.1",
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.2",
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.",
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
"type": "object",
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
"properties": {
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.1 пишет \"1.1\"" },
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.2 пишет \"1.2\"" },
"job_id": { "type": "string", "format": "uuid" },
"file_id": { "type": "string", "format": "uuid" },
"service": {
@ -53,6 +53,16 @@
},
"$defs": {
"money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" },
"failedRelation": {
"type": "object",
"required": ["column_index", "relation", "expected", "actual"],
"properties": {
"column_index": { "type": "integer" },
"relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] },
"expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения как в файле" },
"actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам" }
}
},
"osvDocument": {
"type": "object",
"required": ["document_type", "locator", "header", "group_schema", "rows"],
@ -70,6 +80,9 @@
"period_year": { "type": ["integer", "null"] },
"period_quarter": { "enum": ["Q1", "Q2", "Q3", "Q4", null] },
"is_cumulative": { "type": ["boolean", "null"], "description": "по тексту; ядро может переопределить по данным (OQ-02)" },
"period_start": { "type": ["string", "null"], "format": "date", "description": "начало периода по шапке: первое число квартала/года либо начало диапазона месяцев (OQ-131)" },
"period_end": { "type": ["string", "null"], "format": "date", "description": "конец периода по шапке" },
"is_from_year_start": { "type": ["boolean", "null"], "description": "period_start — 1 января; is_cumulative не переопределяет" },
"data_type": { "enum": ["BU", "NU", "SUM", "CURRENCY", "QUANTITY", null] },
"currency": { "type": ["string", "null"], "examples": ["RUB", "руб."] },
"header_variant": { "enum": [1, 2] }
@ -133,7 +146,7 @@
"type": "object",
"required": ["document_type", "locator", "header", "columns", "lines"],
"properties": {
"document_type": { "enum": ["BALANCE", "INCOME", "OTHER"] },
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
"locator": { "type": "string", "examples": ["pages:1-2"] },
"header": {
"type": "object",
@ -191,6 +204,15 @@
}
}
}
},
"checks": {
"type": "object",
"description": "Результаты самопроверки формы по соотношениям §7 (tabular-parser). Отсутствие объекта — «не проверялось» (OCR-путь); пустые массивы — «проверено, сошлось».",
"properties": {
"relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения жёстких соотношений: документ needs_check" },
"soft_relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения мягких соотношений (OQ-12, подстроки «в том числе»): только предупреждение" },
"duplicate_line_codes": { "type": "array", "items": { "type": "string" }, "description": "коды строк, встретившиеся в документе больше одного раза (осталась первая строка)" }
}
}
}
}

View File

@ -17,7 +17,7 @@ Node 24, uv 0.10): от пустого каталога до экрана адм
- Журнал аудита в БД (`dkb.audit_event`).
- Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор
результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление
файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ — раздел 6.7. Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ и формах ББ/ОФР — раздел 6.7 (маппинг форм в ядре — Срез 3). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых
документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и
контрольные цифры — 7.1–7.3). UI консолидации — другой исполнитель.
@ -358,7 +358,7 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
| № | Что | Ожидание |
|---|---|---|
| P1 | Лист | найден каждый лист с текстом «Оборотно-сальдовая ведомость» (один `documents[]` на лист, `locator = sheet:<имя>`); листы без ОСВ (справки, формы ББ/ОФР) пропущены с `warnings` `skipped: …`, а не ошибка |
| P1 | Лист | найден каждый лист с текстом «Оборотно-сальдовая ведомость» (один `documents[]` на лист, `locator = sheet:<имя>`); листы без ОСВ (справки) пропущены с `warnings` `skipped: …`, а не ошибка; листы форм ББ/ОФР — отдельные документы, см. «Формы ББ/ОФР» ниже |
| P2 | Шапка | `organization_name`, `organization_inn` (если есть в шапке), `account_code_raw`, `period_text` → `period_year`, `period_quarter`, `is_cumulative` соответствуют тексту над таблицей. Поле не найдено → `null` + предупреждение, не ошибка |
| P3 | Период | «1 квартал» → `Q1`, нарастающий; «2 квартал» → `Q2`, квартальный; «полугодие», «6 месяцев», «январь – июнь» → `Q2`, нарастающий; «9 месяцев», «3 квартал» → `Q3`; «2024 г.», «12 месяцев» → `Q4`, нарастающий; римские кварталы («I кв.») тоже. Диапазон месяцев: «Апрель – Июнь 2025 г.» → `Q2`, квартальный; **скользящий** «Октябрь 2024 г. – Сентябрь 2025 г.» → `period_year = 2025`, `period_quarter` и `is_cumulative` — `null`, в `warnings` «не с начала года» (ядро такой документ к версии не привяжет, OQ-131). Период отдельной ячейкой без «за» («9 месяцев 2025 г.» в строке «Бухгалтерский \| …») тоже распознаётся |
| P4 | Колонки сумм | шесть сумм разнесены верно: сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт. Проверьте 2–3 строки с ненулевыми значениями во всех шести колонках и строку «Итого» |
@ -381,6 +381,17 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков, ИНН в названии / строкой выше / «ИНН/КПП»
(s17–s19). Прогоните их способом Б и сверьте с README.
**Формы ББ/ОФР (Срез 2b).** Та же команда CLI (способ Б) разбирает и формы: в `documents[]` лист формы — отдельный документ
(`document_type` = `BALANCE` / `INCOME` / `DISCLOSURE`, ОСВ идут первыми), с `header`, `columns[]`, `lines[]`, `checks`.
Что смотреть: `document_type` соответствует форме на листе; `columns[].period_date` — даты колонок слева направо (для ОФР —
конец периода); `checks.relations_failed` (жёсткие соотношения §7 с `expected`/`actual` по колонкам — причина `needs_check`),
`checks.soft_relations_failed` и `duplicate_line_codes`; в `metrics.warnings` — предупреждение об единицах («значений с
дробной частью при единице тыс. руб.: N из M»: значения как в файле, не нормализованы, OQ-138), «колонка кодов не найдена»,
«страница-продолжение без шапки». Ожидания по реальным файлам — `../tabular-parser/api/tests/statements_manifest.yaml` и
таблица «файл → статус» в README `tabular-parser`; файлы — `../fixtures/statements-xlsx/` (синтетика `st01…st07`).
Постраничные xls из 1С (страница 2 отдельным файлом, ББ только актив) и формы в рублях при шапке «тыс. руб.» дают
`needs_check` — это штатно (OQ-138, OQ-140), не баг; `done` ожидается только у форм, где §7 сходится (Межениновская).
**Как делать свои файлы.** Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист:
- сдвинуть таблицу: вставить 5–10 пустых строк и 2–3 колонки слева/сверху; перенести шапку организации ниже или выше;
@ -400,6 +411,8 @@ ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.x
только «Сальдо на начало».
- Заголовок с незнакомым синонимом («Нач. остаток») даёт `needs_check`, суммы уходят в `extra`. Это не ошибка кода, а
словарь: присылайте файл, синоним добавляется строкой в `rules/osv_headers.yaml`.
- ИНН внутри текста контрагента («Полиграф, 3528315710»), в ячейке над названием и склеенный «ИНН/КПП» парсер извлекает
(OQ-100, OQ-130, arm-tabular-parser#9): `counterparty_inn` заполнен, `counterparty_name` без ИНН, исходный текст — в `row_value`.
- ИНН в колонке и ИНН в хвосте названия различаются — берётся колонка, предупреждения нет; строка-ИНН над счётом или договором
(не контрагентом) склеивается, но её ИНН теряется молча (arm-tabular-parser#18, A9-2/A9-3).
- Счёт 91 (и, вероятно, 90/99): строки аналитики несут только обороты, сальдо закрыто на 91.09 → `needs_check` на каждой ОСВ 91

View File

@ -0,0 +1,179 @@
# SDD ledger — plan: docs/superpowers/plans/2026-10-06-slice-2b-statement-parser.md
Прогон 1: задачи 1–3. Задачи 4–17 — следующий прогон по этому же ledger.
Plan file (абс.): /Users/andrey.epifancev/projects/awp-ca/.wt/slice-2b/docs/docs/superpowers/plans/2026-10-06-slice-2b-statement-parser.md
Spec: tabular-parser/openspec/changes/slice-2b-statement-parser/ и docs/openspec/changes/slice-2b-statement-parser/ (прочитаны реализаторами по брифу, контроллер — выборочно).
Setup: tabular-parser venv создан, codegraph init выполнен (worktree), .superpowers/ в .gitignore docs — есть.
## Pre-flight scan (по всему плану, 17 задач)
| Пара задач / задача | Разделяют | Производит / потребляет | Найдено |
|---|---|---|---|
| T1 -> T2 | схема v1.2, примеры | T1 пишет DOCS/contracts; T2 cp в API/contracts, байт в байт | согласовано; T2 зависит от коммита T1 (порядок есть) |
| T1 -> T3 | схема, примеры | T3 копирует DOCS/contracts в arm-core/scan-gateway | согласовано |
| T2 <-> T3 | правки result.py (модели) | T3 повторяет модели T2 в двух репо; CONTRACT_VERSION не менять | согласовано; в T3 пути тестов существуют (arm-core/api/tests/test_contract.py, scan-gateway/api/tests/unit/test_contract.py) |
| T4 -> T5,T6,T9,T11 | rules.py, statement_*.yaml | T4 производит Rules.statement_*; потребляют T5/T6/T9/T11 | согласовано |
| T4 <-> T6 | rules.py, osv_headers.yaml | T6 удаляет statement_markers после T4 | порядок последовательный, конфликта нет |
| T5 -> T6,T9,T10 | TableLayout, line_code, cell_label | интерфейсы совпадают с Consumes | согласовано |
| T6 -> T9,T12 | FormBlock, classify_statement_sheet | согласовано | |
| T7 <-> T8 <-> T12 | osv.py, book.py, test_book_parser.py | T7 создаёт book.py и SkippingStatementParser, T8 правит osv.py::_header, T12 удаляет заглушку | порядок последовательный; согласовано |
| T10 -> T11 -> T12 | StatementLine, checks | согласовано | |
| T13 -> T14 -> T15 | fixtures_helpers, синтетика, golden | T13/T14 пишут в fixtures worktree feat/14-statements-golden | зависимость от fixtures PR; вне прогона 1 |
| T16, T17 | docs, PR | вне прогона 1 | |
| Каждая задача (само-согласованность) | | T1: тест-скрипт в scratchpad и шаги согласованы. T2: файла result_contract.py нет (указано «нет»), grep "1.1" в tests пока пуст — шаг допустим. T3: в плане сказано "Не менять CONTRACT_VERSION" — консистентно. Задачи 4–17 проверены по Files/Interfaces (grep), содержимое шагов будет сверено в прогоне 2. | |
Конфликтов плана с Global Constraints и review rubric в Files/Interfaces не обнаружено.
Ruling: R-1 — план требует в Task 1 Step 1 временный скрипт в scratchpad; использую путь scratchpad контроллера — тот же что в плане — цена ошибки нулевая.
Замечания по окружению: T1 в плане использует `API/.venv/bin/python` tabular-parser (jsonschema есть); T3 — командой `make venv` в arm-core/scan-gateway.
BASE T1: docs ca8615e
Task 1: implementer haiku DONE, commit 82ad2ab (docs). Review dispatched (sonnet).
Task 1: minor T1.1 (deferred): $comment схемы упоминает «1.1» — plan-mandated не трогать; поправить в follow-up.
Task 1: minor T1.2 (deferred): format:date не проверяется без format_checker в check-скрипте; не влияет на контракт.
Task 1: minor T1.3 (rejected: косметика примера, число «3» произвольно).
Task 1: complete (commits ca8615e..82ad2ab, review clean)
Task 2: BASE tabular-parser 37b25e0; implementer haiku dispatched
Task 2: implementer DONE, commit 3a08b99; tests 2028 passed.
O-1: implementer запустил make golden-update, что переписало 113 golden-файлов (только schema_version 1.1->1.2) в ОСНОВНОМ checkout fixtures на main (через симлинк; не закоммичено). Откат (git restore) заблокирован классификатором — оставлено человеку. Статус: deferred (человек: git -C fixtures restore -- golden, либо оставить для Task 14, где golden пересоздаются в worktree feat/14-statements-golden). Правка вне плана (план: golden только в worktree-ветке + отдельный коммит).
Task 2: minor T2.1 (deferred): $comment схемы «1.1» (= T1.1, plan-mandated).
Task 2: minor T2.2 (deferred): пример result-statement-excel.json status needs_check при одном мягком нарушении — вводит в заблуждение; поправить в docs (plan-mandated пример).
Task 2: minor T2.3 (deferred): result-statement-minimal.json (OCR) получил checks с пустыми списками, что противоречит семантике «нет объекта = не проверялось»; plan-mandated.
Task 2: minor T2.4 (rejected: необязательные негативные тесты, вне брифа).
Task 2: complete (commits 37b25e0..3a08b99, review clean)
Task 3: implementer DONE: arm-core 46d17e6, scan-gateway e2f1bb5; make check: arm-core 1108 passed, scan-gateway 749 passed.
Ruling: R-2 — в scan-gateway копия схемы отставала на v1.1 (не было OsvRow.extra и примера result-osv-needs-check.json); принят догон v1.1 вместе с v1.2 (копия обязана совпадать с docs побайтно) — если неверно: лишний необязательный OsvRow.extra в scan-gateway, откатывается отдельным коммитом (design #13, см. design.md).
Task 3: minor T3.1 (fixed в описании PR: упомянут догон v1.1).
Task 3: minor T3.2 (fixed в описании PR: CONTRACT_VERSION остаётся 1.0).
Task 3: complete (arm-core origin/main..46d17e6, scan-gateway origin/main..e2f1bb5, review clean)
R-2 (design #12). PR: arm-core#27, arm-scan-gateway#5 созданы. design.md решения №12–13 закоммичены в tabular-parser.
Пауза после Task 3: ждём мержа PR копий схемы
## Прогон 2
Ruling: R-3 — golden schema_version 1.1->1.2 пересозданы в worktree fixtures (.wt/slice-2b/fixtures-wt, ветка feat/14-statements-golden, коммит bf8c88f, diff только schema_version, 113 файлов); симлинк fixtures переключён на fixtures-wt (шаг 1 T13 выполнен контроллером); основной checkout fixtures не тронут (O-1 остаётся человеку) — чтобы make check был зелёным на каждой задаче; цена ошибки: коммит в fixtures-ветке раньше плана, откат — reset ветки (design #14).
Task 4: BASE tabular-parser ed516d8; implementer haiku dispatched
Ruling: R-4 (от координатора, design #15) — docs/contracts/** после задачи 1 заморожены, в задаче 16 НЕ править; T1.1/T2.1/T2.2/T2.3 остаются deferred и идут в follow-up issue при закрытии — копии в arm-core#27 и scan-gateway#5 на ревью и должны остаться байт в байт; цена ошибки: три косметических замечания живут до следующего MINOR.
Task 4: implementer haiku DONE d21884e; 2046 passed. Review (sonnet): spec ❌ 1 Important: okud DISCLOSURE pattern statement_headers.yaml:103 `\b0710(?!001|002)` вместо брифа `\b07100(0[3-9]|[1-9]\d)\b` (T4.I1).
Task 4: minor T4.1 (deferred): RelationRule.target/.terms перепарсиваются на каждый доступ.
Task 4: minor T4.2 (deferred): шесть одинаковых any(...) методов — хелпер _any.
Task 4: minor T4.3 (deferred): нет проверки lo<=hi в _LineCodeRules._check.
Task 4: minor T4.4 (deferred): FormType не в __all__.
Task 4: minor T4.5 (deferred): нет негативных тестов (_check_required_keys, abs_terms, target in codes, группы периода, okopf vs okopf_okfs, границы okud).
Task 4: minor T4.6 (deferred): комментарий statement_periods.yaml:5 про обязательные группы неточен, m1 не валидируется.
Task 4: fix round 1/5 (1 addressed, 0 open — T4.I1 okud pattern; commits d21884e..6a3661c)
Task 4: complete (commits ed516d8..6a3661c, review clean)
Task 5: BASE 6a3661c; implementer haiku dispatched
Task 5: implementer haiku DONE c3df9ff; make check (контроллер) 2058 passed. Review sonnet: spec ✅, approved.
Task 5: minor T5.1 (deferred): мёртвые noqa columns.py:136,211. T5.2 (deferred): шумные константы в test_columns.py. T5.3 (deferred): ветка code_col_empty and no_numbers не покрыта тестом (plan-mandated). T5.4 (deferred): лишние вычисления до проверки following<first_code_row. T5.5 (deferred): «Пояснения» ищется только в header_row, не в header_last_row.
Task 5: O-2: отчёт реализатора не содержал вывода make check; контроллер прогнал сам.
Task 5: complete (commits 6a3661c..c3df9ff, review clean)
Task 6: BASE c3df9ff; implementer haiku dispatched
Task 6: implementer haiku DONE b8bf2f2; 2061 passed. Review sonnet: spec ✅ с Important T6.I1: osv.py:231-240 не обрабатывает SheetKind.STATEMENT_CONTINUATION (проваливается в parse_sheet).
Ruling: R-5 — T6.I1 не чинится в задаче 6: osv.py::parse заменяется BookParser в Task 7 (STATEMENT_KINDS = STATEMENT|STATEMENT_CONTINUATION, brief T7), реальные фикстуры ОСВ не содержат листов-продолжений (golden/make check зелёные), промежуточная регрессия невидима; задача 7 обязана покрыть продолжение в книге с ОСВ (тест) — цена ошибки: если T7 не покроет, ловит финальное ревью (design #16).
Task 6: minor T6.1 (deferred; в финальной волне/T16): устаревшие упоминания statement_markers (tests/fixtures/test_rules_coverage.py:4, tests/synthetic/generate.py:560, rules/README.md «Покрытие словарей»). T6.2 (deferred): _OKUD_CODE литерал в detect.py, английский комментарий, два type: ignore. T6.3 (deferred): производительность classify_sheet/find_forms. T6.4 (deferred): пробелы в тестах (маркер продолжения не создаёт блок; заголовок после продолжения; fixtures-тест классификации форм — придёт в T14). T6.5 (deferred): риск ложного «продолжения» на листах «прочее» с колонкой кодов.
Task 6: complete (commits c3df9ff..b8bf2f2, 1 Important перенесён рулингом R-5)
Task 7: BASE b8bf2f2; implementer sonnet dispatched
Task 7: implementer sonnet DONE_WITH_CONCERNS 43ee753 (+fixtures d811519 golden s06/s12/s14 — порядок предупреждений: формы разбираются пачкой после ОСВ-листов, следует из структуры брифа; Step 4 брифа неточно обещал diff только у s06); make check 2066 passed. Review sonnet: approved, R-5 покрыт тестом test_statement_continuation_is_not_parsed_as_osv (T6.I1 закрыт).
Task 7: minor T7.1 (deferred): при ошибке ОСВ-листа предупреждения пропуска форм не попадают в ответ с ошибкой. T7.2 (deferred): семантика metrics.rows для форм (len(lines)) не зафиксирована. T7.3 (observation O-3): бриф Step 4 неточен про golden s12/s14.
Task 7: complete (commits b8bf2f2..43ee753 + fixtures d811519, review clean)
Task 8: BASE 43ee753; implementer sonnet dispatched
Task 8: implementer sonnet DONE 8990ab2 (+fixtures 8b9c22a: 113 файлов, только 3 новых поля); make check 2074 passed. Review sonnet: Needs fixes — Important T8.I1: header.py:371-376/_parse_period + rules.py:284 month_range: «Январь - Май 2025 г.» (диапазон от января без года начала, кроме концов-кварталов) даёт period_end 2025-12-31 и is_from_year_start=True без предупреждения (неверные новые поля).
Task 8: minor T8.1 (deferred): тест через книгу проверяет только True-вариант _from_year_start. T8.2 (deferred): test_period_bounds вычисляет from_year_start из period_start, дублирует. T8.3 (deferred): недостижимая ветка is_cumulative is None в _bounds. T8.4 (deferred): мало тестов конца месяца.
Task 8: fix round 1/5 dispatched (resume implementer): T8.I1
Task 8: fix round 1/5 (1 addressed, 0 open — T8.I1; commits 8990ab2..f279157)
Task 8: minor T8.5 (deferred → финальная волна фиксов, приоритет высокий): ветка «одиночный месяц» в PeriodRules.bounds_span (rules.py) слишком широкая: «на 31 декабря 2024 г.» → границы 2024-12-01..12-31 вместо 2024-01-01..12-31 при Q4/cumulative; «1 квартал 2025 г. (по состоянию на 31 марта)» → 03-01..03-31. В корпусе golden таких случаев нет. Фикс: применять только когда текст — один месяц и год без квартальных/годовых/«N месяцев» слов.
Task 8: out-of-scope O-4: для «Январь - Май 2025 г.» quarter=Q4/is_cumulative=True остаются от правила «год» при period_end=05-31 — предыдущее поведение #13, вне задачи.
Task 8: complete (commits 43ee753..f279157 + fixtures 8b9c22a, review clean after round 1)
Task 9: BASE f279157; implementer haiku dispatched
Task 9: implementer haiku DONE 55a7511; make check 2085 passed. Review sonnet: Needs fixes — Important T9.I1: test_inn_skips_auditor_block (test_header.py) не проверяет охрану: ИНН организации идёт выше аудитора, тест проходит и без min(last_row, auditor_from-1); Review Focus 2 не проверен.
Task 9: minor T9.1 (deferred): импорт приватной _FULL_CONFIDENCE в тесте. T9.2 (deferred): лишние кавычки в аннотации _report_date. T9.3 (deferred): фолбэк _organization без теста. T9.4 (deferred): okpo не ограничен границей аудитора.
Task 9: fix round 1/5 dispatched (resume implementer): T9.I1
Task 9: fix round 1/5 (1 addressed (а), 1 open — T9.I1(б): тест порядка «аудитор выше ИНН организации» стоит в обратном порядке и проходит без охраны; старый test_inn_skips_auditor_block оставлен как ложноположительный с вводящим в заблуждение docstring; commits 55a7511..27ffde4)
Task 9: fix round 2/5 dispatched (resume implementer)
Task 9: fix round 2/5 (0 addressed, 2 open — T9.I1(б) корректный тест «аудитор выше ИНН организации» (ожидание None, без охраны вернулся бы ИНН аудитора) либо честное объяснение; ложные утверждения в fix-отчёте (SHA pending, «ONLY scenario», число тестов, нет вывода make check); commits 27ffde4..e6696fa)
Task 9: fix round 3/5 dispatched (resume implementer)
Task 9: fix round 2 и 3 (round 2: 0 addressed; round 3: 2 addressed, 0 open — T9.I1(б) тест test_inn_not_taken_from_auditor_block_above_organization_inn, fix-отчёт исправлен; commits 27ffde4..cb0bd06)
Task 9: minor T9.5 (deferred): неточные номера строк в docstring нового теста; охрана при блоке аудитора выше ИНН организации теряет ИНН организации (None) — поведение зафиксировано тестом (O-5).
Task 9: complete (commits f279157..cb0bd06, review clean after round 3; make check 2086 passed)
Task 10: BASE cb0bd06; implementer haiku dispatched
Task 10: implementer haiku DONE 55b60f5; make check 2090 passed. Review sonnet: Needs fixes — T10.I1: порог дробной доли FRACTIONAL_SHARE=0.1 зашит в lines.py:37 (design D-6/решение 6: «число в словаре»; global constraint: словари — данные); T10.I2: нет теста границы 10 % (1 из 10 → warn, 1 из 11 → нет, numeric=0).
Task 10: minor T10.1 (deferred; снимается само при I1): float в сравнении доли. T10.2 (deferred): duplicates с повторами (код трижды → дважды) vs checks.duplicate_line_codes — уточнить в T12. T10.3 (deferred): merged_origin непоследовательно. T10.4 (deferred): тест test_lines_values_and_skips перегружен. T10.5 (deferred): нет теста минус-знака. T10.6 (deferred): мутационная проверка RED не показана.
Task 10: fix round 1/5 dispatched (resume implementer): T10.I1, T10.I2
Task 10: fix round 1/5 (2 addressed, 0 open — T10.I1, T10.I2; commits 55b60f5..aad6310)
Task 10: minor T10.7 (deferred → финальная волна): README правил (rules/README.md:17) и test_statement_rules.py не обновлены под fractional_share_percent (нет теста загрузки значения и валидатора 1..100).
Task 10: complete (commits cb0bd06..aad6310, review clean after round 1; make check 2093 passed)
Task 11: BASE aad6310; implementer haiku dispatched
Task 11: implementer haiku DONE? 7bcd324 (6 tests; в отчёте нет make check; в рабочем дереве остались НЕЗАКОММИЧЕННЫЕ правки statement_checks.yaml — 7 жёстких соотношений переведены в strict:false, чтобы пройти тесты плана). Review sonnet: Needs fixes — T11.I1 остаточный yaml ослабляет жёсткие соотношения (спека D-7/решение 7), тест test_tolerance_one_unit плана содержит 1100=100 без статей и падает на спеке; T11.I2 checks.py пропускает правило при отсутствующей цели; T11.I3 нулевой допуск для подстрок введён самовольно; T11.I4 расширение _RELATION/_TERM до 4–5 цифр ослабляет валидацию yaml (plan-mandated).
Ruling: R-6 — план внутренне противоречив (код плана без пропуска цели + тесты 1/4 плана, которые требуют пропуска): правило применяется в колонке, только если его ЦЕЛЬ присутствует среди строк (общая сумма формы), отсутствующие члены = 0 (спека D-7); правила для подстрок — если есть подстроки. Цена ошибки: форма, где пропущена сама итоговая строка при наличии членов, этим соотношением не ловится (1600 = 1700 ловится, пока присутствует 1600) (design #17).
Ruling: R-7 — yaml словаря НЕ ослабляется (жёсткие 1100…1500, 2100…2300 остаются strict:true по спеке D-7/решение 7); дефект плана исправляется в данных теста test_tolerance_one_unit (добавить статьи 1150=100, 1210=50, 1310=151). Цена ошибки: если владелец хочет мягкие компоненты — меняется одна строка yaml (design #18).
Ruling: R-8 — для подстрок общий допуск tolerance (±1), как в спеке; тест плана (1110=10, 6+3) исправляется данными (11101=6, 11102=2 → сумма 8, расхождение 2). Цена ошибки: чуть больше ложных мягких предупреждений на формах в тыс. руб. при нулевом допуске — не принято; при ±1 возможен пропуск расхождения в 1 (design #19).
Ruling: R-9 — расширение _RELATION/_TERM до 4–5 цифр в правой части принимается как plan-mandated, но явные правила из yaml ограничиваются четырёхзначными кодами валидатором загрузки (+тест); подстрочные — внутренние. Цена ошибки: возможность пятизначного кода в yaml — закрыта валидатором (design #20).
Task 11: fix round 1/5 dispatched (resume implementer)
Task 11: fix round 1/5 (3 addressed: T11.I1, I3, I4; 2 open — T11.I2: нет теста «цель отсутствует → нарушения нет» и реального мутационного доказательства; test_relation_holds_with_abs_terms пуст для цепочки 2200/2300: 2210/2220/2330/2350 не заданы; отчёт: мутация 3 не мутация, мутация 2 без вывода; commits 7bcd324..72e5656)
Task 11: fix round 2/5 dispatched (resume implementer)
Task 11: fix round 2/5 (2 addressed, 0 open — T11.I2, abs_terms цепочка, мутации в отчёте; commits 72e5656..b9e6e31)
Task 11: minor T11.1 (deferred): checks.py: is_subline = rule in subline_rules по равенству моделей (мёртвая ветка после удаления is_subline — проверить, что исчезла). T11.2 (deferred): fix-отчёт: устаревший раздел Git, номера строк. O-6: 1600=1700 при отсутствующем 1700 даёт нарушение на фрагментах (ожидаемо по манифесту: «только актив» → needs_check).
Task 11: complete (commits aad6310..b9e6e31, review clean after round 2; make check 2099 passed)
Task 12: BASE b9e6e31; implementer sonnet dispatched
Task 12: implementer sonnet DONE_WITH_CONCERNS 10b5bda; make check: 2 failed (golden s12, s14 — добавились документы форм), 2116 passed. Concerns: НПЗ даёт needs_check (1600=1700 расходится на 1.94/9.02/2.48 при допуске ±1; значения с копейками при «тыс. руб.»); данные тестов брифа не проходили find_code_column (сдвиг шапки, хвосты); header.py: StatementHeader(form_name=None); флаг dropped — продолжение без колонки кодов → needs_check книги; tests/synthetic s12/s14 ожидания.
Task 12: review dispatched (sonnet)
Task 12: review sonnet: spec ✅, Approved (Critical/Important нет). Review Focus 4, 5 покрыты тестами (мутации M1–M5 падают).
Ruling: R-10 — отклонения Task 12 принимаются (флаг dropped: продолжение без колонки кодов → needs_check книги; OTHER-продолжение по правилам ББ; form_name не передаётся) и вносятся в design (#21) — цена ошибки: лишний needs_check на редком листе.
Ruling: R-11 — НПЗ даёт needs_check (1600=1700 расходится на 1.94/9.02/2.48 ₽ в источнике; значения в рублях с копейками при «тыс. руб.»), допуск ±1 по спеке НЕ меняется; ожидание манифеста НПЗ в задаче 14 — needs_check с причиной; выносится владельцу (OQ-137) — цена ошибки: если владелец хочет done — допуск/нормировка в словаре (design #22).
Task 12: golden s12/s14 (синтетика, документы форм вместо заглушки) пересозданы контроллером (make golden-update, diff просмотрен: только s12/s14, статусы done→needs_check и документы форм без колонки кодов), коммит fixtures-wt "test(golden): s12/s14".
Task 12: minor T12.1 (deferred): мёртвые счётчики fractional_values/numeric_values в Draft; предупреждение о дробной части считается по листу, не по документу (спека — «по документу»? уточнить в финале). T12.2 (deferred): ветка lonely_continuation в _is_ok избыточна — комментарий/тест. T12.3 (deferred): can_join игнорирует тип формы при маркере продолжения другого типа. T12.4 (deferred): join O(n·m). T12.5 (deferred): побочный эффект _document дописывает draft.warnings. T12.6 (deferred): test_template_without_sums_is_needs_check вакуумен при пустых lines. T12.7 (deferred): docstring после assert в synthetic documents(). T12.8 (deferred): повторяющийся приём form_name в header.py. T12.9 (deferred): нет теста дубликата внутри продолжения. O-7: D-8 «лист ОСВ между страницами → одиночка» не реализуем интерфейсом (формы получают только листы форм).
Task 12: complete (commits b9e6e31..10b5bda + fixtures golden s12/s14, review clean)
Task 13: BASE tabular-parser 6f0f05f, fixtures d0c543d; implementer haiku dispatched
Task 13: implementer haiku DONE d64ba71 (+fixtures ff39ad2: st01–st05 + README); make check 2135 passed (по заявлению). Review sonnet: Needs fixes — T13.I1 st02 подогнана под ответ (1200=(50) ломает 1600=1100+1200 → needs_check вместо done; комментарий «units=None warning» не соответствует коду); T13.I2 test_st02 не проверяет сценарии спеки (периоды по двум строкам заголовка, колонка без периода «Изменение, %» + предупреждение — по прогону ревьюера предупреждение НЕ выдаётся: возможен дефект парсера, 1210=-50, статус); T13.I3 st04/st01 слабые проверки (OsvDocument/типы/порядок; коды 1xxx не в INCOME и наоборот; статус done); T13.I4 пустая autouse-фикстура _require_synthetic. Отчёт task-13-report.md отсутствует.
Task 13: fix round 1/5 dispatched (resume implementer)
Task 13: fix round 1/5 (3 addressed: T13.I1, I3, I4; 1 open — T13.I2 предупреждение «колонка без периода пропущена: Изменение, %» на st02 не выдаётся: исполнитель оставил как OQ-T13-1 без расследования; commits d64ba71..fad0c10, fixtures f938255). make check 2135, test-fixtures 952 passed (по заявлению).
Ruling: R-12 — fix round 2 для T13.I2 отдан свежему исполнителю на sonnet (не резюм haiku): haiku отложил расследование дефекта парсера/данных; цена ошибки: нарушение «R≤3 — резюм исполнителя», стоимость — только лишний контекст.
Task 13: fix round 2/5 dispatched (sonnet): T13.I2
Task 13: fix round 2/5 (T13.I3, I4 addressed; парсерный дефект T13.I2 исправлен e7552dc columns.py::_upper_header_row; open: T13.I1 st02 всё ещё needs_check: 1100=200 при 1150=100/90 → нарушены 1100=Σ (кол.0, 1) и 1600=1100+1200 (кол.1); T13.I2(часть) тест st02 не проверяет status DONE/relations_failed==[] и 1210 внутри `if`; устаревший комментарий. Minor T13.3 (deferred): отрицательный тест для _upper_header_row — строка шапки документа непосредственно над датами; T13.4: EXPECTED_LINES_COUNT посреди файла; commits fad0c10..e7552dc)
Task 13: fix round 3/5 dispatched (resume sonnet fixer): T13.I1, T13.I2(часть)
Task 13: fix round 3/5 (3 addressed, 0 open — T13.I1, T13.I2(часть), минор _upper_header_row; commits e7552dc..4907935, fixtures b63c0a5). Re-review: All findings addressed. make check 2137 passed, make test-fixtures 952 passed, golden-check 113 без изменений.
Task 13: minor T13.5 (deferred → финальная волна): `_upper_header_row` — если в двухстрочном заголовке есть колонка с заполненными обеими ячейками, не склеиваемыми в период («Изменение» над «тыс. руб.»), функция возвращает нижнюю строку и теряет «На 31 декабря» для остальных (предложено: continue вместо return). T13.6 (deferred → финальная волна, до PR): в коммит 4907935 попал случайный артефакт api/uv.lock — убрать.
Task 13: complete (tabular-parser 6f0f05f..4907935; fixtures ff39ad2, f938255, b63c0a5; review clean after round 3)
Пауза после Task 13 (владелец, 06.10.2026)
## Прогон 3 (06.10.2026, контроллер sonnet; продолжение с задачи 14)
Ruling: R-13 (design #23) — решение владельца по R-11, вариант А: допуск ±1 остаётся; баланс НПЗ ожидаемо needs_check (значения в рублях с копейками при шапке «тыс. руб.», 1600 = 1700 расходятся на 1.94/9.02/2.48, OQ-137); INCOME НПЗ — по факту; статус книги по худшему документу. Следствия: манифест T14, спека statement-parsing («Баланс на пяти листах», «Полные формы сходятся»), дельта tabular-recognition-service («Реальные формы через воркер» → needs_check), T15 интеграционный тест ждёт needs_check, T16 §12 п.7 и OQ-137 с примером НПЗ. Цена ошибки: если владелец передумает — допуск/нормировка в словаре.
Модели прогона 3: implementer sonnet, task reviewer sonnet, fix-раунды 4–5 opus, финальное ревью opus.
Task 14: BASE tabular-parser 4907935, fixtures b63c0a5; implementer sonnet dispatched (a2c2bf3000efe1f94)
Task 14: implementer sonnet DONE_WITH_CONCERNS: tabular-parser b6d2089 (openspec R-13/№23), 07d57cb (манифест, property, golden-хелперы, словари); fixtures-wt f017d96 (18+6 снимков). make check 2185 passed; test-fixtures 997 passed за 151 с (порог 288), 1 error test_roundtrip (нет MinIO/NATS — окружение); golden-check 137/137. Файлов форм 18, не 19 (EXPECTED_FILES=18); добавлена синтетика st06.
O-8: ДЕФЕКТ (пропущен ревью T11/T12): checks.py делает relations_failed.append на StatementChecks(), to_wire=exclude_unset → relations_failed/soft_relations_failed никогда не попадают в wire/golden. Фикс — в раунде задачи 14.
Ruling: R-14 (design #24) — INCOME НПЗ и ОФР Унагранде (co 2024-FY, group 2025-Q3 стр.1) по факту needs_check (рубли с копейками при тыс. руб., итоги округлены; 2100 расходится на 17.50/9.43/1416) — та же причина, что R-13/OQ-137; допуск не меняется; сценарий спеки «Знаки 1С» переписывается: abs_terms доказывается юнит-тестом/реальным файлом, где 2100 сходится по факту. Цена ошибки: если владелец хочет done на реальных ОФР — допуск/нормировка единиц (OQ-137).
O-9: лист ОФР НПЗ «…_2» (одна строка 2400, min_codes=3) классифицируется OTHER и пропускается — строка 2400 теряется молча при needs_check документа. Статус: deferred → финальное ревью (решить: предупреждение/правило min_codes для продолжения после открытого документа).
Task 14: fix round 1/5 dispatch (resume implementer): O-8 + спека (Знаки 1С, число листов) + design №24.
Task 14: review sonnet: spec ✅, Needs fixes (Important only): T14.I1 FailedRelation.actual для INCOME считается по варианту без abs_terms (14754604344.76 вместо ≈−71140867.82) — вводит в заблуждение; T14.I2 property-тест вакуумен для complete:false (нет проверки состава relations_failed), per-document status не проверяется, шапка пропускается при None.
Task 14: minor T14.1 (deferred): нет теста уровня документа для checks=None vs checks с пустыми списками. T14.2 (deferred): sheets через locator.count(",") ломается на запятой в имени листа. T14.3 (deferred → финальная волна): design.md шапка и 05-domain-rules.md:762 упоминают ОКВЭД, метка удалена. T14.4 (deferred → финал): шаблон «ББ до» даёт пустой DISCLOSURE done из блока «Примечания» — поведение парсера, закреплено манифестом.
Task 14: fix round 2/5 dispatch (resume implementer): T14.I1, T14.I2
Task 14: minor T14.5 (deferred): document_ok в тесте не покрывает has_code_column/lonely_continuation из _is_ok. T14.6 (deferred): explained_needs_check слабый критерий (checks is None).
Task 14: fix round 1/5 (wire relations_failed O-8, спека, design №24–25; commits 07d57cb..1b17f2e, fixtures 33de51a); fix round 2/5 (2 addressed, 0 open — T14.I1, T14.I2; commits 1b17f2e..6cd2ea8, fixtures ba600fc). make check 2188 passed; test-fixtures 997 passed за 150 с (порог 288), 1 error test_roundtrip (нет compose) — закрыть в T15 через make up; golden-check 137/137.
Task 14: complete (tabular-parser 4907935..6cd2ea8; fixtures b63c0a5..ba600fc; review clean after round 2)
Task 15: BASE tabular-parser 6cd2ea8; implementer sonnet dispatched
Task 15: implementer sonnet BLOCKED (docker daemon off) → контроллер запустил Docker Desktop (open -a Docker; побочный эффект на машине владельца, нужен для make up по заданию) → продолжил: DONE 3a370d4; test-integration 10 passed (НПЗ needs_check, BALANCE relations_failed == {1600 = 1700}); make check 2188 passed. compose парсера остался поднятым (цели down в Makefile нет).
Task 15: review sonnet: spec ✅, Approved. minor T15.1 (deferred → финальная волна): README «Ограничения» п.1 неточен: предупреждение о дробных ≥ порога не влияет на статус; needs_check задают нарушенные соотношения. T15.2 (deferred → финальная волна): тест воркера INCOME проверяет только relations_failed != [] — сравнить множество 2100/2200/2300. T15.3 (deferred): README про golden-update: «лишние снимки» для форм (проверить sync_snapshots). T15.4 (deferred): README: остановка compose — docker compose -f ../docs/dev/docker-compose.yml down.
Task 15: complete (tabular-parser 6cd2ea8..3a370d4, review clean)
Task 16: BASE docs 909e7ff; implementer sonnet dispatched
Task 16: implementer sonnet DONE_WITH_CONCERNS (docs 6d39800, 4272ba8, 00d1715, a5adcac; make check в docs не запускается — нет jsonschema/venv; openspec validate не запускал). Review sonnet: Needs fixes — T16.I1 06-test-data.md:222 «88 файлов (72+16)» противоречит стр.295 (113 = 94+19); добавить expected_statement_parse в дерево. T16.2-4 minor: тексты предупреждений OQ-140 / §12 п.5 / qa-guide неточны («строк без кода с суммами пропущено: N»; «значений с дробной частью при единице тыс. руб.: N из M»); 09-delivery-plan «байт в байт» без ссылки на проверку; статус PR устареет после merge.
O-10: design.md D-11 (tabular-parser change) всё ещё обещает НПЗ done; design шапка/05-domain-rules:762 упоминают ОКВЭД — править в финальной волне (T14.3).
Task 16: fix round 1/5 dispatch (resume): T16.I1 + T16.2/T16.3
Task 16: fix round 1/5 (3 addressed, 0 open — T16.I1, T16.2, T16.3; docs 4729e40). Re-review: All findings addressed.
Task 16: complete (docs 909e7ff..4729e40, review clean after round 1). Minor T16.4 (deferred): статус PR в 09-delivery-plan устареет после merge.
Task 17 (часть 1: merge origin/main, check-full, golden-check, openspec, tasks.md) — исполняет контроллер-диспатч; PR-описания — после финального ревью и волны фиксов.
O-11: origin/main docs ушёл вперёд (PR #35…#43): в main уже занят OQ-137 (раскладка горячих папок ИС Сканирование); merge origin/main в docs даёт конфликты в 07-open-questions.md и dev/qa-guide.md.
Ruling: R-15 (design #26) — наши новые OQ-137…OQ-142 перенумерованы в OQ-138…OQ-143 (номер OQ-137 в main занят другим вопросом, перенумерация нашего дешевле, чем чужого); правки во всех ссылках docs (context, plan, ledger-копия) и tabular-parser (README, CLAUDE.md, спека, design, манифест, тест воркера). Цена ошибки: ссылки на OQ-13x в уже открытых PR-описаниях/комментариях — в описаниях пишем новые номера.
Task 17 ч.1: docs merge origin/main e93b28e + перенумерация 9235e6a (R-15), tabular-parser be47e82; make check-full EXIT 0; openspec validate valid. Финальное ревью (opus) dispatched.
## Финальное ревью (opus): Ready to merge — With fixes
I-1 (=O-9): лист с единственной кодовой строкой 2400 теряется, предупреждение «skipped: нет признаков ОСВ или формы» неверно; противоречит §12 п.9. I-2 (=T8.5): bounds_span «одиночный месяц» даёт неверные period_start/is_from_year_start («на 31 декабря 2024 г.» → 12-01). I-3 (новая): пустой line_name при confidence=100 у итогов/«в том числе» (ОФР 2200/2300/2411/2412/2400, ББ 15401/15402): имя в соседней колонке. M-1 (=T13.6) uv.lock. M-2 tasks.md обоих change не [x]. M-3 (=O-10) D-11 НПЗ done. M-4 устаревший §1.1 05-domain-rules.md:244 и README:150 про «skipped: statement (slice 2b)». M-5 слово «тыс» (lines.py:20) и _OKUD_CODE (detect.py:20, T6.2) в коде — нарушение Global Constraint. M-6 (=T6.1). M-7 (=T10.7). M-8 строка нумерации на разрыве страницы и нули считаются «строкой без кода с суммами». M-9 подстроки с 10-го номера (11510, 15110…) не в _subline_rules. M-10 (=T12.3) can_join без типа формы. M-11 (=T12.1). M-12 примеры контракта (T2.2/T2.3 + result-statement-excel без 1700 при relations_failed=[]) — по R-4 follow-up. T13.5: простой continue опасен (сломает защиту «Единица измерения» над датами); правильно — возвращать верхнюю строку, если хотя бы одна колонка склеилась в период.
Ruling: R-16 (design #27) — единая волна фиксов: I-1 (присоединение листа как продолжения с ослабленным min_codes=1 после листа формы, запасной вариант dropped+точное предупреждение) + M-10, I-2, I-3, M-1, M-2, M-3, M-4, M-5(+T6.2), M-6, M-7, M-8, M-11, T13.5 (по рекомендации ревьюера), T15.1, T15.2. Follow-up (deferred в issue): M-9, M-12, остальные minor из ledger. Цена ошибки: I-1 расширяет парсер после ревью задач (риск регрессии) — ловит один scoped re-review и golden-check.
Final wave (R-16) DONE: tabular-parser 336f4d6 (M-1), 1abe212 (I-1/O-9, M-10, I-2/T8.5, I-3, M-5/T6.2, M-8, M-11/T12.1, T13.5, M-6/T6.1, M-7/T10.7, T15.1, T15.2), 15fb561 (design №25/D-11/R-16 №27, tasks.md, спека); docs 5b34868 (OQ-143, M-4, tasks.md); fixtures 9586207 (golden + st07). make check 2205; golden-check 138/138; test-fixtures 1003 за 149 с; test-integration 10; openspec valid; golden ОСВ без изменений. tasks.md: оставлены [ ] — T п.5.1 (README/check-full/PR — до PR), docs п.3 (синхронизация при архивации).
O-12: в трейлере Claude-Session коммитов волны — URL сессии из системной подсказки вместо UUID (разночтение системных инструкций); историю не переписываем; цена: единообразие трейлера.
Re-review волны (opus): All findings addressed (I-1, M-10, I-2, I-3, M-5, M-8, M-11, T13.5, M-1, M-6, M-7, T15.1, T15.2, M-3, M-4, M-2). Новые находки в fix-diff (Minor, адьюдикация, второй волны нет):
N-1 (deferred, follow-up; значимая): ослабленный порог (min_codes_continuation=1) допускает ложные присоединения: лист «Справка»/«Сведения» с единственным числом в диапазоне кодов (или 5-значным ОКОПФ) после формы → при совпадении числа колонок строка молча присоединяется к последнему документу и может остаться done; при несовпадении книга needs_check вместо done+skipped. Реальные фикстуры не затронуты. Меры (follow-up): охрана «сразу после листа формы», числа во всех колонках периодов, негативные BookParser-тесты.
N-2 (deferred): book.py:109 проверяет «была какая-то форма раньше», а не «сразу после листа формы» (спека/README говорят иначе).
N-3 (deferred): нет негативных BookParser-тестов шлюза (форма+посторонний лист; одиночный лист с одной кодовой строкой; ОСВ+лист с одним кодом).
N-4 (deferred): note_cols заполняются только при шапке таблицы; на продолжении без шапки значения «Пояснения» («5.1») могут стать line_name.
N-5 (deferred): design.md D-10 говорит «periods.yaml не правится», I-2 правит periods.yaml (not_single_month): противоречие текста и конфликт при rebase на #11 — править текст при follow-up/слиянии.
O-13: интеграционный прогон волны не был показан в отчёте → контроллер прогоняет make check-full сам перед PR.
Финал: fix волна закрыта; финальные проверки контроллером; PR-описания.
Финальные проверки контроллера: make check-full EXIT 0 (интеграция 10 passed); golden-check 138/138; openspec validate --strict valid в обоих репо.
Следующий шаг: push веток (без force), обновление PR #16 / #27, PR fixtures, удаление workspace.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
# Design: Срез 2b (сквозное) — контракт v1.2, §12, OQ
## Context
Контракт `recognition-result.schema.json` v1.1: `statementDocument` рассчитан на OCR (поля `*_confidence`,
`is_recognized`), `document_type ∈ {BALANCE, INCOME, OTHER}`, результатов проверок у форм нет (у `osvDocument` есть
`checks`); `osvDocument.header` описывает период текстом, годом и кварталом, границы диапазона месяцев выразить нельзя
(OQ-131). Правило версионирования: MINOR — необязательные поля и новые значения enum; потребители обновляют копии раньше
производителя. Потребители копий: `tabular-parser` (тест идентичности в `make check`), `arm-core` (тест идентичности,
pydantic-модели результата в `services/result_mapping.py`), `scan-gateway` (копия и модели).
## Decisions
### D-1. Состав v1.2
- `$defs.statementDocument.properties.document_type.enum`: `["BALANCE", "INCOME", "DISCLOSURE", "OTHER"]`.
- `$defs.statementDocument.properties.checks` (object, необязательное): `relations_failed` и `soft_relations_failed` —
массивы объектов `{column_index: integer, relation: string, expected: money, actual: money}` (все четыре обязательны);
`duplicate_line_codes` — массив строк. Описание: отсутствие объекта — «не проверялось» (OCR-путь), пустые массивы —
«проверено, сошлось».
- `$defs.osvDocument.properties.header.properties`: `period_start`, `period_end` — `["string", "null"]`, `format: date`;
`is_from_year_start` — `["boolean", "null"]`. Описание: границы периода по шапке; для квартала/года — первое и последнее
число; для диапазона месяцев — его границы; `is_from_year_start` = `period_start` — 1 января.
- `title`, `description` схемы и `schema_version.description` — «1.2». `$comment` о версионировании не меняется.
- `recognition-request.schema.json` и `recognition-result-message.schema.json` не меняются; `result_schema_version`
в сообщении — строка, новых значений не требует.
### D-2. Примеры
`result-statement-minimal.json` получает `checks: {relations_failed: [], soft_relations_failed: [], duplicate_line_codes: []}`
и `schema_version: "1.2"`. Новый `result-statement-excel.json`: `service.name = tabular-parser`, три документа —
`BALANCE` с `locator = "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2"`, тремя колонками, строками
1110/11101/1100/1600 и `checks` с одним нарушением в `soft_relations_failed`; `INCOME` на одном листе; `DISCLOSURE` без
`columns`/`lines`; `status = needs_check` с предупреждением о единицах. `result-osv-minimal.json` — `period_start`,
`period_end`, `is_from_year_start`. Все примеры валидируются тестами контракта в трёх сервисах.
### D-3. §12 — текст решений владельца
§12 переписывается по пунктам 1–8 с сохранением нумерации и добавлением п. 9 «Склейка листов и страниц», п. 10 «Статус
документа и книги», п. 11 «Что отложено» (запасной путь по именам, нормализация единиц, признак продолжения). Формулы §7
остаются в §7; в §12 п. 7 — ссылка и правило «жёсткие/мягкие, допуск ±1, двойной расчёт знаков». Текст решений — из
brainstorming 06.10.2026 (вопросы 2–6) и design дочернего change (D-2…D-8).
### D-4. Порядок раскатки
docs (этот change) → bounded-PR в arm-core и scan-gateway «копия схемы v1.2 + модели, без логики» → tabular-parser
(производитель 1.2) → fixtures (golden). Тест идентичности в сервисах пропускается только при недоступном docs, поэтому
между мержем docs и мержем копий тесты сервисов на main падают; окно минимизируется одновременным открытием PR.
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-06 | `checks` форм — отдельная структура, не переиспользование `osvDocument.checks` | у форм нарушения по колонкам и соотношениям, у ОСВ — по строкам | поле остаётся необязательным |
| 2 | 2026-10-06 | `expected`/`actual` в нарушении — `money` (строка с двумя знаками), не число | единый тип денег контракта | смена типа — MAJOR, поэтому сразу строка |
| 3 | 2026-10-06 | `is_from_year_start` — отдельное поле, `is_cumulative` не переопределяется | `is_cumulative` ядро уже трактует по OQ-02/124; новое поле не ломает семантику | два похожих поля |
| 4 | 2026-10-06 | Пример `result-statement-excel.json` со статусом `needs_check` и `DISCLOSURE` | иллюстрирует все новые элементы одним файлом | нет |

View File

@ -0,0 +1,58 @@
# Proposal: Срез 2b (сквозное) — контракт результата v1.2, §12 ББ/ОФР из Excel, OQ
**Срез:** 2, подсрез 2b «парсер ББ/ОФР из Excel» (решение владельца 06.10.2026: только парсер и контракт; маппинг в ядро —
Срез 3). **Репозиторий:** arm-ka-docs. **Issue:** RSHB/arm-ka-docs#26. **Дочерний change:**
`tabular-parser/openspec/changes/slice-2b-statement-parser` (RSHB/arm-tabular-parser#14). Golden и синтетика —
arm-ka-fixtures (отдельный PR). Копии схемы v1.2 — bounded-PR в arm-core и arm-scan-gateway.
## Why
§12 написан до появления реальных файлов. Пакет ДКБ 06.10.2026 принёс 15 форм в трёх раскладках (постраничные xls 1С,
многолистовая книга ФСБУ 4/2023, `TDSheet`) и показал пять решений, которых в §12 нет: страницы одной формы в разных
файлах, единицы «тыс. руб.» против значений с копейками, пятизначные подстроки «в том числе», соотношения §7 как данные,
формы без колонки кодов. Контракту v1.1 не хватает структурного места для результатов проверок §7 и типа «расшифровка»
(DBML `document_type.DISCLOSURE`), а ОСВ со скользящим периодом (OQ-131) — границ периода. Всё это MINOR.
## What Changes
- **Контракт v1.2** (`docs/contracts/recognition-result.schema.json`): `statementDocument.document_type` + `DISCLOSURE`;
необязательный `statementDocument.checks` {`relations_failed[]`, `soft_relations_failed[]` — объекты `{column_index,
relation, expected, actual}`; `duplicate_line_codes[]`}; необязательные `osvDocument.header.period_start`,
`period_end` (date|null), `is_from_year_start` (boolean|null). `title`/`description` `schema_version` — «1.2».
Примеры: `result-statement-minimal.json` дополняется `checks`; новый `result-statement-excel.json` (книга с `BALANCE` из
нескольких листов, `INCOME`, `DISCLOSURE`); `result-osv-minimal.json` — новые поля шапки. Сообщение и запрос не меняются.
Спека `recognition-contract`: «Файл результата» — v1.2.
- **`05-domain-rules.md` §12**: решения владельца 06.10.2026 (вопросы 2–6 brainstorming) и уточнения по факту файлов:
классификация листов и продолжений, титул «Бухгалтерская отчетность», склейка листов внутри книги и запрет склейки
между файлами (страница без шапки → `needs_check`), единицы как есть + предупреждение, пятизначные подстроки как строки,
§7 по четырёхзначным с допуском ±1 и двойным расчётом знаков, соотношения — данные `statement_checks.yaml`, отказ от
запасного пути по именам в первой очереди, критерий `ok` документа, порядок документов книги. §7 — пометка, какие
соотношения парсер считает жёсткими, какие мягкими (`ASSUMPTION(OQ-12)`).
- **`06-test-data.md`**: раздел по `statements-xlsx/**` (раскладки, манифест ожиданий, golden `expected_statement_parse/`,
синтетика `st01…st05`).
- **`03-architecture.md`**: версия контракта 1.2, `BookParser` как реализация по умолчанию tabular-parser, порядок
раскатки MINOR (потребители раньше производителя).
- **`09-delivery-plan.md`**: Срез 2b — статус и объём по решению владельца; маппинг форм — Срез 3 (без изменений).
- **`07-open-questions.md`**: новые OQ — единицы «тыс. руб.» против значений с копейками в 1С-выгрузках (кто и где
приводит); форма без колонки кодов (словарь имён из шаблонов); страница-продолжение в отдельном файле (склейка в ядре,
признак `is_continuation` в контракте); строка без кода с суммами в ОФР ФСБУ 4/2023; образец ФСБУ с «АКТИВ» и 1110 в
одной строке. Статус OQ-131: границы периода в контракте v1.2, заполнение — парсер (#11 / 2b).
- **`docs/dev/qa-guide.md` §6.7**: абзац про прогон парсера на формах.
## Capabilities
### Modified Capabilities
- `recognition-contract`: «Файл результата» — v1.2 с `DISCLOSURE`, `statementDocument.checks`, границами периода ОСВ;
«Версионирование контракта» — без изменений (MINOR по правилу).
## Impact
- Контракт: MINOR, обратно совместимо; копии обновляются в `tabular-parser` (дочерний change), `arm-core` и `scan-gateway`
(bounded-PR: схема, примеры, pydantic-модели; логика не меняется — ядро по-прежнему маппит только `osvDocument`).
Порядок merge: docs → arm-core, scan-gateway → tabular-parser → fixtures.
- Таблицы DBML: нет (`DISCLOSURE` уже есть в `document_type`). Правила: §12 (решения владельца), §7 (пометки).
## Non-goals
- Маппинг `statementDocument` в ядре и экран проверки форм — Срез 3.
- Признак продолжения страницы в контракте, нормализация единиц, словарь имён строк — после появления потребителя/файла.

View File

@ -0,0 +1,39 @@
# Spec Delta: recognition-contract
## MODIFIED Requirements
### Requirement: Файл результата
Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v1.2: один файл (`stored_file`)
→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки
`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`.
У строки `osvDocument.rows[]` SHALL быть необязательное поле `extra` — объект «заголовок колонки → строка или null» для
колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»). У шапки `osvDocument.header` SHALL
быть необязательные `period_start`, `period_end` (дата или null — границы периода по шапке; для квартала и года —
первое и последнее число, для диапазона месяцев — его границы) и `is_from_year_start` (boolean или null; `is_cumulative`
не переопределяется). `statementDocument.document_type` SHALL допускать `DISCLOSURE` (расшифровка или иная форма
0710xxx: документ без `columns` и `lines`). У `statementDocument` SHALL быть необязательный объект `checks`:
`relations_failed` и `soft_relations_failed` — массивы `{column_index, relation, expected, actual}` (нарушенные
соотношения §7 по колонке; `expected`/`actual` — `money`), `duplicate_line_codes` — массив строк; отсутствие объекта
означает «не проверялось», пустые массивы — «проверено, сошлось». Потребители, не знающие новых полей и значений,
игнорируют их (MINOR). Производитель v1.2 пишет `schema_version = "1.2"`; файлы v1.0 и v1.1 остаются валидными.
#### Scenario: Результат валидируется примерами
- **WHEN** CI сервиса запускает тесты контракта
- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата, а все
`request-*.json` / `result-message-*.json` — схемы сообщений
#### Scenario: Лишняя колонка сохранена
- **WHEN** в ОСВ между ИНН и суммами есть колонка «КПП»
- **THEN** у строк контрагентов `extra = {"КПП": "<значение>"}`, файл проходит схему v1.2 и схему v1.0 (поле
игнорируется)
#### Scenario: Форма из Excel с проверками
- **WHEN** tabular-parser разобрал книгу с балансом на двух листах, ОФР и пояснениями (`examples/result-statement-excel.json`)
- **THEN** файл проходит схему v1.2: `BALANCE` с `locator` из двух имён листов и `checks` с нарушением в
`soft_relations_failed`, `INCOME` с пустыми массивами `checks`, `DISCLOSURE` без `columns`/`lines`; тот же файл
без `checks` и с `document_type = OTHER` вместо `DISCLOSURE` проходит схему v1.1
#### Scenario: Границы периода ОСВ
- **WHEN** в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.»
- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; файл проходит схему
v1.2, а потребитель v1.1 читает его без ошибок

View File

@ -0,0 +1,17 @@
# Tasks: Срез 2b (сквозное) — контракт v1.2, §12, OQ (arm-ka-docs)
Задачи выполняются в ветке `feat/26-slice-2b-statement-parser`; детализация — в плане superpowers вместе с дочерним
change tabular-parser.
- [x] 1. Контракт v1.2: `docs/contracts/recognition-result.schema.json` — `DISCLOSURE`, `statementDocument.checks`,
`osvDocument.header.period_start/period_end/is_from_year_start`, заголовок и описание «1.2» (design D-1); проверка
схемы валидатором JSON Schema на всех примерах.
- [x] 2. Примеры: `result-statement-minimal.json` (+`checks`, «1.2»), новый `result-statement-excel.json`,
`result-osv-minimal.json` (границы периода) (design D-2).
- [ ] 3. Спека `openspec/specs/recognition-contract` синхронизируется из дельты при архивации; до этого — дельта в change.
- [x] 4. `05-domain-rules.md` §12 — решения владельца и уточнения (design D-3); §7 — пометка жёстких/мягких соотношений.
- [x] 5. `06-test-data.md` — раздел `statements-xlsx/**`, манифест, golden `expected_statement_parse/`, синтетика.
- [x] 6. `03-architecture.md` — контракт 1.2, `BookParser`, порядок раскатки MINOR; `09-delivery-plan.md` — Срез 2b.
- [x] 7. `07-open-questions.md` — новые OQ (единицы против копеек; форма без колонки кодов; страница-продолжение в
отдельном файле; строка без кода с суммами в ОФР ФСБУ; образец ФСБУ «АКТИВ»+1110), статус OQ-131.
- [x] 8. `docs/dev/qa-guide.md` §6.7 — прогон парсера на формах; README `docs/contracts/` (если есть) — v1.2.