Compare commits
4 Commits
ad1cd307a1
...
6047677ed0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6047677ed0 | ||
|
|
429f86760f | ||
|
|
0e680c89e0 | ||
|
|
51d0f935ff |
@ -547,8 +547,8 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550` (жёсткое), `1600 = 1700` (жёсткое); ОФР — `2400 = 2110 − 2120 − 2330 +
|
||||
2340 − 2350 − 2410` (**мягкое**, как `2400` полного набора: знак 2410 не определён, OQ-12; `abs_terms` те же; решение
|
||||
владельца 18.10.2026, Р-22 / S-5). Оба набора — в одном словаре `statement_checks.yaml`. Без упрощённого набора полный
|
||||
дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (результат 1.x, документ до v5.19) —
|
||||
полный набор.
|
||||
дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (документ, оставшийся в БД с времён до
|
||||
v5.19, если база не очищена) — полный набор; у результата 1.x вариант определяется по структуре кодов при приёме.
|
||||
- **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
|
||||
из первых четырёх цифр (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code`.
|
||||
- **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже).
|
||||
@ -556,10 +556,13 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
**Ядро.** При загрузке результата v2.0 ядро §7 **не пересчитывает**: `checks`, `needs_review` и статус документа
|
||||
берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 копией
|
||||
`arm_statements` по текущим суммам (`financial_statement_value.amount` после правок) — с теми же правилами, что в
|
||||
сервисах. Результат 1.x (окно выкладки) и формы, существовавшие до миграции v5.19, `checks` не несут: ядро **один раз**
|
||||
считает §7 копией `arm_statements` (`form_variant = null` — полный набор) — при приёме 1.x и в миграции — и пишет
|
||||
нарушения в `document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из
|
||||
этого расчёта. Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
|
||||
сервисах. Пересчёт идёт точкой входа пакета для пересчёта по сохранённым данным (спека `statement-normalization`):
|
||||
она же заново определяет вариант формы и редакцию документа для проверок ядра. Результат 1.x (окно выкладки) `checks`
|
||||
не несёт: ядро **один раз при приёме** считает §7 той же точкой входа (вариант — по структуре кодов) и пишет нарушения в
|
||||
`document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из этого
|
||||
расчёта. Формы, существовавшие до миграции v5.19, не пересчитываются: перед выкладкой база очищается от форм ББ/ОФР
|
||||
(Р-24, §9.5 п. 2), а у формы, оставшейся в неочищенной базе, записей `document_issue` нет — до первой правки её
|
||||
нарушения не показываются и подтверждение их не проверяет (корректность не гарантируется). Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
|
||||
`document_issue` — у любой формы, какой бы версией контракта она ни пришла. Межформенная `1370(отчётная дата) − 1370(начало
|
||||
года) = 2400` — **мягкая**, считается ядром по агрегату формы по коду (§9.5 п. 2) и не блокирует подтверждение: пара —
|
||||
другой документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый
|
||||
@ -701,7 +704,9 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
|
||||
результатов 1.x на окне выкладки). `RECOGNIZED` — только если одновременно: `document.status = done` из результата и он согласован с
|
||||
содержимым документа (нет строк `unreadable` / `missing` с суммой или текстом в значениях, нет строк, значений и полей
|
||||
шапки с `needs_review`, нет `checks.relations_failed` и предупреждений документа уровня `error`; иначе ядро считает
|
||||
документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса); в `metrics.warnings`
|
||||
документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса; проверка — вызов
|
||||
общей функции статуса §11.9 по входу, собранному из результата, а не пересчёт §7: значения строк `detail` в неё не
|
||||
входят); в `metrics.warnings`
|
||||
файла нет предупреждений уровня `error` (потерянный json, таблица ИС или исключение разбора таблицы — решение № 28 среза
|
||||
3a; `needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены
|
||||
`period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence
|
||||
@ -712,6 +717,12 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
|
||||
`RECOGNIZED` после правки — только вручную, как и прежде. Документ, принятый по 1.x, после правки пересчитывается так же
|
||||
(общим кодом), его `LEGACY_WARNING` и `MAPPING_WARNING` — `info` и статус не держат.
|
||||
|
||||
**Документ ОСВ при приёме контракта v2.0.** `RECOGNIZED` — только если `osvDocument.status = done` (контрольные суммы
|
||||
§1.4 листа сошлись), `checks` листа без нарушений, в `metrics.warnings` файла нет предупреждений уровня `error`,
|
||||
`counterparty_check ≠ MISMATCH` и нет предупреждений маппинга ядра по этому листу; статус конверта не учитывается (как
|
||||
Р-19 у форм: ОСВ в книге с сомнительной ОФР автоподтверждается). Предупреждения листа (`osvDocument.warnings`, `OSV_*`,
|
||||
`info`) хранятся в `document_issue` документа ОСВ и отдаются в `DocumentOut.issues`.
|
||||
|
||||
### 9.4 Задание (`recognition_status`): queued → published → processing → done | needs_check | error | timeout → dlq;
|
||||
replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 20 мин (решение владельца 09.10.2026; note DBML «5 мин» — OQ-159), парсер 5 мин.
|
||||
|
||||
@ -751,9 +762,10 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
§9.3.
|
||||
**Формы по контракту v2.0** (change `unified-statement-normalization`; правила выше — для результатов 1.x на окне
|
||||
выкладки, у них поля v2.0 заполняются по умолчанию: `line_status = ok`, `row_index` по порядку строк результата с 0,
|
||||
`line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений — по
|
||||
прежнему правилу «правлено или уверенность 100 и `is_recognized`», `form_variant = null` (§7 — полный набор),
|
||||
`header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз копией `arm_statements` и пишет в
|
||||
`line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений = **НЕ**
|
||||
«правлено или уверенность 100 и `is_recognized`» (чистое значение — `false`), `form_variant` — по структуре кодов,
|
||||
`file_ordinal` — индекс в `documents[]`, `header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз
|
||||
точкой входа `arm_statements` и пишет в
|
||||
`document_issue` (`source = core`, §7 «Ядро»); предупреждения маппинга 1.x по документу (повтор кода, значение вне
|
||||
колонок) — `document_issue` `MAPPING_WARNING` `info` (`source = core`; статус при приёме — по D-6, после правки они
|
||||
статус не держат); строки `metrics.warnings` 1.x — в `recognition_job.metrics.warnings` структурами `{code:
|
||||
@ -773,7 +785,11 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
RELATION_FAILED`, `error` или `kind = soft`, `code = RELATION_SOFT`, `info`, с `codes`, `relation`, `expected`,
|
||||
`actual`, `column_id`; предупреждение о столбце вне колонок формы — с `column_index`) вместе с проверками ядра уровня
|
||||
`info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`); версия общего кода —
|
||||
`recognition_job.statements_version`. Каждый `BALANCE` / `INCOME` становится отдельным документом с `file_ordinal`:
|
||||
`recognition_job.statements_version`. **Каждый** документ результата (ОСВ, формы, `DISCLOSURE`, `OTHER`) при любом
|
||||
MAJOR получает `file_ordinal` — индекс в `documents[]` (несозданный документ номер не занимает и не сдвигает
|
||||
остальные). Предупреждения файла хранятся в `recognition_job.metrics.warnings` структурами (у 1.x — `LEGACY_WARNING`)
|
||||
и отдаются в `JobBrief.warnings` объектами (строки старых заданий конвертируются при чтении). Каждый `BALANCE` /
|
||||
`INCOME` становится отдельным документом:
|
||||
вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не
|
||||
автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
Все чтения формы «по коду» (межформенная проверка, суммы формы для консолидации и выгрузок, `parent_code`, раскладка
|
||||
@ -781,6 +797,13 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`amount` строк `ok` с кодом по колонке, список `line_id`, справочное имя (DECISION-5, не хранится). Строки `detail`,
|
||||
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8. Записи `document_issue` удаляются вместе
|
||||
с документом при перезагрузке файла.
|
||||
**Перед выкладкой ядра v5.19** база очищается от форм ББ/ОФР (Р-24; скрипт ядра `scripts/purge_statements.py`,
|
||||
`--dry-run` с отчётом, затем `--apply`; владелец или платформенная команда по его заявке, после резервной копии):
|
||||
документы `BALANCE` / `INCOME` с `financial_statement_*`, комментариями и привязками к версиям «в работе»; файлы без
|
||||
ОСВ — целиком с заданиями (объектам S3 — `expires_at`); смешанные «ОСВ + формы» — только формы, или целиком по
|
||||
решению владельца; формы в верифицированных версиях — отказ без изменений; ОСВ, консолидации и справочники не
|
||||
трогаются; системный аудит `FILE_DELETE`. Миграция v5.19 только добавляет колонки и значения по умолчанию (спека
|
||||
`arm-core`, «Миграция DBML v5.19», «Очистка форм перед выкладкой»).
|
||||
3. **Автозамена.** После успешного маппинга другие файлы того же ЮЛ (не `SUPERSEDED`), содержащие документ с теми же
|
||||
`document_type`, `osv_account_code`, `period_year`, `period_quarter`, переводятся в `SUPERSEDED`
|
||||
(`superseded_by_file_id` = новый файл); `version_no` нового = максимум заменённых + 1. Новый файл без контекста (`consolidation_id IS NULL`) наследует `consolidation_id` первого (по `id`) заменённого файла, у которого он задан; файл, загруженный в консолидацию, сохраняет свою (v5.18). Заменяется и файл с `CONFIRMED`
|
||||
@ -861,7 +884,9 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения (`is_confirmed`,
|
||||
`confirmed_by/at`, `needs_review = false`, `is_modified` не ставится) или снять подтверждение (`is_confirmed = false`,
|
||||
`needs_review` пересчитывается); `columns[] {column_id, period_date}` — дата колонке без подписи или с неуверенной
|
||||
подписью (обязательная дата, `null` — 422 ошибка валидации; пересчитывает `is_current` и период документа);
|
||||
подписью (обязательная дата, `null` — 422 ошибка валидации; пересчитывает `is_current` и период документа; ставит
|
||||
`period_kind = absolute`; та же дата у колонки с `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` — подтверждение,
|
||||
предупреждение снимается);
|
||||
`lines[] {line_id, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
|
||||
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить неуверенный код или снять подтверждение.
|
||||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||||
@ -869,8 +894,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` в PATCH принимает только
|
||||
`"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный или
|
||||
подтверждённый код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`;
|
||||
`line_code_original_value` не меняется (код из источника; у строк без кода — `null`); у `detail` родитель
|
||||
пересчитывается как ближайшая строка `ok` выше по `row_index`. Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
|
||||
`line_code_original_value` не меняется (код из источника; у строк без кода — `null`); после правки родитель **всех**
|
||||
строк `detail` пересчитывается как ближайшая строка `ok` выше по `row_index` (нет такой — `null`). Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
|
||||
совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL`
|
||||
(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она
|
||||
удаляется (её `id` убирается из `document_issue.line_ids`), назначенная строка источника занимает её место. Строки
|
||||
@ -883,8 +908,9 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue`
|
||||
видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица
|
||||
§11.9). **Акцепт** сбрасывается правкой данных — суммы, кода или статуса строки, даты колонки, поля шапки;
|
||||
подтверждение ячейки или кода (`confirmed`) суммы не меняет и акцепт не сбрасывает. Расхождение `statements_version`
|
||||
§11.9; значения строк `detail` — без `needs_review`, Р-23). **Акцепт** сбрасывается правкой данных — суммы, кода или
|
||||
статуса строки, даты колонки, поля шапки; подтверждение ячейки или кода (`confirmed`) и подтверждение даты отчёта или
|
||||
колонки тем же значением суммы не меняют и акцепт не сбрасывают (решение владельца 18.10.2026, Р-25). Расхождение `statements_version`
|
||||
задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH —
|
||||
один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям).
|
||||
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
|
||||
@ -1205,9 +1231,14 @@ file:<имя json>;table:<i>`; документа у такой таблицы
|
||||
ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из `segment_breaks`. У
|
||||
сегмента своя раскладка: по его шапке; без шапки — столбец кодов по п. 1, а если сегмент не первый и кодов меньше трёх —
|
||||
ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (лист «_2» НПЗ
|
||||
с единственной строкой 2400, у которого код в другой колонке). Колонки значений сегмента без шапки — столбцы правее
|
||||
кода, где в строках с кодами числа, сопоставление с первым сегментом по порядку; число не совпало —
|
||||
`SEGMENT_COLUMNS_MISMATCH` (`error`), значения по позиции. Столбец кодов не найден ни так, ни так —
|
||||
с единственной строкой 2400, у которого код в другой колонке). Колонки документа задаёт первый сегмент; колонки
|
||||
следующих сегментов новых колонок не создают, а сопоставляются с колонками первого: у сегмента **со своей шапкой**
|
||||
(повторная шапка OCR, лист-продолжение Excel со строкой заголовка — «Бухгалтерский баланс. Лист 4» НПЗ, колонки 8–10
|
||||
при 16 / 21 / 24 у первого листа) — по `period_date`, если у каждой колонки сегмента дата есть и совпадает ровно с
|
||||
одной колонкой первого, иначе по порядку; у сегмента без шапки колонки значений — столбцы правее кода, где в строках с
|
||||
кодами числа, по порядку. Значения несут `column_index` колонки первого сегмента. При сопоставлении по порядку другое
|
||||
число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не
|
||||
попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так —
|
||||
`SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4).
|
||||
3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны
|
||||
`dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025
|
||||
@ -1229,7 +1260,10 @@ file:<имя json>;table:<i>`; документа у такой таблицы
|
||||
что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_date = null`, вид
|
||||
остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в
|
||||
дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря
|
||||
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка).
|
||||
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка). Для этого подтверждения
|
||||
эффективная дата строится **только из уверенных источников** — дата отчёта с уверенностью 100, иначе самая поздняя
|
||||
абсолютная дата колонок с подписью уверенности 100; нет таких — подпись не подтверждена (неуверенное не подтверждает
|
||||
само себя, S-64).
|
||||
4. **Строка заголовка** — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя
|
||||
над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный
|
||||
заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки
|
||||
@ -1265,8 +1299,11 @@ file:<имя json>;table:<i>`; документа у такой таблицы
|
||||
|
||||
Строка данных — строка сетки с ролью `data`, у которой есть **содержимое**: в ячейке кода есть хотя бы одна цифра
|
||||
(разобранный код или мусор вроде «44 30\nI |» aep_6), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`),
|
||||
или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3). Строка только с текстом
|
||||
наименования — не строка данных (заголовок раздела). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
|
||||
или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3), или её текст — «цифра под видом
|
||||
буквы» (после замены двойников без условия соседства есть цифра и нет других букв: `'I'` с уверенностью 100 в строке 38
|
||||
aep_6). Строка только с текстом наименования — не строка данных (заголовок раздела); текст из настоящих букв в колонке
|
||||
значений при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев») — тоже нет, осознанное исключение
|
||||
(S-72). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
|
||||
— строка данных: документ `needs_check` (не все строки `ok` / `detail`), но подтверждение она не блокирует
|
||||
(`LINES_UNRESOLVED` — только строки со значениями, §9.5 п. 4). У строки: `row_index` (номер в документе с
|
||||
0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
@ -1274,13 +1311,14 @@ file:<имя json>;table:<i>`; документа у такой таблицы
|
||||
ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:**
|
||||
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка
|
||||
без единой цифры («}», «:») считается пустой;
|
||||
- `detail` (Р-8) — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах — родитель
|
||||
продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар
|
||||
(41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит; в проверки §7, агрегаты и `done` она
|
||||
не входит; экран по умолчанию не показывает;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом («Выручка» aep_3, где 2110
|
||||
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «[190 :»); ячейка без
|
||||
единой цифры («}», «:», «изо :» — двойники вне соседства с цифрой цифрами не считаются, S-61) считается пустой;
|
||||
- `detail` (Р-8) — строка без кода с содержимым, над которой есть строка `ok` (в том же или в предыдущих сегментах —
|
||||
родитель продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке»,
|
||||
«Товар (41.01)»); родитель — ближайшая строка `ok` выше (`unreadable` / `missing` между ними пропускаются). Сервисы её
|
||||
отдают, ядро хранит; в проверки §7, агрегаты, `needs_review` и `done` она не входит (у её значений `needs_review =
|
||||
false`, Р-23); экран по умолчанию не показывает; после правок ядро пересчитывает родителей всех строк `detail`;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok` («Выручка» aep_3, где 2110
|
||||
уехал в ячейку значения); строки роли `title` сюда не попадают.
|
||||
|
||||
Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. **Счётчиков
|
||||
@ -1314,10 +1352,16 @@ k-е части; уверенность — исходной ячейки. Зн
|
||||
1. **`value` есть (Excel):** число → `amount` с квантованием до 2 знаков half-up, не больше 16 целых цифр (иначе `amount
|
||||
= null`, `string_value` = текст, `is_recognized = false`, `VALUE_OUT_OF_RANGE` `info`); дата или логическое → `amount
|
||||
= null`, `string_value` = текст, `is_recognized = false`.
|
||||
2. **`value` нет — `parse_amount(text, source)`**, один для обоих: двойники (§11.3) и крайние `|`; пробелы, NBSP и тонкие
|
||||
пробелы; минус «−» (U+2212) как знак; скобки → минус, `}` и `]` принимаются как закрывающая скобка (OQ-147:
|
||||
«(7744536}» → −7744536, `is_recognized = false`); прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
|
||||
`is_recognized = true` (OQ-148). Разделители:
|
||||
2. **`value` нет — `parse_amount(text, source)`**, один для обоих, по стадиям (S-57): (а) прочерк (`-`, `–`, `—`, `−`
|
||||
без цифр) и пусто → `amount = null`, `is_recognized = true` (OQ-148); (б) крайние `|` удаляются (до замены
|
||||
двойников), затем двойники вплотную к цифре (§11.3); (в) **ядро числа** — от первого из «цифра, `(`, `-`, `−`, `+`»
|
||||
до последнего из «цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор (`:`, `=`, `■`, `•`, кавычки, `{`, `[` …),
|
||||
выбрасывается с `is_recognized = false` («: : 2856983 = ■» → 2856983, «| : 122477» → 122477); (г) в ядре допустимы
|
||||
только цифры, пробелы (NBSP, тонкие), `,`, `.`, знак в начале и одна пара скобок вокруг всего числа (`(` первым, `)`,
|
||||
`}` или `]` последним — `}` / `]` исправление, OQ-147: «(7744536}» → −7744536, `is_recognized = false`; `{` и `[`
|
||||
открывающей скобкой не считаются); иначе — буква в ядре, скобка без пары или не на краю, знак не в начале, другой
|
||||
символ между цифрами — `amount = null`, `string_value` = текст, `is_recognized = false` («211(1 |», «(Ю4)», «{38378)»);
|
||||
(д) цифр нет — `amount = null`, `string_value` = текст. Разделители:
|
||||
- `ocr`: запятая — тысячи, если весь текст (после `strip` и удаления пробелов, с учётом знака и пары скобок на всю
|
||||
строку) — `[+-]?[1-9]\d{0,2}(,\d{3})+` (AURORA-1405, sg#35: `4,200` → 4200, `1,234,567` → 1234567); точка — тысячи
|
||||
при `[+-]?[1-9]\d*(\.\d{3})+` (sg#37: `837.994` → 837994, `1.234.567` → 1234567, `1 234.567` → 1234567); удаление
|
||||
@ -1363,12 +1407,14 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
|
||||
`type_hint = DISCLOSURE`: подпись «Пояснения» или имя листа «Расшифровка» над таблицей с кодами 1110–1700 не должны
|
||||
превратить баланс в пустое пояснение — большинство кодов в отрезке ББ или ОФР даёт тип по кодам и `TYPE_CONFLICT`
|
||||
(`error`). Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с
|
||||
`CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым — предупреждение **файла**
|
||||
`TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`), у Excel — лист «прочее», предупреждение **файла**
|
||||
`SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа в обоих случаях нет. `DISCLOSURE` (по `type_hint` без
|
||||
`CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым (хотя бы одна ячейка с цифрой
|
||||
или «цифрой под видом буквы») — предупреждение **файла** `TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`),
|
||||
таблица только из текста — `TABLE_SKIPPED` (`info`, файл, S-71); у Excel — лист «прочее», предупреждение **файла**
|
||||
`SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех случаях нет. `DISCLOSURE` (по `type_hint` без
|
||||
кодов форм или по кодам прочих форм) — шаги 3–7 не выполняются, `columns` и `lines` — пустые массивы (ядро строки
|
||||
пояснений не хранит), `checks` нет, документ `done`.
|
||||
2. **Вариант формы** `form_variant` — только по структуре кодов (ОКУД 0710096 — код КНД комплекта, на бланках
|
||||
2. **Вариант формы** `form_variant` — только по структуре кодов всех строк `ok` (ядро определяет его заново при каждом
|
||||
пересчёте после правки; ОКУД 0710096 — код КНД комплекта, на бланках
|
||||
0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или
|
||||
2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` / `OTHER` — `null`. Шаг 7 и перечень
|
||||
кодов берут набор варианта. Образец упрощённой формы — в fixtures при первой возможности (OQ-166).
|
||||
@ -1386,7 +1432,9 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
|
||||
### §11.9 Шаг 8 — `needs_review`, статус, предупреждения
|
||||
|
||||
**Подтверждение проверкой** (Р-5, Р-7, Р-11, Р-17, Р-21). Проверка §7 — арбитр, уверенность — подсказка там, где
|
||||
проверки нет.
|
||||
проверки нет. Правила ниже — для значений строк `ok`, `unreadable`, `missing`; у значений строк `detail` `needs_review =
|
||||
false` всегда (Р-23, решение владельца 18.10.2026): кода у них нет, покрыть их нельзя, в §7, агрегаты и статус они не
|
||||
входят, сомнение видно в подсказке ячейки (`text`, уверенность).
|
||||
|
||||
- Значение **нуждается в покрытии**, если `confidence < 100` (Р-5: у ИС уверенность дискретная — 100, ~80, 71, 67, 60,
|
||||
50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false`
|
||||
@ -1396,11 +1444,13 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
|
||||
допуска ±1, без «округления» (Р-21, решение владельца 18.10.2026). Мягким покрываются строки, у которых жёстких
|
||||
соотношений нет: 2400, 2410, 2411, 2412, 2460 (ОФР полного набора; `2400` упрощённого — тоже мягкое, S-5), 2510, 2520,
|
||||
2530, 2500 (`2500 = 2400 + 2510 + 2520 + 2530`), пятизначные подстроки («сумма подстрок = родитель»).
|
||||
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма записи знака (скобки, минус, без знака —
|
||||
по `text`) совпадает с формой у большинства членов `abs_terms` той же колонки с непустым `amount`, `is_recognized =
|
||||
true` и без правки аналитика; не совпадает или сравнивать не с чем — не подтверждён (то же правило — при покрытии
|
||||
мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и 2220 той же
|
||||
колонки в скобках; «{38378)» при остальных в скобках — подсвечивается.
|
||||
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма знака совпадает с формой строгого
|
||||
большинства **остальных** членов `abs_terms` той же колонки с непустым `amount`, `is_recognized = true` и без правки
|
||||
аналитика (сама ячейка не голосует); форма знака — по результату разбора `parse_amount`: «скобки» (пара, в том числе с
|
||||
`}` / `]`), «минус», «без знака»; голосующих нет или голоса поровну — не подтверждён (S-66; то же правило — при
|
||||
покрытии мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и
|
||||
2220 той же колонки в скобках; сомнительное «38378» без знака при остальных в скобках — подсвечивается; «{38378)»
|
||||
разбором даёт `null` (§11.5) и подсвечивается как непокрытое.
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой
|
||||
текст и уверенность остаются в подсказке ячейки. Мягкое соотношение, не сошедшееся точно, подсветки не даёт.
|
||||
@ -1417,15 +1467,19 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
|
||||
наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня —
|
||||
`contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`):
|
||||
объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215,
|
||||
1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; код без образца в перечень не входит (кандидаты — OQ-168),
|
||||
1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; код без образца в перечень не входит (кандидаты — OQ-168;
|
||||
временное правило утверждено владельцем 18.10.2026, Р-26: неуверенный такой код на скане — `needs_review` строки),
|
||||
упрощённые варианты — по бланку (OQ-166). **Законные повторы кода** (Р-20, решение владельца 18.10.2026) — часть перечня
|
||||
(`repeats`): у `fsbu4` — 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощённой
|
||||
системе налогообложения» (печатная форма 1С, AURORA-1431), у `66n` — раздел III некоммерческой организации после раздела
|
||||
III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность; для **каждой** редакции перечня
|
||||
(66н и ФСБУ 4 — всегда обе: Межениновская за 3 кв. 2025 сдана на бланке 66н) — наибольшая подпоследовательность,
|
||||
согласованная с порядком перечня варианта формы документа (повтор допустим столько раз, сколько код стоит в перечне);
|
||||
редакция — с более длинной (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); уверенные коды —
|
||||
подтверждённые якоря; при нескольких наибольших подпоследовательностях подтверждены только общие коды. Пятизначный код
|
||||
III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность (коды, которых нет в перечне
|
||||
редакции, в её последовательность не входят); для **каждой** редакции перечня (66н и ФСБУ 4 — всегда обе: Межениновская
|
||||
за 3 кв. 2025 сдана на бланке 66н) — **лучшая** подпоследовательность, согласованная с порядком перечня варианта формы
|
||||
документа (повтор допустим столько раз, сколько код стоит в перечне): сначала наибольшее число уверенных кодов (якоря;
|
||||
противоречащие по порядку якоря все войти не могут — входит наибольшее согласованное множество), затем наибольшая
|
||||
длина; редакция — с лучшей (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); неуверенный код
|
||||
подтверждён, только если входит во все лучшие подпоследовательности выбранной редакции (S-65). Вариант формы — по всем
|
||||
строкам `ok` (§11.7); ядро пересчитывает вариант и проверку после правки кода. Пятизначный код
|
||||
подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, которого **нет в перечне**
|
||||
(или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии (Р-16);
|
||||
повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `needs_review` строки,
|
||||
@ -1434,15 +1488,20 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
|
||||
наименования со справочным (§9.3).
|
||||
|
||||
**Совпадение наименований** (ядро; S-4 с оговорками ревью, решение владельца 18.10.2026): обе строки — нижний регистр,
|
||||
`ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются; совпало, если строки равны, или более короткая (≥ 5
|
||||
символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым** справочным именем кода в любой
|
||||
редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы — Р-20). Исключение: если хотя бы
|
||||
одно из сравниваемых имён начинается с «итого по разделу», совпадение — **только равенство** («итого по разделу i»
|
||||
входит в «итого по разделу ii» и похоже на него — без исключения подмена 1100 ↔ 1200 ↔ 1300 не ловилась бы именем).
|
||||
`ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются, крайние обрезаются (`strip`); совпало, если строки
|
||||
равны, или более короткая (≥ 5 символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым**
|
||||
справочным именем кода в любой редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы —
|
||||
Р-20). Исключение: если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера
|
||||
раздела** — начальные буквы `i` / `v` / `x` первого слова после фразы (кириллические «і», «х» приводятся к латинским,
|
||||
хвост слова отбрасывается: «Итого по разделу V1» Счастливочной и «Итого по разделу V /» РБ → V); совпало ⇔ номера равны;
|
||||
номер не выделен — только равенство строк (S-60). Без исключения «итого по разделу i» входило бы в «итого по разделу ii»
|
||||
— подмена 1100 ↔ 1200 ↔ 1300 не ловилась бы именем. В словарь имён ядра `fsbu4` в PR перехода добавляются вторые имена
|
||||
законных повторов 1220, 1520, 2410 и коды 1105 «Гудвил», 1130, 1140, 1530, 2460 (спека `arm-core`).
|
||||
Кода нет в словаре или наименование пусто — не совпало; несовпадение — `NAME_MISMATCH` (`info`) у строки. По
|
||||
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя.
|
||||
|
||||
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок;
|
||||
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок
|
||||
с подписью уверенности 100 (неуверенная подпись дату отчёта не подтверждает, S-64);
|
||||
иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал
|
||||
бы неверный период и автоподтверждение. **У бланка 66н только с относительными подписями** («На отчётную дату», …)
|
||||
абсолютных дат колонок нет, и неуверенная дата отчёта структурой не подтверждается никогда — такой документ всегда
|
||||
@ -1463,14 +1522,15 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
|
||||
|---|---|---|---|
|
||||
| `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка |
|
||||
| `TABLE_PARSE_ERROR` | error, файл | исключение при разборе таблицы ИС (`locator` таблицы; документа нет) | перезагрузка |
|
||||
| `TABLE_LOST` | error, файл | таблица ИС с содержимым без столбца кодов и без типа | перезагрузка |
|
||||
| `TABLE_LOST` | error, файл | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без столбца кодов и без типа | перезагрузка |
|
||||
| `TABLE_SKIPPED` | info, файл | таблица ИС только из текста без столбца кодов и без типа (документа нет) | — |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | форма `BALANCE` / `INCOME` без столбца кодов | перезагрузка |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в т. ч. лист-продолжение) | перезагрузка |
|
||||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | перезагрузка |
|
||||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | перезагрузка |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым | перезагрузка |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | при сопоставлении по порядку число колонок сегмента не совпало с первым | перезагрузка |
|
||||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи | PATCH `columns[].period_date` |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты | PATCH `columns[].period_date` |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты | PATCH `columns[].period_date` (в том числе той же датой — подтверждение) |
|
||||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента | перезагрузка |
|
||||
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` | перезагрузка |
|
||||
| `NO_ORGANIZATION_NO_INN` | error | нет ни организации, ни ИНН | PATCH `header.inn` / `organization_name` |
|
||||
@ -1500,8 +1560,9 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
|
||||
|
||||
**Статус документа** — одна функция общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧ период определён
|
||||
(дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей
|
||||
шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений документа уровня
|
||||
`error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках. Квартал периода,
|
||||
шапки с `needs_review` (значения строк `detail` не учитываются, Р-23) ∧ нет жёстких нарушений §7 вне допуска и
|
||||
округления ∧ нет предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о
|
||||
раскладке, строках и проверках. Квартал периода,
|
||||
сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ `document.status = done`, когда контрольные
|
||||
суммы §1.4 листа сошлись (§1.6, S-22), её предупреждения — `info`.
|
||||
|
||||
@ -1509,19 +1570,23 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
|
||||
`document_type`; `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть хотя бы
|
||||
одна строка из источника, `row_index` не `null`); `has_organization_or_inn`; `period_known` (дата отчёта или абсолютная
|
||||
дата хотя бы одной колонки — у ядра `document.reporting_date` не `null`); строки `{line_status, needs_review}`; значения
|
||||
`{needs_review}`; число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не
|
||||
`{line_status строки, needs_review}` (значения строк `detail` функция не учитывает); число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не
|
||||
учитываются); уровни предупреждений документа (у ядра — записи `document_issue` вида `warning` с `severity = error`).
|
||||
`needs_review` строк и значений на входе уже учитывает подтверждения: у ядра правленое (`is_modified`,
|
||||
`line_code_modified`) или подтверждённое (`is_confirmed`) — `false`. Сервис собирает вход из `FormCandidate` и
|
||||
результата шагов 2–7, ядро — из строк БД; так один код даёт один статус.
|
||||
результата шагов 2–7, ядро — из строк БД (после правки — точкой входа пакета для пересчёта по сохранённым данным,
|
||||
спека `statement-normalization`; при приёме результата — для проверки согласованности `done`, §9.3); так один код даёт
|
||||
один статус.
|
||||
|
||||
**Ожидание на реальных сканах** (`fixtures/ocr`, оценка ревью спеки 18.10.2026, K-4): после Р-20 (повторы 1220 / 1520 /
|
||||
2410 в перечне `fsbu4` и синонимы в словаре имён) и Р-21 (мягкие соотношения ОФР) законные повторы и строки 24xx / 25xx
|
||||
перестают держать документ. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с суммами
|
||||
(aep_3), значения с уверенностью ниже 100 вне соотношений и в несошедшихся соотношениях, разлепленные строки без
|
||||
покрытия, неуверенная дата отчёта. Доля `done` на текущих шести документах ожидается небольшой; golden-эталоны
|
||||
`fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля — в его
|
||||
описании.
|
||||
**Ожидание на реальных сканах** (`fixtures/ocr`, оценки ревью спеки 18.10.2026, K-4 и L-1): после Р-20 (повторы 1220 /
|
||||
1520 / 2410 в перечне `fsbu4` и синонимы в словаре имён), Р-21 (мягкие соотношения ОФР) и Р-23 (значения `detail` не
|
||||
держат статус) законные повторы, строки 24xx / 25xx и ячейки расшифровок документ не держат. По прогону ревьюера ББ и
|
||||
ОФР РБ («Бухгалтерская отчетность 1 кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: все их неуверенные
|
||||
значения строк с кодом покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали
|
||||
их только ячейки расшифровок. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с
|
||||
суммами (aep_3), несошедшиеся соотношения и мусор в значениях (aep_6: «(Ю4)» в 2350), неуверенная дата отчёта.
|
||||
Golden-эталоны `fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля
|
||||
`done` — в его описании.
|
||||
|
||||
**Статус конверта:** `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`;
|
||||
документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser — коды описывают
|
||||
|
||||
@ -173,4 +173,4 @@
|
||||
| OQ-165 | Склейка двух половинок одной формы из **одного** файла (Р-15): Excel с листом-продолжением другого типа, двойная печать формы, две таблицы ИС одного типа и периода. Сейчас вторая форма того же типа и периода хранится отдельным документом (Р-10), ни одна не автоподтверждается (Р-18), подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM`; склейки нет — файл перезагружают. Нужна ли склейка (кнопка «объединить формы») и по каким правилам | change `unified-statement-normalization`, Р-15, Р-18 | arm-core (`document.file_ordinal`, подтверждение), arm-web, §9.5 п. 2 | открыт: от ИС Сканирование такой случай не ожидается (владелец 17.10.2026), основной источник — Excel; допущение — склейки нет, файл перезагружают |
|
||||
| OQ-166 | Упрощённая форма ББ/ОФР (приказ 66н, прил. 5): признак — по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или 2110, нет 2100, 2200, 2300), ОКУД 0710096 признаком не служит. Набор соотношений §7 и перечень кодов упрощённой формы заданы по бланку (`05-domain-rules.md` §7, решение S-5 спеки; ББ — жёсткие, ОФР `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` — мягкое, как `2400` полного набора, решение владельца 18.10.2026, Р-22; перечни — `contracts/statement-code-orders.yaml`, у `fsbu4` — как у 66н) без образца в `fixtures/` | change `unified-statement-normalization`, design (Decisions № 14) | `arm_statements` `statement_checks.yaml`, перечень кодов, `form_variant` | открыт: допущение — формулы §7 «Упрощённый набор»; нужен реальный образец упрощённой отчётности (Excel и скан) в `fixtures/` для проверки признака и формул |
|
||||
| OQ-167 | Несколько отчётных периодов в одном файле, отданном в ИС Сканирование (например, ББ за два квартала в одном PDF): шапка в json ИС одна на json и копируется во все его таблицы | change `unified-statement-normalization`, design шаг 1 (допущение владельца 17.10.2026) | scan-gateway (адаптер шага 1), период документа | допущение владельца: ИС отдаёт разные периоды разными json; образца нет — проверить на первом таком файле, при расхождении — шапка из json неверна для части таблиц |
|
||||
| OQ-168 | Коды упорядоченного перечня форм без образца в `fixtures/` (`contracts/statement-code-orders.yaml`, §11.9): в перечень вошли только коды шаблонов банка и коды печатных форм 1С и сканов, найденные в `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930, повторы 1220 / 1520 / 2410 у `fsbu4`, раздел III НКО у `66n`). Не подтверждены образцом: 1120 в `fsbu4`; 1105, 1215, 2420 в `66n`; 2421, 2430, 2450 (прежние строки ОФР 66н); 2930 и раздел III НКО в `fsbu4`; клиентский 1231 в `fsbu4`; упрощённые перечни обеих редакций (OQ-166). Добавлять ли их без образца | change `unified-statement-normalization`, ревью спеки K-5 (18.10.2026) | `arm_statements` (перечень кодов, структурное подтверждение неуверенного кода), ядро (`CODE_NOT_IN_EDITION`) | открыт: допущение — код без образца в перечень не входит; на скане такой неуверенный код не подтверждается структурой (`needs_review`, `needs_check`), у Excel и уверенного кода — только `info` ядра `CODE_NOT_IN_EDITION`. Пополнение — строкой в yaml с источником (файл fixtures) |
|
||||
| OQ-168 | Коды упорядоченного перечня форм без образца в `fixtures/` (`contracts/statement-code-orders.yaml`, §11.9): в перечень вошли только коды шаблонов банка и коды печатных форм 1С и сканов, найденные в `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930, повторы 1220 / 1520 / 2410 у `fsbu4`, раздел III НКО у `66n`). Не подтверждены образцом: 1120 в `fsbu4`; 1105, 1215, 2420 в `66n`; 2421, 2430, 2450 (прежние строки ОФР 66н); 2930 и раздел III НКО в `fsbu4`; клиентский 1231 в `fsbu4`; упрощённые перечни обеих редакций (OQ-166). Добавлять ли их без образца | change `unified-statement-normalization`, ревью спеки K-5 (18.10.2026) | `arm_statements` (перечень кодов, структурное подтверждение неуверенного кода), ядро (`CODE_NOT_IN_EDITION`) | открыт до появления образцов; **временное правило принято владельцем 18.10.2026 (Р-26):** коды без образца (2421 / 2430 / 2450 в `66n`, 1120 и другие в `fsbu4`) в перечень не входят; неуверенный такой код на скане не подтверждается структурой — `needs_review` строки, `needs_check`; у Excel и уверенного кода — только `info` ядра `CODE_NOT_IN_EDITION`. Пополнение — строкой в yaml с источником (файл fixtures), когда образец появится |
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"$id": "arm-ka/recognition-result/v2",
|
||||
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0",
|
||||
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, header.okei (+ _text, _confidence), смысл is_recognized у OCR отвязан от confidence. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, header.okei (+ _text, _confidence), смысл is_recognized у OCR отвязан от confidence, statementDocument.form_variant обязателен, OTHER удалён из statementDocument.document_type. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
|
||||
"properties": {
|
||||
@ -57,7 +57,7 @@
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] } }, "required": ["document_type"] } } },
|
||||
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"] } }, "required": ["document_type"] } } },
|
||||
"required": ["documents"]
|
||||
},
|
||||
"then": {
|
||||
@ -223,12 +223,12 @@
|
||||
},
|
||||
"statementDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"],
|
||||
"required": ["document_type", "form_variant", "locator", "header", "columns", "lines", "status", "warnings"],
|
||||
"properties": {
|
||||
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
|
||||
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"], "description": "OTHER в v2.0 не порождается: лист «прочее» — предупреждение файла SHEET_OTHER, документа нет (значение OTHER — только в схеме 1.x)" },
|
||||
"locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] },
|
||||
"status": { "$ref": "#/$defs/documentStatus" },
|
||||
"form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов (§11); null — DISCLOSURE/OTHER" },
|
||||
"form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов строк ok (§11); null — DISCLOSURE; обязателен" },
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
|
||||
"header": {
|
||||
"type": "object",
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
version: 1
|
||||
|
||||
# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в
|
||||
# перечне больше одного раза.
|
||||
# перечне больше одного раза: в варианте full код стоит ровно 1 + (число его вхождений в этот список) раз, в simplified —
|
||||
# не чаще (сейчас повторов в simplified нет; validate_contracts.py проверяет оба правила).
|
||||
repeats:
|
||||
66n:
|
||||
# раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих
|
||||
|
||||
@ -79,13 +79,20 @@ sync-statements` и цель `statements-check` сервисов появляю
|
||||
`--source <dir>`, ссылка — `origin/main` или `--ref <ref>`; хост — `--host arm-core|tabular-parser|scan-gateway` или
|
||||
`--host-dir <dir>`. Без `--check` — раскладывает копию нужного коммита в новый worktree хоста на ветке фичи и пишет
|
||||
`VERSION`; issue и PR — как у `sync-web.sh`.
|
||||
- **`--check`, коды возврата:** `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`;
|
||||
`10` — копия равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала; предупреждение, не
|
||||
ошибка); `20` — копия отличается от дерева коммита из `VERSION` (прямая правка) или `VERSION` нет в истории источника,
|
||||
либо байтовая копия перечня кодов `arm_statements/rules/statement_code_orders.yaml` отличается от
|
||||
`docs/contracts/statement-code-orders.yaml`, либо (только `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
|
||||
отсутствует в перечне своей редакции; `2` — нет checkout `arm-statements` или хоста, неизвестный аргумент. Файлы
|
||||
расхождения перечисляются в выводе.
|
||||
- **`--check`, коды возврата** (файл `VERSION` в сравнении не участвует — в источнике его нет): `0` — копия равна дереву
|
||||
`arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов равна docs; `10` — копия равна дереву
|
||||
коммита из `VERSION`, но в `--ref` есть более новый (копия отстала), или байтовая копия перечня кодов
|
||||
`arm_statements/rules/statement_code_orders.yaml` отличается от `docs/contracts/statement-code-orders.yaml` (перечень в
|
||||
docs правился раньше `arm-statements` или пересинхронизации хоста) — предупреждение, не ошибка; `20` — копия отличается
|
||||
от дерева коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список
|
||||
`dependencies` его `pyproject`) отсутствуют в зависимостях хоста, либо (только `arm-core`) код словаря имён
|
||||
`api/rules/statement_lines.yaml` отсутствует в перечне своей редакции копии пакета; `2` — нет checkout
|
||||
`arm-statements` или хоста, неизвестный аргумент. Файлы расхождения перечисляются в выводе. Порядок правки перечня
|
||||
кодов: PR в docs → PR в `arm-statements` → `sync-statements` в хостах («сначала ядро»); до конца цепочки хосты получают
|
||||
`10`.
|
||||
- **Конфиг линтеров и тесты пакета:** пакет проходит самый строгий конфиг `ruff` / `mypy` среди хостов (в своём
|
||||
репозитории), потому что конфиг хоста применяется к копии, а править копию нельзя; тесты пакета — со своим
|
||||
`conftest.py`, `autouse`-фикстуры `conftest` хоста ограничены каталогом `tests/` хоста.
|
||||
- **`statements-check` в `make check-full`:** `../docs/scripts/sync-statements.sh --check --host-dir .` (docs — `../docs`
|
||||
или `ARM_KA_DOCS`); код `0` и `10` — цель зелёная (при `10` предупреждение печатается, в описание PR вписывается, что копия
|
||||
отстала), `20` и `2` — цель красная. В `make check` и в пайплайн платформы не входит: тест, читающий соседний
|
||||
@ -167,16 +174,20 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу
|
||||
| Цель | Что делает |
|
||||
|---|---|
|
||||
| `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` и `PyYAML` — зависимостями скриптов (после обновления репозитория — перезапустить) |
|
||||
| `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против текущих схем JSON Schema 2020-12, примеры `contracts/examples/v1/` — против схем прежнего MAJOR `*.v1.schema.json` (окно перехода на контракт 2.0), форма упорядоченных перечней кодов `contracts/statement-code-orders.yaml`; код 1 при любой ошибке |
|
||||
| `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против текущих схем JSON Schema 2020-12, примеры `contracts/examples/v1/` — против схем прежнего MAJOR `*.v1.schema.json` (окно перехода на контракт 2.0), форма упорядоченных перечней кодов `contracts/statement-code-orders.yaml` (коды — 4 цифры формы; в варианте `full` код стоит 1 + (вхождений в `repeats`) раз, в `simplified` — не чаще); код 1 при любой ошибке |
|
||||
| `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) |
|
||||
| `make check` | обе проверки выше |
|
||||
|
||||
Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`.
|
||||
Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их
|
||||
нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. На окне перехода между MAJOR (сейчас 1 → 2)
|
||||
тест идентичности сервиса сверяет свою копию со схемой **того MAJOR, который сервис пишет**: копия с `$id` `…/v1` — с
|
||||
`*.v1.schema.json`, иначе — с основным файлом (`openspec/specs/recognition-contract`, требование «Копия контракта в
|
||||
сервисе идентична источнику»). Ядро держит копии обоих MAJOR. Перечень кодов `contracts/statement-code-orders.yaml`
|
||||
нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. Правило теста идентичности (у ядра и
|
||||
сервисов одно, верно до, во время и после окна перехода между MAJOR — сейчас 1 → 2): схема копии с `$id` `…/v<M>`
|
||||
сверяется с `<имя>.v<M>.schema.json`, **если такой файл есть в docs**, иначе с основным (у `recognition-request` файла
|
||||
`.v1` нет — копия v1.0 сверяется с основным); пример `examples/<имя>.json` копии — с `examples/v<M>/<имя>.json`, если он
|
||||
есть, иначе с `examples/<имя>.json` (`M` — MAJOR копии схемы этого вида, вид — по префиксу имени); прочие файлы копии
|
||||
(`*.v<N>.schema.json`, `examples/v<N>/` у ядра) — с одноимёнными; файлы docs другого MAJOR сервис не сверяет. Правило
|
||||
вводится во все три репозитория до слияния контракта v2.0 в docs (шаг 0 выкладки; спека `recognition-contract`,
|
||||
требование «Копия контракта в сервисе идентична источнику»). Ядро держит копии обоих MAJOR. Перечень кодов `contracts/statement-code-orders.yaml`
|
||||
копируется не в сервисы, а в пакет `arm_statements` (сверяет `sync-statements --check`).
|
||||
|
||||
## Порядок перед PR
|
||||
|
||||
@ -416,8 +416,9 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
> **Пока не работает на `main`.** Раздел описывает API после change `unified-statement-normalization`
|
||||
> (RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19; дочерние PR arm-core и arm-web ещё не созданы).
|
||||
> Правила — `context/05-domain-rules.md` §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спека `arm-web` того же change.
|
||||
> Поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. После слияния PR раздел
|
||||
> переедет в §5.6 и §1.
|
||||
> В `…/statement` поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ.
|
||||
> **Исключение — карточка файла:** `FileOut.job.warnings` меняет тип со строк на объекты (ниже, «Карточка файла»), это
|
||||
> несовместимо — экран файлов обновляется в той же выкладке, что и ядро. После слияния PR раздел переедет в §5.6 и §1.
|
||||
|
||||
**`GET /documents/{id}/statement` — новые поля `StatementOut`:**
|
||||
|
||||
@ -438,7 +439,9 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
словарь «код → строка», ключ строки — `id` (у достроенных шаблонных — `line_code`, их у кода с реальной строкой `ok` нет).
|
||||
- `values[]` — `text` (сырой текст источника; в подсказку ячейки вместе с `confidence`), `needs_review`, `is_confirmed`.
|
||||
**Подсветка ячейки — только по `needs_review`**; по `confidence < 100` и `is_recognized = false` больше не подсвечивать
|
||||
(сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается).
|
||||
(сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается). У значений
|
||||
строк `detail` `needs_review` всегда `false` (расшифровки статус не держат) — подсказку с `text` и уверенностью
|
||||
показывать и у них.
|
||||
- `issues[]` — предупреждения и нарушения документа: `{id, kind: "warning" | "relation" | "soft", code, severity: "error" |
|
||||
"info", message, line_ids[], column_id?, column_index?, field?, source: "service" | "core", relation?, codes?, expected?,
|
||||
actual?}`; показывать у места (строки, ячейки, поле шапки) или у документа; `error` отличать от `info`. `column_index` без
|
||||
@ -456,7 +459,7 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
| Часть | Тело | Эффект | Ошибки |
|
||||
|---|---|---|---|
|
||||
| подтвердить ячейку / снять подтверждение | `values: [{line_id, column_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review = false` (при `false` — пересчитывается), сумма не меняется, акцепт не сбрасывается | 422 `UNKNOWN_CELL` |
|
||||
| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа; снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN` | 422 `UNKNOWN_COLUMN`; `period_date: null` — 422 массивом |
|
||||
| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа, `period_kind = absolute` (правка даты отчёта её больше не пересчитывает); снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN`; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) | 422 `UNKNOWN_COLUMN`; `period_date: null` — 422 массивом |
|
||||
| назначить / сменить код | `lines: [{line_id, line_code}]` | `unreadable` / `missing` / `detail` / `ok` → `ok` с этим кодом, `line_code_confidence = 100` | 422 `INVALID_LINE_CODE` (не 4–5 цифр), 409 `LINE_CODE_TAKEN` (код занят строкой, созданной правкой, с суммами — сначала очистить её) |
|
||||
| отметить расшифровкой | `lines: [{line_id, line_status: "detail"}]` | строка выходит из сумм и не блокирует подтверждение; обратно — только назначением кода | 422 `LINE_CODE_REQUIRED` (`line_status: "ok"` — в `ok` переводит только `line_code`) |
|
||||
| подтвердить код / снять подтверждение | `lines: [{line_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review` строки = `false` (при `false` — пересчитывается) | — |
|
||||
@ -476,9 +479,17 @@ line_code_text}` — строки `unreadable` / `missing`, у которых в
|
||||
(`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED`.
|
||||
|
||||
**Статус документа.** Форма из OCR может стать `RECOGNIZED` (и сразу автоподтвердиться), если сервис всё подтвердил
|
||||
проверками; на текущих реальных сканах это редкость — `NEEDS_CHECK` остаётся, когда есть что смотреть: подсвеченные ячейки
|
||||
и строки, `issues` уровня `error`, неразобранные строки. Две формы одного типа в одном файле приходят двумя документами;
|
||||
ни одна не автоподтверждается.
|
||||
проверками: сомнительные ячейки расшифровок (`detail`) статус не держат, поэтому чистые сканы 1С (например, ББ и ОФР за
|
||||
1 кв. 2025 в fixtures) могут прийти `RECOGNIZED`. `NEEDS_CHECK` остаётся, когда есть что смотреть: подсвеченные ячейки и
|
||||
строки, `issues` уровня `error`, неразобранные строки. Две формы одного типа и периода в одном файле приходят двумя
|
||||
документами; ни одна не автоподтверждается.
|
||||
|
||||
**Карточка файла и документа (несовместимое изменение).** `FileOut.job.warnings` — список объектов `{code, severity:
|
||||
"error" | "info", message, locator}` вместо строк (у заданий до выкладки ядро отдаёт строки, обёрнутые в `{code:
|
||||
"LEGACY_WARNING", severity: "info", message, locator: null}`); показывать `message` и `locator`, `error` отличать от
|
||||
`info`. `FileOut.job.mapping_warnings` — по-прежнему строки. `DocumentOut.issues[]` — те же записи, что `issues` формы,
|
||||
у любого документа, в том числе ОСВ (предупреждения листа `OSV_*`); `DocumentOut.warnings` (строки маппинга) остаётся на
|
||||
окно перехода.
|
||||
|
||||
**ГСК/ГСЗ.** `crm_id` в `POST …/related-groups` и `PATCH /directories/related-groups/{id}` сохраняется и возвращается в
|
||||
`RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409 `CRM_ID_EXISTS` (arm-core#112, DBML v5.19).
|
||||
@ -733,7 +744,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
|
||||
| Задача | Что поменяется для фронта | Как подготовиться |
|
||||
|---|---|---|
|
||||
| OQ-54 — SSO РАИСА | Меняется только получение токена (вместо dev-формы) | держать слой получения токена изолированным (`frontend-auth-guide.md` §9) |
|
||||
| arm-ka-docs#116 — единая нормализация ББ/ОФР (контракт v2.0, DBML v5.19) | `GET/PATCH …/statement`: строки со статусами и повторами кода, `needs_review`, `issues[]`, `lines`/`columns`/`confirmed` в PATCH; подтверждение — 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`; `crm_id` ГСК/ГСЗ сохраняется | §5.6.1; не держать словарь «код → строка», подсветку строить по `needs_review` |
|
||||
| arm-ka-docs#116 — единая нормализация ББ/ОФР (контракт v2.0, DBML v5.19) | `GET/PATCH …/statement`: строки со статусами и повторами кода, `needs_review`, `issues[]`, `lines`/`columns`/`confirmed` в PATCH; подтверждение — 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`; `crm_id` ГСК/ГСЗ сохраняется; **`FileOut.job.warnings` — объекты вместо строк**, `DocumentOut.issues[]` | §5.6.1; не держать словарь «код → строка», подсветку строить по `needs_review`; тип `JobBrief.warnings` в `types/api.ts` поменять вместе с выкладкой ядра |
|
||||
|
||||
Состояние этих задач — в Gitea (`RSHB/arm-core`, `RSHB/arm-ka-docs`); после слияния каждой этот раздел переезжает в §1.
|
||||
|
||||
|
||||
@ -25,7 +25,8 @@
|
||||
// 6. recognition_job.statements_version — коммит копии общего кода arm_statements, которым посчитан результат.
|
||||
// 7. document: уникальность (stored_file_id, document_type, osv_account_code) — только для типов кроме BALANCE / INCOME
|
||||
// (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа
|
||||
// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — порядковый номер документа в результате, уникален в файле.
|
||||
// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — индекс документа в documents[] результата (у всех
|
||||
// документов при любом MAJOR контракта; несозданный документ номер не занимает), уникален в файле.
|
||||
// 8. related_group.crm_id (varchar, nullable, unique) — ID ГСК/ГСЗ в ЕЦП CRM (AURORA-1619, arm-core#112, OQ-33).
|
||||
// 9. Признак «строка создана правкой аналитика» — financial_statement_line.row_index IS NULL (заменяет прежний
|
||||
// признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет).
|
||||
@ -36,16 +37,20 @@
|
||||
// правится PATCH шапки, как ИНН).
|
||||
// 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок
|
||||
// формы — COLUMN_SKIPPED и др.).
|
||||
// Заполнение при миграции: row_index строк — NULL у строк с line_code_original_value IS NULL (созданных правкой), у
|
||||
// остальных — по возрастанию line_code внутри формы с 0; line_status = ok, needs_review строк = false, needs_review
|
||||
// значений — по прежнему правилу values_ok (правлено или is_recognized не false и confidence 100 / NULL), source_text =
|
||||
// string_value, is_confirmed = false, period_kind = absolute при period_date, иначе none, form_variant = NULL (набор §7 —
|
||||
// полный), header_source — из *_original_value и inn_confidence, organization_name_original_value = organization_name,
|
||||
// file_ordinal — по (created_at, id) внутри файла с 0, related_group.crm_id = NULL; document_issue каждой формы —
|
||||
// нарушения §7, посчитанные копией arm_statements по текущим суммам (source = core, kind relation / soft).
|
||||
// Откат гарантирован на пустой БД и на данных, совместимых с v5.18 (нет повторов кода в форме, строк без кода, двух
|
||||
// форм одного типа в файле, crm_id групп); на несовместимых данных downgrade останавливается с ошибкой и ничего не
|
||||
// удаляет молча.
|
||||
// Перед выкладкой база очищается от форм ББ/ОФР (решение владельца 18.10.2026, Р-24; скрипт ядра
|
||||
// scripts/purge_statements.py): миграция только добавляет колонки, таблицу, enum'ы, индексы и значения по умолчанию,
|
||||
// §7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): row_index строк — NULL у
|
||||
// строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию line_code внутри формы с 0;
|
||||
// line_status = ok, needs_review строк = false, needs_review значений = НЕ values_ok (values_ok — правлено или
|
||||
// is_recognized не false и confidence 100 / NULL), source_text = string_value, is_confirmed = false, period_kind =
|
||||
// absolute при period_date, иначе none, form_variant = NULL (набор §7 — полный), header_source — из *_original_value
|
||||
// и inn_confidence, organization_name_original_value = organization_name, file_ordinal — по (created_at, id) внутри
|
||||
// файла с 0 (у всех документов), related_group.crm_id = NULL; document_issue — пуст: корректность нарушений у форм,
|
||||
// оставшихся в неочищенной базе, не гарантируется до первой правки.
|
||||
// Откат гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода;
|
||||
// downgrade останавливается с ошибкой и ничего не меняет при повторе кода в форме, строке без кода, двух формах одного
|
||||
// типа в файле, непустом related_group.crm_id, organization_name_modified = true; производные и сигнальные колонки
|
||||
// v5.19 и document_issue удаляет с записью числа удалённых значений в журнал миграции.
|
||||
//
|
||||
// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files):
|
||||
// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте
|
||||
@ -847,7 +852,7 @@ Table document {
|
||||
confirmed_at timestamptz
|
||||
created_at timestamptz
|
||||
updated_at timestamptz
|
||||
file_ordinal smallint [not null, default: 0, note: 'v5.19: порядковый номер документа в результате распознавания (с 0); различает две формы одного типа в файле (Р-10)']
|
||||
file_ordinal smallint [not null, default: 0, note: 'v5.19: индекс документа в documents[] результата распознавания (с 0), у всех документов при любом MAJOR контракта; несозданный документ номер не занимает (номера с пропусками); различает две формы одного типа в файле (Р-10)']
|
||||
Indexes {
|
||||
(stored_file_id, document_type, osv_account_code) [note: 'partial UNIQUE WHERE document_type NOT IN (\'BALANCE\', \'INCOME\') — NULLS NOT DISTINCT; v5.19: одна ОСВ по счёту / одна расшифровка / один прочий на файл; формы ББ/ОФР одного типа в файле могут повторяться (Р-10)']
|
||||
(stored_file_id, file_ordinal) [unique, note: 'v5.19: номер документа уникален в файле']
|
||||
@ -1085,7 +1090,7 @@ Table document_issue {
|
||||
actual numeric(18,2) [note: 'для relation / soft: правая часть']
|
||||
created_at timestamptz [not null]
|
||||
Indexes { document_id }
|
||||
Note: 'v5.19: предупреждения и нарушения документа формы. Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)'
|
||||
Note: 'v5.19: предупреждения и нарушения документа (формы — предупреждения и нарушения §7; ОСВ — предупреждения листа OSV_* результата 2.0). Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -13,18 +13,22 @@ Change `openspec/changes/unified-statement-normalization`: формы ББ/ОФ
|
||||
| 2 | `financial_statement_value`: `source_text`, `needs_review`, `is_confirmed`, `confirmed_by`, `confirmed_at` | `needs_review` **хранится** (пересматривает п. 4 v5.13): его считает сервис («подтверждение проверкой» §7 — Р-5, Р-7), ядро пересчитывает только после правок; подтверждение ячейки без правки аналитиком переживает пересчёт. Сырой текст источника — подсказка ячейки |
|
||||
| 3 | `financial_statement_column`: `period_kind`, `label_confidence` | Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года») — дата колонки от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6); колонка без даты остаётся с `period_kind = none` |
|
||||
| 4 | `financial_statement_meta`: `form_variant` (enum `statement_form_variant`), `needs_review_fields`, `header_source` (jsonb), `organization_name_original_value`, `organization_name_modified` | Упрощённая форма — свой набор соотношений §7 (`NULL` — документ до v5.19 или результат 1.x, набор полный); поля шапки, требующие проверки (сейчас — неуверенная дата отчёта); сырой текст и уверенность всех полей шапки результата (`<поле>_text`, `<поле>_confidence`) — подсказка у поля на экране; наименование организации правится, как ИНН |
|
||||
| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке (`column_id`, а вне колонок формы — `column_index`), полю шапки; у нарушения — `codes` (коды цели и членов); как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой. **Единственный источник нарушений** для экрана и подтверждения: для результатов 1.x и форм, существовавших до миграции, ядро один раз считает §7 копией `arm_statements` и пишет записи с `source = core` |
|
||||
| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке (`column_id`, а вне колонок формы — `column_index`), полю шапки; у нарушения — `codes` (коды цели и членов); как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой. **Единственный источник нарушений** для экрана и подтверждения: для результатов 1.x ядро один раз при приёме считает §7 копией `arm_statements` и пишет записи с `source = core`; формы до миграции не пересчитываются (база очищается, Р-24). Ведётся и у документов ОСВ — предупреждения листа `OSV_*` результата 2.0 |
|
||||
| 6 | `recognition_job.statements_version` | Коммит копии общего кода `arm_statements`, которым посчитан результат (`service.statements_version`); расхождение с копией ядра при пересчёте — `info` |
|
||||
| 7 | `document`: уникальность `(stored_file_id, document_type, osv_account_code)` — частичная, кроме `BALANCE` / `INCOME`; новая колонка `file_ordinal` + уникальный `(stored_file_id, file_ordinal)` | Вторая форма того же типа в файле хранится (Р-10, Р-15); при двух формах одного типа и периода ни одна не автоподтверждается, подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` (Р-18). ОСВ по счёту, расшифровка и прочий — по-прежнему один на тип на файл (§9.5 п. 2) |
|
||||
| 8 | `related_group.crm_id` (`varchar`, nullable, unique) | ID ГСК/ГСЗ в ЕЦП CRM из форм `51-03`, `51-04`, `77-02` (AURORA-1619, arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); уникален, как `crm_id` ГК и ЮЛ |
|
||||
|
||||
Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; заполнение: `row_index` — `NULL` у строк,
|
||||
Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; перед выкладкой база очищается от форм
|
||||
ББ/ОФР (решение владельца 18.10.2026, Р-24), поэтому миграция только добавляет колонки, таблицу и значения по умолчанию и
|
||||
§7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): `row_index` — `NULL` у строк,
|
||||
созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status =
|
||||
ok`, `needs_review` значений — по прежнему правилу `values_ok`, `source_text = string_value`, `period_kind` — `absolute`
|
||||
при `period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле;
|
||||
`document_issue` — нарушения §7 каждой формы, посчитанные копией `arm_statements` (`source = core`). Откат миграции
|
||||
гарантирован на пустой БД и на данных, совместимых с v5.18 (без повторов кода, строк без кода и двух форм одного типа в
|
||||
файле); на несовместимых данных `downgrade` останавливается с ошибкой.
|
||||
ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = string_value`, `period_kind` — `absolute` при
|
||||
`period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле;
|
||||
`document_issue` пуст — у форм, оставшихся в неочищенной базе, нарушения до первой правки не показываются. Откат
|
||||
гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода (без
|
||||
повторов кода, строк без кода, двух форм одного типа в файле, `crm_id` групп и правленого наименования организации); на
|
||||
таких данных `downgrade` останавливается с ошибкой, а производные колонки v5.19 и `document_issue` удаляет с записью в
|
||||
журнал миграции.
|
||||
|
||||
## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90)
|
||||
|
||||
@ -135,7 +139,7 @@ ok`, `needs_review` значений — по прежнему правилу `v
|
||||
|---|---|---|---|
|
||||
| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 |
|
||||
| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен (основа «рабочего стола консоли», общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `consolidation`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 |
|
||||
| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. **v5.19:** `file_ordinal` — номер документа в результате; формы ББ/ОФР одного типа в файле могут повторяться (ОСВ по счёту, расшифровка, прочий — один на тип); предупреждения и нарушения — `document_issue`. | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `document_issue`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») |
|
||||
| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. **v5.19:** `file_ordinal` — индекс документа в `documents[]` результата (у всех документов); формы ББ/ОФР одного типа в файле могут повторяться (ОСВ по счёту, расшифровка, прочий — один на тип); предупреждения и нарушения — `document_issue`. | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `document_issue`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») |
|
||||
| `recognition_job` | Одна попытка распознать/распарсить файл. **Единственная точка обмена с NATS** (outbox/inbox, дедуп по `result_msg_id`, DLQ). Хранит сервис и версию, **одну** ссылку на нормализованный JSON-результат в S3 (v5.13), метрики, и **схему группировок шапки ОСВ по листам** (`group_schema jsonb`, v5.13 — вместо `osv_group_schema`). **v5.19:** `statements_version` — версия копии общего кода нормализации, которой посчитан результат форм. | → `stored_file`, `s3_object`; ← `osv_entry`, `financial_statement_meta` | ТЗ 6.2, 6.10; DECISION-4 |
|
||||
|
||||
## Часть 05 — Разобранные ОСВ
|
||||
@ -152,7 +156,7 @@ ok`, `needs_review` значений — по прежнему правилу `v
|
||||
| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. **v5.19:** `period_kind` (абсолютная / относительная подпись бланка / без даты), `label_confidence`. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 |
|
||||
| `financial_statement_line` | Строка формы как в источнике: код (или его сырой текст) и наименование как распознано. **v5.19:** все строки источника — с кодом (`ok`, повторы кода тоже) и без (`unreadable`, `missing`, `detail` — расшифровка под родителем `parent_row_index`); порядок `row_index` (`NULL` ⇔ строка создана правкой аналитика), положение `source_ref`, `needs_review` неуверенного кода, подтверждение кода `is_confirmed`. Раскладка экрана — по коду «как на бумаге». | → `meta`; ← `value`, `document_issue.line_ids` | ТЗ 6.2, Прил. 2 |
|
||||
| `financial_statement_value` | Число в ячейке (строка × колонка): сумма, уверенность, `is_recognized`, ручная правка с исходным значением. **v5.19:** подсветка — только `needs_review` (хранится: уверенность, `is_recognized` и §7 сведены сервисом, ядро пересчитывает после правок); `source_text` — сырой текст источника; `is_confirmed` / `confirmed_by/at` — подтверждено без правки. (v5.13 этот флаг убирала как производный.) | → `line`, `column`, `app_user` | ТЗ 6.2, Прил. 2 |
|
||||
| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа формы: вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке (`column_id` или `column_index`) и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `codes`, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) |
|
||||
| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа (у формы; у ОСВ — предупреждения листа): вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке (`column_id` или `column_index`) и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `codes`, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) |
|
||||
|
||||
## Часть 07 — Консолидация
|
||||
|
||||
|
||||
@ -5,8 +5,9 @@ Issue: arm-ka-docs#116. Статус: **финальная версия** — ц
|
||||
`review-2026-10-17-closure.md`, `review-2026-10-17-blind.md`, `review-2026-10-17-closure-2.md`,
|
||||
`review-2026-10-17-blind-2.md`). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в `05-domain-rules.md`, DBML v5.19)
|
||||
написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (`review-2026-10-18-spec.md`, находки K-1…K-25) и
|
||||
доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже). Где текст дизайна выше расходится с
|
||||
решениями Р-20…Р-22 и S-n, действуют они и спека.
|
||||
доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже), прошла повторное ревью вслепую
|
||||
(`review-2026-10-18-spec-2.md`, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения
|
||||
S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека.
|
||||
|
||||
## Цель
|
||||
|
||||
@ -775,7 +776,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
`07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для
|
||||
неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени.
|
||||
|
||||
### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-22 — 18.10.2026)
|
||||
### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-26 — 18.10.2026)
|
||||
|
||||
- **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому
|
||||
общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и
|
||||
@ -875,6 +876,28 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
Excel-`ref`; S-17 — доопределить по K-23; S-19 — `codes` и `column_index` у `document_issue`; S-21 — по K-2 / K-21.
|
||||
Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже).
|
||||
|
||||
- **Р-23** (18.10.2026; повторное ревью спеки L-1) — значения строк `detail` в `needs_review` и в статус документа не
|
||||
входят: у них `needs_review = false`, функция статуса их не учитывает; подсказка на экране по `text` и `confidence`
|
||||
остаётся. Следствие: на реальных сканах РБ и Счастливочной за 1 кв. 2025 формы могут стать `done` (по прогону
|
||||
ревьюера держали их только ячейки расшифровок). В спеке: `statement-normalization` («Шаг 8 — `needs_review` значения»,
|
||||
функция статуса, сценарий на РБ), `arm-core` (пересчёт после правки, согласованность), `arm-web`, §11.9, proposal; S-51
|
||||
пересмотрено.
|
||||
- **Р-24** (18.10.2026; L-13 и упрощение S-36) — перед выкладкой ядра база очищается от форм ББ/ОФР (документы
|
||||
`BALANCE` / `INCOME`, их `financial_statement_*`, комментарии, привязки к версиям «в работе»; файлы, где кроме форм
|
||||
ничего нет, — целиком с заданиями; ОСВ, консолидации, справочники не трогаются; детали и отказы — S-73). Поэтому
|
||||
миграция v5.19 только добавляет колонки, индексы и значения по умолчанию; §7 для существующих форм не пересчитывается,
|
||||
импорта копии `arm_statements` в ревизии Alembic нет. Миграция обязана проходить и на непустой базе (колонки по
|
||||
умолчанию, `file_ordinal` по порядку, `document_issue` пуст), но корректность нарушений у старых форм не
|
||||
гарантируется — сказано явно. Для приёма 1.x после выкладки (окно перехода) §7 по-прежнему считается ядром копией
|
||||
пакета при приёме (остаётся из S-36). В спеке: proposal (порядок выкладки), `arm-core` («Миграция DBML v5.19»,
|
||||
«Очистка форм перед выкладкой», «Формы результата 1.x — §7 при приёме»), §7 «Ядро», §9.5 п. 2, DBML v5.19 (шапка).
|
||||
- **Р-25** (18.10.2026) — утверждены владельцем решения писателя S-26, S-34, S-36 (в редакции Р-24), S-37, S-41, S-47
|
||||
(подтверждение без правки не сбрасывает акцепт), S-48 (в редакции L-12: ядро вызывает общую функцию статуса). Отмечены
|
||||
ниже.
|
||||
- **Р-26** (18.10.2026; OQ-168) — коды без образца в fixtures (2421 / 2430 / 2450 в 66н, 1120 и другие в ФСБУ 4) в
|
||||
перечень кодов не входят до появления образцов; неуверенный такой код на скане — `needs_review` строки. Временное
|
||||
правило, принятое владельцем; OQ-168 остаётся открытым до образцов.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все
|
||||
@ -1031,6 +1054,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула
|
||||
дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для
|
||||
обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24.
|
||||
Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). **Утверждено владельцем 18.10.2026 (Р-25).**
|
||||
50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как
|
||||
контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность
|
||||
проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs
|
||||
@ -1039,7 +1063,8 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов
|
||||
из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО
|
||||
(1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024).
|
||||
Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166. Все 12
|
||||
Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166 (временное
|
||||
правило утверждено владельцем — Р-26). Все 12
|
||||
последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с
|
||||
неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`.
|
||||
52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии,
|
||||
@ -1047,7 +1072,8 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
`$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs.
|
||||
Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не
|
||||
настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния
|
||||
docs; без него их `make check` красный на окне.
|
||||
docs; без него их `make check` красный на окне. **Заменено S-53** (повторное ревью L-2: файла `.v1` у запроса нет,
|
||||
примеры и ядро не были покрыты).
|
||||
53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в
|
||||
`testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`:
|
||||
`0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы
|
||||
@ -1059,7 +1085,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
`column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый
|
||||
номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта
|
||||
приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их
|
||||
позициями.
|
||||
позициями. Дополнено S-56 (колонки сегментов со своей шапкой).
|
||||
55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет;
|
||||
`TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший
|
||||
разбор таблицы держит весь файл на ручном подтверждении.
|
||||
@ -1070,7 +1096,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`,
|
||||
`organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает
|
||||
`NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки
|
||||
снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам.
|
||||
снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. **Утверждено владельцем 18.10.2026 (Р-25).**
|
||||
58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` →
|
||||
`RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая.
|
||||
59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией
|
||||
@ -1081,11 +1107,13 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия
|
||||
миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для
|
||||
разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2).
|
||||
**Утверждено владельцем 18.10.2026 (Р-25) в редакции Р-24:** миграционного расчёта нет — база перед выкладкой
|
||||
очищается от форм; расчёт при приёме 1.x остаётся (вариант формы — по структуре кодов, S-59).
|
||||
60. **S-37. Признак строки правки — `row_index IS NULL`** (K-1): маппер 1.x и 2.0 всегда пишет `row_index`; миграция
|
||||
ставит `NULL` строкам с `line_code_original_value IS NULL`; `line_code_original_value` — код из источника, правками
|
||||
не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то
|
||||
же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок —
|
||||
только в аудите.
|
||||
только в аудите. **Утверждено владельцем 18.10.2026 (Р-25).**
|
||||
61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра —
|
||||
есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких
|
||||
нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или
|
||||
@ -1099,7 +1127,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх
|
||||
маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась.
|
||||
64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы
|
||||
в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора.
|
||||
в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора. **Утверждено владельцем 18.10.2026 (Р-25).**
|
||||
65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из
|
||||
`needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это
|
||||
единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке.
|
||||
@ -1119,10 +1147,12 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает
|
||||
правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее
|
||||
сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если
|
||||
владелец считает подтверждение правкой — одна строка спеки.
|
||||
владелец считает подтверждение правкой — одна строка спеки. **Утверждено владельцем 18.10.2026 (Р-25):**
|
||||
подтверждение без правки акцепт не сбрасывает.
|
||||
71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением,
|
||||
`needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT`
|
||||
`info`. Цена: проверка на каждом маппинге — линейная по строкам.
|
||||
`info`. Цена: проверка на каждом маппинге — линейная по строкам. **Утверждено владельцем 18.10.2026 (Р-25) в
|
||||
редакции L-12:** согласованность — вызов общей функции статуса по входу из результата (не список признаков).
|
||||
72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка
|
||||
и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на
|
||||
экране — первое имя кода, файловое — рядом.
|
||||
@ -1131,5 +1161,119 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая.
|
||||
74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов
|
||||
останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая.
|
||||
**Пересмотрено Р-23:** значения `detail` статус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут стать
|
||||
`done`; держат `needs_check` aep_3 и aep_6.
|
||||
75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие
|
||||
`1600 = 1700` (страницы 2 баланса ×3), и записи, чьи статусы `done` меняться не должны. Цена: нулевая.
|
||||
|
||||
### Решения при доработке спеки по повторному ревью L-1…L-30 (18.10.2026)
|
||||
|
||||
Решения агента при закрытии находок `review-2026-10-18-spec-2.md` там, где ни ревьюер, ни владелец (Р-23…Р-26) не задали
|
||||
значение, или где агент отступил от предложения ревьюера. Маркер `S-n` продолжает нумерацию; все обратимы правкой спеки,
|
||||
словаря или DBML до реализации.
|
||||
|
||||
76. **S-53. Правило теста идентичности копии контракта** (L-2; заменяет S-29): схема копии с `$id` `…/v<M>` сверяется с
|
||||
`<имя>.v<M>.schema.json` docs, **если такой файл есть**, иначе с основным; пример — с `examples/v<M>/<имя>.json`, если
|
||||
он есть, иначе с `examples/<имя>.json` (`M` — `MAJOR` копии схемы вида примера, вид — по префиксу имени); прочие файлы
|
||||
копии (`.v<N>`, `examples/v<N>/` у ядра) — с одноимёнными; файлы docs другого `MAJOR` сервис не сверяет. Правило
|
||||
вводится в шаг 0 у **всех трёх** потребителей, включая ядро. Основание: существующие тесты (`arm-core/api/tests/
|
||||
test_contract.py`, `tabular-parser` и `scan-gateway` `api/tests/unit/test_contract.py`) сравнивают весь набор
|
||||
`*.json` через `rglob`, включая `examples/`; правило «файл, если есть» проходит до слияния docs (файлов `.v1` нет), на
|
||||
окне и после, у запроса без `.v1`. Цена: три маленьких PR до слияния docs; удаление `MAJOR` 1 — в порядке «сначала
|
||||
ядро убирает копии `.v1`, затем docs».
|
||||
77. **S-54. `file_ordinal` у всех документов** (L-3): индекс документа в `documents[]` результата при любом `MAJOR`, у
|
||||
ОСВ, форм, `DISCLOSURE` и `OTHER`; несозданный документ (второй лист ОСВ с тем же счётом) номер не занимает и других
|
||||
не сдвигает — номера с пропусками. Цена: номер не «порядковый без дыр»; экран его не показывает.
|
||||
78. **S-55. Предупреждения файла и ОСВ в ядре** (L-4): `recognition_job.metrics.warnings` и `JobBrief.warnings` —
|
||||
структуры `fileWarning`; строки старых заданий конвертируются при чтении в `LEGACY_WARNING` (данные не
|
||||
переписываются); `JobBrief.mapping_warnings` — строки, как были. Предупреждения листа ОСВ 2.0 — `document_issue`
|
||||
документа ОСВ, отдаются в новом `DocumentOut.issues[]`. `osvDocument.status` используется: ОСВ 2.0 `RECOGNIZED` ⇔
|
||||
`status = done` ∧ `checks` чисты ∧ нет `error` файла ∧ контрагент не `MISMATCH` ∧ нет предупреждений маппинга листа;
|
||||
статус конверта не входит (как Р-19 для форм). Отступление от гайда «поля только добавляются»: тип `JobBrief.warnings`
|
||||
меняется — записано в гайде фронта. Цена: фронт обязан перейти на объекты в той же выкладке, что и ядро; ОСВ в книге
|
||||
с сомнительной ОФР теперь автоподтверждается (раньше держалась статусом задания).
|
||||
79. **S-56. Колонки сегмента со своей шапкой** (L-6): `columns[]` задаёт первый сегмент; колонки следующего сегмента со
|
||||
своей шапкой сопоставляются с ними по `period_date` (если у каждой колонки сегмента дата есть и совпадает ровно с
|
||||
одной), иначе по порядку; при сопоставлении по порядку другое число колонок — `SEGMENT_COLUMNS_MISMATCH`, лишние
|
||||
столбцы сегмента в `columns[]` не попадают, их номера — в `message`. Цена: сегмент с переставленными колонками без дат
|
||||
сопоставится по порядку неверно — §7 это поймает; НПЗ (лист 4 ББ со своей шапкой) остаётся `done`.
|
||||
80. **S-57. Явное правило мусора в `parse_amount`** (L-7): «ядро числа» от первого «цифра, `(`, знак» до последнего
|
||||
«цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор, выбрасывается с `is_recognized = false`; в ядре — только цифры,
|
||||
пробелы, `,`, `.`, знак в начале и пара скобок вокруг числа, иначе `amount = null` + `string_value` (буква в ядре,
|
||||
непарная скобка, символ между цифрами); крайние `|` удаляются до замены двойников; `{` и `[` открывающей скобкой не
|
||||
считаются. Так `211(1 |` и `(Ю4)` дают `null`, `: : 2856983 = ■` — 2856983, `{38378)` — `null` (сценарий «знак вне
|
||||
конвенции» переписан на значение без знака). Отступление от сегодняшнего `amounts.py` (выбрасывал всё, кроме цифр):
|
||||
осознанно. Цена: «12:34» даёт `null` и требует внимания аналитика; «1 234 руб.» даёт 1234 с `is_recognized = false`
|
||||
(буквы после последней цифры — краевой мусор, значение нуждается в покрытии §7).
|
||||
81. **S-58. Дрейф перечня кодов — код 10, не 20** (L-8): `sync-statements --check` в хосте сверяет копию перечня с
|
||||
docs, расхождение — `10` (предупреждение), а не `20`; `20` — только прямая правка копии, чужая версия, недостающая
|
||||
зависимость пакета и (в ядре) код словаря имён вне перечня **копии**. Порядок правки перечня: docs → `arm-statements` →
|
||||
хосты. Цена: копия с устаревшим перечнем проходит `check-full` с предупреждением; видна в описании PR.
|
||||
82. **S-59. Точка входа пакета для пересчёта** (L-10): `StoredForm` (без сетки) → `{form_variant, edition, checks,
|
||||
needs_review строк и значений, родители detail, status}`; признаки `code_fixed` / `value_fixed`, строки
|
||||
`row_index = null` участвуют в §7, `needs_review_fields` — вход (пересчёт шапку не переоценивает). Ядро вызывает её
|
||||
после PATCH и при приёме 1.x; вариант формы при приёме 1.x — **по структуре кодов**, а не `null` (уточняет S-36: иначе
|
||||
упрощённая форма 1.x получила бы нарушения полного набора до первой правки, а после правки — другой набор). Цена:
|
||||
у формы 1.x `form_variant` может отличаться от «полного по умолчанию»; образца упрощённой формы нет (OQ-166).
|
||||
83. **S-60. «Итого по разделу» — сравнение номеров раздела** (L-11): номер — начальные буквы `i`/`v`/`x` первого слова
|
||||
после фразы (хвост OCR «V1», «V /» отбрасывается), `strip` явно; номера равны ⇔ совпало; номер не выделен — равенство
|
||||
строк. Отступление от S-49 («только равенство»): ломалось на «Итого по разделу V1» Счастливочной. Цена: «II»,
|
||||
прочитанное как «I1», даст I ≠ II — ложное `NAME_MISMATCH` (безопасная сторона).
|
||||
84. **S-61. «изо :» — пустой код, правило не меняется** (L-14): двойники цифрами в ячейке кода без цифр не считаются
|
||||
(иначе слова, затёкшие в столбец кодов, стали бы `unreadable` и блокировали подтверждение); пример в §11.4
|
||||
исправлен. Цена: потерянная строка 1130 aep_3 («изо :», `'3'` с уверенностью 0) уходит в `detail` и скрыта; её сумма
|
||||
не входит в 1100, и `1100 = …` подсвечивает раздел, если сумма больше допуска ±1.
|
||||
85. **S-62. Родитель `detail`** (L-15): ближайшая строка `ok` выше (`unreadable` / `missing` пропускаются; нет `ok` выше —
|
||||
строка `missing`); после каждого PATCH ядро пересчитывает родителей **всех** строк `detail`; нет `ok` выше — родитель
|
||||
`null`, статус остаётся `detail`. Цена: строка `detail` без родителя на экране — в начале раздела.
|
||||
86. **S-63. Дата колонки руками** (L-16, L-17): ручная дата ставит `period_kind = absolute`; дата, равная текущей, у
|
||||
колонки с `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` — подтверждение (аналог S-42), снимает предупреждение и
|
||||
акцепт не сбрасывает. Цена: после ручной даты относительная колонка не следует за правкой даты отчёта — так и нужно.
|
||||
87. **S-64. Подтверждение дат только уверенными источниками** (L-18): для неуверенной подписи колонки эффективная дата —
|
||||
дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с подписью уверенности 100; неуверенная
|
||||
дата отчёта подтверждается только датами уверенных подписей. Для периода и относительных подписей эффективная дата —
|
||||
по всем источникам. Цена: скан, где и дата отчёта, и все подписи неуверенны, — всегда `needs_check` с подтверждением
|
||||
даты руками.
|
||||
88. **S-65. Структурная проверка кода — уточнения** (L-19): «лучшая» подпоследовательность — сначала по числу уверенных
|
||||
кодов (якоря), затем по длине; коды вне перечня редакции в её последовательность не входят; противоречащие якоря —
|
||||
входит наибольшее согласованное их множество; неуверенный код подтверждён, если входит во все лучшие; вариант формы —
|
||||
по всем строкам `ok` и пересчитывается ядром после правки кода. Цена: при перепутанных уверенных соседях неуверенный
|
||||
код между ними может остаться неподтверждённым — `needs_review`, безопасная сторона.
|
||||
89. **S-66. Форма знака** (L-20): по результату разбора (скобки, включая `}` / `]`; минус; без знака); сама ячейка в
|
||||
голосование не входит; нужно строгое большинство, равенство — не подтверждён. Цена: колонка с двумя голосующими
|
||||
разной формы не подтверждает знак ни одной сомнительной ячейки.
|
||||
90. **S-67. Редакция для проверок ядра** (L-21): `CODE_NOT_IN_EDITION` и `CODE_NOT_IN_DICTIONARY` — по редакции точки
|
||||
входа (лучшая подпоследовательность по всем строкам `ok`, ничья — по дате); `edition` ответа API и шаблонные строки —
|
||||
по дате, как сейчас. Цена: на бланке 66н, сданном в 2025 году, шаблонные строки на экране — по `fsbu4`, а проверки — по
|
||||
`66n`; сведение — отдельной задачей при необходимости (O-1 отчёта).
|
||||
91. **S-68. `OTHER` удалён из v2.0, `form_variant` обязателен** (L-22): в схеме v2.0 `statementDocument.document_type` —
|
||||
`BALANCE` | `INCOME` | `DISCLOSURE`, `form_variant` — в `required` (`null` у `DISCLOSURE`); `OTHER` остаётся в схеме
|
||||
1.x и в `document_type` ядра. Цена: нулевая — производителей `OTHER` в 2.0 нет.
|
||||
92. **S-69. Откат миграции — точная формулировка** (L-24; уточняет S-44): `downgrade` останавливается только на данных,
|
||||
которые v5.18 не может представить без потери первичных данных и ручного ввода (повтор кода, строка без кода, две
|
||||
формы одного типа в файле, непустой `crm_id` групп, правленое наименование организации); производные и сигнальные
|
||||
колонки v5.19 и `document_issue` удаляет с записью числа удалённых значений в журнал миграции (подтверждения — в
|
||||
аудите). Цена: откат после работы аналитиков теряет подтверждения ячеек и кодов как признаки (история — в аудите).
|
||||
93. **S-70. Механика копии пакета — уточнения** (L-25): `VERSION` из сравнения исключён (в источнике его нет); зависимости
|
||||
пакета из его `pyproject` обязаны быть в зависимостях хоста (иначе `--check` = 20); пакет проходит самый строгий
|
||||
конфиг `ruff` / `mypy` среди хостов; тесты пакета со своим `conftest`, `autouse`-фикстуры хоста ограничены его
|
||||
каталогом `tests/`. Цена: пакет живёт по самым строгим правилам всех хостов.
|
||||
94. **S-71. Содержимое таблицы ИС и `TABLE_SKIPPED`** (L-27): содержимое — хотя бы одна ячейка с цифрой или «цифрой под
|
||||
видом буквы»; таблица только из текста без столбца кодов и типа — новое предупреждение файла `TABLE_SKIPPED` (`info`),
|
||||
а не `TABLE_LOST` (`error`). Цена: текстовая таблица ИС не блокирует автоподтверждение; если в ней была форма без
|
||||
единой цифры — такой формы не бывает.
|
||||
95. **S-72. «Цифра под видом буквы» — содержимое строки** (L-30): ячейка значений, текст которой после замены двойников
|
||||
без условия соседства содержит цифру и не содержит других букв (`'I'`, `'| О'`), делает строку строкой данных; текст
|
||||
из настоящих букв при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев», «(расшифровка подписи)» у
|
||||
Межениновской) — нет: иначе каждая книга получала бы мусорные строки `detail`. Признано осознанным исключением из
|
||||
«ничего не выбрасывать молча». Цена: текстовая строка без цифр с уверенностью 100 по-прежнему теряется молча.
|
||||
96. **S-73. Очистка форм перед выкладкой — детали** (Р-24): скрипт ядра `scripts/purge_statements.py`, `--dry-run` по
|
||||
умолчанию, `--apply` одной транзакцией; отказ без изменений, если форма упомянута в `ex_snapshot_item` или привязана к
|
||||
версии не «в работе»; файлы только с формами удаляются целиком (как `DELETE /files/{id}`, без запрета
|
||||
`FILE_HAS_CONFIRMED_DOCUMENTS`, задания — тоже); в смешанных файлах «ОСВ + формы» удаляются только формы, файл
|
||||
остаётся — повторно его не загрузить (дедуп `sha256`), поэтому `--dry-run` перечисляет их и владелец решает про
|
||||
`--include-mixed`; системный аудит `FILE_DELETE`. Цена: подтверждённые формы и их привязки к версиям «в работе»
|
||||
исчезают до повторной загрузки; КФО этих версий пересчитывается.
|
||||
97. **S-74. `needs_review` значений после выхода строки из `detail`** (Р-23): строка, которой назначили код, получает
|
||||
`needs_review` значений по общему правилу (покрытие §7); строка, отмеченная `detail`, — `false`; значения
|
||||
`unreadable` / `missing` считаются по общему правилу (документ и так `needs_check` по статусу строки). Цена: нулевая.
|
||||
|
||||
@ -31,7 +31,8 @@
|
||||
- **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json`
|
||||
v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser
|
||||
меняется только в части структуры предупреждений и `document.status`. На окне перехода схемы 1.x лежат рядом
|
||||
(`*.v1.schema.json`, примеры `examples/v1/`), тест идентичности сервиса сверяет копию со схемой своего MAJOR.
|
||||
(`*.v1.schema.json`, примеры `examples/v1/`); тест идентичности копии у ядра и сервисов сверяет файл копии с файлом
|
||||
docs своего MAJOR, если он есть, иначе с основным (схемы и примеры; правило — спека `recognition-contract`).
|
||||
- **Упорядоченные перечни кодов форм** — данные `contracts/statement-code-orders.yaml` (66н / ФСБУ 4, полная /
|
||||
упрощённая, законные повторы кода — Р-20); в пакет `arm_statements` — байтовой копией.
|
||||
- **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`,
|
||||
@ -56,23 +57,37 @@
|
||||
|
||||
### Порядок выкладки — «сначала ядро» (обязателен)
|
||||
|
||||
0. tabular-parser и scan-gateway: тест идентичности копии контракта выбирает файл docs по MAJOR копии (`$id` `…/v1` →
|
||||
`*.v1.schema.json`) — до слияния шага 2, иначе их `make check` краснеет на окне перехода.
|
||||
0. arm-core, tabular-parser и scan-gateway: тест идентичности копии контракта по новому правилу — схема копии
|
||||
сверяется с `<имя>.v<M>.schema.json` своего MAJOR, если такой файл есть в docs, иначе с основным; пример — с
|
||||
`examples/v<M>/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы другого MAJOR не сверяются. Правило
|
||||
проходит и до слияния шага 2 (файлов `.v1` ещё нет), и после; без него `make check` всех трёх краснеет между шагами 2
|
||||
и 3 (ядро) или 2 и 5 (сервисы).
|
||||
1. `arm-statements` — пакет с тестами; `sync-statements` в docs.
|
||||
2. docs: контракт v2.0, DBML v5.19, правила (этот change).
|
||||
3. arm-core: миграция v5.19, маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля
|
||||
API. До этого шага ни один сервис не шлёт 2.0: первый же `line_code = null` дал бы файлу `ERROR`.
|
||||
3. arm-core: **сначала очистка форм ББ/ОФР на контуре** (Р-24: владелец или платформенная команда по его заявке,
|
||||
после резервной копии БД — скрипт ядра `scripts/purge_statements.py`: `--dry-run` с отчётом, затем `--apply`; удаляются
|
||||
документы `BALANCE` / `INCOME`, их `financial_statement_*`, комментарии и привязки к версиям «в работе», а файлы, где
|
||||
кроме форм нет ОСВ, — целиком с заданиями; смешанные файлы «ОСВ + формы» — по решению владельца по отчёту; при
|
||||
формах в верифицированных версиях скрипт отказывает; ОСВ, консолидации, справочники не трогаются — спека `arm-core`,
|
||||
«Очистка форм перед выкладкой»), затем миграция v5.19 (только колонки, индексы и значения по умолчанию; §7 для
|
||||
старых форм не считается), маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля
|
||||
API. Формы пересоздаются повторной загрузкой файлов. До этого шага ни один сервис не шлёт 2.0: первый же
|
||||
`line_code = null` дал бы файлу `ERROR`.
|
||||
4. arm-web: экран по новым полям (поля 1.x-документов заполнены по умолчанию — экран работает и на них).
|
||||
5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга.
|
||||
6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change).
|
||||
|
||||
Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs
|
||||
сверяются с `*.v1.schema.json` (шаг 0); каждый сервис обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих
|
||||
MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удаляются вместе с маппером 1.x (шаг 6).
|
||||
сверяются с `*.v1.schema.json` и `examples/v1/` (правило шага 0; у запроса файла `.v1` нет — с основным); каждый сервис
|
||||
обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих MAJOR с PR шага 3. Схемы `*.v1.schema.json` и
|
||||
`examples/v1/` удаляются вместе с маппером 1.x (шаг 6): сначала PR ядра убирает свои копии `.v1`, затем docs.
|
||||
|
||||
**Чего ждать на реальных сканах.** На шести OCR-документах `fixtures/ocr` после перехода большинство форм останутся
|
||||
`needs_check` (строки без кода с суммами, неуверенные значения вне соотношений, неуверенная дата); законные повторы кода
|
||||
и мягкие строки ОФР перестают держать документ (Р-20, Р-21). Доля `done` фиксируется в PR scan-gateway.
|
||||
**Чего ждать на реальных сканах.** Значения строк `detail` в `needs_review` и в статус не входят (Р-23), законные
|
||||
повторы кода и мягкие строки ОФР документ не держат (Р-20, Р-21). Поэтому ББ и ОФР РБ («Бухгалтерская отчетность 1
|
||||
кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: по прогону ревью все их неуверенные значения строк с кодом
|
||||
покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали их только ячейки
|
||||
расшифровок. aep_3 и aep_6 остаются `needs_check` (строки `unreadable` / `missing` с суммами, несошедшиеся
|
||||
соотношения). Фактическая доля `done` фиксируется в PR scan-gateway.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@ -122,8 +137,10 @@ MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удал
|
||||
- scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами
|
||||
2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в
|
||||
словари пакета.
|
||||
- arm-core: миграция v5.19 (`financial_statement_*`, `document`, `document_issue`, `recognition_job`,
|
||||
`related_group`; `document_issue` существующих форм — §7 копией пакета), `result_mapping`, `statement_mapping`,
|
||||
- arm-core: очистка форм ББ/ОФР перед выкладкой (`scripts/purge_statements.py`, Р-24); миграция v5.19
|
||||
(`financial_statement_*`, `document`, `document_issue`, `recognition_job`, `related_group`; только колонки, индексы и
|
||||
значения по умолчанию, §7 по старым формам не считается — на неочищенной базе их нарушения до первой правки не
|
||||
показываются); `JobBrief.warnings` — структуры, `DocumentOut.issues` (в том числе ОСВ); `result_mapping`, `statement_mapping`,
|
||||
`statements`, `documents`, `auto_confirm`, схемы API формы; признак строки правки — `row_index IS NULL`;
|
||||
`api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён
|
||||
(несколько имён на код, загрузчик повтор кода не запрещает); PATCH шапки принимает `organization_name`.
|
||||
|
||||
@ -11,9 +11,20 @@ false`); каждая пара «строка × колонка» — одна `
|
||||
`financial_statement_meta` с `form_variant`, `needs_review_fields` и `header_source` (раздел «Шапка формы»);
|
||||
предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего кода —
|
||||
`recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)` уникален там,
|
||||
где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document` с
|
||||
`file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится (Р-10); ОСВ по
|
||||
счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document`; вторая
|
||||
форма того же типа в файле хранится (Р-10); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и
|
||||
счёт) на файл. **Каждый** создаваемый `document` (ОСВ, формы, `DISCLOSURE`, `OTHER`) при приёме результата любого
|
||||
`MAJOR` (мапперы `1.x` и `2.0`) SHALL получать `file_ordinal` = индекс документа в `documents[]` результата (с 0);
|
||||
документ, который не создаётся (второй лист ОСВ с тем же счётом, второй `DISCLOSURE`), номер не занимает, но и не
|
||||
сдвигает остальные — номера уникальны в файле и могут идти с пропусками.
|
||||
|
||||
#### Scenario: Книга из двух ОСВ
|
||||
- **WHEN** результат содержит две ОСВ по счетам 60 и 62 (`documents[0]`, `documents[1]`), принятый маппером `1.x` или `2.0`
|
||||
- **THEN** два документа с `file_ordinal` 0 и 1; уникальность `(stored_file_id, file_ordinal)` не нарушена, файл не `ERROR`
|
||||
|
||||
#### Scenario: PDF с ББ и ОФР по 1.x
|
||||
- **WHEN** результат `1.2` содержит `BALANCE` и `INCOME`
|
||||
- **THEN** документы с `file_ordinal` 0 и 1
|
||||
|
||||
#### Scenario: Повтор кода сохраняется
|
||||
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
|
||||
@ -24,10 +35,15 @@ false`); каждая пара «строка × колонка» — одна `
|
||||
- **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`,
|
||||
`line_code_text = "ИЧ,250"` и `row_index` из результата
|
||||
|
||||
#### Scenario: Два баланса в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать)
|
||||
#### Scenario: Два баланса одного периода в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` с одной парой `period_year`, `period_quarter` (лист-продолжение другого
|
||||
типа или двойная печать)
|
||||
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
|
||||
|
||||
#### Scenario: Два баланса разных периодов в одном файле
|
||||
- **WHEN** результат содержит `BALANCE` на 30.09.2025 и `BALANCE` на 31.12.2024
|
||||
- **THEN** два документа с разными `file_ordinal`; каждый получает статус по своим условиям, правило Р-18 их не касается
|
||||
|
||||
### Requirement: Признак строки, созданной правкой
|
||||
Строка формы SHALL считаться созданной правкой аналитика (шаблонной, `origin = template`) тогда и только тогда, когда
|
||||
`row_index IS NULL`. Маппер `1.x` и маппер `2.0` SHALL всегда писать `row_index` (у `1.x` — по порядку строк результата с
|
||||
@ -75,17 +91,22 @@ units_text`; `organization_name_original_value = organization_name`, `inn_confid
|
||||
период — конец квартала (`period_year` и `period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом
|
||||
(`line_code_confidence < 100`) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того
|
||||
же типа и периода (та же пара `period_year`, `period_quarter`; формы без квартала не считаются); иначе `NEEDS_CHECK`.
|
||||
**Согласованность:** `done` противоречит содержимому, если есть строка `unreadable` / `missing` со значением (`amount` или
|
||||
`string_value` не `null`), строка, значение или поле шапки с `needs_review`, `checks.relations_failed` или предупреждение
|
||||
документа уровня `error`; тогда ядро считает документ `needs_check` и пишет `document_issue`
|
||||
`RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Статус конверта и `needs_check` соседнего документа в условие
|
||||
**Согласованность:** ядро SHALL вызывать общую функцию статуса пакета (`statement-normalization`, «Шаг 8 — статус
|
||||
документа и конверта») по входу, собранному из результата: `layout_found` — `lines[]` непуст, `has_organization_or_inn`
|
||||
и `period_known` — по шапке и колонкам результата, строки и значения с их `needs_review` (значения `detail` функция не
|
||||
учитывает), число `checks.relations_failed`, `accepted = false`, уровни `warnings[]` документа. `done` согласован, если
|
||||
функция тоже даёт `done`; иначе ядро считает документ `needs_check` и пишет `document_issue`
|
||||
`RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Это вызов той же функции, а не пересчёт §7 и `needs_review`. Статус конверта и `needs_check` соседнего документа в условие
|
||||
MUST NOT входить (Р-19). Проверки §7, `needs_review` и предупреждения сервиса при загрузке MUST NOT пересчитываться.
|
||||
|
||||
**Совпадение наименований:** обе строки приводятся к нижнему регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел,
|
||||
пробелы схлопываются; совпало, если строки равны, или более короткая (не короче 5 символов) входит в более длинную, или
|
||||
отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** справочным именем кода (у кода может быть
|
||||
несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря ядра. Если хотя бы одно из имён начинается с
|
||||
«итого по разделу», совпадение — только равенство. Кода нет в словаре имён или наименование пусто — не совпало.
|
||||
пробелы схлопываются, крайние пробелы обрезаются (`strip`); совпало, если строки равны, или более короткая (не короче 5
|
||||
символов) входит в более длинную, или отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым**
|
||||
справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря
|
||||
ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера раздела**: номер —
|
||||
начальная часть первого слова после фразы из букв `i`, `v`, `x` (латиница; кириллические «і», «х» приводятся к ним),
|
||||
остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени —
|
||||
совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало.
|
||||
Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка). `RECOGNIZED` SHALL, как и прежде,
|
||||
автоподтверждаться (OQ-157).
|
||||
|
||||
@ -110,8 +131,17 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре
|
||||
|
||||
#### Scenario: Подмена итога раздела
|
||||
- **WHEN** строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
|
||||
- **THEN** наименование не совпало со справочным «Итого по разделу II» (для «итого по разделу» — только равенство),
|
||||
`NAME_MISMATCH`
|
||||
- **THEN** номер раздела I не равен II у справочного «Итого по разделу II», `NAME_MISMATCH`
|
||||
|
||||
#### Scenario: Хвост OCR после номера раздела (Счастливочная, РБ)
|
||||
- **WHEN** строка 1500 (уверенность 56, разлепленная) с наименованием «Итого по разделу V1», или строка с наименованием
|
||||
«Итого по разделу V /»
|
||||
- **THEN** номер раздела V равен номеру справочного «Итого по разделу V», наименование совпало, `NAME_MISMATCH` нет
|
||||
|
||||
#### Scenario: Сервис прислал done при неразобранной строке без значений
|
||||
- **WHEN** документ пришёл `done`, но в нём строка `unreadable` без значений
|
||||
- **THEN** функция статуса даёт `needs_check` (не все строки `ok` / `detail`), документ `NEEDS_CHECK`,
|
||||
`RESULT_STATUS_INCONSISTENT`; автоподтверждения нет
|
||||
|
||||
#### Scenario: Сервис ошибся со статусом
|
||||
- **WHEN** документ пришёл `done`, но в нём строка `unreadable` с суммой
|
||||
@ -130,8 +160,13 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре
|
||||
relation`, `code = RELATION_FAILED`, `severity = error`; из `soft_relations_failed` — `kind = soft`, `code =
|
||||
RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` кодов `codes` в документе. При загрузке пишутся
|
||||
предупреждения и `checks` результата (источник `service`) и проверки ядра уровня `info` (источник `core`):
|
||||
`CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён), `CODE_NOT_IN_EDITION` (код не из перечня редакции — по копии
|
||||
перечня `arm_statements`), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`. При перезагрузке файла записи документа
|
||||
`CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён редакции документа), `CODE_NOT_IN_EDITION` (код не из перечня
|
||||
редакции документа и его варианта — по копии перечня `arm_statements`), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`.
|
||||
**Редакция документа** для этих проверок — `edition` точки входа пакета для пересчёта (`statement-normalization`):
|
||||
редакция с лучшей подпоследовательностью кодов строк `ok` документа, при равенстве — по эффективной дате; так бланк 66н,
|
||||
сданный в 2025 году, проверяется по `66n`, а не по дате. Поле `edition` ответа `GET …/statement` и выбор шаблонных
|
||||
строк по-прежнему — по дате периода (§7 «Полный состав формы»). `document_issue` ведётся у **любого** документа файла: у
|
||||
ОСВ — предупреждения `osvDocument.warnings` результата `2.0` (вид `warning`, источник `service`, `OSV_*`, `info`). При перезагрузке файла записи документа
|
||||
удаляются вместе с ним. При пересчёте после правки записи `relation` и `soft` заменяются целиком независимо от источника;
|
||||
записи `warning` источника `service` сохраняются, кроме снимаемых правкой: правка `header.report_date` (в том числе
|
||||
подтверждение тем же значением) снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`, `REPORT_DATE_UNCERTAIN`; правка
|
||||
@ -148,24 +183,36 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
|
||||
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
|
||||
|
||||
#### Scenario: Бланк 66н в 2025 году без ложного info
|
||||
- **WHEN** форма Межениновской за 3 кв. 2025 на бланке 66н с кодом 1120 (в перечне `fsbu4` его нет)
|
||||
- **THEN** редакция документа для проверок ядра — `66n` (подпоследовательность длиннее), `CODE_NOT_IN_EDITION` у 1120 нет
|
||||
|
||||
#### Scenario: Пропущенная колонка
|
||||
- **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет
|
||||
- **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа
|
||||
|
||||
### Requirement: Документы без `checks` — результаты 1.x и формы до миграции
|
||||
Для формы без `checks` контракта 2.0 — принятой по результату `1.x` и существовавшей до миграции v5.19 — ядро SHALL
|
||||
**один раз** посчитать §7 копией `arm_statements` по текущим суммам (`form_variant = null` — полный набор) и записать
|
||||
нарушения в `document_issue` (источник `core`, виды `relation` / `soft`, как в требовании о `document_issue`): у `1.x` —
|
||||
при приёме результата, у существующих форм — в миграции v5.19. Условие D-6 «проверки сошлись» для `1.x` SHALL браться из
|
||||
этого расчёта. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue`
|
||||
### Requirement: Формы результата 1.x — §7 при приёме
|
||||
Для формы, принятой по результату `1.x` (окно перехода после выкладки ядра; `checks` контракта 2.0 у неё нет), ядро
|
||||
SHALL **один раз при приёме** посчитать §7 точкой входа пакета для пересчёта (`statement-normalization`; вариант формы —
|
||||
по структуре кодов, как у 2.0) по суммам результата и записать нарушения в `document_issue` (источник `core`, виды
|
||||
`relation` / `soft`, как в требовании о `document_issue`) и `form_variant` в `financial_statement_meta`. Условие D-6
|
||||
«проверки сошлись» для `1.x` SHALL браться из этого расчёта; `needs_review` значения при приёме `1.x` = **НЕ**
|
||||
`values_ok` (`values_ok` — `is_recognized` не `false` и `confidence` равна 100 или пуста), `needs_review` строки —
|
||||
`false`. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue`
|
||||
`MAPPING_WARNING` (`info`, источник `core`); строки `metrics.warnings` результата `1.x` — в
|
||||
`recognition_job.metrics.warnings` структурами `{code: "LEGACY_WARNING", severity: "info", message}`. После правки такая
|
||||
форма пересчитывается общим кодом, как форма 2.0; `MAPPING_WARNING` и `LEGACY_WARNING` статус не держат.
|
||||
форма пересчитывается общим кодом, как форма 2.0; `MAPPING_WARNING` и `LEGACY_WARNING` статус не держат. Формы,
|
||||
существовавшие в БД до миграции v5.19, §7 не пересчитываются (требования «Миграция DBML v5.19» и «Очистка форм перед
|
||||
выкладкой»).
|
||||
|
||||
#### Scenario: Мигрированная форма с нарушением
|
||||
- **WHEN** до миграции форма `NEEDS_CHECK` с несошедшимся `1600 = 1700` без акцепта; миграция v5.19 выполнена
|
||||
- **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]`; экран показывает
|
||||
нарушение; `POST …/confirm` — 409 `CHECKS_NOT_PASSED`
|
||||
#### Scenario: Полярность needs_review при приёме 1.x
|
||||
- **WHEN** в результате `1.2` у значения `confidence = 100`, `is_recognized = true`, у другого — `confidence = 80`
|
||||
- **THEN** у первого `needs_review = false`, у второго — `true`
|
||||
|
||||
#### Scenario: Форма 1.x с нарушением
|
||||
- **WHEN** результат `1.2` содержит ББ с несошедшимся `1600 = 1700`
|
||||
- **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]` (источник `core`),
|
||||
документ `NEEDS_CHECK`; `POST …/confirm` без акцепта — 409 `CHECKS_NOT_PASSED`
|
||||
|
||||
#### Scenario: 1.x-документ после PATCH
|
||||
- **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится
|
||||
@ -173,14 +220,18 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет)
|
||||
|
||||
### Requirement: Пересчёт после правки
|
||||
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review` и статус тем же общим кодом
|
||||
`arm_statements` по сохранённым данным (конвенция знака — по `source_text`; `form_variant = null` — полный набор), с
|
||||
правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed`
|
||||
строки ⇒ `needs_review` строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование
|
||||
знака не входят. Отличие `statements_version` задания от версии копии ядра — `document_issue`
|
||||
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review`, `form_variant`, родителей строк `detail`
|
||||
и статус точкой входа пакета для пересчёта по сохранённым данным (`statement-normalization`; конвенция знака — по
|
||||
`source_text`), с правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или
|
||||
`is_confirmed` строки ⇒ `needs_review` строки = `false`; значения строк `detail` — `needs_review = false` (Р-23), у
|
||||
строки, выведенной из `detail` назначением кода, `needs_review` её значений считается по общему правилу; акцепт ⇒
|
||||
жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят. Результат пишется: `needs_review`
|
||||
строк и значений, `form_variant`, `parent_row_index` всех строк `detail` (ближайшая строка `ok` выше по `row_index`;
|
||||
нет такой — `null`, строка остаётся `detail`), записи `document_issue` видов `relation` / `soft`, статус. Отличие `statements_version` задания от версии копии ядра — `document_issue`
|
||||
`STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Один PATCH — один пересчёт и одна
|
||||
запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
|
||||
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) акцепт MUST NOT сбрасывать.
|
||||
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки
|
||||
тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47).
|
||||
|
||||
#### Scenario: Подтверждение ячейки без правки
|
||||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||||
@ -191,6 +242,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением
|
||||
- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит
|
||||
|
||||
#### Scenario: Родитель уходит в расшифровки
|
||||
- **WHEN** у строки 1150 две расшифровки (`detail`, `parent_row_index` = 1150), аналитик отмечает саму 1150 расшифровкой
|
||||
- **THEN** после пересчёта родитель всех трёх строк `detail` — ближайшая строка `ok` выше 1150; строк над ними нет — `null`
|
||||
(на экране — в начале раздела)
|
||||
|
||||
### Requirement: PATCH формы — строки, колонки, подтверждение, наименование организации
|
||||
`PATCH /documents/{id}/statement` SHALL принимать помимо `header {inn, report_date, units}` и `values` (сумма):
|
||||
- `header.organization_name` — непустая строка до 1000 символов: `organization_name_modified = true`,
|
||||
@ -201,8 +257,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
- `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения значения или снять
|
||||
подтверждение;
|
||||
- `columns[] {column_id, period_date}` — дата колонке (обязательная дата; `null` — 422 ошибка валидации; пересчитывает
|
||||
`is_current` и период документа по правилу §9.5 п. 2); правка `header.report_date` (в том числе `null`) пересчитывает
|
||||
`period_date` колонок с `period_kind = relative:*` общим кодом (`null` даёт `null`);
|
||||
`is_current` и период документа по правилу §9.5 п. 2); ручная дата ставит колонке `period_kind = absolute`, чтобы
|
||||
следующая правка даты отчёта её не перезаписала; дата, равная текущей, у колонки с `COLUMN_DATE_UNCERTAIN` или
|
||||
`COLUMN_WITHOUT_DATE` — подтверждение: предупреждение снимается, `period_kind = absolute`, аудит (иначе запрос без
|
||||
изменений — 200 без эффектов); правка `header.report_date` (в том числе `null`) пересчитывает `period_date` колонок с
|
||||
`period_kind = relative:*` общим кодом (`null` даёт `null`);
|
||||
- `lines[] {line_id, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
|
||||
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить код или снять подтверждение.
|
||||
|
||||
@ -211,8 +270,9 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
`unreadable` / `missing` нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла.
|
||||
`line_status` принимает только `"detail"`; `"ok"` — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только
|
||||
`line_code`). Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`,
|
||||
`needs_review` строки = `false`; `line_code_original_value` не меняется; у `detail` `parent_row_index` пересчитывается
|
||||
как ближайшая строка `ok` выше по `row_index`. Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с
|
||||
`needs_review` строки = `false`; `line_code_original_value` не меняется; после любого изменения статусов или кодов
|
||||
`parent_row_index` **всех** строк `detail` документа пересчитывается как ближайшая строка `ok` выше по `row_index`
|
||||
(требование «Пересчёт после правки»). Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с
|
||||
существующим; незнакомый словарю — `info`. Строка с тем же кодом, **созданная правкой** (`row_index IS NULL`), с суммами —
|
||||
409 `LINE_CODE_TAKEN` (сначала очистить её суммы или выбрать другой код); без сумм — удаляется (её `id` убирается из
|
||||
`document_issue.line_ids`), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном
|
||||
@ -250,6 +310,14 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
- **WHEN** `report_date` в `needs_review_fields`, аналитик отправляет PATCH с тем же `header.report_date`
|
||||
- **THEN** `needs_review_fields` пуст, `REPORT_DATE_UNCERTAIN` снят, статус пересчитан, аудит
|
||||
|
||||
#### Scenario: Ручная дата относительной колонки
|
||||
- **WHEN** колонке `relative:prev_year_end` аналитик ставит `period_date` 31.12.2024, затем правит дату отчёта
|
||||
- **THEN** после первой правки `period_kind = absolute`; правка даты отчёта её дату не меняет
|
||||
|
||||
#### Scenario: Подтверждение неуверенной даты колонки
|
||||
- **WHEN** у колонки `COLUMN_DATE_UNCERTAIN`, аналитик отправляет `columns: [{column_id, period_date}]` с той же датой
|
||||
- **THEN** предупреждение снято, `period_kind = absolute`, статус пересчитан, аудит
|
||||
|
||||
#### Scenario: Статус ok без кода
|
||||
- **WHEN** приходит `lines: [{line_id, line_status: "ok"}]`
|
||||
- **THEN** 422 `LINE_CODE_REQUIRED`
|
||||
@ -280,7 +348,11 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
|
||||
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
|
||||
состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды словаря имён есть в
|
||||
перечне. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из
|
||||
перечне. В PR перехода ядра на v2.0 словарь имён SHALL быть дополнен: в редакции `fsbu4` — вторые имена законных
|
||||
повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощенной системе
|
||||
налогообложения» (Р-20) и коды печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные поисковые активы», 1140
|
||||
«Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по `fixtures/ocr` и шаблонам
|
||||
банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают `NAME_MISMATCH`. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из
|
||||
файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча.
|
||||
|
||||
#### Scenario: Код есть только у строки unreadable
|
||||
@ -292,6 +364,10 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
аренде»
|
||||
- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
|
||||
|
||||
#### Scenario: Гудвил с заголовком раздела в имени
|
||||
- **WHEN** строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил»
|
||||
- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет
|
||||
|
||||
### Requirement: Ответ `GET …/statement` по v2.0
|
||||
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`) и
|
||||
`organization_name_modified`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔
|
||||
@ -310,22 +386,41 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
|
||||
string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает
|
||||
|
||||
### Requirement: Миграция DBML v5.19
|
||||
Одна миграция Alembic SHALL приводить схему к DBML v5.19; заполнение существующих данных: `row_index` — `NULL` у строк с
|
||||
`line_code_original_value IS NULL` (созданных правкой), у остальных — по возрастанию `line_code` внутри формы с 0;
|
||||
`line_status = ok`, `needs_review` строк — `false`, `needs_review` значений — по прежнему `values_ok`, `source_text =
|
||||
Одна миграция Alembic SHALL приводить схему к DBML v5.19 и **только** добавлять колонки, таблицу, enum'ы и индексы и
|
||||
заполнять значения по умолчанию (Р-24); §7 по существующим формам миграция MUST NOT считать, импорта копии
|
||||
`arm_statements` в ревизии нет. Перед выкладкой база очищается от форм ББ/ОФР (требование «Очистка форм перед
|
||||
выкладкой»), но миграция SHALL проходить и на непустой базе (стенды разработчиков, база, очищенная не полностью):
|
||||
`row_index` — `NULL` у строк с `line_code_original_value IS NULL` (созданных правкой), у остальных — по возрастанию
|
||||
`line_code` внутри формы с 0; `line_status = ok`, `needs_review` строк — `false`, `needs_review` значений — **НЕ**
|
||||
`values_ok` (`values_ok` — правлено, или `is_recognized` не `false` и `confidence` равна 100 или пуста), `source_text =
|
||||
string_value`, `is_confirmed = false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant =
|
||||
null`, `header_source` — из `*_original_value` и `inn_confidence`, `organization_name_original_value =
|
||||
organization_name`, `document.file_ordinal` — по `(created_at, id)` внутри файла с 0, `related_group.crm_id` — `null`;
|
||||
`document_issue` каждой формы — нарушения §7, посчитанные копией `arm_statements` (требование «Документы без `checks`»).
|
||||
Откат (`downgrade`) SHALL быть гарантирован на пустой БД и на данных, совместимых с v5.18 (нет двух строк с одним кодом в
|
||||
форме, строк с `line_code = null`, двух форм одного типа в файле); на несовместимых данных `downgrade` SHALL
|
||||
останавливаться с ошибкой, называющей причину, и MUST NOT удалять данные молча. Сверка `check_dbml_vs_orm.py` — против
|
||||
`data-model-v5.19.dbml`.
|
||||
organization_name`, `document.file_ordinal` — по `(created_at, id)` внутри файла с 0 (у всех документов),
|
||||
`related_group.crm_id` — `null`; `document_issue` — пуст. **Корректность нарушений у форм, оставшихся с до-миграционных
|
||||
времён, не гарантируется:** у них нет записей `document_issue`, поэтому до первой правки экран не показывает нарушений
|
||||
§7, а `POST …/confirm` не отвечает `CHECKS_NOT_PASSED`; первый PATCH пересчитывает форму общим кодом. Это цена Р-24; на
|
||||
контуре её снимает очистка. Сверка `check_dbml_vs_orm.py` — против `data-model-v5.19.dbml`.
|
||||
|
||||
Откат (`downgrade`) SHALL быть гарантирован на пустой БД и на данных, которые схема v5.18 может представить без потери
|
||||
первичных данных и ручного ввода. `downgrade` SHALL останавливаться с ошибкой, называющей причину и число записей, и
|
||||
ничего не менять, если есть: две строки с одним кодом в форме, строка с `line_code = null`, две формы одного типа в файле,
|
||||
непустой `related_group.crm_id`, `organization_name_modified = true` (исходное наименование хранится только в новой
|
||||
колонке). Остальные новые колонки и таблица `document_issue` — производные и сигнальные данные v5.19 (`needs_review`,
|
||||
`source_text`, `header_source`, `is_confirmed` с `confirmed_by/at`, `period_kind`, `label_confidence`, `form_variant`,
|
||||
`needs_review_fields`, `file_ordinal`, `statements_version`, `document_issue`): `downgrade` удаляет их и пишет в журнал
|
||||
миграции, сколько записей с непустыми значениями удалено (подтверждения аналитика остаются в `audit_event`). Это не
|
||||
«молчаливое удаление»: потеря названа в журнале и в этом требовании.
|
||||
|
||||
#### Scenario: Цикл миграций
|
||||
- **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД
|
||||
- **THEN** все три шага проходят, сверка ORM с DBML v5.19 — без расхождений
|
||||
|
||||
#### Scenario: Миграция на непустой базе
|
||||
- **WHEN** на стенде есть форма ББ, загруженная до v5.19, с несошедшимся `1600 = 1700`, и выполнена миграция
|
||||
- **THEN** миграция проходит: у строк `row_index` по коду, `line_status = ok`, у документа `file_ordinal`, записей
|
||||
`document_issue` нет — нарушение до первой правки не показано (корректность не гарантируется); после любого PATCH форма
|
||||
пересчитана, и запись `RELATION_FAILED` появляется
|
||||
|
||||
#### Scenario: Строка правки до миграции
|
||||
- **WHEN** до миграции у формы есть строка 1250, созданная правкой (`line_code_original_value = null`), с суммой
|
||||
- **THEN** после миграции у неё `row_index = null`, `origin = template`; строки источника пронумерованы с 0
|
||||
@ -334,6 +429,82 @@ organization_name`, `document.file_ordinal` — по `(created_at, id)` внут
|
||||
- **WHEN** в БД есть форма с двумя строками 2410, выполняется `downgrade` ниже v5.19
|
||||
- **THEN** откат останавливается с ошибкой о повторе кода; данные не изменены
|
||||
|
||||
#### Scenario: Откат после подтверждений
|
||||
- **WHEN** в БД нет несовместимых данных, но у ячеек есть `is_confirmed = true` и записи `document_issue`
|
||||
- **THEN** `downgrade` проходит, удаляет колонки и таблицу и пишет в журнал число удалённых подтверждений и записей
|
||||
|
||||
### Requirement: Очистка форм перед выкладкой
|
||||
Перед выкладкой ядра с миграцией v5.19 на контур (шаг 3 порядка выкладки) база SHALL очищаться от форм ББ/ОФР (Р-24):
|
||||
формы пересоздаются повторной загрузкой файлов уже по новым правилам. Очистку выполняет владелец или платформенная
|
||||
команда по его заявке, на контуре — после резервной копии БД и до `alembic upgrade` v5.19 (на стендах разработчиков — по
|
||||
желанию). Инструмент — скрипт ядра `scripts/purge_statements.py` (пишется в PR шага 3; описание здесь, кода нет) с
|
||||
режимами `--dry-run` (по умолчанию: только отчёт) и `--apply`, одной транзакцией:
|
||||
1. **Отбор:** документы `BALANCE` / `INCOME` (у файлов любого статуса, включая `SUPERSEDED`).
|
||||
2. **Отказ без изменений**, если хотя бы один отобранный документ упомянут в `ex_snapshot_item` или привязан
|
||||
(`consolidation_document`) к версии консолидации не «в работе» (`VERIFIED` и далее): отчёт называет версии, решение —
|
||||
за владельцем.
|
||||
3. **Файлы только с формами** — у файла нет документов `OSV`: удаляется файл целиком, как `DELETE /files/{id}` (§9.5 п. 5),
|
||||
но без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`: привязки документов к версиям «в работе» снимаются (статус версий не
|
||||
меняется), удаляются документы всех типов файла (`BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`) с
|
||||
`financial_statement_*` и комментариями документов, задания `recognition_job` файла и `stored_file`; объектам S3
|
||||
ставится `expires_at = now()`; ссылка `superseded_by_file_id` других файлов на удаляемый обнуляется, а заменённые им
|
||||
файлы остаются `SUPERSEDED` (повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ).
|
||||
4. **Смешанные файлы** — у файла есть и ОСВ, и формы: удаляются только документы `BALANCE` / `INCOME` с их
|
||||
`financial_statement_*`, комментариями и привязками; файл, ОСВ, `DISCLOSURE` / `OTHER` и задания остаются. Повторно
|
||||
загрузить такой файл нельзя (дедуп по `sha256`), поэтому отчёт `--dry-run` перечисляет смешанные файлы отдельно, и
|
||||
владелец решает до `--apply`, удалять ли их целиком (ключ `--include-mixed`).
|
||||
5. **Аудит:** на каждый удалённый файл — системное `FILE_DELETE` (`user_id = NULL`, описание «очистка форм перед v5.19»,
|
||||
карточка файла в `before_value`); на документ смешанного файла — то же событие по файлу с перечнем удалённых
|
||||
документов.
|
||||
6. **Не трогаются:** ОСВ (`osv_entry`), консолидации и их версии, корректировки, гудвилл, справочники, пользователи,
|
||||
журнал аудита. Результаты КФО версий «в работе», посчитанные с удалёнными формами, пересчитываются, как после удаления
|
||||
файла.
|
||||
|
||||
#### Scenario: Отчёт перед очисткой
|
||||
- **WHEN** владелец запускает скрипт без ключей
|
||||
- **THEN** скрипт ничего не меняет и печатает число файлов только с формами, смешанных файлов, документов форм,
|
||||
подтверждённых документов и привязок к версиям «в работе»
|
||||
|
||||
#### Scenario: Форма в верифицированной версии
|
||||
- **WHEN** документ ББ привязан к версии `VERIFIED`
|
||||
- **THEN** `--apply` отказывает с перечнем версий, база не изменена
|
||||
|
||||
#### Scenario: Очистка и повторная загрузка
|
||||
- **WHEN** очистка выполнена, ядро v5.19 выложено, аналитик загружает тот же PDF с ББ и ОФР
|
||||
- **THEN** дедуп не мешает (файла больше нет), формы создаются по мапперу 1.x или 2.0 — смотря что шлёт сервис
|
||||
|
||||
### Requirement: Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0
|
||||
`recognition_job.metrics.warnings` SHALL хранить предупреждения уровня файла структурами `{code, severity, message,
|
||||
locator?}` (`fileWarning` контракта): у `2.0` — как пришли, у `1.x` — строки, обёрнутые в `LEGACY_WARNING` `info`.
|
||||
`JobBrief.warnings` карточки файла (`FileOut.job`) SHALL отдавать список этих структур, а не строк; строки заданий,
|
||||
сохранённых до выкладки ядра v5.19, при чтении конвертируются в `{code: "LEGACY_WARNING", severity: "info", message:
|
||||
<строка>, locator: null}` (данные в БД не переписываются). `JobBrief.mapping_warnings` (предупреждения маппинга ядра)
|
||||
остаётся списком строк. Это несовместимое изменение типа поля для фронта — фиксируется в гайде фронта.
|
||||
|
||||
Предупреждения листа ОСВ результата `2.0` (`osvDocument.warnings`, коды `OSV_*`, `info`) SHALL храниться в
|
||||
`document_issue` документа ОСВ (вид `warning`, источник `service`) и отдаваться в `DocumentOut.issues[]` (те же поля,
|
||||
что `issues` формы) вместе с предупреждениями маппинга ядра этого листа; `DocumentOut.warnings` (строки маппинга файла)
|
||||
сохраняется на окно перехода. `osvDocument.status` SHALL использоваться: при приёме `2.0` документ ОСВ `RECOGNIZED` ⇔
|
||||
`osvDocument.status = done` ∧ `checks` листа без нарушений ∧ в `metrics.warnings` файла нет предупреждений уровня
|
||||
`error` ∧ `counterparty_check ≠ MISMATCH` ∧ нет предупреждений маппинга ядра по этому листу (повтор счёта в файле, строки
|
||||
без родителя, нарастающая по данным Q1); статус конверта в условие не входит (как Р-19 для форм). При приёме `1.x` — как
|
||||
прежде (§9.3).
|
||||
|
||||
#### Scenario: Карточка файла задания до выкладки
|
||||
- **WHEN** у задания, сохранённого до v5.19, `metrics.warnings = ["sheet=ОСВ 60 строки без родителя"]`
|
||||
- **THEN** `JobBrief.warnings = [{code: "LEGACY_WARNING", severity: "info", message: "sheet=ОСВ 60 строки без родителя",
|
||||
locator: null}]`
|
||||
|
||||
#### Scenario: Потерянный json в карточке файла
|
||||
- **WHEN** результат `2.0` scan-gateway несёт `metrics.warnings` с `JSON_FAILED`
|
||||
- **THEN** `JobBrief.warnings` содержит `{code: "JSON_FAILED", severity: "error", message, locator}`; предупреждение не
|
||||
теряется
|
||||
|
||||
#### Scenario: ОСВ в файле с ОФР, требующим проверки
|
||||
- **WHEN** книга из ОСВ (`status = done`, контрольные суммы сошлись) и ОФР (`needs_check`), конверт `needs_check`,
|
||||
предупреждений файла `error` нет
|
||||
- **THEN** ОСВ `RECOGNIZED` и автоподтверждается; её предупреждения `OSV_*` — в `DocumentOut.issues`
|
||||
|
||||
### Requirement: `crm_id` ГСК/ГСЗ
|
||||
`related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и
|
||||
`PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
|
||||
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
|
||||
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
|
||||
false` напрямую нет.
|
||||
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их
|
||||
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
|
||||
|
||||
#### Scenario: Догадка подтверждена проверкой
|
||||
- **WHEN** значение `is_recognized = false`, `needs_review = false`
|
||||
@ -33,7 +34,7 @@ false` напрямую нет.
|
||||
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
|
||||
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
|
||||
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате; снятие подтверждения ячейки и кода (`confirmed:
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed:
|
||||
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
|
||||
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
|
||||
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
|
||||
@ -48,3 +49,13 @@ SHALL быть отключена с подсказкой «Разберите
|
||||
#### Scenario: Подтверждение заблокировано
|
||||
- **WHEN** у формы две строки `missing` с суммами
|
||||
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
|
||||
|
||||
### Requirement: Предупреждения файла и документа в карточке файла
|
||||
Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}`
|
||||
(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать
|
||||
`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`.
|
||||
`job.mapping_warnings` остаётся списком строк.
|
||||
|
||||
#### Scenario: Потерянный ОФР в карточке
|
||||
- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]`
|
||||
- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`,
|
||||
`ruff format --check .`, `mypy src` (в сервисе с копией общего кода `arm_statements` — `mypy src arm_statements`, в
|
||||
`arm-core` — ещё `scripts`), `pytest -m "not integration"` (включая тесты копии `arm_statements/tests/`), тест
|
||||
идентичности вендоренного контракта с `../docs/contracts` (на окне перехода между `MAJOR` — со схемой того `MAJOR`,
|
||||
который сервис пишет). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать
|
||||
идентичности вендоренного контракта с `../docs/contracts` (схема копии — с `<имя>.v<M>.schema.json` своего `MAJOR`,
|
||||
если такой файл есть в docs, иначе с основным; примеры — с `examples/v<M>/`, если там есть одноимённый файл, иначе с
|
||||
`examples/`; правило — спека `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать
|
||||
запущенного Docker, PostgreSQL или NATS. Свежесть копии `arm_statements` относительно репозитория `arm-statements` в
|
||||
`make check` MUST NOT проверяться (соседний репозиторий в `make check` не читается).
|
||||
|
||||
|
||||
@ -54,7 +54,9 @@ v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `f
|
||||
`detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`,
|
||||
`is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у
|
||||
документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected,
|
||||
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`; у документа
|
||||
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` (`OTHER` в v2.0 не
|
||||
порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в
|
||||
`document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа
|
||||
без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе.
|
||||
|
||||
#### Scenario: Результат валидируется примерами
|
||||
@ -83,6 +85,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с
|
||||
`status` и `warnings`, файл проходит схему v2.0
|
||||
|
||||
#### Scenario: Документ OTHER в v2.0
|
||||
- **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant`
|
||||
- **THEN** файл не проходит схему v2.0
|
||||
|
||||
#### Scenario: Строка без кода с кодом в поле
|
||||
- **WHEN** у строки `line_status = unreadable` передан непустой `line_code`
|
||||
- **THEN** файл не проходит схему v2.0
|
||||
@ -115,10 +121,25 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
### Requirement: Копия контракта в сервисе идентична источнику
|
||||
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
|
||||
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
|
||||
На окне перехода между `MAJOR` тест идентичности SHALL сверять копию со схемой того `MAJOR`, который сервис пишет (или,
|
||||
у ядра, читает): копия с `$id`, оканчивающимся на `/v1`, — с `docs/contracts/<имя>.v1.schema.json`, иначе — с основным
|
||||
файлом `docs/contracts/<имя>.schema.json`; ядро держит копии обоих `MAJOR`. Правило теста вводится в `tabular-parser` и
|
||||
`scan-gateway` до слияния контракта v2.0 в docs, иначе их `make check` упадёт на окне перехода.
|
||||
Файл копии SHALL сверяться с файлом docs по правилу, одинаковому у ядра и сервисов и верному до, во время и после окна
|
||||
перехода между `MAJOR`:
|
||||
- **схема** `<имя>.schema.json` копии: `M` — номер из её `$id` (`…/v<M>`); пара — `docs/contracts/<имя>.v<M>.schema.json`,
|
||||
**если такой файл есть**, иначе основной `docs/contracts/<имя>.schema.json` (у `recognition-request` файла `.v1` нет
|
||||
— его копия `…/v1` сверяется с основным файлом; до слияния v2.0 в docs файлов `.v1` нет вовсе — все копии сверяются с
|
||||
основными);
|
||||
- **пример** `examples/<имя>.json` копии: вид — по префиксу имени (`request-` — запрос, `result-message-` — конверт,
|
||||
`result-` — файл результата), `M` — `MAJOR` копии схемы этого вида; пара — `docs/contracts/examples/v<M>/<имя>.json`,
|
||||
если такой файл есть, иначе `docs/contracts/examples/<имя>.json`;
|
||||
- **прочие файлы копии** (`<имя>.v<N>.schema.json`, `examples/v<N>/*.json` — их держит ядро на окне) — с одноимённым
|
||||
файлом docs; нет его в docs — расхождение;
|
||||
- **набор:** каждой схеме и каждому примеру верхнего уровня docs (`contracts/*.schema.json` без `.v<N>`,
|
||||
`contracts/examples/*.json`) SHALL соответствовать файл копии; файлы docs `*.v<N>.schema.json` и `examples/v<N>/`
|
||||
требуются в копии, только если копия держит этот `MAJOR` (ядро на окне); файлы другого `MAJOR` сервисом не
|
||||
сверяются.
|
||||
|
||||
Правило вводится в тест идентичности **всех трёх** потребителей (`arm-core`, `tabular-parser`, `scan-gateway`) до
|
||||
слияния контракта v2.0 в docs (шаг 0 выкладки); иначе их `make check` упадёт на окне перехода. Ядро держит копии обоих
|
||||
`MAJOR` с PR шага 3; при удалении `MAJOR` 1 (шаг 6) ядро сначала удаляет свои копии `.v1`, затем docs — файлы `.v1`.
|
||||
|
||||
#### Scenario: Копия устарела
|
||||
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
|
||||
@ -130,8 +151,28 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
окне перехода не краснеет
|
||||
|
||||
#### Scenario: Сервис перешёл на 2.0
|
||||
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`)
|
||||
- **THEN** тест сверяет её с основным файлом docs
|
||||
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`) и примеры до v2.0
|
||||
- **THEN** тест сверяет схему с основным файлом docs (файла `.v2` нет), примеры `result-*` — с `examples/*.json`;
|
||||
`examples/v1/` и `*.v1.schema.json` docs не сверяются
|
||||
|
||||
#### Scenario: Запрос v1.0 после слияния v2.0
|
||||
- **WHEN** в docs уже есть `recognition-result.v1.schema.json`, а `recognition-request.v1.schema.json` нет; копия
|
||||
запроса в сервисе — v1.0 (`$id` `…/v1`), примеры `request-image.json`, `request-tabular.json`
|
||||
- **THEN** копия запроса сверяется с основным `recognition-request.schema.json`, примеры запроса — с
|
||||
`examples/request-*.json` (в `examples/v1/` их нет); тест проходит
|
||||
|
||||
#### Scenario: Примеры прежнего MAJOR
|
||||
- **WHEN** копия `tabular-parser` — v1.2, в ней `examples/result-statement-excel.json` версии 1.2, а в docs
|
||||
`examples/result-statement-excel.json` уже 2.0, `examples/v1/result-statement-excel.json` — 1.2
|
||||
- **THEN** пример копии сверяется с `examples/v1/result-statement-excel.json` и проходит
|
||||
|
||||
#### Scenario: Шаг 0 до слияния docs
|
||||
- **WHEN** тест с новым правилом слит в `arm-core`, `tabular-parser` и `scan-gateway`, а docs ещё на v1.2 без файлов `.v1`
|
||||
- **THEN** все копии сверяются с основными файлами и примерами верхнего уровня, тест проходит, как прежде
|
||||
|
||||
#### Scenario: Ядро на окне перехода
|
||||
- **WHEN** копия ядра после шага 3 держит основные схемы v2.0, `*.v1.schema.json` и `examples/v1/`
|
||||
- **THEN** основные сверяются с основными docs, `.v1` и `examples/v1/` — с одноимёнными файлами docs; набор совпадает
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
@ -140,9 +181,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
|
||||
D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`,
|
||||
а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref =
|
||||
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения — по прежнему `values_ok`
|
||||
(`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при
|
||||
`period_date`, иначе `none`, `form_variant = null`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
|
||||
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения = **НЕ** `values_ok`
|
||||
(`values_ok` — `is_recognized` не `false` и `confidence` 100 или пуста: чистое значение — `needs_review = false`),
|
||||
`needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant` — по
|
||||
структуре кодов (точка входа пакета для пересчёта, спека `arm-core`), `file_ordinal` — индекс в `documents[]`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
|
||||
структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу —
|
||||
`document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0`
|
||||
только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих
|
||||
@ -150,8 +192,13 @@ null`, `line_code_original_value = line_code`, `text = string_value`, `needs_rev
|
||||
|
||||
#### Scenario: Сервис ещё на v1.2
|
||||
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
|
||||
- **THEN** ядро принимает его маппером `1.x`, обе строки кода — по прежнему правилу «остаётся первая» с
|
||||
`MAPPING_WARNING`, нарушения §7 — в `document_issue` от ядра, статус документа — по D-6, файл не `ERROR`
|
||||
- **THEN** ядро принимает его маппером `1.x`: из двух строк с одним кодом хранится первая (прежнее правило маппера
|
||||
`1.x`), вторая не хранится, у документа `MAPPING_WARNING` о повторе; нарушения §7 — в `document_issue` от ядра, статус
|
||||
документа — по D-6, файл не `ERROR`
|
||||
|
||||
#### Scenario: Чистое значение 1.x
|
||||
- **WHEN** в результате `1.2` значение с `confidence = 100` и `is_recognized = true`
|
||||
- **THEN** `needs_review = false`; значение с `confidence = 60` — `needs_review = true`
|
||||
|
||||
#### Scenario: Ack нового сервиса
|
||||
- **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"`
|
||||
|
||||
@ -43,4 +43,10 @@ document` — в шаги 2–6 пакета; `ocr_lookalikes.yaml` — в сл
|
||||
#### Scenario: Строки без кода в aep_3
|
||||
- **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |`
|
||||
- **THEN** строки попадают в результат со статусами (`detail` под строкой с кодом, `missing` в начале таблицы) и сырым
|
||||
текстом; счётчиков «строк без кода с суммами пропущено» нет
|
||||
текстом; значение `211(1 |` — `amount = null`, `string_value = "211(1 |"`, `is_recognized = false` (скобка внутри
|
||||
цифр, правило шага 4), а не 2111; счётчиков «строк без кода с суммами пропущено» нет
|
||||
|
||||
#### Scenario: Таблица ИС только с текстом
|
||||
- **WHEN** в json есть таблица без столбца кодов и без распознанного типа, в ячейках только текст подписей
|
||||
- **THEN** документа нет, в `metrics.warnings` — `TABLE_SKIPPED` уровня `info` с `locator` таблицы; конверт из-за неё
|
||||
не `needs_check`
|
||||
|
||||
@ -33,13 +33,24 @@ NOT делаться.
|
||||
Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C
|
||||
docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source <dir>`, ссылка —
|
||||
`origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir <dir>`. Без `--check` скрипт раскладывает копию коммита
|
||||
`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). `--check`
|
||||
SHALL возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`; `10` — копия
|
||||
равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала); `20` — копия отличается от дерева
|
||||
коммита из `VERSION`, `VERSION` нет в истории источника, копия перечня кодов в пакете отличается от
|
||||
`docs/contracts/statement-code-orders.yaml` или (только у `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
|
||||
отсутствует в перечне своей редакции; `2` — нет checkout источника или хоста, неизвестный аргумент. Файлы и коды
|
||||
расхождения SHALL печататься.
|
||||
`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). Копия
|
||||
сравнивается с деревом `arm_statements/` источника без файла `VERSION` (его в источнике нет). `--check` SHALL
|
||||
возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов
|
||||
в пакете равна `docs/contracts/statement-code-orders.yaml`; `10` — копия равна дереву коммита из `VERSION`, но в
|
||||
`--ref` есть более новый (копия отстала), или копия перечня кодов отличается от файла docs (перечень в docs правился
|
||||
раньше `arm-statements` или пересинхронизации хоста — предупреждение называет файл); `20` — копия отличается от дерева
|
||||
коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список `dependencies`
|
||||
его `pyproject`) отсутствуют в зависимостях хоста или (только у `arm-core`) код словаря имён
|
||||
`api/rules/statement_lines.yaml` отсутствует в перечне своей редакции **копии пакета**; `2` — нет checkout источника
|
||||
или хоста, неизвестный аргумент. Файлы и коды расхождения SHALL печататься. Порядок правки перечня кодов: PR в docs →
|
||||
PR в `arm-statements` (байтовая копия yaml) → `sync-statements` в хостах «сначала ядро»; до конца этой цепочки хосты
|
||||
получают `10`, а не `20`.
|
||||
|
||||
Код пакета SHALL проходить самый строгий конфиг `ruff` и `mypy` среди хостов (пакет проверяется им в своём
|
||||
репозитории): конфиг хоста применяется к копии, а правка копии под хост запрещена. Тесты пакета SHALL не зависеть от
|
||||
фикстур и `conftest` хоста: в `api/arm_statements/tests/` свой `conftest.py`, а `conftest` хоста MUST NOT содержать
|
||||
`autouse`-фикстур без ограничения каталогом `tests/` хоста. Зависимости пакета — только те, что есть в зеркале банка
|
||||
(сейчас PyYAML); хост держит их в своих зависимостях с версией зеркала.
|
||||
|
||||
Цель `statements-check` хоста (`sync-statements.sh --check --host-dir .`, docs — `../docs` или `ARM_KA_DOCS`) SHALL
|
||||
входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR;
|
||||
@ -58,6 +69,11 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat
|
||||
- **WHEN** рядом нет `arm-statements` и `--source` не задан
|
||||
- **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный
|
||||
|
||||
#### Scenario: Перечень кодов в docs новее копии
|
||||
- **WHEN** в docs в `statement-code-orders.yaml` добавлен код, а `arm-statements` и копии в хостах ещё не обновлены
|
||||
- **THEN** `--check` в каждом хосте завершается с кодом 10 и называет файл перечня; `make check-full` зелёный с
|
||||
предупреждением, посторонние PR в сервисы не краснеют
|
||||
|
||||
#### Scenario: Код словаря ядра вне перечня
|
||||
- **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет
|
||||
- **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию
|
||||
@ -138,7 +154,8 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat
|
||||
- **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false`
|
||||
|
||||
### Requirement: Шаг 1 — многостраничная форма
|
||||
Одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
|
||||
Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная
|
||||
шапка внутри таблицы ИС — сегмент шага 2). У Excel одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
|
||||
«Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL
|
||||
сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT`
|
||||
(`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
|
||||
@ -163,8 +180,8 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat
|
||||
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
|
||||
|
||||
#### Scenario: Год и сумма — не код
|
||||
- **WHEN** в ячейке «2024» или четырёхзначная сумма «3450»
|
||||
- **THEN** кодом это не считается (вне отрезков)
|
||||
- **WHEN** в ячейке «2024» или четырёхзначная сумма «1050» или «7450»
|
||||
- **THEN** кодом это не считается (вне отрезков); «3450» — код (отрезок 3100–3699 отчёта об изменениях капитала)
|
||||
|
||||
#### Scenario: Краевая пунктуация и черта
|
||||
- **WHEN** ячейка кода — `"1220 ,"` или `"| 140"`
|
||||
@ -191,10 +208,35 @@ SHALL возвращать: `0` — копия равна дереву `arm_stat
|
||||
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
|
||||
листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
|
||||
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
|
||||
столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с
|
||||
кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение
|
||||
`SEGMENT_COLUMNS_MISMATCH` (`error`) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение
|
||||
`SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, статусы по шагу 3).
|
||||
столбца первого сегмента, минимум один. Колонки документа (`columns[]`) SHALL задаваться первым сегментом; колонки
|
||||
любого следующего сегмента новых `columns[]` не создают, а сопоставляются с колонками первого сегмента:
|
||||
- сегмент со своей шапкой (повторная шапка OCR, лист-продолжение Excel со своей строкой заголовка) — по `period_date`,
|
||||
если у каждой колонки сегмента дата есть и совпадает с датой ровно одной колонки первого сегмента; иначе по порядку;
|
||||
- сегмент без шапки — колонки значений — столбцы правее кода, где в строках с кодами есть числа, сопоставление по
|
||||
порядку.
|
||||
|
||||
Значения сегмента несут `column_index` сопоставленной колонки первого сегмента. При сопоставлении по порядку число
|
||||
колонок сегмента не совпало с первым — предупреждение `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции в
|
||||
пределах меньшего числа колонок,
|
||||
лишние столбцы сегмента в `columns[]` не попадают, их номера в сетке перечислены в `message`. Столбец кодов не найден ни
|
||||
так, ни так — предупреждение `SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`,
|
||||
статусы по шагу 3).
|
||||
|
||||
#### Scenario: Лист-продолжение НПЗ со своей шапкой
|
||||
- **WHEN** у ББ НПЗ («Бухгалтерский баланс. Лист 4») своя строка заголовка, колонки периодов — в столбцах 8–10, у первого
|
||||
листа — в столбцах 16, 21, 24, даты подписей те же (30.09.2025, 31.12.2024, 31.12.2023)
|
||||
- **THEN** новых колонок нет: значения листа 4 получают `column_index` 16, 21, 24 по совпадению `period_date`;
|
||||
предупреждения нет, §7 ББ считается по одной колонке на дату, документ остаётся `done`
|
||||
|
||||
#### Scenario: Повторная шапка с другими подписями
|
||||
- **WHEN** повторная шапка второй страницы скана содержит две колонки без разобранной даты, у первого сегмента — две
|
||||
колонки с датами
|
||||
- **THEN** сопоставление по порядку, значения несут номера колонок первого сегмента, предупреждения нет
|
||||
|
||||
#### Scenario: Сегмент шире первого
|
||||
- **WHEN** у сегмента со своей шапкой четыре колонки периодов, у первого — три
|
||||
- **THEN** `SEGMENT_COLUMNS_MISMATCH` уровня `error`, значения первых трёх — по позиции, номер четвёртого столбца — в
|
||||
`message`
|
||||
|
||||
#### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
|
||||
- **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом
|
||||
@ -263,12 +305,25 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
|
||||
`column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`).
|
||||
|
||||
Подтверждение SHALL опираться только на уверенные источники, чтобы неуверенное не подтверждало само себя: для
|
||||
неуверенной подписи эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с
|
||||
уверенной подписью (`label_confidence = 100`); такой нет — подпись не подтверждена. Неуверенная дата отчёта (шаг 8, поля
|
||||
шапки) подтверждается только самой поздней абсолютной датой колонок с уверенной подписью. Для периода документа и
|
||||
относительных подписей эффективная дата считается, как выше, по всем источникам.
|
||||
|
||||
#### Scenario: Бланк 66н без впечатанных дат
|
||||
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
|
||||
предшествующего предыдущему», дата отчёта шапки 30.09.2025
|
||||
- **THEN** `period_date` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`,
|
||||
`relative:prev_year_end`, `relative:prev_prev_year_end`; предупреждений о дате колонок нет
|
||||
|
||||
#### Scenario: Неуверенная подпись не подтверждает сама себя
|
||||
- **WHEN** даты отчёта в шапке нет, единственная колонка с абсолютной датой — подпись OCR «На 30.09.2О25» с уверенностью
|
||||
60, остальные колонки без подписи
|
||||
- **THEN** эффективная дата для подтверждения строится без этой подписи — уверенных источников нет, подпись не
|
||||
подтверждена: `COLUMN_DATE_UNCERTAIN` уровня `error`; период документа (`period_known`) по-прежнему определяется этой
|
||||
датой
|
||||
|
||||
#### Scenario: Мусорная подпись над числами
|
||||
- **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы
|
||||
- **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
|
||||
@ -306,16 +361,20 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
### Requirement: Шаг 3 — строки данных и их статусы
|
||||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть
|
||||
хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или
|
||||
непуста с уверенностью ниже 100. Строка только с текстом наименования — не строка данных. У строки — `row_index` (порядковый номер в документе с 0,
|
||||
непуста с уверенностью ниже 100, или её текст — «цифра под видом буквы»: после замены двойников без условия «вплотную к
|
||||
цифре» в нём есть цифра и нет других букв (`'I'`, `'| О'`). Строка только с текстом наименования — не строка данных;
|
||||
текст из настоящих букв в колонке значений при уверенности 100 (подписи «Руководитель … Косарев», «(расшифровка
|
||||
подписи)» под формой Excel) содержимым не считается — осознанное исключение из «ничего не выбрасывать молча». У строки — `row_index` (порядковый номер в документе с 0,
|
||||
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>` по правилу шага 1, без столбца кодов — первой непустой
|
||||
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
|
||||
Статусы:
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой;
|
||||
- `detail` — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа);
|
||||
`parent_ref` — `ref` ближайшей строки с кодом выше;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки роли `title`
|
||||
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр (в том числе только из
|
||||
букв-двойников вне соседства с цифрой: «изо :», «}», «:») считается пустой;
|
||||
- `detail` — строка без кода с содержимым, над которой (в том же или в предыдущих сегментах документа) есть строка `ok`;
|
||||
`parent_ref` — `ref` ближайшей строки `ok` выше (строки `unreadable` / `missing` между ними пропускаются);
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok`; строки роли `title`
|
||||
сюда не попадают.
|
||||
|
||||
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
|
||||
@ -334,6 +393,20 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
|
||||
- **THEN** строка `detail`, `parent_ref` = `ref` строки 1150
|
||||
|
||||
#### Scenario: Расшифровка под строкой-мусором
|
||||
- **WHEN** порядок строк: 1150 (`ok`), «ИЧ,250» (`unreadable`), строка без кода с суммой
|
||||
- **THEN** последняя — `detail` с `parent_ref` строки 1150 (ближайшая строка `ok`, `unreadable` пропускается)
|
||||
|
||||
#### Scenario: Код из букв-двойников без цифр (aep_3)
|
||||
- **WHEN** ячейка кода — `"изо :"`, в колонке значений `'3'` с уверенностью 0, выше есть строки `ok`
|
||||
- **THEN** цифр в ячейке кода нет — она пустая, строка `detail`; если это потерянная строка 1130, её сумма не войдёт в
|
||||
1100, и соотношение `1100 = …` подсветит раздел (цена правила, S-61)
|
||||
|
||||
#### Scenario: Цифра под видом буквы (aep_6)
|
||||
- **WHEN** в строке без кода под строкой `ok` единственная непустая ячейка значений — `'I'` с уверенностью 100
|
||||
- **THEN** строка данных `detail` (раньше терялась молча); значение `amount = null`, `string_value = "I"`,
|
||||
`is_recognized = false`
|
||||
|
||||
#### Scenario: Потерянный код в начале таблицы
|
||||
- **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
|
||||
- **THEN** строка `missing`
|
||||
@ -365,14 +438,25 @@ k-я из непустых частей, если их тоже N, иначе ц
|
||||
- `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` =
|
||||
текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`);
|
||||
- `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||||
- `value` нет → `parse_amount(text, source)`: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак,
|
||||
скобки → минус, `}` и `]` — закрывающая скобка; прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
|
||||
`is_recognized = true`; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
|
||||
- `value` нет → `parse_amount(text, source)` по стадиям:
|
||||
1. пусто или прочерк (`-`, `–`, `—`, `−` без цифр) → `amount = null`, `is_recognized = true`; многострочное значение
|
||||
в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||||
2. крайние `|` (след границы таблицы) удаляются до замены двойников, а не становятся единицей (`"|122477"` → 122477);
|
||||
затем буквы-двойники вплотную к цифре → цифры (Р-11); и то и другое — исправление (`is_recognized = false`);
|
||||
3. **ядро числа** — участок от первого из символов «цифра, `(`, `-`, `−`, `+`» до последнего из «цифра, `)`, `}`,
|
||||
`]`»; всё вне ядра — **краевой мусор** (`|`, `:`, `=`, `■`, `•`, кавычки, точки, `{`, `[` и любые другие символы):
|
||||
выбрасывается, а если среди выброшенного есть что-то кроме пробелов — `is_recognized = false`;
|
||||
4. в ядре допустимы только цифры, пробелы (в том числе NBSP и тонкие), `,`, `.`, знак (`-`, `−` как минус, `+`) в
|
||||
начале и одна пара скобок вокруг всего числа: `(` первым символом и `)`, `}` или `]` последним (`}` / `]` —
|
||||
исправление); `{` и `[` открывающей скобкой не считаются. Иначе — буква в ядре (после стадии 2), скобка без пары
|
||||
или не на краю ядра, знак не в начале, любой другой символ между цифрами (`:`, `/`, `=`, `|`) — `amount = null`,
|
||||
`string_value` = текст, `is_recognized = false`;
|
||||
5. цифр нет вовсе → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||||
6. скобки → минус; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
|
||||
тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`,
|
||||
`string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель
|
||||
тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе
|
||||
`null`) и `is_recognized = false`; многострочное значение в строке с однострочным кодом → `amount = null`,
|
||||
`string_value` = текст, `is_recognized = false`;
|
||||
`null`) и `is_recognized = false`;
|
||||
- `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или
|
||||
`]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит;
|
||||
- позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence`
|
||||
@ -389,6 +473,24 @@ k-я из непустых частей, если их тоже N, иначе ц
|
||||
- **WHEN** ячейка OCR — `"(7744536}"`
|
||||
- **THEN** `amount = -7744536`, `is_recognized = false`
|
||||
|
||||
#### Scenario: Скобка внутри цифр (aep_3)
|
||||
- **WHEN** ячейка OCR — `"211(1 |"`
|
||||
- **THEN** крайняя `|` — краевой мусор; в ядре `211(1` скобка без пары и не на краю → `amount = null`, `string_value =
|
||||
"211(1 |"`, `is_recognized = false`
|
||||
|
||||
#### Scenario: Буква в ядре числа (aep_6)
|
||||
- **WHEN** ячейка OCR — `"(Ю4)"`
|
||||
- **THEN** «Ю» — не двойник и стоит в ядре → `amount = null`, `string_value = "(Ю4)"`, `is_recognized = false` (не −4)
|
||||
|
||||
#### Scenario: Краевой мусор с двух сторон
|
||||
- **WHEN** ячейка OCR — `": : 2856983 = ■"` или `"| : 122477"`
|
||||
- **THEN** `amount = 2856983` и `amount = 122477`, `is_recognized = false` (выброшен мусор вне ядра)
|
||||
|
||||
#### Scenario: Фигурная скобка не открывает
|
||||
- **WHEN** ячейка OCR — `"{38378)"`
|
||||
- **THEN** `{` — краевой мусор, в ядре `38378)` закрывающая скобка без пары → `amount = null`, `string_value = "{38378)"`,
|
||||
`is_recognized = false`
|
||||
|
||||
#### Scenario: Запятая по источнику
|
||||
- **WHEN** текст `"4,200"`
|
||||
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
|
||||
@ -428,11 +530,14 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code`
|
||||
при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца
|
||||
кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND`
|
||||
уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`,
|
||||
`locator` таблицы), у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`);
|
||||
документа в обоих случаях нет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
|
||||
`locator` таблицы), таблица ИС без содержимого — предупреждение **файла** `TABLE_SKIPPED` (`info`, `locator` таблицы);
|
||||
у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех
|
||||
этих случаях нет. Содержимое таблицы ИС — хотя бы одна ячейка с цифрой или с «цифрой под видом буквы» (шаг 3); таблица
|
||||
только из текста (подписи, пояснительный текст) содержимого не имеет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
|
||||
не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`.
|
||||
|
||||
`form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||||
`form_variant` SHALL определяться только по структуре кодов всех строк `ok` документа (независимо от уверенности
|
||||
кода; у ядра — заново при каждом пересчёте после правки, точка входа ниже): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||||
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
|
||||
признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле,
|
||||
`confidence` → `<поле>_confidence`.
|
||||
@ -500,16 +605,21 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
|
||||
|
||||
### Requirement: Шаг 8 — `needs_review` значения
|
||||
Сервис SHALL вычислять `needs_review` у каждого значения:
|
||||
Сервис SHALL вычислять `needs_review` у каждого значения строк `ok`, `unreadable` и `missing`. У значений строк
|
||||
`detail` `needs_review = false` всегда (Р-23): у них нет кода, покрыть их соотношением нельзя, в проверки §7, агрегаты и
|
||||
статус документа они не входят; сомнение остаётся видно в подсказке ячейки по `text` и `confidence`. Для остальных
|
||||
значений:
|
||||
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
|
||||
нуждаются;
|
||||
- значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
|
||||
посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без
|
||||
«округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких
|
||||
соотношений нет);
|
||||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус,
|
||||
без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`,
|
||||
`is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён;
|
||||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака совпадает с формой
|
||||
строгого большинства остальных членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без
|
||||
правки аналитика (сама ячейка в голосование не входит); форма знака — по результату разбора `parse_amount`: «скобки»
|
||||
(пара скобок, в том числе с `}` / `]`), «минус», «без знака»; голосующих нет или равенство голосов — знак не
|
||||
подтверждён;
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;
|
||||
- иначе `needs_review = false`.
|
||||
@ -524,9 +634,22 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
- **THEN** у 2120 знак подтверждён, `needs_review = false`
|
||||
|
||||
#### Scenario: Знак вне конвенции
|
||||
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"{38378)"` без минуса после разбора
|
||||
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"38378"` с уверенностью 60 (форма «без знака»), и `2100 = 2110 −
|
||||
2120` сходится по модулю
|
||||
- **THEN** `needs_review = true` у 2120 независимо от соотношения
|
||||
|
||||
#### Scenario: Скобка, которая не открывает
|
||||
- **WHEN** 2120 — `"{38378)"`
|
||||
- **THEN** разбор даёт `amount = null` (шаг 4), значение не покрыто, `needs_review = true`
|
||||
|
||||
#### Scenario: Расшифровки не держат документ (Р-23, РБ 1 кв. 2025)
|
||||
- **WHEN** в ББ и ОФР РБ «Бухгалтерская отчетность 1 кв.25» все неуверенные значения строк с кодом покрыты сошедшимися
|
||||
соотношениями, неуверенные коды подтверждены перечнем `fsbu4`, а неуверенные значения остались только у строк `detail`
|
||||
(в ОФР — `'1 425 130'` с уверенностью 78, `'134 796'` с уверенностью 71, `'(15 567)'` с уверенностью 75)
|
||||
- **THEN** у значений `detail` `needs_review = false`, они статус не держат; документ получает `done`, если выполнены
|
||||
остальные условия функции статуса (шапка, период, предупреждения); подсказка у ячеек `detail` показывает `text` и
|
||||
уверенность
|
||||
|
||||
#### Scenario: Справочная строка без проверки
|
||||
- **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении)
|
||||
- **THEN** `needs_review = true`
|
||||
@ -545,11 +668,15 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`,
|
||||
`fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы
|
||||
кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО).
|
||||
Четырёхзначные коды строк `ok` документа образуют последовательность; для **обеих** редакций берётся наибольшая
|
||||
подпоследовательность, согласованная с порядком перечня варианта формы (итоги после слагаемых; код с повтором может
|
||||
войти столько раз, сколько он стоит в перечне); редакция — с более длинной подпоследовательностью, при равенстве — по
|
||||
эффективной дате (`fsbu4` после 2024-12-31); уверенные коды участвуют как подтверждённые якоря; при нескольких
|
||||
наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён, если
|
||||
Четырёхзначные коды строк `ok` документа образуют последовательность; коды, которых нет в перечне редакции, в
|
||||
последовательность этой редакции не входят. Для **обеих** редакций берётся лучшая подпоследовательность, согласованная
|
||||
с порядком перечня варианта формы (итоги после слагаемых; код с повтором может войти столько раз, сколько он стоит в
|
||||
перечне); «лучшая» — лексикографически: сначала наибольшее число уверенных кодов (уверенные коды — якоря: включаются
|
||||
все, какие можно включить вместе), затем наибольшая длина. Якоря, противоречащие друг другу по порядку бланка, все
|
||||
вместе войти не могут — входит наибольшее согласованное их множество; неуверенный код проверяется против выбранного.
|
||||
Редакция — с лучшей подпоследовательностью (то же сравнение), при равенстве — по эффективной дате (`fsbu4` после
|
||||
2024-12-31). Неуверенный код подтверждён, только если он входит во **все** лучшие подпоследовательности выбранной
|
||||
редакции. Вариант формы для перечня — `form_variant` документа (шаг 6, по всем строкам `ok`). Пятизначный код подтверждён, если
|
||||
подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше,
|
||||
чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из
|
||||
перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review =
|
||||
@ -563,6 +690,11 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по
|
||||
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
|
||||
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check`
|
||||
|
||||
#### Scenario: Якоря противоречат друг другу
|
||||
- **WHEN** порядок кодов 1210 (уверенный), 1240 (уверенный), 1230 (уверенный), 1250 (уверенность 50)
|
||||
- **THEN** 1240 и 1230 вместе войти не могут; лучшие подпоследовательности — 1210, 1240, 1250 и 1210, 1230, 1250; код 1250
|
||||
входит в обе и подтверждён
|
||||
|
||||
#### Scenario: Законный повтор 1220 (Р-20, AURORA-1431)
|
||||
- **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность
|
||||
100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230
|
||||
@ -581,7 +713,9 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по
|
||||
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
|
||||
|
||||
### Requirement: Шаг 8 — поля шапки
|
||||
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе
|
||||
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок с
|
||||
уверенной подписью (`label_confidence = 100`; неуверенная подпись дату отчёта не подтверждает, требование «Шаг 2 —
|
||||
подпись и дата колонки»); иначе
|
||||
поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field =
|
||||
report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта
|
||||
структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH
|
||||
@ -590,9 +724,15 @@ report_date`). У бланка только с относительными по
|
||||
(подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют.
|
||||
|
||||
#### Scenario: Неуверенная, но верная дата
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок с уверенной подписью 30.09.2025
|
||||
- **THEN** `needs_review_fields` пуст, предупреждения о дате нет
|
||||
|
||||
#### Scenario: Неуверенные дата и подпись не подтверждают друг друга
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, единственная абсолютная подпись колонки «На 30.09.2025» — с
|
||||
уверенностью 60
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` и `COLUMN_DATE_UNCERTAIN` уровня `error`;
|
||||
снимаются подтверждением даты отчёта и даты колонки аналитиком
|
||||
|
||||
#### Scenario: Неуверенный год
|
||||
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
|
||||
@ -616,12 +756,13 @@ report_date`). У бланка только с относительными по
|
||||
|---|---|---|
|
||||
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
|
||||
| `TABLE_PARSE_ERROR` | error (файл) | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`); документа нет |
|
||||
| `TABLE_LOST` | error (файл) | таблица ИС с содержимым без колонки кодов и без `type_hint` |
|
||||
| `TABLE_LOST` | error (файл) | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без колонки кодов и без `type_hint` |
|
||||
| `TABLE_SKIPPED` | info (файл) | таблица ИС без содержимого (только текст), без колонки кодов и без `type_hint`; документа нет |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
|
||||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
|
||||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента при сопоставлении по порядку не совпали по числу с первым |
|
||||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты |
|
||||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента |
|
||||
@ -667,9 +808,10 @@ report_date`). У бланка только с относительными по
|
||||
проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`,
|
||||
`layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника,
|
||||
`row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения
|
||||
`{needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни предупреждений документа (у ядра —
|
||||
записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки и подтверждения аналитика
|
||||
(правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не учитываются. Статус ОСВ —
|
||||
`{line_status строки, needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни
|
||||
предупреждений документа (у ядра — записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки
|
||||
и подтверждения аналитика (правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не
|
||||
учитываются. Значения строк `detail` функция MUST NOT учитывать, даже если на входе у них `needs_review = true` (Р-23). Статус ОСВ —
|
||||
`done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔
|
||||
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
|
||||
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
|
||||
@ -691,3 +833,44 @@ report_date`). У бланка только с относительными по
|
||||
#### Scenario: Соседний документ не тянет файл
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
|
||||
- **THEN** конверт `needs_check`, у ББ `document.status = done`
|
||||
|
||||
#### Scenario: Сомнительная ячейка расшифровки
|
||||
- **WHEN** у формы все строки `ok` или `detail`, нарушений и `error` нет, единственное значение с уверенностью ниже 100 —
|
||||
в строке `detail`
|
||||
- **THEN** документ `done`
|
||||
|
||||
### Requirement: Точка входа пакета для пересчёта по сохранённым данным
|
||||
Пакет SHALL предоставлять функцию пересчёта формы по сохранённым данным без сетки и `FormCandidate` — ею ядро
|
||||
пересчитывает форму после PATCH и при приёме результата `1.x`, сервисы ею не пользуются. Вход — тип пакета
|
||||
`StoredForm`, собираемый хостом из своих данных:
|
||||
- `document_type`; `effective_date` (дата отчёта, иначе самая поздняя абсолютная дата колонок — у ядра
|
||||
`document.reporting_date`); `has_organization_or_inn`; `period_known`; `needs_review_fields` (как хранит ядро: поле
|
||||
уходит из него только правкой или подтверждением даты отчёта — сам пересчёт поля шапки не переоценивает); `accepted`;
|
||||
`warnings` — предупреждения документа `{code, severity}` (у ядра — записи `document_issue` вида `warning`);
|
||||
- `columns[] {key, column_index, period_date, period_kind}` (`key` — идентификатор хоста, у ядра `column_id`);
|
||||
- `lines[] {key, row_index | null, line_status, line_code, line_code_confidence, line_name, code_fixed, parent_key,
|
||||
values[] {column_key, amount, text, confidence, is_recognized, value_fixed}}`: `row_index = null` — строка, созданная
|
||||
правкой (участвует в §7, как строка `ok`, пока у неё есть сумма); `code_fixed` — код назначен, сменён или подтверждён
|
||||
аналитиком (`line_code_modified` или `is_confirmed` строки); `value_fixed` — значение правлено или подтверждено
|
||||
(`is_modified` или `is_confirmed`); `text` — сырой текст источника (`source_text`), по нему считается форма знака;
|
||||
у значения, созданного правкой, `text = null`.
|
||||
|
||||
Выход — `{form_variant, edition, checks {relations_failed, soft_relations_failed}, line_needs_review {key → bool},
|
||||
value_needs_review {key → bool}, detail_parents {key → parent_key | null}, status}`. Правила — те же шаги 6–8, что у
|
||||
сервисов, с поправками: `form_variant` определяется заново по кодам строк `ok` (код мог смениться правкой); `edition` —
|
||||
редакция с лучшей подпоследовательностью кодов документа по всем строкам `ok` (как в структурной проверке, при
|
||||
равенстве — по `effective_date`; кодов нет — по `effective_date`); `value_fixed` ⇒ `needs_review = false`, такое значение
|
||||
в голосование знака не входит; `code_fixed` ⇒ `needs_review` строки = `false`; значения строк `detail` — `false`;
|
||||
`accepted` ⇒ жёсткие нарушения статус не держат; `detail_parents` — ближайшая строка `ok` выше по `row_index` (строки
|
||||
`row_index = null` родителями не бывают), нет такой — `null`. Пересчёт не меняет `line_status`, суммы и коды — только
|
||||
выводит признаки.
|
||||
|
||||
#### Scenario: Пересчёт после правки по строкам БД
|
||||
- **WHEN** ядро передаёт форму: ячейка 1230 правлена (`value_fixed`), код 1250 назначен строке бывшей `unreadable`
|
||||
(`code_fixed`), шаблонная строка 1260 с суммой (`row_index = null`), строка `detail` с уверенностью 60
|
||||
- **THEN** `checks` посчитаны с 1250 и 1260 в `1200 = …`; у 1230 и у значения `detail` `needs_review = false`, у строки
|
||||
1250 `needs_review = false`; `status` — по общей функции статуса; `form_variant` и `edition` — по кодам после правки
|
||||
|
||||
#### Scenario: Смена кода меняет вариант
|
||||
- **WHEN** у упрощённого по структуре ББ (есть 1600, нет 1100 и 1200) аналитик назначает строке код 1100
|
||||
- **THEN** точка входа возвращает `form_variant = full`, нарушения считаются полным набором
|
||||
|
||||
@ -12,6 +12,7 @@ import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
@ -32,7 +33,7 @@ SCHEMAS = {
|
||||
EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")]
|
||||
|
||||
CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml"
|
||||
CODE_RE = re.compile(r"^\d{4,5}$")
|
||||
CODE_RE = re.compile(r"^\d{4}$") # в перечне только четырёхзначные коды (пятизначные — через родителя)
|
||||
|
||||
# Порядок важен: result-message-* проверяется раньше result-*.
|
||||
PATTERNS = [
|
||||
@ -150,10 +151,24 @@ def code_order_errors(data):
|
||||
for code in codes:
|
||||
if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first):
|
||||
errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы")
|
||||
repeats = {c for i, c in enumerate(codes) if c in codes[:i]}
|
||||
allowed = set(data.get("repeats", {}).get(edition, {}).get(form, []))
|
||||
if repeats - allowed:
|
||||
errors.append(f"{where}: повтор кода вне перечня repeats: {sorted(repeats - allowed)}")
|
||||
errors.extend(repeat_errors(where, codes, data.get("repeats", {}).get(edition, {}).get(form, []), variant))
|
||||
return errors
|
||||
|
||||
|
||||
def repeat_errors(where, codes, repeats, variant):
|
||||
"""Законные повторы (Р-20): в варианте full код встречается ровно 1 + (число его вхождений в repeats) раз;
|
||||
в simplified повторы допустимы только из repeats и не чаще, чем в full; код из repeats, которого нет в full, — ошибка."""
|
||||
errors = []
|
||||
counts = Counter(c for c in codes if isinstance(c, str))
|
||||
allowed = Counter(repeats)
|
||||
for code in sorted(set(counts) | set(allowed)):
|
||||
expected = 1 + allowed[code]
|
||||
actual = counts[code]
|
||||
if variant == "full":
|
||||
if actual != expected:
|
||||
errors.append(f"{where}: код {code} встречается {actual} раз, а по repeats должен {expected}")
|
||||
elif actual > 1 and actual > expected:
|
||||
errors.append(f"{where}: повтор кода {code} ({actual} раз) вне перечня repeats")
|
||||
return errors
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user