docs(#116): внесены находки ревью F-1…F-25 и решения Р-8…Р-11 (detail, контракт v2.0, уникальность document, двойники в суммах)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3c31e0ecf5
commit
63f134b342
@ -1,7 +1,8 @@
|
||||
# Единый алгоритм нормализации ББ/ОФР (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).
|
||||
Issue: arm-ka-docs#116. Статус: целевая картина по шагам 1–9 согласована с владельцем (решения Р-1…Р-11, 17.10.2026),
|
||||
находки независимого ревью F-1…F-25 внесены (`review-2026-10-17.md`); следующий шаг — спека (дельты OpenSpec,
|
||||
контракт v2.0, §11/§12 в `05-domain-rules.md`, DBML v5.19).
|
||||
|
||||
## Цель
|
||||
|
||||
@ -315,29 +316,35 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
|
||||
|
||||
## Целевая картина
|
||||
|
||||
Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, таблица «различие → к чему
|
||||
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца помечены датой; сводка решений
|
||||
Р-1…Р-7 — в конце (все приняты 17.10.2026). Имена полей — рабочие, уточняются при написании спеки.
|
||||
Каждый шаг ниже: **вход = выход предыдущего шага**, единый выход для обоих сервисов, список «различие → к чему
|
||||
приводим → что меняется в scan-gateway → что меняется в tabular-parser». Решения владельца помечены датой; сводка
|
||||
решений Р-1…Р-11 — в конце (все приняты 17.10.2026). Находки независимого ревью F-1…F-25
|
||||
(`review-2026-10-17.md`) внесены в текст с маркерами. Имена полей — рабочие, уточняются при написании спеки.
|
||||
|
||||
Принципы, из которых выведены решения по строкам:
|
||||
|
||||
1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист
|
||||
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями (форма общего кода — Р-1).
|
||||
2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
|
||||
статусом, а не превращается в счётчик; пользователь её видит и исправляет.
|
||||
статусом, а не превращается в счётчик; пользователь её видит и исправляет. Положение в источнике (`ref`) и порядок
|
||||
строк (`row_index`) доезжают до базы и экрана (F-4).
|
||||
3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа),
|
||||
статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность, `is_recognized` и §7
|
||||
сервис сводит в один признак ячейки `needs_review` (шаг 8) и из него, статусов строк и предупреждений выводит
|
||||
статус документа; ядро при загрузке ничего не пересчитывает, после правок аналитика считает тем же общим кодом.
|
||||
4. **Один словарь — одно место:** проверки §7, периоды, заголовки форм, диапазоны кодов — в общем коде сервисов;
|
||||
соответствие «код → наименование» (словарь строк с редакциями) — только в ядре, сервисы его не знают и отдают
|
||||
распознанный текст (решение владельца 17.10.2026, Р-3).
|
||||
статус документа. Ядро при загрузке ничего не пересчитывает, хранит `needs_review`, нарушения и предупреждения как
|
||||
пришли; после правок аналитика считает тем же общим кодом, добавляя своё знание: правка снимает `needs_review`,
|
||||
акцепт снимает жёсткие нарушения, квартал периода (F-8, F-18).
|
||||
4. **Один словарь — одно место:** проверки §7, периоды, заголовки форм, упорядоченные перечни кодов форм по редакциям
|
||||
(без наименований) — в общем коде сервисов; соответствие «код → наименование» (словарь строк с редакциями) — только
|
||||
в ядре, сервисы его не знают и отдают распознанный текст (решение владельца 17.10.2026, Р-3).
|
||||
5. **Очистка OCR (§11.1) — не отдельный шаг, а первая стадия типовых разборов текста** в общем коде, там, где уже
|
||||
известен смысл ячейки: ИНН и дата шапки (шаг 1), подписи колонок (шаг 2), код строки и разлепление склеенных
|
||||
строк (шаг 3), сумма (шаг 4). Разбор вызывается для любой ячейки без `value`; у Excel он срабатывает только на
|
||||
текстовых ячейках и почти всегда ничего не меняет. Факт исправления фиксируется там же: `is_recognized = false`
|
||||
у значения, `unreadable` у кода, предупреждение у даты. Из `normalizer/ocr/` шлюза `text`, `dates`, `amounts`
|
||||
становятся общими разборами, `layout`, `rows`, `header`, `document` — шагами 2–6 общего кода.
|
||||
строк (шаг 3), сумма (шаг 4). Буквы-двойники («О» → 0, «З» → 3, «б» → 6, «S» → 5, «I», «l», «|» → 1) заменяются во
|
||||
всех четырёх разборах, включая суммы и коды — **решение владельца 17.10.2026 (Р-11)**, отменяет §11.1 п. 1 и решение
|
||||
№ 22 среза 3a («только даты»): сумма или код с заменой получает `is_recognized = false`, а дальше её судьбу решают
|
||||
контрольные соотношения §7 (Р-7 с оговорками F-1, F-2). Разбор вызывается для любой ячейки без `value`; у Excel он
|
||||
срабатывает только на текстовых ячейках и почти всегда ничего не меняет. Из `normalizer/ocr/` шлюза `text`,
|
||||
`dates`, `amounts` становятся общими разборами, `layout`, `rows`, `header`, `document` — шагами 2–6 общего кода.
|
||||
|
||||
### Шаг 1 — Подготовка данных
|
||||
|
||||
@ -350,12 +357,14 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
|
||||
**Единый выход:** список кандидатов в форму на файл, `FormCandidate`:
|
||||
|
||||
- `source` — `ocr` | `excel`;
|
||||
- `locator` — откуда взят (имя json и номер таблицы / имена листов);
|
||||
- `locator` — откуда взят: `file:<имя json>;table:<i>` (как в §11 и golden-эталонах, F-23) / `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, F-16);
|
||||
- `warnings` — предупреждения шага (формат — шаг 8).
|
||||
|
||||
Ячейка `Cell` одна для шапки и таблицы: `text` (как показывает источник), `value` (число или дата, если источник их
|
||||
@ -377,37 +386,37 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
|
||||
- **5. Уверенность** — поле `Cell.confidence`; Excel всегда 100
|
||||
- scan-gateway: без изменений
|
||||
- tabular-parser: добавляет поле
|
||||
- **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа; проверка «число колонок сумм совпало» из склейки уходит (повторная шапка внутри сетки — правило шага 2, общее для обоих: у OCR она и так внутри таблицы); тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение
|
||||
- **6. Многостраничная форма** — одна форма = одна сетка на выходе шага 1; строки продолжения дописываются в конец сетки по маркеру «Форма … с.2» или по позиции листа, граница листа запоминается в `segment_breaks`; тип из маркера сверяется с `type_hint`, расхождение — отдельный кандидат + предупреждение. Раскладка у каждого сегмента своя (шаг 2 п. 2), поэтому разная ширина колонок или объединённые ячейки на листе-продолжении без шапки не сдвигают значения (F-16); число колонок сегментов сверяется там же
|
||||
- scan-gateway: без изменений
|
||||
- tabular-parser: склейка переезжает до раскладки; `join_blocker` остаётся только по типу
|
||||
- tabular-parser: склейка переезжает до раскладки; `join_blocker` остаётся только по типу; проверка числа колонок переезжает в шаг 2 п. 2
|
||||
- **7. Ячейка** — один формат `Cell`; Excel: `text` — как показывает числовой формат, `value` — число по формату (tp#42; формат не разобран — сырое значение + предупреждение); OCR: `text` сырой, `value` `null`, `confidence` от ИС
|
||||
- scan-gateway: обёртка над `value` + `confidence` ИС
|
||||
- tabular-parser: отображаемый текст и значение считаются здесь, а не в шаге 4
|
||||
|
||||
### Шаг 2 — Раскладка таблицы
|
||||
|
||||
**Вход:** `FormCandidate` (сетка, `type_hint`).
|
||||
**Вход:** `FormCandidate` (сетка, `segment_breaks`, `type_hint`).
|
||||
|
||||
**Единый выход:** `TableLayout` — столбец кода, столбец наименования, колонки значений `[{col, label, period_end}]`,
|
||||
роль каждой строки сетки (`header` | `numbering` | `data`), сегменты (участки сетки между повторными шапками, у
|
||||
каждого своя раскладка колонок).
|
||||
**Единый выход:** `TableLayout` — сегменты сетки (участки между повторными шапками и границами листов), у каждого:
|
||||
столбец кода, столбец наименования, колонки значений `[{col, label, period_end, period_kind}]`, сопоставленные с
|
||||
первым сегментом по порядку; роль каждой строки сетки (`header` | `numbering` | `data`).
|
||||
|
||||
- **1. Что считается кодом при поиске столбца** — одно правило в два прохода (уточнено по вопросу владельца 17.10.2026): (1) есть строка заголовка с ячейкой «Код» / «Код строки» → столбец кодов под ней, без порога по числу кодов — документ из двух строк остаётся документом, нечитаемые коды становятся строками `unreadable` (так шлюз сейчас); (2) заголовка нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля среди непустых ≥ порога (так парсер сейчас); нет — «колонка кодов не найдена». Код — 4–5 цифр (допускается `.0`), первые четыре в диапазонах словаря кодов всех форм (`type_hint` неизвестен — объединение диапазонов)
|
||||
- scan-gateway: проход по заголовку — как сейчас; в запасном подсчёте `int(text)` → правило словаря (закрывает AURORA-1517 вместе с п. 5)
|
||||
- tabular-parser: добавляет проход по заголовку до подсчёта; диапазон формы → объединение; `relaxed` для одиноких продолжений не нужен (они склеены в шаге 1)
|
||||
- **2. Повторная шапка внутри таблицы** — одно правило: строка внутри сетки с признаком шапки («Код» / та же сигнатура) открывает новый сегмент; у сегмента своя раскладка (позиции колонок могут сдвинуться); колонки значений сегментов сопоставляются по порядку; число не совпало — предупреждение, значения по позиции
|
||||
- **1. Столбец кодов** — одно правило (уточнено по вопросу владельца 17.10.2026; F-5, F-21, F-24): код — текст без пробелов и переносов, 4–5 цифр (допускается `.0`), первые четыре в диапазонах словаря кодов всех форм, включая 5xxx пояснений (`type_hint` неизвестен — объединение диапазонов). (а) Столбец, над данными которого стоит подпись «Код» / «Код строки» / «Коды» **и под которой есть хотя бы один код из диапазона**; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С первая подпись «Коды» на листе — титульный блок реквизитов, под ней ОКУД, дата, ОКПО, ИНН, ОКЕИ, а не коды строк (F-5). Порога по числу кодов нет — документ из двух строк остаётся документом. (б) Подписи нет → столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля среди непустых ≥ порога (так парсер сейчас); нет — «колонка кодов не найдена». Статусы строк с нечитаемым или пустым кодом — новое поведение для обоих: шлюз сегодня такие строки пропускает со счётчиком, а кодом считает любое целое (F-24)
|
||||
- scan-gateway: проход по подписи получает защиту «под подписью есть код»; в запасном подсчёте `int(text)` → правило словаря (закрывает AURORA-1517 вместе с п. 5)
|
||||
- tabular-parser: добавляет проход по подписи до подсчёта; диапазон формы → объединение; `relaxed` для одиноких продолжений не нужен (они склеены в шаге 1)
|
||||
- **2. Повторная шапка и границы листов** — одно правило: новый сегмент открывает строка с признаком шапки («Код» / та же сигнатура) **или** граница листа из `segment_breaks` (F-16); у сегмента своя раскладка: по его шапке, а без шапки — столбец кодов по правилу п. 1 и колонки значений по числам в строках с кодами (как у продолжений парсера сейчас); колонки значений сегмента сопоставляются с первым сегментом по порядку; число не совпало — предупреждение `error`, значения по позиции
|
||||
- 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 без даты: На отчетную дату»
|
||||
- tabular-parser: бывший «лист-продолжение со своей раскладкой» становится сегментом; бывшая проверка числа колонок в `join_blocker` — предупреждение сегмента
|
||||
- **3. Дата колонки** — один словарь периодов (`statement_periods.yaml`) → `period_end` и `period_kind`: абсолютные подписи («На 31 марта 2025 г.», «За январь – март 2025 г.», «31.12.2024», «За 9 месяцев 2025 г.», «2024») → `absolute`; относительные («На отчётную дату», «На 31 декабря предыдущего года», «На 31 декабря года, предшествующего предыдущему», «За отчётный период», «За аналогичный период прошлого года») → `relative:<вид>`, дата считается от **эффективной даты документа**: дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок (F-6); нет ни той ни другой → `period_end = null`, `period_kind` остаётся относительным, и ядро досчитает дату после правки даты отчёта аналитиком (F-10). Перед разбором — очистка двойников OCR (общий шаг, у Excel не срабатывает). Относительные подписи — решение владельца 17.10.2026 (Р-6): это бланки по приказу 66н без впечатанных дат, у шлюза сейчас «колонка 0 без даты: На отчетную дату»
|
||||
- scan-gateway: `parse_date` «размером с дату» → словарь
|
||||
- tabular-parser: добавляет очистку двойников (no-op)
|
||||
- **4. Без шапки** — одно правило: строка заголовка не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`; одно предупреждение «строка заголовка не найдена»
|
||||
- scan-gateway: синтез шапки уходит (он эквивалентен)
|
||||
- tabular-parser: добавляет очистку двойников (no-op) и относительные подписи
|
||||
- **4. Строка заголовка и её отсутствие** — одно правило (обе ветки парсера сохраняются, F-5): строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись «Код»; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кодов (или одной, не голым годом); двухстрочный заголовок склеивается. Не найдена → колонки значений = столбцы правее кода, где в строках с кодами стоят числа; `label` пустая, `period_end` `null`; одно предупреждение `error` «строка заголовка не найдена»
|
||||
- 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 это тихая потеря периода с возможным автоподтверждением
|
||||
- **6. Подпись без периода** — **решение владельца (17.10.2026, Р-6):** подпись — период по словарю (абсолютный или относительный, п. 3) → колонка с датой; подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск с предупреждением `info`; иначе, если в столбце есть числа в строках с кодами (OCR-мусор вроде «| | г» с уверенностью 17 в aep_3, обрезанная или самодельная подпись) — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, предупреждение `error` с привязкой к колонке, документ `needs_check`, аналитик подтверждает после сверки с источником; иначе пропуск. Ядру колонка без даты не мешает: период формы берётся из эффективной даты (п. 3, `statement_period`). Пропуск числовой колонки с нераспознанной подписью отвергнут: для OCR это тихая потеря периода с возможным автоподтверждением
|
||||
- scan-gateway: добавляет словарь исключений и относительные периоды
|
||||
- tabular-parser: перестаёт молча пропускать неизвестные числовые колонки; добавляет относительные периоды
|
||||
|
||||
@ -416,41 +425,55 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
|
||||
**Вход:** `FormCandidate` + `TableLayout`.
|
||||
|
||||
**Единый выход:** `RawLine[]` — по строке на каждую строку сетки с ролью `data`, у которой есть код или хотя бы одно
|
||||
число: `ref`, ячейка кода, `code_status` (`ok` | `unreadable` — код есть, но не разобран | `missing` — кода нет, суммы
|
||||
есть), ячейка наименования, ячейки значений по колонкам раскладки. Строка без кода и без чисел (заголовок раздела
|
||||
«АКТИВ», «в том числе:») — не строка. Счётчиков отброшенных нет — есть статусы. Повтор кода — не статус: по правилам
|
||||
отчётности один код может стоять у нескольких строк с разными наименованиями, все они `ok`.
|
||||
число: `row_index` (порядковый номер в документе, F-4), `ref`, ячейка кода, `code_status`, ячейка наименования, ячейки
|
||||
значений по колонкам раскладки сегмента, `parent_ref` (для `detail`). Статусы (`ok` | `unreadable` | `missing` |
|
||||
`detail`):
|
||||
|
||||
- **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250)
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — ячейка кода непуста, но кодом не разобралась («ИЧ,250», «изо :», «| 140»);
|
||||
- `detail` — **решение владельца 17.10.2026 (Р-8):** строка без кода с числами, стоящая под строкой с кодом в том же
|
||||
сегменте, — расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар (41.01)»); родитель —
|
||||
ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит, в проверки §7, агрегаты и критерий `done` она не
|
||||
входит, экран по умолчанию не показывает (F-3). Отменяет OQ-36 / OQ-141 («строки без кода не отдаются»);
|
||||
- `missing` — строка без кода с числами, над которой в сегменте нет строки с кодом (кода не было или он потерян в
|
||||
самом начале таблицы).
|
||||
|
||||
Строка без кода и без чисел (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. Счётчиков отброшенных
|
||||
нет — есть статусы. Повтор кода — не статус: по правилам отчётности один код может стоять у нескольких строк с
|
||||
разными наименованиями, все они `ok`. Строка формы с пустой ячейкой кода под другой строкой с кодом попадёт в `detail`
|
||||
и будет скрыта; её значения не войдут в сумму раздела, нарушение §7 подсветит родителя, аналитик раскроет детали и
|
||||
назначит код (F-14) — осознанная цена Р-8.
|
||||
|
||||
- **1. Код-мусор** — статус `unreadable`, строка сохраняется с сырым текстом кода (AURORA-1462: «ИЧ,250» видна, аналитик назначает 1250). Разбор кода: двойники (Р-11), пробелы и переносы убираются («1 100» в aep_3 — это 1100, F-21), затем правило 4–5 цифр
|
||||
- scan-gateway: счётчик → статус
|
||||
- tabular-parser: отличает «нет кода» от «не разобран»
|
||||
- **2. Склейка двух строк в одну** — одно правило: ячейка кода содержит N кодов через перевод строки и ячейки значений — N частей → N строк (OQ-156); у Excel не срабатывает
|
||||
- scan-gateway: как сейчас, через общее правило
|
||||
- **2. Склейка двух строк в одну** — одно правило: ячейка кода содержит N частей через перевод строки и ячейки значений — N частей → N строк (OQ-156), разлепление делается и тогда, когда часть кода нечитаема — она становится строкой `unreadable` (F-25); число частей не совпало — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`; у 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); ядро не достраивает вместо них пустую шаблонную строку молча
|
||||
- **4. Отброшенная строка** — строки не отбрасываются: `unreadable` / `missing` / `detail` идут дальше со статусом, `row_index` и `ref` (AURORA-1634, 1462); ядро не достраивает вместо них пустую шаблонную строку молча. Отмена OQ-36 / OQ-141 — Р-8
|
||||
- scan-gateway: счётчики → статусы
|
||||
- tabular-parser: счётчики → статусы
|
||||
- tabular-parser: счётчики → статусы; строки без кода с суммами начинают отдаваться
|
||||
|
||||
### Шаг 4 — Значения ячеек
|
||||
|
||||
**Вход:** ячейки значений `RawLine` по колонкам раскладки (`Cell` или `null`).
|
||||
|
||||
**Единый выход:** `StatementValue {column_index, amount, string_value, confidence, is_recognized}` — как в контракте,
|
||||
с одним смыслом полей у обоих.
|
||||
с одним смыслом полей у обоих; `needs_review` досчитывается в шаге 8.
|
||||
|
||||
- **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` — число → `amount = value` (2 знака), `is_recognized = true`. `value` нет или не число (дата, логическое) → один `parse_amount(text)` для обоих: двойники, разделители тысяч, скобки → минус, прочерк → `null`; Excel-ячейки «число текстом» идут тем же путём
|
||||
- scan-gateway: `parse_amount` переезжает в общий код
|
||||
- **1. Предобработка текста** — у ячейки после шага 1 либо есть `value`, либо только `text`. `value` — число → `amount = value`, квантование до 2 знаков half-up, не больше 16 целых цифр (`numeric(18,2)`), иначе `amount = null`, `string_value` = текст, `is_recognized = false` и предупреждение (F-20); `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false` (F-20); `value` нет → один `parse_amount(text)` для обоих: двойники (Р-11), разделители тысяч, скобки → минус, причём `}` и `]` принимаются как закрывающая скобка (OQ-147, F-1), прочерк → `null`; «лучшая догадка» из цифр ограничена 16 цифрами, иначе `null` (F-20); Excel-ячейки «число текстом» идут тем же путём
|
||||
- scan-gateway: `parse_amount` переезжает в общий код и получает квантование, предел длины, `}` / `]`
|
||||
- tabular-parser: `to_money` заменяется общим `parse_amount`
|
||||
- **2. Смысл `is_recognized`** — один: текст разобран как число без выброшенных и исправленных символов; `confidence` — отдельно, от источника; оба признака сервис сводит в `needs_review` (шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему
|
||||
- **2. Смысл `is_recognized`** — один: текст разобран как число без выброшенных и исправленных символов (замена двойника — исправление); `confidence` — отдельно, от источника; оба признака сервис сводит в `needs_review` (шаг 8, «Подтверждение проверкой»), экран подсвечивает по нему
|
||||
- scan-gateway: отвязывает от `confidence`
|
||||
- tabular-parser: как сейчас
|
||||
- **3. «42831 1»** — в `parse_amount` пробел допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — лучшая догадка (цифры подряд), `is_recognized = false`, `string_value` — сырой текст (AURORA-1511, 1519)
|
||||
- scan-gateway: меняется через общий код
|
||||
- tabular-parser: то же правило, у Excel почти не срабатывает
|
||||
- **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа
|
||||
- **4. Единицы** — одно правило: единицы «тыс. руб.» и дробных ≥ 10 % → предупреждение документа `info` (как у парсера сейчас, документ остаётся `done`)
|
||||
- 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
|
||||
@ -459,19 +482,19 @@ sheet:<A>,<B>`. `DISCLOSURE` и форма без колонки кодов —
|
||||
|
||||
### Шаг 5 — Код, наименование, редакция
|
||||
|
||||
**Вход:** `RawLine` (статус, ячейки кода и наименования) + `StatementValue[]`.
|
||||
**Вход:** `RawLine` (статус, `row_index`, `ref`, ячейки кода и наименования) + `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`, порядок как в файле.
|
||||
**Единый выход:** `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_status` = `code_status` шага 3;
|
||||
`parent_ref` — у `detail`. Несколько строк с одним кодом — обычные строки `ok`, порядок как в файле.
|
||||
|
||||
- **1. Источник наименования** — **решение владельца (17.10.2026):** сервисы отдают распознанный текст наименования как есть, с уверенностью источника; соответствие «код → наименование» — только в ядре и только когда нужно (показ справочного имени, достройка шаблонных строк). Экран показывает справочное имя, файловое — рядом (Р-3; AURORA-1406, 1522)
|
||||
- scan-gateway: имя из `statement_lines.yaml` → имя из OCR
|
||||
- tabular-parser: как сейчас
|
||||
- **2. Редакция (66н / ФСБУ 4)** — в сервисах не определяется; редакция — свойство словаря строк ядра (по дате отчёта), ядро использует её при достройке шаблонных строк, показе справочного имени и проверке «код не из редакции формы»
|
||||
- scan-gateway: `_choose_edition` уходит
|
||||
- tabular-parser: без изменений
|
||||
- **2. Редакция (66н / ФСБУ 4)** — в сервисах редакция нужна только для упорядоченного перечня кодов (подтверждение кода структурой, шаг 8; F-9) и выбирается по эффективной дате документа (шаг 2 п. 3), без даты — проверяются обе редакции. Наименования, достройка шаблонных строк и проверка «код не из редакции формы» — ядро по его словарю строк
|
||||
- scan-gateway: `_choose_edition` → выбор редакции перечня кодов в общем коде
|
||||
- tabular-parser: добавляет выбор редакции перечня кодов
|
||||
- **3. Справочник строк** — только в ядре; в общий код сервисов не входит; копия в шлюзе удаляется
|
||||
- scan-gateway: удаляет `api/rules/statement_lines.yaml`
|
||||
- tabular-parser: без изменений
|
||||
@ -485,13 +508,13 @@ line_name_confidence, values}`: `line_code` — разобранный код (4
|
||||
|
||||
**Единый выход:** `StatementDocument {document_type, locator, header, columns, lines}` без `checks`.
|
||||
|
||||
- **1. Тип формы** — одно правило: большинство кодов 1xxx → `BALANCE`, 2xxx → `INCOME`; совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение; кодов нет — `type_hint`; без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check`), `type_hint` нет → не документ, предупреждение info
|
||||
- **1. Тип формы** — одно правило: большинство кодов 1xxx → `BALANCE`, 2xxx → `INCOME`, 5xxx и прочие → `DISCLOSURE` (F-22); совпало с `type_hint` или `type_hint` `null` — тип по кодам; противоречит — тип по кодам + предупреждение `error`; кодов нет — `type_hint`; без колонки кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с предупреждением `error`), `type_hint` нет → не документ, предупреждение `info`. Для `DISCLOSURE` шаги 2–7 не выполняются, `columns` / `lines` пустые (как сейчас; ядро строки пояснений не хранит)
|
||||
- scan-gateway: как сейчас, через общее правило
|
||||
- tabular-parser: добавляет сверку с кодами
|
||||
- **2. Склейка страниц** — решена в шаге 1, различия нет
|
||||
- scan-gateway: без изменений
|
||||
- tabular-parser: без изменений
|
||||
- **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `json:<имя>;table:<i>` / `sheet:<A>[,<B>]`
|
||||
- **3. `locator`** — одна грамматика «пары `ключ:значение` через `;`», набор ключей по источнику: `file:<имя json>;table:<i>` / `sheet:<A>[,<B>]` — ключи как сейчас, golden-эталоны и §11 не меняются (F-23)
|
||||
- scan-gateway: без изменений
|
||||
- tabular-parser: без изменений
|
||||
|
||||
@ -502,12 +525,23 @@ line_name_confidence, values}`: `line_code` — разобранный код (4
|
||||
**Вход:** `StatementDocument`.
|
||||
|
||||
**Единый выход:** `StatementChecks` у обоих: `relations_failed[]`, `soft_relations_failed[]` (каждое нарушение — с
|
||||
кодами слагаемых и `column_index`, чтобы экран подсветил ячейки). Проверки по словарю строк (код не в словаре, код не
|
||||
из редакции формы по дате отчёта) — в ядре, сервисы словаря строк не знают (Р-3). `duplicate_line_codes` уходит:
|
||||
соотношения считаются по сумме значений одного кода в
|
||||
колонке (повтор кода — законный случай, шаг 3 п. 3); нарушение подсвечивает все строки этого кода.
|
||||
кодами слагаемых и `column_index`, чтобы экран подсветил ячейки). Правила счёта:
|
||||
|
||||
- **1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает** — проверки — общий код с одним словарём `statement_checks.yaml` у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает `checks` из результата и пересчитывает тем же кодом только после правок аналитика (PATCH)
|
||||
- значение кода в колонке — сумма по всем строкам `ok` с этим кодом (повтор кода — законный случай, шаг 3 п. 3); строки
|
||||
`detail`, `unreadable`, `missing` в соотношения не входят; `duplicate_line_codes` уходит; нарушение подсвечивает все
|
||||
строки кода;
|
||||
- «сошлось» / «не сошлось» (F-13): жёсткое соотношение не сошлось, если расхождение больше допуска ±1 **и** больше
|
||||
округления 0,001 % — только тогда оно в `relations_failed`; расхождение в пределах округления и мягкие соотношения —
|
||||
в `soft_relations_failed`, это `info`, на статус и подсветку не влияют (OQ-138 сохраняется);
|
||||
- соотношение с членами по модулю (`abs_terms`: 2120, 2210, 2220, 2330, 2350, 2410) сходится при любом знаке такого
|
||||
члена и потому **подтверждает только величину, не знак** (F-1) — см. «Подтверждение проверкой»;
|
||||
- соотношение считается **посчитанным** только если хотя бы один его член в колонке непуст; все члены `null` — не
|
||||
проверяется и ничего не подтверждает (F-2; то же правило, что для «родитель = Σ подстрок» по AURORA-1527).
|
||||
|
||||
Проверки по словарю строк (код не в словаре, код не из редакции формы по дате отчёта) — в ядре, сервисы словаря строк
|
||||
не знают (Р-3).
|
||||
|
||||
- **1. Проверки только у парсера; словарь §7 в двух копиях; ядро результат не читает** — проверки — общий код с одним словарём `statement_checks.yaml` у обоих сервисов; для OCR нарушение §7 — главный признак неверно прочитанной цифры; ядро читает `checks` из результата, хранит их (`document_issue`, см. «Что уходит из ядра») и пересчитывает тем же кодом только после правок аналитика (PATCH)
|
||||
- scan-gateway: добавляет шаг
|
||||
- tabular-parser: `run_statement_checks` → общий код
|
||||
|
||||
@ -515,91 +549,137 @@ line_name_confidence, values}`: `line_code` — разобранный код (4
|
||||
|
||||
**Вход:** `StatementDocument` + `StatementChecks` + предупреждения шагов 1–7.
|
||||
|
||||
**Единый выход:** у каждого документа `status` (`done` | `needs_check`) и `warnings[]`; у файла `metrics.warnings[]`;
|
||||
статус конверта — агрегат. Предупреждение — структура `{code, severity: error | info, message, line_ref?,
|
||||
column_index?}`, а не строка.
|
||||
**Единый выход:** у каждого документа `status` (`done` | `needs_check`), `warnings[]`, `needs_review` у каждого
|
||||
значения и у каждой строки; у файла `metrics.warnings[]`; статус конверта — агрегат. Предупреждение — структура
|
||||
`{code, severity: error | info, message, line_ref?, column_index?}`, а не строка.
|
||||
|
||||
- **1. Критерий `done`** — один критерий из самого результата: раскладка найдена ∧ (организация или ИНН) ∧ дата отчёта ∧ все строки `ok` ∧ нет значений и строк `needs_review` (см. «Подтверждение проверкой» ниже) ∧ жёсткие проверки §7 сошлись ∧ нет предупреждений уровня `error`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически
|
||||
- **1. Критерий `done`** — одна функция общего кода с входами «раскладка, шапка, статусы строк, `needs_review`, нарушения, предупреждения» (F-8): раскладка найдена ∧ (организация или ИНН) ∧ период определён (дата отчёта **или** самая поздняя абсолютная дата колонок — `statement_period` ядра; у ИС поле `date` в трёх из четырёх фикстур не дата, F-6) ∧ все строки `ok` или `detail` ∧ нет значений и строк `needs_review` (см. «Подтверждение проверкой» ниже) ∧ нет жёстких нарушений §7 вне допуска и округления (F-13) ∧ нет предупреждений уровня `error`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках (как сейчас). Excel выполняет условия уверенности автоматически. Квартал периода — знание ядра, в `done` не входит (F-18)
|
||||
- scan-gateway: «ничего не выброшено и уверенность 100» → общий критерий
|
||||
- tabular-parser: `_is_ok` → общий критерий; добавляются строки и значения
|
||||
- **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково; уровень каждого предупреждения задаётся в спеке (например, «строка заголовка не найдена», «колонка без даты», «тип по кодам противоречит заголовку» — `error`; «подпись из словаря исключений пропущена», «дробные при тыс. руб.» — `info`)
|
||||
- **2. Влияние предупреждений** — по уровню: `error` → `needs_check`, `info` — не влияет; у обоих одинаково. Таблица уровней (F-12), полная версия — в спеке. `error`: ошибочный json набора при успешном соседе (уровень файла; решение № 28 среза 3a), `BACKEND_PARSING_ERROR` таблицы, таблица или лист с содержимым без колонки кодов (в том числе потерянный лист-продолжение), колонки сегмента не совпали, колонка без даты, строка заголовка не найдена, тип по кодам противоречит заголовку, ИНН не распознан, период не определён (нет ни даты отчёта, ни дат колонок), формат файла не совпал с `file_type`. `info`: подпись из словаря исключений пропущена, дробные при «тыс. руб.», дата отчёта не разобрана при наличии дат колонок, ИНН распознан неуверенно (ядро сверяет с ИНН ЮЛ), единицы не найдены (аналитик правит в шапке), xlsb без числовых форматов (tp#42, документ остаётся `done`), не документ без `type_hint` и кодов
|
||||
- scan-gateway: «любое → `needs_check`» уходит
|
||||
- tabular-parser: предупреждения начинают влиять
|
||||
- **3. Статус документа** — в контракте: `document.status`; конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` (как сейчас)
|
||||
- **3. Статус документа** — в контракте: `document.status`; конверт `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` с кодом по источнику: `RECOGNITION_FAILED` у шлюза, `INVALID_DOCUMENT` у парсера — это различие остаётся, коды описывают разные причины (F-23)
|
||||
- scan-gateway: добавляет
|
||||
- tabular-parser: внутренний `ok` → в контракт
|
||||
- **4. Формат предупреждений** — структура (см. выше) вместо строк `<json>: table:<i>: …` / `sheet=<имя> …`; место — в `locator` документа, а не в тексте
|
||||
- scan-gateway: меняет
|
||||
- tabular-parser: меняет
|
||||
- **5. Привязка** — к документу, строке (`line_ref`) или ячейке (`line_ref` + `column_index`); экран показывает у места
|
||||
- **5. Привязка** — к документу, строке (`line_ref`) или ячейке (`line_ref` + `column_index`); `line_ref` — это `ref` строки, он хранится в ядре вместе с `row_index` (F-4), поэтому экран показывает предупреждение у места и после сохранения
|
||||
- scan-gateway: меняет
|
||||
- tabular-parser: меняет
|
||||
|
||||
**Подтверждение проверкой (вопрос владельца 17.10.2026).** Проверка §7 — арбитр, уверенность — подсказка там, где
|
||||
проверки нет. По каждому значению сервис считает `needs_review`:
|
||||
**Подтверждение проверкой (вопрос владельца 17.10.2026; F-1, F-2, F-13).** Проверка §7 — арбитр, уверенность —
|
||||
подсказка там, где проверки нет. По каждому значению сервис считает `needs_review`:
|
||||
|
||||
- `true`, если `confidence` ниже порога (Р-5: 100) или `is_recognized = false` (число собрано из грязного текста,
|
||||
Р-7) **и** значение не покрыто сошедшимся жёстким соотношением §7 в своей колонке (не входит ни в одно, соотношение
|
||||
не сошлось или оно мягкое);
|
||||
- `true` у всех ячеек соотношения в колонке, где оно не сошлось, независимо от уверенности;
|
||||
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано (хотя бы
|
||||
один член непуст, F-2) и сошлось (в пределах допуска или округления, F-13), и `amount` самого значения не `null`
|
||||
(мусор, давший `null`, покрытым не бывает, F-2);
|
||||
- `needs_review = true`, если `confidence` ниже порога (Р-5: 100) или `is_recognized = false` (число собрано из
|
||||
грязного текста или с заменой двойников, Р-7, Р-11) **и** значение не покрыто;
|
||||
- **знак** (F-1): для члена соотношения с `abs_terms` покрытие подтверждает только величину. Знак подтверждён, если
|
||||
форма записи знака у ячейки (скобки, минус, без знака) совпадает с конвенцией колонки — формой записи у большинства
|
||||
членов `abs_terms` в этой колонке с `is_recognized = true`; не совпадает или сравнивать не с чем — `needs_review =
|
||||
true` независимо от соотношения. Так «(7744536}» с `}` как закрывающей скобкой даёт −7 744 536 и подтверждается,
|
||||
если 2210 и 2220 в той же колонке тоже в скобках; «7744536}» без открывающей скобки при остальных в скобках —
|
||||
подсвечивается;
|
||||
- `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 верна; случайное совпадение требует второй,
|
||||
компенсирующей ошибки в том же равенстве.
|
||||
Экран подсвечивает ячейки только по `needs_review`. Ядро хранит `needs_review` как пришло и пересчитывает его вместе с
|
||||
`checks` общим кодом после PATCH, добавляя своё: правленое значение (`is_modified`) — `needs_review = false`; акцепт
|
||||
нарушений — жёсткие нарушения не считаются; предупреждения `error` документа сохраняются и после правки (F-8). Не
|
||||
покрыто проверками и остаётся на уверенности: поля шапки, справочные строки ОФР 25xx / 29xx, детализация без проверки
|
||||
родителя, `DISCLOSURE`, колонки, где есть только мягкие соотношения. Риск — две взаимно компенсирующие ошибки в одном
|
||||
соотношении; принимается (перекрёстные соотношения баланса его снижают). Закрывает AURORA-1524 без ущерба для 1519.
|
||||
Сошедшееся посчитанное равенство подтверждает и догадку из грязного текста (Р-7, решение владельца 17.10.2026):
|
||||
случайное совпадение требует второй, компенсирующей ошибки в том же равенстве.
|
||||
|
||||
**Код и наименование строки (Р-5, решение владельца 17.10.2026).** Уверенность кода подтверждается не §7 (перепутанные
|
||||
соседи 1220 и 1230 дают ту же сумму раздела), а структурой: код есть в перечне кодов формы и стоит по возрастанию
|
||||
внутри раздела. Подтверждён — уверенность не важна; иначе `needs_review` у строки (`StatementLine.needs_review`),
|
||||
документ — `needs_check`. По `fixtures/ocr` коды часто приходят с уверенностью 50 и 25, поэтому требовать 100, как
|
||||
шлюз сейчас, нельзя. Перечень кодов форм без наименований — часть общего кода (нужен и для поиска столбца кодов), не
|
||||
словарь строк. Уверенность наименования на статус и подсветку в сервисах не влияет: сервисы отдают текст как есть;
|
||||
сопоставление файлового наименования со справочным по коду — вопрос ядра, открыт (см. «Что уходит из ядра»).
|
||||
**Код и наименование строки (Р-5, решение владельца 17.10.2026; F-9).** Уверенность кода подтверждается не §7
|
||||
(перепутанные соседи 1220 и 1230 дают ту же сумму раздела), а структурой: в общем коде лежит **упорядоченный перечень
|
||||
кодов** каждой формы по редакциям (66н, ФСБУ 4) без наименований — порядок бланка, где итоги стоят после слагаемых
|
||||
(1110 … 1190, 1100; 2110, 2120, 2100), а не по возрастанию. Код подтверждён, если он есть в перечне и его положение
|
||||
относительно соседних подтверждённых кодов совпадает с порядком бланка хотя бы в одной из проверяемых редакций
|
||||
(редакция по эффективной дате, без даты — обе); равные соседние коды допустимы (Р-4). Подтверждён — уверенность не
|
||||
важна; иначе `needs_review` у строки (`StatementLine.needs_review`), документ — `needs_check`. По `fixtures/ocr` коды
|
||||
часто приходят с уверенностью 50 и 25, поэтому требовать 100, как шлюз сейчас, нельзя. Перечень кодов — не словарь
|
||||
строк: наименований в нём нет, Р-3 не нарушается. Уверенность наименования на статус и подсветку в сервисах не
|
||||
влияет: сервисы отдают текст как есть; сопоставление файлового наименования со справочным по коду — вопрос ядра,
|
||||
открыт (см. «Что уходит из ядра»).
|
||||
|
||||
### Шаг 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`.
|
||||
Одинаково, как сейчас; контракт `recognition-result.schema.json` → **v2.0** (**решение владельца 17.10.2026, Р-9**: по
|
||||
правилам контракта смена типа поля — MAJOR, F-7). Что меняется: `line_code` nullable, `line_status`, `line_code_text`,
|
||||
`row_index`, `ref`, `parent_ref`, `needs_review` у значения и у строки; `document.status`; `document.warnings[]` и
|
||||
`metrics.warnings[]` — структуры с уровнем и привязкой; `columns[].period_kind`; `checks` обязателен для `BALANCE` /
|
||||
`INCOME` со строками у обоих конвейеров (у документа без строк отсутствует), нарушения с кодами слагаемых, без
|
||||
`duplicate_line_codes`; `service.statements_version` — коммит копии общего кода, которым посчитан результат (F-17).
|
||||
Выкладка: ядро поднимает `SUPPORTED_MAJOR` и на время перехода принимает 1.x и 2.0 (два маппера), сервисы переходят
|
||||
на 2.0 после ядра — порядок «сначала ядро» обязателен, иначе первый же результат с `null` в `line_code` даёт файлу
|
||||
`ERROR` (F-7).
|
||||
|
||||
### Что уходит из ядра
|
||||
### Что уходит из ядра и что в нём меняется
|
||||
|
||||
- `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.
|
||||
- **DBML v5.19 и одна миграция** (F-4, F-8, F-10, Р-4, Р-10): `financial_statement_line` — снять уникальность
|
||||
`(meta_id, line_code)`, добавить `row_index`, `source_ref`, `line_status`, `line_code_text`, `parent_row_index`
|
||||
(родитель `detail`); `financial_statement_value.needs_review` (как пришло от сервиса, пересчитывается ядром после
|
||||
PATCH; пересматривает решение v5.13 «считается, не хранится»); `financial_statement_column.period_kind`;
|
||||
таблица `document_issue` (вид `warning` | `relation` | `soft`, код, уровень, сообщение, `line_id?`, `column_id?`,
|
||||
источник `service` | `core`) — хранит предупреждения и нарушения как пришли и после пересчёта; `document` — снять
|
||||
уникальность `(stored_file_id, document_type, osv_account_code)` и добавить порядковый номер формы в файле
|
||||
(**решение владельца 17.10.2026, Р-10**; F-11: вторая форма того же типа больше не теряется по OQ-77, привязка к
|
||||
версии консолидации — по периоду, как у ОСВ); в ту же миграцию — `crm_id` ГСК / ГСЗ (AURORA-1619, arm-core#112,
|
||||
отложенный «до следующих изменений БД»).
|
||||
- Повторы кодов (решение владельца 17.10.2026): для проверок §7, подтверждения и всего, что идёт после (КФО, выгрузки,
|
||||
сверки), документ читается через **одну функцию «агрегат формы по коду»** — сумма значений по колонке, список
|
||||
`line_id`, справочное имя — чистой функцией, без хранения (DECISION-5). На неё переводятся все точки, где сегодня
|
||||
«одна строка на код» (F-15): `_line_amount` (межформенная проверка 1370 / 2400), `statement_amounts`,
|
||||
`subline_parents` / `parent_code`, `formLayout` (дети под родителем), `_resolve_cells`, `metrics.fields_*`.
|
||||
Экран показывает строки как в файле, по `row_index`.
|
||||
- `statement_status` (D-6): `RECOGNIZED` ⇔ `document.status = done` ∧ ИНН ≠ `MISMATCH` ∧ период — конец квартала
|
||||
(`period_known`, F-18: иначе документ `RECOGNIZED`, но не подтверждаемый, 409 `PERIOD_REQUIRED`); остальные признаки
|
||||
D-6 не пересчитываются.
|
||||
- §7 при загрузке: `checks`, `needs_review` и предупреждения берутся из результата и хранятся (`document_issue`);
|
||||
пересчёт общим кодом — только после PATCH, с правилами `is_modified` ⇒ `needs_review = false`, акцепт ⇒ жёсткие
|
||||
нарушения сняты, предупреждения `error` сервиса сохраняются (F-8). Версия общего кода из результата
|
||||
(`statements_version`) пишется в `recognition_job`; при пересчёте ядро сравнивает её со своей копией, расхождение —
|
||||
предупреждение `error` к документу, не падение (F-17).
|
||||
- Правка даты отчёта (PATCH шапки): для колонок с `period_kind = relative:<вид>` ядро досчитывает `period_end` общим
|
||||
кодом от новой даты (F-10).
|
||||
- **Назначение кода строке** (F-14): `PATCH …/statement` принимает `lines: [{line_id, line_code | null, line_status:
|
||||
ok | detail}]`; назначенный код может совпадать с существующим (Р-4); `line_status` → `ok`, `line_code_modified`,
|
||||
`line_code_original_value`, аудит `RECOGNIZED_DATA_EDIT` с `before / after`, пересчёт `checks` и `needs_review`;
|
||||
строке `detail` можно назначить код (становится `ok`), строку `missing` — объявить расшифровкой (`detail`).
|
||||
- Словарь §7: единственный экземпляр в общем коде (Р-1), копия ядра удаляется. Словарь строк (код → наименование,
|
||||
редакции) остаётся только в ядре, копия шлюза удаляется; проверки по нему (код не в словаре, код не из редакции по
|
||||
дате отчёта) ядро делает само при загрузке и после PATCH, как предупреждения к строке.
|
||||
- Достройка шаблонных строк: по словарю ядра; строки `missing` / `unreadable` показываются как пришли, с
|
||||
подсветкой; код, у которого есть хотя бы одна строка из файла, шаблоном не достраивается.
|
||||
дате отчёта) ядро делает само при загрузке и после PATCH, как предупреждения к строке в `document_issue`.
|
||||
- Достройка шаблонных строк: по словарю ядра; строки `missing` / `unreadable` показываются как пришли, на своём месте по
|
||||
`row_index`, с подсветкой; код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не достраивается.
|
||||
- Экран: подсветка ячеек только по `needs_review` (уверенность, `is_recognized` и нарушения §7 уже сведены сервисом);
|
||||
строк — по `line_status` и `needs_review` строки; предупреждения документа — с привязкой к месту.
|
||||
строк — по `line_status` и `needs_review` строки; строки `detail` скрыты по умолчанию под родителем,
|
||||
переключатель как у пустых строк детализации (Р-8); предупреждения и нарушения — из `document_issue`, с привязкой к
|
||||
месту; поле кода редактируемо у строк `unreadable` / `missing` / `detail`.
|
||||
- Открытый вопрос ядра (не сервисов): сопоставлять ли файловое наименование строки со справочным по коду и что
|
||||
делать при расхождении (владелец 17.10.2026: «наверное, будем пытаться сопоставить»). В сервисах уверенность и
|
||||
текст наименования на статус не влияют.
|
||||
|
||||
### Решения владельца (Р-1…Р-7, 17.10.2026)
|
||||
### Решения владельца (Р-1…Р-11, 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` потребителя проверяет свежесть копии, прямые
|
||||
правки копии запрещены. Когда платформа научится собирать пакеты, копия заменяется строкой зависимости без смены
|
||||
импортов.
|
||||
в свой Nexus, поэтому общий код — dataclass-ы шага 1 (сериализуемые в json, golden-файлы в fixtures), шаги 2–8,
|
||||
словари §7, периодов, заголовков форм, перечни кодов (без словаря строк) со своим загрузчиком — живёт в отдельном
|
||||
репозитории `arm-statements` со своими тестами и оформлен как пакет `arm_statements/` с `pyproject`. Уточнения по
|
||||
F-17: в tabular-parser, scan-gateway и arm-core пакет лежит копией вместе со своими тестами и конфигом линтеров,
|
||||
импортируется по имени пакета `arm_statements` (путь добавляется в `pyproject`), поэтому замена копии на
|
||||
зависимость не трогает импорты; маркер коммита источника попадает в результат как `service.statements_version`;
|
||||
раскладывает и проверяет свежесть скрипт `sync-statements` в docs по образцу `sync-web.sh` (`--check`, код 20 при
|
||||
прямых правках) — **не** `make check` потребителя: тест, читающий соседний репозиторий, уже ломал main (arm-core #33,
|
||||
пайплайн платформы вне репозиториев). Прямые правки копии запрещены.
|
||||
- **Р-2** (шаг 4, п. 5) — **решено владельцем 17.10.2026:** отсутствующая позиция под колонкой — чистый `null` без
|
||||
собственной подсветки. Основание: по fixtures ИС отдаёт ячейку на каждую позицию (пустая — уверенность 100,
|
||||
непрочитанная — 0, и её ловит общее правило `needs_review`); отсутствие позиции бывает только при несовпадении
|
||||
@ -609,42 +689,74 @@ column_index?}`, а не строка.
|
||||
1522; договорённость ДБД «из справочника» выполняется на экране).
|
||||
- **Р-4** (шаг 3, п. 3) — **решено владельцем 17.10.2026:** повторы кодов законны; все строки сохраняются и
|
||||
показываются, для проверок и всех операций после подтверждения или автоподтверждения — суммой по коду. Следствие:
|
||||
снять уникальность `(meta_id, line_code)` в DBML.
|
||||
снять уникальность `(meta_id, line_code)` в DBML; все чтения «одна строка на код» в ядре переводятся на агрегат
|
||||
(F-15).
|
||||
- **Р-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).
|
||||
порога. Код строки подтверждается структурой — упорядоченным перечнем кодов формы по редакции (F-9), не
|
||||
уверенностью. Уверенность наименования в сервисах не используется; сопоставление со справочным — открытый вопрос
|
||||
ядра (= решение № 7 по спринту 2, AURORA-1519 / 1524).
|
||||
- **Р-6** (шаг 2, п. 6) — **решено владельцем 17.10.2026:** числовая колонка с нераспознанной подписью остаётся в
|
||||
документе без даты с предупреждением `error` (`needs_check`, подтверждение руками); пропуск отвергнут как тихая
|
||||
потеря периода. Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года», …)
|
||||
добавляются в словарь периодов и считаются от даты отчёта.
|
||||
добавляются в словарь периодов и считаются от эффективной даты документа (F-6); после правки даты отчёта ядро
|
||||
досчитывает их (F-10).
|
||||
- **Р-7** (шаг 8, «Подтверждение проверкой») — **решено владельцем 17.10.2026: да.** Сошедшееся жёсткое соотношение §7
|
||||
снимает подсветку и с `is_recognized = false`; такой документ может получить `done` и автоподтвердиться, сырой
|
||||
текст остаётся в подсказке ячейки. Варианты «всегда подсвечивать» и «снимать подсветку, но не автоподтверждать»
|
||||
отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт статус «требует проверки» без подсветок.
|
||||
текст остаётся в подсказке ячейки. Оговорки по ревью: соотношение должно быть посчитанным, а не «0 = 0» (F-2); для
|
||||
членов по модулю знак подтверждается конвенцией колонки, а не соотношением (F-1). Варианты «всегда подсвечивать» и
|
||||
«снимать подсветку, но не автоподтверждать» отвергнуты: первый возвращает ложные подсветки AURORA-1524, второй даёт
|
||||
статус «требует проверки» без подсветок.
|
||||
- **Р-8** (шаг 3, F-3) — **решено владельцем 17.10.2026:** расшифровки без кода («в том числе …») отдаются сервисами
|
||||
со статусом `detail` и хранятся ядром; в проверки, агрегаты и `done` не входят; экран по умолчанию их не показывает
|
||||
(переключатель, как у пустых строк детализации). Отменяет OQ-36 / OQ-141.
|
||||
- **Р-9** (шаг 9, F-7) — **решено владельцем 17.10.2026:** контракт **v2.0**, а не аддитивный 1.3; ядро на время
|
||||
перехода принимает 1.x и 2.0; порядок выкладки — сначала ядро.
|
||||
- **Р-10** (ядро, F-11) — **решено владельцем 17.10.2026:** снять уникальность `document` по `(stored_file_id,
|
||||
document_type, osv_account_code)`, вторая форма того же типа в файле хранится; в ту же миграцию DBML v5.19 —
|
||||
`crm_id` ГСК / ГСЗ из AURORA-1619 (arm-core#112).
|
||||
- **Р-11** (принцип 5, F-19) — **решено владельцем 17.10.2026:** буквы-двойники заменяются и в суммах, и в кодах;
|
||||
результат — `is_recognized = false`, дальше его проверяют контрольные соотношения §7, и если всё сошлось,
|
||||
подсвечивать не нужно (Р-7 с оговорками F-1, F-2). Отменяет §11.1 п. 1 «только даты» и решение № 22 среза 3a.
|
||||
Данные: в четырёх json ИС двойников в суммах и кодах нет вовсе — правило на вырост, не под текущие сканы.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все
|
||||
обратимы правкой спеки или словаря.
|
||||
обратимы правкой спеки или словаря. Пункты 9–13 добавлены при внесении находок ревью F-1…F-25.
|
||||
|
||||
1. **Уровни предупреждений** (шаг 8, п. 2): `error` — то, что меняет данные или их полноту (нет строки заголовка,
|
||||
колонка без даты, тип по кодам противоречит заголовку, колонки сегмента не совпали); `info` — остальное (пропущена
|
||||
подпись из словаря исключений, дробные при «тыс. руб.»). Цена ошибки: лишний или пропущенный `needs_check`,
|
||||
правится таблицей уровней в спеке.
|
||||
2. **`DISCLOSURE` всегда `done`** без условий о раскладке, строках и проверках — сохранено текущее поведение парсера.
|
||||
Цена: пояснения не проверяются; так и сейчас.
|
||||
3. **Порог столбца кодов** — «кодов ≥ 3 и доля среди непустых ≥ порога» взят у парсера и действует только в запасном
|
||||
проходе без заголовка «Код»; по заголовку порога нет (уточнено с владельцем 17.10.2026, шаг 2 п. 1). `relaxed` для
|
||||
одиноких продолжений снят, потому что продолжения склеиваются в шаге 1. Цена: таблица без заголовка и с одним-двумя
|
||||
кодами → документ без строк с `needs_check`.
|
||||
колонка без даты, тип по кодам противоречит заголовку, колонки сегмента не совпали, потерянный json или лист,
|
||||
таблица без колонки кодов, ИНН не распознан, период не определён); `info` — остальное (пропущена подпись из
|
||||
словаря исключений, дробные при «тыс. руб.», xlsb без форматов, ИНН неуверенно, единицы не найдены). Цена ошибки:
|
||||
лишний или пропущенный `needs_check`, правится таблицей уровней в спеке.
|
||||
2. **`DISCLOSURE` всегда `done`** без условий о раскладке, строках и проверках; шаги 2–7 для него не выполняются,
|
||||
5xxx в правиле большинства → `DISCLOSURE` (F-22) — сохранено текущее поведение парсера. Цена: пояснения не
|
||||
проверяются; так и сейчас.
|
||||
3. **Столбец кодов** — подпись «Код» принимается только с кодом под ней (F-5), запасной подсчёт «кодов ≥ 3 и доля
|
||||
среди непустых ≥ порога» взят у парсера; `relaxed` для одиноких продолжений снят, потому что продолжения
|
||||
склеиваются в шаге 1. Цена: таблица без подписи и с одним-двумя кодами → документ без строк с `needs_check`.
|
||||
4. **Начальный словарь исключений подписей** («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») — по тестам
|
||||
парсера; пополняется словарём, не кодом.
|
||||
5. **Грамматика `locator`** — пары `ключ:значение` через `;`, ключи по источнику. Цена: минимальная, формат уже такой.
|
||||
6. **`value`, который не число** (дата, логическое) в колонке сумм — разбор как текст. Цена: минимальная.
|
||||
5. **Грамматика `locator`** — пары `ключ:значение` через `;`, ключи по источнику, как сейчас (`file:` у OCR, F-23).
|
||||
Цена: нулевая.
|
||||
6. **`value`, который не число** (дата, логическое) в колонке сумм — `amount = null`, `string_value` = текст,
|
||||
`is_recognized = false` (F-20). Цена: минимальная.
|
||||
7. **`checks` отсутствует у документа без строк**, обязателен у `BALANCE` / `INCOME` со строками.
|
||||
8. **Рабочие имена** `FormCandidate`, `TableLayout`, `RawLine`, `needs_review`, `line_status`, `line_code_text`,
|
||||
`arm-statements`, `api/src/arm_statements/`, `sync-statements` — по образцу существующих (`sync-web.sh`); меняются
|
||||
при написании спеки без последствий.
|
||||
`row_index`, `source_ref`, `parent_ref`, `period_kind`, `document_issue`, `statements_version`, `arm-statements`,
|
||||
`sync-statements` — по образцу существующих (`sync-web.sh`); меняются при написании спеки без последствий.
|
||||
9. **Правило `detail`** (шаг 3): строка без кода с числами под строкой с кодом в том же сегменте; родитель — ближайшая
|
||||
строка с кодом выше. Цена: строка формы с потерянным кодом под другой строкой скрывается до нарушения §7 у
|
||||
родителя; лечится назначением кода (F-14).
|
||||
10. **Конвенция знака** для членов `abs_terms` (F-1): форма записи у большинства членов `abs_terms` колонки с
|
||||
`is_recognized = true`; расхождение или отсутствие образца — `needs_review`. Цена: ОФР, где расходы записаны
|
||||
вперемешку скобками и минусом, подсвечивает расходы; это и нужно.
|
||||
11. **Хранить `needs_review`, нарушения и предупреждения** в ядре (`financial_statement_value.needs_review`,
|
||||
`document_issue`) вместо пересчёта на каждом GET (F-8) — пересматривает решение v5.13 «считается, не хранится».
|
||||
Цена: ещё одна таблица и миграция; взамен ядро при чтении ничего не считает.
|
||||
12. **Эффективная дата документа** = дата отчёта, иначе самая поздняя абсолютная дата колонок (F-6) — та же формула,
|
||||
что `statement_period` ядра; от неё считаются относительные подписи и выбирается редакция перечня кодов.
|
||||
13. **Предел и квантование суммы** в общем `parse_amount` — 16 целых цифр, 2 знака, half-up, как `quantize_money`
|
||||
парсера (F-20); «догадка из цифр» длиннее 16 — `null`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user