chore(release): 0.1.2 #17
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.1...v0.1.2) (2026-09-26)
|
||||||
|
|
||||||
|
### Specifications
|
||||||
|
|
||||||
|
* complete local frontend stack evolution ([ba76f6f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ba76f6fccc63b315e9ebf9d05ce181b283a6c094))
|
||||||
|
* complete persist-tbank-data implementation ([2f5814a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2f5814abdd52984bf1bcbd1d09b27ff4df3d7a25))
|
||||||
|
* define versioned deployment bundle ([7f40372](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f40372c3f67bb67cc77feaad17b61573c65f41e))
|
||||||
|
|
||||||
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.0...v0.1.1) (2026-09-16)
|
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.0...v0.1.1) (2026-09-16)
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Локальная архитектура реализована; независимая публикация отложена. Baseline исследования — frontend `3c99deb`, версия `0.3.0`,
|
||||||
|
2026-09-20. Требования — в
|
||||||
|
[proposal](proposal.md) и [delta](specs/frontend-design-system/spec.md).
|
||||||
|
|
||||||
|
В приложении уже работают React, TypeScript, Vite, Mantine, TanStack Query,
|
||||||
|
RHF/Zod, JSON-токены, общий адаптер темы, Storybook, Vitest/RTL/MSW,
|
||||||
|
dependency-cruiser и Knip. Они сохраняются. Другие production-потребители
|
||||||
|
общего UI или клиентского SDK в исследованном workspace не обнаружены.
|
||||||
|
|
||||||
|
На 2026-09-22 workspace-пакеты `@wealthledger/tokens`, `@wealthledger/ui` и
|
||||||
|
`@wealthledger/icons` используются локально как private packages. Это принятая
|
||||||
|
часть перехода. Описанные далее независимый release и Nexus остаются
|
||||||
|
предложением, отложенным до оценки пилота. Фактическое выполнение и ограничения
|
||||||
|
сохраняются в [tasks](tasks.md).
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** три небольшие границы поставки общего фундамента; единый каталог
|
||||||
|
источников; сохранение внешнего вида и предметных инвариантов; потребление
|
||||||
|
собранного пакета; ограниченные задачи, исполнимые по одной сессии.
|
||||||
|
|
||||||
|
**Non-Goals:** другой UI framework, второй transport/store/form stack,
|
||||||
|
универсальные CRUD/table/form engines, монорепозиторий всего WealthLedger,
|
||||||
|
mobile adapter, тёмная тема, массовый rewrite CSS и тестирование внутренностей
|
||||||
|
Mantine. Календарь платежей требует отдельного продуктового запроса.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Репозиторий и направления зависимостей
|
||||||
|
|
||||||
|
Рекомендация: **npm workspaces внутри существующего frontend**. Корневое
|
||||||
|
приложение сохраняет `src`, Vite entry, собственные providers и `private: true`.
|
||||||
|
Новые пакеты располагаются в `packages/tokens`, `packages/icons`, `packages/ui`.
|
||||||
|
Имена `@wealthledger/*` приняты для локальных packages; регистрация scope
|
||||||
|
и внешняя registry-поставка не подтверждены.
|
||||||
|
|
||||||
|
| Вариант | Польза | Цена в этом проекте | Решение |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Три workspace-пакета в frontend | Один lockfile, toolchain, Storybook и атомарный review источников с потребителем | Нужно явно расширить scopes проверок; локальный link может скрыть дефект поставки | Выбран; обязательна проверка packed consumer |
|
||||||
|
| Один отдельный design-system repo с тремя пакетами | Отдельный owner, release cadence, независимые потребители | Новый CI/catalog/install, синхронизация PR и доступа к registry при одном приложении | Отложить до второго независимого production consumer либо отдельного владельца/release cadence |
|
||||||
|
| Три отдельных Git repo | Полностью независимые права/жизненные циклы | Три workflow и совместимость без подтверждённой выгоды | Не выбирать |
|
||||||
|
| Только существующий shared | Уже работает, нет стоимости публикации | Не даёт запрошенной границы версионируемой поставки | Сохраняется до готовности package foundations, не конечная цель |
|
||||||
|
|
||||||
|
Направление стрелки — «потребляет»; циклов нет:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frontend app / единственный Storybook
|
||||||
|
├─ @wealthledger/ui ── @wealthledger/tokens
|
||||||
|
│ └─ Mantine + React (peers у ui)
|
||||||
|
├─ @wealthledger/icons ── React (peer)
|
||||||
|
└─ entities/features/pages → shared/api, domain models, i18n
|
||||||
|
|
||||||
|
tokens → нет runtime dependencies
|
||||||
|
ui → нет icons dependency до появления реального общего consumer
|
||||||
|
icons → нет ui/tokens/Mantine/router/API dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Общие компоненты принимают уже подготовленные строки и slots. `EntryCard`
|
||||||
|
принимает `ReactNode` и не знает, какой пакет рисует icon/action. Domain выбирает
|
||||||
|
цвет по смыслу и форматирует деньги; UI только отображает переданное значение.
|
||||||
|
|
||||||
|
### 2. Публичная поверхность и сборка
|
||||||
|
|
||||||
|
Имена ниже в колонке «Цель» — принятые публичные exports локальных пакетов.
|
||||||
|
Внутренние `src`, `dist/*` и application alias `@/` не являются package API.
|
||||||
|
|
||||||
|
| Пакет | Цель / entrypoints | Состав поставки | Зависимости |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| tokens | `@wealthledger/tokens/tokens.json` | Доверенный JSON v1; JS, CSS, React-адаптер и `.d.ts` на старте не нужны | Нет runtime dependencies |
|
||||||
|
| icons | Именованные `AddIcon`, `EditIcon`, `DeleteIcon` из `@wealthledger/icons` | ESM JS + declarations; CSS только если переносится собственная анимация | React peer + external; React DOM не используется |
|
||||||
|
| ui | `@wealthledger/ui`: `StateView`, `LoadingSkeleton`, `EntryCard` и их необходимые props types; `@wealthledger/ui/theme`: `theme`, `colorRoles`, `cssVariablesResolver`; `@wealthledger/ui/styles.css` | ESM JS + declarations + собранный CSS, включая CSS Modules | React/React DOM и используемые Mantine core/hooks — peers/external; tokens — точная dependency |
|
||||||
|
|
||||||
|
`designTokens` нужен foundations story: адаптер может сохранить существующий
|
||||||
|
одноимённый export из `ui/theme`, который ссылается на JSON, а не копирует его.
|
||||||
|
JSON остаётся отдельно доступным без Mantine. Не добавлять JS-обёртку в tokens
|
||||||
|
только ради импорта JSON. Не выдавать non-web adapter за готовый.
|
||||||
|
|
||||||
|
Использовать установленный Vite для library build ui/icons и TypeScript для
|
||||||
|
declarations. Один `tsc` недостаточен для CSS Modules `EntryCard`. Формат — ESM;
|
||||||
|
CJS/SSR build не нужен текущему Vite SPA. Настройки library build ещё предстоит
|
||||||
|
проверить на зафиксированной Vite 8.3.0; готовых конфигураций здесь нет.
|
||||||
|
Публикуемые `files`/`exports` ограничивают поверхность. CSS явно включается в
|
||||||
|
side effects; JS импорт сам не устанавливает provider и не читает приложение.
|
||||||
|
Library build использует отдельную конфигурацию нужных plugins, не наследует
|
||||||
|
app Vite config целиком: PWA/manifest/service worker и dev API proxy не должны
|
||||||
|
попасть в поставку библиотеки.
|
||||||
|
|
||||||
|
Exports указывают на собранные artifacts, поэтому обычные local/CI entrypoints
|
||||||
|
сначала собирают packages, затем выполняют consumer typecheck/tests/build.
|
||||||
|
Сейчас `verify` начинает проверки до app build; этот порядок потребуется
|
||||||
|
дополнить при 2.2. Acceptance начинается на чистом checkout после `npm ci`,
|
||||||
|
без старого package dist/cache. Достаточно последовательности npm scripts,
|
||||||
|
без собственного build orchestrator.
|
||||||
|
|
||||||
|
Docker — отдельный потребитель workspace: нынешний `Dockerfile` копирует только
|
||||||
|
root manifests перед `npm ci`, а `.dockerignore` не допускает packages.
|
||||||
|
В 2.1–2.2 нужны точечные manifests/source/build-config allowlists, установка
|
||||||
|
с workspace manifests и package build перед consumer. Host node_modules,
|
||||||
|
готовый локальный dist и реальные `.env` в контекст не попадают. Development
|
||||||
|
и production targets проверяются отдельно. В текущем Compose монтируются
|
||||||
|
только app src/public: изменения packages сначала требуют rebuild dev image.
|
||||||
|
Отдельный package bind-mount/watch в Compose отложен до фактической потребности;
|
||||||
|
host development может использовать package watch без изменения root repo.
|
||||||
|
|
||||||
|
Сначала peer ranges узкие, по проверенной комбинации React 19.3.0 и Mantine
|
||||||
|
9.6.1; расширять их только после проверки потребителя. React/React DOM
|
||||||
|
devDependencies для разработки не должны попадать в bundle. Проверять
|
||||||
|
`npm ls react react-dom @mantine/core` и содержимое bundle/packed consumer;
|
||||||
|
`dedupe` не заменяет корректную упаковку. Две несовместимые версии Mantine
|
||||||
|
также нельзя скрыть локальным symlink.
|
||||||
|
|
||||||
|
### 3. Токены: использовать уже существующий JSON
|
||||||
|
|
||||||
|
В [tokens.json](../../../../frontend/packages/tokens/tokens.json) уже есть
|
||||||
|
переносимые значения и описания. **Не менять формат v1 ради упаковки.**
|
||||||
|
`primitive → semantic → component` — модель владения и ссылок, а не повод
|
||||||
|
немедленно переименовать все ключи в новый стандарт.
|
||||||
|
|
||||||
|
| Уровень | Текущая основа и целевое правило |
|
||||||
|
| --- | --- |
|
||||||
|
| Primitive | `color.base`, `color.palettes`, `typography`, `spacing.values`, `radius.values`, `shadows.values`, `breakpoints.values`; исходные эффективные значения сохраняются |
|
||||||
|
| Semantic | Существующие `color.roles.*` с `color` + `shade` ссылаются на primitive; `primary` с `shade: null` сохраняет разрешение через primaryShade |
|
||||||
|
| Component | Пока новые не нужны. Добавлять только именованный совместно управляемый контракт реального общего компонента, если semantic role недостаточно; ссылаться на semantic, затем primitive при отсутствии смысловой роли |
|
||||||
|
|
||||||
|
Имена описывают роль (`surface`, `border`, `positive`), а не экран или случайный
|
||||||
|
цвет. Ссылки однонаправленные и разрешимые; обратных ссылок из primitive на
|
||||||
|
semantic/component нет. Новый color role включает назначение и ограничение
|
||||||
|
применения. Семантический положительный цвет не вычисляет знак финансовой суммы.
|
||||||
|
|
||||||
|
Сохранить единицы: размеры/тени в px адаптируются существующими `rem`/`rgba`,
|
||||||
|
breakpoints в em не масштабируются, line-height без единицы. Имеющиеся
|
||||||
|
[theme tests](../../../../frontend/packages/ui/src/theme.test.ts) защищают
|
||||||
|
именно преобразования и ссылки, не каждое значение JSON.
|
||||||
|
|
||||||
|
Общие CSS-роли `--wl-color-*` производит прежний `CSSVariablesResolver`.
|
||||||
|
Spacing/radius/type scales продолжают поступать в Mantine theme и его CSS
|
||||||
|
variables. Статическая генерация второго `tokens.css` не нужна: появился бы
|
||||||
|
ещё один адаптер и риск расхождения scale. JSON экспортируется без сборочного
|
||||||
|
token engine; Style Dictionary, DTCG-converter и runtime schema отложены до
|
||||||
|
конкретного внешнего потребителя/формата. Источник остаётся доверенным файлом,
|
||||||
|
не внешним пользовательским документом.
|
||||||
|
|
||||||
|
Светлая схема сохраняется. `primaryShade.dark` не означает готовую dark theme.
|
||||||
|
Бизнес-коды, суммы/валюты, московские даты, pagination limits, brand gradients
|
||||||
|
`BankCardFace` и случайная геометрия SVG не превращаются в глобальные токены.
|
||||||
|
Первое применение semantic roles — точные эквиваленты gray-3/gray-0/green-6/
|
||||||
|
red-6 в `EntryCard`; перенос и эта правка идут отдельно.
|
||||||
|
|
||||||
|
### 4. Mantine, тема и providers
|
||||||
|
|
||||||
|
`Stack`, `Group`, `Button`, `Paper`, `ActionIcon`, поля, Modal, Dates/Charts
|
||||||
|
остаются прямыми импортами Mantine. Изменение defaults решается темой.
|
||||||
|
Wrapper нужен только собственному контракту: busy region у `LoadingSkeleton`,
|
||||||
|
empty/error composition у `StateView`, layout/image fallback/actions у `EntryCard`.
|
||||||
|
|
||||||
|
App владеет MantineProvider, DatesProvider, QueryClientProvider, AuthProvider,
|
||||||
|
Router и i18n. Storybook имеет свой Mantine/Dates/i18n runtime с той же темой.
|
||||||
|
UI не создаёт `WealthLedgerProvider`, не импортирует локализацию продукта и
|
||||||
|
не включает app providers. `ui/styles.css` содержит существующую визуальную
|
||||||
|
основу и стили общих компонентов; app/preview подключают его явно после
|
||||||
|
Mantine CSS. Продуктовые route transitions и bank-card styles остаются в app.
|
||||||
|
|
||||||
|
Theme Modal transition и глобальная CSS animation сейчас сосуществуют.
|
||||||
|
Их визуальный конфликт не доказан. После выделения выбрать один механизм
|
||||||
|
открытия по browser comparison, предпочтительно уже настроенный Mantine
|
||||||
|
transition; нельзя удалить второй механизм на основании одного сходства кода.
|
||||||
|
|
||||||
|
### 5. Иконки и движение
|
||||||
|
|
||||||
|
Начальный набор — реально повторяющиеся plus/pencil/trash из Card/Cards views.
|
||||||
|
Имена `AddIcon`, `EditIcon`, `DeleteIcon` — предлагаемая поверхность, не найденные
|
||||||
|
exports. Размер задаёт `size` (default 24), геометрия сохраняет `viewBox`, цвет
|
||||||
|
получается через `currentColor`. Существующие размеры и strokeWidth 1.5/2
|
||||||
|
сохраняются у callers; выравнивание рисунка не совмещается с переносом.
|
||||||
|
|
||||||
|
Канонический исходник SVG для небольшого набора — **SVG-разметка в именованных
|
||||||
|
React-компонентах** `packages/icons/src/svg/<Name>.tsx`. Это один редактируемый
|
||||||
|
источник, без параллельных ручных `.svg` и `.tsx`. Отдельные raw SVG-файлы в
|
||||||
|
первой поставке не публикуются: non-React consumer пока отсутствует. Если
|
||||||
|
потребуются исходные `.svg` как самостоятельный продуктовый артефакт, перенести
|
||||||
|
канон в `src/svg/*.svg` и сравнить штатный SVGR CLI с ручным поддержанием;
|
||||||
|
не писать собственный генератор. Сейчас ни dependency, ни pipeline для этого
|
||||||
|
не нужны. JS/types несут SVG-компоненты, это явно отражено в составе поставки.
|
||||||
|
|
||||||
|
По умолчанию рисунок декоративный (`aria-hidden`, без фокуса). У standalone
|
||||||
|
meaningful icon потребитель задаёт доступное имя и image semantics. У
|
||||||
|
`ActionIcon` имя остаётся на действии; SVG не дублирует его. Общий пакет не
|
||||||
|
выбирает подпись «редактировать операцию» и не получает DTO.
|
||||||
|
|
||||||
|
`BankOperationIcon` и `operation-appearance.ts` остаются в entity: mapping
|
||||||
|
category/kind → рисунок/цвет/анимация — бизнес-логика. Собственная геометрия и
|
||||||
|
движение рисунка могут перейти в icons позже при реальном reuse. Hover/focus
|
||||||
|
trigger по сценарию, route transitions, bank/card state остаются приложению.
|
||||||
|
Существующий `bind-icon-animation` переиспользуется, если переносится тот же
|
||||||
|
animation contract; он не копируется в пакет ради трёх статичных иконок.
|
||||||
|
Все переносимые движения обязаны сохранять reduced-motion поведение.
|
||||||
|
|
||||||
|
### 6. Каталог источников компонентов
|
||||||
|
|
||||||
|
Порядок поиска: **существующие компоненты приложения → собственная библиотека
|
||||||
|
→ Mantine → уже разрешённые dependencies → обоснованное новое решение**.
|
||||||
|
Новый компонент появляется у владельца реальной ответственности, а не потому,
|
||||||
|
что текущий компонент отсутствует в Storybook. Таблица — компактный маршрут,
|
||||||
|
не каталог каждого JSX-узла. Номера обозначают этапы в [tasks](tasks.md).
|
||||||
|
|
||||||
|
| Назначение | Текущий источник | Целевой источник | Правило применения | Этап |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Layout, buttons, inputs, dialogs | `@mantine/core`, `@mantine/dates` | Те же пакеты | Прямые imports; variant/default через theme, не WL-копия каждого primitive | 1, 2 |
|
||||||
|
| Цветовые роли, шкалы | `shared/theme/tokens.json` | `tokens/tokens.json` + адаптер `ui/theme` | JSON — значения, адаптер — Mantine, бизнес-смысл — consumer | 2 |
|
||||||
|
| Тема/общие стили | `shared/theme` | `ui/theme`, `ui/styles.css` | Одна тема App/Storybook; providers у runtimes | 2 |
|
||||||
|
| Busy skeleton region | `shared/ui/LoadingSkeleton` | `ui` | Label и region общие, skeleton shapes принадлежат странице | 3.1 |
|
||||||
|
| Empty/error | `shared/ui/StateView` | `ui` | Строки/action props; query/error classification вне UI | 3.1 |
|
||||||
|
| Строка с изображением и значением | `shared/ui/EntryCard` | `ui` | Готовые strings/slots, fallback и responsive actions; без money parsing | 3.2–3.3 |
|
||||||
|
| Plus/edit/delete | Inline SVG Card/Cards views | `icons` | Named imports; label и disabled у ActionIcon | 4 |
|
||||||
|
| Навигация и active route | `shared/ui/AppShell` | `app/ui` | Product shell; не публичный компонент ui-пакета | 1.1 |
|
||||||
|
| Карта и её операции | `entities/bank-card` | Там же | `BankCardFace`, `BankOperationIcon`, `BankSummaryView`, `BankCardFields`, `BankFeedback` — domain | Без переноса |
|
||||||
|
| Период банковских данных | `entities/bank-card/BankPeriodPicker`, model/period | Там же | Draft/URL/query semantics сохраняются; не универсальный date service | 5, без extraction |
|
||||||
|
| Период дохода | `pages/account-income/.../IncomePeriodForm.view` | Та же page; pure helpers в её model | Изолировать story от VM, сохранить validation/submit | 5.3 |
|
||||||
|
| Позиции/структура портфеля | `pages/account/ui/components/PositionCard.view`, `PortfolioStructure.view` | Там же | Page-local композиция допустима; не переделывать под EntryCard | Без переноса |
|
||||||
|
| Недавние события | `entities/brokerage-event` + Account page | Там же; общий EntryCard из ui | Первый продуктовый пилот, финансовое представление у entity | 3.2 |
|
||||||
|
| Общие instrument details | `features/instrument-details` | Там же | Реальный reuse stocks/bonds; typed presentation contract отдельно от общего UI | Без переноса |
|
||||||
|
|
||||||
|
Последние непакетные источники выбираются через существующие `index.ts`.
|
||||||
|
В таблице обозначения `shared/ui/...` описывают файлы до миграции; это не разрешение
|
||||||
|
на deep import. Текущие владельцы указаны в колонке цели.
|
||||||
|
|
||||||
|
### 7. Предметный UI, API и конечные словари
|
||||||
|
|
||||||
|
Рекомендация: оставить domain UI в `entities/features/pages`. Второй
|
||||||
|
производственный consumer, которому нужен тот же стабильный domain contract,
|
||||||
|
и отдельный release owner — проверяемое условие рассмотрения business package.
|
||||||
|
Storybook не считается таким потребителем. Повторная карточка может потребовать
|
||||||
|
переноса page → entity внутри приложения, но не нового npm-пакета.
|
||||||
|
|
||||||
|
`shared/api` сохраняет snapshot, generated types, typed transport и auth-aware
|
||||||
|
boundary. Генерируются **типы**, а не весь handwritten client. Query keys,
|
||||||
|
normalization, freshness и invalidation остаются в entity/feature owner;
|
||||||
|
не входят ни в generated artifact, ни в ui. Отдельный `api-client` сейчас
|
||||||
|
потребовал бы отделить Vite environment, auth/retry lifecycle и query policy без
|
||||||
|
второго потребителя. Вернуться к вопросу при втором независимом приложении.
|
||||||
|
|
||||||
|
Backend остаётся единственным editable HTTP source. Frontend получает
|
||||||
|
предоставленный snapshot, локально генерирует и сравнивает declarations;
|
||||||
|
соседний backend checkout не становится входом CI. DTO aliases из generated
|
||||||
|
`paths` сохраняются. UI form models допустимы, когда отличаются от transport.
|
||||||
|
|
||||||
|
Для frontend собственных конечных словарей — принятый `as const` + union:
|
||||||
|
`enum` несовместим с текущим `erasableSyntaxOnly`. Набор operation kinds можно
|
||||||
|
свести у bank entity и проверить через generated `BankOperation['kind']`;
|
||||||
|
не объявлять вторую DTO enum. Wire literals сохраняются. `ProblemDetailsDto.code`
|
||||||
|
остаётся `string`, unknown code получает fallback; нельзя объявлять закрытый
|
||||||
|
список ошибок. Общий `apiErrorCode` извлекает code, одинаковое recovery
|
||||||
|
классифицируется ближайшим допустимым owner без обратных FSD imports.
|
||||||
|
|
||||||
|
### 8. Политика библиотек
|
||||||
|
|
||||||
|
Версии закреплены в frontend package manifests и lockfile. Решения ниже
|
||||||
|
не означают новой установки или обновления.
|
||||||
|
|
||||||
|
| Область | Сейчас | Решение и причина | Этап |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Build/React/Router | Vite, TS, React Compiler, React Router | Сохранить; расширить имеющийся build для library artifacts | 2 |
|
||||||
|
| Server/local state | TanStack Query / React state / URL | Сохранить owners; Redux из transitive deps не делает его app store | Все |
|
||||||
|
| Forms/validation | RHF, resolvers, Zod | Сохранить; vocabulary у entity, controller/view банковской формы отдельно | 5.2, 5.4 |
|
||||||
|
| UI/charts | Mantine; Recharts/react-is обеспечивают peer graph charts | Сохранить, не удалять peer deps из-за отсутствия direct imports | Все |
|
||||||
|
| Styles/условные классы | CSS Modules, template/ternary, data-* | Сохранить один этот подход; clsx/classnames не вводить без сложности, которой здесь нет | 3–5 |
|
||||||
|
| Icons | Inline SVG | Унифицировать повторяющиеся общие рисунки в малом собственном наборе | 4 |
|
||||||
|
| Motion | motion для routes, CSS/native для рисунков | Сохранить по разным владельцам; проверить дублирование Modal отдельно | 5.1 |
|
||||||
|
| Dates | Day.js + Mantine Dates + native Date/Intl | Сохранить роль каждого; Moscow/ISO policy остаётся domain/page | 5.3 |
|
||||||
|
| Money | String + BigInt cents, display formatters | Сохранить точность и unknown≠zero; decimal library не добавлять | Все |
|
||||||
|
| API/errors | openapi-fetch/react-query/typescript | Сохранить; не добавлять axios, второй generator или schema registry | Все |
|
||||||
|
| Tests | Vitest/RTL/MSW | Сохранить; `fireEvent` принят, user-event только transitively | 2–5 |
|
||||||
|
| Catalog | Storybook 10.6.0 | Один runtime, расширить glob и границы на packages | 2–4 |
|
||||||
|
| Quality | Biome, Knip, dependency-cruiser | Дополнить действующие scopes, не ставить Steiger/ещё один analyzer | 1–2 |
|
||||||
|
| Browser/visual CI | Принятого E2E runner/visual diff нет | Ручной browser proof пилота; Playwright — при повторяемой browser-only регрессии и владельце CI | Отложено |
|
||||||
|
| Release | release-it + native npm + Gitea | Использовать текущий подход, адаптировать workspace releases; Changesets/Lerna не нужны для трёх пакетов | 6 |
|
||||||
|
|
||||||
|
### 9. Предлагаемое уточнение conventions
|
||||||
|
|
||||||
|
Это предложения для будущих задач; действующие инструкции сейчас не меняются.
|
||||||
|
AGENTS остаётся коротким router, постоянный каталог переносится в подходящий
|
||||||
|
раздел existing UI convention/README после подтверждённого перехода.
|
||||||
|
|
||||||
|
| Владелец правила | Конкретное уточнение | Когда |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `.ai/architecture.md` | Package graph вне FSD; src public APIs; app shell; никакого package → app или same-layer обхода | 1–2 |
|
||||||
|
| `.ai/conventions/ui.md` | Каталог источников и search order; тема против wrapper; props-only общие компоненты | 2–3 |
|
||||||
|
| UI + Storybook | JSON/semantic roles, explicit CSS import, светлая схема, иконки/currentColor/reduced-motion | 2–4 |
|
||||||
|
| `.ai/conventions/data.md`, TypeScript/React | Generated vocabulary vs local const unions; errors открыты; package не владеет query state | 5.2 |
|
||||||
|
| Engineering/dependencies | Workspace direct deps, peers, exact lock, packed consumer, ограничения exports | 2, 6 |
|
||||||
|
| Storybook | Один runtime, colocated package/product stories, новые reachability scopes | 2–3 |
|
||||||
|
| Testing | Поведение → риск → минимальная boundary; package artifact contract; browser proof не равен build | 2–3, 6 |
|
||||||
|
|
||||||
|
### 10. Storybook и проверки
|
||||||
|
|
||||||
|
Один существующий frontend Storybook показывает `Design System` и `Product`.
|
||||||
|
Общие stories переезжают вместе с ui/icons; foundations читают опубликованный
|
||||||
|
JSON/адаптер. Product stories остаются рядом с entities/pages. Обновляются
|
||||||
|
только globs/aliases/check scopes, не создаётся отдельное приложение каталога.
|
||||||
|
Mantine/theme/Dates/i18n уже есть в preview; Query/Auth/API providers не нужны.
|
||||||
|
Synthetic props и callbacks сохраняются; MSW остаётся в Vitest, не в worker.
|
||||||
|
|
||||||
|
| Наблюдаемый контракт | Минимальный способ доказать |
|
||||||
|
| --- | --- |
|
||||||
|
| JSON → размеры/тени/semantic CSS, unscaled breakpoints | Перенести существующие theme unit tests; не проверять каждый literal |
|
||||||
|
| JS/types/CSS/JSON доступны вне workspace | Pack + чистый consumer с type/build/render и без source aliases |
|
||||||
|
| EntryCard image fallback, actions при focus/touch | Один небольшой RTL case на собственный fallback/действие, synthetic story и browser review |
|
||||||
|
| Деньги/version/idempotency/invalid input | Существующие targeted RTL/MSW tests формы; новые cases только если меняется соответствующая boundary |
|
||||||
|
| Query/auth/recovery | Имеющиеся MSW tests; no real backend; не тестировать это через stories |
|
||||||
|
| Keyboard/name/reduced motion/narrow layout | Выбранные собственные interactions в browser; примитивные гарантии Mantine не переписывать |
|
||||||
|
|
||||||
|
`build:catalog` не исполняет `play`; существующий `storybook.test.tsx` проверяет
|
||||||
|
лишь выбранные группы. Не обещать screenshot regression по факту сборки.
|
||||||
|
Snapshot всех компонентов и полный новый E2E stack не планируются.
|
||||||
|
|
||||||
|
Новые workspace-папки не охвачены автоматически: нынешние TS/Knip/deps/story
|
||||||
|
настройки главным образом ориентированы на `src`. Package type/build/lint,
|
||||||
|
dependencies и запрет app imports вводятся вместе с соответствующим пакетом.
|
||||||
|
Нельзя временно исключить packages целиком и назвать gate полным. Same-layer
|
||||||
|
rule сначала проверяется на текущем graph, затем включается для чистого scope;
|
||||||
|
legacy exception имеет имя, причину и задачу удаления. Existing coverage floors
|
||||||
|
не понижаются; перенос tests/coverage ownership не должен скрыть потерю сценария.
|
||||||
|
|
||||||
|
### 11. Локальная работа и потребление
|
||||||
|
|
||||||
|
Приняты приватные npm workspaces внутри frontend: один install/lockfile,
|
||||||
|
сборка пакетов и потребление через exports. Проверки охватывают приложение,
|
||||||
|
пакеты и Storybook. Собранные tarballs проверены вне workspace без source aliases;
|
||||||
|
эта проверка выявляет отсутствующие exports, типы, CSS и несовместимые peers.
|
||||||
|
|
||||||
|
Публикация, независимый SemVer, package release automation и настройка Nexus
|
||||||
|
не входят в текущий переход. Вернуться к ним можно при подтверждённой пользе
|
||||||
|
для независимого потребителя; тогда отдельно определить registry, права,
|
||||||
|
неизменность версий, совместимость и откат. Текущий app release не меняется.
|
||||||
|
|
||||||
|
## UI/UX Applicability and Package
|
||||||
|
|
||||||
|
**Applicability: N/A для нового визуального дизайна.** Цель — сохранение уже
|
||||||
|
существующего приложения, без новых экранов и рисунков. Новый макет/visuals и
|
||||||
|
hash registry не создаются. Для проверки переноса выбран следующий scope.
|
||||||
|
|
||||||
|
Пилот: `AccountPage → useAccountPageViewModel → useBrokerageEvents →
|
||||||
|
BrokerageEventsPreview → BrokerageEventCard → EntryCard`. Это read-only
|
||||||
|
preview выплат; `PositionCard` не использует EntryCard и не подходит для
|
||||||
|
переноса без изменения представления. Generic icons в пилоте отсутствуют и
|
||||||
|
мигрируют отдельно в Card/Cards.
|
||||||
|
|
||||||
|
Сохраняются loaded/loading/error/empty, missing amount как unknown, а не ноль,
|
||||||
|
registry-date/account metadata, валюта, logo fallback, длинные строки,
|
||||||
|
preview limit и ссылка полного списка. Сверяются desktop и viewport 375px,
|
||||||
|
доступный focus и отсутствие горизонтального overflow. У generic EntryCard
|
||||||
|
actions дополнительно проверяются fine/coarse pointer и reduced motion на
|
||||||
|
synthetic story. Текущий RecentEvents error-priority не меняется молча.
|
||||||
|
|
||||||
|
## Delivery Map
|
||||||
|
|
||||||
|
| Area | Status | Architectural outcome or N/A reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| frontend | affected | Workspaces tokens/icons/ui, ограниченная миграция, checks и package release integration |
|
||||||
|
| backend | N/A | Пакеты не меняют HTTP/auth/storage; замечание об errors вынесено в отложенный scope |
|
||||||
|
| specs | affected | Этот change и новые требования к поставке/иконкам; main specs обновятся только после принятой реализации |
|
||||||
|
| docs | affected | После поставки уточняются design overview и описание frontend container/release boundary |
|
||||||
|
|
||||||
|
## Solution Assessment
|
||||||
|
|
||||||
|
| Решение | Fit / зрелость / ограничения / стоимость | Выбор |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Существующие npm workspaces + Vite/TS | Стандартная упаковка; не требует нового Git repo; CSS/peers и published surface нуждаются в проверке | Да |
|
||||||
|
| Отдельный design-system repo | Ясная независимость ownership, но удваивает окружение каталога/CI при одном app | Позже по явному consumer/owner trigger |
|
||||||
|
| Style Dictionary или собственный token compiler | Полезны нескольким форматам/платформам; сейчас JSON + Mantine adapter уже решают задачу | Нет; runtime trust boundary не меняется |
|
||||||
|
| Ручные React SVG / SVGR pipeline | Несколько рисунков дешевле поддерживать в одном TSX source; SVGR нужен каноническим raw SVG exports | Ручные SVG-компоненты сейчас, без генератора |
|
||||||
|
| release-it/native npm / Changesets | Первый стек уже принят; второй полезен большому потоку независимых release contributions | Сохранить существующий app release; независимый package release отложен |
|
||||||
|
|
||||||
|
Новые внешние runtime dependencies не требуются. Риск supply chain сохраняется
|
||||||
|
под существующим security gate; peer/version checks и registry ACL нельзя
|
||||||
|
заменить предположением о готовом Nexus.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Workspace links скрывают отсутствующие exports/assets и конфликт React:
|
||||||
|
независимый packed consumer обязателен.
|
||||||
|
- Package graph расширяет проверяемую область: старый зелёный `deps src` не
|
||||||
|
подтверждает новые packages. Scopes расширяются при появлении владельца.
|
||||||
|
- CSS order/global reset влияют на всё приложение. Сначала перенос без
|
||||||
|
изменения значений, затем отдельно semantic roles и Modal consolidation.
|
||||||
|
- Общие исходники в frontend — осознанный выбор для одного потребителя;
|
||||||
|
публикация не означает уже существующий независимый каталог/команду.
|
||||||
|
- Изменение view/controller рядом с денежной формой рискованнее UI extraction:
|
||||||
|
это отдельная поздняя задача с сохранением tests version/idempotency.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Границы owners → package foundations → общие компоненты и read-only пилот →
|
||||||
|
иконки и точечное выравнивание → packed consumer → удаление совместимости.
|
||||||
|
Единственный подробный backlog и зависимости находятся в [tasks](tasks.md).
|
||||||
|
|
||||||
|
Во время перехода `shared/theme` и три старых UI entrypoints могут быть тонкими
|
||||||
|
re-exports package API. Они не содержат второй JSON, CSS или реализации.
|
||||||
|
Новые consumers используют package paths; добавлять exports/логику в facade
|
||||||
|
запрещено. Удаление — когда поиск оставшихся runtime/test/story imports даёт
|
||||||
|
ноль и app/catalog/packed consumer прошли gates. Старые inline icons живут до
|
||||||
|
своего consumer batch, но новые копии извлечённых рисунков не добавляются.
|
||||||
|
|
||||||
|
### Rollback Contract
|
||||||
|
|
||||||
|
| Field | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Status | required: меняются локальные imports, сборка и владельцы компонентов |
|
||||||
|
| Trigger | Нет exports/CSS/types, peer conflict, несовместимое представление либо failed pilot/recovery |
|
||||||
|
| Actions | Вернуть согласованный diff приложения, packages, imports и lockfile; при выпуске приложения использовать прежний app image |
|
||||||
|
| Data safety | API/storage/money wire format не меняются; не откатывать чужие продуктовые изменения и не сбрасывать пользовательские данные |
|
||||||
|
| Verification | Повторить соответствующий consumer gate и пилот на прежней версии; проверить восстановление согласованного состояния приложения и packages |
|
||||||
|
|
||||||
|
### Осознанно отложенное
|
||||||
|
|
||||||
|
| Находка/возможность | Причина и условие возврата |
|
||||||
|
| --- | --- |
|
||||||
|
| F8: RecentEvents error приоритетнее cached data | Реальное отличие от общего recovery convention. Не менять при extraction; отдельная product recovery задача после согласования expected stale/error UX, с refetch-error regression test |
|
||||||
|
| F9: ProblemDetails errors schema/example и validation mapping | Отдельная backend contract задача: metadata + HTTP behavior test, затем предоставленный snapshot; не основание вручную исправлять frontend DTO |
|
||||||
|
| F12: Основная frontend-quality-guardrails spec требует initial 500 kB и baseline lazy chunks, а действующий verify не содержит такого gate | Отдельно согласовать актуальность сохранённого требования и способ enforcement. Не считать Vite warning блокирующим budget gate; package migration сравнивает фактический размер сборки, не заявляя выполнения отсутствующей проверки |
|
||||||
|
| Единый recovery classifier всех investment errors | Одинаковые codes ещё не доказывают одинаковый UX; объединять только подтверждённый общий recovery без нарушения FSD |
|
||||||
|
| Business/API packages, dark/mobile, raw SVG pipeline, Playwright/visual service | Нет принятого второго consumer или значимого browser-only regression scope |
|
||||||
|
|
||||||
|
## Решения для подтверждения перед зависимыми действиями
|
||||||
|
|
||||||
|
1. Публичные имена `@wealthledger/tokens`, `@wealthledger/icons`, `@wealthledger/ui`,
|
||||||
|
scope ownership и видимость. Рекомендуется приватная поставка WealthLedger;
|
||||||
|
другое решение меняет доступ и release setup, не текущие данные.
|
||||||
|
2. Nexus npm endpoint, владелец publish/read credentials, политика неизменности
|
||||||
|
и хранения прошлых версий. Эти решения нужны только при отдельном запросе внешней публикации.
|
||||||
|
3. Расхождение F12 между сохранённой spec и текущими quality-командами:
|
||||||
|
рекомендуемый default — сохранять требование до отдельного решения владельца,
|
||||||
|
не ослаблять его и не объявлять выполненным. Автоматизация либо корректировка
|
||||||
|
требования оформляются отдельным ограниченным scope после уточнения причины.
|
||||||
|
|
||||||
|
Остальное — аргументированные defaults данного предложения, а не список
|
||||||
|
неразрешённых вопросов. Перед реализацией нужен отдельный запрос пользователя;
|
||||||
|
workspaces внутри frontend уже реализованы, независимые версии остаются отложенными.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
У WealthLedger уже есть общий визуальный фундамент, каталог компонентов и
|
||||||
|
типизированное взаимодействие с сервером. Следующий шаг — сделать границы
|
||||||
|
переиспользования и поставки этого фундамента явными: участник должен быстро
|
||||||
|
находить подходящее решение, а приложение — получать проверенную совместимую
|
||||||
|
версию без копирования реализаций.
|
||||||
|
|
||||||
|
Исследование текущего frontend выявило конкретные незавершённые границы:
|
||||||
|
повторяющиеся рисунки иконок, обход существующих цветовых ролей, продуктовую
|
||||||
|
навигацию в общем UI и неполную автоматизацию архитектурных ограничений.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
**Статус: локальная реализация и итоговая проверка завершены.**
|
||||||
|
Локальные границы приложения и private workspace-пакеты реализованы;
|
||||||
|
проверенные пункты и оставшиеся ограничения перечислены в [tasks](tasks.md).
|
||||||
|
Независимое версионирование и публикация через Nexus отложены до оценки пользы
|
||||||
|
пилота и не входят в требования этого перехода.
|
||||||
|
|
||||||
|
Первоначальный комплект плановых Markdown-файлов был подготовлен по запросу
|
||||||
|
пользователя в одной сессии. Последующие разрешения касались ограниченных
|
||||||
|
этапов; они не разрешают публикацию, deployment или archive.
|
||||||
|
|
||||||
|
- Выделить используемый общий визуальный фундамент в приватные workspace-пакеты,
|
||||||
|
сохранив внешний вид и работоспособность приложения между небольшими этапами.
|
||||||
|
- Установить единый порядок выбора компонентов и понятную границу между
|
||||||
|
общим представлением, предметным UI и сценариями приложения.
|
||||||
|
- Сделать повторно используемые собственные иконки доступными с единым
|
||||||
|
визуальным и доступным контрактом.
|
||||||
|
- Подтвердить переход существующим небольшим продуктовым срезом и проверкой
|
||||||
|
потребления собранных артефактов, а не только локальных исходников.
|
||||||
|
- Уточнить применимые инженерные правила после соответствующих изменений,
|
||||||
|
сохранить существующие проверки и поэтапно закрыть найденные пробелы.
|
||||||
|
|
||||||
|
Сохраняются денежная точность, обработка неизвестных значений, поведение
|
||||||
|
авторизации, запросов, форм, восстановления после ошибок и существующих страниц.
|
||||||
|
Предметные расчёты и серверные контракты не становятся частью общего UI.
|
||||||
|
|
||||||
|
Не входят редизайн, тёмная тема, мобильный адаптер, календарь обязательных
|
||||||
|
платежей, смена работающего стека, переустройство всех репозиториев и массовое
|
||||||
|
перемещение компонентов. Публикация, deployment, commit, push, PR и archive не разрешены.
|
||||||
|
Локальная реализация ограничена отдельно согласованными этапами.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
Нет: новая capability на каждый пакет или каталог не требуется.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `specs/frontend-design-system/spec.md`: добавить требования к потреблению
|
||||||
|
локальных пакетов общего фундамента и доступному контракту собственных
|
||||||
|
общих иконок. Существующие требования к единой теме и предметной независимости
|
||||||
|
сохраняются. [Delta](specs/frontend-design-system/spec.md).
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Frontend: поставка общей основы, выбор источников компонентов, ограниченная
|
||||||
|
миграция потребителей и развитие существующих проверок.
|
||||||
|
- Backend: производственный контракт и поведение не меняются; отдельное
|
||||||
|
замечание к описанию ошибок остаётся отложенным исследованием владельца API.
|
||||||
|
- Specs: этот change и delta; основные as-built требования пока не изменяются.
|
||||||
|
- Docs: описание текущего локального устройства обновлено в 7.2;
|
||||||
|
независимая registry-поставка не заявляется.
|
||||||
|
|
||||||
|
Решения, альтернативы и каталог источников — в [design](design.md), единственная
|
||||||
|
очередь работ и текущее состояние — в [tasks](tasks.md).
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Общий фундамент доступен через проверяемые локальные пакеты
|
||||||
|
|
||||||
|
Общий визуальный фундамент SHALL принадлежать приватным npm workspaces внутри
|
||||||
|
frontend и иметь явную публичную поверхность. Приложение SHALL использовать
|
||||||
|
пакеты через объявленные exports. Собранные артефакты MUST содержать модули,
|
||||||
|
типы, стили и данные согласно контракту каждого пакета и быть пригодными для
|
||||||
|
проверки вне рабочего дерева frontend. Общие пакеты MUST NOT требовать серверных
|
||||||
|
данных, авторизации, маршрутов или состояния WealthLedger. Совместимость пакетов
|
||||||
|
между собой и с приложением SHALL проверяться; workspace links MUST NOT быть
|
||||||
|
единственным подтверждением пригодности артефактов. Независимые релизы и
|
||||||
|
публикация в registry не входят в этот переход.
|
||||||
|
|
||||||
|
#### Scenario: Приложение потребляет собранную поставку
|
||||||
|
|
||||||
|
- **WHEN** потребитель устанавливает собранные артефакты в чистом окружении без
|
||||||
|
ссылок на рабочее дерево поставщика
|
||||||
|
- **THEN** публичные модули, типы, стили и данные доступны, общий компонент
|
||||||
|
отображается на заявленной теме без зависимостей от приложения поставщика
|
||||||
|
|
||||||
|
#### Scenario: Локальная разработка скрыла отсутствующий артефакт
|
||||||
|
|
||||||
|
- **WHEN** локальная ссылка разрешает импорт, отсутствующий в собранной поставке
|
||||||
|
- **THEN** поставка не признаётся готовой по результату одной локальной сборки;
|
||||||
|
потребление именно собранных артефактов должно быть подтверждено отдельно
|
||||||
|
|
||||||
|
#### Scenario: Приложение использует локальные пакеты
|
||||||
|
|
||||||
|
- **WHEN** frontend устанавливает зависимости из своего lockfile и собирается
|
||||||
|
- **THEN** приватные workspaces доступны через публичные exports без публикации
|
||||||
|
в registry; предметные компоненты и API-клиент остаются в приложении
|
||||||
|
|
||||||
|
### Requirement: Собственные общие иконки имеют доступный контракт
|
||||||
|
|
||||||
|
Общие иконки SHALL иметь предсказуемые размеры и возможность получать цвет
|
||||||
|
от потребителя, не определяя бизнес-смысл операции или состояние приложения.
|
||||||
|
Декоративная иконка MUST NOT создавать отдельную точку фокуса или повторять
|
||||||
|
доступное имя соседнего действия. Смысловая иконка без сопровождающего текста
|
||||||
|
MUST иметь доступное имя. Собственное движение иконки SHALL учитывать
|
||||||
|
предпочтение уменьшенного движения; выбор момента запуска по пользовательскому
|
||||||
|
сценарию SHALL оставаться у потребителя.
|
||||||
|
|
||||||
|
#### Scenario: Иконка находится в именованном действии
|
||||||
|
|
||||||
|
- **WHEN** собственная иконка дополняет кнопку с доступным именем
|
||||||
|
- **THEN** кнопка сохраняет имя и клавиатурное поведение, а рисунок декоративен
|
||||||
|
и не создаёт дополнительную точку фокуса
|
||||||
|
|
||||||
|
#### Scenario: Рисунок самостоятельно передаёт смысл
|
||||||
|
|
||||||
|
- **WHEN** иконка используется без текста и без именованного родительского
|
||||||
|
элемента как самостоятельное изображение
|
||||||
|
- **THEN** потребитель задаёт доступное имя, передающее её назначение
|
||||||
|
|
||||||
|
#### Scenario: Пользователь уменьшил движение
|
||||||
|
|
||||||
|
- **WHEN** включено предпочтение уменьшенного движения
|
||||||
|
- **THEN** необязательная анимация общей иконки не выполняется, а значение и
|
||||||
|
доступность действия сохраняются
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# План перехода frontend
|
||||||
|
|
||||||
|
**На 2026-09-22 выполнены и проверены 1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3** по отдельному
|
||||||
|
разрешению пользователя. Остальные задачи — предложения, не разрешение
|
||||||
|
на реализацию или публикацию. Результаты и ограничения приведены ниже.
|
||||||
|
Прочитать [proposal](proposal.md), затем нужное решение [design](design.md).
|
||||||
|
Здесь единственный backlog инициативы, отдельного roadmap нет.
|
||||||
|
|
||||||
|
## Дорожная карта и зависимости
|
||||||
|
|
||||||
|
```text
|
||||||
|
1.1 AppShell owner ────────┐
|
||||||
|
1.2 import enforcement ────┴─ 2.1 tokens/workspaces ─ 2.2 ui theme
|
||||||
|
│
|
||||||
|
3.1 common states
|
||||||
|
│
|
||||||
|
3.2 EntryCard + pilot ─ 3.3 roles
|
||||||
|
2.1 ─ 4.1 icons ─ 4.2 Card/Cards consumers
|
||||||
|
2.2 ─ 5.1 Modal review
|
||||||
|
1.2 ─ 5.2 operation vocabulary ─ 5.4 form boundary
|
||||||
|
1.2 ─ 5.3 income period helper boundary
|
||||||
|
|
||||||
|
3.3 + 4.2 + packed consumer proof ─ 7.1 retire facades
|
||||||
|
Выполненные локальные 1–5 ─ 7.2 текущее as-built описание
|
||||||
|
3.2–3.3 + 5.1–5.4 + 7.1–7.2 ─ 7.3 acceptance всей инициативы
|
||||||
|
```
|
||||||
|
|
||||||
|
1.1 и 1.2 независимы по смыслу, но могут менять общие конфиги: параллельные PR
|
||||||
|
согласуют diff. 5.1–5.4 не блокируют выпуск основы и не входят скрыто в пилот.
|
||||||
|
Иконки не нужны read-only пилоту RecentEvents. Пункты внешней публикации удалены из текущего плана по решению пользователя:
|
||||||
|
пакеты остаются приватными workspaces frontend. Остальные
|
||||||
|
задачи используют предложенные defaults после отдельного запроса реализации.
|
||||||
|
|
||||||
|
### Общие условия каждой будущей сессии
|
||||||
|
|
||||||
|
- Работать в repo владельца, сначала его AGENTS и только triggered conventions;
|
||||||
|
проверить Git status, текущий HEAD и чужие изменения. План привязан к
|
||||||
|
исследованным ревизиям, не к обещанию актуального remote develop.
|
||||||
|
- Перед Node-командой активировать местную `.nvmrc`: `nvm use --silent`.
|
||||||
|
Для code/dependency PR frontend требует `npm ci` и итоговый `npm run verify`;
|
||||||
|
targeted команды ниже дополняют этот gate, не заменяют его.
|
||||||
|
- Scope PR — одна задача. Не смешивать перемещение с новым поведением,
|
||||||
|
обновлением версий библиотек и исправлением соседнего продукта.
|
||||||
|
- Значимый тест начинается с наблюдаемого результата/риска; сохранять имеющиеся
|
||||||
|
regression tests. Не добавлять тест wrapper existence, Mantine internals
|
||||||
|
или snapshots всех вариантов ради переноса.
|
||||||
|
- По завершении повторить routing, проверить diff/новые файлы, Boy Scout review,
|
||||||
|
`git diff --check`, записать реально выполненные команды и ограничения.
|
||||||
|
Непройденная проверка не отмечается как выполненная задача.
|
||||||
|
|
||||||
|
### Реальные команды и будущие проверки
|
||||||
|
|
||||||
|
Сейчас доступны: `npm run deps`, `npm run deps:storybook`, `npm run knip`,
|
||||||
|
`npm run lint`, `npm run typecheck`, `npm run openapi:check`,
|
||||||
|
`npm run test:run -- <существующий path>`, `npm run test:coverage`,
|
||||||
|
`npm run build`, `npm run build:catalog`, `npm run verify`,
|
||||||
|
`npm run release:preview`. Их поведение исследовано, но они **не запускались**
|
||||||
|
в текущей Markdown-сессии. `typecheck` может записать tsbuildinfo;
|
||||||
|
tests/builds пишут cache/coverage/dist/catalog. `openapi:generate` меняет
|
||||||
|
tracked declarations и не нужен этой миграции.
|
||||||
|
|
||||||
|
Package build/declarations/check scopes и packed consumer проверены.
|
||||||
|
Независимый package release не входит в текущий план. Не
|
||||||
|
считать вымышленный `test:packages` или `release:packages` действующей командой.
|
||||||
|
Native `npm pack --dry-run`, `npm pack`, `npm ls` использованы для проверки пакетов. `release:prepare` приложения
|
||||||
|
создаёт commit; не использовать для пробного package release.
|
||||||
|
|
||||||
|
## 1. Уточнить владельцев и существующие ограничения
|
||||||
|
|
||||||
|
- [x] 1.1 [frontend] Перенести route-aware AppShell к владельцу приложения.
|
||||||
|
- **Covers:** `N/A` — техническое исправление F3; product shell не должен стать публичной частью ui, согласно design §1/4.
|
||||||
|
- **Verification:** `npm run deps`, `npm run typecheck`, `npm run knip`, существующие route/navigation tests, итоговый `npm run verify`; browser navigation/focus и active route.
|
||||||
|
- **Читать / область:** `.ai/architecture.md`, UI, `src/app/ui/AppShell.tsx` и CSS (до переноса — shared/ui), его callers в `app/routes`, связанные auth/route tests. Найти все imports до переноса.
|
||||||
|
- **Изменить / готово:** AppShell и принадлежащие ему navigation styles находятся в `app/ui` с допустимым public import. Labels, links, Outlet, transitions, responsive shell прежние; shared/ui не знает product routes. Коротко уточнён legacy пункт architecture.
|
||||||
|
- **Не входит / сохранить:** Новая навигация, новый wrapper shell, миграция всех SVG, auth policy. Никаких изменений URL и bootstrap states.
|
||||||
|
- **Откат:** Вернуть перенос и imports одним PR; данных/внешних artifacts нет. Зависимость: нет.
|
||||||
|
|
||||||
|
- [x] 1.2 [frontend] Закрыть same-layer imports действующим dependency-cruiser.
|
||||||
|
- **Covers:** `N/A` — enforcement уже принятого FSD profile, F4; не новая пользовательская возможность.
|
||||||
|
- **Verification:** Зафиксировать вывод существующего `npm run deps` до правки; после неё тот же gate отклоняет временный контролируемый forbidden import между двумя slices, чистый graph проходит; удалить пробный import, затем `npm run verify`.
|
||||||
|
- **Читать / область:** Architecture/imports, `dependency-cruiser.cjs`, реальные alias/relative imports в pages/features/entities; public API примеры account-operations и bank-card.
|
||||||
|
- **Изменить / готово:** Точные запреты соседних slices pages/features/entities, без запрета imports внутри одного slice и без ошибочного применения к shared segments. Если baseline выявит legacy, правило включается для чистой области с адресным исключением и условием удаления, без broad ignore.
|
||||||
|
- **Не входит / сохранить:** Новый analyzer, автоматический fixer, массовая раскладка FSD, запрет page-local composition. Не писать постоянный анализатор проверки анализатора.
|
||||||
|
- **Откат:** Вернуть только rule change, обязательный semantic review остаётся. Зависимость: нет.
|
||||||
|
|
||||||
|
## 2. Подготовить пакеты без изменения представления
|
||||||
|
|
||||||
|
- [x] 2.1 [frontend] Ввести workspace tokens и охват его контрактов.
|
||||||
|
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
|
||||||
|
- **Verification:** Будущая package-конфигурация: `npm pack --dry-run` показывает JSON; чистый временный consumer вне workspace читает JSON обычным parser; `npm ci`, расширенные существующие type/lint/Knip/deps scopes, `npm run verify`; чистые Docker builds development/production с новым workspace manifest и JSON.
|
||||||
|
- **Читать / область:** Engineering/dependencies, package/lock, TS/Knip/deps configs, `Dockerfile`, `.dockerignore`, `packages/tokens/tokens.json`, `shared/theme/theme.ts`, `theme.test.ts`; design §1–3/10. После 1.1–1.2; имена остаются предлагаемыми до принятия design.
|
||||||
|
- **Изменить / готово:** `packages/tokens` владеет тем же JSON v1; root app private; один npm workspace lock, прямые зависимости, ограниченные files/exports. Адаптер читает package JSON. Проверки явно видят packages и запрещают package → app. Docker получает workspace manifests перед npm ci и только нужные package inputs через узкую allowlist, без host node_modules/dist/.env.
|
||||||
|
- **Не входит / сохранить:** Новая schema, Style Dictionary, mobile/dark, преобразование всех токенов и CSS. Не поддерживать две редактируемые копии JSON; app appearance и все units прежние.
|
||||||
|
- **Откат:** Вернуть JSON/imports/manifest/lock как связанный diff; package ещё не опубликован.
|
||||||
|
|
||||||
|
- [x] 2.2 [frontend] Упаковать Mantine adapter и общие стили в ui.
|
||||||
|
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
|
||||||
|
- **Verification:** Существующие theme tests на новом owner; будущие Vite library build + declaration emit и packed consumer type/build/render без app alias; на чистом checkout после `npm ci`, без прежнего package dist/cache, `npm run verify` сначала собирает packages, затем проверяет consumer. `npm ls react react-dom @mantine/core`; отдельные чистые Docker development/production builds и render темы.
|
||||||
|
- **Читать / область:** `packages/ui/src/{theme.ts,styles.css,theme.test.ts}`, facade `shared/theme/index.ts`, `app/ui/App.tsx`, `.storybook/{main.ts,preview.tsx,dependency-cruiser.cjs}`, `vite.config.ts`, `vitest.config.ts`, package scripts, `Dockerfile`, `.dockerignore`, design §2–4. Зависимость: 2.1.
|
||||||
|
- **Изменить / готово:** `ui/theme`/`ui/styles.css`, dependency tokens, peers/external React/Mantine, CSS side effects, JS/d.ts/CSS в package. App/preview явно подключают styles и прежние providers. Package build предшествует всем проверкам потребителя в local/CI/Docker; scope tests/coverage следует owner. `shared/theme` только re-export. Host dev допускает package watch; в Docker изменение packages требует документированного rebuild dev image, без нового root orchestration.
|
||||||
|
- **Не входит / сохранить:** Новый global provider, новый Storybook app, component wrappers, Modal animation cleanup. Library build не наследует app PWA/service-worker/proxy plugins. Не менять light scheme, source values, CSS order и effective theme; сравнить фактический bundle size с baseline, не заявляя отсутствующий budget gate F12.
|
||||||
|
- **Откат:** Вернуть adapter/styles/imports и manifest/lock единым PR. Временный facade удаляется в 7.1, новые consumers его не расширяют.
|
||||||
|
|
||||||
|
## 3. Общие компоненты и продуктовый пилот
|
||||||
|
|
||||||
|
- [x] 3.1 [frontend] Перенести StateView и LoadingSkeleton с их stories.
|
||||||
|
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
|
||||||
|
- **Verification:** `npm run deps:storybook`, `npm run build:catalog`, существующие проверки StateView/LoadingSkeleton/composeStories в новом scope, будущий packed component render, `npm run verify`.
|
||||||
|
- **Читать / область:** Три UI-conventions темы, два `src/shared/ui` компонента/stories и callers, `src/test/storybook.test.tsx`, package ui. Зависимость: 2.2.
|
||||||
|
- **Изменить / готово:** Реализации и CSF stories у ui, один catalog glob включает package stories. Props без query/i18n/router; busy label/status и error alert/action сохраняются. Проверка Storybook reachability включает новый каталог. Если нужен facade, он только переэкспортирует.
|
||||||
|
- **Не входит / сохранить:** Перемещение page skeleton shapes, новый loading framework, полный набор stories для Mantine. No backend и реальные финансовые данные.
|
||||||
|
- **Откат:** Вернуть перенос; содержимое/поведение исходных components не менялось. Удаление facade — 7.1.
|
||||||
|
|
||||||
|
- [x] 3.2 [frontend] Перенести EntryCard и подтвердить пилот RecentEvents.
|
||||||
|
- **Статус 2026-09-22:** Реализация, 278 tests/full verify, Docker, packed consumer и browser desktop/375px прошли. Пользователь подтвердил успешную ручную проверку coarse pointer/reduced motion по предложенному чек-листу; browser-ограничение закрыто. CSS перенесён побайтово.
|
||||||
|
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
|
||||||
|
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx`, существующие ledger/CardPage cases использующих EntryCard; минимальный UI-test собственного image fallback при необходимости; `npm run deps:storybook`, `npm run build:catalog`, packed consumer render, `npm run verify`; browser сравнение 375px/desktop.
|
||||||
|
- **Читать / область:** `EntryCard.tsx`/CSS и все callers; `AccountPage.vm.ts::toRecentEvents`, `useBrokerageEvents`, `BrokerageEventsPreview`, `BrokerageEventCard` и их stories, design UI/UX scope. Зависимость: 3.1.
|
||||||
|
- **Изменить / готово:** Один EntryCard в ui с тем же CSS/props; entity пилота импортирует публичный ui package. В Storybook — существующие RecentEvents states и малая generic story длинного контента/image fallback/actions. Пилот реально проходит через API owner до прежнего presentation, без смены data flow. Остальные callers могут пока использовать re-export.
|
||||||
|
- **Не входит / сохранить:** Generic icons, PositionCard rewrite, исправление F8. Сохраняются unknown≠zero, currency/date/account, logo fallback, preview limit/full-list link; у других callers — sign и disabled placeholder/archive guards.
|
||||||
|
- **Откат:** Вернуть consumer import и extraction PR. Не вводить вторую реализацию EntryCard для пилота. Facade removal — 7.1.
|
||||||
|
|
||||||
|
- [x] 3.3 [frontend] Подключить существующие semantic roles внутри EntryCard.
|
||||||
|
- **Статус 2026-09-22:** Реализованы border/subtle/surface. Positive/negative уже были semantic (shade 9); neutral gray-7 оставлен без неточного сопоставления. npm ci/full verify (278 tests) и browser computed colors/narrow/focus прошли. Пользователь подтвердил ручную проверку coarse pointer/reduced motion по чек-листу; пункт закрыт.
|
||||||
|
- **Covers:** `N/A` — F1: применение уже существующей темы после структурного переноса, без нового UI behavior.
|
||||||
|
- **Verification:** Сравнить computed colors и выбранные EntryCard/RecentEvents states до/после; gray-3→border, gray-0→subtle эквивалентны; текущие positive/negative (shade 9) сохранены; background tint/white→surface проверен; `npm run verify` и browser narrow/focus/coarse-pointer/reduced-motion.
|
||||||
|
- **Читать / область:** ui EntryCard/CSS, tokens color.roles, ui/theme resolver, generic и product stories. Зависимость: 3.2.
|
||||||
|
- **Изменить / готово:** Прямые эквиваленты заменены semantic CSS variables; текущий вид и actions не меняются. Если эквивалентность конкретной пары не доказана, оставить её локальной и пояснить, не создавать токен автоматически.
|
||||||
|
- **Не входит / сохранить:** Новые palettes, card brand gradients, компонентный токен на каждое свойство, массовый CSS sweep.
|
||||||
|
- **Откат:** Вернуть только CSS/inline-role diff; package ownership остаётся.
|
||||||
|
|
||||||
|
## 4. Собственные общие иконки
|
||||||
|
|
||||||
|
- [x] 4.1 [frontend] Подготовить минимальный icons package.
|
||||||
|
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`; `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
|
||||||
|
- **Verification:** Будущие package build/declarations/pack consumer checks; synthetic decorative и meaningful icon examples с именем/размером/currentColor; `npm ls react`, `npm run deps:storybook`, `npm run verify`.
|
||||||
|
- **Читать / область:** Card/Cards inline plus/pencil/trash, `BankOperationIcon` как пример accessibility, `bind-icon-animation` только для границ ответственности, design §5. Зависимость: 2.1; после 2.2 можно переиспользовать package build pattern без общего самописного генератора.
|
||||||
|
- **Изменить / готово:** Именованные Add/Edit/Delete SVG-компоненты, один TSX source на рисунок, React peer/external, public types, ограниченные exports. Static package не тянет motion/UI/domain; декоративность по умолчанию и доступное meaningful использование документированы.
|
||||||
|
- **Не входит / сохранить:** SVG registry по строке, raw SVG+TSX дубли, SVGR/dependency, перенос категорий/навигационных анимаций. Сохранить geometry/stroke/size существующих рисунков, без редизайна.
|
||||||
|
- **Откат:** Удалить ещё не используемый package diff; до публикации внешнего контракта нет. Если 4.2 уже интегрирован, откатывать согласованно.
|
||||||
|
|
||||||
|
- [x] 4.2 [frontend] Перевести только повторяющиеся Card/Cards actions на icons.
|
||||||
|
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
|
||||||
|
- **Verification:** `npm run test:run -- src/pages/card/ui/CardPage.test.tsx`, `npm run test:run -- src/pages/cards/ui/CardsPage.test.tsx`, `npm run verify`; browser accessible names/keyboard и appearance выбранных Add/Edit/Delete actions.
|
||||||
|
- **Читать / область:** `pages/card/ui/CardPage.view.tsx`, `pages/cards/ui/CardsPage.view.tsx`, ActionIcon consumers и imports. Зависимость: 4.1.
|
||||||
|
- **Изменить / готово:** Повторяющиеся SVG заменены named imports; размеры/strokeWidth задаются явно, если раньше различались. Имя и disabled остаются на action; внутри SVG нет дополнительного focus. В изменённом scope больше нет старых копий этих рисунков.
|
||||||
|
- **Не входит / сохранить:** BankOperationIcon mappings, формы/mutations, поведение edit/delete и confirm dialogs, иной визуальный стиль.
|
||||||
|
- **Откат:** Вернуть caller diff; пакет может оставаться неиспользуемым до отдельного решения, опубликованную версию не удалять.
|
||||||
|
|
||||||
|
## 5. Адресные улучшения после основы
|
||||||
|
|
||||||
|
Эти задачи самостоятельны. Их незавершённость не оправдывает расширение
|
||||||
|
пакетного PR или утверждение, что изменено поведение формы в пилоте.
|
||||||
|
|
||||||
|
- [x] 5.1 [frontend] Проверить и при необходимости свести Modal animation к одному владельцу.
|
||||||
|
- **Статус 2026-09-22:** BankCardForm browser A/B показал различный lifecycle: CSS даёт mount entrance, Mantine — exit/callback. Production-код сохранён; причины документированы в UI convention. Пользователь подтвердил ручную проверку reduced motion, Escape/Отмена и возврата фокуса; пункт закрыт.
|
||||||
|
- **Covers:** `N/A` — F5, устранение подтверждённого дублирования механизмов без нового продукта.
|
||||||
|
- **Verification:** Browser comparison открытия/закрытия существующей банковской Modal: overlay, motion, focus return, reduced motion; relevant Modal/form tests и `npm run verify` только при code diff.
|
||||||
|
- **Читать / область:** ui theme Modal defaults, ui global styles, BankCardForm/OperationForm consumers. Зависимость: 2.2.
|
||||||
|
- **Изменить / готово:** Выбран один механизм либо документирован конкретный различный смысл обоих. При эквивалентности предпочтён Mantine transition и убран только лишний CSS. Наблюдение «видимых проблем нет» само по себе не требует нового кода.
|
||||||
|
- **Не входит / сохранить:** Новые timings, анимационная система, замена motion маршрутов. Keyboard/focus/closing rules неизменны.
|
||||||
|
- **Откат:** Вернуть animation-only diff; component API/data не меняются.
|
||||||
|
|
||||||
|
- [x] 5.2 [frontend] Свести runtime vocabulary банковских operation kinds у entity.
|
||||||
|
- **Covers:** `N/A` — часть F6, одна типизированная коллекция для существующих schema/choices без изменения wire contract.
|
||||||
|
- **Verification:** `npm run typecheck`, `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`, `npm run deps`, `npm run verify`; все четыре прежних вида и defaults сохранены.
|
||||||
|
- **Читать / область:** `entities/bank-card/api/queries.ts` и index, `OperationForm.tsx`/`.view.tsx`/`.types.ts`, TypeScript/data conventions. Зависимость: 1.2.
|
||||||
|
- **Изменить / готово:** Schema и choices используют один `as const` набор, проверенный относительно generated `BankOperation['kind']`. Не добавлен enum, DTO copy или cast неизвестного code; строки UI остаются i18n.
|
||||||
|
- **Не входит / сохранить:** Generic errors registry, backend enum generation, механическое именование каждого literal, controller refactor.
|
||||||
|
- **Откат:** Вернуть маленький vocabulary/import diff; значения wire прежние.
|
||||||
|
|
||||||
|
- [x] 5.3 [frontend] Отделить pure period helpers от AccountIncomePage VM.
|
||||||
|
- **Covers:** `N/A` — F7, устранение транзитивной зависимости presentation story от VM по design §6/10.
|
||||||
|
- **Verification:** Existing account-income targeted tests, `npm run deps:storybook`, `npm run build:catalog`, `npm run verify`; synthetic form submit/reset/error states без API/query providers.
|
||||||
|
- **Читать / область:** `AccountIncomePage.vm.ts`, `components/IncomePeriodForm.view.tsx`, их imports/tests, соседний page model и Storybook convention. Зависимость: 1.2.
|
||||||
|
- **Изменить / готово:** Только реально используемые pure date helpers у page model и оба callers используют их; полезная isolated presentation story проходит reachability. Нового global date helper/service нет.
|
||||||
|
- **Не входит / сохранить:** Общий DateRangePicker, изменение диапазонов/часового пояса и перенос логики в UI package. Moscow dates, validation и submit/reset прежние.
|
||||||
|
- **Откат:** Вернуть helper move/story diff; URL/API поведение не менялось.
|
||||||
|
|
||||||
|
- [x] 5.4 [frontend] Отделить controller банковской OperationForm от presentation.
|
||||||
|
- **Covers:** `N/A` — часть F6, применение существующей container/VM/view границы без изменения денежных сценариев.
|
||||||
|
- **Verification:** До и после `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`; особенно conflict/draft/explicit version adoption, retained UUID, full refund и max money; `npm run deps:storybook`, затем `npm run verify`.
|
||||||
|
- **Читать / область:** Полные OperationForm/OperationForm.view/types/tests, entity form-validation/mutations и UI/data/testing conventions. Зависимость: 5.2.
|
||||||
|
- **Изменить / готово:** Query/mutation/recovery orchestration принадлежит page VM/controller; существующий fields view расширен только нужными presentation props/callbacks/slots. Runtime helpers сейчас приходят из banking barrel, который переэкспортирует API/hooks: эту транзитивную связь убрать через props/callbacks, type-only DTO imports допустимы. Не вводить deep-import exception и не ослаблять story gate. RHF context в fields остаётся; `CardPage` целиком не переписывается.
|
||||||
|
- **Не входит / сохранить:** Generic form engine, extraction в feature/package без второго consumer, новые requests/optimistic updates. Не менять amount string/BigInt, intent lifecycle, version adoption, source constraints, pending close guards и вложенное создание категории.
|
||||||
|
- **Откат:** Вернуть этот behavior-preserving PR. Любое необходимое изменение поведения остановить и согласовать отдельно, не прятать в refactor.
|
||||||
|
|
||||||
|
## 7. Закончить переход и описать поставленное состояние
|
||||||
|
|
||||||
|
- [x] 7.1 [frontend] Удалить только исчерпанные compatibility facades.
|
||||||
|
- **Уточнение scope:** Пользователь разрешил удалить локальные facades с проверкой packed consumer вместо registry consumer. Nexus и независимый release остаются отложенными.
|
||||||
|
- **Covers:** `N/A` — прекращение временного двойного import path после подтверждённого потребления, design Migration Plan.
|
||||||
|
- **Verification:** `rg` по runtime/test/story imports старых theme/UI paths даёт ноль; `npm run knip`, `npm run deps`, `npm run deps:storybook`, app/catalog/packed consumer checks и `npm run verify`.
|
||||||
|
- **Читать / область:** Facades `shared/theme` и EntryCard/StateView/LoadingSkeleton, все оставшиеся callers, component source catalog, frontend README и conventions. Зависимости: 3.3, 4.2 и подтверждённый packed consumer без workspace/source aliases.
|
||||||
|
- **Изменить / готово:** Оставшиеся consumers используют package API, ненужные re-exports удалены, нет дубля JSON/CSS/implementation. В existing conventions закреплены каталог и import rules, AGENTS только маршрутизирует темы.
|
||||||
|
- **Не входит / сохранить:** Массовая чистка unrelated shared/lib, запрет прямого Mantine, перенос domain UI. Старые подходы вне migrated scope не блокируются новым широким правилом.
|
||||||
|
- **Откат:** Вернуть thin facades/imports; они не восстанавливают копии реализаций. Условия удаления снова проверяются перед повтором.
|
||||||
|
|
||||||
|
- [x] 7.2 [docs] Обновить только подтверждённое as-built описание.
|
||||||
|
- **Уточнение scope 2026-09-22:** По запросу следующего пункта без browser verification описано текущее локальное состояние после 5.4. Завершение 7.1 не требуется для этого описания: переходное состояние и отсутствие registry-поставки указаны явно; после 7.1 описание facades актуализировано. После будущей поставки документация обновляется вместе с ней.
|
||||||
|
- **Covers:** `N/A` — описание фактически принятой реализации и поставки; не новый behavior contract.
|
||||||
|
- **Verification:** Docs `npm run verify` по местному CONTRIBUTING, проверка локальных ссылок/диаграмм и semantic review по поставленному frontend; `git diff --check`.
|
||||||
|
- **Читать / область:** Docs AGENTS/README/CONTRIBUTING, `design/overview.md`, `architecture/containers.md`, при необходимости release boundary в `architecture/deployment.md`; frontend принятый package contract. Основание текущего описания: выполненные 1–5; удаление facades по 7.1 отражено в документации.
|
||||||
|
- **Изменить / готово:** Тематические страницы говорят о реально используемой теме/пакетах/границах frontend и каталога. Указывается, что raw SVG/mobile/dark/API package не поставлены. При неполной 5.x не обещать завершение этих пунктов.
|
||||||
|
- **Не входит / сохранить:** Анализ/план в permanent docs, новый roadmap, переописание backend, ADR на каждый маленький выбор. Один устойчивый ADR возможен лишь если принято значимое repository/release решение и нужен его rationale.
|
||||||
|
- **Откат:** Вернуть описание вместе с отменённой поставкой; не менять исторические archives.
|
||||||
|
|
||||||
|
- [x] 7.3 [specs] Сверить реализацию с delta и закрыть плановые расхождения.
|
||||||
|
- **Статус 2026-09-22:** Semantic сверка завершена для согласованного локального объёма: proposal/design/delta согласованы с private workspaces и packed consumer; внешняя публикация исключена. Код и ручные browser checks проверены ранее. Specs verify, локальные ссылки и whitespace проверены после правок. После установки OpenSpec выполнена команда `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`: change valid. Пункт завершён.
|
||||||
|
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`; `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
|
||||||
|
- **Verification:** Semantic проверка всех delta scenarios, `npm run verify` specs, при доступном CLI `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`; локальные ссылки и `git diff --check`.
|
||||||
|
- **Читать / область:** Specs AGENTS/README, proposal/delta/design/tasks и фактические результаты consumer/rollback/accessibility checks. Зависимости полного acceptance: 3.2–3.3, 5.1–5.4 и 7.1–7.2; требования delta должны соответствовать согласованному локальному объёму.
|
||||||
|
- **Изменить / готово:** Tasks отражают только фактически проверенные результаты; согласованные изменения scope возвращены в planning artifacts, не скрыты. Main specs обновляются только в отдельно разрешённом штатном завершении после реального внедрения; untouched deferred findings остаются явно отложенными.
|
||||||
|
- **Не входит / сохранить:** Автоматический archive/approval, Evidence/state/hash registry, декларация успеха на основании одного build. Промежуточная сверка не означает завершения change.
|
||||||
|
- **Откат:** Исправить несоответствующее предложение/отметку; main specs не описывают ещё не реализованный переход.
|
||||||
|
|
||||||
|
## Переходный период и отложенные вопросы
|
||||||
|
|
||||||
|
Точный перечень отложенного — в design: F8 recovery RecentEvents, F9 backend
|
||||||
|
errors contract, F12 расхождение bundle-budget spec/gate, общий investment recovery classifier и новые потребители.
|
||||||
|
Они не входят скрыто в tasks выше. Если требуется менять HTTP/observable
|
||||||
|
recovery, нужен отдельный согласованный scope и соответствующий spec contract.
|
||||||
|
|
||||||
|
Facades допустимы только от 2.2/3.x до 7.1; новых возможностей они не получают.
|
||||||
|
Новые components/imports следуют целевому каталогу. Старые inline рисунки
|
||||||
|
допустимы лишь до миграции своего caller batch, а новые дубли уже вынесенных
|
||||||
|
рисунков не создаются. Совместимость снимается по нулю imports и успешному
|
||||||
|
consumer proof, а не по истечении произвольного срока.
|
||||||
|
|
||||||
|
Проверены задачи **1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3**.
|
||||||
|
Недостающие browser checks 3.2/3.3/5.1 выполнены вручную пользователем:
|
||||||
|
он подтвердил корректность всех пунктов переданного чек-листа.
|
||||||
|
Независимые версии и Nexus отложены до оценки пользы после пилота;
|
||||||
|
checkbox не разрешает дальнейшую реализацию или публикацию.
|
||||||
|
|
||||||
|
Последний полный frontend `npm run verify`: 49 test files / 282 tests,
|
||||||
|
проверки зависимостей, типов, lint, Knip, OpenAPI и сборки app/catalog прошли.
|
||||||
|
Свежий UI tarball проверен вне workspace: install, typecheck, build и browser render.
|
||||||
|
Docs `npm run verify`: 45 Markdown-файлов, 0 issues.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: wealth-ledger-sdd
|
||||||
|
created: 2026-09-24
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Основание — принятый [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md)
|
||||||
|
и [дельты поведения](specs/tbank-data-sync/spec.md). Это детализация согласованной
|
||||||
|
модели для реализации. Компоненты приложения сейчас не изменены.
|
||||||
|
|
||||||
|
Исследованы локальные владельцы после чтения AGENTS каждого репозитория.
|
||||||
|
Свидетельства относятся к checkout на 2026-09-24, а не к обещаниям API:
|
||||||
|
|
||||||
|
| Граница | Текущий owner и переиспользуемое поведение |
|
||||||
|
| --- | --- |
|
||||||
|
| Хранение | `backend/prisma/schema.prisma` и `src/prisma/prisma.service.ts`: SQLite/Prisma, пользователи, сессии, подключение и ручные карты; T-Bank datasets отсутствуют. Условия владения и транзакции уже применяются в `modules/bank-cards/infrastructure/bank-cards.repository.ts`. |
|
||||||
|
| Доступ | `backend/src/integrations/tbank/tbank-access.service.ts`: `withSupportedAccounts` и `withAccountForConnection` вызывают `getOpenAccounts`; generation вычисляется из encryptedToken. `tbank-account.policy.ts` владеет допустимыми типами счетов. |
|
||||||
|
| Интеграция | `backend/src/integrations/tbank/tbank.client.ts`: общий FIFO, лимит 5 RPC/с по умолчанию, максимум 50 ожидающих, deadline и ограниченные повторы; повторный limiter не нужен. |
|
||||||
|
| Личный источник | `portfolio/application/portfolio.service.ts` хранит RAM-снимок 30 с / fallback 5 мин; `operations/application/brokerage-operations.service.ts` — страницы 30 с и обход курсоров. `income/income-analytics.service.ts` строит selected/previous/yearAgo через общий сборщик операций. Пути внутри `backend/src/modules/investments/`. |
|
||||||
|
| Общий источник | `backend/src/integrations/tbank/instrument-metadata.service.ts` уже разделяет общие metadata и pending lookup. Каталоги stocks/bonds смешивают общую информацию и личную позицию в user+generation RAM-кэше. Market services режут свечи по 365 суток. |
|
||||||
|
| Выплаты и смежные данные | `upcoming-events/upcoming-events.service.ts` отдельно получает portfolio; stock dividends приходят из T-Bank, bond events — из MOEX. Stock company use case получает asset description/fundamentals; bond metrics включают MOEX. Источники не подменяются. |
|
||||||
|
| Frontend | `frontend/src/entities/*/api` владеют generated queries; page VM собирает зависимости. `features/instrument-details/model/section.ts` знает только HTTP/query lifecycle. `features/tbank-connection/model/connection-boundary.ts` уже сбрасывает финансовую границу после успешной mutation. |
|
||||||
|
| Контракт | Backend DTO/controllers/Swagger — editable source. `frontend/src/shared/api/spec/openapi.json` — явно передаваемый snapshot. Accounts и search сейчас массивы; общего sync/coverage API нет. |
|
||||||
|
|
||||||
|
Исходный proto допускает изменение operation ID
|
||||||
|
(`backend/src/integrations/tbank/proto/contracts/operations.proto:496`),
|
||||||
|
но проект явно принимает стабильность. `is_complete` свечи задаёт источник
|
||||||
|
(`marketdata.proto:438`). В `operations.proto:472–489` не определены
|
||||||
|
inclusive/exclusive границы, срок жизни cursor и значение пропущенного `from`.
|
||||||
|
Эти пробелы не заменяются предположениями о гарантиях брокера.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** один постоянный источник чтения в существующей БД; независимые
|
||||||
|
полнота, свежесть и состояние работы; атомарная публикация согласованного
|
||||||
|
набора; безопасный перезапуск; отсутствие RPC на пути ответа заполненной БД;
|
||||||
|
контракт всех экранных зависимостей и минимальный наблюдаемый worker.
|
||||||
|
|
||||||
|
**Non-Goals:** Redis, новый сервер БД, отдельный сервис очередей, cron,
|
||||||
|
распределённое выполнение, версии каждого портфельного снимка, live
|
||||||
|
подтверждение доступа при каждом чтении, архив отключённых счетов,
|
||||||
|
новый frontend store, изменение денежных правил или источников MOEX.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Существующие domain owners читают БД; RPC принадлежит синхронизации
|
||||||
|
|
||||||
|
Сохраняются controllers, application services, нормализация Money и domain
|
||||||
|
политики. Их read path получает сохранённые источники и согласованное
|
||||||
|
metadata, а запрос работы передаёт небольшому T-Bank sync coordinator.
|
||||||
|
PortfolioService становится общим владельцем последнего полного portfolio
|
||||||
|
для accounts summary, личных позиций инструментов и будущих событий.
|
||||||
|
Сборщик операций становится одним локальным источником ledger/income;
|
||||||
|
filtered HTTP page больше не является единицей источниковой полноты.
|
||||||
|
Рассчитанные доходы не требуют отдельной постоянно дублируемой таблицы.
|
||||||
|
|
||||||
|
Синхронизация использует существующие TbankClient, metadata mapping,
|
||||||
|
market-window splitting, точную денежную арифметику и MOEX adapters.
|
||||||
|
Нормализованные числа сохраняются десятичными строками, не float.
|
||||||
|
Presentation metadata не блокирует выдачу финансового снимка; fallback
|
||||||
|
UID/FIGI/name остаётся доступен до её фоновой загрузки.
|
||||||
|
|
||||||
|
Синхронная проверка token при PUT подключения сохраняется. Успешно полученный
|
||||||
|
в этой проверке список можно сразу опубликовать как membership нового
|
||||||
|
подключения в той же транзакции, без второго GetAccounts.
|
||||||
|
|
||||||
|
### 2. Личные и общие модели в SQLite
|
||||||
|
|
||||||
|
Это логические группы таблиц, не универсальный JSON-кэш всех HTTP-ответов:
|
||||||
|
|
||||||
|
| Группа | Ключ, содержимое и инвариант |
|
||||||
|
| --- | --- |
|
||||||
|
| Подключение и membership | Существующий `TbankConnection` получает opaque `revision` и состояние известного отказа token. Список открытых поддерживаемых счетов принадлежит `(userId, revision)`; хранит accountId, type, name, openedOn, upstream order, fetchedAt, membershipRevision. Список заменяется целиком атомарно, включая успешный пустой. |
|
||||||
|
| Portfolio | `(userId, revision, accountId)` → последний полный нормализованный snapshot, fetchedAt и dataRevision. Допустимо хранить состав snapshot как versioned JSON: он читается целиком, не нужен отдельный исторический агрегат. |
|
||||||
|
| Operations | Уникальность `(userId, revision, accountId, brokerOperationId)`; индекс `(userId, revision, accountId, occurredAt, brokerOperationId)`, дополнительные instrument/category поля для фильтров. Полный исходный нормализуемый payload сохраняет точные поля, executed state и version маппинга. Cursor/пустая строка не заменяют отсутствующий ID. |
|
||||||
|
| Инструменты | Общий ключ provider + instrument UID; FIGI/ticker — aliases с существующими правилами неоднозначности. Metadata, company/fundamentals и catalog discovery имеют собственные успешные timestamps и coverage; отрицательный успешный lookup отличается от ошибки. |
|
||||||
|
| Рыночные данные | Общие quote/trading-status и необходимые price dependencies; дневная свеча уникальна по `(provider, instrumentUid, interval, time)`, содержит `isComplete`. Календари имеют source/type/instrument/range и исходные события. MOEX enrichment сохраняет собственный source и существующую семантику недоступности. |
|
||||||
|
| Последний полный прогноз | Private snapshot account events и instrument personal payout estimates по `(userId, revision, account scope, instrument при наличии, applied range)`, с dependency revisions, source timestamps и calculatedAt. Заменяется только после готовности всех обязательных источников; это fallback результата, не история portfolio. |
|
||||||
|
| Покрытие и работа | Завершённые диапазоны, fetchedThrough/fetchedAt, dataRevision, известная нижняя граница и её происхождение; pending intent, attemptId, processRunId, диапазон, стадия и прогресс страниц. Staging страниц текущей попытки изолирован от опубликованного набора. |
|
||||||
|
|
||||||
|
Общие таблицы не содержат user/account/quantity/estimatedAmount или token.
|
||||||
|
Личные payout estimates, holdings и фильтрованная личная история вычисляются
|
||||||
|
из разрешённых accounts и portfolio/operations. Статус пользовательской
|
||||||
|
работы хранится отдельно от общих instrument records; ошибка credentials
|
||||||
|
не становится общим отрицательным lookup.
|
||||||
|
|
||||||
|
FK и unique constraints обеспечивают идентичность; транзакции проверяют
|
||||||
|
владение и revision. Не создаются generic repository framework, event bus
|
||||||
|
или универсальный job engine. Точные SQL-имена выбираются по локальным
|
||||||
|
Prisma conventions без изменения этой модели.
|
||||||
|
|
||||||
|
### 3. Доступ без синхронного GetAccounts
|
||||||
|
|
||||||
|
Read path: локальная сессия → существующее подключение и его revision →
|
||||||
|
известная ошибка token → membership текущей revision → данные разрешённых
|
||||||
|
счетов. Token для этого не расшифровывается.
|
||||||
|
|
||||||
|
- Membership отсутствует: `200` с `data: null`, `availability: missing` и
|
||||||
|
состоянием discovery, без данных неизвестного accountId. После discovery
|
||||||
|
следующий read возвращает данные либо прежний `404`.
|
||||||
|
- Membership свежий: разрешённые данные читаются локально.
|
||||||
|
- Membership старше пяти минут: используется последнее успешное наблюдение,
|
||||||
|
обычный запрос ставит его фоновое обновление. Это ограниченная задержка
|
||||||
|
обнаружения отзыва доступа, принятая локальной моделью, не live authority.
|
||||||
|
- Успешный новый список немедленно ограничивает все последующие одиночные
|
||||||
|
и aggregate reads; добавленный ещё не загруженный счёт делает aggregate
|
||||||
|
неполным. Ответ «Все счета» использует одну согласованную DB read transaction
|
||||||
|
с фиксированной membershipRevision.
|
||||||
|
- Известный `INVALID_TBANK_TOKEN` блокирует личные reads и возвращает прежний
|
||||||
|
`400`; `409 TBANK_NOT_CONNECTED`, `404`, session/validation errors сохраняются.
|
||||||
|
Старые записи не удаляются вследствие transient failure, но не выдаются
|
||||||
|
вопреки известному отказу доступа. Исправление token выполняется существующим PUT.
|
||||||
|
|
||||||
|
При замене успешная транзакция назначает новую случайную revision, сохраняет
|
||||||
|
новый token и очищает/делает недоступными прежние private datasets/jobs
|
||||||
|
по существующей семантике invalidation. Delete остаётся идемпотентным и
|
||||||
|
каскадно убирает личные данные подключения; специальный архив не вводится.
|
||||||
|
Общие неперсональные данные могут оставаться.
|
||||||
|
|
||||||
|
Каждая публикация данных, coverage, ошибки, timestamp и результата работы
|
||||||
|
проверяет в той же короткой транзакции текущие connection revision и
|
||||||
|
attemptId. Проверка после RPC только в памяти недостаточна. Проверка нужна
|
||||||
|
и при общей публикации, начатой credentials старого подключения.
|
||||||
|
Поздняя попытка не вправе изменить даже freshness; результаты старой revision
|
||||||
|
отбрасываются. Транзакция никогда не удерживается во время сетевого вызова.
|
||||||
|
|
||||||
|
### 4. Интервалы и атомарная сверка операций
|
||||||
|
|
||||||
|
Внутри БД диапазоны — `[from, to)` в UTC. Public semantics не меняются:
|
||||||
|
журнал принимает действующие UTC timestamps, его default начинается 1 января
|
||||||
|
UTC; даты доходов включительны в `Europe/Moscow`, и существующий
|
||||||
|
`planIncomePeriod` вычисляет selected/previous/yearAgo. Преобразование границ
|
||||||
|
выполняется один раз до планирования. Полнота доходного месяца проверяется
|
||||||
|
по объединению UTC coverage, а не по совпадению названия месяца.
|
||||||
|
|
||||||
|
Источник по операциям загружается ограниченными календарными UTC-месяцами
|
||||||
|
и фрагментами нужных диапазонов. Верхняя граница текущей попытки фиксируется
|
||||||
|
в момент её создания. Для неизвестной inclusivity upstream применяется
|
||||||
|
перекрытие соседних окон с защитной секундой; записи локально отбираются
|
||||||
|
по точному `[from, to)` и дедуплицируются по исходному ID. Это защищает
|
||||||
|
стык, но не доказывает отсутствие будущих исправлений брокера.
|
||||||
|
|
||||||
|
Обычная полнота счёта строится только по всем EXECUTED операциям интервала
|
||||||
|
без category/instrument фильтров. UI filters и pagination применяются после
|
||||||
|
чтения. Ручной refresh личной истории инструмента может перечитать только
|
||||||
|
`instrumentUid + shown range`; его reconciliation удаляет отсутствующие
|
||||||
|
записи только этого predicate и не присваивает полноту всему счёту.
|
||||||
|
Уже полное общее покрытие может обслуживать такой subset; обратное неверно.
|
||||||
|
|
||||||
|
Каждая успешная страница записывается в staging attempt с page count и
|
||||||
|
прогрессом. При наличии полного прежнего набора staging никогда не виден
|
||||||
|
как его замена. Новые большие периоды становятся доступными по завершении
|
||||||
|
малых интервалов при первичном заполнении, поэтому экран может иметь `partial` coverage без показа
|
||||||
|
непроверенных страниц. Сбой, repeated/missing next cursor, malformed ID или
|
||||||
|
противоречащие версии одного ID внутри попытки оставляют интервал незавершённым.
|
||||||
|
Идентичные дубликаты страницы безопасно схлопываются.
|
||||||
|
|
||||||
|
Только успешное исчерпание всех страниц фиксированного интервала разрешает
|
||||||
|
транзакцию публикации: upsert новых/изменённых полей, удаление отсутствующих записей
|
||||||
|
в перечитанном predicate, публикация coverage и времени успешной сверки.
|
||||||
|
Account dataRevision увеличивается только при изменении rowset;
|
||||||
|
coverage/timestamp/status без изменения строк меняют только stateVersion.
|
||||||
|
Читатель видит старый либо новый набор,
|
||||||
|
не их смесь. Между интервалами частичная готовность допустима и явно видима;
|
||||||
|
сумма всего периода окончательна только при полном покрытии его зависимостей.
|
||||||
|
|
||||||
|
Для повторной сверки уже полного показанного периода единица публикации —
|
||||||
|
весь запрошенный scope, а не отдельный календарный chunk. Staging объединяет
|
||||||
|
его интервалы, сравнения и счета (`all`) в одну publication group; успешные
|
||||||
|
страницы/окна сохраняют прогресс, но commit/upsert/delete/coverage всех
|
||||||
|
участников выполняются только после успеха группы. Любая ошибка оставляет
|
||||||
|
весь прежний результат. Такое же правило действует для ручного refresh
|
||||||
|
показанного диапазона свечей. Первичная загрузка ранее отсутствующих данных
|
||||||
|
по малым интервалам остаётся инкрементальной. Пересекающиеся работы
|
||||||
|
присоединяются к группе и не публикуют её часть независимо. DB transaction
|
||||||
|
короткая и локальная; RPC выполняются до неё, а staging не держится в RAM.
|
||||||
|
|
||||||
|
Для изменяемого конца `to=now` полнота означает «все операции по состоянию
|
||||||
|
на `asOf` последней успешной сверки». Новый ещё не наблюдённый хвост времени
|
||||||
|
в пределах того же текущего месяца и TTL не считается пропуском, запускающим
|
||||||
|
новую загрузку; ответ сохраняет исходный `asOf`, не утверждая покрытие будущих
|
||||||
|
операций. Настоящие пропуски до `asOf`, новый явно запрошенный исторический
|
||||||
|
диапазон и переход месяца этому исключению не подчиняются.
|
||||||
|
Текущий месяц обновляется целиком до зафиксированного now при истечении
|
||||||
|
пяти минут, чтобы учесть изменения уже виденных операций. После смены месяца
|
||||||
|
его покрытие до прежнего now не распространяется до конца месяца:
|
||||||
|
следующий подходящий запрос завершает хвост. Полные прошлые интервалы
|
||||||
|
не сверяются автоматически. Успешная наблюдённая смена полей того же ID
|
||||||
|
обновляет запись и производные результаты; поиск сменившихся ID отсутствует.
|
||||||
|
|
||||||
|
### 5. Планирование, приоритет и восстановление
|
||||||
|
|
||||||
|
Поддерживаемая поставка — один backend process с существующей SQLite.
|
||||||
|
Запрос сохраняет intent, возвращает локальные данные и передаёт выполнение
|
||||||
|
in-process coordinator. На startup нет обхода очереди или запуска импорта.
|
||||||
|
Process получает случайный runId; следующий подходящий запрос перехватывает
|
||||||
|
незавершённую работу другого runId новой attemptId. Persisted provider cursor
|
||||||
|
не переиспользуется после перезапуска: незавершённый интервал начинается
|
||||||
|
с первой страницы, staging прежней попытки очищается. Завершённые интервалы
|
||||||
|
и опубликованные данные сохраняются.
|
||||||
|
|
||||||
|
Приоритеты: discovery доступа → нужные экрану зависимости и интервалы,
|
||||||
|
включая comparisons → остальная история счёта и рыночная история его бумаг.
|
||||||
|
Между окнами/страницами coordinator отдаёт приоритет новой экранной работе;
|
||||||
|
уже отправленный RPC не прерывается ради перестановки. Один producer
|
||||||
|
догрузки не ставит тысячи RPC заранее: страницы идут последовательно,
|
||||||
|
новые окна планируются постепенно, через прежний bounded limiter.
|
||||||
|
Очередь RPC и bounded retry не дублируются на уровне worker.
|
||||||
|
|
||||||
|
Ключ personal work: user + connectionRevision + account + source kind +
|
||||||
|
normalized interval/predicate. Координатор сериализует публикации одного
|
||||||
|
account source и для обычной загрузки вычитает покрытые/выполняемые
|
||||||
|
пересечения; одновременно запрошенные остатки объединяет. Force refresh
|
||||||
|
не вычитает полное покрытие: он обязан перечитать запрошенный target,
|
||||||
|
но присоединяется к уже выполняемой подходящей загрузке. Повторный force refresh присоединяется к
|
||||||
|
подходящей активной попытке, уже получающей полный запрошенный диапазон;
|
||||||
|
TTL при этом не отменяет запрос. Новая область расширяет pending intent.
|
||||||
|
|
||||||
|
Shared work использует provider/instrument/source/range identity.
|
||||||
|
Общий успешный результат переиспользуется независимо от пользователя.
|
||||||
|
У каждого наблюдателя свой авторизованный handle, без выдачи чужих job IDs
|
||||||
|
и credentials errors. Ошибка token инициатора завершает его попытку, не
|
||||||
|
портит общий источник; новый допустимый запрос другого пользователя может
|
||||||
|
запустить попытку со своими credentials. Старый job не перехватывает token
|
||||||
|
другого пользователя сам.
|
||||||
|
|
||||||
|
Остальная история начинается с известной даты открытия; если её нет,
|
||||||
|
используется существующая техническая граница `1970-01-01T00:00:00Z`,
|
||||||
|
явно помеченная как requested floor, а не доказанная дата открытия/минимум
|
||||||
|
источника. Сохраняются confirmed intervals и `historyBoundary: unknown`;
|
||||||
|
«вся история загружена» не показывается до подтверждения нижней границы.
|
||||||
|
Отказ источника за пределами доступного диапазона сохраняется как ограничение,
|
||||||
|
не как пустой полный интервал. Отсутствие `from` не используется как
|
||||||
|
недокументированное обещание all-time. Это не мешает полной готовности
|
||||||
|
подтверждённых периодов экрана.
|
||||||
|
|
||||||
|
После transient failure попытка заканчивается с error; новый обычный запрос
|
||||||
|
может возобновить недостающее/устаревшее, ручной — повторить независимо от TTL.
|
||||||
|
Наблюдение результата этого не делает. Logout/уход останавливает наблюдение
|
||||||
|
frontend, но не отменяет уже запрошенную допустимую серверную работу.
|
||||||
|
Замена/delete подключения останавливает публикацию по revision fence.
|
||||||
|
|
||||||
|
### 6. Общие market data и производные выплаты
|
||||||
|
|
||||||
|
Quotes и portfolio обновляются через минуту; metadata/catalog/company и
|
||||||
|
календари — через сутки. Freshness принадлежит конкретному source/range,
|
||||||
|
поэтому успешный lookup одного инструмента не делает свежим весь каталог.
|
||||||
|
Catalog discovery сохраняет успешный набор UID/порядок отдельно от
|
||||||
|
enrichment по нормализованному поисковому query и типу инструмента, сохраняя
|
||||||
|
существующий предел результатов. `findShares/findBonds` не загружают весь
|
||||||
|
каталог: успешный поиск SBER не подтверждает результат GAZP. Новый query
|
||||||
|
без подтверждённого покрытия запускает discovery, а не возвращает ложный
|
||||||
|
пустой результат. Metadata уже известных UID переиспользуется; повтор того
|
||||||
|
же свежего полного query не вызывает discovery RPC. Отсутствие enrichment
|
||||||
|
не превращает найденный набор в пустой поиск.
|
||||||
|
|
||||||
|
Свечи сохраняют исходную `isComplete`, диапазоны и `historyFrom`.
|
||||||
|
Переиспользуются окна до 365 суток. Завершённый диапазон без свечей хранится
|
||||||
|
как проверенное пустое покрытие. Текущий UTC-день всегда изменяемый,
|
||||||
|
включая пустое окно до торгов; его TTL — минута. Смена UTC-дня сама не
|
||||||
|
закрывает ни пустое открытое окно, ни незавершённую свечу: следующий запрос
|
||||||
|
повторяет прошедшее окно; успешный закрытый пустой диапазон подтверждает
|
||||||
|
отсутствие, для имеющейся свечи нужно `isComplete=true`.
|
||||||
|
|
||||||
|
Известный `first_1dayCandleDate` определяет нижнюю границу all-range.
|
||||||
|
Если его нет, сохраняется существующее `*_HISTORY_START_UNAVAILABLE`,
|
||||||
|
не создаётся ложная полная история. Предзагрузка касается бумаг сохранённого
|
||||||
|
текущего портфеля и использует ту же общую coverage, что открытый график.
|
||||||
|
|
||||||
|
События инструментов сохраняются без пользовательской ожидаемой суммы.
|
||||||
|
Календари купонов/дивидендов и company data охватываются общим read path;
|
||||||
|
future events используют сохранённый portfolio и исходный календарь,
|
||||||
|
сохраняя неопределённость прогноза, cancelled-dividend и offer semantics.
|
||||||
|
MOEX bond events/metrics остаются MOEX: необходимые для экрана результаты
|
||||||
|
также сохраняются и обновляются в этой работе, без нового MOEX scheduler.
|
||||||
|
Недоступность optional enrichment не делает финансовые данные ошибочными;
|
||||||
|
неполнота обязательного календаря не выдаётся за полный пустой прогноз.
|
||||||
|
Последний полный прогноз/личная оценка сохраняется отдельным private
|
||||||
|
snapshot по показанному scope/range. Если новый portfolio добавил бумагу,
|
||||||
|
а её календарь не загрузился, следующий read, в том числе после restart,
|
||||||
|
возвращает прежний полный forecast с его прежними dependency timestamps и
|
||||||
|
ошибкой обновления. При отсутствии такого snapshot возвращается missing/partial
|
||||||
|
без окончательного списка. Готовый новый результат атомарно заменяет snapshot;
|
||||||
|
снимок с иным периодом или membership не выдаётся под новым scope, а доступ
|
||||||
|
всегда повторно ограничивается текущим membership.
|
||||||
|
|
||||||
|
### 7. Публичный контракт и наблюдение работы
|
||||||
|
|
||||||
|
**Contract Gate Decision: required.** Меняется wire shape всех T-Bank data
|
||||||
|
GET, включая bare-array accounts и stock/bond search. Backend сохраняет
|
||||||
|
маршруты и тип внутреннего payload, добавляя единый envelope:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{ data: T | null, meta: {
|
||||||
|
resourceKey, dataRevision, stateVersion,
|
||||||
|
availability: missing | partial | complete,
|
||||||
|
freshness: unknown | fresh | stale, lastSuccessfulAt: timestamp | null,
|
||||||
|
sync: { id: opaque | null, state: idle | queued | running | failed,
|
||||||
|
errorCode: safeCode | null },
|
||||||
|
coverage?: { requested, completeRanges, missingRanges,
|
||||||
|
asOf, historyBoundary, historyState },
|
||||||
|
dependencies?: [{ resourceKey, availability, freshness,
|
||||||
|
lastSuccessfulAt, sync, coverage?, unavailableReason? }]
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
Connection generation/revision остаётся внутренней: существующая security
|
||||||
|
policy запрещает её в API/logs/errors, даже если она случайная. Frontend
|
||||||
|
использует свою существующую session/connection boundary.
|
||||||
|
`resourceKey` — нормализованная идентичность запроса без user secrets и
|
||||||
|
connection generation. `dataRevision` меняется при публикации данных;
|
||||||
|
изменение статуса работы не меняет страницу операций. Availability означает
|
||||||
|
готовность обязательных источников данного результата, а не обещание полноты
|
||||||
|
рынка или будущих выплат. `data:null` используется для missing и для
|
||||||
|
неполных финансовых агрегатов; `partial` ledger/candles может содержать
|
||||||
|
подтверждённые части. Полнота selected/comparisons задаётся отдельно:
|
||||||
|
analytics `data` доступен после полного selected, а `previous` и
|
||||||
|
`yearComparison` nullable до готовности соответствующего сравнения.
|
||||||
|
`meta.dependencies` различает missing/loading/failed comparison и
|
||||||
|
заведомо недоступное по календарю/границе истории сравнение; последнее
|
||||||
|
имеет отдельный `unavailableReason` и не планируется как пропуск.
|
||||||
|
Это явное изменение вложенного analytics DTO, а не только envelope.
|
||||||
|
|
||||||
|
Для зависимостей с разными TTL metadata содержит их состояния по
|
||||||
|
resourceKey; freshness производного результата — худшее обязательное
|
||||||
|
состояние, lastSuccessfulAt — самый ранний из использованных успешных
|
||||||
|
источников. Чтение/`calculatedAt`/`observedAt` не продлевает source freshness.
|
||||||
|
Portfolio `data.lastSuccessfulAt` и `data.isStale` сохраняются для текущих
|
||||||
|
consumers и отображают тот же portfolio source, без прежнего пятиминутного
|
||||||
|
предела хранения. Error фонового обновления не меняет успешный timestamp
|
||||||
|
и сама по себе не отменяет ещё действующий TTL.
|
||||||
|
|
||||||
|
| Операция | Контракт |
|
||||||
|
| --- | --- |
|
||||||
|
| Обычный GET существующего resource | `200` envelope, в том числе cold/partial; планирует missing/stale зависимости. До постановки проверяет auth, connection и известную принадлежность. |
|
||||||
|
| Тот же GET с `sync=observe` | Только локальное чтение; никогда не создаёт и не возобновляет work, даже при missing/stale/error. Нужен для получения изменившихся данных после status observation. |
|
||||||
|
| `POST /api/v1/tbank-sync` | Валидированный screen descriptor, force по TTL; `202` с авторизованным work handle и normalized scope. При совпадении присоединяет к work. Приём не означает успех. |
|
||||||
|
| `GET /api/v1/tbank-sync/:id` | Только статус разрешённого handle, progress, changed resource revisions и terminal outcome; никаких новых запусков/повторов. Чужой/прежней connection handle — `404`. |
|
||||||
|
|
||||||
|
Descriptor — discriminated union `screen: accounts | account | operations |
|
||||||
|
income | events | stocks | bonds | stock | bond`. Поля разрешены только
|
||||||
|
соответствующему screen: account scope (конкретный ID/`all`), applied period,
|
||||||
|
filters, instrumentUid, candle period/range. Сервер использует прежние
|
||||||
|
валидаторы дат/лимитов/UID и сам раскрывает dependency graph; произвольного
|
||||||
|
списка RPC/таблиц/чужого userId во входе нет. Зафиксированный now и нормализованный
|
||||||
|
scope возвращаются клиенту, чтобы ручная операция не переезжала на другой период.
|
||||||
|
|
||||||
|
| Экран | Зависимости force refresh |
|
||||||
|
| --- | --- |
|
||||||
|
| Accounts | Membership + portfolio summary каждого текущего доступного счёта; metadata показанных позиций по необходимости. Не вся история операций. |
|
||||||
|
| Account / all overview | Membership, portfolio, показанный recent-ledger range, current-month income, upcoming events и необходимые instrument metadata/calendars. |
|
||||||
|
| Operations | Применённые account scope и период; категории — локальная проекция. Пагинация не расширяет источниковой период. |
|
||||||
|
| Income | Применённый период + previous/yearAgo из существующего planner, membership, требуемая metadata выплат. |
|
||||||
|
| Events | Membership, portfolio и прогноз применённого включительного from/to из сохранённых календарей; default from=today UTC, to=from+30 дней. |
|
||||||
|
| Stocks / bonds search | Общий каталог, результаты/enrichment текущего поиска и показываемые prices; без истории всего каталога. |
|
||||||
|
| Stock / bond | Detail/company либо metrics, quote и price dependencies, видимый candle range, payout calendar, личная position и применённый диапазон/инструмент personal operations. |
|
||||||
|
|
||||||
|
HTTP ошибки auth/input/access сохраняют `application/problem+json`.
|
||||||
|
`TBANK_UNAVAILABLE` из принятой фоновой работы передаётся как безопасный
|
||||||
|
`meta.sync.errorCode`, включая cold failure; `503` остаётся для запроса,
|
||||||
|
который не удалось принять/обслужить. Известный отказ credentials остаётся
|
||||||
|
`400 INVALID_TBANK_TOKEN`, не успешным envelope с недоступными private data.
|
||||||
|
Frontend различает server work error и transport failure.
|
||||||
|
|
||||||
|
Работа имеет terminal `succeeded | failed | cancelled` в status endpoint;
|
||||||
|
envelope отображает successful отсутствие активной работы как `idle`.
|
||||||
|
Статус имеет `stateVersion` нужных экрану ресурсов и отдельный прогресс background
|
||||||
|
history, поэтому готовые данные можно прочитать до окончания всей догрузки.
|
||||||
|
Frontend наблюдает только известный active handle на открытом экране
|
||||||
|
с интервалом 1–5 секунд и без автоматического retry failed work. После
|
||||||
|
изменения stateVersion или terminal status выполняется `sync=observe` GET.
|
||||||
|
После ухода/terminal status наблюдение прекращается. Это наблюдение уже
|
||||||
|
запрошенной работы, не периодический запуск импорта.
|
||||||
|
|
||||||
|
`stateVersion` — opaque идентичность состояния ответа, меняется при изменении
|
||||||
|
данных, coverage (включая пустое завершённое окно), successfulAt, ошибок,
|
||||||
|
работы и доступа; это не pagination revision. Он вычисляется из версий
|
||||||
|
зависимостей и состояния без выдачи внутренних connection identifiers.
|
||||||
|
Прогресс далёкой истории, не меняющий этот resource, не меняет его
|
||||||
|
stateVersion и не заставляет повторно читать уже готовый экран.
|
||||||
|
Истечение времени не создаёт работу: freshness вычисляется из timestamp.
|
||||||
|
|
||||||
|
Entity query owner записывает observe-ответ в ту же canonical cache identity
|
||||||
|
исходного GET; не создаётся второй cache по параметру observe. Query keys
|
||||||
|
берутся из generated owner, не собираются вручную. Все новые status/mutation
|
||||||
|
ответы участвуют в существующих session/connection boundary fences.
|
||||||
|
Кроме них owner использует latest-request sequence для каждого resource:
|
||||||
|
ответ более раннего запроса не заменяет уже принятый более поздний ответ.
|
||||||
|
При status notification запоминается ожидаемый stateVersion; observe-ответ
|
||||||
|
с прежней версией не публикуется, выполняется только новое observational read.
|
||||||
|
Подтверждённый access loss отменяет/инвалидирует соответствующие personal
|
||||||
|
queries и повышает локальную access epoch; даже поздний успешный ответ той же
|
||||||
|
connection, начатый до отзыва, не восстанавливает скрытые данные.
|
||||||
|
|
||||||
|
### 8. Локальная пагинация и финансовые результаты
|
||||||
|
|
||||||
|
Порядок операций сохраняет действующую сортировку с детерминированным
|
||||||
|
tie-break по accountId/operationId. Оpaque cursor связан с user, connection,
|
||||||
|
membershipRevision для all, нормализованными filters/period/limit и
|
||||||
|
account operations dataRevision. Любое изменение relevant rowset делает
|
||||||
|
cursor недействительным. Binding хранится серверно либо проверяется keyed
|
||||||
|
digest: connection generation не находится даже в декодируемом cursor payload.
|
||||||
|
Изменение rowset возвращает существующий `INVALID_OPERATIONS_CURSOR`;
|
||||||
|
исторические копии всех rowsets не хранятся. Первая реализация использует
|
||||||
|
revision на счёт: изменение другого периода тоже может сбросить его cursor;
|
||||||
|
более узкая revision нужна только при доказанно мешающих сбросах.
|
||||||
|
|
||||||
|
Frontend сбрасывает cursor stack на первую страницу, сохраняя applied filters
|
||||||
|
и объясняя обновление списка; ни страницы, ни суммы разных revisions не
|
||||||
|
склеиваются. `hasNext=false` означает конец текущей локальной выдачи,
|
||||||
|
а не полноту истории. Coverage/status без изменения строк cursor не ломают.
|
||||||
|
|
||||||
|
Income сохраняет точные signed coupon/dividend payments и существующие
|
||||||
|
comparison rules. Stable operation ID устраняет повторный финансовый учёт;
|
||||||
|
прежняя норма о разных entries с одинаковым ID явно изменена delta spec.
|
||||||
|
Analytics читает согласованный набор операций и coverage в read transaction.
|
||||||
|
Неполнота selected скрывает его окончательные totals; неполнота comparison
|
||||||
|
скрывает только его суммы/разницы и не создаёт нулевую базу. Уже опубликованный полный результат того же
|
||||||
|
period/context сохраняется при неудачной повторной сверке; данные другого
|
||||||
|
периода никогда не подписываются новым диапазоном.
|
||||||
|
|
||||||
|
### 9. Измеримый бюджет и стоимость интеграции
|
||||||
|
|
||||||
|
500 мс — цель, не текущий результат. Начальный профиль приёмки: один backend
|
||||||
|
process, локальная SQLite на SSD, 4 CPU / 8 GiB, 3 счёта, по 100 000 операций
|
||||||
|
за 10 лет, 200 инструментов и по 5 000 дневных свечей, включая пустые
|
||||||
|
интервалы; до 1 000 операций/200 доходных выплат на счёт в активном месяце.
|
||||||
|
Измеряются account/all reads, ledger page 50, monthly income с comparisons,
|
||||||
|
instrument detail/quote/company/metrics, year/all candles и payouts/search.
|
||||||
|
|
||||||
|
Бюджет — p95 ≤ 500 мс от входа HTTP на backend до окончания ответа при
|
||||||
|
10 одновременных клиентах и не менее 100 чтений каждой группы после прогрева.
|
||||||
|
Отдельно сохраняются p50/p99/max, размер ответа, состояние БД, hardware и
|
||||||
|
время первого DB-only ответа после перезапуска. Cold import исключён из
|
||||||
|
бюджета заполненной БД, но его HTTP ответ также не ожидает RPC. Больший
|
||||||
|
реальный профиль не объявляется измеренным по этим данным.
|
||||||
|
|
||||||
|
Изоляция от брокера проверяется задержанным/отказывающим T-Bank; источник
|
||||||
|
замеряемых данных — HTTP timing и счётчики RPC существующего logger. Для
|
||||||
|
fresh полной выборки ожидается 0 RPC; для 20 simultaneous stale requests —
|
||||||
|
один логический проход каждой нужной зависимости, с явным учётом pages и
|
||||||
|
разрешённых retries. После завершения, без screen/manual requests — 0 RPC.
|
||||||
|
Повторные старые полные intervals/candles, в том числе после restart, —
|
||||||
|
0 повторных RPC этих диапазонов. Сравнение с исходной реализацией выполняется
|
||||||
|
на одинаковом navigation trace; сохраняются абсолютные числа и доля снижения
|
||||||
|
`1 - new/old`, если baseline ненулевой. Измерения не требуют real tokens
|
||||||
|
в артефактах или логах.
|
||||||
|
|
||||||
|
## UI/UX Applicability and Package
|
||||||
|
|
||||||
|
**Applicability and scope:** `new/changed` — общие состояния и действие
|
||||||
|
обновления всех T-Bank экранов, без перестройки их содержимого.
|
||||||
|
|
||||||
|
**Package revision:** `1`. Пакет полностью inline; локальные PNG/SVG не
|
||||||
|
нужны, поскольку меняются информационные состояния и существующие controls,
|
||||||
|
а не композиция экранов. Self-hash и внешние визуальные зависимости отсутствуют.
|
||||||
|
|
||||||
|
**Screens, states and interactions:**
|
||||||
|
|
||||||
|
| Состояние | Представление и действие |
|
||||||
|
| --- | --- |
|
||||||
|
| Missing без результата | Существующий структурный skeleton только отсутствующих секций, «Загружаем данные», доступное busy-состояние; без нулей/«операций нет». |
|
||||||
|
| Partial period | Подтверждённые строки видны с «Период загружен не полностью». Финансовые totals/comparisons ждут полноты. «История догружается» отдельно от состояния показанного периода. |
|
||||||
|
| Complete fresh | Контент, время источника и «Обновить» в заголовке экрана. Истечение TTL само не запускает UI timer refresh. |
|
||||||
|
| Complete stale / active | Прежний контент и timestamp, «Обновляем» либо «Данные на …»; без skeleton поверх сумм. Повторная кнопка disabled для уже запрошенного текущего scope с доступным пояснением. |
|
||||||
|
| Failed with data | Прежний контент, исходное время, безопасное предупреждение и повторное «Обновить». Transport failure и server work failure различимы; статус не сообщает об успешном обновлении. |
|
||||||
|
| Failed without data | Локальное error-состояние секции, безопасный retry либо действие настроек согласно коду. |
|
||||||
|
| Complete empty | Только подтверждённое пустое состояние; empty portfolio не создаёт нулевые карточки, пустой доход объясняет отсутствие купонов/дивидендов. |
|
||||||
|
| Access lost / connection changed | Очистка недоступного personal view, настройки для token/connection, возврат к счетам для 404. Нельзя удерживать чужую/прежнюю revision под предупреждением. |
|
||||||
|
|
||||||
|
Accounts показывают freshness списка отдельно от локального состояния
|
||||||
|
portfolio summary каждой карточки. Account/all overview сохраняет независимые
|
||||||
|
секции. Operations сохраняет применённые фильтры; income не переименовывает
|
||||||
|
старый snapshot новым period. Stock/bond показывают состояния по sections,
|
||||||
|
не смешивая общий график с личными holdings/payout estimates. Детали candle
|
||||||
|
по-прежнему обозначают незавершённость.
|
||||||
|
|
||||||
|
**Responsive behavior:** на 320–375 px timestamp, предупреждение и кнопка
|
||||||
|
переносятся без горизонтального overflow и обрезания сумм; на широком экране
|
||||||
|
используют существующий header/section layout. Focus сохраняется после
|
||||||
|
обновления; button доступен с клавиатуры, текст статуса не зависит от цвета,
|
||||||
|
`aria-live=polite` объявляет смену стадии/ошибку, но не каждую страницу.
|
||||||
|
Skeleton не фокусируется; reduced motion сохраняется.
|
||||||
|
|
||||||
|
**Reused source and differences:** runtime Mantine controls, существующие
|
||||||
|
Section/VM patterns и generated query owners переиспользуются как код.
|
||||||
|
Это новый inline пакет, не reuse изменяемого или архивного design package;
|
||||||
|
его состояние полностью определено выше.
|
||||||
|
|
||||||
|
## Delivery Map
|
||||||
|
|
||||||
|
| Area | Status | Architectural outcome or N/A reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| frontend | `affected` | Новый snapshot/types, metadata-aware entity queries/VMs всех экранов, scoped refresh и observe-only status, connection/cursor boundaries, inline UI package. |
|
||||||
|
| backend | `affected` | Prisma storage/migration, local access/read owners, durable interval sync/fencing, shared market sources, DTO и OpenAPI; evidence корректности, latency и RPC. |
|
||||||
|
| specs | `affected` | Этот change и семь delta capabilities; traceability реализации без преждевременного изменения main specs или чужих changes. |
|
||||||
|
| docs | `affected` | As-built freshness/storage/access/flows/API/glossary и актуальный статус ADR после поставки; не второй источник требований. |
|
||||||
|
|
||||||
|
## Solution Assessment
|
||||||
|
|
||||||
|
Trigger: storage, external integration, caching, concurrency и public API.
|
||||||
|
|
||||||
|
| Вариант | Fit, maturity, security, operating cost и migration cost |
|
||||||
|
| --- | --- |
|
||||||
|
| Существующие SQLite/Prisma + транзакции, unique keys и небольшой request-driven coordinator | Выбран: зрелая используемая БД, один процесс, отсутствие нового сервиса; ownership/fencing доступны в SQL. Миграция добавляет таблицы и revision. Специфичные broker intervals/reconciliation остаются доменной логикой. |
|
||||||
|
| Продлить RAM/TanStack кэш | Зрелый дешёвый механизм показа, но несовместим с durability и полнотой после restart. Остаётся presentation cache, не решение хранения. |
|
||||||
|
| Redis + BullMQ | Зрелые retries/locks/queues, но требует нового сервиса, persistence/security/backup и двойной согласованности с SQLite; его jobs всё равно не решают domain coverage/атомарную сверку. Нет нужды в распределённых workers, поэтому цена миграции/эксплуатации не оправдана. |
|
||||||
|
| Temporal либо аналогичный workflow server | Зрелое recovery, но отдельная инфраструктура и execution model для одной последовательности RPC; domain data transactions всё равно необходимы. Не соответствует минимальному одному backend. |
|
||||||
|
| Cron/планировщик | Зрелый механизм, но несовместим с явным запретом периодических запусков. |
|
||||||
|
|
||||||
|
Собственный код ограничен доменными интервалами, dependency planning,
|
||||||
|
staging/publish и coalescing; transport queue/retries и DB lifecycle уже
|
||||||
|
существуют. Существующий runtime не предоставляет durable job framework;
|
||||||
|
добавлять его ради этих конечных стадий не требуется. При переходе к нескольким
|
||||||
|
backend processes понадобится отдельное решение о durable claims/leases;
|
||||||
|
эта поставка не выдаёт processRunId за распределённую блокировку.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Локальный membership не замечает отзыв доступа мгновенно. Он обновляется
|
||||||
|
по пятиминутной политике; известный отказ применяется немедленно.
|
||||||
|
- Stable operation ID противоречит возможностям upstream proto, но принят
|
||||||
|
как допущение; сменившийся ID может потребовать отдельной будущей сверки.
|
||||||
|
- Broker cursor не обещает snapshot isolation: полнота означает успешный
|
||||||
|
обход доступного интервала на момент сверки, не отсутствие последующих
|
||||||
|
исправлений. Staging защищает от наших частичных публикаций.
|
||||||
|
- Долгая первичная история расходует rate-limit и диск. Ограниченные окна,
|
||||||
|
приоритет экрана и отсутствие предварительной массовой очереди сдерживают
|
||||||
|
ресурсы; объём и время первой загрузки пока не измерены.
|
||||||
|
- Запись SQLite сериализуется. Короткие транзакции и индексы должны уложиться
|
||||||
|
в бюджет; это проверяется, а не считается доказанным архитектурой.
|
||||||
|
- Envelope — несовместимое wire изменение, поэтому backend/frontend
|
||||||
|
поставляются согласованной парой. API version path сам по себе не делает
|
||||||
|
старый frontend совместимым.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Зафиксировать текущие DB/API invariants и measurement baseline на
|
||||||
|
изолированной базе. Добавить additive schema с revision и пустыми новыми
|
||||||
|
datasets; текущие users/sessions/connection/manual-card записи сохранить.
|
||||||
|
У существующих подключений появляется новая revision без изменения token.
|
||||||
|
2. Реализовать storage/coordinator и local owners; старые RAM snapshots
|
||||||
|
не мигрировать как доказанно полную историю. Проверить crash/rollback и
|
||||||
|
fences до включения нового read path.
|
||||||
|
3. Backend публикует OpenAPI artifact; frontend получает его явно и
|
||||||
|
регенерирует типы локально. Совместно поставляется новый UI и server
|
||||||
|
read path, включая observer semantics и terminal errors.
|
||||||
|
4. Первые пользовательские запросы заполняют данные. Startup/deploy не
|
||||||
|
запускает импорт. После подтверждённых checks обновляются as-built docs;
|
||||||
|
specs остаётся единственным SDD authority.
|
||||||
|
|
||||||
|
### Rollback Contract
|
||||||
|
|
||||||
|
| Field | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Status | `required` |
|
||||||
|
| Trigger | Cross-user/connection leak, частичная публикация финансовых данных, повреждение миграции, несовместимый API pair либо существенная регрессия latency. |
|
||||||
|
| Actions | Остановить новый worker/backend, сохранить согласованную копию SQLite, вернуть совместимую пару backend/frontend. Не запускать старую и новую write-модель одновременно. Отдельно проверить Prisma migration state; не удалять additive tables как способ отката. |
|
||||||
|
| Data safety | Сохранить новые datasets, progress и все прежние User/Session/TbankConnection/manual-card записи. Если старая версия не поддерживает новую Prisma schema, использовать совместимый rollback build либо восстановить backup в отдельной БД; нельзя перезаписать рабочую БД старой копией, потеряв новые ручные операции. |
|
||||||
|
| Verification | Проверить integrity/FK и равенство прежних пользовательских данных, невозможность публикации остановленного worker, auth/connection boundaries, согласованность API pair и повторное безопасное чтение/возобновление после возврата исправленной версии. |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Блокирующих продуктовых вопросов нет. Неизвестны измеренные latency/объём
|
||||||
|
истории и provider semantics при отсутствующем `from`/openedDate. Они не
|
||||||
|
меняют выбранный путь: явные интервалы, replay с начала, unknown lower bound
|
||||||
|
и отсутствие ложного признака полной истории. Подтверждение минимальной
|
||||||
|
даты источника может уточнить только boundary metadata; до него используется
|
||||||
|
описанный безопасный fallback. Числовые результаты будут получены задачами
|
||||||
|
проверки, а не вписаны заранее.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Повторные открытия инвестиционных экранов зависят от скорости и доступности
|
||||||
|
T-Bank, а перезапуск приложения теряет уже загруженную историю. Пользователь
|
||||||
|
согласовал [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md):
|
||||||
|
показывать сохранённые данные сразу и обновлять их в фоне только по запросу.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Все экраны T-Bank — счета, портфель, операции, доходы, инструменты,
|
||||||
|
текущие цены, свечи и выплаты — повторно используют постоянно сохранённые
|
||||||
|
данные, в том числе после перезапуска. Обычный ответ не ждёт брокера;
|
||||||
|
целевой ориентир до 500 мс ещё должен быть подтверждён измерениями.
|
||||||
|
- Запрос экрана запускает обновление отсутствующих или устаревших данных:
|
||||||
|
портфель, текущие цены и незавершённая дневная свеча — 1 минута;
|
||||||
|
текущий месяц операций и список счетов — 5 минут;
|
||||||
|
инструменты и календари выплат — сутки. Само течение времени работу не запускает.
|
||||||
|
- Полные прошлые месяцы операций и завершённые свечи сохраняются.
|
||||||
|
Пропуски догружаются при запросах; ручное обновление перечитывает показанный
|
||||||
|
период и зависимости текущего экрана, а не весь накопленный архив.
|
||||||
|
- При первой загрузке сначала доступны нужный экрану период и сравнения
|
||||||
|
доходов, затем догружается вся доступная история счёта. История бумаг
|
||||||
|
текущего портфеля загружается заранее в рамках этой работы; остальных —
|
||||||
|
при открытии инструмента. Начатая работа может завершиться после ухода.
|
||||||
|
- Пользователь различает свежесть, полноту, загрузку и ошибку обновления.
|
||||||
|
Неполный период не считается окончательным финансовым результатом или
|
||||||
|
подтверждением отсутствия операций. Ошибка сохраняет прежние данные.
|
||||||
|
- Личные данные доступны только своему пользователю в границах текущего
|
||||||
|
подключения. Старое подключение не может опубликовать поздний результат.
|
||||||
|
Существующие проверки доступа и ошибки интеграции сохраняются.
|
||||||
|
|
||||||
|
«Полнота истории счёта» используется в смысле
|
||||||
|
[глоссария](../../../../docs/glossary.md#полнота-истории-счёта): все доступные
|
||||||
|
исполненные операции периода на момент успешной сверки, включая полный пустой
|
||||||
|
период. Полнота не означает свежесть и не исключает поздние исправления.
|
||||||
|
Стабильность ID операции — принятое допущение проекта, не гарантия T-Bank;
|
||||||
|
повторно полученные поля операции могут изменяться.
|
||||||
|
|
||||||
|
**Non-goals:** периодическая синхронизация, автоматический поиск исправлений
|
||||||
|
полной старой истории, обнаружение сменившихся ID, история стоимости портфеля,
|
||||||
|
предзагрузка всего каталога, архив без подключения, новые сценарии отключения
|
||||||
|
и закрытых счетов. Эта сессия создаёт planning artifacts; реализация не выполняется.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- [specs/tbank-data-sync/spec.md](specs/tbank-data-sync/spec.md): постоянное локальное чтение, запросы
|
||||||
|
синхронизации, интервальная полнота, восстановление, общие данные инструментов,
|
||||||
|
границы подключения и проверяемые критерии быстродействия.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- [specs/brokerage-accounts/spec.md](specs/brokerage-accounts/spec.md): локальный список, сохранённые сводки,
|
||||||
|
состояния загрузки и ошибок без синхронного ожидания брокера.
|
||||||
|
- [specs/portfolio-view/spec.md](specs/portfolio-view/spec.md): постоянный последний полный снимок,
|
||||||
|
минутная свежесть и ручное обновление.
|
||||||
|
- [specs/brokerage-account-ledger/spec.md](specs/brokerage-account-ledger/spec.md): локальная пагинация и фильтрация,
|
||||||
|
полнота периода вместо обязательного перечитывания RAM-кэша.
|
||||||
|
- [specs/brokerage-account-income-analytics/spec.md](specs/brokerage-account-income-analytics/spec.md): полнота выбранного периода
|
||||||
|
и сравнений, сохранение результата, идентичность повторно загруженных операций.
|
||||||
|
- [specs/brokerage-account-upcoming-events/spec.md](specs/brokerage-account-upcoming-events/spec.md): прогноз из сохранённых
|
||||||
|
источников с видимой полнотой и ошибкой обновления.
|
||||||
|
- [specs/frontend-data-resilience/spec.md](specs/frontend-data-resilience/spec.md): различимые состояния серверного
|
||||||
|
сохранённого результата и клиентского кэша на всех экранах T-Bank.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Изменение затрагивает пользователей всех инвестиционных экранов, серверное
|
||||||
|
хранение и получение данных, публичный контракт состояний, интерфейс обновления
|
||||||
|
и объясняющую документацию. Финансовые правила, валютная точность, значение
|
||||||
|
прогнозов и изоляция пользователей сохраняются. Основные specs и чужие active
|
||||||
|
changes не изменяются до отдельного завершения lifecycle этого change.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
## 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: Period comparisons preserve calendar and currency meaning
|
||||||
|
|
||||||
|
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Только сравнение с подтверждённым полным покрытием обоих периодов MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
|
||||||
|
|
||||||
|
Отсутствие ещё не загруженной истории сравнения MUST отличаться от невозможности сравнения по календарю или доступной истории счёта. Система MUST сообщать загрузку, неполноту либо ошибку каждого периода сравнения и MUST NOT подменять их нулевыми суммами или денежными разницами. Полный основной период MAY отображаться при отдельно обозначенной незавершённой загрузке сравнения; его значения MUST NOT смешиваться с частичным сравнением.
|
||||||
|
|
||||||
|
#### Scenario: Сравнение полного календарного месяца
|
||||||
|
|
||||||
|
- **WHEN** User применяет период от первого до последнего дня календарного месяца
|
||||||
|
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
|
||||||
|
|
||||||
|
#### Scenario: Сравнение произвольного периода
|
||||||
|
|
||||||
|
- **WHEN** User применяет период, не являющийся полным календарным месяцем
|
||||||
|
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
|
||||||
|
|
||||||
|
#### Scenario: Доступно сравнение год к году
|
||||||
|
|
||||||
|
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
|
||||||
|
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
|
||||||
|
|
||||||
|
#### Scenario: Годовая история недоступна
|
||||||
|
|
||||||
|
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
|
||||||
|
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
|
||||||
|
|
||||||
|
#### Scenario: Основной период полон, сравнение ещё загружается
|
||||||
|
- **WHEN** выбранный период имеет подтверждённое полное покрытие, а необходимый предыдущий период ещё загружается
|
||||||
|
- **THEN** система возвращает полный итог выбранного периода, отмечает загрузку сравнения и не возвращает для него окончательные суммы или разницы
|
||||||
|
|
||||||
|
#### Scenario: Сравнение догружено после основного периода
|
||||||
|
- **WHEN** все необходимые интервалы периода сравнения успешно завершены
|
||||||
|
- **THEN** следующее локальное чтение возвращает его точные валютные суммы и разницы без ожидания T-Bank
|
||||||
|
|
||||||
|
### Requirement: Realized analytics never presents an incomplete result as complete
|
||||||
|
|
||||||
|
Система MUST формировать окончательные totals, trend buckets, instrument breakdown и drill-down выбранного периода только по подтверждённому полному покрытию его исполненных операций. Каждое comparison MUST отдельно требовать полного покрытия своих исходных периодов. Повторное локальное чтение полного периода MUST NOT ожидать повторного получения cursor pages T-Bank. Ошибка любой необходимой страницы фоновой загрузки MUST сохранять подтверждённый ранее результат и покрытие, фиксировать восстанавливаемую ошибку синхронизации и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Если полный выбранный результат ещё не получен, envelope MUST возвращать `data: null` с отсутствующим либо частичным покрытием в `meta`, а интерфейс MUST показывать незавершённую загрузку либо её ошибку без окончательных финансовых итогов. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
|
||||||
|
|
||||||
|
#### Scenario: Все страницы операций успешно получены
|
||||||
|
|
||||||
|
- **WHEN** для покрытия расчёта требуется несколько cursor pages и каждая страница успешно сохранена с подтверждением окончания диапазона
|
||||||
|
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
|
||||||
|
|
||||||
|
#### Scenario: Одна из страниц операций недоступна
|
||||||
|
|
||||||
|
- **WHEN** получение любой необходимой cursor page завершается ошибкой
|
||||||
|
- **THEN** ответ сообщает ошибку синхронизации, а страница сохраняет ранее полный результат с прежним временем либо показывает ошибку незавершённой загрузки без окончательных частичных итогов
|
||||||
|
|
||||||
|
#### Scenario: В периоде нет купонов и дивидендов
|
||||||
|
|
||||||
|
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
|
||||||
|
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
|
||||||
|
|
||||||
|
#### Scenario: Частичная загрузка пока не нашла дохода
|
||||||
|
- **WHEN** только часть операций выбранного периода получена и в этой части нет купонов или дивидендов
|
||||||
|
- **THEN** система не возвращает окончательный нулевой результат и сообщает неполноту периода
|
||||||
|
|
||||||
|
#### Scenario: Ошибка ручной сверки полного периода
|
||||||
|
- **WHEN** ранее полный период перечитывается вручную и промежуточная страница не получена
|
||||||
|
- **THEN** прежний полный результат и время его успешной сверки остаются доступными с ошибкой обновления, а частичная новая выборка не заменяет их
|
||||||
|
|
||||||
|
### Requirement: Account overview has an independent current-month income summary
|
||||||
|
|
||||||
|
Обзор выбранного Brokerage Account MUST показывать компактный source-currency summary «Получено купонами и дивидендами» за период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month период и MUST NOT наследовать последний пользовательский период страницы аналитики. Пустой или неуспешный расчёт summary MUST оставаться локальным состоянием блока и MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
|
||||||
|
|
||||||
|
Summary MUST использовать тот же локальный источник операций и правило полноты, что и полная аналитика. До завершения требуемого текущего периода он MUST показывать состояние загрузки или неполноты без окончательного нулевого дохода. Ошибка фонового обновления MUST сохранять ранее полный summary с прежним временем и безопасным предупреждением.
|
||||||
|
|
||||||
|
#### Scenario: Обзор показывает доход текущего месяца
|
||||||
|
|
||||||
|
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
|
||||||
|
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике
|
||||||
|
|
||||||
|
#### Scenario: Пользовательский период не влияет на обзор
|
||||||
|
|
||||||
|
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
|
||||||
|
- **THEN** summary обзора по-прежнему рассчитывается от первого дня текущего месяца по сегодняшний день
|
||||||
|
|
||||||
|
#### Scenario: Current-month summary недоступен
|
||||||
|
|
||||||
|
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
|
||||||
|
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
|
||||||
|
|
||||||
|
#### Scenario: Текущий месяц ещё не загружен полностью
|
||||||
|
- **WHEN** обзор доступен, но текущий месяц операций имеет пропуски
|
||||||
|
- **THEN** блок дохода обозначает неполноту или загрузку, не выдаёт частичный доход за окончательный и не скрывает Portfolio
|
||||||
|
|
||||||
|
### Requirement: Income payments remain traceable to exact Ledger Entries
|
||||||
|
|
||||||
|
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки как самостоятельные Ledger Entries и MUST отражать одну запись на идентичность User, Brokerage Account, текущее подключение и operation ID; повторная загрузка того же ID MUST обновлять поля и MUST NOT удваивать выплату. Одинаковые operation IDs разных счетов MUST оставаться отдельными записями. Список MUST NOT включать записи вне аналитического итога. Отказ от группировочного UI MUST NOT отменять проверки принадлежности entries к instrumentKey, валюты и receipt count ответа или приводить к пересчёту денежных итогов на клиенте.
|
||||||
|
|
||||||
|
#### Scenario: Каждая выплата показана отдельной карточкой
|
||||||
|
|
||||||
|
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
|
||||||
|
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
|
||||||
|
|
||||||
|
#### Scenario: Список воспроизводит точный состав дохода
|
||||||
|
|
||||||
|
- **WHEN** User просматривает загруженный раздел «Выплаты»
|
||||||
|
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
|
||||||
|
|
||||||
|
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
|
||||||
|
|
||||||
|
- **WHEN** доход содержит отрицательную корректировку, повторно загруженный ID того же счёта либо одинаковые operation IDs разных счетов
|
||||||
|
- **THEN** корректировка отображается с исходным знаком; одна операция того же счёта имеет одну карточку с актуальными полями, а операции разных счетов остаются отдельными карточками
|
||||||
|
|
||||||
|
#### Scenario: Валюта видна в сумме каждой выплаты
|
||||||
|
|
||||||
|
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
|
||||||
|
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
## 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** в сохранённой части периода нет записей, но покрытие периода не завершено
|
||||||
|
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
|
||||||
|
|
||||||
|
### Requirement: User can browse a filtered ledger
|
||||||
|
Система MUST позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система MUST фильтровать сохранённые операции локально, не перечитывая T-Bank из-за категории, instrument UID, размера страницы или перехода между страницами полного периода. Система MUST разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
|
||||||
|
|
||||||
|
Cursor MUST быть привязан к User, текущему подключению, составу счетов, применённым фильтрам, зафиксированной верхней границе периода и версии читаемых данных. Курсор другой границы или изменившейся версии MUST отклоняться существующим `INVALID_OPERATIONS_CURSOR`, позволяя повторить первую страницу с прежними фильтрами. Доступность следующей локальной страницы MUST NOT заменять признак полноты периода. Равнозначные порядок и дубли categories MUST NOT создавать другую идентичность запроса.
|
||||||
|
|
||||||
|
#### Scenario: Фильтрация доходных операций за период
|
||||||
|
- **WHEN** User запрашивает период дат и категорию income
|
||||||
|
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
|
||||||
|
|
||||||
|
#### Scenario: Продолжение истории
|
||||||
|
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала, и версия данных не изменилась
|
||||||
|
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
|
||||||
|
|
||||||
|
#### Scenario: Обновление изменило данные между страницами
|
||||||
|
- **WHEN** после выдачи первой страницы успешная синхронизация изменила её набор операций и User передаёт прежний cursor
|
||||||
|
- **THEN** система возвращает `INVALID_OPERATIONS_CURSOR`, а повторная загрузка начинает первую страницу с сохранёнными применёнными фильтрами без пропуска или дублирования записей
|
||||||
|
|
||||||
|
#### Scenario: Полный период фильтруется локально
|
||||||
|
- **WHEN** User меняет категории или страницы в полностью сохранённом прошлом периоде
|
||||||
|
- **THEN** система формирует результат из сохранённых операций без нового запроса операций T-Bank
|
||||||
|
|
||||||
|
### Requirement: Ledger entries preserve broker operation meaning
|
||||||
|
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение.
|
||||||
|
|
||||||
|
В пределах текущего подключения идентичность Ledger Entry MUST включать User, Brokerage Account и upstream operation ID. Повторное получение того же ID MUST обновлять доступные поля этой операции без создания второй записи; одинаковые ID разных счетов MUST оставаться разными записями. Стабильность ID MUST считаться допущением проекта, а не гарантией T-Bank.
|
||||||
|
|
||||||
|
#### Scenario: Отображение купонной выплаты
|
||||||
|
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
|
||||||
|
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
|
||||||
|
|
||||||
|
#### Scenario: Отображение комиссии сделки без синтетической записи
|
||||||
|
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
|
||||||
|
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
|
||||||
|
|
||||||
|
#### Scenario: Отображение движения бумаг без денег
|
||||||
|
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
|
||||||
|
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
|
||||||
|
|
||||||
|
#### Scenario: Доступна визуальная идентичность инструмента
|
||||||
|
- **WHEN** для инструмента исполненной операции доступна presentation metadata бренда
|
||||||
|
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
|
||||||
|
|
||||||
|
#### Scenario: Повторная операция содержит исправленные поля
|
||||||
|
- **WHEN** успешная синхронизация получает операцию с уже сохранённым ID того же счёта и изменёнными полями
|
||||||
|
- **THEN** последующее чтение возвращает одну запись с обновлёнными полями без удвоения денежного движения
|
||||||
|
|
||||||
|
### Requirement: Account overview includes recent ledger events
|
||||||
|
Обзор счёта MUST показывать не более пяти последних сохранённых исполненных записей журнала этого счёта с состоянием полноты и загрузки требуемого периода и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Отображение последних событий
|
||||||
|
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
|
||||||
|
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
|
||||||
|
|
||||||
|
#### Scenario: Последние записи недоступны
|
||||||
|
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
|
||||||
|
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
|
||||||
|
|
||||||
|
#### Scenario: Последняя запись не содержит visual metadata инструмента
|
||||||
|
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
|
||||||
|
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
|
||||||
|
|
||||||
|
#### Scenario: Первичная история обзора неполна
|
||||||
|
- **WHEN** обзор получил только часть требуемых операций или `data: null`
|
||||||
|
- **THEN** блок обозначает загрузку или неполноту и не утверждает, что показанные записи окончательны либо операции отсутствуют
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
|
||||||
|
|
||||||
|
**Reason:** Постоянное хранение операций и запросная синхронизация заменяют обязательный RAM-кэш на 100 страниц с TTL 30 секунд; его запрет на выдачу старой страницы противоречит локальному чтению сохранённой истории.
|
||||||
|
|
||||||
|
**Migration:** Чтение, свежесть, объединение загрузок и сохранение прогресса переходят к `tbank-data-sync`; локальная фильтрация и привязанный к версии данных cursor регулируются `User can browse a filtered ledger`. Существующие transient cursors не являются доказательством полноты и при несовместимости возвращают `INVALID_OPERATIONS_CURSOR`.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: User can retrieve upcoming events for an account
|
||||||
|
Система MUST предоставлять аутентифицированному User read-only список известных будущих событий выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего T-Bank connection User. Параметры `from` и `to` MUST сохранять существующий контракт: необязательные строгие календарные даты `YYYY-MM-DD` с включительными границами в UTC. Без `from` начало MUST соответствовать текущей календарной дате UTC; без `to` конец MUST соответствовать дате `from` плюс 30 календарных дней. При `from` позже `to` система MUST сохранять существующий отказ `INVALID_EVENTS_PERIOD`, не подменяя период. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта, события MUST сохранять принадлежность счёту. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`; доступный прогноз в `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза.
|
||||||
|
|
||||||
|
Список будущих событий является прогнозом по последнему успешно сохранённому наблюдаемому состоянию счёта и MUST NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection.
|
||||||
|
|
||||||
|
Система MUST формировать прогноз из сохранённых позиций и календарей, не ожидая T-Bank при локальном чтении и подтверждении доступа. Свежесть и загрузка зависимостей MUST быть видимы. Отсутствующий подтверждённый результат MUST возвращаться как `data: null` с состоянием загрузки, а не пустой список. Полнота локальной загрузки поддерживаемых данных MUST NOT означать полноту информации внешнего источника обо всех возможных событиях.
|
||||||
|
|
||||||
|
#### Scenario: Получение событий доступного счёта
|
||||||
|
- **WHEN** аутентифицированный User запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection, без явного периода
|
||||||
|
- **THEN** система возвращает известные события от сегодняшней даты UTC до даты через 30 календарных дней включительно и момент, на который был выполнен расчёт
|
||||||
|
|
||||||
|
#### Scenario: Отсутствие событий в периоде
|
||||||
|
- **WHEN** все необходимые данные применённого периода успешно сохранены и для счёта в нём нет известных событий
|
||||||
|
- **THEN** система возвращает успешный пустой список и момент расчёта
|
||||||
|
|
||||||
|
#### Scenario: Отклонение недоступного счёта
|
||||||
|
- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
|
||||||
|
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
|
||||||
|
|
||||||
|
#### Scenario: Недопустимый период сохраняет ошибку
|
||||||
|
- **WHEN** User передаёт `from` позже `to` для выбранного счёта либо режима `all`
|
||||||
|
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD`, не запускает загрузку другого периода и не возвращает его прогноз
|
||||||
|
|
||||||
|
#### Scenario: Прогноз ещё не может быть рассчитан
|
||||||
|
- **WHEN** позиции либо необходимый поддерживаемый календарь ещё не сохранены полностью
|
||||||
|
- **THEN** система возвращает `200` с `data: null` и состоянием запрошенной загрузки или её ошибки вместо утверждения об отсутствии событий
|
||||||
|
|
||||||
|
### Requirement: Forecasts preserve event meaning and uncertainty
|
||||||
|
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного успешно сохранённого Portfolio Snapshot, использованного при последнем успешном расчёте показываемого прогноза, с указанием времени этого наблюдения. При неудачном обновлении сохранённый прогноз MUST сохранять эту исходную зависимость и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
|
||||||
|
|
||||||
|
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
|
||||||
|
|
||||||
|
#### Scenario: Прогноз купона по текущей позиции
|
||||||
|
- **WHEN** поддерживаемая облигация в текущей Portfolio Position имеет известный купон в пределах периода
|
||||||
|
- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций
|
||||||
|
|
||||||
|
#### Scenario: Прогноз дивиденда
|
||||||
|
- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода
|
||||||
|
- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций
|
||||||
|
|
||||||
|
#### Scenario: Отменённый дивиденд не попадает в прогноз
|
||||||
|
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
|
||||||
|
- **THEN** система не добавляет такое событие в результат
|
||||||
|
|
||||||
|
#### Scenario: Отображение оферты без суммы
|
||||||
|
- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны
|
||||||
|
- **THEN** система возвращает событие offer с прогнозной суммой `null`
|
||||||
|
|
||||||
|
#### Scenario: Неподдерживаемое частичное погашение
|
||||||
|
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
|
||||||
|
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
|
||||||
|
|
||||||
|
### Requirement: Upcoming events use complete source results
|
||||||
|
Система MUST использовать последний прогноз по полным успешно полученным необходимым источникам и MUST NOT возвращать частичный список как полный результат. Если обновление необходимых данных брокера или MOEX неуспешно, система MUST сохранять прежний подтверждённый прогноз и время его расчёта, возвращая `200` с ошибкой синхронизации в `meta`. Если полного прогноза ещё нет, ответ MUST содержать `data: null` и отсутствующее либо частичное покрытие с ошибкой или состоянием загрузки. Подтверждённые ошибки сессии, текущего подключения и доступа MUST сохранять существующие Problem Details и MUST NOT маскироваться прогнозом. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
|
||||||
|
|
||||||
|
#### Scenario: Недоступность источника
|
||||||
|
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен
|
||||||
|
- **THEN** система сообщает ошибку обновления, сохраняя ранее полный прогноз и время его расчёта либо `data: null`, без выдачи частичного списка за полный
|
||||||
|
|
||||||
|
#### Scenario: Инструмент без поддерживаемых данных
|
||||||
|
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
|
||||||
|
- **THEN** система не добавляет для него синтетическую строку в результат
|
||||||
|
|
||||||
|
#### Scenario: MOEX отвечает без запрошенного блока
|
||||||
|
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
|
||||||
|
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой
|
||||||
|
|
||||||
|
### Requirement: Account page presents an upcoming events list
|
||||||
|
Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST сохранять существующий выбор и явное применение периода через `from`/`to` и показывать все полученные события применённого периода компактным списком без пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника.
|
||||||
|
|
||||||
|
Страница MUST показывать время сохранённого прогноза, состояние свежести и загрузки, а также действие «Обновить» для текущего применённого периода и его зависимостей. Неполученный результат MUST отличаться от подтверждённого пустого списка. Временная ошибка обновления MUST NOT скрывать сохранённый полный прогноз.
|
||||||
|
|
||||||
|
#### Scenario: Просмотр ближайших событий
|
||||||
|
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account без явного периода
|
||||||
|
- **THEN** он видит отсортированный список событий периода по умолчанию от сегодняшней даты UTC до даты через 30 календарных дней включительно, где offer обозначен без суммы
|
||||||
|
|
||||||
|
#### Scenario: Пустое состояние страницы
|
||||||
|
- **WHEN** запрос будущих событий возвращает подтверждённый полный пустой список в `data`
|
||||||
|
- **THEN** страница сообщает, что за применённый период событий не найдено, и объясняет ограничения покрытия источника
|
||||||
|
|
||||||
|
#### Scenario: Обновление применённого периода
|
||||||
|
- **WHEN** User применил допустимый период будущих событий и нажал «Обновить»
|
||||||
|
- **THEN** обновляются необходимые позиции и календари для применённого периода, его фильтры сохраняются и обновление не подменяется периодом по умолчанию
|
||||||
|
|
||||||
|
#### Scenario: Ошибка страницы
|
||||||
|
- **WHEN** фоновое обновление будущих событий завершается временной ошибкой
|
||||||
|
- **THEN** страница сохраняет прежний полный прогноз с его временем и предупреждением либо показывает ошибку первичной загрузки при его отсутствии, не выдаёт частичный список за полный и предоставляет действие повторить обновление
|
||||||
|
|
||||||
|
### Requirement: Account overview includes upcoming events
|
||||||
|
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Отображение ближайших событий
|
||||||
|
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
|
||||||
|
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
|
||||||
|
|
||||||
|
#### Scenario: Ближайшие события недоступны
|
||||||
|
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
|
||||||
|
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления сохранённого прогноза обзора
|
||||||
|
- **WHEN** блок обзора уже показывает полный прогноз, а его обновление временно неуспешно
|
||||||
|
- **THEN** блок сохраняет показанные события и время расчёта с локальным предупреждением, не скрывая Portfolio
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Список активных брокерских счетов
|
||||||
|
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий envelope `{ data, meta }`, где `data` содержит сохранённый массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
|
||||||
|
|
||||||
|
Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если успешная сводка Portfolio конкретного доступного счёта ещё не сохранена, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`.
|
||||||
|
|
||||||
|
Список Brokerage Account и его сводки постоянно сохраняются и являются результатом последнего успешного наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
|
||||||
|
|
||||||
|
Обычный запрос MUST читать сохранённый список и доступные сводки без ожидания T-Bank. Срок свежести списка MUST составлять пять минут; сводки MUST следовать минутной свежести Portfolio. Отсутствующий список MUST возвращаться как `200` с `data: null`, `meta.availability: missing` и состоянием запрошенной загрузки. Только успешно полученный полный пустой список MUST возвращаться как `data: []` с `meta.availability: complete`. Время, свежесть и состояние загрузки MUST передаваться по контракту `tbank-data-sync`. Отсутствие либо ошибка загрузки сводки MUST отличаться от отсутствия самого счёта; сохранённая успешная сводка MUST оставаться доступной после временной ошибки её обновления.
|
||||||
|
|
||||||
|
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
|
||||||
|
- **WHEN** User с действующим T-Bank connection запрашивает список, а последняя успешная синхронизация сохранила открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
|
||||||
|
- **THEN** система отвечает `200` с массивом брокерского счёта и ИИС в `data` в исходном порядке T-Bank без дебетового счёта
|
||||||
|
|
||||||
|
#### Scenario: Пустой список поддерживаемых счетов
|
||||||
|
- **WHEN** User с действующим T-Bank connection имеет сохранённый успешно подтверждённый пустой список открытых поддерживаемых инвестиционных счетов
|
||||||
|
- **THEN** система отвечает `200` с `data: []` и `meta.availability: complete`
|
||||||
|
|
||||||
|
#### Scenario: Пустое название получает fallback
|
||||||
|
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
|
||||||
|
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
|
||||||
|
|
||||||
|
#### Scenario: Дата открытия отсутствует
|
||||||
|
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
|
||||||
|
- **THEN** система отвечает этим счётом с `openedOn: null`
|
||||||
|
|
||||||
|
#### Scenario: Мультивалютная сводка счёта
|
||||||
|
- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
|
||||||
|
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
|
||||||
|
|
||||||
|
#### Scenario: Сводка одного счёта временно недоступна
|
||||||
|
- **WHEN** список доступных счетов получен, но Portfolio одного из них ещё не сохранён и его загрузка временно недоступна
|
||||||
|
- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка
|
||||||
|
|
||||||
|
#### Scenario: Доступный Portfolio пуст
|
||||||
|
- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков
|
||||||
|
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||||
|
|
||||||
|
#### Scenario: Список ещё не загружен
|
||||||
|
- **WHEN** User впервые запрашивает список текущего подключения и успешного сохранённого результата ещё нет
|
||||||
|
- **THEN** система возвращает `200` с `data: null`, состоянием загрузки в `meta`, запрашивает фоновую синхронизацию и не выдаёт отсутствие данных за отсутствие счетов
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления сохранённой сводки
|
||||||
|
- **WHEN** обновление Portfolio одного счёта временно неуспешно и его успешная сводка уже сохранена
|
||||||
|
- **THEN** список сохраняет эту сводку с прежним временем и видимым состоянием ошибки обновления вместо `null`
|
||||||
|
|
||||||
|
### Requirement: Состояния недоступности списка счетов
|
||||||
|
Система MUST возвращать последний полностью сохранённый список текущего подключения без синхронной проверки T-Bank. Неполный ответ обновления MUST NOT заменять полный список. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если для текущего подключения уже подтверждено отклонение token как неаутентифицированного или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При временной либо иной upstream-ошибке фонового обновления система MUST сохранять прежний список и время его успешной загрузки, при наблюдении завершённой попытки возвращая `200` с `meta.sync.state: failed` и безопасным `meta.sync.errorCode`. Обычный запрос экрана либо ручное обновление MAY запросить новую попытку согласно `tbank-data-sync`; наблюдение статуса MUST NOT запускать её. При отсутствии сохранённого списка `data` MUST оставаться `null`. Ошибка обновления MUST NOT менять время успешной загрузки и сама по себе MUST NOT делать ещё свежие данные устаревшими; свежесть MUST продолжать определяться сроком соответствующего источника. Подтверждённая недействительность текущего token MUST запрещать выдачу личных данных, включая сохранённый список; локальная сессия и текущая граница подключения MUST проверяться до чтения.
|
||||||
|
|
||||||
|
#### Scenario: Подключение отсутствует
|
||||||
|
- **WHEN** User без сохранённого T-Bank connection запрашивает список
|
||||||
|
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
|
||||||
|
|
||||||
|
#### Scenario: Сохранённый token больше не действителен
|
||||||
|
- **WHEN** фоновая проверка подтвердила, что T-Bank отклоняет token текущего подключения как неаутентифицированный или без прав, и User снова запрашивает список
|
||||||
|
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection
|
||||||
|
|
||||||
|
#### Scenario: T-Bank временно недоступен
|
||||||
|
- **WHEN** фоновое обновление списка завершается временной либо иной upstream-ошибкой
|
||||||
|
- **THEN** последующее наблюдение результата без нового запуска отвечает `200` с последним сохранённым списком либо `data: null`, `meta.sync.state: failed` и `meta.sync.errorCode: TBANK_UNAVAILABLE`, не подтверждая новую успешную загрузку
|
||||||
|
|
||||||
|
### Requirement: Экран списка брокерских счетов
|
||||||
|
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` и предоставлять действие «Обновить», без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать локальный результат и состояние синхронизации; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. Клиент MUST читать список из `data`, показывать свежесть и состояние синхронизации из `meta` и MUST NOT считать успешный HTTP-ответ с `data: null` пустым списком. Ручное обновление MUST обходить проверку давности списка и показанных сводок, не обновляя всю историю счетов. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
|
||||||
|
|
||||||
|
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
|
||||||
|
|
||||||
|
#### Scenario: Пользователь открывает счёт из списка
|
||||||
|
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
|
||||||
|
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
|
||||||
|
|
||||||
|
#### Scenario: Список пуст
|
||||||
|
- **WHEN** клиент получает `data: []` с `meta.availability: complete`
|
||||||
|
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Список берётся из свежего frontend-кэша
|
||||||
|
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
|
||||||
|
- **THEN** клиент показывает сохранённые счета без нового API-запроса
|
||||||
|
|
||||||
|
#### Scenario: Устаревший список загружается повторно
|
||||||
|
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
|
||||||
|
- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление
|
||||||
|
|
||||||
|
#### Scenario: Ошибка фонового обновления не скрывает snapshot
|
||||||
|
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
|
||||||
|
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки
|
||||||
|
|
||||||
|
#### Scenario: Карточка ограничивает валютные итоги
|
||||||
|
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
|
||||||
|
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
|
||||||
|
|
||||||
|
#### Scenario: Недоступность сводки локальна карточке
|
||||||
|
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
|
||||||
|
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
|
||||||
|
|
||||||
|
#### Scenario: Пустая сводка не является ошибкой
|
||||||
|
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||||
|
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
|
||||||
|
|
||||||
|
#### Scenario: Начальная загрузка не становится пустым списком
|
||||||
|
- **WHEN** клиент получает `data: null` со статусом запрошенной синхронизации
|
||||||
|
- **THEN** он показывает состояние первичной загрузки или её ошибки, а не сообщение об отсутствии инвестиционных счетов
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление списка
|
||||||
|
- **WHEN** User нажимает «Обновить» на экране списка счетов
|
||||||
|
- **THEN** клиент запрашивает обновление списка и показанных сводок независимо от давности, сохраняя доступные данные и отображая прогресс
|
||||||
|
|
||||||
|
### Requirement: Клиентские состояния ошибки списка счетов
|
||||||
|
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Client Cached Snapshot. Для временной и неизвестной ошибки без snapshot клиент MUST показать ошибку без списка; при наличии snapshot он MUST сохранить его видимым с безопасным предупреждением. `meta.sync.state: failed` MUST отображаться как ошибка фоновой синхронизации, отдельно от ошибки HTTP-запроса; успешный HTTP-ответ сам по себе MUST NOT скрывать такую ошибку.
|
||||||
|
|
||||||
|
#### Scenario: Подключение требуется
|
||||||
|
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
|
||||||
|
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Известная ошибка не повторяется автоматически
|
||||||
|
- **WHEN** запрос списка завершается известной ошибкой либо сервер сообщает завершённую неуспешную синхронизацию
|
||||||
|
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
|
||||||
|
|
||||||
|
#### Scenario: Token требует внимания
|
||||||
|
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
|
||||||
|
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Временная ошибка T-Bank
|
||||||
|
- **WHEN** клиент получает `TBANK_UNAVAILABLE` и сохранённого списка нет
|
||||||
|
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Неизвестная ошибка загрузки
|
||||||
|
- **WHEN** клиент получает ошибку без известного кода списка счетов и сохранённого списка нет
|
||||||
|
- **THEN** клиент не показывает список и предлагает только повторить загрузку
|
||||||
|
|
||||||
|
#### Scenario: Известная ошибка при сохранённом snapshot
|
||||||
|
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` через HTTP-ошибку либо `meta.sync.errorCode`
|
||||||
|
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
|
||||||
|
Frontend MUST сохранять Client Cached Snapshot на всех экранах с данными T-Bank: список Brokerage Accounts, Portfolio, операции, доходы, инструменты, котировки, свечи и выплаты. Снимок MUST сохраняться для того же пользователя, текущего подключения и набора данных, если ранее получен успешный результат с непустым `data`. Подтверждённые `data: []` или пустой полный Portfolio также MUST считаться успешным результатом. Начальная загрузка без такого ответа может показывать блокирующее
|
||||||
|
состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время
|
||||||
|
фонового обновления и после его временной ошибки; он отличается от постоянно сохранённых серверных данных и MUST NOT изменять поле `Portfolio.isStale`. Клиент MUST независимо показывать свежесть серверного результата, полноту требуемых периодов, ход синхронизации и ошибку HTTP-refetch. Успешный HTTP-ответ с `meta.sync.state: failed` MUST NOT представляться успешным обновлением источника. Поля `meta.lastSuccessfulAt`, `meta.availability`, `meta.freshness`, `meta.sync` и при наличии `meta.coverage` MUST определять эти состояния по `tbank-data-sync`; клиент MUST NOT выводить их из возраста своего query-кэша. Ошибка ручного обновления ещё свежего источника MUST показываться отдельно от его свежести: клиент MUST NOT объявлять данные устаревшими до истечения серверного TTL либо изменять исходное успешное время.
|
||||||
|
|
||||||
|
Состояние `data: null` MUST показывать отсутствие загруженного результата и ход первичной загрузки либо её ошибку. Неполный период MUST явно отличаться от полного и MUST NOT представляться окончательным финансовым итогом или подтверждённым отсутствием операций. Неполное сравнение доходов MUST иметь собственное состояние и MUST NOT подменяться нулём. Статусы MUST быть доступны текстом, не только цветом; действия обновления и повтора MUST быть доступны с клавиатуры и сохранять читаемость на узком экране.
|
||||||
|
|
||||||
|
Действие «Обновить» MUST обходить серверную проверку давности для текущего экрана, его применённого периода и зависимостей, сохраняя доступные данные и фильтры. Обычное повторное получение HTTP-ответа MUST NOT считаться ручным принудительным обновлением. Наблюдение за уже запрошенной работой MUST прекращаться после её завершения, ошибки или ухода с экрана, MUST NOT запускать новую синхронизацию и MUST NOT создавать периодическое обновление источника. Уже начатая серверная работа MAY завершиться после ухода.
|
||||||
|
|
||||||
|
При подтверждённом `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступности счёта либо изменении пользователя/подключения frontend MUST скрыть личный Snapshot прежней границы. Поздние ответы данных и статусов прежнего подключения MUST NOT попадать в новое состояние; обычная временная ошибка MUST NOT менять эту границу.
|
||||||
|
|
||||||
|
#### Scenario: Список Brokerage Accounts обновляется в фоне
|
||||||
|
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается
|
||||||
|
фоновое обновление того же списка
|
||||||
|
- **THEN** frontend оставляет список видимым и показывает только
|
||||||
|
неблокирующий индикатор обновления
|
||||||
|
|
||||||
|
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
|
||||||
|
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot
|
||||||
|
Portfolio, а последующее обновление этого же Portfolio завершается временной ошибкой
|
||||||
|
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное
|
||||||
|
неблокирующее предупреждение о показе последних полученных данных и
|
||||||
|
предоставляет повторную попытку обновления
|
||||||
|
|
||||||
|
#### Scenario: Server-side fallback и client cached snapshot различаются
|
||||||
|
- **WHEN** backend возвращает Portfolio с `isStale` либо фоновое обновление
|
||||||
|
Client Cached Snapshot завершается временной ошибкой
|
||||||
|
- **THEN** frontend отображает соответствующее server-side либо client-side
|
||||||
|
предупреждение независимо, не представляя один вид состояния другим
|
||||||
|
|
||||||
|
#### Scenario: Серверная синхронизация не завершена после успешного HTTP-ответа
|
||||||
|
- **WHEN** экран получает `200` с сохранёнными данными и `meta.sync.state: running`
|
||||||
|
- **THEN** он показывает данные и прежнее успешное время вместе с состоянием обновления, не считая HTTP-успех завершением синхронизации
|
||||||
|
|
||||||
|
#### Scenario: Первичная загрузка без данных
|
||||||
|
- **WHEN** любой экран T-Bank получает `data: null` с отсутствующим либо частичным покрытием
|
||||||
|
- **THEN** он показывает загрузку либо её ошибку без вымышленных нулей, окончательных итогов и утверждения об отсутствии операций
|
||||||
|
|
||||||
|
#### Scenario: Ошибка синхронизации при успешном HTTP-ответе
|
||||||
|
- **WHEN** экран получает `200` с сохранёнными данными, `meta.sync.state: failed` и безопасным кодом ошибки
|
||||||
|
- **THEN** данные и время их успешного получения остаются видимыми с отдельным предупреждением об ошибке обновления и действием повтора
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления не подменяет свежесть
|
||||||
|
- **WHEN** экран получает ещё свежие данные с `meta.freshness: fresh` и `meta.sync.state: failed` после ручного обновления
|
||||||
|
- **THEN** он показывает ошибку обновления и сохранённое успешное время без предупреждения об устаревших данных только из-за этой ошибки
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление сохраняет текущий контекст
|
||||||
|
- **WHEN** User нажимает «Обновить» на экране с выбранным счётом, периодом и фильтрами
|
||||||
|
- **THEN** клиент запрашивает принудительное обновление этого экрана и зависимостей, сохраняет контекст и доступные данные и не запрашивает обновление всей накопленной истории
|
||||||
|
|
||||||
|
#### Scenario: Недействительный token скрывает личный снимок
|
||||||
|
- **WHEN** источник подтвердил недействительность текущего token и клиент получает `INVALID_TBANK_TOKEN` при наличии сохранённого личного Snapshot
|
||||||
|
- **THEN** клиент скрывает личные данные и показывает существующее действие настройки подключения вместо маскировки отказа устаревшими данными
|
||||||
|
|
||||||
|
#### Scenario: Наблюдение не превращается в расписание
|
||||||
|
- **WHEN** запрошенная синхронизация завершилась, завершилась ошибкой либо User ушёл со страницы
|
||||||
|
- **THEN** клиент прекращает наблюдение и не начинает новые периодические синхронизации; уже начатая серверная работа может завершиться
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Получение Portfolio выбранного Brokerage Account
|
||||||
|
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }` по контракту `tbank-data-sync`; сохранённый полный Snapshot в `data` MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
|
||||||
|
|
||||||
|
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Система MUST сохранять последний полный Snapshot постоянно, без накопления исторических снимков стоимости. Это MUST NOT подтверждать доступность Brokerage Account вне текущего T-Bank connection. Проверка локальной сессии, текущего подключения и сохранённого состава доступных счетов MUST выполняться без синхронного запроса списка счетов T-Bank. При отсутствии сохранённого Snapshot ответ MUST иметь `200`, `data: null`, `meta.availability: missing` и состояние запрошенной загрузки; это MUST NOT обозначать пустой Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Получение текущего Portfolio
|
||||||
|
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён и ещё свеж
|
||||||
|
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data` без ожидания T-Bank с `isStale: false` и временем успешного получения финансового снимка
|
||||||
|
|
||||||
|
#### Scenario: Недоступный Brokerage Account
|
||||||
|
- **WHEN** для текущего подключения подтверждено, что `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен
|
||||||
|
- **THEN** система отвечает `404` без Portfolio Snapshot
|
||||||
|
|
||||||
|
#### Scenario: Сохранённый Portfolio читается после перезапуска
|
||||||
|
- **WHEN** backend перезапущен, текущий connection не изменился и полный Snapshot уже сохранён
|
||||||
|
- **THEN** запрос возвращает его из постоянного хранилища без ожидания T-Bank и отдельно сообщает свежесть и состояние фонового обновления
|
||||||
|
|
||||||
|
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||||
|
Система MUST постоянно сохранять последний полный успешный Portfolio Snapshot отдельно для User, Brokerage Account и текущего T-Bank connection. Обычный запрос MUST возвращать сохранённый Snapshot без ожидания T-Bank; при возрасте не менее одной минуты он MUST запросить фоновое обновление. Временная ошибка MUST NOT удалять Snapshot или ограничивать его выдачу прежним пределом пяти минут. `lastSuccessfulAt` MUST сохранять время успешного получения финансового снимка и совпадать с соответствующим временем в `meta`; `isStale` MUST отражать `meta.freshness: stale`, а не состояние клиентского кэша. Ручное действие «Обновить» MUST обходить проверку давности текущего Portfolio и его показанных зависимостей. Неуспешное ручное обновление MUST сохранять исходное `lastSuccessfulAt` и MUST NOT само по себе устанавливать `isStale: true`, пока минутный срок свежести сохранённого финансового снимка не истёк; ошибка MUST отображаться отдельно через `meta.sync`.
|
||||||
|
|
||||||
|
Metadata инструмента MUST NOT влиять на успешность или свежесть Portfolio Snapshot. Сохранённые metadata MUST использоваться без ожидания их обновления, а отсутствующие сведения MUST допускать существующий fallback идентификатора. Новые доступные metadata MUST дополнять представление, не меняя времени финансового снимка. Общие данные инструментов MUST следовать суточной свежести `tbank-data-sync`, без сохранения персональных позиций в общих данных.
|
||||||
|
|
||||||
|
После успешной замены или удаления T-Bank connection прежние Snapshot и поздние результаты его запросов MUST NOT обслуживать новое подключение. Известный отказ token текущего подключения MUST блокировать личное чтение с существующим `INVALID_TBANK_TOKEN`; он MUST NOT маскироваться сохранённым Snapshot.
|
||||||
|
|
||||||
|
#### Scenario: Свежий Snapshot
|
||||||
|
- **WHEN** User повторно запрашивает тот же Portfolio ранее чем через одну минуту после успешного получения
|
||||||
|
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Медленный lookup инструмента
|
||||||
|
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
|
||||||
|
- **THEN** система возвращает сохранённый финансовый Snapshot без ожидания lookup с доступным fallback, а после успешного lookup последующее чтение использует полученное name
|
||||||
|
|
||||||
|
#### Scenario: Fallback после временной upstream-ошибки
|
||||||
|
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а последний полный успешный Snapshot получен более пяти минут назад
|
||||||
|
- **THEN** система отвечает `200` этим Snapshot с `isStale: true`, исходным `lastSuccessfulAt` и ошибкой обновления в `meta.sync`, без повторной синхронной попытки
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление ещё свежего Snapshot неуспешно
|
||||||
|
- **WHEN** User принудительно обновляет Snapshot, полученный менее минуты назад, и попытка завершается временной ошибкой до истечения его TTL
|
||||||
|
- **THEN** наблюдение возвращает прежний Snapshot с `isStale: false`, исходным `lastSuccessfulAt` и отдельной ошибкой синхронизации; ошибка не продлевает TTL и не делает данные устаревшими раньше срока
|
||||||
|
|
||||||
|
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||||
|
- **WHEN** фоновое обновление подтвердило отклонение token текущего подключения как неаутентифицированного или без прав, после чего запрошен Portfolio
|
||||||
|
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
|
||||||
|
|
||||||
|
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||||||
|
|
||||||
|
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
|
||||||
|
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
|
||||||
|
30 секунд, без automatic retry неуспешного запроса, периодической синхронизации или refetch при возврате фокуса. Наблюдение за состоянием уже запрошенной работы MUST следовать `tbank-data-sync` и MUST NOT запускать новую работу.
|
||||||
|
При загрузке без ранее полученного успешного ответа либо при `data: null`
|
||||||
|
с состоянием загрузки для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого
|
||||||
|
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
|
||||||
|
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
|
||||||
|
или получать клавиатурный фокус.
|
||||||
|
|
||||||
|
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
|
||||||
|
Client Cached Snapshot. Во время фонового обновления и после его временной ошибки
|
||||||
|
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
|
||||||
|
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
|
||||||
|
показывать компактную неблокирующую индикацию без замены данных skeleton.
|
||||||
|
При ошибке обновления он MUST показывать локализованное неблокирующее
|
||||||
|
предупреждение о последних полученных данных и предоставлять действие
|
||||||
|
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
|
||||||
|
снимком или подтверждением текущей доступности Brokerage Account.
|
||||||
|
|
||||||
|
Клиент MUST показать штатное empty-состояние только при `meta.availability: complete`, непустом `data` и пустых `positions`
|
||||||
|
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
|
||||||
|
Portfolio клиент MUST сохранять время успешного получения снимка
|
||||||
|
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
|
||||||
|
показывать Snapshot, его исходное время и нейтральное предупреждение
|
||||||
|
об устаревших данных. Ошибка синхронизации MUST иметь отдельное безопасное объяснение. На экране MUST быть доступно действие «Обновить», обновляющее текущий экран и его зависимости независимо от давности. Server-side stale и ошибка фонового
|
||||||
|
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
|
||||||
|
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
|
||||||
|
и действие повтора клиентского обновления MUST оставаться доступными при
|
||||||
|
одновременном наличии этих состояний.
|
||||||
|
|
||||||
|
При ответе `404` для этого же Portfolio клиент MUST показать состояние недоступного счёта
|
||||||
|
с действием возврата к списку счетов без действия повтора. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Snapshot. При отсутствии Snapshot и `TBANK_UNAVAILABLE`
|
||||||
|
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
|
||||||
|
для ошибки действиями; действие повтора разрешено только для временной
|
||||||
|
и неизвестной ошибки.
|
||||||
|
|
||||||
|
#### Scenario: Показ устаревшего Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||||
|
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
|
||||||
|
последнего успешного снимка, нейтральное предупреждение об устаревании
|
||||||
|
и доступное действие «Обновить»
|
||||||
|
|
||||||
|
#### Scenario: Недоступный счёт на клиенте
|
||||||
|
|
||||||
|
- **WHEN** клиент получает `404` для текущего Portfolio
|
||||||
|
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
|
||||||
|
Brokerage Account без действия повтора
|
||||||
|
|
||||||
|
#### Scenario: Первичная загрузка Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
|
||||||
|
полученного успешного ответа либо получает `data: null` с активной загрузкой
|
||||||
|
- **THEN** он показывает структурный skeleton, включая место сводки,
|
||||||
|
с доступным обозначением загрузки, без вымышленных или нулевых сумм
|
||||||
|
и без фокусируемых placeholders
|
||||||
|
|
||||||
|
#### Scenario: Обновление после истечения срока свежести
|
||||||
|
|
||||||
|
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
|
||||||
|
обновление данных того же Brokerage Account
|
||||||
|
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
|
||||||
|
получения, показывает неблокирующую индикацию обновления и не заменяет
|
||||||
|
доступный состав и сводку skeleton-состоянием
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления после успешного ответа
|
||||||
|
|
||||||
|
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
|
||||||
|
а обновление этих же данных завершается временной ошибкой
|
||||||
|
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
|
||||||
|
показывает неблокирующее предупреждение и действие повтора;
|
||||||
|
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||||
|
|
||||||
|
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
|
||||||
|
обновление этого же Portfolio завершается временной ошибкой
|
||||||
|
- **THEN** прежние суммы и время остаются видимыми одновременно
|
||||||
|
с нейтральным server-side предупреждением, отдельным предупреждением
|
||||||
|
об ошибке клиентского обновления и его действием повтора
|
||||||
|
|
||||||
|
#### Scenario: Повторное обновление успешно
|
||||||
|
|
||||||
|
- **WHEN** после ошибки фонового обновления User запускает повторную
|
||||||
|
попытку и получает успешный Portfolio Snapshot
|
||||||
|
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
|
||||||
|
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
|
||||||
|
убирает предупреждение об ошибке клиентского обновления и показывает
|
||||||
|
server-side предупреждение только в соответствии с новым `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||||
|
|
||||||
|
- **WHEN** успешно полученный ответ имеет `meta.availability: complete` и Portfolio в `data` содержит пустые `positions`
|
||||||
|
и `cashBalances`
|
||||||
|
- **THEN** клиент показывает существующее empty-состояние без валютных
|
||||||
|
карточек с вымышленными нулевыми итогами
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Определяет постоянное хранение данных T-Bank, локальное чтение и синхронизацию
|
||||||
|
по запросу с проверяемыми полнотой, свежестью и изоляцией подключения.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Все экраны читают сохранённые данные без ожидания T-Bank
|
||||||
|
|
||||||
|
Система SHALL постоянно сохранять уже загруженные счета, последний полный
|
||||||
|
портфель, исполненные операции, данные инструментов, текущие котировки,
|
||||||
|
дневные свечи и календари выплат. Доходы, личные позиции и прогнозные выплаты
|
||||||
|
SHALL рассчитываться по сохранённым источникам. Все соответствующие экраны,
|
||||||
|
включая режим «Все счета», поиск, сведения о компании и зависимости котировок,
|
||||||
|
MUST читать доступные данные без ожидания ответа T-Bank. Перезапуск backend
|
||||||
|
MUST NOT требовать повторной загрузки сохранённых полных интервалов.
|
||||||
|
|
||||||
|
#### Scenario: Брокер медленно отвечает при заполненном хранилище
|
||||||
|
- **WHEN** User открывает любой экран с сохранёнными данными текущего подключения, а T-Bank задерживает ответ
|
||||||
|
- **THEN** обычный серверный ответ содержит сохранённые данные и их состояние, не ожидая T-Bank, включая проверку доступных счетов
|
||||||
|
|
||||||
|
#### Scenario: Перезапуск сохраняет историю
|
||||||
|
- **WHEN** backend перезапущен и User запрашивает ранее полностью загруженный прошлый период
|
||||||
|
- **THEN** система возвращает его из постоянного хранилища без повторного получения операций или завершённых свечей этого периода
|
||||||
|
|
||||||
|
#### Scenario: Данные ещё не загружены
|
||||||
|
- **WHEN** User впервые запрашивает отсутствующие данные
|
||||||
|
- **THEN** ответ различает отсутствие загруженного результата и подтверждённый пустой результат, сообщает о запрошенной загрузке и не ожидает её завершения
|
||||||
|
|
||||||
|
### Requirement: Обновление запускается только запросом с учётом свежести
|
||||||
|
|
||||||
|
Система MUST запускать синхронизацию только запросом экрана либо ручным
|
||||||
|
обновлением. Обычный запрос SHALL планировать только отсутствующие зависимости,
|
||||||
|
пропуски и данные с истекшим сроком свежести: портфель и текущие цены —
|
||||||
|
60 секунд; операции текущего месяца и доступные счета — 300 секунд;
|
||||||
|
справочник инструментов, включая сведения о компании, и календари выплат —
|
||||||
|
24 часа; незавершённая дневная свеча — 60 секунд. Свежесть MUST отсчитываться
|
||||||
|
от последнего успешного получения соответствующего источника, а не чтения
|
||||||
|
ответа или расчёта производного представления. Полнота и свежесть MUST
|
||||||
|
оставаться независимыми признаками. Периодические запуски MUST NOT выполняться.
|
||||||
|
Для текущего изменяемого периода полнота SHALL относиться к состоянию на
|
||||||
|
последний успешный `asOf`: рост `to=now` внутри TTL не является новым
|
||||||
|
пропуском, требующим RPC. Настоящие пропуски до `asOf` и незавершённый хвост
|
||||||
|
при переходе месяца MUST NOT маскироваться этим правилом.
|
||||||
|
|
||||||
|
#### Scenario: Данные ещё свежие
|
||||||
|
- **WHEN** экран повторно запрошен до истечения сроков всех его полных зависимостей
|
||||||
|
- **THEN** запрос не создаёт новых обращений к T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Текущий период запрошен через секунду
|
||||||
|
- **WHEN** текущий месяц полностью загружен на `asOf` и повторный запрос через секунду имеет более поздний `to=now`
|
||||||
|
- **THEN** система возвращает сохранённый результат с исходным `asOf` без нового RPC до истечения пяти минут, не утверждая наблюдение новых операций после `asOf`
|
||||||
|
|
||||||
|
#### Scenario: Истекла только свежесть портфеля
|
||||||
|
- **WHEN** портфель получен 60 секунд назад, а полные счета и metadata ещё свежие
|
||||||
|
- **THEN** обычный запрос инициирует обновление портфеля и только иных необходимых устаревших зависимостей, сохраняя прежний ответ доступным
|
||||||
|
|
||||||
|
#### Scenario: Пользователь не обращается к системе
|
||||||
|
- **WHEN** запрошенная работа завершилась и сроки свежести истекают без новых запросов
|
||||||
|
- **THEN** никакая синхронизация не запускается, в том числе при запуске backend
|
||||||
|
|
||||||
|
### Requirement: Ручное обновление ограничено текущим экраном
|
||||||
|
|
||||||
|
Действие «Обновить» SHALL обходить давность данных и запрашивать обновление
|
||||||
|
экрана с его зависимостями: текущего счёта либо доступных счетов в режиме
|
||||||
|
«Все счета», применённого периода и сравнений доходов, либо инструмента
|
||||||
|
с показанными свечами, выплатами и личной историей. Сервер MUST определять
|
||||||
|
разрешённые зависимости сам, сохраняя проверку входа и доступа. Обновление
|
||||||
|
MUST NOT перечитывать всю накопленную историю других периодов и инструментов.
|
||||||
|
Подтверждение приёма работы MUST NOT означать успешное обновление данных.
|
||||||
|
|
||||||
|
#### Scenario: Обновление старого месяца
|
||||||
|
- **WHEN** User нажимает «Обновить» на журнале за полностью загруженный прошлый месяц
|
||||||
|
- **THEN** перечитывается показанный период счёта и необходимые зависимости без принудительного перечитывания остальных полных месяцев
|
||||||
|
|
||||||
|
#### Scenario: Обновление доходов
|
||||||
|
- **WHEN** User обновляет аналитику за выбранный период
|
||||||
|
- **THEN** обновление охватывает выбранный и требуемые периоды сравнения, а результаты разных периодов не подменяют друг друга
|
||||||
|
|
||||||
|
#### Scenario: Обновление инструмента
|
||||||
|
- **WHEN** User обновляет инструмент с выбранным диапазоном графика и показанной личной историей
|
||||||
|
- **THEN** обновляются зависимости этого инструмента, диапазон свечей и показанный период личной истории разрешённых счетов без обновления всего каталога
|
||||||
|
|
||||||
|
### Requirement: Первая загрузка приоритизирует экран и продолжает историю
|
||||||
|
|
||||||
|
Система SHALL сначала загружать данные нужного экрану периода, включая
|
||||||
|
периоды сравнения доходов, затем всю доступную историю запрошенного счёта.
|
||||||
|
Для режима «Все счета» это применяется к каждому доступному счёту.
|
||||||
|
В рамках уже запрошенной работы SHALL заранее загружаться доступная рыночная
|
||||||
|
история бумаг текущего портфеля; для остальных инструментов — при первом
|
||||||
|
открытии. Новый запрос нужного экрану пропуска MUST получать приоритет перед
|
||||||
|
ещё не начатой догрузкой старой истории. Начатая работа MAY завершиться
|
||||||
|
после ухода со страницы. Недоказанная нижняя граница доступной истории
|
||||||
|
MUST NOT выдаваться за подтверждение её полной загрузки.
|
||||||
|
|
||||||
|
#### Scenario: Сначала сравнения доходов
|
||||||
|
- **WHEN** история счёта отсутствует и User открывает доходы за месяц
|
||||||
|
- **THEN** выбранный месяц и доступные обязательные периоды сравнения загружаются перед остальной историей; состояние старой истории не задерживает уже полный результат экрана
|
||||||
|
|
||||||
|
#### Scenario: Уход со страницы
|
||||||
|
- **WHEN** User уходит после начала догрузки истории
|
||||||
|
- **THEN** запрошенная работа может закончиться и сохранить результат, но не создаёт расписание последующих обновлений
|
||||||
|
|
||||||
|
#### Scenario: Инструмент отсутствует в портфеле
|
||||||
|
- **WHEN** User впервые открывает такой инструмент
|
||||||
|
- **THEN** система запрашивает и сохраняет его историю, не предзагружая весь каталог
|
||||||
|
|
||||||
|
### Requirement: Полнота подтверждается успешным покрытием интервалов
|
||||||
|
|
||||||
|
Система MUST считать историю счёта полной за период только после успешного
|
||||||
|
получения всех доступных исполненных операций всего этого интервала на момент
|
||||||
|
сверки. Полный период MAY быть пустым. Фильтр категорий, инструмента, размер
|
||||||
|
страницы, отсутствие записей, максимальная дата операции и наличие курсора
|
||||||
|
MUST NOT служить доказательством полноты. Производное представление «Все
|
||||||
|
счета» MUST учитывать покрытие каждого входящего счёта. Полные прошлые месяцы
|
||||||
|
SHALL сохраняться без автоматической повторной сверки; пропуски SHALL
|
||||||
|
догружаться при запросе. Смена месяца MUST NOT автоматически закрывать
|
||||||
|
незагруженный хвост прошлого месяца. Существующая семантика входных дат
|
||||||
|
журнала и доходов MUST сохраняться без пропусков и двойного учёта на границах.
|
||||||
|
|
||||||
|
#### Scenario: Пустой полный интервал
|
||||||
|
- **WHEN** все страницы интервала успешно получены и операций нет
|
||||||
|
- **THEN** период обозначается полным и пустым; последующее обычное чтение прошлого месяца не обращается к брокеру за этим интервалом
|
||||||
|
|
||||||
|
#### Scenario: Сбой промежуточной страницы
|
||||||
|
- **WHEN** одна из нескольких страниц интервала недоступна
|
||||||
|
- **THEN** интервал остаётся неполным, сохранённый результат не удаляется, частичные записи не выдаются за полный период
|
||||||
|
|
||||||
|
#### Scenario: Граница месяца
|
||||||
|
- **WHEN** последний успешный запрос прошлого месяца закончился до его конца
|
||||||
|
- **THEN** первый подходящий запрос нового месяца догружает недостающий хвост; смена даты сама не подтверждает полноту прошлого месяца
|
||||||
|
|
||||||
|
#### Scenario: Неполон один из счетов
|
||||||
|
- **WHEN** все счета, кроме одного, полностью покрывают период общего дохода
|
||||||
|
- **THEN** общий период не считается полным, а отсутствующий вклад счёта не заменяется нулём
|
||||||
|
|
||||||
|
### Requirement: Сверка операций сохраняет идентичность и прежний результат
|
||||||
|
|
||||||
|
Система SHALL использовать стабильный ID брокерской операции в границах
|
||||||
|
пользователя, подключения и счёта как принятое допущение проекта, а не гарантию
|
||||||
|
API. Повторное получение ID MUST обновлять поля той же операции без дубля
|
||||||
|
и двойного финансового учёта. Ручная сверка MUST согласовывать состав и поля
|
||||||
|
исполненных операций показанного интервала: отсутствующие в полном новом
|
||||||
|
ответе записи могут быть удалены только вместе с успешным завершением всех
|
||||||
|
страниц этого интервала. Частичный либо ошибочный новый ответ MUST NOT
|
||||||
|
изменять ранее опубликованный полный финансовый результат и время его
|
||||||
|
успешной сверки. Автоматическое обнаружение изменений полной старой истории
|
||||||
|
и смены ID не требуется.
|
||||||
|
|
||||||
|
#### Scenario: Поля операции исправлены
|
||||||
|
- **WHEN** успешная повторная сверка получает прежний ID с другой суммой или статусом, меняющим вхождение в исполненную историю
|
||||||
|
- **THEN** итоговый набор отражает новый полный ответ без дублирования ID, а доходы пересчитываются из согласованного набора
|
||||||
|
|
||||||
|
#### Scenario: Операция исчезла при ручной сверке
|
||||||
|
- **WHEN** все страницы перечитанного интервала успешно завершились и прежняя операция отсутствует
|
||||||
|
- **THEN** операция исключается из опубликованного набора этого интервала и производных финансовых результатов
|
||||||
|
|
||||||
|
#### Scenario: Перезагрузка полного периода оборвалась
|
||||||
|
- **WHEN** после нескольких страниц ручной сверки возникает ошибка
|
||||||
|
- **THEN** прежний полный набор, его суммы, полнота и время успешной сверки остаются доступными вместе с ошибкой обновления
|
||||||
|
|
||||||
|
#### Scenario: Сбой во втором месяце перечитываемого квартала
|
||||||
|
- **WHEN** ручная сверка полного квартала получила первый месяц, но оборвалась на странице второго
|
||||||
|
- **THEN** весь прежний результат показанного квартала, включая режим «Все счета», остаётся опубликованным; успешный новый первый месяц не смешивается с прежними остальными месяцами
|
||||||
|
|
||||||
|
### Requirement: Прогресс переживает перезапуск и одновременные запросы
|
||||||
|
|
||||||
|
Система MUST сохранять завершённые интервалы, незавершённую работу и её
|
||||||
|
прогресс. После перезапуска следующий подходящий запрос экрана или ручное
|
||||||
|
обновление SHALL возобновлять запрошенную работу, повторяя незавершённый
|
||||||
|
интервал безопасно и без повторной загрузки полных старых интервалов.
|
||||||
|
Одновременные запросы одной границы данных SHALL объединять одинаковую
|
||||||
|
работу, а пересекающиеся интервалы SHALL использовать общую загрузку
|
||||||
|
пересечения. Ошибка MUST оставлять возможность следующего явного запроса,
|
||||||
|
не создавая бесконечный цикл повторов. Поздняя публикация устаревшей попытки
|
||||||
|
MUST NOT перезаписывать результат новой попытки.
|
||||||
|
|
||||||
|
#### Scenario: Двадцать одновременных запросов
|
||||||
|
- **WHEN** двадцать запросов требуют один отсутствующий интервал одного счёта
|
||||||
|
- **THEN** выполняется один проход страниц этого интервала с разрешёнными транспортными повторами, а все запросы видят общее состояние работы
|
||||||
|
|
||||||
|
#### Scenario: Перезапуск между страницами
|
||||||
|
- **WHEN** процесс остановлен после сохранения прогресса, но до полного завершения интервала
|
||||||
|
- **THEN** до следующего запроса работа не возобновляется; после запроса интервал перечитывается без дублей и без ложного признака полноты
|
||||||
|
|
||||||
|
#### Scenario: Ошибка завершила попытку
|
||||||
|
- **WHEN** запрошенная синхронизация исчерпала допустимые транспортные повторы
|
||||||
|
- **THEN** она показывает безопасную ошибку и прекращает попытки до следующего запроса; наблюдение её состояния не запускает её заново
|
||||||
|
|
||||||
|
### Requirement: Общие данные инструментов отделены от личных
|
||||||
|
|
||||||
|
Система SHALL совместно использовать неперсональные metadata, котировки,
|
||||||
|
свечи и календари одного инструмента между пользователями. Общие записи
|
||||||
|
MUST NOT содержать аккаунты, личные позиции, остатки, операции, оценки выплат
|
||||||
|
пользователя, token либо персональные ошибки. Личные данные и вычисления
|
||||||
|
MUST оставаться ограничены пользователем, доступным счётом и текущим
|
||||||
|
подключением. Успешный общий результат MAY переиспользоваться между счетами
|
||||||
|
без повторного получения того же источника.
|
||||||
|
|
||||||
|
#### Scenario: Два пользователя открывают инструмент
|
||||||
|
- **WHEN** оба имеют право пользоваться интеграцией и общие данные инструмента уже загружены
|
||||||
|
- **THEN** они используют одни общие данные, но видят только собственные позиции, операции и оценки выплат
|
||||||
|
|
||||||
|
#### Scenario: Ошибка token одного пользователя
|
||||||
|
- **WHEN** источник отклоняет token одного пользователя при загрузке общего инструмента
|
||||||
|
- **THEN** личная ошибка не становится состоянием инструмента для другого пользователя и не удаляет ранее сохранённые общие данные
|
||||||
|
|
||||||
|
### Requirement: Локальная проверка доступа сохраняет границу подключения
|
||||||
|
|
||||||
|
Каждое чтение и действие синхронизации MUST проверять сессию, текущее
|
||||||
|
подключение и локально подтверждённую принадлежность счёта этому подключению.
|
||||||
|
Отсутствие ранее подтверждённого списка SHALL означать загрузку доступа,
|
||||||
|
а не доступ к произвольному accountId или доказанный пустой список.
|
||||||
|
Устаревший подтверждённый список MAY использоваться до фоновой сверки;
|
||||||
|
успешная сверка MUST отзывать доступ к исчезнувшим, закрытым или неподдержанным
|
||||||
|
счетам. Известный отказ token MUST сохранять существующую ошибку
|
||||||
|
`INVALID_TBANK_TOKEN`, отсутствие подключения — `TBANK_NOT_CONNECTED`,
|
||||||
|
недоступный счёт — `404`. Хранение не вводит архив без подключения.
|
||||||
|
Успешная замена или удаление подключения MUST блокировать выдачу прежних
|
||||||
|
личных данных и публикацию любых поздних результатов прежней работы.
|
||||||
|
Неуспешная замена MUST сохранять действующую границу.
|
||||||
|
|
||||||
|
#### Scenario: Подтверждённый счёт проверяется локально
|
||||||
|
- **WHEN** User читает заполненные данные доступного счёта текущего подключения
|
||||||
|
- **THEN** проверка принадлежности не обращается синхронно к T-Bank и не использует список другого пользователя или подключения
|
||||||
|
|
||||||
|
#### Scenario: Прямой переход до загрузки счетов
|
||||||
|
- **WHEN** список текущего подключения ещё неизвестен и User запрашивает конкретный accountId
|
||||||
|
- **THEN** система сообщает о загрузке проверки доступа, не выдаёт личных данных этого ID и загружает доступные счета
|
||||||
|
|
||||||
|
#### Scenario: Доступ к счёту отозван
|
||||||
|
- **WHEN** успешная фоновая сверка больше не подтверждает доступ к ранее видимому счёту
|
||||||
|
- **THEN** последующие запросы этого счёта возвращают `404`, а клиент прекращает показывать его как доступный
|
||||||
|
|
||||||
|
#### Scenario: Поздний результат прежнего подключения
|
||||||
|
- **WHEN** старый запрос заканчивается после успешной замены или удаления подключения
|
||||||
|
- **THEN** его данные, прогресс, ошибки и признаки свежести не публикуются и не становятся результатом нового подключения
|
||||||
|
|
||||||
|
### Requirement: Завершённость свечей подтверждается источником
|
||||||
|
|
||||||
|
Система SHALL сохранять дневные свечи по инструменту и времени источника.
|
||||||
|
Завершённые свечи MUST NOT перечитываться обычным запросом при полном
|
||||||
|
покрытии диапазона. Незавершённая свеча SHALL обновляться при запросе после
|
||||||
|
60 секунд до явного подтверждения завершённости источником, даже если её
|
||||||
|
календарный день прошёл. Успешное пустое окно MAY подтверждать отсутствие
|
||||||
|
свечей только в завершённом диапазоне и MUST NOT создавать синтетические
|
||||||
|
свечи. Открытое окно текущего дня, в том числе пока пустое, SHALL оставаться
|
||||||
|
изменяемым с минутным сроком свежести. Ручное обновление SHALL
|
||||||
|
перечитывать показанный диапазон, сохраняя прежние данные при ошибке.
|
||||||
|
|
||||||
|
#### Scenario: Наступил новый день
|
||||||
|
- **WHEN** сохранённая свеча предыдущего дня не подтверждена как завершённая источником
|
||||||
|
- **THEN** новый день не делает её окончательной, и подходящий запрос продолжает обновлять её с минутным сроком свежести
|
||||||
|
|
||||||
|
#### Scenario: Выходной без свечей
|
||||||
|
- **WHEN** источник успешно подтвердил пустое окно без торговых свечей
|
||||||
|
- **THEN** окно сохраняется как проверенное без выдуманных цен и повторной обычной загрузки завершённого диапазона
|
||||||
|
|
||||||
|
#### Scenario: До начала торгов свеча отсутствует
|
||||||
|
- **WHEN** запрос текущего дня вернул пустое окно, а после начала торгов User снова открывает график по истечении минуты
|
||||||
|
- **THEN** система запрашивает изменяемое окно и сохраняет появившуюся свечу; утренний ответ не закрепляет окончательное отсутствие свечи
|
||||||
|
|
||||||
|
### Requirement: Интерфейс различает свежесть полноту и состояние работы
|
||||||
|
|
||||||
|
На всех экранах T-Bank клиент MUST показывать время последнего успешного
|
||||||
|
получения, свежесть, загрузку и безопасную ошибку обновления применимых
|
||||||
|
данных. Полнота MUST относиться к показанному периоду и его зависимостям;
|
||||||
|
общий прогресс истории счёта MUST NOT подменять полноту экрана. Отсутствующие
|
||||||
|
данные SHALL иметь состояние первичной загрузки; доступные данные SHALL
|
||||||
|
оставаться видимыми во время обновления. Неполные totals/comparisons MUST
|
||||||
|
NOT показываться как окончательные или как ноль; неполный пустой журнал
|
||||||
|
MUST NOT сообщать, что операций нет. Наблюдение запрошенной работы MUST
|
||||||
|
прекращаться после завершения либо ухода и MUST NOT запускать синхронизацию.
|
||||||
|
Известные ошибки доступа MUST NOT маскироваться сохранённым результатом.
|
||||||
|
|
||||||
|
#### Scenario: Недавний период готов раньше всей истории
|
||||||
|
- **WHEN** показанный период и его зависимости полны, но старая история ещё загружается
|
||||||
|
- **THEN** пользователь видит готовый результат периода и отдельное состояние догрузки истории
|
||||||
|
|
||||||
|
#### Scenario: Полнота изменилась без новых записей
|
||||||
|
- **WHEN** источник подтвердил пустой период экрана, а остальная история ещё загружается
|
||||||
|
- **THEN** наблюдающий клиент получает изменение полноты и показывает подтверждённый пустой результат, не ожидая конца всей истории
|
||||||
|
|
||||||
|
#### Scenario: Ответы наблюдения завершились в обратном порядке
|
||||||
|
- **WHEN** поздний ответ чтения содержит прежние данные или доступ, а клиент уже принял более новое состояние этого ресурса либо отзыв доступа
|
||||||
|
- **THEN** прежний ответ не откатывает показанные данные и не восстанавливает недоступный личный результат
|
||||||
|
|
||||||
|
#### Scenario: Источник временно недоступен
|
||||||
|
- **WHEN** обновление сохранённого полного результата завершается временной ошибкой
|
||||||
|
- **THEN** прежние данные и время сохраняются, ошибка видима и доступно ручное повторное обновление; ошибка не обновляет время успешного получения и не считается успешной сверкой
|
||||||
|
|
||||||
|
#### Scenario: Неполное сравнение дохода
|
||||||
|
- **WHEN** выбранный период готов, а необходимый период сравнения ещё не загружен
|
||||||
|
- **THEN** сравнение обозначается как незавершённое без нулевой базы или окончательной разницы
|
||||||
|
|
||||||
|
### Requirement: Быстродействие и сокращение запросов проверяются измерениями
|
||||||
|
|
||||||
|
Для обычных чтений заполненной БД система SHALL иметь целевой бюджет
|
||||||
|
серверного ответа до 500 мс в зафиксированном профиле нагрузки. Выполнение
|
||||||
|
цели MUST подтверждаться измерениями, а не предполагаться из наличия БД.
|
||||||
|
Свежие полные зависимости MUST давать ноль новых RPC T-Bank; двадцать
|
||||||
|
одновременных запросов одного устаревшего набора MUST создавать не более
|
||||||
|
одной логической загрузки каждой зависимости. Полные старые операции и
|
||||||
|
завершённые свечи MUST давать ноль повторных RPC для своих интервалов при
|
||||||
|
обычном чтении, включая чтение после перезапуска.
|
||||||
|
|
||||||
|
#### Scenario: Измеряется заполненное хранилище
|
||||||
|
- **WHEN** проверяется согласованный набор обычных запросов при задержанном или недоступном T-Bank
|
||||||
|
- **THEN** фиксируются распределение серверных задержек, число RPC и параметры нагрузки; цель 500 мс не объявляется достигнутой без этих результатов
|
||||||
|
|
||||||
|
#### Scenario: Повторные открытия не перечитывают историю
|
||||||
|
- **WHEN** один и тот же полный прошлый период читается повторно без ручного обновления
|
||||||
|
- **THEN** сохранённые данные выдаются без RPC этого интервала, независимо от числа страниц клиентской выдачи
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
schema: wealth-ledger-sdd
|
||||||
|
created: 2026-09-20
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Согласован отдельный репозиторий `deploy` и команда `./deploy.sh <tag>` по SSH.
|
||||||
|
Тег принадлежит комплекту, а не компоненту; соответствие пары хранится в
|
||||||
|
`release.env`. Решения приняты в обсуждении с владельцем и записаны в ADR 016
|
||||||
|
репозитория docs; эти артефакты готовят реализацию, не подтверждают её выполнение.
|
||||||
|
|
||||||
|
На сервере уже есть Compose project `wealthledger`, SQLite, постоянные ключи
|
||||||
|
и read-only mount `certs/tbank-ca.pem` через `TBANK_CA_CERT_PATH`. Внешний HTTPS
|
||||||
|
и Docker подготовлены. Образы публикуются в Nexus независимыми релизами.
|
||||||
|
`containerized-application-delivery` остаётся отдельным active change;
|
||||||
|
его runtime-контракт используется без редактирования или преждевременного archive.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** небольшой комплект с воспроизводимой парой образов, одним entrypoint,
|
||||||
|
безопасным обновлением SQLite и понятным восстановлением существующего сервера.
|
||||||
|
|
||||||
|
**Non-Goals:** provisioning, CI/CD автодеплой, новый registry, Kubernetes,
|
||||||
|
Ansible-инфраструктура, blue-green, замена БД, backup daemon, автоматическая
|
||||||
|
ротация копий, собственная система миграций или управление HTTPS.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Комплект и постоянные ресурсы
|
||||||
|
|
||||||
|
Минимальное содержимое `deploy`: `compose.yaml`, `release.env`, `.env.example`,
|
||||||
|
`deploy.sh`, README и одна исполняемая проверка сценариев. Compose использует
|
||||||
|
готовые frontend/backend images, migration запускается из backend image.
|
||||||
|
Production-комплект самодостаточен и не требует соседних checkout.
|
||||||
|
Существующие root Compose-файлы для development не удаляются этим change.
|
||||||
|
|
||||||
|
На сервере checkout располагается отдельно от `data/`, `certs/`, `backups/`
|
||||||
|
и защищённого server env. Например, checkout — `/opt/wealthledger/deploy`,
|
||||||
|
остальное — соседние пути в `/opt/wealthledger`. Существующий data path
|
||||||
|
не переименовывается; пути bind mounts абсолютные. Ключи не генерируются
|
||||||
|
повторно. Root filesystem остаётся read-only, процесс non-root, backend
|
||||||
|
не получает host port; сертификат доверия монтируется read-only без отключения TLS.
|
||||||
|
|
||||||
|
`release.env` содержит только полные versioned image references из Nexus;
|
||||||
|
`latest` исключён. Server env хранит настройки и секреты, но не выбирает пару.
|
||||||
|
Переменные окружения вызывающего shell не должны переопределять выбранные
|
||||||
|
images или Compose project. Env-файлы обрабатываются Compose как данные,
|
||||||
|
не выполняются shell через `source`. Полный config с секретами не печатается.
|
||||||
|
|
||||||
|
### Исполнение выбранной версии
|
||||||
|
|
||||||
|
Entrypoint получает тег только из настроенного доверенного `origin`, разрешает
|
||||||
|
его в commit и использует содержимое этого commit для всей операции, включая
|
||||||
|
версию deploy-логики. Полученный ранее тег не перезаписывается при расхождении.
|
||||||
|
Работа идёт из временного snapshot выбранного commit; запущенный скрипт и
|
||||||
|
операторские изменения checkout не перезаписываются переключением ветки.
|
||||||
|
Compose file, env files и project `wealthledger` задаются явно: временная
|
||||||
|
директория не должна создать второй project или изменить пути данных.
|
||||||
|
|
||||||
|
До остановки выполняются проверки входов, ресурсов, конфигурации и загрузка
|
||||||
|
образов. Поддерживаемый target — один Linux Docker host; Git, Bash и `flock`
|
||||||
|
входят в перечисленные README предпосылки. Архитектура опубликованных образов
|
||||||
|
должна соответствовать серверу; multi-platform поддержку этот change не добавляет.
|
||||||
|
|
||||||
|
### Обновление, повтор и состояние
|
||||||
|
|
||||||
|
Один `flock` на установку исключает конкурирующие запуски. Не добавляется
|
||||||
|
сервис блокировок. Файл блокировки и минимальные служебные записи находятся
|
||||||
|
вне checkout, с ограниченными правами, без секретов.
|
||||||
|
|
||||||
|
Нужны только запись последнего успешного релиза и маркер незавершённого
|
||||||
|
обновления: выбранный commit, фактические images, этап и расположение backup.
|
||||||
|
Маркер сохраняется до первого изменения runtime; успешная запись атомарно
|
||||||
|
обновляется только после готовности затронутых сервисов. При прерывании
|
||||||
|
маркер остаётся для диагностики; новый запуск требует ручного разбора по README.
|
||||||
|
Это не журнал всех deployments и не отдельная база состояний.
|
||||||
|
При первом переходе предыдущая версия описывается фактической парой images:
|
||||||
|
несуществующий тег прежнего комплекта не придумывается.
|
||||||
|
|
||||||
|
План изменений сверяет желаемые images и Compose service configuration
|
||||||
|
с фактическими контейнерами. Для сравнения использовать native Compose
|
||||||
|
service hash и Docker labels, если их соответствие подтверждено проверкой.
|
||||||
|
Секреты не записываются в служебные файлы. Отсутствие достоверного baseline
|
||||||
|
не разрешает frontend-only shortcut: первый переход с серверного Compose
|
||||||
|
выполняется как полное обновление с backup. Успешная запись сама по себе
|
||||||
|
не заменяет проверку фактически работающих контейнеров.
|
||||||
|
|
||||||
|
При обновлении API: stop backend → проверка отсутствия migration writer →
|
||||||
|
новый уникальный каталог backup → копия всего data directory, включая WAL/SHM →
|
||||||
|
одноразовая migration → backend readiness. Для существующей установки отсутствие
|
||||||
|
`wealthledger.db` — ошибка. Только явный первый запуск `--init` на пустом
|
||||||
|
каталоге без прежнего runtime допускает создание новой базы без backup.
|
||||||
|
Копия отмечается пригодной для восстановления только после успешного окончания
|
||||||
|
копирования; незавершённый каталог не выдаётся за готовый backup.
|
||||||
|
|
||||||
|
Неизменный frontend не пересоздаётся; обновление только frontend не запускает
|
||||||
|
зависимости. При обновлении обоих компонентов новый frontend запускается после
|
||||||
|
готовности backend. Healthchecks используются из готовых images через Compose;
|
||||||
|
новый HTTP endpoint не нужен. Browser smoke через HTTPS остаётся отдельной
|
||||||
|
проверкой выпуска: container health не доказывает login/refresh.
|
||||||
|
|
||||||
|
Backup failure и migration failure прекращают последовательность. При провале
|
||||||
|
readiness backend останавливается. Провал frontend не останавливает исправный
|
||||||
|
backend. Любая ошибка оставляет релиз неподтверждённым, текущую БД и backup
|
||||||
|
неизменёнными процедурой восстановления; диагностика не выводит secrets.
|
||||||
|
|
||||||
|
### Выпуск и инструкция
|
||||||
|
|
||||||
|
После публикации компонентов оператор меняет пару в `release.env`, проверяет
|
||||||
|
её на disposable установке и выпускает отдельный неизменяемый `vX.Y.Z` тег
|
||||||
|
`deploy`. Компонентные pipeline не связываются автоматическим publisher.
|
||||||
|
Первый комплект закрепляет реально доступные образы, не демонстрационные версии.
|
||||||
|
README описывает подготовку Docker/Git/registry/HTTPS, разрешения каталогов,
|
||||||
|
сохранение ключей, подключение действующего Compose project, install/update,
|
||||||
|
диагностику незавершённой операции, ручную очистку backups и recovery.
|
||||||
|
|
||||||
|
## UI/UX Applicability and Package
|
||||||
|
|
||||||
|
**Applicability and scope:** `N/A` — браузерный интерфейс и его состояния
|
||||||
|
не меняются; оператор использует терминал и README. Макеты не нужны.
|
||||||
|
|
||||||
|
## Delivery Map
|
||||||
|
|
||||||
|
| Area | Status | Architectural outcome or N/A reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| frontend | `N/A` | Готовый image переиспользуется, UI и pipeline не меняются |
|
||||||
|
| backend | `N/A` | Готовые runtime, healthcheck и migrations переиспользуются |
|
||||||
|
| specs | `affected` | Контракт deployment-bundle и план его проверки |
|
||||||
|
| docs | `affected` | ADR 016 и обновлённый эксплуатационный маршрут после реализации |
|
||||||
|
| deploy | `affected` | Самодостаточный комплект, сценарии проверки и README |
|
||||||
|
| root | `affected` | Только навигация README к production-комплекту; development Compose не меняется |
|
||||||
|
|
||||||
|
Уточнение старого шаблона: обязательные четыре области сохранены; добавлен
|
||||||
|
реальный repository owner `deploy`, явно выбранный пользователем, и `root`
|
||||||
|
для необходимой поправки workspace-навигации. В Tasks используются `[deploy]`
|
||||||
|
и `[root]`, а не фиктивный `[backend]` или `[workspace]`.
|
||||||
|
Это локальное расширение перечня owners для данного change; глобальная
|
||||||
|
переработка SDD schema и process specs в scope не входит. Native structural
|
||||||
|
validation не доказывает соответствие этому уточнению или semantic approval.
|
||||||
|
|
||||||
|
## Solution Assessment
|
||||||
|
|
||||||
|
| Вариант | Fit, зрелость и безопасность | Стоимость и ограничения |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Только Docker Compose | Зрелый runtime, зависимости, healthchecks и выбор images уже покрыты | Не выполняет stop/copy/migrate и не исключает две операторские операции |
|
||||||
|
| Bash + Git + Compose + flock | Native инструменты владеют загрузкой, запуском и блокировкой; shell связывает существующий безопасный порядок | Небольшой branching требует проверок ошибок и прерываний; подходит одному Linux host |
|
||||||
|
| Ansible | Зрелый механизм управления серверами, но backup/migration порядок всё равно надо определить | Добавляет control environment, inventory и роль при уже подготовленном одном сервере |
|
||||||
|
|
||||||
|
Выбран второй вариант. Чистого Compose недостаточно для согласованной копии
|
||||||
|
при остановленной записи; отдельный provisioning stack не соответствует
|
||||||
|
согласованному отсутствию управления серверной инфраструктурой. Новый package
|
||||||
|
manager, framework или сторонняя runtime-зависимость не вводятся.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Краткий простой и единственный writer сохраняются; внешние процессы записи
|
||||||
|
должны быть исключены оператором при подключении существующей установки.
|
||||||
|
- Локальный backup не спасает от потери диска; свободное место и ручная очистка
|
||||||
|
остаются эксплуатационной обязанностью. Ошибка копирования блокирует миграции.
|
||||||
|
- Native hash может измениться между версиями Compose: сомнение приводит
|
||||||
|
к полному безопасному обновлению, а не к пропуску backup.
|
||||||
|
- Прерванная migration требует разбора, а не слепого повторения команды.
|
||||||
|
Ни служебный маркер, ни последний успешный тег не являются копией базы.
|
||||||
|
- Доверие к Git origin и неизменяемым Nexus release tags остаётся существующей
|
||||||
|
границей поставки; собственная система подписей здесь не добавляется.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Подготовить самодостаточный комплект и проверить безопасный lifecycle на
|
||||||
|
disposable данных. Затем обновить docs, сохранив development-инструкцию.
|
||||||
|
Для действующего сервера сохранить старые настройки и соответствие images,
|
||||||
|
подключить существующие абсолютные пути и project, выполнить первый полный
|
||||||
|
переход с backup. Старый способ запуска не выполнять параллельно.
|
||||||
|
|
||||||
|
### Rollback Contract
|
||||||
|
|
||||||
|
| Field | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Status | `required` |
|
||||||
|
| Trigger | Ошибка migration/readiness, прерывание либо выявленная регрессия |
|
||||||
|
| Actions | Остановить API при риске для данных; отдельно выбрать совместимый image либо explicit restore; незавершённый маркер снять только после проверки состояния |
|
||||||
|
| Data safety | Не восстанавливать БД автоматически; перед explicit restore сохранить текущий каталог, выбрать копию, подтвердить потерю новых записей и наличие прежнего ключа |
|
||||||
|
| Verification | На отдельной копии проверить целостность БД, совместимость схемы, auth/cards и расшифровку подключения; после восстановления повторить smoke через HTTPS |
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
После выпуска компонентов оператор вручную выбирает совместимые версии
|
||||||
|
и повторяет порядок обновления. Нужен один выбранный релиз всей установки
|
||||||
|
и одна команда, сохраняющая данные и настройки существующего сервера.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Версионируемый комплект закрепляет проверенную пару версий интерфейса и API.
|
||||||
|
- Оператор явно выбирает релиз; публикация компонента не обновляет сервер.
|
||||||
|
- Обновление выполняется одной командой с предварительными проверками,
|
||||||
|
сохранением данных и диагностируемым результатом.
|
||||||
|
- Обновление только интерфейса не прерывает работу API и не затрагивает базу.
|
||||||
|
- Перед обновлением API создаётся согласованная резервная копия. Неудачное
|
||||||
|
обновление не запускает автоматическое восстановление или откат.
|
||||||
|
- Инструкция охватывает подготовку сервера, первую установку, подключение
|
||||||
|
существующей установки, выпуск комплекта, обновление и ручное восстановление.
|
||||||
|
|
||||||
|
Non-goals: настройка серверной инфраструктуры командой развёртывания,
|
||||||
|
автоматическое обновление по выходу компонента, непрерывная доступность,
|
||||||
|
несколько экземпляров API, автоматическая очистка копий и внешнее backup-хранилище.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `specs/deployment-bundle/spec.md`: выбор полного релиза, безопасная автоматизация
|
||||||
|
обновления и сохранение существующей установки.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
Нет. Новый контракт дополняет контейнерную поставку; существующий active change
|
||||||
|
`containerized-application-delivery` и его capability `application-delivery`
|
||||||
|
не изменяются и не архивируются в рамках этой работы.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Основной пользователь — оператор одной установки WealthLedger. Изменяются
|
||||||
|
комплект развёртывания, эксплуатационная документация и требования к обновлению.
|
||||||
|
Прикладные функции интерфейса и API остаются прежними.
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Определяет установку выбранного релиза WealthLedger одной командой,
|
||||||
|
сохранение состояния сервера и безопасный результат успешного или неудачного обновления.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Релиз определяет полную пару компонентов
|
||||||
|
|
||||||
|
Комплект SHALL иметь отдельную неизменяемую версию, однозначно определяющую
|
||||||
|
проверенную пару версий интерфейса и API. Оператор MUST явно выбирать релиз
|
||||||
|
комплекта. Публикация компонента MUST NOT автоматически выпускать комплект
|
||||||
|
или обновлять сервер; локальные настройки MUST NOT незаметно подменять пару.
|
||||||
|
|
||||||
|
#### Scenario: Установка выбранного релиза
|
||||||
|
|
||||||
|
- **WHEN** оператор выбирает опубликованный релиз комплекта
|
||||||
|
- **THEN** устанавливается ровно закреплённая в нём пара, даже если опубликованы
|
||||||
|
более новые версии компонентов
|
||||||
|
|
||||||
|
#### Scenario: Отсутствующий или изменённый релиз
|
||||||
|
|
||||||
|
- **WHEN** выбранный релиз отсутствует либо ранее полученная версия теперь
|
||||||
|
указывает на другое содержимое
|
||||||
|
- **THEN** операция завершается ошибкой до изменения работающей установки
|
||||||
|
|
||||||
|
### Requirement: Проверки предшествуют изменению установки
|
||||||
|
|
||||||
|
Одна команда SHALL выполнять установку выбранного релиза на подготовленном
|
||||||
|
сервере. До остановки приложения она MUST проверить доступность комплекта,
|
||||||
|
его компонентов, обязательных настроек и постоянных ресурсов.
|
||||||
|
Отсутствующая база существующей установки MUST NOT трактоваться как новая
|
||||||
|
пустая установка. Секреты MUST NOT попадать в диагностический вывод.
|
||||||
|
|
||||||
|
#### Scenario: Компонент недоступен
|
||||||
|
|
||||||
|
- **WHEN** выбранный компонент нельзя получить или обязательный сертификат
|
||||||
|
либо каталог данных недоступен
|
||||||
|
- **THEN** обновление завершается с понятной ошибкой, не останавливая
|
||||||
|
работающие компоненты и не создавая замену отсутствующим данным
|
||||||
|
|
||||||
|
#### Scenario: Новая установка
|
||||||
|
|
||||||
|
- **WHEN** оператор явно подготовил новую установку с отдельным пустым
|
||||||
|
хранилищем и необходимыми настройками
|
||||||
|
- **THEN** команда подготавливает данные и запускает выбранный релиз,
|
||||||
|
не требуя предыдущей установленной версии
|
||||||
|
|
||||||
|
### Requirement: Существующая установка сохраняет состояние
|
||||||
|
|
||||||
|
Подключение существующей установки и последующие обновления MUST сохранять
|
||||||
|
её пользовательские данные, сессии, ключи шифрования и настройки доверия
|
||||||
|
внешнему провайдеру. Эти ресурсы MUST храниться независимо от обновляемого
|
||||||
|
комплекта. Команда MUST NOT запускать второй API на той же базе или генерировать
|
||||||
|
новые ключи вместо действующих.
|
||||||
|
|
||||||
|
#### Scenario: Переход с прежнего способа запуска
|
||||||
|
|
||||||
|
- **WHEN** оператор подключает существующую установку к комплекту
|
||||||
|
- **THEN** используются её прежние данные и ключи; после запуска доступны
|
||||||
|
существующие записи и расшифровка сохранённого подключения
|
||||||
|
|
||||||
|
### Requirement: Обновление затрагивает только изменённые компоненты
|
||||||
|
|
||||||
|
Если меняется только интерфейс, команда MUST сохранять работающий API,
|
||||||
|
не изменять данные и не запускать миграции или резервное копирование.
|
||||||
|
Изменение настроек API SHALL считаться изменением API, даже при той же версии.
|
||||||
|
Обновление только API MUST NOT пересоздавать неизменный интерфейс.
|
||||||
|
Повторное применение уже установленного исправного релиза с теми же настройками
|
||||||
|
MUST NOT выполнять ненужное обновление или резервное копирование.
|
||||||
|
|
||||||
|
#### Scenario: Новый интерфейс при прежнем API
|
||||||
|
|
||||||
|
- **WHEN** выбранный релиз меняет только интерфейс, а API и его настройки
|
||||||
|
подтверждённо совпадают с установленными
|
||||||
|
- **THEN** обновляется только интерфейс, API продолжает работу без миграций
|
||||||
|
и резервного копирования для этой операции
|
||||||
|
|
||||||
|
#### Scenario: Та же версия API с другими настройками
|
||||||
|
|
||||||
|
- **WHEN** версия API прежняя, но его настройки изменились
|
||||||
|
- **THEN** применяется безопасная процедура обновления API
|
||||||
|
|
||||||
|
#### Scenario: Повтор успешно установленного релиза
|
||||||
|
|
||||||
|
- **WHEN** выбран уже установленный исправный релиз без изменений настроек
|
||||||
|
- **THEN** команда сообщает, что обновление не требуется, без остановки
|
||||||
|
компонентов и без новой резервной копии
|
||||||
|
|
||||||
|
### Requirement: Обновление API сохраняет согласованную копию
|
||||||
|
|
||||||
|
Перед обновлением API существующей установки команда MUST остановить запись
|
||||||
|
и успешно сохранить согласованную копию всех постоянных данных до миграций.
|
||||||
|
Копии MUST находиться вне обновляемого комплекта; команда MUST NOT удалять
|
||||||
|
или перезаписывать прежние копии автоматически. Неуспешная копия MUST
|
||||||
|
блокировать миграции. Новая пустая установка не требует фиктивного backup.
|
||||||
|
|
||||||
|
#### Scenario: Подготовка обновления API
|
||||||
|
|
||||||
|
- **WHEN** выбран релиз с изменением API существующей установки
|
||||||
|
- **THEN** до миграций сохраняется отдельная согласованная копия при отсутствии
|
||||||
|
пишущих процессов, и оператор получает её расположение
|
||||||
|
|
||||||
|
#### Scenario: Копию сохранить не удалось
|
||||||
|
|
||||||
|
- **WHEN** резервное копирование завершилось ошибкой
|
||||||
|
- **THEN** миграции и запуск новой версии не выполняются, существующие данные
|
||||||
|
и прежние копии сохраняются, операция сообщает ошибку
|
||||||
|
|
||||||
|
### Requirement: Неудачное обновление не скрывает риск для данных
|
||||||
|
|
||||||
|
Ошибка миграции MUST блокировать запуск новой версии API. Если после миграции
|
||||||
|
API не готов, команда MUST остановить неисправный API и завершиться с ошибкой.
|
||||||
|
Любая неуспешная проверка обновляемого компонента MUST блокировать отметку
|
||||||
|
релиза как успешно установленного. Диагностика SHALL указывать этап сбоя,
|
||||||
|
предыдущую версию и доступный backup без раскрытия секретов.
|
||||||
|
Автоматический откат или восстановление данных MUST NOT выполняться.
|
||||||
|
|
||||||
|
#### Scenario: Миграция не прошла
|
||||||
|
|
||||||
|
- **WHEN** подготовка схемы завершается ошибкой
|
||||||
|
- **THEN** API остаётся остановленным, а результат содержит сведения для
|
||||||
|
отдельного решения о восстановлении
|
||||||
|
|
||||||
|
#### Scenario: Новый API не готов
|
||||||
|
|
||||||
|
- **WHEN** миграция завершилась успешно, но проверка готовности API не прошла
|
||||||
|
- **THEN** неисправный API останавливается без автоматического запуска старой
|
||||||
|
версии или восстановления базы
|
||||||
|
|
||||||
|
#### Scenario: Неудачное обновление интерфейса
|
||||||
|
|
||||||
|
- **WHEN** при обновлении только интерфейса его проверка запуска не прошла
|
||||||
|
- **THEN** операция завершается ошибкой, API продолжает работу, база не
|
||||||
|
восстанавливается, релиз не отмечается успешно установленным
|
||||||
|
|
||||||
|
### Requirement: Повтор и конкуренция не нарушают порядок обновления
|
||||||
|
|
||||||
|
Команда MUST исключать одновременные изменения одной установки несколькими
|
||||||
|
запусками. Прерванное или неуспешное обновление MUST отличаться от успешно
|
||||||
|
установленного релиза. Повтор команды после потенциального изменения данных
|
||||||
|
MUST NOT скрывать незавершённую операцию или автоматически восстанавливать данные.
|
||||||
|
|
||||||
|
#### Scenario: Второе обновление
|
||||||
|
|
||||||
|
- **WHEN** для установки уже выполняется обновление
|
||||||
|
- **THEN** второй запуск завершается без изменения её состояния
|
||||||
|
|
||||||
|
#### Scenario: Процесс прерван после начала обновления
|
||||||
|
|
||||||
|
- **WHEN** предыдущая операция прервана после остановки API или начала миграции
|
||||||
|
- **THEN** следующий запуск сообщает о незавершённой операции и необходимости
|
||||||
|
проверки состояния до нового обновления
|
||||||
|
|
||||||
|
### Requirement: Оператор располагает полным порядком эксплуатации
|
||||||
|
|
||||||
|
Инструкция комплекта SHALL описывать последовательность подготовки сервера,
|
||||||
|
первой установки и подключения существующей установки, подготовку проверенной
|
||||||
|
пары и выпуск релиза, обновление, диагностику, откат версии и отдельное
|
||||||
|
восстановление базы. Она MUST указывать сохранение ключей, ручную очистку
|
||||||
|
локальных копий и отсутствие защиты от потери диска только локальным backup.
|
||||||
|
Восстановление MUST требовать явного выбора копии и принятия потери более
|
||||||
|
новых данных; откат API — проверки совместимости с текущей схемой.
|
||||||
|
|
||||||
|
#### Scenario: Оператор восстанавливает установку
|
||||||
|
|
||||||
|
- **WHEN** оператор использует инструкцию после неудачного обновления
|
||||||
|
- **THEN** он может отдельно выбрать совместимую версию приложения или
|
||||||
|
восстановление конкретной копии с явным учётом потери новых данных
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
## 1. Самодостаточный комплект
|
||||||
|
|
||||||
|
- [x] 1.1 [deploy] Создать production Compose, release.env и пример server env, переиспользовав существующие images/migration/healthchecks и сохранив read-only CA mount и постоянные абсолютные пути.
|
||||||
|
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Существующая установка сохраняет состояние`
|
||||||
|
- **Verification:** `docker compose --env-file <disposable-server.env> --env-file release.env -f compose.yaml config --quiet`; inspect подтверждает выбранную пару, один backend, общий data mount migration/backend, отсутствие host port API и read-only сертификат; exported image variables не подменяют релиз через entrypoint.
|
||||||
|
|
||||||
|
- [x] 1.2 [deploy] Реализовать получение выбранного тега, выполнение snapshot его commit, preflight, flock и минимальные записи состояния в одном deploy.sh согласно Solution Assessment.
|
||||||
|
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
|
||||||
|
- **Verification:** `bash -n deploy.sh`; сценарная проверка отсутствующего/переназначенного тега, недоступного image/CA/data, отказа второго запуска и отсутствия изменений runtime при preflight failure; snapshot использует deploy-логику выбранного commit.
|
||||||
|
|
||||||
|
## 2. Безопасный lifecycle и его доказательство
|
||||||
|
|
||||||
|
- [x] 2.1 [deploy] Реализовать first install/adoption, выбор изменённых сервисов, stop/backup/migration/readiness и остановку последовательности при ошибке без автоматического rollback.
|
||||||
|
- **Covers:** `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Существующая установка сохраняет состояние`; `deployment-bundle` — `Обновление затрагивает только изменённые компоненты`; `deployment-bundle` — `Обновление API сохраняет согласованную копию`; `deployment-bundle` — `Неудачное обновление не скрывает риск для данных`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
|
||||||
|
- **Verification:** На disposable Compose project проверить явный `--init`, adoption существующей тестовой БД, обновление обоих компонентов и API отдельно, неизменность container ID API при frontend-only update, отсутствие повторного backup при healthy no-op; изменение backend config включает backup даже с прежним image.
|
||||||
|
|
||||||
|
- [x] 2.2 [deploy] Добавить одну исполняемую проверку tests/deploy.sh без нового test framework и закрепить локальный gate для shell и сценариев обновления.
|
||||||
|
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Существующая установка сохраняет состояние`; `deployment-bundle` — `Обновление затрагивает только изменённые компоненты`; `deployment-bundle` — `Обновление API сохраняет согласованную копию`; `deployment-bundle` — `Неудачное обновление не скрывает риск для данных`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
|
||||||
|
- **Verification:** `bash -n deploy.sh tests/deploy.sh`, `bash tests/deploy.sh`, `git diff --check`; fault injection доказывает отсутствие migration после failed copy, отсутствие backend start после failed migration, stop после backend readiness failure, сохранение backend после frontend failure, неизменность данных/старых backups, блокировку конкуренции и незавершённой операции. Проверка использует временные данные; production paths недопустимы.
|
||||||
|
|
||||||
|
## 3. Эксплуатация и документация
|
||||||
|
|
||||||
|
- [x] 3.1 [deploy] Описать в README подготовку сервера, сохранение старых ключей и Compose project, install/update, выпуск тега с проверенной парой, диагностику, ручной rollback/restore и очистку backups; закрепить реально опубликованную первую пару перед выпуском.
|
||||||
|
- **Covers:** `deployment-bundle` — `Оператор располагает полным порядком эксплуатации`; `deployment-bundle` — `Релиз определяет полную пару компонентов`
|
||||||
|
- **Verification:** Пройти инструкцию на изолированной установке с тестовыми данными; проверить login/refresh через HTTPS, сохранение записей и расшифровку тестового подключения прежним ключом, `PRAGMA integrity_check` на копии и explicit restore; сравнить actual images с release.env. Первый тег не публиковать до проверки пары.
|
||||||
|
|
||||||
|
- [x] 3.2 [docs] Завершить ADR 016 и glossary, обновить production-раздел контейнерной поставки под deploy с сохранением development-инструкции и границ ручного recovery.
|
||||||
|
- **Covers:** `N/A` — docs объясняет поставленный механизм из Design и направляет оператора к единственному README комплекта; самостоятельного runtime behavior не добавляет.
|
||||||
|
- **Verification:** После `nvm use --silent` выполнить `npm run verify` в docs; проверить маршрут README → deployment → ADR и отсутствие обещаний ещё не реализованных операций или дублирующего production runbook.
|
||||||
|
|
||||||
|
- [x] 3.3 [root] Направить production-раздел root README к комплекту deploy, сохранив существующие development-команды.
|
||||||
|
- **Covers:** `N/A` — workspace-навигация должна вести к владельцу production lifecycle из Design; поведение приложения и root tooling не меняются.
|
||||||
|
- **Verification:** `git diff --check` в root; просмотр маршрута root README → deploy README и docs/architecture/deployment.md, проверка неизменности development-команд.
|
||||||
|
|
||||||
|
## 4. Сверка поставки
|
||||||
|
|
||||||
|
- [x] 4.1 [specs] Сверить артефакты с фактической реализацией, сохранить coverage всех требований и проверить canonical change без изменения чужого containerized-application-delivery.
|
||||||
|
- **Covers:** `N/A` — requirements и checklist должны оставаться контрактом реализованного комплекта; задача поддерживает точность SDD, а не вводит новое поведение приложения.
|
||||||
|
- **Verification:** После `nvm use --silent`: `openspec schema validate wealth-ledger-sdd --verbose`; `openspec validate versioned-deployment-bundle --type change --strict --store wealth-ledger-specs`; `openspec validate --specs --strict --store wealth-ledger-specs`; `npm run verify`; вручную проверить task coverage и соответствие дополнительному owner deploy из Delivery Map.
|
||||||
|
|
||||||
|
- [x] 4.2 [workspace] Выполнить итоговую сверку сценариев установки, ошибок и восстановления с готовыми образами и результатами локальных gates затронутых репозиториев.
|
||||||
|
- **Covers:** `N/A` — объединяет доказательства deploy/specs/docs для согласованной пары, не владеет реализацией какого-либо репозитория и не заменяет independent review.
|
||||||
|
- **Verification:** На disposable данных сверить успешный update, frontend-only, interrupted update и recovery с Delta Specs; зафиксировать фактические версии и результаты gates. Проверки реального сервера не подменять локальным smoke; production rollout не входит в эту задачу.
|
||||||
|
|
||||||
|
## 5. Implementation checkpoint
|
||||||
|
|
||||||
|
2026-09-20: implementation-owned задачи выполнены; готово к независимой Validation.
|
||||||
|
Change остаётся active. Первый тег deploy не опубликован, рабочий сервер не изменялся.
|
||||||
|
|
||||||
|
- Deploy: `bash -n deploy.sh tests/deploy.sh`, `bash tests/deploy.sh` — PASS.
|
||||||
|
Проверены selective update/no-op, config drift, сохранение ключей, symlink guard,
|
||||||
|
ошибки pull/preflight/copy/migration/backend/frontend, concurrent lock и pending.
|
||||||
|
- `DOCKER_DEFAULT_PLATFORM=linux/amd64 bash tests/deploy.sh --docker-restore` — PASS:
|
||||||
|
заменённый bind-каталог читается после force-recreate. Без пересоздания исходный
|
||||||
|
live repro читал старые данные; README recovery исправлен по этому результату.
|
||||||
|
- Disposable smoke с реальными Nexus images `0.2.3` — PASS: init, no-op по native
|
||||||
|
Compose hash, frontend-only/backend-only config update, stopped backup,
|
||||||
|
`PRAGMA integrity_check`, HTTPS deep link, registration, Secure-cookie и refresh.
|
||||||
|
- Explicit restore на синтетических данных — PASS: login, карта и её баланс,
|
||||||
|
отсутствие более новой карты и расшифровка тестового токена штатным TbankCrypto.
|
||||||
|
Оба тестовых Compose projects удалены; настоящие пользовательские данные
|
||||||
|
и реальное подключение к T-Bank не использовались.
|
||||||
|
- OCI revisions совпали с Git tags компонентов: frontend
|
||||||
|
`f486ae4fb2d06e71779901bde283950542f27ed2`, backend
|
||||||
|
`0862d56c4e4d9dd554556c3992cc073193020ff1`.
|
||||||
|
- Среда интеграционной проверки: Docker Desktop, Linux amd64 images через
|
||||||
|
эмуляцию на arm64 Mac, Compose 5.5.1; host flock в smoke заменён stdlib fcntl.
|
||||||
|
Это не проверка конфигурации целевого Linux-сервера или его registry login.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-specs",
|
"name": "wealth-ledger-specs",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wealth-ledger-specs",
|
"name": "wealth-ledger-specs",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "21.2.2",
|
"@commitlint/cli": "21.2.2",
|
||||||
"@commitlint/config-conventional": "21.2.2",
|
"@commitlint/config-conventional": "21.2.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-specs",
|
"name": "wealth-ledger-specs",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "24.21.0"
|
"node": "24.21.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user