arm-web/docs/superpowers/plans/2026-10-18-statements-v2.md
2026-10-18 13:11:49 +04:00

100 KiB
Raw Blame History

План 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/43-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/43-statements-v2; хук require-plan пропускает правки src/ (план с именем ветки лежит в docs/superpowers/plans/); design.md с разделом ## Decisions made without discussion (туда — P6-n и рулинги контроллера).

  • Step 1: Worktree и зеркальный workspace (порядок — ~/.claude/CLAUDE.md, «Роли инструментов»):

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:
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 и ветка:
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/43-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 вместо 43 (sed "s/43/<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, #43).

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):
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):
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"; i7warningNAME_MISMATCHinfo, sourcecore, 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, предупреждения файла (#43).

Task 2: Раскладка — вложения строк без кода и расшифровок, фильтр показа, имена

Files:

  • Modify: src/widgets/DocumentReview/formLayout.ts, src/widgets/DocumentReview/formLayout.test.ts

Interfaces:

  • Consumes: GridRow (T1).
  • Produces:
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. Вставка вложений — отдельной внутренней функцией после построения базовой раскладки:
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): раскладка строк без кода и расшифровок на своих местах (#43).

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:
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, переключатель расшифровок (#43).

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:
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 — «строка без кода «»» (без текста — «строка без кода»), detail — «расшифровка «»»; ячейка — «<строка>, колонка «»»; колонка — «колонка «»»; поле — «поле «<подпись поля>»»; документ — «документ», с 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): предупреждения и нарушения у строк, ячеек, колонок и в списке документа (#43).

Task 5: Шапка формы — наименование организации, поля на проверку, подсказки источника

Files:

  • Modify: src/widgets/DocumentReview/headerFields.ts, headerFields.test.ts, StatementHeaderBlock.tsx

Interfaces:

  • Consumes: StatementHeaderOut (T1), IssuePlaces.byField (T4).
  • Produces:
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] — «Распознано: «» · уверенность 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): шапка — организация, поля на проверку и подсказки распознанного текста (#43).

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:
// 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 (#43).

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:
// 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 (#43).

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:
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): подтверждение ячеек, дата колонки, наименование организации и подтверждение даты (#43).

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:
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 (#43).

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:
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): предупреждения файла структурами и предупреждения документа в панели файлов и у ОСВ (#43).

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:

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 и карта модулей (#43).

Завершение

  • 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/43-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