openspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service
Refs #14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
This commit is contained in:
parent
1b9296f966
commit
cc78937673
215
openspec/specs/statement-parsing/spec.md
Normal file
215
openspec/specs/statement-parsing/spec.md
Normal file
@ -0,0 +1,215 @@
|
||||
# statement-parsing Specification
|
||||
|
||||
## Purpose
|
||||
Разбор бухгалтерского баланса (0710001) и отчёта о финансовых результатах (0710002) из книг Excel в
|
||||
`statementDocument` контракта v1.2 по §12 `05-domain-rules.md`: тип формы, шапка, колонки-периоды, строки с кодами и
|
||||
значения, соотношения §7, склейка листов-продолжений внутри книги. Структура ищется по содержимому: ни буквы колонок,
|
||||
ни номера строк, ни имена листов не являются контрактом; синонимы и соотношения — данные в `rules/statement_*.yaml`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Листы форм и их продолжения находятся по содержимому
|
||||
Классификация листа SHALL различать `OSV`, `STATEMENT`, `STATEMENT_CONTINUATION` и `OTHER`. Лист SHALL считаться
|
||||
`STATEMENT`, если в любой строке до первой строки заголовка таблицы формы или первой строки с кодом формы есть заголовок
|
||||
формы или код ОКУД из словаря `statement_headers.yaml`. Лист без заголовка формы SHALL считаться `STATEMENT_CONTINUATION`,
|
||||
если на нём есть маркер продолжения («Форма 0710001 с.2»), либо строка заголовка таблицы формы (ячейка «Код» и правее
|
||||
ячейки-периоды), либо колонка, в которой не меньше 60 % непустых значений — коды строк форм. Признак ОСВ SHALL
|
||||
проверяться первым. Тип формы SHALL определяться по заголовку или ОКУД: баланс / 0710001 → `BALANCE`, отчёт о
|
||||
финансовых результатах (о прибылях и убытках) / 0710002 → `INCOME`, пояснения, расшифровки и прочие формы 0710xxx →
|
||||
`DISCLOSURE`. Титул «Бухгалтерская отчетность» MUST NOT задавать тип: тип берётся из ближайшего следующего заголовка.
|
||||
Два заголовка форм на одном листе SHALL давать два документа, второй — со строки своего заголовка.
|
||||
|
||||
#### Scenario: Заголовок формы после титульного блока
|
||||
- **WHEN** разбирается `statements-xlsx/npz/2025-Q3/Бухгалтерская отчетность … .xlsx`, лист «Бухгалтерский баланс. Лист 1»,
|
||||
где заголовок «Бухгалтерский баланс» стоит ниже титула «Бухгалтерская отчетность за январь - сентябрь 2025 г.»
|
||||
- **THEN** лист классифицирован как `STATEMENT`, документ `BALANCE`, `header.okud = "0710001"`
|
||||
|
||||
#### Scenario: Лист-продолжение без заголовка
|
||||
- **WHEN** разбирается лист «Бухгалтерский баланс. Лист 2» той же книги (первая строка — «в том числе:», далее 11901, 1100…)
|
||||
- **THEN** лист классифицирован как `STATEMENT_CONTINUATION` и не даёт отдельного документа
|
||||
|
||||
#### Scenario: Маркер «с.2» в 1С-выгрузке
|
||||
- **WHEN** разбирается `statements-xlsx/unagrande-co/2024-FY/Б.Б. Лист 2.xls` (первая строка «Форма 0710001 с.2»)
|
||||
- **THEN** лист классифицирован как `STATEMENT_CONTINUATION`, тип по ОКУД маркера — `BALANCE`
|
||||
|
||||
#### Scenario: Две формы на одном листе
|
||||
- **WHEN** разбирается синтетический лист `st01_both_forms_one_sheet` (баланс, затем ОФР)
|
||||
- **THEN** результат содержит два документа — `BALANCE` и `INCOME` с одинаковым `locator` листа, строки ОФР не попали в баланс
|
||||
|
||||
#### Scenario: Расшифровка
|
||||
- **WHEN** разбирается лист с заголовком «Пояснения к бухгалтерскому балансу и отчёту о финансовых результатах»
|
||||
- **THEN** документ `DISCLOSURE` с `locator`, без `columns` и `lines`; статус книги от него не зависит
|
||||
|
||||
### Requirement: Заголовок таблицы, колонка кодов и колонки-периоды определяются по тексту
|
||||
Строкой заголовка таблицы SHALL считаться первая после заголовка формы строка с ячейкой «Код»/«Коды» (синонимы в
|
||||
словаре) и хотя бы одной ячейкой-периодом правее; заголовок, разнесённый на две строки или с переносами внутри ячейки,
|
||||
SHALL склеиваться; строка нумерации колонок («1 2 3 4 5») SHALL пропускаться. Колонкой кодов SHALL считаться колонка,
|
||||
в которой не меньше 60 % непустых значений блока — четырёх- или пятизначные коды строк форм (1100–1799 для ББ, 2100–2999
|
||||
для ОФР и их пятизначные расширения, 5xxx для расшифровок); коды читаются как текст из ячеек любого типа. Колонкой
|
||||
наименований SHALL считаться ближайшая левее кодов колонка с преобладанием текста; колонка «Пояснения» в наименования
|
||||
MUST NOT попадать. Колонками-периодами SHALL быть все ячейки заголовка правее кодов с распознанным периодом, по порядку
|
||||
слева направо, `column_index` с нуля, `period_date` — дата или конец периода из `statement_periods.yaml`. Ячейки заголовка
|
||||
без периода MUST NOT становиться колонками; их значения не читаются, в предупреждения пишется одна строка. Если колонки
|
||||
кодов нет, документ SHALL отдаваться без строк со статусом `needs_check` и предупреждением «колонка кодов не найдена».
|
||||
|
||||
#### Scenario: Три раскладки, одни и те же колонки
|
||||
- **WHEN** разбираются `unagrande-co/2024-FY/Б.Б Лист 1.xls` (колонки «Пояснения | Наименование показателя | Код | На 31
|
||||
декабря 2024 г. | … 2023 | … 2022»), `npz/2025-Q3/…xlsx` («Пояснения | Наименование показателя | Код | На 30 сентября
|
||||
2025 г. | …») и `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` («Наименование показателя | Коды | За 9 месяцев 2025 года
|
||||
| На 31 декабря 2024 года | …», под шапкой строка «1 2 3 4 5»)
|
||||
- **THEN** у каждого документа три колонки с `period_date` `2024-12-31/2023-12-31/2022-12-31`, `2025-09-30/2024-12-31/2023-12-31`
|
||||
и `2025-09-30/2024-12-31/2023-12-31` соответственно, строка нумерации не стала строкой данных, «Пояснения» не попали в
|
||||
`line_name`
|
||||
|
||||
#### Scenario: Заголовок в две строки
|
||||
- **WHEN** разбирается синтетический лист `st02_csv_like` («На 31 декабря» в одной строке, «2024 г.» в следующей, коды
|
||||
без подписи «Код»)
|
||||
- **THEN** колонки-периоды найдены по склеенному тексту (`2024-12-31`, `2023-12-31`), колонка кодов найдена без подписи
|
||||
|
||||
#### Scenario: Пустые шаблоны форм
|
||||
- **WHEN** разбираются `templates/*.xlsx` (образцы 66н и ФСБУ 4/2023: коды есть, сумм и шапки организации нет; год в
|
||||
заголовке колонки разнесён по ячейкам «20» и « г.»)
|
||||
- **THEN** документы `BALANCE`/`INCOME` со строками-кодами без значений, статус `needs_check`, ошибки разбора нет;
|
||||
колонки-периоды могут отсутствовать
|
||||
|
||||
#### Scenario: Колонка без периода
|
||||
- **WHEN** в синтетическом листе правее колонок дат есть колонка «Изменение, %»
|
||||
- **THEN** она не входит в `columns`, предупреждение «колонка без периода пропущена: Изменение, %»
|
||||
|
||||
#### Scenario: Нет колонки кодов
|
||||
- **WHEN** разбирается синтетический лист `st03_no_codes` (наименования и суммы без кодов)
|
||||
- **THEN** документ `BALANCE` с пустыми `lines`, статус `needs_check`, предупреждение «колонка кодов не найдена»
|
||||
|
||||
### Requirement: Шапка документа
|
||||
Поля шапки SHALL искаться по меткам из `statement_headers.yaml` в области от начала блока формы до строки заголовка
|
||||
таблицы; значение — ближайшая непустая ячейка правее метки, иначе первая непустая ячейка следующей строки. `inn` SHALL
|
||||
быть 10 или 12 цифр у метки «ИНН», причём строки после метки аудитора MUST NOT использоваться. `organization_name` SHALL
|
||||
браться после метки «Организация», а при пустом значении — из ближайшей строки выше с признаком ОПФ. `units` SHALL
|
||||
отдаваться текстом как в файле без нормализации. `report_date_text`/`report_date` SHALL браться из подзаголовка формы
|
||||
(«на <дата>», «за <период>»), иначе из тройки «число, месяц, год»; для ОФР `report_date` — конец периода. Найденные поля
|
||||
SHALL иметь `*_confidence = 100`. У документа без шапки все поля SHALL быть `null`, кроме `form_name` из маркера.
|
||||
|
||||
#### Scenario: Полная шапка 1С
|
||||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls`
|
||||
- **THEN** `organization_name = 'Акционерное общество "Унагранде Компани"'`, `inn = "3249004697"` (не ИНН аудитора
|
||||
7716021332), `okpo = "47888105"`, `okud = "0710001"`, `units = "в тыс. рублей"`, `report_date = 2024-12-31`,
|
||||
`report_date_text = "на 31 декабря 2024 г."`, `address` начинается с «242440»
|
||||
|
||||
#### Scenario: Организация над меткой, ОФР за период
|
||||
- **WHEN** разбирается лист «Отчет о финансовых результата» книги НПЗ
|
||||
- **THEN** `organization_name = 'ООО "НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД"'`, `inn = null` (на листе ОФР метки «ИНН» нет, а
|
||||
шапка другого листа не используется), `okpo = "16098811"`, `report_date = 2025-09-30`, `units = "тыс. руб."`; у
|
||||
документа `BALANCE` той же книги `inn = "2315995868"` из титульного блока над заголовком формы
|
||||
|
||||
#### Scenario: Дата из тройки чисел
|
||||
- **WHEN** разбирается `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` (ячейки 30 | 9 | 2025 как числа)
|
||||
- **THEN** `report_date = 2025-09-30`, `inn = "7016006635"`, `okpo = "85262693"`
|
||||
|
||||
### Requirement: Строки с кодом и значения
|
||||
Каждая строка блока с кодом в колонке кодов SHALL давать `line` с `line_code` (текст без пробелов), `line_name` (trim,
|
||||
переносы заменены пробелом), `line_code_confidence = line_name_confidence = 100` и по одному `value` на каждую
|
||||
колонку-период. Строки без кода MUST NOT отдаваться (ASSUMPTION(OQ-36)); пятизначные подстроки SHALL отдаваться как
|
||||
обычные строки. Число SHALL приводиться к `Decimal` по §1.2 (скобки → минус, пробелы убираются); пусто, «–», «-» →
|
||||
`amount = null`, `is_recognized = true`; нечисловой текст → `amount = null`, `string_value` = текст, `is_recognized =
|
||||
false`; `confidence = 100`. Повтор кода внутри документа SHALL оставлять первую строку, записывать код в
|
||||
`checks.duplicate_line_codes` и переводить документ в `needs_check`. Единицы MUST NOT нормализоваться: при `units` с
|
||||
«тыс.» и дробной частью не меньше чем у 10 % значений в предупреждения SHALL писаться «значений с дробной частью при
|
||||
единице тыс. руб.: N».
|
||||
|
||||
#### Scenario: Подстроки и значения с копейками
|
||||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls`
|
||||
- **THEN** есть строки `1110` («Нематериальные активы», `5971264.14`) и `11101` («Нематериальные активы в организации»,
|
||||
`5852464.14`); строка без кода с нулями после 11103 не отдана; предупреждение о дробной части при «тыс. рублей» есть
|
||||
|
||||
#### Scenario: Пустые значения и прочерки
|
||||
- **WHEN** в `mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` у строки 1170 пусто в первой колонке и 10 в остальных
|
||||
- **THEN** `values[0].amount = null`, `values[1].amount = "10"`, все `is_recognized = true`
|
||||
|
||||
#### Scenario: Отрицательные в скобках
|
||||
- **WHEN** в синтетическом ОФР 2120 записана как «(6 162 067)»
|
||||
- **THEN** `amount = "-6162067"`
|
||||
|
||||
### Requirement: Соотношения §7 — данные, проверка по колонкам
|
||||
Соотношения SHALL храниться в `statement_checks.yaml` в виде `{form, relation, strict, abs_terms}` и вычисляться чистой
|
||||
функцией над «код → сумма» по каждой колонке-периоду отдельно и только по четырёхзначным кодам; отсутствующий код —
|
||||
0; соотношение SHALL считаться выполненным, если сходится правая часть как записана либо с членами `abs_terms` по
|
||||
модулю, с допуском ±1 в единицах формы. Нарушение жёсткого соотношения SHALL попадать в `checks.relations_failed`
|
||||
(`column_index`, `relation`, `expected`, `actual`) и переводить документ в `needs_check`; нарушение мягкого — в
|
||||
`checks.soft_relations_failed` и предупреждение. Проверка «сумма пятизначных подстрок = родитель» SHALL генерироваться по
|
||||
коду как мягкая, когда подстроки есть. Перечень нарушений SHALL дублироваться строками в `metrics.warnings`.
|
||||
|
||||
#### Scenario: Полные формы сходятся
|
||||
- **WHEN** разбираются обе формы Межениновской (`1_Форма1_бух_09.xlsx`, `1_Форма2_бух_09.xlsx`)
|
||||
- **THEN** у документов `BALANCE` и `INCOME` `checks.relations_failed = []` по всем колонкам, статус `done`
|
||||
|
||||
#### Scenario: Знаки 1С
|
||||
- **WHEN** в ОФР Межениновской расходы записаны со знаком минус (2120 = −417659 при 2110 = 276851 и 2100 = −140808)
|
||||
- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms`, в `relations_failed` его нет; то же доказывает
|
||||
unit-тест `test_relation_holds_with_abs_terms`. Реальные ОФР Унагранде (страница 1) и НПЗ расходятся по 2100 из-за единиц
|
||||
(рубли с копейками при шапке «тыс. руб.», OQ-138), а не из-за знака
|
||||
|
||||
#### Scenario: Испорченный итог
|
||||
- **WHEN** в синтетическом балансе 1600 на 5 больше суммы 1100 + 1200
|
||||
- **THEN** `relations_failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected, actual}`, статус `needs_check`
|
||||
|
||||
#### Scenario: Мягкое соотношение
|
||||
- **WHEN** в ОФР подстроки 2411 + 2412 не равны 2410
|
||||
- **THEN** нарушение в `soft_relations_failed`, статус документа не меняется
|
||||
|
||||
### Requirement: Склейка листов-продолжений и статус документа
|
||||
Листы книги SHALL обрабатываться по порядку; лист `STATEMENT_CONTINUATION` SHALL присоединяться к последнему открытому
|
||||
документу формы при совпадении числа колонок-сумм, строки добавляются в конец, `locator` SHALL перечислять имена листов
|
||||
через запятую. Продолжение без открытого документа SHALL давать документ с пустой шапкой, типом по ОКУД маркера (иначе
|
||||
`OTHER`), статусом `needs_check` и предупреждением «страница-продолжение без шапки: загрузите форму одним файлом».
|
||||
Документ SHALL считаться `ok`, если все жёсткие соотношения сошлись по всем колонкам, дубликатов кодов нет, колонка
|
||||
кодов найдена, есть хотя бы одна колонка-период, в шапке есть организация или ИНН и документ не является одиночным
|
||||
продолжением. Книга SHALL получать `done`, если все документы ОСВ и форм `ok`, иначе `needs_check`; книга без документов
|
||||
ОСВ и форм SHALL давать `error INVALID_DOCUMENT`.
|
||||
|
||||
#### Scenario: Баланс на пяти листах
|
||||
- **WHEN** разбирается книга НПЗ
|
||||
- **THEN** два документа: `BALANCE` с `locator = "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2,…,Лист 5"`
|
||||
и строками от 1105 до 1700 включительно, `INCOME` на трёх листах (лист отчёта с единственной строкой 2400 присоединяется
|
||||
как продолжение: после листа формы хватает одного кода, `min_codes_continuation`), строка 2400 присутствует; `checks.relations_failed` баланса содержит только
|
||||
`1600 = 1700` по трём колонкам (расхождения 1.94 / 9.02 / 2.48 руб.: значения в рублях с копейками при шапке
|
||||
«тыс. руб.», OQ-138; допуск ±1 не меняется); `INCOME` — `needs_check` по `2100`, `2200`, `2300` (OQ-138); статус
|
||||
книги `needs_check`
|
||||
|
||||
#### Scenario: Лист-продолжение с одной кодовой строкой
|
||||
- **WHEN** после листа формы идёт лист с единственной кодовой строкой (`2400`, меньше `min_codes`) и тем же числом колонок сумм
|
||||
- **THEN** лист присоединяется к последнему открытому документу, строка присутствует; если присоединить нельзя (число
|
||||
колонок или тип формы маркера не совпали), книга `needs_check` и предупреждение «лист с кодами форм (N < min_codes) не
|
||||
присоединён: …»; первая страница формы с одной кодовой строкой формой не считается
|
||||
|
||||
#### Scenario: Одинокая страница 2
|
||||
- **WHEN** разбирается `unagrande-group/2024-FY/ОФР. Лист 2.xls` (стр. 1 в пакете нет)
|
||||
- **THEN** один документ `INCOME` со строками 2510–2910, пустой шапкой, статус `needs_check`, предупреждение о продолжении
|
||||
|
||||
#### Scenario: Страница 1 без продолжения
|
||||
- **WHEN** разбирается `unagrande-co/2024-FY/Б.Б Лист 1.xls` (только актив)
|
||||
- **THEN** документ `BALANCE` со строками 1110–1600, шапка полная, `1600 = 1100 + 1200` выполнено, `1700`/`1300` отсутствуют
|
||||
(считаются нулём: соотношение `1600 = 1700` нарушено → `needs_check` с перечнем)
|
||||
|
||||
#### Scenario: Смесь ОСВ и форм в одной книге
|
||||
- **WHEN** разбирается синтетическая книга `st04_mixed_book` (лист ОСВ 60 и лист баланса)
|
||||
- **THEN** результат содержит `osvDocument` и `statementDocument`, статус книги по обоим
|
||||
|
||||
### Requirement: Словари форм, координаты, golden
|
||||
Словари `statement_headers.yaml`, `statement_periods.yaml`, `statement_checks.yaml` SHALL валидироваться при старте;
|
||||
каждая запись SHALL срабатывать хотя бы на одном файле `../fixtures/statements-xlsx/**` или синтетике. Код
|
||||
`src/parser/statements/**` и `src/parser/book.py` MUST NOT содержать литералов координат (линтер `test_no_coordinates`).
|
||||
Снимки `../fixtures/golden/expected_statement_parse/**` SHALL покрывать все файлы `statements-xlsx/**` и обновляться
|
||||
только `make golden-update`; property-тест по манифесту `tests/fixtures/statements_manifest.yaml` SHALL проверять тип,
|
||||
ИНН, ожидаемый статус и сходимость жёстких соотношений для полных форм.
|
||||
|
||||
#### Scenario: Новый синоним без релиза кода
|
||||
- **WHEN** в `statement_headers.yaml` добавлен синоним заголовка «Баланс предприятия» и файл с таким заголовком
|
||||
- **THEN** лист классифицирован как `BALANCE` без изменения кода
|
||||
|
||||
#### Scenario: Мёртвая запись словаря
|
||||
- **WHEN** запись словаря не срабатывает ни на одном файле
|
||||
- **THEN** тест покрытия падает с именем записи
|
||||
|
||||
#### Scenario: Регрессия по снимкам
|
||||
- **WHEN** результат разбора любого файла `statements-xlsx/**` отличается от снимка
|
||||
- **THEN** golden-тест падает с diff; обновление — только `make golden-update` и отдельный коммит в fixtures
|
||||
@ -86,12 +86,14 @@ Stateless-сервис разбора табличных файлов (XLS/XLSX/
|
||||
|
||||
### Requirement: Парсер — заменяемый интерфейс
|
||||
Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о
|
||||
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (`get_parser()`) SHALL разбирать
|
||||
ОСВ по спеке `osv-parsing` и писать файл результата с `schema_version = "1.1"`; листы с признаками ББ/ОФР SHALL
|
||||
пропускаться с предупреждением до подсреза 2b. Заглушка `NullParser` (`error NOT_IMPLEMENTED`) SHALL использоваться только
|
||||
NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (`get_parser()`) SHALL быть
|
||||
оркестратором книги `BookParser`: он читает книгу один раз, классифицирует листы по содержимому и отдаёт листы ОСВ
|
||||
конвейеру спеки `osv-parsing`, листы форм и их продолжения — конвейеру спеки `statement-parsing`; прочие листы
|
||||
пропускаются с предупреждением. Файл результата SHALL писаться с `schema_version = "1.2"`; документы книги — ОСВ в
|
||||
порядке листов, затем формы в порядке листов. Заглушка `NullParser` (`error NOT_IMPLEMENTED`) SHALL использоваться только
|
||||
в тестах воркера и MUST NOT быть реализацией по умолчанию. Реализация по умолчанию SHALL создаваться один раз при старте
|
||||
приложения (словари читаются и проверяются там же). Каталог `api/rules/` SHALL содержать словари ОСВ (`osv_headers`,
|
||||
`osv_groups`, `periods`, `data_types`) и README.
|
||||
`osv_groups`, `periods`, `data_types`), словари форм (`statement_headers`, `statement_periods`, `statement_checks`) и README.
|
||||
|
||||
#### Scenario: Подмена реализации в тесте
|
||||
- **WHEN** в тесте воркеру передан парсер, возвращающий один `osvDocument`
|
||||
@ -99,9 +101,20 @@ NATS и S3; воркер MUST NOT зависеть от конкретной р
|
||||
|
||||
#### Scenario: Реальная ОСВ через воркер
|
||||
- **WHEN** в интеграционном тесте в S3 лежит `rb/2024-FY/ОСВ_66 за 2024.xls` и опубликован запрос `file_type = XLS`
|
||||
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "1.1"`, файл результата проходит схему v1.1 и
|
||||
- **THEN** после ack публикуется конверт `done` с `result_schema_version = "1.2"`, файл результата проходит схему v1.2 и
|
||||
содержит один `osvDocument` с `account_code_raw = "66"`
|
||||
|
||||
#### Scenario: Реальные формы через воркер
|
||||
- **WHEN** в интеграционном тесте в S3 лежит книга `statements-xlsx/npz/2025-Q3/…xlsx` и опубликован запрос `file_type = XLSX`
|
||||
- **THEN** после ack публикуется конверт `needs_check` с `result_schema_version = "1.2"` (книга НПЗ: `1600 = 1700`
|
||||
расходится на 1.94 / 9.02 / 2.48 руб. — значения в рублях при шапке «тыс. руб.», OQ-138), файл результата содержит два
|
||||
`statementDocument` (`BALANCE`, `INCOME`); `checks.relations_failed` баланса содержит только `1600 = 1700` по трём
|
||||
колонкам, у `INCOME` — нарушения `2100`, `2200`, `2300` по двум колонкам (те же рубли при «тыс. руб.», OQ-138)
|
||||
|
||||
#### Scenario: Книга без ОСВ и форм
|
||||
- **WHEN** в S3 лежит книга Excel, ни один лист которой не имеет признаков ОСВ или формы
|
||||
- **THEN** публикуется конверт `error`, `error.code = INVALID_DOCUMENT`, сообщение называет число просмотренных листов
|
||||
|
||||
### Requirement: Копия контракта идентична источнику
|
||||
`api/contracts/*.schema.json` SHALL быть байт в байт равны файлам `../docs/contracts/`; pydantic-модели SHALL валидировать
|
||||
все `../docs/contracts/examples/*.json` и порождать JSON, проходящий схему. Проверка входит в `make check`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user