Andrey Epifancev be47e82e72 docs/openspec: перенумерация OQ-137…142 -> OQ-138…143 (R-15)
Номер 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
2026-10-06 21:28:25 +04:00

229 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-описаниях |