openspec(#116): план 3 — вызовы пакета приведены к публичному API плана 1 (O-8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
f84e89288b
commit
0b7d691735
@ -37,7 +37,7 @@ mypy; `tea` (Gitea), OpenSpec, codegraph.
|
||||
| План | Что | Связь с этим планом |
|
||||
|---|---|---|
|
||||
| 0 | тест идентичности копий контракта по MAJOR (arm-core, tabular-parser, scan-gateway) | слит в arm-core **до** задачи A5 |
|
||||
| 1 | пакет `arm_statements` (новый репозиторий) | его раздел «Публичный API пакета» сверяется с интерфейсом-заглушкой ниже до старта части Б |
|
||||
| 1 | пакет `arm_statements` (новый репозиторий) | ядро вызывает только имена раздела «Публичный API пакета» плана 1 (сверено, раздел «Что ядро вызывает» ниже) |
|
||||
| 2 | `sync-statements`: копия пакета в `api/arm_statements/`, `statements-check` в `make check-full` | PR плана 2 в arm-core слит **до** части Б |
|
||||
| docs PR #117 | контракт v2.0, DBML v5.19, правила | слит **до** слияния PR части А (dbml-check, тест идентичности против `docs` main) |
|
||||
| 6 | arm-web: экран по новым полям, затем `sync-web` в `arm-core/web` | выкладывается вместе с ядром: `FileOut.job.warnings` меняет тип |
|
||||
@ -56,7 +56,7 @@ mypy; `tea` (Gitea), OpenSpec, codegraph.
|
||||
| Worktree arm-core | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-core-a/arm-core` | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-core-b/arm-core` |
|
||||
| Change OpenSpec arm-core | `openspec/changes/unified-normalization-core-a/` | `openspec/changes/unified-normalization-core-b/` |
|
||||
| Задачи | A0–A13 (14) | B0–B6 (7) |
|
||||
| Старт | сразу (A5 — после плана 0) | после слияния PR части А и PR плана 2 в arm-core, после сверки имён пакета (O-8) |
|
||||
| Старт | сразу (A5 — после плана 0) | после слияния PR части А и PR плана 2 в arm-core |
|
||||
|
||||
Зеркальный workspace — как в плане 0 (`context/00-workspace-layout.md`, «Worktree вместо веток»): в каталоге части
|
||||
лежат worktree `arm-core/` (ветка фичи), **отсоединённый** worktree arm-ka-docs `docs/` (до слияния #117 — на
|
||||
@ -186,45 +186,59 @@ cd $WS/.wt/normalization-core-a/arm-core && codegraph init && make venv
|
||||
|
||||
---
|
||||
|
||||
## Интерфейс-заглушка пакета (часть Б)
|
||||
## Что ядро вызывает у пакета (часть Б)
|
||||
|
||||
> **Сверить с разделом «Публичный API пакета» плана 1** до старта части Б (O-8). Ниже — имена и типы, которые ожидает
|
||||
> ядро; если в плане 1 они иные, меняется только адаптер `statements_engine.py` и эта таблица, остальной код ядра
|
||||
> работает с протоколом.
|
||||
Источник истины по именам и типам — раздел «Публичный API пакета» плана 1 (`2026-10-18-plan-1-arm-statements.md`);
|
||||
здесь они не копируются. Ядро импортирует имена верхнего уровня `arm_statements` только в
|
||||
`api/src/services/statements_engine.py` и `api/src/domain/stored_form.py` (сборка входа пересчёта); остальной код
|
||||
работает с протоколом ниже.
|
||||
|
||||
```python
|
||||
# api/src/services/statements_engine.py — единственная точка импорта arm_statements
|
||||
class StatementsEngine(Protocol):
|
||||
@property
|
||||
def version(self) -> str | None: ... # содержимое api/arm_statements/VERSION; нет файла — None
|
||||
def recompute(self, form: "StoredForm") -> "StoredFormResult": ...
|
||||
def document_status(self, status_input: "StatusInput") -> Literal["done", "needs_check"]: ...
|
||||
def code_order(self, edition: str, document_type: str, form_variant: str | None) -> tuple[str, ...]: ...
|
||||
def editions(self) -> tuple[str, ...]: ... # порядок редакций перечня: ("66n", "fsbu4")
|
||||
def relative_period_date(self, period_kind: str, effective_date: datetime.date | None) -> datetime.date | None: ...
|
||||
def tolerance(self) -> Decimal: ... # допуск ±1 словаря §7 пакета — для межформенной проверки ядра
|
||||
# api/src/services/statements_engine.py — единственная точка вызова функций arm_statements
|
||||
from arm_statements import (DocStatus, DocumentType, Edition, FormVariant, PeriodKind, RecomputeResult, Rules,
|
||||
StatusInput, StoredForm, default_rules, document_status, recompute, resolve_relative,
|
||||
statements_version)
|
||||
|
||||
def default_engine() -> StatementsEngine: ... # ArmStatementsEngine поверх копии пакета, кэш на процесс
|
||||
class StatementsEngine(Protocol): # тонкая обёртка: подмена в модульных тестах (FakeEngine), кэш правил на процесс
|
||||
@property
|
||||
def version(self) -> str | None: ... # statements_version(); нет файла VERSION — None
|
||||
def recompute(self, form: StoredForm) -> RecomputeResult: ... # recompute(form, rules)
|
||||
def document_status(self, status_input: StatusInput) -> DocStatus: ... # document_status(status_input)
|
||||
def code_order(self, edition: Edition, document_type: DocumentType, form_variant: FormVariant) -> tuple[str, ...]: ...
|
||||
# rules.code_orders.sequence(edition, document_type, form_variant)
|
||||
def editions(self) -> tuple[Edition, ...]: ... # rules.code_orders.editions — порядок редакций перечня
|
||||
def resolve_relative(self, kind: PeriodKind, effective_date: datetime.date | None) -> datetime.date | None: ...
|
||||
def tolerance(self) -> Decimal: ... # rules.checks.tolerance — допуск ±1, для межформенной проверки ядра
|
||||
|
||||
def default_engine() -> StatementsEngine: ... # ArmStatementsEngine(default_rules()), кэш на процесс
|
||||
```
|
||||
|
||||
Ожидаемые имена пакета (`arm_statements`, импорт верхнего уровня):
|
||||
|
||||
| Имя | Вид | Поля / сигнатура |
|
||||
| Вызов ядра | Имя плана 1 | Где используется |
|
||||
|---|---|---|
|
||||
| `StoredForm` | frozen dataclass | `document_type: str`, `effective_date: date \| None`, `has_organization_or_inn: bool`, `period_known: bool`, `needs_review_fields: tuple[str, ...]`, `accepted: bool`, `warnings: tuple[StoredWarning, ...]`, `columns: tuple[StoredColumn, ...]`, `lines: tuple[StoredLine, ...]` |
|
||||
| `StoredWarning` | frozen dataclass | `code: str`, `severity: Literal["error", "info"]` |
|
||||
| `StoredColumn` | frozen dataclass | `key: str`, `column_index: int`, `period_date: date \| None`, `period_kind: str` |
|
||||
| `StoredLine` | frozen dataclass | `key: str`, `row_index: int \| None`, `line_status: Literal["ok", "unreadable", "missing", "detail"]`, `line_code: str \| None`, `line_code_confidence: Decimal \| None`, `line_name: str \| None`, `code_fixed: bool`, `parent_key: str \| None`, `values: tuple[StoredValue, ...]` |
|
||||
| `StoredValue` | frozen dataclass | `column_key: str`, `amount: Decimal \| None`, `text: str \| None`, `confidence: Decimal \| None`, `is_recognized: bool \| None`, `value_fixed: bool` |
|
||||
| `recompute_stored_form` | функция | `(form: StoredForm) -> StoredFormResult` |
|
||||
| `StoredFormResult` | frozen dataclass | `form_variant: Literal["full", "simplified"] \| None`, `edition: str`, `checks: StatementChecks`, `line_needs_review: Mapping[str, bool]` (ключ строки), `value_needs_review: Mapping[tuple[str, str], bool]` (ключ строки, ключ колонки), `detail_parents: Mapping[str, str \| None]`, `status: Literal["done", "needs_check"]` |
|
||||
| `StatementChecks` | frozen dataclass | `relations_failed: tuple[FailedRelation, ...]`, `soft_relations_failed: tuple[FailedRelation, ...]` |
|
||||
| `FailedRelation` | frozen dataclass | `column_index: int`, `relation: str`, `codes: tuple[str, ...]`, `expected: Decimal \| None`, `actual: Decimal \| None` |
|
||||
| `StatusInput` | frozen dataclass | `document_type: str`, `layout_found: bool`, `has_organization_or_inn: bool`, `period_known: bool`, `lines: tuple[tuple[str, bool], ...]` (`line_status`, `needs_review`), `values: tuple[tuple[str, bool], ...]` (`line_status` строки, `needs_review`), `relations_failed: int`, `accepted: bool`, `warning_severities: tuple[str, ...]` |
|
||||
| `document_status` | функция | `(status_input: StatusInput) -> Literal["done", "needs_check"]` |
|
||||
| `load_code_orders` | функция | `() -> CodeOrders`; `CodeOrders.order(edition, document_type, variant) -> tuple[str, ...]`, `CodeOrders.editions -> tuple[str, ...]` |
|
||||
| `relative_period_date` | функция | `(period_kind: str, effective_date: date \| None) -> date \| None` (`relative:current`, `relative:prev_year_end`, `relative:prev_prev_year_end`, `relative:prev_period`) |
|
||||
| `load_check_rules` | функция | `() -> CheckRules` с полем `tolerance: Decimal` |
|
||||
| `engine.recompute(form)` | `recompute(form: StoredForm, rules) -> RecomputeResult` | B2 (после `PATCH`), B3 (приём `1.x`), B4 (только `edition`) |
|
||||
| `engine.document_status(...)` | `document_status(StatusInput) -> DocStatus` | B4 (страховка статуса результата `2.0`) |
|
||||
| `engine.code_order(...)` | `default_rules().code_orders.sequence(edition, form, variant)` | B4, B6 |
|
||||
| `engine.editions()` | `default_rules().code_orders.editions` | B6 |
|
||||
| `engine.resolve_relative(...)` | `resolve_relative(kind, effective)` | B2 (правка `header.report_date`) |
|
||||
| `engine.tolerance()` | `default_rules().checks.tolerance` | B6 |
|
||||
| `engine.version` | `statements_version()` | B2 (`STATEMENTS_VERSION_MISMATCH`) |
|
||||
| `StoredForm`, `StoredColumn`, `StoredLine`, `StoredValue`, `StoredWarning` | одноимённые dataclass-ы пакета | B1 (`build_stored_form`) |
|
||||
| `StatusInput`, `StatusLine`, `StatusValue` | одноимённые dataclass-ы пакета | B1 (`status_input_from_v2`) |
|
||||
| `FailedRelation`, `StatementChecks` | из `RecomputeResult.checks` | B2 (`relations_to_issues`) |
|
||||
|
||||
Соглашения на границе ядра и пакета (все — в `stored_form.py` / адаптере, остальной код ядра их не видит):
|
||||
|
||||
- Перечисления пакета — `StrEnum` (подкласс `str`, значения — строки контракта и БД): строка БД превращается в
|
||||
`LineStatus(...)`, `DocumentType(...)`, `PeriodKind(...)`, `Severity(...)` при сборке `StoredForm`; `period_kind = NULL`
|
||||
в БД → `PeriodKind.NONE`. Обратно значения пишутся в БД как `str(...)` / `.value`.
|
||||
- Уверенности пакета — `float | None` (0–100), в БД — `Numeric(5,2)`: при сборке `float(confidence)`, `None` остаётся `None`.
|
||||
`StoredValue.is_recognized` — `bool`: `NULL` в БД читается как `False`.
|
||||
- `StoredColumn.column_index: int | None` (колонка без номера столбца допустима); ключи строки и колонки — `str(uuid)`.
|
||||
- `RecomputeResult.edition: Edition | None` — `None` при нераспознанной редакции (нет даты и ничья): проверки по редакции
|
||||
(`CODE_NOT_IN_EDITION`) тогда не пишутся.
|
||||
- `FailedRelation` несёт `column_index: int | None` и `column_key: str`; при пересчёте из БД `column_key` — это
|
||||
`StoredColumn.key`, то есть `str(column_id)`: колонка находится по ключу без обратного сопоставления по индексу.
|
||||
`expected` и `actual` — всегда `Decimal`.
|
||||
|
||||
Ключи хоста — `str(uuid)` строки и колонки (`key`, `column_key`, `parent_key`).
|
||||
|
||||
@ -267,7 +281,15 @@ def default_engine() -> StatementsEngine: ... # ArmStatementsEngine повер
|
||||
- **O-6.** Действие аудита при смене `crm_id` ГСК/ГСЗ (P3-18): сейчас правки ГСК/ГСЗ пишут `MEMBERSHIP_CHANGE`.
|
||||
- **O-7.** Конверт с неизвестным MAJOR (`3.0`) inbox, как и сейчас, отбрасывает с предупреждением в лог (задание закроется
|
||||
по сроку); сценарий спеки «файл получает `ERROR` с `mapping_error`» выполняется для файла результата. Оставить?
|
||||
- **O-8.** Имена публичного API пакета — по заглушке выше; свести с планом 1 до старта части Б.
|
||||
- **O-8.** Сверка с планом 1 выполнена (ядро вызывает имена его раздела «Публичный API пакета»). Пробелы API пакета для
|
||||
ядра, план 1 не правился: (а) типы возврата `Rules.code_orders.sequence` (`tuple[str, ...]`?), `.editions`
|
||||
(`tuple[Edition, ...]`?) и `Rules.checks.tolerance` (`Decimal`?) в плане 1 не заданы, а параметр `variant` у `sequence`
|
||||
— `FormVariant` или `FormVariant | None` (для `DISCLOSURE`) не сказан; ядро предполагает перечисленные в «Что ядро
|
||||
вызывает»; (б) `Edition` формы без строк, то есть в B4 при приёме `2.0`, публично определяется только через
|
||||
`recompute(StoredForm)` (ядро строит `StoredForm` из документа результата ради одного поля `edition`) — нужна
|
||||
функция вроде `choose_edition(effective, codes)`; (в) поведение `resolve_relative` для `ABSOLUTE` / `NONE` не задано
|
||||
(ядро вызывает её только для `relative:*`); (г) `StatusInput` собирает хост — функции «документ результата ->
|
||||
`StatusInput`» в пакете нет, `status_input_from_v2` (B1) её заменяет.
|
||||
- **O-9.** Выкладка: PR части А в `main` без снимка контура до части Б; встроенный фронт `arm-core/web` получает новые типы
|
||||
только через `sync-web` после плана 6 — выкладка ядра и фронта одним снимком.
|
||||
- **O-10.** Метрики `fields_*` ядро сейчас не считает по строкам формы (копирует из конверта, `recognition_exchange.FIELD_COUNTERS`):
|
||||
@ -1003,8 +1025,7 @@ def test_has_error_warning():
|
||||
## Часть Б
|
||||
|
||||
> Старт — после слияния PR части А и PR плана 2 (копия `api/arm_statements/`, `mypy src scripts arm_statements`,
|
||||
> `statements-check` в `check-full`) в arm-core и после сверки интерфейса-заглушки с планом 1 (O-8). Worktree части Б
|
||||
> создаётся от `origin/main` с обоими слияниями.
|
||||
> `statements-check` в `check-full`) в arm-core. Worktree части Б создаётся от `origin/main` с обоими слияниями.
|
||||
|
||||
### Task B0: Change OpenSpec части Б в arm-core
|
||||
|
||||
@ -1024,32 +1045,44 @@ def test_has_error_warning():
|
||||
- Test: `api/tests/test_domain_stored_form.py`, `api/tests/test_statements_engine.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: копия `arm_statements` (имена — таблица «Интерфейс-заглушка пакета»).
|
||||
- Consumes: копия `arm_statements` (имена — раздел «Публичный API пакета» плана 1; вызовы ядра — раздел «Что ядро вызывает»).
|
||||
- Produces:
|
||||
- `StatementsEngine` (протокол) и `default_engine()` — как в разделе «Интерфейс-заглушка»; `ArmStatementsEngine.version`
|
||||
читает `api/arm_statements/VERSION` (`strip()`), нет файла — `None`;
|
||||
- `StatementsEngine` (протокол) и `default_engine()` — как в разделе «Что ядро вызывает»; `ArmStatementsEngine.version`
|
||||
возвращает `statements_version()` (содержимое `api/arm_statements/VERSION`; нет файла — `None`), `recompute` /
|
||||
`code_order` / `editions` / `tolerance` берут `default_rules()` пакета;
|
||||
- `build_stored_form(document: Document, meta: FinancialStatementMeta, columns: Sequence[FinancialStatementColumn],
|
||||
lines: Sequence[tuple[FinancialStatementLine, Sequence[FinancialStatementValue]]], warnings: Sequence[DocumentIssue])
|
||||
-> StoredForm`: `effective_date = document.reporting_date`; `has_organization_or_inn` — непустые
|
||||
-> StoredForm` (типы пакета, перечисления — `StrEnum`: `DocumentType`, `LineStatus`, `PeriodKind`, `Severity`;
|
||||
уверенности — `float(...)`, `NULL` значения `is_recognized` — `False`; `period_kind = NULL` — `PeriodKind.NONE`):
|
||||
`effective_date = document.reporting_date`; `has_organization_or_inn` — непустые
|
||||
`meta.organization_name` или `meta.inn`; `period_known = document.reporting_date is not None`;
|
||||
`needs_review_fields = tuple(meta.needs_review_fields or ())`; `accepted = meta.checks_accepted_at is not None`;
|
||||
`warnings` — записи `kind = warning` (`code`, `severity`); `code_fixed = bool(line.line_code_modified) or
|
||||
line.is_confirmed`; `value_fixed = value.is_modified or value.is_confirmed`; `text = value.source_text`;
|
||||
`parent_key` — ключ строки с `row_index == line.parent_row_index` (у `detail`), иначе `None`;
|
||||
- `status_input_from_v2(doc: StatementDocumentV2) -> StatusInput` — вход страховки статуса из результата: `layout_found =
|
||||
bool(doc.lines)`, `has_organization_or_inn` — по `header.organization_name`/`header.inn`, `period_known` —
|
||||
`header.report_date` или хотя бы одна колонка с `period_date`, строки и значения с `needs_review`, `relations_failed =
|
||||
len(checks.relations_failed)` (нет `checks` — 0), `accepted = False`, уровни `doc.warnings`;
|
||||
- `FakeEngine` для модульных тестов: возвращает заданный `StoredFormResult`, запоминает вход.
|
||||
`parent_key` — ключ строки с `row_index == line.parent_row_index` (у `detail`), иначе `None`; `key` строки и колонки —
|
||||
`str(uuid)`;
|
||||
- `status_input_from_v2(doc: StatementDocumentV2) -> StatusInput` — вход страховки статуса из результата (поля
|
||||
`StatusInput` плана 1): `document_type = DocumentType(doc.document_type)`, `layout_found = bool(doc.lines)`,
|
||||
`has_organization_or_inn` — по `header.organization_name`/`header.inn`, `period_known` — `header.report_date` или
|
||||
хотя бы одна колонка с `period_date`, `needs_review_fields = tuple(header.needs_review_fields)`,
|
||||
`lines = tuple(StatusLine(LineStatus(l.line_status), l.needs_review))`, `values = tuple(StatusValue(LineStatus(l.line_status),
|
||||
v.needs_review))` по значениям каждой строки (`line_status` — статус строки значения), `hard_failures =
|
||||
len(checks.relations_failed)` (нет `checks` — 0), `accepted = False`, `warning_severities = tuple(Severity(w.severity)
|
||||
for w in doc.warnings)`;
|
||||
- `FakeEngine` для модульных тестов: возвращает заданный `RecomputeResult`, запоминает вход `StoredForm`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests:** `test_build_stored_form_maps_flags` (шаблонная строка → `row_index is None`;
|
||||
правленая ячейка → `value_fixed`; подтверждённая ячейка → `value_fixed`; строка с назначенным кодом → `code_fixed`;
|
||||
`detail` с `parent_row_index = 3` → `parent_key` строки `row_index 3`; `warnings` — только `kind = warning`);
|
||||
`test_status_input_from_v2` (на `contracts/examples/result-statement-minimal.json`: `layout_found is True`,
|
||||
`relations_failed == 0`, строка `unreadable` во входе); `test_engine_scenario_recompute_after_edit` (реальный пакет,
|
||||
`hard_failures == 0`, `needs_review_fields` из шапки, строка `unreadable` во входе в `lines` и `values`); `test_engine_scenario_recompute_after_edit` (реальный пакет,
|
||||
сценарий спеки «Пересчёт после правки по строкам БД»: ячейка 1230 `value_fixed`, код 1250 `code_fixed` у бывшей
|
||||
`unreadable`, шаблонная 1260 с суммой, `detail` с уверенностью 60 → у 1230 и у значения `detail` `needs_review` `False`,
|
||||
у строки 1250 `False`, соотношение `1200 = …` посчитано с 1250 и 1260); `test_engine_version_reads_file`.
|
||||
у строки 1250 `False`, соотношение `1200 = …` посчитано с 1250 и 1260; ожидаемый `RecomputeResult.checks` и
|
||||
`value_needs_review` — с ключами `(str(line_id), str(column_id))`, `edition` — `Edition.FSBU4` или `RAS_66N` по дате);
|
||||
`test_stored_form_types` (уверенность `Decimal("67.50")` в БД → `67.5` типа `float`; `is_recognized = NULL` → `False`;
|
||||
`period_kind = NULL` → `PeriodKind.NONE`; `line_status` — `LineStatus`); `test_engine_version_reads_file`
|
||||
(`engine.version == statements_version()`; `statements_version(tmp_path / "VERSION")` без файла — `None`).
|
||||
- [ ] **Step 2:** `cd api && .venv/bin/pytest tests/test_domain_stored_form.py tests/test_statements_engine.py -v` — FAIL.
|
||||
- [ ] **Step 3: Implement**; `ArmStatementsEngine` кэшируется на процесс (`functools.cache` у `default_engine`).
|
||||
- [ ] **Step 4:** запуск и `make check` (в т. ч. `mypy src scripts arm_statements`) — PASS.
|
||||
@ -1071,17 +1104,21 @@ def test_has_error_warning():
|
||||
- `async def recompute_form(session, *, document, meta, engine: StatementsEngine, job: RecognitionJob | None) -> None`:
|
||||
строит вход, вызывает `engine.recompute`, пишет `needs_review` строк и значений, `meta.form_variant`,
|
||||
`parent_row_index` строк `detail` (ключ → `row_index`), заменяет записи `relation`/`soft` (любого источника) и записи
|
||||
`core` по P3-8, пишет `STATEMENTS_VERSION_MISMATCH` (`info`, `core`), если `job.statements_version` не `None` и не равен
|
||||
`engine.version`; статус (кроме `CONFIRMED`) — `form_status_v2(service_done = result.status == "done",
|
||||
`core` по P3-8 (`RecomputeResult` пакета: `form_variant: FormVariant | None`, `edition: Edition | None`,
|
||||
`checks`, `line_needs_review`, `value_needs_review`, `detail_parents`, `status: DocStatus`), пишет `STATEMENTS_VERSION_MISMATCH` (`info`, `core`), если `job.statements_version` не `None` и не равен
|
||||
`engine.version`; статус (кроме `CONFIRMED`) — `form_status_v2(service_done = result.status == DocStatus.DONE,
|
||||
file_error_warning = has_error_warning(job.metrics["warnings"]), counterparty, period_known = year и quarter,
|
||||
names_ok = нет NAME_MISMATCH, duplicate_in_file)`;
|
||||
- `relations_to_issues(checks, *, columns, aggregates, source: IssueSource) -> list[IssueDraft]` — `relation` →
|
||||
`RELATION_FAILED`/`error`, `soft` → `RELATION_SOFT`/`info`; `column_id` по `column_index`; `line_ids` — все строки `ok`
|
||||
кодов `codes`; `message` — `f"не сошлось {relation}: {expected} ≠ {actual}"`;
|
||||
- `relations_to_issues(checks: StatementChecks, *, columns, aggregates, source: IssueSource) -> list[IssueDraft]` —
|
||||
`relations_failed` → `RELATION_FAILED`/`error`, `soft_relations_failed` → `RELATION_SOFT`/`info` (`FailedRelation`
|
||||
плана 1); `column_id` — по `FailedRelation.column_key` (при пересчёте из БД это `str(column_id)`; при приёме `1.x`
|
||||
колонки уже вставлены и ключ строится так же) — по `column_index` только для проверок сервиса `2.0`
|
||||
(`column_key = str(column_index)`, B4); `line_ids` — все строки `ok` кодов `codes`; `expected`/`actual` — `Decimal`;
|
||||
`message` — `f"не сошлось {relation}: {expected} ≠ {actual}"`;
|
||||
- `StatementService.recompute` вызывает `recompute_form` (тело A12 удаляется, `nearest_ok_parents` A13 удаляется);
|
||||
- `statement_hard_failures` и `checks.failed`/`soft_failed` ответа — из записей `relation`/`soft` (с `codes`,
|
||||
`line_ids`); `NOTHING_TO_ACCEPT` — нет записей `relation`; правка `header.report_date` (в т. ч. `null`) пересчитывает
|
||||
`period_date` колонок с `period_kind` `relative:*` через `engine.relative_period_date` до пересчёта.
|
||||
`period_date` колонок с `period_kind` `relative:*` через `engine.resolve_relative(PeriodKind(kind), effective_date)` до пересчёта.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests:**
|
||||
- `test_1x_form_after_patch_recomputed` (сценарий «1.x-документ после PATCH») — форма `1.2` с несошедшимся `1200 = 1210 +
|
||||
@ -1093,7 +1130,7 @@ def test_has_error_warning():
|
||||
- `test_unconfirm_uncovered_cell` (сценарий «Снятие подтверждения») — ячейка, не покрытая соотношением → `needs_review
|
||||
is True` после пересчёта;
|
||||
- `test_statements_version_mismatch` — `job.statements_version = "abc1234"`, `engine.version = "def5678"` → запись
|
||||
`STATEMENTS_VERSION_MISMATCH` после `PATCH`; `job.statements_version = None` — нет;
|
||||
`STATEMENTS_VERSION_MISMATCH` после `PATCH` (`FakeEngine.version` / реальный `statements_version()`); `job.statements_version = None` — нет;
|
||||
- `test_relative_column_follows_report_date` — колонка `relative:prev_year_end`, `PATCH header.report_date =
|
||||
2025-09-30` → `period_date == 2024-12-31`; после ручной даты (A12) — не меняется; `report_date = null` → `period_date
|
||||
is None` у относительных;
|
||||
@ -1111,7 +1148,7 @@ def test_has_error_warning():
|
||||
- Test: `api/tests/test_mapping_statements.py`, `api/tests/test_api_statements.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `engine.recompute`, `build_stored_form`, `relations_to_issues` (B1, B2).
|
||||
- Consumes: `engine.recompute` (`RecomputeResult.form_variant`, `.checks`; `.status` здесь не используется), `build_stored_form`, `relations_to_issues` (B1, B2).
|
||||
- Produces: `async def checks_on_intake_1x(session, *, document, meta, engine) -> bool` — один вызов
|
||||
`engine.recompute` по вставленной форме; пишет `meta.form_variant` и записи `relation`/`soft` (`source = core`); возвращает
|
||||
«жёстких нарушений нет» — условие D-6 «проверки сошлись»; `needs_review` значений и строк **не** перезаписывает (у `1.x`
|
||||
@ -1137,7 +1174,7 @@ def test_has_error_warning():
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: модели `result_v2` (A5), `IssueDraft`/`add_issues` (A9), `form_status_v2`, `duplicate_period_forms`,
|
||||
`name_mismatches`, `has_error_warning` (A10), `engine.document_status`, `engine.recompute` (только `edition`),
|
||||
`name_mismatches`, `has_error_warning` (A10), `engine.document_status`, `engine.recompute` (только `RecomputeResult.edition`, O-8б),
|
||||
`engine.code_order`, `status_input_from_v2` (B1).
|
||||
- Produces:
|
||||
- `build_statement_v2(doc: StatementDocumentV2) -> StatementDraftV2` — строки как пришли (`row_index`, `source_ref =
|
||||
@ -1151,10 +1188,10 @@ def test_has_error_warning():
|
||||
`inn_confidence`, `needs_review_fields`, `form_variant`; P3-9;
|
||||
- `service_issues(doc, *, line_ids_by_ref, column_ids_by_index, ok_line_ids_by_code) -> list[IssueDraft]` —
|
||||
`doc.warnings` (`warning`, `service`; `line_refs` → `line_ids`; `column_index` → `column_id`, если колонка есть, иначе
|
||||
`column_index`) и `doc.checks` (`relation`/`soft`, `service`);
|
||||
- `core_info_issues(*, lines, edition, form_variant, catalog, engine) -> list[IssueDraft]` — `CODE_NOT_IN_DICTIONARY`,
|
||||
`CODE_NOT_IN_EDITION` (P3-7), `NAME_MISMATCH` (строка);
|
||||
- статус формы: `service_done = doc.status == "done" and engine.document_status(status_input_from_v2(doc)) == "done"`;
|
||||
`column_index`) и `doc.checks` (`relation`/`soft`, `service`; `column_id` — по `column_index` колонки: у сервиса `FailedRelation.column_key = str(column_index)`);
|
||||
- `core_info_issues(*, lines, edition: Edition | None, form_variant: FormVariant | None, catalog, engine) -> list[IssueDraft]` — `CODE_NOT_IN_DICTIONARY`,
|
||||
`CODE_NOT_IN_EDITION` (P3-7), `NAME_MISMATCH` (строка); `edition is None` или `form_variant is None` — проверки по редакции не пишутся;
|
||||
- статус формы: `service_done = doc.status == DocStatus.DONE and engine.document_status(status_input_from_v2(doc)) == DocStatus.DONE`;
|
||||
`doc.status == "done"` при несогласии — запись `RESULT_STATUS_INCONSISTENT` (`info`, `core`, у документа);
|
||||
`file_error_warning = has_error_warning(result.metrics.warnings)` (P3-15); `RECOGNIZED` → автоподтверждение (как сейчас);
|
||||
- `job.statements_version = result.service.statements_version`; `DISCLOSURE` — один документ на файл (P3-6),
|
||||
@ -1225,13 +1262,13 @@ def test_has_error_warning():
|
||||
- Delete: `api/rules/statement_checks.yaml`
|
||||
- Modify: `api/src/domain/statement_checks.py` (остаются `cross_form_check` и `subline_parents`; `RelationRule`,
|
||||
`CheckRules`, `load_check_rules`, `run_checks`, `FailedCheck.codes` удаляются), `api/src/services/statements.py`
|
||||
(`checks_report`: допуск — `engine.tolerance()`), `api/src/domain/statement_lines.py` (порядок и состав редакций —
|
||||
`engine.editions()`), `CLAUDE.md` (раздел «Формы ББ/ОФР»: §7 — копия `arm_statements`, проверки хранятся в
|
||||
(`checks_report`: допуск — `engine.tolerance()` = `default_rules().checks.tolerance`), `api/src/domain/statement_lines.py` (порядок и состав редакций —
|
||||
`engine.editions()` = `default_rules().code_orders.editions` (`Edition`)), `CLAUDE.md` (раздел «Формы ББ/ОФР»: §7 — копия `arm_statements`, проверки хранятся в
|
||||
`document_issue`), `README.md`
|
||||
- Test: `api/tests/test_domain_statement_checks.py`, `api/tests/test_domain_statement_lines.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `engine.tolerance()`, `engine.editions()`, `engine.code_order()` (B1).
|
||||
- Consumes: `engine.tolerance()`, `engine.editions()`, `engine.code_order(edition, document_type, form_variant)` (B1; `Rules.code_orders.sequence` плана 1).
|
||||
- Produces: `cross_form_check(balance_1370, income_2400, tolerance: Decimal) -> FailedCheck | None` без изменений
|
||||
сигнатуры; `test_name_dictionary_codes_in_code_orders` — каждый код `statement_lines.yaml` редакции есть в перечне
|
||||
этой редакции копии пакета (любой вариант формы).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user