line_code_confidence и okei в типах, header.source.confidence — строка, подсказка кода из StatementOut.edition_lines, ошибка элемента lines[] у поля строки по detail.line_id (lineErrorTarget вместо codeTakenKeys), значок документа в панели файлов по DocumentBrief.issue_counts (rowBadge). Закрыты O-1…O-6, снят В-2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
100 KiB
План 6 — arm-web: экран формы ББ/ОФР и карточка файла по API v5.19. План реализации
Для исполнителей-агентов: обязательный навык — superpowers:subagent-driven-development (в плане 12 задач, задачи опираются на интерфейсы друг друга). Контроллер SDD — фоновый сабагент на модели sonnet; исполнители — по SDD Model Selection (
~/.claude/CLAUDE.md, «Исполнение»): T1 — haiku (типы и фикстура заданы планом), остальные — sonnet, финальное ревью ветки — opus. Шаги размечены чекбоксами (- [ ]).
Goal: экран проверки формы ББ/ОФР [41] показывает все строки источника на своих местах (строки без кода — под
ближайшей предыдущей строкой с кодом, расшифровки — под переключателем), подсвечивает только то, что требует проверки
(needs_review), показывает предупреждения и нарушения у места, даёт в режиме «Редактировать» разобрать строки,
подтвердить ячейки, коды и даты одним PATCH, блокирует «Подтвердить данные» при неразобранных строках; карточка файла
читает job.warnings структурами.
Architecture: вся логика — чистые функции в .ts рядом с компонентами (src/widgets/DocumentReview/,
src/widgets/FilesDrawer/), покрытые vitest; компоненты React только рисуют то, что вернули функции, и не тестируются
юнит-тестами (соглашение arm-web/CLAUDE.md). Черновик правок аналитика — одна структура StatementDraft, из которой
одна функция собирает тело единственного PATCH. Экран проверяется сценарием в Playwright на локальном стенде; до того
как сервисы распознавания начнут слать контракт 2.0, строки со статусами подставляются в ответ GET …/statement
перехватом запроса (фикстура плана).
Tech Stack: React 18 + TypeScript strict, antd 6 (Table, AutoComplete, Dropdown, Popover, Tooltip,
Switch), rsbuild, vitest 3.1.4, biome + ESLint + Prettier; Playwright (MCP-плагин сессии) для сценария; tea
(Gitea), OpenSpec.
Spec: /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/
(ветка feat/116-unified-normalization репозитория arm-ka-docs): главное — specs/arm-web/spec.md; API, которое экран
потребляет, — specs/arm-core/spec.md (требования «Ответ GET …/statement по v2.0», «PATCH формы — строки, колонки,
подтверждение, наименование организации», «Подтверждение формы — порядок 409», «Предупреждения и нарушения документа —
document_issue», «Предупреждения файла и ОСВ в API»); гайд фронта — dev/frontend-api-guide.md §5.6.1; правила —
context/05-domain-rules.md §9.5 п. 2, §11.9 (таблица предупреждений: коды и уровни); точные схемы ответов и коды —
план 3 (plans/2026-10-18-plan-3-arm-core.md), задачи A5, A8, A11, A12, A13; решения — design.md (Р-14 экран, Р-23
значения detail, Р-25 подтверждение без правки, S-62 родитель detail, S-47 пробелы PATCH). Макет — mockup/screens/41-document-check.png,
41-02-document-check-edit.png, 39-org-documents.png.
Место в работе и порядок
| План | Что | Связь с этим планом |
|---|---|---|
| 3, часть А | arm-core: схема v5.19, StatementOut с новыми полями, PATCH (lines, columns, confirmed, organization_name), порядок 409, JobBrief.warnings структурами |
задачи T1–T10 пишутся по спеке и фикстуре до слияния А; T11 (стенд) — после слияния А в main arm-core |
| 3, часть Б | приём контракта 2.0, §7 в document_issue |
для T11 желательна (нарушения §7 в issues у форм 1.x появляются с Б) |
сервисы (шаг 5 proposal.md) |
tabular-parser и scan-gateway шлют 2.0 | до них реальных строк unreadable / missing / detail на стенде нет — их показывает перехват (P6-14); повторный прогон на aep_3 после шага 5 — follow-up |
sync-web |
перенос arm-web@test в arm-core/web |
после merge PR этого плана в test; снимок контура — только вместе с ядром А + Б (O-9 плана 3) |
Выкладка: фронт выходит в той же выкладке, что ядро: FileOut.job.warnings меняет тип со строк на объекты
(несовместимо), а встроенный фронт контура — arm-core/web, который обновляется только make -C docs sync-web.
Как это устроено — для владельца
- Раскладка «как на бумаге». Ядро отдаёт строки списком; разделы баланса и места итогов строит фронт
(
formLayout.ts, так уже работает с Среза 3b). Новое: строки без кода (unreadable,missing) и расшифровки (detail) не участвуют в раскладке по коду, а «прицепляются» к строке с кодом — к той, что стояла перед ними в файле (row_index), расшифровка — к своему родителю (parent_row_index). Это отдельная чистая функция, её проверяют тесты на сценариях спеки («ИЧ,250 под 1240»). - Почему черновик, а не запрос на каждое действие. Спека требует: «Сохранить» отправляет один
PATCH. Поэтому всё, что аналитик делает в режиме «Редактировать» (суммы, подтверждения ячеек, коды строк, даты колонок, шапка), копится в одном объектеStatementDraft, а функцияbuildStatementPatchпревращает его в тело запроса и заранее ловит ошибки, которые иначе вернул бы сервер (неверный формат кода или даты, конфликт «шаблонная строка + тот же код»). Ответ сервера целиком заменяет состояние экрана — так пересчитанные ядром статусы и проверки всегда актуальны. - Почему тесты только на функции. Так принято в arm-web: компоненты не тестируются, а всё, что решает «что
показать», вынесено в
.ts-функции. Компоненты проверяются руками в браузере по сценарию T11 со сверкой с макетом. - Новых npm-пакетов нет: всё нужное (выпадающая подсказка кода, меню строки, всплывающий список) есть в antd; в зеркале npm банка не всё доступно (vitest ≤ 3.1.4).
Разбивка на PR
| Issue | RSHB/arm-web: «Единая нормализация ББ/ОФР: экран формы и карточка файла по API v5.19 (arm-ka-docs#116)» |
| Ветка | feat/<issue>-statements-v2 (база — origin/test) |
| Зеркальный workspace | /Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/ |
| Worktree arm-web | /Users/andrey.epifancev/projects/awp-ca/.wt/normalization-web/arm-web |
| Change OpenSpec arm-web | openspec/changes/statements-v2/ |
| PR | в test (не в main); merge — только по команде владельца |
| После merge | make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web → PR в arm-core (arm-core/web напрямую не править) |
Global Constraints
- Типы
src/types/api.tsповторяют схемы ядра v5.19 (спекаarm-core«ОтветGET …/statementпо v2.0», план 3 A5/A8) и гайд §5.6.1; полей, которых там нет, не придумывать. - Все запросы — через
src/api/client.ts(api.get|post|patch); прямогоfetchнет. - Суммы — строки (
string | null); арифметики над суммами на фронте нет;Number()— только для уверенности (0..100). - Подсветка ячейки «требует внимания» — только по
values[].needs_review; поconfidence < 100иis_recognized = falseнапрямую не подсвечивать; «исправлено» (is_modified) — прежний синий. - Тексты интерфейса — дословно: «Показать расшифровки (N)», «Разберите строки без кода: N», «Это расшифровка», «Назначить код», «Сделать расшифровкой», «Подтвердить код», «Подтвердить данные», «Редактировать», «Сохранить».
- Переключателя «как в файле» нет (Р-14).
- «Сохранить» отправляет ровно один
PATCH /documents/{id}/statement(header,values,columns,lines); пустой черновик — запроса нет; ответ заменяетStatementOutцеликом. - Коды ошибок — дословно: 409
LINES_UNRESOLVED(detail.lines[] {line_id, row_index, line_code_text}),DUPLICATE_FORM(detail.document_id),LINE_CODE_TAKEN(detail.line_id,detail.line_code),PERIOD_REQUIRED,CHECKS_NOT_PASSED(detail.failures); 422INVALID_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.mdarm-web, раздел «Decisions made without discussion». - Каждый экран после правок сверяется с PNG макета (
docs/mockup/screens/41-*.png,39-*.png) при окне 1600×1000 и прогоняется самим исполнителем в браузере (память владельца «сверять фронт с макетом»).
Review Focus
- Якорь строки без кода, который скрыт или повторён — строка
unreadableпосле пустой пятизначной подстроки (скрыта по умолчанию), после второй из двух строк 2410, после строки в «Прочих строках»: вложение стоит под своим экземпляром (по ключу строки, не по коду) и не исчезает, когда якорь скрыт. Тесты — T2. - Предупреждение у скрытой или несуществующей строки — запись
issuesу строкиdetailпри выключенном переключателе (ссылка «Показать» включает расшифровки и ведёт к строке);line_idsсо строкой, которой нет в ответе (удалённая шаблонная), и поле шапки, которого нет в блоке шапки (okpo,okei), — уходят в список документа, а не теряются. Тесты — T4. - Смешанный черновик в одном сохранении — сумма и подтверждение одной ячейки, действие строки и сумма той же
строки, сумма в шаблонную строку 1250 и назначение 1250 строке без кода, пустое поле кода, код, равный текущему:
тело
PATCHне нарушает правил ядра (одно действие на строку,UNKNOWN_CELLпо P3-10), пустое не отправляется. Тесты — T6. - Форма, принятая по 1.x (все строки
ok,row_indexпо порядку,source_ref = null, у чистых значенийneeds_review = false, вissues—MAPPING_WARNING) — экран выглядит как до изменения, кроме подсветки поneeds_review; переключателя расшифровок нет (N = 0); кнопка «Подтвердить данные» не заблокирована. Тесты — T2, T9; сценарий — T11. job.warningsстарого формата — строки, смесь строк и объектов, не список, объект безseverity: панель файлов не падает, строки показываются какLEGACY_WARNINGinfo, мусор пропускается. Тесты — 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и 422INVALID_LINE_CODE,UNKNOWN_LINE,INVALID_LINE_PATCH,LINE_CODE_REQUIREDвdetailестьline_idиline_code(P3-24); экран ставит ошибку у поля строки (P6-9,lineErrorTarget, T7). - O-3. Закрыт:
StatementOut.edition_lines [{code, name}](P3-22), у формы без даты — коды всех редакций; подсказка кода — из него (P6-1). - O-4. Закрыт:
DocumentBrief.issue_counts {error, info}(P3-23); панель помечает документ без запроса (P6-12,rowBadge, T10). - O-5. Закрыт:
header.source.<поле>.confidence— строка Decimal (P3-20); P6-17. - O-6. Закрыт:
StatementHeaderOut.okei(P3-21); в блоке шапки поле не показывается (четыре поля T5), записьissuesсfield = "okei"по-прежнему уходит в список документа (Review Focus 2).
Открытые вопросы владельцу
- В-1. Приёмка экрана на шаге 4 выкладки — по перехвату с фикстурой (P6-14) и регрессии на формах 1.x; проверка на
реальных 2.0-данных (
aep_3, РБ 1 кв. 2025) — после перехода сервисов (шаг 5), отдельной задачей. Устраивает? - В-2. Снят 18.10.2026:
DocumentBrief.issue_counts(план 3 P3-23) — панель помечает и документы только с предупреждениями документа (P6-12). - В-3. Подтверждённая ячейка помечается маленькой галочкой (P6-15) — в спеке и макете вида нет. Устраивает?
Task 0: Подготовка — worktree, issue, ветка, OpenSpec change arm-web
Files:
- Create:
docs/superpowers/plans/2026-10-18-statements-v2.md(копия этого плана),openspec/changes/statements-v2/proposal.md,design.md,specs/document-review-ui/spec.md,specs/primary-files-ui/spec.md
Interfaces:
-
Produces: worktree на ветке
feat/<issue>-statements-v2; хук require-plan пропускает правкиsrc/(план с именем ветки лежит вdocs/superpowers/plans/);design.mdс разделом## Decisions made without discussion(туда —P6-nи рулинги контроллера). -
Step 1: Worktree и зеркальный workspace (порядок —
~/.claude/CLAUDE.md, «Роли инструментов»):
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/<issue>-statements-v2
cd $WS/.wt/normalization-web/arm-web && codegraph init
Все вызовы codegraph_explore — с projectPath=$WS/.wt/normalization-web/arm-web; пустой ответ — один раз отметить в
ledger и дальше Read/Grep.
- Step 4: Копия плана — этот файл копируется в
docs/superpowers/plans/2026-10-18-statements-v2.mdс подстановкой номера issue вместо<issue>(sed "s/<issue>/<N>/g"); проверка:grep -c "feat/<N>-statements-v2"≥ 1. - Step 5: Change OpenSpec —
openspec new change statements-v2;proposal.md: ссылка на change docs (unified-statement-normalization, arm-ka-docs#116), что меняется (по разделу Goal), non-goals (переключатель «как в файле», склейка форм), зависимость от ядра v5.19. Спеки-дельты (формулировки требований — изspecs/arm-web/spec.mddocs, дословно):document-review-ui: MODIFIED «Форма ББ/ОФР — просмотр и правка» (подсветка «требует внимания» — поneeds_review; правка расширена строками, колонками, подтверждениями, наименованием организации; полный текст требования с изменёнными фразами); MODIFIED «Подтверждение документа» (порядок 409 сLINES_UNRESOLVED,DUPLICATE_FORM, отключение кнопки); ADDED «Раскладка строк формы со статусами», «Подсветка и предупреждения у места», «Правка строк, колонок и подтверждение в режиме «Редактировать»», «Предупреждения ОСВ-документа» (P6-19) — со сценариями из docs;primary-files-ui: ADDED «Предупреждения файла и документа в карточке файла».design.md— раздел «Decisions made without discussion» с таблицейP6-1…P6-19(дата 2026-10-18).
- Step 6:
openspec validate statements-v2 --strict— без ошибок. - Step 7: Commit
openspec(statements-v2): change и план экрана формы по API v5.19 (arm-ka-docs#116, #<issue>).
Task 1: Типы API v5.19, фикстура, строки сетки
Files:
- Modify:
src/types/api.ts,src/widgets/DocumentReview/statementGrid.ts,src/utils/labels.ts,src/widgets/DocumentReview/statementGrid.test.ts,src/widgets/DocumentReview/formLayout.test.ts(хелперrow()),src/widgets/FilesDrawer/documentRows.test.ts(warnings: []— тип) - Create:
src/widgets/DocumentReview/__fixtures__/statement-v2.json,src/utils/labels.test.ts(дополнить, если есть)
Interfaces:
- Produces (в
src/types/api.ts):
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, 2025Q3,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, предупреждения файла (#<issue>).
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): раскладка строк без кода и расшифровок на своих местах (#<issue>).
Task 3: Таблица формы — вид строк и ячеек, переключатель расшифровок
Files:
- Create:
src/widgets/DocumentReview/cellView.ts,cellView.test.ts,CodeCell.tsx,ValueCell.tsx - Modify:
src/widgets/DocumentReview/StatementTable.tsx,StatementReview.tsx
Interfaces:
- Consumes:
formLayout,filterLayout,detailCount(T2),cellFlags(T1). - Produces:
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(сумма, галочка подтвержденияCheckOutlined10px серым, подсказка — строкиcellTipLinesчерез<br/>);StatementTable: колонка наименования —displayName, ниже серымsecondaryName, подсказкаfullName; фон строкиunreadable/missing— во всех ячейках строки;StatementReview: переключатель «Показать расшифровки (N)» (Switch, как «Показать пустые строки детализации»), показывается приdetailCount > 0; пустые подстроки по-прежнему считаетemptySublineKeys— теперь какhiddenKeysдляfilterLayout. - Step 4:
make lint typecheck test build— PASS. - Step 5: Сверка с макетом.
make start; экран ББ на стенде (форма 1.x) и с перехватом фикстуры (рецепт — T11, шаг 2): окно 1600×1000, скриншот противdocs/mockup/screens/41-document-check.png— разделы, итоги, «БАЛАНС» фиолетовым, строка «ИЧ,250» под 1240 красноватым фоном с серым кодом, переключатель «Показать расшифровки (2)»; расхождения — в ledger (O-n). - Step 6: Commit
feat(statement): вид строк по статусам, подсветка по needs_review, переключатель расшифровок (#<issue>).
Task 4: Предупреждения и нарушения у места
Files:
- Create:
src/widgets/DocumentReview/issuePlaces.ts,issuePlaces.test.ts,IssuesPanel.tsx - Modify:
StatementTable.tsx,ValueCell.tsx,StatementReview.tsx,ChecksPanel.tsx
Interfaces:
- Consumes:
GridRow,ColumnOut,IssueOut(T1);columnTitle(reviewView.ts);lineStatusLabel,severityLabel(T1). - Produces:
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): предупреждения и нарушения у строк, ячеек, колонок и в списке документа (#<issue>).
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): шапка — организация, поля на проверку и подсказки распознанного текста (#<issue>).
Task 6: Черновик правок и один PATCH
Files:
- Create:
src/widgets/DocumentReview/statementDraft.ts,statementDraft.test.ts - Modify:
src/widgets/DocumentReview/statementGrid.ts(удалитьdraftToPatch;applyDraftInput,emptySublineKeysостаются),statementGrid.test.ts(тестыdraftToPatchпереезжают),src/utils/dates.ts,src/utils/dates.test.ts,StatementReview.tsx,StatementTable.tsx,StatementHeaderBlock.tsx
Interfaces:
- Consumes:
GridRow,cellKey(T1),HeaderFieldKey(T5),parseAmountInput(utils/money.ts). - Produces:
// 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–W27qa-guide§8.5 проходят. - Step 4:
make lint typecheck test build— PASS. - Step 5: Commit
refactor(statement): один черновик правок и сборка единственного PATCH (#<issue>).
Task 7: Режим «Редактировать» — коды строк, меню строки, LINE_CODE_TAKEN
Files:
- Create:
src/widgets/DocumentReview/codeSuggestions.ts,codeSuggestions.test.ts,lineMenu.ts,lineMenu.test.ts,LineMenu.tsx - Modify:
CodeCell.tsx,StatementTable.tsx,StatementReview.tsx
Interfaces:
- Consumes:
GridRow,EditionLineOut,LineErrorDetail(T1),StatementDraft,LineAction(T6). - Produces:
// 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файла вне справочника (добавить в тест строкуok1231) — в конце, с её именем; у формы с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—ApiError409{code: 'LINE_CODE_TAKEN', message, line_id: 'l-u7', line_code: '1170'}→{lineId: 'l-u7', lineCode: '1170'}; 422INVALID_LINE_PATCHсline_code: null→{lineId, lineCode: null}; 409LINES_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[](409LINE_CODE_TAKEN, 422 коды строк из Global Constraints):lineErrorTarget— красное поле кода этой строки с текстом сервера иmessage.error; строки на экране нет — тот же текст над таблицей (P6-9). Прочие 422 —message.error(e.message); черновик сохраняется. - Step 4:
make lint typecheck test build— PASS; на перехвате фикстуры: ввести 1250 у «ИЧ,250» → перехваченное телоPATCHсодержитlines: [{line_id: 'l-u7', line_code: '1250'}]и только его. - Step 5: Commit
feat(statement): правка строк — код с подсказкой из справочника, меню строки, LINE_CODE_TAKEN (#<issue>).
Task 8: Подтверждение ячеек, даты колонок, правка шапки
Files:
- Create:
src/widgets/DocumentReview/ColumnHeader.tsx,src/widgets/DocumentReview/editRules.ts,editRules.test.ts - Modify:
ValueCell.tsx,StatementTable.tsx,StatementHeaderBlock.tsx,StatementReview.tsx
Interfaces:
- Consumes:
StatementDraft(T6),IssuePlaces.byColumn(T4),HeaderFieldRow.canConfirm(T5). - Produces:
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», датуc22024-12-31, «Подтвердить дату» отчёта, организацию «ООО Ромашка» — одно сохранение, в телеvalues[{…confirmed: true}],columns,header {report_date: '2025-09-30', organization_name: 'ООО Ромашка'}. - Step 5: Commit
feat(statement): подтверждение ячеек, дата колонки, наименование организации и подтверждение даты (#<issue>).
Task 9: «Подтвердить данные» — неразобранные строки и новые 409
Files:
- Create:
src/widgets/DocumentReview/confirmBlockers.ts,confirmBlockers.test.ts - Modify:
src/widgets/DocumentReview/confirmError.ts,confirmError.test.ts,StatementReview.tsx
Interfaces:
- Consumes:
GridRow(T1),focusRow(T4),UnresolvedLineOut(T1). - Produces:
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с 409DUPLICATE_FORMдаёт ссылку. - Step 5: Commit
feat(statement): блокировка подтверждения при строках без кода, LINES_UNRESOLVED и DUPLICATE_FORM (#<issue>).
Task 10: Карточка файла и ОСВ — предупреждения структурами
Files:
- Create:
src/widgets/FilesDrawer/fileWarnings.ts,fileWarnings.test.ts,FileWarningsPopover.tsx - Modify:
src/widgets/FilesDrawer/documentRows.ts,documentRows.test.ts,FilesList.tsx,src/widgets/DocumentReview/OsvDocument.tsx
Interfaces:
- Consumes:
FileWarningOut,IssueOut,JobBrief(T1);IssuesPanel(T4);getDocument(services/api/documents.ts). - Produces:
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, строка-ссылка не срабатывает) открываетPopoverFileWarningsPopover: «Предупреждения файла» (тег уровня +warningText), «Предупреждения обработки» (mappingWarnings,info), «Предупреждения документа» — при открытииgetDocument(documentId)→issues(загрузка —Spin, ошибка — «Не удалось загрузить», безdocumentIdраздела нет).OsvDocument—IssuesPanel mode='document'над таблицей при непустомdocument.issues. - Step 4:
make lint typecheck test build— PASS. - Step 5: Сверка с макетом — панель файлов против
docs/mockup/screens/39-org-documents.png(1600×1000): значок не ломает высоту строки и ширину колонки «Статус»; расхождения — в ledger. - Step 6: Commit
feat(files): предупреждения файла структурами и предупреждения документа в панели файлов и у ОСВ (#<issue>).
Task 11: Сценарий на стенде, сверка с макетом, документация репозитория
Files:
- Modify:
README.md(раздел про экран проверки формы — что нового, как включить перехват для демо не нужно),CLAUDE.mdarm-web («Архитектура»: новые чистые модулиDocumentReview—formLayout,issuePlaces,statementDraft,cellView,codeSuggestions,lineMenu,editRules,confirmBlockers;FilesDrawer/fileWarnings)
Interfaces:
-
Consumes: всё выше; стенд —
docs/dev/qa-guide.md§4.1–§4.4 (ядро — из worktree илиmainarm-core с частью А, лучше А + Б;make startв worktree arm-web), формы — §6.9 (Excel Межениновской) и §8.5 (aep_6.pdf, при поднятых сервисах §6.8). -
Step 1: Регрессия на формах 1.x (реальный стенд, пользователь
ka_go): прогнать W19–W28bqa-guide§8.5 и форму Excel1_Форма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_unsafePlaywright:
const fixture = /* содержимое src/widgets/DocumentReview/__fixtures__/statement-v2.json, id документа заменён на открытый */;
await page.route('**/api/v1/documents/*/statement', async (route) => {
if (route.request().method() === 'PATCH') { globalThis.lastPatch = route.request().postDataJSON(); }
await route.fulfill({ json: fixture });
});
Сценарии спеки arm-web по очереди: «Код-мусор на своём месте» (под 1240, подсвечена, серый «ИЧ,250» с подсказкой);
«Расшифровки скрыты» (не видно, «Показать расшифровки (2)», включённый показывает под 1150 и в начале раздела I);
«Догадка подтверждена проверкой» (1230: не подсвечена, подсказка «Распознано: «42831 1»»); предупреждения у места
(1370 — значок ROW_SPLIT, колонка 2 — COLUMN_WITHOUT_DATE, организация и дата — значки, COLUMN_SKIPPED — в списке);
«Назначить код» (тело PATCH — lines: [{line_id: 'l-u7', line_code: '1250'}]); «Подтверждение заблокировано»
(подсказка «Разберите строки без кода: 2», клик ведёт к первой); подтверждение ячейки, дата колонки, организация,
подтверждение даты — одно тело. Перехват POST …/confirm с 409 LINES_UNRESOLVED и DUPLICATE_FORM, PATCH с 409
LINE_CODE_TAKEN (detail {line_id: 'l-u7', line_code: '1170'}) — тексты и места по T7, T9. Перехват
GET /legal-entities/*/files с job.warnings (JSON_FAILED error, locator 'file:x_ofr.json') и
documents[].issue_counts — сценарий «Потерянный ОФР в карточке», значок у документа без предупреждений файла.
- Step 3: итог сверки — в ledger и в описание PR: что сверено (экраны 41, 41-02, 39), что отличается и почему (новые элементы, которых нет в макете: поле кода, меню строки, кнопки подтверждения, значки уровней).
- Step 4: README и CLAUDE.md arm-web;
npm run format;make lint typecheck test build— PASS. - Step 5: Commit
docs(arm-web): экран формы v5.19 — README и карта модулей (#<issue>).
Завершение
make lint typecheck test build— PASS, вывод — в описание PR (с пометкой P6-18:npm installпропущен, node_modules — симлинк).- Финальное ревью ветки (opus): код-ревью TS/React с объяснением решений простым языком для владельца (правило
arm-web/CLAUDE.md); таблица находок## Findingsв ledger (| Маркер | Суть | Статус |). - PR:
tea pulls create --login home --repo RSHB/arm-web --base test --head feat/<issue>-statements-v2, ссылка на issue и на arm-ka-docs#116; в описании — «Rulings I made»,P6-n, O-1…O-6, В-1…В-3, итог сверки с макетом,deferred-находки, строка «выкладывается только вместе с ядром v5.19 (план 3 А + Б); после merge —make -C docs sync-web». Merge — по команде владельца. - После merge в
test:make -C /Users/andrey.epifancev/projects/awp-ca/docs sync-web SYNC_ARGS=--check(ожидание — код 10, отстаёт), затемmake -C …/docs sync-web— issue и PR в arm-core (ревью кода против бэкенда — за агентом; merge — по команде владельца).arm-core/webнапрямую не править. - При «закрывай» — чеклист
~/.claude/CLAUDE.md:openspec archive statements-v2в arm-web, SDD workspace, ветка и worktree.wt/normalization-web(симлинкnode_modulesудаляется вместе с worktree, цель симлинка не трогать), follow-up issue сdeferredи повторным прогоном наaep_3после шага 5 выкладки (В-1).
Покрытие спеки arm-web
| Требование / сценарий | Задачи |
|---|---|
Раскладка строк формы со статусами: разделы, ok по коду, повторы по row_index, шаблонные и пятизначные на местах |
T2, T3 |
… unreadable / missing под ближайшей предыдущей, без неё — в начале первого раздела; подсветка целиком, серый line_code_text |
T2, T3 |
… detail под переключателем «Показать расшифровки (N)», под родителем с отступом |
T2, T3 |
| … справочное имя основным, файловое рядом при отличии; переключателя «как в файле» нет | T2, T3 |
| Сценарии «Код-мусор на своём месте», «Расшифровки скрыты» | T2 (тесты), T11 |
Подсветка ячеек по needs_review с text и уверенностью; строки — по needs_review строки |
T1, T3 |
Поля шапки — по needs_review_fields, подсказка из header.source |
T5 |
issues и checks у строк, ячеек, полей, документа; column_index без column_id — у документа; error ≠ info |
T4, T5 |
Значения detail не подсвечиваются, подсказка есть; сценарий «Догадка подтверждена проверкой» |
T3 |
| Режим «Редактировать»: поле кода с подсказкой, «Это расшифровка», «Назначить код», «Сделать расшифровкой», смена кода, «Подтвердить код» | T7 |
| Подтверждение и снятие подтверждения ячейки и кода; дата колонки (в том числе подтверждение той же даты) | T7, T8 |
| Наименование организации; подтверждение даты отчёта тем же значением | T5, T8 |
Один PATCH (header, values, columns, lines); сценарий «Назначить код» |
T6, T7, T11 |
«Подтвердить данные» отключена с подсказкой «Разберите строки без кода: N» и переходом; 409 LINES_UNRESOLVED, DUPLICATE_FORM, LINE_CODE_TAKEN |
T7, T9 |
| Сценарий «Подтверждение заблокировано» | T9, T11 |
Карточка файла: job.warnings структурами, message и locator, error ≠ info; DocumentOut.issues; mapping_warnings строками; сценарий «Потерянный ОФР в карточке» |
T10, T11 |