diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/.openspec.yaml b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/.openspec.yaml new file mode 100644 index 0000000..44f55ff --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-23 diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/design.md b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/design.md new file mode 100644 index 0000000..7f52737 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/design.md @@ -0,0 +1,120 @@ +## Контекст + +Мотивация изложена в `proposal.md`, а поведенческий contract — в delta specs. +Backend документирует `200` через OpenAPI decorators, но Nest использует +стандартный `201` для трёх auth `POST` handlers, поскольку их runtime status +не задан явно. Runtime OpenAPI verifier сравнивает два документа, поэтому не +может обнаружить это расхождение HTTP responses. + +Во frontend Accounts и Account view models выбирают блокирующий loading/error +state до проверки cached query data. У auth provider два пути refresh: +cold start напрямую вызывает API без `catch`, а общий refresh может отклонить +Promise при повторе защищённого запроса. + +## Цели и исключения + +**Цели:** + +- Согласовать фактические auth success responses, OpenAPI, snapshot и + generated types на `200` с детерминированным regression gate без Prisma или + базы данных. +- Отображать существующие данные Accounts и Portfolio во время refetch и после + его ошибки с доступным локализованным warning и retry action. +- Дать cold start и повтору защищённого запроса единый non-throwing результат + refresh, сохранив текущие serialization и защиту routes. +- Фиксировать active remediation и обновлять feature/development документацию + только фактически поставленным поведением. + +**Исключения:** + +- Исправлять Prisma migration/e2e infrastructure или создавать workspace-wide + verifier, отложенный как WL-P2-05 и WL-P2-13. +- Менять refresh-cookie attributes, token lifetime, session rotation, + unauthorised-error codes, auth routes, cache TTL, refetch triggers или + backend Portfolio fallback policy. +- Распространять Client Cached Snapshot на постраничные operations/events, + чьи cursor и filters могут определять иной result set. + +## Решения + +### Явно установить `200` для трёх auth handlers успешного ответа + +Handlers register, login и refresh получают явный HTTP `200`, сохраняя +существующие `ApiOkResponse` и payload/cookie code. Frontend artifacts остаются +сгенерированными из backend document: запускается штатный sync workflow, а +artifact коммитится только если этот workflow создаёт изменение; ручное +редактирование generated schema запрещено. + +Небольшой независимый от БД backend test проверяет status metadata каждого +handler. Он защищает decorator, управляющий реальным Nest output, пока +Prisma-backed e2e gate остаётся deferred. Существующие e2e assertions меняются +с `201` на `200`, но не являются terminal evidence до восстановления Prisma +bootstrap. + +Отклонённые альтернативы: неявный `201` потребовал бы большего перехода +snapshot/types и противоречил бы согласованному единообразному contract; +опора только на runtime OpenAPI comparison повторила бы исходную blind spot. + +### Определять представление refetch по наличию данных, а не `isFetching` или `isError` + +Accounts и Account view models сначала определяют наличие Client Cached +Snapshot. Без данных они сохраняют текущие loading, error и empty views. При +наличии данных они возвращают loaded/empty presentation с опциональной +информацией об обновлении и его ошибке. Page components сначала отображают +данные, добавляют неблокирующий progress indicator во время fetch и передают +локализованное warning/retry action в loaded presentation после ошибки. + +Account view сохраняет backend warning `Portfolio.isStale` отдельно от client +warning об ошибке refresh. Оба могут отображаться, когда независимо истинны. +Новая терминология документируется в `CONTEXT.md`; это не новое API field и +не интерпретация server freshness. + +Отклонённые альтернативы: полный error/loading view после refetch скрывает уже +полезные финансовые данные; применение правила к operations/events могло бы +показать прошлую cursor/filter page как запрошенный результат. + +### Сделать общий refresh возвращающим `false` при любом неуспешном исходе + +Auth provider централизует успешное применение session и локальную очистку +session. Single-flight refresh function перехватывает transport rejection API, +а также no-data/HTTP failures, очищает token, profile и React Query cache и +возвращает `false`; она всегда освобождает in-flight Promise. Cold start +использует эту общую function и устанавливает `ready` в `finally`. +`authenticatedFetch` сохраняет existing one-retry behavior: при `false` он +возвращает исходный `401` после очистки, позволяя protected-route state +перенаправить на login. + +Отклонённые альтернативы: перехват только mount effect оставляет 401 retry +path способным отклонить Promise; добавление третьего startup error state +меняет согласованный anonymous/login recovery flow. + +## Риски и компромиссы + +- [Metadata test не является HTTP e2e test] → WL-P2-05 остаётся deferred, + существующие e2e expectations обновляются, а реальное HTTP coverage станет + обязательным после выполнения Prisma trigger. +- [Ошибка refresh утрачивает in-memory session] → после cold start браузер не + имеет действительного access token; очистка state предотвращает неоднозначный + protected UI и следует существующему 401 recovery behavior. +- [Client warning и backend stale warning могут сосуществовать] → каждому + назначается отдельное localized message и проверяется их независимый render. +- [Generated sync может затронуть frontend artifacts] → он запускается явно + только после backend contract change, а итоговый diff проверяется до принятия. + +## План изменений + +1. Сначала добавить regression tests, затем сделать backend status явным и + обновить существующие e2e assertions, не заявляя, что deferred e2e gate + прошёл. +2. Обновить OpenAPI artifacts через штатную sync command, проверить + согласованность runtime document и generated client и оставить только + намеренный итоговый diff. +3. Добавить observable frontend tests для состояний refetch cached data и + ошибок refresh, затем отрефакторить view models, pages, localized copy и + auth provider до их прохождения. +4. Обновить feature docs и remediation registry, выполнить применимые + non-mutating gates и OpenSpec verification до archive. + +Rollback затрагивает только код: отмена затронутых commits восстанавливает +предыдущий status/UX; data migration, dependency change и очистка persisted +state не нужны. diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/proposal.md b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/proposal.md new file mode 100644 index 0000000..b20ca9f --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/proposal.md @@ -0,0 +1,51 @@ +## Обоснование + +Auth endpoints фактически отвечают `201`, хотя их runtime OpenAPI-документ, +зафиксированный frontend snapshot и сгенерированный client обещают `200`. +Одновременно frontend скрывает успешные данные во время фонового обновления и +допускает необработанное отклонение Promise при транспортной ошибке начального +или повторного refresh. + +Это следующий рекомендуемый remediation change для WL-P2-04, WL-P2-07 и +WL-P2-08: предыдущие P1 и связанные query-identity инварианты уже закрыты, а +Prisma e2e остаётся честно deferred отдельным finding. + +## Изменения + +- Зафиксировать единый HTTP `200` contract успешного ответа для register, + login и refresh; согласовать Nest runtime, OpenAPI snapshot, + сгенерированные frontend types и регрессионные проверки, не меняя logout + `204`, error semantics или cookie/session policy. +- Ввести UX Client Cached Snapshot для списка Brokerage Accounts и Portfolio: + уже полученные данные остаются видимыми при фоновом обновлении и его ошибке, + а ошибка отображается неблокирующе с повторной попыткой. +- Нормализовать транспортную ошибку общего frontend refresh flow в + контролируемое неавторизованное состояние без необработанного отклонения + Promise; очистка in-memory session и query cache сохраняет существующий + protected-route flow. +- Обновить устойчивую документацию и audit remediation registry по мере + реализации; не представлять metadata regression test как восстановленный + Prisma HTTP e2e gate. + +## Возможности + +### Новые возможности + +- `frontend-data-resilience`: поведение Client Cached Snapshot и + неблокирующих refresh states для Accounts и Portfolio. + +### Изменённые возможности + +- `user-authentication`: единый auth success status и контролируемое + frontend-поведение при транспортной ошибке refresh. + +## Затрагиваемые области + +- Backend: auth controller и независимый от БД status regression test; текущий + Prisma e2e gate не меняется. +- Frontend: auth model, Accounts/Account view models и их observable UI tests; + сгенерированные OpenAPI artifacts обновляются только штатным sync workflow при + необходимости. +- Specs/docs: delta specs, `CONTEXT.md`, feature/development documentation и + remediation registry. Новых runtime dependencies, T-Bank credentials, + persisted migrations и публичных routes не требуется. diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/frontend-data-resilience/spec.md b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/frontend-data-resilience/spec.md new file mode 100644 index 0000000..6b6c2a4 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/frontend-data-resilience/spec.md @@ -0,0 +1,34 @@ +## Purpose + +Определяет, когда frontend сохраняет последний успешный ответ на экране и как +сообщает пользователю о неудачном фоновом обновлении без искажения доменных +состояний Portfolio. + +## ADDED Requirements + +### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio +Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или +Portfolio на экране, если для этого же набора данных уже был успешно получен +ответ. Начальная загрузка без такого ответа может показывать блокирующее +состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время +фонового обновления и после его ошибки; он не является server-side fallback и MUST NOT +изменять или интерпретировать поле `Portfolio.isStale`. + +#### 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 + предупреждение независимо, не представляя один вид состояния другим diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/user-authentication/spec.md b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/user-authentication/spec.md new file mode 100644 index 0000000..a47ffb3 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/specs/user-authentication/spec.md @@ -0,0 +1,38 @@ +## ADDED Requirements + +### Requirement: Единый HTTP contract успешных auth endpoints +Успешные `POST /api/v1/auth/register`, `POST /api/v1/auth/login` и +`POST /api/v1/auth/refresh` MUST отвечать HTTP `200`; опубликованный +OpenAPI-документ, зафиксированный frontend snapshot и сгенерированные +transport types MUST описывать этот же status. Каждая операция сохраняет свои +существующие payload, refresh-cookie и session semantics. Logout MUST +продолжать отвечать `204`. + +#### Scenario: Успешная регистрация возвращает контрактный status +- **WHEN** пользователь отправляет новую валидную регистрацию +- **THEN** система устанавливает refresh-cookie, возвращает профиль и access + token с HTTP `200` + +#### Scenario: Успешный login или refresh возвращает контрактный status +- **WHEN** пользователь успешно входит либо восстанавливает Session +- **THEN** система устанавливает или заменяет refresh-cookie, возвращает + профиль и access token с HTTP `200` + +### Requirement: Клиент контролируемо завершает неуспешный refresh +Frontend MUST обрабатывать HTTP и транспортную ошибку общего refresh flow без +необработанного отклонения Promise. При неуспешном cold-start refresh frontend +MUST завершить инициализацию как неавторизованный, не сохраняя access token +или профиль. При неуспешном refresh после защищённого запроса frontend MUST +очистить in-memory session и query cache перед existing navigation to login. + +#### Scenario: Cold-start refresh завершается транспортной ошибкой +- **WHEN** приложение запускается без access token и refresh запрос не может + получить транспортный response +- **THEN** frontend завершается в готовом неавторизованном состоянии без + необработанного отклонения Promise и показывает обычный public/login flow + +#### Scenario: Повторный refresh после protected 401 завершается транспортной ошибкой +- **WHEN** защищённый API отвечает `401`, а единственный serialised refresh + завершается транспортной ошибкой +- **THEN** frontend очищает in-memory session и query cache и направляет User + в существующий login flow без необработанного отклонения Promise diff --git a/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/tasks.md b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/tasks.md new file mode 100644 index 0000000..d65aff3 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-align-api-contract-and-client-resilience/tasks.md @@ -0,0 +1,68 @@ +## 1. Auth success-status contract + +- [x] 1.1 Добавить независимый от БД backend regression test, который читает + Nest response-status metadata для `register`, `login` и `refresh` и + проверяет `200`; cookie/payload behavior не входит в его scope. +- [x] 1.2 Добавить явный runtime status `200` в три auth POST handlers, не + меняя logout `204`, error responses и session/cookie logic; добиться + прохождения нового metadata regression test. +- [x] 1.3 Обновить существующие auth e2e success expectations с `201` на + `200`, включая явный successful login assertion; задокументировать, что + suite остаётся deferred по WL-P2-05, а не считается passing gate. +- [x] 1.4 Запустить штатный frontend OpenAPI sync workflow после backend + change; принять только artifacts, сгенерированные этим workflow, затем + выполнить generated-artifact и runtime-contract checks. + +## 2. Представление Client Cached Snapshot + +- [x] 2.1 Добавить observable Accounts page regression coverage для + существующего списка во время in-flight refetch и после его ошибки: список + остаётся видимым, loading indicator неблокирующий, а retry локализованного + warning вновь вызывает тот же query. +- [x] 2.2 Расширить Account page coverage для эквивалентных Portfolio refetch + states, включая независимый render server `isStale` и client warnings об + ошибке refresh. +- [x] 2.3 Отрефакторить Accounts и Account view models, чтобы blocking + loading/error states выбирались только при отсутствии query data; раскрыть + progress, failure и retry refresh рядом с данными Client Cached Snapshot без + изменения query TTL, retry или refetch policies. +- [x] 2.4 Обновить Accounts и Account page presentations и localized copy для + отображения неблокирующего progress и доступных client refresh warnings с + retry, сохранив operations/events pagination вне scope. + +## 3. Контролируемая ошибка refresh + +- [x] 3.1 Добавить AuthProvider regression coverage для отклонённого + cold-start refresh: он устанавливает `ready`, оставляет user/access token + пустыми, очищает query cache и не создаёт необработанного отклонения Promise. +- [x] 3.2 Добавить coverage для protected `401`, чей serialised refresh имеет + транспортную ошибку: refresh возвращает контролируемую ошибку, локальный + auth/cache очищен, а исходный unauthorized response сохранён для + существующего route handling. +- [x] 3.3 Централизовать применение и очистку local auth state, направить + cold start и refresh защищённого запроса через общий non-throwing refresh + path и сохранить one-refresh-at-a-time behavior. + +## 4. Устойчивая документация и состояние remediation + +- [x] 4.1 Обновить feature documentation authentication, brokerage accounts и + portfolio с фактически поставленными `200` auth responses и различием между + server-side Portfolio fallback и Client Cached Snapshot refresh UX; limits и + deferred Prisma evidence должны остаться точными. +- [x] 4.2 Поддерживать `docs/development/audit-remediation.md` согласованным с + active status change и verification evidence; только после успешного archive + записать terminal archive path, commits и checks, затем отметить WL-P2-04, + WL-P2-07 и WL-P2-08 `done` вне этого task list. + +## 5. Верификация перед archive + +- [x] 5.1 Выполнить затронутые frontend и backend non-mutating lint, typecheck + и unit-test gates под Node 22; выполнить frontend `verify`, когда его + generated artifacts окончательны, и записать точные observed results. +- [x] 5.2 Выполнить `bash specs/scripts/verify-runtime-openapi.sh`, OpenSpec + authority verifier, strict change/spec validation и `git diff --check` в + каждом затронутом Git repository; устранить каждый blocking result. +- [x] 5.3 Провести независимый read-only review implementation, delta specs, + generated-contract diff, error-state tests и документации; выполнить + применимую `opsx-verify` procedure и устранить все blocking findings до + archive. diff --git a/openspec/specs/frontend-data-resilience/spec.md b/openspec/specs/frontend-data-resilience/spec.md new file mode 100644 index 0000000..f0193c0 --- /dev/null +++ b/openspec/specs/frontend-data-resilience/spec.md @@ -0,0 +1,36 @@ +# frontend-data-resilience Specification + +## Purpose + +Определяет, когда frontend сохраняет последний успешный ответ на экране и как +сообщает пользователю о неудачном фоновом обновлении без искажения доменных +состояний Portfolio. + +## Requirements + +### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio +Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или +Portfolio на экране, если для этого же набора данных уже был успешно получен +ответ. Начальная загрузка без такого ответа может показывать блокирующее +состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время +фонового обновления и после его ошибки; он не является server-side fallback и MUST NOT +изменять или интерпретировать поле `Portfolio.isStale`. + +#### 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 + предупреждение независимо, не представляя один вид состояния другим diff --git a/openspec/specs/user-authentication/spec.md b/openspec/specs/user-authentication/spec.md index 8abbb0d..aaf2b1b 100644 --- a/openspec/specs/user-authentication/spec.md +++ b/openspec/specs/user-authentication/spec.md @@ -123,3 +123,40 @@ #### Scenario: Выход из настроек - **WHEN** пользователь нажимает logout на `/settings` - **THEN** клиент очищает локальную сессию и cache и направляет пользователя на `/login` независимо от результата сетевого запроса + +### Requirement: Единый HTTP contract успешных auth endpoints +Успешные `POST /api/v1/auth/register`, `POST /api/v1/auth/login` и +`POST /api/v1/auth/refresh` MUST отвечать HTTP `200`; опубликованный +OpenAPI-документ, зафиксированный frontend snapshot и сгенерированные +transport types MUST описывать этот же status. Каждая операция сохраняет свои +существующие payload, refresh-cookie и session semantics. Logout MUST +продолжать отвечать `204`. + +#### Scenario: Успешная регистрация возвращает контрактный status +- **WHEN** пользователь отправляет новую валидную регистрацию +- **THEN** система устанавливает refresh-cookie, возвращает профиль и access + token с HTTP `200` + +#### Scenario: Успешный login или refresh возвращает контрактный status +- **WHEN** пользователь успешно входит либо восстанавливает Session +- **THEN** система устанавливает или заменяет refresh-cookie, возвращает + профиль и access token с HTTP `200` + +### Requirement: Клиент контролируемо завершает неуспешный refresh +Frontend MUST обрабатывать HTTP и транспортную ошибку общего refresh flow без +необработанного отклонения Promise. При неуспешном cold-start refresh frontend +MUST завершить инициализацию как неавторизованный, не сохраняя access token +или профиль. При неуспешном refresh после защищённого запроса frontend MUST +очистить in-memory session и query cache перед existing navigation to login. + +#### Scenario: Cold-start refresh завершается транспортной ошибкой +- **WHEN** приложение запускается без access token и refresh запрос не может + получить транспортный response +- **THEN** frontend завершается в готовом неавторизованном состоянии без + необработанного отклонения Promise и показывает обычный public/login flow + +#### Scenario: Повторный refresh после protected 401 завершается транспортной ошибкой +- **WHEN** защищённый API отвечает `401`, а единственный serialised refresh + завершается транспортной ошибкой +- **THEN** frontend очищает in-memory session и query cache и направляет User + в существующий login flow без необработанного отклонения Promise