diff --git a/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md b/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md index 6924918..dea9726 100644 --- a/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md +++ b/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-6-arm-web.md @@ -89,8 +89,10 @@ - «Сохранить» отправляет ровно один `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`, `PERIOD_REQUIRED`, `CHECKS_NOT_PASSED` (`detail.failures`); 422 - `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`, `UNKNOWN_CELL`, `LINE_CODE_REQUIRED`. + (`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 — то же, вывод @@ -142,7 +144,7 @@ | `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` | подсказка кода из справочника, пункты меню строки, `LINE_CODE_TAKEN` у поля | +| `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` | сборка экрана, переключатели, сохранение, переход к строке | @@ -156,7 +158,7 @@ | Маркер | Решение | Обоснование | Цена ошибки | |---|---|---|---| -| P6-1 | Подсказка кода — из самого ответа `StatementOut`: все строки `ok` с кодом (из файла и достроенные шаблонные) дают пары «код → `reference_name` (иначе `line_name`)»; отдельного справочника не запрашиваем | шаблонные строки ядро достраивает по полному составу редакции, поэтому ответ уже содержит весь справочник редакции; эндпоинта справочника нет | у формы без даты (редакции нет) подсказка беднее — O-3 | +| 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 («задолжен-ность») не должны давать ложного «отличается» | лишнее или пропущенное второе имя — только показ | @@ -164,42 +166,41 @@ | 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 | 409 `LINE_CODE_TAKEN` показывается у полей кода черновика, чей код совпадает с кодом строки, созданной правкой (`origin = template`, `id` не `null`), у которой есть сумма; не нашли таких — текстом сервера над таблицей | в `detail` ответа нет кода и строки (O-2) | при ответе ядра с другой причиной красное поле не там — текст сервера показывается всё равно | +| 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` (и у строки файла без документов) — значок с числом предупреждений, красный при хотя бы одном `error`; клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` этого документа, загружаемые при открытии (`GET /documents/{id}`). Значка «есть предупреждения документа» без предупреждений файла нет — их видно на экране документа | `DocumentBrief` не несёт `issues` (O-4); N запросов на каждое открытие панели — дорого | документ с предупреждением листа ОСВ без предупреждений файла не помечен в панели | +| 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` читается как `number \| string \| null` и форматируется через `Number()` | ядро отдаёт `header_source` из JSONB как пришло (в контракте — число), прочие уверенности — строки Decimal (O-5) | нет | +| 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 не правился) +## Чего не хватает в API ядра для экрана (`O-n`) -- **O-1.** План 3, A8: в `LineOut` не перечислен `line_code_confidence`, хотя спека `arm-core` («Ответ `GET …/statement` - по v2.0») его требует. Экран показывает его в подсказке кода (P6-16). Без поля подсказка без уверенности; тип — - `string | null`. -- **O-2.** 409 `LINE_CODE_TAKEN` (A13) и 422 `INVALID_LINE_CODE` / `UNKNOWN_LINE` / `INVALID_LINE_PATCH` не несут в - `detail` ни `line_id`, ни `line_code`. Экран вычисляет место сам (P6-9) или показывает текст над таблицей. Просьба к - ядру: `detail.line_id` и `detail.line_code` у этих кодов. -- **O-3.** Нет эндпоинта справочника строк редакции (код → наименования). Подсказка кода строится из ответа (P6-1); у - формы без даты отчёта (редакции нет, шаблонные строки не достраиваются) подсказка — только из строк файла. Кандидат: - `GET /directories/statement-lines?document_type=&edition=`. -- **O-4.** `DocumentBrief` (в `FileOut.documents[]`) не несёт сводки `issues` — панель файлов не может пометить документ с - предупреждениями без запроса на каждый документ (P6-12). Кандидат: `DocumentBrief.issues_summary {error: int, info: int}`. -- **O-5.** Тип `header.source.<поле>.confidence` не зафиксирован (A8: `dict[str, dict[str, Any]]` из JSONB — число; - остальные уверенности API — строки Decimal). Нужна одна строка в гайде §5.6.1. -- **O-6.** В `StatementHeaderOut` нет `okei`, хотя `headerField` контракта его содержит; запись `issues` с `field = "okei"` - экран покажет у документа. Не блокирует. +Все пункты закрыты сведением с планом 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.** Панель файлов не помечает документы, у которых есть только предупреждения документа (O-4, P6-12). Достаточно до - доработки `DocumentBrief`? +- **В-2.** Снят 18.10.2026: `DocumentBrief.issue_counts` (план 3 P3-23) — панель помечает и документы только с + предупреждениями документа (P6-12). - **В-3.** Подтверждённая ячейка помечается маленькой галочкой (P6-15) — в спеке и макете вида нет. Устраивает? --- @@ -292,7 +293,10 @@ export interface IssueOut { 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: number | 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 }) @@ -308,13 +312,15 @@ export interface StatementPatch { Изменения существующих: `JobBrief.warnings: FileWarningOut[]` (`mapping_warnings` — `string[]`); `DocumentOut.issues: IssueOut[]` (`warnings: string[]` остаётся); `StatementHeaderOut` + `needs_review_fields: string[]`, `source: - Partial>`, `organization_name_modified: boolean`; `ColumnOut` + `period_kind: PeriodKind`, + Partial>`, `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` (O-1), `reference_name: string | null`, + 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.issues: IssueOut[]`, `StatementOut.edition_lines: EditionLineOut[]`; `DocumentBrief.issue_counts: + IssueCountsOut` (и `DocumentOut`, который его расширяет). - Produces (`statementGrid.ts`): ```ts @@ -341,8 +347,8 @@ export function cellFlags(value: ValueOut | undefined): { attention: boolean; mo документ `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"]`, - `source {organization_name: {text: "ООО «Ромашкa»", confidence: 67}, report_date: {text: "30 сентября 2025 г.", - confidence: 67}}`; колонки `c1` (`column_index 2`, «На 30 сентября 2025 г.», `2025-09-30`, `absolute`) и `c2` + `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`) | @@ -370,6 +376,9 @@ export function cellFlags(value: ValueOut | undefined): { attention: boolean; mo 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`, @@ -666,15 +675,18 @@ export function buildStatementPatch(draft: StatementDraft, ctx: { rows: GridRow[ - Modify: `CodeCell.tsx`, `StatementTable.tsx`, `StatementReview.tsx` **Interfaces:** -- Consumes: `GridRow` (T1), `StatementDraft`, `LineAction` (T6). +- Consumes: `GridRow`, `EditionLineOut`, `LineErrorDetail` (T1), `StatementDraft`, `LineAction` (T6). - Produces: ```ts // codeSuggestions.ts export type CodeOption = { code: string; name: string }; -export function codeOptions(rows: GridRow[]): CodeOption[]; // P6-1: строки ok с кодом, name = referenceName ?? name ?? '', по коду, без повторов +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 codeTakenKeys(draft: StatementDraft, rows: GridRow[]): string[]; // P6-9 +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 }[]; @@ -690,8 +702,10 @@ export function showsCodeInput(row: GridRow, action: LineAction | undefined): bo любой строки с действием `code`. - [ ] **Step 1: Write the failing tests:** - - `codeOptions` (фикстура) — содержит `{code: '1250', name: 'Денежные средства и денежные эквиваленты'}` (шаблонная) и - `{code: '1370', name: 'Нераспределенная прибыль'}` один раз; нет строк `unreadable` / `detail`; + - `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', @@ -699,16 +713,17 @@ export function showsCodeInput(row: GridRow, action: LineAction | undefined): bo действии — последним `reset`; шаблонная 1250 → `[]`; подписи дословно; - `applyLineMenu` — `confirm_code` → `{kind: 'confirm', confirmed: true}`; `reset` убирает ключ; новое действие заменяет прежнее; - - `codeTakenKeys` — `{l-u7: code '1170'}` → `['l-u7']` (`l-t1170` создана правкой, сумма 3); `{l-u7: code '1250'}` → `[]` - (шаблонная 1250 пуста). + - `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) с опциями «<код> — <имя>», значение — код; ошибка формата или `codeTakenKeys` — `status='error'` + режиме правки — 160) с опциями «<код> — <имя>», значение — код; ошибка формата или строка `lineErrorTarget` ответа — `status='error'` с подсказкой; у строки с действием `detail` — серый тег «→ расшифровка», с `confirm` — «→ код подтверждён» / «→ подтверждение снято». `LineMenu` — кнопка `MoreOutlined` в ячейке наименования (`Dropdown`, `trigger click`), - только в режиме правки. Ответ 409 `LINE_CODE_TAKEN` на сохранение: `codeTakenKeys` не пуст — красные поля и - `message.error` с текстом сервера; пуст — тот же текст над таблицей (P6-9). Ответ 422 с кодами из Global Constraints — - `message.error(e.message)`, черновик сохраняется. + только в режиме правки. Ответ на сохранение с ошибкой элемента `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 (#)`. @@ -802,7 +817,10 @@ export function confirmErrorInfo(e: unknown): { code: string | undefined; messag 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[] +// 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). @@ -817,9 +835,12 @@ export const warningText = (w: FileWarningOut): string => (w.locator ? `${w.mess - `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` — в колонке «Статус» под плашкой значок с числом (`warningsLevel` → цвет P6-15), +- [ ] **Step 3: Implement**; `FilesList` — в колонке «Статус» под плашкой значок с числом (`rowBadge` → цвет P6-15), клик (`stopPropagation`, строка-ссылка не срабатывает) открывает `Popover` `FileWarningsPopover`: «Предупреждения файла» (тег уровня + `warningText`), «Предупреждения обработки» (`mappingWarnings`, `info`), «Предупреждения документа» — при открытии `getDocument(documentId)` → `issues` (загрузка — `Spin`, ошибка — «Не удалось загрузить», без @@ -863,8 +884,9 @@ await page.route('**/api/v1/documents/*/statement', async (route) => { «Назначить код» (тело `PATCH` — `lines: [{line_id: 'l-u7', line_code: '1250'}]`); «Подтверждение заблокировано» (подсказка «Разберите строки без кода: 2», клик ведёт к первой); подтверждение ячейки, дата колонки, организация, подтверждение даты — одно тело. Перехват `POST …/confirm` с 409 `LINES_UNRESOLVED` и `DUPLICATE_FORM`, `PATCH` с 409 - `LINE_CODE_TAKEN` (код 1170) — тексты и места по T7, T9. Перехват `GET /legal-entities/*/files` с `job.warnings` - (`JSON_FAILED` `error`, `locator 'file:x_ofr.json'`) — сценарий «Потерянный ОФР в карточке». + `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.