76 KiB
Единый алгоритм нормализации ББ/ОФР (scan-gateway и tabular-parser) — дизайн
Issue: arm-ka-docs#116. Статус: целевая картина по шагам 1–9 согласована с владельцем (решения Р-1…Р-7, 17.10.2026);
следующий шаг — спека (дельты OpenSpec, контракт v1.3, §11/§12 в 05-domain-rules.md, DBML v5.19).
Цель
Нормализация результата распознавания форм ББ/ОФР в 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 | Что считается кодом при поиске столбца | любое целое число (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…Р-7 — в конце (все приняты 17.10.2026). Имена полей — рабочие, уточняются при написании спеки.
Принципы, из которых выведены решения по строкам:
- Один выход на шаг. Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1).
- Ничего не выбрасывать молча. Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со статусом, а не превращается в счётчик; пользователь её видит и исправляет.
- Качество — структурно и с привязкой к месту: уверенность ячейки (от источника),
is_recognized(разбор числа), статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность,is_recognizedи §7 сервис сводит в один признак ячейкиneeds_review(шаг 8) и из него, статусов строк и предупреждений выводит статус документа; ядро при загрузке ничего не пересчитывает, после правок аналитика считает тем же общим кодом. - Один словарь — одно место: проверки §7, периоды, заголовки форм, диапазоны кодов — в общем коде сервисов; соответствие «код → наименование» (словарь строк с редакциями) — только в ядре, сервисы его не знают и отдают распознанный текст (решение владельца 17.10.2026, Р-3).
- Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста в общем коде, там, где уже
известен смысл ячейки: ИНН и дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных
строк (шаг 3), сумма (шаг 4). Разбор вызывается для любой ячейки без
value; у Excel он срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Факт исправления фиксируется там же:is_recognized = falseу значения,unreadableу кода, предупреждение у даты. Изnormalizer/ocr/шлюзаtext,dates,amountsстановятся общими разборами,layout,rows,header,document— шагами 2–6 общего кода.
Шаг 1 — Подготовка данных
Решения владельца (17.10.2026): 1) кто размечает — различие остаётся; 2) на выходе одинаковый контракт; 3) шапка своя у каждой таблицы; 4) тип формы — единый подход; 5) уверенность есть и у парсера, всегда 100; 6) парсер склеивает продолжения в шаге 1; 7) ячейка — один формат.
Вход: байты файла (шаг 0).
Единый выход: список кандидатов в форму на файл, FormCandidate:
source—ocr|excel;locator— откуда взят (имя json и номер таблицы / имена листов);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;warnings— предупреждения шага (формат — шаг 8).
Ячейка Cell одна для шапки и таблицы: text (как показывает источник), value (число или дата, если источник их
знает; у OCR — null), confidence 0–100 (у Excel — 100), ref (позиция в источнике для сообщений: row_index /
column_index у OCR, адрес A1 у Excel).
- 1. Кто размечает — остаётся, фиксируется полем
source- scan-gateway: без изменений
- tabular-parser: без изменений
- 2. Единица разметки — файл → список кандидатов; шаги 2–8 работают только с кандидатом (Р-1 — форма общего кода)
- scan-gateway: адаптер «таблица json → кандидат»
- tabular-parser: адаптер «блок формы на листе → кандидат»
- 3. Шапка — у каждого кандидата своя; поля = поля
headerконтракта, каждое —Cell; разбор текста поля (ИНН → цифры, дата →date, единицы → словарь) — общий код по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» или по позиции листа; проверка «число колонок сумм совпало» из склейки уходит (повторная шапка внутри сетки — правило шага 2, общее для обоих: у OCR она и так внутри таблицы); тип из маркера сверяется с
type_hint, расхождение — отдельный кандидат + предупреждение- 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 (сетка, type_hint).
Единый выход: TableLayout — столбец кода, столбец наименования, колонки значений [{col, label, period_end}],
роль каждой строки сетки (header | numbering | data), сегменты (участки сетки между повторными шапками, у
каждого своя раскладка колонок).
- 1. Что считается кодом при поиске столбца — одно правило: 4–5 цифр (допускается
.0), первые четыре — в диапазонах словаря кодов всех форм (type_hintнеизвестен — объединение диапазонов); столбец с наибольшим числом кодов, кодов ≥ 3, доля среди непустых ≥ порога; нет — «колонка кодов не найдена»- scan-gateway:
int(text)→ правило словаря (закрывает AURORA-1517 вместе с п. 5) - tabular-parser: диапазон формы → объединение;
relaxedдля одиноких продолжений не нужен (они склеены в шаге 1)
- scan-gateway:
- 2. Повторная шапка внутри таблицы — одно правило: строка внутри сетки с признаком шапки («Код» / та же сигнатура) открывает новый сегмент; у сегмента своя раскладка (позиции колонок могут сдвинуться); колонки значений сегментов сопоставляются по порядку; число не совпало — предупреждение, значения по позиции
- scan-gateway: как сейчас, через общее правило
- tabular-parser: добавляет: бывший «лист-продолжение со своей раскладкой» становится сегментом
- 3. Дата колонки — один словарь периодов (
statement_periods.yaml) →period_end: абсолютные подписи («На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «За 9 месяцев 2025 г.», «2024») и относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года» — дата считается от даты отчёта из шапки; даты отчёта нет → колонка без даты). Перед разбором — очистка двойников OCR (общий шаг, у Excel не срабатывает). Относительные подписи — решение владельца 17.10.2026 (Р-6): это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»- scan-gateway:
parse_date«размером с дату» → словарь - tabular-parser: добавляет очистку двойников (no-op)
- scan-gateway:
- 4. Без шапки — одно правило: строка заголовка не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа;
labelпустая,period_endnull; одно предупреждение «строка заголовка не найдена»- scan-gateway: синтез шапки уходит (он эквивалентен)
- tabular-parser: добавляет предупреждение
- 5. Строка нумерации «1 2 3 4 5» — одно правило
is_numbering_row→ рольnumbering, в данные не попадает- scan-gateway: добавляет (AURORA-1517)
- tabular-parser: как сейчас
- 6. Подпись без периода — решение владельца (17.10.2026, Р-6): подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением
info; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть иperiod_end = null, предупреждениеerrorс привязкой к колонке, документneeds_check, аналитик подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает: период формы берётся из даты отчёта, без неё — из самой поздней даты остальных колонок (statement_period). Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением- scan-gateway: добавляет словарь исключений и относительные периоды
- tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды
Шаг 3 — Отбор строк данных
Вход: FormCandidate + TableLayout.
Единый выход: RawLine[] — по строке на каждую строку сетки с ролью data, у которой есть код или хотя бы одно
число: ref, ячейка кода, code_status (ok | unreadable — код есть, но не разобран | missing — кода нет, суммы
есть), ячейка наименования, ячейки значений по колонкам раскладки. Строка без кода и без чисел (заголовок раздела
«АКТИВ», «в том числе:») — не строка. Счётчиков отброшенных нет — есть статусы. Повтор кода — не статус: по правилам
отчётности один код может стоять у нескольких строк с разными наименованиями, все они ok.
- 1. Код-мусор — статус
unreadable, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250)- scan-gateway: счётчик → статус
- tabular-parser: отличает «нет кода» от «не разобран»
- 2. Склейка двух строк в одну — одно правило: ячейка кода содержит N кодов через перевод строки и ячейки значений — N частей → N строк (OQ-156); у Excel не срабатывает
- scan-gateway: как сейчас, через общее правило
- tabular-parser: правило общее, no-op
- 3. Повтор кода — решение владельца (17.10.2026): повтор кода — законный случай (один код, разные наименования), не ошибка и не предупреждение. Все строки сохраняются как есть со статусом
ok; текстовое предупреждение иchecks.duplicate_line_codesуходят. Для проверок §7, подтверждения и всего, что идёт после, значения одного кода идут суммой (см. «Что уходит из ядра»; договорённость ДБД в AURORA-1431; закрывает AURORA-1406). Неверно прочитанный код, случайно совпавший с существующим, ловит §7 через сумму- scan-gateway: «оставлена первая» → все строки
- tabular-parser:
duplicatesуходит
- 4. Отброшенная строка — строки не отбрасываются:
unreadable/missingидут дальше со статусом (AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча- scan-gateway: счётчики → статусы
- tabular-parser: счётчики → статусы
Шаг 4 — Значения ячеек
Вход: ячейки значений RawLine по колонкам раскладки (Cell или null).
Единый выход: StatementValue {column_index, amount, string_value, confidence, is_recognized} — как в контракте,
с одним смыслом полей у обоих.
- 1. Предобработка текста — у ячейки после шага 1 либо есть
value, либо толькоtext.value— число →amount = value(2 знака),is_recognized = true.valueнет или не число (дата, логическое) → одинparse_amount(text)для обоих: двойники, разделители тысяч, скобки → минус, прочерк →null; Excel-ячейки «число текстом» идут тем же путём- scan-gateway:
parse_amountпереезжает в общий код - tabular-parser:
to_moneyзаменяется общимparse_amount
- 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,string_value— сырой текст (AURORA-1511, 1519)- scan-gateway: меняется через общий код
- tabular-parser: то же правило, у Excel почти не срабатывает
- 4. Единицы — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа
- scan-gateway: добавляет
- tabular-parser: как сейчас
- 5. Пустая ячейка — решение владельца (17.10.2026, Р-2): одно правило: позиции нет, текст пуст или прочерк →
amount = null,string_value = null,is_recognized = true,confidenceячейки (нет ячейки — 100), собственной подсветки нет. Поfixtures/ocrИС отдаёт ячейку на каждую позицию каждой строки: пустые — пустой текст с уверенностью 100, непрочитанные — с уверенностью 0, и такие идут вneeds_reviewпо общему правилу шага 8; позиция отсутствует только при несовпадении колонок сегмента, что покрыто предупреждениемerrorшага 2 п. 2- scan-gateway: «ячейки нет → 0, не распознано» уходит
- tabular-parser: как сейчас
Шаг 5 — Код, наименование, редакция
Вход: RawLine (статус, ячейки кода и наименования) + StatementValue[].
Единый выход: StatementLine {line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence, values}: line_code — разобранный код (4–5 цифр) или null при unreadable / missing;
line_code_text — сырой текст ячейки кода; line_status = code_status шага 3 (ok | unreadable | missing).
Несколько строк с одним кодом — обычные строки ok, порядок как в файле.
- 1. Источник наименования — решение владельца (17.10.2026): сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк). Экран показывает справочное имя, файловое — рядом (Р-3; AURORA-1406, 1522)
- scan-gateway: имя из
statement_lines.yaml→ имя из OCR - tabular-parser: как сейчас
- scan-gateway: имя из
- 2. Редакция (66н / ФСБУ 4) — в сервисах не определяется; редакция — свойство словаря строк ядра (по дате отчёта), ядро использует её при достройке шаблонных строк, показе справочного имени и проверке «код не из редакции формы»
- scan-gateway:
_choose_editionуходит - tabular-parser: без изменений
- scan-gateway:
- 3. Справочник строк — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется
- scan-gateway: удаляет
api/rules/statement_lines.yaml - tabular-parser: без изменений
- scan-gateway: удаляет
- 4. Уверенность кода — от источника (ИС / 100), без срезания до 99 по справочнику; «код не в справочнике» — проверка ядра по его словарю, не сервисов
- scan-gateway: убирает
min(conf, 99) - tabular-parser: как сейчас
- scan-gateway: убирает
Шаг 6 — Сборка документа
Вход: FormCandidate (type_hint, evidence, header) + TableLayout + StatementLine[].
Единый выход: StatementDocument {document_type, locator, header, columns, lines} без checks.
- 1. Тип формы — одно правило: большинство кодов 1xxx →
BALANCE, 2xxx →INCOME; совпало сtype_hintилиtype_hintnull— тип по кодам; противоречит — тип по кодам + предупреждение; кодов нет —type_hint; без колонки кодов:type_hintесть → документ без строк (DISCLOSURE—done, иначеneeds_check),type_hintнет → не документ, предупреждение info- scan-gateway: как сейчас, через общее правило
- tabular-parser: добавляет сверку с кодами
- 2. Склейка страниц — решена в шаге 1, различия нет
- scan-gateway: без изменений
- tabular-parser: без изменений
- 3.
locator— одна грамматика «парыключ:значениечерез;», набор ключей по источнику:json:<имя>;table:<i>/sheet:<A>[,<B>]- scan-gateway: без изменений
- tabular-parser: без изменений
Шапка документа — поля header кандидата как есть (text → *_text, value → поле, confidence → *_confidence).
Шаг 7 — Самопроверки
Вход: StatementDocument.
Единый выход: StatementChecks у обоих: relations_failed[], soft_relations_failed[] (каждое нарушение — с
кодами слагаемых и column_index, чтобы экран подсветил ячейки). Проверки по словарю строк (код не в словаре, код не
из редакции формы по дате отчёта) — в ядре, сервисы словаря строк не знают (Р-3). duplicate_line_codes уходит:
соотношения считаются по сумме значений одного кода в
колонке (повтор кода — законный случай, шаг 3 п. 3); нарушение подсвечивает все строки этого кода.
- 1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает — проверки — общий код с одним словарём
statement_checks.yamlу обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читаетchecksиз результата и пересчитывает тем же кодом только после правок аналитика (PATCH)- scan-gateway: добавляет шаг
- tabular-parser:
run_statement_checks→ общий код
Шаг 8 — Статус и предупреждения
Вход: StatementDocument + StatementChecks + предупреждения шагов 1–7.
Единый выход: у каждого документа status (done | needs_check) и warnings[]; у файла metrics.warnings[];
статус конверта — агрегат. Предупреждение — структура {code, severity: error | info, message, line_ref?, column_index?}, а не строка.
- 1. Критерий
done— один критерий из самого результата: раскладка найдена ∧ (организация или ИНН) ∧ дата отчёта ∧ все строкиok∧ нет значений и строкneeds_review(см. «Подтверждение проверкой» ниже) ∧ жёсткие проверки §7 сошлись ∧ нет предупреждений уровняerror.DISCLOSURE—doneбез условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически- scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
- tabular-parser:
_is_ok→ общий критерий; добавляются строки и значения
- 2. Влияние предупреждений — по уровню:
error→needs_check,info— не влияет; у обоих одинаково; уровень каждого предупреждения задаётся в спеке (например, «строка заголовка не найдена», «колонка без даты», «тип по кодам противоречит заголовку» —error; «подпись из словаря исключений пропущена», «дробные при тыс. руб.» —info)- scan-gateway: «любое →
needs_check» уходит - tabular-parser: предупреждения начинают влиять
- scan-gateway: «любое →
- 3. Статус документа — в контракте:
document.status; конвертdone⇔ все документыdone∧ нет предупреждений файла уровняerror; иначеneeds_check; документов нет —error(как сейчас)- scan-gateway: добавляет
- tabular-parser: внутренний
ok→ в контракт
- 4. Формат предупреждений — структура (см. выше) вместо строк
<json>: table:<i>: …/sheet=<имя> …; место — вlocatorдокумента, а не в тексте- scan-gateway: меняет
- tabular-parser: меняет
- 5. Привязка — к документу, строке (
line_ref) или ячейке (line_ref+column_index); экран показывает у места- scan-gateway: меняет
- tabular-parser: меняет
Подтверждение проверкой (вопрос владельца 17.10.2026). Проверка §7 — арбитр, уверенность — подсказка там, где
проверки нет. По каждому значению сервис считает needs_review:
true, еслиconfidenceниже порога (Р-5: 100) илиis_recognized = false(число собрано из грязного текста, Р-7) и значение не покрыто сошедшимся жёстким соотношением §7 в своей колонке (не входит ни в одно, соотношение не сошлось или оно мягкое);trueу всех ячеек соотношения в колонке, где оно не сошлось, независимо от уверенности;- иначе
false: значение подтверждено проверкой, не подсвечивается и статус не трогает; сырой текст и уверенность остаются в подсказке ячейки.
Экран подсвечивает ячейки только по needs_review; после PATCH ядро пересчитывает checks и needs_review общим
кодом. Не покрыто проверками и остаётся на уверенности: поля шапки, справочные строки ОФР 25xx / 29xx, детализация
без проверки родителя, DISCLOSURE, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие
ошибки в одном соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без
ущерба для 1519. Сошедшееся жёсткое равенство подтверждает и догадку из грязного текста (Р-7, решение владельца
17.10.2026): «(7744536}» → −7 744 536 при 2100 = 2110 − 2120 верна; случайное совпадение требует второй,
компенсирующей ошибки в том же равенстве.
Код и наименование строки (Р-5, решение владельца 17.10.2026). Уверенность кода подтверждается не §7 (перепутанные
соседи 1220 и 1230 дают ту же сумму раздела), а структурой: код есть в перечне кодов формы и стоит по возрастанию
внутри раздела. Подтверждён — уверенность не важна; иначе needs_review у строки (StatementLine.needs_review),
документ — needs_check. По fixtures/ocr коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как
шлюз сейчас, нельзя. Перечень кодов форм без наименований — часть общего кода (нужен и для поиска столбца кодов), не
словарь строк. Уверенность наименования на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть;
сопоставление файлового наименования со справочным по коду — вопрос ядра, открыт (см. «Что уходит из ядра»).
Шаг 9 — Результат
Одинаково, как сейчас; контракт recognition-result.schema.json → v1.3: line_status, line_code nullable,
line_code_text; needs_review у значения и у строки; document.status; document.warnings[] и metrics.warnings[]
структурные (уровень, привязка); checks обязателен для BALANCE / INCOME со строками у обоих конвейеров (у
документа без строк отсутствует), нарушения с кодами слагаемых, без duplicate_line_codes.
Что уходит из ядра
build_statement: повторная дедупликация колонок, кодов и значений (контракт гарантирует уникальность колонок; повторы кодов — законные строки, хранятся все как пришли).- Повторы кодов (решение владельца 17.10.2026): в
financial_statement_lineхранятся все строки; уникальный индекс(meta_id, line_code)из DBML v5.18 снимается (DBML v5.19, миграция). Для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки, сверки), документ читается через агрегат по коду — сумма значений, справочное имя — чистой функцией, без хранения (DECISION-5). Экран показывает строки как в файле. statement_status(D-6):RECOGNIZED⇔document.status = done∧ ИНН ≠MISMATCH(единственное знание ядра); остальные признаки D-6 не пересчитываются.- §7 при загрузке:
checksберутся из результата; пересчёт общим кодом — только после PATCH. - Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь строк (код → наименование, редакции) остаётся только в ядре, копия шлюза удаляется; проверки по нему (код не в словаре, код не из редакции по дате отчёта) ядро делает само при загрузке и после PATCH, как предупреждения к строке.
- Достройка шаблонных строк: по словарю ядра; строки
missing/unreadableпоказываются как пришли, с подсветкой; код, у которого есть хотя бы одна строка из файла, шаблоном не достраивается. - Экран: подсветка ячеек только по
needs_review(уверенность,is_recognizedи нарушения §7 уже сведены сервисом); строк — поline_statusиneeds_reviewстроки; предупреждения документа — с привязкой к месту. - Открытый вопрос ядра (не сервисов): сопоставлять ли файловое наименование строки со справочным по коду и что делать при расхождении (владелец 17.10.2026: «наверное, будем пытаться сопоставить»). В сервисах уверенность и текст наименования на статус не влияют.
Решения владельца (Р-1…Р-7, 17.10.2026)
- Р-1 (шаг 1, п. 2) — решено 17.10.2026: копия по скрипту. Платформа не собирает Python-пакеты и не кладёт их
в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json, golden-файлы в fixtures), шаги 2–8 и
словари §7, периодов, заголовков форм, диапазонов кодов (без словаря строк) — живёт в отдельном репозитории
arm-statementsсо своими тестами и оформлен как пакетarm_statements/сpyproject. В tabular-parser, scan-gateway и arm-core он лежит копией вapi/src/arm_statements/с маркером коммита источника; раскладывает скриптsync-statementsв docs по образцуsync-web.sh,make checkпотребителя проверяет свежесть копии, прямые правки копии запрещены. Когда платформа научится собирать пакеты, копия заменяется строкой зависимости без смены импортов. - Р-2 (шаг 4, п. 5) — решено владельцем 17.10.2026: отсутствующая позиция под колонкой — чистый
nullбез собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100, непрочитанная — 0, и её ловит общее правилоneeds_review); отсутствие позиции бывает только при несовпадении колонок сегмента и покрыто предупреждением шага 2 п. 2. - Р-3 (шаг 5, п. 1) — решено владельцем 17.10.2026: сервисы отдают распознанный текст наименования; соответствие «код → наименование» — только в ядре, при необходимости (= решение № 2 по спринту 2, AURORA-1406 / 1522; договорённость ДБД «из справочника» выполняется на экране).
- Р-4 (шаг 3, п. 3) — решено владельцем 17.10.2026: повторы кодов законны; все строки сохраняются и
показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие:
снять уникальность
(meta_id, line_code)в DBML. - Р-5 (шаг 8, «Подтверждение проверкой») — решено владельцем 17.10.2026: порог для значений — 100 (у ИС уверенность дискретная: 100, ~80, 71, 67, 60, 50, 25, 0, промежутка 90–99 нет; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80); ложных подсветок становится меньше за счёт подтверждения проверкой §7, а не порога. Код строки подтверждается структурой (перечень кодов формы + порядок), не уверенностью. Уверенность наименования в сервисах не используется; сопоставление со справочным — открытый вопрос ядра (= решение № 7 по спринту 2, AURORA-1519 / 1524).
- Р-6 (шаг 2, п. 6) — решено владельцем 17.10.2026: числовая колонка с нераспознанной подписью остаётся в
документе без даты с предупреждением
error(needs_check, подтверждение руками); пропуск отвергнут как тихая потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …) добавляются в словарь периодов и считаются от даты отчёта. - Р-7 (шаг 8, «Подтверждение проверкой») — решено владельцем 17.10.2026: да. Сошедшееся жёсткое соотношение §7
снимает подсветку и с
is_recognized = false; такой документ может получитьdoneи автоподтвердиться, сырой текст остаётся в подсказке ячейки. Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок.
Decisions made without discussion
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все обратимы правкой спеки или словаря.
- Уровни предупреждений (шаг 8, п. 2):
error— то, что меняет данные или их полноту (нет строки заголовка, колонка без даты, тип по кодам противоречит заголовку, колонки сегмента не совпали);info— остальное (пропущена подпись из словаря исключений, дробные при «тыс. руб.»). Цена ошибки: лишний или пропущенныйneeds_check, правится таблицей уровней в спеке. DISCLOSUREвсегдаdoneбез условий о раскладке, строках и проверках — сохранено текущее поведение парсера. Цена: пояснения не проверяются; так и сейчас.- Порог столбца кодов — «кодов ≥ 3 и доля среди непустых ≥ порога» взят у парсера как есть;
relaxedдля одиноких продолжений снят, потому что продолжения склеиваются в шаге 1. Цена: таблица с одним-двумя кодами без столбца кодов → документ без строк сneeds_check. - Начальный словарь исключений подписей («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам парсера; пополняется словарём, не кодом.
- Грамматика
locator— парыключ:значениечерез;, ключи по источнику. Цена: минимальная, формат уже такой. value, который не число (дата, логическое) в колонке сумм — разбор как текст. Цена: минимальная.checksотсутствует у документа без строк, обязателен уBALANCE/INCOMEсо строками.- Рабочие имена
FormCandidate,TableLayout,RawLine,needs_review,line_status,line_code_text,arm-statements,api/src/arm_statements/,sync-statements— по образцу существующих (sync-web.sh); меняются при написании спеки без последствий.