Compare commits

..

No commits in common. "6047677ed0dff9969b6667902947951a50696b09" and "ad1cd307a1065d2d810a698fc0ae5b0ee7ae7972" have entirely different histories.

17 changed files with 255 additions and 947 deletions

View File

@ -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 + 1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550` (жёсткое), `1600 = 1700` (жёсткое); ОФР — `2400 = 2110 − 2120 − 2330 +
2340 − 2350 − 2410` (**мягкое**, как `2400` полного набора: знак 2410 не определён, OQ-12; `abs_terms` те же; решение 2340 − 2350 − 2410` (**мягкое**, как `2400` полного набора: знак 2410 не определён, OQ-12; `abs_terms` те же; решение
владельца 18.10.2026, Р-22 / S-5). Оба набора — в одном словаре `statement_checks.yaml`. Без упрощённого набора полный владельца 18.10.2026, Р-22 / S-5). Оба набора — в одном словаре `statement_checks.yaml`. Без упрощённого набора полный
дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (документ, оставшийся в БД с времён до дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (результат 1.x, документ до v5.19) —
v5.19, если база не очищена) — полный набор; у результата 1.x вариант определяется по структуре кодов при приёме. полный набор.
- **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код - **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
из первых четырёх цифр (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code`. из первых четырёх цифр (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code`.
- **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже). - **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже).
@ -556,13 +556,10 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
**Ядро.** При загрузке результата v2.0 ядро §7 **не пересчитывает**: `checks`, `needs_review` и статус документа **Ядро.** При загрузке результата v2.0 ядро §7 **не пересчитывает**: `checks`, `needs_review` и статус документа
берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 копией берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 копией
`arm_statements` по текущим суммам (`financial_statement_value.amount` после правок) — с теми же правилами, что в `arm_statements` по текущим суммам (`financial_statement_value.amount` после правок) — с теми же правилами, что в
сервисах. Пересчёт идёт точкой входа пакета для пересчёта по сохранённым данным (спека `statement-normalization`): сервисах. Результат 1.x (окно выкладки) и формы, существовавшие до миграции v5.19, `checks` не несут: ядро **один раз**
она же заново определяет вариант формы и редакцию документа для проверок ядра. Результат 1.x (окно выкладки) `checks` считает §7 копией `arm_statements` (`form_variant = null` — полный набор) — при приёме 1.x и в миграции — и пишет
не несёт: ядро **один раз при приёме** считает §7 той же точкой входа (вариант — по структуре кодов) и пишет нарушения в нарушения в `document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из
`document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из этого этого расчёта. Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
расчёта. Формы, существовавшие до миграции v5.19, не пересчитываются: перед выкладкой база очищается от форм ББ/ОФР
(Р-24, §9.5 п. 2), а у формы, оставшейся в неочищенной базе, записей `document_issue` нет — до первой правки её
нарушения не показываются и подтверждение их не проверяет (корректность не гарантируется). Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
`document_issue` — у любой формы, какой бы версией контракта она ни пришла. Межформенная `1370(отчётная дата) − 1370(начало `document_issue` — у любой формы, какой бы версией контракта она ни пришла. Межформенная `1370(отчётная дата) − 1370(начало
года) = 2400` — **мягкая**, считается ядром по агрегату формы по коду (§9.5 п. 2) и не блокирует подтверждение: пара — года) = 2400` — **мягкая**, считается ядром по агрегату формы по коду (§9.5 п. 2) и не блокирует подтверждение: пара —
другой документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый другой документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый
@ -704,9 +701,7 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
результатов 1.x на окне выкладки). `RECOGNIZED` — только если одновременно: `document.status = done` из результата и он согласован с результатов 1.x на окне выкладки). `RECOGNIZED` — только если одновременно: `document.status = done` из результата и он согласован с
содержимым документа (нет строк `unreadable` / `missing` с суммой или текстом в значениях, нет строк, значений и полей содержимым документа (нет строк `unreadable` / `missing` с суммой или текстом в значениях, нет строк, значений и полей
шапки с `needs_review`, нет `checks.relations_failed` и предупреждений документа уровня `error`; иначе ядро считает шапки с `needs_review`, нет `checks.relations_failed` и предупреждений документа уровня `error`; иначе ядро считает
документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса; проверка — вызов документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса); в `metrics.warnings`
общей функции статуса §11.9 по входу, собранному из результата, а не пересчёт §7: значения строк `detail` в неё не
входят); в `metrics.warnings`
файла нет предупреждений уровня `error` (потерянный json, таблица ИС или исключение разбора таблицы — решение № 28 среза файла нет предупреждений уровня `error` (потерянный json, таблица ИС или исключение разбора таблицы — решение № 28 среза
3a; `needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены 3a; `needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены
`period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence `period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence
@ -717,12 +712,6 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
`RECOGNIZED` после правки — только вручную, как и прежде. Документ, принятый по 1.x, после правки пересчитывается так же `RECOGNIZED` после правки — только вручную, как и прежде. Документ, принятый по 1.x, после правки пересчитывается так же
(общим кодом), его `LEGACY_WARNING` и `MAPPING_WARNING` — `info` и статус не держат. (общим кодом), его `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; ### 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 мин. replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 20 мин (решение владельца 09.10.2026; note DBML «5 мин» — OQ-159), парсер 5 мин.
@ -762,10 +751,9 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
§9.3. §9.3.
**Формы по контракту v2.0** (change `unified-statement-normalization`; правила выше — для результатов 1.x на окне **Формы по контракту v2.0** (change `unified-statement-normalization`; правила выше — для результатов 1.x на окне
выкладки, у них поля v2.0 заполняются по умолчанию: `line_status = ok`, `row_index` по порядку строк результата с 0, выкладки, у них поля v2.0 заполняются по умолчанию: `line_status = ok`, `row_index` по порядку строк результата с 0,
`line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений = **НЕ** `line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений — по
«правлено или уверенность 100 и `is_recognized`» (чистое значение — `false`), `form_variant` — по структуре кодов, прежнему правилу «правлено или уверенность 100 и `is_recognized`», `form_variant = null` (§7 — полный набор),
`file_ordinal` — индекс в `documents[]`, `header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз `header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз копией `arm_statements` и пишет в
точкой входа `arm_statements` и пишет в
`document_issue` (`source = core`, §7 «Ядро»); предупреждения маппинга 1.x по документу (повтор кода, значение вне `document_issue` (`source = core`, §7 «Ядро»); предупреждения маппинга 1.x по документу (повтор кода, значение вне
колонок) — `document_issue` `MAPPING_WARNING` `info` (`source = core`; статус при приёме — по D-6, после правки они колонок) — `document_issue` `MAPPING_WARNING` `info` (`source = core`; статус при приёме — по D-6, после правки они
статус не держат); строки `metrics.warnings` 1.x — в `recognition_job.metrics.warnings` структурами `{code: статус не держат); строки `metrics.warnings` 1.x — в `recognition_job.metrics.warnings` структурами `{code:
@ -785,11 +773,7 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
RELATION_FAILED`, `error` или `kind = soft`, `code = RELATION_SOFT`, `info`, с `codes`, `relation`, `expected`, RELATION_FAILED`, `error` или `kind = soft`, `code = RELATION_SOFT`, `info`, с `codes`, `relation`, `expected`,
`actual`, `column_id`; предупреждение о столбце вне колонок формы — с `column_index`) вместе с проверками ядра уровня `actual`, `column_id`; предупреждение о столбце вне колонок формы — с `column_index`) вместе с проверками ядра уровня
`info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`); версия общего кода — `info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`); версия общего кода —
`recognition_job.statements_version`. **Каждый** документ результата (ОСВ, формы, `DISCLOSURE`, `OTHER`) при любом `recognition_job.statements_version`. Каждый `BALANCE` / `INCOME` становится отдельным документом с `file_ordinal`:
MAJOR получает `file_ordinal` — индекс в `documents[]` (несозданный документ номер не занимает и не сдвигает
остальные). Предупреждения файла хранятся в `recognition_job.metrics.warnings` структурами (у 1.x — `LEGACY_WARNING`)
и отдаются в `JobBrief.warnings` объектами (строки старых заданий конвертируются при чтении). Каждый `BALANCE` /
`INCOME` становится отдельным документом:
вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не
автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл. автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
Все чтения формы «по коду» (межформенная проверка, суммы формы для консолидации и выгрузок, `parent_code`, раскладка Все чтения формы «по коду» (межформенная проверка, суммы формы для консолидации и выгрузок, `parent_code`, раскладка
@ -797,13 +781,6 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
`amount` строк `ok` с кодом по колонке, список `line_id`, справочное имя (DECISION-5, не хранится). Строки `detail`, `amount` строк `ok` с кодом по колонке, список `line_id`, справочное имя (DECISION-5, не хранится). Строки `detail`,
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8. Записи `document_issue` удаляются вместе `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`), содержащие документ с теми же 3. **Автозамена.** После успешного маппинга другие файлы того же ЮЛ (не `SUPERSEDED`), содержащие документ с теми же
`document_type`, `osv_account_code`, `period_year`, `period_quarter`, переводятся в `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` (`superseded_by_file_id` = новый файл); `version_no` нового = максимум заменённых + 1. Новый файл без контекста (`consolidation_id IS NULL`) наследует `consolidation_id` первого (по `id`) заменённого файла, у которого он задан; файл, загруженный в консолидацию, сохраняет свою (v5.18). Заменяется и файл с `CONFIRMED`
@ -884,9 +861,7 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
`values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения (`is_confirmed`, `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения (`is_confirmed`,
`confirmed_by/at`, `needs_review = false`, `is_modified` не ставится) или снять подтверждение (`is_confirmed = false`, `confirmed_by/at`, `needs_review = false`, `is_modified` не ставится) или снять подтверждение (`is_confirmed = false`,
`needs_review` пересчитывается); `columns[] {column_id, period_date}` — дата колонке без подписи или с неуверенной `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, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить неуверенный код или снять подтверждение. расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить неуверенный код или снять подтверждение.
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail` Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
@ -894,8 +869,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` в PATCH принимает только исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` в PATCH принимает только
`"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный или `"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный или
подтверждённый код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`; подтверждённый код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`;
`line_code_original_value` не меняется (код из источника; у строк без кода — `null`); после правки родитель **всех** `line_code_original_value` не меняется (код из источника; у строк без кода — `null`); у `detail` родитель
строк `detail` пересчитывается как ближайшая строка `ok` выше по `row_index` (нет такой — `null`). Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может пересчитывается как ближайшая строка `ok` выше по `row_index`. Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL` совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL`
(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она (шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она
удаляется (её `id` убирается из `document_issue.line_ids`), назначенная строка источника занимает её место. Строки удаляется (её `id` убирается из `document_issue.line_ids`), назначенная строка источника занимает её место. Строки
@ -908,9 +883,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue` подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue`
видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица
§11.9; значения строк `detail` — без `needs_review`, Р-23). **Акцепт** сбрасывается правкой данных — суммы, кода или §11.9). **Акцепт** сбрасывается правкой данных — суммы, кода или статуса строки, даты колонки, поля шапки;
статуса строки, даты колонки, поля шапки; подтверждение ячейки или кода (`confirmed`) и подтверждение даты отчёта или подтверждение ячейки или кода (`confirmed`) суммы не меняет и акцепт не сбрасывает. Расхождение `statements_version`
колонки тем же значением суммы не меняют и акцепт не сбрасывают (решение владельца 18.10.2026, Р-25). Расхождение `statements_version`
задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH — задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH —
один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям). один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям).
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный 10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
@ -1231,14 +1205,9 @@ file:<имя json>;table:<i>`; документа у такой таблицы
ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из `segment_breaks`. У ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из `segment_breaks`. У
сегмента своя раскладка: по его шапке; без шапки — столбец кодов по п. 1, а если сегмент не первый и кодов меньше трёх — сегмента своя раскладка: по его шапке; без шапки — столбец кодов по п. 1, а если сегмент не первый и кодов меньше трёх —
ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (лист «_2» НПЗ ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (лист «_2» НПЗ
с единственной строкой 2400, у которого код в другой колонке). Колонки документа задаёт первый сегмент; колонки с единственной строкой 2400, у которого код в другой колонке). Колонки значений сегмента без шапки — столбцы правее
следующих сегментов новых колонок не создают, а сопоставляются с колонками первого: у сегмента **со своей шапкой** кода, где в строках с кодами числа, сопоставление с первым сегментом по порядку; число не совпало —
(повторная шапка OCR, лист-продолжение Excel со строкой заголовка — «Бухгалтерский баланс. Лист 4» НПЗ, колонки 8–10 `SEGMENT_COLUMNS_MISMATCH` (`error`), значения по позиции. Столбец кодов не найден ни так, ни так —
при 16 / 21 / 24 у первого листа) — по `period_date`, если у каждой колонки сегмента дата есть и совпадает ровно с
одной колонкой первого, иначе по порядку; у сегмента без шапки колонки значений — столбцы правее кода, где в строках с
кодами числа, по порядку. Значения несут `column_index` колонки первого сегмента. При сопоставлении по порядку другое
число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не
попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так —
`SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4). `SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4).
3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны 3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны
`dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025 `dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025
@ -1260,10 +1229,7 @@ file:<имя json>;table:<i>`; документа у такой таблицы
что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_date = null`, вид что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_date = null`, вид
остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в
дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка). Для этого подтверждения предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка).
эффективная дата строится **только из уверенных источников** — дата отчёта с уверенностью 100, иначе самая поздняя
абсолютная дата колонок с подписью уверенности 100; нет таких — подпись не подтверждена (неуверенное не подтверждает
само себя, S-64).
4. **Строка заголовка** — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя 4. **Строка заголовка** — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя
над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный
заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки
@ -1299,11 +1265,8 @@ file:<имя json>;table:<i>`; документа у такой таблицы
Строка данных — строка сетки с ролью `data`, у которой есть **содержимое**: в ячейке кода есть хотя бы одна цифра Строка данных — строка сетки с ролью `data`, у которой есть **содержимое**: в ячейке кода есть хотя бы одна цифра
(разобранный код или мусор вроде «44 30\nI |» aep_6), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), (разобранный код или мусор вроде «44 30\nI |» aep_6), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`),
или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3), или её текст — «цифра под видом или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3). Строка только с текстом
буквы» (после замены двойников без условия соседства есть цифра и нет других букв: `'I'` с уверенностью 100 в строке 38 наименования — не строка данных (заголовок раздела). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
aep_6). Строка только с текстом наименования — не строка данных (заголовок раздела); текст из настоящих букв в колонке
значений при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев») — тоже нет, осознанное исключение
(S-72). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
— строка данных: документ `needs_check` (не все строки `ok` / `detail`), но подтверждение она не блокирует — строка данных: документ `needs_check` (не все строки `ok` / `detail`), но подтверждение она не блокирует
(`LINES_UNRESOLVED` — только строки со значениями, §9.5 п. 4). У строки: `row_index` (номер в документе с (`LINES_UNRESOLVED` — только строки со значениями, §9.5 п. 4). У строки: `row_index` (номер в документе с
0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные 0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
@ -1311,14 +1274,13 @@ aep_6). Строка только с текстом наименования —
ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:** ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:**
- `ok` — код разобран; - `ok` — код разобран;
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «[190 :»); ячейка без - `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка
единой цифры («}», «:», «изо :» — двойники вне соседства с цифрой цифрами не считаются, S-61) считается пустой; без единой цифры («}», «:») считается пустой;
- `detail` (Р-8) — строка без кода с содержимым, над которой есть строка `ok` (в том же или в предыдущих сегментах — - `detail` (Р-8) — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах — родитель
родитель продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке», продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар
«Товар (41.01)»); родитель — ближайшая строка `ok` выше (`unreadable` / `missing` между ними пропускаются). Сервисы её (41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит; в проверки §7, агрегаты и `done` она
отдают, ядро хранит; в проверки §7, агрегаты, `needs_review` и `done` она не входит (у её значений `needs_review = не входит; экран по умолчанию не показывает;
false`, Р-23); экран по умолчанию не показывает; после правок ядро пересчитывает родителей всех строк `detail`; - `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом («Выручка» aep_3, где 2110
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok` («Выручка» aep_3, где 2110
уехал в ячейку значения); строки роли `title` сюда не попадают. уехал в ячейку значения); строки роли `title` сюда не попадают.
Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. **Счётчиков Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. **Счётчиков
@ -1352,16 +1314,10 @@ k-е части; уверенность — исходной ячейки. Зн
1. **`value` есть (Excel):** число → `amount` с квантованием до 2 знаков half-up, не больше 16 целых цифр (иначе `amount 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`, `VALUE_OUT_OF_RANGE` `info`); дата или логическое → `amount
= null`, `string_value` = текст, `is_recognized = false`. = null`, `string_value` = текст, `is_recognized = false`.
2. **`value` нет — `parse_amount(text, source)`**, один для обоих, по стадиям (S-57): (а) прочерк (`-`, `–`, `—`, `−` 2. **`value` нет — `parse_amount(text, source)`**, один для обоих: двойники (§11.3) и крайние `|`; пробелы, NBSP и тонкие
без цифр) и пусто → `amount = null`, `is_recognized = true` (OQ-148); (б) крайние `|` удаляются (до замены пробелы; минус «−» (U+2212) как знак; скобки → минус, `}` и `]` принимаются как закрывающая скобка (OQ-147:
двойников), затем двойники вплотную к цифре (§11.3); (в) **ядро числа** — от первого из «цифра, `(`, `-`, `−`, `+`» «(7744536}» → −7744536, `is_recognized = false`); прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
до последнего из «цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор (`:`, `=`, `■`, `•`, кавычки, `{`, `[` …), `is_recognized = true` (OQ-148). Разделители:
выбрасывается с `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` и удаления пробелов, с учётом знака и пары скобок на всю - `ocr`: запятая — тысячи, если весь текст (после `strip` и удаления пробелов, с учётом знака и пары скобок на всю
строку) — `[+-]?[1-9]\d{0,2}(,\d{3})+` (AURORA-1405, sg#35: `4,200` → 4200, `1,234,567` → 1234567); точка — тысячи строку) — `[+-]?[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); удаление при `[+-]?[1-9]\d*(\.\d{3})+` (sg#37: `837.994` → 837994, `1.234.567` → 1234567, `1 234.567` → 1234567); удаление
@ -1407,14 +1363,12 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
`type_hint = DISCLOSURE`: подпись «Пояснения» или имя листа «Расшифровка» над таблицей с кодами 1110–1700 не должны `type_hint = DISCLOSURE`: подпись «Пояснения» или имя листа «Расшифровка» над таблицей с кодами 1110–1700 не должны
превратить баланс в пустое пояснение — большинство кодов в отрезке ББ или ОФР даёт тип по кодам и `TYPE_CONFLICT` превратить баланс в пустое пояснение — большинство кодов в отрезке ББ или ОФР даёт тип по кодам и `TYPE_CONFLICT`
(`error`). Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с (`error`). Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с
`CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым (хотя бы одна ячейка с цифрой `CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым — предупреждение **файла**
или «цифрой под видом буквы») — предупреждение **файла** `TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`), `TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`), у Excel — лист «прочее», предупреждение **файла**
таблица только из текста — `TABLE_SKIPPED` (`info`, файл, S-71); у Excel — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа в обоих случаях нет. `DISCLOSURE` (по `type_hint` без
`SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех случаях нет. `DISCLOSURE` (по `type_hint` без
кодов форм или по кодам прочих форм) — шаги 3–7 не выполняются, `columns` и `lines` — пустые массивы (ядро строки кодов форм или по кодам прочих форм) — шаги 3–7 не выполняются, `columns` и `lines` — пустые массивы (ядро строки
пояснений не хранит), `checks` нет, документ `done`. пояснений не хранит), `checks` нет, документ `done`.
2. **Вариант формы** `form_variant` — только по структуре кодов всех строк `ok` (ядро определяет его заново при каждом 2. **Вариант формы** `form_variant` — только по структуре кодов (ОКУД 0710096 — код КНД комплекта, на бланках
пересчёте после правки; ОКУД 0710096 — код КНД комплекта, на бланках
0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или 0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или
2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` / `OTHER` — `null`. Шаг 7 и перечень 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` / `OTHER` — `null`. Шаг 7 и перечень
кодов берут набор варианта. Образец упрощённой формы — в fixtures при первой возможности (OQ-166). кодов берут набор варианта. Образец упрощённой формы — в fixtures при первой возможности (OQ-166).
@ -1432,9 +1386,7 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
### §11.9 Шаг 8 — `needs_review`, статус, предупреждения ### §11.9 Шаг 8 — `needs_review`, статус, предупреждения
**Подтверждение проверкой** (Р-5, Р-7, Р-11, Р-17, Р-21). Проверка §7 — арбитр, уверенность — подсказка там, где **Подтверждение проверкой** (Р-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, - Значение **нуждается в покрытии**, если `confidence < 100` (Р-5: у ИС уверенность дискретная — 100, ~80, 71, 67, 60,
50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false` 50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false`
@ -1444,13 +1396,11 @@ false` всегда (Р-23, решение владельца 18.10.2026): ко
допуска ±1, без «округления» (Р-21, решение владельца 18.10.2026). Мягким покрываются строки, у которых жёстких допуска ±1, без «округления» (Р-21, решение владельца 18.10.2026). Мягким покрываются строки, у которых жёстких
соотношений нет: 2400, 2410, 2411, 2412, 2460 (ОФР полного набора; `2400` упрощённого — тоже мягкое, S-5), 2510, 2520, соотношений нет: 2400, 2410, 2411, 2412, 2460 (ОФР полного набора; `2400` упрощённого — тоже мягкое, S-5), 2510, 2520,
2530, 2500 (`2500 = 2400 + 2510 + 2520 + 2530`), пятизначные подстроки («сумма подстрок = родитель»). 2530, 2500 (`2500 = 2400 + 2510 + 2520 + 2530`), пятизначные подстроки («сумма подстрок = родитель»).
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма знака совпадает с формой строгого - **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма записи знака (скобки, минус, без знака —
большинства **остальных** членов `abs_terms` той же колонки с непустым `amount`, `is_recognized = true` и без правки по `text`) совпадает с формой у большинства членов `abs_terms` той же колонки с непустым `amount`, `is_recognized =
аналитика (сама ячейка не голосует); форма знака — по результату разбора `parse_amount`: «скобки» (пара, в том числе с true` и без правки аналитика; не совпадает или сравнивать не с чем — не подтверждён (то же правило — при покрытии
`}` / `]`), «минус», «без знака»; голосующих нет или голоса поровну — не подтверждён (S-66; то же правило — при мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и 2220 той же
покрытии мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и колонки в скобках; «{38378)» при остальных в скобках — подсвечивается.
2220 той же колонки в скобках; сомнительное «38378» без знака при остальных в скобках — подсвечивается; «{38378)»
разбором даёт `null` (§11.5) и подсвечивается как непокрытое.
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех - `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой
текст и уверенность остаются в подсказке ячейки. Мягкое соотношение, не сошедшееся точно, подсветки не даёт. текст и уверенность остаются в подсказке ячейки. Мягкое соотношение, не сошедшееся точно, подсветки не даёт.
@ -1467,19 +1417,15 @@ false` всегда (Р-23, решение владельца 18.10.2026): ко
наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня — наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня —
`contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`): `contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`):
объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215, объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 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) — часть перечня упрощённые варианты — по бланку (OQ-166). **Законные повторы кода** (Р-20, решение владельца 18.10.2026) — часть перечня
(`repeats`): у `fsbu4` — 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощённой (`repeats`): у `fsbu4` — 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощённой
системе налогообложения» (печатная форма 1С, AURORA-1431), у `66n` — раздел III некоммерческой организации после раздела системе налогообложения» (печатная форма 1С, AURORA-1431), у `66n` — раздел III некоммерческой организации после раздела
III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность (коды, которых нет в перечне III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность; для **каждой** редакции перечня
редакции, в её последовательность не входят); для **каждой** редакции перечня (66н и ФСБУ 4 — всегда обе: Межениновская (66н и ФСБУ 4 — всегда обе: Межениновская за 3 кв. 2025 сдана на бланке 66н) — наибольшая подпоследовательность,
за 3 кв. 2025 сдана на бланке 66н) — **лучшая** подпоследовательность, согласованная с порядком перечня варианта формы согласованная с порядком перечня варианта формы документа (повтор допустим столько раз, сколько код стоит в перечне);
документа (повтор допустим столько раз, сколько код стоит в перечне): сначала наибольшее число уверенных кодов (якоря; редакция — с более длинной (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); уверенные коды —
противоречащие по порядку якоря все войти не могут — входит наибольшее согласованное множество), затем наибольшая подтверждённые якоря; при нескольких наибольших подпоследовательностях подтверждены только общие коды. Пятизначный код
длина; редакция — с лучшей (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); неуверенный код
подтверждён, только если входит во все лучшие подпоследовательности выбранной редакции (S-65). Вариант формы — по всем
строкам `ok` (§11.7); ядро пересчитывает вариант и проверку после правки кода. Пятизначный код
подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, которого **нет в перечне** подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, которого **нет в перечне**
(или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии (Р-16); (или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии (Р-16);
повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `needs_review` строки, повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `needs_review` строки,
@ -1488,20 +1434,15 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
наименования со справочным (§9.3). наименования со справочным (§9.3).
**Совпадение наименований** (ядро; S-4 с оговорками ревью, решение владельца 18.10.2026): обе строки — нижний регистр, **Совпадение наименований** (ядро; S-4 с оговорками ревью, решение владельца 18.10.2026): обе строки — нижний регистр,
`ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются, крайние обрезаются (`strip`); совпало, если строки `ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются; совпало, если строки равны, или более короткая (≥ 5
равны, или более короткая (≥ 5 символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым** символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым** справочным именем кода в любой
справочным именем кода в любой редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы — редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы — Р-20). Исключение: если хотя бы
Р-20). Исключение: если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера одно из сравниваемых имён начинается с «итого по разделу», совпадение — **только равенство** («итого по разделу i»
раздела** — начальные буквы `i` / `v` / `x` первого слова после фразы (кириллические «і», «х» приводятся к латинским, входит в «итого по разделу ii» и похоже на него — без исключения подмена 1100 ↔ 1200 ↔ 1300 не ловилась бы именем).
хвост слова отбрасывается: «Итого по разделу 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`) у строки. По Кода нет в словаре или наименование пусто — не совпало; несовпадение — `NAME_MISMATCH` (`info`) у строки. По
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя. `fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя.
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок **Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок;
с подписью уверенности 100 (неуверенная подпись дату отчёта не подтверждает, S-64);
иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал
бы неверный период и автоподтверждение. **У бланка 66н только с относительными подписями** («На отчётную дату», …) бы неверный период и автоподтверждение. **У бланка 66н только с относительными подписями** («На отчётную дату», …)
абсолютных дат колонок нет, и неуверенная дата отчёта структурой не подтверждается никогда — такой документ всегда абсолютных дат колонок нет, и неуверенная дата отчёта структурой не подтверждается никогда — такой документ всегда
@ -1522,15 +1463,14 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
|---|---|---|---| |---|---|---|---|
| `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка | | `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка |
| `TABLE_PARSE_ERROR` | error, файл | исключение при разборе таблицы ИС (`locator` таблицы; документа нет) | перезагрузка | | `TABLE_PARSE_ERROR` | error, файл | исключение при разборе таблицы ИС (`locator` таблицы; документа нет) | перезагрузка |
| `TABLE_LOST` | error, файл | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без столбца кодов и без типа | перезагрузка | | `TABLE_LOST` | error, файл | таблица ИС с содержимым без столбца кодов и без типа | перезагрузка |
| `TABLE_SKIPPED` | info, файл | таблица ИС только из текста без столбца кодов и без типа (документа нет) | — |
| `CODE_COLUMN_NOT_FOUND` | error | форма `BALANCE` / `INCOME` без столбца кодов | перезагрузка | | `CODE_COLUMN_NOT_FOUND` | error | форма `BALANCE` / `INCOME` без столбца кодов | перезагрузка |
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в т. ч. лист-продолжение) | перезагрузка | | `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в т. ч. лист-продолжение) | перезагрузка |
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | перезагрузка | | `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | перезагрузка |
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | перезагрузка | | `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | перезагрузка |
| `SEGMENT_COLUMNS_MISMATCH` | error | при сопоставлении по порядку число колонок сегмента не совпало с первым | перезагрузка | | `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым | перезагрузка |
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи | PATCH `columns[].period_date` | | `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 | заголовка нет ни у одного сегмента | перезагрузка | | `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента | перезагрузка |
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` | перезагрузка | | `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` | перезагрузка |
| `NO_ORGANIZATION_NO_INN` | error | нет ни организации, ни ИНН | PATCH `header.inn` / `organization_name` | | `NO_ORGANIZATION_NO_INN` | error | нет ни организации, ни ИНН | PATCH `header.inn` / `organization_name` |
@ -1560,9 +1500,8 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
**Статус документа** — одна функция общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧ период определён **Статус документа** — одна функция общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧ период определён
(дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей
шапки с `needs_review` (значения строк `detail` не учитываются, Р-23) ∧ нет жёстких нарушений §7 вне допуска и шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений документа уровня
округления ∧ нет предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках. Квартал периода,
раскладке, строках и проверках. Квартал периода,
сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ `document.status = done`, когда контрольные сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ `document.status = done`, когда контрольные
суммы §1.4 листа сошлись (§1.6, S-22), её предупреждения — `info`. суммы §1.4 листа сошлись (§1.6, S-22), её предупреждения — `info`.
@ -1570,23 +1509,19 @@ III (aep_3). Алгоритм: четырёхзначные коды строк
`document_type`; `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть хотя бы `document_type`; `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть хотя бы
одна строка из источника, `row_index` не `null`); `has_organization_or_inn`; `period_known` (дата отчёта или абсолютная одна строка из источника, `row_index` не `null`); `has_organization_or_inn`; `period_known` (дата отчёта или абсолютная
дата хотя бы одной колонки — у ядра `document.reporting_date` не `null`); строки `{line_status, needs_review}`; значения дата хотя бы одной колонки — у ядра `document.reporting_date` не `null`); строки `{line_status, needs_review}`; значения
`{line_status строки, needs_review}` (значения строк `detail` функция не учитывает); число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не `{needs_review}`; число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не
учитываются); уровни предупреждений документа (у ядра — записи `document_issue` вида `warning` с `severity = error`). учитываются); уровни предупреждений документа (у ядра — записи `document_issue` вида `warning` с `severity = error`).
`needs_review` строк и значений на входе уже учитывает подтверждения: у ядра правленое (`is_modified`, `needs_review` строк и значений на входе уже учитывает подтверждения: у ядра правленое (`is_modified`,
`line_code_modified`) или подтверждённое (`is_confirmed`) — `false`. Сервис собирает вход из `FormCandidate` и `line_code_modified`) или подтверждённое (`is_confirmed`) — `false`. Сервис собирает вход из `FormCandidate` и
результата шагов 2–7, ядро — из строк БД (после правки — точкой входа пакета для пересчёта по сохранённым данным, результата шагов 2–7, ядро — из строк БД; так один код даёт один статус.
спека `statement-normalization`; при приёме результата — для проверки согласованности `done`, §9.3); так один код даёт
один статус.
**Ожидание на реальных сканах** (`fixtures/ocr`, оценки ревью спеки 18.10.2026, K-4 и L-1): после Р-20 (повторы 1220 / **Ожидание на реальных сканах** (`fixtures/ocr`, оценка ревью спеки 18.10.2026, K-4): после Р-20 (повторы 1220 / 1520 /
1520 / 2410 в перечне `fsbu4` и синонимы в словаре имён), Р-21 (мягкие соотношения ОФР) и Р-23 (значения `detail` не 2410 в перечне `fsbu4` и синонимы в словаре имён) и Р-21 (мягкие соотношения ОФР) законные повторы и строки 24xx / 25xx
держат статус) законные повторы, строки 24xx / 25xx и ячейки расшифровок документ не держат. По прогону ревьюера ББ и перестают держать документ. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с суммами
ОФР РБ («Бухгалтерская отчетность 1 кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: все их неуверенные (aep_3), значения с уверенностью ниже 100 вне соотношений и в несошедшихся соотношениях, разлепленные строки без
значения строк с кодом покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали покрытия, неуверенная дата отчёта. Доля `done` на текущих шести документах ожидается небольшой; golden-эталоны
их только ячейки расшифровок. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с `fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля — в его
суммами (aep_3), несошедшиеся соотношения и мусор в значениях (aep_6: «(Ю4)» в 2350), неуверенная дата отчёта. описании.
Golden-эталоны `fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля
`done` — в его описании.
**Статус конверта:** `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; **Статус конверта:** `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`;
документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser — коды описывают документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser — коды описывают

View File

@ -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-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-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-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`) | открыт до появления образцов; **временное правило принято владельцем 18.10.2026 (Р-26):** коды без образца (2421 / 2430 / 2450 в `66n`, 1120 и другие в `fsbu4`) в перечень не входят; неуверенный такой код на скане не подтверждается структурой — `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`) | открыт: допущение — код без образца в перечень не входит; на скане такой неуверенный код не подтверждается структурой (`needs_review`, `needs_check`), у Excel и уверенного кода — только `info` ядра `CODE_NOT_IN_EDITION`. Пополнение — строкой в yaml с источником (файл fixtures) |

View File

@ -3,7 +3,7 @@
"$id": "arm-ka/recognition-result/v2", "$id": "arm-ka/recognition-result/v2",
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0", "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0",
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.", "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, statementDocument.form_variant обязателен, OTHER удалён из statementDocument.document_type. Схема 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. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.",
"type": "object", "type": "object",
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
"properties": { "properties": {
@ -57,7 +57,7 @@
}, },
{ {
"if": { "if": {
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"] } }, "required": ["document_type"] } } }, "properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] } }, "required": ["document_type"] } } },
"required": ["documents"] "required": ["documents"]
}, },
"then": { "then": {
@ -223,12 +223,12 @@
}, },
"statementDocument": { "statementDocument": {
"type": "object", "type": "object",
"required": ["document_type", "form_variant", "locator", "header", "columns", "lines", "status", "warnings"], "required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"],
"properties": { "properties": {
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE"], "description": "OTHER в v2.0 не порождается: лист «прочее» — предупреждение файла SHEET_OTHER, документа нет (значение OTHER — только в схеме 1.x)" }, "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
"locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] }, "locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] },
"status": { "$ref": "#/$defs/documentStatus" }, "status": { "$ref": "#/$defs/documentStatus" },
"form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов строк ok (§11); null — DISCLOSURE; обязателен" }, "form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов (§11); null — DISCLOSURE/OTHER" },
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } }, "warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
"header": { "header": {
"type": "object", "type": "object",

View File

@ -22,8 +22,7 @@
version: 1 version: 1
# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в # Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в
# перечне больше одного раза: в варианте full код стоит ровно 1 + (число его вхождений в этот список) раз, в simplified — # перечне больше одного раза.
# не чаще (сейчас повторов в simplified нет; validate_contracts.py проверяет оба правила).
repeats: repeats:
66n: 66n:
# раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих # раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих

View File

@ -79,20 +79,13 @@ sync-statements` и цель `statements-check` сервисов появляю
`--source <dir>`, ссылка — `origin/main` или `--ref <ref>`; хост — `--host arm-core|tabular-parser|scan-gateway` или `--source <dir>`, ссылка — `origin/main` или `--ref <ref>`; хост — `--host arm-core|tabular-parser|scan-gateway` или
`--host-dir <dir>`. Без `--check` — раскладывает копию нужного коммита в новый worktree хоста на ветке фичи и пишет `--host-dir <dir>`. Без `--check` — раскладывает копию нужного коммита в новый worktree хоста на ветке фичи и пишет
`VERSION`; issue и PR — как у `sync-web.sh`. `VERSION`; issue и PR — как у `sync-web.sh`.
- **`--check`, коды возврата** (файл `VERSION` в сравнении не участвует — в источнике его нет): `0` — копия равна дереву - **`--check`, коды возврата:** `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`;
`arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов равна docs; `10` — копия равна дереву `10` — копия равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала; предупреждение, не
коммита из `VERSION`, но в `--ref` есть более новый (копия отстала), или байтовая копия перечня кодов ошибка); `20` — копия отличается от дерева коммита из `VERSION` (прямая правка) или `VERSION` нет в истории источника,
`arm_statements/rules/statement_code_orders.yaml` отличается от `docs/contracts/statement-code-orders.yaml` (перечень в либо байтовая копия перечня кодов `arm_statements/rules/statement_code_orders.yaml` отличается от
docs правился раньше `arm-statements` или пересинхронизации хоста) — предупреждение, не ошибка; `20` — копия отличается `docs/contracts/statement-code-orders.yaml`, либо (только `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
от дерева коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список отсутствует в перечне своей редакции; `2` — нет checkout `arm-statements` или хоста, неизвестный аргумент. Файлы
`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` - **`statements-check` в `make check-full`:** `../docs/scripts/sync-statements.sh --check --host-dir .` (docs — `../docs`
или `ARM_KA_DOCS`); код `0` и `10` — цель зелёная (при `10` предупреждение печатается, в описание PR вписывается, что копия или `ARM_KA_DOCS`); код `0` и `10` — цель зелёная (при `10` предупреждение печатается, в описание PR вписывается, что копия
отстала), `20` и `2` — цель красная. В `make check` и в пайплайн платформы не входит: тест, читающий соседний отстала), `20` и `2` — цель красная. В `make check` и в пайплайн платформы не входит: тест, читающий соседний
@ -174,20 +167,16 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу
| Цель | Что делает | | Цель | Что делает |
|---|---| |---|---|
| `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` и `PyYAML` — зависимостями скриптов (после обновления репозитория — перезапустить) | | `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` (коды — 4 цифры формы; в варианте `full` код стоит 1 + (вхождений в `repeats`) раз, в `simplified` — не чаще); код 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`; код 1 при любой ошибке |
| `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) | | `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) |
| `make check` | обе проверки выше | | `make check` | обе проверки выше |
Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`. Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`.
Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их
нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. Правило теста идентичности (у ядра и нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. На окне перехода между MAJOR (сейчас 1 → 2)
сервисов одно, верно до, во время и после окна перехода между MAJOR — сейчас 1 → 2): схема копии с `$id` `…/v<M>` тест идентичности сервиса сверяет свою копию со схемой **того MAJOR, который сервис пишет**: копия с `$id` `…/v1` — с
сверяется с `<имя>.v<M>.schema.json`, **если такой файл есть в docs**, иначе с основным (у `recognition-request` файла `*.v1.schema.json`, иначе — с основным файлом (`openspec/specs/recognition-contract`, требование «Копия контракта в
`.v1` нет — копия v1.0 сверяется с основным); пример `examples/<имя>.json` копии — с `examples/v<M>/<имя>.json`, если он сервисе идентична источнику»). Ядро держит копии обоих MAJOR. Перечень кодов `contracts/statement-code-orders.yaml`
есть, иначе с `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`). копируется не в сервисы, а в пакет `arm_statements` (сверяет `sync-statements --check`).
## Порядок перед PR ## Порядок перед PR

View File

@ -416,9 +416,8 @@ UI прячет элементы по `GET /me → permissions` (объедин
> **Пока не работает на `main`.** Раздел описывает API после change `unified-statement-normalization` > **Пока не работает на `main`.** Раздел описывает API после change `unified-statement-normalization`
> (RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19; дочерние PR arm-core и arm-web ещё не созданы). > (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. > Правила — `context/05-domain-rules.md` §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спека `arm-web` того же change.
> В `…/statement` поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. > Поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. После слияния PR раздел
> **Исключение — карточка файла:** `FileOut.job.warnings` меняет тип со строк на объекты (ниже, «Карточка файла»), это > переедет в §5.6 и §1.
> несовместимо — экран файлов обновляется в той же выкладке, что и ядро. После слияния PR раздел переедет в §5.6 и §1.
**`GET /documents/{id}/statement` — новые поля `StatementOut`:** **`GET /documents/{id}/statement` — новые поля `StatementOut`:**
@ -439,9 +438,7 @@ UI прячет элементы по `GET /me → permissions` (объедин
словарь «код → строка», ключ строки — `id` (у достроенных шаблонных — `line_code`, их у кода с реальной строкой `ok` нет). словарь «код → строка», ключ строки — `id` (у достроенных шаблонных — `line_code`, их у кода с реальной строкой `ok` нет).
- `values[]` — `text` (сырой текст источника; в подсказку ячейки вместе с `confidence`), `needs_review`, `is_confirmed`. - `values[]` — `text` (сырой текст источника; в подсказку ячейки вместе с `confidence`), `needs_review`, `is_confirmed`.
**Подсветка ячейки — только по `needs_review`**; по `confidence < 100` и `is_recognized = false` больше не подсвечивать **Подсветка ячейки — только по `needs_review`**; по `confidence < 100` и `is_recognized = false` больше не подсвечивать
(сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается). У значений (сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается).
строк `detail` `needs_review` всегда `false` (расшифровки статус не держат) — подсказку с `text` и уверенностью
показывать и у них.
- `issues[]` — предупреждения и нарушения документа: `{id, kind: "warning" | "relation" | "soft", code, severity: "error" | - `issues[]` — предупреждения и нарушения документа: `{id, kind: "warning" | "relation" | "soft", code, severity: "error" |
"info", message, line_ids[], column_id?, column_index?, field?, source: "service" | "core", relation?, codes?, expected?, "info", message, line_ids[], column_id?, column_index?, field?, source: "service" | "core", relation?, codes?, expected?,
actual?}`; показывать у места (строки, ячейки, поле шапки) или у документа; `error` отличать от `info`. `column_index` без actual?}`; показывать у места (строки, ячейки, поле шапки) или у документа; `error` отличать от `info`. `column_index` без
@ -459,7 +456,7 @@ UI прячет элементы по `GET /me → permissions` (объедин
| Часть | Тело | Эффект | Ошибки | | Часть | Тело | Эффект | Ошибки |
|---|---|---|---| |---|---|---|---|
| подтвердить ячейку / снять подтверждение | `values: [{line_id, column_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review = false` (при `false` — пересчитывается), сумма не меняется, акцепт не сбрасывается | 422 `UNKNOWN_CELL` | | подтвердить ячейку / снять подтверждение | `values: [{line_id, column_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review = false` (при `false` — пересчитывается), сумма не меняется, акцепт не сбрасывается | 422 `UNKNOWN_CELL` |
| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа, `period_kind = absolute` (правка даты отчёта её больше не пересчитывает); снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN`; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) | 422 `UNKNOWN_COLUMN`; `period_date: null` — 422 массивом | | дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа; снимает `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_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, 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` — пересчитывается) | — | | подтвердить код / снять подтверждение | `lines: [{line_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review` строки = `false` (при `false` — пересчитывается) | — |
@ -479,17 +476,9 @@ line_code_text}` — строки `unreadable` / `missing`, у которых в
(`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED`. (`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED`.
**Статус документа.** Форма из OCR может стать `RECOGNIZED` (и сразу автоподтвердиться), если сервис всё подтвердил **Статус документа.** Форма из OCR может стать `RECOGNIZED` (и сразу автоподтвердиться), если сервис всё подтвердил
проверками: сомнительные ячейки расшифровок (`detail`) статус не держат, поэтому чистые сканы 1С (например, ББ и ОФР за проверками; на текущих реальных сканах это редкость — `NEEDS_CHECK` остаётся, когда есть что смотреть: подсвеченные ячейки
1 кв. 2025 в fixtures) могут прийти `RECOGNIZED`. `NEEDS_CHECK` остаётся, когда есть что смотреть: подсвеченные ячейки и и строки, `issues` уровня `error`, неразобранные строки. Две формы одного типа в одном файле приходят двумя документами;
строки, `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}` сохраняется и возвращается в **ГСК/ГСЗ.** `crm_id` в `POST …/related-groups` и `PATCH /directories/related-groups/{id}` сохраняется и возвращается в
`RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409 `CRM_ID_EXISTS` (arm-core#112, DBML v5.19). `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409 `CRM_ID_EXISTS` (arm-core#112, DBML v5.19).
@ -744,7 +733,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
| Задача | Что поменяется для фронта | Как подготовиться | | Задача | Что поменяется для фронта | Как подготовиться |
|---|---|---| |---|---|---|
| OQ-54 — SSO РАИСА | Меняется только получение токена (вместо dev-формы) | держать слой получения токена изолированным (`frontend-auth-guide.md` §9) | | 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` ГСК/ГСЗ сохраняется; **`FileOut.job.warnings` — объекты вместо строк**, `DocumentOut.issues[]` | §5.6.1; не держать словарь «код → строка», подсветку строить по `needs_review`; тип `JobBrief.warnings` в `types/api.ts` поменять вместе с выкладкой ядра | | 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` |
Состояние этих задач — в Gitea (`RSHB/arm-core`, `RSHB/arm-ka-docs`); после слияния каждой этот раздел переезжает в §1. Состояние этих задач — в Gitea (`RSHB/arm-core`, `RSHB/arm-ka-docs`); после слияния каждой этот раздел переезжает в §1.

View File

@ -25,8 +25,7 @@
// 6. recognition_job.statements_version — коммит копии общего кода arm_statements, которым посчитан результат. // 6. recognition_job.statements_version — коммит копии общего кода arm_statements, которым посчитан результат.
// 7. document: уникальность (stored_file_id, document_type, osv_account_code) — только для типов кроме BALANCE / INCOME // 7. document: уникальность (stored_file_id, document_type, osv_account_code) — только для типов кроме BALANCE / INCOME
// (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа // (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа
// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — индекс документа в documents[] результата (у всех // в файле хранится (Р-10, Р-15); новая колонка file_ordinal — порядковый номер документа в результате, уникален в файле.
// документов при любом MAJOR контракта; несозданный документ номер не занимает), уникален в файле.
// 8. related_group.crm_id (varchar, nullable, unique) — ID ГСК/ГСЗ в ЕЦП CRM (AURORA-1619, arm-core#112, OQ-33). // 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 (заменяет прежний // 9. Признак «строка создана правкой аналитика» — financial_statement_line.row_index IS NULL (заменяет прежний
// признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет). // признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет).
@ -37,20 +36,16 @@
// правится PATCH шапки, как ИНН). // правится PATCH шапки, как ИНН).
// 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок // 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок
// формы — COLUMN_SKIPPED и др.). // формы — COLUMN_SKIPPED и др.).
// Перед выкладкой база очищается от форм ББ/ОФР (решение владельца 18.10.2026, Р-24; скрипт ядра // Заполнение при миграции: row_index строк — NULL у строк с line_code_original_value IS NULL (созданных правкой), у
// scripts/purge_statements.py): миграция только добавляет колонки, таблицу, enum'ы, индексы и значения по умолчанию, // остальных — по возрастанию line_code внутри формы с 0; line_status = ok, needs_review строк = false, needs_review
// §7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): row_index строк — NULL у // значений — по прежнему правилу values_ok (правлено или is_recognized не false и confidence 100 / NULL), source_text =
// строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию line_code внутри формы с 0; // string_value, is_confirmed = false, period_kind = absolute при period_date, иначе none, form_variant = NULL (набор §7 —
// line_status = ok, needs_review строк = false, needs_review значений = НЕ values_ok (values_ok — правлено или // полный), header_source — из *_original_value и inn_confidence, organization_name_original_value = organization_name,
// is_recognized не false и confidence 100 / NULL), source_text = string_value, is_confirmed = false, period_kind = // file_ordinal — по (created_at, id) внутри файла с 0, related_group.crm_id = NULL; document_issue каждой формы —
// absolute при period_date, иначе none, form_variant = NULL (набор §7 — полный), header_source — из *_original_value // нарушения §7, посчитанные копией arm_statements по текущим суммам (source = core, kind relation / soft).
// и inn_confidence, organization_name_original_value = organization_name, file_ordinal — по (created_at, id) внутри // Откат гарантирован на пустой БД и на данных, совместимых с v5.18 (нет повторов кода в форме, строк без кода, двух
// файла с 0 (у всех документов), related_group.crm_id = NULL; document_issue — пуст: корректность нарушений у форм, // форм одного типа в файле, crm_id групп); на несовместимых данных downgrade останавливается с ошибкой и ничего не
// оставшихся в неочищенной базе, не гарантируется до первой правки. // удаляет молча.
// Откат гарантирован на пустой БД и на данных, которые 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): // v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files):
// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте // 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте
@ -852,7 +847,7 @@ Table document {
confirmed_at timestamptz confirmed_at timestamptz
created_at timestamptz created_at timestamptz
updated_at timestamptz updated_at timestamptz
file_ordinal smallint [not null, default: 0, note: 'v5.19: индекс документа в documents[] результата распознавания (с 0), у всех документов при любом MAJOR контракта; несозданный документ номер не занимает (номера с пропусками); различает две формы одного типа в файле (Р-10)'] file_ordinal smallint [not null, default: 0, note: 'v5.19: порядковый номер документа в результате распознавания (с 0); различает две формы одного типа в файле (Р-10)']
Indexes { 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, 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: номер документа уникален в файле'] (stored_file_id, file_ordinal) [unique, note: 'v5.19: номер документа уникален в файле']
@ -1090,7 +1085,7 @@ Table document_issue {
actual numeric(18,2) [note: 'для relation / soft: правая часть'] actual numeric(18,2) [note: 'для relation / soft: правая часть']
created_at timestamptz [not null] created_at timestamptz [not null]
Indexes { document_id } Indexes { document_id }
Note: 'v5.19: предупреждения и нарушения документа (формы — предупреждения и нарушения §7; ОСВ — предупреждения листа OSV_* результата 2.0). Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)' Note: 'v5.19: предупреждения и нарушения документа формы. Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)'
} }

View File

@ -13,22 +13,18 @@ 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), ядро пересчитывает только после правок; подтверждение ячейки без правки аналитиком переживает пересчёт. Сырой текст источника — подсказка ячейки | | 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` | | 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`) — подсказка у поля на экране; наименование организации правится, как ИНН | | 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`; формы до миграции не пересчитываются (база очищается, Р-24). Ведётся и у документов ОСВ — предупреждения листа `OSV_*` результата 2.0 | | 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` |
| 6 | `recognition_job.statements_version` | Коммит копии общего кода `arm_statements`, которым посчитан результат (`service.statements_version`); расхождение с копией ядра при пересчёте — `info` | | 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) | | 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` ГК и ЮЛ | | 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`; перед выкладкой база очищается от форм Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; заполнение: `row_index` — `NULL` у строк,
ББ/ОФР (решение владельца 18.10.2026, Р-24), поэтому миграция только добавляет колонки, таблицу и значения по умолчанию и
§7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): `row_index` — `NULL` у строк,
созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status = созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status =
ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = string_value`, `period_kind` — `absolute` при ok`, `needs_review` значений — по прежнему правилу `values_ok`, `source_text = string_value`, `period_kind` — `absolute`
`period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле; при `period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле;
`document_issue` пуст — у форм, оставшихся в неочищенной базе, нарушения до первой правки не показываются. Откат `document_issue` — нарушения §7 каждой формы, посчитанные копией `arm_statements` (`source = core`). Откат миграции
гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода (без гарантирован на пустой БД и на данных, совместимых с v5.18 (без повторов кода, строк без кода и двух форм одного типа в
повторов кода, строк без кода, двух форм одного типа в файле, `crm_id` групп и правленого наименования организации); на файле); на несовместимых данных `downgrade` останавливается с ошибкой.
таких данных `downgrade` останавливается с ошибкой, а производные колонки v5.19 и `document_issue` удаляет с записью в
журнал миграции.
## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90) ## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90)
@ -139,7 +135,7 @@ ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = st
|---|---|---|---| |---|---|---|---|
| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 | | `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 | | `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` — индекс документа в `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; макет (чек-лист, «Подтвердить данные») | | `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; макет (чек-лист, «Подтвердить данные») |
| `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 | | `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 — Разобранные ОСВ ## Часть 05 — Разобранные ОСВ
@ -156,7 +152,7 @@ ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = st
| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. **v5.19:** `period_kind` (абсолютная / относительная подпись бланка / без даты), `label_confidence`. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 | | `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_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 | | `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 — Консолидация ## Часть 07 — Консолидация

View File

@ -5,9 +5,8 @@ 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-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) `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) и написана 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-26…S-52 ниже). Где текст дизайна выше расходится с
(`review-2026-10-18-spec-2.md`, находки L-1…L-30) и доработана по нему (решения владельца Р-23…Р-26, решения решениями Р-20…Р-22 и S-n, действуют они и спека.
S-53…S-74 ниже). Где текст дизайна выше расходится с решениями Р-20…Р-26 и S-n, действуют они и спека.
## Цель ## Цель
@ -776,7 +775,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
`07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для `07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для
неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени. неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени.
### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-26 — 18.10.2026) ### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-22 — 18.10.2026)
- **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому - **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому
общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и
@ -876,28 +875,6 @@ 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. 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 (отмечены ниже). Явно утверждены владельцем 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 ## Decisions made without discussion
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все
@ -1054,7 +1031,6 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула
дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для
обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24. обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24.
Уточнено S-65 (лучшая подпоследовательность — сначала по числу уверенных кодов). **Утверждено владельцем 18.10.2026 (Р-25).**
50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как 50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как
контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность
проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs
@ -1063,8 +1039,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов 51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов
из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО
(1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024). (1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024).
Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166 (временное Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166. Все 12
правило утверждено владельцем — Р-26). Все 12
последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с
неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`. неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`.
52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии, 52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии,
@ -1072,8 +1047,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
`$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs. `$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs.
Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не
настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния
docs; без него их `make check` красный на окне. **Заменено S-53** (повторное ревью L-2: файла `.v1` у запроса нет, docs; без него их `make check` красный на окне.
примеры и ядро не были покрыты).
53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в 53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в
`testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`: `testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`:
`0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы `0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы
@ -1085,7 +1059,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
`column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый `column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый
номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта
приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их
позициями. Дополнено S-56 (колонки сегментов со своей шапкой). позициями.
55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет; 55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет;
`TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший `TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший
разбор таблицы держит весь файл на ручном подтверждении. разбор таблицы держит весь файл на ручном подтверждении.
@ -1096,7 +1070,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`, 57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`,
`organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает `organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает
`NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки `NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки
снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам. **Утверждено владельцем 18.10.2026 (Р-25).** снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам.
58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` → 58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` →
`RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая. `RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая.
59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией 59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией
@ -1107,13 +1081,11 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия
миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для
разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2). разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (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`; миграция 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` — код из источника, правками ставит `NULL` строкам с `line_code_original_value IS NULL`; `line_code_original_value` — код из источника, правками
не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то
же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок — же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок —
только в аудите. **Утверждено владельцем 18.10.2026 (Р-25).** только в аудите.
61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра — 61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра —
есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких
нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или
@ -1127,7 +1099,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх
маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась. маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась.
64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы 64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы
в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора. **Утверждено владельцем 18.10.2026 (Р-25).** в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора.
65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из 65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из
`needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это `needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это
единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке. единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке.
@ -1147,12 +1119,10 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает
правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее
сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если
владелец считает подтверждение правкой — одна строка спеки. **Утверждено владельцем 18.10.2026 (Р-25):** владелец считает подтверждение правкой — одна строка спеки.
подтверждение без правки акцепт не сбрасывает.
71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением, 71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением,
`needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT` `needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT`
`info`. Цена: проверка на каждом маппинге — линейная по строкам. **Утверждено владельцем 18.10.2026 (Р-25) в `info`. Цена: проверка на каждом маппинге — линейная по строкам.
редакции L-12:** согласованность — вызов общей функции статуса по входу из результата (не список признаков).
72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка 72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка
и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на
экране — первое имя кода, файловое — рядом. экране — первое имя кода, файловое — рядом.
@ -1161,119 +1131,5 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая. на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая.
74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов 74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов
останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая. останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая.
**Пересмотрено Р-23:** значения `detail` статус не держат, ББ и ОФР РБ и Счастливочной за 1 кв. 2025 могут стать
`done`; держат `needs_check` aep_3 и aep_6.
75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие 75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие
`1600 = 1700` (страницы 2 баланса ×3), и записи, чьи статусы `done` меняться не должны. Цена: нулевая. `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` по статусу строки). Цена: нулевая.

