Compare commits
3
Commits
v0.1.6
...
a2d2df3b5a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d2df3b5a | ||
|
|
f3a6b78461 | ||
|
|
322ddf6a7f |
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd-lab
|
||||
created: 2026-10-29
|
||||
@@ -0,0 +1,216 @@
|
||||
# Engineering Design
|
||||
|
||||
## Inputs
|
||||
|
||||
- Change: `wealth-ledger-specs`, `openspec/changes/group-account-rows-by-instrument/`.
|
||||
- Утверждённые входы: `proposal.md`, `ux-design.md`, `specs/brokerage-account-ledger/spec.md`, `specs/brokerage-account-upcoming-events/spec.md`, `specs/brokerage-account-income-analytics/spec.md` относительно change.
|
||||
- Issue: `WealthLedger/main#47`; рабочая ветка specs: `feature/47-group-rows-by-instrument`. При различии формулировки ISIN в Issue и утверждённых входов используется identity Instrument из входов.
|
||||
- Проверенные источники реализации: `frontend/src/shared/api/generated/schema.d.ts`, `frontend/AGENTS.md`, `frontend/.ai/architecture.md`, conventions `engineering.md`, `typescript-react.md`, `ui.md`, `data.md`, `testing.md`.
|
||||
- Ниже пути `src/` и `packages/` относятся к `frontend/`; этот документ не изменяет код. UX остаётся владельцем flows, визуальных решений и a11y.
|
||||
|
||||
## Architecture Drivers
|
||||
|
||||
- `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`: точный ключ, минимум две записи, отсутствие изменений single-account.
|
||||
- `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`: сохранение каждого денежного движения, точность и запрет ложного итога.
|
||||
- `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`: серверный порядок и только показанные записи.
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`: дата вместе с `dateKind`, identity и точный тип.
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`: неизвестный прогноз и неподтверждённая полнота не превращаются в ноль.
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`: ограничение превью до группировки.
|
||||
- `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`: визуальная агрегация не заменяет записи или серверные аналитические итоги.
|
||||
- Ограничения: существующие API и query lifecycle, локальный FSD profile, без новых runtime dependencies и без хранения производного состояния.
|
||||
|
||||
## Technical Goals / Non-Goals
|
||||
|
||||
- Реализовать одну чистую операцию группировки и переиспользуемую точную decimal-арифметику; предметные ключи остаются у владельцев сущностей/страницы.
|
||||
- Сохранить число исходных записей, знаки, валюты, порядок, provenance и прежние ветки одиночных карточек.
|
||||
- Не менять backend, фильтры, cursor protocol, аналитические totals, доступ, синхронизацию, URL или часовые пояса текущего отображения.
|
||||
- Не вводить межстраничное накопление, поиск identity через сеть, валютную конвертацию, новый feature/widget или универсальный aggregation framework.
|
||||
|
||||
## Implementation Impact Map
|
||||
|
||||
| Area | Affected | Outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| main | Нет | Issue уже существует; workspace и tooling не меняются. |
|
||||
| frontend | Да | Shared grouping/decimal, entity presentation, четыре page slices и EntryCard workspace package. |
|
||||
| backend | Нет | DTO и envelope достаточны; endpoints и вычисления сервера остаются прежними. |
|
||||
| docs | Да, после реализации | Уточнить AS-IS представление списков и границу клиентских итогов; точные страницы ниже. |
|
||||
| specs | Да | Этот design; утверждённые proposal/specs/UX не переписываются. |
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Одна чистая группировка, предметные ключи снаружи
|
||||
|
||||
Драйвер: `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`.
|
||||
В `src/shared/lib/group-items.ts` разместить `groupItems<T>(items: readonly T[], keyOf: (item: T) => string | null)`.
|
||||
Результат — discriminated union: single с исходным item либо group с key и исходными items (не менее двух).
|
||||
Функция не знает DTO, денег, счетов, React и режима маршрута; `null` всегда оставляет самостоятельный single.
|
||||
Map сохраняет позицию первого появления ключа; последующие совпадения добавляются к его записям без сортировки.
|
||||
Вход не мутируется; ссылки на записи сохраняются, записи одного счёта не схлопываются. Сложность O(n), память O(n).
|
||||
Три независимых реализации отклонены из-за дублирования инварианта; framework с reducers/plugins — избыточен.
|
||||
|
||||
### D2. Ключи соответствуют реально отображаемому дню
|
||||
|
||||
Драйверы: `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`; `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`.
|
||||
|
||||
| Список | День | Identity: первое непустое поле | Тип и дополнительные поля |
|
||||
| --- | --- | --- | --- |
|
||||
| Журнал и последние операции | Локальный календарный день `occurredAt` в timezone браузера | `instrumentUid`, затем `figi` | Исходный `type` без нормализации |
|
||||
| Выплаты | Тот же локальный день `occurredAt` | `instrumentUid`, затем `figi` | Исходный `type` |
|
||||
| События и их превью | `date`, отображаемая через `formatDate` в UTC | `instrumentUid`, затем `figi`, затем `isin` | Исходный `type` и `dateKind` |
|
||||
|
||||
`LedgerEntryRow` передаёт `formatSnapshotTime(occurredAt)` и в обычную карточку, и в executed PayoutCard; formatter содержит дату и время без `timeZone` (`src/shared/lib/display.ts:132–139`).
|
||||
Добавить рядом day-only formatter с тем же timezone браузера: использовать календарные части, не полную строку с временем и не `occurredAt.slice(0, 10)`.
|
||||
`formatDate` уже фиксирует UTC (`display.ts:19–38`); для date-only события его ISO-дата достаточна как ключ.
|
||||
`moscowDate` из `src/shared/lib/calendar.ts` обслуживает периоды/границы фильтров; не подменяет день отображения выплат.
|
||||
Identity кодируется парой namespace/value; весь ключ — сериализованный tuple дня, identity, type и, для события, dateKind.
|
||||
`account.id`, валюта, category, ticker, description и logo не входят в ключ. Пустая identity возвращает null.
|
||||
`IncomeAnalyticsEntryDto.instrumentKey` — opaque membership аналитики, не доказанная identity Instrument: не использовать как fallback и не парсить.
|
||||
Это подтверждает existing test `AccountIncomePage.test.tsx`, «opaque membership rather than instrument identity»: один instrumentKey допускает разные UID.
|
||||
Сопоставление UID одной записи с FIGI другой через словарь aliases не вводится: нет авторитетного локального соответствия; одинаковый ticker не доказывает одну бумагу.
|
||||
|
||||
### D3. Точное сложение и отдельная политика допустимости итога
|
||||
|
||||
Драйверы: `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`.
|
||||
В `shared/lib` сейчас есть string-formatting, проверка нуля и BigInt-округление, но нет экспортированного exact add.
|
||||
Однако `src/pages/account/model/positions.ts:41–64,170–179` уже содержит decimal parse/serialize и точную сумму scaled BigInt.
|
||||
Выделить только эту арифметику в `src/shared/lib/decimal.ts`, перевести существующего owner на неё; расчёт процентов и округление портфеля оставить локальными.
|
||||
Публичная минимальная операция `sumDecimalStrings(values)` возвращает decimal string; внутри выравниваются scales и складываются signed BigInt, без Number/parseFloat и без округления.
|
||||
Предусмотреть пустой ввод → `"0"`, канонический ноль без минуса, дробную часть любой поддержанной контрактом длины; не вводить библиотеку decimal.
|
||||
Чистая `sumMoneyByCurrency` в shared принимает структурные `{ currency, value } | null`, возвращает список сумм либо недоступность; Map сохраняет первое появление валюты.
|
||||
Если хотя бы один исходный amount отсутствует (а для журнала и «Выплат» также если он численно равен нулю), скрывается весь итог, а не только проблемная валюта. Некорректная decimal-строка также не превращается в ноль.
|
||||
Только `meta?.completeness === "complete"` разрешает показ результата; отсутствие meta и `incomplete` запрещают его. Это predicate владельца ресурса, не арифметики.
|
||||
Для ledger/income источник — только `payment`, не price/quantity/commission; для событий — `projectedAmount`, null означает неизвестный прогноз, включая offer.
|
||||
Компенсация ненулевых слагаемых до нуля допустима: `+100 + (-100)` даёт видимый нейтральный ноль, в отличие от нулевого исходного amount.
|
||||
Финальное представление — `formatMoney` с `withSign: true`, без `withRoundedCents`; серверные `totalsByCurrency`, buckets и instruments не пересчитываются.
|
||||
|
||||
### D4. Граница группировки — текущая выборка, не query cache
|
||||
|
||||
Драйверы: `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`; `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`.
|
||||
`src/entities/broker-operation/model/useBrokerageOperationHistory.ts` хранит стек cursors и читает одну страницу: `onNext` добавляет cursor, `onPrevious` снимает его.
|
||||
Это previous/next pagination, не infinite append: группировать только текущие `operations.data.items`; не объединять cached pages.
|
||||
Тем самым «собирается только из показанных записей» выполняется буквально; `hasNext` не означает partial и не скрывает итог текущей группы.
|
||||
Превью операций получает `limit: 5` в `AccountPage.vm.ts`; событий — `slice(0, RECENT_EVENTS_LIMIT)` в `pages/account/model/previews.ts`.
|
||||
Группировать после этих ограничений; никогда не добирать шестую запись ради пары. При смене периода/счёта/cursor результат вычисляется заново.
|
||||
|
||||
### D5. Композиция вместо новых разновидностей каждой карточки
|
||||
|
||||
Драйверы: `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`.
|
||||
Общий недоменный `EntryGroup` в `src/shared/ui/EntryGroup/` принимает готовый header, accessible label и children; он владеет только композицией группы.
|
||||
Предметные `LedgerEntryGroup` и `LedgerAccountRow` принадлежат `entities/broker-operation/ui`; `BrokerageEventGroup` и `BrokerageEventAccountRow` — `entities/brokerage-event/ui`.
|
||||
Account row лёгкий, без вложенного полноценного LedgerEntryRow/Avatar; у выплат его существующие данные отображаются в карточном варианте, у остальных — строковом.
|
||||
В `packages/ui/src/EntryCard/EntryCard.tsx` расширить только value slot с string до ReactNode (включая null); сохранить string callers и прежний DOM для них.
|
||||
Слот допускает несколько самостоятельно оформленных валютных значений и отсутствие видимого значения; общий tone карточки не должен перекрашивать все валюты одним знаком.
|
||||
Для одной внешней рамки использовать существующий `details` slot EntryCard под содержимое группы, не вкладывать вторую Paper вокруг целой карточки.
|
||||
Singleton продолжает вызывать `LedgerEntryRow` либо `BrokerageEventCard`, а через них существующий `PayoutCard`; его API не расширяется ради grouping.
|
||||
Альтернативы — копировать EntryCard или добавлять group/account варианты в PayoutCard — увеличивают дублирование либо смешивают ответственности.
|
||||
|
||||
### D6. Явный all-mode на границе page
|
||||
|
||||
Драйвер: `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`.
|
||||
Page VM читает route param и передаёт `isAllAccounts = accountId === "all"`; это существующий sentinel, например `pages/account/ui/AccountPage/components/AccountHeading.view.tsx`.
|
||||
При false сохраняется прежний flat-render path без вызова группировки и новой политики сумм. Наличие account у записи не используется как определение режима.
|
||||
Не извлекать режим из URL внутри entity, имени счёта, количества счетов или query result.
|
||||
|
||||
## Components and Responsibilities
|
||||
|
||||
| Owner / путь | Ответственность и направление зависимостей |
|
||||
| --- | --- |
|
||||
| `shared/lib/group-items.ts`, `decimal.ts` | Недоменная чистая механика; экспорт через `shared/lib/index.ts`. |
|
||||
| `entities/broker-operation/model/display.ts` | Ключ операции и group presentation; reuse `paymentPresentation`, `isIncomePayment`, `operationLabelKey`, а не category как type. |
|
||||
| `entities/brokerage-event/model/event-key.ts` | Добавить отдельный grouping key; `brokerageEventKey` оставить ключом исходного события, не заменять им группировку. |
|
||||
| `entities/*/ui/*Group`, `*AccountRow` | Преобразование предметных данных в shared UI; без queries и зависимости сущностей друг от друга. |
|
||||
| `pages/account-income/model/display.ts` | Сохранить occurrence keys каждой выплаты, сформировать grouped sequence только в all; income DTO структурно подходит operation renderer. |
|
||||
| `pages/account-operations`, `pages/account-events` | VM передаёт all и подтверждение полноты; loaded view выбирает single/group. |
|
||||
| `pages/account/model/previews.ts` | Передать all/completeness каждого собственного ресурса после ограничения записей. |
|
||||
| `RecentOperations.view.tsx`, `BrokerageEventsPreview.tsx` | Композиция preview single/group; состояния блоков не подменяют основной портфель. |
|
||||
|
||||
Импорты через public `index.ts`; page не импортирует соседнюю page. Компонент с тестом/CSS живёт в `Component/Component.tsx`, `.test.tsx`, `.module.css` по UI conventions; чистые тесты colocated `.test.ts`.
|
||||
|
||||
## Data / Storage Changes
|
||||
|
||||
N/A: новые данные не сохраняются. Группы и BigInt существуют только при вычислении presentation; ни localStorage, ни второй query cache, ни миграции БД не нужны.
|
||||
|
||||
## Contract Implementation
|
||||
|
||||
N/A для изменения контракта: проверены следующие существующие поля `src/shared/api/generated/schema.d.ts`.
|
||||
|
||||
| DTO | Счёт / дата / тип | Identity | Деньги |
|
||||
| --- | --- | --- | --- |
|
||||
| `BrokerageOperationDto` (1087) | `account?`, `occurredAt`, `type` | nullable `instrumentUid`, `figi`; ISIN нет | nullable `payment`, `price`, `accruedInterest`, `commission` |
|
||||
| `UpcomingEventDto` (1229) | `account?`, `date`, `dateKind`, `type` | nullable `instrumentUid`, `figi`, `isin` | nullable `projectedAmount` |
|
||||
| `IncomeAnalyticsEntryDto` (1158) | `account?`, `occurredAt`, `type` | nullable `instrumentUid`, `figi`, opaque `instrumentKey` | nullable `payment` и остальные поля операции |
|
||||
|
||||
`MoneyDto` содержит string `value` и `currency`; resource envelopes несут meta. Полей достаточно для алгоритма без нового endpoint, enrichment и ручных правок generated types.
|
||||
Optional `account` нужен для single-account DTO; all-response должен фактически содержать `{id,name}` согласно `docs/domains/portfolio/all-accounts.md`. Типы сами эту гарантию не доказывают: проверить fixture/response на consumer boundary.
|
||||
При отсутствующем account не угадывать происхождение и не строить ссылку из `all`; это нарушение данных, а не повод менять API в данном change.
|
||||
|
||||
## Cross-cutting Implementation
|
||||
|
||||
`src/shared/api/resource-query.ts:128–147` раскрывает data и meta одного envelope. Entity hooks и useScreenSync остаются владельцами загрузки и обновления.
|
||||
Operations/events VM уже передают `source: query.meta` и `partial`; для итога использовать source.completeness, не инверсию partial.
|
||||
Income VM сейчас передаёт только `partial: analytics.meta?.completeness === "incomplete"`; добавить явный `totalsAllowed` от meta и провести через loaded view в IncomePayments.
|
||||
`previews.ts` теряет подтверждение complete при сведении к loaded/partial: добавить `totalsAllowed` отдельно для operations/events; meta портфеля здесь не подходит.
|
||||
Stale/refresh failure не равны incomplete: сохранённые данные продолжают отображаться по прежней policy; новая группа не хранит собственный snapshot.
|
||||
React keys групп — tuple-key; строки — прежняя identity с account и occurrence для повторений. Не дедуплицировать совпадающие события; индекс допустим лишь как occurrence внутри идентичного ключа.
|
||||
Переводы и озвучивание остаются в presentation; shared арифметика не импортирует i18n. Новых telemetry, запросов и зависимостей нет.
|
||||
|
||||
## UI Implementation
|
||||
|
||||
Композиция: route → existing query/VM → текущие ограниченные items + all + totalsAllowed → pure grouping → entity single/group → shared EntryCard/EntryGroup.
|
||||
Производные группы не помещаются в useState/effect; существующие состояния запроса и pagination остаются источником истины.
|
||||
Ниже привязка к состояниям `ux-design.md#ui-states`, без переопределения их представления:
|
||||
|
||||
| UX state | Как производится |
|
||||
| --- | --- |
|
||||
| Одиночная запись | Single ветка результата или прежний single-account path. |
|
||||
| Группа с итогом / мультивалютный итог | Два и более items; totalsAllowed; успешная сумма по валютам, переданная value slot. |
|
||||
| Итог скрыт | Неподтверждённая completeness либо недопустимый amount; null visual value и перевод для accessible name. |
|
||||
| Нулевой итог | Все слагаемые допустимы, точная сумма равна нулю; neutral presentation без знака. |
|
||||
| Offer без сумм | Null projectedAmount; event-row использует существующий `events.noAmount`. |
|
||||
| Прогноз | Event presenter использует `payout.forecast`, независимо от знака и числа валют. |
|
||||
| Строка без денег | Existing payment semantics, нулевой/null amount; group total недоступен. |
|
||||
| Обзор, 5 записей / граница страницы | D4; состав определяется до группировки, никаких additional fetch. |
|
||||
| Loading / empty / error / partial-Alert | Existing VM/view guards; группировка только доступного массива, прежние alerts и SourceSyncStatus сохраняются. |
|
||||
|
||||
В all-ветке контейнеры loaded lists получают list semantics из UX; account-row использует данные счёта, не делает lookup через другой entity.
|
||||
Подписи/дата/лого заголовка берутся из предметного presenter первой записи; у событий dateKind общий благодаря ключу.
|
||||
EntryCard value slot и CSS module EntryGroup реализуют responsive/a11y контракт UX; отдельный локальный state раскрытия не создаётся.
|
||||
|
||||
## Developer Verification Strategy
|
||||
|
||||
Test Intent: доказать сохранение записей и денежных значений при изменении только композиции; риск — ложный итог, смешение типов/дней, межстраничная агрегация.
|
||||
- Unit grouping: пустой список, одиночный ключ, null identity, не соседние совпадения, один счёт дважды, порядок первого появления и children, отсутствие мутации.
|
||||
- Unit key adapters: разные type внутри одной category; UID/FIGI/ISIN priority; отсутствие identity; разный dateKind; одинаковый день с разным временем, граница суток в разных timezone.
|
||||
- Unit decimal/money: разные scales, отрицательные дроби, значения больше safe integer, `0.1 + 0.2`, несколько валют, точная компенсация до нуля, null/zero/negative-zero, malformed input, пустой ввод.
|
||||
- Regression выделенной арифметики: существующие `pages/account/model/positions.test.ts` и display tests проходят без изменения портфельных результатов.
|
||||
- Component/route tests: все четыре поверхности all, singleton и single-account, account provenance, partial и отсутствующая meta, complete zero, unknown forecast; серверные income totals неизменны.
|
||||
- Pagination через MSW: next/previous, тот же ключ на двух страницах, reset фильтра и invalid cursor; после перехода нет строк предыдущей страницы.
|
||||
- Preview test: пять исходных записей, шестая не подтягивается; UI group не считается одной записью лимита. React keys не теряют повторные выплаты.
|
||||
- RTL semantic queries проверяют nested lists, доступные имена и отсутствие ложного итога; browser proof — переносы, 320 px, 200% zoom и focus согласно UX, не проверки CSS через JSDOM.
|
||||
- После загрузки `$HOME/.nvm/nvm.sh` и `nvm use --silent` из frontend: адресный `npm run test:run -- <path>`, `npm run typecheck`, `npm run lint`; на итоговом code diff — один `npm run verify`.
|
||||
- Verify уже включает offline `openapi:check`, coverage, Knip, dependency boundaries, app/catalog build; отдельный runner или CI pipeline не добавлять. Для одного этого Markdown — semantic review и `git diff --check`, без application build.
|
||||
|
||||
## Migration / Rollout / Rollback
|
||||
|
||||
Сначала выделить арифметику с regression checks; затем pure grouping и entity composition; затем подключить all-ветки и провести meta до потребителей.
|
||||
Существующие EntryCard callers сохраняют совместимость; выпуск возможен одним frontend build без координации backend или миграции cache/data.
|
||||
Откат — предыдущий frontend build либо возврат flat-render all-веток; серверные данные и API не затронуты, feature flag ради этого change не нужен.
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
N/A: нет новых auth, storage, integrations, caching, concurrency или shared tooling; изменение локально и обратимо. Риски точной арифметики и UI описаны проверками выше.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- В UX есть неоднозначность: Accessibility требует account links в строках вообще, Presentation Decisions п. 4 исключает обзор/события. До реализации ссылок владелец UX должен подтвердить область применения; дизайн её молча не меняет.
|
||||
- UTC календарь событий, browser timezone исполненных записей и Moscow период аналитики различаются уже сейчас; намеренно не исправляем их этим change.
|
||||
- Отсутствие общей identity или account нельзя восстановить из названия; проверка фактических all fixtures обязательна. instrumentKey не устраняет этот риск.
|
||||
- Canonical «Account page presents an upcoming events list» говорит «без фильтров», а страница уже фильтрует период. Этот drift существовал до change и записан в WealthLedger/specs#63; фильтры в рамках группировки не трогаются.
|
||||
|
||||
## ADR Candidates
|
||||
|
||||
Отдельный ADR не требуется: API, архитектурный стиль и технологии не меняются; локальные обратимые решения D1–D6 достаточно сохранить в этом change.
|
||||
|
||||
## Docs Impact
|
||||
|
||||
Цели и итоговый AS-IS перечислены в разделе `## Docs Impact` в tasks.md, docs-impact не выбран. `docs/design/overview.md` не меняется.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Process Record
|
||||
|
||||
**Mode:** lab
|
||||
**Class:** standard
|
||||
**Issue:** https://git.personal.ksv741.ru/WealthLedger/main/issues/47
|
||||
|
||||
## Change Profile
|
||||
|
||||
| Artifact | Selected | Reason |
|
||||
| --- | --- | --- |
|
||||
| research | no | Это потребность самого Change Owner, а as-is уже разобран и записан в Proposal Evidence. Рынок и альтернативы изучать не нужно. |
|
||||
| ux-design | yes | Trigger: на четырёх экранах режима «все счета» появляется новый паттерн представления (заголовок группы, итог, разбивка по счетам), и он должен уживаться с существующими карточками событий и операций. |
|
||||
| system-analysis | no | Не trigger: межсистемный contract, data ownership и интеграции не меняются. Это представление уже полученных данных внутри frontend. |
|
||||
| data-mapping | no | Модуль требует system-analysis, а она не выбрана. Ключ группы, правило итога и запрет смешивания валют задаются Requirements в specs и проверяются в test-cases. |
|
||||
| failure-model | no | Нет внешней интеграции, фоновой синхронизации и многошаговой записи. Состояния загрузки и ошибок списков не меняются. |
|
||||
| nfr | no | Группировка охватывает уже загруженные строки списка. Значимых ограничений по performance, availability или capacity нет. |
|
||||
| observability | no | Клиентское представление без фоновых процессов. Оператору нечего отличать. |
|
||||
| security-analysis | no | Модель доступа, trust boundaries и состав раскрываемых данных не меняются: показываются те же записи текущего User. |
|
||||
| test-cases | yes | Trigger: денежные итоги групп, граничные случаи ключа (тип, дата, отсутствие Instrument, один счёт, разные валюты) и стык загруженных страниц. Для их проверки нужны конкретные данные и oracle. |
|
||||
| docs-impact | no | Обновляются только AS-IS страницы доменов (4 файла в `docs/domains/`). ADR, architecture- и integration-страниц нет. Цели перечислены в `## Docs Impact` в tasks.md. |
|
||||
|
||||
## Gate Log
|
||||
|
||||
| Gate | Date | Scope | Validation | Findings | Decision |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| G1 | 2026-10-29 | proposal, change-profile | `check.cjs plan` OK; `openspec validate` — ожидаемая ошибка «no deltas» до specs; G1 review product-analyst | 2 blocker, 5 major, 1 minor — закрыты решениями владельца в Proposal | approved "все ок, идем дальше" |
|
||||
| G2 | 2026-10-29 | specs | `openspec validate --strict` valid; `check.cjs plan` OK; G2 review qa-engineer + independent-reviewer | 2 blocker, 6 major, 8 minor — закрыты правками specs и решением владельца (итог скрыт при `partial`) | approved "да, все ок, идем дальше" |
|
||||
| G2 | 2026-10-29 | specs | — | UX нашёл 2 пробела в specs: итог при неполном прогнозе; нулевая сумма | invalidated: решения владельца в текущей сессии («Скрывать, как в журнале», «Скрывает итог») изменили Requirements об итоге группы журнала и событий |
|
||||
| G2 | 2026-10-29 | specs | `openspec validate --strict` valid; `check.cjs plan` OK | Правки по 2 UX findings внесены в specs | approved "да, все ок" |
|
||||
| G3 | 2026-10-29 | design, ux-design, qa-analysis, test-cases, tasks | `openspec validate --strict` valid; `check.cjs plan` planning complete; `git diff --check` clean; G3 review independent-reviewer | 3 major, 5 minor — исправлены | approved "да, давай дальше" |
|
||||
| G4 | 2026-10-29 | implementation, review, QA, docs | frontend `npm run verify` exit 0 (530 tests, coverage 92.35%); docs `npm run verify` OK; independent review; QA в браузере (18 TC/сценариев pass); openspec-verify-change: 0 critical, все warnings закрыты; `check.cjs apply` OK | Review 3 major + 5 minor, verify 2 warning + 2 suggestion; всё исправлено. Flaky `screen-sync.test.tsx` есть и до change, файл не менялся | approved "да, все ок, давай коммититься" |
|
||||
|
||||
## Descoped Work
|
||||
|
||||
| Task | Reason | Gate |
|
||||
| --- | --- | --- |
|
||||
|
||||
## Lab Journal
|
||||
|
||||
| Stage | Role / subagent | Unique findings | Decisions | Risks found / excluded | Handoff issues | Skills | Cost | Transferable |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| proposal + change-profile | product-analyst (G1 review) | Режим «Все счета» потерян в canonical ledger/income; запрет frontend-пересчёта итогов в «Выплатах»; неоднозначности даты и типа, порядка, лимита превью | 7 решений Change Owner внесены в Proposal | blocker ×2, major ×5, minor ×1; все закрыты | Ответ сабагента дважды обрезался, пришлось запрашивать продолжение | openspec-new-change | 3 вызова сабагента | Делать G1 review до показа владельцу: он нашёл canonical drift |
|
||||
| specs | qa-engineer, independent-reviewer (G2 review) | Затронутые canonical блоки (compact rows, обзор, список событий без итога) нужно было модифицировать; восстановление `all` было неполным относительно archive; конфликт итога с frontend-data-resilience; ключ «точный тип» неоднозначен для покупки и продажи | Итог скрывается при `partial` (решение владельца); граница страницы — явное исключение; блоки восстановлены из archive целиком | 2 blocker, 6 major, 8 minor; все закрыты правками. Вне scope: canonical drift persist-tbank-data шире двух восстановленных блоков | Ответы сабагентов трижды обрезались | openspec-new-change | 4 вызова сабагентов | Ревьюеру консистентности давать список canonical блоков, которые затрагивает UI-изменение |
|
||||
| design + ux-design + qa-analysis + test-cases + tasks | ux-designer, solution-architect, qa-engineer, independent-reviewer (G3 review) | UX: 2 пробела в specs (неполный прогноз, нулевая сумма), из-за них G2 переутверждён. Design: `instrumentKey` не является identity, у журнала пагинация назад/вперёд, а не дописывание, BigInt-decimal уже есть в `positions.ts`. Review: test-cases ссылались на несуществующие сценарии, неверные oracle в TC-PAG | Ссылки на счёт оставлены текстом, как сейчас; лимит превью считается по записям; Docs Impact перенесён в tasks | G3 review: 3 major, 5 minor, все исправлены. Вне scope: drift с фильтрами событий записан в specs#63 | qa-engineer сначала выдал 46 TC вместо case-only; сабагенты обрезались | — | 7 вызовов сабагентов | Давать QA правило «case только сверх Scenario» сразу; проверять ссылки на сценарии через grep |
|
||||
| apply + G4 checks | frontend-engineer, documentation-engineer, independent-reviewer, qa-engineer | Review: имя группы для скринридера неполное, у выплат терялись occurrence keys, выплаты в группе не были карточками, ссылки на счета появились вопреки UX. QA: 18 проверок pass в браузере со stubbed API | Все findings исправлены. QA D1 (заголовок «Дивиденд») — артефакт заглушки с `type: "Купон"` вместо `OPERATION_TYPE_COUPON`, не дефект. D2 опровергнут кодом: `aria-label` стоит на вложенном `ul` по UX | Flaky `screen-sync.test.tsx` есть и до change, файл не менялся; на baseline 3/3 pass | Исполнитель дважды молча отклонился от UX; поймано review | — | ~9 вызовов сабагентов | Давать исполнителю явный список Presentation Decisions; QA строить заглушки только из enum generated types |
|
||||
| verify (openspec-verify-change) | основной агент | Строка записи в группе журнала показывала дату вместо подписи типа (расхождение с `Full ledger presents compact rows`); у сценариев BUY/SELL, «итог в нескольких валютах» для событий и «оферта» не было автотестов | Исправлено: подпись типа в строке, `aria-label` у карточки-строки, 4 новых теста | 0 critical, 2 warning, 2 suggestion; все закрыты | — | openspec-verify-change | 1 проход | Verify по сценариям находит то, что пропустили review и QA |
|
||||
|
||||
## Retrospective
|
||||
|
||||
**Факты**
|
||||
- 4 gate, 1 инвалидация G2: UX нашёл 2 пробела в specs (итог при неполном прогнозе, нулевая сумма).
|
||||
- Независимый review находил реальные дефекты на каждом gate:
|
||||
- G1: в canonical specs потерян режим `all`; спека «Выплат» запрещала frontend считать итоги.
|
||||
- G2: группировка нарушала canonical requirements, которые change не модифицировал.
|
||||
- G3: test-cases ссылались на несуществующие сценарии, а oracle пагинации были неверны.
|
||||
- G4: имя группы для скринридера, повторные ключи выплат, ссылки на счета вопреки UX.
|
||||
- Verify по сценариям нашёл расхождение, которое пропустили review и QA: в строке группы не было подписи типа.
|
||||
- Побочный результат — WealthLedger/specs#63: canonical drift после archive `persist-tbank-data`.
|
||||
- Стоимость: около 25 вызовов сабагентов. Ответы часто обрезались, так что прятались продолжения.
|
||||
|
||||
**Оценка**
|
||||
- Окупились: G1/G2 review (без них delta строилась бы на неполном каноне), ux-design (нашёл пробелы в specs), verify по сценариям.
|
||||
- Избыточным для standard UI-change оказался первый вариант QA-артефактов: 46 TC вместо cases «сверх Scenario». Пришлось переписать.
|
||||
- Изменить в schema: в instruction для qa-analysis/test-cases явно требовать ссылки на существующие `#### Scenario` и проверку их grep-ом. В brief для исполнителя передавать Presentation Decisions из ux-design как обязательный список.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Proposal
|
||||
|
||||
Issue: WealthLedger/main#47
|
||||
|
||||
## Why
|
||||
|
||||
В режиме «Все счета» события и операции показываются плоским списком: одна строка — одна запись одного счёта. Одна и та же бумага часто есть сразу на брокерском счёте и на ИИС. В этом случае выплаты по ней в один день разбросаны по отдельным строкам, и пользователю приходится самому связывать их и сравнивать суммы по счетам.
|
||||
|
||||
## What Changes
|
||||
|
||||
- В режиме «Все счета» записи с одним днём, одним Instrument и одним точным типом события или операции объединяются в одну визуальную группу.
|
||||
- В заголовке группы — бумага, тип и итог. Итог — точная сумма записей группы, отдельно по каждой валюте и со знаком. Под заголовком всегда раскрыта разбивка: каждая запись остаётся отдельной строкой со своим счётом и суммой (например, «Брокерский — 200 ₽», «ИИС — 100 ₽»).
|
||||
- Группировка применяется к предстоящим событиям и к исполненным операциям любой категории, если у записи есть identity Instrument.
|
||||
- Группа образуется из двух и более записей с одинаковым ключом, даже если все они относятся к одному счёту.
|
||||
- Группа стоит на месте своей первой записи в серверном порядке. Записи внутри группы и остальные строки сохраняют относительный серверный порядок.
|
||||
- Режим одного счёта, записи без Instrument и одиночные записи не меняются.
|
||||
- В нормативные specs журнала и аналитики дохода возвращается уже существующий режим «Все счета». Он был утрачен при синхронизации canonical specs; поведение при этом не меняется.
|
||||
- Для «Выплат» в режиме «Все счета» вводится исключение из действующих запретов: группировочные карточки по Instrument и вычисление итога группы на клиенте разрешены. Аналитические итоги дохода по-прежнему приходят с сервера и не пересчитываются.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Страница предстоящих событий в режиме «Все счета».
|
||||
- Страница журнала операций в режиме «Все счета».
|
||||
- Превью ближайших событий и последних операций на обзоре в режиме «Все счета». Лимит считается по записям, как сейчас.
|
||||
- Раздел «Выплаты» на странице доходов в режиме «Все счета».
|
||||
- Восстановление в canonical specs журнала и дохода нормативного описания режима «Все счета» в его текущем поведении.
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
- Режим одного счёта.
|
||||
- Группировка за пределами одного дня, по эмитенту или между разными типами: покупка ≠ продажа, купон ≠ налог с купона, налог с купона ≠ налог с дивиденда.
|
||||
- Изменение состава, фильтрации, серверного порядка, пагинации, лимитов превью или денежных значений, которые возвращает сервер.
|
||||
- Записи без identity Instrument.
|
||||
- Сворачивание групп и пометка, что группа может быть неполной.
|
||||
- Аналитические итоги дохода и их пересчёт.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Нет.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `specs/brokerage-account-upcoming-events/spec.md`: представление событий (страница и превью) в режиме «Все счета».
|
||||
- `specs/brokerage-account-ledger/spec.md`: восстановление режима «Все счета», представление журнала и превью операций в этом режиме.
|
||||
- `specs/brokerage-account-income-analytics/spec.md`: восстановление режима «Все счета», раздел «Выплаты» в этом режиме с исключением из запретов на группировку и клиентский итог.
|
||||
|
||||
## Impact
|
||||
|
||||
- Пользователи с несколькими счетами, которые смотрят режим «Все счета».
|
||||
- Меняется только представление уже полученных данных. Источники данных, серверные ответы и интеграция с T-Bank остаются прежними.
|
||||
- Вероятно затрагиваются клиентское представление списков и AS-IS документация экранов.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Купоны по одной облигации за один день (200 ₽ на брокерском счёте и 100 ₽ на ИИС) в режиме «Все счета» видны одной группой с итогом 300 ₽ и отдельной строкой для каждого счёта.
|
||||
- Записи разных типов или дней по одной бумаге не объединяются.
|
||||
- Итог группы точен, показывается отдельно по каждой валюте и учитывает знак записей.
|
||||
- В режиме одного счёта и для записей без Instrument списки выглядят как раньше.
|
||||
|
||||
## Evidence
|
||||
|
||||
- [F] В режиме «Все счета» страницы событий, операций, превью обзора и «Выплаты» показывают плоский список, по строке на запись, со счётом в каждой строке. Группировки по Instrument нет. Источник: AS-IS frontend, разбор кода 2026-10-29.
|
||||
- [F] Canonical `brokerage-account-income-analytics` («Income payments remain traceable to exact Ledger Entries») запрещает группировочные карточки и subtotals по Instrument в «Выплатах» и пересчёт денежных итогов на frontend. Источник: canonical spec, archive `2026-10-14-unify-income-payment-cards`.
|
||||
- [F] Режим «Все счета» для журнала и дохода описан в delta specs archive `2026-10-09-persist-tbank-data`, но отсутствует в текущих canonical specs `brokerage-account-ledger` и `brokerage-account-income-analytics`. В `brokerage-account-upcoming-events` он есть. Источник: specs store, G1 review 2026-10-29.
|
||||
- [F] Потребность: одна облигация на брокерском счёте и ИИС, выплаты по ней в один день нужно видеть рядом с суммой по каждому счёту. Источник: запрос Change Owner, WealthLedger/main#47.
|
||||
- [H] Сервер в режиме «Все счета» уже возвращает для каждой записи счёт, дату, тип, identity Instrument и точную сумму. Подтверждается в Design.
|
||||
|
||||
## Assumptions / Dependencies
|
||||
|
||||
- День записи берётся тот же, что строка уже показывает на этом экране. Часовой пояс отображения не меняется.
|
||||
- Группа собирается только из записей, показанных на текущей странице журнала или в превью. При переходе на другую страницу группы собираются заново из её записей, поэтому группа на границе страницы может быть неполной.
|
||||
|
||||
## Open Decisions
|
||||
|
||||
Нет. Change Owner принял в текущей сессии: ключ (день как на экране, identity Instrument, точный тип), клиентский итог по валютам, позиция группы на месте первой записи, лимит превью по записям, неполная группа без пометки, строка на каждую запись, восстановление режима «Все счета» в этом change.
|
||||
@@ -0,0 +1,89 @@
|
||||
# QA Analysis
|
||||
|
||||
Объект проверки: frontend-only изменение группировки строк по Instrument в режиме «Все счета» (журнал, обзор, будущие события, «Выплаты»). Backend и API не меняются; контракт используется как есть.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Affected requirements | Likelihood | Impact | Verification depth |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Неверный ключ группы: объединение записей разных типов/дней или не-объединение одинаковых | ledger/upcoming `groups same-day…` | Средняя | Высокая | Полный перебор граничных комбинаций ключа (тип, день, Instrument, один счёт) |
|
||||
| Неверный итог: конвертация/округление валют, потеря знака, итог при отсутствии/нулевой сумме, при `partial` | ledger/upcoming `group shows each entry and …` | Средняя | Высокая | Мультивалютные, знаковые, нулевые и запрещающие сценарии итога; сверка с серверными суммами |
|
||||
| Нарушен серверный порядок или лимит (5 записей обзора, пагинация журнала) из-за группировки | `preserves order and limits` | Средняя | Средняя | Проверка позиций групп, границы страницы, состава «последних 5» |
|
||||
| Группировка случайно применяется в режиме одного счёта или к записям без Instrument | `groups same-day…`, MODIFIED «Full ledger…» | Низкая | Высокая | Регрессионная проверка одиночного режима и записей без Instrument |
|
||||
| «Выплаты»: группировка нарушает запреты одиночного режима или подменяет аналитические итоги | `Income payments remain traceable…` | Средняя | Высокая | Проверка обоих режимов, неизменности серверных аналитических итогов |
|
||||
| Пропадание записей внутри группы (свернуто, скрыто) или потеря fallback при отсутствии metadata | `group shows each entry…`, MODIFIED `Full ledger…`, MODIFIED overview | Низкая | Высокая | Визуальная проверка разбивки без раскрытия, fallback-представление |
|
||||
| Обзор «Все счета»: режим не открывается / принадлежность счёту потеряна (восстановление режима `all`) | MODIFIED `User can retrieve an account ledger`, MODIFIED overview, MODIFIED upcoming list | Средняя | Средняя | Проверка доступности и контента режима `all`, `partial`-поведение |
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
| Requirement | Check | Level | Test cases |
|
||||
| --- | --- | --- | --- |
|
||||
| `brokerage-account-ledger` — `User can retrieve an account ledger` | Режим `all` доступен; принадлежность записей; `partial`; пустая незагруженная часть | System / E2E | Scenario: Получение журнала с периодом по умолчанию; Scenario: Общий журнал сохраняет принадлежность записей; Scenario: Общий журнал содержит неполный счёт; Scenario: Пустая загруженная часть не доказывает отсутствие операций |
|
||||
| `brokerage-account-ledger` — `Account overview includes recent ledger events` | Группы в обзоре «Все счета»; лимит 5; fallback без metadata | System / E2E | Scenario: Отображение последних событий; Scenario: Последняя запись не содержит visual metadata инструмента; Scenario: Группировка в последних записях обзора; TC-PAG-002, TC-PAG-003 |
|
||||
| `brokerage-account-ledger` — `Full ledger presents compact rows` | Строка в группе: счёт, сумма, знак, подпись типа; заголовок группы; fallback | System / E2E | Scenario: Запись внутри группы режима «Все счета»; Scenario: Подпись типа не дублируется; Scenario: Отсутствует visual metadata инструмента; TC-GRP-001, TC-TOT-001 |
|
||||
| `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` | Ключ группы: два счёта, один счёт, разные типы, покупка/продажа, разные дни, без Instrument, один счёт | System / E2E | Scenario: Купоны одной облигации на двух счетах; Scenario: Две операции одного типа на одном счёте; Scenario: Разные типы по одной бумаге не объединяются; Scenario: Покупка и продажа не объединяются; Scenario: Разные дни не объединяются; Scenario: Запись без Instrument; Scenario: Режим одного счёта не меняется; TC-GRP-001, TC-GRP-002, TC-GRP-003, TC-GRP-004 |
|
||||
| `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` | Точный итог: мультивалюта, знак, ноль, без изменения, `partial`; разбивка всегда видна | System / E2E | Scenario: Итог по нескольким валютам; Scenario: Итог со знаком; Scenario: Нулевой итог; Scenario: Группа с записью без денежного изменения; Scenario: Запись с нулевой суммой скрывает итог; Scenario: Итог скрыт при неполном периоде; TC-TOT-001, TC-TOT-002, TC-TOT-003, TC-TOT-004 |
|
||||
| `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits` | Позиция группы; граница страницы; лимит 5 обзора | System / E2E | Scenario: Позиция группы в списке; Scenario: Группа на границе страницы; TC-PAG-001, TC-GRP-002 |
|
||||
| `brokerage-account-upcoming-events` — `Account page presents an upcoming events list` | Страница в `all`; групповой итог ≠ итог списка; дисклеймер | System / E2E | Scenario: Просмотр ближайших событий; Scenario: Пустое состояние страницы; Scenario: Прогнозный дивиденд с неизвестной суммой |
|
||||
| `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument` | Группы событий: два счёта, разные типы/даты, без Instrument, один счёт | System / E2E | Scenario: Прогноз купонов одной облигации на двух счетах; Scenario: Разные типы или даты не объединяются; Scenario: Событие без Instrument; Scenario: Режим одного счёта не меняется; TC-GRP-005 |
|
||||
| `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` | Прогнозный итог: неизвестная сумма, оферта, мультивалюта, неполный прогноз, дата реестра | System / E2E | Scenario: Событие с неизвестной суммой в группе; Scenario: Оферта без сумм; Scenario: Прогнозный итог по нескольким валютам; Scenario: Прогнозный итог скрыт при неполном прогнозе; Scenario: Группа дивидендов по дате реестра; TC-GRP-005, TC-TOT-005 |
|
||||
| `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits` | Позиция группы событий; ближайшие события обзора | System / E2E | Scenario: Позиция группы событий; Scenario: Группировка в ближайших событиях обзора; TC-PAG-002 (по аналогии для событий обзора) |
|
||||
| `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound` | Режим `all` в аналитике; полнота всех счетов; периоды | System / E2E | Scenario: Общий доход требует полноты всех счетов; Scenario: Открыт ещё не загруженный период (изменение frontend не затрагивает — smoke) |
|
||||
| `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` | «Выплаты»: одиночный режим без групп; группы в `all`; итог при неполноте; аналитика не пересчитана; корректировки и дубли ID | System / E2E | Scenario: Каждая выплата показана отдельной карточкой; Scenario: Список воспроизводит точный состав дохода; Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются; Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»; TC-GRP-006, TC-TOT-004 (правило скрытия итога) |
|
||||
|
||||
## Negative and Boundary Focus
|
||||
|
||||
- Ключ группы: разные точные upstream-типы, покупка vs продажа, разные экранные дни, отсутствие identity Instrument, одна запись (нет пары), все записи на одном счёте.
|
||||
- Итог: мультивалюта без конвертации; копеечные суммы без округления (+150.10 −50.05); нулевой итог без знака; запись без денежного изменения; нулевая сумма; `partial`-период; неполный прогноз; событие с неизвестной суммой; оферта без сумм.
|
||||
- Порядок и лимиты: группа из записей первой и следующей страницы журнала; лимит 5 записей обзора (считается по записям, не по группам); позиция группы по первой записи.
|
||||
- Режимы: один счёт — полное отсутствие группировки; записи без Instrument не сливаются.
|
||||
|
||||
## Cross-system Verification
|
||||
|
||||
system-analysis не выбран, backend и API в рамках change не меняются — отдельная кросс-системная проверка не требуется. Существующие серверные данные проверяются как вход: реальные ответы API (`{ data, meta }`, принадлежность записей счёту, `partial`-состояние, серверные аналитические итоги, порядок entries) используются как эталон, с которым сверяется клиентское представление (состав групп, суммы, порядок). Ключевой инвариант: аналитические денежные итоги и receipt count приходят только с сервера и не пересчитываются; единственный клиентский расчёт — итог визуальной группы, который сверяется с суммой серверных записей группы.
|
||||
|
||||
## Test Data and Environment Needs
|
||||
|
||||
Локальный frontend + локальный backend с сохранёнными данными (или stubbed API в браузере через devtools/сервис-воркер-заглушку). Фикстуры:
|
||||
|
||||
- Два счёта (брокерский + ИИС) с одной и той же облигацией: исполненные купоны в один день (+200 RUB и +100 RUB) — базовая группа.
|
||||
- Мультипанель: одна группа с записями в RUB и USD; выплаты одной бумаги в разных валютах.
|
||||
- Граничные суммы: +150.10 / −50.05 / −150.10; запись с нулевой суммой; запись без денежного изменения; отрицательная корректировка.
|
||||
- Разнообразие типов: купон + налог в один день, покупка + продажа одной бумаги в один день, две покупки на одном счёте, купоны в разные (экранные) дни.
|
||||
- Записи без identity Instrument (например, переводы) — два в один день одного типа.
|
||||
- Записи без presentation metadata инструмента — fallback.
|
||||
- `partial`: счёт с незагруженным интервалом в запрошенном периоде; пустая сохранённая часть при неполном покрытии.
|
||||
- Граница страницы: настройки пагинации журнала так, чтобы записи одной группы попали на разные страницы; подтверждённо полный период.
|
||||
- Будущие события: купон на двух счетах в одну дату; дивиденды по дате реестра (известная и неизвестная суммы); оферта на двух счетах; события без суммы; неполный прогноз.
|
||||
- Обзор: ровно 5 последних записей, среди которых пара с одинаковым ключом.
|
||||
- Режим одного счёта с теми же данными (два купона одной бумаги в один день).
|
||||
|
||||
Источник: подготовленные фикстуры в локальной БД тестового backend; при отсутствии — заглушка API-ответов в браузере с теми же контрактными envelope.
|
||||
|
||||
Примечания по окружению (не блокеры): тестовые данные границы страницы строятся вокруг фактического размера страницы, который возвращает сервер (TC-PAG-001 фиксирует конкретное содержимое страниц в момент прогона); состояние `partial` и неполный прогноз воспроизводятся заглушкой API-ответа в браузере (MSW / Playwright route) с `meta` partial.
|
||||
|
||||
## Manual / Exploratory Scope
|
||||
|
||||
- Визуальное чтение группы без раскрытия: разбивка всегда видна, заголовок не дублирует подпись типа в строках, знак и семантический цвет не единственные носители направления.
|
||||
- Устойчивость к обновлению: приход `partial` → подтверждённой полноты — итог появляется/исчезает корректно; фоновая догрузка не меняет состав и порядок показанных записей.
|
||||
- Переходы между режимами «Все счета» ↔ один счёт на одних данных: группировка появляется/исчезает целиком.
|
||||
- Прокрутка/пагинация журнала в `all`: стабильность позиций групп между страницами.
|
||||
- Consent-контент: дивидендные группы с датой реестра, оферта без итога — читаемость при отсутствии сумм.
|
||||
|
||||
## Automation Candidates
|
||||
|
||||
Black-box E2E (Playwright, режим «Все счета»): TC-GRP-001, TC-GRP-002, TC-GRP-005, TC-GRP-006, TC-TOT-001, TC-TOT-002, TC-TOT-003, TC-TOT-004, TC-TOT-005, TC-PAG-001, TC-PAG-002, TC-PAG-003 — стабильные контрактные сценарии групп, итогов и порядка. TC-GRP-003 и TC-GRP-004 — ручные до уточнения Specs. Статус всех: **deferred until QA repo exists**.
|
||||
|
||||
## Regression Impact
|
||||
|
||||
- Режим одного счёта: журнал, обзор, «Будущие события», «Выплаты» — полное отсутствие группировки (Scenario-блоки требований; ручная регрессия).
|
||||
- Существующие карточки и строки журнала вне групп: представление выплаты купона/дивиденда, подписи типов, знаки, fallback без metadata — без изменений.
|
||||
- Состояния загрузки/пустоты/ошибки: первичное ожидание, нейтральная недоступность, сохранение прежнего результата при временном сбое, `partial` и пустая-но-неполная выборка — не деградируют из-за группировки.
|
||||
- Восстановленный режим `all` в specs журнала и аналитики — поведение не меняется, только smoke доступности (Scenario: Получение журнала с периодом по умолчанию; Scenario: Общий доход требует полноты всех счетов).
|
||||
- Аналитические итоги дохода и receipt count — только серверные, отсутствие клиентского пересчёта (TC-GRP-006).
|
||||
|
||||
## Gaps / Blockers
|
||||
|
||||
Нет. Два вопроса из test design закрыты без изменения specs:
|
||||
- Identity-ключ: specs требуют одинаковую identity Instrument; design D2 определяет её как первый непустой идентификатор, а записи, равенство которых не доказано, не объединяются (TC-GRP-003).
|
||||
- День: specs задают «день, как на экране»; oracle — показанная в строке дата (TC-GRP-004).
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Realized income analytics is account-scoped and period-bound
|
||||
|
||||
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) из счетов, доступных через текущий T-Bank connection User. В режиме `all` система MUST проверять доступ каждого входящего счёта и подтверждать полноту каждого исходного периода по всем входящим счетам; отсутствующий вклад счёта MUST NOT заменяться нулём. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
|
||||
|
||||
Чтение MUST использовать сохранённые операции текущего подключения и локальное подтверждение доступа без синхронного запроса T-Bank. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`. Полнота MUST определяться для выбранного периода и каждого необходимого периода сравнения отдельно. Запрос MUST приоритетно загружать их пропуски до фоновой догрузки остальной истории; известные правила календарных дат MUST сохраняться.
|
||||
|
||||
#### Scenario: Аналитика за период по умолчанию
|
||||
|
||||
- **WHEN** аутентифицированный User запрашивает аналитику доступного Brokerage Account без периода
|
||||
- **THEN** система рассчитывает результат от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно
|
||||
|
||||
#### Scenario: Аналитика за допустимый пользовательский период
|
||||
|
||||
- **WHEN** User применяет включительный период любой длительности, в том числе более 366 дней, без будущих дат
|
||||
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
|
||||
|
||||
#### Scenario: Отклонение недопустимого периода
|
||||
|
||||
- **WHEN** User запрашивает период с некорректной или будущей датой либо с `from` после `to`
|
||||
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
|
||||
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
|
||||
|
||||
#### Scenario: Общий доход требует полноты всех счетов
|
||||
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
|
||||
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
|
||||
|
||||
#### Scenario: Открыт ещё не загруженный период
|
||||
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
|
||||
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
|
||||
|
||||
### Requirement: Income payments remain traceable to exact Ledger Entries
|
||||
|
||||
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; заголовки валютных групп и receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы по каждой валюте, и он MUST NOT подменять аналитические итоги.
|
||||
|
||||
#### Scenario: Каждая выплата показана отдельной карточкой
|
||||
|
||||
- **WHEN** в режиме одного Brokerage Account выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
|
||||
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
|
||||
|
||||
#### Scenario: Список воспроизводит точный состав дохода
|
||||
|
||||
- **WHEN** User просматривает загруженный раздел «Выплаты»
|
||||
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
|
||||
|
||||
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
|
||||
|
||||
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
|
||||
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
|
||||
|
||||
#### Scenario: Валюта видна в сумме каждой выплаты
|
||||
|
||||
- **WHEN** подходящие в режиме одного Brokerage Account выплаты одного Instrument представлены в нескольких исходных валютах
|
||||
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
|
||||
|
||||
#### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»
|
||||
|
||||
- **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
|
||||
- **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: User can retrieve an account ledger
|
||||
Система MUST предоставлять аутентифицированный журнал только для чтения выбранного Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего User и его T-Bank connection. В режиме `all` доступ MUST проверяться для каждого входящего счёта, операции MUST сохранять принадлежность исходному счёту, а полнота периода MUST учитывать покрытие каждого входящего счёта. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года в UTC до момента запроса.
|
||||
|
||||
Система MUST читать сохранённые операции и локальное подтверждение доступа текущего подключения без синхронного обращения к T-Bank, включая запрос списка счетов. Ответ MUST содержать envelope `{ data, meta }` по `tbank-data-sync`, отражающий полноту запрошенного периода независимо от текущей страницы. Отсутствующие данные MUST возвращаться с `data: null` и состоянием загрузки, а частично сохранённый период MUST обозначаться как `partial`. Только подтверждённо полный пустой период MUST обозначать отсутствие операций.
|
||||
|
||||
#### Scenario: Получение журнала с периодом по умолчанию
|
||||
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
|
||||
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
|
||||
|
||||
#### Scenario: Общий журнал сохраняет принадлежность записей
|
||||
- **WHEN** аутентифицированный User запрашивает журнал в режиме `all`
|
||||
- **THEN** система возвращает исполненные операции доступных ему счетов, и каждая запись указывает свой исходный счёт
|
||||
|
||||
#### Scenario: Общий журнал содержит неполный счёт
|
||||
- **WHEN** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
|
||||
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
|
||||
|
||||
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
|
||||
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
|
||||
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
|
||||
|
||||
### Requirement: Account overview includes recent ledger events
|
||||
|
||||
Обзор счёта MUST показывать пять последних исполненных записей журнала этого
|
||||
счёта, а в режиме «Все счета» — пять последних записей всех входящих счетов, и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
|
||||
визуальную идентичность инструмента и семантику денежного движения, что и запись
|
||||
полного журнала; в группе режима «Все счета» визуальная идентичность, дата и тип
|
||||
показываются в заголовке группы. Неуспешный или пустой запрос MUST оставаться локальным состоянием
|
||||
блока и MUST NOT заменять успешно отображённый Portfolio. При временном сбое
|
||||
обновления блок MUST сохранять доступные записи без технического предупреждения
|
||||
или ручного повтора. При отсутствии результата он SHALL показывать первичное
|
||||
ожидание либо нейтральную недоступность, автоматически восстанавливая чтение.
|
||||
Неподтверждённая пустота MUST NOT представляться отсутствием операций.
|
||||
|
||||
#### Scenario: Отображение последних событий
|
||||
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
|
||||
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
|
||||
|
||||
#### Scenario: Последние записи недоступны
|
||||
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается временной ошибкой
|
||||
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние записи либо показывает нейтральную недоступность без технического повтора и восстанавливается автоматически
|
||||
|
||||
#### Scenario: Последняя запись не содержит visual metadata инструмента
|
||||
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
|
||||
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
|
||||
|
||||
### Requirement: Full ledger presents compact rows
|
||||
|
||||
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком.
|
||||
|
||||
#### Scenario: Отображение записи только с инструментом
|
||||
|
||||
- **WHEN** полный журнал отображает запись без денежной выплаты
|
||||
- **THEN** страница определяет её как запись без движения денег
|
||||
|
||||
#### Scenario: Отображение поступления
|
||||
|
||||
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
|
||||
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
|
||||
|
||||
#### Scenario: Отображение списания
|
||||
|
||||
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
|
||||
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
|
||||
|
||||
#### Scenario: Отображение купона
|
||||
|
||||
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
|
||||
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
|
||||
|
||||
#### Scenario: Отображение дивиденда
|
||||
|
||||
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
|
||||
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
|
||||
|
||||
#### Scenario: Подпись типа не дублируется
|
||||
|
||||
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
|
||||
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
|
||||
|
||||
#### Scenario: Отсутствует visual metadata инструмента
|
||||
|
||||
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
|
||||
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
|
||||
|
||||
#### Scenario: Запись внутри группы режима «Все счета»
|
||||
|
||||
- **WHEN** полный журнал в режиме «Все счета» показывает запись в составе группы
|
||||
- **THEN** её дата, тип и инструмент читаются из заголовка группы, а строка записи показывает счёт и денежное изменение со знаком и подписью типа
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: All-accounts ledger groups same-day entries of one instrument
|
||||
В режиме «Все счета» полный журнал и последние записи обзора MUST объединять в визуальную группу две и более показанных записи с одинаковыми днём (как на экране), identity Instrument и точным upstream-типом операции, даже если они на одном счёте. Записи без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
|
||||
|
||||
#### Scenario: Купоны одной облигации на двух счетах
|
||||
- **WHEN** в режиме «Все счета» журнал показывает за один день купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
|
||||
- **THEN** они показаны одной группой с облигацией, типом «Купон» и итогом +300 RUB, а под заголовком видны строки «брокерский счёт +200 RUB» и «ИИС +100 RUB»
|
||||
|
||||
#### Scenario: Две операции одного типа на одном счёте
|
||||
- **WHEN** в режиме «Все счета» журнал показывает за один день две покупки одной бумаги на одном счёте
|
||||
- **THEN** они показаны одной группой, где каждая покупка остаётся отдельной строкой со своим денежным изменением
|
||||
|
||||
#### Scenario: Разные типы по одной бумаге не объединяются
|
||||
- **WHEN** в режиме «Все счета» за один день по одной облигации есть купон и налог с этого купона
|
||||
- **THEN** купоны и налоги показаны раздельно, и ни одна группа не содержит записи разных типов
|
||||
|
||||
#### Scenario: Покупка и продажа не объединяются
|
||||
- **WHEN** в режиме «Все счета» за один день показаны покупка и продажа одной бумаги
|
||||
- **THEN** они не входят в одну группу
|
||||
|
||||
#### Scenario: Разные дни не объединяются
|
||||
- **WHEN** в режиме «Все счета» купоны по одной облигации на двух счетах датированы на экране разными днями
|
||||
- **THEN** они показаны отдельными записями без группы
|
||||
|
||||
#### Scenario: Запись без Instrument
|
||||
- **WHEN** в режиме «Все счета» за один день показаны две записи одного типа без identity Instrument
|
||||
- **THEN** они показаны отдельными записями так же, как без группировки
|
||||
|
||||
#### Scenario: Режим одного счёта не меняется
|
||||
- **WHEN** User открывает журнал или обзор одного Brokerage Account, где есть два купона по одной облигации за один день
|
||||
- **THEN** записи показаны отдельными строками без группы, как до изменения
|
||||
|
||||
### Requirement: All-accounts ledger group shows each entry and an exact total
|
||||
Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком по каждой исходной валюте без конвертации и округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена.
|
||||
|
||||
#### Scenario: Итог по нескольким валютам
|
||||
- **WHEN** группа содержит записи с денежными изменениями в RUB и в USD
|
||||
- **THEN** заголовок показывает отдельный точный итог в RUB и отдельный в USD, без конвертации
|
||||
|
||||
#### Scenario: Итог со знаком
|
||||
- **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB
|
||||
- **THEN** итог группы равен +100.05 RUB, а обе записи видны отдельными строками со своими знаками
|
||||
|
||||
#### Scenario: Нулевой итог
|
||||
- **WHEN** группа содержит +150.10 RUB и −150.10 RUB
|
||||
- **THEN** заголовок показывает итог 0 RUB без знака направления, а обе записи видны со своими знаками
|
||||
|
||||
#### Scenario: Группа с записью без денежного изменения
|
||||
- **WHEN** у одной из записей группы нет денежного изменения
|
||||
- **THEN** заголовок группы не показывает итог, а записи остаются видны отдельными строками
|
||||
|
||||
#### Scenario: Запись с нулевой суммой скрывает итог
|
||||
- **WHEN** группа содержит запись +100 RUB и запись с денежным изменением 0 RUB
|
||||
- **THEN** заголовок группы не показывает итог, а запись с нулём обозначена как запись без движения денег
|
||||
|
||||
#### Scenario: Итог скрыт при неполном периоде
|
||||
- **WHEN** журнал в режиме «Все счета» показывает группу, а период обозначен как `partial`
|
||||
- **THEN** заголовок группы не показывает итог, а записи группы остаются видны со своими суммами
|
||||
|
||||
### Requirement: All-accounts ledger grouping preserves order and limits
|
||||
Группа журнала MUST занимать позицию своей первой записи в серверном порядке; записи внутри группы и остальные строки MUST сохранять относительный серверный порядок. Группировка MUST NOT менять состав, фильтрацию, пагинацию или лимит обзора, который считается по записям. Группа собирается только из показанных записей; это осознанное исключение: её итог относится только к ним и не сопровождается пометкой о неполноте.
|
||||
|
||||
#### Scenario: Позиция группы в списке
|
||||
- **WHEN** серверный порядок записей A, B, C, где A и C образуют группу, а B не входит ни в одну группу
|
||||
- **THEN** список показывает группу (A, C) на месте A, затем B
|
||||
|
||||
#### Scenario: Группа на границе страницы
|
||||
- **WHEN** записи одной группы попадают на текущую и на ещё не показанную страницу журнала при подтверждённой полноте периода
|
||||
- **THEN** группа на текущей странице содержит только её показанные записи, итог равен их сумме, и заголовок не содержит пометки о неполноте
|
||||
|
||||
#### Scenario: Группировка в последних записях обзора
|
||||
- **WHEN** в режиме «Все счета» среди пяти последних записей есть две записи с одинаковым ключом
|
||||
- **THEN** блок обзора показывает те же пять записей, что и без группировки, две из них одной группой
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Account page presents an upcoming events list
|
||||
|
||||
Страница Brokerage Account MUST предоставлять действие перехода на отдельную
|
||||
страницу «Будущие события». Эта страница MUST показывать все полученные события
|
||||
компактным списком без фильтров, пагинации и суммарного итога списка, с датой, типом,
|
||||
инструментом и прогнозной суммой в исходной валюте, если она известна. Прогнозный итог
|
||||
визуальной группы режима «Все счета» не является суммарным итогом списка. Страница
|
||||
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
|
||||
быть неполным из-за доступности данных источника. Это предметное ограничение
|
||||
прогноза MUST NOT превращаться в отчёт о технической синхронизации.
|
||||
|
||||
При временном сбое обновления страница MUST сохранять ранее доступный результат
|
||||
той же выборки с его исходными `calculatedAt` и окном, без индикации
|
||||
собственного чтения и ручного повтора. Состояние источника из `meta`, включая
|
||||
неуспех последней часовой попытки и неполноту, MUST представляться по
|
||||
`frontend-data-resilience`. При отсутствии результата SHALL показываться
|
||||
первичное ожидание либо нейтральная недоступность с автоматическим
|
||||
восстановлением по `frontend-data-resilience`. Подтверждённая потеря доступа
|
||||
MUST скрывать личные данные независимо от наличия сохранённого результата.
|
||||
|
||||
#### Scenario: Просмотр ближайших событий
|
||||
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
|
||||
- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы
|
||||
|
||||
#### Scenario: Пустое состояние страницы
|
||||
- **WHEN** запрос будущих событий успешно возвращает полный пустой список
|
||||
- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет предметные ограничения покрытия источника
|
||||
|
||||
#### Scenario: Ошибка страницы
|
||||
- **WHEN** запрос будущих событий завершается временной ошибкой
|
||||
- **THEN** страница сохраняет доступный результат той же выборки либо показывает нейтральную недоступность без ручного повтора, восстанавливая чтение автоматически и не выдавая частичные данные за полный прогноз
|
||||
|
||||
#### Scenario: Прогнозный дивиденд с неизвестной суммой
|
||||
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
|
||||
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
|
||||
|
||||
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
|
||||
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
|
||||
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном и отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
|
||||
|
||||
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
|
||||
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
|
||||
- **THEN** страница показывает доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка и восстанавливает чтение автоматически
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: All-accounts upcoming events group same-day events of one instrument
|
||||
В режиме «Все счета» страница «Будущие события» и ближайшие события обзора MUST объединять в визуальную группу два и более показанных Future Cash Event с одинаковыми датой и видом даты (как на экране), identity Instrument и типом события, даже если они на одном счёте. События без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
|
||||
|
||||
#### Scenario: Прогноз купонов одной облигации на двух счетах
|
||||
- **WHEN** в режиме «Все счета» на одну дату ожидается купон по облигации на брокерском счёте 200 RUB и купон по той же облигации на ИИС 100 RUB
|
||||
- **THEN** они показаны одной группой с облигацией, типом «Купон» и прогнозным итогом 300 RUB, а под заголовком видны строки «брокерский счёт 200 RUB» и «ИИС 100 RUB»
|
||||
|
||||
#### Scenario: Разные типы или даты не объединяются
|
||||
- **WHEN** в режиме «Все счета» по одной облигации ожидаются купон и погашение на одну дату либо купоны на разные даты
|
||||
- **THEN** ни одна группа не содержит события разных типов или разных дат
|
||||
|
||||
#### Scenario: Событие без Instrument
|
||||
- **WHEN** в режиме «Все счета» на одну дату показаны два события одного типа без identity Instrument
|
||||
- **THEN** они показаны отдельными строками без группы
|
||||
|
||||
#### Scenario: Режим одного счёта не меняется
|
||||
- **WHEN** User открывает «Будущие события» одного Brokerage Account
|
||||
- **THEN** события показаны отдельными строками без группы, как до изменения
|
||||
|
||||
### Requirement: All-accounts upcoming event group shows each event and a forecast total
|
||||
Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком по каждой валюте без конвертации и округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена.
|
||||
|
||||
#### Scenario: Событие с неизвестной суммой в группе
|
||||
- **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы
|
||||
- **THEN** заголовок группы не показывает итог, а строка второго счёта обозначает сумму как неизвестную
|
||||
|
||||
#### Scenario: Оферта без сумм
|
||||
- **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах
|
||||
- **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт
|
||||
|
||||
#### Scenario: Прогнозный итог по нескольким валютам
|
||||
- **WHEN** группа содержит дивиденд 10 USD на одном счёте и дивиденд 200 RUB по той же бумаге на ту же дату на другом счёте
|
||||
- **THEN** заголовок показывает отдельные прогнозные итоги 10 USD и 200 RUB без конвертации
|
||||
|
||||
#### Scenario: Прогнозный итог скрыт при неполном прогнозе
|
||||
- **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами
|
||||
- **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами
|
||||
|
||||
#### Scenario: Группа дивидендов по дате реестра
|
||||
- **WHEN** группа содержит дивиденды, датированные датой реестра
|
||||
- **THEN** заголовок обозначает дату как дату реестра
|
||||
|
||||
### Requirement: All-accounts upcoming event grouping preserves order and limits
|
||||
Группа событий MUST занимать позицию своего первого события в порядке ответа; события внутри группы и остальные строки MUST сохранять относительный порядок ответа. Группировка MUST NOT менять состав, окно, порядок по дате или лимит обзора, который считается по событиям; группы собираются только из показанных событий.
|
||||
|
||||
#### Scenario: Группировка в ближайших событиях обзора
|
||||
- **WHEN** в режиме «Все счета» среди пяти ближайших событий есть два события с одинаковым ключом
|
||||
- **THEN** блок обзора показывает те же пять событий, что и без группировки, два из них одной группой
|
||||
|
||||
#### Scenario: Позиция группы событий
|
||||
- **WHEN** порядок ответа содержит события A, B, C, где A и C образуют группу, а B не входит ни в одну группу
|
||||
- **THEN** список показывает группу (A, C) на месте A, затем B
|
||||
@@ -0,0 +1,73 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Общая механика и ключи
|
||||
|
||||
- [x] 1.1 [frontend] Implementation: чистая группировка `groupItems` и точное десятичное сложение по валютам в `shared/lib` с unit-тестами
|
||||
- **Covers:** `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`
|
||||
- **Source:** design.md — D1, D3; Developer Verification Strategy (unit grouping, unit decimal/money)
|
||||
- **Verification:** `npm run test:run -- src/shared/lib src/pages/account/model`; `npm run typecheck`
|
||||
- **Depends on:** none
|
||||
- [x] 1.2 [frontend] Implementation: ключи группы и правило показа итога для операций и событий (день как на экране, identity, точный тип, `dateKind`), unit-тесты key adapters
|
||||
- **Covers:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`, `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`, `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
|
||||
- **Source:** design.md — D2, D3; Components and Responsibilities (`entities/broker-operation/model/display.ts`, `entities/brokerage-event/model/event-key.ts`)
|
||||
- **Verification:** `npm run test:run -- src/entities/broker-operation src/entities/brokerage-event`
|
||||
- **Depends on:** 1.1
|
||||
|
||||
## 2. Представление групп
|
||||
|
||||
- [x] 2.1 [frontend] Implementation: компоненты группы и строки счёта для операций и событий по ux-design (заголовок, вложенный список, скрытый итог, доступные имена), с component-тестами
|
||||
- **Covers:** `brokerage-account-ledger` — `Full ledger presents compact rows`, `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`, `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
|
||||
- **Source:** design.md — UI Implementation; ux-design.md — Information Architecture, UI States, Accessibility
|
||||
- **Verification:** `npm run test:run -- src/entities packages/ui`; проверка в браузере на 320 px и при zoom 200%
|
||||
- **Depends on:** 1.2
|
||||
- [x] 2.2 [frontend] Implementation: группировка в режиме `all` на страницах журнала и «Будущие события». Полнота берётся из `meta`, путь одного счёта не меняется. Route-тесты, включая pagination через MSW
|
||||
- **Covers:** `brokerage-account-ledger` — `User can retrieve an account ledger`, `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`, `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
|
||||
- **Source:** design.md — D4, Components and Responsibilities (`pages/account-operations`, `pages/account-events`)
|
||||
- **Verification:** `npm run test:run -- src/pages/account-operations src/pages/account-events`
|
||||
- **Depends on:** 2.1
|
||||
- [x] 2.3 [frontend] Implementation: группировка в превью обзора `/accounts/all` после лимита в 5 записей, с preview-тестами
|
||||
- **Covers:** `brokerage-account-ledger` — `Account overview includes recent ledger events`, `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
|
||||
- **Source:** design.md — Components and Responsibilities (`pages/account/model/previews.ts`, `RecentOperations.view.tsx`, `BrokerageEventsPreview.tsx`)
|
||||
- **Verification:** `npm run test:run -- src/pages/account`
|
||||
- **Depends on:** 2.1
|
||||
- [x] 2.4 [frontend] Implementation: группировка «Выплат» в режиме `all`. Ключи повторных выплат сохраняются, серверные аналитические итоги не меняются. Route-тесты
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`, `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`
|
||||
- **Source:** design.md — Components and Responsibilities (`pages/account-income/model/display.ts`)
|
||||
- **Verification:** `npm run test:run -- src/pages/account-income`
|
||||
- **Depends on:** 2.1
|
||||
- [x] 2.5 [frontend] Verification: полный набор frontend gates на итоговом diff
|
||||
- **Covers:** N/A — сводная проверка задач 1.1–2.4
|
||||
- **Source:** design.md — Developer Verification Strategy
|
||||
- **Verification:** `npm run verify` (после `nvm use --silent`)
|
||||
- **Depends on:** 2.2, 2.3, 2.4
|
||||
|
||||
## 3. Документация
|
||||
|
||||
- [x] 3.1 [docs] Docs: обновить AS-IS страниц операций, будущих событий, дохода и режима «Все счета»
|
||||
- **Covers:** N/A — docs отражают реализованное поведение
|
||||
- **Source:** tasks.md — Docs Impact
|
||||
- **Verification:** semantic review против реализации; локальные проверки docs-репозитория по его AGENTS
|
||||
- **Depends on:** 2.5
|
||||
|
||||
## 4. Приёмка
|
||||
|
||||
- [x] 4.1 [frontend] Independent Review: read-only review кода против specs, ux-design и design
|
||||
- **Covers:** N/A — проверка реализации всех ADDED и MODIFIED Requirements
|
||||
- **Source:** process-record.md — протокол G4
|
||||
- **Verification:** отчёт independent-reviewer без открытых blocker и major
|
||||
- **Depends on:** 2.5
|
||||
- [x] 4.2 [frontend] QA Verification: системная проверка по qa-analysis и test-cases на локальном стенде
|
||||
- **Covers:** N/A — проверка всех ADDED и MODIFIED Requirements по Coverage Matrix
|
||||
- **Source:** qa-analysis.md — Coverage Matrix; test-cases/*.md
|
||||
- **Verification:** отчёт qa-engineer по каждому TC и Scenario из матрицы
|
||||
- **Depends on:** 2.5
|
||||
|
||||
## Docs Impact
|
||||
|
||||
Обновляются AS-IS страницы в `docs` (по design.md — Docs Impact):
|
||||
- `docs/domains/operations/overview.md`: группы журнала и превью; точный клиентский итог по показанным записям.
|
||||
- `docs/domains/income/upcoming-events.md`: группы полного списка и превью, `dateKind`, когда показывается прогнозный итог.
|
||||
- `docs/domains/income/overview.md`: группировка «Выплат» в режиме `all`; визуальный итог группы отдельно от серверной аналитики.
|
||||
- `docs/domains/portfolio/all-accounts.md`: группировка в общих списках только в пределах страницы, cursor protocol прежний.
|
||||
|
||||
`docs/design/overview.md` не меняется: граница между UI foundation и предметным представлением остаётся прежней. ADR не требуется.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# Test Cases — grouping
|
||||
|
||||
## TC-GRP-001 — Интерливинг трёх записей одной группы в серверном порядке
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: порядок внутри группы из 3+ записей с интерливингом чужих записей)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета»; брокерский счёт и ИИС доступны; период подтверждённо полный.
|
||||
**Test Data:** Серверный порядок записей одного дня: A1 (купон X, Брокерский, +200 RUB), B (перевод, без Instrument), A2 (купон X, ИИС, +100 RUB), C (купон Y, Брокерский, +50 RUB), A3 (купон X, Брокерский, +150 RUB).
|
||||
**Steps:**
|
||||
1. Открыть полный журнал в режиме «Все счета» за этот день.
|
||||
2. Зафиксировать порядок групп и записей.
|
||||
**Expected Results:** Группа X (A1+A2+A3) стоит на месте A1 — до B; внутри группы порядок A1, A2, A3; затем B, затем C. Относительный порядок B и C сохранён. Итог группы +450 RUB.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-GRP-002 — Две разные группы, записи которых чередуются
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` / `All-accounts ledger grouping preserves order and limits` (new combination)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** Порядок: X1 (купон X), Y1 (дивиденд Y), X2, Y2, X3. X — облигация, Y — акция; купоны X по 100 RUB (Брокерский, ИИС, Брокерский), дивиденды Y по 50 RUB (ИИС, Брокерский).
|
||||
**Steps:**
|
||||
1. Открыть журнал «Все счета» за день.
|
||||
**Expected Results:** Две группы: группа X на месте X1 с итогом +300 RUB, группа Y на месте Y1 с итогом +100 RUB; порядок групп X → Y; внутри каждой — серверный порядок записей.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-GRP-003 — Одна и та же бумага: figi-only identity против instrumentUid — не смешивать разные идентичности
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: строгость identity-ключа)
|
||||
**Risk / Priority:** Средний / High
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** В один день: купон облигации Z на счёте 1 (+100 RUB, identity по figi), купон облигации Z на счёте 2 (+100 RUB, identity по instrumentUid без figi); и купон облигации W на двух счетах (+100 RUB и +50 RUB), где обе записи имеют одинаковый identity.
|
||||
**Steps:**
|
||||
1. Открыть журнал «Все счета» за день.
|
||||
**Expected Results:** Облигация W образует группу с итогом +150 RUB. Две записи облигации Z показаны отдельно: равенство identity не доказано, ключ строится по первому непустому идентификатору (design D2), а группировка без доказанного равенства не выполняется.
|
||||
**Automation Candidate:** yes (E2E со stubbed API, отложено до появления QA repo)
|
||||
|
||||
## TC-GRP-004 — Граница дня около полуночи местного времени
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: экранный день в местном времени)
|
||||
**Risk / Priority:** Средний / Medium
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** Купон X +100 RUB с execution time 23:59:30 Europe/Moscow и купон X +100 RUB с execution time 00:00:30 следующего календарного дня Europe/Moscow.
|
||||
**Steps:**
|
||||
1. Открыть журнал «Все счета», охватывающий оба момента.
|
||||
**Expected Results:** Экранный день определяется так же, как дата, которую строка показывает на этом экране (spec: «как на экране», design D2 — локальная timezone браузера). При timezone браузера Europe/Moscow записи показаны в разных днях и группу не образуют. Oracle — дата, показанная в строке, а не UTC-дата `occurredAt`.
|
||||
**Automation Candidate:** yes (E2E с фиксированной timezone браузера, отложено до появления QA repo)
|
||||
|
||||
## TC-GRP-005 — Прогноз: неизвестная сумма + известные суммы в двух валютах в одной группе
|
||||
|
||||
**Verifies:** `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` (new combination)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета», страница «Будущие события».
|
||||
**Test Data:** Дивиденд D: событие на Брокерском 10 USD (известно), событие на ИИС 200 RUB (известно), событие на третьем счёте без суммы; все — одна дата реестра, один точный тип.
|
||||
**Steps:**
|
||||
1. Открыть «Будущие события» в режиме «Все счета».
|
||||
**Expected Results:** Одна группа из трёх событий; строка без суммы показывает «неизвестно»; итог группы скрыт, потому что у одного события сумма неизвестна (прогноз полный, но не полностью известен по суммам); известные 10 USD и 200 RUB показаны отдельными строками, без конвертации и без общего итога. Дата в заголовке обозначена как дата реестра.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-GRP-006 — «Выплаты» в `all`: отрицательная корректировка и дубли operation IDs внутри одной группы, аналитические итоги не меняются
|
||||
|
||||
**Verifies:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` / Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются (new combination: то же внутри группы `all`)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета», раздел «Выплаты», период подтверждённо полный.
|
||||
**Test Data:** За один день по облигации X: купон +200 RUB (Брокерский, opId=77), купон +100 RUB (ИИС, opId=77 — дубликат), корректировка −50 RUB (Брокерский, тот же тип). Аналитические итоги страницы зафиксированы до.
|
||||
**Steps:**
|
||||
1. Открыть «Выплаты» в режиме «Все счета» за период.
|
||||
2. Сравнить аналитические итоги с зафиксированными.
|
||||
**Expected Results:** Одна группа из трёх карточек; обе записи opId=77 — отдельные карточки; корректировка со знаком «−»; итог группы +250 RUB; валютные заголовки групп и receipt count отсутствуют; аналитические итоги страницы не пересчитаны и не изменились.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Test Cases — pagination
|
||||
|
||||
## TC-PAG-001 — Группа, разрезанная границей страницы журнала: конкретное содержимое страниц и переходы
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits` / Scenario: Группа на границе страницы (new combination: конкретное содержимое страниц, next/prev)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета»; полный период; тестовые данные строятся вокруг фактического размера страницы N, который возвращает сервер (фиксируется в момент прогона).
|
||||
**Test Data:** Серверный порядок: p1..p5 (прочие записи), X1 (купон X, +100 RUB), X2 (купон X, +150 RUB), X3 (купон X, +50 RUB), p9..p12. Размер страницы N — как у сервера; данные готовятся так, чтобы X1 был последней записью страницы 1, а X2, X3 — началом страницы 2.
|
||||
**Steps:**
|
||||
1. Открыть страницу 1 журнала; зафиксировать состав.
|
||||
2. Перейти на страницу 2.
|
||||
3. Вернуться на страницу 1.
|
||||
**Expected Results:** Страница 1 заканчивается записью X1 как обычной строкой: группы нет (для группы нужно ≥2 показанных записей), итог не показывается. Страница 2 начинается группой X2+X3 (итог +200 RUB, разбивка X2/X3), затем p9…. Возврат на страницу 1 воспроизводит тот же состав. Серверный относительный порядок нигде не нарушен; записи одной группы не «стягиваются» через границу страницы.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-PAG-002 — Обзор «Все счета»: пять записей, шестая подходящая существует, но не показана
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `Account overview includes recent ledger events` / Scenario: Отображение последних событий (new combination: группа на границе лимита 5)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Обзор «Все счета».
|
||||
**Test Data:** Последние 6 записей в серверном порядке (свежая → старая): X1 (купон X, ИИС, +100 RUB), p2, p3, p4, p5, X0 (купон X, Брокерский, +200 RUB — шестая, за пределами выборки). X1 и X0 приходятся на один и тот же экранный день и один точный тип — по ключу они образовали бы группу, но X0 не входит в выборку обзора.
|
||||
**Steps:**
|
||||
1. Открыть обзор «Все счета».
|
||||
2. Перейти в полный журнал.
|
||||
**Expected Results:** В обзоре ровно 5 записей; X1 не образует группу (второй записи с тем же ключом в выборке нет) и показан отдельной строкой; X0 в обзоре отсутствует. Полный журнал показывает группу X0+X1 с итогом +300 RUB на месте своей первой записи.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-PAG-003 — Обзор «Все счета»: лимит 5 считается по записям, группа занимает одну визуальную позицию
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `Account overview includes recent ledger events` / Scenario: Группировка в последних записях обзора (new combination: счётчик лимита)
|
||||
**Risk / Priority:** Средний / Medium
|
||||
**Preconditions:** Обзор «Все счета».
|
||||
**Test Data:** Последние записи (свежая → старая): p1, X1 (купон X, +100 RUB), X0 (купон X, +200 RUB, тот же экранный день и точный тип), p4, p5, p6.
|
||||
**Steps:**
|
||||
1. Открыть обзор «Все счета».
|
||||
2. Посчитать визуальные позиции и проверить отсутствие p6.
|
||||
**Expected Results:** Лимит 5 считается по записям: показаны записи p1, X1, X0, p4, p5 — это 4 визуальные позиции: p1, группа X (на месте X1, итог +300 RUB, разбивка X1/X0), p4, p5. Запись p6 не показана: она шестая по счётчику записей, свободной позиции от лимита нет.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Test Cases — totals
|
||||
|
||||
## TC-TOT-001 — Мультивалютная группа со смешанными знаками и большой разрядностью: точный десятичный оракул
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` (new combination: calculated oracle)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** Один день, облигация X, точный тип «купон»: +0.1 RUB (Брокерский), +0.2 RUB (ИИС), −1500000.45 RUB (Брокерский, корректировка того же типа), +200 RUB (ИИС); плюс в той же группе день выплат USD: +0.01 USD и −0.02 USD.
|
||||
**Steps:**
|
||||
1. Открыть журнал «Все счета» за день.
|
||||
2. Прочитать итог группы по каждой валюте.
|
||||
**Expected Results:** Итог RUB = −1499800.15 (точная десятичная сумма: 0.1+0.2−1500000.45+200), без плавающей точки артефактов (не −1499800.1499999); итог USD = −0.01; два отдельных итога, без конвертации и без общего итога; каждая строка показывает свою сумму со знаком.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-TOT-002 — Разное число знаков после запятой у записей группы
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` (new combination)
|
||||
**Risk / Priority:** Средний / High
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** Группа купонов X: +100.5 RUB, +49.50 RUB, +0.005 RUB (если сервер отдаёт такую точность).
|
||||
**Steps:**
|
||||
1. Открыть журнал, прочитать итог.
|
||||
**Expected Results:** Итог = 150.005 RUB ровно, без округления до 2 знаков и без потери младшего разряда; строки показывают суммы как отданы сервером.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-TOT-003 — Нулевой итог и запись без денежного изменения в одной группе
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` / Scenario: Нулевой итог; Scenario: Группа с записью без денежного изменения (new combination: обе записи в одной группе)
|
||||
**Risk / Priority:** Средний / Medium
|
||||
**Preconditions:** Режим «Все счета», полный период.
|
||||
**Test Data:** Группа X: +150.10 RUB, −150.10 RUB, запись без денежного изменения (например, только количество бумаг).
|
||||
**Steps:**
|
||||
1. Открыть журнал, прочитать группу.
|
||||
**Expected Results:** Итог группы скрыт; запись без изменения обозначена как «без движения», а не как 0 RUB; строки остаются видимыми.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-TOT-004 — `partial` → подтверждённая полнота: итог появляется
|
||||
|
||||
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` / Scenario: Итог скрыт при неполном периоде (new combination: смена состояния)
|
||||
**Risk / Priority:** Высокий / High
|
||||
**Preconditions:** Режим «Все счета»; заглушка API (MSW/Playwright route) с управляемым `meta`.
|
||||
**Test Data:** Группа X: +200 RUB и +100 RUB. Первый ответ — `meta` с `partial` для одного из счетов; второй ответ — подтверждённая полнота.
|
||||
**Steps:**
|
||||
1. Открыть журнал с partial-ответом; зафиксировать группу.
|
||||
2. Подтвердить полноту (повторный запрос/фоновая догрузка с полной `meta`).
|
||||
**Expected Results:** При `partial`: строки видны, итог скрыт. После полноты: появляется точный итог +300 RUB; состав и порядок строк не изменились.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
|
||||
## TC-TOT-005 — Прогнозный итог при частично неизвестных суммах скрыт, при полной известности — показан по валютам
|
||||
|
||||
**Verifies:** `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` (new combination)
|
||||
**Risk / Priority:** Средний / High
|
||||
**Preconditions:** Режим «Все счета», «Будущие события».
|
||||
**Test Data:** Два соседних дня: день 1 — купоны X на двух счетах по 100 RUB (обе суммы известны); день 2 — купоны X на двух счетах, у одной суммы нет.
|
||||
**Steps:**
|
||||
1. Открыть «Будущие события».
|
||||
**Expected Results:** Группа дня 1: итог +200 RUB. Группа дня 2: итог скрыт, строка без суммы обозначена как неизвестная. Итоги списком не сводятся.
|
||||
**Automation Candidate:** yes (E2E)
|
||||
@@ -0,0 +1,211 @@
|
||||
# UX Design
|
||||
|
||||
Задача пользователя: в режиме «Все счета» (`/accounts/all/...`) быстро увидеть, что
|
||||
несколько записей — это один и тот же факт (один день, одна бумага, один тип), понять
|
||||
общую сумму и при этом сразу видеть вклад каждого счёта. Нормативное поведение
|
||||
(ключ группы, состав, порядок, правила итога) задано в `proposal.md` и трёх spec;
|
||||
этот документ его не повторяет и не меняет, а только определяет представление.
|
||||
|
||||
Допущение: design system — Mantine + `@wealthledger/ui` по `docs/design/overview.md`
|
||||
(семантические роли, направление несут знак и текст, не только цвет).
|
||||
|
||||
## Requirements Covered
|
||||
|
||||
- `brokerage-account-ledger` — `Full ledger presents compact rows` (MODIFIED, группа в полном журнале)
|
||||
- `brokerage-account-ledger` — `Account overview includes recent ledger events` (MODIFIED, группа в обзоре)
|
||||
- `brokerage-account-ledger` — `User can retrieve an account ledger` (MODIFIED, режим `all`, `partial`)
|
||||
- `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`
|
||||
- `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`
|
||||
- `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`
|
||||
- `brokerage-account-upcoming-events` — `Account page presents an upcoming events list` (MODIFIED, прогнозный итог группы)
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
|
||||
- `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
|
||||
- `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound` (MODIFIED, режим `all`)
|
||||
- `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` (MODIFIED, «Выплаты» в режиме `all`)
|
||||
|
||||
## User Flows
|
||||
|
||||
Цель пользователя — «что и сколько пришло/придёт по бумаге X и с каких счетов».
|
||||
|
||||
1. **Просмотр списка.** Пользователь открывает любой из 4 списков в режиме «Все счета» →
|
||||
видит заголовок группы (дата, бумага, тип, итог) → читает строки счетов под ним,
|
||||
раскрывать ничего не нужно.
|
||||
2. **Переход к счёту.** Из строки группы (журнал, выплаты) пользователь открывает
|
||||
`/accounts/{id}` по имени счёта; возврат — кнопкой «Назад» браузера, список и позиция
|
||||
восстанавливаются как сейчас.
|
||||
3. **Листание журнала.** «Назад/Вперёд» меняют страницу; группа собирается заново из показанных
|
||||
записей страницы. Пользователь не получает пометок о неполноте на границе страницы.
|
||||
4. **Обзор.** На `/accounts/all` в блоках «Ближайшие события» и «Последние операции» группа
|
||||
занимает место первой записи; блок по-прежнему содержит не более 5 записей/событий, как сейчас.
|
||||
5. **Одиночная запись / одиночный счёт.** Запись без пары, без Instrument или любой список в
|
||||
режиме одного счёта выглядят как сейчас; пользовательский путь не меняется.
|
||||
|
||||
Ветвление по итогу: итог известен → показан в заголовке; итог скрыт (см. UI States) →
|
||||
пользователь складывает строки сам или смотрит на состояние страницы (`partial`-Alert).
|
||||
|
||||
## Information Architecture
|
||||
|
||||
Группа — один визуальный блок (рамка `Paper withBorder`) внутри существующего списка, на
|
||||
месте первой записи. Состав блока: заголовок + вложенный список строк счетов. Заголовок
|
||||
повторяет композицию текущей карточки (иконка/логотип | название и метаданные | значение),
|
||||
строка счёта — компактнее, без логотипа.
|
||||
|
||||
Лейблы заголовка: заголовок = «`<тип>` · `<инструмент>`» (как в карточках событий/выплат),
|
||||
метаданные = дата (+ «дата реестра» для дивиденда по реестру), значение = итог,
|
||||
подпись значения = подпись типа (в событиях — «Прогноз»).
|
||||
|
||||
### Журнал (`/accounts/all/operations`, обзор «Последние операции»)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ [logo] Купон · X +300 RUB │ заголовок
|
||||
│ 12 мая 2026 Купон │
|
||||
│ ──────────────────────────────────────────────────────── │
|
||||
│ Брокерский +200 RUB Купон │ строка
|
||||
│ ИИС +100 RUB Купон │ строка
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Строка: имя счёта слева; справа денежное изменение со знаком и подпись типа
|
||||
(по `Full ledger presents compact rows`). Количество и цена в строках группы не показываются (Presentation Decisions, п. 3).
|
||||
Имя счёта — обычный текст во всех списках, как в текущей метадате строки (`withDetails` в этих списках не используется).
|
||||
|
||||
### События (`/accounts/all/events`, обзор «Ближайшие события»)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ [logo] Дивиденд · X 10 USD │
|
||||
│ 12 мая 2026 · дата реестра 200 RUB │ итоги по валютам
|
||||
│ Прогноз │
|
||||
│ ──────────────────────────────────────────────────────── │
|
||||
│ Брокерский 10 USD │
|
||||
│ ИИС Сумма неизвестна│
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Пример выше показывает два разных состояния сразу; на практике итог при строке
|
||||
«Сумма неизвестна» скрыт (см. UI States). Строки счёта в событиях — только текст (ссылки
|
||||
на счёт в текущей карточке события нет).
|
||||
|
||||
### Выплаты (`/accounts/all/income`)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ [logo] Купон · X +300 RUB │
|
||||
│ Исполнено 12 мая 2026 Купон │
|
||||
│ ──────────────────────────────────────────────────────── │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Брокерский +200 RUB │ │ отдельная карточка
|
||||
│ └──────────────────────────────────────────────────────┘ │ внутри группы
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ ИИС +100 RUB │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
В «Выплатах» spec требует «отдельную сразу видимую карточку» записи внутри группы:
|
||||
вложенные карточки без логотипа и с границей, но визуально легче внешней. Подпись «Исполнено»
|
||||
сохраняет различие факта и прогноза.
|
||||
|
||||
Размещение в 4 списках: страница журнала и «Выплаты» — вертикальный стек с зазором `sm`;
|
||||
страница событий — тот же стек; блоки обзора — тот же компонент группы без дополнительной
|
||||
рамки блока. Позиция — по правилам порядка из spec (группа на месте первой записи).
|
||||
|
||||
## UI States
|
||||
|
||||
| Состояние | Представление |
|
||||
| --- | --- |
|
||||
| Одиночная запись | Как сейчас: текущая карточка, без группы и вложенного списка |
|
||||
| Группа с итогом | Заголовок с итогом `+300 RUB` (тон по знаку), строки счетов |
|
||||
| Мультивалютный итог | В заголовке столбец значений: по строке на валюту, `+150 RUB` над `+10 USD`; порядок — по первому появлению валюты в записях группы (Presentation Decisions, п. 2); без конвертации и общей суммы |
|
||||
| Итог скрыт | Значение в заголовке отсутствует; строки счетов видны. Причина: запись без суммы / событие с неизвестной суммой / период `partial`. Для экранных дикторов — скрытый текст «Итог недоступен» (без различения причин) |
|
||||
| Нулевой итог | `0 RUB` без знака, нейтральный тон; строки с их знаками |
|
||||
| Offer без сумм | Группа без итога; строки: имя счёта + «Сумма не определена» (существующий текст `events.noAmount`) |
|
||||
| Прогноз | Итог и суммы строк в событиях несут подпись «Прогноз» (существующий `payout.forecast`); тон нейтральный, как у текущей карточки прогноза |
|
||||
| Строка без денег (ledger) | Нейтральный текст `payout.noMoney`/«без движения денег» как в одиночной записи; итог группы скрыт |
|
||||
| Обзор, 5 записей | Не более 5 записей/событий, как сейчас; группа может занимать часть из них, например 2 из 5; кнопка «Все …» ведёт на полную страницу |
|
||||
| Граница страницы журнала | Группа из показанных записей, без пометки о неполноте и без «ещё N на другой странице» |
|
||||
| Loading / empty / error / partial-Alert | Не меняются: `LoadingSkeleton`, `StateView`, `Alert` и `SourceSyncStatus` из существующих страниц; группы строятся только для загруженных данных |
|
||||
|
||||
Знак: «+» и «−» всегда в тексте; цвет (positive/negative/neutral) — дополнение.
|
||||
|
||||
## Visual Package
|
||||
|
||||
Решение: **reuse + один новый тонкий контейнер**. Изображения (PNG/SVG) не создаются:
|
||||
изменение — повторная композиция существующих карточек, а структура и тексты описаны
|
||||
текстовыми макетами выше; отдельный макет дал бы расхождение с живыми компонентами и
|
||||
токенами Mantine.
|
||||
|
||||
Переиспользуется:
|
||||
|
||||
- `EntryCard` (`@wealthledger/ui`) — заголовок группы: логотип/инициал, `title`, `metadata`
|
||||
(дата), `value` (итог), `valueLabel` (тип/«Прогноз»), `tone`. Отличие: в `value` нужно
|
||||
выводить несколько значений для нескольких валют (сейчас одна строка) и допустить
|
||||
отсутствие значения.
|
||||
- `LedgerEntryRow` / `PayoutCard` (executed) — единичные записи и карточки внутри группы
|
||||
выплат; в группе опускаются логотип, дата и название инструмента (они в заголовке).
|
||||
- `BrokerageEventCard` / `PayoutCard` (forecast) — единичные события; тексты
|
||||
`events.type.*`, `events.noAmount`, `payout.forecast`.
|
||||
- Форматирование: `formatMoney`, `formatDate`, `instrumentLogoUrl`; тексты `ledger.noInstrument`,
|
||||
`payout.noMoney`.
|
||||
- Токены: `--wl-color-border`, `--wl-color-subtle`, `--wl-color-surface`, `Paper withBorder`.
|
||||
|
||||
Новое (предложение, с обоснованием «нужно показать иерархию заголовок → строки»):
|
||||
|
||||
- Контейнер группы (рамка, заголовок, вложенный список) и компактная строка счёта
|
||||
(имя счёта слева, сумма справа, без `Avatar`). `EntryCard` всегда рисует `Avatar`, поэтому
|
||||
для строки счёта нужна его облегчённая форма либо отдельная разметка; выбор — за реализацией.
|
||||
Отступ строк слева — под текстовым блоком заголовка (≈ 40 px аватар + `sm`), либо без
|
||||
отступа на узкой ширине.
|
||||
|
||||
## Responsive
|
||||
|
||||
- Базовый макет — mobile-first: заголовок — сетка из трёх колонок как `EntryCard`
|
||||
(`auto | 1fr | auto`).
|
||||
- **≥ 768 px**: заголовок в одну строку; строки счетов — имя слева, сумма и подпись справа.
|
||||
- **< 576 px (узкий мобильный)**: колонка значения заголовка переносится под название
|
||||
(название и метаданные — вверху, итог — следующей строкой справа/слева), мультивалютный итог
|
||||
— столбцом; название инструмента и имя счёта переносятся (`wrap`, без `lineClamp`),
|
||||
отступ строк слева убирается. Строка счёта: имя счёта сверху, сумма с подписью типа ниже, если
|
||||
вместе не помещаются; длинное имя счёта не обрезается.
|
||||
- Никакого горизонтального скролла; минимальная ширина — 320 px.
|
||||
- Ввод: touch и мышь; в группе нет новых целей нажатия.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Ориентир — WCAG 2.2 AA.
|
||||
|
||||
- **Семантика списка.** Внешний список — `<ul>`; каждая одиночная запись и каждая группа — `<li>`.
|
||||
Внутри группы-`<li>` заголовок (обычный блок, не `<h*>`, чтобы не засорять навигацию по
|
||||
заголовкам страницы) и вложенный `<ul>` со строками-`<li>`. Группы в обзоре — внутри уже
|
||||
существующих секций с их заголовками.
|
||||
- **Доступное имя группы.** Вложенный `<ul>` получает `aria-label` вида: «Купон, X, 12 мая 2026,
|
||||
итог +300 RUB» (дата, инструмент, тип, итог, мультивалютный итог — через запятую). При скрытом
|
||||
итоге — «… итог недоступен». Для знаков в озвучке: «плюс 300 рублей», «минус …» — через
|
||||
`aria-label` значения либо скрытый текст; сам знак в тексте остаётся.
|
||||
- **Итог скрыт.** Визуально значения нет; в DOM есть `visually-hidden` текст «Итог недоступен» и
|
||||
он включён в имя группы. Причина скрытия `partial` уже объявляется существующим `Alert role="status"`.
|
||||
- **Фокус.** Заголовок и строки группы не фокусируемы: новых интерактивных элементов нет. Если одиночная карточка содержит ссылку, она остаётся как есть, а порядок Tab совпадает с порядком DOM.
|
||||
- **Не только цвет.** Направление передаёт знак «+»/«−» (минус как «−»), подпись типа и слова
|
||||
«Прогноз»/«Исполнено»; цвет только усиливает. Контраст текста ≥ 4.5:1 (dimmed `gray.6` на `subtle`
|
||||
проверить), границы групп ≥ 3:1 не требуются — это декор, смысл не теряется без границы.
|
||||
- **Динамика.** Смена страницы журнала не перемещает фокус неожиданно; пагинация — как сейчас.
|
||||
- **Масштаб.** Текст масштабируется до 200 % без потери информации (перенос строк).
|
||||
|
||||
## Presentation Decisions
|
||||
|
||||
Решения, которые относятся только к представлению и не меняют поведение по specs:
|
||||
|
||||
1. **Видимая причина скрытого итога.** Видимого маркера нет. Скринридер получает текст «Итог недоступен». Строки с суммами остаются видны, так что отсутствие итога не выглядит пустотой.
|
||||
2. **Несколько валют в заголовке.** Каждая валюта на отдельной строке, в порядке первого появления в записях группы.
|
||||
3. **Количество и цена в строках сделок.** Не показываются, как и в несгруппированном журнале. Specs требуют только счёт и денежное изменение.
|
||||
4. **Ссылка на счёт.** Сохраняется текущее поведение: имя счёта — текст во всех четырёх списках.
|
||||
5. **Подпись «Исполнено» / «Прогноз».** Ставится у итога в заголовке. В строках событий подпись остаётся у суммы, как в одиночной карточке.
|
||||
6. **Дата реестра.** Применяется только к группам прогнозных событий. Исполненные выплаты датируются датой исполнения.
|
||||
|
||||
## Findings → Specs
|
||||
|
||||
Нет. Два пробела, найденных при проектировании, закрыты решением Change Owner и внесены в specs, G2 инвалидирован:
|
||||
- прогнозный итог скрывается, если полнота прогноза не подтверждена;
|
||||
- запись с нулевой суммой считается записью без денежного изменения и скрывает итог.
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env node
|
||||
// Deterministic checks for wealth-ledger-sdd-lab changes. OpenSpec CLI 1.13.1 has no conditional
|
||||
// artifacts, gate records or archive policy, so this file covers only what the CLI cannot.
|
||||
// It is the single source for profile, ordering, gate and readiness rules; schema.yaml points here.
|
||||
// Semantic quality (contradictions, invented facts, design fit) stays with reviewers.
|
||||
//
|
||||
// Usage: node check.cjs <plan|apply|archive> <change-name> [--root <openspec-root>]
|
||||
// plan — artifact consistency while planning; missing gates are notes, not errors.
|
||||
// apply — plan + every apply blocker is an error.
|
||||
// archive — apply + all tasks done, G4 approved, lab retrospective.
|
||||
// Exit: 0 ok, 1 violations, 2 invocation/environment error.
|
||||
"use strict";
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const SA_MODULES = ["data-mapping", "failure-model", "nfr", "observability"];
|
||||
// Primary file of each conditional artifact, relative to the change directory.
|
||||
const PRIMARY = {
|
||||
research: "research.md",
|
||||
"ux-design": "ux-design.md",
|
||||
"system-analysis": "system-analysis/system-analysis.md",
|
||||
"data-mapping": "system-analysis/data-mapping.md",
|
||||
"failure-model": "system-analysis/failure-model.md",
|
||||
nfr: "system-analysis/nfr.md",
|
||||
observability: "system-analysis/observability.md",
|
||||
"security-analysis": "security-analysis.md",
|
||||
"test-cases": "test-cases",
|
||||
"docs-impact": "docs-impact.md",
|
||||
};
|
||||
const CONDITIONAL = Object.keys(PRIMARY);
|
||||
// A selected conditional input must exist before any of these files is written.
|
||||
const BEFORE_SOLUTION = ["design.md", "qa-analysis.md", "tasks.md"];
|
||||
const DOWNSTREAM = {
|
||||
research: ["specs", ...BEFORE_SOLUTION],
|
||||
"ux-design": BEFORE_SOLUTION,
|
||||
"system-analysis": BEFORE_SOLUTION,
|
||||
...Object.fromEntries(SA_MODULES.map((id) => [id, BEFORE_SOLUTION])),
|
||||
"security-analysis": BEFORE_SOLUTION,
|
||||
"test-cases": ["tasks.md"],
|
||||
"docs-impact": ["tasks.md"],
|
||||
};
|
||||
// Gate that may approve each artifact. Complex changes approve system analysis at G2 (contract-first).
|
||||
const SA_IDS = ["system-analysis", ...SA_MODULES];
|
||||
const allowedGates = (id, complex, hasG2) => {
|
||||
if (id === "proposal") return ["G1"];
|
||||
if (id === "change-profile") return ["G1", "G2", "G3"]; // later profile changes need a recorded decision
|
||||
if (id === "specs") return hasG2 ? ["G2"] : ["G3"];
|
||||
if (SA_IDS.includes(id)) return complex ? ["G2"] : hasG2 ? ["G2", "G3"] : ["G3"];
|
||||
return ["G3"];
|
||||
};
|
||||
const NOT_APPROVED = new Set(["research", "process-record"]); // input context / the log itself
|
||||
const REF = /`([a-z0-9][a-z0-9/-]*)`\s*[—–-]\s*`([^`]+)`/g;
|
||||
|
||||
const fail = (msg) => { console.error(msg); process.exit(2); };
|
||||
const args = process.argv.slice(2);
|
||||
const [mode, changeName] = args;
|
||||
if (!["plan", "apply", "archive"].includes(mode) || !changeName) {
|
||||
fail("Usage: node check.cjs <plan|apply|archive> <change-name> [--root <openspec-root>]");
|
||||
}
|
||||
const rootIdx = args.indexOf("--root");
|
||||
if (rootIdx > 0 && !args[rootIdx + 1]) fail("--root needs a path");
|
||||
const root = path.resolve(rootIdx > 0 ? args[rootIdx + 1] : path.join(__dirname, "..", ".."));
|
||||
const changeDir = path.join(root, "changes", changeName);
|
||||
if (!fs.existsSync(changeDir)) fail(`Change directory not found: ${changeDir}`);
|
||||
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const notes = [];
|
||||
const rel = (p) => path.relative(changeDir, p);
|
||||
const read = (p) => fs.readFileSync(p, "utf8");
|
||||
const at = (p) => path.join(changeDir, p);
|
||||
const walk = (dir) => fs.existsSync(dir)
|
||||
? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
||||
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])
|
||||
: [];
|
||||
const present = (p) => p === "test-cases" || p === "specs"
|
||||
? walk(at(p)).some((f) => f.endsWith(".md"))
|
||||
: fs.existsSync(at(p));
|
||||
const mdFiles = walk(changeDir).filter((f) => f.endsWith(".md"));
|
||||
const strip = (s) => (s ?? "").replace(/`/g, "").trim();
|
||||
// Columns: fixed count, the last cell keeps any `|` it contains.
|
||||
const tableRows = (text, heading, columns) => {
|
||||
const section = text.split(/^## /m).find((s) => s.startsWith(heading));
|
||||
if (!section) return null;
|
||||
return section.split("\n").filter((l) => /^\|/.test(l)).slice(2).map((l) => {
|
||||
const parts = l.replace(/^\|/, "").replace(/\|\s*$/, "").split("|");
|
||||
return [...parts.slice(0, columns - 1), parts.slice(columns - 1).join("|")].map((c) => c.trim());
|
||||
});
|
||||
};
|
||||
|
||||
finishIfMissing();
|
||||
const record = read(at("process-record.md"));
|
||||
|
||||
// 1. Unfilled templates.
|
||||
for (const f of mdFiles) {
|
||||
if (read(f).includes("<!-- guide:")) errors.push(`${rel(f)}: unfilled template guide comment remains`);
|
||||
}
|
||||
|
||||
// 2. Mode, class, Change Profile.
|
||||
const labMode = (record.match(/\*\*Mode:\*\*\s*(lab|delivery)\b/i) || [])[1]?.toLowerCase();
|
||||
if (!labMode) errors.push("process-record.md: **Mode:** must be `lab` or `delivery`");
|
||||
const klass = (record.match(/\*\*Class:\*\*\s*(standard|complex)\b/i) || [])[1]?.toLowerCase();
|
||||
if (!klass) errors.push("process-record.md: **Class:** must be `standard` or `complex`");
|
||||
const complex = klass === "complex";
|
||||
const hasG2 = labMode === "lab" || complex;
|
||||
|
||||
const profile = new Map();
|
||||
for (const [rawId, selected, reason] of tableRows(record, "Change Profile", 3) ?? []) {
|
||||
const id = strip(rawId);
|
||||
if (!CONDITIONAL.includes(id)) { errors.push(`Change Profile: unknown artifact '${id}'`); continue; }
|
||||
if (!/^(yes|no)$/i.test(strip(selected))) errors.push(`Change Profile: '${id}' Selected must be yes|no`);
|
||||
if (!reason) errors.push(`Change Profile: '${id}' needs a trigger or reason`);
|
||||
profile.set(id, /^yes$/i.test(strip(selected)));
|
||||
}
|
||||
const pending = [];
|
||||
for (const id of CONDITIONAL) {
|
||||
if (!profile.has(id)) { errors.push(`Change Profile: no decision for '${id}'`); continue; }
|
||||
const has = present(PRIMARY[id]);
|
||||
if (profile.get(id) && !has) {
|
||||
const consumer = DOWNSTREAM[id].find(present);
|
||||
if (consumer) errors.push(`'${id}' is selected but ${PRIMARY[id]} does not exist while ${consumer} was already written`);
|
||||
else pending.push(id);
|
||||
}
|
||||
if (!profile.get(id) && has) errors.push(`'${id}' is not selected but ${PRIMARY[id]} exists; select it through a gate decision or remove the file`);
|
||||
}
|
||||
for (const id of SA_MODULES) {
|
||||
if (profile.get(id) && !profile.get("system-analysis")) errors.push(`'${id}' requires system-analysis to be selected`);
|
||||
}
|
||||
if (fs.existsSync(at("system-analysis")) && !present(PRIMARY["system-analysis"])) {
|
||||
errors.push("system-analysis/ exists without system-analysis/system-analysis.md");
|
||||
}
|
||||
|
||||
// 3. Requirement references resolve; delta requirements are covered.
|
||||
const skipSpecs = present(".openspec.yaml") && /^skip_specs:\s*true/m.test(read(at(".openspec.yaml")));
|
||||
if (skipSpecs && present("specs")) errors.push("skip_specs: true but specs/ contains delta files");
|
||||
const deltaReqs = [];
|
||||
const known = new Map();
|
||||
const removed = new Set();
|
||||
const addKnown = (cap, h) => (known.get(cap) ?? known.set(cap, new Set()).get(cap)).add(h);
|
||||
const key = (cap, req) => `${cap}\u0000${req}`;
|
||||
for (const f of walk(at("specs")).filter((x) => x.endsWith("spec.md"))) {
|
||||
const cap = path.relative(at("specs"), path.dirname(f)).split(path.sep).join("/");
|
||||
let op = null;
|
||||
const seen = new Set();
|
||||
for (const line of read(f).split("\n")) {
|
||||
op = (line.match(/^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements/) || [])[1] ?? op;
|
||||
const req = (line.match(/^### Requirement:\s*(.+?)\s*$/) || [])[1];
|
||||
if (req) {
|
||||
if (seen.has(req)) errors.push(`specs/${cap}/spec.md: duplicate Requirement '${req}'`);
|
||||
seen.add(req);
|
||||
if (op === "REMOVED") removed.add(key(cap, req));
|
||||
else addKnown(cap, req);
|
||||
if (op === "ADDED" || op === "MODIFIED") deltaReqs.push(key(cap, req));
|
||||
}
|
||||
const to = (line.match(/^-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/) || [])[1];
|
||||
if (to) addKnown(cap, to);
|
||||
}
|
||||
}
|
||||
for (const f of walk(path.join(root, "specs")).filter((x) => x.endsWith("spec.md"))) {
|
||||
const cap = path.relative(path.join(root, "specs"), path.dirname(f)).split(path.sep).join("/");
|
||||
for (const m of read(f).matchAll(/^### Requirement:\s*(.+?)\s*$/gm)) {
|
||||
if (!removed.has(key(cap, m[1]))) addKnown(cap, m[1]);
|
||||
}
|
||||
}
|
||||
const refs = (text) => [...text.matchAll(REF)].map((m) => ({ cap: m[1], req: m[2] }));
|
||||
for (const f of mdFiles.filter((x) => !rel(x).startsWith(`specs${path.sep}`))) {
|
||||
for (const { cap, req } of refs(read(f))) {
|
||||
if (!known.get(cap)?.has(req)) errors.push(`${rel(f)}: unknown requirement reference \`${cap}\` — \`${req}\``);
|
||||
}
|
||||
}
|
||||
const tasksPath = at("tasks.md");
|
||||
const tasksText = fs.existsSync(tasksPath) ? read(tasksPath) : "";
|
||||
const coversLines = tasksText.split("\n").filter((l) => /\*\*Covers:\*\*/.test(l)).join("\n");
|
||||
const taskCover = new Set(refs(coversLines).map((r) => key(r.cap, r.req)));
|
||||
const qaText = [at("qa-analysis.md"), ...walk(at("test-cases"))].filter(fs.existsSync).map(read).join("\n");
|
||||
const qaCover = new Set(refs(qaText).map((r) => key(r.cap, r.req)));
|
||||
for (const k of deltaReqs) {
|
||||
const label = k.replace("\u0000", "` — `");
|
||||
if (tasksText && !taskCover.has(k)) errors.push(`tasks.md: delta requirement \`${label}\` has no task Covers`);
|
||||
if (present("qa-analysis.md") && !qaCover.has(k)) warnings.push(`qa-analysis: delta requirement \`${label}\` has no verification reference (allowed only with a stated risk reason)`);
|
||||
}
|
||||
if (!skipSpecs && present("specs") && deltaReqs.length === 0) warnings.push("no ADDED/MODIFIED requirements found in delta specs");
|
||||
|
||||
// 4. Test case identity and traceability.
|
||||
const tcIds = new Set();
|
||||
for (const f of walk(at("test-cases")).filter((x) => x.endsWith(".md"))) {
|
||||
for (const block of read(f).split(/^###\s+(?=TC-)/m).slice(1)) {
|
||||
const id = block.split(/\s/)[0];
|
||||
if (tcIds.has(id)) errors.push(`${rel(f)}: duplicate test case id ${id}`);
|
||||
tcIds.add(id);
|
||||
if (!refs(block).length) errors.push(`${rel(f)}: test case ${id} has no requirement reference`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Contracts and diagrams are referenced, JSON parses, required task kinds exist.
|
||||
const saText = walk(at("system-analysis")).filter((f) => f.endsWith(".md")).map(read).join("\n");
|
||||
const contracts = walk(at("system-analysis/contracts"));
|
||||
for (const f of [...contracts, ...walk(at("system-analysis/diagrams"))]) {
|
||||
if (!saText.includes(path.basename(f))) errors.push(`${rel(f)}: not referenced from any system-analysis/*.md`);
|
||||
if (f.endsWith(".json")) {
|
||||
try { JSON.parse(read(f)); } catch (e) { errors.push(`${rel(f)}: invalid JSON (${e.message})`); }
|
||||
}
|
||||
}
|
||||
const taskLines = tasksText.split("\n").filter((l) => /^\s*[-*+]\s*\[.?\]/.test(l));
|
||||
if (tasksText) {
|
||||
if (!taskLines.some((l) => /Independent Review:/.test(l))) errors.push("tasks.md: no `Independent Review:` task");
|
||||
if (contracts.length && !taskLines.some((l) => /Contract Parity:/.test(l))) errors.push("tasks.md: contracts exist but no `Contract Parity:` task");
|
||||
if (profile.get("docs-impact") === false && !/^## Docs Impact/m.test(tasksText)) {
|
||||
errors.push("tasks.md: docs-impact not selected, so a `## Docs Impact` section (changes or no-change reason) is required");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Gate Log: chronological; invalidating Gn withdraws Gn and every later gate.
|
||||
const gates = new Map();
|
||||
const order = [];
|
||||
for (const [rawGate, , scope, validation, , decision] of tableRows(record, "Gate Log", 6) ?? []) {
|
||||
const id = (strip(rawGate).match(/^G[1-4]\b/) || [])[0];
|
||||
if (!id) continue;
|
||||
if (/^invalidated\b/i.test(decision)) {
|
||||
for (const g of [...gates.keys()]) if (g >= id) gates.delete(g);
|
||||
continue;
|
||||
}
|
||||
if (!/^approved\b/i.test(decision)) continue;
|
||||
if (!/"[^"]+"|«[^»]+»/.test(decision)) errors.push(`Gate ${id}: approval must quote the Change Owner decision`);
|
||||
if (!validation) errors.push(`Gate ${id}: approval has no validation evidence`);
|
||||
const prior = ["G1", "G2", "G3", "G4"].filter((g) => g < id && (g !== "G2" || hasG2));
|
||||
if (prior.some((g) => !gates.has(g))) errors.push(`Gate ${id}: approved before ${prior.filter((g) => !gates.has(g)).join(", ")}`);
|
||||
const ids = strip(scope).split(/[,\s]+/).filter(Boolean);
|
||||
for (const a of ids) {
|
||||
if (id !== "G4" && !allowedGates(a, complex, hasG2).includes(id)) errors.push(`Gate ${id}: '${a}' must be approved at ${allowedGates(a, complex, hasG2).join(" or ")}`);
|
||||
}
|
||||
gates.set(id, ids);
|
||||
order.push(id);
|
||||
}
|
||||
if (gates.has("G1") && !gates.get("G1").includes("change-profile")) errors.push("Gate G1: scope must include change-profile");
|
||||
|
||||
// 7. Readiness.
|
||||
const artifactPaths = [
|
||||
["proposal", "proposal.md"], ["specs", "specs"], ["design", "design.md"],
|
||||
["qa-analysis", "qa-analysis.md"], ["tasks", "tasks.md"],
|
||||
...CONDITIONAL.map((id) => [id, PRIMARY[id]]),
|
||||
];
|
||||
const approvedScope = new Set([...gates.values()].flat());
|
||||
const required = hasG2 ? ["G1", "G2", "G3"] : ["G1", "G3"];
|
||||
const missingGates = required.filter((g) => !gates.has(g));
|
||||
const unapproved = artifactPaths.filter(([id, p]) => !NOT_APPROVED.has(id) && present(p) && !approvedScope.has(id)).map(([id]) => id);
|
||||
const blockers = [
|
||||
...(tasksText ? [] : ["tasks.md missing"]),
|
||||
...(missingGates.length ? [`missing gates: ${missingGates.join(", ")}`] : []),
|
||||
...(unapproved.length ? [`not in any approved gate scope: ${unapproved.join(", ")}`] : []),
|
||||
...(pending.length ? [`selected but missing: ${pending.join(", ")}`] : []),
|
||||
];
|
||||
notes.push(`apply-ready: ${blockers.length || errors.length ? `no (${[...blockers, ...(errors.length ? ["errors"] : [])].join("; ")})` : "yes"}`);
|
||||
if (mode !== "plan") blockers.forEach((b) => errors.push(`apply blocker: ${b}`));
|
||||
|
||||
const sequence = ["proposal", "process-record", "research", "specs", "ux-design", "system-analysis",
|
||||
...SA_MODULES, "security-analysis", "design", "qa-analysis", "test-cases", "docs-impact", "tasks"];
|
||||
const pathOf = Object.fromEntries([...artifactPaths, ["process-record", "process-record.md"]]);
|
||||
const next = sequence.find((id) => {
|
||||
if (CONDITIONAL.includes(id) && !profile.get(id)) return false;
|
||||
if (id === "specs" && skipSpecs) return false;
|
||||
return !present(pathOf[id]);
|
||||
});
|
||||
notes.push(`next artifact: ${next ?? "none (planning complete for this profile)"}`);
|
||||
|
||||
// 8. Archive readiness. Native archive treats any non-[x] marker as open, so descoped work is
|
||||
// removed from tasks.md by a gate decision and recorded in "Descoped Work".
|
||||
if (mode === "archive") {
|
||||
for (const l of taskLines) if (!/\[x\]/i.test(l)) errors.push(`tasks.md: open task: ${l.trim()}`);
|
||||
for (const [task, reason, gate] of tableRows(record, "Descoped Work", 3) ?? []) {
|
||||
if (task && (!reason || !/^G[1-4]$/.test(strip(gate)))) errors.push(`Descoped Work '${task}': needs reason and approving gate`);
|
||||
}
|
||||
if (!gates.has("G4")) errors.push("Gate G4 Delivery Acceptance is not approved");
|
||||
if (labMode === "lab") {
|
||||
const retro = record.split(/^## /m).find((s) => s.startsWith("Retrospective")) ?? "";
|
||||
if (!retro.replace(/^Retrospective.*\n/, "").trim()) errors.push("process-record.md: Retrospective is empty (lab mode)");
|
||||
if ((tableRows(record, "Lab Journal", 9) ?? []).length < gates.size) warnings.push("Lab Journal has fewer rows than approved gates");
|
||||
}
|
||||
}
|
||||
finish();
|
||||
|
||||
function finishIfMissing() {
|
||||
if (fs.existsSync(at("process-record.md"))) return;
|
||||
errors.push("process-record.md is missing: Change Profile and Gate Log are required");
|
||||
finish();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
for (const n of notes) console.log(`note: ${n}`);
|
||||
for (const w of warnings) console.log(`warning: ${w}`);
|
||||
for (const e of errors) console.log(`error: ${e}`);
|
||||
console.log(errors.length ? `FAIL ${mode} ${changeName}: ${errors.length} error(s)` : `OK ${mode} ${changeName}`);
|
||||
process.exit(errors.length ? 1 : 0);
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
name: wealth-ledger-sdd-lab
|
||||
version: 1
|
||||
description: Experimental Engineering Lab workflow - proposal + change profile → specs → selected analysis modules → design ∥ qa-analysis → tasks → apply, with 4 decision gates
|
||||
|
||||
# CLI contract (OpenSpec 1.13.1): an artifact is complete when its `generates` path exists;
|
||||
# `requires` is a hard edge; there is no conditional artifact. Conditional artifacts below are
|
||||
# therefore never required by a mandatory artifact or by apply. Selection, ordering of selected
|
||||
# artifacts, gates and archive readiness are checked by `check.cjs` in this schema directory.
|
||||
|
||||
artifacts:
|
||||
- id: proposal
|
||||
generates: proposal.md
|
||||
description: Change Owner intent, scope, capabilities, evidence and open decisions
|
||||
template: proposal.md
|
||||
instruction: |
|
||||
Owner: Change Owner; draft — product-analyst. Gate: G1 Product Decision.
|
||||
|
||||
Purpose:
|
||||
Proposal фиксирует, зачем нужно изменение, какой результат ожидается, где
|
||||
проходит scope и какие решения ещё не приняты. Это не requirements, не system
|
||||
analysis и не design.
|
||||
|
||||
Start of the lab lifecycle (обязательно до записи):
|
||||
- Change создаётся явно: `openspec new change <name> --schema wealth-ledger-sdd-lab
|
||||
--store wealth-ledger-specs`. Schema не становится default и не применяется к
|
||||
существующим changes.
|
||||
- Small (локальный fix, известное behavior) не идёт в OpenSpec: правка canonical spec
|
||||
в том же Issue. Lab schema принимает только Standard и Complex.
|
||||
- Если существует research (`research.md` в change, Issue, внешний отчёт), прочитай
|
||||
его выводы; Proposal ссылается на research в **Evidence**, а не копирует его.
|
||||
Research может закончиться решением не делать change — тогда change не создаётся.
|
||||
- Общий протокол (Change Profile, gates, делегирование сабагентам, `check.cjs`) —
|
||||
instruction artifact `process-record`; он действует на каждой стадии.
|
||||
|
||||
Read First:
|
||||
- Релевантные canonical specs из `specs/openspec/specs/` и AS-IS pages `docs/`
|
||||
(`overview.md`, `architecture/`, `domains/`, `integrations/`, `adr/`, `design/`).
|
||||
- Code-level реализацию не исследуй глубже, чем нужно для scope/impact.
|
||||
|
||||
Critical Rules:
|
||||
- Один Proposal = одно связное намерение; независимо поставляемое разделяй.
|
||||
- Capabilities перечисляют каждую новую/изменяемую behavior spec точным path.
|
||||
Если observable behavior не меняется — `.openspec.yaml: skip_specs: true`.
|
||||
- Breaking observable behavior помечай **BREAKING**.
|
||||
- Evidence разделяет факты (с источником) и гипотезы.
|
||||
- Open Decisions: вопрос, кто решает, что блокирует. Вопрос, меняющий scope или
|
||||
capabilities, решается до G1.
|
||||
|
||||
MUST NOT Include:
|
||||
- files/classes/functions/tables/ORM entities; libraries/frameworks/transport;
|
||||
endpoint/payload/error-code contracts; diagrams; test plans; tasks/PR plan;
|
||||
пиксельные UI-детали.
|
||||
|
||||
Feedback Routing: research gap → research; всё остальное возвращается сюда.
|
||||
|
||||
Validation:
|
||||
- `openspec validate <change> --store wealth-ledger-specs` (Why/What Changes);
|
||||
structural success не является approval.
|
||||
- Затем создай `process-record.md` (Change Profile) и предъяви G1 оба файла вместе.
|
||||
requires: []
|
||||
|
||||
- id: research
|
||||
generates: research.md
|
||||
description: Optional product research used as input context; never approved as a requirement source
|
||||
template: research.md
|
||||
instruction: |
|
||||
Owner: product-researcher. Conditional: создавай только если Change Profile выбрал
|
||||
`research`. Не требуется ни одним artifact и не блокирует apply. Не входит в gates:
|
||||
это входной контекст, не утверждаемый artifact.
|
||||
|
||||
Trigger: новый продуктовый функционал, незнакомая предметная область, конкуренты и
|
||||
альтернативы, проверка гипотезы, высокая продуктовая неопределённость.
|
||||
Если исследование уже существует вне change (Issue, `audit-results/`, отчёт),
|
||||
не копируй его: Proposal ссылается на источник, Change Profile ставит `no` + ссылку.
|
||||
Исследование, начатое до решения о change, живёт вне OpenSpec до G1.
|
||||
|
||||
Critical Rules:
|
||||
- Каждое утверждение: [F] факт с источником, [I] интерпретация или [H] гипотеза.
|
||||
- Не выдумывай источники, цифры и пользователей; нет данных — так и напиши.
|
||||
- Recommendation допускает «не делать» и «сузить scope».
|
||||
|
||||
MUST NOT Include: окончательные requirements, обязательные архитектурные решения,
|
||||
contracts, tasks.
|
||||
|
||||
Feedback Routing: выводы потребляет Proposal (Evidence, Scope, Open Decisions).
|
||||
requires: []
|
||||
|
||||
- id: process-record
|
||||
generates: process-record.md
|
||||
description: Change Profile, gate decisions, descoped work and lab journal for the change
|
||||
template: process-record.md
|
||||
instruction: |
|
||||
Owner: основной агент (оркестратор). Change Profile утверждает Change Owner на G1.
|
||||
Это журнал процесса, а не planning authority: он не содержит requirements,
|
||||
contracts или design.
|
||||
|
||||
## Общий протокол lab schema (действует на каждой стадии)
|
||||
|
||||
Modes and Classes:
|
||||
- **Mode** `lab` — целенаправленная практика: G2 обязателен, независимая проверка
|
||||
на каждом gate, Lab Journal после каждой стадии, Retrospective перед archive,
|
||||
условные artifacts можно выбирать ради названной практики.
|
||||
- **Mode** `delivery` — минимально достаточный процесс: artifacts только по
|
||||
risk-триггеру, qa-analysis компактный, Lab Journal необязателен.
|
||||
- **Class** `standard` | `complex`. Complex: межсистемно, деньги/синхронизация/
|
||||
данные, необратимые миграции или высокая неопределённость. Mode и Class
|
||||
независимы.
|
||||
|
||||
Change Profile (обязательная таблица, все 10 строк):
|
||||
- `yes` только при конкретном trigger из instruction artifact; в lab допустим
|
||||
`yes` с названной практикой («lab: data mapping practice»).
|
||||
- `no` с конкретной причиной. Пустой документ с «N/A» вместо `no` запрещён.
|
||||
- Profile меняется только решением Change Owner, записанным строкой Gate Log
|
||||
со scope `change-profile`; уже утверждённые gates при этом инвалидируются.
|
||||
- Выбранный условный input пишется до artifact, который его потребляет
|
||||
(research, ux-design, system-analysis и modules, security-analysis — до design,
|
||||
qa-analysis и tasks; test-cases, docs-impact — до tasks).
|
||||
|
||||
Deterministic checks — `check.cjs` (из `specs/`):
|
||||
`node openspec/schemas/wealth-ledger-sdd-lab/check.cjs <plan|apply|archive> <change>`.
|
||||
Он единственный владелец правил profile/порядка/gates/readiness; не дублируй их
|
||||
проверку вручную, а читай его вывод. `plan` — после каждого artifact; `apply` —
|
||||
перед apply; `archive` — перед native archive. Exit 1 = нарушение artifact,
|
||||
exit 2 = ошибка вызова/окружения (не исправляй artifact из-за неё).
|
||||
|
||||
Next artifact:
|
||||
- `openspec status` показывает невыбранные условные artifacts как `ready`, а
|
||||
`isPlanningComplete` остаётся `false`, пока они не созданы. Это ограничение CLI:
|
||||
не создавай artifact только потому, что он `ready`.
|
||||
- Следующий artifact = `note: next artifact` из `check.cjs plan`.
|
||||
|
||||
Decision Gates (Gate Log, append-only):
|
||||
| Gate | Scope | Когда |
|
||||
| --- | --- | --- |
|
||||
| G1 Product Decision | proposal, change-profile | всегда |
|
||||
| G2 Requirements Baseline | specs (+ system-analysis и модули для complex) | lab или complex |
|
||||
| G3 Solution Approval | всё остальное planning, включая tasks | всегда |
|
||||
| G4 Delivery Acceptance | implementation, review, QA, parity, docs | перед archive |
|
||||
- Без G2 (delivery standard) specs входят в scope G3.
|
||||
- Complex: system-analysis и выбранные модули входят в G2, чтобы системные
|
||||
обязательства были согласованы до design (contract-first).
|
||||
- Решение gate явно разрешает draft следующего пакета artifacts. Approval
|
||||
перечисляет каждый artifact id в Scope и дословно цитирует слова Change Owner
|
||||
из текущей сессии или комментария Issue (укажи ссылку). Никогда не записывай
|
||||
решение, которого владелец не давал; существование файла или строки Gate Log
|
||||
не является approval — `check.cjs` проверяет только форму записи.
|
||||
- Каждый gate различает: Artifact Validation (CLI + `check.cjs`), Independent
|
||||
Review (findings) и Decision Approval (владелец). Validation и Findings
|
||||
заполняются до запроса решения.
|
||||
- Семантическое изменение утверждённого artifact: строка `invalidated: <reason>`
|
||||
для gate, покрывающего самый ранний затронутый artifact (это снимает и все
|
||||
последующие gates), затем повторное approval по порядку. Механические правки
|
||||
(опечатка, формат) не инвалидируют.
|
||||
- Release Approval находится вне OpenSpec (release-it, Gitea); G4 его не заменяет.
|
||||
|
||||
Independent Review (lab — на каждом gate; delivery — минимум G4):
|
||||
- G1: product-analyst проверяет проблему/scope на противоречия и скрытые решения.
|
||||
- G2: qa-engineer — testability; independent-reviewer — противоречия с canonical
|
||||
specs и пропущенные error/boundary cases.
|
||||
- G3: independent-reviewer — согласованность specs ↔ system-analysis ↔ design ↔
|
||||
qa-analysis ↔ tasks; security-reviewer, если выбран security-analysis.
|
||||
- G4: independent-reviewer — код против approved artifacts; qa-engineer — system
|
||||
verification по qa-analysis.
|
||||
- Reviewer не исправляет artifact; findings маршрутизируются по Feedback Routing.
|
||||
|
||||
Delegation to subagents:
|
||||
- Сабагент — самостоятельный профессионал: не знает OpenSpec, gates и других
|
||||
сабагентов. Основной агент выбирает роль, пишет Task Brief, получает результат,
|
||||
размещает его в artifact, проверяет согласованность и выносит вопросы человеку.
|
||||
- Task Brief: профессиональная цель («спроектируй межсистемные взаимодействия
|
||||
для …», не «создай artifact system-analysis»); точные входные файлы/разделы;
|
||||
ограничения из MUST NOT Include artifact, сформулированные профессионально;
|
||||
ожидаемые разделы ответа; правило «неизвестное — вернуть вопросом, не
|
||||
выдумывать».
|
||||
- Сабагент не принимает решений за другие роли; конфликт с upstream возвращается
|
||||
как finding.
|
||||
|
||||
Lab Journal (lab): строка после каждой стадии — уникальные находки (что нашла
|
||||
только эта стадия), решения, риски, проблемы передачи контекста, skills, стоимость
|
||||
(вызовы, токены/время), что переносимо на крупные проекты. Не пиши ожидаемые
|
||||
результаты заранее.
|
||||
requires:
|
||||
- proposal
|
||||
|
||||
- id: specs
|
||||
generates: "specs/**/*.md"
|
||||
description: Normative black-box behavioral deltas, business rules and invariants
|
||||
template: spec.md
|
||||
instruction: |
|
||||
Owner: product-analyst (requirements engineering). Gate: G2 (lab/complex) или G3.
|
||||
Reviewers: qa-engineer (testability), independent-reviewer (consistency).
|
||||
|
||||
Purpose:
|
||||
Delta Specs — нормативный black-box contract: что система обязана делать для
|
||||
пользователя или внешнего наблюдателя. После archive становятся canonical specs.
|
||||
|
||||
Preconditions:
|
||||
- G1 approved; используются только capabilities из Proposal.
|
||||
- Для existing capability прочитай canonical spec целиком.
|
||||
- `skip_specs: true` — spec files не создаются.
|
||||
|
||||
Critical Rules:
|
||||
- Один Requirement = одно устойчивое observable behavior/invariant.
|
||||
- BCP 14 по policy `config.yaml`: uppercase SHALL/MUST только для обязательств.
|
||||
- Каждый Requirement имеет ≥1 `#### Scenario:` с конкретными WHEN/THEN.
|
||||
- Бизнес-правила, инварианты (например, сохранение суммы, однократный учёт денег,
|
||||
знак и точка округления) и граничные случаи — явные Requirements/Scenarios.
|
||||
- Допущение не оформляется как факт: неизвестное правило → вопрос в Proposal
|
||||
Open Decisions, а не выдуманный Requirement.
|
||||
- Не дели Requirements по repositories; system names/transport/endpoints —
|
||||
System Analysis, если сама их identity не является внешним обязательством.
|
||||
|
||||
Delta Operations:
|
||||
- `ADDED`; `MODIFIED` — ПОЛНЫЙ existing block со всеми scenarios, heading точно;
|
||||
`REMOVED` — **Reason** и **Migration**; `RENAMED` — `FROM:`/`TO:`.
|
||||
- Новая capability: `## Purpose` 1–2 предложения (≥50 символов).
|
||||
- Не оставляй пустые operation sections.
|
||||
|
||||
Reference Format (используется всеми downstream artifacts и `check.cjs`):
|
||||
`` `capability-path` — `Requirement heading` `` — точный heading.
|
||||
|
||||
MUST NOT Include: implementation details, поведение вне Proposal, «корректно»/
|
||||
«быстро»/«удобно» без критерия, противоречия canonical specs.
|
||||
|
||||
Feedback Routing: intent/scope/capabilities → Proposal; systems/contracts/
|
||||
mappings → позже System Analysis; interaction details → UX Design.
|
||||
|
||||
Validation: `openspec validate <change> --strict --store wealth-ledger-specs`.
|
||||
requires:
|
||||
- process-record
|
||||
|
||||
- id: ux-design
|
||||
generates: ux-design.md
|
||||
description: Optional interaction design of approved behavior; part of the Design package
|
||||
template: ux-design.md
|
||||
instruction: |
|
||||
Owner: ux-designer. Conditional: только если Change Profile выбрал `ux-design`.
|
||||
Gate: G3 вместе с design (единый Design package, отдельного UX approval нет).
|
||||
|
||||
Trigger: новые экраны, существенное изменение пользовательского сценария,
|
||||
редизайн, сложные формы, многосостоянийный интерфейс. Иначе UI описывается в
|
||||
разделе UI Implementation `design.md` (reuse или N/A с причиной).
|
||||
|
||||
Authority:
|
||||
- ux-design.md — UX-часть единого Design package (не отдельный planning
|
||||
authority и не отдельный approval): внутри пакета он один владеет flows, IA,
|
||||
состояниями, feedback, responsive и a11y. design.md ссылается на него и
|
||||
фиксирует только инженерную реализацию UI.
|
||||
- Observable behavior остаётся в Specs; UX не вводит новое behavior. Состояние или
|
||||
ошибка, которых нет в Specs, — finding → Specs.
|
||||
|
||||
Critical Rules:
|
||||
- Ссылайся на Requirements, не пересказывай их.
|
||||
- UI States покрывает loading/empty/error/success/partial только там, где они
|
||||
реально возможны.
|
||||
- Visual materials следуют правилу Visual Package из instruction `design`.
|
||||
- Используй существующую design system (`docs/design/`, `frontend` UI kit) прежде
|
||||
нового паттерна; отклонение обоснуй.
|
||||
|
||||
MUST NOT Include: серверная архитектура, contracts, component tree, state
|
||||
management, data fetching.
|
||||
|
||||
Feedback Routing: behavior → Specs; реализуемость → Design; данные/ошибки
|
||||
интеграции → System Analysis.
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: system-analysis
|
||||
generates: system-analysis/system-analysis.md
|
||||
description: Optional hub of cross-system context, boundaries, flows, data ownership and contracts
|
||||
template: system-analysis.md
|
||||
instruction: |
|
||||
Owner: system-analyst. Conditional: только если Change Profile выбрал
|
||||
`system-analysis`. Gate: G2 для complex, иначе G3.
|
||||
|
||||
Trigger: новый/изменённый межсистемный contract (HTTP API, внешняя интеграция,
|
||||
event), изменение data ownership, несколько систем в одном flow, сложная
|
||||
миграция/coexistence. Изменение внутри одного repo без contract — не trigger.
|
||||
|
||||
Structure (hub + modules; каждый module — отдельный выбор Change Profile):
|
||||
- `system-analysis/system-analysis.md` — context, boundaries, As-Is/To-Be, impact,
|
||||
responsibilities, data ownership, flows overview, contract semantics, error
|
||||
semantics на границах, compatibility, ссылки на modules.
|
||||
- `system-analysis/contracts/` — machine-readable target contracts
|
||||
(OpenAPI/AsyncAPI/JSON Schema/Proto). Не отдельный artifact: принадлежит hub.
|
||||
- `system-analysis/diagrams/*.mmd` — Mermaid sequence/state/data-flow, только
|
||||
если помогают понять поведение. Каждый файл упоминается в hub или module.
|
||||
- Modules: `data-mapping.md`, `failure-model.md`, `nfr.md`, `observability.md`.
|
||||
- Расширения (audit, fraud, feature toggles, CMS, внешние системы) добавляются
|
||||
новым module только при реальном requirement; schema их не предопределяет.
|
||||
|
||||
Contract Authority (сохранено из v2):
|
||||
- Новый/изменённый contract определяется в `contracts/` до implementation.
|
||||
- Backend-generated OpenAPI остаётся executable/runtime source после реализации
|
||||
и MUST быть semantic-compatible с approved target contract до G4.
|
||||
- Не создавай второй вечный canonical OpenAPI в `specs/`; target contract остаётся
|
||||
в archived change. Mismatch generated ↔ target возвращает change сюда.
|
||||
- Для existing HTTP API прочитай текущий backend-generated OpenAPI
|
||||
(`frontend/src/shared/api/spec/openapi.json` — опубликованный snapshot).
|
||||
- Каждый contract помечает compatibility: additive | breaking (+ migration).
|
||||
|
||||
Critical Rules:
|
||||
- Не вводи observable behavior молча: gap → Specs и повторный G2/G3.
|
||||
- Hub не копирует содержимое modules; module не копирует hub.
|
||||
- Нет internal classes/modules/DB tables/developer tests.
|
||||
|
||||
Feedback Routing: behavior → Specs; internal implementation → Design;
|
||||
verification → QA Analysis.
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: data-mapping
|
||||
generates: system-analysis/data-mapping.md
|
||||
description: Optional field-level mapping, transformations and conservation invariants between systems
|
||||
template: data-mapping.md
|
||||
instruction: |
|
||||
Owner: system-analyst; reviewer — database-specialist при изменении хранения.
|
||||
Conditional: только если выбран `data-mapping` (требует `system-analysis`).
|
||||
|
||||
Trigger: преобразование данных между системами или моделями (внешний API →
|
||||
domain, domain → API, агрегаты), денежные суммы, валюты, знаки, агрегаты,
|
||||
классификация записей по группам.
|
||||
|
||||
Critical Rules:
|
||||
- Каждое target-поле: source, transformation, unit/currency/scale, nullability,
|
||||
default, validation, поведение при ошибке.
|
||||
- Invariants: правила сохранения (сумма частей = итог, каждая денежная единица
|
||||
учитывается ровно в одном агрегате), знаковые соглашения, точка округления —
|
||||
со ссылкой на защищаемый Requirement. Нет Requirement — finding → Specs.
|
||||
- Double-counting: каждый тип исходной записи, который может попасть в два
|
||||
агрегата, и правило, относящее его ровно к одному.
|
||||
- Большой mapping — machine-checkable файл в `contracts/` со ссылкой отсюда.
|
||||
|
||||
MUST NOT Include: ORM/SQL/код mappers, retry/idempotency (failure-model).
|
||||
|
||||
Feedback Routing: инвариант без Requirement → Specs; ownership → hub;
|
||||
реализация → Design.
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: failure-model
|
||||
generates: system-analysis/failure-model.md
|
||||
description: Optional boundary failure semantics - timeouts, retries, idempotency, partial failure and recovery
|
||||
template: failure-model.md
|
||||
instruction: |
|
||||
Owner: system-analyst; reviewer — reliability-specialist.
|
||||
Conditional: только если выбран `failure-model` (требует `system-analysis`).
|
||||
|
||||
Trigger: внешняя интеграция, фоновая синхронизация, повторяемые операции,
|
||||
многошаговая запись, возможны дубликаты/out-of-order/частичный отказ.
|
||||
|
||||
Critical Rules:
|
||||
- Каждый failure scenario: граница, отказ, обнаружение, требуемое поведение
|
||||
системы, observable результат, Requirement.
|
||||
- Idempotency: ключ, scope, срок хранения, результат повторного запроса/события.
|
||||
- Partial failure: что остаётся согласованным, compensation или recovery.
|
||||
- Обязательства формулируются на границе систем; механизм (lock, queue,
|
||||
transaction) — Design.
|
||||
|
||||
MUST NOT Include: численные SLO (nfr), выбор библиотек, код.
|
||||
|
||||
Feedback Routing: новое user-visible поведение при отказе → Specs; механизм →
|
||||
Design; проверка → QA Analysis.
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: nfr
|
||||
generates: system-analysis/nfr.md
|
||||
description: Optional measurable non-functional requirements with thresholds and evidence
|
||||
template: nfr.md
|
||||
instruction: |
|
||||
Owner: system-analyst; reviewer — reliability-specialist.
|
||||
Conditional: только если выбран `nfr` (требует `system-analysis`).
|
||||
|
||||
Trigger: значимые performance/availability/capacity/retention/security
|
||||
constraints, которые меняют design или verification.
|
||||
|
||||
Critical Rules:
|
||||
- Каждый NFR: id, threshold, способ измерения, rationale/источник.
|
||||
- Без threshold — раздел Constraints Without Thresholds с владельцем решения;
|
||||
не выдумывай числа.
|
||||
- Security-NFR живут здесь; security-analysis ссылается на них по id.
|
||||
|
||||
MUST NOT Include: механизмы достижения (Design), failure semantics
|
||||
(failure-model), события наблюдаемости (observability).
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: observability
|
||||
generates: system-analysis/observability.md
|
||||
description: Optional semantic observability obligations - events, signals, correlation, SLI and alerts
|
||||
template: observability.md
|
||||
instruction: |
|
||||
Owner: system-analyst; reviewer — reliability-specialist.
|
||||
Conditional: только если выбран `observability` (требует `system-analysis`).
|
||||
|
||||
Trigger: фоновые/асинхронные процессы, финансовые расчёты или синхронизация,
|
||||
где оператор должен отличать успех, частичный успех и отказ; новые SLI.
|
||||
|
||||
Critical Rules:
|
||||
- Semantic events с trigger и обязательными атрибутами (например,
|
||||
`code=1 → NO_MONEY`); correlation через границы систем.
|
||||
- Diagnostic scenarios: «когда X, оператор определяет Y по Z».
|
||||
- SLI/alerts — только с определением и целевым значением; цель из nfr — ссылкой.
|
||||
|
||||
MUST NOT Include: SDK/exporter/logger/инфраструктура (Design), секреты и
|
||||
персональные данные в атрибутах.
|
||||
requires:
|
||||
- system-analysis
|
||||
|
||||
- id: security-analysis
|
||||
generates: security-analysis.md
|
||||
description: Optional change-scoped threat model with mitigations and residual risks
|
||||
template: security-analysis.md
|
||||
instruction: |
|
||||
Owner: security-reviewer. Conditional: только если выбран `security-analysis`.
|
||||
Gate: G3; validation requirements проверяются на G4.
|
||||
|
||||
Trigger: authentication/authorization, токены и секреты, новая trust boundary,
|
||||
sensitive/financial personal data, внешняя интеграция, изменение модели доступа.
|
||||
Не создавай для изменений без этих признаков.
|
||||
|
||||
Inputs: Specs; system-analysis (hub, nfr) если выбран; существующие ADR и
|
||||
`docs/architecture/cross-cutting/`.
|
||||
|
||||
Critical Rules:
|
||||
- Threat model только этого change (STRIDE как способ поиска, не как чек-лист).
|
||||
- Каждая mitigation назначена artifact-владельцу: observable требование → Specs,
|
||||
межсистемное обязательство → System Analysis, механизм → Design.
|
||||
- Security-NFR не дублируй: ссылайся на id из `nfr.md`.
|
||||
- Residual risks явно принимает Change Owner на G3.
|
||||
|
||||
MUST NOT Include: универсальный security checklist, эксплуатация внешних систем,
|
||||
секреты.
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: design
|
||||
generates: design.md
|
||||
description: Engineering design of how affected repositories implement approved behavior and system obligations
|
||||
template: design.md
|
||||
instruction: |
|
||||
Owner: solution-architect; reviewers — affected frontend/backend engineers,
|
||||
database-specialist при миграциях. Gate: G3.
|
||||
|
||||
Purpose:
|
||||
Как affected repositories реализуют свою сторону approved Specs и выбранных
|
||||
system-analysis/ux-design/security-analysis. Не повторный system analysis.
|
||||
|
||||
Preconditions:
|
||||
- Все выбранные research, ux-design, system-analysis и modules,
|
||||
security-analysis уже существуют (`check.cjs plan` без errors). Complex:
|
||||
system-analysis утверждён на G2.
|
||||
- Upstream contracts/sequences/mappings — входные ограничения.
|
||||
|
||||
Critical Rules:
|
||||
- Не добавляй observable behavior; не меняй system contracts. Нереализуемый
|
||||
contract → System Analysis, а не private contract здесь.
|
||||
- Каждое существенное решение ссылается на driver: Requirement, NFR id, failure
|
||||
id, threat id или constraint. Новая технология/компонент без driver запрещены.
|
||||
- UI: при ux-design.md — только инженерная реализация (composition, state, data
|
||||
fetching, производство каждого UI state); без ux-design.md — раздел UI
|
||||
Implementation даёт new/changed/reuse/N/A с причиной и нужные экраны/состояния.
|
||||
Неизменяемый UI и N/A не требуют макетов.
|
||||
- Visual Package (единые правила для ux-design.md и UI Implementation): версия
|
||||
пакета; для reuse — immutable locator (store ID + path от OpenSpec root),
|
||||
применимая часть и отличия; локальные PNG/SVG под `visuals/` с relative path,
|
||||
purpose и SHA-256; SVG самодостаточен (без scripts, handlers, внешних
|
||||
fonts/assets); mutable внешняя ссылка не является утверждённой версией.
|
||||
ux-design.md и design.md — один Design package с одним approval на G3.
|
||||
- Solution Assessment обязателен для нового auth, security, storage, external
|
||||
integration, observability, caching, concurrency, shared tooling или иного
|
||||
дорогого необратимого решения; иначе N/A + reason.
|
||||
- Rollback contract обязателен для security, destructive migration, public
|
||||
contract и труднообратимых решений.
|
||||
- Developer verification определяют implementation owners; QA matrix не копируй.
|
||||
- ADR Candidates: только долгоживущие архитектурные решения с alternatives.
|
||||
|
||||
MUST NOT Include: approved sequences/contracts/mappings (ссылки, не копии);
|
||||
UX flows/states при наличии ux-design.md; QA coverage; docs plan; tasks/PR plan;
|
||||
длинный production code.
|
||||
|
||||
Feedback Routing: intent → Proposal; behavior → Specs; interaction → UX Design;
|
||||
contracts/mappings/failure semantics/semantic observability → System Analysis;
|
||||
threats → Security Analysis.
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: qa-analysis
|
||||
generates: qa-analysis.md
|
||||
description: Risk-based system verification strategy independent from developer-owned tests
|
||||
template: qa-analysis.md
|
||||
instruction: |
|
||||
Owner: qa-engineer. Gate: G3. Может выполняться параллельно с design.
|
||||
|
||||
Purpose:
|
||||
Что нужно проверить на уровне системы, чтобы доказать соответствие Specs и
|
||||
выбранным system-analysis modules, и с какой глубиной — по риску.
|
||||
|
||||
Depth (условные секции внутри обязательного artifact):
|
||||
- Всегда: Risk Assessment, Coverage Matrix (строка на каждый delta Requirement),
|
||||
Gaps / Blockers.
|
||||
- Delivery standard: остальные разделы только при наличии содержания; удаляй
|
||||
пустые разделы, не пиши N/A.
|
||||
- Lab или complex: все разделы; Cross-system Verification обязателен при
|
||||
выбранном system-analysis, failure-model или data-mapping (каждый failure id
|
||||
и invariant имеет проверку или обоснованный risk-based отказ).
|
||||
|
||||
Critical Rules:
|
||||
- QA проверяет system behavior, не устройство кода; не предписывай unit/component
|
||||
tests, mocks, coverage percentage.
|
||||
- Не создавай behavior: неопределённый expected result → gap в Specs;
|
||||
неопределённый flow/error/contract → gap в System Analysis.
|
||||
- Ссылайся на Scenarios, не переписывай их. Отдельные test cases — только через
|
||||
artifact `test-cases`.
|
||||
- Планы проверок отделены от результатов: результаты фиксируются на G4, не здесь.
|
||||
- Automation: black-box API/E2E кандидаты; QA repo пока нет — `deferred until QA
|
||||
repo exists`.
|
||||
|
||||
Feedback Routing: behavior → Specs; contracts/failures → System Analysis;
|
||||
testability hooks → Design (как need, без проектирования).
|
||||
requires:
|
||||
- specs
|
||||
|
||||
- id: test-cases
|
||||
generates: "test-cases/*.md"
|
||||
description: Optional explicit test design for high-risk scenarios that need concrete data, steps or oracles
|
||||
template: test-cases.md
|
||||
instruction: |
|
||||
Owner: qa-engineer. Conditional: только если выбран `test-cases`. Gate: G3.
|
||||
|
||||
Trigger: финансовые расчёты, сложные бизнес-сценарии, значимые интеграции,
|
||||
много ошибок и граничных случаев, критичное пользовательское поведение — когда
|
||||
проверка требует конкретных данных, шагов или oracle, которых нет в Scenario.
|
||||
|
||||
Critical Rules:
|
||||
- Один файл на область: `test-cases/<area>.md`; id `TC-<AREA>-<NNN>` уникален.
|
||||
- Каждый case: Verifies (reference format), Risk/Priority, Preconditions, Test
|
||||
Data, Steps, Expected Results, Automation Candidate.
|
||||
- Case добавляет информацию сверх Scenario (числа, комбинации, порядок событий,
|
||||
расчётный oracle). Если не добавляет — не пиши case; coverage matrix ссылается
|
||||
на Scenario.
|
||||
- Не копируй acceptance criteria дословно; expected result выводится из
|
||||
Requirement, а не придумывается. Неопределённость → gap в Specs.
|
||||
- qa-analysis Coverage Matrix ссылается на TC ids.
|
||||
requires:
|
||||
- qa-analysis
|
||||
|
||||
- id: docs-impact
|
||||
generates: docs-impact.md
|
||||
description: Optional promotion plan from change knowledge to canonical AS-IS documentation
|
||||
template: docs-impact.md
|
||||
instruction: |
|
||||
Owner: documentation-engineer. Conditional: только если выбран `docs-impact`.
|
||||
Gate: G3.
|
||||
|
||||
Trigger: несколько docs targets, новый ADR, перенос diagrams, изменение
|
||||
architecture/integration pages, lab-практика Documentation Impact Analysis.
|
||||
Иначе — раздел `## Docs Impact` в tasks.md (targets или конкретная no-change
|
||||
причина).
|
||||
|
||||
Canonical Documentation Model: `docs/` — AS-IS (`overview.md`, `glossary.md`,
|
||||
`architecture/`, `architecture/cross-cutting/`, `domains/`, `integrations/`,
|
||||
`adr/`, `design/`). Accepted ADR не переписывается.
|
||||
|
||||
Critical Rules:
|
||||
- Promote только знание, полезное после завершения change; без migration diary.
|
||||
- Не копируй machine-readable contract в docs как второй source of truth.
|
||||
- Каждая entry: Action, exact Target (существующий path проверен), Category,
|
||||
Resulting AS-IS, Source, Verification.
|
||||
- Diagram promotion по умолчанию — нет.
|
||||
- docs-impact не редактирует `docs/`; Tasks создаёт `[docs]` items.
|
||||
requires:
|
||||
- design
|
||||
|
||||
- id: tasks
|
||||
generates: tasks.md
|
||||
description: Dependency-ordered, repository-owned delivery checklist with verification, review and parity tasks
|
||||
template: tasks.md
|
||||
instruction: |
|
||||
Owner: основной агент (delivery coordination); reviewers — affected engineers.
|
||||
Gate: G3.
|
||||
|
||||
Preconditions:
|
||||
- design и qa-analysis существуют; все выбранные условные artifacts существуют
|
||||
(`check.cjs plan` без errors).
|
||||
- Для complex/lab: G2 approved до записи tasks.
|
||||
|
||||
Task Format:
|
||||
`- [ ] X.Y [main|frontend|backend|docs|specs] <Kind>: <action + outcome>`
|
||||
с полями **Covers**, **Source**, **Verification**, **Depends on**.
|
||||
Kinds: Implementation, Migration, Automated Testing, Contract Parity, Docs,
|
||||
Independent Review, QA Verification, Verification.
|
||||
|
||||
Required Tasks (только релевантные; `check.cjs` проверяет отмеченные *):
|
||||
- Implementation/Automated Testing per repository по Design.
|
||||
- Migration: при изменении хранения — backup/rollback и проверка на копии данных.
|
||||
- * Contract Parity: при наличии `system-analysis/contracts/` — сравнение
|
||||
backend-generated OpenAPI/runtime с target contract.
|
||||
- Docs: из docs-impact или раздела `## Docs Impact`.
|
||||
- * Independent Review: read-only review реализации против approved artifacts.
|
||||
- QA Verification: system checks по qa-analysis (и test-cases).
|
||||
- Security: проверки из security-analysis Validation Requirements, если выбран.
|
||||
- Native sync/archive не является task; canonical specs не правятся task'ом.
|
||||
|
||||
Critical Rules:
|
||||
- Одна task = один repository + один independently reviewable outcome.
|
||||
- * Каждый ADDED/MODIFIED Requirement появляется в Covers хотя бы одной task.
|
||||
- Covers использует reference format; неизвестный heading — ошибка `check.cjs`.
|
||||
- `[qa]` запрещён, пока QA repo не существует.
|
||||
- Не создавай ceremonial tasks; нерелевантная Kind не нужна.
|
||||
- Descope после G3: удали task из tasks.md и запиши в process-record
|
||||
`Descoped Work` с причиной и gate решения владельца.
|
||||
|
||||
MUST NOT Include: новое behavior/contract/design/QA/docs decision (→ upstream
|
||||
artifact), commits/branches, будущие результаты.
|
||||
|
||||
Feedback Routing: semantic change → самый ранний затронутый artifact,
|
||||
инвалидируя его gate и последующие.
|
||||
requires:
|
||||
- design
|
||||
- qa-analysis
|
||||
|
||||
apply:
|
||||
requires:
|
||||
- tasks
|
||||
tracks: tasks.md
|
||||
instruction: |
|
||||
Apply реализует approved plan и не переоткрывает upstream decisions без feedback cycle.
|
||||
|
||||
Preconditions (CLI проверяет только наличие tasks.md, остальное — `check.cjs`):
|
||||
- `check.cjs apply <change>` = OK (G1, G2 для lab/complex, G3; каждый
|
||||
существующий artifact утверждён на своём gate; выбранные artifacts существуют).
|
||||
- Перед зависимой UI-работой проверь наличие и SHA-256 материалов из таблицы
|
||||
Materials (ux-design.md или UI Implementation design.md).
|
||||
- Store `wealth-ledger-specs`; protocol — instruction `process-record`.
|
||||
|
||||
Context Loading: `contextFiles` из apply instructions, затем `AGENTS.md` только
|
||||
affected repositories, затем relevant upstream sections для каждой task.
|
||||
|
||||
Execution:
|
||||
- Pending tasks по dependency order; один repository на task; delegate по роли
|
||||
(frontend-engineer, backend-engineer, database-specialist) с Task Brief.
|
||||
- Checkbox отмечается только после observed success Verification.
|
||||
- Contract Parity: backend-generated OpenAPI semantic-compatible с target contract.
|
||||
- Independent Review выполняет independent-reviewer (read-only) по полному diff
|
||||
против approved artifacts; Major findings исправляются или возвращаются в gate.
|
||||
- QA Verification выполняет qa-engineer по qa-analysis; результаты — в Gate Log G4.
|
||||
- Lab: строка Lab Journal после implementation, review и QA.
|
||||
|
||||
Before archive (G4 Delivery Acceptance):
|
||||
- Все tasks `[x]`; descoped — только через `Descoped Work`.
|
||||
- Validation G4: developer checks, parity, review verdict, QA results, docs.
|
||||
- Lab: Retrospective заполнен.
|
||||
- `check.cjs archive <change>` = OK (включает все apply-проверки), затем `openspec archive <change>
|
||||
--store wealth-ledger-specs`. Никогда `--no-validate`. `--yes` подавляет и
|
||||
подтверждение spec updates, и предупреждение о незакрытых tasks, поэтому
|
||||
допустим только сразу после успешного `check.cjs archive` (в non-TTY агентской
|
||||
среде без него archive не выполняется).
|
||||
|
||||
Sync / Archive Semantics: native archive обновляет canonical specs;
|
||||
system-analysis, contracts и process-record остаются в archived change; `docs/`
|
||||
меняются только `[docs]` tasks; значимые решения — immutable ADR.
|
||||
|
||||
Feedback Routing:
|
||||
- intent/scope → Proposal; behavior/invariants → Specs; interaction → UX Design;
|
||||
systems/contracts/mappings/failures/NFR/semantic observability → System Analysis
|
||||
и modules; threats → Security Analysis; internal implementation → Design;
|
||||
verification → QA Analysis/Test Cases; docs → Docs Impact; decomposition → Tasks.
|
||||
- При semantic conflict останови downstream work, сообщи evidence, инвалидируй
|
||||
gates от самого раннего затронутого artifact.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Data Mapping
|
||||
|
||||
<!-- guide: Field-level transformation between systems. Large mappings go to a
|
||||
machine-checkable file in contracts/ and are referenced here. Remove guides. -->
|
||||
|
||||
## Source and Target
|
||||
|
||||
| Mapping | Source (owner) | Target (owner) | Direction | Trigger |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
## Field Mapping
|
||||
|
||||
| Target field | Source field(s) | Transformation | Unit / currency / scale | Nullability | Default | Validation | On failure |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Invariants
|
||||
|
||||
<!-- guide: Conservation rules that must hold after mapping, e.g. "total = sum of
|
||||
positions + cash; each money unit counted once", sign conventions, rounding
|
||||
point. Each invariant references the spec requirement it protects. -->
|
||||
|
||||
## Double-counting and Classification Risks
|
||||
|
||||
<!-- guide: Every source record type that could fall into two target aggregates,
|
||||
and the rule that assigns it to exactly one. -->
|
||||
|
||||
## Transformation Errors
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,58 @@
|
||||
# Engineering Design
|
||||
|
||||
<!-- guide: How affected repositories implement approved specs and system
|
||||
obligations. Reference upstream ids; do not restate them. Remove guides. -->
|
||||
|
||||
## Inputs
|
||||
|
||||
<!-- guide: Exact upstream artifacts this design implements (specs, selected
|
||||
system-analysis modules, ux-design, security-analysis). -->
|
||||
|
||||
## Architecture Drivers
|
||||
|
||||
<!-- guide: Requirements, NFR ids, constraints and risks that force decisions. -->
|
||||
|
||||
## Technical Goals / Non-Goals
|
||||
|
||||
## Implementation Impact Map
|
||||
|
||||
| Area | Affected | Outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| main | | |
|
||||
| frontend | | |
|
||||
| backend | | |
|
||||
|
||||
## Decisions
|
||||
|
||||
<!-- guide: Per decision: context, chosen option, rejected alternatives, driver it
|
||||
satisfies. Group by repository when that helps. -->
|
||||
|
||||
## Components and Responsibilities
|
||||
|
||||
## Data / Storage Changes
|
||||
|
||||
## Contract Implementation
|
||||
|
||||
<!-- guide: How each contract is produced/consumed and how published OpenAPI is
|
||||
compared with the target contract. N/A when no contracts changed. -->
|
||||
|
||||
## Cross-cutting Implementation
|
||||
|
||||
## UI Implementation
|
||||
|
||||
<!-- guide: With ux-design.md: component composition, state, data fetching, how
|
||||
each UI state is produced — no restated UX. Without ux-design.md: state
|
||||
new/changed, reuse or N/A with reason, plus the needed screen/state detail. -->
|
||||
|
||||
## Developer Verification Strategy
|
||||
|
||||
## Migration / Rollout / Rollback
|
||||
|
||||
## Solution Assessment
|
||||
|
||||
<!-- guide: Required for new auth, security, storage, integration, observability,
|
||||
caching, concurrency or shared tooling; otherwise N/A with reason. -->
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
## ADR Candidates
|
||||
@@ -0,0 +1,18 @@
|
||||
# Documentation Impact
|
||||
|
||||
<!-- guide: Plan only; docs/ is edited by [docs] tasks. Remove guides. -->
|
||||
|
||||
## Summary
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Diagram Promotion
|
||||
|
||||
## ADR Decision
|
||||
|
||||
## Glossary / Navigation Impact
|
||||
|
||||
## No-Change Rationale
|
||||
@@ -0,0 +1,26 @@
|
||||
# Failure Model
|
||||
|
||||
<!-- guide: Boundary-level failure semantics. Implementation of mechanisms (queues,
|
||||
locks, libraries) belongs to design.md. Remove guides before G3. -->
|
||||
|
||||
## Failure Scenarios
|
||||
|
||||
| ID | Boundary / step | Failure | Detection | Required system behavior | Observable result | Requirement |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Timeouts and Retries
|
||||
|
||||
## Idempotency and Duplicate Handling
|
||||
|
||||
<!-- guide: Idempotency key, scope, retention; what a duplicate request/event
|
||||
yields. -->
|
||||
|
||||
## Ordering and Concurrency
|
||||
|
||||
## Partial Failure and Compensation
|
||||
|
||||
## Recovery Behavior
|
||||
|
||||
## Compatibility During Failure
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,14 @@
|
||||
# Non-Functional Requirements
|
||||
|
||||
<!-- guide: Only measurable constraints that change design or verification.
|
||||
Each row needs a threshold and a way to measure it. Remove guides. -->
|
||||
|
||||
| ID | Category | Requirement | Threshold | Measurement / evidence | Rationale / source |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Constraints Without Thresholds
|
||||
|
||||
<!-- guide: Constraints that are real but not measurable yet; state why and who
|
||||
decides the threshold. -->
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,24 @@
|
||||
# Observability
|
||||
|
||||
<!-- guide: What must be observable and why; not which SDK, exporter or logger
|
||||
(design.md). Remove guides before G3. -->
|
||||
|
||||
## Semantic Events
|
||||
|
||||
| Event | Trigger | Required attributes | Consumer / purpose |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## Logs, Metrics and Traces
|
||||
|
||||
## Correlation
|
||||
|
||||
## SLI and Alert Conditions
|
||||
|
||||
| SLI | Definition | Target | Alert condition |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## Diagnostic Scenarios
|
||||
|
||||
<!-- guide: "When X happens, an operator can determine Y from Z." -->
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,57 @@
|
||||
# Process Record
|
||||
|
||||
**Mode:** <!-- guide: lab | delivery -->
|
||||
**Class:** <!-- guide: standard | complex -->
|
||||
**Issue:** <!-- guide: Gitea Issue link -->
|
||||
|
||||
## Change Profile
|
||||
|
||||
<!-- guide: One row per conditional artifact, all ten rows required. Selected =
|
||||
yes|no. Reason cites the concrete trigger from the instruction table or why
|
||||
it does not apply; "lab practice" is a valid reason only in lab mode and
|
||||
must name the practice. -->
|
||||
|
||||
| Artifact | Selected | Reason |
|
||||
| --- | --- | --- |
|
||||
| research | | |
|
||||
| ux-design | | |
|
||||
| system-analysis | | |
|
||||
| data-mapping | | |
|
||||
| failure-model | | |
|
||||
| nfr | | |
|
||||
| observability | | |
|
||||
| security-analysis | | |
|
||||
| test-cases | | |
|
||||
| docs-impact | | |
|
||||
|
||||
## Gate Log
|
||||
|
||||
<!-- guide: Append-only, chronological. Scope = artifact ids covered by the
|
||||
decision. Validation = commands run and their results, review verdicts.
|
||||
Decision = `approved "<quoted owner words>"`, `rejected`, `changes requested`
|
||||
or `invalidated: <reason>`. Never write a decision the owner did not give. -->
|
||||
|
||||
| Gate | Date | Scope | Validation | Findings | Decision |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Descoped Work
|
||||
|
||||
<!-- guide: Tasks removed from tasks.md after G3. Reason and approving gate are
|
||||
required. Leave the header row only when nothing was descoped. -->
|
||||
|
||||
| Task | Reason | Gate |
|
||||
| --- | --- | --- |
|
||||
|
||||
## Lab Journal
|
||||
|
||||
<!-- guide: lab mode: one row per completed stage, written right after it.
|
||||
Unique findings = what this stage found that no other stage did.
|
||||
Cost = subagent calls, approximate tokens or time. delivery mode: optional. -->
|
||||
|
||||
| Stage | Role / subagent | Unique findings | Decisions | Risks found / excluded | Handoff issues | Skills | Cost | Transferable |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Retrospective
|
||||
|
||||
<!-- guide: Written after G4. Which stages paid off, which were redundant for this
|
||||
class of change, what to change in the schema. Facts first, then opinion. -->
|
||||
@@ -0,0 +1,36 @@
|
||||
# Proposal
|
||||
|
||||
<!-- guide: Language of the Change Owner. No files, APIs, tables, libraries,
|
||||
diagrams or test plans. Remove every guide comment before G1. -->
|
||||
|
||||
## Why
|
||||
|
||||
## What Changes
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
## Impact
|
||||
|
||||
## Success Criteria
|
||||
|
||||
## Evidence
|
||||
|
||||
<!-- guide: Facts that justify the change, each with a source: Issue, research
|
||||
(link or `research.md`), incident, measurement. Mark hypotheses as such. -->
|
||||
|
||||
## Assumptions / Dependencies
|
||||
|
||||
## Open Decisions
|
||||
|
||||
<!-- guide: Decisions not yet taken that may change scope. Each item: question,
|
||||
who decides, what blocks until it is answered. Empty only if none exist. -->
|
||||
@@ -0,0 +1,34 @@
|
||||
# QA Analysis
|
||||
|
||||
<!-- guide: Risk-based system verification strategy. Reference scenarios; do not
|
||||
restate them. No developer unit/component test plans. Remove guides. -->
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Affected requirements | Likelihood | Impact | Verification depth |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
## Coverage Matrix
|
||||
|
||||
<!-- guide: One row per delta requirement. Check = system-level way of proving
|
||||
it; test case ids when test-cases is selected; or "not verified: <risk
|
||||
reason>". -->
|
||||
|
||||
| Requirement | Check | Level | Test cases |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## Negative and Boundary Focus
|
||||
|
||||
## Cross-system Verification
|
||||
|
||||
## Test Data and Environment Needs
|
||||
|
||||
## Manual / Exploratory Scope
|
||||
|
||||
## Automation Candidates
|
||||
|
||||
## Regression Impact
|
||||
|
||||
## Gaps / Blockers
|
||||
|
||||
<!-- guide: Each gap names its routing destination artifact. -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Product Research
|
||||
|
||||
<!-- guide: Input context, not a requirement source. Every claim is marked
|
||||
[F] fact with source, [I] interpretation or [H] hypothesis. Remove guides. -->
|
||||
|
||||
## Problem Statement
|
||||
|
||||
## Target Audience
|
||||
|
||||
## Research Questions
|
||||
|
||||
## Existing Solutions and Alternatives
|
||||
|
||||
| Option | What it does | Strengths | Weaknesses | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
## Evidence & Sources
|
||||
|
||||
## Assumptions
|
||||
|
||||
## Risks
|
||||
|
||||
## Conclusions
|
||||
|
||||
## Recommendation
|
||||
|
||||
<!-- guide: build / do not build / narrow scope / research more, with reasons.
|
||||
"Do not build" is a valid outcome. -->
|
||||
@@ -0,0 +1,26 @@
|
||||
# Security Analysis
|
||||
|
||||
<!-- guide: Threat model of this change only. Reference security NFR rows by id;
|
||||
do not restate them. No generic checklist. Remove guides before G3. -->
|
||||
|
||||
## Scope
|
||||
|
||||
## Assets
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
## Threats
|
||||
|
||||
| ID | Threat (STRIDE) | Attack scenario | Asset / boundary | Existing control | Likelihood / impact |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Recommended Mitigations
|
||||
|
||||
| Threat | Mitigation | Owner artifact (specs / system-analysis / design) |
|
||||
| --- | --- | --- |
|
||||
|
||||
## Residual Risks
|
||||
|
||||
## Validation Requirements
|
||||
|
||||
<!-- guide: Evidence needed before G4: review focus, negative tests, tooling. -->
|
||||
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
Choose only the sections this delta needs; delete this guide before approval.
|
||||
|
||||
New capability only: add `## Purpose` with 1–2 sentences (50+ characters).
|
||||
Existing capability: do not add Purpose.
|
||||
|
||||
Supported operations:
|
||||
- `## ADDED Requirements`: new complete Requirement blocks.
|
||||
- `## MODIFIED Requirements`: full existing Requirement blocks with every
|
||||
surviving Scenario, then edited behavior; heading matches exactly.
|
||||
- `## REMOVED Requirements`: Requirement heading plus **Reason** and **Migration**.
|
||||
- `## RENAMED Requirements`: `FROM:` and `TO:` heading pairs only.
|
||||
|
||||
Every normative Requirement uses BCP 14 (RFC 2119 + RFC 8174) uppercase
|
||||
SHALL/MUST deliberately, and at least one `#### Scenario:` with `- **WHEN**`
|
||||
and `- **THEN**`. Lowercase must/should/may are ordinary language.
|
||||
Do not leave empty operation sections or a sample Requirement.
|
||||
-->
|
||||
@@ -0,0 +1,50 @@
|
||||
# System Analysis
|
||||
|
||||
<!-- guide: Hub document. Owns context, boundaries, flows overview and contract
|
||||
semantics; details live in selected modules and are referenced, not copied.
|
||||
Remove guides before G3. -->
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
| Requirement | Flow / contract | Module |
|
||||
| --- | --- | --- |
|
||||
|
||||
## System Context and Boundaries
|
||||
|
||||
## As-Is / To-Be
|
||||
|
||||
## Impact Analysis
|
||||
|
||||
## Systems and Responsibilities
|
||||
|
||||
## Data Ownership
|
||||
|
||||
| Data | Source of truth | Writers | Readers |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## Interaction Flows
|
||||
|
||||
<!-- guide: Reference diagrams/*.mmd; text only for what a diagram cannot carry. -->
|
||||
|
||||
## Contracts
|
||||
|
||||
<!-- guide: Per contract: producer/consumer, operation/event, version, file in
|
||||
contracts/, compatibility class (additive / breaking), published source it
|
||||
must match after implementation. -->
|
||||
|
||||
| Contract | Producer → Consumer | File | Compatibility | Published source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
## Error Semantics
|
||||
|
||||
<!-- guide: Business vs technical failures at boundaries. Detailed retry/
|
||||
idempotency/recovery belongs to failure-model.md when selected. -->
|
||||
|
||||
## Compatibility / Migration
|
||||
|
||||
## Detailed Modules
|
||||
|
||||
<!-- guide: Link each selected module (data-mapping, failure-model, nfr,
|
||||
observability) with one line on what it decides. -->
|
||||
|
||||
## Open Questions
|
||||
@@ -0,0 +1,18 @@
|
||||
# Tasks
|
||||
|
||||
<!-- guide: Task line format:
|
||||
- [ ] X.Y [main|frontend|backend|docs|specs] <Kind>: <action + outcome>
|
||||
- **Covers:** `capability` — `Requirement heading` pairs, or `N/A` — <reason>
|
||||
- **Source:** exact upstream artifact section / contract / docs-impact entry
|
||||
- **Verification:** owner-controlled command, observable check or artifact
|
||||
- **Depends on:** task ids or none
|
||||
Kinds: Implementation, Migration, Automated Testing, Contract Parity, Docs,
|
||||
Independent Review, QA Verification, Verification. Remove guides before G3. -->
|
||||
|
||||
## 1. <!-- guide: delivery phase -->
|
||||
|
||||
## Docs Impact
|
||||
|
||||
<!-- guide: Only when docs-impact is not selected: list docs/ targets with
|
||||
resulting AS-IS, or one concrete no-change reason. Delete when docs-impact.md
|
||||
exists. -->
|
||||
@@ -0,0 +1,16 @@
|
||||
# Test Cases — <!-- guide: area -->
|
||||
|
||||
<!-- guide: One file per area under test-cases/. A case adds concrete data, steps
|
||||
or an oracle that the scenario does not already state; otherwise reference the
|
||||
scenario from qa-analysis instead of writing a case. Remove guides. -->
|
||||
|
||||
### TC-<!-- guide: AREA-NNN --> <!-- guide: title -->
|
||||
|
||||
- **Verifies:** `capability` — `Requirement heading` / Scenario
|
||||
- **Risk / Priority:**
|
||||
- **Preconditions:**
|
||||
- **Test Data:**
|
||||
- **Steps:**
|
||||
1.
|
||||
- **Expected Results:**
|
||||
- **Automation Candidate:** <!-- guide: yes (level) | no (reason) -->
|
||||
@@ -0,0 +1,40 @@
|
||||
# UX Design
|
||||
|
||||
<!-- guide: Interaction design of observable behavior from Delta Specs. Reference
|
||||
requirements as `capability` — `Requirement heading`; do not restate them.
|
||||
No server architecture, contracts or component/state-management choices.
|
||||
Remove guides before G3. -->
|
||||
|
||||
## User Goals
|
||||
|
||||
## User Flows
|
||||
|
||||
<!-- guide: Mermaid flowchart or numbered steps per flow, with entry and exit points. -->
|
||||
|
||||
## Information Architecture and Screen Structure
|
||||
|
||||
## Interaction Behavior
|
||||
|
||||
## UI States
|
||||
|
||||
| Screen / region | Loading | Empty | Error | Success | Partial / stale |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
||||
## Validation Feedback
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
## Accessibility
|
||||
|
||||
<!-- guide: Keyboard path, focus order, names/roles, contrast, non-color cues. -->
|
||||
|
||||
## Design System Reuse
|
||||
|
||||
## Materials
|
||||
|
||||
<!-- guide: Inline Markdown by default; local files follow the Visual Package rule. -->
|
||||
|
||||
| Material ID | Relative path / reference | Screen/state and purpose | SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## Open UX Questions
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
### Requirement: Realized income analytics is account-scoped and period-bound
|
||||
|
||||
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода только для одного открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
|
||||
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) из счетов, доступных через текущий T-Bank connection User. В режиме `all` система MUST проверять доступ каждого входящего счёта и подтверждать полноту каждого исходного периода по всем входящим счетам; отсутствующий вклад счёта MUST NOT заменяться нулём. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
|
||||
|
||||
Чтение MUST использовать сохранённые операции текущего подключения и локальное подтверждение доступа без синхронного запроса T-Bank. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`. Полнота MUST определяться для выбранного периода и каждого необходимого периода сравнения отдельно. Запрос MUST приоритетно загружать их пропуски до фоновой догрузки остальной истории; известные правила календарных дат MUST сохраняться.
|
||||
|
||||
#### Scenario: Аналитика за период по умолчанию
|
||||
|
||||
@@ -29,6 +31,14 @@
|
||||
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
|
||||
|
||||
#### Scenario: Общий доход требует полноты всех счетов
|
||||
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
|
||||
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
|
||||
|
||||
#### Scenario: Открыт ещё не загруженный период
|
||||
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
|
||||
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
|
||||
|
||||
### Requirement: Realized income uses exact executed coupon and dividend payments
|
||||
|
||||
Система MUST рассчитывать фактически полученный доход только по исполненным Ledger Entries с точным upstream-типом coupon или dividend. Для каждой исходной валюты система MUST отдельно возвращать общий итог, купонный subtotal и дивидендный subtotal как точные суммы исходных broker-reported payments. Знак каждой выплаты MUST сохраняться, поэтому отрицательная корректировка MUST уменьшать соответствующий итог и оставаться доступной в составе результата. Dividend transfer, погашение principal, offer, trade, transfer, fee, tax, overnight income, иной тип операции, нереализованный результат и изменение рыночной цены MUST NOT входить в фактически полученный доход. Система MUST NOT выполнять валютную конвертацию или формировать общий итог по разным валютам.
|
||||
@@ -155,11 +165,11 @@ MUST NOT заменять успешно отображённый Portfolio ил
|
||||
|
||||
### Requirement: Income payments remain traceable to exact Ledger Entries
|
||||
|
||||
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
|
||||
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; заголовки валютных групп и receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы по каждой валюте, и он MUST NOT подменять аналитические итоги.
|
||||
|
||||
#### Scenario: Каждая выплата показана отдельной карточкой
|
||||
|
||||
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
|
||||
- **WHEN** в режиме одного Brokerage Account выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
|
||||
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
|
||||
|
||||
#### Scenario: Список воспроизводит точный состав дохода
|
||||
@@ -174,9 +184,14 @@ MUST NOT заменять успешно отображённый Portfolio ил
|
||||
|
||||
#### Scenario: Валюта видна в сумме каждой выплаты
|
||||
|
||||
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
|
||||
- **WHEN** подходящие в режиме одного Brokerage Account выплаты одного Instrument представлены в нескольких исходных валютах
|
||||
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
|
||||
|
||||
#### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»
|
||||
|
||||
- **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
|
||||
- **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются
|
||||
|
||||
### Requirement: Analytics summary remains readable per currency
|
||||
|
||||
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
|
||||
|
||||
@@ -7,16 +7,30 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: User can retrieve an account ledger
|
||||
Система MUST предоставлять аутентифицированный журнал только для чтения одного Brokerage Account, принадлежащего текущему User. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года до момента запроса.
|
||||
Система MUST предоставлять аутентифицированный журнал только для чтения выбранного Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего User и его T-Bank connection. В режиме `all` доступ MUST проверяться для каждого входящего счёта, операции MUST сохранять принадлежность исходному счёту, а полнота периода MUST учитывать покрытие каждого входящего счёта. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года в UTC до момента запроса.
|
||||
|
||||
Система MUST читать сохранённые операции и локальное подтверждение доступа текущего подключения без синхронного обращения к T-Bank, включая запрос списка счетов. Ответ MUST содержать envelope `{ data, meta }` по `tbank-data-sync`, отражающий полноту запрошенного периода независимо от текущей страницы. Отсутствующие данные MUST возвращаться с `data: null` и состоянием загрузки, а частично сохранённый период MUST обозначаться как `partial`. Только подтверждённо полный пустой период MUST обозначать отсутствие операций.
|
||||
|
||||
#### Scenario: Получение журнала с периодом по умолчанию
|
||||
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
|
||||
- **THEN** система возвращает его исполненные операции от начала текущего календарного года до момента запроса
|
||||
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
- **WHEN** аутентифицированный User запрашивает журнал счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
|
||||
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
|
||||
|
||||
#### Scenario: Общий журнал сохраняет принадлежность записей
|
||||
- **WHEN** аутентифицированный User запрашивает журнал в режиме `all`
|
||||
- **THEN** система возвращает исполненные операции доступных ему счетов, и каждая запись указывает свой исходный счёт
|
||||
|
||||
#### Scenario: Общий журнал содержит неполный счёт
|
||||
- **WHEN** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
|
||||
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
|
||||
|
||||
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
|
||||
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
|
||||
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
|
||||
|
||||
### Requirement: User can browse a filtered ledger
|
||||
Система MUST позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система MUST разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
|
||||
|
||||
@@ -50,9 +64,10 @@
|
||||
### Requirement: Account overview includes recent ledger events
|
||||
|
||||
Обзор счёта MUST показывать пять последних исполненных записей журнала этого
|
||||
счёта и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
|
||||
счёта, а в режиме «Все счета» — пять последних записей всех входящих счетов, и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
|
||||
визуальную идентичность инструмента и семантику денежного движения, что и запись
|
||||
полного журнала. Неуспешный или пустой запрос MUST оставаться локальным состоянием
|
||||
полного журнала; в группе режима «Все счета» визуальная идентичность, дата и тип
|
||||
показываются в заголовке группы. Неуспешный или пустой запрос MUST оставаться локальным состоянием
|
||||
блока и MUST NOT заменять успешно отображённый Portfolio. При временном сбое
|
||||
обновления блок MUST сохранять доступные записи без технического предупреждения
|
||||
или ручного повтора. При отсутствии результата он SHALL показывать первичное
|
||||
@@ -73,7 +88,7 @@
|
||||
|
||||
### Requirement: Full ledger presents compact rows
|
||||
|
||||
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
|
||||
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком.
|
||||
|
||||
#### Scenario: Отображение записи только с инструментом
|
||||
|
||||
@@ -110,6 +125,11 @@
|
||||
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
|
||||
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
|
||||
|
||||
#### Scenario: Запись внутри группы режима «Все счета»
|
||||
|
||||
- **WHEN** полный журнал в режиме «Все счета» показывает запись в составе группы
|
||||
- **THEN** её дата, тип и инструмент читаются из заголовка группы, а строка записи показывает счёт и денежное изменение со знаком и подписью типа
|
||||
|
||||
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
|
||||
Система SHALL кэшировать не более 100 недавно использованных страниц Account Ledger в каждом backend process не более чем на 30 секунд. Cache key MUST включать User, Brokerage Account, нормализованные период и categories, opaque cursor и limit; порядок или дублирование categories MUST NOT создавать отдельную cache identity, а отсутствующий конец периода MUST использовать текущий 30-секундный bucket. Просроченная страница MUST NOT возвращаться, а при переполнении cache MUST удалять наименее недавно использованную страницу.
|
||||
|
||||
@@ -177,3 +197,76 @@
|
||||
#### Scenario: Следующая страница ещё не получена
|
||||
- **WHEN** пользователь перешёл к следующей странице, но её загрузка временно не удалась
|
||||
- **THEN** область новой страницы показывает ожидание или нейтральную недоступность, не выдаёт прежние записи за новую страницу и восстанавливает чтение автоматически
|
||||
|
||||
### Requirement: All-accounts ledger groups same-day entries of one instrument
|
||||
В режиме «Все счета» полный журнал и последние записи обзора MUST объединять в визуальную группу две и более показанных записи с одинаковыми днём (как на экране), identity Instrument и точным upstream-типом операции, даже если они на одном счёте. Записи без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
|
||||
|
||||
#### Scenario: Купоны одной облигации на двух счетах
|
||||
- **WHEN** в режиме «Все счета» журнал показывает за один день купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
|
||||
- **THEN** они показаны одной группой с облигацией, типом «Купон» и итогом +300 RUB, а под заголовком видны строки «брокерский счёт +200 RUB» и «ИИС +100 RUB»
|
||||
|
||||
#### Scenario: Две операции одного типа на одном счёте
|
||||
- **WHEN** в режиме «Все счета» журнал показывает за один день две покупки одной бумаги на одном счёте
|
||||
- **THEN** они показаны одной группой, где каждая покупка остаётся отдельной строкой со своим денежным изменением
|
||||
|
||||
#### Scenario: Разные типы по одной бумаге не объединяются
|
||||
- **WHEN** в режиме «Все счета» за один день по одной облигации есть купон и налог с этого купона
|
||||
- **THEN** купоны и налоги показаны раздельно, и ни одна группа не содержит записи разных типов
|
||||
|
||||
#### Scenario: Покупка и продажа не объединяются
|
||||
- **WHEN** в режиме «Все счета» за один день показаны покупка и продажа одной бумаги
|
||||
- **THEN** они не входят в одну группу
|
||||
|
||||
#### Scenario: Разные дни не объединяются
|
||||
- **WHEN** в режиме «Все счета» купоны по одной облигации на двух счетах датированы на экране разными днями
|
||||
- **THEN** они показаны отдельными записями без группы
|
||||
|
||||
#### Scenario: Запись без Instrument
|
||||
- **WHEN** в режиме «Все счета» за один день показаны две записи одного типа без identity Instrument
|
||||
- **THEN** они показаны отдельными записями так же, как без группировки
|
||||
|
||||
#### Scenario: Режим одного счёта не меняется
|
||||
- **WHEN** User открывает журнал или обзор одного Brokerage Account, где есть два купона по одной облигации за один день
|
||||
- **THEN** записи показаны отдельными строками без группы, как до изменения
|
||||
|
||||
### Requirement: All-accounts ledger group shows each entry and an exact total
|
||||
Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком по каждой исходной валюте без конвертации и округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена.
|
||||
|
||||
#### Scenario: Итог по нескольким валютам
|
||||
- **WHEN** группа содержит записи с денежными изменениями в RUB и в USD
|
||||
- **THEN** заголовок показывает отдельный точный итог в RUB и отдельный в USD, без конвертации
|
||||
|
||||
#### Scenario: Итог со знаком
|
||||
- **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB
|
||||
- **THEN** итог группы равен +100.05 RUB, а обе записи видны отдельными строками со своими знаками
|
||||
|
||||
#### Scenario: Нулевой итог
|
||||
- **WHEN** группа содержит +150.10 RUB и −150.10 RUB
|
||||
- **THEN** заголовок показывает итог 0 RUB без знака направления, а обе записи видны со своими знаками
|
||||
|
||||
#### Scenario: Группа с записью без денежного изменения
|
||||
- **WHEN** у одной из записей группы нет денежного изменения
|
||||
- **THEN** заголовок группы не показывает итог, а записи остаются видны отдельными строками
|
||||
|
||||
#### Scenario: Запись с нулевой суммой скрывает итог
|
||||
- **WHEN** группа содержит запись +100 RUB и запись с денежным изменением 0 RUB
|
||||
- **THEN** заголовок группы не показывает итог, а запись с нулём обозначена как запись без движения денег
|
||||
|
||||
#### Scenario: Итог скрыт при неполном периоде
|
||||
- **WHEN** журнал в режиме «Все счета» показывает группу, а период обозначен как `partial`
|
||||
- **THEN** заголовок группы не показывает итог, а записи группы остаются видны со своими суммами
|
||||
|
||||
### Requirement: All-accounts ledger grouping preserves order and limits
|
||||
Группа журнала MUST занимать позицию своей первой записи в серверном порядке; записи внутри группы и остальные строки MUST сохранять относительный серверный порядок. Группировка MUST NOT менять состав, фильтрацию, пагинацию или лимит обзора, который считается по записям. Группа собирается только из показанных записей; это осознанное исключение: её итог относится только к ним и не сопровождается пометкой о неполноте.
|
||||
|
||||
#### Scenario: Позиция группы в списке
|
||||
- **WHEN** серверный порядок записей A, B, C, где A и C образуют группу, а B не входит ни в одну группу
|
||||
- **THEN** список показывает группу (A, C) на месте A, затем B
|
||||
|
||||
#### Scenario: Группа на границе страницы
|
||||
- **WHEN** записи одной группы попадают на текущую и на ещё не показанную страницу журнала при подтверждённой полноте периода
|
||||
- **THEN** группа на текущей странице содержит только её показанные записи, итог равен их сумме, и заголовок не содержит пометки о неполноте
|
||||
|
||||
#### Scenario: Группировка в последних записях обзора
|
||||
- **WHEN** в режиме «Все счета» среди пяти последних записей есть две записи с одинаковым ключом
|
||||
- **THEN** блок обзора показывает те же пять записей, что и без группировки, две из них одной группой
|
||||
|
||||
@@ -87,8 +87,9 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
|
||||
|
||||
Страница Brokerage Account MUST предоставлять действие перехода на отдельную
|
||||
страницу «Будущие события». Эта страница MUST показывать все полученные события
|
||||
компактным списком без фильтров, пагинации и суммарного итога, с датой, типом,
|
||||
инструментом и прогнозной суммой в исходной валюте, если она известна. Страница
|
||||
компактным списком без фильтров, пагинации и суммарного итога списка, с датой, типом,
|
||||
инструментом и прогнозной суммой в исходной валюте, если она известна. Прогнозный итог
|
||||
визуальной группы режима «Все счета» не является суммарным итогом списка. Страница
|
||||
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
|
||||
быть неполным из-за доступности данных источника. Это предметное ограничение
|
||||
прогноза MUST NOT превращаться в отчёт о технической синхронизации.
|
||||
@@ -192,3 +193,56 @@ continue to use the existing URL-backed period behavior.
|
||||
a range whose `from` date is after its `to` date
|
||||
- **THEN** the page applies the default seven-calendar-date range
|
||||
- **AND** invalid `from` and `to` parameters are removed from the URL
|
||||
|
||||
### Requirement: All-accounts upcoming events group same-day events of one instrument
|
||||
В режиме «Все счета» страница «Будущие события» и ближайшие события обзора MUST объединять в визуальную группу два и более показанных Future Cash Event с одинаковыми датой и видом даты (как на экране), identity Instrument и типом события, даже если они на одном счёте. События без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
|
||||
|
||||
#### Scenario: Прогноз купонов одной облигации на двух счетах
|
||||
- **WHEN** в режиме «Все счета» на одну дату ожидается купон по облигации на брокерском счёте 200 RUB и купон по той же облигации на ИИС 100 RUB
|
||||
- **THEN** они показаны одной группой с облигацией, типом «Купон» и прогнозным итогом 300 RUB, а под заголовком видны строки «брокерский счёт 200 RUB» и «ИИС 100 RUB»
|
||||
|
||||
#### Scenario: Разные типы или даты не объединяются
|
||||
- **WHEN** в режиме «Все счета» по одной облигации ожидаются купон и погашение на одну дату либо купоны на разные даты
|
||||
- **THEN** ни одна группа не содержит события разных типов или разных дат
|
||||
|
||||
#### Scenario: Событие без Instrument
|
||||
- **WHEN** в режиме «Все счета» на одну дату показаны два события одного типа без identity Instrument
|
||||
- **THEN** они показаны отдельными строками без группы
|
||||
|
||||
#### Scenario: Режим одного счёта не меняется
|
||||
- **WHEN** User открывает «Будущие события» одного Brokerage Account
|
||||
- **THEN** события показаны отдельными строками без группы, как до изменения
|
||||
|
||||
### Requirement: All-accounts upcoming event group shows each event and a forecast total
|
||||
Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком по каждой валюте без конвертации и округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена.
|
||||
|
||||
#### Scenario: Событие с неизвестной суммой в группе
|
||||
- **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы
|
||||
- **THEN** заголовок группы не показывает итог, а строка второго счёта обозначает сумму как неизвестную
|
||||
|
||||
#### Scenario: Оферта без сумм
|
||||
- **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах
|
||||
- **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт
|
||||
|
||||
#### Scenario: Прогнозный итог по нескольким валютам
|
||||
- **WHEN** группа содержит дивиденд 10 USD на одном счёте и дивиденд 200 RUB по той же бумаге на ту же дату на другом счёте
|
||||
- **THEN** заголовок показывает отдельные прогнозные итоги 10 USD и 200 RUB без конвертации
|
||||
|
||||
#### Scenario: Прогнозный итог скрыт при неполном прогнозе
|
||||
- **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами
|
||||
- **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами
|
||||
|
||||
#### Scenario: Группа дивидендов по дате реестра
|
||||
- **WHEN** группа содержит дивиденды, датированные датой реестра
|
||||
- **THEN** заголовок обозначает дату как дату реестра
|
||||
|
||||
### Requirement: All-accounts upcoming event grouping preserves order and limits
|
||||
Группа событий MUST занимать позицию своего первого события в порядке ответа; события внутри группы и остальные строки MUST сохранять относительный порядок ответа. Группировка MUST NOT менять состав, окно, порядок по дате или лимит обзора, который считается по событиям; группы собираются только из показанных событий.
|
||||
|
||||
#### Scenario: Группировка в ближайших событиях обзора
|
||||
- **WHEN** в режиме «Все счета» среди пяти ближайших событий есть два события с одинаковым ключом
|
||||
- **THEN** блок обзора показывает те же пять событий, что и без группировки, два из них одной группой
|
||||
|
||||
#### Scenario: Позиция группы событий
|
||||
- **WHEN** порядок ответа содержит события A, B, C, где A и C образуют группу, а B не входит ни в одну группу
|
||||
- **THEN** список показывает группу (A, C) на месте A, затем B
|
||||
|
||||
Reference in New Issue
Block a user