Номер OQ-137 в main docs занят вопросом о раскладке горячих папок; design: решение №26. Refs #14 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
229 lines
38 KiB
Markdown
229 lines
38 KiB
Markdown
# Design: Срез 2b — парсер ББ и ОФР из Excel
|
||
|
||
## Context
|
||
|
||
- Точка подключения прежняя: `get_parser(settings)` → `Parser.parse(data, file_type) -> ParseOutcome`; воркер
|
||
(`core/worker.py`) вызывает парсер в `asyncio.to_thread`, собирает конверт и пишет `results/{file_id}/{job_id}/result.json`.
|
||
`ParseOutcome` требует непустые `documents` при `done|needs_check`.
|
||
- Сейчас `OsvParser.parse` делает и оркестрацию книги (лимит, чтение, дедлайн, `classify_sheet`, цикл по листам), и
|
||
конвейер листа ОСВ (`parse_sheet`). Листы `SheetKind.STATEMENT` пропускаются с предупреждением.
|
||
- Контракт v1.1 уже содержит `statementDocument{document_type BALANCE|INCOME|OTHER, locator, header{form_name,
|
||
organization_name, inn, report_date_text, report_date, units, okud, okpo, okopf, okfs, address, *_confidence},
|
||
columns[{column_index, label, period_date}], lines[{line_code, line_name, *_confidence, values[{column_index, amount,
|
||
string_value, confidence, is_recognized}]}]}` — поля «как у OCR». Табличный путь заполняет `confidence = 100`,
|
||
`is_recognized = true` (§12).
|
||
- Реальные файлы (`../fixtures/statements-xlsx/**`, 15 файлов + 4 шаблона), три раскладки:
|
||
1. **1С-xls постранично** (Унагранде Компани, Унагранде Групп): один файл = одна страница формы; на странице 1 титульный
|
||
блок (организация, ИНН, ОКПО, ОКВЭД, ОКОПФ/ОКФС, единица «в тыс. рублей», ОКЕИ 384, адрес, аудитор с **своим** ИНН),
|
||
заголовок таблицы «Пояснения | Наименование показателя | Код | На 31 декабря 2024 г. | … 2023 | … 2022»; на странице 2
|
||
только «Форма 0710001 с.2» и та же строка заголовка таблицы. Под строками 1110/1150/1540 — пятизначные подстроки
|
||
11101, 11102 (1С «в том числе» без слов), есть строки без кода и без имени с нулями. Значения — рубли с копейками при
|
||
единице «тыс. руб.».
|
||
2. **Многолистовая xlsx ФСБУ 4/2023** (НПЗ): ББ на 5 листах, ОФР на 3; на первом листе титул «Бухгалтерская отчетность
|
||
за январь - сентябрь 2025 г.» и ниже заголовок «Бухгалтерский баланс | на 30 сентября 2025 г. | 0710001»; листы 2–5
|
||
начинаются сразу со строк («в том числе:», 11901, 1100, …) без заголовка; есть строка 1105 «Гудвил», подстроки
|
||
12301…12314, 23201…, в ОФР строка без кода с суммами («Реализация иного имущества…»).
|
||
3. **`TDSheet`** (Межениновская): вся форма на одном листе, заголовок «БУХГАЛТЕРСКИЙ БАЛАНС / за 9 месяцев 2025 г.»,
|
||
дата тройкой «30 | 9 | 2025» (числа, не строки), колонки «Наименование показателя | Коды | За 9 месяцев\n2025 года |
|
||
На 31 декабря\n2024 года | …», под шапкой строка нумерации «1 | 2 | 3 | 4 | 5», значения — тысячи с дробной частью.
|
||
4. **Шаблоны** 66н и ФСБУ 4/2023: заголовок таблицы разнесён на две строки («На» / «20__ г.»), коды есть, сумм нет,
|
||
лист содержит обе страницы формы с маркером «Форма 0710001 с. 2» посередине; в шаблоне ФСБУ строка «АКТИВ» и код 1110
|
||
стоят в одной строке из-за объединений (образец формы, не данные).
|
||
- Параллельно в ветках issues #9/#10/#11 правятся модули ОСВ (`header.py`, `classify.py`, `checks.py`, `hierarchy.py`,
|
||
словари ОСВ, golden ОСВ). Этот change их не трогает.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** все 15 реальных файлов разбираются без правил под файл, организацию, имя листа или номер строки; полные формы
|
||
(НПЗ, Межениновская) дают `done` с сошедшимися жёсткими соотношениями §7; постраничные xls дают документы с данными и
|
||
честный `needs_check` для страниц без шапки; формулы §7 и синонимы — данные; конвейер форм изолирован от ОСВ так, что
|
||
параллельные правки ОСВ не конфликтуют; контракт растёт только MINOR-ом. **Non-Goals:** см. proposal.
|
||
|
||
## Decisions
|
||
|
||
### D-1. `BookParser` — оркестратор книги, `OsvParser` — конвейер листа
|
||
`book.py`: `BookParser(osv: OsvParser, statements: StatementParser, rules: Rules, max_bytes, timeout_seconds, clock)`.
|
||
`parse()` повторяет нынешний `OsvParser.parse`/`_parse_book`: лимит `PARSE_MAX_BYTES` → `read_book` → `Deadline` → для
|
||
каждого листа `classify_sheet` → `OSV` в `osv.parse_sheet`, `STATEMENT`/`STATEMENT_CONTINUATION` накапливаются и
|
||
передаются `statements.parse_sheets(sheets_in_order, deadline)` **одним вызовом** (склейка продолжений требует видеть
|
||
соседние листы), `OTHER` → предупреждение. Документы книги — ОСВ в порядке листов, затем формы в порядке листов. Статус
|
||
`done`, если все документы `ok`; ни одного документа ОСВ или формы → `InvalidDocumentError` («в книге нет листов ОСВ или
|
||
форм»). `OsvParser` теряет `parse`/`_parse_book`, оставляет `parse_sheet` и свойства; `get_parser` собирает `BookParser`.
|
||
`NullParser` без изменений. *Альтернативы:* ветка внутри `osv.py` (смешение доменов, конфликты с #9–#11); два
|
||
`Parser`-а с композитом (книга читается дважды, дедлайн и лимиты делятся). *Цена ошибки:* `BookParser` тонкий,
|
||
переносится.
|
||
|
||
### D-2. Классификация листа — три вида для форм, признаки по области до таблицы
|
||
`detect.classify_sheet(sheet, rules) -> SheetKind` расширяется значением `STATEMENT_CONTINUATION`. Порядок проверки
|
||
прежний: признак ОСВ → `OSV`; иначе признак формы (заголовок формы или ОКУД из `statement_headers.yaml`) в любой строке
|
||
**до** первой строки, похожей на заголовок таблицы форм («Код»/«Коды» и справа ячейки-периоды) либо до первой строки с
|
||
кодом строки формы → `STATEMENT`; иначе маркер продолжения («Форма 0710001 с.2» / «с. 2») **или** строка заголовка
|
||
таблицы форм без заголовка формы **или** (без заголовка таблицы) колонка, где ≥ 60 % непустых значений — коды форм, →
|
||
`STATEMENT_CONTINUATION`; иначе `OTHER`. Ограничение «первые N строк» для признаков формы снимается: у НПЗ заголовок
|
||
формы стоит после титульного блока. Признак ОСВ по-прежнему ищется там же, где сейчас (модуль ОСВ не меняется).
|
||
|
||
### D-3. Тип формы и разрез листа
|
||
`statements/detect.py`: `find_forms(sheet, rules) -> list[FormBlock(kind: BALANCE|INCOME|DISCLOSURE, title_row,
|
||
okud, table_header_row, first_data_row, last_row)]`. Тип по заголовку/ОКУД из `statement_headers.yaml`:
|
||
«Бухгалтерский баланс» / 0710001 → BALANCE; «Отчет о финансовых результатах», «Отчет о прибылях и убытках» / 0710002 →
|
||
INCOME; «Пояснения к бухгалтерскому балансу», «Расшифровка», «Отчет об изменениях капитала» (0710003), «Отчет о
|
||
движении денежных средств» (0710004) и прочие ОКУД 0710xxx → DISCLOSURE. Титул «Бухгалтерская отчетность» — не тип: тип
|
||
берёт ближайший следующий заголовок. Два заголовка форм на листе → два блока, второй начинается со своего заголовка
|
||
(§12 п. 1). Если ОКУД и текст заголовка противоречат — предупреждение, тип по ОКУД. DISCLOSURE → документ с `locator`,
|
||
шапкой (если нашлась) и пустыми `columns`/`lines`; на статус книги не влияет.
|
||
|
||
### D-4. Заголовок таблицы, колонка кодов, колонка наименований, колонки-периоды
|
||
`statements/columns.py`. Строка заголовка таблицы — первая после заголовка формы строка, где есть ячейка «Код»/«Коды»
|
||
(синонимы в словаре) и правее неё хотя бы одна ячейка, матчащая период из `statement_periods.yaml`; при отсутствии
|
||
слова «Код» — строка, под которой начинается колонка кодов. Заголовок, разнесённый на две строки (шаблоны: «На» +
|
||
«20__ г.»; Межениновская: перенос строки внутри ячейки), склеивается: текст ячейки и текст ячейки той же колонки в
|
||
следующей строке, если та строка не содержит кодов и сумм. Строка нумерации колонок («1 2 3 4 5» подряд) пропускается.
|
||
Колонка кодов: среди колонок между наименованием и первой колонкой-периодом та, где ≥ 60 % непустых значений в блоке —
|
||
коды форм (`^\d{4}$` или `^\d{5}$` в диапазонах 1100–1799 / 2100–2999 и их пятизначных расширениях, плюс 5xxx для
|
||
расшифровок); значения читаются как текст (числа 1110.0 → «1110»). Нет такой колонки → документ без строк, `needs_check`,
|
||
предупреждение «колонка кодов не найдена» (решение владельца 06.10.2026, вопрос 6). Колонка наименований — ближайшая
|
||
левее кодов колонка с преобладанием текста; колонка «Пояснения» исключается по заголовку и по тому, что её значения —
|
||
пустые или короткие (≤ 3 символов). Колонки-периоды — все ячейки заголовка правее кодов с распознанным периодом, по
|
||
порядку слева направо: `column_index` 0, 1, 2…; `period_date` — дата («На 31 декабря 2024 г.», «31.12.2024») или
|
||
конец периода («За 9 месяцев 2025 г.» → 2025-09-30, «За январь - сентябрь 2025 г.» → 2025-09-30, «За 2024 г.» →
|
||
2024-12-31). Ячейки заголовка правее кодов без периода («Изменение», «%») не становятся колонками, их значения не
|
||
читаются, одно предупреждение на документ. `is_current` в контракте нет — ядро берёт самую позднюю дату.
|
||
|
||
### D-5. Шапка документа
|
||
`statements/header.py`: область — от начала блока формы до строки заголовка таблицы; для первого блока на листе — от
|
||
начала листа (титульный блок НПЗ над заголовком формы входит в шапку баланса); шапка другого листа MUST NOT
|
||
использоваться (у ОФР НПЗ нет метки «ИНН» → `inn = null`). Метки и регулярки — в
|
||
`statement_headers.yaml`; значение метки — ближайшая непустая ячейка правее в той же строке, иначе первая непустая ячейка
|
||
следующей строки (адрес, имя организации под меткой). Особые правила: `inn` — 10 или 12 цифр у метки «ИНН», строки после
|
||
метки «аудитор» исключаются; `organization_name` — после «Организация», а если справа пусто и строкой выше есть текст с
|
||
признаком ОПФ (ООО/АО/ПАО/ЗАО/кавычки) — он (НПЗ); `okopf`/`okfs` — у общей метки «по ОКОПФ / ОКФС» два числа по порядку;
|
||
`units` — текст после «Единица измерения» как есть, ОКЕИ 384/385 рядом только подтверждает; `report_date_text` — подзаголовок
|
||
формы («на 31 декабря 2024 г.», «за 9 месяцев 2025 г.», «за январь - сентябрь 2025 г.»), `report_date` — его дата или конец
|
||
периода; при отсутствии — тройка «число, месяц, год» у метки «Дата»/«Отчетная дата» (строки или числа). Для ОФР дата отчёта
|
||
— конец периода. `*_confidence = 100` там, где поле найдено, `null` — где нет. Блок продолжения без шапки → все поля
|
||
`null`, `form_name` из маркера «Форма 0710001 с.2» (тип по ОКУД в маркере).
|
||
|
||
### D-6. Строки и значения
|
||
`statements/lines.py`: строка блока с кодом в колонке кодов → `StatementLine(line_code=<текст без пробелов>,
|
||
line_name=<trim, переносы → пробел>, line_code_confidence=100, line_name_confidence=100)`; строки без кода (разделы,
|
||
«в том числе:», пустые с нулями, строка «Реализация иного имущества…» у НПЗ) не отдаются (`ASSUMPTION(OQ-36)`), их число —
|
||
в предупреждение «строк без кода пропущено: N» только если среди них есть суммы. Пятизначные подстроки отдаются как
|
||
обычные строки (решение владельца, вопрос 4). Повтор кода внутри документа → остаётся первая строка, код в
|
||
`checks.duplicate_line_codes`, `needs_check`. Значения: для каждой колонки-периода `StatementValue(column_index, amount,
|
||
string_value, confidence=100, is_recognized)`: число → `amount` через `numbers.parse_money` (Decimal, 2 знака, скобки →
|
||
минус, пробелы/NBSP убираются), `is_recognized = true`; пусто/«–»/«-» → `amount = null`, `string_value = null`,
|
||
`is_recognized = true`; нечисловой текст → `amount = null`, `string_value = <текст>`, `is_recognized = false`. Единицы не
|
||
нормализуются: если `header.units` содержит «тыс.» и хотя бы у 10 % значений дробная часть ≠ 0, в предупреждения пишется
|
||
«N значений с дробной частью при единице тыс. руб.» (решение владельца, вопрос 3; новый OQ в docs).
|
||
|
||
### D-7. Соотношения §7 — данные в `statement_checks.yaml`
|
||
Формат записи: `{form: BALANCE|INCOME, relation: "1600 = 1100 + 1200", strict: true|false, abs_terms: [2120, 2210, …]}`.
|
||
Вычисление в `statements/checks.py` — чистая функция над `Mapping[str, Decimal | None]` по колонке: отсутствующий код —
|
||
0; правая часть считается дважды — как записано и с членами `abs_terms` по модулю (1С показывает расходы со знаком минус,
|
||
66н — в скобках, оба варианта допустимы); соотношение выполнено, если сходится хотя бы один вариант с допуском ±1 в
|
||
единицах формы (`Decimal("1")`). Жёсткие (`strict`): ББ `1100 = 1110 + 1120 + 1130 + 1140 + 1150 + 1160 + 1170 + 1180 + 1190`
|
||
(+1105 при наличии, `ASSUMPTION(OQ-37)`), `1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260` (+1215), `1300 = 1310 + 1320 +
|
||
1340 + 1350 + 1360 + 1370`, `1400 = 1410 + 1420 + 1430 + 1450`, `1500 = 1510 + 1520 + 1530 + 1540 + 1550`, `1600 = 1100 +
|
||
1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700`; ОФР `2100 = 2110 − 2120`, `2200 = 2100 − 2210 − 2220`, `2300 = 2200 +
|
||
2310 + 2320 − 2330 + 2340 − 2350`. Мягкие (`strict: false`, только предупреждение): `2400 = 2300 − 2410 + 2420` и
|
||
варианты ФСБУ 4/2023 с 2411/2412/2460 (`ASSUMPTION(OQ-12)`), `2500 = 2400 + 2510 + 2520 + 2530`, и «сумма пятизначных
|
||
подстрок = родитель» (генерируется по коду: `1110 = 11101 + 11102 + …`, только когда подстроки есть). Проверка по каждой
|
||
колонке отдельно; результат — `StatementChecks(relations_failed[{column_index, relation, expected, actual}],
|
||
soft_relations_failed[…], duplicate_line_codes[])` + строки в предупреждения. Для форм без строк (нет колонки кодов,
|
||
DISCLOSURE) `checks` не заполняется.
|
||
|
||
### D-8. Склейка листов-продолжений внутри книги, статус документа
|
||
`statements/merge.py`: `parse_sheets` идёт по листам книги по порядку; блок `STATEMENT` открывает документ; лист
|
||
`STATEMENT_CONTINUATION` присоединяется к последнему открытому документу формы, если число колонок-сумм в нём совпадает
|
||
(или заголовка таблицы на листе нет и число колонок с числами совпадает) — строки добавляются в конец, `locator` =
|
||
`sheet:<первый>` + `,<следующие имена>` через запятую. Продолжение без открытого документа (книга из одной страницы 2,
|
||
лист ОСВ между страницами) → документ-одиночка: шапка пустая, тип по ОКУД маркера или `OTHER`, `needs_check`,
|
||
предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, вопрос 2; переход к
|
||
признаку `is_continuation` в контракте — две необязательных поля, без смены логики). Документ `ok` ⇔ все жёсткие
|
||
соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов ∧ есть хотя бы одна колонка-период ∧
|
||
в шапке есть организация или ИНН ∧ документ не одиночка-продолжение. Шаблоны (коды есть, сумм нет) → все значения
|
||
`null`, соотношения «0 = 0» сходятся, шапка пустая → `needs_check`.
|
||
|
||
### D-9. Контракт v1.2 — модели и копия
|
||
`api/src/contract/result.py`: `StatementDocument.document_type: Literal["BALANCE","INCOME","DISCLOSURE","OTHER"]`;
|
||
`StatementChecks` и `StatementDocument.checks: OptionalNotNull[StatementChecks] = None`; `FailedRelation(column_index,
|
||
relation, expected: Money, actual: Money)`; `OsvHeader.period_start: date | None`, `period_end: date | None`,
|
||
`is_from_year_start: bool | None`. `CONTRACT_VERSION = "1.2"`; копия схемы и примеры из `../docs/contracts` v1.2 (тест
|
||
идентичности). Заполнение новых полей шапки ОСВ: `period_start`/`period_end` — по разобранному периоду (квартал/год →
|
||
первое и последнее число; диапазон месяцев из issue #11 — его границы), `is_from_year_start` — `period_start` = 1 января.
|
||
Если ветка #11 сливается раньше и уже вычисляет границы диапазона, заполнение делается здесь одной задачей на его
|
||
результате; если позже — поля в этом change заполняются для квартала/года, а для диапазона — в #11. Поля необязательные,
|
||
оба порядка совместимы.
|
||
|
||
### D-10. Словари форм — отдельные файлы
|
||
`statement_headers.yaml` (заголовки форм и ОКУД по типам, маркер продолжения, метки шапки, синонимы «Код», «Наименование
|
||
показателя», «Пояснения», признаки ОПФ, метки-исключения «аудитор»), `statement_periods.yaml` (месяцы, формы «на <дата>»,
|
||
«за <N> месяцев <год>», «за <месяц> - <месяц> <год>», «за <год> г.», «<дд.мм.гггг>», «<год>»), `statement_checks.yaml`
|
||
(D-7). `periods.yaml` ОСВ не переиспользуется и не правится — ветка #11 его меняет. Загрузчик `rules.py` расширяется
|
||
моделями форм; тест покрытия словарей — на объединении файлов `statements-xlsx/**` и синтетики.
|
||
|
||
### D-11. Тесты, golden, синтетика
|
||
Unit (`tests/unit/parser/statements/`) на `gridbuilder`; property (`tests/fixtures/test_statements_property.py`, маркер
|
||
`fixtures`) по манифесту `tests/fixtures/statements_manifest.yaml` (каталог → организация, ИНН, ожидаемые типы и статусы,
|
||
полная форма или страница); golden `../fixtures/golden/expected_statement_parse/<org>/<period>/<file>.json` через
|
||
обобщённые `golden_helpers` и `make golden-update`; синтетика в `tests/synthetic/generate.py`: `st01_both_forms_one_sheet`,
|
||
`st02_csv_like`, `st03_no_codes`, `st04_mixed_book` (ОСВ + ББ), `st05_continuation_only`; линтер «нет координат»
|
||
включает `statements/**` и `book.py`; интеграционный тест воркера: книга НПЗ → `done`, `result_schema_version = "1.2"`,
|
||
два документа. CLI печатает формы так же, как ОСВ.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Колонка кодов не найдена из-за кодов-чисел или объединений] → коды читаются как текст из любого типа ячейки, merged
|
||
берётся по origin; синтетика `st02`. Остаточный риск закрывается фикстурой + синонимом, не `if`.
|
||
- [Соотношения §7 не сходятся из-за знаков 1С] → двойной расчёт с `abs_terms`; при несходимости обоих — `needs_check`,
|
||
перечень в `checks`; знак не угадывается.
|
||
- [Постраничные xls всегда `needs_check`] → осознанно (вопрос 2); ядро в Срезе 3 решит склейку; данные не теряются.
|
||
- [Конфликт с ветками #9–#11 в `osv.py`/`test_osv_parser.py`] → правило при rebase: логика ОСВ из их ветки, оркестрация
|
||
книги отсюда; PR создаётся после rebase на main.
|
||
- [Контракт 1.2 ломает идентичность копии в arm-core и scan-gateway] → bounded-PR с копией схемы и моделями раньше мержа
|
||
парсера; порядок merge docs → arm-core/scan-gateway → tabular-parser → fixtures.
|
||
- [Большие книги с формами] → те же лимиты и дедлайн, что у ОСВ; формы маленькие (≤ 100 строк на лист).
|
||
|
||
## Migration Plan
|
||
|
||
Не требуется: сервис без состояния. После merge — пересборка образа; потребители контракта обновляют копии раньше.
|
||
|
||
## Open Questions
|
||
|
||
Не блокируют: OQ-12 (знаки 2410/2420/2430 — мягкие проверки), OQ-36 (строки без кода — не отдаются), OQ-23
|
||
(расшифровки — только классификация), OQ-37 (1105 как есть), OQ-131 (границы диапазона — D-9). Новые вопросы вносятся в
|
||
`07-open-questions.md` сквозным change'ем: единицы «тыс. руб.» против значений с копейками в 1С-выгрузках; форма без
|
||
колонки кодов (сопоставление по именам); страница-продолжение в отдельном файле (склейка в ядре, признак в контракте);
|
||
строка без кода с суммами в ОФР ФСБУ (НПЗ «Реализация иного имущества…»); шаблон ФСБУ с «АКТИВ» и 1110 в одной строке.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| № | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| 1 | 2026-10-06 | D-2: продолжение определяется маркером «с.2» либо заголовком таблицы/колонкой кодов без заголовка формы | у НПЗ листы 2–5 без маркера, у 1С — с маркером | добавить признак в словарь |
|
||
| 2 | 2026-10-06 | D-3: 0710003/0710004 и прочие ОКУД 0710xxx → DISCLOSURE, не OTHER | DBML различает расшифровку и прочее; ОДДС/ОИК для консолидации не нужны | переназначить тип |
|
||
| 3 | 2026-10-06 | D-4: порог 60 % кодов в колонке и диапазоны 1100–1799 / 2100–2999 (+5xxx) | §12 п. 2; 1105 и 1215 внутри диапазона | подвинуть границы в словаре |
|
||
| 4 | 2026-10-06 | D-4: колонка заголовка без периода не читается, одно предупреждение | §12 п. 4 «в extra», но у `statementDocument` поля `extra` нет; MINOR ради диагностики не оправдан | добавить `extra` в v1.x |
|
||
| 5 | 2026-10-06 | D-5: ИНН аудитора исключается по метке «аудитор» выше | иначе у Унагранде второй ИНН в шапке | синоним в словаре |
|
||
| 6 | 2026-10-06 | D-6: порог 10 % значений с дробной частью для предупреждения о единицах | единичные копейки в тысячах бывают от округления | число в словаре |
|
||
| 7 | 2026-10-06 | D-7: допуск ±1 единица формы, отсутствующий код = 0, двойной расчёт с `abs_terms` | §7 «расхождение > 0», но формы в тыс. руб. округляются построчно; знаки 1С vs 66н | допуск в словаре |
|
||
| 8 | 2026-10-06 | D-7: «сумма подстрок = родитель» — мягкая проверка, генерируется по коду | подстроки бывают неполными («прочее» без кода) | перевести в строгие |
|
||
| 9 | 2026-10-06 | D-8: критерий `ok` документа включает «в шапке есть организация или ИНН» | без шапки ядро не сверит владельца; страница 2 должна быть `needs_check` | ослабить критерий |
|
||
| 10 | 2026-10-06 | D-9: `period_start/period_end/is_from_year_start` в шапке ОСВ добавляются в контракт здесь, заполнение делится с #11 | владелец: поля в v1.2 (вопрос 5) | follow-up на заполнение |
|
||
| 11 | 2026-10-06 | D-1: документы книги — сначала ОСВ, потом формы | детерминированный порядок для golden | порядок листов |
|
||
| 12 | 2026-10-06 | Контракт v1.2 раскатан в arm-core (PR #27) и scan-gateway (PR #5) копиями; в scan-gateway вместе с v1.2 догнан v1.1 (`OsvRow.extra`, пример `result-osv-needs-check.json`), `CONTRACT_VERSION` обоих репозиториев не менялся | копия схемы обязана быть байт в байт с docs; потребители версию не повышают, пока не пишут новых полей | откат отдельным коммитом, поле необязательное (R-2) |
|
||
| 13 | 2026-10-06 | Минор-находки ревью задач 1–2 по `$comment` схемы («1.1»), статусу примера excel и `checks` в OCR-примере minimal отложены (правка в docs/contracts + пересинхронизация копий) | план запрещал трогать `$comment`; примеры предписаны планом | косметика примеров; ядро/парсер не затронуты (T1.1, T2.2, T2.3) |
|
||
| 14 | 2026-10-06 | Golden `schema_version` 1.1 → 1.2 (113 файлов) пересозданы сразу и закоммичены в ветку `feat/14-statements-golden` worktree `fixtures-wt` (шаг 1 задачи 13 выполнен раньше), симлинк `fixtures` переключён на worktree; основной checkout fixtures не тронут | смена `CONTRACT_VERSION` ломала golden до задачи 14, `make check` был бы красным на задачах 4–13 | коммит в fixtures-ветке раньше плана; откат — reset ветки (R-3) |
|
||
| 15 | 2026-10-06 | `docs/contracts/**` после задачи 1 заморожены: в задаче 16 не правятся; T1.1/T2.1/T2.2/T2.3 остаются deferred и уходят в follow-up | копии схемы в arm-core#27 и scan-gateway#5 на ревью и должны остаться байт в байт равными docs/contracts на main | три косметических замечания живут до следующего MINOR (R-4, указание координатора) |
|
||
| 16 | 2026-10-06 | Находка T6.I1 (osv.py не знает SheetKind.STATEMENT_CONTINUATION) не чинится в задаче 6, закрывается заменой osv.py::parse на BookParser в задаче 7 | реальные фикстуры ОСВ листов-продолжений не содержат, промежуточное состояние невидимо | если T7 не покроет — поймает финальное ревью (R-5) |
|
||
| 17 | 2026-10-06 | Task 11: соотношение проверяется в колонке, только если присутствует его цель (строка-итог); отсутствующие члены = 0; подстрочные правила — когда подстроки есть | код и тесты плана противоречили друг другу (тесты 1/4 требуют пропуска цели); фрагменты форм (страница 1 ОФР без 2200) не должны давать ложных нарушений | итоговая строка, пропущенная при наличии членов, этим соотношением не ловится (R-6) |
|
||
| 18 | 2026-10-06 | Task 11: словарь `statement_checks.yaml` не ослаблен — жёсткие соотношения ББ 1100…1500 и ОФР 2100…2300 остаются `strict: true`; данные теста плана исправлены | спека D-7 и решение №7; ослабление словаря ради зелёного теста — не решение исполнителя | меняется одна строка yaml (R-7) |
|
||
| 19 | 2026-10-06 | Task 11: для подстрок «сумма подстрок = родитель» действует общий допуск ±1, не нулевой | спека: допуск ±1 без исключений; формы в тыс. руб. округляются построчно | расхождение в 1 единицу у подстрок не ловится (R-8) |
|
||
| 20 | 2026-10-06 | Task 11: `_RELATION/_TERM` допускают пятизначные коды справа (предписано планом), но явные правила из yaml валидатор загрузки ограничивает четырёхзначными | подстрочные правила генерируются кодом; спека: словарь — только четырёхзначные | пятизначный код в yaml отвергается при загрузке (R-9) |
|
||
| 21 | 2026-10-06 | Task 12: лист-продолжение без колонки кодов при непустом результате не теряется молча — книга получает `needs_check` (флаг `dropped`); продолжение с типом `OTHER` проверяется по правилам ББ; `form_name` продолжения без маркера не передаётся | данные не должны теряться при `done`; продолжение без шапки не имеет собственного названия | лишний `needs_check` на редком листе (R-10) |
|
||
| 22 | 2026-10-06 | Task 12: допуск соотношений не менялся: НПЗ (`npz/2025-Q3`) даёт `needs_check` — 1600 = 1700 расходятся на 1.94 / 9.02 / 2.48 (значения в рублях с копейками при заголовке «тыс. руб.», сам файл не сходится) | спека D-6/D-7: единицы не нормализуются, допуск ±1 в единицах формы; подгонка допуска под файл — решение владельца (OQ-138) | если владелец хочет `done` для НПЗ: допуск/нормировка единиц, одна строка в словаре; манифест в задаче 14 фиксирует `needs_check` с причиной (R-11) |
|
||
| 23 | 2026-10-06 | R-13: допуск ±1 не меняется; баланс НПЗ ожидаемо `needs_check` (значения в рублях с копейками при шапке «тыс. руб.», 1600 = 1700 расходятся на 1.94/9.02/2.48, OQ-138); решение владельца, вариант А | единицы не нормализуются (D-6), допуск ±1 (D-7) | если владелец передумает — допуск/нормировка в словаре |
|
||
| 24 | 2026-10-06 | R-14: ОФР НПЗ и ОФР Унагранде (стр. 1) по факту `needs_check` — рубли с копейками при тыс. руб., 2100 расходится на 17.50/9.43/1416; допуск ±1 не меняется | то же, что №23 (OQ-138) | нормировка единиц/допуск в словаре, если владелец хочет `done` |
|
||
| 25 | 2026-10-06 | O-9: лист ОФР НПЗ с одной строкой 2400 (`min_codes` = 3) не распознаётся как форма и пропускается — строка 2400 теряется при `needs_check` документа | отложено на финальное ревью | потеря строки 2400 у НПЗ; понизить `min_codes` или принимать лист-продолжение по маркеру |
|
||
| 26 | 2026-10-06 | R-15: наши OQ-137…142 перенумерованы в OQ-138…143 | номер OQ-137 в main занят вопросом о раскладке горячих папок (docs #41/#43) | ссылки в PR-описаниях |
|