Compare commits

...

4 Commits

Author SHA1 Message Date
Andrey Epifancev
d096459c75 docs: change intercompany-matrix-presets и README — ревью #141, раунд 2 (AURORA-1747)
README: возвращена фраза про XLSX результирующей таблицы, потерянная при merge.
Change: отменённое решение 4, ветка-закладка wip/shahmatka-corrections-unpaired,
ссылки на снятый раздел Known issues, шапка xlsx пресета — счета правила и «кроме»,
статьи сторон при side_1 = K, tasks раунда 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:47:04 +04:00
Andrey Epifancev
5d4fd67d3d Merge origin/main в add-preset-shahmatka (ревью #141, раунд 2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:45:50 +04:00
aep
63fa528c48 Merge pull request 'chore(arm_statements): копия arm-statements@25b2bb8 (#162)' (#163) from feat/162-statements-sync-25b2bb8 into main 2026-10-22 15:47:36 +03:00
Andrey Epifancev
ad86a55716 chore(arm_statements): копия arm-statements@25b2bb8 (#162)
Копия api/arm_statements: 3390f92..25b2bb8 (1 коммитов), скрипт docs/scripts/sync-statements.sh.
Коммиты arm-statements:
- 72e2e72 fix(#8): шаг 2 — нумерация со сбоем OCR под заголовком, повтор шапки со склеенной нумерацией (AURORA-1517)

statements-check arm-core: 0 — копия синхронна с arm-statements@origin/main (25b2bb8)

Co-Authored-By: sync-statements.sh <noreply@aepif.ru>
2026-10-22 16:46:45 +04:00
9 changed files with 104 additions and 23 deletions

View File

@ -870,7 +870,8 @@ Q2–Q4 не подходят (R-14a). Привязываются только `
API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица API: список счетов `GET /directories/accounts`, правила версии `GET …/elimination-rules`, матрица
`GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица `GET …/intercompany-matrix/all` и `POST …/manual`, их XLSX-выгрузки, статичная таблица
`GET …/intercompany-matrix/result-table`. Режим PRESET (счета и показатель из правила элиминации): `GET …/intercompany-matrix/result-table` и её XLSX `GET …/intercompany-matrix/result-table/export`
(аудит `EXPORT`, `report = "intercompany_result_table"`). Режим PRESET (счета и показатель из правила элиминации):
`GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`. `GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`.
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной `GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной
версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`, версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,

View File

@ -1 +1 @@
3390f92daddbfd4919d7f799ad2959cc35bfbd92 25b2bb81195bb1e99f73b90e336cd175ae8b344c

View File

@ -17,6 +17,7 @@ _MIN_NUMBERING_CELLS = 3
_SHORT_NOTE = 3 # «Пояснения» без подписи: значения не длиннее 3 символов _SHORT_NOTE = 3 # «Пояснения» без подписи: значения не длиннее 3 символов
_SKIP_STEP = 2 # скачок через одну пропущенную цифру нумерации _SKIP_STEP = 2 # скачок через одну пропущенную цифру нумерации
_NUMBER = re.compile(r"[0-9]+") _NUMBER = re.compile(r"[0-9]+")
_NUMBERING_TAIL = re.compile(r"\s+[0-9]{1,2}$")
_FULL_CONFIDENCE = 100.0 _FULL_CONFIDENCE = 100.0
@ -24,6 +25,11 @@ def _text(cell: Cell | None) -> str:
return "" if cell is None else cell.text.strip() return "" if cell is None else cell.text.strip()
def caption_text(text: str) -> str:
"""Подпись шапки для сравнения: `normalize_caption` без номера столбца, склеенного снизу («Коды\\n2» → «коды»)."""
return _NUMBERING_TAIL.sub("", normalize_caption(text))
def cell_at(rows: Grid, row: int, col: int) -> Cell | None: def cell_at(rows: Grid, row: int, col: int) -> Cell | None:
"""Ячейка сетки; вне строки или сетки — `None`.""" """Ячейка сетки; вне строки или сетки — `None`."""
if row < 0 or row >= len(rows) or col < 0: if row < 0 or row >= len(rows) or col < 0:
@ -138,17 +144,27 @@ def first_code_row(rows: Grid, first: int, last: int, code_col: int, rules: Rule
return None return None
def is_numbering_row(cells: Sequence[Cell | None], rules: Rules) -> bool: def is_numbering_row(cells: Sequence[Cell | None], rules: Rules, *, noisy: bool = False) -> bool:
"""≥ 3 непустых ячеек-целых, слева направо +1, начало 1 или 2, не больше одного пропуска.""" """≥ 3 непустых ячеек-целых, слева направо +1, начало 1 или 2, не больше одного пропуска.
`noisy` — строка сразу под шапкой (AURORA-1517): ячейка со сбоем OCR даёт своё единственное целое («• ■ 1» → 1,
«3гола» → 3), ячейка без цифр («/», «V») — пропуск, два целых в ячейке — не нумерация.
"""
numbers: list[int] = [] numbers: list[int] = []
for cell in cells: for cell in cells:
text = _text(cell) text = _text(cell)
if not text: if not text:
continue continue
fixed = fix_lookalikes(text, rules.lookalikes, anywhere=True).strip() fixed = fix_lookalikes(text, rules.lookalikes, anywhere=True).strip()
if _NUMBER.fullmatch(fixed) is None: if _NUMBER.fullmatch(fixed) is not None:
numbers.append(int(fixed))
continue
if not noisy:
return False return False
numbers.append(int(fixed)) found = _NUMBER.findall(text)
if len(found) > 1:
return False
numbers.extend(int(number) for number in found)
if len(numbers) < _MIN_NUMBERING_CELLS or numbers[0] not in (1, 2): if len(numbers) < _MIN_NUMBERING_CELLS or numbers[0] not in (1, 2):
return False return False
skips = 0 skips = 0
@ -212,7 +228,7 @@ def find_header_rows(rows: Grid, first: int, first_code: int, code_col: int, rul
"""Строка заголовка (одна или две, склеенные); не найдена — `()`.""" """Строка заголовка (одна или две, склеенные); не найдена — `()`."""
candidates = [r for r in range(first, first_code) if not is_numbering_row(rows[r], rules)] candidates = [r for r in range(first, first_code) if not is_numbering_row(rows[r], rules)]
captioned = [ captioned = [
r for r in candidates if rules.headers.code_caption.search(normalize_caption(_text(cell_at(rows, r, code_col)))) r for r in candidates if rules.headers.code_caption.search(caption_text(_text(cell_at(rows, r, code_col))))
] ]
header: int | None = captioned[-1] if captioned else None header: int | None = captioned[-1] if captioned else None
if header is None: if header is None:
@ -229,15 +245,17 @@ def find_header_rows(rows: Grid, first: int, first_code: int, code_col: int, rul
def segment_roles(rows: Grid, first: int, last: int, header_rows: tuple[int, ...], rules: Rules) -> list[RowRole]: def segment_roles(rows: Grid, first: int, last: int, header_rows: tuple[int, ...], rules: Rules) -> list[RowRole]:
"""Роли строк `first..last`: выше заголовка — TITLE (только при найденном заголовке).""" """Роли строк `first..last`: выше заголовка — TITLE (только при найденном заголовке); строка сразу под заголовком —
нумерация и при сбое OCR (`is_numbering_row(noisy=True)`)."""
roles: list[RowRole] = [] roles: list[RowRole] = []
top = min(header_rows) if header_rows else None top = min(header_rows) if header_rows else None
under = max(header_rows) + 1 if header_rows else None
for row in range(first, last + 1): for row in range(first, last + 1):
if row in header_rows: if row in header_rows:
roles.append(RowRole.HEADER) roles.append(RowRole.HEADER)
elif top is not None and row < top: elif top is not None and row < top:
roles.append(RowRole.TITLE) roles.append(RowRole.TITLE)
elif is_numbering_row(rows[row], rules): elif is_numbering_row(rows[row], rules, noisy=row == under):
roles.append(RowRole.NUMBERING) roles.append(RowRole.NUMBERING)
else: else:
roles.append(RowRole.DATA) roles.append(RowRole.DATA)
@ -251,7 +269,7 @@ def _caption_column(
for col, cell in enumerate(rows[row]): for col, cell in enumerate(rows[row]):
if col == code_col or (left_of_code and col > code_col): if col == code_col or (left_of_code and col > code_col):
continue continue
if pattern.search(normalize_caption(_text(cell))): if pattern.search(caption_text(_text(cell))):
return col return col
return None return None

View File

@ -18,6 +18,7 @@ from arm_statements.columns import (
from arm_statements.issues import WarningCode, make_warning from arm_statements.issues import WarningCode, make_warning
from arm_statements.layout import ( from arm_statements.layout import (
Grid, Grid,
caption_text,
cell_at, cell_at,
find_code_column, find_code_column,
find_header_rows, find_header_rows,
@ -35,7 +36,6 @@ from arm_statements.model import (
StatementHeader, StatementHeader,
StatementWarning, StatementWarning,
) )
from arm_statements.text import normalize_caption
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@ -100,7 +100,7 @@ def _chunks(rows: Grid, breaks: Sequence[int]) -> list[tuple[int, int]]:
def _caption_cells(rows: Grid, header_rows: Sequence[int]) -> frozenset[str]: def _caption_cells(rows: Grid, header_rows: Sequence[int]) -> frozenset[str]:
cells = (normalize_caption(_text(rows, r, c)) for r in header_rows for c in range(len(rows[r]))) cells = (caption_text(_text(rows, r, c)) for r in header_rows for c in range(len(rows[r])))
return frozenset(c for c in cells if c) return frozenset(c for c in cells if c)
@ -110,7 +110,7 @@ def _is_repeat_header(rows: Grid, row: int, code_col: int | None, reference: fro
return False return False
if code_col is not None and is_code_cell(_text(rows, row, code_col), rules): if code_col is not None and is_code_cell(_text(rows, row, code_col), rules):
return False return False
normalized = [normalize_caption(c.text) for c in rows[row] if c is not None and c.text.strip()] normalized = [caption_text(c.text) for c in rows[row] if c is not None and c.text.strip()]
if any(rules.headers.code_caption.search(text) for text in normalized): if any(rules.headers.code_caption.search(text) for text in normalized):
return True return True
return sum(1 for text in normalized if text and text in reference) >= _MIN_SAME_CELLS return sum(1 for text in normalized if text and text in reference) >= _MIN_SAME_CELLS

View File

@ -215,3 +215,45 @@ def test_headerless_continuation_sheet_with_outside_numbers(rules: Rules) -> Non
assert layout.warnings[0].locator == "Лист1!F2\nЛист2!F1" assert layout.warnings[0].locator == "Лист1!F2\nЛист2!F1"
assert layout.segments[1].value_cols == ((3, 3), (4, 4)) assert layout.segments[1].value_cols == ((3, 3), (4, 4))
assert layout.segments[1].extra_cols == () assert layout.segments[1].extra_cols == ()
BALANCE_CAPTIONS = ["Наименование показателя", "Коды", "На 30 июня\n2026 года", "На 31 декабря\n2025 года"]
ASSETS = [["Запасы", "1210", "5", "4"], ["Дебиторская задолженность", "1230", "3", "2"], ["Итого", "1200", "8", "6"]]
LIABILITIES = [["Заемные средства", "1510", "3", "2"], ["Кредиторская задолженность", "1520", "5", "4"]]
def test_numbering_with_ocr_noise_right_under_header(rules: Rules) -> None:
"""AURORA-1517: нумерация под шапкой со сбоем OCR («/» вместо 1, мусор, протёкшее «года», «V» вместо 4)."""
for numbering in (
["/", "2", "3", "4"],
["• ■ 1", "2", "3", "4"],
["1", "2", "3гола", "гола\n4"],
["1", "2", "3", "V"],
):
layout = find_layout(ocr([BALANCE_CAPTIONS, numbering, *ASSETS]), parse_header(hdr(), rules), rules)
assert layout.roles[1] is RowRole.NUMBERING, numbering
def test_numbering_with_ocr_noise_under_repeat_header(rules: Rules) -> None:
"""AURORA-1517, «Светлый путь»: «• ■ 1 2 3 4 5» под повторной шапкой второй страницы."""
rows = [BALANCE_CAPTIONS, ["1", "2", "3", "4"], *ASSETS, BALANCE_CAPTIONS, ["• ■ 1", "2", "3", "4"], *LIABILITIES]
layout = find_layout(ocr(rows), parse_header(hdr(), rules), rules)
assert [s.first_row for s in layout.segments] == [0, 5]
assert layout.roles[6] is RowRole.NUMBERING and layout.roles[7] is RowRole.DATA
def test_noisy_numbering_elsewhere_stays_data(rules: Rules) -> None:
"""Мягкая проверка — только сразу под шапкой; строка с кодом формы — не нумерация."""
rows = [BALANCE_CAPTIONS, ["Запасы 1", "1210", "2", "3"], *ASSETS, ["/", "2", "3", "4"], *LIABILITIES]
layout = find_layout(ocr(rows), parse_header(hdr(), rules), rules)
assert layout.roles[1] is RowRole.DATA and layout.roles[5] is RowRole.DATA
def test_repeat_header_glued_with_numbering(rules: Rules) -> None:
"""AURORA-1517, «Будамшинское»: повторная шапка, в каждой подписи которой снизу номер столбца."""
glued = ["Наименование показателя\n1", "Коды\n2", "На 30 июня\n2026 года\n3", "На 31 декабря\n2025 года\n4"]
rows = [BALANCE_CAPTIONS, ["1", "2", "3", "4"], *ASSETS, glued, *LIABILITIES]
layout = find_layout(ocr(rows), parse_header(hdr(), rules), rules)
assert [s.first_row for s in layout.segments] == [0, 5]
assert layout.roles[5] is RowRole.HEADER and layout.roles[6] is RowRole.DATA
assert layout.segments[1].value_cols == ((2, 2), (3, 3))

View File

@ -17,7 +17,7 @@ change не входит. Шахматка — чистая функция от
`ACCOUNTS_REQUIRED`; неверные `side_1`/`measure`/`company_no` — 422 `INVALID_RULE`; не передан `rule_id` — 422 `ACCOUNTS_REQUIRED`; неверные `side_1`/`measure`/`company_no` — 422 `INVALID_RULE`; не передан `rule_id` — 422
`RULE_REQUIRED`. `RULE_REQUIRED`.
3. Показатель (`closing`/`turnover`) берётся из правила; ALL и MANUAL остаются на `closing`. 3. Показатель (`closing`/`turnover`) берётся из правила; ALL и MANUAL остаются на `closing`.
4. Применённые корректировки правила (`status = APPLIED`, тот же `rule_id` и версия консолидации) уменьшают 4. *Отменено решением владельца 21.10.2026 (п. 3 ниже), в реализацию не входит.* Применённые корректировки правила (`status = APPLIED`, тот же `rule_id` и версия консолидации) уменьшают
`amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и `amount_a` ячейки `(A, B)` на `debit_adjustment` корректировки (`legal_entity_id = A`, `counterparty_id = B`) и
`amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID `amount_b` на `credit_adjustment` корректировки `(B, A)`; расхождение, процент и порог пересчитываются, ID
корректировок попадают в `correction_ids_debit/credit`. корректировок попадают в `correction_ids_debit/credit`.
@ -38,7 +38,7 @@ change не входит. Шахматка — чистая функция от
3. **Корректировки из шахматки убираются** (решение 4 выше и `correction_ids_*` в ячейке). Причины: корректировки в 3. **Корректировки из шахматки убираются** (решение 4 выше и `correction_ids_*` в ячейке). Причины: корректировки в
ядре никто не создаёт (спринт корректировок), семантика ключа `(rule, A, B)` относительно ячейки (A, B) / (B, A) не ядре никто не создаёт (спринт корректировок), семантика ключа `(rule, A, B)` относительно ячейки (A, B) / (B, A) не
утверждена, счета корректировки и `parent_correction_id` не учитывались; §6 описывает шахматку как суммы первички. утверждена, счета корректировки и `parent_correction_id` не учитывались; §6 описывает шахматку как суммы первички.
Код корректировок выделен в ветку `AURORA-1747` и возвращается после решения по аналитике Код корректировок сохранён в ветке `wip/shahmatka-corrections-unpaired` (PR #164 не сливается) и возвращается после решения по аналитике
AURORA-1643 «Учитывание корректировок в шахматке». AURORA-1643 «Учитывание корректировок в шахматке».
4. **Ручка `…/preset/{rule_id}/unpaired-osv` убирается** (решение 5 выше). Причины: «без пары» по наличию строк 4. **Ручка `…/preset/{rule_id}/unpaired-osv` убирается** (решение 5 выше). Причины: «без пары» по наличию строк
(дефект AURORA-1651), ложные срабатывания при пересечении счетов сторон (76 в правиле № 6), нет ячейки и стороны (дефект AURORA-1651), ложные срабатывания при пересечении счетов сторон (76 в правиле № 6), нет ячейки и стороны
@ -53,7 +53,8 @@ change не входит. Шахматка — чистая функция от
- 2026-10-21. Change пишет ревьюер в ветке автора по решению владельца. Обоснование: автор не оформила OpenSpec. Цена - 2026-10-21. Change пишет ревьюер в ветке автора по решению владельца. Обоснование: автор не оформила OpenSpec. Цена
ошибки: автор переписывает change. ошибки: автор переписывает change.
- 2026-10-21. Спека описывает фактическое поведение PR; спорное вынесено в Known issues. Обоснование: домен решает - 2026-10-21. Спека описывает фактическое поведение PR; спорное вынесено в Known issues (раздел снят в раунде 2 —
все пункты закрыты автором). Обоснование: домен решает
автор и владелец, не ревьюер. Цена ошибки: после решения владельца спека меняется дельтой. автор и владелец, не ревьюер. Цена ошибки: после решения владельца спека меняется дельтой.
- 2026-10-21. Поле `as_of` удалено из `EliminationRulesOut` как мёртвое: после правок по ревью всегда `None`, в main - 2026-10-21. Поле `as_of` удалено из `EliminationRulesOut` как мёртвое: после правок по ревью всегда `None`, в main
его не было, потребителей нет. Цена ошибки: если фронт ждал `as_of`, поле нужно вернуть. его не было, потребителей нет. Цена ошибки: если фронт ждал `as_of`, поле нужно вернуть.
@ -63,3 +64,6 @@ change не входит. Шахматка — чистая функция от
(`consolidated_osv.py`) разбита на две без изменения результата (ruff E501). Цена ошибки: нулевая. (`consolidated_osv.py`) разбита на две без изменения результата (ruff E501). Цена ошибки: нулевая.
- 2026-10-21. В MODIFIED-блоке требования про правила элиминации и аудит экспорта приведён полный текст; требование - 2026-10-21. В MODIFIED-блоке требования про правила элиминации и аудит экспорта приведён полный текст; требование
«XLSX шахматки» (ALL/MANUAL) не менялось, поведение PRESET описано отдельным ADDED-требованием. «XLSX шахматки» (ALL/MANUAL) не менялось, поведение PRESET описано отдельным ADDED-требованием.
- 2026-10-22. Раунд 2 ревью: в ветку влит `main` (#163), в README возвращена фраза про XLSX результирующей таблицы,
потерянная при merge автора (b7d9794). Показатель пресета в docs (#146, §6) приведён к change: `measure` правила,
у правил ББ — `closing`. Цена ошибки: нулевая.

View File

@ -3,8 +3,8 @@
Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен Шахматка ВГО (ТЗ §6) считалась только по всем счетам (ALL) или по счетам, выбранным вручную (MANUAL). Аналитику нужен
режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3). режим «пресет»: счета и показатель берутся из правила элиминации (Прил. 3).
Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова). Реализация — PR RSHB/arm-core#141 (ветка `add-preset-shahmatka`, автор — Потапова).
Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); спорное поведение вынесено в Требования оформлены как дельты этого change'а при ревью (решение владельца 21.10.2026); решения владельца и ревью —
`design.md`, `## Known issues`, и в спеке не закреплено как окончательное. `design.md`.
## What Changes ## What Changes

View File

@ -35,14 +35,21 @@
### Requirement: XLSX шахматки пресета ### Requirement: XLSX шахматки пресета
`GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт `GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт
пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL
содержать режим «Пресет», коды счетов дебета и кредита правила, номер правила, статьи А и Б, фактическую версию правил содержать режим «Пресет», счета дебета и кредита так, как они заданы в правиле (без раскрытия по субсчетам), и отдельно
и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение исключения правила («кроме …»), номер правила, статьи сторон дебета и кредита (при `side_1 = K` статьи меняются местами
вместе со сторонами), фактическую версию правил и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение
«Сальдо»). «Сальдо»).
#### Scenario: Выгрузка по оборотам #### Scenario: Выгрузка по оборотам
- **WHEN** пользователь выгружает пресет с `measure = turnover` - **WHEN** пользователь выгружает пресет с `measure = turnover`
- **THEN** шапка содержит «Режим — Пресет», «Показатель — Оборот», номер, статьи и версию правила; имя файла заканчивается на `_rule-balance-<rule_no>.xlsx` - **THEN** шапка содержит «Режим — Пресет», «Показатель — Оборот», номер, статьи и версию правила; имя файла заканчивается на `_rule-balance-<rule_no>.xlsx`
#### Scenario: Шапка правила с исключениями
- **WHEN** выгружается пресет по правилу № 6 (seed v1: дебет `60.02, 60.22, 60.32, 62.01, 62.03, 62.21, 62.31, 76`,
исключения `76.07.5, 76.07.9`)
- **THEN** в шапке счета дебета — эти восемь кодов, отдельной строкой — «кроме 76.07.5, 76.07.9»; раскрытых субсчетов
(`76.01`, `76.02`, …) в шапке нет
#### Scenario: Правило не найдено #### Scenario: Правило не найдено
- **WHEN** `rule_id` не существует - **WHEN** `rule_id` не существует
- **THEN** 404, файл не отдаётся, событие аудита не пишется - **THEN** 404, файл не отдаётся, событие аудита не пишется

View File

@ -1,14 +1,23 @@
## 1. Реализация ## 1. Реализация
Реализация выполнена автором в PR RSHB/arm-core#141; задач плана нет. Доработки по замечаниям ревью — см. Реализация выполнена автором в PR RSHB/arm-core#141; задач плана нет. Доработки по замечаниям ревью — комментарии PR
`design.md`, `## Known issues`. (раунды 1–2) и разделы решений в `design.md`.
## 2. Доработки по решениям владельца 2026-10-21 (автор) ## 2. Доработки по решениям владельца 2026-10-21 (автор)
- [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`, - [x] 2.1 Отдельным коммитом убрать учёт корректировок (`_apply_corrections`, `correction_for_version_consolidation`,
`correction_ids_*`) вместе с тестами; код выделен в `AURORA-1747`. `correction_ids_*`) вместе с тестами; код сохранён в `wip/shahmatka-corrections-unpaired`.
- [x] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами; - [x] 2.1a Убрать ручку `unpaired-osv` (`get_unpaired_rows`, `UnpairedOsvOut`, маршрут) вместе с тестами;
код сохранён в `wip/shahmatka-corrections-unpaired`. код сохранён в `wip/shahmatka-corrections-unpaired`.
- [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест. - [x] 2.2 PRESET — только правила `statement_type = balance`; правило ОФР — 422 `INVALID_RULE`, тест.
- [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест. - [x] 2.3 Несколько открытых `version_no` — только максимальная версия; вернуть её номер, тест.
- [x] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README. - [x] 2.4 Ревьюер: убрать из дельты `consolidated-osv` требования про корректировки и `unpaired-osv`, обновить README.
## 3. Ревью, раунд 2 (2026-10-22)
- [x] 3.1 Ревьюер: влить `main`, вернуть фразу README про XLSX результирующей таблицы, поправить design/proposal/tasks.
- [x] 3.2 Ревьюер: показатель пресета в docs #146 §6 — `measure` правила.
- [ ] 3.3 Автор: название и описание PR, шапка xlsx пресета (счета правила и «кроме»), подписи статей при `side_1 = K`,
лишняя проверка в `_export_response`, реквизиты правила в `MatrixExportInfo` и `_filename`, описания ошибок preset в
OpenAPI.
- [ ] 3.4 После слияния: архив change.