Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
198 KiB
Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн
Issue: arm-ka-docs#116. Статус: финальная версия — целевая картина по шагам 1–9 согласована с владельцем
(решения Р-1…Р-19, 17.10.2026) и прошла три раунда независимого ревью (review-2026-10-17.md,
review-2026-10-17-closure.md, review-2026-10-17-blind.md, review-2026-10-17-closure-2.md,
review-2026-10-17-blind-2.md). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в 05-domain-rules.md, DBML v5.19)
написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (review-2026-10-18-spec.md, находки K-1…K-25) и
доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже), прошла повторное ревью вслепую
(review-2026-10-18-spec-2.md, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения
S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека.
Цель
Нормализация результата распознавания форм ББ/ОФР в scan-gateway (PDF → ИС Сканирование) и в tabular-parser (Excel) должна идти по одной цепочке шагов. У каждого шага — определённый результат, который передаётся в следующий шаг; результаты между шагами одинаковы для обоих сервисов (где это возможно). Все нестыковки собраны в таблицы различий по каждому шагу.
Зачем: нормализовать каждый шаг; убрать бойлерплейт в arm-core (повторная дедупликация, пересчёт проверок, статус, который ядро не может взять из результата); улучшить поддерживаемость (один словарь — одно место); улучшить пользовательский опыт заказчика (старые ошибки распознавания AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634 — свойство конвейера, а не отдельные баги, см. issue #116).
Как работает сейчас: цепочка шагов
Каждый шаг описан так: вход → что делает scan-gateway → что делает tabular-parser → выход → различия.
Код: scan-gateway api/src/core/worker.py, gateway/, normalizer/ocr/; tabular-parser api/src/core/worker.py,
parser/book.py, parser/readers/, parser/statements/. Правила: 05-domain-rules.md §7, §11, §11.1, §12.
Шаг 0. Загрузка и доставка до сервиса
Вход: файл пользователя.
Общая часть (arm-core): файл принимается в карточку ЮЛ или в консолидацию (primary_files.upload_one): тип — по
расширению и сигнатуре, дедуп по (ЮЛ, sha256), файл — в S3, запись stored_file и recognition_job (queued).
Конвейер: xls / xlsx / xlsb → tabular, PDF / PNG / JPG → image. Публикатор outbox собирает recognition-request
(job_id, file_id, pipeline, file_type, original_name, ссылка S3, reply_deadline, attempt) и публикует
в subject конвейера (files.tabular_uploaded / files.img_uploaded). Срок ответа по умолчанию: OCR — 20 мин,
парсер — 5 мин; нет ack 30 с — повтор, после трёх повторов — dlq, файл ERROR.
В сервисе (одинаково): запрос проверяется (JSON-объект, MAJOR версии 1, свой pipeline, схема; иначе
SCHEMA_MISMATCH без ack); повтор job_id в работе — только повторный ack; ack processing до чтения S3; файл
читается из S3 (не прочитан — FILE_READ_ERROR). scan-gateway дополнительно: бакет должен быть его единственным;
reply_deadline истёк до отправки — TIMEOUT.
Выход: байты исходного файла + запрос (file_type, original_name, reply_deadline).
Различия: нет, кроме сроков ответа и проверки бакета.
Шаг 1. Подготовка данных
Вход: байты файла.
1.1 Разбор и чтение сырых данных
scan-gateway — разбор делает ИС Сканирование:
- Файл пишется во «Входящие» горячей папки как
<job_id>.<ext>(нет расширения / папка недоступна —RECOGNITION_FAILED). - ИС кладёт в «Исходящие»
<job_id>/один или несколько json — по одному на распознанный комплект отчётности. - Опрос каждые
SCAN_POLL_SECONDS(5 с); набор готов, когда есть*.jsonи он не менялсяSCAN_SETTLE_SECONDS(2 с); срок —min(reply_deadline, запись + SCAN_TIMEOUT_SECONDS)(1200 с), иначеTIMEOUT. - Склейка: каждый json разбирается независимо, по возрастанию имени →
{job_id, files: [{name, response, parse_error}]}; сохраняется в S3raw/{job_id}.jsonдо нормализации; затем (только приSCAN_CLEANUP) папка очищается.
Содержимое одного json (по fixtures/ocr/*.json, §11): корень {file_uid, error {code, message}, data_ocr},
успех — error.code == ""; data_ocr: поля шапки label, organization, inn, units, date — каждое
{form_name, confidence} (сырой OCR-текст), одна шапка на json; table_structure[] — таблицы форм {label {form_name, confidence}, rows[{row_index, cells[{column_index, value, confidence}]}]}, column_index не обязательно
подряд. Наблюдается: json с таблицами содержит ББ и ОФР одного периода под общей шапкой. Допущение владельца
(17.10.2026): несколько периодов в одном файле ИС отдаёт разными json; образца нет — проверить на первом таком файле.
tabular-parser — чтение делает парсер:
- Файл больше
PARSE_MAX_BYTES(50 МБ) —UNSUPPORTED_FORMAT;PARSE_TIMEOUT_SECONDS(120 с) — от начала разбора. - Формат — по сигнатуре байтов (OLE2 → XLS, zip с
xl/workbook.bin→ XLSB, иной zip → XLSX, прочее —UNSUPPORTED_FORMAT); несовпадение сfile_type— предупреждение; лимиты распакованного zip и площади объединённых ячеек. - Книга → листы
Sheet: непустые ячейки (текст, значение, числовой формат), объединённые диапазоны, уровни группировки, габариты. Библиотека упала —UNREADABLE; xlsb без стилей и группировки — предупреждение.
1.2 Базовая разметка
scan-gateway — разметку сделала ИС, шлюз её принимает: ошибочный json (parse_error, непустой error.code, корень
не объект) — предупреждение <имя>: <код>: <сообщение> и пропуск; все json ошибочны — error с кодом первого. Шапка
json: ИНН — только цифры, организация, дата (двойники + разбор), единицы; уверенность ИС у каждого поля;
предупреждения уровня файла «ИНН не распознан» / «ИНН распознан неуверенно» / «дата отчёта не разобрана». Каждая
таблица table_structure[i] — кандидат в документ; тип — по label (rules/statement_labels.yaml).
tabular-parser — разметка по содержимому (§12 п. 1, 6): вид листа (признак ведомости в первых 30 строках —
ОСВ, отдельный путь; заголовок формы или ОКУД — форма; маркер «Форма … с.2», шапка «Код» с периодами, колонка кодов
или 1–2 кода сразу после листа формы — продолжение; иначе прочее с предупреждением). Формы на листе: строки с
заголовком («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД —
кандидаты; без кодов между ними сливаются; заголовок после кодов — новая форма. Тип по ОКУД (0710001 → BALANCE,
0710002 → INCOME, прочие 0710xxx и «Пояснения» → DISCLOSURE), заголовок спорит с ОКУД — ОКУД и предупреждение.
Шапка своя у каждой формы в границах блока: организация, ИНН (без ИНН аудитора), ОКПО, ОКОПФ, ОКФС, единицы,
дата, адрес; найдено — 100, нет — null; предупреждения «в шапке не найдена организация» / «…единица измерения».
Листы-продолжения присоединяются к последней открытой форме — но решение требует раскладки колонок (шаг 2).
Выход: список кандидатов в документы форм — признак типа, шапка, сырые строки таблицы (строки × ячейки).
Различия (шаг 1):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Кто размечает | ИС Сканирование | парсер, по содержимому |
| 2 | Единица разметки | json (комплект одного периода) → таблицы | книга → листы → блоки форм |
| 3 | Шапка | одна на json, общая для его таблиц | своя у каждой формы |
| 4 | Тип формы | label таблицы (уточнение по кодам — шаг 6) |
ОКУД, иначе заголовок |
| 5 | Уверенность | у каждого поля шапки и каждой ячейки, 0–100 | нет (найдено — 100) |
| 6 | Многостраничная форма | одна таблица = одна форма, склейки нет | листы-продолжения присоединяются (шаг 6) |
| 7 | Ячейка | сырой OCR-текст + confidence |
текст, значение и числовой формат |
Шаг 2. Раскладка таблицы
Вход: сырые строки таблицы одной формы.
scan-gateway (layout.py, rows.parse_table_rows, §11.1 п. 3): шапка — строка с ячейкой, начинающейся на
«код»; наименование — ячейка «наименование», иначе ближайшая заполненная слева от кода; колонки значений — правее
кода, где в тексте четыре цифры года или текст начинается на «на » / «за ». Повторная шапка внутри таблицы
перезаписывает раскладку (число колонок изменилось — предупреждение; значения строки берутся по позиции, лишние
отбрасываются, недостающие — «ячейки нет»). Без шапки раскладка выводится по данным: код — столбец с наибольшим
числом значений из справочника кодов, наименование — левее с наибольшим числом текстов, значения — правее, где
суммы не реже мусора; шапка синтезируется, колонки без дат, предупреждение. Ни шапки, ни столбца кода — таблица
пропускается («колонка кодов не найдена»). Колонка: label — текст шапки, period_date — разбор даты после замены
букв-двойников, только если текст «размером с дату»; нет даты — «колонка N без даты».
tabular-parser (columns.find_layout, §12 п. 2–4): колонка кодов — столбец с наибольшим числом кодов (4–5
цифр, первые четыре в диапазоне формы), кодов ≥ 3 (у продолжения ≥ 1) и доля среди непустых ≥ порога; нет —
документ без строк, «колонка кодов не найдена». Строка заголовка — последняя над первым кодом с подписью «Код», иначе
последняя с двумя и более периодами правее кодов (или одним не голым годом); двухстрочный заголовок склеивается;
строка нумерации «1 2 3 4 5» — часть заголовка. Колонки-периоды — правее кодов с распознанным периодом по словарю
statement_periods.yaml; подпись без периода («Изменение», «%») пропускается, одно предупреждение со списком.
Наименование — левее кодов с наибольшим числом текстов, кроме «Пояснения». Без заголовка (продолжение): колонки
значений — где в строках с кодами есть числа, подпись пустая, дата null.
Выход: раскладка формы — столбец кода, столбец наименования, колонки значений [{label, period_date}], первая
строка данных, строки, которые не данные (шапка, нумерация).
Различия (шаг 2):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Что считается кодом при поиске столбца | столбец — по справочнику кодов (infer_layout), строка — любое целое (int(text)) |
4–5 цифр в диапазоне формы (словарь) |
| 2 | Повторная шапка внутри таблицы | перезаписывает раскладку | не ищется (одна раскладка на блок) |
| 3 | Дата колонки | «размером с дату» + двойники + parse_date |
словарь периодов period_end |
| 4 | Без шапки | синтез шапки, колонки без дат, предупреждение | колонки по числам, подпись пустая |
| 5 | Строка нумерации «1 2 3 4 5» | не распознаётся (становится строкой данных, AURORA-1517) | часть заголовка, в данных пропускается |
| 6 | Подпись без периода | колонка значений, «без даты» | колонка пропускается, предупреждение |
Шаг 3. Отбор строк данных
Вход: раскладка + сырые строки.
scan-gateway (rows.py, §11.1 п. 4): склеенная строка («1500\n1700» с N частями в суммах) разлепляется на N
(OQ-156, предупреждение-счётчик). Классификация по ячейке кода: пустой код — пропуск (есть суммы — счётчик «строк без
кода с суммами пропущено»); нечисловой код («4 400\nI #4У», «ИЧ,250») — пропуск (счётчик «строк с нечисловым
кодом пропущено»); целое число — строка данных (вид ROW / TOTAL / BALANCE по наименованию — дальше не
используется). Повтор кода — остаётся первая, предупреждение «код N повторяется: оставлена первая строка». Пропущенные
строки не восстанавливаются.
tabular-parser (lines.collect_lines, §12 п. 5): пустые строки пропускаются; код — 4–5 цифр в диапазоне, иначе
строка «без кода»: нумерация «1 2 3 4 5» — пропуск; без кода с ненулевыми суммами — счётчик «строк без кода с суммами
пропущено»; повтор кода — остаётся первая, код в duplicates.
Выход: строки данных формы — сырой код, сырое наименование, ячейки значений по колонкам; счётчики отброшенных.
Различия (шаг 3):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Код-мусор | отдельный счётчик «нечисловой код» | не отличается от «без кода» |
| 2 | Склейка двух строк в одну | разлепляется (OQ-156) | нет такого случая |
| 3 | Повтор кода | предупреждение текстом | checks.duplicate_line_codes |
| 4 | Отброшенная строка | только счётчик; строка исчезает (AURORA-1634, 1462) | только счётчик; строка исчезает |
Шаг 4. Значения ячеек
Вход: ячейки значений строки.
scan-gateway (amounts.parse_amount, document._value, §11.1 п. 7): пробелы удаляются первыми; запятая /
точка как разделитель тысяч по шаблонам (4,200, 837.994); запятая → точка; скобки → минус; из строки выбрасывается
всё, кроме цифр, +, -, .; больше одной точки — все удаляются; больше двух знаков после точки — amount = null,
текст в string_value. Прочерк / пусто — null. is_recognized = confidence == 100 и при разборе не
выбрасывались символы. Ячейки под колонкой нет — amount = null, confidence = 0, is_recognized = false.
tabular-parser (lines._value, statement_money, number_format.display_amount, §12 п. 5): число с числовым
форматом — как показывает формат ячейки (масштабирующие запятые → тысячи, число знаков, секции и условия,
округление half-up; формат не разобран — сырое значение и одно предупреждение на документ); иначе to_money:
Decimal, скобки → минус, прочерк / пусто → null, нечисловой текст → string_value, is_recognized = false.
confidence всегда 100. Счётчики дробных значений: при «тыс.» и ≥ 10 % дробных — предупреждение.
Выход: StatementValue {column_index, amount, string_value, confidence, is_recognized}.
Различия (шаг 4):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Предобработка текста | OCR-мусор: пробелы, двойники, тысячи, скобки } / ] |
числовой формат ячейки Excel |
| 2 | Смысл is_recognized |
уверенность 100 ∧ ничего не выброшено | текст разобрался как число |
| 3 | «42831 1» | пробел удалён → 428311, «чистое» (AURORA-1511) | не бывает |
| 4 | Единицы | не проверяются | предупреждение о дробных при «тыс.» |
| 5 | Пустая ячейка там, где была колонка | null, confidence ИС |
null, 100 |
Шаг 5. Код, наименование, редакция строки
Вход: строка данных с сырым кодом и наименованием.
scan-gateway (document._line, _choose_edition, §11.1 п. 6): редакция формы — по дате отчёта, иначе по самой
поздней дате колонок, иначе по кодам (предупреждение); код очищается до букв, цифр, пробела и точки; наименование —
из справочника редакции по коду (api/rules/statement_lines.yaml шлюза), уверенность кода и имени = уверенность
ячейки кода, если текст чистый и код в справочнике, иначе min(conf, 99); кода нет в справочнике — имя из OCR со
своей уверенностью, код min(conf, 99), предупреждение «код N из редакции …», если он есть в другой.
tabular-parser (lines.collect_lines, _line_name): код как найден (4–5 цифр); наименование — из файла:
ячейка колонки наименований, если пуста — ближайшая текстовая левее кодов, кроме «Пояснения»; уверенность кода 100,
имени 100 (пусто — null). Редакции и справочника нет.
Выход: StatementLine {line_code, line_code_confidence, line_name, line_name_confidence, values}.
Различия (шаг 5):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Источник наименования | справочник по коду (AURORA-1406, 1522) | файл |
| 2 | Редакция (66н / ФСБУ 4) | определяется, влияет на имена | не определяется |
| 3 | Справочник строк | свой statement_lines.yaml (не совпадает с ядром) |
нет |
| 4 | Уверенность кода | от ИС, срезается до 99, если не в справочнике | всегда 100 |
Шаг 6. Сборка документа
Вход: раскладка, строки, шапка кандидата.
scan-gateway (document.build_document, _resolve_type): тип — по label; все коды одной группы и противоречат
label — тип по кодам с предупреждением; label не сопоставлен — по большинству кодов. Колонки — из первой шапки.
locator = file:<json>;table:<i>. Шапка — поля json (общая).
tabular-parser (statement._document, merge): лист-продолжение присоединяется к последней открытой форме,
если число колонок сумм совпало и тип в маркере не противоречит (слабое продолжение с 1–2 кодами — ещё и коды из
диапазона формы); не присоединился — одинокое продолжение с пустой шапкой и предупреждение. locator = sheet:<A>,<B>. DISCLOSURE и форма без колонки кодов — без columns / lines.
Выход: StatementDocument {document_type, locator, header, columns, lines} (без checks).
Различия (шаг 6):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Тип формы | label, сверка с кодами |
ОКУД / заголовок, с кодами не сверяется |
| 2 | Склейка страниц | нет | листы-продолжения |
| 3 | locator |
file:…;table:i |
sheet:A,B |
Шаг 7. Самопроверки
Вход: документ.
scan-gateway: не делает; checks отсутствует («не проверялось»).
tabular-parser (statements/checks.run_statement_checks, §12 п. 7): соотношения §7 по каждой колонке из
statement_checks.yaml: жёсткие → relations_failed, мягкие → soft_relations_failed; допуск ±1, округление
0,001 % уводит жёсткое в мягкие; abs_terms; «родитель = Σ подстрок» только при непустой детализации (AURORA-1527);
повторы кодов → duplicate_line_codes; предупреждения текстом на каждое нарушение.
Выход: StatementChecks или ничего.
Различия (шаг 7): есть только у парсера; словарь §7 у парсера и ядра — две копии; ядро результат парсера не читает, считает заново.
Шаг 8. Статус и предупреждения
Вход: документы и накопленные предупреждения.
scan-gateway (_Build.fully_recognized, _normalize_file, normalize): документ «полностью распознан», если ни
одного предупреждения, все ячейки is_recognized, уверенность кода и имени 100, ИНН и организация с уверенностью 100,
дата распознана. Файл — все документы распознаны ∧ нет предупреждений файла. Конверт done ⇔ все файлы распознаны ∧
ни один json не ошибочен; иначе needs_check; ни одной таблицы — error RECOGNITION_FAILED. Предупреждения —
плоские строки <json>: table:<i>: <текст> в metrics.warnings.
tabular-parser (statement._is_ok, book.parse): документ ok ⇔ колонка кодов найдена ∧ не одинокое
продолжение ∧ есть колонки ∧ в шапке организация или ИНН ∧ checks есть ∧ нет жёстких нарушений ∧ нет повторов
кодов; DISCLOSURE всегда ok. Книга done ⇔ все документы ok ∧ ни один лист не потерян; книга без листов ОСВ и
форм — INVALID_DOCUMENT. Предупреждения — строки sheet=<имя> <текст> в metrics.warnings; на статус не влияют.
Выход: ResultStatus конверта + metrics.warnings[].
Различия (шаг 8):
| № | scan-gateway | tabular-parser | |
|---|---|---|---|
| 1 | Критерий done |
всё с уверенностью 100 и без предупреждений | структура найдена, проверки сошлись |
| 2 | Влияние предупреждений | любое → needs_check |
не влияют |
| 3 | Статус документа | нет (только конверт) | внутренний ok, в контракт не попадает |
| 4 | Формат предупреждений | <json>: table:<i>: … |
sheet=<имя> … |
| 5 | Привязка предупреждения | к файлу | к документу (по префиксу) |
Общее для обоих: статус один на конверт; предупреждения — свободный текст без привязки к строке или ячейке; ядро и экран не могут показать, что именно и где не так.
Шаг 9. Результат
Одинаково: RecognitionResult (schema_version, job_id, file_id, status, documents, metrics) → S3
results/{file_id}/{job_id}/result.json → конверт в files.tabular_parsed с Nats-Msg-Id = result:<job_id>:<attempt>.
У шлюза в конверте ещё raw_s3_key.
Что с результатом делает ядро (для полноты картины)
result_mapping.map_file→statement_mapping.build_statement: повторно отбрасывает повторы колонок, кодов и значений (защита от обоих производителей), пишетfinancial_statement_*как пришло.statement_status(D-6):RECOGNIZED⇔ конвертdone∧ проверки §7 пересчитаны ядром и сошлись ∧ ИНН неMISMATCH∧ период известен ∧ все ячейки с уверенностью 100 иis_recognized∧ есть строки ∧ нет предупреждений маппинга; иначеNEEDS_CHECK.checksпарсера не читаются;metrics.warningsсервиса на статус не влияют.RECOGNIZEDсразу автоподтверждается.GET …/statement: §7 считается заново по словарю ядра (копия словаря парсера); недостающие строки достраиваются изstatement_lines.yamlядра (не совпадает со справочником шлюза); предупреждения в ответе отсутствуют.- Экран: ячейка подсвечивается только по
confidence < 100илиis_recognized = false; нарушения §7 — списком без привязки к ячейкам; предупреждения сервиса и маппинга нигде не показываются.
Бойлерплейт и потери, которые порождены различиями шагов 2–8: три копии словарей (§7 в парсере и ядре, строки в шлюзе и ядре); двойная дедупликация; пересчёт проверок; статус, который ядро собирает заново из косвенных признаков; три несвязанных канала качества (уверенность ячейки, проверки §7, предупреждения), из которых пользователь видит полтора.
Целевая картина
Каждый шаг ниже: вход = выход предыдущего шага, единый выход для обоих сервисов, список «различие → к чему
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца собраны в конце
(Р-1…Р-19, все приняты 17.10.2026). Текст прошёл три раунда независимого ревью (review-2026-10-17*.md рядом с этим
файлом); находки внесены. Имена полей — рабочие, уточняются при написании спеки.
Принципы, из которых выведены решения по строкам:
- Один выход на шаг. Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1). Единственное место, где общий код смотрит на источник, — разбор запятой и точки в сумме (шаг 4).
- Ничего не выбрасывать молча. Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
статусом, а не превращается в счётчик; пользователь её видит и исправляет. Положение в источнике (
ref), порядок строк (row_index) и сырой текст ячейки (text) доезжают до базы и экрана. Подтвердить документ, не разобрав строки с суммами без кода, нельзя (Р-13). - Качество — структурно и с привязкой к месту: уверенность ячейки (от источника),
is_recognized(разбор числа), статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность,is_recognizedи §7 сервис сводит в один признакneeds_reviewу значения, строки и поля шапки (шаг 8) и из них выводит статус документа. Ядро при загрузке не пересчитывает ни проверки, ни статус, ниneeds_review: хранит их как пришли и добавляет только предупрежденияinfoпо своему словарю имён. После правок аналитика ядро считает тем же общим кодом, добавляя своё знание: правка или подтверждение снимаетneeds_review, акцепт снимает жёсткие нарушения, квартал периода, сверка ИНН и наименований с справочником — условияRECOGNIZED. - Один словарь — одно место: проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, упорядоченные
перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде сервисов;
соответствие «код → наименование» — только в ядре, сервисы его не знают и отдают распознанный текст (Р-3). Порядок и
состав редакций ядро берёт из своей копии общего кода, словарь ядра хранит только имена;
sync-statements --checkсверяет, что все коды словаря ядра есть в перечне. - Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста в общем коде, там, где уже
известен смысл ячейки: дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных строк
(шаг 3), сумма (шаг 4). Буквы-двойники («О» → 0, «З» → 3, «б» → 6, «S» → 5, «I», «l», «|» → 1) заменяются в датах,
подписях, кодах и суммах (Р-11, отменяет §11.1 п. 1 и решение № 22 среза 3a «только даты»): сумма с заменой
получает
is_recognized = false, код с заменой считается неуверенным (эффективная уверенность ниже 100), а дальше их судьбу решают контрольные соотношения §7 и структурная проверка кода. В суммах крайние «|» (след границы таблицы: «| : 122477») убираются, а не заменяются. ИНН двойники не трогают: только цифры, как сейчас — иначе «3|1 21 2|» из aep_3 даёт лишние единицы иMISMATCHс ЮЛ. Разбор вызывается для любой ячейки безvalue; у Excel он срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Изnormalizer/ocr/шлюзаtext,dates,amountsстановятся общими разборами,layout,rows,header,document— шагами 2–6 общего кода.
Шаг 1 — Подготовка данных
Решения владельца: 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт; 3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100; 6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат.
Вход: байты файла (шаг 0).
Единый выход: список кандидатов в форму на файл, FormCandidate:
source—ocr|excel;locator— откуда взят:file:<имя json>;table:<i>(как в §11 и golden-эталонах) /sheet:<A>[,<B>];type_hint—BALANCE|INCOME|DISCLOSURE|null, по единому правилу (п. 4);evidence— ОКУД и заголовок как ячейки (для сверки с кодами в шаге 6);header— поляheaderконтракта (organization_name,inn,report_date,units,okud,okpo,okopf,okfs,address), каждое — ячейка илиnull;rows— плотная сетка строк × ячеек; все страницы и листы формы уже в одной сетке; пустая позиция —null;segment_breaks— номера строк сетки, с которых начинается новый лист Excel (у OCR пусто): граница листа — всегда граница сегмента раскладки (шаг 2);warnings— предупреждения шага (формат — шаг 8).
Ячейка Cell одна для шапки и таблицы: text (как показывает источник), value (число или дата, если источник их
знает; у OCR — null), confidence 0–100 (у Excel — 100), ref — позиция в источнике: OCR — row:<row_index>,
Excel — <лист>!<A1>. Уникальность в документе требуется от ref строки (шаг 3), не ячейки.
- 1. Кто размечает — остаётся, фиксируется полем
source- scan-gateway: без изменений
- tabular-parser: без изменений
- 2. Единица разметки — файл → список кандидатов; у OCR каждая таблица ИС — кандидат, у Excel — каждый блок формы на листе; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
- scan-gateway: адаптер «таблица json → кандидат»
- tabular-parser: адаптер «блок формы на листе → кандидат»
- 3. Шапка — у каждого кандидата своя; поля = поля
headerконтракта, каждое —Cell; разбор текста поля — общий код поtext: ИНН → только цифры, без двойников; дата → словарь периодов (шаг 2 п. 3), который включает все шаблоны сегодняшнегоdates.pyшлюза; единицы → словарь. Уверенность и сырой текст каждого поля доезжают до контракта (*_confidence,*_text)- scan-gateway: шапку json копирует в каждую свою таблицу
- tabular-parser: добавляет сырой текст и уверенность 100
- 4. Тип формы — одно правило: ОКУД → заголовок по одному словарю →
null; окончательно — по кодам строк в шаге 6, у обоих- scan-gateway:
labelтаблицы становится заголовком;statement_labels.yamlуходит в общий словарь - tabular-parser: добавляет сверку по кодам (шаг 6)
- scan-gateway:
- 5. Уверенность — поле
Cell.confidence; Excel всегда 100- scan-gateway: без изменений
- tabular-parser: добавляет поле
- 6. Многостраничная форма — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в
segment_breaks; тип из маркера сверяется сtype_hint, расхождение — отдельный кандидат + предупреждениеerror. Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы (как_weak_blockerсейчас) и, при трёх и более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат с предупреждениемerror. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения- scan-gateway: без изменений
- tabular-parser: склейка переезжает до раскладки;
join_blockerсохраняет проверки по типу, диапазону кодов и числу колонок
- 7. Ячейка — один формат
Cell; Excel:text— как показывает числовой формат,value— число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR:textсырой,valuenull,confidenceот ИС- scan-gateway: обёртка над
value+confidenceИС - tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4
- scan-gateway: обёртка над
Шаг 2 — Раскладка таблицы
Вход: FormCandidate (сетка, segment_breaks, type_hint).
Единый выход: TableLayout — сегменты сетки (участки между повторными шапками и границами листов), у каждого:
столбец кода, столбец наименования, колонки значений [{col, label, label_confidence, period_end, period_kind}],
сопоставленные с первым сегментом по порядку; роль каждой строки сетки (title | header | numbering | data).
Диапазоны кодов (словарь, границы уточняются в спеке): ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала 3100–3699, отчёт о движении денежных средств 4100–4499, отчёт о целевом использовании 6100–6499, пояснения 5000–5999. «Кодовые диапазоны» для поиска столбца — объединение этих отрезков (годы 2000–2099 и четырёхзначные суммы на 1–6 вне отрезков кодами не считаются). «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца; им проверяются разлепление строк (шаг 3), слабые продолжения и наследование столбца в сегментах.
- 1. Столбец кодов — одно правило: код — текст без пробелов, переносов и краевой пунктуации («1220 ,» → 1220), после замены двойников (порядок: убрать пробелы и переносы → двойники → проверка; «|» → 1 только если результат попадает в кодовый диапазон: «| 140» → 1140), 4–5 цифр (допускается
.0), первые четыре в кодовых диапазонах. Многострочная ячейка — код, если каждая её часть код. (а) Столбец, над данными которого стоит подпись «Код» / «Коды» / «Код строки» (словарь подписей) и под которой доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; несколько кандидатов — столбец с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под подписью «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267 у АО, 12300 у ООО — по первым четырём цифрам это «коды баланса»), ОКЕИ: один код из девяти, доля 0,11. Документ из двух строк остаётся документом: два из двух, доля 1,0. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6 (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком- scan-gateway: столбец по подписи получает защиту по доле; запасной подсчёт — по кодовым диапазонам, как сейчас; классификация строк
int(text)→ статусы - tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → кодовые диапазоны
- scan-gateway: столбец по подписи получает защиту по доле; запасной подсчёт — по кодовым диапазонам, как сейчас; классификация строк
- 2. Повторная шапка и границы листов — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) или граница листа из
segment_breaks; у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1, и если сегмент не первый и кодов в нём меньше трёх, ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (такrelaxed+_weak_blockerпарсера сейчас: лист «_2» НПЗ с единственной строкой 2400, у которого столбец кодов в другой колонке, чем на листе 1); колонки значений без шапки — по числам в строках с кодами, сопоставление с первым сегментом по порядку; число не совпало — предупреждениеerror, значения по позиции; столбец кодов не найден ни так, ни так — предупреждениеerrorсегмента, строки сегмента не теряются (рольdata, статусы по шагу 3)- scan-gateway: как сейчас, через общее правило
- tabular-parser: бывший «лист-продолжение со своей раскладкой» становится сегментом; бывшая проверка числа колонок в
join_blocker— предупреждение сегмента
- 3. Дата колонки — один словарь периодов (
statement_periods.yaml), объединяющий сегодняшний yaml парсера и шаблоныdates.pyшлюза: «На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «за I квартал 2025 г.», «1 полугодие 2026», «с 01.01.2025 по 31.03.2025», «2024», двузначный год, хвост «0:00:00», клетки «2|0|2|4|»; строкиtest_dates.py::test_parses_real_formsшлюза — обязательные тесты словаря. Результат —period_endиperiod_kind: абсолютные подписи →absolute; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») →relative:<вид>, дата считается от эффективной даты документа: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок; нет ни той ни другой →period_end = null,period_kindостаётся относительным, ядро досчитает после правки даты отчёта; подписи нет →period_kind = none. Подпись OCR с уверенностью ниже 100, разобранная в дату, подтверждается, если дата равна одной из ожидаемых (эффективная дата, минус год, минус два года) — иначеerror«дата колонки неуверенна». Перед разбором — очистка двойников OCR. Относительные подписи — Р-6: это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»- scan-gateway:
parse_date→ словарь, его шаблоны переезжают в словарь - tabular-parser: добавляет очистку двойников (no-op), относительные подписи и шаблоны шлюза
- scan-gateway:
- 4. Строка заголовка и её отсутствие — одно правило (обе ветки парсера сохраняются): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Роль
title— только строки выше найденной строки заголовка (титульный блок «Дата 30 | 9 | 2025» Межениновской); в сегменте без заголовка ролиtitleнет, строки выше первого кода с содержимым идут в данные (шаг 3). Заголовка нет ни у одного сегмента документа → колонки значений первого сегмента = столбцы правее кода, где в строках с кодами стоят числа;labelпустая,period_endnull,period_kind = none; одно предупреждениеerror«строка заголовка не найдена». Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки (п. 2) без предупреждения: листы 2–5 ББ НПЗ без шапки остаютсяdone- scan-gateway: синтез шапки уходит (он эквивалентен); добавляет ветку «заголовок по периодам»
- tabular-parser: добавляет предупреждение для документа без единого заголовка
- 5. Строка нумерации «1 2 3 4 5» — одно правило
is_numbering_row→ рольnumbering, в данные не попадает- scan-gateway: добавляет (AURORA-1517)
- tabular-parser: как сейчас
- 6. Подпись без периода — Р-6: подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением
info; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть,period_end = null,period_kind = none, предупреждениеerrorс привязкой к колонке, документneeds_check; аналитик ставит дату колонке через PATCH или подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает для периода формы (эффективная дата, п. 3), но межформенная проверка и гудвилл «на отчётную дату» по такой колонке не считаются, пока дата не проставлена. Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением- scan-gateway: добавляет словарь исключений и относительные периоды
- tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды
- 7. Столбец наименования — одно правило: подпись «Наименование (показателя)» в строке заголовка → этот столбец; иначе столбец левее кодов с наибольшим числом текстов, кроме столбца «Пояснения»; наименование строки — ячейка этого столбца, пусто → ближайшая текстовая ячейка левее кода, кроме «Пояснения»
- scan-gateway: «наименование», иначе слева от кода → общее правило
- tabular-parser:
_name_column/_line_name→ общее правило
Шаг 3 — Отбор строк данных
Вход: FormCandidate + TableLayout.
Единый выход: RawLine[] — по строке на каждую строку сетки с ролью data, у которой есть содержимое: код,
число или непустой текст в колонке значений с уверенностью ниже 100 (признак потерянной суммы: aep_3 row 8 «[190 :»
с «Ё», «" :»). У строки: row_index (порядковый номер в документе), ref (уникален в документе; у разлепленных строк
row:<i>#<k>), ячейка кода, code_status, ячейка наименования, ячейки значений по колонкам раскладки сегмента,
parent_ref (для detail). Статусы (ok | unreadable | missing | detail):
ok— код разобран;unreadable— ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка кода без единой цифры («}», «:») считается пустой;detail— Р-8: строка без кода с содержимым, стоящая под строкой с кодом (в том же сегменте или в предыдущих сегментах документа — родитель продолжается через разрыв страницы), — расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар (41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит, в проверки §7, агрегаты и критерийdoneона не входит, экран по умолчанию не показывает. Отменяет OQ-36 / OQ-141 («строки без кода не отдаются»); OQ-149 («восстановление строк по наименованию») остаётся открытым;missing— строка без кода с содержимым, над которой во всём документе нет строки с кодом (кода не было или он потерян в самом начале таблицы: «Выручка» aep_3, где 2110 уехал в ячейку значения); строки ролиtitleсюда не попадают.
Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков
отброшенных нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких
строк с разными наименованиями, все они ok. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в
detail и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя (цель соотношения
отсутствует при непустых членах — нарушение, шаг 7), аналитик раскроет расшифровки и назначит код (Р-13) — осознанная
цена Р-8.
- 1. Код-мусор — статус
unreadable, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода — по правилу шага 2 п. 1- scan-gateway: счётчик → статус
- tabular-parser: отличает «нет кода» от «не разобран»
- 2. Склейка двух строк в одну — одно правило, выполняется до разбора кода: ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156) с
refrow:<i>#<k>; разлепление делается, если хотя бы одна часть кода разобралась как код диапазона столбца («44 30\nI |» aep_6 не множится: 4430 не в диапазоне ББ), нечитаемая часть становится строкойunreadable; число частей не совпало или ни одна часть не код — одна строка, многострочное значение →amount = null,string_value= текст,is_recognized = false. Значения разлепленных строк получаютis_recognized = falseи нуждаются в покрытии проверками §7 (Р-17: сошлись — подсветки нет, иначеneeds_review); у Excel не срабатывает- scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое; статус документа после разлепления — по общему правилу, а не безусловный
needs_check - tabular-parser: правило общее, no-op
- scan-gateway: как сейчас, через общее правило; разлепление при нечитаемой части — новое; статус документа после разлепления — по общему правилу, а не безусловный
- 3. Повтор кода — Р-4: повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом
ok; текстовое предупреждение иchecks.duplicate_line_codesуходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит структурная проверка кода: повтор, у которого хотя бы одна копия с уверенностью ниже 100, —needs_reviewу этой строки (Р-16)- scan-gateway: «оставлена первая» → все строки
- tabular-parser:
duplicatesуходит
- 4. Отброшенная строка — строки не отбрасываются:
unreadable/missing/detailидут дальше со статусом,row_indexиref(AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча; подтвердить документ, пока уunreadable/missingесть суммы, нельзя (Р-13). Отмена OQ-36 / OQ-141 — Р-8- scan-gateway: счётчики → статусы
- tabular-parser: счётчики → статусы; строки без кода с суммами начинают отдаваться
Шаг 4 — Значения ячеек
Вход: ячейки значений RawLine по колонкам раскладки (Cell или null).
Единый выход: StatementValue {column_index, amount, string_value, text, confidence, is_recognized}: text —
сырой текст источника всегда, как Cell.text; string_value — как сейчас, только когда сумма не разобрана
(amount = null, непустой текст); needs_review досчитывается в шаге 8.
- 1. Предобработка текста — у ячейки после шага 1 либо есть
value, либо толькоtext.value— число →amount = value, квантование до 2 знаков half-up, не больше 16 целых цифр (numeric(18,2)), иначеamount = null,string_value= текст,is_recognized = falseи предупреждение;value— дата или логическое →amount = null,string_value= текст,is_recognized = false;valueнет → одинparse_amount(text, source)для обоих: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак, скобки → минус, причём}и]принимаются как закрывающая скобка (OQ-147), прочерк →null; разделители: уocrзапятая — тысячи при группе из трёх цифр (AURORA-1405, sg#35) и точка — тысячи при группах из трёх цифр («837.994», sg#37), текст с тремя и более знаками после разделителя —amount = null,string_value= текст (решение № 30 среза 3a сохраняется); уexcelзапятая и точка — десятичный разделитель («4,200» = 4,20, «1.500» = 1,5, какto_moneyсейчас); «лучшая догадка» из цифр ограничена 16 цифрами, иначеnull; многострочное значение в строке с однострочным кодом →amount = null,string_value= текст,is_recognized = false; Excel-ячейки «число текстом» идут тем же путём. Разбор ОСВ (to_moneyвclassify.py) общимparse_amountне заменяется — он остаётся за пределами этой работы- scan-gateway:
parse_amountпереезжает в общий код и получает квантование, предел длины,}/], «−», тонкие пробелы - tabular-parser:
to_moneyдля форм заменяется общимparse_amountсsource = excel
- scan-gateway:
- 2. Смысл
is_recognized— один: текст разобран как число без выброшенных и исправленных символов (замена двойника — исправление;}/]вместо скобки — тоже);confidence— отдельно, от источника; оба признака сервис сводит вneeds_review(шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему- scan-gateway: отвязывает от
confidence - tabular-parser: как сейчас
- scan-gateway: отвязывает от
- 3. «42831 1» — в
parse_amountпробел допустим только как разделитель тысяч (группы ровно по три цифры); иначеamount— лучшая догадка (цифры подряд),is_recognized = false, сырой текст — вtext(AURORA-1511, 1519)- scan-gateway: меняется через общий код
- tabular-parser: то же правило, у Excel почти не срабатывает
- 4. Единицы — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа
info(как у парсера сейчас, документ остаётсяdone); то же сочетание у книги xlsb без числовых форматов —error: вероятен масштаб ×1000 (рубли с копейками, которые формат переводил в тысячи), а §7 сходится при любом масштабе- scan-gateway: добавляет
- tabular-parser: как сейчас; xlsb-сочетание — новое
- 5. Пустая ячейка — Р-2: позиции нет, текст пуст или прочерк →
amount = null,string_value = null,is_recognized = true,confidenceячейки (нет ячейки — 100), собственной подсветки нет. Поfixtures/ocrИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100; непрочитанные — пустой текст с уверенностью 0, и такие нуждаются в покрытии по общему правилу шага 8 (снимается покрытием или подтверждением ячейки аналитиком); позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждениемerrorшага 2 п. 2- scan-gateway: «ячейки нет → 0, не распознано» уходит
- tabular-parser: как сейчас
Шаг 5 — Код, наименование, редакция
Вход: RawLine (статус, row_index, ref, ячейки кода и наименования) + StatementValue[].
Единый выход: StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence, parent_ref, values}: line_code — разобранный код (4–5 цифр) или null при
unreadable / missing / detail; line_code_text — сырой текст ячейки кода; line_code_confidence — от источника,
а у кода с заменой двойника — ниже 100 (неуверенный); line_status = code_status шага 3; parent_ref — у detail.
Несколько строк с одним кодом — обычные строки ok, порядок как в файле.
- 1. Источник наименования — Р-3: сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк, сверка для неуверенных кодов — Р-16). Экран показывает справочное имя, файловое — рядом (AURORA-1406, 1522)
- scan-gateway: имя из
statement_lines.yaml→ имя из OCR - tabular-parser: как сейчас
- scan-gateway: имя из
- 2. Редакция (66н / ФСБУ 4) — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования и достройка шаблонных строк — ядро по его словарю имён; проверка «код не из редакции формы» — ядро по своей копии перечня общего кода
- scan-gateway:
_choose_edition→ выбор редакции перечня кодов в общем коде - tabular-parser: добавляет выбор редакции перечня кодов
- scan-gateway:
- 3. Справочник строк — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется
- scan-gateway: удаляет
api/rules/statement_lines.yaml - tabular-parser: без изменений
- scan-gateway: удаляет
- 4. Уверенность кода — от источника (ИС / 100), без срезания до 99 по справочнику; «код не в справочнике» — проверка ядра, предупреждение
infoк строке, не сервисов- scan-gateway: убирает
min(conf, 99) - tabular-parser: как сейчас
- scan-gateway: убирает
Шаг 6 — Сборка документа
Вход: FormCandidate (type_hint, evidence, header) + TableLayout + StatementLine[].
Единый выход: StatementDocument {document_type, form_variant, locator, header, columns, lines} без checks.
- 1. Тип формы — одно правило: большинство кодов в диапазоне ББ →
BALANCE, ОФР →INCOME, прочих форм и пояснений →DISCLOSURE; совпало сtype_hintилиtype_hintnull— тип по кодам; противоречит — тип по кодам + предупреждениеerror; кодов нет —type_hint. Без колонки кодов:type_hintесть → документ без строк (DISCLOSURE—done, иначеneeds_checkс предупреждениемerror);type_hintнет: уocrкаждая таблица ИС с содержимым — потерянный документ, предупреждениеerrorфайла (сегодня шлюз: «нет шапки и столбца кода» →needs_check), уexcelлист «прочее» —info.DISCLOSUREпоtype_hint— шаги 2–7 не выполняются;DISCLOSUREпо кодам — шаги 2–5 выполнены, строки и колонки отбрасываются (ядро строки пояснений не хранит), проверки не считаются, документdone. Вариант формыform_variant—full|simplified, только по структуре кодов (ОКУД для этого не годится: 0710096 — код КНД комплекта упрощённой отчётности, на бланках стоят 0710001 / 0710002): ББ — есть 1600 и нет ни 1100, ни 1200; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300; шаг 7 и перечень кодов берут упрощённый набор. Образец упрощённой формы — в fixtures при первой возможности- scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант и
errorза потерянную таблицу - tabular-parser: добавляет сверку с кодами и упрощённый вариант
- scan-gateway: как сейчас, через общее правило; добавляет упрощённый вариант и
- 2. Склейка страниц — решена в шаге 1, различия нет
- scan-gateway: без изменений
- tabular-parser: без изменений
- 3.
locator— одна грамматика «парыключ:значениечерез;», набор ключей по источнику:file:<имя json>;table:<i>/sheet:<A>[,<B>]— ключи как сейчас, golden-эталоны и §11 не меняются- scan-gateway: без изменений
- tabular-parser: без изменений
Шапка документа — поля header кандидата как есть (text → *_text, value → поле, confidence → *_confidence
у всех полей).
Шаг 7 — Самопроверки
Вход: StatementDocument.
Единый выход: StatementChecks у обоих: relations_failed[], soft_relations_failed[] (каждое нарушение — с
кодами слагаемых и column_index, чтобы экран подсветил ячейки). Правила счёта:
- значение кода в колонке — сумма по всем строкам
okс этим кодом (повтор кода — законный случай, шаг 3 п. 3); строкиdetail,unreadable,missingв соотношения не входят;duplicate_line_codesуходит; нарушение подсвечивает все строки кода; - соотношение считается посчитанным, если непуста цель или хотя бы один член; пропускается, только когда пусто
всё (ни строки
okс кодом цели, ни одного непустого члена). Пустая цель при непустых членах — жёсткое нарушение («БАЛАНС» без кода, ушедший вdetail, делает 1600 невидимым, и это должно подсветить родителя); непустая цель при пустых членах — тоже нарушение (лист только с активом: «1600 = 1700» при отсутствующем 1700 —needs_check, как сегодня у трёх файлов манифеста парсера). Сегодня оба сервиса и ядро соотношение с отсутствующей целью пропускают — поведение меняется; посчитанное соотношение из всехnullничего не подтверждает (то же правило, что для «родитель = Σ подстрок» по AURORA-1527); - «сошлось» / «не сошлось»: жёсткое соотношение не сошлось, если расхождение больше допуска ±1 и больше
округления 0,001 % — только тогда оно в
relations_failed; расхождение в пределах округления и мягкие соотношения — вsoft_relations_failed, этоinfo, на статус и подсветку не влияют (OQ-138 сохраняется); - соотношение с членами по модулю (
abs_terms: 2120, 2210, 2220, 2330, 2350, 2410) сходится при любом знаке такого члена и потому подтверждает только величину, не знак — см. «Подтверждение проверкой»; - набор соотношений выбирается по
form_variant: полный или упрощённый (нет подытогов 1100 / 1200 / 1300 у ББ, 2100 / 2200 / 2300 у ОФР, свои равенства), оба — в одном словареstatement_checks.yaml; - родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
из первых четырёх цифр (
subline_parents, §12 п. 7: 12310 под 1230).
Проверки по словарю имён (код не в словаре) и по перечню кодов (код не из редакции формы по эффективной дате) — в
ядре, как предупреждения info к строке; сервисы словаря имён не знают (Р-3, Р-12).
- 1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает — проверки — общий код с одним словарём
statement_checks.yamlу обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читаетchecksиз результата, хранит их (document_issue, см. «Что уходит из ядра») и пересчитывает тем же кодом только после правок аналитика (PATCH)- scan-gateway: добавляет шаг
- tabular-parser:
run_statement_checks→ общий код
Шаг 8 — Статус и предупреждения
Вход: StatementDocument + StatementChecks + предупреждения шагов 1–7.
Единый выход: у каждого документа status (done | needs_check), warnings[], needs_review у каждого
значения, каждой строки и каждого поля шапки; у файла metrics.warnings[]; статус конверта — агрегат.
Предупреждение — структура {code, severity: error | info, message, line_refs?: [], column_index?, field?}, а не
строка; line_refs — список, потому что нарушение подсвечивает все строки кода.
- 1. Критерий
done— одна функция общего кода с входами «раскладка, шапка, статусы строк,needs_review, нарушения, предупреждения, подтверждения и акцепт аналитика»: раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта или самая поздняя абсолютная дата колонок —statement_periodядра; у ИС полеdateчасто мусор: у aep_6 и «БО 1 кв 2025» там название организации) ∧ все строкиokилиdetail∧ нет значений, строк и полей шапки сneeds_review(см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений уровняerror.DISCLOSURE—doneбез условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически; структурная проверка кодов к Excel не применяется (Р-12). Квартал периода, статус конверта, сверка ИНН и наименований — знание ядра, вdoneне входят- scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
- tabular-parser:
_is_ok→ общий критерий; добавляются строки и значения
- 2. Влияние предупреждений — по уровню:
error→needs_check,info— не влияет; у обоих одинаково. Таблица уровней, полная версия — в спеке.error: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a),BACKEND_PARSING_ERRORтаблицы, таблица ИС с содержимым без колонки кодов и без типа (уровень файла), сегмент с содержимым без колонки кодов (в том числе потерянный лист-продолжение), лист с тремя и более кодами и другим числом колонок, не присоединившийся к форме, колонки сегмента не совпали, колонка без даты, дата колонки неуверенна, заголовка нет ни у одного сегмента, тип по кодам противоречит заголовку, тип маркера продолжения противоречит форме, ни организации, ни ИНН в шапке, период не определён (нет ни даты отчёта, ни дат колонок), дата отчёта неуверенна и не совпала с датами колонок, xlsb без форматов при «тыс. руб.» и дробных.info: ИНН не найден или неуверен при найденной организации (сверку с ЮЛ делает ядро —MISMATCH; НПЗ ОФР без ИНН остаётсяdone), организация не найдена или неуверенна при найденном ИНН, дата отчёта не разобрана при наличии дат колонок, единицы не найдены (аналитик правит в шапке), формат файла не совпал сfile_type(файл прочитан верно), подпись из словаря исключений пропущена, дробные при «тыс. руб.» (кроме xlsb без форматов), xlsb без числовых форматов само по себе (tp#42, документ остаётсяdone), лист Excel «прочее» безtype_hintи кодов, разлеплена склеенная строка, код не в словаре имён или не из редакции (ядро). Принцип:error— только то, что меняет данные или их полноту- scan-gateway: «любое →
needs_check» уходит - tabular-parser: предупреждения начинают влиять
- scan-gateway: «любое →
- 3. Статус документа — в контракте:
document.statusу каждого документа, включая ОСВ (у ОСВdone, когда ведомость разобрана, как сейчас; её предупреждения —info); конвертdone⇔ все документыdone∧ нет предупреждений файла уровняerror; иначеneeds_check; документов нет —errorс кодом по источнику:RECOGNITION_FAILEDу шлюза,INVALID_DOCUMENTу парсера — это различие остаётся, коды описывают разные причины. Статус конверта в условиеRECOGNIZEDядра не входит, туда входят предупрежденияerrorуровня файла (Р-19)- scan-gateway: добавляет
- tabular-parser: внутренний
ok→ в контракт
- 4. Формат предупреждений — структура (см. выше) вместо строк
<json>: table:<i>: …/sheet=<имя> …у всех документов результата, включая ОСВ; место — вlocatorдокумента, а не в тексте- scan-gateway: меняет
- tabular-parser: меняет, включая путь ОСВ
- 5. Привязка — к документу, строкам (
line_refs), ячейкам (line_refs+column_index) или полю шапки (field);refстроки уникален в документе и хранится в ядре вместе сrow_index, поэтому экран показывает предупреждение у места и после сохранения- scan-gateway: меняет
- tabular-parser: меняет
Подтверждение проверкой. Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет. По каждому
значению сервис считает needs_review:
- значение нуждается в покрытии, если
confidenceниже порога (Р-5: 100) илиis_recognized = false(число собрано из грязного текста, с заменой двойников или из разлепленной строки — Р-7, Р-11, Р-17); чистые значения (уверенность 100 иis_recognized = true) в покрытии не нуждаются: правило покрытия и правило знака к ним не применяются; - значение покрыто, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
(в пределах допуска или округления), и
amountсамого значения неnull(мусор, давшийnull, покрытым не бывает); - знак: для члена соотношения с
abs_termsпокрытие подтверждает только величину. Знак подтверждён, если форма записи знака у ячейки (скобки, минус, без знака — поtext) совпадает с конвенцией колонки — формой записи у большинства членовabs_termsэтой колонки с непустымamount,is_recognized = trueи без правки аналитика (правленые ячейки в голосование не входят); не совпадает или сравнивать не с чем —needs_review = trueнезависимо от соотношения. Так «(7744536}» с}как закрывающей скобкой даёт −7 744 536 и подтверждается, если 2210 и 2220 в той же колонке тоже в скобках; «{38378)» при остальных в скобках — подсвечивается; needs_review = true, если значение нуждается в покрытии и не покрыто (или не подтверждён знак);trueу всех ячеек жёсткого соотношения в колонке, где оно не сошлось вне допуска и округления, независимо от уверенности и чистоты; мягкие соотношения и расхождения в пределах округления подсветки не дают;- иначе
false: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст (text) и уверенность остаются в подсказке ячейки.
Экран подсвечивает ячейки только по needs_review. Ядро хранит needs_review и text как пришли и пересчитывает
needs_review вместе с checks общим кодом после PATCH (конвенция знака считается от сохранённого text), добавляя
своё: правленое значение (is_modified) или подтверждённое аналитиком без изменения (is_confirmed, хранится) —
needs_review = false; акцепт нарушений — жёсткие нарушения не считаются; предупреждения error документа
сохраняются и после правки, кроме снимаемых соответствующей правкой (таблица — в «Что уходит из ядра»). Не покрыто
проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя,
DISCLOSURE, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие ошибки в одном
соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без ущерба для 1519.
Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста (Р-7): случайное совпадение требует второй,
компенсирующей ошибки в том же равенстве.
Поля шапки. У каждого поля шапки свой needs_review: дата отчёта с уверенностью ниже 100 подтверждена, если
совпала с самой поздней абсолютной датой колонок, иначе needs_review = true и предупреждение error «дата отчёта
неуверенна» — иначе неверно прочитанный год давал бы неверный период и автоподтверждение; ИНН и организация с
уверенностью ниже 100 — info, их сверяет ядро (ИНН — MISMATCH с ЮЛ, организация — по алиасам); единицы и прочие
поля на статус не влияют. Сегодня шлюз требует уверенность 100 у ИНН и организации и распознанную дату — правило
становится точнее, не слабее.
Код строки (Р-5, Р-12, Р-16). Уверенность кода подтверждается не §7 (перепутанные соседи 1220 и 1230 дают ту же
сумму раздела), а структурой и наименованием; правило применяется только к неуверенным кодам — уверенность ниже
100 или замена двойника (Р-12): у Excel и чистых OCR-кодов расхождение с перечнем даёт предупреждение info ядра «код
не из редакции», а не needs_check — иначе пятизначные подстроки 1С (11101, 23201) и нестандартные строки клиента
(1231) блокировали бы автоподтверждение. В общем коде лежит упорядоченный перечень кодов каждой формы по
редакциям (66н, ФСБУ 4) и вариантам (полная, упрощённая) без наименований — порядок бланка, где итоги стоят после
слагаемых (1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Источник перечня — объединение кодов шаблонов
банка (словарь ядра) и кодов печатных форм 1С по редакции: 1105, 1530, 2420, 2460, 2930 и другие, которых в словаре
ядра нет. Алгоритм: четырёхзначные коды строк документа образуют последовательность; для каждой проверяемой
редакции берётся наибольшая подпоследовательность, согласованная с порядком бланка; редакция — та, где она длиннее
(при равенстве — по эффективной дате); если таких подпоследовательностей несколько, подтверждены только коды, общие
для всех; коды с уверенностью 100 участвуют в последовательности как подтверждённые якоря. Пятизначный код
подтверждён, если подтверждён его родитель (правило шага 7) и подстрока стоит после него. Равные соседние коды
допустимы, но повтор кода, у которого хотя бы одна копия неуверенна, — needs_review у неуверенной строки (Р-16).
Неподтверждённый неуверенный код — needs_review у строки (StatementLine.needs_review), документ — needs_check.
Структура не ловит подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120, когда 1120 на
бланке пропущен): это закрывает ядро — для строк с неуверенным кодом условие RECOGNIZED включает совпадение
распознанного наименования со справочным по коду (Р-16; порог сходства — в спеке), расхождение подсвечивает строку
через document_issue. По fixtures/ocr коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как
шлюз сейчас, нельзя. Перечень кодов — не словарь строк: наименований в нём нет, Р-3 не нарушается; ядро пользуется
своей копией перечня для проверки «код не из редакции», словарь ядра хранит только имена. Уверенность наименования
на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть.
Шаг 9 — Результат
Одинаково, как сейчас; контракт recognition-result.schema.json → v2.0 (Р-9: по правилам контракта смена типа
поля — MAJOR). Что меняется: line_code nullable, line_status, line_code_text, row_index, ref,
parent_ref, needs_review у значения, строки и полей шапки, values[].text, header.*_confidence и *_text у
всех полей; document.status у всех документов, включая ОСВ, document.form_variant; document.warnings[] и
metrics.warnings[] — структуры с уровнем и привязкой у всех документов, включая ОСВ; columns[].period_kind и
label_confidence; checks обязателен для BALANCE / INCOME со строками у обоих конвейеров (у документа без строк
отсутствует), нарушения с кодами слагаемых, без duplicate_line_codes; service.statements_version — коммит копии
общего кода, которым посчитан результат. Выкладка: ядро поднимает SUPPORTED_MAJOR и на время перехода принимает
1.x и 2.0 (два маппера, ack тоже 1.x и 2.0; при приёме 1.x статус документа считается по прежнему правилу D-6 —
job_done ∧ проверки ∧ values_ok ∧ …, новые поля заполняются по умолчанию: line_status = ok, row_index по
порядку, text = string_value), сервисы переходят на 2.0 после ядра — порядок «сначала ядро» обязателен, иначе
первый же результат с null в line_code даёт файлу ERROR. Запрос recognition-request остаётся 1.x. Путь ОСВ
в парсере меняется только в части структуры предупреждений и document.status.
Что уходит из ядра и что в нём меняется
build_statement: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок; повторы кодов — законные строки, хранятся все как пришли).- DBML v5.19 и одна миграция:
financial_statement_line— снять уникальность(meta_id, line_code), добавитьrow_index(у строк из источника; у шаблонных и созданных правкой —null),source_ref(частичный уникальный индекс по(meta_id, source_ref)там, где неnull),line_status,line_code_text,parent_row_index(родительdetail),needs_review,is_confirmed(подтверждение кода аналитиком);financial_statement_value—source_text(изvalues[].text),needs_review(как пришло от сервиса, пересчитывается ядром после PATCH; пересматривает решение v5.13 «считается, не хранится»),is_confirmed(подтверждено без изменения, хранится вместе сconfirmed_by / at);financial_statement_column.period_kind,label_confidence;financial_statement_meta.form_variant,needs_reviewполей шапки; таблицаdocument_issue(видwarning|relation|soft, код, уровень, сообщение, списокline_id,column_id?,field?, источникservice|core) — хранит предупреждения и нарушения как пришли; при перезагрузке файла очищается; при пересчёте после PATCH записиrelation/softзаменяются целиком независимо от источника, записиwarningисточникаserviceсохраняются, кроме снимаемых правкой;recognition_job.statements_version;document— снять уникальность(stored_file_id, document_type, osv_account_code)только дляBALANCE/INCOME(частичный индекс: ОСВ по счёту иDISCLOSURE/OTHERостаются «один на тип на файл», §9.5 п. 2) и добавить порядковый номер формы в файле (Р-10); в ту же миграцию —crm_idГСК / ГСЗ (AURORA-1619, arm-core#112). Заполнение при миграции:row_indexсуществующих строк — по порядку кода,line_status = ok,needs_reviewзначений — по прежнемуvalues_ok,source_text = string_value. - Повторы кодов (Р-4): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки, сверки), документ
читается через одну функцию «агрегат формы по коду» — сумма значений строк
okпо колонке, списокline_id, справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где сегодня «одна строка на код»:_line_amount(межформенная проверка 1370 / 2400),statement_amounts,subline_parents/parent_code,formLayout(дети под родителем),_resolve_cells,metrics.fields_*. statement_status(D-6) при приёме 2.0:RECOGNIZED⇔document.status = done∧ нет предупрежденийerrorуровня файла (потерянный json или таблица ИС, решение № 28 среза 3a;needs_checkсоседнего документа само по себе не блокирует — Р-19) ∧ ИНН ≠MISMATCH∧ период — конец квартала (period_known: иначе документRECOGNIZED, но не подтверждаемый, 409PERIOD_REQUIRED) ∧ у строк с неуверенным кодом распознанное наименование совпало со справочным (Р-16) ∧ в файле нет второй формы того же типа и периода (Р-18: при двух и более — ни одна не автоподтверждается). Остальные признаки D-6 не пересчитываются.- §7 при загрузке:
checks,needs_reviewи предупреждения берутся из результата и хранятся (document_issue); ядро добавляет толькоinfoпо словарю имён и перечню кодов и свою сверку наименований для неуверенных кодов. Пересчёт общим кодом — только после PATCH, с правиламиis_modifiedилиis_confirmed⇒needs_review = falseу значения;line_code_modifiedилиis_confirmed⇒needs_reviewстроки = false; акцепт ⇒ жёсткие нарушения сняты. Версия общего кода из результата (statements_version) пишется вrecognition_job; при пересчёте ядро сравнивает её со своей копией, расхождение — предупреждениеinfoк документу (на окне выкладки «сначала ядро» расхождение штатно). - Какая правка какое
errorснимает: PATCHheader.report_date— «период не определён», «дата отчёта не разобрана», «дата отчёта неуверенна»; PATCHheader.innилиorganization_name— «ни организации, ни ИНН»; PATCHcolumns[].period_date— «колонка без даты», «дата колонки неуверенна»; назначение кода или отметкаdetail— нарушения, пересчитываемые §7. Остальныеerrorсервиса (колонки сегмента не совпали, заголовка нет, тип противоречит, потерянный json, лист или таблица, xlsb с масштабом) снимаются только перезагрузкой файла. - PATCH
…/statement(Р-13): помимоheaderиvaluesпринимаетvalues[].confirmed(подтвердить ячейку без изменения значения:is_confirmed,needs_review = false,is_modifiedне ставится, аудит),columns: [{column_id, period_date}](дата колонке без подписи или с неуверенной подписью; пересчитываетis_current;report_date: nullобнуляетperiod_endотносительных колонок) иlines: «назначить код» (line_id,line_code), «отметить расшифровкой» (line_id,line_status = detail), «подтвердить код» (line_id,confirmed). Переходы:unreadable/missing/detail→okназначением кода;unreadable/missing/ok→detailотметкой;ok→okс другим кодом (смена кода, прежний вline_code_original_value);okнеуверенный → подтверждён. Правила: код 4–5 цифр, может совпадать с существующим (Р-4); незнакомый словарю —info; назначенный или подтверждённый код снимаетneeds_reviewстроки,line_code_confidenceстановится 100,line_code_modified,line_code_original_value; уdetailродитель пересчитывается как ближайшая строкаokвыше поrow_index; шаблонная строка с таким кодом, если она уже создана правкой аналитика с суммами, — 409LINE_EXISTSс подсказкой (аналитик сначала очищает её суммы или выбирает другой код), без сумм — удаляется, её место занимает настоящая; пара «line_status = okбез кода» — 422; удаления строк нет (первичка не мутируется,detail— способ вывести строку из сумм). Строки, созданные правкой:row_index = null,source_ref = null,line_status = ok. АудитRECOGNIZED_DATA_EDITсbefore / afterпо коду, статусу, родителю и подтверждению. Один PATCH — один пересчёт и одна запись. - Подтверждение документа (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4:
PERIOD_REQUIRED→LINES_UNRESOLVED(есть строкиunreadable/missingс непустымamount; строки без сумм не мешают) →DUPLICATE_FORM(в файле уже подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §9.5 п. 3) → жёсткие нарушения без акцепта. АвтоподтвержденияLINES_UNRESOLVEDне касается (doneтаких строк не допускает).detailв агрегат, КФО и снимок ЕХ не попадают — цена Р-8. - Правка даты отчёта (PATCH шапки): для колонок с
period_kind = relative:<вид>ядро досчитываетperiod_endобщим кодом от новой даты. - Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь имён строк (код → наименование по редакциям) остаётся только в ядре, копия шлюза удаляется; порядок и состав редакций ядро берёт из своей копии перечня общего кода.
- Достройка шаблонных строк: по словарю ядра; код, у которого есть хотя бы одна строка
okиз файла, шаблоном не достраивается. - Экран (Р-14): раскладка остаётся «как на бумаге» — разделы, внутри строки
okпо возрастанию кода, несколько строк одного кода в порядке файла, шаблонные строки на своих местах, пятизначные подстроки под родителем. Строкаunreadable/missingставится под ближайшей предыдущей поrow_indexстрокой, которая разместилась по коду, а без такой — в начало первого раздела; подсвечена целиком, в ячейке кода серым сырой текст с подсказкой. Строкиdetailскрыты под переключателем «Показать расшифровки (N)», как пустые строки детализации, показываются под родителем с отступом. Подсветка ячеек только поneeds_review(уверенность,is_recognizedи нарушения §7 уже сведены сервисом), в подсказке —textи уверенность; поля шапки сneeds_reviewподсвечены; предупреждения и нарушения — изdocument_issue, с привязкой к месту. В режиме «Редактировать»: ячейка кода уunreadable/missing— поле ввода с подсказкой из справочника (код → справочное имя), меню строки «Это расшифровка»; уdetail— «Назначить код»; у обычной строки — «Сделать расшифровкой», смена кода и «Подтвердить код» у неуверенного; подтверждение ячейки без правки; дата у колонки без подписи. Кнопка «Подтвердить данные» при неразобранных строках с суммами отключена с подсказкой «Разберите строки без кода: N» и переходом к первой; сервер отвечает тем же 409. - Открытый вопрос: склейка двух половинок формы из одного файла не предоставляется, файл перезагружают — в
07-open-questions.md(Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для неуверенных кодов это условиеRECOGNIZED, для остальных — только показ справочного имени.
Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-43 — 18.10.2026)
-
Р-1 (шаг 1, п. 2) — копия по скрипту. Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и диапазоны кодов (без словаря имён) со своим загрузчиком — живёт в отдельном репозитории
arm-statementsсо своими тестами и оформлен как пакетarm_statements/сpyproject. Границы пакета: пакет владеет своими типами входа и выхода, хост конвертирует выход в свою модель контракта (у ядра на переходе их две); golden-тесты пакета — внутри пакета на синтетических и обезличенных данных, тесты на реальных фикстурах остаются в scan-gateway и tabular-parser и соседний репозиторий не читают. В tabular-parser, scan-gateway и arm-core пакет лежит копией вместе со своими тестами и конфигом линтеров (конфиг пакета подключается кpytestиruffхоста как include, чтобы правила не конфликтовали), импортируется по имени пакетаarm_statements(путь добавляется вpyproject), поэтому замена копии на зависимость не трогает импорты; маркер коммита источника попадает в результат какservice.statements_version; раскладывает и проверяет свежесть скриптsync-statementsв docs по образцуsync-web.sh(--check, код 20 при прямых правках), вызывается изmake check-fullперед PR локально — не изmake checkи не из пайплайна: тест, читающий соседний репозиторий, уже ломал main (arm-core #33, пайплайн платформы вне репозиториев). Прямые правки копии запрещены. Срочное исправление общего кода: PR вarm-statements→sync-statements→ PR в каждом потребителе, порядок выкладки «сначала ядро». -
Р-2 (шаг 4, п. 5) — отсутствующая позиция под колонкой — чистый
nullбез собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100, непрочитанная — 0, и её ловит общее правилоneeds_review); отсутствие позиции бывает только при несовпадении колонок сегмента и покрыто предупреждением шага 2 п. 2. -
Р-3 (шаг 5, п. 1) — сервисы отдают распознанный текст наименования; соответствие «код → наименование» — только в ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 / 1522; договорённость ДБД «из справочника» выполняется на экране).
-
Р-4 (шаг 3, п. 3) — повторы кодов законны; все строки сохраняются и показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие: снять уникальность
(meta_id, line_code)в DBML; все чтения «одна строка на код» в ядре переводятся на агрегат. -
Р-5 (шаг 8, «Подтверждение проверкой») — порог для значений — 100 (у ИС уверенность дискретная: 100, ~80, 71, 67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80); ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не порога. Код строки подтверждается структурой — упорядоченным перечнем кодов формы по редакции, не уверенностью; область применения — Р-12, дополнение — Р-16 (= решение № 7 по спринту 2, AURORA-1519 / 1524).
-
Р-6 (шаг 2, п. 6) — числовая колонка с нераспознанной подписью остаётся в документе без даты с предупреждением
error(needs_check, подтверждение руками); пропуск отвергнут как тихая потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …) добавляются в словарь периодов и считаются от эффективной даты документа; после правки даты отчёта ядро досчитывает их; дату колонке можно поставить руками. -
Р-7 (шаг 8, «Подтверждение проверкой») — да: сошедшееся жёсткое соотношение §7 снимает подсветку и с
is_recognized = false; такой документ может получитьdoneи автоподтвердиться, сырой текст остаётся в подсказке ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0»; для членов по модулю знак подтверждается конвенцией колонки, а не соотношением; правило знака — только для сомнительных ячеек. Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок. -
Р-8 (шаг 3) — расшифровки без кода («в том числе …») отдаются сервисами со статусом
detailи хранятся ядром; в проверки, агрегаты иdoneне входят; экран по умолчанию их не показывает (переключатель, как у пустых строк детализации). Отменяет OQ-36 / OQ-141. -
Р-9 (шаг 9) — контракт v2.0, а не аддитивный 1.3; ядро на время перехода принимает 1.x и 2.0; порядок выкладки — сначала ядро.
-
Р-10 (ядро) — снять уникальность
documentпо(stored_file_id, document_type, osv_account_code)для форм ББ / ОФР, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 —crm_idГСК / ГСЗ из AURORA-1619 (arm-core#112). -
Р-11 (принцип 5) — буквы-двойники заменяются и в суммах, и в кодах; результат —
is_recognized = falseу суммы и неуверенный код, дальше их проверяют контрольные соотношения §7 и структура, и если всё сошлось, подсвечивать не нужно (Р-7 с оговорками). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a. ИНН двойники не трогают. Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе — правило на вырост, не под текущие сканы. -
Р-12 (шаг 8, «Код строки») — структурное подтверждение кода применяется только к неуверенным кодам (уверенность ниже 100 или замена двойника); у Excel и чистых OCR-кодов расхождение с перечнем даёт
infoот ядра, неneeds_check. -
Р-13 (подтверждение документа) — подтверждение документа блокируется (409
LINES_UNRESOLVED), пока есть строкиunreadable/missingс суммами; аналитик назначает код или отмечает строку расшифровкой в режиме правки, черезPATCH …/statement lines(детали — в «Что уходит из ядра»). -
Р-14 (экран) — раскладка по коду «как на бумаге» остаётся (просьба владельца 09.10.2026); строки со статусами ставятся под ближайшей предыдущей размещённой строкой по
row_index, расшифровки под родителем и скрыты по умолчанию; переключатель «как в файле» — не в этой работе. -
Р-15 (ядро) — вторая форма того же типа и периода у одного ЮЛ из того же файла хранится; склейка половинок формы из одного файла не предоставляется (файл перезагружают), вопрос — в
07-open-questions.md. От ИС Сканирование такой случай не ожидается (владелец), основной источник — Excel с листом-продолжением другого типа или двойной печатью. Уточнено Р-18. -
Р-16 (шаг 8, «Код строки»; ядро) — защита от подмены неуверенного кода на соседний: повтор кода, у которого хотя бы одна копия неуверенна, —
needs_reviewу этой строки (сервис); для строк с неуверенным кодом условиеRECOGNIZEDв ядре включает совпадение распознанного наименования со справочным по коду, расхождение подсвечивает строку. Закрывает открытый вопрос о сопоставлении наименований: для неуверенных кодов — условие автоподтверждения, для остальных — только показ справочного имени. -
Р-17 (шаг 3, п. 2) — разлепленные строки OQ-156 не держат документ в
needs_checkбезусловно (решение 08.10.2026 заменяется): их значения нуждаются в покрытии, как грязные, и при сошедшихся соотношениях §7 не подсвечиваются; само разлепление — предупреждениеinfo. -
Р-18 (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается; подтверждение второй при подтверждённой первой — 409
DUPLICATE_FORMтолько внутри файла (между файлами работает автозамена §9.5 п. 3). -
Р-19 (ядро) —
RECOGNIZEDне зависит от статуса конверта: достаточноdocument.status = doneи отсутствия предупрежденийerrorуровня файла (потерянный json или таблица ИС блокируют,needs_checkсоседнего документа из-за одной ячейки — нет). -
Р-20 (18.10.2026; ревью спеки K-4, повторы кодов) — законные повторы кодов (в реальных ББ 2025 — 1220 «НДС, начисленный по отгрузке» и 1520 «Обязательство по аренде») допускаются в упорядоченном перечне кодов: структура подтверждает обе копии. Правило Р-16 «повтор с неуверенной копией —
needs_review» применяется только к повтору, которого нет в перечне. Словарь имён ядра — список имён (синонимов) на код; загрузчик перестаёт запрещать повтор кода в этом смысле (сверка наименования — с любым из имён). В спеке: переченьcontracts/statement-code-orders.yaml(repeats: уfsbu4— 1220, 1520, 2410 печатной формы 1С; у66n— раздел III НКО, см. S-28), §11.9, §7 «Полный состав формы», спекиstatement-normalizationиarm-core. -
Р-21 (18.10.2026; ревью спеки K-4, мягкие соотношения ОФР) — неуверенное значение подтверждается и мягким соотношением, если оно сошлось точно (расхождение 0 или в пределах допуска ±1; не «в пределах округления»). Применяется к 2400, 2410, 2411, 2412, 2460 и прочим строкам, у которых есть только мягкие соотношения. Знак для
abs_terms— по прежнему правилу конвенции колонки. В спеке: §7, §11.9, требование «Шаг 8 —needs_reviewзначения». -
Р-22 (18.10.2026; решения писателя S-1…S-25) — приняты все, с оговорками ревьюера из таблицы S-n в
review-2026-10-18-spec.md: S-4 — синонимы на код и исключение «итого по разделу» из правила вхождения; S-5 — в упрощённом ОФР2400мягкое, как в полном, ББ — жёсткие; S-7 — OQ-148 закрывается этим решением; S-10 / S-22 — одна формулировка; S-13 — двойники внутри детектора нумерации и допуск одного пропуска; S-15 — экранирование имени листа в Excel-ref; S-17 — доопределить по K-23; S-19 —codesиcolumn_indexуdocument_issue; S-21 — по K-2 / K-21. Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже). -
Р-23 (18.10.2026; повторное ревью спеки L-1) — значения строк
detailвneeds_reviewи в статус документа не входят: у нихneeds_review = false, функция статуса их не учитывает; подсказка на экране поtextиconfidenceостаётся. Следствие: на реальных сканах РБ и Счастливочной за 1 кв. 2025 формы могут статьdone(по прогону ревьюера держали их только ячейки расшифровок). В спеке:statement-normalization(«Шаг 8 —needs_reviewзначения», функция статуса, сценарий на РБ),arm-core(пересчёт после правки, согласованность),arm-web, §11.9, proposal; S-51 пересмотрено. -
Р-24 (18.10.2026; L-13 и упрощение S-36) — перед выкладкой ядра база очищается от форм ББ/ОФР (документы
BALANCE/INCOME, ихfinancial_statement_*, комментарии, привязки к версиям «в работе»; файлы, где кроме форм ничего нет, — целиком с заданиями; ОСВ, консолидации, справочники не трогаются; детали и отказы — S-73). Поэтому миграция v5.19 только добавляет колонки, индексы и значения по умолчанию; §7 для существующих форм не пересчитывается, импорта копииarm_statementsв ревизии Alembic нет. Миграция обязана проходить и на непустой базе (колонки по умолчанию,file_ordinalпо порядку,document_issueпуст), но корректность нарушений у старых форм не гарантируется — сказано явно. Для приёма 1.x после выкладки (окно перехода) §7 по-прежнему считается ядром копией пакета при приёме (остаётся из S-36). В спеке: proposal (порядок выкладки),arm-core(«Миграция DBML v5.19», «Очистка форм перед выкладкой», «Формы результата 1.x — §7 при приёме»), §7 «Ядро», §9.5 п. 2, DBML v5.19 (шапка). -
Р-25 (18.10.2026) — утверждены владельцем решения писателя S-26, S-34, S-36 (в редакции Р-24), S-37, S-41, S-47 (подтверждение без правки не сбрасывает акцепт), S-48 (в редакции L-12: ядро вызывает общую функцию статуса). Отмечены ниже.
-
Р-26 (18.10.2026; OQ-168) — коды без образца в fixtures (2421 / 2430 / 2450 в 66н, 1120 и другие в ФСБУ 4) в перечень кодов не входят до появления образцов; неуверенный такой код на скане —
needs_reviewстроки. Временное правило, принятое владельцем; OQ-168 остаётся открытым до образцов. -
Р-27 (18.10.2026; S-73, открытый вопрос доработки) — смешанные файлы «ОСВ + формы» при очистке перед выкладкой: удалять целиком (
--include-mixed) или только формы — владелец решает по отчётуpurge_statements.py --dry-runв момент очистки; до этого скрипт по умолчанию удаляет только формы. -
Р-28 (18.10.2026; S-55) — принято: при приёме 2.0 ОСВ получает
RECOGNIZEDпо своемуosvDocument.statusи отсутствию предупрежденийerrorуровня файла, независимо от статуса соседней формы в той же книге (как Р-19 для форм). -
Р-29 (18.10.2026; S-67) — расхождение «шаблонные строки экрана по редакции по дате, проверки ядра — по редакции по структуре» (бланк 66н, сданный в 2025 году) в этой работе не сводится; отдельная задача после реализации.
-
Р-30 (18.10.2026; S-59) — принято: вариант формы документа, принятого по контракту 1.x, определяется по структуре кодов (как у 2.0), а не остаётся пустым.
Решения владельца по открытым вопросам планов реализации (18.10.2026, plans/):
- Р-31 (план 4 В-2, план 5 Q-3) — если на прогоне реальных файлов (РБ, Счастливочная, НПЗ, Межениновская) форма
получает
needs_checkпо правилу, которое спека считает законным, исполнитель останавливается и поднимает вопрос владельцу по конкретному случаю; адаптер под файл не подгоняется. - Р-32 (план 5 Q-1) — успешный json ИС без таблиц даёт
JSON_FAILED, а не молчаливый пропуск. - Р-33 (план 5 Q-2) — если все json набора ошибочны, конверт
errorперечисляетJSON_FAILEDкаждого json; код ошибки файла — от первого. - Р-34 (план 3 O-2) — подтверждение кода строки уверенность кода не меняет: ставится флаг
is_confirmed,line_code_confidenceостаётся как пришло,line_code_modifiedне ставится; снятие подтверждения возвращаетneeds_reviewпо прежнему правилу. Спека arm-core приводится к этому. - Р-35 (план 3 O-5, P3-12) — очистка форм перед выкладкой отказывает только при привязке формы к версии консолидации в статусе «Верифицирована» или дальше; версии в работе, на согласовании, отклонённые и отменённые не мешают. На стенде 18.10.2026 версий дальше «в работе» нет.
- Р-36 (план 0 O-1) — в окне перехода (до перехода сервисов на 2.0) новые примеры контракта верхнего уровня в docs не добавляются.
- Р-37 (план 6 В-1) — экран принимается по подстановке ответа 2.0 из фикстуры и регрессии на 1.x; прогон на aep_3 — follow-up после перехода scan-gateway на 2.0.
- Р-38 (план 6 В-3) — подтверждённая ячейка помечается серой галочкой.
- Р-39 (план 4 В-1) — коды предупреждений ОСВ — по источнику (
OSV_*), без дробления. - Р-40 (план 3 O-3) — предупреждения
CODE_NOT_IN_DICTIONARY/CODE_NOT_IN_EDITIONпишутся только для четырёхзначных кодов. - Р-41 (план 3 O-6) — смена
crm_idГСК / ГСЗ пишется в аудит как изменение группы. - Р-42 (план 2 O-3) —
sync-statements --checkдобавляется в сборку снимка для контура (prepare-contour-snapshot.sh), по аналогии сsync-web. - Р-43 (план 2 O-4) — репозиторий
arm-statementsвносится в таблицу workspace (CLAUDE.mdworkspace иcontext/00-workspace-layout.md) в рамках плана 1.
Decisions made without discussion
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все обратимы правкой спеки или словаря.
- Уровни предупреждений (шаг 8, п. 2):
error— только то, что меняет данные или их полноту; полный список — в шаге 8 п. 2, таблица «какая правка какоеerrorснимает» — в «Что уходит из ядра». Цена ошибки: лишний или пропущенныйneeds_check, правится таблицей уровней в спеке. DISCLOSUREвсегдаdoneбез условий о раскладке, строках и проверках; поtype_hintшаги 2–7 не выполняются, по кодам прочих форм строки отбрасываются — сохранено текущее поведение парсера. Цена: пояснения и формы 3, 4, 6 не проверяются; так и сейчас.- Столбец кодов — подпись «Код» принимается при доле кодов под ней ≥ 0,6 или кодах ≥ 3, при нескольких
кандидатах — больше кодов; запасной подсчёт «кодов ≥ 3 и доля ≥ 0,6» взят у парсера; сегмент не первый с кодами
< 3 — ослабленный поиск по диапазону столбца первого сегмента (как
relaxedпарсера). Цена: таблица без подписи и с одним-двумя кодами в первом сегменте → документ без строк сneeds_check. - Диапазоны кодов — явные отрезки по формам (шаг 2), объединение для поиска столбца, «диапазон столбца» для разлепления и продолжений; границы форм 3, 4, 6 уточняются в спеке. Цена: код вне отрезка не распознаётся как код; правится словарём.
- Начальный словарь исключений подписей («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам парсера; пополняется словарём, не кодом.
- Грамматика
locator— парыключ:значениечерез;, ключи по источнику, как сейчас. Цена: нулевая. - Грамматика
ref— OCRrow:<row_index>[#<k>], Excel<лист>!<A1>;refстроки уникален в документе. value, который не число (дата, логическое) в колонке сумм —amount = null,string_value= текст,is_recognized = false. Цена: минимальная.- Предел и квантование суммы — 16 целых цифр, 2 знака, half-up — только для числового
valueExcel; текст OCR с тремя и более знаками после разделителя остаётсяnull+string_value(решение № 30 среза 3a); «догадка из цифр» длиннее 16 —null. parse_amountзнает источник: запятая и точка у Excel — десятичные, у OCR — тысячи при группах из трёх цифр. Единственное место, где шаги 2–8 смотрят наsource. Разбор ОСВ не затрагивается.checksотсутствует у документа без строк, обязателен уBALANCE/INCOMEсо строками.- Правило
detail: строка без кода с содержимым под строкой с кодом (родитель продолжается через границу сегмента); ячейка кода без единой цифры — пустая. Цена: строка формы с потерянным кодом под другой строкой скрывается до нарушения §7 у родителя (цель отсутствует — нарушение); лечится назначением кода (Р-13). - Посчитанное соотношение — непуста цель или хотя бы один член; пустая цель при непустых членах и непустая
цель при пустых членах — нарушение; сегодня соотношение с отсутствующей целью пропускается у обоих сервисов и
ядра. Цена: формы без подытогов — только с упрощённым набором соотношений, иначе
needs_check, как и сегодня. - Упрощённая форма определяется только по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или
2110, нет 2100, 2200, 2300) и получает свой набор соотношений и перечень кодов; ОКУД 0710096 признаком не служит.
Цена: ошибка определения → не те соотношения →
needs_check; образца упрощённой формы в fixtures пока нет. - Конвенция знака для членов
abs_terms: форма записи у большинства членовabs_termsколонки с непустымamount,is_recognized = trueи без правки; проверяется только у ячеек, нуждающихся в покрытии. Цена: ОФР, где сомнительные расходы записаны не так, как остальные, подсвечивает их; это и нужно. - Подтверждение даты отчёта структурой: неуверенная дата отчёта подтверждается совпадением с самой поздней
датой колонок, неуверенная подпись колонки — попаданием в ожидаемые даты; иначе
error. Цена: скан с верной, но неуверенной датой без колонок с датами —needs_check, снимается правкой даты. - Хранить
needs_review,text, подтверждения, нарушения и предупреждения в ядре (financial_statement_value,financial_statement_line,financial_statement_meta,document_issue) вместо пересчёта на каждом GET — пересматривает решение v5.13 «считается, не хранится». Цена: ещё одна таблица и миграция; взамен ядро при чтении ничего не считает, а подтверждения аналитика переживают пересчёт. - Эффективная дата документа = дата отчёта, иначе самая поздняя абсолютная дата колонок — та же формула, что
statement_periodядра; от неё считаются относительные подписи и выбирается редакция перечня кодов. document_issue: очищается при перезагрузке файла, записиrelation/softзаменяются целиком при пересчёте,line_id— список;errorсервиса снимаются только правкой из таблицы соответствия.- Границы пакета
arm_statements: свои типы входа и выхода, конвертация в модель контракта на стороне хоста, golden внутри пакета на синтетике, тесты на реальных фикстурах — только в сервисах; расхождение версий копий —info. - Алгоритм структурного подтверждения кода: наибольшая подпоследовательность по порядку бланка, редакция с
длиннейшей, пересечение при неоднозначности, уверенные коды — якоря, пятизначные — через родителя. Цена: при
двух переставленных неуверенных соседях оба
needs_review; это и нужно. - Слияние шаблонной строки с назначенным кодом: шаблонная строка с суммами аналитика — 409
LINE_EXISTS, без сумм — заменяется настоящей. Цена: лишний шаг у аналитика в редком случае; двойного счёта нет. - Рабочие имена
FormCandidate,TableLayout,RawLine,needs_review,line_status,line_code_text,row_index,source_ref,parent_ref,period_kind,form_variant,document_issue,statements_version,is_confirmed,LINES_UNRESOLVED,DUPLICATE_FORM,arm-statements,sync-statements— по образцу существующих (sync-web.sh); меняются при написании спеки без последствий.
Решения при написании спеки (18.10.2026)
Решения агента при оформлении спеки, контракта v2.0, правил §11 и DBML v5.19 там, где дизайн оставлял «уточняется в
спеке» или не давал значения. Маркер S-n — в отчёте спеки; все обратимы правкой спеки, словаря или DBML до реализации.
- S-1. Имя способности контракта — дельта к существующей
recognition-contract, а не новаяrecognition-result-contract. Основание: спека с этим смыслом уже есть вopenspec/specs/. Цена: нулевая. - S-2. Конверт
recognition-result-messageтоже v2.0:metrics.warningsв конверте — те же структуры, что в файле результата; производитель пишет в конверт тот же MAJOR. Основание: «ack тоже 1.x и 2.0» (шаг 9) и одна форма предупреждений. Цена: ещё одна схема поднимает MAJOR; ядро и так принимает оба. - S-3. Диапазоны кодов — как в дизайне, без изменений (3100–3699, 4100–4499, 5000–5999, 6100–6499). Цена: код вне отрезка не код; правится словарём.
- S-4. Совпадение наименований (Р-16): нижний регистр,
ё→е, всё кроме букв и цифр — пробел; совпало при равенстве, вхождении более короткой строки (≥ 5 символов) илиdifflib.SequenceMatcher≥ 0,8 с любым справочным именем кода в любой редакции; нет кода в словаре имён — не совпало. Цена: ложное несовпадение —NEEDS_CHECKи ручное подтверждение; ложное совпадение маловероятно у разных статей одного раздела. Утверждено владельцем 18.10.2026 (Р-22) с оговоркой: у кода несколько имён (синонимы, Р-20); если одно из имён начинается с «итого по разделу», совпадение — только равенство (S-49). - S-5. Формулы упрощённого набора по бланку приказа 66н, прил. 5 (ББ:
1600 = 1150 + 1170 + 1210 + 1230 + 1250,1700 = 1300 + 1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550,1600 = 1700; ОФР:2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410), все жёсткие;ASSUMPTIONдо образца (OQ-166). Цена: неверная формула —needs_checkупрощённых форм. Утверждено владельцем 18.10.2026 (Р-22) с изменением: ОФР2400 = …упрощённого набора — мягкое, как2400полного (знак 2410, OQ-12; ревью K-11); соотношения ББ — жёсткие. - S-6. Словари подписей: кода — «код», «коды», «код строки», «код показателя»; наименования — подпись, начинающаяся с «наименование». Цена: пополняется словарём.
- S-7. Прочерк (
-,–,—,−без цифр) — пустое значение,is_recognized = true(как пустая ячейка, Р-2); OQ-148 — допущение. Цена: тире-мусор на месте цифры не подсвечивается сам по себе, его ловит §7 (пустой член). Утверждено владельцем 18.10.2026 (Р-22): OQ-148 закрыт этим решением. - S-8. Каталог кодов предупреждений (таблица §11.9
05-domain-rules.md);codeв контракте — строка^[A-Z][A-Z0-9_]*$, не перечисление (новый код — не MAJOR);VALUE_OUT_OF_RANGE—info(значение и такneeds_review). Цена: опечатка кода не ловится схемой — ловится тестами пакета. - S-9. Предупреждение уровня файла имеет
locator(json набора ИС, лист). Цена: нулевая. - S-10.
needs_reviewполей шапки — массивheader.needs_review_fieldsв контракте иjsonbвfinancial_statement_meta; сейчас туда попадает толькоreport_date: сомнения в ИНН и организации —infoпо дизайну,needs_reviewу них дал быneeds_checkвопреки «НПЗ ОФР без ИНН остаётсяdone». Цена: экран подсвечивает ИНН и организацию поissuesсfield, а не поneeds_review. Утверждено владельцем 18.10.2026 (Р-22); формулировка одна во всех документах:needs_review_fields— поля, которые держат документ вneeds_check; сырой текст и уверенность всех полей шапки хранятся (header_source, S-33). - S-11. Значения
period_kind:absolute,relative:current,relative:prev_year_end,relative:prev_prev_year_end,relative:prev_period,none; «За аналогичный период» — эффективная дата − 1 год (29.02 → 28.02). Цена: новая относительная подпись — новое значение (MINOR). - S-12. Ожидаемые даты неуверенной подписи колонки — эффективная дата, она же − 1 и − 2 года и 31 декабря
предыдущего и предшествующего ему года. Основание: у квартального ББ колонки «на 31.12 прошлого года», и набор
дизайна (дата, − 1 год, − 2 года) давал бы
errorна каждой такой форме. Цена: чуть шире окно подтверждения. Утверждено владельцем 18.10.2026 (Р-22). - S-13. Строка нумерации — не меньше трёх непустых ячеек, все целые, подряд с 1 слева направо. Цена: нумерация с пропуском не распознаётся (строка уйдёт в данные со статусом). Утверждено владельцем 18.10.2026 (Р-22) с оговоркой: двойники внутри детектора и допуск одного пропуска — уточнено S-40.
- S-14. Признак повторной шапки — подпись кода в строке или ≥ 2 непустых ячеек, совпавших с шапкой первого сегмента. Цена: шапка без «Код» и с другими подписями не начнёт сегмент. Утверждено владельцем 18.10.2026 (Р-22).
- S-15.
refиrow_index:row_indexс 0; Excelrefстроки — адрес ячейки столбца кодов (без столбца — первой непустой ячейки); разлепленныеrow:<i>#<k>с k от 1; у разлепленных строкline_code_textиtext— k-е части. Цена: нулевая. Оговорка Р-22: имя листа в Excel-refэкранируется по правилу Excel (кавычки, удвоенный апостроф), примеры контракта исправлены. - S-16. Уверенность кода с заменой двойника — не выше 99 (так «неуверенный» виден из одного поля). Цена: нулевая.
- S-17. Формат PATCH
lines— один элемент = одно действие (line_code|line_status: "detail"|confirmed); коды 422INVALID_LINE_CODE,LINE_CODE_REQUIRED,INVALID_LINE_PATCH,UNKNOWN_LINE,UNKNOWN_COLUMN; правка суммы поline_codeпри наличии строкиok— 422UNKNOWN_CELL. Цена: имена кодов меняются до реализации без последствий. Оговорка Р-22: доопределено по K-23 (S-47); 409LINE_EXISTSпереименован вLINE_CODE_TAKEN(S-43). - S-18. Детали 409:
LINES_UNRESOLVED—detail.lines[] {line_id, row_index, line_code_text};DUPLICATE_FORM—detail.document_id. Цена: нулевая. - S-19. Имена DBML v5.19:
document.file_ordinal(с 0, уникален в файле),financial_statement_meta.needs_review_fields(jsonb),document_issue.line_ids(jsonb— сверка ORM↔DBML не знает массивов), enum'ыstatement_line_status,statement_form_variant,document_issue_kind,issue_severity,issue_source; индекс(meta_id, line_code)остаётся неуникальным; у строкиis_confirmedбезconfirmed_by/at(строго по дизайну, кто — в аудите). Новые колонки — последними в таблицах (как их добавит миграция). Цена: нулевая до миграции. Оговорка Р-22: уdocument_issueдобавленыcodesиcolumn_index(S-31, S-35); висячийline_idудалённой шаблонной строки убирается изline_ids. - S-20.
related_group.crm_idуникален (как у ГК и ЮЛ), повтор — 409CRM_ID_EXISTS. Цена: две группы с одним ID CRM завести нельзя; так и должно быть. Утверждено владельцем 18.10.2026 (Р-22). - S-21. Значения по умолчанию при приёме 1.x:
ref = null,needs_reviewстрокиfalse,period_kind—absoluteпри дате иначеnone,form_variant = null, строки-предупреждения —LEGACY_WARNINGinfo. Цена: на окне выкладки документы 1.x без новых сигналов качества. Оговорка Р-22: по K-2 / K-21 — S-36. - S-22. Статус документа ОСВ —
done, когда контрольные суммы §1.4 листа сошлись, иначеneeds_check(прежнее правило конверта, применённое к листу). Цена: нулевая — конверт ведёт себя как сейчас. Утверждено владельцем 18.10.2026 (Р-22); формулировка «контрольные суммы §1.4 листа сошлись» теперь одна в контракте, §11, спекахrecognition-contractиtabular-recognition-service. - S-23.
service.statements_versionобязателен, если в результате естьstatementDocument(схема проверяет). Цена: результат только с ОСВ может его не нести. - S-24. Редакция перечня кодов в сервисах —
fsbu4при эффективной дате после 2024-12-31 (то же правило, чтоeditionядра, OQ-145). Цена: досрочный ФСБУ 4/2023 — проверяются обе через «длиннейшую подпоследовательность». Уточнено S-26: проверяются всегда обе, дата решает только ничью. - S-25. Квартал периода (условие
RECOGNIZED): дата не конец квартала →NEEDS_CHECK(как D-6), ручное подтверждение — 409PERIOD_REQUIRED. Основание: оговорка дизайна «иначе документRECOGNIZED, но не подтверждаемый» противоречит автоподтверждениюRECOGNIZED. Цена: нулевая — поведение как сейчас. Утверждено владельцем 18.10.2026 (Р-22).
Решения при доработке спеки по ревью K-1…K-25 (18.10.2026)
Решения агента при закрытии находок review-2026-10-18-spec.md там, где ни ревьюер, ни владелец не задали значение,
или где агент отступил от предложения ревьюера. Маркер S-n продолжает нумерацию; все обратимы правкой спеки, словаря
или DBML до реализации.
- S-26. Редакции перечня кодов — всегда обе (K-5): для неуверенного кода считаются наибольшие
подпоследовательности по
66nиfsbu4, берётся длиннейшая, эффективная дата решает только ничью. Основание: Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24. Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). Утверждено владельцем 18.10.2026 (Р-25). - S-27. Перечень кодов — данные в docs (K-5): источник —
contracts/statement-code-orders.yaml(обращение «как контракт»: общие данные сервисов и ядра), в пакете — байтовая копияrules/statement_code_orders.yaml, идентичность проверяетsync-statements --check; форма файла —scripts/validate_contracts.py, для этого вmake venvdocs добавлен PyYAML. Цена: ещё одна зависимость docs-проверок (make venvнужно перезапустить); перенос источника вarm-statements— правкой одного требования. - S-28. Состав перечня — только подтверждённые образцом коды (K-5): шаблоны банка + коды печатных форм 1С и сканов
из
fixtures/(1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО (1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор66n; клиентский 1231 — в66n(Унагранде 2024). Коды без образца (1120 уfsbu4, 2421 / 2430 / 2450 у66n, упрощённые перечни) — OQ-168, OQ-166 (временное правило утверждено владельцем — Р-26). Все 12 последовательностей кодов изfixtures(Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с неподтверждённым кодом и неуверенностью ниже 100 —needs_reviewстроки; у Excel и уверенного кода — толькоinfo. - S-29. Окно перехода контракта (K-8): схемы 1.2 / 1.0 лежат в docs как
*.v1.schema.json(байтовые копии, сервисы сверяются с ними без правки копий), примеры —examples/v1/; тест идентичности сервиса выбирает файл по$idкопии (…/v1→*.v1.schema.json) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs. Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак —$idкопии, а не настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния docs; без него ихmake checkкрасный на окне. Заменено S-53 (повторное ревью L-2: файла.v1у запроса нет, примеры и ядро не были покрыты). - S-30. Механика
sync-statements(K-9): копия —api/arm_statements/(импорт какsrc), тесты пакета вtestpathsхоста,mypy src arm_statements; маркер — файлapi/arm_statements/VERSIONс полным хэшем;--check:0синхронно,10отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы подтягивать чужие изменения пакета),20прямая правка / чужая версия / расхождение перечня кодов / код словаря ядра вне перечня,2нет checkout или аргумент. Без checkoutarm-statementscheck-fullкрасный (код 2): это проверка перед PR, а неmake check. Цена: отставшая копия проходитcheck-full; видна в описании PR. - S-31.
column_index— номер столбца сетки кандидата (K-6): у OCR —column_indexИС как есть (разреженный), у Excel — номер столбца листа первого сегмента с 0; уdocument_issue—column_idдля колонки формы иcolumn_indexдля столбца вне колонок;line_refs— только строкиlines[]. Отступление от варианта «порядковый номер колонки значений»: иначе пропущенная колонка (COLUMN_SKIPPED) не имела бы номера. Примеры контракта приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их позициями. Дополнено S-56 (колонки сегментов со своей шапкой). - S-32.
TABLE_PARSE_ERRORиSHEET_OTHER— предупреждения файла (K-7): сlocator; документа нет;TABLE_PARSE_ERROR(error) блокируетRECOGNIZEDвсех документов файла, какJSON_FAILED. Цена: один упавший разбор таблицы держит весь файл на ручном подтверждении. - S-33. Шапка:
header_source jsonb+ прежние*_original_value(K-3): сырой текст и уверенность всех десяти полей — одной колонкой{поле: {text, confidence}};inn_original_value,report_date_original_value,units_original_valueзаполняются как прежде,inn_confidenceостаётся. Отступление от варианта «колонки на каждое поле»: 20 колонок ради подсказки на экране. Цена: поиск по сырому тексту поля шапки в SQL — черезjsonb. - S-34.
organization_nameв PATCH шапки (K-12): непустая строка до 1000 символов,organization_name_modified,organization_name_original_value(новые колонки DBML), пересчётcounterparty_check, аудит; снимаетNO_ORGANIZATION_NO_INN. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. Утверждено владельцем 18.10.2026 (Р-25). - S-35. Коды нарушений в
document_issue(K-13):relation→RELATION_FAILED/error,soft→RELATION_SOFT/info, колонкаcodes jsonb,line_ids— все строкиokкодов. Цена: нулевая. - S-36. Формы без
checks(K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией пакета (form_variant = null— полный набор) и пишетdocument_issue(source = core): при приёме 1.x и в ревизии миграции (ревизия импортирует копиюarm_statements). D-6 «проверки сошлись» для 1.x — из этого расчёта;GETиconfirmчитают нарушения только изdocument_issue. Предупреждения маппера 1.x по документу —MAPPING_WARNINGinfo; строкиmetrics.warnings1.x —LEGACY_WARNINGвrecognition_job.metrics(уровень файла, неdocument_issue);STATEMENTS_VERSION_MISMATCHприstatements_version = nullне пишется. Цена: ревизия миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2). Утверждено владельцем 18.10.2026 (Р-25) в редакции Р-24: миграционного расчёта нет — база перед выкладкой очищается от форм; расчёт при приёме 1.x остаётся (вариант формы — по структуре кодов, S-59). - S-37. Признак строки правки —
row_index IS NULL(K-1): маппер 1.x и 2.0 всегда пишетrow_index; миграция ставитNULLстрокам сline_code_original_value IS NULL;line_code_original_value— код из источника, правками не меняется. Отступление от дизайна («смена кода — прежний вline_code_original_value»): при первой смене это то же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок — только в аудите. Утверждено владельцем 18.10.2026 (Р-25). - S-38. Функция статуса и «содержимое» строки (K-14): вход — данные без типов источника (
layout_found, у ядра — есть строка из источника;has_organization_or_inn;period_known; статусы иneeds_review; число жёстких нарушений;accepted; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или непустая ячейка значений с уверенностью ниже 100; строкаunreadableбез значений («44 30\nI |» aep_6) — строка данных: документneeds_check, подтверждение не блокирует. Цена: такая строка требует от аналитика «Это расшифровка» или кода, хотя сумм в ней нет. - S-39. Столбец кодов ищется всегда, и у
DISCLOSURE(K-10):type_hint = DISCLOSUREпри кодах ББ/ОФР → тип по кодам +TYPE_CONFLICT; уDISCLOSUREшаги 3–7 не выполняются,columns/lines— пустые массивы. Цена: лишний проход поиска столбца по пояснениям. - S-40. Детектор нумерации (S-13 с оговоркой Р-22, K-15): ≥ 3 непустых ячеек, каждая после замены двойников — целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась.
- S-41.
LINES_UNRESOLVED— сумма или текст (K-16): блокирует строкаunreadable/missing, у которой хотя бы в одной ячейкеamountилиstring_valueнеnull. Цена: строка с мусором вместо суммы требует разбора. Утверждено владельцем 18.10.2026 (Р-25). - S-42. Подтверждение неуверенной даты отчёта (K-17): PATCH
header.report_dateтем же значением у поля изneeds_review_fieldsснимает его иREPORT_DATE_UNCERTAIN; у бланка 66н только с относительными подписями это единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке. - S-43. Имена (K-18): 409
LINE_EXISTS→LINE_CODE_TAKEN(имя занято сообщением 422UNKNOWN_CELLв коде ядра);period_endколонки →period_dateво всех документах (как в контракте и DBML); порядокlinesв API — «по возрастанию кода, как сейчас», «как на бумаге» — раскладка экрана;DISCLOSURE— пустые массивы. Цена: нулевая. - S-44. Откат миграции (K-19): гарантирован на пустой и совместимой с v5.18 БД; на несовместимых данных
downgradeостанавливается с ошибкой и ничего не удаляет. Цена: откат после появления данных v2.0 — только восстановлением из резервной копии. - S-45. «Того же периода» (K-20) — та же пара
period_year,period_quarter(как автозамена §9.5 п. 3); формы без квартала не считаются (их и так держитPERIOD_REQUIRED). Цена: нулевая. - S-46. Единицы и ОКЕИ (K-22): словарь
statement_units.yamlв пакете (написания и коды ОКЕИ 383 / 384 / 385); единицы по тексту, иначе по ОКЕИ;okei(+_text,_confidence) — вheaderконтракта v2.0 иheaderField. Цена: ОКЕИ, противоречащий тексту единиц, не ловится (текст главнее). - S-47. Пробелы PATCH (K-23, S-17):
confirmed: falseснимает подтверждение ячейки и кода;line_statusпринимает только"detail"("ok"— 422LINE_CODE_REQUIRED); обратногоdetail→unreadable/missingнет (исправляется назначением кода, исходное — перезагрузкой);columns[].period_date = null— 422 валидации; акцепт сбрасывает правка данных (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (confirmed) — нет. Последнее сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если владелец считает подтверждение правкой — одна строка спеки. Утверждено владельцем 18.10.2026 (Р-25): подтверждение без правки акцепт не сбрасывает. - S-48. Страховка
done(K-24):doneиз результата при строкеunreadable/missingсо значением,needs_review,relations_failedилиerror-предупреждении документа →NEEDS_CHECKиRESULT_STATUS_INCONSISTENTinfo. Цена: проверка на каждом маппинге — линейная по строкам. Утверждено владельцем 18.10.2026 (Р-25) в редакции L-12: согласованность — вызов общей функции статуса по входу из результата (не список признаков). - S-49. Словарь имён ядра (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка
и
reference_nameберут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на экране — первое имя кода, файловое — рядом. - S-50. Толкование Р-21: мягкое соотношение, сошедшееся в пределах ±1, покрывает значения любого своего кода; у кода в несошедшемся жёстком соотношении подсветка остаётся (правило «все ячейки несошедшегося жёсткого»), поэтому на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая.
- S-51. Ожидания на реальных сканах признаны в §11.9 и proposal (K-4): большинство из шести OCR-документов
останутся
needs_check; формулировка гайда фронта смягчена («может статьRECOGNIZED»). Цена: нулевая. Пересмотрено Р-23: значенияdetailстатус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут статьdone; держатneeds_checkaep_3 и aep_6. - S-52. Манифест tabular-parser (K-25): в спеке
tabular-recognition-serviceперечислены записи, получающие1600 = 1700(страницы 2 баланса ×3), и записи, чьи статусыdoneменяться не должны. Цена: нулевая.
Решения при доработке спеки по повторному ревью L-1…L-30 (18.10.2026)
Решения агента при закрытии находок review-2026-10-18-spec-2.md там, где ни ревьюер, ни владелец (Р-23…Р-26) не задали
значение, или где агент отступил от предложения ревьюера. Маркер S-n продолжает нумерацию; все обратимы правкой спеки,
словаря или DBML до реализации.
- S-53. Правило теста идентичности копии контракта (L-2; заменяет S-29): схема копии с
$id…/v<M>сверяется с<имя>.v<M>.schema.jsondocs, если такой файл есть, иначе с основным; пример — сexamples/v<M>/<имя>.json, если он есть, иначе сexamples/<имя>.json(M—MAJORкопии схемы вида примера, вид — по префиксу имени); прочие файлы копии (.v<N>,examples/v<N>/у ядра) — с одноимёнными; файлы docs другогоMAJORсервис не сверяет. Правило вводится в шаг 0 у всех трёх потребителей, включая ядро. Основание: существующие тесты (arm-core/api/tests/ test_contract.py,tabular-parserиscan-gatewayapi/tests/unit/test_contract.py) сравнивают весь набор*.jsonчерезrglob, включаяexamples/; правило «файл, если есть» проходит до слияния docs (файлов.v1нет), на окне и после, у запроса без.v1. Цена: три маленьких PR до слияния docs; удалениеMAJOR1 — в порядке «сначала ядро убирает копии.v1, затем docs». - S-54.
file_ordinalу всех документов (L-3): индекс документа вdocuments[]результата при любомMAJOR, у ОСВ, форм,DISCLOSUREиOTHER; несозданный документ (второй лист ОСВ с тем же счётом) номер не занимает и других не сдвигает — номера с пропусками. Цена: номер не «порядковый без дыр»; экран его не показывает. - S-55. Предупреждения файла и ОСВ в ядре (L-4):
recognition_job.metrics.warningsиJobBrief.warnings— структурыfileWarning; строки старых заданий конвертируются при чтении вLEGACY_WARNING(данные не переписываются);JobBrief.mapping_warnings— строки, как были. Предупреждения листа ОСВ 2.0 —document_issueдокумента ОСВ, отдаются в новомDocumentOut.issues[].osvDocument.statusиспользуется: ОСВ 2.0RECOGNIZED⇔status = done∧checksчисты ∧ нетerrorфайла ∧ контрагент неMISMATCH∧ нет предупреждений маппинга листа; статус конверта не входит (как Р-19 для форм). Отступление от гайда «поля только добавляются»: типJobBrief.warningsменяется — записано в гайде фронта. Цена: фронт обязан перейти на объекты в той же выкладке, что и ядро; ОСВ в книге с сомнительной ОФР теперь автоподтверждается (раньше держалась статусом задания). - S-56. Колонки сегмента со своей шапкой (L-6):
columns[]задаёт первый сегмент; колонки следующего сегмента со своей шапкой сопоставляются с ними поperiod_date(если у каждой колонки сегмента дата есть и совпадает ровно с одной), иначе по порядку; при сопоставлении по порядку другое число колонок —SEGMENT_COLUMNS_MISMATCH, лишние столбцы сегмента вcolumns[]не попадают, их номера — вmessage. Цена: сегмент с переставленными колонками без дат сопоставится по порядку неверно — §7 это поймает; НПЗ (лист 4 ББ со своей шапкой) остаётсяdone. - S-57. Явное правило мусора в
parse_amount(L-7): «ядро числа» от первого «цифра,(, знак» до последнего «цифра,),},]»; всё вне ядра — краевой мусор, выбрасывается сis_recognized = false; в ядре — только цифры, пробелы,,,., знак в начале и пара скобок вокруг числа, иначеamount = null+string_value(буква в ядре, непарная скобка, символ между цифрами); крайние|удаляются до замены двойников;{и[открывающей скобкой не считаются. Так211(1 |и(Ю4)даютnull,: : 2856983 = ■— 2856983,{38378)—null(сценарий «знак вне конвенции» переписан на значение без знака). Отступление от сегодняшнегоamounts.py(выбрасывал всё, кроме цифр): осознанно. Цена: «12:34» даётnullи требует внимания аналитика; «1 234 руб.» даёт 1234 сis_recognized = false(буквы после последней цифры — краевой мусор, значение нуждается в покрытии §7). - S-58. Дрейф перечня кодов — код 10, не 20 (L-8):
sync-statements --checkв хосте сверяет копию перечня с docs, расхождение —10(предупреждение), а не20;20— только прямая правка копии, чужая версия, недостающая зависимость пакета и (в ядре) код словаря имён вне перечня копии. Порядок правки перечня: docs →arm-statements→ хосты. Цена: копия с устаревшим перечнем проходитcheck-fullс предупреждением; видна в описании PR. - S-59. Точка входа пакета для пересчёта (L-10):
StoredForm(без сетки) →{form_variant, edition, checks, needs_review строк и значений, родители detail, status}; признакиcode_fixed/value_fixed, строкиrow_index = nullучаствуют в §7,needs_review_fields— вход (пересчёт шапку не переоценивает). Ядро вызывает её после PATCH и при приёме 1.x; вариант формы при приёме 1.x — по структуре кодов, а неnull(уточняет S-36: иначе упрощённая форма 1.x получила бы нарушения полного набора до первой правки, а после правки — другой набор). Цена: у формы 1.xform_variantможет отличаться от «полного по умолчанию»; образца упрощённой формы нет (OQ-166). - S-60. «Итого по разделу» — сравнение номеров раздела (L-11): номер — начальные буквы
i/v/xпервого слова после фразы (хвост OCR «V1», «V /» отбрасывается),stripявно; номера равны ⇔ совпало; номер не выделен — равенство строк. Отступление от S-49 («только равенство»): ломалось на «Итого по разделу V1» Счастливочной. Цена: «II», прочитанное как «I1», даст I ≠ II — ложноеNAME_MISMATCH(безопасная сторона). - S-61. «изо :» — пустой код, правило не меняется (L-14): двойники цифрами в ячейке кода без цифр не считаются
(иначе слова, затёкшие в столбец кодов, стали бы
unreadableи блокировали подтверждение); пример в §11.4 исправлен. Цена: потерянная строка 1130 aep_3 («изо :»,'3'с уверенностью 0) уходит вdetailи скрыта; её сумма не входит в 1100, и1100 = …подсвечивает раздел, если сумма больше допуска ±1. - S-62. Родитель
detail(L-15): ближайшая строкаokвыше (unreadable/missingпропускаются; нетokвыше — строкаmissing); после каждого PATCH ядро пересчитывает родителей всех строкdetail; нетokвыше — родительnull, статус остаётсяdetail. Цена: строкаdetailбез родителя на экране — в начале раздела. - S-63. Дата колонки руками (L-16, L-17): ручная дата ставит
period_kind = absolute; дата, равная текущей, у колонки сCOLUMN_DATE_UNCERTAIN/COLUMN_WITHOUT_DATE— подтверждение (аналог S-42), снимает предупреждение и акцепт не сбрасывает. Цена: после ручной даты относительная колонка не следует за правкой даты отчёта — так и нужно. - S-64. Подтверждение дат только уверенными источниками (L-18): для неуверенной подписи колонки эффективная дата —
дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с подписью уверенности 100; неуверенная
дата отчёта подтверждается только датами уверенных подписей. Для периода и относительных подписей эффективная дата —
по всем источникам. Цена: скан, где и дата отчёта, и все подписи неуверенны, — всегда
needs_checkс подтверждением даты руками. - S-65. Структурная проверка кода — уточнения (L-19): «лучшая» подпоследовательность — сначала по числу уверенных
кодов (якоря), затем по длине; коды вне перечня редакции в её последовательность не входят; противоречащие якоря —
входит наибольшее согласованное их множество; неуверенный код подтверждён, если входит во все лучшие; вариант формы —
по всем строкам
okи пересчитывается ядром после правки кода. Цена: при перепутанных уверенных соседях неуверенный код между ними может остаться неподтверждённым —needs_review, безопасная сторона. - S-66. Форма знака (L-20): по результату разбора (скобки, включая
}/]; минус; без знака); сама ячейка в голосование не входит; нужно строгое большинство, равенство — не подтверждён. Цена: колонка с двумя голосующими разной формы не подтверждает знак ни одной сомнительной ячейки. - S-67. Редакция для проверок ядра (L-21):
CODE_NOT_IN_EDITIONиCODE_NOT_IN_DICTIONARY— по редакции точки входа (лучшая подпоследовательность по всем строкамok, ничья — по дате);editionответа API и шаблонные строки — по дате, как сейчас. Цена: на бланке 66н, сданном в 2025 году, шаблонные строки на экране — поfsbu4, а проверки — по66n; сведение — отдельной задачей при необходимости (O-1 отчёта). - S-68.
OTHERудалён из v2.0,form_variantобязателен (L-22): в схеме v2.0statementDocument.document_type—BALANCE|INCOME|DISCLOSURE,form_variant— вrequired(nullуDISCLOSURE);OTHERостаётся в схеме 1.x и вdocument_typeядра. Цена: нулевая — производителейOTHERв 2.0 нет. - S-69. Откат миграции — точная формулировка (L-24; уточняет S-44):
downgradeостанавливается только на данных, которые v5.18 не может представить без потери первичных данных и ручного ввода (повтор кода, строка без кода, две формы одного типа в файле, непустойcrm_idгрупп, правленое наименование организации); производные и сигнальные колонки v5.19 иdocument_issueудаляет с записью числа удалённых значений в журнал миграции (подтверждения — в аудите). Цена: откат после работы аналитиков теряет подтверждения ячеек и кодов как признаки (история — в аудите). - S-70. Механика копии пакета — уточнения (L-25):
VERSIONиз сравнения исключён (в источнике его нет); зависимости пакета из егоpyprojectобязаны быть в зависимостях хоста (иначе--check= 20); пакет проходит самый строгий конфигruff/mypyсреди хостов; тесты пакета со своимconftest,autouse-фикстуры хоста ограничены его каталогомtests/. Цена: пакет живёт по самым строгим правилам всех хостов. - S-71. Содержимое таблицы ИС и
TABLE_SKIPPED(L-27): содержимое — хотя бы одна ячейка с цифрой или «цифрой под видом буквы»; таблица только из текста без столбца кодов и типа — новое предупреждение файлаTABLE_SKIPPED(info), а неTABLE_LOST(error). Цена: текстовая таблица ИС не блокирует автоподтверждение; если в ней была форма без единой цифры — такой формы не бывает. - S-72. «Цифра под видом буквы» — содержимое строки (L-30): ячейка значений, текст которой после замены двойников
без условия соседства содержит цифру и не содержит других букв (
'I','| О'), делает строку строкой данных; текст из настоящих букв при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев», «(расшифровка подписи)» у Межениновской) — нет: иначе каждая книга получала бы мусорные строкиdetail. Признано осознанным исключением из «ничего не выбрасывать молча». Цена: текстовая строка без цифр с уверенностью 100 по-прежнему теряется молча. - S-73. Очистка форм перед выкладкой — детали (Р-24): скрипт ядра
scripts/purge_statements.py,--dry-runпо умолчанию,--applyодной транзакцией; отказ без изменений, если форма упомянута вex_snapshot_itemили привязана к версии не «в работе»; файлы только с формами удаляются целиком (какDELETE /files/{id}, без запретаFILE_HAS_CONFIRMED_DOCUMENTS, задания — тоже); в смешанных файлах «ОСВ + формы» удаляются только формы, файл остаётся — повторно его не загрузить (дедупsha256), поэтому--dry-runперечисляет их и владелец решает про--include-mixed; системный аудитFILE_DELETE. Цена: подтверждённые формы и их привязки к версиям «в работе» исчезают до повторной загрузки; КФО этих версий пересчитывается. - S-74.
needs_reviewзначений после выхода строки изdetail(Р-23): строка, которой назначили код, получаетneeds_reviewзначений по общему правилу (покрытие §7); строка, отмеченнаяdetail, —false; значенияunreadable/missingсчитаются по общему правилу (документ и такneeds_checkпо статусу строки). Цена: нулевая.