docs(specs): archive api contract resilience remediation

This commit is contained in:
2026-08-24 00:51:06 +03:00
parent 6e20cdffaf
commit 800450d075
8 changed files with 386 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-23
@@ -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 не нужны.
@@ -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 не требуется.
@@ -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
предупреждение независимо, не представляя один вид состояния другим
@@ -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
@@ -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.
@@ -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
предупреждение независимо, не представляя один вид состояния другим
@@ -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