Andrey Epifancev 1146c0bc3f plan(#116): план 6 — экран на дополненном API ядра
line_code_confidence и okei в типах, header.source.confidence — строка, подсказка кода из
StatementOut.edition_lines, ошибка элемента lines[] у поля строки по detail.line_id
(lineErrorTarget вместо codeTakenKeys), значок документа в панели файлов по
DocumentBrief.issue_counts (rowBadge). Закрыты O-1…O-6, снят В-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 13:02:40 +04:00

934 lines
100 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План 6 — arm-web: экран формы ББ/ОФР и карточка файла по API v5.19. План реализации
> **Для исполнителей-агентов:** обязательный навык — superpowers:subagent-driven-development (в плане 12 задач, задачи
> опираются на интерфейсы друг друга). Контроллер SDD — фоновый сабагент на модели **sonnet**; исполнители — по SDD Model
> Selection (`~/.claude/CLAUDE.md`, «Исполнение»): T1 — haiku (типы и фикстура заданы планом), остальные — sonnet,
> финальное ревью ветки — opus. Шаги размечены чекбоксами (`- [ ]`).
**Goal:** экран проверки формы ББ/ОФР [41] показывает все строки источника на своих местах (строки без кода — под
ближайшей предыдущей строкой с кодом, расшифровки — под переключателем), подсвечивает только то, что требует проверки
(`needs_review`), показывает предупреждения и нарушения у места, даёт в режиме «Редактировать» разобрать строки,
подтвердить ячейки, коды и даты одним `PATCH`, блокирует «Подтвердить данные» при неразобранных строках; карточка файла
читает `job.warnings` структурами.
**Architecture:** вся логика — чистые функции в `.ts` рядом с компонентами (`src/widgets/DocumentReview/`,
`src/widgets/FilesDrawer/`), покрытые vitest; компоненты React только рисуют то, что вернули функции, и не тестируются
юнит-тестами (соглашение `arm-web/CLAUDE.md`). Черновик правок аналитика — одна структура `StatementDraft`, из которой
одна функция собирает тело единственного `PATCH`. Экран проверяется сценарием в Playwright на локальном стенде; до того
как сервисы распознавания начнут слать контракт 2.0, строки со статусами подставляются в ответ `GET …/statement`
перехватом запроса (фикстура плана).
**Tech Stack:** React 18 + TypeScript strict, antd 6 (`Table`, `AutoComplete`, `Dropdown`, `Popover`, `Tooltip`,
`Switch`), rsbuild, vitest **3.1.4**, biome + ESLint + Prettier; Playwright (MCP-плагин сессии) для сценария; `tea`
(Gitea), OpenSpec.
**Spec:** `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/`
(ветка `feat/116-unified-normalization` репозитория arm-ka-docs): главное — `specs/arm-web/spec.md`; API, которое экран
потребляет, — `specs/arm-core/spec.md` (требования «Ответ `GET …/statement` по v2.0», «PATCH формы — строки, колонки,
подтверждение, наименование организации», «Подтверждение формы — порядок 409», «Предупреждения и нарушения документа —
`document_issue`», «Предупреждения файла и ОСВ в API»); гайд фронта — `dev/frontend-api-guide.md` §5.6.1; правила —
`context/05-domain-rules.md` §9.5 п. 2, §11.9 (таблица предупреждений: коды и уровни); точные схемы ответов и коды —
план 3 (`plans/2026-10-18-plan-3-arm-core.md`), задачи A5, A8, A11, A12, A13; решения — `design.md` (Р-14 экран, Р-23
значения `detail`, Р-25 подтверждение без правки, S-62 родитель `detail`, S-47 пробелы PATCH). Макет — `mockup/screens/41-document-check.png`,
`41-02-document-check-edit.png`, `39-org-documents.png`.
---
## Место в работе и порядок
| План | Что | Связь с этим планом |
|---|---|---|
| 3, часть А | arm-core: схема v5.19, `StatementOut` с новыми полями, `PATCH` (`lines`, `columns`, `confirmed`, `organization_name`), порядок 409, `JobBrief.warnings` структурами | задачи T1–T10 пишутся по спеке и фикстуре **до** слияния А; T11 (стенд) — после слияния А в `main` arm-core |
| 3, часть Б | приём контракта 2.0, §7 в `document_issue` | для T11 желательна (нарушения §7 в `issues` у форм 1.x появляются с Б) |
| сервисы (шаг 5 `proposal.md`) | tabular-parser и scan-gateway шлют 2.0 | до них реальных строк `unreadable` / `missing` / `detail` на стенде нет — их показывает перехват (P6-14); повторный прогон на `aep_3` после шага 5 — follow-up |
| `sync-web` | перенос `arm-web@test` в `arm-core/web` | после merge PR этого плана в `test`; снимок контура — только вместе с ядром А + Б (O-9 плана 3) |
**Выкладка:** фронт выходит в той же выкладке, что ядро: `FileOut.job.warnings` меняет тип со строк на объекты
(несовместимо), а встроенный фронт контура — `arm-core/web`, который обновляется только `make -C docs sync-web`.
## Как это устроено — для владельца
- **Раскладка «как на бумаге».** Ядро отдаёт строки списком; разделы баланса и места итогов строит фронт
(`formLayout.ts`, так уже работает с Среза 3b). Новое: строки без кода (`unreadable`, `missing`) и расшифровки
(`detail`) не участвуют в раскладке по коду, а «прицепляются» к строке с кодом — к той, что стояла перед ними в файле
(`row_index`), расшифровка — к своему родителю (`parent_row_index`). Это отдельная чистая функция, её проверяют тесты
на сценариях спеки («ИЧ,250 под 1240»).
- **Почему черновик, а не запрос на каждое действие.** Спека требует: «Сохранить» отправляет **один** `PATCH`. Поэтому
всё, что аналитик делает в режиме «Редактировать» (суммы, подтверждения ячеек, коды строк, даты колонок, шапка),
копится в одном объекте `StatementDraft`, а функция `buildStatementPatch` превращает его в тело запроса и заранее
ловит ошибки, которые иначе вернул бы сервер (неверный формат кода или даты, конфликт «шаблонная строка + тот же код»).
Ответ сервера целиком заменяет состояние экрана — так пересчитанные ядром статусы и проверки всегда актуальны.
- **Почему тесты только на функции.** Так принято в arm-web: компоненты не тестируются, а всё, что решает «что
показать», вынесено в `.ts`-функции. Компоненты проверяются руками в браузере по сценарию T11 со сверкой с макетом.
- **Новых npm-пакетов нет:** всё нужное (выпадающая подсказка кода, меню строки, всплывающий список) есть в antd; в
зеркале npm банка не всё доступно (vitest ≤ 3.1.4).
## Разбивка на PR
| | |
|---|---|
| Issue | RSHB/arm-web: «Единая нормализация ББ/ОФР: экран формы и карточка файла по API v5.19 (arm-ka-docs#116)» |
| Ветка | `feat/<issue>-statements-v2` (база — `origin/test`) |
| Зеркальный workspace | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/` |
| Worktree arm-web | `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/arm-web` |
| Change OpenSpec arm-web | `openspec/changes/statements-v2/` |
| PR | в `test` (не в `main`); merge — только по команде владельца |
| После merge | `make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web` → PR в arm-core (`arm-core/web` напрямую не править) |
## Global Constraints
- Типы `src/types/api.ts` повторяют схемы ядра v5.19 (спека `arm-core` «Ответ `GET …/statement` по v2.0», план 3 A5/A8)
и гайд §5.6.1; полей, которых там нет, не придумывать.
- Все запросы — через `src/api/client.ts` (`api.get|post|patch`); прямого `fetch` нет.
- Суммы — строки (`string | null`); арифметики над суммами на фронте нет; `Number()` — только для уверенности (0..100).
- Подсветка ячейки «требует внимания» — **только** по `values[].needs_review`; по `confidence < 100` и
`is_recognized = false` напрямую не подсвечивать; «исправлено» (`is_modified`) — прежний синий.
- Тексты интерфейса — дословно: «Показать расшифровки (N)», «Разберите строки без кода: N», «Это расшифровка»,
«Назначить код», «Сделать расшифровкой», «Подтвердить код», «Подтвердить данные», «Редактировать», «Сохранить».
- Переключателя «как в файле» нет (Р-14).
- «Сохранить» отправляет ровно один `PATCH /documents/{id}/statement` (`header`, `values`, `columns`, `lines`); пустой
черновик — запроса нет; ответ заменяет `StatementOut` целиком.
- Коды ошибок — дословно: 409 `LINES_UNRESOLVED` (`detail.lines[] {line_id, row_index, line_code_text}`), `DUPLICATE_FORM`
(`detail.document_id`), `LINE_CODE_TAKEN` (`detail.line_id`, `detail.line_code`), `PERIOD_REQUIRED`,
`CHECKS_NOT_PASSED` (`detail.failures`); 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`,
`LINE_CODE_REQUIRED` (у всех четырёх — `detail.line_id`, `detail.line_code` первого неверного элемента, план 3
P3-24), `UNKNOWN_COLUMN`, `UNKNOWN_CELL`.
- Новых зависимостей npm нет; `vitest` остаётся `3.1.4`; `package-lock.json` в git не попадает.
- Компоненты (`.tsx`) юнит-тестами не покрываются; каждая чистая функция плана — в `.ts` с `.test.ts` рядом.
- Перед коммитом — `npm run format`; зелёным считается `make lint typecheck test build` (P6-18); перед PR — то же, вывод
в описание PR.
- Язык комментариев, коммитов, issue, PR — русский; идентификаторы — английские, как в API.
- Коммиты заканчиваются строками `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` и `Claude-Session:
https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W`; описание PR — строкой `🤖 Generated with [Claude
Code](https://claude.com/claude-code)`.
- Находки и решения — с маркерами (`T<n>.<k>`, `C-n`/`I-n`/`M-n`, `R-n`, `O-n`, `H-n`); решения этого плана — `P6-n`
(ниже); исполнитель переносит их в `openspec/changes/statements-v2/design.md` arm-web, раздел «Decisions made without
discussion».
- Каждый экран после правок сверяется с PNG макета (`docs/mockup/screens/41-*.png`, `39-*.png`) при окне 1600×1000 и
прогоняется самим исполнителем в браузере (память владельца «сверять фронт с макетом»).
## Review Focus
1. **Якорь строки без кода, который скрыт или повторён** — строка `unreadable` после пустой пятизначной подстроки
(скрыта по умолчанию), после второй из двух строк 2410, после строки в «Прочих строках»: вложение стоит под своим
экземпляром (по ключу строки, не по коду) и не исчезает, когда якорь скрыт. Тесты — T2.
2. **Предупреждение у скрытой или несуществующей строки** — запись `issues` у строки `detail` при выключенном
переключателе (ссылка «Показать» включает расшифровки и ведёт к строке); `line_ids` со строкой, которой нет в ответе
(удалённая шаблонная), и поле шапки, которого нет в блоке шапки (`okpo`, `okei`), — уходят в список документа, а не
теряются. Тесты — T4.
3. **Смешанный черновик в одном сохранении** — сумма и подтверждение одной ячейки, действие строки и сумма той же
строки, сумма в шаблонную строку 1250 и назначение 1250 строке без кода, пустое поле кода, код, равный текущему:
тело `PATCH` не нарушает правил ядра (одно действие на строку, `UNKNOWN_CELL` по P3-10), пустое не отправляется.
Тесты — T6.
4. **Форма, принятая по 1.x** (все строки `ok`, `row_index` по порядку, `source_ref = null`, у чистых значений
`needs_review = false`, в `issues` — `MAPPING_WARNING`) — экран выглядит как до изменения, кроме подсветки по
`needs_review`; переключателя расшифровок нет (N = 0); кнопка «Подтвердить данные» не заблокирована. Тесты — T2,
T9; сценарий — T11.
5. **`job.warnings` старого формата** — строки, смесь строк и объектов, не список, объект без `severity`: панель файлов
не падает, строки показываются как `LEGACY_WARNING` `info`, мусор пропускается. Тесты — T10.
---
## Карта файлов (arm-web, относительно корня репозитория)
| Файл | Ответственность |
|---|---|
| `src/types/api.ts` | DTO v5.19: строки, значения, колонки, шапка, `issues`, `JobBrief.warnings`, тела `PATCH` |
| `src/utils/labels.ts` | подписи статуса строки и уровня предупреждения |
| `src/utils/dates.ts` | проверка даты `ГГГГ-ММ-ДД` |
| `src/widgets/DocumentReview/__fixtures__/statement-v2.json` | эталонный `StatementOut` v2 для тестов и перехвата в Playwright |
| `src/widgets/DocumentReview/statementGrid.ts` | `StatementOut` → строки сетки `GridRow` с полями v2 |
| `src/widgets/DocumentReview/cellView.ts` | вид ячейки значения и ячейки кода: фон, подсказка, приглушённый текст |
| `src/widgets/DocumentReview/formLayout.ts` | раскладка по разделам + вложения (`unreadable` / `missing` / `detail`), фильтр показа, имена |
| `src/widgets/DocumentReview/issuePlaces.ts` | привязка `issues` к месту (строка, ячейка, колонка, поле, документ), пункты списка |
| `src/widgets/DocumentReview/IssuesPanel.tsx` | список предупреждений документа со ссылкой «Показать» |
| `src/widgets/DocumentReview/headerFields.ts`, `StatementHeaderBlock.tsx` | шапка: наименование организации, `needs_review_fields`, подсказки из `header.source` |
| `src/widgets/DocumentReview/statementDraft.ts` | черновик правок и сборка тела `PATCH` (заменяет `draftToPatch`) |
| `src/widgets/DocumentReview/codeSuggestions.ts`, `lineMenu.ts` | подсказка кода из `edition_lines`, пункты меню строки, ошибка элемента `lines[]` у поля строки |
| `src/widgets/DocumentReview/editRules.ts` | когда у колонки поле даты и у ячейки кнопка подтверждения |
| `src/widgets/DocumentReview/CodeCell.tsx`, `LineMenu.tsx`, `ValueCell.tsx`, `ColumnHeader.tsx` | ячейки таблицы и заголовок колонки |
| `src/widgets/DocumentReview/StatementTable.tsx`, `StatementReview.tsx`, `ChecksPanel.tsx` | сборка экрана, переключатели, сохранение, переход к строке |
| `src/widgets/DocumentReview/confirmBlockers.ts`, `confirmError.ts` | неразобранные строки, разбор 409 подтверждения |
| `src/widgets/DocumentReview/OsvDocument.tsx` | `DocumentOut.issues` у ОСВ-документа |
| `src/widgets/FilesDrawer/fileWarnings.ts`, `documentRows.ts`, `FilesList.tsx`, `FileWarningsPopover.tsx` | предупреждения файла в панели файлов |
---
## Решения плана без обсуждения (`P6-n`)
| Маркер | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| P6-1 | Подсказка кода — `StatementOut.edition_lines` (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк `ok` файла, которых в справочнике нет (имя — `reference_name` ?? `line_name`); отдельного запроса нет | справочник приходит с ответом формы | нет |
| P6-2 | Якорь строки `unreadable` / `missing` — строка `ok` из источника (`row_index` не `null`) с наибольшим `row_index` меньше своего; якорь `detail` — строка `ok` из источника с `row_index = parent_row_index`; нет якоря (в том числе `detail` с `parent_row_index = null` или указывающим не на `ok`) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по `row_index` | спека `arm-web` «Раскладка», S-62 («строка `detail` без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра) | нет |
| P6-3 | Скрытие (пустые подстроки, расшифровки) применяется **после** раскладки, фильтром | иначе скрытый якорь сдвинул бы вложения к соседу | нет |
| P6-4 | Справочное имя (`reference_name`) — основное; файловое (`line_name`, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, `ё` → `е`, оставлены только буквы и цифры | переносы OCR («задолжен-ность») не должны давать ложного «отличается» | лишнее или пропущенное второе имя — только показ |
| P6-5 | Маркеры «у места» строятся из **всех** записей `issues` (в том числе `relation` / `soft` — у них `line_ids` + `column_id`); список `IssuesPanel` показывает только `kind = warning`; нарушения §7 остаются списком в `ChecksPanel` (из `checks`, с акцептом) и получают ссылку «Показать» по `line_ids[0]`. Запись с `column_id` без `line_ids` (например, `COLUMN_WITHOUT_DATE`) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа | одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки | нет |
| P6-6 | Подтверждение ячейки — в черновике; доступно, только если у строки есть `id` и у значения есть `id` (у достроенных ячеек — нет, ядро ответит `UNKNOWN_CELL`); сумма в черновике той же ячейки отменяет её подтверждение | `PATCH`: в элементе `values` ровно одно из `amount` / `confirmed`; правка суммы и так снимает `needs_review` | нет |
| P6-7 | Одно действие на строку в черновике (`code` / `detail` / `confirm`), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (`id = null`) действий строки нет | 422 `INVALID_LINE_PATCH` при двух действиях; `lines[]` адресуется только `line_id` | нет |
| P6-8 | Сумма в шаблонную строку по `line_code` X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит | ядро ответит 422 `UNKNOWN_CELL` (P3-10), а сообщение ядра не говорит, что делать | нет |
| P6-9 | Ошибка элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`) показывается у поля кода строки `detail.line_id` (текст сервера в подсказке поля и `message.error`); строки с этим `id` на экране нет — текстом над таблицей | ядро отдаёт `detail.line_id` и `detail.line_code` (план 3 P3-24) | нет |
| P6-10 | Строка «неразобранная» для кнопки «Подтвердить данные» — `line_status ∈ {unreadable, missing}` и хотя бы у одного значения `amount !== null` или `string_value !== null` (ровно условие ядра для 409 `LINES_UNRESOLVED`) | кнопка и сервер считают одинаково | нет |
| P6-11 | Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из `needs_review_fields` (уходит `header.report_date` тем же значением); дата колонки — поле `ГГГГ-ММ-ДД` у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` | перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты | нет |
| P6-12 | Панель файлов: у строки документа — значок с числом «предупреждения файла (`job.warnings`) + `issue_counts.error` + `issue_counts.info`» документа, красный при хотя бы одном `error` (в файле или `issue_counts.error > 0`); у строки файла без документов — только `job.warnings`; нуль — значка нет. Клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` документа, загружаемые при открытии (`GET /documents/{id}`) | `DocumentBrief.issue_counts` (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику | акцептованное нарушение тоже даёт значок — подробности в списке |
| P6-13 | `job.warnings` на клиенте нормализуется так же, как ядро: строка → `{code: "LEGACY_WARNING", severity: "info", message, locator: null}`, объект с `code`, `severity ∈ {error, info}`, `message` — как есть, прочее пропускается, не список → `[]` | экран не падает при ошибке порядка выкладки | нет |
| P6-14 | Сценарий T11 — на локальном стенде; строки `unreadable` / `missing` / `detail` и записи `issues` всех видов подставляются перехватом `GET` и `PATCH …/statement` (`page.route`) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия | до шага 5 выкладки 2.0-данных на стенде нет | повторный прогон на `aep_3` после шага 5 — follow-up |
| P6-15 | Цвета: ячейка `needs_review` — `#fff7e6`, исправлено — `#e6f4ff`, строка `unreadable` / `missing` целиком — `#fff1f0`, строка `ok` с `needs_review` — фон ячеек наименования и кода `#fff7e6`; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень `error` — красный значок `ExclamationCircleOutlined` `#cf1322`, `info` — синий `InfoCircleOutlined` `#1677ff` | палитра уже используется на экране и в панели файлов | только вид |
| P6-16 | Ячейка кода: `ok` — код; `unreadable` — серым `line_code_text` (пусто — «—»), подсказка «Код не распознан: «<текст>»»; `missing` — серым «—», подсказка «Кода в строке нет»; `detail` — пусто, подсказка «Расшифровка — в суммы формы не входит»; у `ok` в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком» | спека: «в ячейке кода серым — сырой текст с подсказкой» | только вид |
| P6-17 | `header.source.<поле>.confidence` — `string \| null` (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — `Number()` и округление до целого | одно правило для всех уверенностей | нет |
| P6-18 | `node_modules` worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо `make check` (он начинается с `npm install` и поставил бы пакеты в чужой каталог) гоняется `make lint typecheck test build` — те же проверки без установки | память владельца: симлинк, как в прежних срезах | нет; в описании PR сказано, что install пропущен |
| P6-19 | Предупреждения ОСВ-документа (`DocumentOut.issues`, `OSV_*`) — блок `IssuesPanel` над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются `line_ids`) | спека: «предупреждения листа ОСВ … из `DocumentOut.issues[]`»; экран документа уже загружает `DocumentOut` | нет |
## Чего не хватает в API ядра для экрана (`O-n`)
Все пункты закрыты сведением с планом 3 (18.10.2026, его P3-20…P3-24; спека `arm-core` и гайд §5.6.1 дополнены);
маркеры сохранены.
- **O-1.** Закрыт: `LineOut.line_code_confidence` (строка Decimal) в A8 плана 3; тип — `string | null` (T1).
- **O-2.** Закрыт: у 409 `LINE_CODE_TAKEN` и 422 `INVALID_LINE_CODE`, `UNKNOWN_LINE`, `INVALID_LINE_PATCH`,
`LINE_CODE_REQUIRED` в `detail` есть `line_id` и `line_code` (P3-24); экран ставит ошибку у поля строки (P6-9,
`lineErrorTarget`, T7).
- **O-3.** Закрыт: `StatementOut.edition_lines [{code, name}]` (P3-22), у формы без даты — коды всех редакций; подсказка
кода — из него (P6-1).
- **O-4.** Закрыт: `DocumentBrief.issue_counts {error, info}` (P3-23); панель помечает документ без запроса (P6-12,
`rowBadge`, T10).
- **O-5.** Закрыт: `header.source.<поле>.confidence` — строка Decimal (P3-20); P6-17.
- **O-6.** Закрыт: `StatementHeaderOut.okei` (P3-21); в блоке шапки поле не показывается (четыре поля T5), запись `issues`
с `field = "okei"` по-прежнему уходит в список документа (Review Focus 2).
## Открытые вопросы владельцу
- **В-1.** Приёмка экрана на шаге 4 выкладки — по перехвату с фикстурой (P6-14) и регрессии на формах 1.x; проверка на
реальных 2.0-данных (`aep_3`, РБ 1 кв. 2025) — после перехода сервисов (шаг 5), отдельной задачей. Устраивает?
- **В-2.** Снят 18.10.2026: `DocumentBrief.issue_counts` (план 3 P3-23) — панель помечает и документы только с
предупреждениями документа (P6-12).
- **В-3.** Подтверждённая ячейка помечается маленькой галочкой (P6-15) — в спеке и макете вида нет. Устраивает?
---
## Task 0: Подготовка — worktree, issue, ветка, OpenSpec change arm-web
**Files:**
- Create: `docs/superpowers/plans/2026-10-18-statements-v2.md` (копия этого плана), `openspec/changes/statements-v2/proposal.md`,
`design.md`, `specs/document-review-ui/spec.md`, `specs/primary-files-ui/spec.md`
**Interfaces:**
- Produces: worktree на ветке `feat/<issue>-statements-v2`; хук require-plan пропускает правки `src/` (план с именем
ветки лежит в `docs/superpowers/plans/`); `design.md` с разделом `## Decisions made without discussion` (туда — `P6-n` и
рулинги контроллера).
- [ ] **Step 1: Worktree и зеркальный workspace** (порядок — `~/.claude/CLAUDE.md`, «Роли инструментов»):
```bash
WS=/Users/andrey.epifancev/projects/awp-ca
mkdir -p $WS/.wt/normalization-web
git -C $WS/arm-web fetch origin
git -C $WS/arm-web worktree add -b tmp/statements-v2 $WS/.wt/normalization-web/arm-web origin/test
git -C $WS/docs fetch origin
# до слияния arm-ka-docs#116 — ветка change; после — origin/main
git -C $WS/docs worktree add --detach $WS/.wt/normalization-web/docs origin/feat/116-unified-normalization
for l in fixtures reference-old-arm CLAUDE.md .claude arm-core; do ln -s $WS/$l $WS/.wt/normalization-web/$l; done
```
Пути — абсолютные (`git -C <repo> worktree add` считает относительный путь от репозитория).
- [ ] **Step 2: node_modules** (P6-18) — симлинк на каталог, где есть eslint, prettier и vitest 3.1.4:
```bash
for c in $WS/.wt/fmt/arm-web/node_modules $WS/.wt/web-proto/arm-web/node_modules; do
[ -d $c/eslint ] && [ -d $c/prettier ] && grep -q '"version": "3.1.4"' $c/vitest/package.json && echo $c && break
done
```
Найденный — `ln -s <путь> $WS/.wt/normalization-web/arm-web/node_modules`. Не нашёлся — `npm install` в worktree
(свой каталог; `package-lock.json` не коммитить) и отметка `O-n` в ledger. Проверка: `cd $WS/.wt/normalization-web/arm-web
&& make lint typecheck test build` — PASS на базе `test`.
- [ ] **Step 3: Issue и ветка:**
```bash
tea issues create --login home --repo RSHB/arm-web \
--title "Единая нормализация ББ/ОФР: экран формы и карточка файла по API v5.19 (arm-ka-docs#116)" \
--description "Спека: arm-ka-docs, change unified-statement-normalization, specs/arm-web/spec.md. План 6: openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md. Выкладка вместе с ядром (plan 3 А+Б): JobBrief.warnings меняет тип."
git -C $WS/.wt/normalization-web/arm-web branch -m feat/<issue>-statements-v2
cd $WS/.wt/normalization-web/arm-web && codegraph init
```
Все вызовы `codegraph_explore` — с `projectPath=$WS/.wt/normalization-web/arm-web`; пустой ответ — один раз отметить в
ledger и дальше Read/Grep.
- [ ] **Step 4: Копия плана** — этот файл копируется в `docs/superpowers/plans/2026-10-18-statements-v2.md` с подстановкой
номера issue вместо `<issue>` (`sed "s/<issue>/<N>/g"`); проверка: `grep -c "feat/<N>-statements-v2"` ≥ 1.
- [ ] **Step 5: Change OpenSpec** — `openspec new change statements-v2`; `proposal.md`: ссылка на change docs
(`unified-statement-normalization`, arm-ka-docs#116), что меняется (по разделу Goal), non-goals (переключатель «как в
файле», склейка форм), зависимость от ядра v5.19. Спеки-дельты (формулировки требований — из `specs/arm-web/spec.md`
docs, дословно):
- `document-review-ui`: **MODIFIED** «Форма ББ/ОФР — просмотр и правка» (подсветка «требует внимания» — по
`needs_review`; правка расширена строками, колонками, подтверждениями, наименованием организации; полный текст
требования с изменёнными фразами); **MODIFIED** «Подтверждение документа» (порядок 409 с `LINES_UNRESOLVED`,
`DUPLICATE_FORM`, отключение кнопки); **ADDED** «Раскладка строк формы со статусами», «Подсветка и предупреждения у
места», «Правка строк, колонок и подтверждение в режиме «Редактировать»», «Предупреждения ОСВ-документа» (P6-19) —
со сценариями из docs;
- `primary-files-ui`: **ADDED** «Предупреждения файла и документа в карточке файла».
`design.md` — раздел «Decisions made without discussion» с таблицей `P6-1…P6-19` (дата 2026-10-18).
- [ ] **Step 6:** `openspec validate statements-v2 --strict` — без ошибок.
- [ ] **Step 7: Commit** `openspec(statements-v2): change и план экрана формы по API v5.19 (arm-ka-docs#116, #<issue>)`.
## Task 1: Типы API v5.19, фикстура, строки сетки
**Files:**
- Modify: `src/types/api.ts`, `src/widgets/DocumentReview/statementGrid.ts`, `src/utils/labels.ts`,
`src/widgets/DocumentReview/statementGrid.test.ts`, `src/widgets/DocumentReview/formLayout.test.ts` (хелпер `row()`),
`src/widgets/FilesDrawer/documentRows.test.ts` (`warnings: []` — тип)
- Create: `src/widgets/DocumentReview/__fixtures__/statement-v2.json`, `src/utils/labels.test.ts` (дополнить, если есть)
**Interfaces:**
- Produces (в `src/types/api.ts`):
```ts
export type LineStatus = 'ok' | 'unreadable' | 'missing' | 'detail';
export type Severity = 'error' | 'info';
export type IssueKind = 'warning' | 'relation' | 'soft';
export type PeriodKind = 'absolute' | 'relative:current' | 'relative:prev_year_end' | 'relative:prev_prev_year_end'
| 'relative:prev_period' | 'none';
export interface FileWarningOut { code: string; severity: Severity; message: string; locator: string | null }
export interface IssueOut {
id: string; kind: IssueKind; code: string; severity: Severity; message: string;
line_ids: string[]; column_id: string | null; column_index: number | null; field: string | null;
source: 'service' | 'core'; relation: string | null; codes: string[]; expected: string | null; actual: string | null;
}
export interface HeaderSourceOut { text: string | null; confidence: string | null }
export interface EditionLineOut { code: string; name: string }
export interface IssueCountsOut { error: number; info: number }
export interface LineErrorDetail { code: string; message: string; line_id: string; line_code: string | null }
export interface UnresolvedLineOut { line_id: string; row_index: number | null; line_code_text: string | null }
export type CellTarget = { line_id: string } | { line_code: string };
export type CellPatch = (CellTarget & { column_id: string; amount: string | null })
| { line_id: string; column_id: string; confirmed: boolean };
export type LinePatch = { line_id: string; line_code: string } | { line_id: string; line_status: 'detail' }
| { line_id: string; confirmed: boolean };
export interface ColumnPatch { column_id: string; period_date: string }
export interface StatementPatch {
header?: { inn?: string; report_date?: string; units?: string; organization_name?: string };
values?: CellPatch[]; columns?: ColumnPatch[]; lines?: LinePatch[];
}
```
Изменения существующих: `JobBrief.warnings: FileWarningOut[]` (`mapping_warnings` — `string[]`); `DocumentOut.issues:
IssueOut[]` (`warnings: string[]` остаётся); `StatementHeaderOut` + `needs_review_fields: string[]`, `source:
Partial<Record<string, HeaderSourceOut>>`, `organization_name_modified: boolean`, `okei: string | null`; `ColumnOut` +
`period_kind: PeriodKind`,
`label_confidence: string | null`; `LineOut` + `row_index: number | null`, `source_ref: string | null`, `line_status:
LineStatus`, `line_code_text: string | null`, `line_code_confidence: string | null`, `reference_name: string | null`,
`parent_row_index: number | null`, `needs_review: boolean`, `is_confirmed: boolean`, `line_code_modified: boolean`;
`ValueOut` + `text: string | null`, `needs_review: boolean`, `is_confirmed: boolean`; `FailedCheckOut` + `codes:
string[]`, `line_ids: string[]`; `StatementOut.document.form_variant: 'full' | 'simplified' | null`;
`StatementOut.issues: IssueOut[]`, `StatementOut.edition_lines: EditionLineOut[]`; `DocumentBrief.issue_counts:
IssueCountsOut` (и `DocumentOut`, который его расширяет).
- Produces (`statementGrid.ts`):
```ts
export type GridRow = {
key: string; lineId: string | null; code: string | null;
name: string | null; // line_name — из файла
referenceName: string | null; // справочное имя по коду
origin: 'source' | 'template'; parentCode: string | null;
lineStatus: LineStatus; rowIndex: number | null; parentRowIndex: number | null;
codeText: string | null; codeConfidence: string | null;
needsReview: boolean; isConfirmed: boolean; codeModified: boolean;
cells: Record<string, ValueOut | undefined>;
};
export function buildStatementGrid(statement: StatementOut): { columns: ColumnOut[]; rows: GridRow[] };
export function cellFlags(value: ValueOut | undefined): { attention: boolean; modified: boolean; confirmed: boolean };
```
Ключ строки — прежний: `id`, у достроенной — `code-<код>`. `draftToPatch`, `applyDraftInput`, `emptySublineKeys`,
`cellKey` пока остаются (переезд — T6).
- Produces (`labels.ts`): `lineStatusLabel(s: LineStatus): string` — `ok` «Строка формы», `unreadable` «Код не
распознан», `missing` «Кода нет», `detail` «Расшифровка»; `severityLabel(s: Severity): string` — `error` «Ошибка»,
`info` «Информация».
- Produces (фикстура `statement-v2.json`, тип `StatementOut`; у **каждой** пары «строка × колонка» есть `ValueOut`):
документ `BALANCE`, `NEEDS_CHECK`, 2025 `Q3`, `reporting_date "2025-09-30"`, `form_variant "full"`, `edition "fsbu4"`;
шапка: ИНН `7700000000` (уверенность `100`), дата отчёта `2025-09-30` (`original_value "30 сентября 2025 г."`,
уверенность `67`), единицы «тыс. руб.», `organization_name "ООО Ромашка"`, `needs_review_fields ["report_date"]`,
`okei "384"`, `source {organization_name: {text: "ООО «Ромашкa»", confidence: "67.00"}, report_date: {text: "30
сентября 2025 г.", confidence: "67.00"}}`; колонки `c1` (`column_index 2`, «На 30 сентября 2025 г.», `2025-09-30`, `absolute`) и `c2`
(`column_index 3`, `label null`, `period_date null`, `none`); строки:
| id | `row_index` | статус | код / `line_code_text` | наименование (`reference_name`) | `parent_row_index` | особенности (колонка `c1`) |
|---|---|---|---|---|---|---|
| `l-1110` | 0 | ok | 1110 | Нематериальные активы (то же) | — | `10` |
| `l-1150` | 1 | ok | 1150 | Основные средства (то же) | — | `70045` |
| `l-d2` | 2 | detail | — | в том числе: пригодные к использованию | 1 | `70045`, `text "70045"`, уверенность `67`, `needs_review false` |
| `l-1100` | 3 | ok | 1100 | Итого по разделу I (то же) | — | `70055` |
| `l-1210` | 4 | ok | 1210 | Запасы (то же) | — | `100` |
| `l-1230` | 5 | ok | 1230, уверенность кода `50`, строка `needs_review true` | «Дебиторская задолжен-ность» (`Дебиторская задолженность`) | — | `428311`, `text "42831 1"`, `is_recognized false`, `needs_review false` |
| `l-1240` | 6 | ok | 1240 | Финансовые вложения (то же) | — | `5` |
| `l-u7` | 7 | unreadable | `ИЧ,250` | Денежные средства | — | `50`, уверенность `67`, `needs_review true` |
| `l-m8` | 8 | missing | `null` | Прочие оборотные активы | — | `7` |
| `l-1200` | 9 | ok | 1200 | Итого по разделу II (то же) | — | `200`, `needs_review true` |
| `l-1600` | 10 | ok | 1600 | БАЛАНС (то же) | — | `70255` |
| `l-1370a` | 11 | ok | 1370 | Нераспределенная прибыль (то же) | — | `500` |
| `l-1370b` | 12 | ok | 1370 | в т.ч. прошлых лет (Нераспределенная прибыль) | — | `300` |
| `l-d13` | 13 | detail | — | справочно | `null` | `amount null`, `string_value "211(1 |"` |
| `null` | `null` | ok, `origin template` | 1250 | (Денежные средства и денежные эквиваленты) | — | все пустые, `id` значений `null` |
| `l-t1170` | `null` | ok, `origin template` | 1170 | (Финансовые вложения) | — | `3`, `is_modified true` (строка создана правкой) |
`issues`: `i1` warning `ROW_SPLIT` info, `line_ids [l-1370a, l-1370b]`; `i2` warning `COLUMN_WITHOUT_DATE` error,
`column_id c2`; `i3` warning `ORGANIZATION_UNCERTAIN` info, `field "organization_name"`; `i4` warning `COLUMN_SKIPPED`
info, `column_index 5`, `column_id null`; `i5` warning `REPORT_DATE_UNCERTAIN` error, `field "report_date"`; `i6`
relation `RELATION_FAILED` error, `line_ids [l-1200, l-1210, l-1230, l-1240]`, `column_id c1`, `relation "1200 = 1210 + 1220
+ 1230 + 1240 + 1250 + 1260"`, `codes [...]`, `expected "155"`, `actual "200"`; `i7` warning `NAME_MISMATCH` info, source
`core`, `line_ids [l-1230]`. `checks.failed` — одно нарушение с теми же `codes`, `line_ids`, `column_index 2`.
`edition_lines` — коды ББ `fsbu4` по одному в порядке бланка с первыми именами (в том числе `{code: "1250", name:
"Денежные средства и денежные эквиваленты"}`, `{code: "1370", name: "Нераспределенная прибыль (непокрытый убыток)"}`,
`{code: "1170", name: "Финансовые вложения"}`; кода `1120` нет).
- [ ] **Step 1: Write the failing tests** (`statementGrid.test.ts`):
- `buildStatementGrid переносит поля v2` — на фикстуре строка `l-u7`: `lineStatus 'unreadable'`, `rowIndex 7`,
`codeText 'ИЧ,250'`, `code null`, `key 'l-u7'`; `l-d2`: `parentRowIndex 1`; шаблонная 1250: `key 'code-1250'`,
`rowIndex null`, `referenceName 'Денежные средства и денежные эквиваленты'`; `l-1230`: `needsReview true`,
`codeConfidence '50'`.
- `cellFlags — внимание только по needs_review` — `{confidence: '50', is_recognized: false, needs_review: false}` →
`attention false`; `{confidence: '100', needs_review: true}` → `attention true`; `{is_confirmed: true}` → `confirmed
true`; `undefined` → все `false`.
- `labels.test.ts`: четыре подписи статуса и две уровня — дословно, как в Interfaces.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/statementGrid.test.ts src/utils/labels.test.ts` — FAIL.
- [ ] **Step 3: Implement** типы, фикстуру, `GridRow`, `cellFlags`, подписи; хелперы существующих тестов (`row()` в
`formLayout.test.ts`, `v()` в `statementGrid.test.ts`, `job` в `documentRows.test.ts`) дополняются полями по умолчанию
(`lineStatus 'ok'`, `rowIndex` по порядку, `needs_review false` и т. д.), ожидания старых тестов не меняются, кроме
теста `cellFlags` по уверенности — он заменяется новым.
- [ ] **Step 4:** `make lint typecheck test build` — PASS (компоненты, читающие `job.warnings` как строки, на `test` нет).
- [ ] **Step 5: Commit** `feat(types): API формы v5.19 — статусы строк, needs_review, issues, предупреждения файла (#<issue>)`.
## Task 2: Раскладка — вложения строк без кода и расшифровок, фильтр показа, имена
**Files:**
- Modify: `src/widgets/DocumentReview/formLayout.ts`, `src/widgets/DocumentReview/formLayout.test.ts`
**Interfaces:**
- Consumes: `GridRow` (T1).
- Produces:
```ts
export type LayoutRow =
| { kind: 'section'; key: string; title: string; level: 1 | 2 }
| { kind: 'line'; key: string; row: GridRow; total: boolean; grand: boolean;
child: boolean; // подстрока под родителем или расшифровка — отступ
displayName: string; // справочное имя, иначе файловое без приклеенных заголовков
secondaryName: string | null; // файловое имя, если отличается (P6-4)
fullName: string | null }; // сырое line_name для подсказки, если показано не оно
export function formLayout(type: DocumentType, rows: GridRow[]): LayoutRow[];
export function filterLayout(layout: LayoutRow[], opts: { showDetails: boolean; hiddenKeys: ReadonlySet<string> }): LayoutRow[];
export function detailCount(rows: GridRow[]): number;
```
`formLayout` раскладывает по разделам только строки `lineStatus === 'ok'` (как сейчас), одинаковые коды — по
`rowIndex` (`null` последним), подстроки под родителем — по коду, затем `rowIndex`; затем вставляет вложения по P6-2.
Расшифровки возвращаются всегда; скрывает их `filterLayout`.
- [ ] **Step 1: Write the failing tests** (`shape` в тесте: раздел — `# <заголовок>`, строка `ok` — код, `unreadable` /
`missing` — `?<codeText ?? '-'>`, `detail` — `> <name>`):
- `строка без кода под ближайшей предыдущей по row_index` — `1230 r0, 1240 r1, unreadable 'ИЧ,250' r2, 1250 r3` →
в разделе II: `1230`, `1240`, `?ИЧ,250`, `1250`;
- `якорь по файлу, а не по коду` — `1250 r0, unreadable r1, 1210 r2` → `1210`, `1250`, `?…`;
- `нет якоря — в начало первого раздела` — `missing r0, 1110 r1` (ББ) → `['# АКТИВ', '# I. Внеоборотные активы', '?-',
'1110', …]`; ОФР `missing r0, 2110 r1` → первая строка `?-`;
- `расшифровки под родителем по row_index с отступом` — `1150 r4, d r6 (parent 4), d r5 (parent 4)` → `1150`, `> r5`,
`> r6`, у обеих `child true`;
- `расшифровка без родителя — в начало первого раздела` — `parentRowIndex null` и `parentRowIndex 99` (нет такой
строки);
- `повтор кода: по row_index, вложение у своего экземпляра` — `2410 r9, 2410 r6, unreadable r7` (ОФР) → `2410` (r6),
`?…`, `2410` (r9) — Review Focus 1;
- `шаблонная строка не якорь` — `1250 (rowIndex null, template)`, `unreadable r1` после `1210 r0` → вложение под 1210;
- `якорь — пятизначная подстрока` — `1150 r0, 11501 r1 (parentCode 1150), unreadable r2` → `1150`, `11501`, `?…`;
- `filterLayout скрывает расшифровки и пустые подстроки, вложение скрытого якоря остаётся` — `showDetails false` убирает
`> …`; `hiddenKeys {11501}` убирает `11501`, `?…` остаётся после `1150` — Review Focus 1;
- `detailCount` — фикстура → `2`; форма без `detail` → `0` (Review Focus 4);
- `имена` — `referenceName 'Дебиторская задолженность'`, `name 'Дебиторская задолжен-ность'` → `displayName` справочное,
`secondaryName null`; `name 'Прочие оборотные'` → `secondaryName 'Прочие оборотные'`; `referenceName 'Гудвил'`,
`name 'АКТИВ I. Внеоборотные активы Гудвил'` → `secondaryName null`, `fullName` = сырое имя; строка без
`referenceName` — `displayName` из `name`, `secondaryName null`;
- `фикстура целиком` — порядок: `# АКТИВ`, `# I. Внеоборотные активы`, `> справочно` (l-d13 без родителя), `1110`,
`1150`, `> в том числе…`, `1170`, `1100`, `# II. Оборотные активы`, `1210`, `1230`, `1240`, `?ИЧ,250`, `?-`, `1250`,
`1200`, `1600`, `# ПАССИВ`, `# III. Капитал и резервы`, `1370` (a), `1370` (b).
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/formLayout.test.ts` — FAIL.
- [ ] **Step 3: Implement.** Вставка вложений — отдельной внутренней функцией после построения базовой раскладки:
```text
anchorKey(a) = detail → ключ строки ok из источника с rowIndex === a.parentRowIndex, иначе null
иначе → ключ строки ok из источника с max rowIndex < a.rowIndex, иначе null (a.rowIndex null → null)
группы по anchorKey, внутри — по rowIndex (null последним), затем key
обход базы: после строки с ключом-якорем — её группа; группа null — после ведущих строк kind = 'section'
```
Имена (P6-4): нормализация для сравнения — `toLowerCase`, `ё` → `е`, удалить всё, кроме букв и цифр.
- [ ] **Step 4:** тот же запуск — PASS; `make lint typecheck test build` — PASS.
- [ ] **Step 5: Commit** `feat(statement): раскладка строк без кода и расшифровок на своих местах (#<issue>)`.
## Task 3: Таблица формы — вид строк и ячеек, переключатель расшифровок
**Files:**
- Create: `src/widgets/DocumentReview/cellView.ts`, `cellView.test.ts`, `CodeCell.tsx`, `ValueCell.tsx`
- Modify: `src/widgets/DocumentReview/StatementTable.tsx`, `StatementReview.tsx`
**Interfaces:**
- Consumes: `formLayout`, `filterLayout`, `detailCount` (T2), `cellFlags` (T1).
- Produces:
```ts
export const COLORS: { attention: '#fff7e6'; modified: '#e6f4ff'; unresolved: '#fff1f0'; section: '#f2f4f7'; grand: '#f9f0ff' };
export function valueBackground(value: ValueOut | undefined): string | undefined; // modified → attention → нет
export function rowBackground(row: GridRow): string | undefined; // unresolved для unreadable/missing; attention для ok с needsReview (только наименование и код)
export function cellTipLines(value: ValueOut, issues?: IssueOut[]): string[];
export function codeCellView(row: GridRow): { text: string; muted: boolean; tip: string | undefined };
```
`StatementTable` получает готовые `layout: LayoutRow[]` (раскладку и фильтр считает `StatementReview` в `useMemo`), а не
`rows`. Строка таблицы получает `id="row-<key>"` (`onRow`) — для перехода к строке (T4, T8).
- [ ] **Step 1: Write the failing tests** (`cellView.test.ts`):
- `cellTipLines` — значение `{text: '42831 1', confidence: '100', is_recognized: false}` → `['Распознано: «42831 1»',
'Уверенность: 100%']`; исправленное `{is_modified: true, original_amount: '293709'}` → первой строкой `` `Исходное: ${formatAmount('293709', { fraction: 'auto', empty: '-' })}` `` (как сейчас);
`{is_confirmed: true}` → содержит `'Подтверждено аналитиком'`; `text null`, `confidence null` → `[]`; значение
строки `detail` с `needs_review false`, `text '70045'`, `confidence '67'` → `['Распознано: «70045»', 'Уверенность: 67%']`
(подсказка есть и без подсветки, Р-23);
- `codeCellView` — тексты P6-16 дословно для `ok` (с уверенностью 50 → `'Уверенность кода: 50%'`; `codeModified true` →
`'Код назначен аналитиком'`, уверенности нет), `unreadable 'ИЧ,250'` → `{text: 'ИЧ,250', muted: true, tip: 'Код не
распознан: «ИЧ,250»'}`, `missing` → `{text: '—', muted: true, tip: 'Кода в строке нет'}`, `detail` → `{text: '',
muted: true, tip: 'Расшифровка — в суммы формы не входит'}`;
- `valueBackground` / `rowBackground` — `needs_review true` → `#fff7e6`, `is_modified true` → `#e6f4ff` (перекрывает),
`confidence '50'` без `needs_review` → `undefined`; строка `unreadable` → `#fff1f0`, `ok` с `needsReview` → `#fff7e6`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/cellView.test.ts` — FAIL.
- [ ] **Step 3: Implement** функции; компоненты: `CodeCell` (просмотр — `codeCellView`; серым — `Typography.Text
type='secondary'`), `ValueCell` (сумма, галочка подтверждения `CheckOutlined` 10px серым, подсказка — строки
`cellTipLines` через `<br/>`); `StatementTable`: колонка наименования — `displayName`, ниже серым `secondaryName`,
подсказка `fullName`; фон строки `unreadable` / `missing` — во всех ячейках строки; `StatementReview`: переключатель
«Показать расшифровки (N)» (`Switch`, как «Показать пустые строки детализации»), показывается при `detailCount > 0`;
пустые подстроки по-прежнему считает `emptySublineKeys` — теперь как `hiddenKeys` для `filterLayout`.
- [ ] **Step 4:** `make lint typecheck test build` — PASS.
- [ ] **Step 5: Сверка с макетом.** `make start`; экран ББ на стенде (форма 1.x) и с перехватом фикстуры (рецепт — T11,
шаг 2): окно 1600×1000, скриншот против `docs/mockup/screens/41-document-check.png` — разделы, итоги, «БАЛАНС»
фиолетовым, строка «ИЧ,250» под 1240 красноватым фоном с серым кодом, переключатель «Показать расшифровки (2)»;
расхождения — в ledger (`O-n`).
- [ ] **Step 6: Commit** `feat(statement): вид строк по статусам, подсветка по needs_review, переключатель расшифровок (#<issue>)`.
## Task 4: Предупреждения и нарушения у места
**Files:**
- Create: `src/widgets/DocumentReview/issuePlaces.ts`, `issuePlaces.test.ts`, `IssuesPanel.tsx`
- Modify: `StatementTable.tsx`, `ValueCell.tsx`, `StatementReview.tsx`, `ChecksPanel.tsx`
**Interfaces:**
- Consumes: `GridRow`, `ColumnOut`, `IssueOut` (T1); `columnTitle` (`reviewView.ts`); `lineStatusLabel`, `severityLabel` (T1).
- Produces:
```ts
export const HEADER_BLOCK_FIELDS = ['organization_name', 'inn', 'report_date', 'units'] as const;
export interface IssuePlaces {
byRow: Map<string, IssueOut[]>; byCell: Map<string, IssueOut[]>; // ключ ячейки — cellKey(rowKey, columnId)
byColumn: Map<string, IssueOut[]>; byField: Map<string, IssueOut[]>; document: IssueOut[];
}
export function issuePlaces(issues: IssueOut[], rows: GridRow[], columns: ColumnOut[],
fields?: readonly string[] /* по умолчанию HEADER_BLOCK_FIELDS */): IssuePlaces;
export interface IssueListItem { id: string; severity: Severity; message: string; place: string; targetKey: string | null }
export function issueListItems(issues: IssueOut[], places: IssuePlaces, rows: GridRow[], columns: ColumnOut[]): IssueListItem[];
export function worstSeverity(issues: IssueOut[]): Severity | null;
```
Правила `issuePlaces` (P6-5): `field` из `fields` → `byField`, `field` вне `fields` → `document`; иначе непустые
`line_ids` → известные строки (по `lineId`); ни одной известной → `document`; есть `column_id` известной колонки →
`byCell` для каждой известной строки, иначе `byRow`; без `line_ids` с известным `column_id` → `byColumn`; всё прочее
(в том числе `column_index` без `column_id`) → `document`. `issueListItems` — только `kind === 'warning'`, сначала
`error`, затем `info`, внутри — по порядку `issues`; `place`: строка `ok` — «строка <код>», `unreadable` / `missing` —
«строка без кода «<codeText>»» (без текста — «строка без кода»), `detail` — «расшифровка «<name>»»; ячейка — «<строка>,
колонка «<columnTitle>»»; колонка — «колонка «<columnTitle>»»; поле — «поле «<подпись поля>»»; документ — «документ»,
с `column_index` без `column_id` — «столбец <column_index + 1> вне колонок формы»; `targetKey` — ключ первой строки места
(иначе `null`).
- [ ] **Step 1: Write the failing tests** (`issuePlaces.test.ts`, на фикстуре):
- `привязка фикстуры` — `i1` в `byRow` у `l-1370a` и `l-1370b`; `i6` в `byCell` у четырёх ячеек `c1`; `i2` в
`byColumn.c2`; `i3`, `i5` в `byField`; `i4` в `document`; `i7` в `byRow.l-1230`;
- `неизвестные строки и поля — у документа` — `line_ids ['l-gone']` → `document`; `line_ids ['l-gone', 'l-1210']` →
только `l-1210`; `field 'okpo'` → `document` — Review Focus 2;
- `issueListItems` — на фикстуре 6 пунктов (без `i6`), первыми `i2` и `i5` (`error`); `i4.place === 'столбец 6 вне
колонок формы'`, `targetKey null`; `i1.place === 'строка 1370'`, `targetKey 'l-1370a'`; запись у `l-d13` → `place
'расшифровка «справочно»'`;
- `worstSeverity` — `[info, error]` → `'error'`; `[]` → `null`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/issuePlaces.test.ts` — FAIL.
- [ ] **Step 3: Implement** функции и UI: значок уровня (P6-15) в ячейке наименования (`byRow`), в углу ячейки значения
(`byCell`, сообщения — в её подсказке после `cellTipLines`), в заголовке колонки (`byColumn`); `IssuesPanel` — `Alert`
«Предупреждения (N)» (тип `error`, если есть `error`, иначе `info`), список с прокруткой (`maxHeight 120`, как
`failureList`), у пункта — тег уровня (`severityLabel`), текст, место и ссылка «Показать» при `targetKey`;
`ChecksPanel` — ссылка «Показать» у нарушения по `line_ids[0]`. Переход к строке — функция `focusRow(key)` в
`StatementReview`: если строка — скрытая расшифровка или скрытая пустая подстрока, сначала включает соответствующий
переключатель; после отрисовки `document.getElementById('row-' + key)?.scrollIntoView({block: 'center'})` и подсветка
рамкой на 2 с (Review Focus 2).
- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате фикстуры: «Показать» у `ROW_SPLIT` прокручивает к
1370; у записи на `l-d13` при выключенных расшифровках — включает их.
- [ ] **Step 5: Commit** `feat(statement): предупреждения и нарушения у строк, ячеек, колонок и в списке документа (#<issue>)`.
## Task 5: Шапка формы — наименование организации, поля на проверку, подсказки источника
**Files:**
- Modify: `src/widgets/DocumentReview/headerFields.ts`, `headerFields.test.ts`, `StatementHeaderBlock.tsx`
**Interfaces:**
- Consumes: `StatementHeaderOut` (T1), `IssuePlaces.byField` (T4).
- Produces:
```ts
export type HeaderFieldKey = 'organization_name' | 'inn' | 'report_date' | 'units';
export type HeaderFieldRow = { field: HeaderFieldKey; label: string; text: string; raw: string | null;
attention: boolean; modified: boolean; tip: string | undefined; issues: IssueOut[]; canConfirm: boolean };
export function headerFields(header: StatementHeaderOut, byField?: Map<string, IssueOut[]>): HeaderFieldRow[];
```
Порядок и подписи: «Организация», «ИНН», «Дата отчёта», «Единица измерения». `attention` — поле в
`needs_review_fields` (только); `modified` — `organization_name_modified` или `HeaderFieldOut.modified`; `tip`:
исправлено → «Исходное: <original_value>» (у организации — «Распознано: «<source.organization_name.text>»»), иначе при
наличии `source[field]` — «Распознано: «<text>» · уверенность N%» (`Number(confidence)`, P6-17); `canConfirm` —
`field === 'report_date'` ∧ поле в `needs_review_fields` ∧ `raw !== null`.
- [ ] **Step 1: Write the failing tests** (`headerFields.test.ts`, фикстура):
- `четыре поля по порядку` — `['organization_name', 'inn', 'report_date', 'units']`, подписи дословно;
- `внимание только по needs_review_fields` — дата отчёта `attention true`, `canConfirm true`; ИНН с уверенностью `50`
вне `needs_review_fields` → `attention false`; пустое ИНН вне списка → `attention false`;
- `подсказка источника` — организация: `tip === 'Распознано: «ООО «Ромашкa»» · уверенность 67%'`; `confidence` строкой
`'67.00'` — тот же текст `67%`;
- `исправленная организация` — `organization_name_modified true` → `modified true`, `tip` с текстом источника;
- `предупреждения поля` — `byField` с `i3` → `issues` у организации — `[i3]`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/headerFields.test.ts` — FAIL.
- [ ] **Step 3: Implement**; `StatementHeaderBlock` рисует строку «Организация» шире остальных (ячейка `minWidth 320`),
значки уровня из `issues`, в режиме правки — `Input` у всех четырёх полей и кнопку «Подтвердить дату» при `canConfirm`
(черновик — T6, T8). Блок по-прежнему справа над заголовком формы, как в `41-document-check.png`.
- [ ] **Step 4:** `make lint typecheck test build` — PASS.
- [ ] **Step 5: Commit** `feat(statement): шапка — организация, поля на проверку и подсказки распознанного текста (#<issue>)`.
## Task 6: Черновик правок и один PATCH
**Files:**
- Create: `src/widgets/DocumentReview/statementDraft.ts`, `statementDraft.test.ts`
- Modify: `src/widgets/DocumentReview/statementGrid.ts` (удалить `draftToPatch`; `applyDraftInput`, `emptySublineKeys`
остаются), `statementGrid.test.ts` (тесты `draftToPatch` переезжают), `src/utils/dates.ts`, `src/utils/dates.test.ts`,
`StatementReview.tsx`, `StatementTable.tsx`, `StatementHeaderBlock.tsx`
**Interfaces:**
- Consumes: `GridRow`, `cellKey` (T1), `HeaderFieldKey` (T5), `parseAmountInput` (`utils/money.ts`).
- Produces:
```ts
// utils/dates.ts
export function isIsoDate(text: string): boolean; // строго ГГГГ-ММ-ДД и существующая дата
// statementDraft.ts
export type LineAction = { kind: 'code'; code: string } | { kind: 'detail' } | { kind: 'confirm'; confirmed: boolean };
export type HeaderDraft = Partial<Record<HeaderFieldKey, string>> & { confirm_report_date?: boolean };
export interface StatementDraft {
amounts: Record<string, string>; // cellKey → введённый текст
confirms: Record<string, boolean>; // cellKey → confirmed
lines: Record<string, LineAction>; // lineId → действие
columns: Record<string, string>; // columnId → введённая дата
header: HeaderDraft;
}
export const emptyDraft = (): StatementDraft => ({ amounts: {}, confirms: {}, lines: {}, columns: {}, header: {} });
export function isDraftEmpty(draft: StatementDraft): boolean;
export type DraftError = { kind: 'amount' | 'code' | 'date' | 'header' | 'conflict'; key: string; message: string };
export function buildStatementPatch(draft: StatementDraft, ctx: { rows: GridRow[]; header: StatementHeaderOut }):
{ ok: true; body: StatementPatch | null } | { ok: false; errors: DraftError[] };
```
Правила сборки: `values` — суммы (строка с `id` → `line_id`, достроенная → `line_code`, как прежний `draftToPatch`),
затем подтверждения ячеек без суммы в черновике (P6-6); `lines` — действия по P6-7 (код — `^\d{4,5}$`, иначе ошибка
`code` «Код — 4 или 5 цифр»); `columns` — непустые даты, `isIsoDate`, иначе ошибка `date` «Дата в формате ГГГГ-ММ-ДД»;
`header` — `inn`, `report_date`, `units`, `organization_name` после `trim`, непустые (пустое — не отправляется);
`organization_name` длиннее 1000 — ошибка `header`; `confirm_report_date` без введённой даты → `report_date` = текущее
значение; конфликт P6-8 — ошибка `conflict` с `key` строки действия. Нет ни одной части → `body: null`.
- [ ] **Step 1: Write the failing tests** (`statementDraft.test.ts`, фикстура; `dates.test.ts`):
- `суммы как раньше` — `l-1210:c1 = '120'` → `values [{line_id: 'l-1210', column_id: 'c1', amount: '120'}]`; шаблонная
`code-1250:c1 = '5'` → `{line_code: '1250', …}`; `'12,3,4'` → ошибка `amount`;
- `подтверждение ячейки` — `confirms {l-u7:c1: true}` → `{line_id: 'l-u7', column_id: 'c1', confirmed: true}`; та же
ячейка и в `amounts` → только элемент с `amount` (Review Focus 3);
- `действия строки` — `{l-u7: code '1250'}` → `lines [{line_id: 'l-u7', line_code: '1250'}]`; `{l-m8: detail}` →
`{line_id: 'l-m8', line_status: 'detail'}`; `{l-1230: confirm true}` → `{line_id: 'l-1230', confirmed: true}`; код
`''` → `body null`; код `'1230'` у `l-1230` → `body null`; код `'12a4'` и `'123'` → ошибка `code` с `key 'l-u7'`;
`'11501'` → проходит;
- `действие строки и сумма той же строки` — `{l-m8: detail}` и `l-m8:c1 = '8'` → оба в теле (`lines` и `values`);
- `конфликт шаблонной суммы и кода` — `code-1250:c1 = '5'` и `{l-u7: code '1250'}` → ошибка `conflict`, текст P6-8
дословно с `1250` — Review Focus 3;
- `колонки` — `{c2: '2024-12-31'}` → `columns [{column_id: 'c2', period_date: '2024-12-31'}]`; `'31.12.2024'` и
`'2024-02-30'` → ошибка `date`; `''` → не отправляется;
- `шапка` — `organization_name ' ООО Ромашка '` → `'ООО Ромашка'`; 1001 символ → ошибка `header`; `confirm_report_date`
→ `report_date '2025-09-30'`; `confirm_report_date` и введённая `'2025-06-30'` → `'2025-06-30'`; `inn ''` → нет `inn`;
- `пустой черновик` — `isDraftEmpty(emptyDraft()) === true`, `buildStatementPatch` → `{ok: true, body: null}`;
- `isIsoDate` — `'2024-12-31'` да; `'2024-2-1'`, `'2024-02-30'`, `'31.12.2024'`, `''` нет.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/statementDraft.test.ts src/utils/dates.test.ts` — FAIL.
- [ ] **Step 3: Implement**; `StatementReview` держит одно состояние `draft: StatementDraft` вместо `draft` и `header`;
`save()` вызывает `buildStatementPatch`: ошибки — `message.error` с первой ошибкой и красная рамка у полей (ключи
ошибок передаются в таблицу и шапку), `body null` — выход из режима правки без запроса, иначе один `patchStatement`;
`dirty` = `!isDraftEmpty(draft)`. Поведение сумм и шапки (ИНН, дата, единицы) не меняется — сценарии W21–W27
`qa-guide` §8.5 проходят.
- [ ] **Step 4:** `make lint typecheck test build` — PASS.
- [ ] **Step 5: Commit** `refactor(statement): один черновик правок и сборка единственного PATCH (#<issue>)`.
## Task 7: Режим «Редактировать» — коды строк, меню строки, LINE_CODE_TAKEN
**Files:**
- Create: `src/widgets/DocumentReview/codeSuggestions.ts`, `codeSuggestions.test.ts`, `lineMenu.ts`, `lineMenu.test.ts`,
`LineMenu.tsx`
- Modify: `CodeCell.tsx`, `StatementTable.tsx`, `StatementReview.tsx`
**Interfaces:**
- Consumes: `GridRow`, `EditionLineOut`, `LineErrorDetail` (T1), `StatementDraft`, `LineAction` (T6).
- Produces:
```ts
// codeSuggestions.ts
export type CodeOption = { code: string; name: string };
export function codeOptions(editionLines: EditionLineOut[], rows: GridRow[]): CodeOption[];
// P6-1: сначала edition_lines по порядку, затем коды строк ok файла вне справочника (name = referenceName ?? name ?? ''), без повторов
export function filterCodeOptions(options: CodeOption[], query: string, limit?: number /* 20 */): CodeOption[];
export function lineErrorTarget(error: unknown): { lineId: string; lineCode: string | null } | null;
// P6-9: detail.line_id / detail.line_code ошибки элемента lines[] (409 LINE_CODE_TAKEN, 422 INVALID_LINE_CODE,
// INVALID_LINE_PATCH, UNKNOWN_LINE, LINE_CODE_REQUIRED); иная ошибка или нет line_id — null
// lineMenu.ts
export type LineMenuKey = 'mark_detail' | 'assign_code' | 'change_code' | 'confirm_code' | 'unconfirm_code' | 'reset';
export function lineMenuItems(row: GridRow, action: LineAction | undefined): { key: LineMenuKey; label: string }[];
export function applyLineMenu(row: GridRow, key: LineMenuKey, lines: Record<string, LineAction>): Record<string, LineAction>;
export function showsCodeInput(row: GridRow, action: LineAction | undefined): boolean;
```
Пункты (дословно): `unreadable` / `missing` — «Это расшифровка» (`mark_detail`); `detail` — «Назначить код»
(`assign_code` → действие `{kind: 'code', code: ''}`); `ok` из источника — «Сделать расшифровкой» (`mark_detail`),
«Сменить код» (`change_code` → `{kind: 'code', code: <текущий>}`), «Подтвердить код» (`confirm_code`, при `needsReview` и
не `isConfirmed`), «Снять подтверждение кода» (`unconfirm_code`, при `isConfirmed`); при наличии действия — «Отменить
изменение строки» (`reset`); строка с `lineId === null` — пусто. Поле кода видно у `unreadable` / `missing` всегда и у
любой строки с действием `code`.
- [ ] **Step 1: Write the failing tests:**
- `codeOptions` (фикстура) — первые элементы — `edition_lines` по порядку; `{code: '1250', name: 'Денежные средства и
денежные эквиваленты'}` и `1370` — по одному разу; код строки `ok` файла вне справочника (добавить в тест строку
`ok` 1231) — в конце, с её именем; у формы с `edition_lines: []` — только коды строк `ok`; нет строк `unreadable` /
`detail`;
- `filterCodeOptions` — `'12'` → коды на `12…`; `'денеж'` → 1250; `'ДЕНЁЖ'` → 1250 (`ё` = `е`, регистр не важен); `''` →
первые 20;
- `lineMenuItems` — `l-u7` → `['mark_detail']`; `l-d2` → `['assign_code']`; `l-1230` (`needsReview`) → `['mark_detail',
'change_code', 'confirm_code']`; `l-1230` с `isConfirmed` → `['mark_detail', 'change_code', 'unconfirm_code']`; при
действии — последним `reset`; шаблонная 1250 → `[]`; подписи дословно;
- `applyLineMenu` — `confirm_code` → `{kind: 'confirm', confirmed: true}`; `reset` убирает ключ; новое действие заменяет
прежнее;
- `lineErrorTarget` — `ApiError` 409 `{code: 'LINE_CODE_TAKEN', message, line_id: 'l-u7', line_code: '1170'}` →
`{lineId: 'l-u7', lineCode: '1170'}`; 422 `INVALID_LINE_PATCH` с `line_code: null` → `{lineId, lineCode: null}`; 409
`LINES_UNRESOLVED` → `null`; 422 массивом (pydantic) → `null`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/codeSuggestions.test.ts src/widgets/DocumentReview/lineMenu.test.ts` — FAIL.
- [ ] **Step 3: Implement**; `CodeCell` в режиме правки при `showsCodeInput` — `AutoComplete` (ширина колонки кода в
режиме правки — 160) с опциями «<код> — <имя>», значение — код; ошибка формата или строка `lineErrorTarget` ответа — `status='error'`
с подсказкой; у строки с действием `detail` — серый тег «→ расшифровка», с `confirm` — «→ код подтверждён» /
«→ подтверждение снято». `LineMenu` — кнопка `MoreOutlined` в ячейке наименования (`Dropdown`, `trigger click`),
только в режиме правки. Ответ на сохранение с ошибкой элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 коды строк из
Global Constraints): `lineErrorTarget` — красное поле кода этой строки с текстом сервера и `message.error`; строки на
экране нет — тот же текст над таблицей (P6-9). Прочие 422 — `message.error(e.message)`; черновик сохраняется.
- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате фикстуры: ввести 1250 у «ИЧ,250» → перехваченное
тело `PATCH` содержит `lines: [{line_id: 'l-u7', line_code: '1250'}]` и только его.
- [ ] **Step 5: Commit** `feat(statement): правка строк — код с подсказкой из справочника, меню строки, LINE_CODE_TAKEN (#<issue>)`.
## Task 8: Подтверждение ячеек, даты колонок, правка шапки
**Files:**
- Create: `src/widgets/DocumentReview/ColumnHeader.tsx`, `src/widgets/DocumentReview/editRules.ts`, `editRules.test.ts`
- Modify: `ValueCell.tsx`, `StatementTable.tsx`, `StatementHeaderBlock.tsx`, `StatementReview.tsx`
**Interfaces:**
- Consumes: `StatementDraft` (T6), `IssuePlaces.byColumn` (T4), `HeaderFieldRow.canConfirm` (T5).
- Produces:
```ts
export const COLUMN_DATE_CODES: ReadonlySet<string>; // COLUMN_WITHOUT_DATE, COLUMN_DATE_UNCERTAIN
export function columnDateEditable(col: ColumnOut, columnIssues: IssueOut[]): { editable: boolean; canConfirm: boolean };
export function cellConfirmState(row: GridRow, value: ValueOut | undefined, draft: StatementDraft, key: string):
'confirm' | 'unconfirm' | 'pending-confirm' | 'pending-unconfirm' | null;
```
`columnDateEditable`: `editable` — (`period_kind === 'none'` ∧ `period_date === null`) или у колонки запись из
`COLUMN_DATE_CODES`; `canConfirm` — `editable` ∧ `period_date !== null` ∧ запись из `COLUMN_DATE_CODES` есть.
`cellConfirmState`: `null`, если у строки или значения нет `id` или в черновике есть сумма этой ячейки; черновик
`confirms[key]` → `pending-*`; иначе `needs_review` → `confirm`, `is_confirmed` → `unconfirm`, прочее — `null`.
- [ ] **Step 1: Write the failing tests** (`editRules.test.ts`, фикстура):
- `columnDateEditable` — `c2` с `i2` → `{editable: true, canConfirm: false}`; `c1` без записей → `{false, false}`; `c1` с
записью `COLUMN_DATE_UNCERTAIN` → `{true, true}`;
- `cellConfirmState` — `l-u7:c1` (`needs_review`) → `'confirm'`; с `confirms[key] = true` → `'pending-confirm'`; с
суммой в черновике → `null`; ячейка шаблонной 1250 → `null`; значение `is_confirmed true` → `'unconfirm'`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/editRules.test.ts` — FAIL.
- [ ] **Step 3: Implement**; `ValueCell` в режиме правки — рядом с полем суммы маленькая кнопка: `confirm` — `CheckOutlined`
«Подтвердить значение», `unconfirm` — `UndoOutlined` «Снять подтверждение», `pending-*` — та же кнопка нажатой
(повторный клик убирает из черновика); `ColumnHeader` — подпись `columnTitle`, значки `byColumn`, в режиме правки при
`editable` — `Input` `ГГГГ-ММ-ДД` (значение — черновик или `period_date`) и «Подтвердить дату» при `canConfirm` (кладёт
в черновик текущую дату); шапка — ввод организации и «Подтвердить дату» (`confirm_report_date`).
- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате: подтвердить ячейку «ИЧ,250», дату `c2`
`2024-12-31`, «Подтвердить дату» отчёта, организацию «ООО Ромашка» — одно сохранение, в теле `values[{…confirmed:
true}]`, `columns`, `header {report_date: '2025-09-30', organization_name: 'ООО Ромашка'}`.
- [ ] **Step 5: Commit** `feat(statement): подтверждение ячеек, дата колонки, наименование организации и подтверждение даты (#<issue>)`.
## Task 9: «Подтвердить данные» — неразобранные строки и новые 409
**Files:**
- Create: `src/widgets/DocumentReview/confirmBlockers.ts`, `confirmBlockers.test.ts`
- Modify: `src/widgets/DocumentReview/confirmError.ts`, `confirmError.test.ts`, `StatementReview.tsx`
**Interfaces:**
- Consumes: `GridRow` (T1), `focusRow` (T4), `UnresolvedLineOut` (T1).
- Produces:
```ts
export function unresolvedLines(rows: GridRow[]): GridRow[]; // P6-10, по rowIndex
export const unresolvedHint = (n: number): string => `Разберите строки без кода: ${n}`;
// confirmError.ts
export function confirmErrorInfo(e: unknown): { code: string | undefined; message: string; failures: FailedCheckOut[];
lines: UnresolvedLineOut[]; documentId: string | null };
```
- [ ] **Step 1: Write the failing tests:**
- `unresolvedLines` (фикстура) — `['l-u7', 'l-m8']`; строка `missing` без значений — не входит; `unreadable` только со
`string_value '211(1 |'` — входит; `detail` с суммой — не входит; форма 1.x (все `ok`) — `[]` (Review Focus 4);
- `unresolvedHint(2) === 'Разберите строки без кода: 2'`;
- `confirmErrorInfo` — `ApiError(409, 'LINES_UNRESOLVED', …, {detail: {lines: [{line_id: 'l-m8', row_index: 8,
line_code_text: null}]}})` → `lines` из одного элемента; `DUPLICATE_FORM` с `detail.document_id 'doc-0'` →
`documentId 'doc-0'`; `CHECKS_NOT_PASSED` — `failures` как раньше; не `ApiError` — прежний текст, `lines []`,
`documentId null`.
- [ ] **Step 2:** `npx vitest run src/widgets/DocumentReview/confirmBlockers.test.ts src/widgets/DocumentReview/confirmError.test.ts` — FAIL.
- [ ] **Step 3: Implement**; кнопка «Подтвердить данные» отключена при `unresolvedLines(rows).length > 0` (вдобавок к
`reviewActions`), обёрнута в `Tooltip` с `Typography.Link` «Разберите строки без кода: N» → `focusRow(первая.key)`;
ответ 409: `LINES_UNRESOLVED` — красный блок с текстом `unresolvedHint(detail.lines.length)` и ссылкой к первой строке
по `line_id`; `DUPLICATE_FORM` — текст сервера и ссылка «Открыть подтверждённую форму» (`navigate('/documents/' +
documentId)`); `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` — как сейчас.
- [ ] **Step 4:** `make lint typecheck test build` — PASS; на перехвате: кнопка отключена, подсказка «Разберите строки
без кода: 2», клик ведёт к «ИЧ,250»; перехват `POST …/confirm` с 409 `DUPLICATE_FORM` даёт ссылку.
- [ ] **Step 5: Commit** `feat(statement): блокировка подтверждения при строках без кода, LINES_UNRESOLVED и DUPLICATE_FORM (#<issue>)`.
## Task 10: Карточка файла и ОСВ — предупреждения структурами
**Files:**
- Create: `src/widgets/FilesDrawer/fileWarnings.ts`, `fileWarnings.test.ts`, `FileWarningsPopover.tsx`
- Modify: `src/widgets/FilesDrawer/documentRows.ts`, `documentRows.test.ts`, `FilesList.tsx`,
`src/widgets/DocumentReview/OsvDocument.tsx`
**Interfaces:**
- Consumes: `FileWarningOut`, `IssueOut`, `JobBrief` (T1); `IssuesPanel` (T4); `getDocument` (`services/api/documents.ts`).
- Produces:
```ts
export function toFileWarnings(value: unknown): FileWarningOut[]; // P6-13
export function warningsLevel(list: readonly { severity: Severity }[]): Severity | null;
export const warningText = (w: FileWarningOut): string => (w.locator ? `${w.message} — ${w.locator}` : w.message);
// documentRows.ts: DocumentRow + fileWarnings: FileWarningOut[]; mappingWarnings: string[]; issueCounts: IssueCountsOut
export function rowBadge(row: DocumentRow): { count: number; level: Severity } | null;
// P6-12: count = fileWarnings.length + issueCounts.error + issueCounts.info; level 'error', если есть error в файле
// или issueCounts.error > 0; count 0 — null
```
`IssuesPanel` получает режим `mode: 'statement' | 'document'`: в `document` все записи — пункты без ссылок (P6-19).
- [ ] **Step 1: Write the failing tests:**
- `toFileWarnings` — `['sheet=ОСВ 60 строки без родителя']` → `[{code: 'LEGACY_WARNING', severity: 'info', message:
'sheet=ОСВ 60 строки без родителя', locator: null}]`; `['a', {code: 'JSON_FAILED', severity: 'error', message: 'm',
locator: 'file:x_ofr.json'}, 5, {code: 'X'}, {code: 'Y', severity: 'warn', message: 'z'}]` → два элемента
(`LEGACY_WARNING` «a», `JSON_FAILED`); `'строка'` → `[]`; `null` → `[]`; объект без `locator` → `locator null` —
Review Focus 5;
- `warningsLevel` — `[info, error]` → `'error'`; `[]` → `null`;
- `warningText` — `'m — file:x_ofr.json'`; без `locator` — `'m'`;
- `buildDocumentRows` — файл с двумя документами и `job.warnings` из одного `JSON_FAILED` → у обеих строк
`fileWarnings` длины 1; файл `ERROR` без документов — у строки файла тоже; `job null` → `[]`; `mapping_warnings`
переносятся как есть; `issue_counts` документа → `issueCounts` (строка файла без документов — `{error: 0, info: 0}`);
- `rowBadge` — документ без предупреждений файла с `issue_counts {error: 0, info: 2}` → `{count: 2, level: 'info'}`;
`JSON_FAILED` в файле и `{error: 0, info: 1}` → `{count: 2, level: 'error'}`; `{error: 1, info: 0}` без файла →
`level 'error'`; всё по нулям → `null`.
- [ ] **Step 2:** `npx vitest run src/widgets/FilesDrawer/fileWarnings.test.ts src/widgets/FilesDrawer/documentRows.test.ts` — FAIL.
- [ ] **Step 3: Implement**; `FilesList` — в колонке «Статус» под плашкой значок с числом (`rowBadge` → цвет P6-15),
клик (`stopPropagation`, строка-ссылка не срабатывает) открывает `Popover` `FileWarningsPopover`: «Предупреждения
файла» (тег уровня + `warningText`), «Предупреждения обработки» (`mappingWarnings`, `info`), «Предупреждения документа»
— при открытии `getDocument(documentId)` → `issues` (загрузка — `Spin`, ошибка — «Не удалось загрузить», без
`documentId` раздела нет). `OsvDocument` — `IssuesPanel mode='document'` над таблицей при непустом `document.issues`.
- [ ] **Step 4:** `make lint typecheck test build` — PASS.
- [ ] **Step 5: Сверка с макетом** — панель файлов против `docs/mockup/screens/39-org-documents.png` (1600×1000): значок
не ломает высоту строки и ширину колонки «Статус»; расхождения — в ledger.
- [ ] **Step 6: Commit** `feat(files): предупреждения файла структурами и предупреждения документа в панели файлов и у ОСВ (#<issue>)`.
## Task 11: Сценарий на стенде, сверка с макетом, документация репозитория
**Files:**
- Modify: `README.md` (раздел про экран проверки формы — что нового, как включить перехват для демо не нужно),
`CLAUDE.md` arm-web («Архитектура»: новые чистые модули `DocumentReview` — `formLayout`, `issuePlaces`,
`statementDraft`, `cellView`, `codeSuggestions`, `lineMenu`, `editRules`, `confirmBlockers`; `FilesDrawer/fileWarnings`)
**Interfaces:**
- Consumes: всё выше; стенд — `docs/dev/qa-guide.md` §4.1–§4.4 (ядро — из worktree или `main` arm-core с частью А, лучше
А + Б; `make start` в worktree arm-web), формы — §6.9 (Excel Межениновской) и §8.5 (`aep_6.pdf`, при поднятых
сервисах §6.8).
- [ ] **Step 1: Регрессия на формах 1.x** (реальный стенд, пользователь `ka_go`): прогнать W19–W28b `qa-guide` §8.5 и
форму Excel `1_Форма1_бух_09.xlsx` (§6.9). Ожидание: раскладка и итоги как прежде; оранжевые только ячейки с
`needs_review` (у чистых — нет); переключателя расшифровок нет; «Подтвердить данные» доступна; в шапке — строка
«Организация»; в панели файлов у заданий, сохранённых до v5.19, предупреждения видны как «Информация». Скриншоты против
`41-document-check.png`, `41-02-document-check-edit.png`, `39-org-documents.png`.
- [ ] **Step 2: Перехват v2** (P6-14) — через `browser_run_code_unsafe` Playwright:
```js
const fixture = /* содержимое src/widgets/DocumentReview/__fixtures__/statement-v2.json, id документа заменён на открытый */;
await page.route('**/api/v1/documents/*/statement', async (route) => {
if (route.request().method() === 'PATCH') { globalThis.lastPatch = route.request().postDataJSON(); }
await route.fulfill({ json: fixture });
});
```
Сценарии спеки `arm-web` по очереди: «Код-мусор на своём месте» (под 1240, подсвечена, серый «ИЧ,250» с подсказкой);
«Расшифровки скрыты» (не видно, «Показать расшифровки (2)», включённый показывает под 1150 и в начале раздела I);
«Догадка подтверждена проверкой» (1230: не подсвечена, подсказка «Распознано: «42831 1»»); предупреждения у места
(1370 — значок `ROW_SPLIT`, колонка 2 — `COLUMN_WITHOUT_DATE`, организация и дата — значки, `COLUMN_SKIPPED` — в списке);
«Назначить код» (тело `PATCH` — `lines: [{line_id: 'l-u7', line_code: '1250'}]`); «Подтверждение заблокировано»
(подсказка «Разберите строки без кода: 2», клик ведёт к первой); подтверждение ячейки, дата колонки, организация,
подтверждение даты — одно тело. Перехват `POST …/confirm` с 409 `LINES_UNRESOLVED` и `DUPLICATE_FORM`, `PATCH` с 409
`LINE_CODE_TAKEN` (`detail {line_id: 'l-u7', line_code: '1170'}`) — тексты и места по T7, T9. Перехват
`GET /legal-entities/*/files` с `job.warnings` (`JSON_FAILED` `error`, `locator 'file:x_ofr.json'`) и
`documents[].issue_counts` — сценарий «Потерянный ОФР в карточке», значок у документа без предупреждений файла.
- [ ] **Step 3:** итог сверки — в ledger и в описание PR: что сверено (экраны 41, 41-02, 39), что отличается и почему
(новые элементы, которых нет в макете: поле кода, меню строки, кнопки подтверждения, значки уровней).
- [ ] **Step 4:** README и CLAUDE.md arm-web; `npm run format`; `make lint typecheck test build` — PASS.
- [ ] **Step 5: Commit** `docs(arm-web): экран формы v5.19 — README и карта модулей (#<issue>)`.
## Завершение
- [ ] `make lint typecheck test build` — PASS, вывод — в описание PR (с пометкой P6-18: `npm install` пропущен, node_modules —
симлинк).
- [ ] Финальное ревью ветки (opus): код-ревью TS/React с объяснением решений простым языком для владельца (правило
`arm-web/CLAUDE.md`); таблица находок `## Findings` в ledger (`| Маркер | Суть | Статус |`).
- [ ] PR: `tea pulls create --login home --repo RSHB/arm-web --base test --head feat/<issue>-statements-v2`, ссылка на issue и
на arm-ka-docs#116; в описании — «Rulings I made», `P6-n`, O-1…O-6, В-1…В-3, итог сверки с макетом, `deferred`-находки,
строка «выкладывается только вместе с ядром v5.19 (план 3 А + Б); после merge — `make -C docs sync-web`». Merge — по
команде владельца.
- [ ] После merge в `test`: `make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web SYNC_ARGS=--check` (ожидание —
код 10, отстаёт), затем `make -C …/docs sync-web` — issue и PR в arm-core (ревью кода против бэкенда — за агентом; merge
— по команде владельца). `arm-core/web` напрямую не править.
- [ ] При «закрывай» — чеклист `~/.claude/CLAUDE.md`: `openspec archive statements-v2` в arm-web, SDD workspace, ветка и
worktree `.wt/normalization-web` (симлинк `node_modules` удаляется вместе с worktree, цель симлинка не трогать), follow-up
issue с `deferred` и повторным прогоном на `aep_3` после шага 5 выкладки (В-1).
---
## Покрытие спеки `arm-web`
| Требование / сценарий | Задачи |
|---|---|
| Раскладка строк формы со статусами: разделы, `ok` по коду, повторы по `row_index`, шаблонные и пятизначные на местах | T2, T3 |
| … `unreadable` / `missing` под ближайшей предыдущей, без неё — в начале первого раздела; подсветка целиком, серый `line_code_text` | T2, T3 |
| … `detail` под переключателем «Показать расшифровки (N)», под родителем с отступом | T2, T3 |
| … справочное имя основным, файловое рядом при отличии; переключателя «как в файле» нет | T2, T3 |
| Сценарии «Код-мусор на своём месте», «Расшифровки скрыты» | T2 (тесты), T11 |
| Подсветка ячеек по `needs_review` с `text` и уверенностью; строки — по `needs_review` строки | T1, T3 |
| Поля шапки — по `needs_review_fields`, подсказка из `header.source` | T5 |
| `issues` и `checks` у строк, ячеек, полей, документа; `column_index` без `column_id` — у документа; `error` ≠ `info` | T4, T5 |
| Значения `detail` не подсвечиваются, подсказка есть; сценарий «Догадка подтверждена проверкой» | T3 |
| Режим «Редактировать»: поле кода с подсказкой, «Это расшифровка», «Назначить код», «Сделать расшифровкой», смена кода, «Подтвердить код» | T7 |
| Подтверждение и снятие подтверждения ячейки и кода; дата колонки (в том числе подтверждение той же даты) | T7, T8 |
| Наименование организации; подтверждение даты отчёта тем же значением | T5, T8 |
| Один `PATCH` (`header`, `values`, `columns`, `lines`); сценарий «Назначить код» | T6, T7, T11 |
| «Подтвердить данные» отключена с подсказкой «Разберите строки без кода: N» и переходом; 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`, `LINE_CODE_TAKEN` | T7, T9 |
| Сценарий «Подтверждение заблокировано» | T9, T11 |
| Карточка файла: `job.warnings` структурами, `message` и `locator`, `error` ≠ `info`; `DocumentOut.issues`; `mapping_warnings` строками; сценарий «Потерянный ОФР в карточке» | T10, T11 |