From 2f5814abdd52984bf1bcbd1d09b27ff4df3d7a25 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 26 Sep 2026 21:29:24 +0300 Subject: [PATCH] spec: complete persist-tbank-data implementation Record the approved proposal, delta specifications, design and 27 verified implementation tasks. Keep the change active; archiving and main-spec synchronization are deferred. --- .../changes/persist-tbank-data/.openspec.yaml | 2 + openspec/changes/persist-tbank-data/design.md | 566 +++++++++ .../changes/persist-tbank-data/proposal.md | 73 ++ .../spec.md | 145 +++ .../specs/brokerage-account-ledger/spec.md | 95 ++ .../brokerage-account-upcoming-events/spec.md | 104 ++ .../specs/brokerage-accounts/spec.md | 133 ++ .../specs/frontend-data-resilience/spec.md | 59 + .../specs/portfolio-view/spec.md | 144 +++ .../specs/tbank-data-sync/spec.md | 313 +++++ openspec/changes/persist-tbank-data/tasks.md | 1116 +++++++++++++++++ 11 files changed, 2750 insertions(+) create mode 100644 openspec/changes/persist-tbank-data/.openspec.yaml create mode 100644 openspec/changes/persist-tbank-data/design.md create mode 100644 openspec/changes/persist-tbank-data/proposal.md create mode 100644 openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md create mode 100644 openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md create mode 100644 openspec/changes/persist-tbank-data/tasks.md diff --git a/openspec/changes/persist-tbank-data/.openspec.yaml b/openspec/changes/persist-tbank-data/.openspec.yaml new file mode 100644 index 0000000..070137e --- /dev/null +++ b/openspec/changes/persist-tbank-data/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd +created: 2026-09-24 diff --git a/openspec/changes/persist-tbank-data/design.md b/openspec/changes/persist-tbank-data/design.md new file mode 100644 index 0000000..eccaed2 --- /dev/null +++ b/openspec/changes/persist-tbank-data/design.md @@ -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. Числовые результаты будут получены задачами +проверки, а не вписаны заранее. diff --git a/openspec/changes/persist-tbank-data/proposal.md b/openspec/changes/persist-tbank-data/proposal.md new file mode 100644 index 0000000..85d2ccc --- /dev/null +++ b/openspec/changes/persist-tbank-data/proposal.md @@ -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. diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md b/openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md new file mode 100644 index 0000000..2700329 --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md @@ -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 с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md b/openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md new file mode 100644 index 0000000..78b911a --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md @@ -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`. diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md b/openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md new file mode 100644 index 0000000..9a13470 --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md @@ -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 diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md b/openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md new file mode 100644 index 0000000..8015d5b --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md @@ -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 diff --git a/openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md b/openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md new file mode 100644 index 0000000..c330a8d --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md @@ -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** клиент прекращает наблюдение и не начинает новые периодические синхронизации; уже начатая серверная работа может завершиться diff --git a/openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md b/openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md new file mode 100644 index 0000000..6da60ec --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md @@ -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-состояние без валютных + карточек с вымышленными нулевыми итогами diff --git a/openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md b/openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md new file mode 100644 index 0000000..449524e --- /dev/null +++ b/openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md @@ -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 этого интервала, независимо от числа страниц клиентской выдачи diff --git a/openspec/changes/persist-tbank-data/tasks.md b/openspec/changes/persist-tbank-data/tasks.md new file mode 100644 index 0000000..ae5b115 --- /dev/null +++ b/openspec/changes/persist-tbank-data/tasks.md @@ -0,0 +1,1116 @@ +Выполнены implementation-owned задачи 1.1–7.5 (27/27). Archive не выполнялся. Текущее evidence — в checkpoint ниже. +Входы: [Design revision 1](design.md), включая полностью inline UI/UX package, +и [delta specs](specs/tbank-data-sync/spec.md). Перед работой в каждом repo +прочитать его AGENTS и применимый routing; перед Node — местный `nvm use --silent`. +Ни одна задача не требует изменения root или чтения соседнего checkout в CI. + +## 1. Основание и сохраняемая модель + +- [x] 1.1 [backend] Применить Solution Assessment: использовать текущие Prisma/SQLite/TbankClient, зафиксировать baseline HTTP/RPC и подготовить изолированные исходные данные профиля Design без новой очереди или scheduler. + - **Covers:** `N/A` — техническое основание storage/concurrency решения и сопоставимых измерений из Design; самостоятельного пользовательского поведения не добавляет. + - **Verification:** сохранить версии runtime, hardware, объём/распределение fixture и числа RPC/latency исходного navigation trace; проверить, что тесты не используют рабочую SQLite и реальные token; проверить существующие owners и отсутствие добавленной инфраструктуры. + +- [x] 1.2 [backend] После 1.1 добавить additive Prisma migration для connection revision, membership, private datasets и последних полных прогнозов, общих инструментальных источников, coverage/work/staging с описанными unique keys и индексами. + - **Covers:** `tbank-data-sync` — `Все экраны читают сохранённые данные без ожидания T-Bank`; `tbank-data-sync` — `Общие данные инструментов отделены от личных`. + - **Verification:** `npx prisma validate`, generate/typecheck и migration на пустой и заполненной временной SQLite; автоматическая проверка integrity/FK, сохранности User/Session/TbankConnection/manual cards и исходных зашифрованных token; decimal round-trip без float; подтверждение ключей/индексов и невозможности дублирующей операции. + +- [x] 1.3 [backend] После 1.2 реализовать локальный membership owner и атомарную границу PUT/DELETE подключения, включая использование списка успешной проверки token и publication fence по revision/attempt. + - **Covers:** `tbank-data-sync` — `Локальная проверка доступа сохраняет границу подключения`; `brokerage-accounts` — `Состояния недоступности списка счетов`. + - **Verification:** реальные SQLite/HTTP проверки двух пользователей, unknown/empty/stale membership, closed/unsupported/removed account, invalid token, failed replacement и late completion после replace/delete; на warm access path spy фиксирует 0 GetAccounts до HTTP-ответа; данные/coverage/errors старой revision не публикуются. + +## 2. Интервалы и выполнение запрошенной работы + +- [x] 2.1 [backend] После 1.2–1.3 реализовать операции staging → atomic reconciliation → coverage/dataRevision с обязательным исходным ID и локальной фильтрацией границ. + - **Covers:** `tbank-data-sync` — `Сверка операций сохраняет идентичность и прежний результат`; `tbank-data-sync` — `Полнота подтверждается успешным покрытием интервалов`; `brokerage-account-ledger` — `Ledger entries preserve broker operation meaning`. + - **Verification:** SQLite integration на нескольких страницах: одинаковый ID, изменённые amount/date/state, исчезнувшая операция, identical duplicate, conflicting duplicate, missing ID/cursor и сбой средней страницы; полная старая выборка/сумма/время не меняются до commit, удаление ограничено account/interval/predicate, частичный income-filter не подтверждает весь счёт. Refresh полного квартала/all с ошибкой второго месяца сохраняет весь прежний результат до атомарного commit publication group. + +- [x] 2.2 [backend] После 2.1 реализовать durable intent и in-process выполнение с coalescing, priority, ограниченной подачей RPC и request-only recovery по processRunId/attemptId. + - **Covers:** `tbank-data-sync` — `Прогресс переживает перезапуск и одновременные запросы`; `tbank-data-sync` — `Обновление запускается только запросом с учётом свежести`. + - **Verification:** автоматические гонки 20 одинаковых запросов, пересечения диапазонов и stale attempt; kill/restart между страницами и между stage/publish; без нового запроса 0 запусков, следующий запрос начинает незавершённое окно с первой страницы без дублей; после terminal failure нет retry loop; existing limiter/retry bounds сохраняются. + +- [x] 2.3 [backend] После 2.2 реализовать planner экранных зависимостей, TTL и первичной догрузки истории с учётом календарей и unknown lower bound. + - **Covers:** `tbank-data-sync` — `Первая загрузка приоритизирует экран и продолжает историю`; `tbank-data-sync` — `Полнота подтверждается успешным покрытием интервалов`; `tbank-data-sync` — `Ручное обновление ограничено текущим экраном`. + - **Verification:** controlled-clock сценарии 59/60, 299/300 и 86399/86400 секунд; повтор current `to=now` через секунду не создаёт RPC и сохраняет asOf; selected/previous/yearAgo выполняются до старой истории; новый видимый пропуск обгоняет pending backfill; Moscow/UTC boundary, конец месяца/года/февраля и незагруженный хвост; openedOn отсутствует — coverage явно ограничена, вся история не объявлена полной. Зафиксировать проверенные source boundary semantics без выдуманного default `from`. + +## 3. Локальные владельцы данных и производные результаты + +- [x] 3.1 [backend] После раздела 2 перевести accounts, portfolio и все consumers личных позиций на общий persistent portfolio/membership owner. + - **Covers:** `brokerage-accounts` — `Список активных брокерских счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `tbank-data-sync` — `Все экраны читают сохранённые данные без ожидания T-Bank`. + - **Verification:** configured HTTP/SQLite сценарии warm/cold/empty/stale portfolio, independent summary failure, all-account membership revision, отсутствующий metadata fallback и known access error; прежние Money/order/unsupported account правила сохранены; portfolio старше пяти минут остаётся доступен при допустимом доступе без блокирующих RPC. + +- [x] 3.2 [backend] После 2.1–2.3 перевести ledger и instrument personal history на локальные фильтры, детерминированную пагинацию и revision-bound cursor; убрать обязательный RAM-page TTL read path. + - **Covers:** `brokerage-account-ledger` — `User can retrieve an account ledger`; `brokerage-account-ledger` — `User can browse a filtered ledger`; `brokerage-account-ledger` — `Кэш страниц журнала ограничивает локальные ресурсы`. + - **Verification:** HTTP/DB cursor tests на равных timestamps, нескольких счетах, эквивалентных filters, connection/membership/rowset change, invalid cursor и `hasNext=false` при неполном coverage; categories/UID/page limit не создают полноту; повторная старая страница не вызывает RPC; personal instrument force удаляет только записи своего predicate. + +- [x] 3.3 [backend] После 3.2 перевести income/summary на сохранённые операции и раздельную готовность selected/previous/yearAgo, сохранив точные денежные правила. + - **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`. + - **Verification:** денежные integration tests с несколькими валютами, отрицательной корректировкой, одинаковыми ID одного/разных счетов, empty complete и missing periods; selected complete/previous loading не даёт нулевую базу; all aggregation требует каждого счёта; обновлённая запись пересчитывается один раз; новый period не возвращает старый под новой подписью. + +- [x] 3.4 [backend] После 2.2–2.3 разделить shared catalog/metadata/company/quotes и личные projections; сохранить existing UID/FIGI/ticker lookup и источники bond metrics. + - **Covers:** `tbank-data-sync` — `Общие данные инструментов отделены от личных`; `tbank-data-sync` — `Все экраны читают сохранённые данные без ожидания T-Bank`; `tbank-data-sync` — `Обновление запускается только запросом с учётом свежести`. + - **Verification:** два пользователя используют сохранённый общий источник без повторного RPC и без передачи private fields/errors; каталог без enrichment не выдаёт ложный пустой поиск; свежий query SBER не подтверждает unknown GAZP, повтор того же query использует сохранённый discovery; company/fundamentals included; minute/day TTL независимы; stale initiating connection не публикует даже общий результат; bond quote percent/nominal/ACI и optional MOEX failures сохраняют прежний смысл. + +- [x] 3.5 [backend] После 3.4 реализовать durable candle coverage, mutable tail и portfolio-instrument prefetch через тот же planner. + - **Covers:** `tbank-data-sync` — `Завершённость свечей подтверждается источником`; `tbank-data-sync` — `Первая загрузка приоритизирует экран и продолжает историю`. + - **Verification:** автоматические window tests: 365-day seams/dedupe, complete candle без RPC после restart, old incomplete candle после полуночи, утреннее пустое окно → новая свеча, закрытое пустое окно, missing historyFrom, ошибка ручной перечитки; prefetch только бумаг текущего portfolio, открытие другого инструмента запускает его историю. + +- [x] 3.6 [backend] После 3.1 и 3.4 перевести instrument payouts и account upcoming events на общие сохранённые календари и локальные personal estimates, устранив отдельный synchronous portfolio fetch. + - **Covers:** `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Forecasts preserve event meaning and uncertainty`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`; `tbank-data-sync` — `Общие данные инструментов отделены от личных`. + - **Verification:** полные/неполные/пустые календари, cancelled dividends, offer без суммы, MOEX missing block, stock/bond source identity, holdings-dependent estimate и отсутствие private values в shared tables; новый portfolio с новой бумагой + ошибка её календаря + restart возвращает прежний полный прогноз/estimate того же scope с исходным временем; новый period/membership не получает чужой snapshot. Events refresh сохраняет применённые from/to. + +## 4. Публичный API и передача контракта + +- [x] 4.1 [backend] После раздела 3 оформить envelope/meta/dependency coverage всех существующих data GET, observe mode, screen-scoped POST sync и авторизованный GET status в DTO/controllers/Swagger. + - **Covers:** `tbank-data-sync` — `Ручное обновление ограничено текущим экраном`; `tbank-data-sync` — `Интерфейс различает свежесть полноту и состояние работы`; `tbank-data-sync` — `Локальная проверка доступа сохраняет границу подключения`. + - **Verification:** configured HTTP contract tests для каждого route family, включая accounts/search arrays, stock company, quotes/candles/payouts и all scope; `202` не означает завершение, malformed descriptor отклонён, чужой job 404; `sync=observe`/status не создают RPC при stale/missing/failed; generation отсутствует в API/errors/logs и декодируемых cursors; Problem Details и nullable previous/yearComparison/summary dependency states соответствуют Design. Empty coverage completion меняет stateVersion без изменения pagination revision. + +- [x] 4.2 [backend] После 4.1 опубликовать сгенерированный OpenAPI artifact для frontend и проверить runtime parity, не превращая specs в editable HTTP-контракт. + - **Covers:** `N/A` — передача изменённого публичного контракта из Contract Gate Decision, без самостоятельной бизнес-возможности. + - **Verification:** `npm run test:openapi-contract` и сформированный backend-owned artifact; новые DTO/status/errors представлены, secret examples отсутствуют; artifact передаваем явно, backend CI не читает соседний frontend. + +- [x] 4.3 [frontend] После 4.2 принять предоставленный snapshot, выполнить локальную генерацию и адаптировать entity query owners, включая observe reads и connection/session fences. + - **Covers:** `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `tbank-data-sync` — `Локальная проверка доступа сохраняет границу подключения`. + - **Verification:** `npm run openapi:generate`, `npm run openapi:check`; typecheck всех consumers, включая breadcrumbs, document title и scope switch; MSW/RTL доказывает одну canonical cache identity для ordinary/observe, поздние data/status ответы старой connection/session игнорируются, all financial caches очищаются успешной mutation и сохраняются после неуспешной. + +## 5. Экранные состояния и ручное обновление + +- [x] 5.1 [frontend] После 4.3 добавить page-owned screen descriptor и observer запрошенной работы по inline UI/UX package revision 1; разделить transport и source lifecycle. + - **Covers:** `tbank-data-sync` — `Интерфейс различает свежесть полноту и состояние работы`; `tbank-data-sync` — `Ручное обновление ограничено текущим экраном`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`. + - **Verification:** MSW/RTL controlled-clock checks: manual refresh обходит TTL только текущего scope; status poll + observe не создаёт новую работу даже после истечения минуты; observer прекращается при terminal/уходе/logout, active duplicate button недоступна; empty coverage completion отображается до завершения дальней истории; reverse-order observe ответы не откатывают данные/доступ; old data/timestamp остаются при transient failure, failed force до TTL сохраняет fresh, known access error очищает personal view. + +- [x] 5.2 [frontend] После 5.1 адаптировать accounts/account/all screens и независимые overview sections к server completeness/freshness и scoped refresh. + - **Covers:** `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`. + - **Verification:** RTL scenarios unknown/complete-empty accounts, independent summary staleness, portfolio + recent ops + income + events partial states, сохранённые суммы при failed refresh, 404/invalid token, новая membership в all; кнопка обновляет dependencies overview, не только portfolio. + +- [x] 5.3 [frontend] После 5.1 адаптировать operations/income/events pages: отдельная полнота периодов и comparisons, cursor recovery и сохранение applied filters. + - **Covers:** `brokerage-account-ledger` — `User can browse a filtered ledger`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`. + - **Verification:** RTL/MSW: partial empty не показывает «операций/доходов нет», selected готов до comparison без нулевой базы, отрицательная корректировка/одна карточка на identity, INVALID_OPERATIONS_CURSOR возвращает первую страницу с сохранением фильтров, смена периода не переименовывает старые суммы; полный старый events result виден с ошибкой обновления. + +- [x] 5.4 [frontend] После 5.1 адаптировать stock/bond search и detail sections: quote, company/metrics, candles, position, payouts и personal history к общим статусам и текущему screen scope. + - **Covers:** `tbank-data-sync` — `Все экраны читают сохранённые данные без ожидания T-Bank`; `tbank-data-sync` — `Завершённость свечей подтверждается источником`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`. + - **Verification:** section-adapter и page integration tests различают missing/partial/complete-empty/stale/failed, неполные свечи и личные estimates; refresh сохраняет выбранный график/UID и обновляет показанную personal history, не весь каталог; metadata completion не меняет личную cache boundary. + +## 6. Доказательства корректности и быстродействия + +- [x] 6.1 [backend] После раздела 4 выполнить интегрированный crash/durability/rollback прогон на заполненной изолированной SQLite, расширив существующий подход сохранности данными T-Bank. + - **Covers:** `tbank-data-sync` — `Прогресс переживает перезапуск и одновременные запросы`; `tbank-data-sync` — `Сверка операций сохраняет идентичность и прежний результат`; `tbank-data-sync` — `Локальная проверка доступа сохраняет границу подключения`. + - **Verification:** сохранить runnable DB/HTTP tests для kill-before/after-publish, concurrent readers, membership change, replace/delete и shared result fence; подтвердить Rollback Contract без потери ручных карт и новых данных; unit mocks не заменяют реальные транзакции и reopen DB. + +- [x] 6.2 [backend] После 6.1 измерить HTTP budget и RPC reduction на профиле Design с задержанным/отказывающим upstream и одинаковым baseline trace из 1.1. + - **Covers:** `tbank-data-sync` — `Быстродействие и сокращение запросов проверяются измерениями`; `tbank-data-sync` — `Все экраны читают сохранённые данные без ожидания T-Bank`; `tbank-data-sync` — `Обновление запускается только запросом с учётом свежести`. + - **Verification:** ≥100 запросов каждой указанной группы, 10 concurrent clients, p50/p95/p99/max/размеры ответов/hardware; цель p95 ≤500 мс, отдельно первый read после restart; fresh RPC=0, старые полные intervals RPC=0, 20 одинаковых stale requests=1 логический проход/dependency, завершённая работа без нового запроса RPC=0. Показать old/new counts и снижение; провал бюджета не объявлять успехом и не менять порог без явного решения. + +- [x] 6.3 [frontend] После раздела 5 проверить inline UI/UX package revision 1 в браузере и сохранить уместные состояния в существующих stories на вымышленных данных. + - **Covers:** `tbank-data-sync` — `Интерфейс различает свежесть полноту и состояние работы`; `brokerage-accounts` — `Экран списка брокерских счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`. + - **Verification:** journey accounts → account/all → ledger → income/comparison → events → stock/bond: cold, complete, partial, stale, failure, force refresh, leave/reopen, connection replacement; 320/375 px и широкий экран, длинные суммы, keyboard/focus, polite announcements, busy/skeleton/reduced motion. Browser evidence дополняет tests; Storybook build не доказывает состояния. + +## 7. Документация и итоговые gates + +- [x] 7.1 [docs] После подтверждённой реализации обновить as-built storage/access/freshness, инвестиционные flows, public API handoff и термин полноты, сохранив ADR как историю принятого решения. + - **Covers:** `N/A` — delivery объяснений поставленной архитектуры из Design, без параллельного источника требований. + - **Verification:** сверить architecture/containers, cross-cutting data-freshness/ui-resilience, http-api, integrations/tbank, domains accounts/operations/portfolio/income/instruments и glossary со всеми delivered flows; обновить отметку отсутствующей реализации ADR 017 только по факту поставки, сохранить исторические решения и чужие изменения. Пути README → тема → термин/API существуют. + +- [x] 7.2 [backend] После 6.1–6.2 выполнить canonical gate для итогового backend и передать финальный OpenAPI artifact при отличиях от 4.2. + - **Covers:** `N/A` — canonical backend verification из Delivery Map и Contract Gate Decision; не отдельное поведение. + - **Verification:** `npm ci`, `npm run verify`, `npm run test:openapi-contract`; результаты обязательных DB/HTTP/latency checks относятся к тому же итоговому состоянию. Type/lint/build не объявляются доказательством migration/concurrency или latency. + +- [x] 7.3 [frontend] После 6.3 и финального artifact backend выполнить canonical frontend gate для итоговых types/queries/UI/stories. + - **Covers:** `N/A` — canonical frontend verification изменённого API/UI package из Delivery Map, без самостоятельной бизнес-возможности. + - **Verification:** `npm ci`, `npm run verify`, включая локальный OpenAPI drift; браузерные и boundary checks должны относиться к итоговому коду. Frontend CI использует свой snapshot, а не checkout backend. + +- [x] 7.4 [docs] После 7.1 выполнить canonical docs gate и смысловую проверку изменённых маршрутов чтения. + - **Covers:** `N/A` — проверка связности as-built documentation из Delivery Map, без новых требований. + - **Verification:** `npm run verify`, проверка новых/изменённых ссылок и терминов; если изменена диаграмма, проверить её renderer либо явно назвать ограничение. Не выдавать Markdown lint за семантическое подтверждение. + +- [x] 7.5 [specs] После implementation-owned задач сверить выполненный результат с каждой delta requirement и обновить этот Tasks по наблюдённым результатам без преждевременного archive/main-spec sync. + - **Covers:** `N/A` — поддержание существующего SDD change и traceability реализации из Delivery Map; новых behaviors или отдельного журнала доказательств не создаёт. + - **Verification:** все 35 delta requirement headings, включая REMOVED, имеют выполненные задачи и подходящее evidence; `npm run verify`, `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate persist-tbank-data --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`; проверить новые файлы, ссылки и diff. Structural success не означает реализации или approval. + +Независимое review и последующее завершение lifecycle не маскируются задачами, +которые implementation agent может закрыть самостоятельно. Эта подготовка +не разрешает менять приложение, применять миграцию к рабочей БД или +архивировать change; текущий результат — комплект для начала реализации. + +## Implementation checkpoint — 2026-09-24 + +Пользователь разрешил Apply утверждённых артефактов, включая изменения приложения, +публичного API и документации. Архивирование не входит в текущую работу. + +- Completed: 0/27. Задача 1.1 выполнена частично; checkbox не установлен. +- Backend: ветка `codex/persist-tbank-data` от полученного `origin/develop` + `0862d56c4e4d9dd554556c3992cc073193020ff1`. Production source/schema не изменены. + Добавлены `test/tbank-read-baseline.e2e-spec.ts` и + `test/fixtures/tbank-read-baseline.json` с исходным HTTP trace и результатами. +- Наблюдённый baseline: временная SQLite со штатными migrations, синтетическое + подключение, 3 счёта × 100 000 доступных upstream операций, 200 инструментов; + 7 групп × 100 чтений, 10 клиентов. Node v24.21.0, Apple M1 Pro, + 10 CPU, 32 GiB. Счётчики считают вызовы TbankClient, а не реальные transport RPC. + p95 client end-to-end: accounts 33.05 мс, portfolio 15.21 мс, + all portfolio 30.39 мс, ledger 13.17 мс, all ledger 16.20 мс, + income 2345.84 мс, all income 7128.47 мс. Полные percentile/size/counters + сохранены в JSON; это исходная реализация и не доказательство новой модели. +- Наблюдённые проверки: backend `npm run build`, `npm run typecheck:test`, + targeted ESLint, `npm run verify` (35 suites, 257 tests) — success; + `node test/sqlite.cjs tbank-read-baseline.e2e-spec.ts` — success (1 test). + HTTP runner потребовал разрешённый loopback listener вне sandbox. + Рабочая пользовательская БД и реальные credentials не использовались. +- Specs: `npm run verify`, native schema validation, strict change validation + с `--store wealth-ledger-specs` и strict validation всех 25 main specs — success. + Повтор `npm ci` с сетевым доступом завершился HTTP 502 настроенного Nexus + для `undici-types-8.9.0.tgz`; dependencies/lockfile не менялись. +- Backend `npm ci` после проверок также завершился HTTP 502 Nexus для + `zeptomatch-2.1.0.tgz`. Установка могла оставить неполный node_modules; + успешный verify выше относится к прежнему установленному окружению. + Воспроизводимая установка и canonical gate текущей поставки не закрыты. +- Remaining: задачи 1.1–7.5. Baseline пока не включает market/candles/payouts, + delayed/failing transport, профиль 4 CPU / 8 GiB или заполненные новые таблицы. + Миграция, новая модель чтения, frontend/API handoff и as-built docs не реализованы. +- Next implementation outcome: закончить 1.1 полным воспроизводимым trace + профиля Design и подготовкой fixtures, затем переходить к additive migration 1.2. + +### Продолжение установки + +Повторные установки через Nexus снова получили HTTP 502. Выполнен успешный +`npm ci --registry=https://registry.npmjs.org --replace-registry-host=never` +в backend (1065 packages) и specs (239 packages), с прежними lockfile/integrity. +Manifest, lockfile и постоянная registry configuration не менялись. +Это восстанавливает локальное окружение, но не исправляет доступность Nexus. + + +### Checkpoint после продолжения Apply + +- Completed: 1.1, 1.2 (2/27); remaining: 1.3–7.5. Предыдущий checkpoint + описывает предыдущий прогон, а не текущее состояние реализации. +- 1.1: расширенный baseline успешно измерил 23 группы × 100 чтений, + 10 concurrent clients, включая stock/bond detail, quote, company/metrics, + year/all candles, payouts/search и account/all events. Fixture предоставляет + 3 × 100 000 операций и 200 × 5000 свечей; все внешние providers синтетические. + Полный trace/percentiles/bytes/счётчики вызовов внешних clients сохранены + в backend `test/fixtures/tbank-read-baseline.json`. Это измерение исходной + модели на фактическом host, не доказательство budget новой модели 6.2; + transport limiter/retries исключены из этих счётчиков. +- 1.2: добавлены Prisma models и migration `20260924170000_persist_tbank_data`. + Revision присваивается существующим подключениям без изменения ciphertext; + SQLite rebuild подключения и новые таблицы выполняются одной транзакцией. + Private membership/portfolio/operations/coverage/forecasts/work/staging + связаны FK с revision; shared metadata/lookups/catalog/sources/candles/coverage + хранятся отдельно. Decimal payload и candle close остаются строками. +- Наблюдённые проверки 1.2: Prisma validate/generate, app/test typechecks, + `node test/sqlite.cjs tbank-migration.e2e-spec.ts` (2 passed). + Проверены пустая/заполненная SQLite, reopen, integrity/FK, сохранность старых + User/Session/connection/manual-card records, unique operation identity, + одинаковые ID разных счетов, decimal round-trip, индекс чтения, отказ позднего + staging attempt, cascade private rows и сохранение shared candle. +- 1.3 частично: PUT повторно использует accounts из validateToken, атомарно + заменяет connection/private rows и публикует membership. Local access method + не расшифровывает token и не делает RPC; различает unknown/empty/stale и + известный invalid token. `node test/sqlite.cjs tbank-connection.e2e-spec.ts` + (3 passed) проверяет HTTP auth, two users, failed replacement, DB rollback, + revision rotation и поздние private writes после replace/delete. + Известный invalid token блокирует и существующий HTTP accounts read до RPC; + финальный повтор HTTP suite (постоянный loopback listener) и verify прошёл. + Data GET ещё используют прежние owners; общий publication fence и discovery + через coordinator ещё не реализованы. Checkbox 1.3 не установлен. +- Итоговый backend `npm run verify`: 34 suites / 253 tests passed. + `npm run test:durability -- /tmp/wealthledger-tbank-rollback.PuzfjF` passed + на отдельной предыдущей сборке из `0862d56` и временной SQLite: manual data, + restart, rollback/return и backup/restore сохранены. Этот прогон не закрывает + задачу 6.1 о новых datasets и worker crash; результатов для неё пока нет. +- Frontend, docs, публичный envelope и OpenAPI handoff не изменены. + Рабочая пользовательская БД не использовалась. Change не архивирован. +- Next implementation outcome: завершить 1.3 — membership discovery/publication + с transaction fence текущей revision/attempt, затем staging/reconciliation 2.1. + +### Checkpoint — membership publication и операции staging + +- Completed остаётся 2/27. В 1.3 добавлена транзакционная публикация membership + через существующий access owner: revision/attempt fence, сохранение datasets + неизменных accounts, cascade удалённых accounts, отказ устаревшему invalid-token + результату. Отдельный coordinator ещё не подключён; checkbox не изменён. +- Начата persistence boundary 2.1: concrete operations repository сохраняет + страницы и continuation progress в SQLite staging, требует исходный ID, + проверяет pagination и противоречащие payload, локально фильтрует `[from,to)`. + Publication group обновляет операции/coverage всех accounts и intervals одной + транзакцией; instrument predicate ограничивает удаления и coverage. + operationsRevision изменяется только вместе с rowset, один раз на account. +- Наблюдённые проверки: `node test/sqlite.cjs tbank-connection.e2e-spec.ts` + — 4 passed; `node test/sqlite.cjs tbank-operations-persistence.e2e-spec.ts` + — 4 passed. Проверены rollback неполной группы, отрицательная корректировка, + одинаковый ID разных accounts, repeated/missing cursor, отсутствие ID, + exact upper boundary, конфликт версий между intervals, predicate isolation, + прежняя attempt, concurrent read отдельным SQLite connection и reopen. + Использованы только изолированные временные SQLite. +- Backend `npm run verify` прошёл (34 suites / 253 tests, lint, typechecks, build). + Это проверка текущего промежуточного состояния, не финальный gate задачи 7.2. +- Оставшиеся ограничения: HTTP owners ещё не читают новые datasets; + worker/coalescing/recovery и interval planner не реализованы. Repository + publication пока использует построчные Prisma operations; производительность + на 300 000 строк и budget 500 мс не подтверждены. Проверка kill процесса, + shared publication fence и полный Rollback Contract остаются в 6.1–6.2. + Frontend/docs и публичный API не изменены, OpenAPI handoff не выполнялся. + +### Checkpoint — request-driven operations worker + +- Реализован следующий backend этап 2.1–2.2: operations worker получает token + только для действующей revision/attempt, сохраняет intent и staging progress, + загружает EXECUTED pages через прежний TbankClient и публикует coverage вместе + с завершением работы. Уже полное покрытие автоматически требует атомарной + повторной сверки всей группы, даже при запросе инкрементального режима. +- Нормализуются пересекающиеся targets одного account/predicate; RPC windows + ограничены UTC-месяцем, с защитной секундой по краям и точным storage filter. + Двадцать одинаковых запросов присоединяются к одной попытке. Частичное + пересечение расширяет pending target только непокрытым остатком. Запрос, + соединяющий несколько активных групп, fenced supersedes их producers и + перезапускает объединённый staging новой попыткой; старые handles наблюдают + successor без запуска работы. Этот случай может повторно загрузить ранее + staged страницы и должен учитываться в итоговых измерениях. +- Coordinator подаёт одну страницу за turn и выбирает screen перед history; + сетевой RPC не удерживает reservation/publication lock. Retry policy остаётся + в TbankClient. После terminal failure нет автоматического повторения. + При новой runId следующий request очищает unfinished staging/cursor; + ранее опубликованные интервалы первичной загрузки сохраняются. Observation + и создание нового service не запускают импорт. +- Evidence: `node test/sqlite.cjs tbank-operations-sync.e2e-spec.ts + tbank-operations-persistence.e2e-spec.ts` — 12 + 4 passed. Sync suite включает + настоящий дочерний process и SIGKILL между страницами, перед publish и сразу + после publish; child использует текущую сборку из `npm run build` и только + SQLite штатного isolated runner. После kill наблюдение не вызывает RPC, + запрос запускает unfinished window с первой страницы; committed result + и terminal state переживают kill-after-publish. +- Проверены полный квартал двух accounts, failure второго месяца и сохранность + всех прежних rows/coverage/timestamps, последующий atomic commit, 20-way + coalescing, overlap remainder, bridge handles, page priority, known invalid + token и отказ чужому account до RPC. Backend `npm run verify` — 35 suites / + 255 tests, lint, app/test typechecks и build passed. +- Completed остаётся 2/27: 1.3 пока не подтверждена на новом локальном HTTP + read path; 2.1–2.2 имеют проверенное operations execution ядро, но ещё не + подключены к экранным owners/public sync API. Общий discovery/shared-source + scheduling, source TTL/coverage planner 2.3, HTTP envelopes и frontend + остаются впереди. Это не итоговое доказательство 6.1, 6.2 или gate 7.2. + Бюджет 500 мс и полный RPC navigation trace пока не измерялись. + +### Checkpoint — operations coverage/freshness planner, 2026-09-25 + +- Продолжена 2.3: pure planner разделяет availability, freshness и asOf; + полнота вычисляется по union интервалов, независимо от pagination. Account + coverage обслуживает instrument subset, обратное не допускается. Current + moving end сохраняет наблюдённый asOf и не создаёт RPC на новый хвост внутри + TTL; в 300 секунд назначается сверка текущего UTC-месяца. Исторические полные + интервалы автоматически не перечитываются, реальные holes не скрываются, + после смены месяца незавершённый хвост прошлого месяца остаётся missing. +- Operations sync service получил `readPlan`: читает durable coverage, + проверяет account membership, возвращает plan и при необходимости запускает + existing request-driven worker. Observe всегда read-only; force перечитывает + показанные targets. Автоматическая сверка не добавляет RPC для уже полных + прошлых месяцев. Верхняя граница новой попытки ограничена временем запроса: + будущее не публикуется как подтверждённое coverage. +- Verification: backend `npm run verify` — 35 suites / 261 tests passed. + Pure calendar/planner suite — 8 tests: 299/300 секунд, current moving end, + explicit end, реальные gaps, instrument predicate, rollover и force. + `node test/sqlite.cjs tbank-operations-sync.e2e-spec.ts + tbank-operations-persistence.e2e-spec.ts` — 14 + 4 passed на итоговой сборке; + сохранены SIGKILL/race/atomic-quarter проверки. Новые SQLite сценарии + подтверждают 0 RPC свежего readPlan и stale observe, RPC обычного stale + request/force и отсутствие будущего coverage. +- Completed по-прежнему 2/27: это operations-часть planner, а не весь 2.3. + Source TTL 60/86400, discovery и shared sources, screen dependency plans, + comparisons/backfill/unknown lower bound, локальные HTTP owners и API envelope + ещё не поставлены. Новые функции пока не вызываются существующими HTTP GET. + Frontend/docs не изменялись; OpenAPI artifact не изменён. Budget 500 мс и + полный navigation RPC trace пока не измерены; change не архивирован. + + +### Checkpoint — local accounts/portfolio HTTP, 2026-09-25 + +- Закрыта 1.3: локальный membership/access owner, атомарная замена и удаление + подключения, discovery только по запросу, coalescing и revision/attempt fence. + SQLite/HTTP evidence: `tbank-connection.e2e-spec.ts` (4 tests) и + `tbank-local-reads.e2e-spec.ts` (6 tests). Проверены unknown/empty/stale, + два пользователя, unsupported/removed membership (закрытый счёт отсутствует + в GetAccounts с ACCOUNT_STATUS_OPEN), failed replacement, invalid token, + late publication после replace/delete и сохранение retained account data. +- Accounts и portfolio HTTP теперь читают локальные snapshots и возвращают + envelope; missing snapshot даёт null, observe не запускает работу. Полный + portfolio сохраняется без временного предела при неудачном обновлении. + Warm GET не вызывает GetAccounts, decrypt или metadata RPC. Exact total + `21.100000002` сохраняется. Двадцать одновременных reads объединяют discovery; + abandoned attempt восстанавливается только следующим обычным запросом. +- Исправлена freshness составного metadata: худшее обязательное состояние и + самый ранний успешный timestamp источников. Пустой aggregate использует TTL + membership (300 секунд); непустой portfolio — TTL snapshot (60 секунд). + Unit regression проверяет разные TTL и неизменность source timestamps. +- Verification: `npm run verify` — 36 suites / 255 tests passed; + `npm run openapi:generate` выполнен; runtime OpenAPI parity — 1 passed; + SQLite operations persistence/sync — 4 + 14 passed на актуальной сборке, + включая SIGKILL/recovery и атомарный refresh полного квартала/all. +- Completed 3/27. 3.1 не закрыта: legacy consumers личных позиций ещё используют + прежний путь. 2.3 и остальные source owners/API endpoints остаются в работе. + Backend OpenAPI сгенерирован; передача frontend snapshot и адаптация consumer + остаются задачами 4.2–4.3. Frontend/docs на этом этапе не изменялись. + Финальный gate 7.2, полный durability/rollback и профиль 500 мс/RPC reduction + не подтверждены. Change не архивирован. + + +### Checkpoint — batch 2.1–2.3, 3.1–3.2, 2026-09-25 + +- Закрыты **2.1, 2.2, 2.3, 3.1, 3.2**; completed **8/27**, remaining **19**. +- **2.1:** staging проверяет исходные ID, курсоры и конфликтующие версии, + включая смену state. Reconciliation обновляет дату/сумму, удаляет исчезнувшие + и отменённые операции только в перечитанном account/interval/predicate. + Полный квартал/all публикуется атомарно; ошибка средней страницы/месяца + сохраняет прежние строки, суммы, coverage и время. SQLite persistence — + **5 passed**, включая отдельного читателя, reopen и instrument predicate. +- **2.2:** coalescing пересечений включает whole-account/instrument scope; + новый экранный интервал обгоняет pending history, после публикации экранной + части остаток получает background priority. Force показанного периода не + ждёт несвязанный backfill: внутри durable intent хранится отдельная atomic + publication group. Её результат сохраняется при последующей ошибке истории. + SQLite sync — **17 passed**: 20 одинаковых requests, overlaps/bridge, + stale attempt, priority, failure без retry loop и реальные SIGKILL между + страницами, до/после publish; recovery запускается только новым запросом. + TbankClient остаётся единственным владельцем RPC limiter/retries. +- **2.3:** screen dependency planner описывает membership/portfolio, + applied operations, selected/previous/yearAgo через существующий income + planner, shared metadata/catalog/quote/company/metrics/calendars/candles. + Force исключает архив; ordinary ledger request после экранных интервалов + ставит недостающее account history с низким приоритетом. Оконные RPC идут + последовательно, без предварительной массовой подачи в limiter. + Unknown opening использует явный requested floor 1970, сохраняет unknown + boundary и не объявляет полную историю. TTL policy проверена на 59/60, + 299/300, 86399/86400 секундах; calendar tests покрывают Moscow/UTC, + февраль, смену месяца/года, moving end и missing tail. Proto не обещает + default-from/inclusivity: используются явные bounds, защитная секунда и + точный локальный [from,to), без нового допущения о нижней границе источника. + План shared зависимостей готов; их persistent owners/execution и candle + prefetch поставляются задачами 3.4–3.6, public screen dispatch — 4.1. +- **3.1:** старые RAM portfolio snapshots удалены; accounts, stock/bond + personal positions и upcoming-events используют общий persistent owner. + Отдельный upstream portfolio fetch событий удалён. Missing member скрывает + aggregate/position, не превращается в ноль; summary другого счёта остаётся + доступной. Проверены metadata fallback, source timestamp, indefinitely stale + portfolio, known access failure и reopen. Local reads — **7 passed**; + portfolio structure HTTP теперь использует настоящую SQLite — **4 passed** + (точные валюты, отрицательные остатки и неизвестные valuations). +- **3.2:** ledger/person-instrument history читают SQLite transaction со + стабильным порядком occurredAt/accountId/brokerOperationId. HMAC cursor + связан с user/connection, all membership, нормализованным filter/limit и + account row revisions, без connection identifier в payload. Изменение + coverage/time не ломает cursor; изменение rows/owner/connection — ломает. + Read-only observe не ставит work; partial coverage не становится complete + из-за hasNext=false, category/UID или размера страницы. HTTP SQLite — + **5 passed**, включая reopen, равные даты/ID, equivalent categories и + отсутствие RPC старых полных страниц. RAM page TTL read path удалён. + Legacy RPC collector сохранён только для ещё не переведённого income (3.3). +- **Gates:** `npm run verify` — **38 suites / 235 tests passed**, lint, + app/test typecheck и build успешны. `npm run openapi:generate` выполнен; + `openapi-contract.e2e-spec.ts` — **1 passed**. Дополнительно сохранены + connection SQLite/HTTP — **4 passed**. Всего relevant SQLite/HTTP — + **43 passed** (1 + 4 + 5 + 17 + 7 + 5 + 4), рабочая БД не использовалась. + `git diff --check` успешен; native validation и specs gate выполнены. +- Frontend/docs в этом batch не изменялись. Generated backend `openapi.json` + содержит новые envelopes; явная передача consumer snapshot остаётся 4.2. + Профиль 500 мс/RPC reduction, итоговый durability/rollback и gates 7.2–7.5 + не закрыты; change не архивирован и не объявляется завершённым. +- **Следующий Implementation outcome:** 3.3 — income/summary из согласованного + локального набора операций с отдельной готовностью selected/previous/yearAgo + и сохранением прежнего полного результата при неудачной сверке. + + +### Checkpoint — local income/summary, 2026-09-25 + +- Закрыта **3.3**, completed **9/27**, remaining **18**. Income/summary читают + операции, coverage и membership одной SQLite read transaction. Удалён + последний synchronous operations collector; income больше не получает + cursor pages или metadata до HTTP-ответа. +- Selected, previous и yearAgo имеют отдельные coverage/status; неполный + selected возвращает data:null, незагруженное сравнение — null без нулевой + базы. Невозможное календарное/историческое сравнение имеет unavailableReason. + Moscow inclusive dates преобразуются в half-open UTC; historical period + не расширяется до now, current month сохраняет подтверждённый asOf. + Расчёты используют прежние decimal helpers и source timestamps. +- Наблюдённые проверки: `npm run verify` — **36 suites / 232 tests passed**, + lint, app/test typecheck и build; `npm run openapi:generate` выполнен. + `node test/sqlite.cjs tbank-income-local.e2e-spec.ts + tbank-ledger-local.e2e-spec.ts tbank-operations-persistence.e2e-spec.ts + tbank-operations-sync.e2e-spec.ts openapi-contract.e2e-spec.ts` — + **7 + 5 + 5 + 17 + 1 passed**. Только временные изолированные SQLite. + Income HTTP проверяет exact signed multi-currency sums, одинаковые ID разных + accounts, correction без повторного учёта, partial all/complete empty, + раздельные comparisons, другой period, source time/asOf, reopen, observe, + blocked upstream, access/period validation и невидимость unfinished staging + с сохранением старого финансового результата после failure. +- Старые mock-only suites синхронного collector заменены SQLite income + проверками; pure финансовые проверки сохранены. Backend OpenAPI содержит + income envelopes и nullable previous. Передача consumer snapshot остаётся + задачей 4.2; frontend/docs на этом этапе не изменялись. +- В запрошенном batch **3.4–3.6 и 4.1 ещё не выполнены**. Общие источники, + свечи, durable forecasts и public sync API требуют дальнейшей реализации. + Измерения профиля 500 мс/RPC reduction и финальные gates не закрыты. + Change не архивирован и implementation complete не заявлен. +- **Следующий Implementation outcome:** 3.4 — общие сохранённые catalog, + metadata, quotes, company/metrics с раздельной freshness и revision fence. + +### Checkpoint — shared UID/FIGI metadata, 2026-09-25 + +- Продолжена **3.4**; checkbox остаётся открытым, completed **9/27**, + remaining **18**. Existing InstrumentMetadataService теперь использует + сохранённые Instrument/InstrumentLookup и private SyncWork вместо RAM TTL. + Успешное отсутствие отделено от failed lookup; UID/FIGI fallback и aliases + сохраняются, FIGI не превращается в выдуманный provider UID. +- Успешная metadata переиспользуется между пользователями и после reopen. + Для одинакового UID/FIGI descriptor requests объединяются в producer, + наблюдатели получают собственные handles. Publication и ошибки fenced + по текущей connection revision/attempt; удалённый/заменённый инициатор не + изменяет даже общий timestamp. Ошибка его credentials не становится общим + отрицательным lookup и не передаётся другому пользователю. +- Worker запускается только запросом, читает credentials только для актуальной + attempt и подаёт последовательные вызовы через прежний TbankClient. + Observe и создание нового service не восстанавливают unfinished intent; + новый обычный запрос меняет attempt. Нет startup scan, timer или retry loop. + Внутренний force обходит TTL, присоединяется к активной работе и сохраняет + ещё свежий источник/время при ошибке. Public screen sync остаётся в 4.1. +- Portfolio normalization и ledger читают metadata из SQLite. Обычные запросы + планируют отсутствующую/устаревшую metadata показанных позиций/операций; + observe этого не делает. Изменение metadata журнала меняет stateVersion, + сохраняя pagination binding и действующий cursor. Batch 1000 identities + проверен реально: обнаруженный SQLite/Prisma parameter limit устранён + ограниченными IN queries в одной read transaction. +- Evidence: `npm run verify` — **35 suites / 216 tests passed**, lint, + app/test typecheck и build. Старые RAM-only metadata mocks заменены реальной + SQLite suite. `node test/sqlite.cjs tbank-metadata-persistence.e2e-spec.ts + tbank-local-reads.e2e-spec.ts tbank-ledger-local.e2e-spec.ts + portfolio-structure.e2e-spec.ts tbank-income-local.e2e-spec.ts + openapi-contract.e2e-spec.ts` — **15 + 7 + 6 + 4 + 7 + 1 passed**. + Проверены 20-way coalescing, два пользователя, UID/FIGI/negative/sparse + outcomes, transient fallback, 86399/86400 seconds, failed force, late + completion после replace/delete, superseded attempt, reopen/request-only + recovery, сохранность финансовых read paths и runtime OpenAPI parity. + Использованы только временные изолированные SQLite; `git diff --check` passed. +- Не закрыты catalog discovery/enrichment, stock/bond company/metrics/quotes + и оставшиеся shared projections 3.4; candles 3.5, forecasts 3.6 и API 4.1 + также впереди. Полный профиль 500 мс/RPC reduction не измерен; новые RPC + assertions относятся к указанным controlled-source tests. Frontend/docs + не изменены, consumer artifact не передавался, change не архивирован. +- **Следующий Implementation outcome:** продолжить 3.4 — persistent catalog + discovery с отдельными enrichment/quote sources и локальные stock/bond reads, + сохранив query identity, ticker ambiguity и MOEX/NCR semantics. + +### Checkpoint — persistent catalog discovery owner, 2026-09-25 + +- Продолжена **3.4**, checkbox открыт: completed **9/27**, remaining **18**. + Добавлен concrete CatalogDiscoveryRepository/Service в instruments module. + Успешный discovery сохраняет упорядоченные UID по нормализованному query/type + отдельно от минимальных public catalog identities в InstrumentSource. + Отсутствующий query возвращает null; подтверждённый пустой — []. Сохранённый + SBER не подтверждает GAZP или bond query. Enrichment не требуется для чтения + найденных identities и не считается подтверждённым этим discovery. +- Existing stock/bond selection вынесен в один используемый обоими owners + helper: trade availability, exact ticker, preferred class, UID ordering, + ISIN dedup и отдельные missing-ISIN UID сохранены. Discovery сохраняет + прежний предел 30 после dedup. Минимальный catalog payload явно выбирает + public поля и не перезаписывает уже имеющуюся полную Instrument metadata + или её timestamp; InstrumentLookup completeness не создаётся автоматически. +- Read/request/observe API нового owner использует local access, durable private + work handles, дневной TTL и existing TbankClient. Одинаковые query/type двух + пользователей объединяются; credentials берутся только у актуального + producer. Shared writes и final states защищены connection/attempt fence. + Recovery запускается только обычным запросом; observe и создание service + работу не возобновляют. Failed force сохраняет прежний набор и timestamp. +- Evidence: backend `npm run verify` — **35 suites / 216 tests passed**, + lint, оба typecheck и build. `node test/sqlite.cjs + tbank-catalog-discovery.e2e-spec.ts tbank-metadata-persistence.e2e-spec.ts` — + **10 + 15 passed** на disposable SQLite со штатными migrations. Проверены + normalized/different queries/types, successful empty/unknown, reopen, + 20-way coalescing, private handles, whitelist shared fields, независимость + полной metadata, late replace/delete, invalid-token isolation, superseded + attempt, request-only recovery, failed force и 86399/86400-second boundary. +- **Ограничение этапа:** новый discovery owner зарегистрирован в DI, но HTTP + search/detail ещё использует legacy orchestration. Его переключение вместе + с отдельными persistent enrichment/quote sources остаётся в 3.4; заявлять + локальный HTTP-поиск или отсутствие его broker wait пока нельзя. Company, + bond metrics/MOEX/NCR, candles, forecasts и public sync API также остаются. + Wire contract не менялся; frontend/docs и consumer snapshot не затронуты. + Профиль 500 мс/RPC navigation trace не измерялся; change не архивирован. +- Specs `npm run verify` и native strict validation `persist-tbank-data` + с явным `--store wealth-ledger-specs` успешны; backend/specs diff whitespace + checks успешны, включая новые source/test files. + + +### Checkpoint — local stock/bond search and persistent enrichment, 2026-09-25 + +- Продолжена **3.4**, completed **9/27**, remaining **18**. HTTP GET `/stocks` + и `/bonds` теперь читают сохранённые discovery/detail/last-price sources, + возвращают `{data, meta}` и поддерживают `sync=observe`. Неизвестный query + даёт null, подтверждённый пустой — []; найденные identities доступны при + незавершённых/отказавших enrichment sources. Метаданные описывают отдельные + зависимости, их availability/freshness/private work handles; resourceKey + стабилен между missing и последующим результатом. +- CatalogEnrichmentService сохраняет типизированные детали по UID и prices + раздельно. TTL деталей — сутки, prices — минута; повторный query использует + уже сохранённые sources независимо от пользователя. Cold discovery запускает + запрошенные enrichment dependencies самостоятельно, поэтому observe не обязан + инициировать следующий этап. UID работ объединяются между query/users; + до 30 цен одного producer идут одним existing getLastPrices RPC, подробности + запрашиваются последовательно через existing TbankClient. Новых dependencies, + migration, scheduler или внешней очереди нет. +- Публикация каждого source проверяет current connection/revision/attempt; + batch дополнительно проверяет initiating attempt. Удаление/замена подключения + или superseded initiating attempt не позволяют опубликовать остальные цены + batch. Failed force оставляет прежние данные и timestamps. Shared payload + явно выбирает public fields. Успешные полезные детали также обновляют canonical + Instrument и UID/FIGI aliases внутри той же transaction: generic metadata + owner переиспользует результат без повторного RPC. Successful typed miss + не становится отрицательным generic UID lookup. +- Сохранены decimal price mapping, bond pricePercent отдельно от Money, + nominal/ACI, UID matching пакетных цен, selection/dedup/limit. Legacy mock + tests синхронного search заменены реальными SQLite/HTTP сценариями; прежние + ticker/market/company финансовые tests продолжают проходить. +- Evidence на итоговом коде: backend `npm run verify` — **35 suites / 209 tests**, + lint, оба typecheck и build passed. `npm run openapi:generate` выполнен. + `node test/sqlite.cjs tbank-catalog-local.e2e-spec.ts + tbank-catalog-discovery.e2e-spec.ts tbank-metadata-persistence.e2e-spec.ts + tbank-ledger-local.e2e-spec.ts stocks.e2e-spec.ts bonds.e2e-spec.ts + openapi-contract.e2e-spec.ts` — **15 + 10 + 15 + 6 + 16 + 18 + 1 = 81 passed**. + HTTP sandbox EPERM повторён с разрешённым локальным loopback; итоговый прогон + успешен. Только disposable SQLite, рабочая БД не использовалась. +- Проверены blocked discovery/enrichment без ожидания HTTP, partial/empty/unknown, + shared reuse после reopen без decrypt/GetAccounts/RPC, одна price batch на + 30 бумаг, межпользовательские/private handles, failed force, независимые minute/ + day TTL, wrong detail UID, late replace/delete и superseded batch origin, + request-only recovery, финансовые строковые значения и configured access/DTO + validation. Эти RPC assertions не заменяют полный профиль измерений 6.2: + 500 мс и navigation RPC reduction пока не подтверждены. +- **Остаток 3.4:** локальные UID/ticker detail endpoints, company/fundamentals, + полноценные quote/metrics owners и MOEX/NCR semantics; legacy synchronous + read paths этих sections пока сохранены. Candles/forecasts/public sync API + остаются 3.5/3.6/4.1. Backend OpenAPI обновлён, явная передача consumer snapshot + остаётся 4.2; frontend/docs в этом этапе не менялись. Change не архивирован, + задача 3.4 и общий implementation complete не объявлены. + + +### Checkpoint — local instrument sections, 2026-09-25 + +- Завершена **3.4**, completed **10/27**, remaining **17**. UID/ticker, + company/fundamentals, quotes и bond metrics читают SQLite и поддерживают + observe; missing не превращается в пустой финансовый результат. Exact ticker + проверяется до listing selection/ISIN dedup; неоднозначность сохраняется, + optional alias не подменяет запрошенный UID другой площадкой. +- Details/company/ratings и minute quote sources сохраняются раздельно. + MOEX yield и NCR rating сохраняют source identity, точные значения и прежние + timestamps при failed force. Shared sources не содержат private projections; + два пользователя используют общий результат с private handles. Сохранённая + metadata обновляет названия/логотипы portfolio без изменения финансовых сумм + и времени portfolio snapshot. +- Исправлена гонка присоединения наблюдателя во время SQLite publication: + producer остаётся зарегистрированным до обработки всех присоединений. + Реальный DB test доказывает один RPC и два завершённых private handles. +- Evidence: `npm run verify` — **35 suites / 196 unit tests**, lint, app/test + typecheck, build passed; `npm run openapi:generate` passed. SQLite/HTTP suites + catalog-local/discovery/metadata/local-reads/portfolio/ledger/stocks/bonds/ + openapi-contract: **22+10+15+7+4+6+16+18+1 passed**; после финальных additions + local-reads **8 passed**, catalog-local **24 passed**. Runtime OpenAPI parity + passed; HTTP fixtures используют постоянный loopback listener. Только + disposable SQLite и synthetic credentials; `git diff --check` passed. +- Tasks **3.5, 3.6, 4.1, 4.2** запрошенного batch ещё не завершены. Свечи, + payouts/events пока сохраняют legacy orchestration. Передача OpenAPI consumer + snapshot ещё не выполнена; frontend/docs не изменены. Полный профиль latency/ + RPC 6.2 не измерен; implementation complete и archive не заявлены. +- **Следующий Implementation outcome:** 3.5 — durable candle coverage с + source-confirmed closed ranges, mutable tail и request-only recovery. + + +### Checkpoint — persistent candle reads and atomic reconciliation, 2026-09-25 + +- Продолжена **3.5**, checkbox открыт; completed **10/27**, remaining **17**. + Stock/bond candles HTTP теперь возвращает локальный `{data, meta}` с observe. + Durable CandleRepository использует существующие Candle/Coverage/SyncWork/ + SyncStage; новых schema, dependencies, scheduler или startup scan нет. +- Диапазоны до 365 суток собираются в staging и публикуются общей transaction. + Повторная сверка удаляет исчезнувшие свечи только своего диапазона, сохраняет + coverage вне него и при ошибке следующего окна оставляет предыдущие данные + и timestamps целиком. Late/superseded/deleted attempts не публикуют данные. +- Closed empty coverage переиспользуется после reopen; source isComplete и + точные decimal strings сохраняются, bond close остаётся процентом. Текущий + UTC-день, включая пустой, изменяемый с minute TTL; смена дня сама не закрывает + старое empty/incomplete окно. Полнота ответа отделена от TTL: истечение минуты + сохраняет полный результат с stale freshness. Unknown historyFrom для all + сохраняет STOCK/BOND_HISTORY_START_UNAVAILABLE. +- 20 одновременных запросов объединяются, typed metadata ожидается только внутри + worker через существующего source owner. Observe не возобновляет abandoned + intent; следующий обычный запрос меняет attempt и запускает работу. Worker + последовательно подаёт RPC через existing TbankClient. +- Evidence итогового кода: `npm run verify` — **36 suites / 197 unit tests**, + lint, оба typecheck и build passed; дополнительный `npm run typecheck:test` + после последнего SQLite scenario passed. OpenAPI generated; последовательный + runtime parity passed. `node test/sqlite.cjs + tbank-candles-persistence.e2e-spec.ts tbank-catalog-local.e2e-spec.ts + tbank-local-reads.e2e-spec.ts stocks.e2e-spec.ts bonds.e2e-spec.ts + openapi-contract.e2e-spec.ts` — **8+26+8+16+18+1 = 77 passed**. + Первый parity run пересёкся с build и увидел прежний dist; итоговый прогон + выполнен после завершения build/generation. Только disposable SQLite; + `git diff --check` passed. +- **Остаток 3.5:** portfolio-only prefetch через screen planner, продолжение + дальней истории после показанного периода и интеграция quote comparison + с общей candle coverage. Нужна итоговая проверка очередности/предзагрузки + и полного crash/restart поведения после этой интеграции. Поэтому 3.5 не + отмечена выполненной. Tasks **3.6, 4.1, 4.2** также не завершены; весь batch + из пяти задач не выполнен. Frontend/docs и consumer snapshot не изменялись. + Профиль p95 500 мс/RPC reduction не измерялся; archive не выполнялся. +- **Следующий Implementation outcome:** завершить 3.5 — подключить planner + предзагрузки только сохранённых portfolio instruments к тому же candle owner + без циклических DI dependencies и подтвердить remaining Verification. + + +### Checkpoint — portfolio candle prefetch and screen priority, 2026-09-25 + +- Завершена **3.5**, completed **11/27**, remaining **16**. Existing shared + instrument providers собраны в InstrumentSourcesModule без дублирования DI + instances и forwardRef; PortfolioModule использует публичный CandleService. +- Предзагрузка отбирает только сохранённые STOCK/BOND positions с UID, удаляет + дубликаты и применяет existing screen planner. На warm portfolio read её DB + планирование не блокирует HTTP; одинаковые активные prefetch объединяются. + Открытый не входящий в portfolio инструмент получает собственную историю. +- После показанного периода очередь продолжает all-history с меньшим + приоритетом. Каждый RPC-window уступает место экранным dependencies; + automatic windows публикуются постепенно, force остаётся атомарным для + всего показанного периода. Coverage повторно проверяется между окнами. + Quote comparison сохраняет и переиспользует тот же durable candle coverage. +- Evidence: backend `npm run verify` — **36 suites / 197 tests**, lint, оба + typecheck, build passed. OpenAPI generated. SQLite candles/catalog/local reads/ + portfolio/OpenAPI suites — **9+26+8+4+1 = 48 passed**, включая ordering между + окнами, portfolio-only scope, opening another instrument, request-only recovery, + midnight/empty coverage, failed force, exact bond percentage и comparison reuse. + Использована только disposable SQLite; latency/RPC profile 6.2 не измерялся. +- **Следующий Implementation outcome:** 3.6 — shared payout calendars и private + last-complete forecast fallback с исходными timestamps и membership/range fence. + + +### Checkpoint — durable payout calendars and forecasts, 2026-09-25 + +- Завершена **3.6**, completed **12/27**, remaining **15**. Instrument dividends, + coupons и account/all events читают saved calendars + persistent portfolio, + возвращают envelope и поддерживают observe. Stock/bond legacy RAM callbacks + и отдельные synchronous calendar/portfolio fetch удалены. Общие источники + сохраняют raw events без quantities/estimates; account bond events остаются + MOEX, instrument coupons — T-Bank; missing MOEX block не становится []. +- ForecastService/Repository принадлежат existing private portfolio owner и + используют TbankForecast. Publication проверяет current connection/membership, + portfolio versions и shared source versions/timestamps в SQLite transaction. + Старый полный snapshot сохраняется при missing новой зависимости, ошибке + календаря и reopen; другой applied range/membership не получает его данные. + Successful одинаковые dependencies не меняют calculatedAt/исходные timestamps. + Public payout rows при missing personal snapshot не получают неподтверждённые + estimates; private calculations не возвращаются мимо publication fence. +- Сохранены exact signed/fractional money, валюты, registry dates, cancelled + dividends, offer без суммы, redemption отдельно от income totals, future + unfixed payout entitlement и optional metadata fallback к portfolio ticker. +- Evidence: `npm run verify` — **36 suites / 188 unit tests**, lint, оба + typecheck, build passed; старые synchronous-cache orchestration tests заменены + SQLite tests, pure financial checks сохранены. OpenAPI generated. SQLite/HTTP + upcoming-events/catalog/candles/local reads/stocks/bonds/OpenAPI — + **15+26+9+8+16+18+1 = 93 passed**, включая реальную optimistic-publication race, + two-user reuse/private values, new holding failure + reopen, membership/range + isolation, cancelled/empty/unknown calendars, MOEX failure, late connection + deletion, default/inclusive dates и observer without RPC. Только disposable DB. +- Consumer snapshot ещё не передан; frontend/docs не изменены. Профиль 500 мс/ + RPC reduction остаётся 6.2, archive не выполнялся. +- **Следующий Implementation outcome:** 4.1 — validated screen-scoped force + dispatch и observational authorized status с DTO/OpenAPI и resource states. + +### Checkpoint — sync contract preparation, 2026-09-25 + +- Запрошенная пятёрка **3.5, 3.6, 4.1, 4.2, 4.3**: первые две завершены; + **4.1 остаётся открытой**, общий счёт **12/27**. Добавлены DTO и подготовка + durable screen intent, нормализация разрешённых screen fields и scope; + это пока не подключённые публичные POST/status endpoints. +- Dependency DTO теперь описывает фактически возвращаемые dataRevision и + stateVersion. Общий mapper учитывает running/queued/failed dependencies, + сохраняя availability и successful timestamps прежнего результата. + Candle meta явно возвращает requested/complete/missing coverage независимо + от freshness; stale current tail не превращается в missing interval. +- Проверки: backend `npm run verify` — **37 suites / 192 tests passed**, + lint, оба typecheck и build; `npm run openapi:generate` passed. + `node test/sqlite.cjs tbank-candles-persistence.e2e-spec.ts + tbank-catalog-local.e2e-spec.ts tbank-local-reads.e2e-spec.ts + upcoming-events.e2e-spec.ts openapi-contract.e2e-spec.ts` — + **9+26+8+15+1 = 59 passed**. Только disposable SQLite и synthetic credentials. + Исправлена устаревшая неполная metadata fixture candle-тестов; unit income + изолирует Prisma import после расширения operations public provider exports. +- Frontend snapshot не передан, frontend не изменён; existing docs edits + сохранены. Полный профиль 500 мс/RPC 6.2 не измерялся; archive не выполнялся. +- **Следующий Implementation outcome:** завершить 4.1: подключить scoped + dispatcher к existing owners и observational status с текущими resource + stateVersion, проверить configured HTTP authorization/validation/observe + и coalescing на SQLite. Затем последовательно выполнить 4.2 и 4.3. + + +### Checkpoint — public sync/status and explicit OpenAPI handoff, 2026-09-25 + +- Завершены **4.1–4.2**, итог **14/27**. POST tbank-sync строго валидирует + screen scope, нормализует поля, резервирует private handle и возвращает 202 + до broker completion. Concrete coordinator раскрывает dependency graph + через existing membership/portfolio/operations/catalog/calendar/candle owners. + Force не запускает full-history prefetch и сохраняет атомарность показанного + периода operations. Новая инфраструктура/dependencies не добавлены. +- Status авторизует current connection/account, использует только observe reads, + возвращает текущие resource stateVersion и terminal outcome. Поддержаны handles + обычных GET; abandoned work не запускается. Screen progress фиксирует child + attempts, superseded результат не приписывается прежнему запросу. +- Evidence: backend `npm run verify` — **37 suites / 192 tests**, lint, app/test + typecheck и build passed. SQLite/HTTP run screen-sync/all/local-reads/ledger/ + income/catalog/candles/events/stocks/bonds — **10+18+8+6+7+26+9+15+16+18**. + Финальный screen-sync — **11 passed**, включая empty coverage stateVersion + без изменения dataRevision; runtime OpenAPI parity — **1 passed**. + 20-way POST coalescing, 202 при заблокированном upstream, malformed/foreign + scope, private handles, failure без retry, old-process cancellation и + old-connection 404 проверены на disposable SQLite. All route tests обновлены + с legacy synchronous mock на настоящую локальную DB boundary. +- Backend `openapi.json` явно скопирован в frontend + `src/shared/api/spec/openapi.json`; frontend `npm ci`, локальные + `openapi:generate` и `openapi:check` успешны. Frontend CI не читает backend. + **4.3 остаётся открытой** до итоговых frontend lifecycle tests/gates. +- Профиль latency/RPC 6.2 не измерялся; archive не выполнялся. Следующий + Implementation outcome: завершить entity query envelope/observe owners и + доказать session/connection/access/latest-response fences во frontend. + + +### Checkpoint — frontend resource owners and response fences, 2026-09-25 + +- Завершена **4.3**, итог **15/27**, remaining **12**. Все investment entity + query owners используют backend envelope. Canonical generated query identity + хранит envelope, owners возвращают payload и meta существующим consumers. + Missing null не становится []/нулевым финансовым успехом. Types derived from + snapshot, включая nullable periods, проходят проверку всех consumers; account + VM использует accounts owner вместо прежнего raw-array cache cast. +- Observe использует тот же generated key с отдельным transport mode. + При несовпадении ожидаемого stateVersion stale response не публикуется; + повторяется только observational read. Transport объединяет identity ordinary/ + observe для latest-request fencing. In-flight records с ограниченным временем жизни очищаются после + завершения; новая инфраструктура или зависимости не добавлены. +- Успешная connection mutation отменяет publication прежних data/status/mutation + responses и сбрасывает финансовые GET caches, включая sync status. Failed + mutation сохраняет caches/boundary. Session generation и unmount защищают + ответы от прежней session. Confirmed private access loss очищает snapshots, + отменяет связанные запросы и повышает access epoch; поздний success не + восстанавливает скрытые данные. Обычный 503 сохраняет cached snapshot. +- Sync mutation/status hooks готовы в existing tbank-connection feature; + автоматический page observer и действия refresh остаются **5.1**, не включены + в эти hooks. Existing frontend fixtures обновлены до нового wire envelope; + generic transport tests проверяют envelope, presentation tests — payload. +- Evidence: frontend `npm ci`, `openapi:generate`, `openapi:check`; полный + `npm run verify` успешен: typecheck, Biome, **51 suites / 287 tests** с coverage, + Knip, dependency/FSD и Storybook dependency gates, production build и + Storybook build. После усиления status-publication assertions targeted + sync tests **2 passed**, финальный typecheck passed. Полный gate ранее также + прошёл на итоговом production code. MSW/RTL проверяют canonical observe cache, + rejection старой версии, late ordinary/observe ordering, missing/failed null, + access loss + late sibling response, connection/session status и mutation. + Subscription на query cache подтверждает отсутствие публикации old status. +- Backend финальный `npm run verify` — **192 tests**, lint/type/build; + итоговые screen-sync **11 passed** и runtime OpenAPI parity **1 passed**. + Nested descriptor errors содержат field pointers. Backend artifact и явно + переданный frontend snapshot побайтово совпадают (`cmp`). Все DB проверки + использовали disposable SQLite. Diff whitespace checks успешны. +- Docs и root не менялись этим batch; прежние docs edits сохранены. Frontend + production/Storybook builds сообщают предупреждения о крупных chunks; + gates завершаются успешно. Browser proof/новые UI states остаются **6.3**, + latency/RPC profile **6.2** не измерялся. Archive не выполнялся. +- **Следующий Implementation outcome:** 5.1 — page-owned descriptor и passive + observer известной работы по inline UI/UX revision 1, с отдельными transport + и source lifecycle, остановкой observation после ухода или terminal outcome. + + +### Checkpoint — scoped screen refresh and populated rollback proof, 2026-09-25 + +- Завершены **5.1–5.4 и 6.1**, итог **20/27**, remaining **7**. Descriptors + принадлежат page VMs; existing tbank-connection feature наблюдает только + полученные handles. Status polling и canonical observe reads не инициируют + новые imports; terminal/leave/logout останавливают polling. Dependency-only + completion также перечитывает projection. Повторный POST текущего scope + защищён; поздний acceptance другого scope не заменяет текущий handle. +- Entity owner отдельно отдаёт transport status/error, сохраняя source metadata. + Known access failures очищают private snapshots с существующим epoch fence; + NOT_FOUND самого work handle не трактуется как потеря доступа ко всем счетам. + Transient transport failure и failed source work показываются раздельно; + прежние суммы, source timestamps и fresh до TTL сохраняются. +- Accounts различает unknown и complete-empty; карточки показывают timestamps + и freshness отдельных portfolio summaries. Account/all refresh охватывает + membership, portfolio, operations, income и events. Частичный portfolio + не показывает окончательные totals/structure; подтверждённые позиции и + operations/events остаются видимыми с сообщением о неполноте. Overview + sections сохраняют независимые partial/error/stale состояния. +- Operations/income/events используют applied scope и source completeness. + Partial empty не получает финансовый empty message; selected income не + ждёт year comparison и не получает вымышленную нулевую базу. Cursor recovery + возвращает первую страницу с прежними dates/categories. Смена периода не + переименовывает прежний snapshot; сохранённый forecast виден с исходным + временем при failed source refresh. +- Stock/bond search и detail используют те же source states. Ручной refresh + передаёт текущие UID, candle period и personal-history categories/limit, + не сбрасывает график и не запрашивает весь каталог. Общий section adapter + учитывает missing/partial/stale/failed и source timestamps; незавершённые + свечи и неподтверждённые personal estimates не превращаются в нули. +- Frontend evidence: итоговый `npm run verify` — **53 suites / 306 tests**, + OpenAPI check, typecheck, Biome, coverage, Knip, FSD/dependency и Storybook + dependency checks, production и Storybook builds passed. MSW/RTL проверяют + duplicate POST, terminal/leave/logout, переход времени за TTL без ordinary + GET, empty completion при ещё running history, dependency completion, + access loss, fresh snapshot при failed force, scope stock/bond, independent + overview/summary freshness, полный overview observe и invalid cursor recovery. + Ранее созданные race/negative-correction/period-boundary проверки также + входят в этот итоговый gate. Browser/reduced-motion/viewport proof остаётся + **6.3**, успешная Storybook build его не заменяет. +- Backend evidence: `npm run verify` — **37 suites / 192 tests**, lint, оба + typecheck и build passed. Исправлены unsafe Jest asymmetric matcher values + в screen-sync test, runtime assertions field pointers сохранены. Через + `node test/sqlite.cjs` прошли operations-sync/migration/connection/catalog/ + candles/events — **17+2+4+26+9+15**, затем operations-persistence/ledger-local/ + screen-sync — **5+6+11**; всего **95 SQLite/HTTP checks**. Включены SIGKILL + между страницами и до/после publish, отдельный concurrent SQLite reader, + reopen/request-only recovery, membership replace/delete и shared fences. +- Existing `test/bank-cards-durability.cjs` расширен populated T-Bank datasets, + coverage, forecasts, shared sources, staging/progress и migration state. + `npm run test:durability -- /tmp/wealthledger-tbank-rollback.PuzfjF` passed: + equality после restart, rollback/return и restore в другую SQLite; integrity + и FK checks; User/Session/connection/manual cards, refunds/tombstones и новые + datasets сохранены. Evidence result: + `/var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-7MLwhL/result.json`. +- Rollback pair проверена явно: JSON OpenAPI, сгенерированный прежним backend + **0862d56c4e4d9dd554556c3992cc073193020ff1**, равен consumer snapshot frontend + **a209d715892862bb6d48084df0c84a10b8d56bf1** (`db184c8^`). Более новый + frontend HEAD содержит другой validation/conflict contract и не объявлен + подходящей парой этого rollback build. Deploy/rollback реального приложения + не выполнялись. Текущие backend OpenAPI и явно переданный frontend snapshot + побайтово совпадают (`cmp`); новых public DTO изменений в этом batch нет. +- Только disposable SQLite/synthetic credentials; рабочая БД не использовалась. + Root/docs не менялись, прежние docs edits сохранены. `git diff --check` passed. + Builds сохраняют предупреждения о крупных chunks. Профиль **500 мс/RPC + reduction 6.2 ещё не измерен**; implementation complete не объявлена. + Remaining: **6.2, 6.3, 7.1–7.5**. Archive/main-spec sync не выполнялись. +- **Следующий Implementation outcome:** 6.2 — измерить HTTP/RPC по согласованному + профилю и baseline trace; затем 6.3 — browser verification inline UI revision 1. + + +### Checkpoint — measured reads, browser proof and application gates, 2026-09-25 + +- Завершены **6.2, 6.3, 7.1, 7.2, 7.3**, итог **25/27**, remaining **2**. + Backend сохраняет existing owners: batch reads coverage/source work заменяют + загрузку свечей и N+1 queries при prefetch/projection; Intl formatter reused. + Проверка source versions прогноза разбита на batches внутри одной transaction, + чтобы 200 инструментов не превышали SQLite parameter limit. Financial + publication fences и fallback сохранены; новой инфраструктуры нет. +- Performance runner: `node test/sqlite.cjs tbank-read-baseline.e2e-spec.ts`. + Evidence: backend `test/fixtures/tbank-read-persistent.json`. Изолированный + Docker, network none, без volumes; cgroup 4 CPU / 8 GiB (VM доступно 7.65 GiB), + Apple M1 Pro SSD host, Node 24.21.0. SQLite 619343872 bytes: 300000 операций, + 1000000 свечей, 3 счёта / 200 инструментов. Dependency entries image lock + совпадают с текущим lock; отличается только version корневого package. +- **2300 warmed reads: 23 группы × 100, concurrency 10**. Server ingress → + response finish p95 **7.3–382.6 ms**, все группы ≤500 ms. p50/p99/max/bytes + сохранены. Первый DB-only read после Nest/provider restart: **71.2 ms client** + (HTTP logger 16 ms). Это не OS-process restart; отдельный process crash/reopen + проверен durability suite. Fresh mutable timestamps обновлялись перед каждой + группой, чтобы измерять именно fresh path; полный прошлый coverage сохранён. +- Baseline navigation trace: **42913 → 0** synthetic TbankClient calls (100% + reduction), включая первые reads; baseline repeated-only часть — 41821. + Реальный внешний broker network не измерялся; baseline host latency не + сравнивается с container latency. 20 stale requests: один portfolio и один + positions pass, retries 0; все ответы за 1717 ms при upstream delay 2000 ms. + Cold import ответ 18.3 ms; завершённая работа без новых запросов RPC не делает. + **Client p95 достигает 3556.4 ms**: server budget не означает end-to-end + browser latency ≤500 ms; client timings сохранены отдельно. +- Frontend: existing stories independent summary freshness/source failure; + фоновые warnings используют polite status announcements. Chrome/Playwright + **56 app checks**, **9 Storybook viewport checks**: 320/375/1440, длинные суммы, + account/all, ledger, income, events, stock/bond, complete/partial/stale/failed/ + missing, refresh, leave/reopen, connection replacement, keyboard/focus, + skeleton/busy и reduced motion; JS errors 0. Synthetic API fixtures, без + реальных credentials. Проверка resize ждёт завершения responsive chart layout; + первоначальный transient overflow воспроизводился до перерасчёта, после него + отсутствует. CSS workaround не добавлялся. Evidence текущего запуска: + `/tmp/wealthledger-browser-evidence.json`, `/tmp/wealthledger-ui-final3.log`, + `/tmp/wealthledger-stories-check.log`, screenshots `/tmp/wl-*.png`. +- Backend: `npm ci`, `npm run verify` (**37 suites / 192 tests**), + `npm run test:openapi-contract`, `npm run openapi:generate` passed. + SQLite/HTTP candles/catalog/discovery/events/income/local reads/screen-sync: + **86 checks passed**; durability/rollback повторно passed на disposable DB, + result `wealthledger-bank-durability-kWQOEX`. Рабочая БД не использовалась. + Финальный backend OpenAPI побайтово равен явно переданному frontend snapshot. +- Frontend: `npm ci`, `npm run verify` (**53 suites / 306 tests**), включая + local OpenAPI drift, type/lint/coverage/Knip/dependencies, production и + Storybook builds passed. Крупные chunks остаются build warnings. +- Docs обновляет storage/access/freshness, flows accounts/operations/portfolio/ + income/instruments, HTTP handoff и glossary; ADR 017 сохраняет историческое + решение и отмечает проверенную реализацию в рабочих ветках, не production + deployment. README routes к темам/терминам/API сохранены. Полный отдельный + semantic/diagram gate **7.4 остаётся открытым**. +- Specs `npm ci` успешно повторён без изменения dependencies после прежнего + registry 502. Diff/owner/callers review выполнен; чужие active changes и + staged/unstaged изменения сохранены. Итоговая requirement-by-requirement + сверка **7.5 остаётся открытой**. Реализация целиком завершённой не объявляется; + archive/main-spec sync и deployment не выполнялись. +- Финальные `npm run verify` docs (**46 Markdown files, 0 issues**) и specs + passed; strict native validation change с explicit store passed. Native apply + подтверждает **25 completed / 2 remaining**. Эти проверки не заменяют 7.4/7.5. +- **Следующий Implementation outcome:** 7.4 — финальная semantic/link/diagram + проверка docs, затем 7.5 — все 35 delta requirements и native final gates. + + +### Checkpoint — final semantic and requirement verification, 2026-09-26 + +- Завершены **7.4–7.5**, итог **27/27 implementation-owned tasks**. + Сверены все **35 delta requirement headings**, включая REMOVED; таблица ниже + связывает каждый heading с задачами и наблюдаемыми проверками. +- Semantic review обнаружил и устранил пропуски frontend: список счетов и его + skeleton теперь одноколоночные согласно delta requirement; аналитика показывает + previous comparison через существующий компонент отдельно от yearAgo. Для + каждого сравнения различаются incomplete, source failure и недоступность + годового сравнения по календарю/истории; null не становится нулевой базой. + Financial Money приходит с сервера, новые calculations/API поля не добавлялись. +- Frontend `npm ci`, targeted income (**41 tests**) и `npm run verify` + (**53 suites / 307 tests**) passed: local OpenAPI drift, type/lint/coverage, + Knip/dependency checks, production и Storybook builds. Финальный browser run: + **56 app cases**, **9 Storybook viewport checks**, затем **14 focused income + checks**, включая видимость обоих сравнений на 320/375/1440 px; JS errors 0. + Storybook проверяет расположение второй account card ниже первой на каждой + ширине. Evidence: `/tmp/wealthledger-frontend-verify27.log`, + `/tmp/wealthledger-ui27.log`, `/tmp/wealthledger-stories27.log`, + `/tmp/wealthledger-income-browser27.log`, screenshots `/tmp/wl-previous-*.png`. +- Docs: пройдены маршруты README → architecture/flows → glossary/HTTP API. + Убраны оставшиеся утверждения о временном portfolio, потере snapshots после + restart, отсутствии force action и совместимости старого клиента с envelope. + Уточнены local filtering, private portfolio/shared calendars, предыдущий + период дохода и уже согласованный applied period событий. Исторические ADR + сохранены; отдельные прежние ограничения CURRENCY-card и налогового смысла + dividend_net не скрыты и не объявлены исправленными этим change. +- Уточнение evidence MOEX: успешный отсутствующий block даёт [] по существующему + MoexClient и его unit test `treats a successful response without the requested + block as no events`. Название старого e2e case про missing block моделирует + rejected adapter promise, то есть source failure; оно не доказывает отказ + реального adapter при отсутствующем block. Поведение adapter не менялось и + соответствует delta scenario; повреждённый block/transport error — иной случай. +- Docs `npm run verify`: **46 Markdown files, 0 issues**, relative links/anchors + и diff passed. Изменённый label в существующей Mermaid sequence проверен по + PUT transaction flow и синтаксису; доступного локального Mermaid renderer нет, + **отрисовка диаграммы не проверена**. Это явно допустимое ограничение 7.4; + Markdown gate не выдаётся за render proof. Нового renderer/dependency нет. +- Backend production/tests не менялись в этом checkpoint; результаты 7.2, + SQLite/crash/durability/rollback и versioned performance fixture актуальны. + Backend OpenAPI и frontend snapshot по-прежнему побайтово равны. + Серверный максимум p95 **382.6 ms** в профиле Design, client p95 до **3556.4 ms**; + synthetic RPC boundary **42913 → 0**. Реальный broker network/production + rollout этим не измерены. Рабочая пользовательская БД не использовалась. +- Native gates: `openspec schema validate wealth-ledger-sdd --verbose`, + `openspec validate persist-tbank-data --type change --strict --store wealth-ledger-specs`, + `openspec validate --specs --strict --store wealth-ledger-specs` passed + (**25 main specs / 0 failed**); `npm run verify` specs и final diff checks passed. + Long requirement INFO не является validation failure. Проверены staged, + unstaged и новые файлы; root/dependencies/main specs/чужие changes сохранены. +- Implementation-owned work завершена. Independent review перед archive, + archive/main-spec sync, commits/push и deployment этим checkpoint не выполняются. + +#### Requirement evidence index + +Здесь B — backend, F — frontend; пути служат адресами evidence, не входами CI +соседнего repo. Результаты запусков приведены в checkpoint соответствующих задач. + +- **B-access:** `test/tbank-connection.e2e-spec.ts`, `test/tbank-local-reads.e2e-spec.ts`. +- **B-ops:** `test/tbank-operations-persistence.e2e-spec.ts`, + `test/tbank-operations-sync.e2e-spec.ts` (включая SIGKILL и concurrent reader). +- **B-ledger:** `test/tbank-ledger-local.e2e-spec.ts`. +- **B-income:** `test/tbank-income-local.e2e-spec.ts`, income period/analytics unit tests. +- **B-catalog:** `test/tbank-catalog-local.e2e-spec.ts`, `test/tbank-catalog-discovery.e2e-spec.ts`. +- **B-candles:** `test/tbank-candles-persistence.e2e-spec.ts`, candle coverage unit tests. +- **B-events:** `test/upcoming-events.e2e-spec.ts`, `src/integrations/moex/moex.client.spec.ts`. +- **B-sync:** `test/tbank-screen-sync.e2e-spec.ts`. +- **B-perf:** `test/tbank-read-baseline.e2e-spec.ts`, `test/fixtures/tbank-read-persistent.json`. +- **F-accounts:** `AccountsPage.test.tsx`, `BrokerageAccountList.test.tsx` и viewport stories. +- **F-overview:** `AccountPage.test.tsx`, `AccountPage.vm.test.tsx`, `AllAccounts.test.tsx`. +- **F-ledger:** `AccountOperationsPage.test.tsx`, filters tests и instrument page tests. +- **F-income:** `AccountIncomePage.test.tsx`, filters tests; browser comparisons. +- **F-events:** `AccountEventsPage.test.tsx`, filters tests, `BrokerageEventsPreview` consumers. +- **F-boundary:** `src/shared/api/client.test.ts`, authenticated query tests, + `src/features/tbank-connection/model/screen-sync.test.tsx`, `sync.test.tsx`. +- **UI:** browser/reduced-motion/keyboard/state checks 6.3 и final checkpoint выше. + +| Capability / requirement | Tasks | Evidence | +| --- | --- | --- | +| `brokerage-account-income-analytics` — Realized income analytics is account-scoped and period-bound | 3.3, 5.3 | B-income, F-income | +| `brokerage-account-income-analytics` — Period comparisons preserve calendar and currency meaning | 3.3, 5.3 | B-income, F-income; independent previous/year states | +| `brokerage-account-income-analytics` — Realized analytics never presents an incomplete result as complete | 2.1, 3.3, 5.3 | B-ops, B-income, F-income | +| `brokerage-account-income-analytics` — Account overview has an independent current-month income summary | 3.3, 5.2 | B-income, F-overview | +| `brokerage-account-income-analytics` — Income payments remain traceable to exact Ledger Entries | 2.1, 3.3, 5.3 | B-ops, B-income, F-income; exact signed entries | +| `brokerage-account-ledger` — User can retrieve an account ledger | 3.2, 5.3 | B-access, B-ledger, F-ledger | +| `brokerage-account-ledger` — User can browse a filtered ledger | 3.2, 5.3 | B-ledger; revision-bound cursor and recovery | +| `brokerage-account-ledger` — Ledger entries preserve broker operation meaning | 2.1, 3.2 | B-ops, B-ledger; exact money and identity | +| `brokerage-account-ledger` — Account overview includes recent ledger events | 3.2, 5.2 | B-ledger, F-overview | +| `brokerage-account-ledger` — Кэш страниц журнала ограничивает локальные ресурсы | 2.1, 3.2 | REMOVED: B-ledger reopen/no RPC; persistent row cursor replaces RAM page TTL | +| `brokerage-account-upcoming-events` — User can retrieve upcoming events for an account | 3.6, 5.3 | B-events, F-events | +| `brokerage-account-upcoming-events` — Forecasts preserve event meaning and uncertainty | 3.6 | B-events; decimal, cancelled, registry, null offer | +| `brokerage-account-upcoming-events` — Upcoming events use complete source results | 3.6 | B-events; fallback/reopen/new dependency failure, MOEX adapter semantics | +| `brokerage-account-upcoming-events` — Account page presents an upcoming events list | 5.3, 6.3 | F-events, UI | +| `brokerage-account-upcoming-events` — Account overview includes upcoming events | 5.2 | F-overview, B-events | +| `brokerage-accounts` — Список активных брокерских счетов | 1.3, 3.1 | B-access; order/empty/independent summaries | +| `brokerage-accounts` — Состояния недоступности списка счетов | 1.3, 3.1 | B-access; known invalid/failed replacement/observe | +| `brokerage-accounts` — Экран списка брокерских счетов | 5.2, 6.3 | F-accounts, UI; final single-column geometry | +| `brokerage-accounts` — Клиентские состояния ошибки списка счетов | 4.3, 5.2 | F-accounts, F-boundary | +| `frontend-data-resilience` — Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio | 4.3, 5.1–5.4, 6.3 | F-boundary, F-accounts, F-overview, F-income, F-events, UI | +| `portfolio-view` — Получение Portfolio выбранного Brokerage Account | 3.1 | B-access; blocked upstream/reopen/local membership | +| `portfolio-view` — Свежесть и fallback полного Portfolio Snapshot | 3.1 | B-access; stale >5min/metadata overlay/late revision fence | +| `portfolio-view` — Состояния и ошибки Portfolio на клиенте | 5.1–5.2, 6.3 | F-overview, F-boundary, UI | +| `tbank-data-sync` — Все экраны читают сохранённые данные без ожидания T-Bank | 1.2, 3.1–3.6, 6.2 | B-access, B-ledger, B-income, B-catalog, B-candles, B-events, B-perf | +| `tbank-data-sync` — Обновление запускается только запросом с учётом свежести | 2.2–2.3, 3.1–3.6 | B-ops, B-access, B-catalog, B-candles; TTL/observe/no startup work | +| `tbank-data-sync` — Ручное обновление ограничено текущим экраном | 4.1, 5.1–5.4 | B-sync, F-boundary, F-ledger, F-income, UI | +| `tbank-data-sync` — Первая загрузка приоритизирует экран и продолжает историю | 2.3, 3.5 | B-ops priority/replay/unknown floor; B-candles prefetch | +| `tbank-data-sync` — Полнота подтверждается успешным покрытием интервалов | 2.1–2.3, 3.2–3.3 | B-ops, B-ledger, B-income; pages ≠ completeness | +| `tbank-data-sync` — Сверка операций сохраняет идентичность и прежний результат | 2.1, 6.1 | B-ops; atomic quarter/all publish and separate reader | +| `tbank-data-sync` — Прогресс переживает перезапуск и одновременные запросы | 2.2, 6.1 | B-ops, B-candles, B-catalog; SIGKILL/reopen/coalescing/fences | +| `tbank-data-sync` — Общие данные инструментов отделены от личных | 1.2, 3.4–3.6 | B-catalog, B-events; two users/shared sources/private forecasts | +| `tbank-data-sync` — Локальная проверка доступа сохраняет границу подключения | 1.3, 4.3 | B-access, B-sync, F-boundary | +| `tbank-data-sync` — Завершённость свечей подтверждается источником | 3.5 | B-candles; incomplete prior day/current empty/closed gaps/failed force | +| `tbank-data-sync` — Интерфейс различает свежесть полноту и состояние работы | 4.1–4.3, 5.1–5.4, 6.3 | B-sync, F-boundary, F-income, UI | +| `tbank-data-sync` — Быстродействие и сокращение запросов проверяются измерениями | 6.2 | B-perf; 23×100, 10 clients, server p95, 20-way stale coalescing | + + +### Checkpoint — feature commit verification, 2026-09-26 + +- Implementation-owned status remains **27/27**; archive/main-spec synchronization + and release preparation remain deferred by the Change Owner. +- Fixed journal regressions after implementation: a current-day or paginated + moving period preserves confirmed completeness; the moving requested end no + longer changes `stateVersion` between unchanged status/observe reads. Actual + coverage changes still change the version. Disposable SQLite regressions prove + these outcomes without additional operations RPC. +- Fresh `npm ci` completed in all four affected repositories. Docs required a + retry with network access after the configured Nexus host returned ENOTFOUND; + dependencies and lockfiles were not changed. +- Backend `npm run verify` passed: **37 suites / 193 unit tests**, lint, both + typechecks and build. Frontend `npm run verify` passed: **53 suites / 307 tests**, + OpenAPI/type/lint/dependency gates, production and Storybook builds. Generated + backend OpenAPI remains byte-identical to the explicitly supplied frontend + snapshot. Docs verify: **46 files / 0 issues**. Native schema/change/main-spec + validation passed; apply reports **27 complete / 0 remaining**. +- Full backend `npm run test:e2e` passed in disposable Linux Docker: + **21 suites / 226 tests**, including runtime OpenAPI, migrations, financial + integrity, crash/restart, concurrency and the persisted read budget. Current + tracked source was copied into the existing development image; dependency + lock contents match exactly after excluding only root package version. + Node **24.21.0**, **4 CPU / 8 GiB**, no network, no host data volumes. +- This repeat of the Design profile measured **2300 warm reads**, maximum server + **p95 215.7 ms**, synthetic RPC **42913 → 0**. The original versioned measurement + remains in `test/fixtures/tbank-read-persistent.json`; the repeat is recorded in + `/tmp/wealthledger-feature-linux-e2e-final.log` and + `/tmp/wealthledger-feature-budget.json`. This is not a real-broker or production + latency measurement and does not establish a 500 ms browser response budget. +- Host macOS full HTTP verification is **not green**: two attempts aborted in + the native `better-sqlite3` statement cleanup with Node + `RemoveEnvironmentCleanupHook`. The same authentication suite and the complete + HTTP gate passed in Linux; no dependency workaround or application change was + introduced for the host failure. Earlier rollback/durability and browser + evidence remains applicable; Mermaid rendering remains unverified as recorded + in the preceding checkpoint. +- Staged secret scans and whitespace checks passed. Three known public + `income/yearAgo` resource identifiers have narrowly scoped Gitleaks comments; + a false positive in the Design delivery-map terminology was reworded without + changing its meaning. No credentials, working database, local backups or + unrelated active changes enter the feature commits.