View File

@ -31,8 +31,7 @@
- **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json` - **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json`
v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser
меняется только в части структуры предупреждений и `document.status`. На окне перехода схемы 1.x лежат рядом меняется только в части структуры предупреждений и `document.status`. На окне перехода схемы 1.x лежат рядом
(`*.v1.schema.json`, примеры `examples/v1/`); тест идентичности копии у ядра и сервисов сверяет файл копии с файлом (`*.v1.schema.json`, примеры `examples/v1/`), тест идентичности сервиса сверяет копию со схемой своего MAJOR.
docs своего MAJOR, если он есть, иначе с основным (схемы и примеры; правило — спека `recognition-contract`).
- **Упорядоченные перечни кодов форм** — данные `contracts/statement-code-orders.yaml` (66н / ФСБУ 4, полная / - **Упорядоченные перечни кодов форм** — данные `contracts/statement-code-orders.yaml` (66н / ФСБУ 4, полная /
упрощённая, законные повторы кода — Р-20); в пакет `arm_statements` — байтовой копией. упрощённая, законные повторы кода — Р-20); в пакет `arm_statements` — байтовой копией.
- **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`, - **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`,
@ -57,37 +56,23 @@
### Порядок выкладки — «сначала ядро» (обязателен) ### Порядок выкладки — «сначала ядро» (обязателен)
0. arm-core, tabular-parser и scan-gateway: тест идентичности копии контракта по новому правилу — схема копии 0. tabular-parser и scan-gateway: тест идентичности копии контракта выбирает файл docs по MAJOR копии (`$id` `…/v1` →
сверяется с `<имя>.v<M>.schema.json` своего MAJOR, если такой файл есть в docs, иначе с основным; пример — с `*.v1.schema.json`) — до слияния шага 2, иначе их `make check` краснеет на окне перехода.
`examples/v<M>/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы другого MAJOR не сверяются. Правило
проходит и до слияния шага 2 (файлов `.v1` ещё нет), и после; без него `make check` всех трёх краснеет между шагами 2
и 3 (ядро) или 2 и 5 (сервисы).
1. `arm-statements` — пакет с тестами; `sync-statements` в docs. 1. `arm-statements` — пакет с тестами; `sync-statements` в docs.
2. docs: контракт v2.0, DBML v5.19, правила (этот change). 2. docs: контракт v2.0, DBML v5.19, правила (этот change).
3. arm-core: **сначала очистка форм ББ/ОФР на контуре** (Р-24: владелец или платформенная команда по его заявке, 3. arm-core: миграция v5.19, маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля
после резервной копии БД — скрипт ядра `scripts/purge_statements.py`: `--dry-run` с отчётом, затем `--apply`; удаляются API. До этого шага ни один сервис не шлёт 2.0: первый же `line_code = null` дал бы файлу `ERROR`.
документы `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-документов заполнены по умолчанию — экран работает и на них). 4. arm-web: экран по новым полям (поля 1.x-документов заполнены по умолчанию — экран работает и на них).
5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга. 5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга.
6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change). 6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change).
Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs
сверяются с `*.v1.schema.json` и `examples/v1/` (правило шага 0; у запроса файла `.v1` нет — с основным); каждый сервис сверяются с `*.v1.schema.json` (шаг 0); каждый сервис обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих
обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих MAJOR с PR шага 3. Схемы `*.v1.schema.json` и MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удаляются вместе с маппером 1.x (шаг 6).
`examples/v1/` удаляются вместе с маппером 1.x (шаг 6): сначала PR ядра убирает свои копии `.v1`, затем docs.
**Чего ждать на реальных сканах.** Значения строк `detail` в `needs_review` и в статус не входят (Р-23), законные **Чего ждать на реальных сканах.** На шести OCR-документах `fixtures/ocr` после перехода большинство форм останутся
повторы кода и мягкие строки ОФР документ не держат (Р-20, Р-21). Поэтому ББ и ОФР РБ («Бухгалтерская отчетность 1 `needs_check` (строки без кода с суммами, неуверенные значения вне соотношений, неуверенная дата); законные повторы кода
кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: по прогону ревью все их неуверенные значения строк с кодом и мягкие строки ОФР перестают держать документ (Р-20, Р-21). Доля `done` фиксируется в PR scan-gateway.
покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали их только ячейки
расшифровок. aep_3 и aep_6 остаются `needs_check` (строки `unreadable` / `missing` с суммами, несошедшиеся
соотношения). Фактическая доля `done` фиксируется в PR scan-gateway.
## Non-goals ## Non-goals
@ -137,10 +122,8 @@
- scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами - scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами
2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в 2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в
словари пакета. словари пакета.
- arm-core: очистка форм ББ/ОФР перед выкладкой (`scripts/purge_statements.py`, Р-24); миграция v5.19 - arm-core: миграция v5.19 (`financial_statement_*`, `document`, `document_issue`, `recognition_job`,
(`financial_statement_*`, `document`, `document_issue`, `recognition_job`, `related_group`; только колонки, индексы и `related_group`; `document_issue` существующих форм — §7 копией пакета), `result_mapping`, `statement_mapping`,
значения по умолчанию, §7 по старым формам не считается — на неочищенной базе их нарушения до первой правки не
показываются); `JobBrief.warnings` — структуры, `DocumentOut.issues` (в том числе ОСВ); `result_mapping`, `statement_mapping`,
`statements`, `documents`, `auto_confirm`, схемы API формы; признак строки правки — `row_index IS NULL`; `statements`, `documents`, `auto_confirm`, схемы API формы; признак строки правки — `row_index IS NULL`;
`api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён `api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён
(несколько имён на код, загрузчик повтор кода не запрещает); PATCH шапки принимает `organization_name`. (несколько имён на код, загрузчик повтор кода не запрещает); PATCH шапки принимает `organization_name`.

View File

@ -11,20 +11,9 @@ false`); каждая пара «строка × колонка» — одна `
`financial_statement_meta` с `form_variant`, `needs_review_fields` и `header_source` (раздел «Шапка формы»); `financial_statement_meta` с `form_variant`, `needs_review_fields` и `header_source` (раздел «Шапка формы»);
предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего кода — предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего кода —
`recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)` уникален там, `recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)` уникален там,
где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document`; вторая где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document` с
форма того же типа в файле хранится (Р-10); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и `file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится (Р-10); ОСВ по
счёт) на файл. **Каждый** создаваемый `document` (ОСВ, формы, `DISCLOSURE`, `OTHER`) при приёме результата любого счёту, `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: Повтор кода сохраняется #### Scenario: Повтор кода сохраняется
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`) - **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
@ -35,15 +24,10 @@ false`); каждая пара «строка × колонка» — одна `
- **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`, - **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`,
`line_code_text = "ИЧ,250"` и `row_index` из результата `line_code_text = "ИЧ,250"` и `row_index` из результата
#### Scenario: Два баланса одного периода в одном файле #### Scenario: Два баланса в одном файле
- **WHEN** результат содержит два `BALANCE` с одной парой `period_year`, `period_quarter` (лист-продолжение другого - **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать)
типа или двойная печать)
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18) - **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
#### Scenario: Два баланса разных периодов в одном файле
- **WHEN** результат содержит `BALANCE` на 30.09.2025 и `BALANCE` на 31.12.2024
- **THEN** два документа с разными `file_ordinal`; каждый получает статус по своим условиям, правило Р-18 их не касается
### Requirement: Признак строки, созданной правкой ### Requirement: Признак строки, созданной правкой
Строка формы SHALL считаться созданной правкой аналитика (шаблонной, `origin = template`) тогда и только тогда, когда Строка формы SHALL считаться созданной правкой аналитика (шаблонной, `origin = template`) тогда и только тогда, когда
`row_index IS NULL`. Маппер `1.x` и маппер `2.0` SHALL всегда писать `row_index` (у `1.x` — по порядку строк результата с `row_index IS NULL`. Маппер `1.x` и маппер `2.0` SHALL всегда писать `row_index` (у `1.x` — по порядку строк результата с
@ -91,22 +75,17 @@ units_text`; `organization_name_original_value = organization_name`, `inn_confid
период — конец квартала (`period_year` и `period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом период — конец квартала (`period_year` и `period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом
(`line_code_confidence < 100`) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того (`line_code_confidence < 100`) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того
же типа и периода (та же пара `period_year`, `period_quarter`; формы без квартала не считаются); иначе `NEEDS_CHECK`. же типа и периода (та же пара `period_year`, `period_quarter`; формы без квартала не считаются); иначе `NEEDS_CHECK`.
**Согласованность:** ядро SHALL вызывать общую функцию статуса пакета (`statement-normalization`, «Шаг 8 — статус **Согласованность:** `done` противоречит содержимому, если есть строка `unreadable` / `missing` со значением (`amount` или
документа и конверта») по входу, собранному из результата: `layout_found` — `lines[]` непуст, `has_organization_or_inn` `string_value` не `null`), строка, значение или поле шапки с `needs_review`, `checks.relations_failed` или предупреждение
и `period_known` — по шапке и колонкам результата, строки и значения с их `needs_review` (значения `detail` функция не документа уровня `error`; тогда ядро считает документ `needs_check` и пишет `document_issue`
учитывает), число `checks.relations_failed`, `accepted = false`, уровни `warnings[]` документа. `done` согласован, если `RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Статус конверта и `needs_check` соседнего документа в условие
функция тоже даёт `done`; иначе ядро считает документ `needs_check` и пишет `document_issue`
`RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Это вызов той же функции, а не пересчёт §7 и `needs_review`. Статус конверта и `needs_check` соседнего документа в условие
MUST NOT входить (Р-19). Проверки §7, `needs_review` и предупреждения сервиса при загрузке MUST NOT пересчитываться. MUST NOT входить (Р-19). Проверки §7, `needs_review` и предупреждения сервиса при загрузке MUST NOT пересчитываться.
**Совпадение наименований:** обе строки приводятся к нижнему регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел, **Совпадение наименований:** обе строки приводятся к нижнему регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел,
пробелы схлопываются, крайние пробелы обрезаются (`strip`); совпало, если строки равны, или более короткая (не короче 5 пробелы схлопываются; совпало, если строки равны, или более короткая (не короче 5 символов) входит в более длинную, или
символов) входит в более длинную, или отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** справочным именем кода (у кода может быть
справочным именем кода (у кода может быть несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря ядра. Если хотя бы одно из имён начинается с
ядра. Если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера раздела**: номер — «итого по разделу», совпадение — только равенство. Кода нет в словаре имён или наименование пусто — не совпало.
начальная часть первого слова после фразы из букв `i`, `v`, `x` (латиница; кириллические «і», «х» приводятся к ним),
остальное в слове отбрасывается («v1» → V, «v» → V); совпало ⇔ номера равны; номер не выделен хотя бы у одного имени —
совпадение только при равенстве строк целиком. Кода нет в словаре имён или наименование пусто — не совпало.
Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка). `RECOGNIZED` SHALL, как и прежде, Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка). `RECOGNIZED` SHALL, как и прежде,
автоподтверждаться (OQ-157). автоподтверждаться (OQ-157).
@ -131,17 +110,8 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре
#### Scenario: Подмена итога раздела #### Scenario: Подмена итога раздела
- **WHEN** строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I» - **WHEN** строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
- **THEN** номер раздела I не равен II у справочного «Итого по разделу II», `NAME_MISMATCH` - **THEN** наименование не совпало со справочным «Итого по разделу 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: Сервис ошибся со статусом #### Scenario: Сервис ошибся со статусом
- **WHEN** документ пришёл `done`, но в нём строка `unreadable` с суммой - **WHEN** документ пришёл `done`, но в нём строка `unreadable` с суммой
@ -160,13 +130,8 @@ MUST NOT входить (Р-19). Проверки §7, `needs_review` и пре
relation`, `code = RELATION_FAILED`, `severity = error`; из `soft_relations_failed` — `kind = soft`, `code = relation`, `code = RELATION_FAILED`, `severity = error`; из `soft_relations_failed` — `kind = soft`, `code =
RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` кодов `codes` в документе. При загрузке пишутся RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` кодов `codes` в документе. При загрузке пишутся
предупреждения и `checks` результата (источник `service`) и проверки ядра уровня `info` (источник `core`): предупреждения и `checks` результата (источник `service`) и проверки ядра уровня `info` (источник `core`):
`CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён редакции документа), `CODE_NOT_IN_EDITION` (код не из перечня `CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён), `CODE_NOT_IN_EDITION` (код не из перечня редакции — по копии
редакции документа и его варианта — по копии перечня `arm_statements`), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`. перечня `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` заменяются целиком независимо от источника; удаляются вместе с ним. При пересчёте после правки записи `relation` и `soft` заменяются целиком независимо от источника;
записи `warning` источника `service` сохраняются, кроме снимаемых правкой: правка `header.report_date` (в том числе записи `warning` источника `service` сохраняются, кроме снимаемых правкой: правка `header.report_date` (в том числе
подтверждение тем же значением) снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`, `REPORT_DATE_UNCERTAIN`; правка подтверждение тем же значением) снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`, `REPORT_DATE_UNCERTAIN`; правка
@ -183,36 +148,24 @@ RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` ко
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения - **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует) - **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
#### Scenario: Бланк 66н в 2025 году без ложного info
- **WHEN** форма Межениновской за 3 кв. 2025 на бланке 66н с кодом 1120 (в перечне `fsbu4` его нет)
- **THEN** редакция документа для проверок ядра — `66n` (подпоследовательность длиннее), `CODE_NOT_IN_EDITION` у 1120 нет
#### Scenario: Пропущенная колонка #### Scenario: Пропущенная колонка
- **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет - **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет
- **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа - **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа
### Requirement: Формы результата 1.x — §7 при приёме ### Requirement: Документы без `checks` — результаты 1.x и формы до миграции
Для формы, принятой по результату `1.x` (окно перехода после выкладки ядра; `checks` контракта 2.0 у неё нет), ядро Для формы без `checks` контракта 2.0 — принятой по результату `1.x` и существовавшей до миграции v5.19 — ядро SHALL
SHALL **один раз при приёме** посчитать §7 точкой входа пакета для пересчёта (`statement-normalization`; вариант формы — **один раз** посчитать §7 копией `arm_statements` по текущим суммам (`form_variant = null` — полный набор) и записать
по структуре кодов, как у 2.0) по суммам результата и записать нарушения в `document_issue` (источник `core`, виды нарушения в `document_issue` (источник `core`, виды `relation` / `soft`, как в требовании о `document_issue`): у `1.x` —
`relation` / `soft`, как в требовании о `document_issue`) и `form_variant` в `financial_statement_meta`. Условие D-6 при приёме результата, у существующих форм — в миграции v5.19. Условие D-6 «проверки сошлись» для `1.x` SHALL браться из
«проверки сошлись» для `1.x` SHALL браться из этого расчёта; `needs_review` значения при приёме `1.x` = **НЕ** этого расчёта. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue`
`values_ok` (`values_ok` — `is_recognized` не `false` и `confidence` равна 100 или пуста), `needs_review` строки —
`false`. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue`
`MAPPING_WARNING` (`info`, источник `core`); строки `metrics.warnings` результата `1.x` — в `MAPPING_WARNING` (`info`, источник `core`); строки `metrics.warnings` результата `1.x` — в
`recognition_job.metrics.warnings` структурами `{code: "LEGACY_WARNING", severity: "info", message}`. После правки такая `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: Полярность needs_review при приёме 1.x #### Scenario: Мигрированная форма с нарушением
- **WHEN** в результате `1.2` у значения `confidence = 100`, `is_recognized = true`, у другого — `confidence = 80` - **WHEN** до миграции форма `NEEDS_CHECK` с несошедшимся `1600 = 1700` без акцепта; миграция v5.19 выполнена
- **THEN** у первого `needs_review = false`, у второго — `true` - **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]`; экран показывает
нарушение; `POST …/confirm` — 409 `CHECKS_NOT_PASSED`
#### 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 #### Scenario: 1.x-документ после PATCH
- **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится - **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится
@ -220,18 +173,14 @@ SHALL **один раз при приёме** посчитать §7 точко
пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет) пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет)
### Requirement: Пересчёт после правки ### Requirement: Пересчёт после правки
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review`, `form_variant`, родителей строк `detail` После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review` и статус тем же общим кодом
и статус точкой входа пакета для пересчёта по сохранённым данным (`statement-normalization`; конвенция знака — по `arm_statements` по сохранённым данным (конвенция знака — по `source_text`; `form_variant = null` — полный набор), с
`source_text`), с правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed`
`is_confirmed` строки ⇒ `needs_review` строки = `false`; значения строк `detail` — `needs_review = false` (Р-23), у строки ⇒ `needs_review` строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование
строки, выведенной из `detail` назначением кода, `needs_review` её значений считается по общему правилу; акцепт ⇒ знака не входят. Отличие `statements_version` задания от версии копии ядра — `document_issue`
жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят. Результат пишется: `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 — один пересчёт и одна `STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Один PATCH — один пересчёт и одна
запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) акцепт MUST NOT сбрасывать.
тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47).
#### Scenario: Подтверждение ячейки без правки #### Scenario: Подтверждение ячейки без правки
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы - **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
@ -242,11 +191,6 @@ SHALL **один раз при приёме** посчитать §7 точко
- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением - **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением
- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит - **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит
#### Scenario: Родитель уходит в расшифровки
- **WHEN** у строки 1150 две расшифровки (`detail`, `parent_row_index` = 1150), аналитик отмечает саму 1150 расшифровкой
- **THEN** после пересчёта родитель всех трёх строк `detail` — ближайшая строка `ok` выше 1150; строк над ними нет — `null`
(на экране — в начале раздела)
### Requirement: PATCH формы — строки, колонки, подтверждение, наименование организации ### Requirement: PATCH формы — строки, колонки, подтверждение, наименование организации
`PATCH /documents/{id}/statement` SHALL принимать помимо `header {inn, report_date, units}` и `values` (сумма): `PATCH /documents/{id}/statement` SHALL принимать помимо `header {inn, report_date, units}` и `values` (сумма):
- `header.organization_name` — непустая строка до 1000 символов: `organization_name_modified = true`, - `header.organization_name` — непустая строка до 1000 символов: `organization_name_modified = true`,
@ -257,11 +201,8 @@ SHALL **один раз при приёме** посчитать §7 точко
- `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения значения или снять - `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения значения или снять
подтверждение; подтверждение;
- `columns[] {column_id, period_date}` — дата колонке (обязательная дата; `null` — 422 ошибка валидации; пересчитывает - `columns[] {column_id, period_date}` — дата колонке (обязательная дата; `null` — 422 ошибка валидации; пересчитывает
`is_current` и период документа по правилу §9.5 п. 2); ручная дата ставит колонке `period_kind = absolute`, чтобы `is_current` и период документа по правилу §9.5 п. 2); правка `header.report_date` (в том числе `null`) пересчитывает
следующая правка даты отчёта её не перезаписала; дата, равная текущей, у колонки с `COLUMN_DATE_UNCERTAIN` или `period_date` колонок с `period_kind = relative:*` общим кодом (`null` даёт `null`);
`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, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить код или снять подтверждение. расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить код или снять подтверждение.
@ -270,9 +211,8 @@ SHALL **один раз при приёме** посчитать §7 точко
`unreadable` / `missing` нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла. `unreadable` / `missing` нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла.
`line_status` принимает только `"detail"`; `"ok"` — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_status` принимает только `"detail"`; `"ok"` — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только
`line_code`). Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`, `line_code`). Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`,
`needs_review` строки = `false`; `line_code_original_value` не меняется; после любого изменения статусов или кодов `needs_review` строки = `false`; `line_code_original_value` не меняется; у `detail` `parent_row_index` пересчитывается
`parent_row_index` **всех** строк `detail` документа пересчитывается как ближайшая строка `ok` выше по `row_index` как ближайшая строка `ok` выше по `row_index`. Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с
(требование «Пересчёт после правки»). Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с
существующим; незнакомый словарю — `info`. Строка с тем же кодом, **созданная правкой** (`row_index IS NULL`), с суммами — существующим; незнакомый словарю — `info`. Строка с тем же кодом, **созданная правкой** (`row_index IS NULL`), с суммами —
409 `LINE_CODE_TAKEN` (сначала очистить её суммы или выбрать другой код); без сумм — удаляется (её `id` убирается из 409 `LINE_CODE_TAKEN` (сначала очистить её суммы или выбрать другой код); без сумм — удаляется (её `id` убирается из
`document_issue.line_ids`), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном `document_issue.line_ids`), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном
@ -310,14 +250,6 @@ SHALL **один раз при приёме** посчитать §7 точко
- **WHEN** `report_date` в `needs_review_fields`, аналитик отправляет PATCH с тем же `header.report_date` - **WHEN** `report_date` в `needs_review_fields`, аналитик отправляет PATCH с тем же `header.report_date`
- **THEN** `needs_review_fields` пуст, `REPORT_DATE_UNCERTAIN` снят, статус пересчитан, аудит - **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 без кода #### Scenario: Статус ok без кода
- **WHEN** приходит `lines: [{line_id, line_status: "ok"}]` - **WHEN** приходит `lines: [{line_id, line_status: "ok"}]`
- **THEN** 422 `LINE_CODE_REQUIRED` - **THEN** 422 `LINE_CODE_REQUIRED`
@ -348,11 +280,7 @@ SHALL **один раз при приёме** посчитать §7 точко
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды словаря имён есть в состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды словаря имён есть в
перечне. В PR перехода ядра на v2.0 словарь имён SHALL быть дополнен: в редакции `fsbu4` — вторые имена законных перечне. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из
повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощенной системе
налогообложения» (Р-20) и коды печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные поисковые активы», 1140
«Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по `fixtures/ocr` и шаблонам
банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают `NAME_MISMATCH`. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из
файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча. файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча.
#### Scenario: Код есть только у строки unreadable #### Scenario: Код есть только у строки unreadable
@ -364,10 +292,6 @@ SHALL **один раз при приёме** посчитать §7 точко
аренде» аренде»
- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность» - **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
#### Scenario: Гудвил с заголовком раздела в имени
- **WHEN** строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил»
- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет
### Requirement: Ответ `GET …/statement` по v2.0 ### Requirement: Ответ `GET …/statement` по v2.0
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`) и `StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`) и
`organization_name_modified`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔ `organization_name_modified`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔
@ -386,41 +310,22 @@ SHALL **один раз при приёме** посчитать §7 точко
string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает
### Requirement: Миграция DBML v5.19 ### Requirement: Миграция DBML v5.19
Одна миграция Alembic SHALL приводить схему к DBML v5.19 и **только** добавлять колонки, таблицу, enum'ы и индексы и Одна миграция Alembic SHALL приводить схему к DBML v5.19; заполнение существующих данных: `row_index` — `NULL` у строк с
заполнять значения по умолчанию (Р-24); §7 по существующим формам миграция MUST NOT считать, импорта копии `line_code_original_value IS NULL` (созданных правкой), у остальных — по возрастанию `line_code` внутри формы с 0;
`arm_statements` в ревизии нет. Перед выкладкой база очищается от форм ББ/ОФР (требование «Очистка форм перед `line_status = ok`, `needs_review` строк — `false`, `needs_review` значений — по прежнему `values_ok`, `source_text =
выкладкой»), но миграция 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 = string_value`, `is_confirmed = false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant =
null`, `header_source` — из `*_original_value` и `inn_confidence`, `organization_name_original_value = null`, `header_source` — из `*_original_value` и `inn_confidence`, `organization_name_original_value =
organization_name`, `document.file_ordinal` — по `(created_at, id)` внутри файла с 0 (у всех документов), organization_name`, `document.file_ordinal` — по `(created_at, id)` внутри файла с 0, `related_group.crm_id` — `null`;
`related_group.crm_id` — `null`; `document_issue` — пуст. **Корректность нарушений у форм, оставшихся с до-миграционных `document_issue` каждой формы — нарушения §7, посчитанные копией `arm_statements` (требование «Документы без `checks`»).
времён, не гарантируется:** у них нет записей `document_issue`, поэтому до первой правки экран не показывает нарушений Откат (`downgrade`) SHALL быть гарантирован на пустой БД и на данных, совместимых с v5.18 (нет двух строк с одним кодом в
§7, а `POST …/confirm` не отвечает `CHECKS_NOT_PASSED`; первый PATCH пересчитывает форму общим кодом. Это цена Р-24; на форме, строк с `line_code = null`, двух форм одного типа в файле); на несовместимых данных `downgrade` SHALL
контуре её снимает очистка. Сверка `check_dbml_vs_orm.py` — против `data-model-v5.19.dbml`. останавливаться с ошибкой, называющей причину, и MUST NOT удалять данные молча. Сверка `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: Цикл миграций #### Scenario: Цикл миграций
- **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД - **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД
- **THEN** все три шага проходят, сверка ORM с DBML v5.19 — без расхождений - **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: Строка правки до миграции #### Scenario: Строка правки до миграции
- **WHEN** до миграции у формы есть строка 1250, созданная правкой (`line_code_original_value = null`), с суммой - **WHEN** до миграции у формы есть строка 1250, созданная правкой (`line_code_original_value = null`), с суммой
- **THEN** после миграции у неё `row_index = null`, `origin = template`; строки источника пронумерованы с 0 - **THEN** после миграции у неё `row_index = null`, `origin = template`; строки источника пронумерованы с 0
@ -429,82 +334,6 @@ organization_name`, `document.file_ordinal` — по `(created_at, id)` внут
- **WHEN** в БД есть форма с двумя строками 2410, выполняется `downgrade` ниже v5.19 - **WHEN** в БД есть форма с двумя строками 2410, выполняется `downgrade` ниже v5.19
- **THEN** откат останавливается с ошибкой о повторе кода; данные не изменены - **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` ГСК/ГСЗ ### Requirement: `crm_id` ГСК/ГСЗ
`related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и `related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и
`PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409 `PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409

View File

@ -23,8 +23,7 @@
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` — (`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized = столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их false` напрямую нет.
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
#### Scenario: Догадка подтверждена проверкой #### Scenario: Догадка подтверждена проверкой
- **WHEN** значение `is_recognized = false`, `needs_review = false` - **WHEN** значение `is_recognized = false`, `needs_review = false`
@ -34,7 +33,7 @@ false` напрямую нет. У значений строк `detail` `needs_r
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать (код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed: колонки без даты (`period_kind = none`) или с предупреждением о дате; снятие подтверждения ячейки и кода (`confirmed:
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409 SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
@ -49,13 +48,3 @@ SHALL быть отключена с подсказкой «Разберите
#### Scenario: Подтверждение заблокировано #### Scenario: Подтверждение заблокировано
- **WHEN** у формы две строки `missing` с суммами - **WHEN** у формы две строки `missing` с суммами
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой - **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** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки

View File

@ -4,9 +4,8 @@
Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`, Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`,
`ruff format --check .`, `mypy src` (в сервисе с копией общего кода `arm_statements` — `mypy src arm_statements`, в `ruff format --check .`, `mypy src` (в сервисе с копией общего кода `arm_statements` — `mypy src arm_statements`, в
`arm-core` — ещё `scripts`), `pytest -m "not integration"` (включая тесты копии `arm_statements/tests/`), тест `arm-core` — ещё `scripts`), `pytest -m "not integration"` (включая тесты копии `arm_statements/tests/`), тест
идентичности вендоренного контракта с `../docs/contracts` (схема копии — с `<имя>.v<M>.schema.json` своего `MAJOR`, идентичности вендоренного контракта с `../docs/contracts` (на окне перехода между `MAJOR` — со схемой того `MAJOR`,
если такой файл есть в docs, иначе с основным; примеры — с `examples/v<M>/`, если там есть одноимённый файл, иначе с который сервис пишет). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать
`examples/`; правило — спека `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать
запущенного Docker, PostgreSQL или NATS. Свежесть копии `arm_statements` относительно репозитория `arm-statements` в запущенного Docker, PostgreSQL или NATS. Свежесть копии `arm_statements` относительно репозитория `arm-statements` в
`make check` MUST NOT проверяться (соседний репозиторий в `make check` не читается). `make check` MUST NOT проверяться (соседний репозиторий в `make check` не читается).

View File

@ -54,9 +54,7 @@ v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `f
`detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`, `detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`,
`is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у `is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у
документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected, документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected,
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` (`OTHER` в v2.0 не actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`; у документа
порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в
`document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа
без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе. без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе.
#### Scenario: Результат валидируется примерами #### Scenario: Результат валидируется примерами
@ -85,10 +83,6 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с - **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с
`status` и `warnings`, файл проходит схему v2.0 `status` и `warnings`, файл проходит схему v2.0
#### Scenario: Документ OTHER в v2.0
- **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant`
- **THEN** файл не проходит схему v2.0
#### Scenario: Строка без кода с кодом в поле #### Scenario: Строка без кода с кодом в поле
- **WHEN** у строки `line_status = unreadable` передан непустой `line_code` - **WHEN** у строки `line_status = unreadable` передан непустой `line_code`
- **THEN** файл не проходит схему v2.0 - **THEN** файл не проходит схему v2.0
@ -121,25 +115,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
### Requirement: Копия контракта в сервисе идентична источнику ### Requirement: Копия контракта в сервисе идентична источнику
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check` Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением). их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
Файл копии SHALL сверяться с файлом docs по правилу, одинаковому у ядра и сервисов и верному до, во время и после окна На окне перехода между `MAJOR` тест идентичности SHALL сверять копию со схемой того `MAJOR`, который сервис пишет (или,
перехода между `MAJOR`: у ядра, читает): копия с `$id`, оканчивающимся на `/v1`, — с `docs/contracts/<имя>.v1.schema.json`, иначе — с основным
- **схема** `<имя>.schema.json` копии: `M` — номер из её `$id` (`…/v<M>`); пара — `docs/contracts/<имя>.v<M>.schema.json`, файлом `docs/contracts/<имя>.schema.json`; ядро держит копии обоих `MAJOR`. Правило теста вводится в `tabular-parser` и
**если такой файл есть**, иначе основной `docs/contracts/<имя>.schema.json` (у `recognition-request` файла `.v1` нет `scan-gateway` до слияния контракта v2.0 в docs, иначе их `make check` упадёт на окне перехода.
— его копия `…/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: Копия устарела #### Scenario: Копия устарела
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет - **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
@ -151,28 +130,8 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
окне перехода не краснеет окне перехода не краснеет
#### Scenario: Сервис перешёл на 2.0 #### Scenario: Сервис перешёл на 2.0
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`) и примеры до v2.0 - **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`)
- **THEN** тест сверяет схему с основным файлом docs (файла `.v2` нет), примеры `result-*` — с `examples/*.json`; - **THEN** тест сверяет её с основным файлом docs
`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 ## ADDED Requirements
@ -181,10 +140,9 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу `2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`, D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`,
а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref = а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref =
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения = **НЕ** `values_ok` null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения — по прежнему `values_ok`
(`values_ok` — `is_recognized` не `false` и `confidence` 100 или пуста: чистое значение — `needs_review = false`), (`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при
`needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant` — по `period_date`, иначе `none`, `form_variant = null`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
структуре кодов (точка входа пакета для пересчёта, спека `arm-core`), `file_ordinal` — индекс в `documents[]`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу — структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу —
`document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0` `document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0`
только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих
@ -192,13 +150,8 @@ null`, `line_code_original_value = line_code`, `text = string_value`, `needs_rev
#### Scenario: Сервис ещё на v1.2 #### Scenario: Сервис ещё на v1.2
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes` - **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
- **THEN** ядро принимает его маппером `1.x`: из двух строк с одним кодом хранится первая (прежнее правило маппера - **THEN** ядро принимает его маппером `1.x`, обе строки кода — по прежнему правилу «остаётся первая» с
`1.x`), вторая не хранится, у документа `MAPPING_WARNING` о повторе; нарушения §7 — в `document_issue` от ядра, статус `MAPPING_WARNING`, нарушения §7 — в `document_issue` от ядра, статус документа — по D-6, файл не `ERROR`
документа — по 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 нового сервиса #### Scenario: Ack нового сервиса
- **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"` - **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"`

View File

@ -43,10 +43,4 @@ document` — в шаги 2–6 пакета; `ocr_lookalikes.yaml` — в сл
#### Scenario: Строки без кода в aep_3 #### Scenario: Строки без кода в aep_3
- **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |` - **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |`
- **THEN** строки попадают в результат со статусами (`detail` под строкой с кодом, `missing` в начале таблицы) и сырым - **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`

View File

@ -33,24 +33,13 @@ NOT делаться.
Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C
docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source <dir>`, ссылка — docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source <dir>`, ссылка —
`origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir <dir>`. Без `--check` скрипт раскладывает копию коммита `origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir <dir>`. Без `--check` скрипт раскладывает копию коммита
`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). Копия `--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). `--check`
сравнивается с деревом `arm_statements/` источника без файла `VERSION` (его в источнике нет). `--check` SHALL SHALL возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`; `10` — копия
возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала); `20` — копия отличается от дерева
в пакете равна `docs/contracts/statement-code-orders.yaml`; `10` — копия равна дереву коммита из `VERSION`, но в коммита из `VERSION`, `VERSION` нет в истории источника, копия перечня кодов в пакете отличается от
`--ref` есть более новый (копия отстала), или копия перечня кодов отличается от файла docs (перечень в docs правился `docs/contracts/statement-code-orders.yaml` или (только у `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
раньше `arm-statements` или пересинхронизации хоста — предупреждение называет файл); `20` — копия отличается от дерева отсутствует в перечне своей редакции; `2` — нет checkout источника или хоста, неизвестный аргумент. Файлы и коды
коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список `dependencies` расхождения SHALL печататься.
его `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 Цель `statements-check` хоста (`sync-statements.sh --check --host-dir .`, docs — `../docs` или `ARM_KA_DOCS`) SHALL
входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR; входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR;
@ -69,11 +58,6 @@ PR в `arm-statements` (байтовая копия yaml) → `sync-statements`
- **WHEN** рядом нет `arm-statements` и `--source` не задан - **WHEN** рядом нет `arm-statements` и `--source` не задан
- **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный - **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный
#### Scenario: Перечень кодов в docs новее копии
- **WHEN** в docs в `statement-code-orders.yaml` добавлен код, а `arm-statements` и копии в хостах ещё не обновлены
- **THEN** `--check` в каждом хосте завершается с кодом 10 и называет файл перечня; `make check-full` зелёный с
предупреждением, посторонние PR в сервисы не краснеют
#### Scenario: Код словаря ядра вне перечня #### Scenario: Код словаря ядра вне перечня
- **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет - **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет
- **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию - **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию
@ -154,8 +138,7 @@ PR в `arm-statements` (байтовая копия yaml) → `sync-statements`
- **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false` - **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false`
### Requirement: Шаг 1 — многостраничная форма ### Requirement: Шаг 1 — многостраничная форма
Требование относится к адаптеру Excel: у OCR каждая таблица ИС — отдельный кандидат, склейки таблиц нет (повторная Одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
шапка внутри таблицы ИС — сегмент шага 2). У Excel одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
«Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL «Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL
сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT` сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT`
(`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и (`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
@ -180,8 +163,8 @@ PR в `arm-statements` (байтовая копия yaml) → `sync-statements`
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца. код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
#### Scenario: Год и сумма — не код #### Scenario: Год и сумма — не код
- **WHEN** в ячейке «2024» или четырёхзначная сумма «1050» или «7450» - **WHEN** в ячейке «2024» или четырёхзначная сумма «3450»
- **THEN** кодом это не считается (вне отрезков); «3450» — код (отрезок 3100–3699 отчёта об изменениях капитала) - **THEN** кодом это не считается (вне отрезков)
#### Scenario: Краевая пунктуация и черта #### Scenario: Краевая пунктуация и черта
- **WHEN** ячейка кода — `"1220 ,"` или `"| 140"` - **WHEN** ячейка кода — `"1220 ,"` или `"| 140"`
@ -208,35 +191,10 @@ PR в `arm-statements` (байтовая копия yaml) → `sync-statements`
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
столбца первого сегмента, минимум один. Колонки документа (`columns[]`) SHALL задаваться первым сегментом; колонки столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с
любого следующего сегмента новых `columns[]` не создают, а сопоставляются с колонками первого сегмента: кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение
- сегмент со своей шапкой (повторная шапка OCR, лист-продолжение Excel со своей строкой заголовка) — по `period_date`, `SEGMENT_COLUMNS_MISMATCH` (`error`) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение
если у каждой колонки сегмента дата есть и совпадает с датой ровно одной колонки первого сегмента; иначе по порядку; `SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, статусы по шагу 3).
- сегмент без шапки — колонки значений — столбцы правее кода, где в строках с кодами есть числа, сопоставление по
порядку.
Значения сегмента несут `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» НПЗ со столбцом кодов в другой колонке #### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
- **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом - **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом
@ -305,25 +263,12 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`, года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
`column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`). `column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`).
Подтверждение SHALL опираться только на уверенные источники, чтобы неуверенное не подтверждало само себя: для
неуверенной подписи эффективная дата — дата отчёта с уверенностью 100, иначе самая поздняя абсолютная дата колонок с
уверенной подписью (`label_confidence = 100`); такой нет — подпись не подтверждена. Неуверенная дата отчёта (шаг 8, поля
шапки) подтверждается только самой поздней абсолютной датой колонок с уверенной подписью. Для периода документа и
относительных подписей эффективная дата считается, как выше, по всем источникам.
#### Scenario: Бланк 66н без впечатанных дат #### Scenario: Бланк 66н без впечатанных дат
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года, - **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
предшествующего предыдущему», дата отчёта шапки 30.09.2025 предшествующего предыдущему», дата отчёта шапки 30.09.2025
- **THEN** `period_date` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`, - **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`; предупреждений о дате колонок нет `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: Мусорная подпись над числами #### Scenario: Мусорная подпись над числами
- **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы - **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы
- **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня - **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
@ -361,20 +306,16 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
### Requirement: Шаг 3 — строки данных и их статусы ### Requirement: Шаг 3 — строки данных и их статусы
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть
хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или
непуста с уверенностью ниже 100, или её текст — «цифра под видом буквы»: после замены двойников без условия «вплотную к непуста с уверенностью ниже 100. Строка только с текстом наименования — не строка данных. У строки — `row_index` (порядковый номер в документе с 0,
цифре» в нём есть цифра и нет других букв (`'I'`, `'| О'`). Строка только с текстом наименования — не строка данных;
текст из настоящих букв в колонке значений при уверенности 100 (подписи «Руководитель … Косарев», «(расшифровка
подписи)» под формой Excel) содержимым не считается — осознанное исключение из «ничего не выбрасывать молча». У строки — `row_index` (порядковый номер в документе с 0,
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>` по правилу шага 1, без столбца кодов — первой непустой `row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>` по правилу шага 1, без столбца кодов — первой непустой
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`. ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
Статусы: Статусы:
- `ok` — код разобран; - `ok` — код разобран;
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр (в том числе только из - `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой;
букв-двойников вне соседства с цифрой: «изо :», «}», «:») считается пустой; - `detail` — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа);
- `detail` — строка без кода с содержимым, над которой (в том же или в предыдущих сегментах документа) есть строка `ok`; `parent_ref` — `ref` ближайшей строки с кодом выше;
`parent_ref` — `ref` ближайшей строки `ok` выше (строки `unreadable` / `missing` между ними пропускаются); - `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки роли `title`
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok`; строки роли `title`
сюда не попадают. сюда не попадают.
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
@ -393,20 +334,6 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой - **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
- **THEN** строка `detail`, `parent_ref` = `ref` строки 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: Потерянный код в начале таблицы #### Scenario: Потерянный код в начале таблицы
- **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет - **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
- **THEN** строка `missing` - **THEN** строка `missing`
@ -438,25 +365,14 @@ k-я из непустых частей, если их тоже N, иначе ц
- `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` = - `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` =
текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`); текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`);
- `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`; - `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`;
- `value` нет → `parse_amount(text, source)` по стадиям: - `value` нет → `parse_amount(text, source)`: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак,
1. пусто или прочерк (`-`, `–`, `—`, `−` без цифр) → `amount = null`, `is_recognized = true`; многострочное значение скобки → минус, `}` и `]` — закрывающая скобка; прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
в строке с однострочным кодом → `amount = null`, `string_value` = текст, `is_recognized = false`; `is_recognized = true`; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
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`, тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`,
`string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель `string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель
тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе
`null`) и `is_recognized = false`; `null`) и `is_recognized = false`; многострочное значение в строке с однострочным кодом → `amount = null`,
`string_value` = текст, `is_recognized = false`;
- `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или - `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или
`]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит; `]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит;
- позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence` - позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence`
@ -473,24 +389,6 @@ k-я из непустых частей, если их тоже N, иначе ц
- **WHEN** ячейка OCR — `"(7744536}"` - **WHEN** ячейка OCR — `"(7744536}"`
- **THEN** `amount = -7744536`, `is_recognized = false` - **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: Запятая по источнику #### Scenario: Запятая по источнику
- **WHEN** текст `"4,200"` - **WHEN** текст `"4,200"`
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20` - **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
@ -530,14 +428,11 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code`
при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца
кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND` кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND`
уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`, уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`,
`locator` таблицы), таблица ИС без содержимого — предупреждение **файла** `TABLE_SKIPPED` (`info`, `locator` таблицы); `locator` таблицы), у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`);
у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех документа в обоих случаях нет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
этих случаях нет. Содержимое таблицы ИС — хотя бы одна ячейка с цифрой или с «цифрой под видом буквы» (шаг 3); таблица
только из текста (подписи, пояснительный текст) содержимого не имеет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`. не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`.
`form_variant` SHALL определяться только по структуре кодов всех строк `ok` документа (независимо от уверенности `form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
кода; у ядра — заново при каждом пересчёте после правки, точка входа ниже): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле, признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле,
`confidence` → `<поле>_confidence`. `confidence` → `<поле>_confidence`.
@ -605,21 +500,16 @@ actual}`, где `codes` — коды цели и членов соотноше
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет - **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
### Requirement: Шаг 8 — `needs_review` значения ### Requirement: Шаг 8 — `needs_review` значения
Сервис SHALL вычислять `needs_review` у каждого значения строк `ok`, `unreadable` и `missing`. У значений строк Сервис SHALL вычислять `needs_review` у каждого значения:
`detail` `needs_review = false` всегда (Р-23): у них нет кода, покрыть их соотношением нельзя, в проверки §7, агрегаты и
статус документа они не входят; сомнение остаётся видно в подсказке ячейки по `text` и `confidence`. Для остальных
значений:
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не - значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
нуждаются; нуждаются;
- значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое - значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без
«округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких «округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких
соотношений нет); соотношений нет);
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака совпадает с формой - **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус,
строгого большинства остальных членов `abs_terms` этой колонки с непустым `amount`, `is_recognized = true` и без без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`,
правки аналитика (сама ячейка в голосование не входит); форма знака — по результату разбора `parse_amount`: «скобки» `is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён;
(пара скобок, в том числе с `}` / `]`), «минус», «без знака»; голосующих нет или равенство голосов — знак не
подтверждён;
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех - `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности; ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;
- иначе `needs_review = false`. - иначе `needs_review = false`.
@ -634,22 +524,9 @@ actual}`, где `codes` — коды цели и членов соотноше
- **THEN** у 2120 знак подтверждён, `needs_review = false` - **THEN** у 2120 знак подтверждён, `needs_review = false`
#### Scenario: Знак вне конвенции #### Scenario: Знак вне конвенции
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"38378"` с уверенностью 60 (форма «без знака»), и `2100 = 2110 − - **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"{38378)"` без минуса после разбора
2120` сходится по модулю
- **THEN** `needs_review = true` у 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: Справочная строка без проверки #### Scenario: Справочная строка без проверки
- **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении) - **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении)
- **THEN** `needs_review = true` - **THEN** `needs_review = true`
@ -668,15 +545,11 @@ actual}`, где `codes` — коды цели и членов соотноше
неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`, неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`,
`fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы `fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы
кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО). кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО).
Четырёхзначные коды строк `ok` документа образуют последовательность; коды, которых нет в перечне редакции, в Четырёхзначные коды строк `ok` документа образуют последовательность; для **обеих** редакций берётся наибольшая
последовательность этой редакции не входят. Для **обеих** редакций берётся лучшая подпоследовательность, согласованная подпоследовательность, согласованная с порядком перечня варианта формы (итоги после слагаемых; код с повтором может
с порядком перечня варианта формы (итоги после слагаемых; код с повтором может войти столько раз, сколько он стоит в войти столько раз, сколько он стоит в перечне); редакция — с более длинной подпоследовательностью, при равенстве — по
перечне); «лучшая» — лексикографически: сначала наибольшее число уверенных кодов (уверенные коды — якоря: включаются эффективной дате (`fsbu4` после 2024-12-31); уверенные коды участвуют как подтверждённые якоря; при нескольких
все, какие можно включить вместе), затем наибольшая длина. Якоря, противоречащие друг другу по порядку бланка, все наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён, если
вместе войти не могут — входит наибольшее согласованное их множество; неуверенный код проверяется против выбранного.
Редакция — с лучшей подпоследовательностью (то же сравнение), при равенстве — по эффективной дате (`fsbu4` после
2024-12-31). Неуверенный код подтверждён, только если он входит во **все** лучшие подпоследовательности выбранной
редакции. Вариант формы для перечня — `form_variant` документа (шаг 6, по всем строкам `ok`). Пятизначный код подтверждён, если
подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше, подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше,
чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из
перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review = перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review =
@ -690,11 +563,6 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240 - **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check` - **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) #### Scenario: Законный повтор 1220 (Р-20, AURORA-1431)
- **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность - **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность
100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230 100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230
@ -713,9 +581,7 @@ true`. У Excel и чистых OCR-кодов `needs_review` строки по
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра - **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
### Requirement: Шаг 8 — поля шапки ### Requirement: Шаг 8 — поля шапки
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок с Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе
уверенной подписью (`label_confidence = 100`; неуверенная подпись дату отчёта не подтверждает, требование «Шаг 2 —
подпись и дата колонки»); иначе
поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field = поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field =
report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта
структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH
@ -724,15 +590,9 @@ report_date`). У бланка только с относительными по
(подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют. (подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют.
#### Scenario: Неуверенная, но верная дата #### Scenario: Неуверенная, но верная дата
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок с уверенной подписью 30.09.2025 - **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025
- **THEN** `needs_review_fields` пуст, предупреждения о дате нет - **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: Неуверенный год #### Scenario: Неуверенный год
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024 - **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check` - **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
@ -756,13 +616,12 @@ report_date`). У бланка только с относительными по
|---|---|---| |---|---|---|
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе | | `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
| `TABLE_PARSE_ERROR` | error (файл) | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`); документа нет | | `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` нет столбца кодов | | `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов |
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) | | `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | | `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | | `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме |
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента при сопоставлении по порядку не совпали по числу с первым | | `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым |
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи | | `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи |
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты | | `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты |
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента | | `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента |
@ -808,10 +667,9 @@ report_date`). У бланка только с относительными по
проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`, проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`,
`layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника, `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника,
`row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения `row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения
`{line_status строки, needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни `{needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни предупреждений документа (у ядра —
предупреждений документа (у ядра — записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки и подтверждения аналитика
и подтверждения аналитика (правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не (правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не учитываются. Статус ОСВ —
учитываются. Значения строк `detail` функция MUST NOT учитывать, даже если на входе у них `needs_review = true` (Р-23). Статус ОСВ —
`done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔ `done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error` все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
@ -833,44 +691,3 @@ report_date`). У бланка только с относительными по
#### Scenario: Соседний документ не тянет файл #### Scenario: Соседний документ не тянет файл
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет - **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
- **THEN** конверт `needs_check`, у ББ `document.status = done` - **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`, нарушения считаются полным набором

View File

@ -12,7 +12,6 @@ import datetime
import json import json
import re import re
import sys import sys
from collections import Counter
from pathlib import Path from pathlib import Path
from jsonschema import Draft202012Validator, FormatChecker from jsonschema import Draft202012Validator, FormatChecker
@ -33,7 +32,7 @@ SCHEMAS = {
EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")] EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")]
CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml" CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml"
CODE_RE = re.compile(r"^\d{4}$") # в перечне только четырёхзначные коды (пятизначные — через родителя) CODE_RE = re.compile(r"^\d{4,5}$")
# Порядок важен: result-message-* проверяется раньше result-*. # Порядок важен: result-message-* проверяется раньше result-*.
PATTERNS = [ PATTERNS = [
@ -151,24 +150,10 @@ def code_order_errors(data):
for code in codes: for code in codes:
if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first): if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first):
errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы") errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы")
errors.extend(repeat_errors(where, codes, data.get("repeats", {}).get(edition, {}).get(form, []), variant)) repeats = {c for i, c in enumerate(codes) if c in codes[:i]}
return errors allowed = set(data.get("repeats", {}).get(edition, {}).get(form, []))
if repeats - allowed:
errors.append(f"{where}: повтор кода вне перечня repeats: {sorted(repeats - allowed)}")
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 return errors