diff --git a/.gitea/ISSUE_TEMPLATE/bug.md b/.gitea/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..c7edcc9 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,12 @@ +--- +name: Bug Report +about: Неверное поведение +--- +## Проблема и шаги воспроизведения + +## Факт / ожидание + +## Acceptance Criteria +- [ ] + +## Окружение / ссылки (если применимо) diff --git a/.gitea/ISSUE_TEMPLATE/engineering.md b/.gitea/ISSUE_TEMPLATE/engineering.md new file mode 100644 index 0000000..96b826e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/engineering.md @@ -0,0 +1,10 @@ +--- +name: Engineering Task +about: CI, тесты, документация, процесс и прочее +--- +## Контекст и результат + +## Scope / связанные Issues (если есть) + +## Acceptance Criteria +- [ ] diff --git a/.gitea/ISSUE_TEMPLATE/feature.md b/.gitea/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..b10e51f --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,12 @@ +--- +name: Feature Request +about: Новое наблюдаемое поведение +--- +## Зачем и какой результат + +## Ожидаемое поведение + +## Acceptance Criteria +- [ ] + +## Зависимости / связанные Issues (если есть) diff --git a/.gitea/ISSUE_TEMPLATE/refactor.md b/.gitea/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 0000000..f67cf63 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,12 @@ +--- +name: Refactoring / Technical Debt +about: Улучшение без нового поведения +--- +## Проблема и результат + +## Сохраняемые инварианты / границы + +## Acceptance Criteria +- [ ] + +## Зависимости / ссылки (если есть) diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f31e344 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Summary + +## Linked Issue +Refs + +## Changes + +## Verification +- Команда / результат / ограничения: + +## Breaking Changes (если есть) diff --git a/.gitea/workflows/commits.yml b/.gitea/workflows/commits.yml index 2372d2c..b485a61 100644 --- a/.gitea/workflows/commits.yml +++ b/.gitea/workflows/commits.yml @@ -2,7 +2,11 @@ name: Conventional commits on: pull_request: - types: [opened, synchronize, reopened, edited] + types: [opened, synchronize, reopened] + +concurrency: + group: specs-commits-${{ gitea.event.pull_request.number }} + cancel-in-progress: true jobs: commits: @@ -23,4 +27,8 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict - npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA" + if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then + npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA" + else + npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA" + fi diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml index abce889..05edff5 100644 --- a/.gitea/workflows/security.yml +++ b/.gitea/workflows/security.yml @@ -2,12 +2,16 @@ name: Specs security on: push: - branches: - - '**' + branches: [develop, main] pull_request: + types: [opened, synchronize, reopened] schedule: - cron: '17 5 * * *' +concurrency: + group: specs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }} + cancel-in-progress: true + jobs: security: runs-on: ubuntu-latest diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml index e59c2d5..cd6d109 100644 --- a/.gitea/workflows/verify.yml +++ b/.gitea/workflows/verify.yml @@ -2,8 +2,13 @@ name: Specs verify on: push: - branches: ['**'] + branches: [develop, main] pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: specs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }} + cancel-in-progress: true jobs: verify: diff --git a/AGENTS.md b/AGENTS.md index a8e9696..c87a4db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,8 +2,11 @@ Репозиторий содержит существующие behavioral specifications и SDD history. Для обычной разработки не создавай и не обновляй planning artifacts ради -процесса. Требование пользователя определяет работу; OpenSpec применяется -только по явному запросу. +процесса. До edits найди/создай подходящую Gitea Issue, проверь AC и ветку; +чтение допускается без Issue. Выбор lightweight/SDD — по +[Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md) +(main#28; после merge — актуальная версия `develop`); +OpenSpec lifecycle начинается только после явного решения Change Owner. Если нужна существующая behavioral spec, выбери точный target через [AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI: @@ -16,11 +19,14 @@ anchors и `git diff --check`; application gates принадлежат affected Если явно запрошена работа через OpenSpec, следуй [локальным инструкциям OpenSpec](README.md#начало-работы) и используй store `wealth-ledger-specs`. Перед Node-командами активируй -местную `.nvmrc` через `nvm use --silent`. +местную `.nvmrc`: если `nvm` не загружен в non-interactive shell, загрузи +установленный `$HOME/.nvm/nvm.sh`, затем выполни `nvm use --silent` из specs. +Если `nvm.sh` недоступен, остановись; не используй системный Node. ## Ветки и релизы -Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую +Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы). -Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`. +Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release); +для спецификаций допустим `spec:`. Перед подготовкой релиза прочитай местный README; версии вручную не повышать. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f46a22..c73f2c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.3...v0.1.4) (2026-10-12) + +### Bug Fixes + +* align specs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a29cbf6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a29cbf6aae7ebe48a51ae1979ff3f30b9fa8e305)) + +### Documentation + +* clarify specs Node bootstrap ([18518e0](https://git.personal.ksv741.ru/WealthLedger/specs/commit/18518e0d2daecb843fea1b4d55d1c7d2cabf24f6)) +* pin specs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([7f12327](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f12327c37055ba903f962b1220138da9c7edd06)) + +### Specifications + +* add harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([14174a7](https://git.personal.ksv741.ru/WealthLedger/specs/commit/14174a7bb335152c8cc76d9a6bbaefaaaa58b22f)) +* add stock screener ([951f1a9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/951f1a9a20a09695e2ff85d1ee96632e98a202d8)) +* address hourly-sync review findings (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([77b2111](https://git.personal.ksv741.ru/WealthLedger/specs/commit/77b211186c297df4187e044804786ffc9096ce19)) +* archive harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([fe295aa](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fe295aa73ab5bc9d826067892930b7161fb4f1b9)) +* define hourly portfolio sync history ([b24a686](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b24a6861887e831ef2d73a97f36cc213be2be0b8)) +* finalize wealth-ledger SDD migration ([329d66e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/329d66e767f7e89a40b96bb8e5b499f39f67feb6)) + +### CI + +* avoid duplicate PR checks ([c132770](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c132770fbb4a4e48cae0a4ceca7407bb6a768ded)) +* skip checks on PR metadata edits ([fcdc805](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121)) + +### Maintenance + +* align specs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a441b08](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a441b083785d8f50c42dea543e684b23f27732a2)) + ## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.2...v0.1.3) (2026-10-07) ### Bug Fixes diff --git a/README.md b/README.md index cbc971f..17e921c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ # WealthLedger Specs -Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace), новые changes используют project-local schema `wealth-ledger-sdd`. +Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace). Новые changes используют `wealth-ledger-sdd-v2`; существующие active changes с `schema: wealth-ledger-sdd` завершаются по legacy schema без автоматической миграции. ## Начало работы -OpenSpec применяется по явному запросу согласно [AGENTS](AGENTS.md). Proposal, Delta Specs, Design и Tasks создаются последовательно по одному после approval Change Owner. Для любой команды, читающей или изменяющей change, явно передавайте `--store wealth-ledger-specs`. +OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner. -Design должен оценить `frontend`, `backend`, `specs`, `docs` в Delivery Map и при обязательном trigger включить Solution Assessment. Docs описывает as-built state и не становится параллельным store требований. +| Change | Lifecycle | +| --- | --- | +| Active `schema: wealth-ledger-sdd` | Proposal → Delta Specs → Design → Tasks → Apply; действующие approvals сохраняются. Legacy Design оценивает `frontend`, `backend`, `specs`, `docs` в Delivery Map. | +| New `schema: wealth-ledger-sdd-v2` | Proposal → Delta Specs → System Analysis → Design + QA Analysis → Docs Impact → Tasks → Apply. Design и QA Analysis готовы после System Analysis; Docs Impact ждёт обоих. | + +Для changes без изменения observable behavior используется `skip_specs: true`. В v2 System Analysis/QA Analysis/Docs Impact всё равно фиксируют только применимое содержание и обоснованное N/A, а не фиктивные contracts или tests. `specs/openspec/specs/` хранит нормативное поведение, `docs/` — текущий AS-IS, backend-generated OpenAPI — текущий executable HTTP contract. Target contracts остаются в change до архива; docs обновляются только по согласованному Docs Impact через `[docs]` tasks. После завершения последнего legacy change эту переходную заметку можно удалить отдельным изменением. ## Проверка @@ -14,11 +19,16 @@ Design должен оценить `frontend`, `backend`, `specs`, `docs` в Del ```bash openspec schema validate wealth-ledger-sdd --verbose +openspec schema validate wealth-ledger-sdd-v2 --verbose +openspec status --change --store wealth-ledger-specs openspec validate --type change --strict --store wealth-ledger-specs openspec validate --specs --strict --store wealth-ledger-specs ``` -Structural validation не заменяет semantic verification и independent read-only review перед archive. +Версия OpenSpec CLI не закреплена отдельным файлом: при обновлении проверяйте обе +schema, затронутые changes и canonical specs, фиксируя `openspec --version` вместе +с результатами проверок. Structural validation не заменяет semantic verification +и independent read-only review перед archive. Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`. `npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой @@ -27,9 +37,11 @@ Node задаётся собственной `.nvmrc`; после `nvm use --sil ## Релизы Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые -релизы. Работа: `codex/` от актуального `develop`, PR в `develop`. +релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в +`develop`; общие правила — в Engineering Workflow по ссылке из AGENTS. Обычные PR допускают squash; release и обратная синхронизация сохраняют историю. -Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай. +Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release), +включая `spec:`. Версии вручную не повышай. 1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что `develop` содержит последний релиз. Создай `release/` от проверенного diff --git a/commitlint.config.cjs b/commitlint.config.cjs index 4aa31e2..512700c 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.cjs @@ -1,4 +1,12 @@ module.exports = { extends: ["@commitlint/config-conventional"], - rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] }, + defaultIgnores: false, + ignores: [ + message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()), + message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()), + ], + rules: { + "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]], + "references-empty": [2, "never"], + }, }; diff --git a/openspec/.cli-version b/openspec/.cli-version deleted file mode 100644 index 81c871d..0000000 --- a/openspec/.cli-version +++ /dev/null @@ -1 +0,0 @@ -1.10.0 diff --git a/openspec/changes/add-manual-bank-cards/.openspec.yaml b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/.openspec.yaml similarity index 100% rename from openspec/changes/add-manual-bank-cards/.openspec.yaml rename to openspec/changes/archive/2026-10-09-add-manual-bank-cards/.openspec.yaml diff --git a/openspec/changes/archive/2026-10-09-add-manual-bank-cards/completion-evidence.md b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/completion-evidence.md new file mode 100644 index 0000000..f2205c2 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/completion-evidence.md @@ -0,0 +1,40 @@ +# Completion Evidence: add-manual-bank-cards + +## Finalization Authority + +Change Owner authorized Finalization, canonical sync and archive after a current +independent read-only Validation PASS. The PASS found no Critical or Important +findings after the monthly-summary presentation correction. + +## Delivery + +| Area | Result | Delivery commit | +| --- | --- | --- | +| frontend | Manual card screens and summaries; Finalization correction shows exact expenses, refunds and net totals with a regression test. | `ff67a76`, `d3ac8c2`, `734c5bd` | +| backend | Bank-card persistence, calculations, API and isolated verification. | `e6f58ef` | +| docs | Cards overview states the three displayed summary totals. | `3a27268` | +| specs | Approved legacy Proposal, Delta Specs, Design, completed Tasks and Finalization evidence; native sync creates canonical behavior spec. | `834f2d1` plus finalization/archive commit | + +## Observed Verification + +| Area | Command or review | Result | +| --- | --- | --- | +| frontend | `npm run test:run -- src/entities/bank-card/ui/BankSummary/BankSummary.view.test.tsx` | PASS: 1 file, 1 test | +| frontend | `npm run typecheck` | PASS | +| docs | `npm run verify` | PASS | +| specs | `openspec schema validate wealth-ledger-sdd --verbose` | PASS | +| specs | `openspec schema validate wealth-ledger-sdd-v2 --verbose` | PASS | +| specs | `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs` | PASS | +| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 25 specs, 0 failed | +| all affected areas | `git diff --check` | PASS | + +## Independent Validation + +PASS. The review compared Proposal, all 17 Delta Requirements, Design and +completed Tasks with current backend calculations/DTOs, shared frontend summary +rendering and cards documentation. It verified that `/cards` and `/cards/:id` +share the corrected summary view and found no Critical or Important findings. + +Known non-blocking limitation: a plain Prisma fresh migration deploy still needs +`RUST_LOG=info`; the verified migration path and its evidence remain recorded in +the change Tasks. diff --git a/openspec/changes/add-manual-bank-cards/design.md b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/design.md similarity index 100% rename from openspec/changes/add-manual-bank-cards/design.md rename to openspec/changes/archive/2026-10-09-add-manual-bank-cards/design.md diff --git a/openspec/changes/add-manual-bank-cards/proposal.md b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/proposal.md similarity index 100% rename from openspec/changes/add-manual-bank-cards/proposal.md rename to openspec/changes/archive/2026-10-09-add-manual-bank-cards/proposal.md diff --git a/openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/specs/manual-bank-cards/spec.md similarity index 100% rename from openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md rename to openspec/changes/archive/2026-10-09-add-manual-bank-cards/specs/manual-bank-cards/spec.md diff --git a/openspec/changes/add-manual-bank-cards/tasks.md b/openspec/changes/archive/2026-10-09-add-manual-bank-cards/tasks.md similarity index 100% rename from openspec/changes/add-manual-bank-cards/tasks.md rename to openspec/changes/archive/2026-10-09-add-manual-bank-cards/tasks.md diff --git a/openspec/changes/containerized-application-delivery/.openspec.yaml b/openspec/changes/archive/2026-10-09-containerized-application-delivery/.openspec.yaml similarity index 100% rename from openspec/changes/containerized-application-delivery/.openspec.yaml rename to openspec/changes/archive/2026-10-09-containerized-application-delivery/.openspec.yaml diff --git a/openspec/changes/archive/2026-10-09-containerized-application-delivery/completion-evidence.md b/openspec/changes/archive/2026-10-09-containerized-application-delivery/completion-evidence.md new file mode 100644 index 0000000..31e73e3 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-containerized-application-delivery/completion-evidence.md @@ -0,0 +1,31 @@ +# Completion Evidence: containerized-application-delivery + +## Outcome + +The local and disposable single-server container delivery path is implemented +and verified. External publication and target-server gates remain unavailable in +the current environment and are recorded as blockers rather than represented as +successful results. + +## Observed Results + +- Frontend and backend development/production images build and run locally. +- Development HMR/watch, same-origin API routing, production SPA fallback, + readiness, persistence, migration failure handling, backup and isolated + restore were verified on disposable data. +- Docs verification and Compose configuration checks passed. +- No production data, secrets, registry credentials or real server were used. + +## External Blockers + +The following approved implementation tasks remain externally blocked: + +- Docker-capable Gitea runner acceptance; +- registry publisher and server pull-only credentials; +- authenticated candidate publication and digest pull; +- target-server Docker/Compose verification; +- first server rollout pair confirmation. + +These tasks remain unchecked in the archived checklist. The change is archived +as externally blocked after owner authorization; this is not a claim that the +external gates passed and does not authorize production rollout. diff --git a/openspec/changes/containerized-application-delivery/design.md b/openspec/changes/archive/2026-10-09-containerized-application-delivery/design.md similarity index 100% rename from openspec/changes/containerized-application-delivery/design.md rename to openspec/changes/archive/2026-10-09-containerized-application-delivery/design.md diff --git a/openspec/changes/containerized-application-delivery/proposal.md b/openspec/changes/archive/2026-10-09-containerized-application-delivery/proposal.md similarity index 100% rename from openspec/changes/containerized-application-delivery/proposal.md rename to openspec/changes/archive/2026-10-09-containerized-application-delivery/proposal.md diff --git a/openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md b/openspec/changes/archive/2026-10-09-containerized-application-delivery/specs/application-delivery/spec.md similarity index 100% rename from openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md rename to openspec/changes/archive/2026-10-09-containerized-application-delivery/specs/application-delivery/spec.md diff --git a/openspec/changes/containerized-application-delivery/tasks.md b/openspec/changes/archive/2026-10-09-containerized-application-delivery/tasks.md similarity index 99% rename from openspec/changes/containerized-application-delivery/tasks.md rename to openspec/changes/archive/2026-10-09-containerized-application-delivery/tasks.md index f7c6cc6..74de193 100644 --- a/openspec/changes/containerized-application-delivery/tasks.md +++ b/openspec/changes/archive/2026-10-09-containerized-application-delivery/tasks.md @@ -92,7 +92,7 @@ secrets и опубликованные release tags не используютс - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`. - **Verification:** dry-run runbook на disposable data проходит без неописанного шага; frontend-only и backend-only paths не пересоздают другой component; restore требует отдельного решения и сообщает о потере данных после backup. -- [ ] 5.3 [docs] Выполнить docs repository completion gate. +- [x] 5.3 [docs] Выполнить docs repository completion gate. - **Covers:** `N/A` — repository-owned проверка durable as-built documentation и ссылок без нового runtime behavior. - **Verification:** `npm ci`, `npm run verify`, `git diff --check`; проверить Mermaid rendering, взаимные ADR/architecture/runbook links и отсутствие credentials или host-private values. diff --git a/openspec/changes/archive/2026-10-09-evolve-frontend-stack/.openspec.yaml b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/.openspec.yaml new file mode 100644 index 0000000..2eea917 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/.openspec.yaml @@ -0,0 +1 @@ +schema: wealth-ledger-sdd diff --git a/openspec/changes/evolve-frontend-stack/design.md b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/design.md similarity index 100% rename from openspec/changes/evolve-frontend-stack/design.md rename to openspec/changes/archive/2026-10-09-evolve-frontend-stack/design.md diff --git a/openspec/changes/evolve-frontend-stack/proposal.md b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/proposal.md similarity index 100% rename from openspec/changes/evolve-frontend-stack/proposal.md rename to openspec/changes/archive/2026-10-09-evolve-frontend-stack/proposal.md diff --git a/openspec/changes/evolve-frontend-stack/specs/frontend-design-system/spec.md b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/specs/frontend-design-system/spec.md similarity index 100% rename from openspec/changes/evolve-frontend-stack/specs/frontend-design-system/spec.md rename to openspec/changes/archive/2026-10-09-evolve-frontend-stack/specs/frontend-design-system/spec.md diff --git a/openspec/changes/evolve-frontend-stack/tasks.md b/openspec/changes/archive/2026-10-09-evolve-frontend-stack/tasks.md similarity index 100% rename from openspec/changes/evolve-frontend-stack/tasks.md rename to openspec/changes/archive/2026-10-09-evolve-frontend-stack/tasks.md diff --git a/openspec/changes/reduce-screen-sync-polling/.openspec.yaml b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/.openspec.yaml similarity index 100% rename from openspec/changes/reduce-screen-sync-polling/.openspec.yaml rename to openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/.openspec.yaml diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/design.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/design.md new file mode 100644 index 0000000..dca002b --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/design.md @@ -0,0 +1,96 @@ +## Context + +Backend — один NestJS process и SQLite. Сейчас screen-driven синхронизация раскладывает запросы экранов на source work; владельцы membership, portfolio, operations, forecast, catalog и candles используют reservations, attempt/revision fences и короткие транзакции без RPC внутри них. Текущий portfolio snapshot перезаписывается и каскадно удаляется при замене подключения или исчезновении счёта. `TbankSyncService` принимает экранный POST; frontend `screen-sync` запускает его и наблюдает status. Справочные данные, результаты поиска и отрицательные lookup живут в сохранённых таблицах, а detail является зависимостью событий и рыночных данных. Новый режим требует смены инициатора, не второй конкурирующей системы записи. + +## Goals / Non-Goals + +**Goals:** сохранить единственного владельца публикации каждого источника; сделать расписание устойчивым к рестарту в рамках одного процесса; связать точки истории с успешной публикацией портфеля и подтверждённым доступом; отделить сетевое чтение интерфейса от брокерского обновления; перевести подробные сведения инструментов на онлайн-источник без потери сохранённых рыночных и личных данных. + +**Non-Goals:** график и публичная история точек, новый механизм синхронизации между процессами, повторное сканирование полной рыночной истории и уже полных операций, изменение банковских карт и auth-refresh. + +## Decisions + +### Один durable clock поверх существующих source owners + +В `tbank-sync` вводится одиночный backend scheduler с persisted последней попыткой для текущего подключения: начало, завершение/состояние и диагностические разделы без токенов. При успешном PUT подключения фоновой работе передаётся новая граница; при старте process scheduler сверяет её и последнее начало, не опираясь на frontend GET. Проверка часовым таймером только предлагает работу; атомарное резервирование сверяет user/revision, время начала предыдущей попытки и отсутствие ещё активной попытки. Во время работы новое резервирование не допускается; пересечённый срок пропускается, а не исполняется «вдогонку». После рестарта незавершённая попытка старого process становится прерванной, но её время начала остаётся основанием следующего запуска; новый process не публикует её поздние результаты. Успешное подключение начинает новую первую попытку, даже если прежний токен был заменён незадолго до срока. Штатный процесс инициализирует таймер и освобождает его при shutdown. + +Scheduler делает один обход подтверждённого membership, затем даёт portfolio owner обновить каждый доступный счёт, operations owner — только изменяемый хвост, forecast/source owners — события бумаг состава. Передаёт доступ и ревизию владельцам, не пишет их личные наборы сам и не держит транзакцию при сетевых вызовах. Результаты отдельных источников дают общий итог `succeeded/partial/failed` и безопасные идентификаторы счёта/раздела. При недействительном токене применяется существующая блокировка личных ответов. Альтернатива — reuse screen intent с поддельным экраном `accounts`: он не покрывает операции и события для каждого счёта и случайно включает ненужные зависимости; выделение фонового orchestration над теми же owners точнее. + +### История отдельно от revision, но внутри атомарной публикации + +Состояние истории принадлежит `(userId, accountId)` и переживает смену token/revision; текущий snapshot и source work по-прежнему принадлежат `(userId, revision, accountId)`. Точки используют точную десятичную строку, ISO-валюту и фактическое время успешной публикации, без состава позиций. Проверка полноты оценки всех позиций предшествует вставке любых валютных точек; пустой безвалютный портфель не даёт строк. Для однозначности повторной публикации точка привязана к идентификатору успешной попытки portfolio, а её запись и замена snapshot происходят в одной короткой транзакции после conditional attempt claim. При потере revision/доступа вся транзакция отменяется. Неоценённый состав сохраняется текущим без исторических точек. + +Видимость истории определяется текущим подтверждённым membership и признаком удаления счёта, а не одной лишь идентичностью исторических строк. Успешное подтверждение списка, успешный PUT нового token и DELETE обновляют пометки доступа вместе со сменой текущей границы; неуспешный PUT/GET membership не меняет их. Чтение возможной истории в будущем обязано проверять session, текущее неотклонённое подключение и активное подтверждение конкретного accountId: `deletedAt = null` само по себе не является полномочием. Это позволяет сохранить прошлые точки, не воскресить приватный снимок прежней revision и не раскрыть историю между отключением и повторным подтверждением. Поскольку endpoint истории сейчас не предусмотрен, API для её чтения не проектируется. + +### Сначала разделить источники по смыслу, затем удалить screen import + +Обычные GET инвестиционных областей становятся локальными для membership/portfolio и изменяемых операций/прогнозов; их freshness отражает час, а ошибки попыток живут отдельно. Исключения — первичная загрузка отсутствующего исторического интервала операций и впервые запрошенного диапазона свечей; покрытие остаётся владельцем операций/candles. Текущие quotes/незавершённые свечи обновляются по запросу не чаще часа. Удаление ручного screen POST и его наблюдения следует после перехода всех consumers на чистые reads и статус последнего обхода. Иначе нынешние `sync=observe` GET и retry-кнопка могут обходить новый график; транспортный retry сохраняется, но не вызывает брокерский импорт. Публичный DTO статуса описывает только последнюю попытку и читается под той же auth/access boundary. Backend публикует OpenAPI как контракт, frontend обновляет свой snapshot из него отдельно. + +### Онлайн-справочник без разрушения зависимостей + +Публичный instrument detail, company и catalog search идут через существующие T-Bank/MOEX/NCR clients по запросу с проверкой UID/ticker и сохранением различия «не найдено / неоднозначно / источник недоступен». Общая активная работа может коалесцировать одинаковые сетевые вызовы в процессе, но отдельная постоянная копия справочника и отрицательного lookup не создаётся. Для прогноза необходимые атрибуты запрашиваются внутри его работы; ошибка не заменяет последний успешный forecast. Для сохранённых quotes, candle coverage, событий и portfolio labels остаётся только минимальная стабильная identity, нужная для ключей и чтения; ссылки и пользовательские данные не зависят от удаляемого detail payload. Старые справочные записи выводятся из использования до рассмотрения их удаления; каскады не должны уничтожать свечи. При сбое онлайн-источника блок detail показывает отдельную ошибку, остальные сохранённые данные доступны в своих границах. + +## UI/UX Applicability and Package + +**Applicability and scope:** `new/changed` — на инвестиционных экранах убираются ручные команды синхронизации, добавляются 30-минутное перечитывание и статус последнего обхода на кабинете. Макет графика не нужен. Пакет полностью inline; локальные PNG/SVG не требуются. + +**Package revision:** `1` (этот Design). + +**Screens, states and interactions:** + +| Экран / блок | Без результата | Данные сохранены | Сбой / частичный успех | Действие | +| --- | --- | --- | --- | --- | +| Кабинет/счета | штатная первая загрузка и отсутствие доступных счетов | прежние карточки и время обновления | сообщение о последнем обходе: состояние, время, названия проблемных счетов/разделов без token/error payload | переход к счёту или настройкам; кнопки синхронизации нет | +| Портфель/операции/доход/события | локальная загрузка либо недостающий исторический период | сохранённые значения и исходное время остаются при перечитывании | server-side stale отдельно от transport error; прежние значения не становятся новым snapshot | повтор только HTTP-чтения при сетевой ошибке | +| Карточка инструмента и поиск | справочный блок ожидает онлайн-ответ; свечи могут впервые загружаться отдельно | сохранённые позиции, операции, подписи, котировки/свечи и события сохраняют самостоятельные состояния | ошибка справочного блока не скрывает независимые данные; подпись snapshot не обещает свежие подробности | открытие и повтор HTTP-чтения сведений без команды синхронизации личных счетов | + +30-минутное перечитывание действует только пока инвестиционный экран открыт; для онлайн-справочного блока оно также означает новый запрос сведений. Время последней попытки не подменяет `lastSuccessfulAt` отдельного счёта. Текст частичного успеха включает доступный список проблемных разделов; не показываются сырые upstream сообщения. + +**Responsive behavior:** использовать существующие адаптивные контейнеры и ширины списков/карточек; строка статуса переносит названия счетов и разделов на узком экране, не закрывает содержимое и имеет доступное текстовое обозначение помимо цвета. Новых плотных таблиц, графиков или мобильной навигации нет. + +**Reused source and differences:** существующие as-built страницы кабинета, портфеля, операций и карточек инструмента сохраняют компоновку; новый пакет не зависит от изменяемой внешней ссылки либо архивного макета. Отличия перечислены выше: статус обхода вместо ручного refresh и локальная ошибка online detail. Идентификатор immutable archived reuse не применяется: новый статус и новые переходы состояний описаны здесь inline, не объявляются точной копией прошлой версии. + +## Delivery Map + +| Area | Status | Architectural outcome or N/A reason | +| --- | --- | --- | +| frontend | `affected` | Убрать screen-triggered mutation и инвестиционные sync-кнопки, сохранить transport retry, добавить периодическое чтение и статус на кабинете, независимо показывать ошибку online detail. | +| backend | `affected` | Durable scheduler, учёт частичного результата и доступа, точки истории, локальные read boundaries, онлайн-справочник и опубликованный API статуса. | +| specs | `affected` | Нормативные delta requirements для нового расписания, истории и обновлённых инвестиционных/клиентских сценариев. | +| docs | `affected` | Обновить объяснение свежести, областей и глоссарий, зафиксировать новое решение относительно ADR 017 без переписывания его исторического контекста. | + +## Solution Assessment + +| Вариант | Fit и maturity | Security / data integrity | Operating cost и ограничения | Migration cost | +| --- | --- | --- | --- | --- | +| Внешний cron/worker с отдельным экземпляром | Зрелый для fleet, но дублирует приложение при single-process SQLite | Нужны распределённые lock/lease и граница токенов между процессами | Отдельный deployment и координация | Высокая; против текущей single-process модели | +| Nest scheduling dependency | Зрелые cron decorators/timers, но сама библиотека не хранит результат попыток | В любом случае нужны persisted CAS/fences для restart и подключения | Ещё один runtime package и жизненный цикл без выигрыша от cron expression | Небольшая, но ненужная зависимость | +| Native Node timer + существующие SQLite/Prisma reservations | Подходит для одного процесса; точность минутных тиков не подменяет persisted время начала | Conditional reservation + revision/attempt fence, история в DB transaction | Нет нового сервиса или пакета; при нескольких процессах требуется отдельная координация | Умеренная, повторное использование владельцев источников | + +Выбран native timer с durable CAS: уже существующий `TbankSyncWork` и SQLite дают необходимые primitives, но сам таймер не является гарантом уникальности. Для истории использовать уже установленный Prisma/SQLite вместо time-series базы; данные малы, rollback и backup живут в существующем deployment. Для справочных сведений использовать действующих integration clients и online reads; новая cache/service boundary не оправдана, а долговременное кэширование прямо противоречит принятым требованиям. Отказ от него повышает зависимость detail от T-Bank, поэтому ошибки изолируются от сохранённых финансовых наборов. Перед настройкой фактического числа upstream вызовов требуется сверить действующие первичные ограничения провайдера и измерить размер/прирост локальной базы на реальном объёме; произвольные внешние числовые лимиты не фиксируются как допущение. + +## Risks / Trade-offs + +- При простое backend точки пропускаются; нет искусственной интерполяции и накопленной очереди RPC. +- Один timer без отдельной межпроцессной аренды небезопасен для нескольких экземпляров: deployment сохраняет единственный process. +- Переход на онлайн-справочник увеличивает latency и чувствительность detail/search к upstream; нельзя распространять ошибку на сохранённый портфель или его события. +- Исчезновение/возврат счёта и одновременная смена token требуют fence внутри коротких транзакций; `deletedAt` без актуального подтверждения membership не даёт права чтения. +- Удаление таблиц справочника до развязки FK/dependencies может стереть историю свечей; destructive cleanup откладывается до проверки сохранности рыночных данных. +- Старый frontend и новый backend должны сосуществовать в переходе: публикация DTO статуса предшествует удалению screen POST; потребителю передаётся явный OpenAPI artifact. + +## Migration Plan + +1. Добавить историю и metadata последней попытки совместимым расширением SQLite; не переписывать уже применённые migrations и не выполнять reset на рабочей базе. Привязать запись истории к существующему portfolio publisher и видимость к подтверждённому membership/connection lifecycle. +2. Ввести backend-only расписание поверх существующих source owners и read-only статус; включить первичную загрузку и восстановление после рестарта при сохранённом времени попытки. Только после проверки idempotency и отказов перевести GET на read-only для текущих личных данных. +3. Развязать online reference от долговременных detail/search/lookup записей, сохранив минимальные identity и таблицы quotes/candles/forecast; сначала перевести readers, затем совместимой миграцией удалять лишь действительно неиспользуемые справочные payload, не каскадируя рыночную историю. +4. Получить опубликованный backend OpenAPI и отдельно обновить frontend query/статус/кнопки в рамках согласованного contract. Затем убрать неиспользуемый screen-triggered endpoint; зафиксировать изменение ADR и as-built страниц. Измерить рост SQLite и provider call volume до изменения частоты/лимитера. + +### Rollback Contract + +| Field | Decision | +| --- | --- | +| Status | `required` | +| Trigger | Нарушение доступа к истории после замены token, повторная публикация точек, порча сохранённых свечей при переходе справочника либо несовместимость нового frontend с API статуса. | +| Actions | Остановить scheduler, вернуть предыдущие совместимые сборки из проверенного комплекта и восстановить DB из согласованного backup перед destructive переходом; при additive стадии допустимо вернуть старый runtime с сохранением новых неиспользуемых таблиц. Не возобновлять прежний screen-triggered режим одновременно с включённым hourly scheduler. | +| Data safety | Сохранить исходные snapshots, coverage, forecast и точки истории; никогда не удалять или заново создавать production SQLite ради rollback. Перед очисткой справочника обеспечить проверенный backup/restore и доказать отсутствие каскадов в candles. | +| Verification | Проверить на копии с представительными данными доступность прежних личных данных только у текущего подключения, идемпотентность точек, сохранность candles/coverage и совместимость опубликованного API до возврата трафика. | diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/proposal.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/proposal.md new file mode 100644 index 0000000..7a8f6d9 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/proposal.md @@ -0,0 +1,32 @@ +## Why + +Сейчас актуализация инвестиционных данных зависит от открытия экранов и ручного действия. Неактивный кабинет не получает новые данные, а долгосрочную динамику стоимости счёта восстановить по последнему снимку невозможно. Нужны предсказуемое часовое обновление независимо от frontend и накопление пригодных для будущего графика оценок. Согласованный объём — [issue #22](https://git.personal.ksv741.ru/WealthLedger/main/issues/22). + +## What Changes + +- Обновлять доступные счета, состав и стоимость портфелей, актуальные операции и будущие события по бумагам портфеля раз в час силами backend; первое обновление выполнять после подключения. Сохранённые данные отдавать сразу, сохраняя последнее успешное состояние при ошибке и показывая результат последней попытки, включая частичный успех. +- Накапливать оценку стоимости каждого счёта по валютам на фактическое время успешного получения полной оценки, без конвертации и подмены отсутствующей цены нулём. Сохранять историю при смене токена и отключении, скрывать её без подтверждённого доступа и продолжать при восстановлении доступа к тому же счёту. +- Убрать ручную синхронизацию с T-Bank и обновление личных данных при открытии инвестиционных экранов. Открытые экраны перечитывают сохранённые данные каждые 30 минут. Сохранить первичную загрузку отсутствующих исторических интервалов и диапазонов свечей, но не перечитывать полностью загруженную старую историю автоматически. +- Получать справочные данные инструмента и результаты поиска онлайн без отдельного долговременного хранения; сохранить подписи последнего снимка портфеля, события, котировки и свечи согласно их разным правилам актуализации. +- Вне объёма: сам график и публичное чтение его точек, валютная конвертация, многопроцессное расписание, автоматическая повторная сверка уже полных старых интервалов операций и свечей, а также изменение действий для ручных банковских карт и повторного чтения после сетевой ошибки. + +## Capabilities + +### New Capabilities + +- `specs/hourly-investment-sync/spec.md`: независимый от открытых экранов часовой обход и наблюдаемый результат последней попытки. +- `specs/portfolio-value-history/spec.md`: точная история оценки по счёту и валюте с учётом полноты и границы доступа. +- `specs/online-instrument-information/spec.md`: получение справочных сведений и поиска онлайн без сохранённой самостоятельной копии, отдельно от котировок, свечей и событий. + +### Modified Capabilities + +- `specs/portfolio-view/spec.md`: чтение последнего снимка без запуска синхронизации, часовая свежесть, отсутствие ручной синхронизации и сохранение доступных подписей позиций. +- `specs/brokerage-accounts/spec.md`: быстрое чтение сохранённых счетов и сводок, видимость частичных ошибок и периодическое перечитывание экрана. +- `specs/tbank-connection/spec.md`: первая загрузка после подключения и сохранение скрытой истории стоимости при отключении/замене токена. +- `specs/brokerage-account-upcoming-events/spec.md`: обновление прогноза по расписанию с сохранением последнего полного результата при сбое. +- `specs/frontend-data-resilience/spec.md`: разделение повтора HTTP-чтения и запрещённой ручной синхронизации при часовом обновлении. +- `specs/brokerage-account-ledger/spec.md`: первичная загрузка отсутствующего периода без повторной сверки уже полного исторического периода при открытии страницы. + +## Impact + +Пользователь быстрее открывает инвестиционные экраны и видит время и возможную неполноту последней синхронизации; открытый экран получает новые сохранённые данные без команды обновления. Затронуты инвестиционные экраны, фоновое обновление и хранение личных данных, правила доступа после смены подключения, отображение ошибок и онлайн-получение информации об инструментах. Переход требует согласовать действующие требования, которые пока предписывают короткие сроки свежести и ручные повторы. diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-ledger/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-ledger/spec.md new file mode 100644 index 0000000..437bfbb --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-ledger/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Отсутствующий исторический интервал загружается только впервые + +Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение недостающего интервала без ожидания T-Bank в локальном ответе. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы, часового обхода или удалённой команды ручной синхронизации. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным. + +#### Scenario: Первый просмотр отсутствующего прошлого периода + +- **WHEN** User открывает доступный исторический период без подтверждённой полноты +- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных + +#### Scenario: Повторное открытие полного периода + +- **WHEN** User повторно открывает уже полностью загруженный прошлый период +- **THEN** система читает сохранённые операции без повторной сверки с T-Bank + +#### Scenario: Ошибка страницы загрузки + +- **WHEN** загрузка части отсутствующего периода завершилась ошибкой +- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается как полная diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-upcoming-events/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-upcoming-events/spec.md new file mode 100644 index 0000000..5056112 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-account-upcoming-events/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Upcoming events use complete source results +Система MUST возвращать только последний полностью успешно сформированный прогноз по доступному счёту. Если необходимые данные брокера или MOEX недоступны во время часового обновления, система MUST NOT заменять последний успешный прогноз частичным списком или изменять время его успеха. Инструменты, для которых источник не предоставляет поддерживаемые события, MUST пропускаться без синтетических строк «нет данных». Получение сохранённого прогноза MUST NOT запускать синхронизацию при открытии экрана; ошибку последней попытки MUST показывать отдельно. + +#### Scenario: Недоступность источника +- **WHEN** необходимый источник данных брокера или MOEX временно недоступен при часовом обновлении +- **THEN** последний полный прогноз остаётся доступен с исходным временем, частичный список не публикуется, а ошибка отражается в статусе обхода + +#### Scenario: Инструмент без поддерживаемых данных +- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события +- **THEN** система не добавляет для него синтетическую строку в результат + +#### Scenario: MOEX отвечает без запрошенного блока +- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов) +- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой + +#### Scenario: Чтение при временном сбое обновления +- **WHEN** User открывает события доступного счёта после неудачного часового обновления и для счёта существует прежний полный прогноз +- **THEN** система возвращает прежний прогноз без ожидания источника и обозначает ошибку обновления отдельно diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-accounts/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-accounts/spec.md new file mode 100644 index 0000000..9435a80 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/brokerage-accounts/spec.md @@ -0,0 +1,84 @@ +## MODIFIED Requirements + +### Requirement: Состояния недоступности списка счетов +Система MUST читать подтверждённый доступный список счетов текущего подключения из последнего сохранённого результата и MUST NOT ожидать T-Bank или запускать проверку только из-за GET. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank уже отклонил сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection, не выдавая личных данных. Временная ошибка часового обновления MUST оставлять последний подтверждённый список доступным с исходным временем и отдельным состоянием ошибки; при ещё неизвестном списке система MUST обозначать ожидание первой загрузки, не выдавая частичный список за полный. + +#### Scenario: Подключение отсутствует +- **WHEN** User без сохранённого T-Bank connection запрашивает список +- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED` + +#### Scenario: Сохранённый token больше не действителен +- **WHEN** T-Bank отклоняет сохранённый token при часовом обновлении списка как неаутентифицированный или без прав +- **THEN** последующее чтение получает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные, но connection сохраняется + +#### Scenario: T-Bank временно недоступен +- **WHEN** часовая проверка списка завершается временной либо иной upstream-ошибкой после ранее успешной проверки +- **THEN** обычное чтение возвращает последний подтверждённый список без ожидания T-Bank, сохраняя его исходное время и отдельную ошибку обновления + +#### Scenario: Первый обход ещё не завершён +- **WHEN** User открывает список после подключения до получения первого подтверждённого списка +- **THEN** система не выдаёт частичный или неподтверждённый список за полный и сообщает о продолжающейся загрузке + +### Requirement: Экран списка брокерских счетов +Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручной синхронизации и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MAY перечитать локальный результат; пока экран открыт, он MUST перечитывать результат backend каждые 30 минут без запуска импорта. При уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового перечитывания и после его ошибки, ясно отделяя это состояние от успешного нового ответа. При успешном непустом ответе клиент 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** клиент получает успешный пустой массив +- **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** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности + +### Requirement: Клиентские состояния ошибки списка счетов +Клиент MUST NOT автоматически повторять неуспешный запрос списка вне согласованного периодического перечитывания открытого экрана. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного чтения. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового перечитывания, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. Эти действия MUST NOT инициировать синхронизацию с T-Bank. + +#### Scenario: Подключение требуется +- **WHEN** клиент получает `TBANK_NOT_CONNECTED` +- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank + +#### Scenario: Известная ошибка не повторяется автоматически +- **WHEN** запрос списка завершается известной ошибкой +- **THEN** клиент не выполняет немедленный повтор, пока User не выберет действие повторного чтения либо не наступит очередной 30-минутный срок открытого экрана + +#### 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` +- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить чтение либо открыть настройки T-Bank diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/frontend-data-resilience/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/frontend-data-resilience/spec.md new file mode 100644 index 0000000..9ea6699 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/frontend-data-resilience/spec.md @@ -0,0 +1,16 @@ +## MODIFIED 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`. Повтор после транспортной ошибки MUST повторять чтение, а не инициировать синхронизацию с T-Bank. + +#### 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-10-09-hourly-portfolio-sync-history/specs/hourly-investment-sync/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/hourly-investment-sync/spec.md new file mode 100644 index 0000000..956d049 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/hourly-investment-sync/spec.md @@ -0,0 +1,50 @@ +## Purpose + +Задаёт независимое от открытых экранов часовое обновление инвестиционных данных пользователя и наблюдаемый результат последней попытки при частичных сбоях. + +## ADDED Requirements + +### Requirement: Часовой обход личных инвестиционных данных + +Backend SHALL начинать первый обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки для подключения. При старте после простоя он MUST учитывать время последней попытки, выполнять не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Если попытки ещё не было и данных нет либо они старше часа, обход MUST начинаться при старте. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока даже после перезапуска. Перекрывающиеся обходы MUST NOT выполняться; если срок настал во время ещё идущего обхода, соответствующий запуск пропускается. + +Обход MUST проверять доступные счета и обновлять портфель каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего портфеля. Он MUST NOT повторно загружать уже полные прошлые интервалы операций, завершённые свечи или полную рыночную историю. Он MUST NOT зависеть от открытого frontend. Смена подключения MUST NOT позволять завершившейся поздно попытке старого подключения изменять личные данные или время их успеха. + +#### Scenario: Первое подключение между часовыми запусками + +- **WHEN** пользователь успешно подключает T-Bank после запуска backend +- **THEN** первый обход начинается без ожидания следующего календарного часа, а следующий допускается через час от начала первого + +#### Scenario: Рестарт после неуспешной попытки + +- **WHEN** backend перезапускается до истечения часа после неуспешного обхода с отсутствующими данными +- **THEN** обход не повторяется при рестарте, а последнее успешное состояние остаётся доступным согласно действующей границе доступа + +#### Scenario: Перекрытый часовой срок + +- **WHEN** начатый час назад обход всё ещё выполняется +- **THEN** второй обход не запускается, а пропущенная попытка не ставится в очередь сразу после завершения первой + +#### Scenario: Возобновление после простоя + +- **WHEN** backend не работал несколько часов и после старта обнаружил, что последняя попытка старше часа +- **THEN** он запускает только один текущий обход без синтетических прошлых обновлений + +### Requirement: Сохранность и статус последней попытки + +Успех одного счёта или источника MUST сохраняться независимо от ошибки другого; неполный либо неуспешный результат источника MUST NOT заменять его последнее успешное состояние или время. Backend SHALL предоставлять аутентифицированному пользователю статус только его последнего обхода: время начала, состояние (выполняется, полностью успешен, частично успешен или неуспешен) и проблемные счета и разделы без чувствительных сведений. Кабинет MUST показывать результат, время и проблемные счета/разделы при частичном успехе; отсутствие состоявшейся попытки MUST быть отличимо от успеха. Статус не предоставляет возможности начать синхронизацию с frontend. + +#### Scenario: Ошибка одного счёта + +- **WHEN** портфели двух счетов обновились, а третий не обновился +- **THEN** новые данные первых двух сохраняются, у третьего остаётся последнее успешное состояние, а статус обхода обозначает частичный успех и проблемный счёт + +#### Scenario: Ошибка событий при успешных портфелях + +- **WHEN** все портфели обновлены, но событие одной бумаги не обновилось +- **THEN** последний успешный прогноз сохраняется и статус обхода обозначает частичный успех с проблемным разделом + +#### Scenario: Пользователь другого подключения запрашивает статус + +- **WHEN** авторизованный пользователь запрашивает статус чужого обхода либо попытки старого подключения +- **THEN** система не раскрывает детали чужой или прежней границы доступа diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/online-instrument-information/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/online-instrument-information/spec.md new file mode 100644 index 0000000..d8f6657 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/online-instrument-information/spec.md @@ -0,0 +1,33 @@ +## Purpose + +Определяет получение справочных сведений и результатов поиска инструментов непосредственно от источника без самостоятельной долговременной копии, отдельно от личных данных и рыночной истории. + +## ADDED Requirements + +### Requirement: Справочные сведения и поиск запрашиваются онлайн + +При чтении карточки акции или облигации и выполнении поиска система MUST получать справочные сведения инструмента, включая доступные название, тикер, ISIN, описание и показатели компании, онлайн, а не из отдельной долговременной сохранённой копии. Повторное чтение открытой страницы MUST запрашивать эти сведения снова. Недоступность источника MUST отображаться как ошибка соответствующего справочного блока, не скрывая доступные сохранённые позиции, операции, котировки, свечи и события. Подписи в последнем успешном снимке портфеля MAY использоваться как fallback для идентификации инструмента; они MUST NOT представляться как успешно полученные сейчас подробные сведения. Для расчёта будущих событий MAY запрашиваться необходимые справочные данные без их отдельного хранения. + +#### Scenario: Повторное открытие карточки акции + +- **WHEN** пользователь повторно открывает страницу акции после прежнего успешного чтения +- **THEN** справочные сведения запрашиваются онлайн заново, а сохранённая копия карточки не выдаётся за актуальный ответ + +#### Scenario: Источник справочника недоступен + +- **WHEN** подробные сведения акции получить не удалось, но доступен последний сохранённый снимок портфеля +- **THEN** справочный блок показывает локальную ошибку, а сохранённые позиции и их доступные подписи остаются видимы без обещания актуальности подробных сведений + +### Requirement: Рыночные и событийные данные имеют отдельную политику + +Система MUST продолжать сохранять котировки и запрошенные диапазоны свечей; при открытии страницы обновление текущей котировки и незавершённой свечи MUST выполняться не чаще одного раза в час. Будущие дивиденды, купоны и другие поддерживаемые события MUST сохраняться и обновляться в составе часового обхода бумаг портфеля. Полностью загруженные прошлые диапазоны свечей MUST NOT повторно загружаться автоматически. Получение этих данных MUST NOT запускать синхронизацию личных счетов при открытии страницы. + +#### Scenario: Свежая котировка + +- **WHEN** пользователь повторно открывает страницу инструмента менее чем через час после получения котировки +- **THEN** система использует сохранённую котировку без преждевременного повторного запроса цены + +#### Scenario: Исторический диапазон свечей открыт впервые + +- **WHEN** пользователь открывает диапазон свечей, которого ещё нет локально +- **THEN** система загружает его для просмотра, не начиная синхронизацию личного портфеля diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-value-history/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-value-history/spec.md new file mode 100644 index 0000000..53a8955 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-value-history/spec.md @@ -0,0 +1,48 @@ +## Purpose + +Определяет накопление достоверных точек стоимости брокерского счёта по исходным валютам с сохранением истории между подключениями без раскрытия её вне подтверждённого доступа. + +## ADDED Requirements + +### Requirement: Полная оценка становится точкой истории + +Система MUST добавлять точки на фактическое время каждого успешно полученного полного финансового снимка отдельного Brokerage Account, включая первую загрузку; стоимость MUST храниться раздельно по валютам без конвертации, полным JSON состава или подстановки искусственных точек за часы простоя. Если хотя бы у одной ненулевой позиции текущая стоимость не определена, точки MUST NOT записываться ни для одной валюты этого обновления, хотя новый состав счёта MAY оставаться текущим снимком. Пустой счёт без известной валюты MUST NOT создавать нулевую денежную точку. Ошибка получения обязательных данных MUST NOT менять предыдущие точки или последний успешный снимок. История MUST NOT иметь заранее установленного срока удаления. + +#### Scenario: Полная оценка двух валют + +- **WHEN** успешно получен полный снимок счёта со стоимостью в RUB и USD +- **THEN** появляются отдельные точки RUB и USD с фактическим временем получения, без общего конвертированного значения + +#### Scenario: Неизвестная цена одной позиции + +- **WHEN** получен новый состав счёта с оценёнными RUB-активами и позицией без известной цены +- **THEN** состав доступен как текущий снимок с неполной оценкой, но новые точки истории не появляются ни по одной валюте + +#### Scenario: Пустой счёт без валюты + +- **WHEN** успешно полученный счёт не имеет позиций, денег и известной валюты +- **THEN** история не получает точку с придуманной валютой или суммой + +### Requirement: История переживает смену подключения без потери приватности + +История MUST продолжаться для того же User и идентификатора Brokerage Account после замены токена или повторного подключения, если доступ к счёту подтверждён текущим подключением. При отключении либо после успешно полученного списка без ранее доступного счёта история MUST оставаться сохранённой, но помечаться удалённой и скрываться от чтения; при повторном подтверждении того же счёта пометка MUST сниматься. Ошибка получения списка MUST NOT менять видимость истории. Подтверждённый отказ токена MUST скрывать личные данные до восстановления доступа; временный сбой MUST сохранять видимым последний успешный результат при действующей границе доступа. История другого пользователя MUST NOT раскрываться. + +#### Scenario: Замена токена с прежним счётом + +- **WHEN** новый проверенный токен пользователя даёт доступ к прежнему идентификатору счёта +- **THEN** линия его истории продолжается, а поздняя работа старого токена не добавляет точек + +#### Scenario: Счёт исчезает из подтверждённого списка + +- **WHEN** успешно полученный полный список доступных счетов не содержит ранее доступный счёт +- **THEN** его история сохраняется, помечается удалённой и не раскрывается до повторного подтверждения доступа + +#### Scenario: Временный сбой списка + +- **WHEN** обновление списка счетов завершилось временной ошибкой +- **THEN** пометки удалённых счетов не меняются + +#### Scenario: Отключение и повторное подключение + +- **WHEN** пользователь отключил T-Bank, а позже подключил проверенный токен с тем же счётом +- **THEN** до подтверждения доступа история скрыта, после подтверждения она снова видима и пополняется без разрыва идентичности счёта diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-view/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-view/spec.md new file mode 100644 index 0000000..e6f2c99 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/portfolio-view/spec.md @@ -0,0 +1,70 @@ +## MODIFIED Requirements + +### Requirement: Свежесть и fallback полного Portfolio Snapshot +Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST читать сохранённые данные без ожидания T-Bank и MUST NOT запускать повторное обновление личного портфеля, даже если снимок отсутствует или старше часа; актуализацией владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обновления MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность либо свежесть финансового снимка. Сохранённые подписи позиций MAY оставаться доступны при недоступности онлайн-справочника. + +Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться в виде долговременного справочника; ошибка lookup не подтверждает отсутствие инструмента. + +#### Scenario: Свежий Snapshot +- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения +- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank + +#### Scenario: Медленный lookup инструмента +- **WHEN** сведения об инструменте ещё запрашиваются онлайн +- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую в Snapshot доступную identity; подробные сведения остаются отдельным состоянием + +#### Scenario: Fallback после временной upstream-ошибки +- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot +- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt` + +#### Scenario: Недействительный token не маскируется fallback-данными +- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio +- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` + +#### Scenario: Неполная оценка при новом составе +- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены +- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции, без подстановки нуля + +### Requirement: Состояния и ошибки Portfolio на клиенте + +Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry или refetch при возврате фокуса. Открытый инвестиционный экран MUST перечитывать сохранённый результат каждые 30 минут без запуска синхронизации с T-Bank. Наблюдение известной активной работы MAY обновлять отображаемые ресурсы; чтение статуса и данных MUST NOT запускать импорт. Status сообщает состояние и ошибку без hash notifications и численных progress counters. После ухода со страницы наблюдение MUST прекращаться. + +Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться. Новый диапазон свечей загружается при его открытии; initial loading допустим. При загрузке без ранее полученного успешного ответа для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус. + +Истечение срока свежести MUST NOT само по себе скрывать уже доступный Client Cached Snapshot. Во время фонового обновления и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, включая валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST показывать компактную неблокирующую индикацию без замены данных skeleton. При ошибке перечитывания он MUST показывать локализованное неблокирующее предупреждение о последних полученных данных и предоставлять действие повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. Клиент MUST NOT предоставлять действие ручной синхронизации портфеля с T-Bank. + +Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`, без нулевых валютных карточек. Для показываемого состава Portfolio клиент MUST сохранять время успешного получения снимка `lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST показывать Snapshot, его исходное время и нейтральное предупреждение об устаревших данных. Это предупреждение само по себе MUST NOT добавлять действие ручного обновления. Server-side stale и ошибка перечитывания Client Cached Snapshot MUST оставаться раздельными состояниями: ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения и действие повтора чтения MUST оставаться доступными при одновременном наличии этих состояний. + +При отсутствии ранее успешно полученного Client Cached Snapshot этого же Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора чтения разрешено только для временной и неизвестной ошибки. + +#### Scenario: Показ устаревшего Portfolio +- **WHEN** клиент получает успешный ответ с `isStale: true` +- **THEN** клиент показывает позиции, остатки, валютные итоги, время последнего успешного снимка и нейтральное предупреждение; само это предупреждение не добавляет действие ручного обновления + +#### Scenario: Недоступный счёт на клиенте +- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный Client Cached Snapshot и клиент получает `404` +- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора + +#### Scenario: Первичная загрузка Portfolio +- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа +- **THEN** он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders + +#### Scenario: Обновление после истечения срока свежести +- **WHEN** срок свежести показанного Portfolio истёк и начался часовой обход данных того же Brokerage Account +- **THEN** клиент сохраняет прежние валютные итоги и время их успешного получения, показывает неблокирующую индикацию обновления и не заменяет доступный состав и сводку skeleton-состоянием + +#### Scenario: Ошибка обновления после успешного ответа +- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а повторное HTTP-чтение этих же данных завершается ошибкой +- **THEN** клиент сохраняет данные, суммы и время прежнего снимка, показывает неблокирующее предупреждение и действие повтора чтения; ошибка не делает сохранённый снимок свежим и не изменяет `isStale` + +#### Scenario: Ошибка клиентского обновления при server-side stale +- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание этого же Portfolio завершается ошибкой +- **THEN** прежние суммы и время остаются видимыми одновременно с нейтральным server-side предупреждением, отдельным предупреждением об ошибке клиентского перечитывания и его действием повтора чтения + +#### Scenario: Повторное обновление успешно +- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot +- **THEN** до ответа прежние суммы и время остаются видимыми, затем клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке клиентского чтения и показывает server-side предупреждение только в соответствии с новым `isStale` + +#### Scenario: Штатный пустой Portfolio на клиенте +- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances` +- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/tbank-connection/spec.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/tbank-connection/spec.md new file mode 100644 index 0000000..27a7888 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/specs/tbank-connection/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Создание и замена подключения T-Bank +Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первая фоновая загрузка доступных личных данных без команды frontend. История стоимости прежних счетов MUST сохраняться, но показываться только для счетов, доступ к которым подтверждён новым подключением. + +#### Scenario: Успешное создание подключения без счетов +- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов +- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: }`; первая загрузка не создаёт фиктивные счета + +#### Scenario: Успешная замена подключения +- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет +- **THEN** система заменяет token подключения, обновляет `connectedAt`, отвечает текущим статусом без token и начинает обновление подтверждённых счетов, не теряя их прежние исторические точки + +#### Scenario: Невалидный token не заменяет существующее подключение +- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав +- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений + +#### Scenario: T-Bank временно недоступен при проверке token +- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки +- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение + +### Requirement: Удаление подключения T-Bank +Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные текущие данные T-Bank. История стоимости счёта MUST сохраняться с пометкой удаления и MUST NOT раскрываться до подтверждения нового доступа к тому же счёту. + +#### Scenario: Удаление существующего подключения +- **WHEN** User удаляет существующее подключение +- **THEN** система удаляет token и связанные кэшированные текущие данные User, скрывает сохранённую историю стоимости и отвечает `204` + +#### Scenario: Повторное удаление подключения +- **WHEN** User удаляет уже отсутствующее подключение +- **THEN** система отвечает `204`, не раскрывая сохранённую историю diff --git a/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/tasks.md b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/tasks.md new file mode 100644 index 0000000..d745385 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-hourly-portfolio-sync-history/tasks.md @@ -0,0 +1,68 @@ +## 1. Данные и граница доступа + +- [x] 1.1 [backend] Добавить совместимое хранение точек оценки и видимости истории отдельно от revision; сохранять по валютам только полную оценку в той же транзакции, что и успешно заявленный снимок, с дедупликацией попытки. + - **Covers:** `portfolio-value-history` — `Полная оценка становится точкой истории`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot` + - **Verification:** Изолированная SQLite с применёнными прежними migrations: новая миграция сохраняет старые данные; повторная/поздняя попытка не создаёт лишних точек, неизвестная цена не записывает ни одной валюты, неуспешная публикация сохраняет снимок и историю. +- [x] 1.2 [backend] Связать видимость истории с успешными PUT/DELETE подключения и публикацией полного membership; закрыть доступ старой revision и оставить историю для повторно подтверждённого accountId. + - **Covers:** `portfolio-value-history` — `История переживает смену подключения без потери приватности`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank` + - **Verification:** SQLite/HTTP проверки смены токена, отключения, отсутствующего и вернувшегося счёта, ошибки membership, чужого User и поздней попытки; прежние текущие datasets не раскрываются и история не исчезает. + +## 2. Часовой обход и локальные чтения + +- [x] 2.1 [backend] Применить выбранный в Design Solution Assessment: один native timer и durable conditional reservation для запуска и восстановления часовой попытки без параллельного обхода. + - **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных` + - **Verification:** SQLite/integration сценарии первого подключения, рестарта до и после 60 минут, пропущенных часов, перекрытого тика, замены revision и перезапуска незавершённой работы; ни один сценарий не создаёт конкурирующих попыток. +- [x] 2.2 [backend] Оркестрировать подтверждённый membership, портфели счетов, изменяемый хвост операций и события через существующих владельцев источников; агрегировать результат последней попытки и открыть только read-only статус авторизованному User. + - **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Сохранность и статус последней попытки`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results` + - **Verification:** Integration/HTTP проверки: ошибка одного счёта или события даёт частичный статус с безопасным разделом, успешные источники публикуются, старый прогноз и время переживают сбой; чужой User/старая revision не видны, OpenAPI contract parity проходит. +- [x] 2.3 [backend] Перевести обычные GET счетов, портфеля и прогноза на локальное чтение без запуска обновления личных источников; согласовать часовую свежесть, первый незавершённый обход и отдельно отображаемый сбой. + - **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results` + - **Verification:** HTTP/integration проверки локального ответа без RPC при missing/stale, неизвестного membership, временного сбоя, известного invalid token, неполной оценки и доступности последнего успешного снимка. +- [x] 2.4 [backend] Оставить для журнала запуск только первичной загрузки отсутствующих интервалов; обновление изменяемого хвоста перенести в часовой обход, не перечитывая полные прошлые интервалы. + - **Covers:** `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных` + - **Verification:** SQLite/integration проверка пустого полного периода, неполной страницы, повторного открытия прошлого месяца, движения конца текущего периода и неповторного чтения сохранённой истории. + +## 3. Инструменты и граница публичного API + +- [x] 3.1 [backend] Развязать сохранённые котировки, свечи и события от справочных payload; обновлять текущую котировку и изменяемую свечу не чаще часа, сохраняя покрытия завершённых диапазонов и полные прогнозы. + - **Covers:** `online-instrument-information` — `Рыночные и событийные данные имеют отдельную политику`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results` + - **Verification:** Integration с изолированной SQLite: первая загрузка диапазона, отсутствие повторной загрузки полного прошлого диапазона, запрос свежей котировки, неудача онлайн-справочника и сохранность forecast/coverage/candles; миграция не удаляет связанные рыночные строки. +- [x] 3.2 [backend] Перевести detail, company, поиск и lookup инструментов на онлайн-ответ через существующие integration clients; отделить их ошибку от сохранённых подписей и финансовых ресурсов без самостоятельной долговременной копии. + - **Covers:** `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot` + - **Verification:** HTTP/service проверки повторного онлайн-запроса, совпадения UID и ticker, пустого/неоднозначного результата и upstream-сбоя при видимом сохранённом портфеле; не создаются новые долговременные detail/search/negative lookup. +- [x] 3.3 [backend] Сформировать публичный OpenAPI со статусом обхода, сохранив переходный контракт screen POST до готовности frontend. + - **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте` + - **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite для действующего и переходного wire contract; обычный GET не инициирует screen POST. +- [x] 3.4 [backend] Проверить рост SQLite на представительном объёме и фактическую частоту upstream-вызовов; сопоставить её с действующей первичной документацией T-Bank перед настройкой частоты, выполнить штатные backend gates и durability для новой схемы. + - **Covers:** `N/A` — операционная проверка выбранного в Design хранения и scheduler, без нового пользовательского поведения. + - **Verification:** Выполнено: SQLite 617,263,104 bytes на synthetic profile (3 accounts, 300,000 operations, 200 instruments, 1,000,000 candles), persisted-read phase сделал 0 upstream calls; `npx prisma validate`, durability с pre-history baseline, `npm run test:e2e` и performance budget проходят. Ограничение `TBANK_RATE_LIMIT_PER_SECOND` (default 5 per backend instance) зафиксировано в `docs/adr/020-hourly-sync-verification-evidence.md`; production quota не расширяется. + +## 4. Клиентские экраны + +- [x] 4.1 [frontend] Получить опубликованный backend OpenAPI artifact и обновить локальный snapshot/generated types для статуса без импорта backend internals. + - **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки` + - **Verification:** `npm run openapi:generate` и `npm run openapi:check` из frontend на предоставленном artifact; статус типизирован, отсутствие попытки и изоляция ошибки не заменены вымышленным успехом. +- [x] 4.2 [frontend] Удалить инвестиционные кнопки/POST ручной синхронизации и source-recovery triggers; сохранить транспортный retry только как повтор чтения, а открытые инвестиционные экраны перечитывать раз в 30 минут. + - **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio` + - **Verification:** UI/query проверки 30-минутного активного экрана, прекращения опроса после ухода, сохранения снимка при сбое и повторного HTTP GET без POST; auth-refresh и действия ручных карт не меняются. +- [x] 4.3 [frontend] Показать в кабинете время/исход последнего обхода и названия проблемных счетов и разделов; справочный блок карточки отделить от позиций, событий, свечей и локальной ошибки чтения. + - **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `brokerage-accounts` — `Экран списка брокерских счетов` + - **Verification:** Проверки состояний `none/running/succeeded/partial/failed`, локального сбоя detail при видимых финансовых данных, доступных текстовых подписей и переноса статуса на узком экране согласно inline UI package Design revision 1. +- [x] 4.4 [frontend] Завершить клиентский переход штатными gates и сверкой критического маршрута нового подключения → первый обход → статус → сохранённый портфель на открытом экране. + - **Covers:** `N/A` — интеграционная проверка принятого UI package и публичного контракта после изменения нескольких экранов, без отдельного нового требования. + - **Verification:** `npm run verify` в frontend и наблюдаемые состояния критического маршрута с предоставленным backend OpenAPI. + +## 5. Завершение перехода + +- [x] 5.1 [backend] После перехода frontend убрать неиспользуемый screen POST и наблюдение прежней работы без затрагивания нового статуса обхода. + - **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте` + - **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite: старый trigger недоступен, обычный GET и повтор чтения не запускают импорт. + +## 6. Нормативная и as-built документация + +- [x] 6.1 [specs] Проверить полное соответствие девяти Delta Specs одобренному Proposal и Design, сохранить точные имена Requirements и валидировать change и основные specs. + - **Covers:** `N/A` — самостоятельная проверка нормативного артефакта и согласованной границы изменения, без добавления нового поведения. + - **Verification:** `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate hourly-portfolio-sync-history --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, `npm run verify` в specs. +- [x] 6.2 [docs] После проверки реализованного поведения дополнить глоссарий и объяснение свежести/истории, обновить соответствующие страницы инвестиций и зафиксировать новый ADR, который связывает изменённое решение с ADR 017, не переписывая исторический контекст. + - **Covers:** `N/A` — as-built объяснение подтверждённых решений Design в отдельном docs owner, не параллельная норма требований. + - **Verification:** Проверить маршруты чтения, локальные ссылки и смысл финансовых значений; `npm run verify` из docs. diff --git a/openspec/changes/persist-tbank-data/.openspec.yaml b/openspec/changes/archive/2026-10-09-persist-tbank-data/.openspec.yaml similarity index 100% rename from openspec/changes/persist-tbank-data/.openspec.yaml rename to openspec/changes/archive/2026-10-09-persist-tbank-data/.openspec.yaml diff --git a/openspec/changes/persist-tbank-data/design.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/design.md similarity index 100% rename from openspec/changes/persist-tbank-data/design.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/design.md diff --git a/openspec/changes/persist-tbank-data/proposal.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/proposal.md similarity index 100% rename from openspec/changes/persist-tbank-data/proposal.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/proposal.md diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-income-analytics/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/brokerage-account-income-analytics/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-income-analytics/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-ledger/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/brokerage-account-ledger/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-ledger/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-account-upcoming-events/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-accounts/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/brokerage-accounts/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/brokerage-accounts/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/frontend-data-resilience/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/frontend-data-resilience/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/frontend-data-resilience/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/portfolio-view/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/portfolio-view/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/portfolio-view/spec.md diff --git a/openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/specs/tbank-data-sync/spec.md similarity index 100% rename from openspec/changes/persist-tbank-data/specs/tbank-data-sync/spec.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/specs/tbank-data-sync/spec.md diff --git a/openspec/changes/persist-tbank-data/tasks.md b/openspec/changes/archive/2026-10-09-persist-tbank-data/tasks.md similarity index 100% rename from openspec/changes/persist-tbank-data/tasks.md rename to openspec/changes/archive/2026-10-09-persist-tbank-data/tasks.md diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/.openspec.yaml b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/.openspec.yaml new file mode 100644 index 0000000..515ccd5 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd-v2 +created: 2026-10-09 diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/completion-evidence.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/completion-evidence.md new file mode 100644 index 0000000..f54e899 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/completion-evidence.md @@ -0,0 +1,57 @@ +# Completion Evidence: reconcile-hourly-investment-sync + +## Outcome + +The approved reconciliation was implemented across backend, frontend and docs. +The hourly walk is the normal personal-data update owner; ordinary resource GETs +and retries remain local/read-only, with the approved first-load exceptions. +Resource metadata now distinguishes confirmed data state from hourly attempt state +and browser transport state. + +## Delivery Map + +| Area | Result | Commit | +| --- | --- | --- | +| backend | Resource envelope, hourly attempt projections, saved forecast window semantics, read-only events GET, candle read boundary and generated OpenAPI | `aee330c` | +| frontend | Generated contract consumer, Accounts/Portfolio/operations/events status presentation, HTTP-only retry, accessible status text and snapshot retention | `6b1b26a` | +| docs | Six approved AS-IS documentation and ADR updates | `a45d3e7` | +| specs | Approved v2 planning package; native sync/archive remains finalization work | pending archive commit | +| main | N/A: no workspace/config/Compose orchestration change | N/A | +| qa | N/A: separate QA repository does not exist | N/A | + +## Verification + +| Repository | Verification | Result | +| --- | --- | --- | +| backend | `npm run typecheck:app` | PASS | +| backend | `npm run typecheck:test` | PASS | +| backend | `npm test -- --runInBand` | PASS: 37 suites, 229 tests | +| backend | `npm run test:sqlite -- upcoming-events.e2e-spec.ts` | PASS: 22/22 | +| backend | `npm run openapi:generate` | PASS | +| backend | `npm run test:openapi-contract` | PASS: 3 tests | +| frontend | `npm run verify` | PASS: 65 files, 436 tests, typecheck, lint, OpenAPI, dependency gates, app and Storybook builds | +| docs | `npm run verify` | PASS | +| specs | `openspec validate reconcile-hourly-investment-sync --type change --strict --store wealth-ledger-specs` | PASS | +| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 26 specs, 0 failed | +| all | `git diff --check` | PASS | + +The backend repository-wide lint command reports existing unused ESLint +suppressions. Changed-file lint with `--pass-on-unpruned-suppressions` passed; +all application/test/type/OpenAPI gates above passed. This repository hygiene +issue is not introduced by the reconciliation implementation. + +## Independent Validation + +Independent review found no remaining Critical or Important behavioral findings +after fixing ordinary events GET locality, saved forecast window/calculatedAt, +candle reread behavior and Accounts metadata presentation. The reviewed scope +covered approved Proposal, ten Delta capabilities, System Analysis, Design, QA +Analysis, Docs Impact, Tasks, current backend/frontend/docs diffs and generated +OpenAPI. + +## Approval Binding + +Completion uses the current approved planning artifacts in this change directory. +Finalization must re-run native sync/archive checks against the current files and +must not treat artifact existence as approval. Legacy active changes remain on +their legacy schema and are not implicitly migrated by this archive. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/design.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/design.md new file mode 100644 index 0000000..7fd427d --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/design.md @@ -0,0 +1,358 @@ +# Engineering Design + +## Context and Constraints + +The deployed backend is a single NestJS process with SQLite/Prisma. Its durable +hourly walk, connection revision publication fence, saved investment resources, +and frontend 30-minute rereads already provide most of the approved model. +The remaining mismatch is at the resource boundary: backend +`InvestmentResourceMetaDto` and `investmentResourceMeta` expose legacy +`missing|partial|complete` availability and `idle|running|failed` work state, +while the frontend projects that legacy metadata through `useResourceQuery` and +uses `sync=observe` as a transport mode. The generated backend `openapi.json` +and frontend snapshot agree with each other, but they describe this AS-IS +contract rather than the approved target. + +The backend-generated OpenAPI remains the executable contract. The change-local +`system-analysis/contracts/investment-resource-envelope.schema.json` is a +semantic target and must not become a second OpenAPI source, generated client, +or runtime validator. Route identities, authentication/access boundaries, +Problem Details codes, and resource payload DTOs remain established unless an +approved target requirement explicitly changes them. + +No normal personal resource GET may wait for or call T-Bank. The only +acquisition exceptions remain the first unconfirmed ledger interval and first +unconfirmed candle range. The design preserves the single-process boundary; it +does not add distributed scheduling, a job service, polling endpoint, manual +sync command, public value-history read, or a new UI package dependency. + +## Technical Goals / Non-Goals + +**Goals** + +- Publish one generated HTTP envelope for Accounts, Portfolio, operations, and + events: `{ data, meta }`, with Accounts-only top-level `sync`. +- Make resource-result state and hourly-attempt state independently truthful: + confirmed data stays available after a failed/partial attempt, while an + initial result remains `null` until complete. +- Preserve local-read, connection-revision, and first-load invariants while + migrating Swagger DTOs, mapper inputs, runtime responses, and frontend + generated types together. +- Keep the existing successful TanStack Query snapshot during read retries and + retain the existing 30-minute open-screen reread cadence as a local HTTP + reread, not an import trigger. + +**Non-Goals** + +- Rebuild source owners, hourly scheduling, portfolio-value storage, or the + legacy status endpoint beyond removing their use as a source-work command + from these screens. +- Introduce a compatibility envelope, client-side DTO normalizer, duplicate + cache, new route/version, manual refresh/import action, or a copy of OpenAPI + in specs. +- Plan documentation edits, implementation tasks, or product behavior beyond + the approved proposal, delta specs, and System Analysis. + +## Implementation Impact Map + +### frontend + +`affected` - generated transport types, `shared/api/resource-query.ts`, the +existing brokerage-account/portfolio/operations/events query owners, and their +page VMs/views consume the target metadata. `useScreenSync` remains the common +30-minute open-screen reread owner, but its source-error derivation must read +`meta.lastAttempt` rather than legacy `meta.sync`; retry stays HTTP-only. + +### backend + +`affected` - investment resource DTOs, mapper, endpoint response DTOs, +resource readers, connection/hourly-attempt projections, Swagger annotations, +and generated `openapi.json` implement the semantic envelope. Existing durable +attempt, revision, membership, snapshot, forecast, coverage, and history +owners remain the data owners. + +### specs + +`affected` - this Design makes the approved deltas and System Analysis +implementable without duplicating the executable contract. + +### docs + +`affected` - durable AS-IS references are limited to ADR 019, ADR 017, +`architecture/cross-cutting/data-freshness.md`, +`domains/portfolio/flows/portfolio-loading.md`, and +`domains/income/upcoming-events.md`. Their impact is recorded here only; +documentation sequencing is not designed in this artifact. + +### qa + +`N/A` - no separate QA/E2E repository exists. + +## Decisions + +### Backend: one resource-meta projection and one connection-walk projection + +Replace the legacy shared metadata vocabulary at the existing +`InvestmentResourceMetaDto`/`investmentResourceMeta` seam. Every affected +resource envelope has required `meta.freshness`, `meta.completeness`, +`meta.lastSuccessfulAt`, `meta.lastAttempt`, and `meta.availability`. +`lastAttempt` is a resource-scoped projection with state +`none|running|succeeded|partial|failed`, nullable start/end times, and only +safe failed section/account/error-code entries. `none` is a concrete status, +not an omitted field or success. + +The mapper derives freshness and successful time only from the returned +confirmed resource result. A later running, partial, or failed walk neither +changes that time nor makes data fresh. `data: null` pairs with `unknown`/ +incomplete and either `initial-loading`, `temporarily-unavailable`, or +`connection-unavailable`; a confirmed empty collection pairs with available +and complete. The ledger's permitted first-load rows retain incomplete +coverage and are never represented as a confirmed complete result. + +Accounts additionally projects the connection-wide walk as `sync: null` when +no attempt exists, otherwise the same complete safe attempt shape. Other +resource envelopes do not gain top-level `sync`. This preserves the single +shared metadata owner while avoiding the current ambiguity between per-resource +`meta.sync` and Accounts-level walk status. + +The existing `resourceKey`, `dataRevision`, `coverage`, and dependency details +remain internal projection inputs or endpoint-specific metadata only where +their approved behavior still requires publication; they must not substitute +for any of the five required target meanings. No client must infer target state +from their presence. + +### Backend: retain storage owners and publication fences + +The durable hourly attempt remains the sole normal publisher. Resource readers +read the saved current connection revision and project its persisted result; +they do not reserve work or contact upstream. Each successful section can +persist independently, preserving the previous confirmed resource result and +its success time when a later section fails. Forecast publication remains +complete-source-only, and exact per-currency value-history writes remain in the +same successful Portfolio publication transaction. + +Connection PUT establishes a new revision before its first walk; DELETE and +replacement prevent old personal snapshots and old attempt status from being +read or published. Invalid-token knowledge blocks disclosure rather than being +masked by saved data. Data and storage changes must be additive when a new +persisted distinction is necessary; existing snapshots, coverage, forecasts, +attempts, revisions, and retained value history are not reset or reclassified +by a destructive migration. + +### Contract: Swagger is produced at the DTO boundary + +Swagger DTO annotations and endpoint response classes are the only place the +wire schema is authored. The backend regenerates `openapi.json` from the +implemented DTOs, then the frontend receives that published snapshot and runs +its existing `openapi-typescript` generation. Hand-editing frontend +`schema.d.ts`, adding handwritten parallel interfaces, or retaining a +compatibility mapper would hide drift and is excluded. + +The resulting generated contract must express nullable `data`, all required +`meta` fields, nullable attempt timestamps, nullable problem fields, Accounts +top-level nullable `sync`, and absence of top-level `sync` on the other +resource responses. It must retain `sync=observe` only as a read-observation +compatibility parameter while it exists in the generated route contract; it +cannot cause source work and is not used to represent the new hourly state. +The generated contract reconciles field spelling with the target schema before +release, not through a second contract artifact. + +### Frontend: one query cache and metadata-aware screen state + +`useResourceQuery` remains the sole adapter that projects resource envelopes +into `data`, `meta`, and Accounts `sync`, preserving its existing query key, +authenticated transport, access-group invalidation, and Client Cached Snapshot +behavior. It must project the generated target types directly. Resource query +owners remain at their existing entity slices; page VMs, not presentation +views, choose initial loading, unavailable/error, empty, and loaded states. + +`useScreenSync` remains the single 30-minute timer for open investment +screens. It calls ordinary local `refetch()` and its retry path continues to +use the existing observation read without changing cache identity. It derives +server attempt warning state from `meta.lastAttempt`, keeps transport failures +separate, and never starts personal synchronization. Existing Accounts +30-minute `staleTime` remains aligned with this cadence; Portfolio and other +screen resources keep their current short cache freshness where already +established while the open-screen timer supplies the approved reread rhythm. + +All affected account screens render the five metadata meanings in text or an +element with an accessible name, not color alone. A complete empty resource is +normal empty content. `data: null` renders initial-loading or unavailable +states without invented values. A failed/partial hourly attempt is an +independent nonblocking warning beside confirmed data and its original time. +`Portfolio.isStale` remains snapshot-specific and is shown separately from +hourly-attempt and client HTTP-error warnings. A client snapshot stays visible +during and after a failed background HTTP reread; connection replacement or +deletion still invalidates personal queries. + +## Cross-cutting Implementation + +The canonical state split is: + +| Concern | Owner | Observable representation | +| --- | --- | --- | +| Returned confirmed resource result | Saved resource owner | `data`, freshness, completeness, `lastSuccessfulAt`, availability | +| Resource-relevant hourly outcome | Durable walk and resource projection | `meta.lastAttempt` | +| Connection-wide hourly outcome | Durable walk | Accounts `sync` only | +| In-flight/failed browser reread | TanStack Query | Existing query lifecycle and local retry warning | +| Snapshot-specific Portfolio source age | Portfolio owner | `Portfolio.isStale` | + +This split prevents a source failure from becoming a synchronous GET failure, +prevents the UI from treating its cache as server fallback, and preserves safe +access invalidation. Safe attempt problems contain only the approved section, +optional account ID, and safe error code: no token, upstream payload, cursor, +connection revision, or raw exception text is serialized or logged as status +metadata. + +Events retains its existing account and `all` scopes and its saved forecast +window/filter semantics. Its metadata independently reports the latest events +attempt, so a saved forecast can remain available when a later refresh fails. +Operations retains its coverage semantics; only its explicitly approved missing +historical first-load acquisition may work outside the hourly walk. + +## Data / Storage Changes + +No new storage architecture is needed. Existing durable tables/records for the +current connection revision, hourly attempts, snapshots, operation coverage, +forecast source windows, and retained portfolio-value points are authoritative. +If implementation needs persisted terminal-state or problem-detail values not +already durable, it extends the existing attempt record/schema additively and +backfills conservatively: absent historical attempt information is projected as +`none`, never as `succeeded`. + +Migrations must preserve all current user, connection, personal-resource, +coverage, forecast, and history rows. They must not reset SQLite, rewrite an +old partial result as complete, expose retained history across a new connection +boundary, or delete old data merely to fit the new vocabulary. Compatibility is +achieved by deploying the backend DTO/runtime and its generated OpenAPI as one +server contract, followed by the frontend snapshot/types and rendering change; +there is no claim that an old frontend understands the target envelope. + +## Contract Implementation + +The implementation contract is the generated result of the following mapping: + +| Target concept | Backend source | HTTP location | +| --- | --- | --- | +| Last confirmed result or no complete result | Existing local resource reader | `data` | +| Result freshness and completeness | Resource mapper from confirmed result/coverage | `meta.freshness`, `meta.completeness` | +| Original confirmed-result time | Saved source timestamp | `meta.lastSuccessfulAt` | +| Relevant hourly result | Durable attempt plus resource section projection | `meta.lastAttempt` | +| Whether complete data can be used | Connection/access/read result | `meta.availability` | +| Complete current-connection walk | Durable connection-wide attempt | Accounts `sync` | + +The target schema's `problemSection` shape is used for both scopes. For a +connection-wide Accounts walk, its list may include all affected sections; for +a resource-level attempt, it is restricted to failures relevant to that +resource. A running attempt has `finishedAt: null`; terminal attempts have a +completion time; `none` has null timestamps and no problems. These invariants +are validated at runtime tests and in the generated OpenAPI, rather than by +shipping the JSON Schema to application consumers. + +## Developer Verification Strategy + +Backend verification proves the contract at DTO, mapper, controller, and +SQLite boundaries: successful empty data; first load with `data: null`; stale +fallback after a failed walk; partial walk with preserved data; resource versus +Accounts attempt scoping; events forecast-window filtering; invalid-token and +connection-revision disclosure fences; no upstream personal call on ordinary +GET; and both first-load exceptions staying narrow. It regenerates OpenAPI and +runs the existing OpenAPI contract, type, lint, and relevant E2E suites. + +Frontend verification updates the supplied snapshot, runs `openapi:check`, and +uses MSW/page-VM tests to prove generated metadata projection, no automatic +retry, HTTP-only retry, 30-minute open-screen reread, cache retention after a +background error, connection invalidation, and distinct server stale/hourly +attempt/client transport states. UI checks cover keyboard access, accessible +status text, loading/error/empty/loaded states, and narrow viewport wrapping. + +Before release, verify the backend-generated OpenAPI and frontend snapshot are +the same published artifact, then run each repository's configured verification +commands. In this specs store, run both v2 schema validation and strict change +validation; structural success is supplemented by a semantic comparison against +the approved deltas and System Analysis. + +## UI/UX Package + +**Applicability and scope:** `new/changed`. This is an inline package for the +existing Accounts, account Portfolio, operations, and events screens. It adds +the approved status vocabulary and preserves 30-minute local rereads; it does +not introduce new composition, graphics, navigation, or controls. + +**Package revision:** `1` (this Design). No visual files, external references, + +**Screens, states and interactions:** + +| Screen/state | Presentation | Interaction | +| --- | --- | --- | +| No complete result, initial attempt running | Existing structural loading state, labelled as loading | No manual sync action | +| No complete result, attempt failed/unavailable | Safe unavailable/error state with target status text | Retry rereads HTTP only when recovery allows | +| Complete empty result | Normal domain empty state and target status text | Existing navigation/actions only | +| Complete data with running/succeeded/partial/failed attempt | Content and original success time remain visible; attempt appears as separate accessible status | No source-work action | +| Client Cached Snapshot during/after HTTP failure | Existing content remains visible; transport warning remains distinct from server state | Existing retry rereads HTTP only | + +**Responsive behavior:** reuse existing Mantine/theme components and page +layouts. Status text wraps without hiding account content or money values on +narrow screens; color supplements an accessible label. Existing skeletons +remain noninteractive and unfocusable. Existing cards, buttons, and icons are +used through their public package exports; no wrapper, SVG asset, token, or +component package revision is introduced. + +**Reused source and differences:** `N/A` for approved visual reuse. This is a +new inline status package revision `1`, not an immutable reuse of a prior +approved visual package. Existing cards, layouts, hooks, and UI packages are +implementation inputs only; they are not the approved visual source. The +package adds semantic presentation of the five target metadata fields and +separate hourly-attempt/client-transport warnings. No local visual export is +required. + +## Risks / Trade-offs + +- Envelope evolution is a coordinated backend/frontend release risk. Generated + artifacts prevent hidden type drift but cannot make an older client interpret + new enum meanings. +- Mapping resource relevance from connection-wide attempts can over-report or + under-report a failure if it is not constrained by resource/section scope; + mapper tests must cover each affected resource. +- A durable `none` projection for old rows is intentionally less informative + than inventing a historic success; it preserves truthfulness. +- The 30-minute client timer can issue local reads while an hourly walk runs; + this is acceptable because reads do not contend for upstream work and retain + the last confirmed result. +- Single-process scheduling remains unsuitable for multi-process deployment; + adding leases, a worker, or distributed coordination requires a separate + decision. +- Documentation currently contains legacy request-driven/manual-refresh + descriptions. Until the identified durable references are updated, they must + not be used as implementation authority over the approved behavior. + +## Migration / Rollout / Rollback + +Rollout is backend-first at the contract source: apply only additive persistence +changes if required, implement and test runtime projection/fences, regenerate +and publish backend OpenAPI, then update the frontend snapshot/generated types +and renderers. Do not enable the new projection alongside a second personal +write owner. A frontend built from the old snapshot is not the compatibility +target; rollout uses the matched generated artifact pair. + +| Field | Decision | +| --- | --- | +| Status | `required` | +| Trigger | Personal-data disclosure across connection revisions, false complete/fresh state, malformed generated contract, failure to preserve confirmed fallback, or a frontend unable to read the published contract. | +| Actions | Stop the affected rollout, preserve a SQLite copy, return to the last compatible backend/frontend pair, and fix forward. Do not restore screen-triggered/manual synchronization while the hourly owner remains enabled. | +| Data safety | Never reset production SQLite, delete additive attempt metadata, delete snapshots/coverage/forecasts/value history, or use a destructive downgrade. Old rows without target details safely project `none`. | +| Verification | On a representative copy, verify migration integrity, revision/access fences, ordinary local GET behavior, preserved fallback timestamps, generated OpenAPI/snapshot identity, and frontend rendering before traffic resumes. | + +## Solution Assessment + +| Option | Assessment | +| --- | --- | +| Evolve the existing shared DTO/mapper and generated OpenAPI | Chosen. It is the narrowest path: one resource-meta projection already owns all affected envelope metadata, Swagger already produces the contract, and frontend types already derive from the supplied snapshot. | +| Add a target adapter/parallel endpoint or versioned envelope | Rejected. It duplicates mapping and cache paths, delays removal of ambiguous states, and creates an unnecessary second compatibility surface. | +| Handwrite frontend target types or runtime validation | Rejected. Generated `paths`/schemas already own transport typing; manual copies would drift and a validator adds no trust boundary. | +| Add a scheduler/queue/cache service to express status | Rejected. Durable single-process attempts and SQLite records already express the required state. A new service cannot improve this contract without changing the explicitly retained deployment model. | +| Add a new design-system package/component family | Rejected. Existing Mantine and `@wealthledger/ui` states cover the textual, accessible status presentation; a new package would be abstraction without a new visual primitive. | + +The chosen solution is intentionally a contract-and-projection reconciliation, +not a new synchronization system. It minimizes code and migration surface while +making the approved observable model expressible through the existing owners. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/docs-impact.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/docs-impact.md new file mode 100644 index 0000000..9f456be --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/docs-impact.md @@ -0,0 +1,50 @@ +# Documentation Impact + +## Summary + +Durable AS-IS documentation requires updates. The current pages still describe +request-driven freshness, screen-started work, and legacy `sync` metadata; +after delivery they must describe the hourly owner, local resource reads, and +the independent resource-result and hourly-attempt state. + +## Documentation Changes + +| Action | Target | Category | Resulting AS-IS | Source | Verification | +|---|---|---|---|---|---| +| update | `architecture/cross-cutting/data-freshness.md` | cross-cutting | Ordinary personal-resource GETs and HTTP retries read saved local results and never start personal T-Bank work. The hourly walk is the normal update owner; only an unconfirmed historical ledger interval and an unopened candle range may acquire their own missing result. Document the independent `data` freshness, completeness, `lastSuccessfulAt`, availability, and resource-relevant last hourly attempt; retained complete data remains available after a later failed or partial attempt, while no complete result is `data: null`. Keep Client Cached Snapshot, `Portfolio.isStale`, and failed browser rereads distinct from server result and hourly-attempt state. | `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics; Design § Cross-cutting Implementation. | Compare the page with delivered resource responses and UI: normal GET/retry has no upstream personal call; documented state meanings match generated OpenAPI/runtime responses; stale fallback, no-result state, client read failure, and hourly failure remain separately explained. | +| update | `domains/portfolio/flows/portfolio-loading.md` | domain | Portfolio loading reads the saved current-connection snapshot through a read-only envelope. The hourly walk refreshes Portfolio independently of the screen; the open screen's 30-minute reread and retry are local HTTP reads only. The page explains `data: null` before a complete snapshot, preserved snapshot/time after a failed or partial walk, separate resource metadata and `Portfolio.isStale`, and current-connection/revision disclosure fences without a manual refresh or status-observer flow. | `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; System Analysis §§ Contracts, Data and State Semantics, Error and Recovery Semantics; Design § Frontend: one query cache and metadata-aware screen state. | Read the delivered Portfolio route and screen against the page: GET and retry do not call T-Bank or expose a manual sync action; snapshot fallback retains its original time; no-data, empty, stale, failed hourly attempt, and failed HTTP reread are not conflated. | +| update | `domains/income/upcoming-events.md` | domain | Upcoming events reads the saved forecast for account or `all` scope without starting personal sync. It documents strict inclusive UTC request dates, defaults, filtering to the intersection with the forecast's original inclusive window, and retention of that forecast's `calculatedAt` and source window. A later failed hourly events update leaves the last complete forecast readable and reports its failure independently; absent complete forecast is `data: null`, while an empty intersection is a successful empty result. | `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`; System Analysis § Contracts; System Analysis §§ Data and State Semantics, Error and Recovery Semantics; QA Analysis § Coverage Matrix: Events query and saved forecast. | Compare account and `all` responses/screens with the page: invalid range returns `INVALID_EVENTS_PERIOD` without source work; window intersection preserves original forecast metadata; empty intersection and failed-refresh fallback are correctly distinguished. | +| update | `architecture/http-api.md` | architecture | The investment API section describes the generated backend OpenAPI as the executable contract for `{ data, meta }` resource responses: nullable `data`; required independent resource-result meanings; resource-scoped hourly `lastAttempt`; and Accounts-only connection-wide `sync`. It states that legacy `sync=observe` may remain a read-observation compatibility parameter but cannot start source work, removes the request-started screen-sync command/status model as current AS-IS, and retains established route identities and Problem Details behavior. | System Analysis §§ Contracts, Compatibility / Migration; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Contract: Swagger is produced at the DTO boundary; QA Analysis §§ Cross-system Verification, Regression Impact. | Compare prose and examples with the backend-generated OpenAPI and runtime responses: affected envelopes expose target nullability/enums and required metadata, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work. | +| update | `adr/017-local-tbank-reads-and-background-sync.md` | ADR | Preserve ADR 017's historical decision and rationale, but mark its request-driven, manual-refresh, and no-periodic-walk decisions superseded by ADR 019. Retain its still-valid local-storage, completeness, and access-boundary context, with a link to current freshness documentation. | System Analysis § Durable Documentation Candidates; Proposal § Why; Design § Risks / Trade-offs. | ADR 017 explicitly identifies superseded decisions and links to ADR 019; it no longer presents a manual/request-driven sync model as current architecture. | +| update | `adr/019-hourly-reads-and-online-instrument-reference.md` | ADR | Clarify the accepted hourly architecture: the last walk is scoped to the current connection; ordinary GET/retry does not acquire personal data; only the first unconfirmed ledger interval and first unopened candle range are narrow acquisition exceptions. Clarify that resource-result state and hourly-attempt state remain independent and that observation is not a command. | System Analysis §§ Interaction Flows, Contracts, Observability Semantics; `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Cross-cutting Implementation. | ADR 019 describes only the delivered hourly model and its bounded exceptions; it agrees with the updated freshness page and does not introduce a second HTTP contract source. | + +## Diagram Promotion + +None. `system-analysis/sequences/hourly-investment-walk.mmd` and +`system-analysis/sequences/read-only-resource-get.mmd` are approval-time +interaction evidence. Their durable operational meaning is covered by the +updated freshness, portfolio-loading, events, and HTTP API pages; promoting +them would duplicate those explanations without adding a durable navigation or +architecture view. + +## ADR Decision + +N/A. No new architectural decision is required: ADR 019 already accepts the +durable hourly-read architecture and is clarified above. ADR 017 is an +historical decision whose conflicting request-driven/manual-refresh portions +must be explicitly superseded, not rewritten into a new decision. + +## Glossary / Navigation Impact + +N/A. The change refines existing concepts (freshness, completeness, Client +Cached Snapshot, Portfolio snapshot, and T-Bank connection) rather than adding +a durable term needing a glossary definition or a new navigation entry. + +## No-Change Rationale + +No new documentation page, diagram, or machine-readable HTTP artifact is +promoted. Backend-generated OpenAPI remains the executable HTTP contract; the +change-local schema and sequences remain archived planning evidence. No docs +entry is planned for private retained portfolio-value history because this +change does not introduce a public history read, and its access boundary is +already covered by the connection/freshness documentation. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/proposal.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/proposal.md new file mode 100644 index 0000000..80e47fc --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/proposal.md @@ -0,0 +1,94 @@ +# Proposal + +## Why + +Два завершённых legacy changes описывают несовместимые модели инвестиционной +синхронизации. `persist-tbank-data` закрепляет запуск обновления запросом и +ручное обновление, а `hourly-portfolio-sync-history` и фактическая система +используют часовой обход и локальные read-only GET. Пока canonical specs и +AS-IS документация содержат обе трактовки, ни один legacy change нельзя +безопасно синхронизировать или архивировать. + +## What Changes + +- Зафиксировать единственную observable model: часовой обход обновляет личные + инвестиционные данные, а обычные GET читают сохранённый результат и не + запускают личную синхронизацию. +- Сохранить узкие исключения первой загрузки отсутствующего исторического + интервала операций и впервые открытого диапазона свечей. +- Уточнить видимые состояния свежести, полноты, последней попытки обхода и + доступности личных данных после смены подключения. +- Согласовать canonical behavior specs и durable AS-IS documentation с этой + моделью, не создавая ещё один current OpenAPI source of truth. + +## Scope + +### In Scope + +- Investment accounts, Portfolio, операции изменяемого периода, forecasts/events, + personal connection boundary и связанные frontend recovery states. +- Capabilities `brokerage-accounts`, `portfolio-view`, + `brokerage-account-ledger`, `brokerage-account-upcoming-events`, + `frontend-data-resilience`, `tbank-connection` и `tbank-data-sync`. +- Описание и compatibility guidance для `{ data, meta }` resource responses и + backend-generated OpenAPI как executable HTTP contract. +- Обновление durable documentation и ADR supersession, когда это необходимо. + +### Out of Scope / Non-Goals + +- Новый способ запуска синхронизации, публичное чтение истории стоимости, + валютная конвертация и многопроцессный scheduler. +- Переписывание historical legacy changes или автоматическое объявление их + approval/Validation успешными. +- Новая product feature, не требуемая для согласования уже поставленного + поведения. + +## Capabilities + +### New Capabilities + +- `hourly-investment-sync`: нормативно зафиксировать текущий наблюдаемый + часовой обход и его статус, если capability ещё отсутствует в canonical specs. +- `portfolio-value-history`: зафиксировать существующую историю оценок и + границу её видимости, если capability ещё отсутствует в canonical specs. +- `online-instrument-information`: зафиксировать online-only справочные + сведения, если capability ещё отсутствует в canonical specs. + +### Modified Capabilities + +- `brokerage-accounts`: локальное чтение и результат последнего обхода. +- `portfolio-view`: read-only GET, часовая свежесть и отсутствие ручной + личной синхронизации. +- `brokerage-account-ledger`: первая загрузка отсутствующего интервала и + локальное чтение полного прошлого периода. +- `brokerage-account-upcoming-events`: последний complete forecast и + независимое состояние ошибки обновления. +- `frontend-data-resilience`: distinction between retrying an HTTP read and + starting source work. +- `tbank-connection`: границы текущих данных и скрытая история оценок после + смены либо удаления подключения. + +## Impact + +Пользователь получает предсказуемое обновление инвестиций без скрытого запуска +брокерских запросов при чтении экранов. Backend и frontend уже содержат большую +часть целевого поведения; change формализует его, устраняет расхождения +requirements/docs и создаёт основу для корректного завершения legacy history. + +## Success Criteria + +- Canonical specs дают один непротиворечивый ответ о владельце обновления, + исключениях первой загрузки и read-only GET. +- Backend-generated OpenAPI/runtime interface семантически соответствует + approved target contract при затронутых HTTP resource responses. +- AS-IS docs и ADR guidance не обещают request-driven/manual sync там, где + фактическая система использует hourly model. +- Legacy `persist-tbank-data` и `hourly-portfolio-sync-history` получают + безопасный дальнейший путь к archival finalization без конкурирующей sync. + +## Assumptions / Dependencies + +- Часовой scheduler, local resource reads и frontend 30-minute rereads остаются + целевой моделью, подтверждённой Change Owner. +- System Analysis определит точные contracts, semantics forecasts/events и + допустимые first-load exceptions до implementation tasks. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/qa-analysis.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/qa-analysis.md new file mode 100644 index 0000000..979484a --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/qa-analysis.md @@ -0,0 +1,92 @@ +# QA Analysis + +## Verification Scope + +### In Scope + +- Authorised, black-box verification of the affected personal-resource HTTP reads: Accounts, account and `all` Portfolio, operations, and account and `all` events. +- The distinction between a local ordinary GET/retry and the permitted acquisition paths: the hourly walk, a first unconfirmed historical operations interval, and a first unopened candle range. +- Observable hourly-walk scheduling and outcome semantics, including connection-scoped Accounts `sync` and resource `meta.lastAttempt`. +- Resource-envelope semantics for confirmed data, empty confirmed data, first load, stale fallback, partial/failed attempts, and connection unavailability. +- Current-connection access boundaries after T-Bank connection creation, replacement, and deletion, including retained value-history non-disclosure. +- Events scope, date-window, saved-forecast, and failed-refresh behavior. +- Compatibility of the backend-generated OpenAPI/runtime interface with the approved target envelope and preservation of established route identities and Problem Details behavior. +- User-visible resilience: retry performs an HTTP reread only; cached successful screen data, server fallback, and failed background HTTP reads remain distinguishable. + +### Out of Scope + +- Scheduler implementation, persistence layout, internal job reservations, upstream-client mechanics, and frontend test structure. +- New public reading of portfolio value-history, manual personal synchronization, currency conversion, multi-process scheduling, and changes to unrelated investment or banking resources. +- Verifying T-Bank or public-reference-provider correctness beyond controlled responses needed to observe WealthLedger's published behavior. + +## Coverage Matrix + +| Approved behavior | Black-box verification | Expected observable result | +| --- | --- | --- | +| Ordinary personal GETs are read-only | Read Accounts, Portfolio, operations, and events repeatedly, including user retry and screen reopen, while upstream personal calls are observable. | Each read returns only locally held state and causes no personal upstream work or new hourly attempt. | +| First-load exceptions stay narrow | Open an unconfirmed historical operations interval and, separately, an unopened candle range; then reread each after confirmation. | Only the requested missing interval/range may acquire data. It exposes incomplete local state until confirmed; no Accounts, Portfolio, events, other interval, or range refresh occurs. Confirmed reads stay local. | +| Hourly ownership and cadence | Connect successfully; observe initial work, an elapsed eligible term, a restart after downtime, and a term arriving during an active walk. | Connection starts one initial attempt. Subsequent attempts begin no earlier than 60 minutes from the preceding start; restart performs at most one current attempt; overlapping terms neither run nor queue. | +| Hourly result is observational | Observe no attempt, running, succeeded, partial, and failed results through affected resources and Accounts. | `meta.lastAttempt` distinguishes all five states with safe scoped problems; Accounts `sync` is `null` before an attempt and otherwise the connection-wide result. Reading it starts nothing. | +| Independent section persistence | Make one account/section succeed and another fail during a walk. | Successful current data persists; the earlier complete result for the failed section remains visible; the attempt is `partial` with only safe section/account/error-code problem data. | +| Envelope state meanings | Exercise a fresh complete result, stale complete fallback, no complete result while work runs, no complete result after failure, and confirmed empty Accounts/Portfolio/events. | Every success has `{ data, meta }`; `meta` separately reports freshness, completeness, `lastSuccessfulAt`, `lastAttempt`, and availability. `[]` is available and complete, while no complete result is `data: null` and never presented as complete or fresh. | +| Failed refresh preserves known data | Cause a temporary hourly failure after a confirmed Accounts, Portfolio, operations, or events result. | `200` retains the original complete data and success time; freshness reflects that result; the failed attempt is separately visible. A completed background failure is not a synchronous `503`. | +| Invalid connection boundary | Request personal resources with no connection, then after a known-invalid token result. | No connection returns established `409 TBANK_NOT_CONNECTED`; invalid token returns established `400 INVALID_TBANK_TOKEN`, retains the connection record, and discloses no personal fallback data. | +| Connection replacement and deletion | Replace a valid connection while an old walk is pending or running, then delete the replacement twice. | PUT validates before changing the prior connection, establishes a new boundary and initial walk on success; old data/status cannot reappear. DELETE returns `204` idempotently, stops use of its token, and hides current personal data and retained history until the same account is confirmed again. | +| Events query and saved forecast | Read account and `all` events for default, explicit, intersecting, non-intersecting, and empty windows; submit `from > to`; fail a later refresh. | Dates are strict inclusive UTC dates; defaults are current UTC date through +30 calendar days; invalid range returns `400 INVALID_EVENTS_PERIOD` without source work. Results are the intersection with the forecast's original window and retain its `calculatedAt`/window. Empty intersections are successful empty data. `all` is complete only with every included account contribution. Failed refresh retains the previous forecast and reports the attempt failure separately. | +| Frontend recovery and accessibility | Observe a successful resource screen during HTTP reread failure, a server stale/fallback result, a failed hourly attempt, and each no-data first-load state. | Successful cached screen data remains visible during/after background HTTP failure. Retry only rereads HTTP. Client-read failure, server stale data, and failed hourly work are distinct, and the five `meta` meanings are exposed in text or accessible names rather than color alone. | + +## Cross-system Verification + +1. Use a controlled upstream boundary to prove that normal GETs and retry actions do not cause personal upstream requests, while an eligible hourly walk and each permitted first-load exception have only their approved source effect. +2. Verify the API response and the rendered Accounts, Portfolio, operations, and events states together for the same server conditions. In particular, a saved complete result plus a failed attempt must remain readable and visibly distinguishable from a client HTTP-read failure. +3. Verify that successful PUT replacement and DELETE invalidate visible personal-resource state. Late responses or late old-connection work must not restore accounts, Portfolio, events, `sync`, or retained history across the new boundary. +4. Compare the generated backend OpenAPI published for delivery with runtime responses and the consumer-generated types. Confirm the target `{ data, meta }` resources, Accounts-only `sync`, all required meta meanings, nullability, and enum values are represented without relying on legacy `sync=observe` to start work. + +## Risk-based Priorities + +| Priority | Risk | Verification focus | +| --- | --- | --- | +| P0 | A screen read initiates brokerage work or leaks data across a connection boundary. | Upstream-call absence on GET/retry; replacement/deletion fence; invalid-token and no-connection responses. | +| P0 | A failed refresh hides or mislabels financial data. | Complete fallback retention; `data: null` when no complete result exists; empty complete results; independent meta and attempt states. | +| P0 | Events return an incorrect period, silently omit an account in `all`, or relabel an old forecast. | Strict UTC window validation/defaults/intersection; `all` completeness; preserved `calculatedAt` and original forecast window after failure. | +| P1 | Scheduler work overlaps, catches up, or reports unsafe/incorrect status. | First walk, 60-minute eligibility from start, downtime, overlap drop, partial result, safe problem scope. | +| P1 | Contract evolution breaks the generated frontend consumer or existing API callers. | Generated OpenAPI/runtime semantic comparison, route and established Problem Details compatibility, legacy observation endpoint non-command behavior. | +| P2 | Presentation obscures recovery and freshness meaning. | Accessible status text, separate client/server/hourly indicators, retained screen snapshot, empty states. | + +## Test Data and Environment Needs + +- An authenticated user with controlled valid, invalid, and unavailable T-Bank outcomes; at least two supported accounts plus an unsupported account to establish membership and partial-result cases. +- Persisted fixtures for fresh and stale complete Accounts, Portfolio, operations, and forecasts; no-result first-load state; confirmed empty Accounts; empty Portfolio; empty event intersection; and a forecast whose source window differs from the request window. +- Controllable clock and observable upstream boundary to establish hourly eligibility, running overlap, restart behavior, and proof that normal reads have no personal upstream activity. +- Two valid connection identities with disjoint accounts and one shared account, plus retained value-history data, to verify replacement/deletion concealment and re-confirmed access. +- Published backend OpenAPI for the delivered build and the corresponding generated frontend contract, both from the same build candidate. + +## Manual / Exploratory Scope + +- Navigate Accounts, account Portfolio, operations, and upcoming events through first load, available empty, stale fallback, failed hourly update, invalid token, and no-connection states. For operations, include an incomplete first-loaded interval and its later confirmed result. Confirm status meaning is understandable without color and no manual personal-sync control is exposed. +- Keep an investment screen open across its periodic reread and induce a failed HTTP read. Confirm the prior screen snapshot remains visible and that retry changes only the HTTP-read state. +- Replace or delete a connection while navigating between account and event views. Confirm neither navigation, browser history, nor a late response exposes data from the prior connection. +- Explore boundary dates around UTC midnight, the inclusive forecast window endpoints, leap-day dates, and empty intersections for both account and `all` events scopes. + +## Automation Candidates + +- Repeatable system checks for the resource-envelope state matrix, hourly cadence/overlap, local-read upstream-call absence, connection revision fence, and event window/scope matrix. +- Consumer-contract compatibility checks that compare the generated backend OpenAPI, runtime resource responses, and regenerated frontend transport types for affected public operations. +- Browser-level checks for retry-only reads, retained successful UI data, invalidation after connection change, and accessible status names. + +## Observability Verification + +- Confirm resource responses expose no attempt as `none`, running work without `finishedAt`, and terminal succeeded/partial/failed work with only safe section, account, and error-code problem details. +- Confirm `lastSuccessfulAt`, freshness, completeness, and availability continue to describe the returned data rather than the later attempt; `sync` is `null` before any Accounts walk and remains connection-scoped thereafter. +- Confirm no response, OpenAPI example, UI status, or observable problem detail discloses a token, upstream payload, cursor, internal connection revision, or old-connection status. + +## Regression Impact + +- Existing clients of the affected GET routes must retain route identities, account and `all` scopes, date validation/defaults, response ordering where specified, and established Problem Details codes unless the approved generated contract explicitly changes them. +- Existing `sync=observe` and `/api/v1/tbank-sync/{id}` observation behavior may remain compatible but must not regain request-started personal synchronization semantics. +- Accounts, Portfolio, operations, and events callers must handle the approved envelope evolution, including nullable `data`, independent attempt state, empty complete data, and Accounts-only `sync`; generated frontend types must match the published backend OpenAPI. +- Unaffected public history-reading capability is not introduced. Retained value history remains unavailable outside a confirmed current connection boundary. + +## Gaps / Blockers + +None identified in the approved System Analysis and deltas. Field names used by the generated OpenAPI remain a delivery compatibility check: they must faithfully represent the approved target semantics before release. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-ledger/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-ledger/spec.md new file mode 100644 index 0000000..b941c70 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-ledger/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Отсутствующий исторический интервал загружается только впервые +Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение только недостающего интервала без ожидания T-Bank в локальном ответе. Это является исключением первой загрузки и MUST NOT запускать обновление других личных данных. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы или часового обхода. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным. + +#### Scenario: Первый просмотр отсутствующего прошлого периода +- **WHEN** User открывает доступный исторический период без подтверждённой полноты +- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных + +#### Scenario: Повторное открытие полного периода +- **WHEN** User повторно открывает уже полностью загруженный прошлый период +- **THEN** система читает сохранённые операции без повторной сверки с T-Bank + +#### Scenario: Ошибка загрузки +- **WHEN** загрузка части отсутствующего периода завершается ошибкой +- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-upcoming-events/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-upcoming-events/spec.md new file mode 100644 index 0000000..0463156 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-account-upcoming-events/spec.md @@ -0,0 +1,120 @@ +## MODIFIED Requirements + +### Requirement: User can retrieve upcoming events for an account +Система MUST предоставлять аутентифицированному User read-only `GET /api/v1/brokerage-account-upcoming-events` для выбранного открытого поддерживаемого 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`, не подменять период, не запускать загрузку другого периода и не возвращать его прогноз. Обычный GET MUST NOT запускать личную синхронизацию. + +Ответ MUST иметь `{ data, meta }`, где `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Доступный `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, `calculatedAt`, а в режиме `all` -- принадлежность каждого события счёту. Прогноз MUST быть рассчитан по количеству бумаг полного Portfolio Snapshot на `calculatedAt`; сохранённый прогноз MUST сохранять свой исходный `calculatedAt` и рассчитанное от него включительное окно, даже когда запрошенное `from`/`to` отличается. При чтении сохранённого прогноза система MUST возвращать только события, чьи даты попадают во включительное пересечение исходного окна прогноза и запрошенного UTC-окна; пустое пересечение либо отсутствие событий в пересечении MUST возвращать успешный пустой список с теми же `calculatedAt`, `meta` и исходным окном. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта; отсутствие вклада счёта MUST NOT подменяться полным результатом. Список будущих событий является прогнозом по наблюдаемому состоянию счёта и количеству бумаг на `calculatedAt` и MUST NOT означать долговременное право владения Brokerage Account независимо от T-Bank connection. + +#### Scenario: Получение событий доступного счёта +- **WHEN** аутентифицированный User запрашивает события доступного счёта с последним полным прогнозом +- **THEN** система возвращает из сохранённого прогноза события в запрошенном включительном UTC-окне, его исходные `calculatedAt` и рассчитанное окно без обращения к T-Bank + +#### Scenario: Запрошенное окно не пересекается с сохранённым прогнозом +- **WHEN** запрошенный включительный UTC-окно не пересекается с исходным окном последнего полного прогноза +- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta` + +#### Scenario: Отсутствие событий в периоде +- **WHEN** пересечение запрошенного и исходного окна прогноза не содержит событий +- **THEN** система возвращает успешный пустой список, момент расчёта, исходное окно и `meta` + +#### Scenario: Некорректный период +- **WHEN** User передаёт `from` позже `to` +- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD` без прогноза и без запуска личной синхронизации + +#### Scenario: Прогноз всех счетов +- **WHEN** User запрашивает режим `all` и для каждого входящего счёта доступен полный прогноз +- **THEN** система возвращает события всех счетов в запрошенном окне с принадлежностью каждого события счёту + +#### Scenario: Отклонение недоступного счёта +- **WHEN** User запрашивает события счёта, недоступного через его текущий T-Bank connection +- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события + +### Requirement: Forecasts preserve event meaning and uncertainty +Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного Portfolio Snapshot на `calculatedAt`, использованного для сохранённого прогноза, а не по количеству бумаг на момент HTTP-запроса. Сохранённый прогноз MUST сохранять свой `calculatedAt` и эту исходную зависимость, в том числе после неуспешного обновления, и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы. + +Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position. + +#### Scenario: Прогноз купона по текущей позиции +- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза +- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot + +#### Scenario: Прогноз дивиденда +- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза +- **THEN** система возвращает сохранённое событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на количество акций этого Snapshot + +#### Scenario: Отменённый дивиденд не попадает в прогноз +- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled` +- **THEN** система не добавляет такое событие в сохранённый прогноз + +#### Scenario: Отображение оферты без суммы +- **WHEN** для поддерживаемой облигации известна оферта в пределах исходного окна прогноза, но её денежные условия недоступны +- **THEN** система возвращает событие offer с прогнозной суммой `null` + +#### Scenario: Неподдерживаемое частичное погашение +- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации +- **THEN** система НЕ создаёт синтетическое событие частичного погашения + +### Requirement: Upcoming events use complete source results +Система MUST публиковать прогноз только после полного получения необходимых данных брокера и MOEX для полного Portfolio Snapshot, по количеству бумаг которого он рассчитан. Неуспешное обновление MUST NOT заменять последний полный прогноз, его `calculatedAt` или окно событий частичным списком. При неуспешном обновлении и существующем последнем полном прогнозе система MUST возвращать отфильтрованную для запрошенного окна часть этого прогноза с его исходными `calculatedAt`, окном и `meta`, отдельно обозначающим неуспех последней попытки, даже если результат фильтрации пуст. При отсутствии последнего полного прогноза система MUST возвращать `data: null` и в `meta` обозначать выполнение первой загрузки либо её неуспех без частичного списка. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных». + +#### Scenario: Недоступность источника +- **WHEN** необходимый источник данных брокера или MOEX временно недоступен во время часового обновления +- **THEN** система не публикует частичный список как полный прогноз + +#### Scenario: Ошибка обновления при сохранённом прогнозе +- **WHEN** часовой обход не завершает обновление необходимых источников, а последний полный прогноз существует +- **THEN** система возвращает его события только в запрошенном окне, исходные `calculatedAt` и окно, а `meta` отдельно обозначает ошибку обновления + +#### Scenario: Ошибка обновления при сохранённом прогнозе вне запрошенного окна +- **WHEN** часовой обход не завершает обновление, последний полный прогноз существует, но его окно не пересекается с запрошенным +- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`, обозначающим ошибку обновления + +#### Scenario: Инструмент без поддерживаемых данных +- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события +- **THEN** система не добавляет для него синтетическую строку в результат + +#### Scenario: MOEX отвечает без запрошенного блока +- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных +- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой + +### Requirement: Account page presents an upcoming events list +Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию. + +#### Scenario: Просмотр ближайших событий +- **WHEN** User открывает «Будущие события» из обзора Brokerage Account +- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы + +#### Scenario: Пустое состояние страницы +- **WHEN** запрос будущих событий успешно возвращает пустой список +- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника + +#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза +- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение + +#### Scenario: Ошибка страницы +- **WHEN** запрос будущих событий завершается ошибкой +- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение + +#### Scenario: Ошибка обновления при сохранённом прогнозе на странице +- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления +- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение + +### Requirement: Account overview includes upcoming events +Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение. + +#### Scenario: Отображение ближайших событий +- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события +- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице + +#### Scenario: Ближайшие события недоступны без сохранённого прогноза +- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка + +#### Scenario: Ближайшие события недоступны +- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой +- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку + +#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз +- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления +- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-accounts/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-accounts/spec.md new file mode 100644 index 0000000..5f33b29 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/brokerage-accounts/spec.md @@ -0,0 +1,135 @@ +## MODIFIED Requirements + +### Requirement: Список активных брокерских счетов +Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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. + +#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке +- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт +- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank + +#### Scenario: Пустой список поддерживаемых счетов +- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов +- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат + +#### Scenario: Пустое название получает fallback +- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя +- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа + +#### Scenario: Дата открытия отсутствует +- **WHEN** последний подтверждённый счёт не имеет даты открытия +- **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** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков +- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency` + +### Requirement: Состояния недоступности списка счетов +Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный. + +#### Scenario: Подключение отсутствует +- **WHEN** User без сохранённого T-Bank connection запрашивает список +- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED` + +#### Scenario: Сохранённый token больше не действителен +- **WHEN** T-Bank отклоняет сохранённый token при часовом обходе списка как неаутентифицированный или без прав +- **THEN** последующее чтение отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные + +#### Scenario: Ошибка обхода после успешного списка +- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки +- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank + +#### Scenario: T-Bank временно недоступен +- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой +- **THEN** система сообщает о неуспешной первой загрузке без счетов + +#### Scenario: Первый обход ещё не завершён +- **WHEN** User открывает список после подключения до получения первого подтверждённого списка +- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка + +### Requirement: Экран списка брокерских счетов +Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Он MUST использовать успешный Client Cached Snapshot списка в течение 30 минут. Открытый экран Accounts MUST перечитывать локальный backend-результат каждые 30 минут и MUST NOT запускать импорт T-Bank. Клиент MUST показывать наблюдаемые freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. При уже имеющемся успешном списке клиент MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового HTTP-перечитывания и после его ошибки, ясно отделяя это состояние от свежего backend-результата. При успешном непустом ответе клиент 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** клиент получает успешный пустой массив +- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank + +#### Scenario: Последняя попытка неуспешна после полного списка +- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки +- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке + +#### Scenario: Первая загрузка списка выполняется или неуспешна +- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного + +#### Scenario: Список берётся из свежего frontend-кэша +- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки +- **THEN** клиент показывает сохранённые счета без нового API-запроса + +#### Scenario: Устаревший список загружается повторно +- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список +- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление + +#### Scenario: Открытый экран перечитывает список +- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения +- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank + +#### Scenario: Ошибка фонового обновления не скрывает snapshot +- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot +- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки + +#### Scenario: Карточка ограничивает валютные итоги +- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах +- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения + +#### Scenario: Недоступность сводки локальна карточке +- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null` +- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта + +#### Scenario: Пустая сводка не является ошибкой +- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency` +- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности + +### Requirement: Клиентские состояния ошибки списка счетов +Клиент MUST NOT автоматически повторять неуспешное HTTP-чтение списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного HTTP-чтения. Повтор MUST перечитывать backend-результат и MUST NOT запускать импорт T-Bank. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. + +#### Scenario: Подключение требуется +- **WHEN** клиент получает `TBANK_NOT_CONNECTED` +- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank + +#### Scenario: Известная ошибка не повторяется автоматически +- **WHEN** HTTP-чтение списка завершается известной ошибкой +- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения + +#### Scenario: Token требует внимания +- **WHEN** клиент получает `INVALID_TBANK_TOKEN` +- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank + +#### Scenario: Временная ошибка T-Bank +- **WHEN** клиент получает `TBANK_UNAVAILABLE` +- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank + +#### Scenario: Неизвестная ошибка загрузки +- **WHEN** клиент получает ошибку без известного кода списка счетов +- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение + +#### Scenario: Известная ошибка при сохранённом snapshot +- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` +- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/frontend-data-resilience/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/frontend-data-resilience/spec.md new file mode 100644 index 0000000..1a213f3 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/frontend-data-resilience/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio +Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для того же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Для каждого личного resource response клиент MUST представлять freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` текстом либо доступно именованным элементом, а не только цветом; он MUST различать отсутствие попытки, выполнение, полный успех, частичный успех, неуспех, доступный пустой полный результат и отсутствие полного результата. Client Cached Snapshot MUST оставаться видимым во время фонового HTTP-перечитывания и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повторное действие User MUST повторять только HTTP-чтение и MUST NOT запускать личную синхронизацию. Открытый инвестиционный экран MAY периодически перечитывать backend-результат, но MUST NOT создавать импорт T-Bank. + +#### Scenario: Список Brokerage Accounts обновляется в фоне +- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое HTTP-перечитывание того же списка +- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор перечитывания + +#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа +- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее HTTP-чтение завершается ошибкой +- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение и предоставляет повтор только HTTP-чтения + +#### Scenario: Server-side fallback и client cached snapshot различаются +- **WHEN** backend возвращает Portfolio с `isStale` либо HTTP-перечитывание Client Cached Snapshot завершается ошибкой +- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя одно состояние другим + +#### Scenario: Resource сохраняется после неуспешной часовой попытки +- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки +- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/hourly-investment-sync/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/hourly-investment-sync/spec.md new file mode 100644 index 0000000..01acd43 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/hourly-investment-sync/spec.md @@ -0,0 +1,39 @@ +## Purpose + +Определяет независимое от открытых экранов часовое обновление личных инвестиционных данных и безопасно наблюдаемый результат последней попытки. + +## ADDED Requirements + +### Requirement: Часовой обход личных инвестиционных данных +Система SHALL начинать первый часовой обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки этого подключения. После простоя система MUST выполнить не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока. Перекрывающиеся обходы MUST NOT выполняться: срок, наступивший во время выполняющегося обхода, MUST быть пропущен и MUST NOT ставиться в очередь; следующий обход становится допустимым только в следующий 60-минутный срок, отсчитанный от начала выполнявшейся попытки. Обход MUST обновлять доступные счета, Portfolio каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего Portfolio. Он MUST NOT повторно загружать полные прошлые интервалы операций, завершённые свечи или полную рыночную историю и MUST NOT зависеть от открытия frontend-экрана. + +#### Scenario: Первое подключение +- **WHEN** User успешно подключает T-Bank между часовыми сроками +- **THEN** первый обход начинается без ожидания следующего календарного часа + +#### Scenario: Возобновление после простоя +- **WHEN** backend запускается после пропуска нескольких часовых сроков +- **THEN** система выполняет один актуальный обход без синтетических прошлых обновлений + +#### Scenario: Перекрытый срок +- **WHEN** предыдущий обход всё ещё выполняется в момент очередного срока +- **THEN** система не начинает второй обход и не ставит пропущенный обход в очередь + +#### Scenario: Длительный обход пропускает один срок +- **WHEN** обход начался в 10:00, всё ещё выполняется в 11:00 и завершился до 12:00 +- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00 + +### Requirement: Последний результат обхода доступен только для наблюдения +Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию. + +#### Scenario: Частичный успех +- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился +- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт + +#### Scenario: Наблюдение не запускает обход +- **WHEN** User запрашивает результат последнего обхода +- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank + +#### Scenario: Старая граница подключения +- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода +- **THEN** система не раскрывает результат прежней границы подключения diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/online-instrument-information/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/online-instrument-information/spec.md new file mode 100644 index 0000000..16888fc --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/online-instrument-information/spec.md @@ -0,0 +1,16 @@ +## Purpose + +Определяет online-only получение справочных сведений и результатов поиска инструментов отдельно от сохранённых личных финансовых данных. + +## ADDED Requirements + +### Requirement: Справочные сведения и поиск инструментов получают онлайн +Система SHALL получать подробные справочные сведения об Instrument и результаты поиска онлайн при их запросе. Такие сведения MUST NOT становиться самостоятельной долговременной копией справочника. Недоступность либо отсутствие онлайн-сведений MUST NOT задерживать сохранённый финансовый ответ, изменять его свежесть или подтверждать отсутствие Instrument. + +#### Scenario: Медленный online lookup +- **WHEN** подробные сведения об Instrument ещё не получены онлайн при чтении сохранённого Portfolio +- **THEN** система возвращает доступный сохранённый Portfolio без ожидания lookup + +#### Scenario: Online lookup не найден +- **WHEN** online lookup не возвращает Instrument +- **THEN** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-value-history/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-value-history/spec.md new file mode 100644 index 0000000..18c66fe --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-value-history/spec.md @@ -0,0 +1,27 @@ +## Purpose + +Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек. + +## ADDED Requirements + +### Requirement: Полные оценки Portfolio сохраняются как история +Система SHALL сохранять для каждого доступного Brokerage Account точку оценки по каждой исходной валюте только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение без валютной конвертации и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки. + +#### Scenario: Полная мультивалютная оценка +- **WHEN** успешный полный Portfolio содержит вычислимые итоги RUB и USD +- **THEN** система сохраняет две отдельные точки с исходными значениями и валютами + +#### Scenario: Неполная оценка +- **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость +- **THEN** система сохраняет новый Portfolio без точки истории оценки + +### Requirement: История оценки скрыта вне подтверждённого доступа +Система MUST сохранять историю оценки при замене token или удалении подключения, но MUST NOT раскрывать её без текущего подтверждённого доступа User к тому же Brokerage Account. Успешное новое подключение, подтверждающее доступ к счёту, MUST снова делать его сохранённую историю доступной для будущих поддерживаемых представлений. Система MUST NOT предоставлять отдельное публичное чтение точек истории этим требованием. + +#### Scenario: Удалённое подключение +- **WHEN** User удаляет T-Bank connection +- **THEN** сохранённые точки оценки этого User не доступны для чтения + +#### Scenario: Повторно подтверждённый счёт +- **WHEN** новое успешное подключение подтверждает доступ User к тому же Brokerage Account +- **THEN** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-view/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-view/spec.md new file mode 100644 index 0000000..b3a8637 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/portfolio-view/spec.md @@ -0,0 +1,82 @@ +## 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 }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio. + +Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection. + +#### Scenario: Получение текущего Portfolio +- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён +- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta` + +#### Scenario: Недоступный Brokerage Account +- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection +- **THEN** система отвечает `404` без Portfolio Snapshot + +### Requirement: Свежесть и fallback полного Portfolio Snapshot +Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST возвращать `{ data, meta }`, где `data` является сохранённым Snapshot, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot. Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника. + +Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента. + +#### Scenario: Свежий Snapshot +- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения +- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank + +#### Scenario: Медленный lookup инструмента +- **WHEN** сведения об инструменте ещё запрашиваются онлайн +- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity + +#### Scenario: Fallback после временной upstream-ошибки +- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot +- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt` + +#### Scenario: Недействительный token не маскируется fallback-данными +- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при часовом обходе Portfolio +- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` + +#### Scenario: Неполная оценка при новом составе +- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены +- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля + +### Requirement: Состояния и ошибки Portfolio на клиенте +Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд без automatic retry или refetch при возврате фокуса. Открытый экран MUST перечитывать сохранённый результат backend каждые 30 минут без запуска синхронизации T-Bank. Клиент MUST показывать freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность из `meta` текстом или доступно именованным элементом, а не только цветом. Клиент MUST NOT предоставлять действие ручной синхронизации Portfolio. При отсутствии ранее успешного ответа он MUST показывать структурный skeleton с доступным обозначением занятости без вымышленных сумм; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус. + +Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями. + +При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки. + +#### Scenario: Показ устаревшего Portfolio +- **WHEN** клиент получает успешный ответ с `isStale: true` +- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации + +#### Scenario: Первичная загрузка Portfolio +- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа +- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders + +#### Scenario: Неуспешная последняя попытка сохраняет Portfolio +- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки +- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки + +#### Scenario: Недоступный счёт на клиенте +- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404` +- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора + +#### Scenario: Обновление после истечения срока свежести +- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание +- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием + +#### Scenario: Ошибка обновления после успешного ответа +- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой +- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения + +#### Scenario: Ошибка клиентского обновления при server-side stale +- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой +- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению + +#### Scenario: Повторное обновление успешно +- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot +- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale` + +#### Scenario: Штатный пустой Portfolio на клиенте +- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances` +- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-connection/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-connection/spec.md new file mode 100644 index 0000000..7120af7 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-connection/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Создание и замена подключения T-Bank +Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первый часовой обход без команды frontend. История стоимости прежних счетов MUST сохраняться, но MUST быть доступна только для счетов, доступ к которым подтверждён новым подключением. + +#### Scenario: Успешное создание подключения без счетов +- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов +- **THEN** система сохраняет подключение, отвечает `200` с `{ connected: true, connectedAt: }` и не создаёт фиктивные счета + +#### Scenario: Успешная замена подключения +- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет +- **THEN** система заменяет token, обновляет `connectedAt`, отвечает текущим статусом без token и начинает первый часовой обход нового подключения + +#### Scenario: Невалидный token не заменяет существующее подключение +- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав +- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений + +#### Scenario: T-Bank временно недоступен при проверке token +- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки +- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение + +### Requirement: Удаление подключения T-Bank +Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и сделать относящиеся к User текущие личные данные недоступными. История стоимости счёта MUST сохраняться, но MUST NOT раскрываться до подтверждения нового доступа к тому же счёту. + +#### Scenario: Удаление существующего подключения +- **WHEN** User удаляет существующее подключение +- **THEN** система удаляет token, скрывает текущие личные данные и историю стоимости и отвечает `204` + +#### Scenario: Повторное удаление подключения +- **WHEN** User удаляет уже отсутствующее подключение +- **THEN** система отвечает `204`, не раскрывая сохранённую историю diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-data-sync/spec.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-data-sync/spec.md new file mode 100644 index 0000000..5b10513 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/specs/tbank-data-sync/spec.md @@ -0,0 +1,36 @@ +## Purpose + +Определяет единственного владельца обновления личных данных T-Bank и узкие +исключения первой загрузки данных, которых ещё нет в локальном результате. + +## ADDED Requirements + +### Requirement: Часовой обход владеет обновлением личных данных T-Bank +Система MUST обновлять личные инвестиционные данные T-Bank только часовым обходом для текущего подключения User. Обычные resource reads, действия повтора и открытие экранов MUST NOT запускать импорт, ручную личную синхронизацию или обращение к T-Bank. Единственными исключениями являются первая загрузка отсутствующего интервала Account Ledger и впервые открытого диапазона свечей: они MAY начать получение только этого отсутствующего интервала либо диапазона, когда для него нет подтверждённого локального результата. Эти исключения MUST NOT обновлять Accounts, Portfolio, иные интервалы операций, прогноз будущих событий или другой диапазон свечей. После подтверждения полноты запрошенного прошлого интервала или диапазона его повторное чтение MUST оставаться локальным. + +#### Scenario: Обычное чтение не запускает импорт +- **WHEN** User открывает Accounts, Portfolio, Account Ledger, будущие события либо повторяет неуспешное HTTP-чтение +- **THEN** система читает локальный результат и не начинает личную синхронизацию T-Bank + +#### Scenario: Первый отсутствующий интервал операций +- **WHEN** User впервые открывает интервал Account Ledger без подтверждённого локального результата +- **THEN** система MAY начинает получение только этого интервала и возвращает его локальное состояние неполноты до подтверждения результата + +#### Scenario: Впервые открытый диапазон свечей +- **WHEN** User впервые открывает диапазон свечей, для которого нет подтверждённого локального результата +- **THEN** система MAY начинает получение только этого диапазона и не загружает другие диапазоны или полную историю свечей + +### Requirement: Результат личного обновления остаётся наблюдаемым +Каждый response resource read личных данных текущего подключения MUST возвращать `{ data, meta }`. `meta` MUST отдельно делать наблюдаемыми freshness и полноту именно `data`, момент последнего успешного полного результата resource, момент и состояние последней часовой попытки, затронувшей resource, и доступность resource. Состояние последней попытки MUST различать отсутствие попытки, выполнение, полный успех, частичный успех и неуспех. Доступность MUST различать доступный полный результат, первую загрузку без полного результата, временную недоступность без полного результата и недоступность из-за отсутствующего либо недействительного подключения. Неполный, продолжающийся или неуспешный результат MUST NOT обозначаться полным или свежим. Успешный пустой Accounts либо иной пустой полный результат MUST обозначаться доступным и полным, а не отсутствующим. При неуспешном обновлении последний подтверждённый полный результат и его время MUST оставаться доступными, когда отдельное требование capability не предписывает недоступность данных; его freshness MUST определяться исходным временем, а `meta` MUST отдельно обозначать неуспех последней попытки. Наблюдение этих состояний MUST NOT предоставлять способ начать синхронизацию. + +#### Scenario: Ошибка после полного результата +- **WHEN** очередное обновление набора личных данных завершается ошибкой после ранее подтверждённого полного результата +- **THEN** система возвращает предыдущий полный `data`, его исходное время и freshness, а `meta` отдельно обозначает неуспех последней попытки + +#### Scenario: Первая загрузка ещё не завершена +- **WHEN** для набора личных данных ещё нет подтверждённого полного результата +- **THEN** система возвращает `data: null` и в `meta` обозначает выполнение первой загрузки либо её неуспех без частичного результата, выданного за полный + +#### Scenario: Успешный пустой список счетов +- **WHEN** последний полный результат Accounts подтверждает отсутствие открытых поддерживаемых счетов +- **THEN** система возвращает `data: []`, а `meta` обозначает доступный полный результат, его время и состояние последней часовой попытки diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/analysis.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/analysis.md new file mode 100644 index 0000000..267da89 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/analysis.md @@ -0,0 +1,108 @@ +# System Analysis + +## Requirement Coverage + +| Approved delta | System decision covered here | +| --- | --- | +| `hourly-investment-sync`: hourly walk and observable result | Per-connection durable hourly attempt, no catch-up or overlap, and `lastAttempt`/Accounts `sync` semantics. | +| `tbank-data-sync`: one update owner and observable resource state | Local resource reads, two narrow first-load exceptions, and the target `{ data, meta }` contract. | +| `brokerage-accounts` | Saved Accounts and per-account summaries, plus the connection-wide hourly result exposed as `sync`. | +| `portfolio-view` and `portfolio-value-history` | Saved complete snapshots, stale fallback, connection-revision publication fence, and private retained value history. | +| `brokerage-account-ledger` | First opening of an unconfirmed historical interval may load only that interval; complete history remains local. | +| `brokerage-account-upcoming-events` | Saved forecast query/window semantics, complete-source publication, and independent failed-update status. | +| `online-instrument-information` | Online reference lookup is separate from personal-resource reads and cannot delay or alter their status. | +| `frontend-data-resilience` | A retry reissues only HTTP GET; frontend cache, server fallback, and hourly-attempt failure remain distinct. | +| `tbank-connection` | PUT starts the new connection's first walk; replacement/deletion hides old personal data and old walk status. | + +## Systems and Responsibilities + +| System | Responsibility | Must not do | +| --- | --- | --- | +| Frontend | Read resources, render `data` and all observable `meta` fields, retain its own successful screen cache, and retry only the GET. | Start personal sync, infer freshness from a client error, or expose a manual personal-sync action. | +| Backend resource API | Authenticate, enforce the current connection boundary, read local persisted resources, and return the target envelope. | Wait for or call T-Bank while serving ordinary resource GETs. | +| Durable hourly scheduler | Reserve at most one current walk per connection; refresh membership, each available Portfolio, mutable operations tail, then events. | Replay missed terms, queue an overlapped term, or refresh complete historical ledger intervals/candle ranges. | +| Local store | Keep last confirmed complete data, coverage, resource timestamps, hourly attempt/result, and connection revision. | Publish data or status from a superseded connection revision. | +| T-Bank and public reference sources | Supply upstream data to connection PUT validation, the hourly walk, and explicitly online reference lookups. | Become a synchronous dependency of a normal personal resource GET. | + +## Interaction Flows + +`sequences/read-only-resource-get.mmd` defines the normal read and retry boundary. A `200` read uses only local state; a boundary error may occur before any data lookup. + +`sequences/hourly-investment-walk.mmd` defines the sole normal personal-data updater. A successful connection starts its first attempt immediately. Thereafter a next attempt is eligible no earlier than 60 minutes after the preceding attempt started. Startup performs at most one current attempt, not a replay. If a term arrives while an attempt is running, it is dropped rather than queued; the next eligible term is measured from the running attempt's start. + +The walk updates current membership, every available account's Portfolio, each account's mutable operations tail, and forecasts/events for instruments in the current Portfolio. Each successful section persists independently. A failed section preserves its earlier complete result and its original success time. Complete past operation intervals, completed candles, and full market history are not refreshed by the walk. + +### First-load exceptions + +1. Opening an unconfirmed historical Account Ledger interval may start retrieval of only that missing interval and immediately returns local rows plus incomplete coverage/status. It does not refresh membership, Accounts, Portfolio, events, another operations interval, or candles. A confirmed complete interval is never rechecked due to a GET or the hourly walk. +2. Opening a candle range with no confirmed local result may start retrieval of only that range. It does not retrieve other ranges or all history; a confirmed range is subsequently local. + +These are acquisition exceptions, not variants of manual sync. HTTP retry of either resource rereads its local state; it does not broaden or restart work. + +## Contracts + +`contracts/investment-resource-envelope.schema.json` is the minimal target cross-system artifact. It describes the changed resource envelope rather than duplicating backend `openapi.json`. + +All affected personal resource successes use `{ data, meta }`: Accounts; account and `all` Portfolio; account and `all` operations; account and `all` events. `data` is the last confirmed complete result, except that ledger first-load may return locally held rows with `meta.completeness: "incomplete"`. Where no complete result exists, `data` is `null`; a partial result must never be represented as complete. + +`meta` has five independent meanings: + +| Field | Target meaning | +| --- | --- | +| `freshness` | `fresh`/`stale` applies to the timestamp of the returned confirmed result; `unknown` applies when none exists. A failed or running attempt never makes data fresh. | +| `completeness` | `complete` only after the resource's required source result is confirmed. `incomplete` describes an in-progress/failed first-load or ledger coverage gap. | +| `lastSuccessfulAt` | Timestamp of the returned complete result, or `null` when none exists. It is not replaced by a later failed attempt. | +| `lastAttempt` | The resource-relevant hourly result: `none`, `running`, `succeeded`, `partial`, or `failed`, with nullable start/end timestamps and only safe failed section/account/error-code entries. `partial` means at least one section succeeded and at least one failed. | +| `availability` | `available` has a complete result, including `[]`; `initial-loading` and `temporarily-unavailable` have no complete result; `connection-unavailable` means the current connection cannot expose the resource. | + +Accounts also returns top-level `sync`: `null` if the current connection has no hourly attempt, otherwise the complete connection-wide last-walk status. Its `problems` list is the safe, scoped failure list for `membership`, `portfolio`, `operations`, and `events`; an account ID is present only for account-scoped sections. `sync` is observation only and is not a command or progress API. + +Events preserves the existing account and `all` scope. `from` and `to` are optional strict `YYYY-MM-DD` UTC dates with inclusive bounds; omitted `from` is the current UTC date and omitted `to` is `from + 30` calendar days. `from > to` returns `400 INVALID_EVENTS_PERIOD`, does not read a substitute range, and starts no source work. A saved forecast retains `calculatedAt` and its original inclusive calculation window. The response filters events to the intersection of that original window and the requested window. Empty intersection and no matching events are successful empty `data` with the same forecast metadata. `all` is complete only if every included account has its required complete forecast sources; it cannot hide a missing account contribution. Events' `meta.lastAttempt` reports the resource-relevant hourly events result independently of the saved forecast, so a failed update can coexist with an available forecast. + +## Data and State Semantics + +An empty confirmed Accounts list, empty Portfolio, empty event intersection, and empty event list are all available complete results, never missing data. A stale complete result remains available indefinitely until a connection boundary makes it unavailable. `Portfolio.isStale` retains its snapshot-specific meaning and is not a substitute for `meta.lastAttempt` or a frontend request error. + +Forecast publication requires complete broker and public-source results for the Portfolio Snapshot used to calculate it. It stores the Portfolio quantity basis, `calculatedAt`, and source window together. A failed refresh cannot publish a partial forecast or relabel an older forecast as calculated from a newer Portfolio. + +Successful complete Portfolio evaluations may append one exact, per-currency value-history point at the actual successful Portfolio time. Missing valuation must not be replaced with zero and cannot create a point. Retry/publication of one attempt is idempotent. History survives connection replacement/deletion but is readable only after the current connection confirms access to the same account; no public history-read endpoint is introduced. + +The connection revision is the publication fence. PUT validates the candidate token, atomically establishes a new revision and membership boundary, then triggers its first walk. DELETE is idempotent, prevents further token use, and hides current personal data. Old attempts cannot publish into a replacement connection, and neither old `sync` nor retained value history is disclosed across an unconfirmed boundary. + +## Error and Recovery Semantics + +Authentication errors remain HTTP boundary errors. No connection is `409 TBANK_NOT_CONNECTED`. A token known invalid/unauthorized from an hourly attempt is `400 INVALID_TBANK_TOKEN`; it retains the connection record but blocks personal-data disclosure and must not be masked by fallback data. A requested account outside current confirmed membership is `404`; unknown membership does not authorize an arbitrary account ID. Connection PUT validation failures remain `400 INVALID_TBANK_TOKEN` or `503 TBANK_UNAVAILABLE` and leave the prior connection unchanged. + +Temporary upstream failure during an hourly attempt is represented in `meta.lastAttempt` and, on Accounts, `sync`; it is not a synchronous GET failure. With a previous complete result, GET returns `200` and the preserved data. Without one, GET returns `{ data: null, meta }` with `initial-loading` or `temporarily-unavailable`. `503 TBANK_UNAVAILABLE` remains reserved for an HTTP request that itself cannot be served, not a completed background failure. + +Frontend recovery is limited to rereading the resource. It retains its Client Cached Snapshot during a background HTTP failure, while separately showing server stale/fallback and the failed hourly attempt where supplied. Replacing/deleting a connection invalidates personal client queries so a late response cannot revive hidden data. + +## Observability Semantics + +The durable hourly attempt is the observable unit: start time, terminal state, finish time, and safe failed section/account/error-code list. Absence of an attempt is `none` in resource `lastAttempt` and `null` in Accounts `sync`; it is not success. A running attempt has `finishedAt: null`. The status is scoped to the current connection revision and contains no token, upstream payload, cursor, or internal revision. + +Resource `lastSuccessfulAt`, freshness, completeness, and availability describe returned `data`; the hourly result describes the attempt. This separation permits a truthful response after partial/failed refresh. Frontend status text or accessible names must expose all five resource meanings and must not rely on color alone. + +## Security and NFR at System Boundaries + +All personal reads require the authenticated User and current-connection access check. T-Bank tokens remain server-only, encrypted, and absent from response metadata, OpenAPI examples, logs, and status failures. Online instrument reference failure cannot prove an instrument absent or alter personal snapshot freshness. + +The scheduler is a single durable reservation owner for the deployed single backend process. The target explicitly does not add multi-process scheduling. Local read latency is isolated from upstream latency because ordinary GETs do not invoke upstream personal data calls. The existing upstream limiter remains the boundary for PUT validation and hourly work. + +## Compatibility / Migration + +The current backend-generated `openapi.json` and frontend snapshot are identical and already expose resource envelopes, Accounts top-level `sync`, and events `sync=observe`; they are evidence of AS-IS, not the target authority. Current generated `InvestmentResourceMetaDto` uses `availability: missing|partial|complete` plus per-work `sync: idle|running|failed`, while `HourlySyncStatusDto` adds `cancelled`; these do not fully express the approved target's first-load availability, `none|succeeded|partial` attempt distinction, and resource-scoped last attempt. Current `sync=observe` and `/api/v1/tbank-sync/{id}` preserve legacy screen-work observation and must not be used to reintroduce request-started personal synchronization. + +Implementation must evolve backend Swagger DTOs/controllers and regenerate backend OpenAPI. The frontend must receive that published snapshot and regenerate its local transport types. The HTTP route identities and established Problem Details codes remain compatible unless the generated target contract explicitly changes them; no copy of the full OpenAPI belongs in this change. The target schema is intentionally additive/semantic and must be reconciled with generated field names before release. + +## Durable Documentation Candidates + +- `docs/adr/019-hourly-reads-and-online-instrument-reference.md`: clarify that hourly status is connection-scoped and that only the two first-load exceptions may acquire personal/market data. +- `docs/architecture/cross-cutting/data-freshness.md`: replace legacy request-driven freshness table and `sync=observe` language with target resource/attempt separation and `data: null` first-load states. +- `docs/domains/portfolio/flows/portfolio-loading.md`: replace screen-triggered refresh/status flow with local GET and hourly-walk flow. +- `docs/domains/income/upcoming-events.md`: describe saved forecast source-window intersection, `calculatedAt`, and independent failed hourly update state. +- `docs/adr/017-local-tbank-reads-and-background-sync.md`: mark its request-driven/manual-refresh decisions superseded by ADR 019 rather than leaving competing guidance. + +## Open Questions + +None. The approved Proposal and Delta Specs determine the target semantics; implementation will reconcile field naming and route annotations through backend-generated OpenAPI without treating current generated artifacts as a second authority. diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/contracts/investment-resource-envelope.schema.json b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/contracts/investment-resource-envelope.schema.json new file mode 100644 index 0000000..f5a181b --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/contracts/investment-resource-envelope.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "reconcile-hourly-investment-sync/investment-resource-envelope.schema.json", + "title": "Target investment resource envelope", + "description": "Target cross-system semantics only; the backend-generated OpenAPI remains the executable HTTP contract.", + "type": "object", + "required": ["data", "meta"], + "properties": { + "data": { + "description": "The last confirmed complete result, an allowed incomplete ledger first-load result, or null when no complete result is available." + }, + "meta": { "$ref": "#/$defs/resourceMeta" }, + "sync": { "$ref": "#/$defs/hourlyWalk" } + }, + "$defs": { + "resourceMeta": { + "type": "object", + "required": ["freshness", "completeness", "lastSuccessfulAt", "lastAttempt", "availability"], + "properties": { + "freshness": { "enum": ["fresh", "stale", "unknown"] }, + "completeness": { "enum": ["complete", "incomplete"] }, + "lastSuccessfulAt": { "type": ["string", "null"], "format": "date-time" }, + "lastAttempt": { "$ref": "#/$defs/attempt" }, + "availability": { "enum": ["available", "initial-loading", "temporarily-unavailable", "connection-unavailable"] } + } + }, + "attempt": { + "type": "object", + "required": ["state", "startedAt", "finishedAt", "problems"], + "properties": { + "state": { "enum": ["none", "running", "succeeded", "partial", "failed"] }, + "startedAt": { "type": ["string", "null"], "format": "date-time" }, + "finishedAt": { "type": ["string", "null"], "format": "date-time" }, + "problems": { "type": "array", "items": { "$ref": "#/$defs/problemSection" } } + } + }, + "hourlyWalk": { + "description": "Present only on Accounts; absent from other resource envelopes.", + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/attempt" } + ] + }, + "problemSection": { + "type": "object", + "required": ["section", "accountId", "errorCode"], + "properties": { + "section": { "enum": ["membership", "portfolio", "operations", "events"] }, + "accountId": { "type": ["string", "null"] }, + "errorCode": { "type": ["string", "null"] } + } + } + } +} diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/hourly-investment-walk.mmd b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/hourly-investment-walk.mmd new file mode 100644 index 0000000..7ac5aca --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/hourly-investment-walk.mmd @@ -0,0 +1,18 @@ +sequenceDiagram + participant Scheduler + participant DB as Durable reservation/status + participant TB as T-Bank and public sources + participant Store as Local resource store + participant API as Resource GET + + Scheduler->>DB: Reserve current connection walk + alt Due and no walk running + DB-->>Scheduler: Attempt with connection revision + Scheduler->>TB: Refresh membership, portfolios, mutable operations, events + Scheduler->>Store: Publish each complete successful result for the same revision + Scheduler->>DB: Settle succeeded, partial, or failed with safe failed sections + else Not due or an attempt is running + DB-->>Scheduler: No work + end + API->>Store: Read persisted result and attempt status + Store-->>API: Last confirmed data and metadata diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/read-only-resource-get.mmd b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/read-only-resource-get.mmd new file mode 100644 index 0000000..bdd2a1e --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/system-analysis/sequences/read-only-resource-get.mmd @@ -0,0 +1,13 @@ +sequenceDiagram + actor User + participant UI as Frontend + participant API as Backend resource GET + participant DB as Local store + participant TB as T-Bank + + User->>UI: Open screen or retry HTTP read + UI->>API: GET resource + API->>DB: Authorize current connection and read saved result/status + DB-->>API: data, resource state, last hourly attempt + API-->>UI: 200 { data, meta } or boundary Problem Details + Note over API,TB: No T-Bank call and no personal sync work from GET or retry diff --git a/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/tasks.md b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/tasks.md new file mode 100644 index 0000000..b6ee2ee --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reconcile-hourly-investment-sync/tasks.md @@ -0,0 +1,67 @@ +# Tasks + +## 1. Backend runtime and published contract + +- [ ] 1.1 [backend] Привести durable hourly walk, connection-revision fence и локальные resource readers к утверждённой модели: только hourly owner для обычного обновления, узкие first-load исключения, сохранение complete fallback и изолированное наблюдение resource/connection attempt. + - **Covers:** `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `portfolio-value-history` — `Полные оценки Portfolio сохраняются как история`; `portfolio-value-history` — `История оценки скрыта вне подтверждённого доступа`; `online-instrument-information` — `Справочные сведения и поиск инструментов получают онлайн`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank` + - **Source:** System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics, Security and NFR at System Boundaries; Design §§ Backend: retain storage owners and publication fences, Data / Storage Changes, Developer Verification Strategy; QA Analysis §§ Coverage Matrix, Cross-system Verification. + - **Verification:** Run the backend's relevant runtime, SQLite/migration, controller, and E2E tests proving local ordinary GET/retry has no personal upstream call; hourly cadence and dropped overlaps; narrow first-load acquisition; persisted fallback/partial results; connection replacement/deletion fences; events windows; and no token or internal details in observable status. + - **Depends on:** none + +- [ ] 1.2 [backend] Replace the shared investment resource DTO/mapper and endpoint responses with the target `{ data, meta }` projection, Accounts-only connection `sync`, then regenerate the backend OpenAPI from Swagger annotations. + - **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `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` + - **Source:** System Analysis §§ Contracts, Compatibility / Migration; Design §§ Backend: one resource-meta projection and one connection-walk projection, Contract: Swagger is produced at the DTO boundary, Contract Implementation; QA Analysis §§ Cross-system Verification, Regression Impact. + - **Verification:** Regenerate backend `openapi.json` and run the backend's OpenAPI contract/type/lint tests plus runtime response tests. Confirm semantic compatibility with `system-analysis/contracts/investment-resource-envelope.schema.json`: nullable `data`; all five required `meta` meanings; `none|running|succeeded|partial|failed`; nullable attempt fields; Accounts-only nullable top-level `sync`; established routes and Problem Details; and `sync=observe` never starting source work. + - **Depends on:** 1.1 + +## 2. Frontend generated consumer and states + +- [ ] 2.1 [frontend] Receive the published backend OpenAPI snapshot and regenerate the local transport types; project its target resource envelope directly through the existing resource-query seam without handwritten parallel types or compatibility normalization. + - **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account` + - **Source:** System Analysis § Compatibility / Migration; Design §§ Contract: Swagger is produced at the DTO boundary, Frontend: one query cache and metadata-aware screen state; QA Analysis §§ Cross-system Verification, Regression Impact. + - **Verification:** Run `openapi:check` and the frontend typecheck; verify the committed generated snapshot is the backend-published artifact and generated types expose nullable `data`, required target metadata, and Accounts-only `sync`. + - **Depends on:** 1.2 + +- [ ] 2.2 [frontend] Update Accounts, Portfolio, operations, and events query owners, page VMs, and views to render the five metadata meanings and separate server attempt, server stale, and client HTTP-read states; retain successful snapshots, keep retry/reread HTTP-only, and preserve access invalidation. + - **Covers:** `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events` + - **Source:** System Analysis §§ Error and Recovery Semantics, Observability Semantics; Design §§ Frontend: one query cache and metadata-aware screen state, UI/UX Package, Developer Verification Strategy; QA Analysis §§ Coverage Matrix: Frontend recovery and accessibility, Manual / Exploratory Scope. + - **Verification:** Run frontend unit/component tests and configured UI checks covering no automatic retry, retry as GET only, 30-minute reread, cache retention after background error, connection invalidation, no-data/empty/loaded states, distinct stale/attempt/transport warnings, keyboard-accessible status text, and narrow viewport wrapping. + - **Depends on:** 2.1 + +## 3. Durable AS-IS documentation + +- [ ] 3.1 [docs] Update `architecture/cross-cutting/data-freshness.md` with the approved hourly-owner, local-read, first-load-exception, and independent resource/attempt/client-state model. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `architecture/cross-cutting/data-freshness.md`. + - **Verification:** Compare the page against delivered runtime responses and UI: ordinary GET/retry has no personal upstream call; target generated-contract meanings, stale fallback, no-result, client read failure, and hourly failure remain distinct. + - **Depends on:** 1.2, 2.2 + +- [ ] 3.2 [docs] Update `domains/portfolio/flows/portfolio-loading.md` with read-only saved snapshot loading, 30-minute local rereads, fallback semantics, and connection/revision disclosure fences. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `domains/portfolio/flows/portfolio-loading.md`. + - **Verification:** Compare the page with the delivered Portfolio route and screen: GET/retry does not call T-Bank or expose manual sync; no-data, empty, stale, failed hourly attempt, and failed HTTP reread remain distinct. + - **Depends on:** 1.2, 2.2 + +- [ ] 3.3 [docs] Update `domains/income/upcoming-events.md` with saved forecast window intersection, `calculatedAt`, independent failed-update state, and empty/no-result semantics. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `domains/income/upcoming-events.md`. + - **Verification:** Compare the page with delivered account and `all` events responses/screens: invalid periods return `INVALID_EVENTS_PERIOD` without source work, window intersection preserves forecast metadata, and empty intersection differs from failed-refresh fallback. + - **Depends on:** 1.2, 2.2 + +- [ ] 3.4 [docs] Update the investment section of `architecture/http-api.md` to identify backend-generated OpenAPI as the executable `{ data, meta }` contract and remove the request-started screen-sync model from AS-IS guidance. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `architecture/http-api.md`. + - **Verification:** Compare prose and examples with the backend-generated OpenAPI and runtime responses: target nullability/enums and required metadata match, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work. + - **Depends on:** 1.2 + +- [ ] 3.5 [docs] Update `adr/017-local-tbank-reads-and-background-sync.md` to preserve historical context while explicitly superseding its request-driven/manual-refresh decisions through ADR 019. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `adr/017-local-tbank-reads-and-background-sync.md`. + - **Verification:** Confirm ADR 017 explicitly identifies superseded decisions, links to ADR 019 and current freshness documentation, and no longer presents manual/request-driven sync as current architecture. + - **Depends on:** 3.1, 3.6 + +- [ ] 3.6 [docs] Update `adr/019-hourly-reads-and-online-instrument-reference.md` with the current-connection-scoped hourly result, ordinary read-only GET/retry boundary, two narrow acquisition exceptions, and observation-not-command rule. + - **Covers:** `N/A` — approved documentation promotion; no additional product behavior. + - **Source:** Docs Impact § Documentation Changes: `adr/019-hourly-reads-and-online-instrument-reference.md`. + - **Verification:** Confirm ADR 019 describes only the delivered hourly model and bounded exceptions, agrees with the updated freshness page, and does not introduce another HTTP contract source. + - **Depends on:** 1.2, 2.2 diff --git a/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/.openspec.yaml b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/.openspec.yaml new file mode 100644 index 0000000..9123314 --- /dev/null +++ b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd +created: 2026-10-07 diff --git a/openspec/changes/reduce-screen-sync-polling/design.md b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/design.md similarity index 100% rename from openspec/changes/reduce-screen-sync-polling/design.md rename to openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/design.md diff --git a/openspec/changes/reduce-screen-sync-polling/proposal.md b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/proposal.md similarity index 100% rename from openspec/changes/reduce-screen-sync-polling/proposal.md rename to openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/proposal.md diff --git a/openspec/changes/reduce-screen-sync-polling/specs/frontend-data-resilience/spec.md b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/specs/frontend-data-resilience/spec.md similarity index 100% rename from openspec/changes/reduce-screen-sync-polling/specs/frontend-data-resilience/spec.md rename to openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/specs/frontend-data-resilience/spec.md diff --git a/openspec/changes/reduce-screen-sync-polling/tasks.md b/openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/tasks.md similarity index 100% rename from openspec/changes/reduce-screen-sync-polling/tasks.md rename to openspec/changes/archive/2026-10-09-reduce-screen-sync-polling/tasks.md diff --git a/openspec/changes/versioned-deployment-bundle/.openspec.yaml b/openspec/changes/archive/2026-10-09-versioned-deployment-bundle/.openspec.yaml similarity index 100% rename from openspec/changes/versioned-deployment-bundle/.openspec.yaml rename to openspec/changes/archive/2026-10-09-versioned-deployment-bundle/.openspec.yaml diff --git a/openspec/changes/versioned-deployment-bundle/design.md b/openspec/changes/archive/2026-10-09-versioned-deployment-bundle/design.md similarity index 100% rename from openspec/changes/versioned-deployment-bundle/design.md rename to openspec/changes/archive/2026-10-09-versioned-deployment-bundle/design.md diff --git a/openspec/changes/versioned-deployment-bundle/proposal.md b/openspec/changes/archive/2026-10-09-versioned-deployment-bundle/proposal.md similarity index 100% rename from openspec/changes/versioned-deployment-bundle/proposal.md rename to openspec/changes/archive/2026-10-09-versioned-deployment-bundle/proposal.md diff --git a/openspec/changes/versioned-deployment-bundle/specs/deployment-bundle/spec.md b/openspec/changes/archive/2026-10-09-versioned-deployment-bundle/specs/deployment-bundle/spec.md similarity index 100% rename from openspec/changes/versioned-deployment-bundle/specs/deployment-bundle/spec.md rename to openspec/changes/archive/2026-10-09-versioned-deployment-bundle/specs/deployment-bundle/spec.md diff --git a/openspec/changes/versioned-deployment-bundle/tasks.md b/openspec/changes/archive/2026-10-09-versioned-deployment-bundle/tasks.md similarity index 100% rename from openspec/changes/versioned-deployment-bundle/tasks.md rename to openspec/changes/archive/2026-10-09-versioned-deployment-bundle/tasks.md diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/.openspec.yaml b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/.openspec.yaml new file mode 100644 index 0000000..d2578e7 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd-v2 +created: 2026-10-12 diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/design.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/design.md new file mode 100644 index 0000000..a4795af --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/design.md @@ -0,0 +1,220 @@ +# Engineering Design + +## Context and Constraints + +- Один backend-процесс; durable fence — reservation попытки в SQLite; таймер + процесса только предлагает работу (существующий инвариант, комментарии в + `tbank-hourly.scheduler.ts`). +- Итог попытки пишется CAS-обновлением только из состояния `running`: + устаревшая публикация после появления преемника уже отклоняется существующим + fence (`settleHourly` where `state: 'running'`; `read()` при несовпадении + `progress.attempts` помечает шаг `cancelled`). +- Сегодня итог вычисляется по in-memory списку ошибок walk (пуст при + исключении — ложный `succeeded`), ошибка `settleHourly` подавляется + `.catch(() => undefined)`, `settledWorks` поллит без deadline, а id строк + шагов не персистятся на hourly-попытке (`progress: { sections: [] }`). +- Финализаторы enrichment/operations могут очистить память без durable finish — + шаг остаётся `running`. +- Топология не меняется: без lease и мультреплик; CAS-ограничения + `attempt`/`revision`/`source` не ослабляются. + +## Technical Goals / Non-Goals + +**Goals** + +- Итог попытки выводится из durable-состояния шагов; ложный `succeeded` + невозможен ни на одном error-пути. +- Каждая попытка достигает терминального итога в ограниченное время + (ограничение ожидания на каждый шаг). +- Попытка с утраченным выполнением закрывается диагностируемо (`INTERRUPTED`) + и не блокирует следующий обход дольше обычного часового срока. +- Поздняя публикация шага текущей попытки сохраняется до фиксации итога; + после появления преемника отклоняется существующим fence. + +**Non-Goals** + +- Lease, очередь, вторая реплика, параллельный scheduler. +- Отмена уже начатых обращений к T-Bank. +- Новый публичный статус попытки и изменения HTTP contract. +- Graceful drain — только если тест контролируемой остановки докажет + необходимость. + +## Implementation Impact Map + +### main + +`N/A` — workspace/dev/release orchestration не меняется. + +### frontend + +`N/A` — публичный словарь состояний и форма `meta` не меняются. + +### backend + +`affected` — координатор `tbank-hourly.scheduler.ts` (walk/settle/ +settledWorks/launch), `tbank-sync.repository.ts` (`reserveHourly`/ +`settleHourly`), персист id шагов на попытке, durable finish в финализаторах +`catalog-enrichment.service.ts` и `brokerage-operations-sync.service.ts`, +изолированные SQLite-тесты reservation/coordinator. + +### qa + +`N/A` — QA repository не существует; системная верификация остаётся ручной по +QA Analysis. + +## Decisions + +### backend + +1. **Итог выводится из durable-состояния шагов (корневой фикс).** + `settleHourly` в одной транзакции читает строки шагов попытки и вычисляет + итог: `succeeded` — только когда каждый обязательный шаг успешен и закрытие + инициировано нормальным завершением walk; `partial` — успешны некоторые; + `failed` — ни один. Вызывающие больше не передают результат и список + ошибок; walk передаёт максимум `cause: 'INTERRUPTED'` (закрытие до + нормального завершения), который капит итог до `partial`. Исключение — + обязательные секции, чей dispatch не создал durable-строку шага: walk + передаёт их как дополнительные problem-записи (наблюдаемый факт + диспатча, не результат), иначе попытка могла бы получить ложный успех. + Альтернатива + «прокидывать настоящий failures-map в catch» отклонена: не закрывает + lost-settle и краш-пути — каждый error-путь требовал бы своего guard. + Отдельный janitor-сервис отклонён: issue требует чинить существующих + владельцев, не вводить параллельный scheduler. + +2. **Id шагов персистятся на попытке в момент dispatch.** `progress` + hourly-попытки переиспользует существующую форму `children: string[]` + + `attempts: { [id]: attemptId }` (паттерн `ScreenProgress`/`progress()`). + Walk записывает id каждой строки шага сразу после dispatch, до ожидания — + иначе recovery после краша не знает множество шагов попытки, а шаг, упавший + в dispatch, исчезает из множества. + +3. **Ограниченное ожидание на каждый шаг.** `settledWorks` получает per-id + deadline: константа `WORK_WAIT_BOUND_MS = 15 * 60 * 1000`; истёкший id + возвращается как проблемный шаг с причиной `DEADLINE`. Тем же bound покрыты + и ожидания walk до появления durable-строк шагов (review-замечание, + follow-up WealthLedger/backend#78): `membership.request`/`member.done`, + `access.local` и dispatch `portfolio`/`operations`/`events.refresh` гоняются + с `WORK_WAIT_BOUND_MS` через race; истечение трактуется как проблемная + секция (как failed dispatch), для `access.local` — как закрытие + `INTERRUPTED`. Вызов не отменяется; поздняя durable-строка fenced по + `attemptId`, как любая поздняя публикация. Управляемые часы в + тестах — jest fake timers, инъекция clock в production-код не вводится. + RPC не отменяется; поздний результат шага сохраняется его собственной + строкой — учтён ли он в итоге, решает fence и момент фиксации. + +4. **In-memory активные обходы + recovery на существующем tick.** + `TbankHourlyScheduler` держит `active: Set` (attemptId): добавление + в начале `walk`, удаление в `finally`. `launch()` перед `reserveHourly` + вызывает `recoverAbandoned(userId)`: если строка hourly `running`, + `processRunId` свой и attemptId не в `active` — попытка потеряла settle и + закрывается через `settleHourly` с `cause: 'INTERRUPTED'` (итог из детей, + максимум `partial`). Это же закрывает застрявшие legacy-строки `running`. + Новых таймеров и санитаров нет — восстановление едет на существующем + минутном tick. + +5. **Restart-ветка `reserveHourly` закрывает попытку итогом.** Существующая + ветка «running + чужой `processRunId`» вместо голой записи + `state: 'cancelled'` выполняет тот же derived settle с + `cause: 'INTERRUPTED'` внутри транзакции reservation; далее прежний + `due`-гейт: новый обход — не ранее следующего часового срока от начала + попытки. `cancelled` для hourly-строк перестаёт писаться; существующие + маппинги `cancelled` в `hourly()`/`hourlyAttempt()` остаются толерантным + чтением и не удаляются. + +6. **Причина прерывания — в существующем поле `errorCode`.** `INTERRUPTED` — + уровень попытки (закрытие до нормального завершения); `DEADLINE` — уровень + шага (истечение ожидания), попадает в `errorCode` попытки как первая ошибка + секции по существующему precedence. Коды ошибок шагов остаются в + `progress.sections[].errorCode`. Поле — открытое строковое: форма ответа и + enum `TbankErrorCode` не расширяются (маркеры жизненного цикла — не ошибки + T-Bank). + +7. **Durable finish в дочерних финализаторах.** Внешний catch финализаторов + (enrichment, operations drain/step) обязан перед возвратом или пробросом + записать терминальный неуспешный результат своей строки шага существующим + путём записи ошибки, а не только очистить память. Иначе шаг остаётся + `running`, и попытка не проходит terminal barrier. + +## Cross-cutting Implementation + +- Подавление ошибки `settleHourly` (`.catch(() => undefined)`) допустимо только + как пара к решению 4: `walk` удаляет attemptId из `active` в `finally`, и + следующий tick закрывает строку recovery. Ошибка фиксируется существующим + структурным логом. +- Новых фоновых таймеров, очередей и таблиц нет; concurrency не меняется + (один walk на подключение). + +## Data / Storage Changes + +- Миграции нет: `children`/`attempts` живут в существующем JSON `progress` + строки `tbankSyncWork`; поле `errorCode` уже в схеме + (`20260924170000_persist_tbank_data/migration.sql:177`). +- Legacy-застрявшие `running` строки закрываются первым recovery-проходом + (итог из детей + `INTERRUPTED`); чтение уже закрытых строк не меняется. + +## Contract Implementation + +- HTTP contract не меняется: форма `meta` и словарь состояний + (`running`/`succeeded`/`partial`/`failed`) прежние; `cancelled` остаётся в + enum DTO как толерантное чтение. +- До закрытия change: backend-generated OpenAPI проверяется на семантическую + совместимость с текущим контрактом — изменения формы не ожидаются; новые + значения существуют только внутри открытого строкового поля диагностики и + в hourly-read наружу не попадают. + +## Developer Verification Strategy + +- Изолированные SQLite-тесты reservation/coordinator (не stub + `upcoming-events`): ветки `first`, `not-due`, `running`, `restart`, + `partial`; итог проверяется после terminal barrier. +- Инъекция ошибок отдельно в дочерние шаги и в родительский settle; + восстановление после ошибки сохранения; защита от overlap и от поздней + публикации; controlled stop/restart — нет бесконечного drain, после + остановки новые reservations не создаются. +- Управляемые часы — jest fake timers. +- Сохранить существующие наборы auth/money/publication; полный + `backend verify` на финальном diff. +- Graceful drain не добавляется, пока тест остановки не покажет + необходимость. + +## UI/UX Package + +`N/A` — UI не меняется; новый mockup не требуется. + +## Risks / Trade-offs + +- Derivation зависит от полноты `children`: пропуск записи id при dispatch + делает шаг невидимым для итога. Митигируется решением 2 (запись до await) и + тестом инъекции ошибки в dispatch. +- Per-step deadline может закрыть попытку как `partial` при медленном, но + живом шаге; поздний результат сохраняется строкой шага, данные не теряются. +- In-memory `active` не переживает рестарт — этот случай закрывает + restart-ветка reservation (решение 5). +- Константа 15 минут без конфигурации: потолок опыта одного процесса + (`ponytail:` вынести в конфиг, если медленные шаги станут регулярными). + +## Migration / Rollout / Rollback + +- Rollout: один PR в backend; миграций и флагов нет; следующий tick после + деплоя закрывает застрявшие строки. +- Rollback contract: `N/A` — нет миграции, security или public contract + изменения; откат — revert кода, уже закрытые recovery строки остаются в + корректном терминальном виде. + +## Solution Assessment + +Обязателен (concurrency/recovery). Варианты: + +- (a) Root-cause: settle выводит итог из durable-детей; recovery на + существующем tick/reservation; один guard в общей точке. +- (b) Минимальный патч: настоящий failures-map в catch плюс guard в каждом + вызывающем. +- (c) Отдельный janitor/очередь с lease. + +Выбор — (a): закрывает все error-пути одной точкой (exception, lost settle, +краш, deadline), переиспользует существующие fence и `progress`-паттерн, без +новых компонентов. (b) оставляет открытыми lost-settle и краш — каждый новый +error-путь снова требует своего guard, что и есть текущий баг. (c) отклонён: +issue явно запрещает параллельный scheduler, lease не даёт выгоды в +одно-процессной топологии, а стоимость нового компонента выше фикса. diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/docs-impact.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/docs-impact.md new file mode 100644 index 0000000..e526f8d --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/docs-impact.md @@ -0,0 +1,43 @@ +# Documentation Impact + +## Summary + +Durable docs update нужен ограниченно: наблюдаемая семантика итога часовой +попытки уже описана в AS-IS, но правила вывода итога, ограниченного ожидания и +восстановления после сбоя сохранения/перезапуска в docs отсутствуют. Переносится +только это знание; migration history остаётся в archived change. + +## Documentation Changes + +| Action | Target | Category | Resulting AS-IS | Source | Verification | +|---|---|---|---|---|---| +| update | `docs/architecture/cross-cutting/data-freshness.md` | cross-cutting | Рядом со словарём `lastAttempt` (`none`, `running`, `succeeded`, `partial`, `failed`): итог попытки выводится из фактических результатов её шагов; попытка, не завершившая обязательный шаг, не обозначается полным успехом; ожидание шага ограничено, зависший шаг учитывается как проблемный; попытка, потерявшая выполнение (сбой сохранения, остановка процесса), закрывается частичным успехом либо неуспехом и не блокирует следующий обход дольше обычного срока | System Analysis — Data and State Semantics, Error and Recovery Semantics; Design 1–5 | Страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta` | +| update | `docs/integrations/tbank.md` | integration | В разделе «Состояние и ошибки»: диагностические причины жизненного цикла попытки (`INTERRUPTED` — прервана до нормального завершения, `DEADLINE` — истечение ожидания шага) — внутренняя диагностика, не ошибки T-Bank; в hourly-read наружу не попадают, начатые обращения к T-Bank не отменяются | System Analysis — Observability Semantics; Design 3, 6 | Раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла | + +PR реализации: WealthLedger/docs#37 (open) — обе страницы обновлены, diff +соответствует таблице выше (задачи 6.1 и 6.2 верифицируемы по нему). + +## Diagram Promotion + +Не переносить. Sequence-диаграммы (`hourly-attempt-terminal-barrier.mmd`, +`hourly-bounded-wait.mmd`, `hourly-recovery.mmd`) — planning evidence этого +change; AS-IS достаточно текстового описания семантики в `data-freshness.md`. + +## ADR Decision + +`N/A` — архитектурно значимого решения с новыми alternatives/consequences нет: +чинятся существующие владельцы строк в прежней одно-процессной топологии, +без новых компонентов и контрактов (Solution Assessment остаётся в archived +change). + +## Glossary / Navigation Impact + +Нет: новые термины (`terminal barrier`, преемник попытки) — внутренние; +публичный словарь состояний не расширяется, navigation не меняется. + +## No-Change Rationale + +`docs/architecture/containers.md` не меняется: раздел «Сохранение и +восстановление данных» описывает backup/restore уровня базы, а не восстановление +попытки приложением. Остальные AS-IS страницы не затрагиваются: HTTP contract +и словарь состояний не изменились. diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/proposal.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/proposal.md new file mode 100644 index 0000000..497a4c5 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/proposal.md @@ -0,0 +1,114 @@ +# Proposal + +## Why + +Почасовая синхронизация T-Bank может завершиться в неконсистентном состоянии +после ошибки сохранения: незавершённая попытка записывается как полный успех, +ошибка финализации либо подавленная ошибка закрытия оставляет попытку в +состоянии выполнения навсегда, а локальная reservation не позволяет запустить +следующий обход в том же процессе. Пользователь видит ложный успех и перестаёт +получать обновления до перезапуска backend, а неудавшееся ожидание дочерних +шагов может ждать без ограничения. Механизм подтверждён статически (BE-002, +BE-015, RISK-001, RISK-002, TQ-003); частота проявления в production пока не +установлена. + +## What Changes + +- Итог попытки определяется фактическим результатом каждого обязательного шага: + невыполненный шаг даёт частичный успех либо неуспех, но не полный успех, а + уже полученные успехи отдельных счетов и разделов сохраняются. +- Прерванная попытка (ошибка, сбой сохранения, остановка процесса, истечение + ожидания) обозначается частичным успехом либо неуспехом и не получает нового + публичного состояния; причина прерывания фиксируется существующим + диагностическим полём. +- Попытка достигает итога в ограниченное время: ожидание дочернего шага + ограничено, а зависшая попытка не остаётся в выполнении бесконечно. +- Состояние в SQLite восстанавливается после сбоя сохранения: брошенная попытка + закрывается диагностируемым итогом, ожидание завершается, следующая + допустимая попытка может стартовать без перезапуска процесса. +- Восстановление не создаёт перекрытия обходов и не принимает устаревшие + результаты: поздняя публикация относительно актуального преемника + отклоняется. + +## Scope + +### In Scope + +- Итоговый статус попытки часового обхода и обработка ошибок в дочерних + финализаторах. +- Ограниченное ожидание дочерних шагов и восстановление после ошибок + завершения шага, публикации результата и закрытия попытки. +- Защита от перекрытия обходов и от поздней публикации устаревших данных при + восстановлении. +- Контролируемая остановка и перезапуск процесса и их влияние на незавершённую + попытку, подтверждённые тестами. +- Наблюдаемый результат попытки: время начала, состояние, безопасный перечень + проблемных счетов и разделов. + +### Out of Scope / Non-Goals + +- Распределённая очередь, lease, поддержка нескольких реплик и изменение + топологии deployment. +- Удаление данных. +- Ослабление существующих ограничений CAS для попытки, ревизии подключения и + источника данных. +- Новый публичный статус попытки и изменения HTTP contract. +- Новый параллельный scheduler или замена владельцев строк синхронизации. +- Проверка часовой cadence и cleanup workflow — независимые задачи. + +## Capabilities + +### New Capabilities + +- Нет. + +### Modified Capabilities + +- `hourly-investment-sync`: итог попытки из фактических результатов шагов, + ограниченное ожидание шага, диагностируемое закрытие брошенной попытки, + отсутствие перекрытий и устаревшей публикации. + +## Impact + +- **Пользователь**: достоверный статус последнего обхода вместо ложного успеха + и бесконечного «выполняется» после сбоя сохранения; обновления продолжаются + без перезапуска процесса. +- **Backend**: координатор часового обхода, дочерние финализаторы обновления + данных и reservation попытки в SQLite. +- **Frontend**: не затрагивается — наблюдаемый словарь состояний и форма + ответов не меняются. +- **Внешняя система T-Bank**: contract не меняется; начатые запросы к T-Bank + могут завершаться поздно и должны корректно отклоняться как устаревшие. +- **Процессы**: диагностика причин прерывания попыток и восстановление после + ошибок сохранения. + +## Success Criteria + +- Попытка с невыполненным обязательным шагом никогда не наблюдается как полный + успех; частичные успехи счетов и разделов остаются видимыми. +- После однократной ошибки завершения шага, публикации результата либо закрытия + попытки и последующего восстановления ожидание завершается, а следующая + допустимая попытка стартует. +- Устаревшая попытка наблюдается относительно актуального преемника; поздняя + публикация старого процесса отклоняется и не меняет данные. +- Координатор различает состояния `first`, `not-due`, `running`, `restart` и + `partial`; итог проверяется после terminal barrier. +- Сценарий остановки и перезапуска доказан тестами: нет бесконечного ожидания, + после остановки новые попытки не создаются. +- Сохранены существующие наборы проверок auth/money/publication, полный + `backend verify` на финальном изменении зелёный. + +## Assumptions / Dependencies + +- Топология остаётся одно-процессной; межпроцессный lease и мультреплики не + требуются. +- Истечение ожидания не отменяет уже начатый запрос к T-Bank; защита + опирается на существующие ограничения CAS и правило срока следующего обхода. +- Причина прерывания фиксируется существующим диагностическим полём, поэтому + HTTP contract и публичный словарь состояний не меняются; упомянутое в задаче + состояние `interrupted` реализуется как частичный успех либо неуспех. +- Graceful drain добавляется только если тест контролируемой остановки докажет + такую необходимость. +- Правило устаревшей публикации относится к итогу попытки и данным её шагов; + существующие требования `tbank-data-sync` уже запрещают выдавать неполный или + продолжающийся результат за полный, поэтому их изменение не планируется. diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/qa-analysis.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/qa-analysis.md new file mode 100644 index 0000000..d556bb7 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/qa-analysis.md @@ -0,0 +1,90 @@ +# QA Analysis + +## Verification Scope + +### In Scope + +- Дельта `hourly-investment-sync`: «Последний результат обхода доступен только + для наблюдения» (MODIFIED) и ADDED «Каждая попытка обхода достигает итога в + ограниченное время», «Восстановление после сбоя сохранения не блокирует + последующие обходы», «Устаревшая публикация не влияет на актуальную попытку». +- Системные проверки через resource GET `meta` и durable-состояние попытки + после контролируемых действий backend (инъекции сбоев, остановка/запуск + процесса). + +### Out of Scope + +- Внутренние unit/component тесты — developer-owned, определены Design. +- Неизменяемые поведения `tbank-data-sync`/`tbank-connection` — только + regression. +- Проверка часовой cadence (T03) и cleanup workflow — независимые задачи. + +## Coverage Matrix + +| Requirement / Scenario | Системная проверка | +| --- | --- | +| Последний результат… — Сбой после частичного обновления | Инъекция сбоя шага после успешного обновления Portfolio одного счёта: GET обозначает частичный успех с проблемным шагом, данные первого счёта остаются обновлёнными | +| Последний результат… — Сбой до первого обновления | Инъекция сбоя до завершения любого шага: GET обозначает неуспех, не полный успех | +| Последний результат… — Частичный успех / Наблюдение не запускает обход / Старая граница подключения | Regression существующего поведения `meta` | +| Каждая попытка… — Шаг не завершается | Зависший шаг с управляемыми часами: попытка достигает терминального итога в ограниченное время, шаг учтён как проблемный | +| Каждая попытка… — Наблюдение не ждёт без ограничения | Наблюдение результата получает итоговое состояние, а не бесконечное «выполняется» | +| Восстановление… — Потерянное закрытие попытки | Ошибка закрытия при живом процессе: следующий проход закрывает попытку диагностируемо, следующая допустимая попытка стартует | +| Восстановление… — Перезапуск процесса | Остановка процесса во время обхода и повторный запуск: брошенная попытка закрыта, новый обход не ранее следующего часового срока, перекрытия нет | +| Устаревшая публикация… — После появления преемника | Позднее завершение шага старой попытки после запуска следующей: данные не изменились, итог актуальной попытки не изменился | +| Устаревшая публикация… — До фиксации итога | Позднее завершение шага до фиксации итога попытки: результат сохранён и учтён в итоге | + +## Cross-system Verification + +- T-Bank upstream не отменяется при истечении ожидания; поздний ответ после + появления преемника не изменяет данные (проверяется на симулированном + upstream в контролируемой среде backend-тестов). +- Внешние ошибки шагов отображаются только безопасным перечнем проблемных + счетов/разделов; причины `INTERRUPTED`/`DEADLINE` не раскрывают наружу + ничего сверх существующей формы `meta`. + +## Risk-based Priorities + +- **Critical**: ложный полный успех (целостность данных), бесконечное + «выполняется», перекрытие обходов, порча данных устаревшей публикацией. +- **Compatibility**: публичный словарь состояний `meta` неизменен; + `cancelled` в enum остаётся толерантным значением. +- **Money/data integrity**: успешные результаты шагов не теряются при сбоях + остальных. + +## Test Data and Environment Needs + +- Изолированный backend на SQLite с управляемыми часами и симулированным + upstream T-Bank (контролируемая тестовая среда; внутренние mocks — на + усмотрение разработчиков по Design). +- Подключение с не менее чем двумя доступными счетами для сценариев частичного + успеха. + +## Manual / Exploratory Scope + +- Ручная проверка отображения результата последнего обхода на инвестиционных + экранах после симулированного сбоя сохранения. +- Exploratory: повторные остановки процесса в разных фазах обхода; наблюдение + результата сразу после старта процесса. + +## Automation Candidates + +- Black-box API-кандидаты (GET `meta` после инъекций сбоев, контролируемая + остановка) — `deferred until QA repo exists`. + +## Observability Verification + +- Причины `INTERRUPTED`/`DEADLINE` наблюдаются в durable-диагностике попытки, + а не в публичном API; подавленная ошибка закрытия фиксируется структурным + логом backend. + +## Regression Impact + +- Существующие наборы auth/money/publication и полный `backend verify`. +- Happy path часового обхода, первый обход после подключения, скрытие + результата прежней границы подключения. + +## Gaps / Blockers + +- Gaps нет. Graceful drain остаётся условным: добавляется только если + controlled-stop тест докажет необходимость (routing: пересмотр Design/QA + после теста). diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/specs/hourly-investment-sync/spec.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/specs/hourly-investment-sync/spec.md new file mode 100644 index 0000000..f24c79d --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/specs/hourly-investment-sync/spec.md @@ -0,0 +1,59 @@ +## MODIFIED Requirements + +### Requirement: Последний результат обхода доступен только для наблюдения +Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию. + +#### Scenario: Частичный успех +- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился +- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт + +#### Scenario: Наблюдение не запускает обход +- **WHEN** User запрашивает результат последнего обхода +- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank + +#### Scenario: Старая граница подключения +- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода +- **THEN** система не раскрывает результат прежней границы подключения + +#### Scenario: Сбой после частичного обновления +- **WHEN** попытка прервана ошибкой либо сбоем сохранения после обновления Portfolio одного доступного счёта +- **THEN** результат попытки обозначает частичный успех с проблемными шагами, а не полный успех, а обновлённый Portfolio сохраняется + +#### Scenario: Сбой до первого обновления +- **WHEN** попытка прервана до обновления любого обязательного шага +- **THEN** результат попытки обозначает неуспех, а не полный успех + +## ADDED Requirements + +### Requirement: Каждая попытка обхода достигает итога в ограниченное время +Каждая начатая попытка часового обхода MUST достигать итогового состояния в ограниченное время. Ожидание завершения каждого обязательного шага MUST быть ограничено; шаг, не завершившийся в это ограничение, MUST быть учтён как проблемный, а попытка MUST быть закрыта частичным успехом либо неуспехом после того, как каждый обязательный шаг достиг терминального результата либо был учтён как проблемный. Истечение ограничения MUST NOT отменять уже начатые шаги. + +#### Scenario: Шаг не завершается +- **WHEN** обязательный шаг не покидает состояние выполнения дольше ограничения ожидания +- **THEN** шаг учитывается как проблемный, попытка закрывается частичным успехом либо неуспехом, а её состояние перестаёт быть выполняющимся + +#### Scenario: Наблюдение не ждёт без ограничения +- **WHEN** попытка не может завершиться из-за незавершённых шагов +- **THEN** наблюдение результата получает итоговое состояние в ограниченное время и не ожидает завершения бесконечно + +### Requirement: Восстановление после сбоя сохранения не блокирует последующие обходы +Если попытка осталась в состоянии выполнения после сбоя завершения шага, публикации результата либо закрытия попытки, система MUST привести её к диагностируемому итоговому состоянию: частичный успех либо неуспех с сохранёнными результатами шагов, а не полный успех. Последующий обход MUST становиться допустимым по обычному правилу срока, отсчитанному от начала этой попытки. Восстановление MUST NOT создавать перекрывающийся обход и MUST NOT повторять уже полученные результаты шагов. + +#### Scenario: Потерянное закрытие попытки +- **WHEN** закрытие попытки не сохранилось, а процесс продолжает работать +- **THEN** система закрывает попытку итогом из фактических результатов шагов, наблюдение получает итоговое состояние, а следующая допустимая попытка может стартовать + +#### Scenario: Перезапуск процесса +- **WHEN** процесс остановился после начала попытки и запускается снова +- **THEN** брошенная попытка обозначается частичным успехом либо неуспехом с сохранёнными результатами шагов, а новый обход начинается не ранее следующего часового срока от её начала + +### Requirement: Устаревшая публикация не влияет на актуальную попытку +Результат шага попытки, у которой появился преемник, MUST отклоняться как устаревший: он MUST NOT обновлять данные и MUST NOT учитываться в итоге актуальной попытки. Шаг устаревшей попытки MUST наблюдаться как устаревший относительно актуального преемника. Результат шага текущей попытки, полученный до фиксации её итога, MUST сохраняться и учитываться в этом итоге независимо от того, наступил ли он до или после истечения ограничения ожидания. + +#### Scenario: Поздняя публикация после появления преемника +- **WHEN** шаг старой попытки завершается после запуска следующей попытки +- **THEN** его результат отклоняется как устаревший, данные не обновляются, а итог актуальной попытки не меняется + +#### Scenario: Поздняя публикация до фиксации итога +- **WHEN** шаг завершился после истечения ограничения ожидания, но итог его попытки ещё не зафиксирован +- **THEN** его результат сохраняется и учитывается при выводе итога этой попытки diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/analysis.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/analysis.md new file mode 100644 index 0000000..ae62614 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/analysis.md @@ -0,0 +1,140 @@ +# System Analysis + +## Requirement Coverage + +| Requirement / Scenario | Flow | Contract / Semantics | +| --- | --- | --- | +| Последний результат обхода… — Сбой после частичного обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation, terminal barrier | +| Последний результат обхода… — Сбой до первого обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation | +| Последний результат обхода… — Частичный успех | `hourly-attempt-terminal-barrier.mmd` | Step results preserved | +| Каждая попытка обхода достигает итога в ограниченное время — Шаг не завершается | `hourly-bounded-wait.mmd` | Bounded wait per required step | +| Каждая попытка обхода достигает итога в ограниченное время — Наблюдение не ждёт без ограничения | `hourly-bounded-wait.mmd` | Bounded terminal outcome | +| Восстановление после сбоя сохранения… — Потерянное закрытие попытки | `hourly-recovery.mmd` | Recovery of lost settle | +| Восстановление после сбоя сохранения… — Перезапуск процесса | `hourly-recovery.mmd` | Recovery of abandoned attempt | +| Устаревшая публикация… — Поздняя публикация после появления преемника | `hourly-recovery.mmd` | Successor fencing | +| Устаревшая публикация… — Поздняя публикация до фиксации итога | `hourly-bounded-wait.mmd` | Late publication counts before outcome is recorded | + +## Systems and Responsibilities + +- **Hourly walk coordinator** (backend): резервирует попытку часового обхода для + текущего подключения, запускает обязательные шаги, ждёт их терминального + результата с ограничением, выводит итог попытки и закрывает её. +- **Durable attempt status** (SQLite): единственная durable-запись попытки + (состояние, диагностическая причина, результаты шагов) и источник итога для + наблюдения и восстановления. +- **Child step owners**: шаги membership, Portfolio, операций и будущих событий + публикуют свой полный результат в durable-хранилище и обозначают свой + терминальный результат; финализатор шага обязан фиксировать результат даже + при ошибке. +- **T-Bank и публичные источники**: внешние данные; начатые обращения не + отменяются по внутреннему ограничению ожидания. +- **Resource GET observer**: читает сохранённые данные и `meta` с состоянием + последней попытки; наблюдение не запускает обход. + +## Interaction Flows + +- `sequences/hourly-attempt-terminal-barrier.mmd` — попытка, шаги, terminal + barrier и вывод итога из фактических результатов шагов. +- `sequences/hourly-bounded-wait.mmd` — ограничение ожидания шага, закрытие + попытки по его истечении и учёт позднего результата до появления преемника. +- `sequences/hourly-recovery.mmd` — восстановление после сбоя сохранения и + после перезапуска процесса, отклонение устаревшей публикации. + +## Contracts + +- Новых и изменённых межсистемных HTTP/event contracts нет. Публичный словарь + состояний последней попытки (`выполнение`, `полный успех`, `частичный успех`, + `неуспех`) и форма `{ data, meta }` не меняются. +- Машинно-читаемый target contract в `system-analysis/contracts/` не создаётся: + изменения ограничены уточнением значений внутри существующих полей. +- Диагностическое поле причины остаётся открытым строковым полем; значения + причин прерывания (`INTERRUPTED` — ошибка, сбой сохранения, остановка + процесса; `DEADLINE` — истечение ограничения ожидания шага) дополняют + существующие коды ошибок шагов и не требуют изменения формы ответа. +- Compatibility check до закрытия change: backend-generated OpenAPI остаётся + семантически совместимым с текущим контрактом (ожидаются только дополнительные + значения открытого строкового поля диагностики). + +## Data and State Semantics + +- **Попытка**: `running` → терминальный итог (`succeeded` | `partial` | `failed`). + Попытка, не завершившая обязательный шаг, не может получить `succeeded`. +- **Terminal barrier**: итог попытки выводится только после того, как каждый + обязательный шаг достиг терминального результата либо учтён как истёкший по + ограничению ожидания. Итог не вычисляется по неполному списку шагов. +- **Вывод итога**: `succeeded` — только когда все обязательные шаги успешны и + координатор сам зафиксировал закрытие попытки; `partial` — успешны некоторые + шаги; `failed` — не успешен ни один. Успешные результаты шагов сохраняются + независимо от итога. +- **Причина прерывания**: попытка, закрытая до нормального завершения всех + шагов, сохраняет причину прерывания в диагностическом поле и не превышает + `partial`. +- **Internal coordinator decisions** `first`, `not-due`, `running`, `restart`, + `partial`: внутренние метки решения о запуске и проверки итога после terminal + barrier; в HTTP contract не входят и публично не наблюдаемы. +- **Шаги**: каждый шаг владеет своей durable-строкой результата; строка шага + устаревшей попытки помечается устаревшей относительно преемника. + +## Error and Recovery Semantics + +- **Сбой шага** (внешняя ошибка T-Bank): шаг фиксирует терминальный неуспех со + своим кодом ошибки; остальные шаги продолжаются; итог — `partial` или `failed`. +- **Сбой сохранения** (завершение шага, публикация результата, закрытие + попытки): ошибка не подавляется молча в durable-состоянии; попытка, оставшаяся + `running`, закрывается восстановлением по фактическим результатам шагов. +- **Остановка процесса**: начатая попытка остаётся `running` в SQLite; + следующий запуск процесса закрывает её как прерванную (`partial` или `failed` + с сохранёнными результатами шагов) и не создаёт перекрывающийся обход. +- **Ограничение ожидания**: истечение ограничивает только ожидание шага; уже + начатое обращение к T-Bank не отменяется. Его поздний результат сохраняется и + учитывается при выводе итога своей попытки, если итог ещё не зафиксирован, и + отклоняется как устаревший после появления преемника. +- **Повтор**: неуспешная попытка не повторяется ранее следующего часового срока, + отсчитанного от начала этой попытки; восстановление не сокращает и не + продлевает этот срок. +- **Overlapping**: следующая попытка не резервируется, пока предыдущая не + терминальна; восстановление обязано сначала закрыть предыдущую. + +## Observability Semantics + +- Наблюдаемый итог попытки и безопасный перечень проблемных счетов/разделов — + существующее обязательство; значения итога не расширяются. +- Семантические коды причин: `INTERRUPTED` — попытка прервана до нормального + завершения (ошибка, сбой сохранения, остановка процесса); `DEADLINE` — шаг не + завершился в ограничение ожидания. Коды сохраняются в диагностическом поле + попытки и/или шага и дополняют коды ошибок шагов без изменения формы `meta`. +- SDK/exporter/logger — вне границ этого изменения; используется существующий + структурный лог backend. + +## Security and NFR at System Boundaries + +- Граница наблюдения попытки остаётся границей текущего подключения: результат + прежней границы не раскрывается (существующее обязательство). +- Ограниченное время достижения итога попытки — наблюдаемое обязательство + надёжности: ожидание шага и ожидание наблюдателя ограничены. +- Новых trust boundaries, authn/authz требований нет. + +## Compatibility / Migration + +- Схема данных и HTTP contract не меняются; миграция не требуется. +- Уже застрявшие строки попыток в состоянии выполнения закрываются первым + проходом восстановления по правилам этого изменения: итог из фактических + результатов шагов, причина прерывания, без полного успеха. Это ожидаемое + разовое приведение legacy-состояния к диагностируемому виду. +- Совместимость с одно-процессной топологией сохраняется; межпроцессный lease + не требуется. + +## Durable Documentation Candidates + +- `docs/integrations/tbank.md` (раздел «Состояние и ошибки»): итог часовой + попытки, причины прерывания и правило ограниченного ожидания как часть + наблюдаемого состояния интеграции. +- `docs/architecture/containers.md` (раздел «Сохранение и восстановление + данных»): восстановление попытки после сбоя сохранения и после перезапуска + процесса как durable-свойство системы. +- Итоговое решение о переносе принимает `docs-impact`. + +## Open Questions + +- Нет блокирующих вопросов. Частота проявления механизма в production остаётся + не установленной и не влияет на contract. diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-attempt-terminal-barrier.mmd b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-attempt-terminal-barrier.mmd new file mode 100644 index 0000000..55a8412 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-attempt-terminal-barrier.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + participant Coordinator + participant DB as Durable attempt status + participant Steps as Child step owners + participant TB as T-Bank and public sources + participant API as Resource GET + + Coordinator->>DB: Reserve attempt for current connection + DB-->>Coordinator: Attempt with connection revision + Coordinator->>Steps: Run required steps (membership, portfolios, operations, events) + Steps->>TB: Fetch missing data + Steps->>DB: Publish complete successful step result + Steps->>DB: Record terminal step result (success or failure with code) + Coordinator->>DB: Read step results after terminal barrier + alt All required steps succeeded + Coordinator->>DB: Settle succeeded + else Some required steps succeeded + Coordinator->>DB: Settle partial with safe problem sections + else No required step succeeded + Coordinator->>DB: Settle failed with safe problem sections + end + API->>DB: Read last attempt result + DB-->>API: Time, outcome, safe problem list diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-bounded-wait.mmd b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-bounded-wait.mmd new file mode 100644 index 0000000..d92ef62 --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-bounded-wait.mmd @@ -0,0 +1,24 @@ +sequenceDiagram + participant Coordinator + participant DB as Durable attempt status + participant Steps as Child step owners + participant TB as T-Bank and public sources + + Coordinator->>Steps: Run required steps + Steps->>TB: Start upstream call + Coordinator->>Coordinator: Wait for step result within bound + alt Step reaches terminal result in time + Steps->>DB: Publish complete result + Coordinator->>DB: Settle attempt after terminal barrier + else Wait bound expires + Note over Coordinator,TB: Upstream call is not cancelled + Coordinator->>DB: Record step problem cause DEADLINE + Coordinator->>DB: Settle partial or failed after terminal barrier + Steps->>TB: Upstream call completes later + Steps->>DB: Publish late result for the same attempt + alt Outcome not recorded yet and no successor + DB-->>Steps: Late result is preserved and counts at outcome derivation + else Successor attempt already started + DB-->>Steps: Publication rejected as stale + end + end diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-recovery.mmd b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-recovery.mmd new file mode 100644 index 0000000..4da444f --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/system-analysis/sequences/hourly-recovery.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + participant Coordinator + participant DB as Durable attempt status + participant Steps as Child step owners + + Note over Coordinator,DB: Attempt is still running but its walk no longer executes + Coordinator->>DB: Detect running attempt without active walk + alt Process still alive after lost settle + Coordinator->>DB: Read actual step results + Coordinator->>DB: Close attempt as partial or failed with cause INTERRUPTED + else Process restarted after stop + Coordinator->>DB: Read actual step results + Coordinator->>DB: Close abandoned attempt as partial or failed with cause INTERRUPTED + end + DB-->>Coordinator: Attempt is terminal, no overlap possible + Coordinator->>DB: Reserve next attempt at next hourly term from attempt start + alt Next term reached + DB-->>Coordinator: Successor attempt starts + Steps->>DB: Late publication from previous attempt + DB-->>Steps: Publication rejected as stale relative to successor + else Next term not reached + DB-->>Coordinator: No work (not-due) + end diff --git a/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/tasks.md b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/tasks.md new file mode 100644 index 0000000..88686ca --- /dev/null +++ b/openspec/changes/archive/2026-10-12-harden-hourly-sync-lifecycle/tasks.md @@ -0,0 +1,111 @@ +# Tasks + +## 1. Данные попытки и вывод итога + +- [x] 1.1 [backend] `settleHourly` выводит итог попытки из durable-строк шагов + в одной транзакции; `cause: 'INTERRUPTED'` капит итог до `partial`; + вызывающие не передают результат и список ошибок + - **Covers:** `hourly-investment-sync` — «Последний результат обхода доступен только для наблюдения» + - **Source:** Design — Decisions 1, 6; System Analysis — Data and State Semantics + - **Verification:** изолированный SQLite-тест: сбой после частичного обновления даёт `partial` с сохранённым успехом; сбой до первого шага даёт `failed`; ложный `succeeded` невозможен ни на одном error-пути + - **Depends on:** none +- [x] 1.2 [backend] id строк шагов персистятся в `progress` hourly-попытки + (`children` + `attempts`) в момент dispatch, до ожидания шага + - **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы», «Устаревшая публикация не влияет на актуальную попытку» + - **Source:** Design — Decisions 2, 5 + - **Verification:** SQLite-тест: после потери выполнения попытки множество шагов восстанавливается из `progress`, включая шаг, упавший в dispatch + - **Depends on:** 1.1 + +## 2. Ограниченное ожидание и восстановление + +- [x] 2.1 [backend] ограничение ожидания на каждый шаг в `settledWorks` + (константа ~15 минут, per-id deadline); истёкший шаг учитывается проблемным + с причиной `DEADLINE`; начатые обращения к T-Bank не отменяются; часы в + тестах — jest fake timers без инъекции clock в production-код + - **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время» + - **Source:** Design — Decision 3; System Analysis — Error and Recovery Semantics + - **Verification:** SQLite-тест с зависшим шагом на fake timers: попытка терминальна в ограниченное время, наблюдение не ждёт бесконечно + - **Depends on:** 1.1 +- [x] 2.2 [backend] in-memory `active`-реестр обходов + `recoverAbandoned` + перед reservation: running-попытка своего `processRunId` без активного walk + закрывается derived settle с `INTERRUPTED`; застрявшие legacy-строки + закрываются первым проходом + - **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Потерянное закрытие попытки) + - **Source:** Design — Decisions 4, 6; System Analysis — Error and Recovery Semantics + - **Verification:** SQLite-тест: подавленная ошибка закрытия при живом процессе — следующий проход закрывает попытку, следующая допустимая попытка стартует + - **Depends on:** 1.1, 1.2 +- [x] 2.3 [backend] restart-ветка `reserveHourly` закрывает чужую running-попытку + derived settle с `INTERRUPTED` вместо голого `cancelled`; `due`-гейт и + защита от overlap прежние + - **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Перезапуск процесса) + - **Source:** Design — Decision 5 + - **Verification:** SQLite-тест перезапуска процесса: брошенная попытка закрыта `partial`/`failed` с сохранёнными результатами шагов, новый обход не ранее следующего часового срока, перекрытия нет + - **Depends on:** 1.1, 1.2 + +- [x] 2.4 [backend] ожидания walk до durable-строк шагов + (`membership.request`/`member.done`, `access.local`, dispatch + `portfolio`/`operations`/`events.refresh`, post-wait `events.get`) + ограничены тем же `WORK_WAIT_BOUND_MS` через race; истечение — проблемная + секция либо `INTERRUPTED`-закрытие без отмены вызова + (WealthLedger/backend#78) + - **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время» + - **Source:** Design — Decision 3; review specs#33 (CRITICAL) + - **Verification:** SQLite-тесты с никогда не завершающимися dispatch/membership/`done`/`get`: попытка терминальна в ограниченное время, поздние вызовы не отменяются + - **Depends on:** 2.1 + +## 3. Финализаторы шагов + +- [x] 3.1 [backend] внешний catch финализаторов шагов (enrichment, operations + drain/step) записывает терминальный неуспешный результат своей строки шага + существующим путём записи ошибки перед возвратом или пробросом + - **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время» (шаг покидает running) + - **Source:** Design — Decision 7 + - **Verification:** SQLite-тест с инъекцией ошибки финализатора: строка шага терминальна, попытка проходит terminal barrier + - **Depends on:** none + +## 4. Поздняя публикация + +- [x] 4.1 [backend] подтверждено fence-поведение публикации шага: результат до + фиксации итога попытки сохраняется и учитывается в итоге; после появления + преемника — отклоняется без изменения данных и итога актуальной попытки + - **Covers:** `hourly-investment-sync` — «Устаревшая публикация не влияет на актуальную попытку» + - **Source:** Design — Decisions 1–3; System Analysis — Error and Recovery Semantics + - **Verification:** SQLite-тесты поздней публикации: до фиксации итога — учтён; после successor — данные и итог актуальной попытки не изменились + - **Depends on:** 1.1, 1.2, 2.1 + +## 5. Системная верификация backend + +- [x] 5.1 [backend] полный набор изолированных SQLite-тестов coordinator: + ветки `first`, `not-due`, `running`, `restart`, `partial`, итог после + terminal barrier; инъекции ошибок отдельно в дочерние шаги и родительский + coordinator; controlled stop/restart без бесконечного drain и без новых + reservations после остановки; scheduler stub `upcoming-events` не является + единственным доказательством + - **Covers:** `hourly-investment-sync` — все Requirements дельты + - **Source:** Design — Developer Verification Strategy; QA Analysis — Coverage Matrix, Risk-based Priorities + - **Verification:** полный `backend verify` зелёный на финальном diff; наборы auth/money/publication сохранены + - **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1 +- [x] 5.2 [backend] backend-generated OpenAPI семантически совместим с + текущим контрактом: форма `meta` и словарь состояний не изменились, новые + значения только внутри открытого строкового поля диагностики + - **Covers:** `N/A` — защита контракта (изменений контракта нет) + - **Source:** Design — Contract Implementation; System Analysis — Contracts + - **Verification:** diff сгенерированного OpenAPI до/после изменения не содержит shape-изменений + - **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1 + +## 6. Canonical documentation + +- [x] 6.1 [docs] обновить `data-freshness.md`: вывод итога из фактических + результатов шагов, ограниченное ожидание шага, закрытие брошенной попытки + без блокировки расписания — рядом со словарём `lastAttempt` + - **Covers:** `N/A` — docs promotion + - **Source:** docs-impact — Documentation Changes, строка 1 + - **Verification:** страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta` + - **Depends on:** 5.1 +- [x] 6.2 [docs] обновить раздел «Состояние и ошибки» `tbank.md`: причины + жизненного цикла `INTERRUPTED`/`DEADLINE` — внутренняя диагностика, не + ошибки T-Bank; начатые обращения не отменяются + - **Covers:** `N/A` — docs promotion + - **Source:** docs-impact — Documentation Changes, строка 2 + - **Verification:** раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла + - **Depends on:** 5.1 diff --git a/openspec/changes/stock-screener/.openspec.yaml b/openspec/changes/stock-screener/.openspec.yaml new file mode 100644 index 0000000..368f09c --- /dev/null +++ b/openspec/changes/stock-screener/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd-v2 +created: 2026-10-10 diff --git a/openspec/changes/stock-screener/design.md b/openspec/changes/stock-screener/design.md new file mode 100644 index 0000000..8d8a04a --- /dev/null +++ b/openspec/changes/stock-screener/design.md @@ -0,0 +1,93 @@ +# Engineering Design + +## Context and Constraints + +Текущий текстовый поиск акций использует `FindInstrument` и ограничивает результат 30 выпусками до получения подробных сведений и цены. Он не может быть источником скринера. Новый поток использует полный `Shares(INSTRUMENT_STATUS_ALL)` и целевой контракт из [System Analysis](system-analysis/analysis.md); существующие ссылки на страницы отдельных акций продолжают использовать точный UID. Справочные сведения остаются online-only, а личный портфель не является каталогом. + +## Technical Goals / Non-Goals + +- Получить полный список один раз при явном поиске, отфильтровать и зафиксировать упорядоченную выдачу до первой страницы; чтение страницы не вызывает новый поиск T-Invest. +- Изолировать результат от другого пользователя и от другого поколения его подключения; обеспечить ограниченный расход памяти без нового долговременного справочника. +- Не обогащать все инструменты ценами и фундаментальными показателями и не менять старый текстовый endpoint или поиск облигаций. + +## Implementation Impact Map + +### main + +N/A: workspace, Compose и release orchestration не меняются. + +### frontend + +Affected: раздел `/stocks` становится формой скринера с черновыми и применёнными условиями, явным запуском и страницами результата; использует опубликованный API contract. + +### backend + +Affected: отдельные операции скринера, получение списка `Shares`, фильтрация и временный результат с безопасной выдачей страниц. Текущий поиск и публичные страницы отдельных акций остаются доступными. + +### qa + +N/A: отдельного QA repository пока нет; системная проверка описана в `qa-analysis.md`. + +## Decisions + +### main + +N/A. + +### frontend + +На странице акций форма содержит необязательные текст, сектор, тип акции, валюту, площадку, страну риска и верхнюю границу лота. Черновик полей отделён от последнего применённого поиска; изменение поля само по себе не отправляет запрос и не заменяет результат. Нажатие «Найти» всегда отправляет новый POST, сбрасывает номер страницы и связывает ответ со своим `searchId`. Последующие страницы читаются GET; поздний ответ прежнего запуска не заменяет результат нового. Ошибка или потеря снимка отображается отдельно от подтверждённого пустого списка. + +Повторно использовать имеющуюся карточку `InstrumentSearchCard` с точным `instrumentUid` в ссылке. Текущая карточка цены не подходит для скринера без цены: показывать только доступные каталожные поля и не приписывать им котировку. Список опций берётся из известной предметной лексики и доступных значений каталога, если они предоставлены; фильтрация выполняется на сервере. Если динамические опции требуют дополнительного контракта, вернуть этот вопрос в System Analysis до реализации, а не добавлять неоговорённый запрос. + +### backend + +Расширить T-Bank client операцией `Shares` через существующий gRPC transport, очередь, deadline и retry. Перед выбором страницы проверить полноту полученного ответа, обязательные признаки покупки/торговли и все выбранные поля на каждом инструменте. Сначала отбор по инструментам, затем детерминированный представитель по нормализованному ISIN (при отсутствии — UID), сортировка по тикеру/UID, затем страницы. Не переносить имя, режим или характеристики между UID при дедупликации. + +Временный снимок полного результата на backend, привязанный к пользователю и revision подключения, содержит выбранные элементы и `asOf`; случайный `searchId` — только указатель, не пропуск доступа. Держать снимок в ограниченной RAM с конечным сроком жизни и пределом числа результатов. Истечение, вытеснение или рестарт дают 404 при чтении страницы и предложение начать новый поиск; никогда не пересчитывать старую страницу по новому каталогу. Для одного экземпляра backend достаточно локального хранилища в памяти; распределённое хранение не требуется текущему deployment. При каждом GET проверить ownership и revision до выдачи страницы. Повторный POST всегда обращается к T-Invest, даже при одинаковых условиях. + +### qa + +N/A. + +## Cross-cutting Implementation + +Не передавать T-Bank token в браузер или результат; служебные события регистрируют исход поиска, время каталога, количество исходных инструментов/выпусков и отсутствие снимка без полного текста поискового условия. При смене подключения прежняя revision теряет доступ к снимку независимо от того, успел ли он вытесниться. Отказ источника не публикует частичный снимок и не превращается в пустую страницу. Ограничить размер памяти/число одновременных результатов; локальный rate limiter остаётся единственным владельцем gRPC-повторов. + +## Data / Storage Changes + +Prisma schema и миграция не нужны: новый результат поиска краткоживущий и не является самостоятельной долговременной копией каталога. После перезапуска backend прежние `searchId` более недоступны. Персональные сохранённые данные не меняются. + +## Contract Implementation + +Backend DTO и Swagger metadata задают обе операции согласно [target OpenAPI](system-analysis/contracts/stock-screener.openapi.yaml). Валидация входа и статусы 400/401/404/503 соответствуют утверждённым ошибкам; success shape включает `items`, `searchId`, `asOf`, `page`, `hasNext`. Сгенерированный backend OpenAPI сравнить семантически с target contract до переноса snapshot в frontend и проверить опубликованный artifact и сгенерированные клиентские типы. Не подменять утверждённый контракт отдельным frontend DTO. Старый `GET /api/v1/stocks` остаётся совместимым. + +## Developer Verification Strategy + +Backend owner проверяет отбор по обоим флагам, все поля условий, граничный лот, дедупликацию после отбора, порядок страниц и привязку к пользователю/revision на собственных тестовых границах; отдельно HTTP validation и соответствие generated OpenAPI target. Frontend owner проверяет разделение черновика и применённых условий, повторный POST, переход между страницами и отображение пустого результата/ошибки на уровне страницы. На этапе реализации применяются соответствующие repo-local verify gates; проверки не требуют живого T-Invest token. + +## UI/UX Package + +**Applicability and scope:** new/changed UI для страницы `/stocks`. Пакет полностью inline; графические материалы не требуются. + +**Package revision:** 1, `wealth-ledger-specs:openspec/changes/stock-screener/design.md`. + +**Screens, states and interactions:** необязательный текст, шесть фильтров и кнопка «Найти»; до запуска нет утверждения об отсутствии акций. После запуска — загрузка, карточки и время списка либо подтверждённое пустое состояние; ошибка источника отдельно с возможностью повторить поиск. Кнопки перехода к страницам показывают тот же снимок; при недоступном снимке — предложение нового поиска. Черновые изменения не меняют показанный результат до нажатия. Формулировка доступности указывает на признаки каталога, а не на возможность немедленной сделки. + +**Responsive behavior:** форма перестраивается в одну колонку на узком экране, карточки и навигация доступны без горизонтального прокручивания. + +**Reused source and differences:** повторно используются текущий раздел акций, его компоненты карточки и состояния загрузки/ошибки; точный вход в текущий AS-IS — `frontend/src/pages/stocks/ui/StocksPage/StocksPage.view.tsx`, `frontend/src/features/instrument-search/ui/InstrumentSearch/InstrumentSearch.view.tsx`. Скринер заменяет автоматический поиск при вводе на явный запуск и не обязан показывать цену. Долговременное обоснование текущего поведения — `docs/domains/instruments/stocks.md`; сам пакет остаётся по locator версии выше и доступен после архива. + +## Risks / Trade-offs + +- Полный `Shares(ALL)` может быть объёмным; предельное потребление памяти и допустимую длительность live-поиска нужно измерить до rollout. Локальный снимок экономит повторные вызовы и сохраняет порядок, но теряется при рестарте или вытеснении; предусмотрен явный повторный поиск. +- Опции фильтров по сектору/стране могут изменяться вместе с каталогом. Нельзя обещать полноту пользовательского выбора, если показывать только фиксированные значения без подтверждения состава каталога. +- Поток не знает права конкретного счёта и состояние торгов; текст интерфейса не должен обещать немедленную покупку. + +## Migration / Rollout / Rollback + +Сначала backend реализует и публикует совместимый контракт, затем frontend получает новый artifact и использует скринер; действующий текстовый endpoint остаётся доступным. При откате frontend продолжает работать со старым endpoint; временные снимки можно удалить без миграции. Изменений личных данных и разрушительных миграций нет. + +## Solution Assessment + +Сопоставлены: (1) фильтрация первых 30 результатов `FindInstrument` — дешёвая, но неверная полнота, отклонена; (2) сохранённый постоянный каталог с индексацией — облегчает массовые запросы, но противоречит online-only границе и добавляет синхронизацию; (3) полный online `Shares` и ограниченный краткоживущий снимок результата — соответствует полноте и стабильным страницам при текущем single-instance deployment. Последний вариант принят для изменения. При реальной потребности в нескольких экземплярах потребуется отдельное согласование shared storage или stateless модели страниц, а не молчаливое изменение контракта. diff --git a/openspec/changes/stock-screener/docs-impact.md b/openspec/changes/stock-screener/docs-impact.md new file mode 100644 index 0000000..bb8366c --- /dev/null +++ b/openspec/changes/stock-screener/docs-impact.md @@ -0,0 +1,32 @@ +# Documentation Impact + +## Summary + +После реализации нужна актуализация существующих AS-IS страниц: раздел «Акции» перестанет быть только текстовым поиском. Целевой контракт и мотивы выбора временного снимка остаются в change; документация описывает получившуюся функцию и границы данных. + +## Documentation Changes + +| Action | Target | Category | Resulting AS-IS | Source | Verification | +| --- | --- | --- | --- | --- | --- | +| update | `docs/domains/instruments/stocks.md` | domain | Описать скринер с явной кнопкой поиска, необязательным текстом и шестью условиями, отбор только по подтверждённым признакам покупки и торговли через API, одну карточку на выпуск, стабильные страницы по 30 и время получения каталога. Разъяснить, что это не проверка возможности немедленной сделки и что подтверждённая пустота отличается от отказа источника. Прежнее описание автопоиска на `/stocks` заменить на фактическое поведение; сохранённый текстовый API и переход к отдельной акции описывать отдельно. | [Delta spec](specs/stock-screener/spec.md), [System Analysis](system-analysis/analysis.md), [Design](design.md) | Сверить страницу с реализованным UI, порядком выдачи и доступными полями; не называть признак покупки гарантией сделки или цену условием скринера. | +| update | `docs/integrations/tbank.md` | integration | Кратко объяснить источник полного online-каталога для скринера и смысл признаков доступности; не смешивать справочный каталог с личным портфелем или торговым статусом. | [System Analysis](system-analysis/analysis.md#contracts) | Сверить с фактическим вызовом полного списка акций и опубликованным поведением backend, не дублировать протокол целиком. | +| update | `docs/architecture/http-api.md` | architecture | Отметить семантику нового запуска поиска и чтения страницы зафиксированного результата: повторный запуск получает новый список, чтение страницы сохраняет состав и время, недоступный результат не становится пустым. Ссылаться на backend-generated OpenAPI для точной формы операций. | [Target contract](system-analysis/contracts/stock-screener.openapi.yaml), [System Analysis](system-analysis/analysis.md#data-and-state-semantics) | Сверить описание с опубликованным backend OpenAPI и фактической обработкой ошибок, не переносить YAML в docs. | +| update | `docs/overview.md` | overview | Задача «Изучить конкретный инструмент» включает подбор акций по параметрам наряду с открытием отдельной акции и поиском облигаций. | [Proposal](proposal.md), [Delta spec](specs/stock-screener/spec.md) | Убедиться, что обзор ссылается на актуальную страницу акций и не обещает фильтры по финансовым показателям. | +| update | `docs/README.md` | overview | Строка навигации «Акции и облигации» явно называет скринер акций, не создавая новую страницу ради функции. | [Delta spec](specs/stock-screener/spec.md) | Проверить ссылку на существующий раздел акций и согласованность с обзором. | +| update | `docs/glossary.md` | domain | Определить «Скринер акций» как подбор выпусков по условиям из каталога с указанием времени получения; отличить его от текстового поиска конкретного инструмента, личного портфеля и подтверждения исполнимости сделки. | [Delta spec](specs/stock-screener/spec.md), [System Analysis](system-analysis/analysis.md#data-and-state-semantics) | Определение не содержит маршрутов, классов, механизма хранения или перечня HTTP-полей. | + +## Diagram Promotion + +По умолчанию не переносить `system-analysis/sequences/stock-screener.mmd`: он объясняет согласование change и не нужен для понимания устойчивого пользовательского поведения сверх тематической страницы. Если после реализации поток страниц окажется существенным для AS-IS архитектуры, пересмотреть отдельно по фактическому состоянию, а не копировать диаграмму автоматически. + +## ADR Decision + +N/A: краткоживущий снимок результатов — обратимый способ обеспечить стабильные страницы при текущем одном экземпляре backend. Альтернативы и последствия уже записаны в Design; нового долгоживущего инфраструктурного стандарта не вводится. + +## Glossary / Navigation Impact + +Добавить один термин в существующий `docs/glossary.md`; обновить существующие строки `docs/README.md` и `docs/overview.md`. Новая страница или раздел индекса не требуются. + +## No-Change Rationale + +`docs/architecture/cross-cutting/data-freshness.md` уже различает online-справочные сведения и сохранённые личные данные; обновление нужно лишь при изменении этой границы при реализации. Прочие страницы, включая облигации, не затрагиваются. Канонический OpenAPI в docs не создаётся. diff --git a/openspec/changes/stock-screener/proposal.md b/openspec/changes/stock-screener/proposal.md new file mode 100644 index 0000000..75cdba9 --- /dev/null +++ b/openspec/changes/stock-screener/proposal.md @@ -0,0 +1,51 @@ +# Proposal + +## Why + +Раздел акций сейчас позволяет искать по тексту, но не помогает найти подходящие акции по характеристикам без заранее известного названия или тикера. Пользователю нужен скринер, который проверяет условия по каталогу доступных к покупке акций и показывает результаты без ограничения исходного набора первыми совпадениями текстового поиска. + +## What Changes + +- В разделе акций появляется поиск по выбранным параметрам с явным запуском кнопкой «Найти»; строка названия или тикера остаётся необязательным условием. +- Результаты включают только акции, доступные для покупки и торговли через API по сведениям каталога T-Invest на момент получения списка. Это не гарантия исполнения заявки конкретного пользователя прямо сейчас. +- Пользователь получает устойчиво упорядоченную постраничную выдачу по 30 выпусков и видит время получения списка. + +## Scope + +### In Scope + +- Необязательные условия по сектору, типу акции, валюте расчётов, площадке, стране риска и максимальному размеру лота; необязательный текстовый поиск. +- Одна карточка на выпуск (ISIN), при отсутствии ISIN — отдельная карточка на инструмент; выбор представителя выпуска сохраняет ссылку на конкретный инструмент. +- Новый поиск после изменения условий или повторного нажатия «Найти» возвращает на первую страницу; переход между страницами относится к одному полученному списку. +- Неизвестное значение выбранного условия не считается совпадением. + +### Out of Scope / Non-Goals + +- Просмотр всех акций, включая недоступные для покупки, и переключатель «Все акции». +- Фильтрация по цене, дивидендной доходности, капитализации и финансовым мультипликаторам. +- Проверка личной возможности совершить сделку, её исполнения или актуального состояния биржевой сессии. +- Изменение страниц облигаций и состава личного портфеля. + +## Capabilities + +### New Capabilities + +- `specs/stock-screener/spec.md` — отбор и просмотр акций по параметрам каталога. + +### Modified Capabilities + +- Нет: существующее требование `specs/online-instrument-information/spec.md` об онлайн-получении сведений остаётся в силе. + +## Impact + +Затронуты пользовательский раздел акций, получение каталога и представление результатов. Скринер зависит от доступности T-Invest; поиск и навигация по выпускам должны согласовываться с уже существующей страницей акции. Личные финансовые данные и портфель не становятся источником полного каталога для скринера. + +## Success Criteria + +- Пользователь без названия или тикера выбирает условия, нажимает «Найти» и получает полный относительно выбранных условий список доступных к покупке выпусков, а не только отфильтрованные первые результаты текстового поиска. +- Текстовое условие совместимо с фильтрами; неизвестные значения не создают ложных совпадений. +- Повторный поиск показывает результат нового получения каталога с отметкой времени и начинает выдачу с первой страницы; листание ранее полученного результата не даёт пропусков или дублей. + +## Assumptions / Dependencies + +- Каталог T-Invest предоставляет признаки доступности покупки и торговли через API, необходимые характеристики акций и время получения сведений. Отсутствие или недоступность онлайн-сведений не означает подтверждённого отсутствия акций. diff --git a/openspec/changes/stock-screener/qa-analysis.md b/openspec/changes/stock-screener/qa-analysis.md new file mode 100644 index 0000000..c2ed321 --- /dev/null +++ b/openspec/changes/stock-screener/qa-analysis.md @@ -0,0 +1,58 @@ +# QA Analysis + +## Verification Scope + +### In Scope + +Новая capability `stock-screener`: полный online-поиск доступных к покупке акций, шесть фильтров и необязательный текст, выпускная дедупликация, стабильные страницы и различение подтверждённой пустоты от ошибки. Проверка перехода по точному инструменту и совместимости старых адресов акций. + +### Out of Scope + +Финансовые показатели, исполнение заявки, личная возможность покупки, изменение облигаций, проверка цены или актуальности биржевой сессии. + +## Coverage Matrix + +| Requirement / Scenario в `specs/stock-screener/spec.md` | Проверка на уровне системы | +| --- | --- | +| «Скринер ищет среди доступных к покупке акций» / «Поиск без названия или тикера» | Отправить пустые условия; убедиться, что результат получен без текста и содержит выпуски из полного доступного каталога, включая кандидатов вне первых 30 совпадений старого поиска; показать время списка. | +| Тот же requirement / «Акция недоступна для покупки» | Сопоставить инструменты со всеми комбинациями двух флагов: в результат попадают только подтверждённые `true` + `true`. | +| «Условия скринера применяются совместно» / «Несколько условий без текста», «Текст совместно с характеристиками» | Выбрать по очереди и в сочетании все шесть условий, дополнить названием/тикером; проверить пересечение по полному каталогу до страниц. | +| Тот же requirement / «Неизвестный сектор», «Граница максимального лота» | Отсутствующее выбранное свойство исключает результат; лот, равный границе, включён, больший и неизвестный исключены. | +| «Результат представляет отдельные выпуски» / три сценария | Один ISIN в нескольких режимах даёт одну карточку; только подходящий режим даёт ссылку на него; разные ISIN и инструменты без ISIN не склеиваются. | +| «Страницы относятся к одному результату поиска» / «Переход к следующей странице» | Выдача более 30 выпусков: страницы имеют общий `asOf`, нет повторов/пропусков, порядок по тикеру и UID; каждая содержит не более 30. | +| Тот же requirement / «Повторный поиск», «Поля изменены, но поиск не запущен» | Черновые поля не подменяют выдачу; повторное «Найти» запускает новый поиск, первую страницу и новое время списка. | +| «Отсутствие совпадений отличается от недоступности поиска» / оба сценария | Успешный поиск без совпадений показывает пустое состояние; недоступный каталог — ошибку с возможностью повторить, без утверждения об отсутствии акций. | + +## Cross-system Verification + +Сопоставить обе HTTP-операции с [утверждённым target contract](system-analysis/contracts/stock-screener.openapi.yaml): обязательность полей, схема ошибок и статусы, авторизация, страницы одного `searchId`. Проверить отказ или неполноту источника, смену подключения между страницами, недоступный/чужой `searchId`, устаревший ответ прежнего поиска после нового; ни один случай не должен возвращать пустую подтверждённую страницу или пересчитывать старый результат. Проверить, что `Shares` запрашивается для полного каталога до отбора, а переход «Далее» не запускает новый поиск. Дублирования/перестановки на границе страниц не допускаются. + +## Risk-based Priorities + +1. Критично: ложная полнота при фильтрации только первых 30, неправильно подтверждённая доступность покупки, выдача чужого или старого результата подключения. +2. Высокий риск: дубли/пропуски выпуска между страницами, смешение атрибутов разных UID, неполный ответ как успешная пустота. +3. Средний риск: несоответствие текста и фильтров, различие времени снимка на страницах, сломанные ссылки на отдельную акцию и старый текстовый маршрут. + +## Test Data and Environment Needs + +Управляемый полный каталог со следующими группами: более 30 подходящих выпусков; одинаковый ISIN в разных режимах, разные ISIN одного эмитента и отсутствующий ISIN; значения каждого фильтра, пропуски полей, пограничный размер лота; комбинации двух флагов покупки и торговли. Два пользователя и два последовательных подключения одного пользователя позволяют проверить область доступа. Предусмотреть системный сценарий отказа или неполного ответа T-Invest и недоступного результата между страницами; токены и личные финансовые данные для этого не нужны. + +## Manual / Exploratory Scope + +Проверить поиск пустой формой и заполненной формой на широком и узком экране; состояние до поиска, загрузку, подтверждённую пустоту, ошибку и повтор. С клавиатуры пройти поля, запуск и навигацию страниц; убедиться, что активные условия и время результата различимы, ссылка ведёт к выбранной акции, а подпись «доступна к покупке» не выдаёт каталожный признак за обещание сделки. + +## Automation Candidates + +Black-box проверки HTTP-контракта и сценариев поиска/листинга/доступа; сквозной сценарий формы, фильтров, повторного запуска и страницы акции. Repository automation: **deferred until QA repo exists**. Этот artifact не определяет внутренние тесты frontend/backend. + +## Observability Verification + +На системной границе проверить отличимость успешного пустого поиска от отказа источника, фиксацию `asOf` и количества исходных/подходящих выпусков при успехе, регистрации недоступного результата и отказа чужому пользователю. Секрет подключения и полный текст запроса не должны попадать в наблюдаемые события. + +## Regression Impact + +Старый текстовый `GET /api/v1/stocks?query=...`, прямые страницы по тикеру/UID и поиск облигаций остаются действующими. Смена подключения по-прежнему отделяет личные данные; новая выдача акций не меняет портфель. + +## Gaps / Blockers + +Для управляемой системной проверки нужен способ получить предсказуемый каталог и вызвать отказ источника и потерю результата: тестируемость среды адресуется Engineering Design, без требования к внутренней реализации. Динамическая полнота опций фильтров не определена контрактом; если от неё зависит достижимость акций пользователем, вернуть вопрос в System Analysis до approval Design. diff --git a/openspec/changes/stock-screener/specs/stock-screener/spec.md b/openspec/changes/stock-screener/specs/stock-screener/spec.md new file mode 100644 index 0000000..139d8f7 --- /dev/null +++ b/openspec/changes/stock-screener/specs/stock-screener/spec.md @@ -0,0 +1,76 @@ +## Purpose + +Определяет поиск доступных к покупке акций по характеристикам всего доступного каталога, просмотр совпавших выпусков и навигацию по результату поиска. + +## ADDED Requirements + +### Requirement: Скринер ищет среди доступных к покупке акций +Система SHALL по нажатию «Найти» искать по всему доступному на момент запроса каталогу акций, не ограничивая исходный набор первыми совпадениями текстового поиска. В результаты MUST включаться только акции, для которых в полученных сведениях подтверждены и доступность покупки, и возможность торговли через API. Скринер MUST показывать время получения списка и MUST NOT представлять эти признаки как гарантию, что конкретный пользователь сможет исполнить заявку прямо сейчас. + +#### Scenario: Поиск без названия или тикера +- **WHEN** пользователь не ввёл текст и нажал «Найти» +- **THEN** система ищет по доступному каталогу и показывает выпуски, удовлетворяющие условиям и обоим признакам доступности + +#### Scenario: Акция недоступна для покупки +- **WHEN** у акции отсутствует подтверждение доступности покупки или торговли через API +- **THEN** система не включает её в результаты скринера независимо от остальных совпавших условий + +### Requirement: Условия скринера применяются совместно +Система SHALL позволять независимо задавать необязательные условия по сектору, типу акции, валюте расчётов, площадке, стране риска и максимальному количеству бумаг в лоте, а также необязательный текстовый запрос по названию или тикеру. После нажатия «Найти» система SHALL возвращать только акции, удовлетворяющие всем заданным условиям. Отсутствующее или неизвестное значение выбранного условия MUST NOT считаться совпадением. + +#### Scenario: Несколько условий без текста +- **WHEN** пользователь выбрал сектор и валюту, оставил текст пустым и нажал «Найти» +- **THEN** в результате остаются только доступные к покупке акции выбранного сектора и валюты + +#### Scenario: Текст совместно с характеристиками +- **WHEN** пользователь ввёл название или тикер, выбрал тип акции и нажал «Найти» +- **THEN** система отбирает совпадения по тексту и типу среди доступного каталога до разделения результата на страницы + +#### Scenario: Неизвестный сектор +- **WHEN** пользователь выбрал сектор, а у акции сектор не указан +- **THEN** акция не считается совпадением + +#### Scenario: Граница максимального лота +- **WHEN** задано максимальное количество бумаг в лоте +- **THEN** акции с известным размером лота не больше указанного включаются при соблюдении остальных условий, а акции с неизвестным или большим лотом исключаются + +### Requirement: Результат представляет отдельные выпуски +Система SHALL показывать не более одной карточки для каждого выпуска с одинаковым ISIN; акции с разными ISIN SHALL оставаться отдельными результатами. При отсутствии ISIN отдельным результатом SHALL быть инструмент с собственным идентификатором. Представитель выпуска MUST выбираться среди инструментов, удовлетворяющих заданным условиям, а карточка MUST вести к выбранному инструменту. + +#### Scenario: Несколько подходящих режимов одного выпуска +- **WHEN** несколько инструментов с одним ISIN удовлетворяют всем условиям +- **THEN** система показывает одну карточку выпуска со ссылкой на один из этих инструментов + +#### Scenario: Один режим не соответствует условиям +- **WHEN** два инструмента имеют один ISIN, но только один удовлетворяет всем условиям +- **THEN** карточка выпуска ведёт к соответствующему условиям инструменту + +#### Scenario: Разные выпуски одного эмитента +- **WHEN** обыкновенная и привилегированная акции имеют разные ISIN и обе удовлетворяют условиям +- **THEN** система показывает их как отдельные результаты + +### Requirement: Страницы относятся к одному результату поиска +Система SHALL упорядочивать совпадения по тикеру, затем по идентификатору инструмента, и показывать не более 30 выпусков на странице. Переход между страницами одного поиска MUST сохранять состав и порядок полученного результата без пропусков и повторов. Изменение полей без нажатия «Найти» MUST NOT заменять текущий результат; повторное нажатие «Найти», в том числе без изменения условий, SHALL запускать новый поиск и возвращать к первой странице. + +#### Scenario: Переход к следующей странице +- **WHEN** пользователь открывает следующую страницу полученного результата +- **THEN** он видит следующие до 30 выпусков в том же порядке и с тем же временем получения списка, без повторения выпусков предыдущей страницы + +#### Scenario: Повторный поиск +- **WHEN** пользователь изменил условия или оставил их прежними и нажал «Найти» повторно +- **THEN** система получает новый результат с новым временем получения списка и показывает его первую страницу + +#### Scenario: Поля изменены, но поиск не запущен +- **WHEN** пользователь изменяет условия и ещё не нажал «Найти» +- **THEN** ранее полученные страницы не подменяются результатом для новых условий + +### Requirement: Отсутствие совпадений отличается от недоступности поиска +Система MUST показывать пустой результат только после успешного поиска без совпадений. Если получить сведения для поиска не удалось, система SHALL сообщать о недоступности поиска и MUST NOT выдавать отсутствие акций за подтверждённый результат. + +#### Scenario: Успешный поиск без совпадений +- **WHEN** поиск завершился успешно, но ни одна акция не удовлетворяет условиям +- **THEN** система показывает, что подходящие акции не найдены + +#### Scenario: Источник каталога недоступен +- **WHEN** поиск не может получить сведения о каталоге +- **THEN** система сообщает об ошибке получения результата, не утверждая, что подходящих акций нет diff --git a/openspec/changes/stock-screener/system-analysis/analysis.md b/openspec/changes/stock-screener/system-analysis/analysis.md new file mode 100644 index 0000000..2d6c6d3 --- /dev/null +++ b/openspec/changes/stock-screener/system-analysis/analysis.md @@ -0,0 +1,67 @@ +# System Analysis + +## Requirement Coverage + +| Требование delta spec | Межсистемная граница и поток | +| --- | --- | +| Скринер ищет среди доступных к покупке акций | Запуск поиска, полный список Shares, обязательные признаки и время получения: [поиск](sequences/stock-screener.mmd), [контракт](contracts/stock-screener.openapi.yaml). | +| Условия скринера применяются совместно | Необязательные параметры в запросе, отбор по всему полученному списку до разделения на страницы; неизвестное не совпадает. | +| Результат представляет отдельные выпуски | После отбора группировка по нормализованному ISIN, иначе по UID; карточка выбранного представителя с точным UID. | +| Страницы относятся к одному результату поиска | Создание нового результата при каждом запуске, чтение последующих страниц того же результата по непрозрачному идентификатору; не более 30 выпусков на страницу. | +| Отсутствие совпадений отличается от недоступности поиска | Успешный пустой набор имеет `200` и `items: []`; ошибка каталога или потеря результата возвращает ошибку, не пустую страницу. | + +## Systems and Responsibilities + +- Пользователь задаёт условия и запускает поиск в приложении. +- Frontend хранит черновик условий отдельно от применённого поиска, показывает карточки, время получения и управляет навигацией по страницам. Он не отбирает только первые найденные строки и не склеивает результаты разных поисков. +- Backend проверяет локальную сессию и действующее подключение T-Bank, получает полный список акций, применяет обязательные признаки и выбранные условия, выбирает один подходящий инструмент на выпуск, фиксирует результат и выдаёт страницы. Инструмент и личный портфель — разные данные. +- T-Invest поставляет список акций с характеристиками и признаками доступности. Это источник сведений для нового поиска, но не подтверждение доступа конкретного счёта к сделке. + +## Interaction Flows + +[stock-screener.mmd](sequences/stock-screener.mmd) описывает новый поиск без текста, переход к следующей странице того же результата, повторный поиск, неподтверждённую доступность инструмента, отказ внешнего источника и отсутствие сохранённого результата. Чтение карточки выбранной акции после перехода по ссылке относится к существующему маршруту инструмента. + +## Contracts + +- **Frontend → Backend**, HTTP JSON, новая пара операций `POST /api/v1/stocks/screener/search` и `GET /api/v1/stocks/screener/search/{searchId}`. Целевой контракт версии `/api/v1`: [stock-screener.openapi.yaml](contracts/stock-screener.openapi.yaml). Каждый POST — новый поиск даже при совпадающих параметрах; повтор того же POST не идемпотентен в смысле получения данных. GET следующей страницы не повторяет обращение к каталогу. Ответ содержит `items`, `asOf`, `searchId`, `page`, `hasNext`. `searchId` непрозрачен и принадлежит аутентифицированному пользователю и текущему подключению; `page` — номер страницы с единицы. Контракт не задаёт срок хранения результата и не позволяет подменить применённые условия в запросе страницы. +- **Backend → T-Invest**, существующий gRPC over TLS: `InstrumentsService.Shares(InstrumentsRequest)` с `instrument_status = INSTRUMENT_STATUS_ALL` возвращает список `Share`. Базовый список не должен заранее исключать выпуск, у которого оба необходимых признака подтверждены. Отбор по сектору, типу, валюте, площадке, стране риска и лоту выполняется после получения всего списка: этих параметров нет в `InstrumentsRequest`. Ожидаются `buy_available_flag`, `api_trade_available_flag`, `uid`, `isin`, `ticker`, `name`, `sector`, `share_type`, `currency`, `real_exchange`, `country_of_risk`, `lot`. `FindInstrument` с обязательной поисковой строкой не является источником полного скринера. +- Контракт старого `GET /api/v1/stocks?query=...` не меняется в этой версии. Его обязательный текст, ограничение и текущая форма ответа продолжают работать для существующих потребителей; новое представление не выводится из его первых 30 результатов. + +## Data and State Semantics + +Источник истины для нового поиска — полученный целиком ответ Shares, а не накопленные инструменты портфеля или прежние текстовые запросы. Backend фиксирует полный набор совпавших выпусков и время успешного получения каталога до выдачи первой страницы. Пустая выдача подтверждается только успешным получением списка. Результат поиска отделён от долговременного самостоятельного справочника инструментов; правила существующего `online-instrument-information` остаются в силе. + +Признаки `buy_available_flag = true` и `api_trade_available_flag = true` обязательны вместе. Неизвестное либо неподтверждённое значение любого из них исключает инструмент. Эти признаки относятся к снимку каталога, а не к биржевой сессии, счёту пользователя или исполнению заявки. + +Применённые условия сравниваются с данными конкретного инструмента: текст — с названием или тикером, выбранные категории — с соответствующими значениями, `maxLot` — с известным положительным размером лота не больше границы. Отсутствие выбранного свойства исключает инструмент; отсутствие незаданного свойства не мешает совпадению. Страна риска — код страны основного бизнеса, а не юрисдикция регистрации. «Площадка» означает `real_exchange`, а не секцию/режим `class_code` и не дилерский `instrument_exchange`. + +После отбора инструменты с одинаковым нормализованным ISIN представляют один выпуск; пустой ISIN заменяется точным UID, а не общим пустым ключом. При нескольких подходящих инструментах одного выпуска выбор представителя должен быть детерминированным; он определяет ссылку и все сведения карточки, данные режимов не смешиваются. Затем выпуски упорядочиваются по тикеру и UID выбранного инструмента и делятся на страницы по 30. `asOf` и порядок неизменны для всех страниц одного `searchId`. Повторное нажатие «Найти» не использует старый результат как новый, даже если условия совпадают. + +## Error and Recovery Semantics + +- Неверные границы или формат условий / страницы: `400` с публичной ошибкой в стандартном профиле. Отсутствующая сессия: `401`. Отсутствующее/недействительное подключение T-Bank: соответствующая ошибка доступа подключения; секрет и upstream-исключение не раскрываются. +- Источник каталога недоступен, возвращает неполный или неинтерпретируемый список: ошибка запроса, не `items: []`. При новом поиске предыдущий успешно показанный результат не становится доказательством нового поиска. Пользователь может повторить запуск. +- Идентификатор результата больше не доступен или не принадлежит текущему пользователю/подключению: ошибка вместо пустой страницы или пересчёта по новому каталогу. Frontend предлагает выполнить новый поиск. Потеря результата в рамках действующего поиска — техническая деградация; время и данные прошлого результата не выдаются за новый. +- Нет утверждений о «прямо сейчас можно купить»: статус торгов и права конкретного счёта не проверяются. Недоступная цена/логотип не меняет состав подходящих выпусков. + +## Observability Semantics + +Существенны попытка нового поиска, успешное получение каталога (`asOf`, число исходных инструментов, подходящих выпусков), технический отказ источника, чтение страницы недоступного результата и отказ доступа к чужому результату. Секрет подключения, пользовательский токен и полный текст запроса не нужны для диагностики. Успешный пустой результат отличим от отказа источника. + +## Security and NFR at System Boundaries + +Обе HTTP-операции требуют локальной аутентификации; доступ к T-Invest выполняется только на backend с токеном текущего подключения. Идентификатор результата не является правом доступа и не переносит данные между пользователями или поколениями подключения. Полный каталог нужен до пагинации, чтобы 30 карточек не превратились в ограничение области поиска. Значение `asOf` относится к получению каталога, не к актуальности возможности купить позже. Количественная задержка/SLA для получения списка пока не установлена; не следует обещать мгновенную выдачу при сбое внешнего сервиса. + +## Compatibility / Migration + +Новый маршрут добавляется без изменения действующего текстового поиска и адресов отдельных акций. Frontend переходит на новый контракт для скринера; старый маршрут остаётся доступен его текущим потребителям. После реализации backend-generated OpenAPI должен быть семантически совместим с целевым контрактом в архивируемом change. Документ в этом change не становится вторым постоянным источником исполняемой схемы. + +## Durable Documentation Candidates + +- Новое назначение раздела акций, набор фильтров и смысл «доступна к покупке по данным каталога» в описании домена акций. +- Граница между online-каталогом, страницей акции и личным портфелем; снимок результата, время получения и стабильные страницы. +- Семантика новых HTTP-операций на уровне описания API без дублирования machine-readable OpenAPI. Окончательный выбор принадлежит `docs-impact`. + +## Open Questions + +Нет блокирующих вопросов по согласованным границам поведения. Выбор механизма хранения результата и допустимой внутренней длительности его доступности принадлежит Engineering Design; потеря результата остаётся ошибкой, а не молчаливой заменой поиска. diff --git a/openspec/changes/stock-screener/system-analysis/contracts/stock-screener.openapi.yaml b/openspec/changes/stock-screener/system-analysis/contracts/stock-screener.openapi.yaml new file mode 100644 index 0000000..18dd447 --- /dev/null +++ b/openspec/changes/stock-screener/system-analysis/contracts/stock-screener.openapi.yaml @@ -0,0 +1,177 @@ +openapi: 3.0.3 +info: + title: WealthLedger stock screener target contract + version: 1.0.0 + description: Целевой контракт change; исполняемый HTTP-контракт после реализации генерирует backend. +paths: + /api/v1/stocks/screener/search: + post: + operationId: createStockScreenerSearch + summary: Получить новый результат скринера по полному каталогу акций + security: + - bearer: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StockScreenerCriteria' + responses: + '200': + description: Первая страница нового результата, включая подтверждённый пустой список + content: + application/json: + schema: + $ref: '#/components/schemas/StockScreenerPage' + '400': + $ref: '#/components/responses/Problem' + '401': + $ref: '#/components/responses/Problem' + '503': + $ref: '#/components/responses/Problem' + /api/v1/stocks/screener/search/{searchId}: + get: + operationId: readStockScreenerPage + summary: Прочитать страницу того же зафиксированного результата + security: + - bearer: [] + parameters: + - name: searchId + in: path + required: true + schema: + type: string + description: Непрозрачный идентификатор результата; не является полномочием доступа. + - name: page + in: query + required: true + schema: + type: integer + minimum: 1 + description: Номер страницы с единицы. Условия поиска при чтении страницы не меняются. + responses: + '200': + description: Страница зафиксированного результата с тем же временем получения каталога + content: + application/json: + schema: + $ref: '#/components/schemas/StockScreenerPage' + '400': + $ref: '#/components/responses/Problem' + '401': + $ref: '#/components/responses/Problem' + '404': + $ref: '#/components/responses/Problem' + '503': + $ref: '#/components/responses/Problem' +components: + securitySchemes: + bearer: + type: http + scheme: bearer + responses: + Problem: + description: Ошибка в текущем публичном профиле WealthLedger; отсутствие результата не кодируется пустым списком. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/Problem' + schemas: + Problem: + type: object + required: [status, code] + properties: + status: + type: integer + code: + type: string + title: + type: string + detail: + type: string + StockScreenerCriteria: + type: object + description: Все условия необязательны; отсутствие полей означает поиск по всем доступным к покупке акциям каталога. + additionalProperties: false + properties: + query: + type: string + maxLength: 80 + description: Необязательная подстрока названия или тикера; пустая строка равнозначна отсутствию условия. + sector: + type: string + minLength: 1 + shareType: + type: string + minLength: 1 + currency: + type: string + minLength: 1 + realExchange: + type: string + minLength: 1 + description: Реальная площадка исполнения, не код торгового режима. + countryOfRisk: + type: string + minLength: 1 + description: Код страны основного бизнеса. + maxLot: + type: integer + minimum: 1 + description: Включительная верхняя граница числа бумаг в лоте. + StockScreenerPage: + type: object + required: [searchId, asOf, page, hasNext, items] + properties: + searchId: + type: string + description: Идентификатор только этого поиска, ограничен пользователем и подключением. + asOf: + type: string + format: date-time + description: Время получения полного списка акций от T-Invest, одинаковое на всех страницах результата. + page: + type: integer + minimum: 1 + hasNext: + type: boolean + items: + type: array + maxItems: 30 + items: + $ref: '#/components/schemas/StockScreenerItem' + StockScreenerItem: + type: object + required: [instrumentUid, name, ticker, isin, shareType, sector, currency, realExchange, countryOfRisk, lot] + properties: + instrumentUid: + type: string + format: uuid + description: Точный выбранный инструмент для ссылки на страницу акции; нельзя подменять неоднозначным тикером. + name: + type: string + ticker: + type: string + nullable: true + isin: + type: string + nullable: true + shareType: + type: string + nullable: true + sector: + type: string + nullable: true + currency: + type: string + nullable: true + realExchange: + type: string + nullable: true + countryOfRisk: + type: string + nullable: true + lot: + type: integer + minimum: 1 + nullable: true diff --git a/openspec/changes/stock-screener/system-analysis/sequences/stock-screener.mmd b/openspec/changes/stock-screener/system-analysis/sequences/stock-screener.mmd new file mode 100644 index 0000000..9278c71 --- /dev/null +++ b/openspec/changes/stock-screener/system-analysis/sequences/stock-screener.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + actor U as Пользователь + participant F as WealthLedger frontend + participant B as WealthLedger backend + participant T as T-Invest API + U->>F: Выбирает условия и нажимает «Найти» + F->>B: POST /api/v1/stocks/screener/search (условия) + B->>B: Проверка сессии и подключения + alt Нет доступа или неверные условия + B-->>F: 400 / 401 / ошибка подключения + F-->>U: Объяснение ошибки без нового результата + else Доступ подтверждён + B->>T: Shares (INSTRUMENT_STATUS_ALL) + alt Каталог получен полностью + T-->>B: Share[] + B->>B: Проверить доступность, фильтры, выпуски, сортировку и зафиксировать asOf + B-->>F: 200, items (до 30), searchId, asOf, hasNext + F-->>U: Первая страница или подтверждённый пустой результат + opt Есть следующая страница + U->>F: Далее + F->>B: GET /api/v1/stocks/screener/search/{searchId}?page=2 + alt Результат доступен тому же пользователю и подключению + B-->>F: 200, следующие items, тот же asOf + F-->>U: Следующая страница без пропусков и повторов + else Результат недоступен или сменилось подключение + B-->>F: Ошибка без пересчёта по новому каталогу + F-->>U: Результат недоступен, предложить новый поиск + end + end + else Каталог недоступен или неполон + T-->>B: Ошибка / неполные сведения + B-->>F: Ошибка поиска, не пустая выборка + F-->>U: Поиск недоступен, можно повторить + end + end + opt Условия изменены или нажат повторный поиск + U->>F: Нажимает «Найти» + F->>B: Новый POST с текущими условиями + Note over B,T: Новый список и новая первая страница без подмены прежнего searchId + end diff --git a/openspec/changes/stock-screener/tasks.md b/openspec/changes/stock-screener/tasks.md new file mode 100644 index 0000000..f951c74 --- /dev/null +++ b/openspec/changes/stock-screener/tasks.md @@ -0,0 +1,57 @@ +# Tasks + +## 1. Каталог и отбор + +- [x] 1.1 [backend] Получать полный список акций T-Invest для каждого нового запуска скринера, сохраняя действующие границы доступа и обработки недоступности источника. + - **Covers:** `stock-screener` — `Скринер ищет среди доступных к покупке акций`; `stock-screener` — `Отсутствие совпадений отличается от недоступности поиска`. + - **Source:** `system-analysis/analysis.md` — Contracts, Error and Recovery Semantics; `design.md` — Decisions/backend. + - **Verification:** управляемый ответ Shares с подходящей акцией за пределами первых 30 текстовых совпадений и отказом источника; backend developer tests и repo-local verify. + - **Depends on:** none + +- [x] 1.2 [backend] Применять обязательные признаки доступности и все необязательные условия к полному списку, выбирать только соответствующего условиям представителя каждого выпуска и упорядочивать результаты до разделения на страницы. + - **Covers:** `stock-screener` — `Скринер ищет среди доступных к покупке акций`; `stock-screener` — `Условия скринера применяются совместно`; `stock-screener` — `Результат представляет отдельные выпуски`. + - **Source:** `specs/stock-screener/spec.md` — соответствующие Requirements; `system-analysis/analysis.md` — Data and State Semantics; `design.md` — Decisions/backend. + - **Verification:** backend developer tests проверяют пересечение условий, неизвестное значение, включительную границу лота, одинаковый и разный ISIN, отсутствие ISIN и ссылку на точный выбранный UID. + - **Depends on:** 1.1 + +## 2. Зафиксированные страницы и публичный контракт + +- [x] 2.1 [backend] Предоставить новый поиск и чтение страниц одного ограниченного по времени результата с привязкой к пользователю и поколению подключения, отличая пустой успешный поиск от ошибки и не пересчитывая старые страницы. + - **Covers:** `stock-screener` — `Страницы относятся к одному результату поиска`; `stock-screener` — `Отсутствие совпадений отличается от недоступности поиска`; `stock-screener` — `Результат представляет отдельные выпуски`. + - **Source:** `system-analysis/contracts/stock-screener.openapi.yaml`; `system-analysis/analysis.md` — Error and Recovery Semantics, Security and NFR at System Boundaries; `design.md` — Decisions/backend, Data / Storage Changes. + - **Verification:** backend HTTP и access checks: до 30 элементов, одинаковые `asOf` и порядок на следующих страницах, новый POST создаёт новый результат, чужой/истёкший `searchId` и смена подключения не выдают данные. + - **Depends on:** 1.2 + +- [x] 2.2 [backend] Опубликовать сгенерированный OpenAPI для операций скринера и подтвердить его семантическую совместимость с утверждённым target contract, сохранив старый текстовый поиск. + - **Covers:** `stock-screener` — `Условия скринера применяются совместно`; `stock-screener` — `Страницы относятся к одному результату поиска`; `stock-screener` — `Отсутствие совпадений отличается от недоступности поиска`. + - **Source:** `system-analysis/contracts/stock-screener.openapi.yaml`; `design.md` — Contract Implementation, Migration / Rollout / Rollback. + - **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract`, сравнение двух маршрутов, запросов, ответов и ошибок с target contract; backend repo-local verify и regression старого GET. + - **Depends on:** 2.1 + +## 3. Пользовательский поиск + +- [x] 3.1 [frontend] Подключить экран акций к опубликованному контракту: сохранять черновые условия отдельно от применённых, отправлять новый поиск только по кнопке и получать страницы по `searchId` без подмены результата поздним ответом. + - **Covers:** `stock-screener` — `Условия скринера применяются совместно`; `stock-screener` — `Страницы относятся к одному результату поиска`. + - **Source:** `design.md` — Decisions/frontend, Contract Implementation; `system-analysis/contracts/stock-screener.openapi.yaml`. + - **Verification:** frontend developer checks для повторного нажатия «Найти», чернового изменения полей, смены страницы и позднего ответа; генерация типов от предоставленного backend OpenAPI и frontend repo-local verify. + - **Depends on:** 2.2 + +- [x] 3.2 [frontend] Показать фильтры, каталожные карточки выбранных выпусков, время списка, страницы, подтверждённую пустоту и восстановление после недоступности поиска без обещания немедленной покупки. + - **Covers:** `stock-screener` — `Скринер ищет среди доступных к покупке акций`; `stock-screener` — `Результат представляет отдельные выпуски`; `stock-screener` — `Страницы относятся к одному результату поиска`; `stock-screener` — `Отсутствие совпадений отличается от недоступности поиска`. + - **Source:** `design.md` — UI/UX Package, Decisions/frontend; `qa-analysis.md` — Manual / Exploratory Scope. + - **Verification:** на широкой и узкой ширине, в том числе с клавиатуры, проверить запуск пустой формой, применение всех фильтров, точную UID-ссылку, состояния loading/empty/error и переход «Далее»; frontend repo-local verify. + - **Depends on:** 3.1 + +## 4. Итоговая документация + +- [x] 4.1 [docs] Обновить существующее описание акций, интеграции T-Invest и HTTP API до реализованного поведения скринера, не копируя целевой OpenAPI и change-диаграмму в AS-IS. + - **Covers:** `stock-screener` — `Скринер ищет среди доступных к покупке акций`; `stock-screener` — `Страницы относятся к одному результату поиска`; `stock-screener` — `Отсутствие совпадений отличается от недоступности поиска`. + - **Source:** `docs-impact.md` — entries `docs/domains/instruments/stocks.md`, `docs/integrations/tbank.md`, `docs/architecture/http-api.md`. + - **Verification:** сверить опубликованные страницы с реализованной формой, ответами и backend-generated OpenAPI; docs repo-local Markdown/link checks и `git diff --check`. + - **Depends on:** 2.2, 3.2 + +- [x] 4.2 [docs] Обновить обзор продукта, существующий навигационный пункт акций и глоссарий с точным определением скринера без технических деталей хранения результата. + - **Covers:** `stock-screener` — `Скринер ищет среди доступных к покупке акций`. + - **Source:** `docs-impact.md` — entries `docs/overview.md`, `docs/README.md`, `docs/glossary.md`; Glossary / Navigation Impact. + - **Verification:** проверить ссылки, согласованность термина с разделом акций, docs repo-local Markdown/link checks и `git diff --check`. + - **Depends on:** 3.2 diff --git a/openspec/config.yaml b/openspec/config.yaml index 4bc7bb4..1934181 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,9 +1,69 @@ -schema: wealth-ledger-sdd +schema: wealth-ledger-sdd-v2 context: | - WealthLedger состоит из независимых frontend, backend, specs и docs Git-репозиториев. - Canonical OpenSpec authority — store wealth-ledger-specs в локальном openspec/. - Инструкции и validation specs находятся в этом репозитории; соседние checkout не требуются. - Локальная разработка и CI используют Node из собственной .nvmrc; порядок работы задаёт AGENTS.md. - Project artifacts объясняют решения по-русски; exact OpenSpec headings, tokens, - commands и normative keywords SHALL/MUST сохраняются на английском. + WealthLedger — multi-repository workspace. The `main` repository is the workspace + root and contains sibling repositories `frontend/`, `backend/`, `docs/` and `specs/`. + OpenSpec configuration, schemas, changes and canonical behavior specs live only in + `specs/openspec/` and are accessed as store `wealth-ledger-specs`. There is no + `openspec/` directory or `config.yaml` in `main`, `frontend`, `backend` or `docs`. + When an OpenSpec command is invoked from the workspace root, target the canonical + store explicitly when that command supports store selection. + New changes use `wealth-ledger-sdd-v2`; active changes explicitly pinned to + `wealth-ledger-sdd` keep their original proposal → specs → design → tasks → apply + lifecycle and existing approvals until completion. Never infer approval from + the existence of an artifact. Schema selection comes from each change's + `.openspec.yaml`, not from the current default. + + Repository responsibilities and sources of truth: + - `main` (`./`): workspace instructions, shared configuration, development Compose + and release/workspace orchestration. It MUST NOT contain product feature code. + - `frontend/`: React/Vite product implementation. + - `backend/`: NestJS product API, Prisma data model and SQLite persistence. + - `docs/`: canonical AS-IS product and architecture knowledge. Current architecture, + integrations and durable diagrams describe the system as it exists now; historical + architectural rationale is preserved in ADRs and archived changes. + - `specs/`: OpenSpec changes plus canonical behavior specs. Canonical specs contain + normative observable behavior only, not durable implementation documentation. + - Future `qa/`: Playwright/E2E automation when the repository actually exists. + + Contract authority: + - For v2 changes, new or changed cross-system contracts are planned contract-first + in the change's `system-analysis/contracts/` before implementation. + - After implementation, backend-generated OpenAPI is the executable/current HTTP API + contract and MUST remain semantically compatible with the approved target contract. + - Do not create a second permanent machine-readable API source of truth in canonical + OpenSpec specs or docs. + + Documentation authority: + - `docs/architecture/`, `docs/domains/`, `docs/integrations/` and related pages hold + durable AS-IS explanations. + - V2 changes select durable knowledge through approved `docs-impact`; legacy + changes continue their approved delivery map. Change-specific reasoning, + migration history and rejected options remain in the archived OpenSpec change + unless an ADR is explicitly required. + + Language and normative terminology: + - Human-facing project artifacts are written in Russian, while exact OpenSpec headings, + tokens, commands, identifiers and machine-readable contract syntax remain unchanged. + - Normative keywords follow BCP 14 (RFC 2119 + RFC 8174). `MUST`, `MUST NOT`, + `REQUIRED`, `SHALL`, `SHALL NOT`, `SHOULD`, `SHOULD NOT`, `RECOMMENDED`, + `NOT RECOMMENDED`, `MAY` and `OPTIONAL` have normative meaning only when written + in uppercase. Lowercase forms have their ordinary-language meaning. + - Do not use uppercase BCP 14 keywords merely for emphasis. Use them only when the + statement intentionally imposes a normative obligation or permitted variation. + + A downstream artifact cannot silently change upstream semantics: route changes + to the earliest affected artifact and reapprove only its affected dependents. + Native OpenSpec sync/archive owns behavior delta → canonical specs; docs updates + follow approved delivery tasks, not an automatic copy of system analysis. + + Repository-local `AGENTS.md` files remain authoritative for implementation conventions + inside their repositories. Load only the affected repository instructions and relevant + code/docs; do not scan the entire workspace without need. + +operations: + archive: + guidance: + - Keep the change's selected schema and explicit approvals; do not impose v2-only artifacts on legacy changes. + - Complete implementation, developer checks, independent verification and canonical documentation before archive. For v2 changes, also check applicable QA Analysis, docs-impact and target contract compatibility with backend-generated OpenAPI. + - Native OpenSpec sync/archive updates canonical behavior specs, not implementation documentation. diff --git a/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml b/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml new file mode 100644 index 0000000..100d4ab --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml @@ -0,0 +1,642 @@ +name: wealth-ledger-sdd-v2 +version: 8 +description: Multi-repo SDD workflow - proposal → specs → system-analysis → design + qa-analysis → docs-impact → tasks → apply + +artifacts: + - id: proposal + generates: proposal.md + description: Owner-approved intent, scope, capabilities, impact and success criteria + template: proposal.md + instruction: | + Purpose: + Proposal фиксирует намерение изменения: зачем оно нужно, что меняется на + уровне продукта/системы, где проходит scope и по каким наблюдаемым признакам + change можно считать успешным. Это не requirements specification, не system + analysis и не engineering design. + + Read First: + - Прочитай релевантные canonical behavior specs из `specs/openspec/specs/`. + - Прочитай только релевантные AS-IS pages в `docs/`: `overview.md`, + `architecture/`, `domains/`, `integrations/`, `adr/`, `design/`. + - Не исследуй code-level реализацию глубже, чем требуется для корректного + scope/impact. Proposal не должен зависеть от случайной структуры кода. + + Critical Rules: + - Один Proposal = одно связное намерение. Независимо поставляемые или + независимо согласуемые изменения предпочитай разделять. + - Пиши языком Change Owner: проблема, цель, scope, observable outcome. + - Не дублируй будущие Requirements/Scenarios и не проектируй решение. + - Новую/изменяемую интеграцию можно обозначить как impact/dependency, но + protocol, endpoint, payload, error mapping и sequence здесь запрещены. + - Capabilities перечисляют каждую новую или изменяемую behavior spec точным + canonical path. + - Если observable behavior не меняется, не выдумывай Requirement: capability + delta остаётся пустой, а change использует `.openspec.yaml: skip_specs: true`. + - Breaking observable behavior помечай **BREAKING**. + - Вопрос, способный изменить intent/scope/capabilities, должен быть разрешён + до approval Proposal. + + Required Shape: + - **Why**: проблема/возможность и почему ею нужно заняться сейчас. + - **What Changes**: краткий список изменяемых outcomes. + - **Scope**: + - **In Scope** + - **Out of Scope / Non-Goals** + - **Capabilities**: + - **New Capabilities**: exact `specs//spec.md`. + - **Modified Capabilities**: exact existing capability path только если + меняются Requirements. + - **Impact**: пользователи, процессы, домены, внешние системы и вероятно + затрагиваемые areas на high level; без code/repository topology деталей. + - **Success Criteria**: продуктовые/системные наблюдаемые признаки успеха. + - **Assumptions / Dependencies**: только существенные для scope. + + MUST NOT Include: + - files/classes/functions/hooks/tables/ORM entities; + - libraries/frameworks/storage/cache/queue/transport choices; + - endpoint/payload/error-code contracts; + - sequence/component diagrams; + - unit/integration/E2E plan; + - implementation/tasks/PR/commit/branch plan; + - пиксельные UI details или инженерные решения. + + Validation and Approval: + - После записи используй native OpenSpec validation against the resolved planning home. + - Structural success не является semantic approval. + - Покажи Proposal Change Owner и остановись до explicit approval. + requires: [] + + - id: specs + generates: "specs/**/*.md" + description: Normative black-box behavioral deltas and acceptance-level invariants + template: spec.md + instruction: | + Purpose: + Delta Specs — нормативный black-box behavior contract. Они отвечают только + на вопрос: "Что система обязана делать с точки зрения пользователя или + внешнего наблюдателя?" После sync/archive они становятся canonical behavior + specs в repository `specs/`. + + Preconditions: + - Proposal explicitly approved. + - Используй только capabilities из approved Proposal. + - Для existing capability прочитай canonical spec целиком. + - При `.openspec.yaml: skip_specs: true` не создавай spec files и не + выдумывай behavior ради workflow. + + Critical Rules: + - Один Requirement = одно устойчивое observable behavior/invariant. + - Нормативный язык следует project BCP 14 policy из `config.yaml`: + SHALL/MUST для обязательств; SHOULD/MAY только при намеренной вариативности. + - Каждый Requirement имеет минимум один `#### Scenario:` с конкретным + WHEN/THEN; GIVEN допустим для precondition. + - Scenario должен быть проверяемым на уровне системы, но не предписывает + конкретный test level/automation implementation. + - Покрой значимые happy path, business errors, permissions, boundaries, + state transitions и compatibility cases; не перечисляй комбинации ради + искусственной полноты. + - Security/privacy/reliability/performance constraints включай только если + они являются observable/contractual obligation. + - Технический термин допустим только когда он сам является стабильным + внешним обязательством. Internal implementation terms запрещены. + - Не дели Requirements на frontend/backend/main/docs/qa: spec описывает + систему как единое целое. + - System names, transport, endpoint, schema и code mappings по умолчанию + принадлежат System Analysis. Они попадают в spec только если их identity + сама является частью публичного/регуляторного behavior contract. + + Delta Operations: + - `ADDED`: новый Requirement или capability. + - `MODIFIED`: скопируй ПОЛНЫЙ existing Requirement block со всеми scenarios, + затем измени behavior; heading должен совпасть точно. + - `REMOVED`: добавь **Reason** и **Migration**. + - `RENAMED`: только rename heading в формате `FROM:` / `TO:`. + - Не оставляй пустые operation sections. + + New Capability: + - Добавь `## Purpose` из 1–2 предложений (не менее 50 characters). + - Existing Purpose в delta не дублируй. + - Не переименовывай capability path обычной MODIFIED delta. + + Quality Gate: + - Requirement однозначен и атомарен. + - Scenario имеет ясный trigger и проверяемый outcome. + - Нет "работает корректно", "быстро", "удобно" без критерия. + - Нет implementation details. + - Нет поведения вне approved Proposal. + - Нет противоречия canonical specs. + - Manual QA способен понять expected behavior без чтения implementation code. + + Feedback Routing: + - intent/scope/capabilities → Proposal. + - Systems/contracts/mappings/integration semantics → не решай здесь; + детализируй позже в System Analysis. + + Validation and Approval: + - После записи используй native OpenSpec validation against the resolved planning home. + - Owner: System Analyst. QA выполняет testability review; affected engineers + проверяют реализуемость, не подменяя requirements design. + - Остановись до explicit Delta Specs approval. + requires: + - proposal + + - id: system-analysis + generates: system-analysis/analysis.md + description: Cross-system decomposition, complete flows, contract-first interfaces, mappings, failures and semantic observability + template: system-analysis.md + instruction: | + Purpose: + System Analysis детализирует approved Delta Specs на уровне взаимодействия + систем. Он отвечает на вопрос: "Как требуемое behavior распределяется между + системами и какие межсистемные contracts/flows должны существовать?" + Это артефакт системного аналитика, а не internal engineering design. + + Read First: + - Approved Proposal и все Delta Specs change. + - Релевантный AS-IS из `docs/`: `architecture/`, `domains/`, `integrations/`, + `adr/` и только нужные cross-cutting pages. + - Для существующего HTTP API прочитай текущий backend-generated OpenAPI. + - Для существующих внешних integration contracts прочитай их canonical/runtime + source, если он доступен. + - Не принимай текущий code layout за обязательный contract. + + Critical Rules: + - System Analysis MAY elaborate Requirements, но MUST NOT молча вводить + новое observable behavior. Uppercase BCP 14 keywords используй только для + реальных межсистемных обязательств, не как усилители обычной прозы. Если обнаружен behavioral gap, сначала обнови + Delta Specs и переутверди их. + - System-level interactions и contracts согласуются здесь ДО Design. + - Sequence diagrams здесь — полные system-level flows: участники, calls/events + и релевантные success/business-error/technical-failure branches. + - Design не переизобретает согласованные здесь interactions/contracts. + - Для сложного процесса предпочти несколько читаемых sequence diagrams одной + огромной диаграмме. + - Не описывай internal classes/hooks/modules/repositories/FSD/NestJS layout, + DB tables или developer unit-test strategy. + + Contract-First Policy: + - Для нового/изменяемого межсистемного HTTP/event contract target contract + определяется и согласуется ДО implementation. + - Machine-readable target contracts храни в + `system-analysis/contracts/` (OpenAPI/AsyncAPI/Proto/JSON Schema). + - Markdown фиксирует semantics и ссылается на contract files, не копирует + большие schemas. + - Frontend и Backend могут независимо планировать/реализовывать работу против + approved target contract. + - WealthLedger backend-generated OpenAPI остаётся executable/runtime source + после реализации. До закрытия change он MUST быть semantic-compatible с + approved contract from System Analysis. + - Не создавай второй вечный canonical OpenAPI в `specs/`: approved contract + остаётся частью archived change. Текущее executable API подтверждается + backend code/generated OpenAPI; `docs/` хранит durable explanation, а не + дублированную machine-readable truth. + - Semantic mismatch generated OpenAPI ↔ approved contract возвращает change + в System Analysis; нельзя тихо принять реализацию как новую спецификацию. + + Required Shape: + - **Requirement Coverage**: + mapping релевантных Requirements/Scenarios на flows/contracts. + - **Systems and Responsibilities**: + participating applications/services/external systems, role и boundary. + - **Interaction Flows**: + flows со ссылками на `system-analysis/sequences/*.mmd`. + - **Contracts**: + producer/consumer, protocol/transport, operation/event, version, + machine-readable schema reference, compatibility expectations, + correlation/idempotency semantics где нужны. + - **Data and State Semantics**: + source of truth/data ownership, cross-system mappings, state transitions, + validation boundaries, consistency expectations. + - **Error and Recovery Semantics**: + business vs technical failures, external→domain mapping, timeout, + duplicate/out-of-order/callback semantics и retry только на boundary level. + - **Observability Semantics**: + какие значимые product/business/system events обязаны фиксироваться. + Допустимы semantic mappings вроде `code=1 → NO_MONEY`, + `code=2 → FRAUD`, `Pay click → BUTTON_CLICK`, required attributes/triggers. + SDK/exporter/logger/instrument — Design. + - **Security and NFR at System Boundaries**: + authn/authz/trust boundaries, SLA/latency/rate/compatibility constraints, + только если они являются system contract. + - **Compatibility / Migration**: + system-level transition (например SOAP → REST), coexistence/cutover, + backward compatibility и dependency sequencing. + - **Durable Documentation Candidates**: + перечисли только факты/диаграммы/решения, которые вероятно должны остаться + в AS-IS `docs/` после завершения change. Финальное решение принимает + `docs-impact` после Engineering Design. + - **Open Questions**: + только genuinely non-blocking. Вопрос, меняющий behavior/contract/ + implementability, блокирует approval. + + MUST NOT Include: + - internal implementation files/classes/framework choices; + - internal DB schema/migration details; + - developer unit/component/internal integration test prescriptions; + - task checklist; + - новое behavior вне Delta Specs; + - попытку заранее переписать `docs/`: этот artifact описывает TO-BE change, + а promotion в canonical AS-IS планируется отдельно. + + Review and Approval: + - Owner: System Analyst. + - Mandatory reviewers: affected Frontend/Backend engineers; QA reviews + testability/failure semantics. + - behavior change → Specs; intent/scope change → Proposal. + - После записи используй native OpenSpec validation against the resolved planning home. + - Остановись до explicit System Analysis approval. + requires: + - specs + + - id: design + generates: design.md + description: Internal engineering design for implementing approved behavior and system contracts in main/frontend/backend + template: design.md + instruction: | + Purpose: + Design отвечает на вопрос: "Как affected implementation repositories + реализуют свою сторону approved Specs и System Analysis?" + Это internal engineering design, а не повторный system analysis. + + Preconditions: + - Proposal, applicable Specs и System Analysis approved. + - System-level sequences/contracts/mappings считаются input constraints. + + Critical Rules: + - Не добавляй observable behavior вне Specs. + - Не изменяй system contract/interaction semantics из System Analysis. + - Технически нереализуемый contract возвращай в System Analysis, не создавай + альтернативный private contract внутри Design. + - Не пересказывай upstream artifacts; проектируй внутренний implementation. + - Files/classes/modules/components допустимы только когда помогают зафиксировать + существенное решение; Design не file-by-file checklist. + - Developer-owned test strategy принадлежит implementation owners. QA не + диктует unit/component/internal integration tests frontend/backend. + - Blocking вопрос по approach/migration/task decomposition реши до approval. + + Required Shape: + - **Context and Constraints**: relevant current implementation constraints. + - **Technical Goals / Non-Goals**. + - **Implementation Impact Map**: + рассмотри `main`, `frontend`, `backend` и future `qa`. + Для каждой area: `affected` + outcome либо `N/A` + reason. + `main` affected только при workspace/dev/release configuration impact. + `qa` существует только после появления реального repository. + - **Decisions**: + ключевые implementation decisions, rationale, rejected alternatives. + Разделяй по affected repositories: + - `main`: Compose/workspace/release/config orchestration only. + - `frontend`: React/Vite structure, state/data flow, routing, UI composition, + API client integration, frontend developer verification approach. + - `backend`: NestJS modules/services, Prisma/SQLite persistence, migrations, + transactions, background work, backend developer verification approach. + - `qa`: только когда repo существует; internal Playwright architecture + принадлежит automation owner. + - **Cross-cutting Implementation**: + security implementation, feature flags, technical observability/logging, + caching, concurrency, performance mechanisms, internal error handling. + - **Data / Storage Changes**: + Prisma/SQLite schema, migration/backfill/data-safety details. + - **Contract Implementation**: + как implementation будет соответствовать approved contract; для backend + укажи, как generated OpenAPI будет verified against target contract. + - **Developer Verification Strategy**: + разработчики сами определяют unit/component/internal integration coverage + и technical gates. Не копируй QA system verification matrix. + - **UI/UX Package**: + `new/changed`, `reuse` или `N/A`; relevant source/revision, screens, states, + interactions, responsive/a11y expectations. Не требуй новый mockup для + неизменяемого UI. Approved package remains accessible after archive. + - **Risks / Trade-offs**. + - **Migration / Rollout / Rollback**: + implementation sequencing across repos, flags, data safety; mandatory + rollback contract для security/destructive migration/public contract или + другого труднообратимого решения, иначе `N/A` + reason. + - **Solution Assessment**: + обязателен для нового/существенно заменяемого auth, security, storage, + external integration, observability, caching, concurrency, shared + infrastructure/tooling или другого дорогостоящего необратимого решения. + Сравни mature options по fit, maturity, security, operating cost, + constraints и migration cost; custom implementation обоснуй конкретной + несовместимостью alternatives. Не создавай отдельный artifact. + - При UI/UX scope Design сохраняет package revision и exact immutable reuse + locator (store ID и path от OpenSpec root), применимые части и отличия. + Inline Markdown достаточен по умолчанию; необходимые PNG/SVG хранятся + локально под `visuals/` с relative path, purpose и SHA-256 каждого файла. + SVG самодостаточен: без scripts, handlers, external assets/fonts. + External-only mutable URL или временный чат не заменяют согласованный + пакет. Единственный approval Design охватывает всю версию пакета; + self-hash Design не требуется. При Apply проверь наличие и SHA-256 + перечисленных материалов перед зависимой работой. + + MUST NOT Include: + - approved system-level sequence diagrams; + - новые/изменённые API/event contracts без возврата в System Analysis; + - новое behavior без возврата в Specs; + - canonical docs update plan (это `docs-impact`); + - QA system test cases/coverage strategy; + - implementation checklist/PR plan/future results; + - длинные production code listings. + + Feedback Routing: + - intent/scope → Proposal; + - observable behavior → Specs; + - systems/contracts/mappings/error semantics/semantic observability → System Analysis; + - internal implementation choice → Design. + + Validation and Approval: + - После записи используй native OpenSpec validation against the resolved planning home. + - Reviewers: owners affected implementation repositories. + - Остановись до explicit Design approval. + requires: + - system-analysis + + - id: qa-analysis + generates: qa-analysis.md + description: Risk-based system verification analysis independent from developer-owned internal test architecture + template: qa-analysis.md + instruction: | + Purpose: + QA Analysis отвечает на вопрос: "Что необходимо проверить, чтобы доказать + работоспособность системы согласно Specs и System Analysis?" + Это product/system verification artifact, не internal test design кода. + + Preconditions: + - Specs approved. + - System Analysis approved. + - QA Analysis может выполняться параллельно с Engineering Design. + + Critical Rules: + - QA проверяет system behavior, а не устройство кода. + - Не предписывай разработчикам unit/component/internal integration tests, + mocks внутри application code, coverage percentage, filenames/classes. + - Не создавай новое behavior: undefined expected result → gap в Specs. + - Undefined flow/error mapping/contract → gap в System Analysis. + - Нужный testability hook/environment support формулируй как need для Design, + но не проектируй его implementation за разработчика. + - Manual QA и Automation QA используют один artifact. Automation scope — + black-box API/cross-system/E2E. Internal code tests остаются developer-owned. + - Не дублируй scenarios; ссылайся на exact Requirement/Scenario. + + Required Shape: + - **Verification Scope**: in/out capabilities/requirements/scenarios. + - **Coverage Matrix**: Requirement/Scenario → system-level checks. + - **Cross-system Verification**: contracts/error mapping/timeouts/unavailable/ + duplicates/out-of-order only where relevant. + - **Risk-based Priorities**: critical journeys, money/security/data-integrity, + compatibility/regression hotspots. + - **Test Data and Environment Needs**: system-level states, accounts, + permissions, external sandboxes/stubs; без internal mocking prescriptions. + - **Manual / Exploratory Scope**. + - **Automation Candidates**: black-box API/cross-system/E2E candidates. + Пока отдельного QA repo нет, помечай repository automation как + `deferred until QA repo exists`; не выдумывай paths/tasks. + - **Observability Verification**: как на system level подтвердить semantic + events (`NO_MONEY`, `FRAUD`, `BUTTON_CLICK` и т.п.), без SDK code. + - **Regression Impact**. + - **Gaps / Blockers**: каждый gap имеет routing destination. + + MUST NOT Include: + - unit/component/internal integration plan frontend/backend; + - private code classes/functions/files; + - application architecture decisions; + - guessed expected behavior; + - дублирование Design. + + Review and Approval: + - Owner: Manual QA / QA responsible for system verification. + - Automation QA reviews automation feasibility when role exists. + - Product/System Analyst reviews acceptance interpretation. + - Affected engineers review testability dependencies. + - После записи используй native OpenSpec validation against the resolved planning home. + - Остановись до explicit QA Analysis approval. + requires: + - system-analysis + + - id: docs-impact + generates: docs-impact.md + description: Promotion plan from change-specific knowledge to canonical AS-IS documentation in docs repository + template: docs-impact.md + instruction: | + Purpose: + Docs Impact отвечает на вопрос: "Какое durable knowledge после успешной + реализации должно изменить canonical AS-IS documentation в `docs/`?" + Это planning artifact в `specs/`, а не сама документация. + + Canonical Documentation Model: + `docs/` — source of truth для актуального product/architecture description: + - `README.md`, `overview.md`, `glossary.md` — navigation/product overview/terms; + - `architecture/` — context, components, HTTP API, deployment; + - `architecture/cross-cutting/` — cross-cutting system concerns; + - `domains/` — durable domain/product behavior explanations; + - `integrations/` — external systems and integrations; + - `adr/` — historical architectural decisions; accepted ADR не переписывается + под новое текущее состояние; + - `design/` — durable visual/design principles. + + Preconditions: + - System Analysis approved. + - Engineering Design approved. + - Прочитай только релевантные current pages в `docs/` и не создавай duplicate + page, если подходящее canonical location уже существует. + + Critical Rules: + - Не копируй весь change в docs. Promote только knowledge, полезное после + того как change перестанет быть актуальной задачей. + - Canonical docs описывают resulting AS-IS, а не migration history. + - Migration rationale/history остаётся в archived OpenSpec change; если + решение архитектурно значимо, создай/обнови ADR plan. + - Не требуй переносить каждую system-analysis sequence. Promote только + важные долгоживущие diagrams, которые помогают понимать текущую систему. + - Не копируй machine-readable OpenAPI из System Analysis как второй canonical + contract. `docs/` может описывать API/integration semantics и ссылаться на + generated/runtime API source. + - `docs-impact` не редактирует `docs/`; он создаёт точный promotion plan, + из которого Tasks создаст `[docs]` implementation items. + + Required Shape: + - **Summary**: нужен ли durable docs update вообще. + - **Documentation Changes**: таблица/список entries со свойствами: + - `Action`: create | update | remove | none; + - `Target`: exact path under `docs/`; + - `Category`: overview | architecture | cross-cutting | domain | + integration | ADR | design; + - `Resulting AS-IS`: что именно должно стать истинным после change; + - `Source`: ссылка на Specs/System Analysis/Design decision; + - `Verification`: как понять, что canonical docs согласованы с реализацией. + - **Diagram Promotion**: какие diagrams из `system-analysis/sequences/` + становятся durable docs, куда именно и почему; default = не переносить. + - **ADR Decision**: + - `required` только если принято значимое, долгоживущее архитектурное + решение с alternatives/consequences; + - иначе `N/A` + reason. + - **Glossary / Navigation Impact**: только если реально нужны новые термины + или navigation entries. + - **No-Change Rationale**: если docs не меняются, конкретно объясни почему. + + MUST NOT Include: + - временный migration diary как canonical AS-IS; + - копию proposal/specs/system-analysis; + - machine-readable API contract как второй source of truth; + - implementation tasks; они создаются в Tasks; + - fictional docs paths без проверки существующей структуры. + + Review and Approval: + - Owner: System Analyst + affected engineering owner совместно подтверждают, + что planned docs отражают target system accurately. + - Для ADR engineering owner подтверждает decision/consequences. + - После записи используй native OpenSpec validation against the resolved planning home. + - Остановись до explicit Docs Impact approval. + requires: + - design + - qa-analysis + - system-analysis + + - id: tasks + generates: tasks.md + description: Dependency-ordered, repository-owned delivery checklist derived from approved planning artifacts + template: tasks.md + instruction: | + Purpose: + Tasks — исполняемый delivery plan. Он отвечает только на вопрос: + "Какие independently reviewable outcomes нужно доставить и проверить, чтобы + реализовать approved plan?" + + Preconditions: + - Design, QA Analysis и Docs Impact explicitly approved. + - Proposal/Specs/System Analysis остаются authoritative upstream contracts. + + Repository Owners: + - `[main]` — только workspace/dev/release orchestration changes. + - `[frontend]` — React/Vite product implementation. + - `[backend]` — NestJS/Prisma/SQLite product implementation. + - `[docs]` — canonical AS-IS documentation changes из approved Docs Impact. + - `[specs]` — только genuine specs-repository maintenance beyond native + change/sync/archive (например schema/governance change). + - `[qa]` — только после появления реального QA/E2E repository. + + Task Format: + Каждый checkbox: + `- [ ] X.Y [main|frontend|backend|docs|specs|qa] ` + и содержит: + - **Covers:** exact `` `capability-path` — `Requirement heading` `` pairs + либо `` `N/A` — ``. + - **Source:** exact approved artifact section/contract/docs-impact entry. + - **Verification:** owner-controlled command, observable check или delivered + artifact; не будущий result. + - **Depends on:** task IDs либо `none`, если зависимость неочевидна. + + Critical Rules: + - Одна task = один owner repository + один independently reviewable outcome. + - Cross-repository work всегда разделяй. + - Product feature code никогда не помещай в `[main]`. + - `[docs]` tasks MUST соответствовать approved `docs-impact` entries; не + придумывай документацию заново на этапе Tasks. + - `[qa]` task запрещена, пока repo не существует. Manual QA/acceptance не + маскируй под implementation checkbox. + - Каждый Delta Requirement должен иметь implementation coverage либо ясное + объяснение, почему implementation work не требуется. + - При изменении contract обязательно запланируй verification, что + backend-generated OpenAPI/runtime interface semantic-compatible с approved + System Analysis contract до закрытия change. + - Canonical behavior specs не обновляй вручную task'ом: их merge выполняет + native OpenSpec sync/archive. + - Developer-owned tests следуют Design; QA system verification следует + QA Analysis. QA task не диктует unit/component tests разработчика. + - Не создавай empty/ceremonial tasks. + + Grouping: + - Группируй numbered `##` sections по dependency/delivery sequence. + - Repository ownership остаётся явным внутри каждой группы. + - Documentation updates обычно выполняются после того, как target state + подтверждён implementation, если только docs нужны раньше для coordination. + + MUST NOT Include: + - новое behavior → Specs; + - новый/изменённый system contract/sequence/error semantics → System Analysis; + - новый internal architecture decision → Design; + - новый QA coverage decision → QA Analysis; + - новый docs promotion decision → Docs Impact; + - business rationale → Proposal; + - duplicated payload/schema/design prose; + - commits/branches/issues/post-hoc completion evidence. + + Feedback Routing: + - semantic change возвращается в самый ранний затронутый artifact и + инвалидирует downstream approvals. + - Vague/multi-owner/oversized decomposition исправляется здесь. + + Validation and Approval: + - После записи используй native OpenSpec validation against the resolved planning home. + - Покажи Tasks Change Owner(s) и остановись до explicit approval. + requires: + - design + - qa-analysis + - docs-impact + - specs + +apply: + requires: + - tasks + tracks: tasks.md + instruction: | + Purpose: + Apply реализует approved delivery plan и не переоткрывает upstream decisions + без явного feedback cycle. + + Preconditions: + - Explicit approvals: Proposal; Delta Specs если не `skip_specs`; System + Analysis; Design; QA Analysis; Docs Impact; Tasks. + - Используй canonical store `wealth-ledger-specs` для OpenSpec commands. + + Context Loading: + - Прочитай `contextFiles`, возвращённые native OpenSpec apply instructions. + - Затем repository-local `AGENTS.md` только affected areas. + - Для каждой task загружай релевантные upstream sections/contracts, а не весь + workspace без необходимости. + + Execution: + - Выполняй pending tasks по dependency order. + - Не объединяй разные repositories в одну task. + - Product behavior реализуется только в `frontend/`/`backend/`; `main` меняется + только при approved workspace/config impact. + - Запускай task Verification и отмечай checkbox только после observed success. + - Developer-owned unit/component/internal integration tests выполняются по + Design/repository rules. + - QA system/manual validation не считается выполненной только потому, что + developer tests зелёные; она следует QA Analysis отдельным validation flow. + - При changed contract backend-generated OpenAPI/runtime contract должен пройти + semantic comparison с approved System Analysis target before completion. + - `[docs]` tasks обновляют canonical `docs/` до resulting AS-IS из Docs Impact; + не переносят change history вместо текущего состояния. + - Если implementation-owned work остаётся, сохрани checkpoint: completed, + remaining, observed checks и следующий concrete outcome. + - READY_FOR_VALIDATION допустим после всех implementation/docs-owned tasks и + relevant checks. Apply не подменяет independent QA/acceptance. + + Sync / Archive Semantics: + - Native OpenSpec sync/archive обновляет canonical behavior specs в `specs/`. + - `docs/` обновляется только через approved `[docs]` tasks из Docs Impact. + - Full System Analysis, target contracts и migration history остаются в + archived change как historical planning evidence. + - Significant architectural decision сохраняется отдельно как immutable ADR в + `docs/adr/`; AS-IS architecture/integration pages показывают только current state. + + Feedback Routing: + - intent/scope/capabilities → Proposal; + - observable behavior/invariants → Specs; + - systems/sequences/contracts/mappings/error semantics/semantic observability + → System Analysis; + - internal architecture/storage/UI implementation/technical observability/ + developer test strategy → Design; + - system verification/test data/automation candidates → QA Analysis; + - canonical docs promotion/ADR/diagram retention → Docs Impact; + - decomposition/ownership/dependencies/task verification → Tasks. + + Change Control: + - При semantic conflict останови downstream work и сообщи evidence. + - Инвалидируй approvals от самого раннего затронутого artifact. + - Mechanical typo/formatting без normative meaning change может не требовать + reapproval; Requirement/Scenario/contract/sequence/architecture/QA expected + result/docs target/task verification semantic changes требуют reapproval. + - Structural validation не заменяет semantic review, tests и independent QA. diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/design.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/design.md new file mode 100644 index 0000000..00b7116 --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/design.md @@ -0,0 +1,67 @@ +# Engineering Design + +## Context and Constraints + +## Technical Goals / Non-Goals + +## Implementation Impact Map + +### main + +### frontend + +### backend + +### qa + + +## Decisions + +### main + +### frontend + +### backend + +### qa + +## Cross-cutting Implementation + +## Data / Storage Changes + +## Contract Implementation + +## Developer Verification Strategy + +## UI/UX Package + + + +**Applicability and scope:** + +**Package revision:** + +**Screens, states and interactions:** + +**Responsive behavior:** + +**Reused source and differences:** + + + +| Material ID | Relative path | Screen/state and purpose | SHA-256 | +| --- | --- | --- | --- | +| | | | | + +## Risks / Trade-offs + +## Migration / Rollout / Rollback + +## Solution Assessment diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/docs-impact.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/docs-impact.md new file mode 100644 index 0000000..e3787b3 --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/docs-impact.md @@ -0,0 +1,16 @@ +# Documentation Impact + +## Summary + +## Documentation Changes + +| Action | Target | Category | Resulting AS-IS | Source | Verification | +|---|---|---|---|---|---| + +## Diagram Promotion + +## ADR Decision + +## Glossary / Navigation Impact + +## No-Change Rationale diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/proposal.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/proposal.md new file mode 100644 index 0000000..5878bda --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/proposal.md @@ -0,0 +1,23 @@ +# Proposal + +## Why + +## What Changes + +## Scope + +### In Scope + +### Out of Scope / Non-Goals + +## Capabilities + +### New Capabilities + +### Modified Capabilities + +## Impact + +## Success Criteria + +## Assumptions / Dependencies diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/qa-analysis.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/qa-analysis.md new file mode 100644 index 0000000..75dbb6e --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/qa-analysis.md @@ -0,0 +1,25 @@ +# QA Analysis + +## Verification Scope + +### In Scope + +### Out of Scope + +## Coverage Matrix + +## Cross-system Verification + +## Risk-based Priorities + +## Test Data and Environment Needs + +## Manual / Exploratory Scope + +## Automation Candidates + +## Observability Verification + +## Regression Impact + +## Gaps / Blockers diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/spec.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/spec.md new file mode 100644 index 0000000..b838743 --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/spec.md @@ -0,0 +1,18 @@ + diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/system-analysis.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/system-analysis.md new file mode 100644 index 0000000..5de8c22 --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/system-analysis.md @@ -0,0 +1,27 @@ +# System Analysis + +## Requirement Coverage + +## Systems and Responsibilities + +## Interaction Flows + + + +## Contracts + + + +## Data and State Semantics + +## Error and Recovery Semantics + +## Observability Semantics + +## Security and NFR at System Boundaries + +## Compatibility / Migration + +## Durable Documentation Candidates + +## Open Questions diff --git a/openspec/schemas/wealth-ledger-sdd-v2/templates/tasks.md b/openspec/schemas/wealth-ledger-sdd-v2/templates/tasks.md new file mode 100644 index 0000000..76d04ee --- /dev/null +++ b/openspec/schemas/wealth-ledger-sdd-v2/templates/tasks.md @@ -0,0 +1,9 @@ +# Tasks + +## 1. + +- [ ] 1.1 [backend] + - **Covers:** `N/A` — + - **Source:** + - **Verification:** + - **Depends on:** none diff --git a/openspec/specs/agent-sdd-session-workflow/spec.md b/openspec/specs/agent-sdd-session-workflow/spec.md index e9caeaa..12d02d7 100644 --- a/openspec/specs/agent-sdd-session-workflow/spec.md +++ b/openspec/specs/agent-sdd-session-workflow/spec.md @@ -15,7 +15,11 @@ Session-oriented SDD workflow SHALL направлять каждую agent-се phase record или ожидание решения MUST NOT само по себе принуждать к новой сессии той же роли. Specification MUST поддерживать bounded progression `Proposal + Delta Specs` и `Design + Tasks`, сохраняя отдельную explicit -approval boundary перед каждым downstream artifact. +approval boundary перед каждым downstream artifact. Для `wealth-ledger-sdd-v2` +Specification SHALL также поддерживать System Analysis после Delta Specs, +параллельные Design и QA Analysis после System Analysis, Docs Impact после +обоих и Tasks после Docs Impact. Для active `wealth-ledger-sdd` сохраняются +прежние пары и approvals без автоматической миграции. #### Scenario: Начинается новый change @@ -40,14 +44,14 @@ approval boundary перед каждым downstream artifact. - **WHEN** Change Owner явно одобряет неизменившиеся Delta Specs - **THEN** workflow завершает bounded `Proposal + Delta Specs` progression и - направляет следующий Specification outcome к Design + направляет следующий Specification outcome к Design для legacy либо к System Analysis для v2 #### Scenario: Design явно одобрен - **WHEN** Change Owner явно одобряет неизменившийся Design в bounded `Design + Tasks` progression -- **THEN** Specification MAY продолжить в той же роли и создать Tasks без - перехода к Implementation до отдельного approval Tasks +- **THEN** для legacy Specification MAY продолжить в той же роли и создать Tasks; + для v2 Tasks ожидает также QA Analysis и Docs Impact; Implementation ждёт отдельного approval Tasks #### Scenario: Завершилась реализация diff --git a/openspec/specs/brokerage-account-ledger/spec.md b/openspec/specs/brokerage-account-ledger/spec.md index 1cf98d8..36c10b2 100644 --- a/openspec/specs/brokerage-account-ledger/spec.md +++ b/openspec/specs/brokerage-account-ledger/spec.md @@ -127,3 +127,18 @@ #### Scenario: Пагинация по применённому фильтру - **WHEN** User переходит к следующей или предыдущей странице журнала при изменённом, но не применённом черновике - **THEN** пагинация выполняется по последнему применённому фильтру, а черновик в контролах сохраняется + +### Requirement: Отсутствующий исторический интервал загружается только впервые +Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение только недостающего интервала без ожидания T-Bank в локальном ответе. Это является исключением первой загрузки и MUST NOT запускать обновление других личных данных. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы или часового обхода. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным. + +#### Scenario: Первый просмотр отсутствующего прошлого периода +- **WHEN** User открывает доступный исторический период без подтверждённой полноты +- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных + +#### Scenario: Повторное открытие полного периода +- **WHEN** User повторно открывает уже полностью загруженный прошлый период +- **THEN** система читает сохранённые операции без повторной сверки с T-Bank + +#### Scenario: Ошибка загрузки +- **WHEN** загрузка части отсутствующего периода завершается ошибкой +- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной diff --git a/openspec/specs/brokerage-account-upcoming-events/spec.md b/openspec/specs/brokerage-account-upcoming-events/spec.md index 83f4ad4..cd450da 100644 --- a/openspec/specs/brokerage-account-upcoming-events/spec.md +++ b/openspec/specs/brokerage-account-upcoming-events/spec.md @@ -7,41 +7,53 @@ ## Requirements ### Requirement: User can retrieve upcoming events for an account -Система MUST предоставлять аутентифицированному User read-only список известных будущих событий открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User, от момента запроса до 30 календарных дней после него включительно. Ответ MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза. +Система MUST предоставлять аутентифицированному User read-only `GET /api/v1/brokerage-account-upcoming-events` для выбранного открытого поддерживаемого 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`, не подменять период, не запускать загрузку другого периода и не возвращать его прогноз. Обычный GET MUST NOT запускать личную синхронизацию. -Список будущих событий является прогнозом по текущему наблюдаемому состоянию счёта и MUST NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection. +Ответ MUST иметь `{ data, meta }`, где `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Доступный `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, `calculatedAt`, а в режиме `all` -- принадлежность каждого события счёту. Прогноз MUST быть рассчитан по количеству бумаг полного Portfolio Snapshot на `calculatedAt`; сохранённый прогноз MUST сохранять свой исходный `calculatedAt` и рассчитанное от него включительное окно, даже когда запрошенное `from`/`to` отличается. При чтении сохранённого прогноза система MUST возвращать только события, чьи даты попадают во включительное пересечение исходного окна прогноза и запрошенного UTC-окна; пустое пересечение либо отсутствие событий в пересечении MUST возвращать успешный пустой список с теми же `calculatedAt`, `meta` и исходным окном. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта; отсутствие вклада счёта MUST NOT подменяться полным результатом. Список будущих событий является прогнозом по наблюдаемому состоянию счёта и количеству бумаг на `calculatedAt` и MUST NOT означать долговременное право владения Brokerage Account независимо от T-Bank connection. #### Scenario: Получение событий доступного счёта -- **WHEN** аутентифицированный User запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection -- **THEN** система возвращает известные события на следующие 30 календарных дней и момент, на который был выполнен расчёт +- **WHEN** аутентифицированный User запрашивает события доступного счёта с последним полным прогнозом +- **THEN** система возвращает из сохранённого прогноза события в запрошенном включительном UTC-окне, его исходные `calculatedAt` и рассчитанное окно без обращения к T-Bank + +#### Scenario: Запрошенное окно не пересекается с сохранённым прогнозом +- **WHEN** запрошенный включительный UTC-окно не пересекается с исходным окном последнего полного прогноза +- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta` #### Scenario: Отсутствие событий в периоде -- **WHEN** для счёта нет известных событий в ближайшие 30 календарных дней -- **THEN** система возвращает успешный пустой список и момент расчёта +- **WHEN** пересечение запрошенного и исходного окна прогноза не содержит событий +- **THEN** система возвращает успешный пустой список, момент расчёта, исходное окно и `meta` + +#### Scenario: Некорректный период +- **WHEN** User передаёт `from` позже `to` +- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD` без прогноза и без запуска личной синхронизации + +#### Scenario: Прогноз всех счетов +- **WHEN** User запрашивает режим `all` и для каждого входящего счёта доступен полный прогноз +- **THEN** система возвращает события всех счетов в запрошенном окне с принадлежностью каждого события счёту #### Scenario: Отклонение недоступного счёта -- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection +- **WHEN** User запрашивает события счёта, недоступного через его текущий T-Bank connection - **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события ### Requirement: Forecasts preserve event meaning and uncertainty -Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по текущему количеству бумаг на момент запроса. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы. +Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного Portfolio Snapshot на `calculatedAt`, использованного для сохранённого прогноза, а не по количеству бумаг на момент HTTP-запроса. Сохранённый прогноз MUST сохранять свой `calculatedAt` и эту исходную зависимость, в том числе после неуспешного обновления, и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы. Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position. #### Scenario: Прогноз купона по текущей позиции -- **WHEN** поддерживаемая облигация в текущей Portfolio Position имеет известный купон в пределах периода -- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций +- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза +- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot #### Scenario: Прогноз дивиденда -- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода -- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций +- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза +- **THEN** система возвращает сохранённое событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на количество акций этого Snapshot #### Scenario: Отменённый дивиденд не попадает в прогноз - **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled` -- **THEN** система не добавляет такое событие в результат +- **THEN** система не добавляет такое событие в сохранённый прогноз #### Scenario: Отображение оферты без суммы -- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны +- **WHEN** для поддерживаемой облигации известна оферта в пределах исходного окна прогноза, но её денежные условия недоступны - **THEN** система возвращает событие offer с прогнозной суммой `null` #### Scenario: Неподдерживаемое частичное погашение @@ -49,42 +61,66 @@ Instrument в Future Cash Event MUST иметь тот же доменный с - **THEN** система НЕ создаёт синтетическое событие частичного погашения ### Requirement: Upcoming events use complete source results -Система MUST завершать запрос ошибкой, если необходимые для прогноза данные брокера или MOEX недоступны, и MUST NOT возвращать частичный список как полный результат. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных». +Система MUST публиковать прогноз только после полного получения необходимых данных брокера и MOEX для полного Portfolio Snapshot, по количеству бумаг которого он рассчитан. Неуспешное обновление MUST NOT заменять последний полный прогноз, его `calculatedAt` или окно событий частичным списком. При неуспешном обновлении и существующем последнем полном прогнозе система MUST возвращать отфильтрованную для запрошенного окна часть этого прогноза с его исходными `calculatedAt`, окном и `meta`, отдельно обозначающим неуспех последней попытки, даже если результат фильтрации пуст. При отсутствии последнего полного прогноза система MUST возвращать `data: null` и в `meta` обозначать выполнение первой загрузки либо её неуспех без частичного списка. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных». #### Scenario: Недоступность источника -- **WHEN** необходимый источник данных брокера или MOEX временно недоступен -- **THEN** система возвращает ошибку запроса будущих событий без частичного списка +- **WHEN** необходимый источник данных брокера или MOEX временно недоступен во время часового обновления +- **THEN** система не публикует частичный список как полный прогноз + +#### Scenario: Ошибка обновления при сохранённом прогнозе +- **WHEN** часовой обход не завершает обновление необходимых источников, а последний полный прогноз существует +- **THEN** система возвращает его события только в запрошенном окне, исходные `calculatedAt` и окно, а `meta` отдельно обозначает ошибку обновления + +#### Scenario: Ошибка обновления при сохранённом прогнозе вне запрошенного окна +- **WHEN** часовой обход не завершает обновление, последний полный прогноз существует, но его окно не пересекается с запрошенным +- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`, обозначающим ошибку обновления #### Scenario: Инструмент без поддерживаемых данных - **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события - **THEN** система не добавляет для него синтетическую строку в результат #### Scenario: MOEX отвечает без запрошенного блока -- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов) -- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой +- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных +- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой ### Requirement: Account page presents an upcoming events list -Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать все полученные события компактным списком без фильтров, пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника. +Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию. #### Scenario: Просмотр ближайших событий - **WHEN** User открывает «Будущие события» из обзора Brokerage Account -- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы +- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы #### Scenario: Пустое состояние страницы - **WHEN** запрос будущих событий успешно возвращает пустой список -- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет ограничения покрытия источника +- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника + +#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза +- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение #### Scenario: Ошибка страницы - **WHEN** запрос будущих событий завершается ошибкой -- **THEN** страница не показывает частичный или устаревший список и предоставляет действие повторить запрос +- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение + +#### Scenario: Ошибка обновления при сохранённом прогнозе на странице +- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления +- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение ### Requirement: Account overview includes upcoming events -Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio. +Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение. #### Scenario: Отображение ближайших событий -- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события -- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице +- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события +- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице + +#### Scenario: Ближайшие события недоступны без сохранённого прогноза +- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка #### Scenario: Ближайшие события недоступны - **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой - **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку + +#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз +- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления +- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение diff --git a/openspec/specs/brokerage-accounts/spec.md b/openspec/specs/brokerage-accounts/spec.md index 7f74a4a..f56a022 100644 --- a/openspec/specs/brokerage-accounts/spec.md +++ b/openspec/specs/brokerage-accounts/spec.md @@ -7,57 +7,65 @@ ## Requirements ### Requirement: Список активных брокерских счетов -Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив открытых поддерживаемых инвестиционных счетов 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 удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа. +Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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`. +Каждый элемент 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. #### Scenario: Поддерживаемые счета возвращаются в upstream-порядке -- **WHEN** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт -- **THEN** система отвечает `200` массивом брокерского счёта и ИИС в исходном порядке T-Bank без дебетового счёта +- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт +- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank #### Scenario: Пустой список поддерживаемых счетов -- **WHEN** User с действующим T-Bank connection не имеет открытых поддерживаемых инвестиционных счетов -- **THEN** система отвечает `200` пустым массивом +- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов +- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат #### Scenario: Пустое название получает fallback -- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем +- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя - **THEN** система отвечает этим счётом с непустым названием его нормализованного типа #### Scenario: Дата открытия отсутствует -- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта +- **WHEN** последний подтверждённый счёт не имеет даты открытия - **THEN** система отвечает этим счётом с `openedOn: null` #### Scenario: Мультивалютная сводка счёта -- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD +- **WHEN** последний Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD - **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance #### Scenario: Сводка одного счёта временно недоступна -- **WHEN** список доступных счетов получен, но Portfolio одного из них временно недоступен -- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка +- **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio +- **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null` #### Scenario: Доступный Portfolio пуст -- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков +- **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков - **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency` ### Requirement: Состояния недоступности списка счетов -Система MUST NOT возвращать частичный или сохранённый ранее список счетов. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклоняет сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При иной неуспешной попытке получить список из T-Bank система MUST отвечать `503` Problem Details с code `TBANK_UNAVAILABLE`. +Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный. #### Scenario: Подключение отсутствует - **WHEN** User без сохранённого T-Bank connection запрашивает список - **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED` #### Scenario: Сохранённый token больше не действителен -- **WHEN** T-Bank отклоняет сохранённый token при запросе списка как неаутентифицированный или без прав -- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection +- **WHEN** T-Bank отклоняет сохранённый token при часовом обходе списка как неаутентифицированный или без прав +- **THEN** последующее чтение отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные + +#### Scenario: Ошибка обхода после успешного списка +- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки +- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank #### Scenario: T-Bank временно недоступен -- **WHEN** запрос списка не завершается из-за временной либо иной upstream-ошибки -- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` без счетов +- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой +- **THEN** система сообщает о неуспешной первой загрузке без счетов + +#### Scenario: Первый обход ещё не завершён +- **WHEN** User открывает список после подключения до получения первого подтверждённого списка +- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка ### Requirement: Экран списка брокерских счетов -Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать актуальный список; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов. +Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Он MUST использовать успешный Client Cached Snapshot списка в течение 30 минут. Открытый экран Accounts MUST перечитывать локальный backend-результат каждые 30 минут и MUST NOT запускать импорт T-Bank. Клиент MUST показывать наблюдаемые freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. При уже имеющемся успешном списке клиент MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового HTTP-перечитывания и после его ошибки, ясно отделяя это состояние от свежего backend-результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов. Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку. @@ -69,17 +77,29 @@ - **WHEN** клиент получает успешный пустой массив - **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank +#### Scenario: Последняя попытка неуспешна после полного списка +- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки +- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке + +#### Scenario: Первая загрузка списка выполняется или неуспешна +- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех +- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного + #### Scenario: Список берётся из свежего frontend-кэша -- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки +- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки - **THEN** клиент показывает сохранённые счета без нового API-запроса #### Scenario: Устаревший список загружается повторно -- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список -- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление +- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список +- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление + +#### Scenario: Открытый экран перечитывает список +- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения +- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank #### Scenario: Ошибка фонового обновления не скрывает snapshot -- **WHEN** обновление устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot -- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки +- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot +- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки #### Scenario: Карточка ограничивает валютные итоги - **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах @@ -94,15 +114,15 @@ - **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности ### Requirement: Клиентские состояния ошибки списка счетов -Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. +Клиент MUST NOT автоматически повторять неуспешное HTTP-чтение списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного HTTP-чтения. Повтор MUST перечитывать backend-результат и MUST NOT запускать импорт T-Bank. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. #### Scenario: Подключение требуется - **WHEN** клиент получает `TBANK_NOT_CONNECTED` - **THEN** клиент не показывает список и предлагает открыть настройки T-Bank #### Scenario: Известная ошибка не повторяется автоматически -- **WHEN** запрос списка завершается известной ошибкой -- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки +- **WHEN** HTTP-чтение списка завершается известной ошибкой +- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения #### Scenario: Token требует внимания - **WHEN** клиент получает `INVALID_TBANK_TOKEN` @@ -110,12 +130,12 @@ #### Scenario: Временная ошибка T-Bank - **WHEN** клиент получает `TBANK_UNAVAILABLE` -- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank +- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank #### Scenario: Неизвестная ошибка загрузки - **WHEN** клиент получает ошибку без известного кода списка счетов -- **THEN** клиент не показывает список и предлагает только повторить загрузку +- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение #### Scenario: Известная ошибка при сохранённом snapshot -- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` -- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank +- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` +- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank diff --git a/openspec/specs/frontend-data-resilience/spec.md b/openspec/specs/frontend-data-resilience/spec.md index f0193c0..cf269b8 100644 --- a/openspec/specs/frontend-data-resilience/spec.md +++ b/openspec/specs/frontend-data-resilience/spec.md @@ -9,28 +9,20 @@ ## 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`. +Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для того же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Для каждого личного resource response клиент MUST представлять freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` текстом либо доступно именованным элементом, а не только цветом; он MUST различать отсутствие попытки, выполнение, полный успех, частичный успех, неуспех, доступный пустой полный результат и отсутствие полного результата. Client Cached Snapshot MUST оставаться видимым во время фонового HTTP-перечитывания и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повторное действие User MUST повторять только HTTP-чтение и MUST NOT запускать личную синхронизацию. Открытый инвестиционный экран MAY периодически перечитывать backend-результат, но MUST NOT создавать импорт T-Bank. #### Scenario: Список Brokerage Accounts обновляется в фоне -- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается - фоновое обновление того же списка -- **THEN** frontend оставляет список видимым и показывает только - неблокирующий индикатор обновления +- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое HTTP-перечитывание того же списка +- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор перечитывания #### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа -- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot - Portfolio, а последующее обновление этого же Portfolio завершается ошибкой -- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное - неблокирующее предупреждение о показе последних полученных данных и - предоставляет повторную попытку обновления +- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее HTTP-чтение завершается ошибкой +- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение и предоставляет повтор только HTTP-чтения #### Scenario: Server-side fallback и client cached snapshot различаются -- **WHEN** backend возвращает Portfolio с `isStale` либо фоновое обновление - Client Cached Snapshot завершается ошибкой -- **THEN** frontend отображает соответствующее server-side либо client-side - предупреждение независимо, не представляя один вид состояния другим +- **WHEN** backend возвращает Portfolio с `isStale` либо HTTP-перечитывание Client Cached Snapshot завершается ошибкой +- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя одно состояние другим + +#### Scenario: Resource сохраняется после неуспешной часовой попытки +- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки +- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки diff --git a/openspec/specs/hourly-investment-sync/spec.md b/openspec/specs/hourly-investment-sync/spec.md new file mode 100644 index 0000000..848f981 --- /dev/null +++ b/openspec/specs/hourly-investment-sync/spec.md @@ -0,0 +1,81 @@ +# hourly-investment-sync Specification + +## Purpose +Определяет независимое от открытых экранов часовое обновление личных инвестиционных данных и безопасно наблюдаемый результат последней попытки. + +## Requirements + +### Requirement: Часовой обход личных инвестиционных данных +Система SHALL начинать первый часовой обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки этого подключения. После простоя система MUST выполнить не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока. Перекрывающиеся обходы MUST NOT выполняться: срок, наступивший во время выполняющегося обхода, MUST быть пропущен и MUST NOT ставиться в очередь; следующий обход становится допустимым только в следующий 60-минутный срок, отсчитанный от начала выполнявшейся попытки. Обход MUST обновлять доступные счета, Portfolio каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего Portfolio. Он MUST NOT повторно загружать полные прошлые интервалы операций, завершённые свечи или полную рыночную историю и MUST NOT зависеть от открытия frontend-экрана. + +#### Scenario: Первое подключение +- **WHEN** User успешно подключает T-Bank между часовыми сроками +- **THEN** первый обход начинается без ожидания следующего календарного часа + +#### Scenario: Возобновление после простоя +- **WHEN** backend запускается после пропуска нескольких часовых сроков +- **THEN** система выполняет один актуальный обход без синтетических прошлых обновлений + +#### Scenario: Перекрытый срок +- **WHEN** предыдущий обход всё ещё выполняется в момент очередного срока +- **THEN** система не начинает второй обход и не ставит пропущенный обход в очередь + +#### Scenario: Длительный обход пропускает один срок +- **WHEN** обход начался в 10:00, всё ещё выполняется в 11:00 и завершился до 12:00 +- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00 + +### Requirement: Последний результат обхода доступен только для наблюдения +Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию. + +#### Scenario: Частичный успех +- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился +- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт + +#### Scenario: Наблюдение не запускает обход +- **WHEN** User запрашивает результат последнего обхода +- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank + +#### Scenario: Старая граница подключения +- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода +- **THEN** система не раскрывает результат прежней границы подключения + +#### Scenario: Сбой после частичного обновления +- **WHEN** попытка прервана ошибкой либо сбоем сохранения после обновления Portfolio одного доступного счёта +- **THEN** результат попытки обозначает частичный успех с проблемными шагами, а не полный успех, а обновлённый Portfolio сохраняется + +#### Scenario: Сбой до первого обновления +- **WHEN** попытка прервана до обновления любого обязательного шага +- **THEN** результат попытки обозначает неуспех, а не полный успех + +### Requirement: Каждая попытка обхода достигает итога в ограниченное время +Каждая начатая попытка часового обхода MUST достигать итогового состояния в ограниченное время. Ожидание завершения каждого обязательного шага MUST быть ограничено; шаг, не завершившийся в это ограничение, MUST быть учтён как проблемный, а попытка MUST быть закрыта частичным успехом либо неуспехом после того, как каждый обязательный шаг достиг терминального результата либо был учтён как проблемный. Истечение ограничения MUST NOT отменять уже начатые шаги. + +#### Scenario: Шаг не завершается +- **WHEN** обязательный шаг не покидает состояние выполнения дольше ограничения ожидания +- **THEN** шаг учитывается как проблемный, попытка закрывается частичным успехом либо неуспехом, а её состояние перестаёт быть выполняющимся + +#### Scenario: Наблюдение не ждёт без ограничения +- **WHEN** попытка не может завершиться из-за незавершённых шагов +- **THEN** наблюдение результата получает итоговое состояние в ограниченное время и не ожидает завершения бесконечно + +### Requirement: Восстановление после сбоя сохранения не блокирует последующие обходы +Если попытка осталась в состоянии выполнения после сбоя завершения шага, публикации результата либо закрытия попытки, система MUST привести её к диагностируемому итоговому состоянию: частичный успех либо неуспех с сохранёнными результатами шагов, а не полный успех. Последующий обход MUST становиться допустимым по обычному правилу срока, отсчитанному от начала этой попытки. Восстановление MUST NOT создавать перекрывающийся обход и MUST NOT повторять уже полученные результаты шагов. + +#### Scenario: Потерянное закрытие попытки +- **WHEN** закрытие попытки не сохранилось, а процесс продолжает работать +- **THEN** система закрывает попытку итогом из фактических результатов шагов, наблюдение получает итоговое состояние, а следующая допустимая попытка может стартовать + +#### Scenario: Перезапуск процесса +- **WHEN** процесс остановился после начала попытки и запускается снова +- **THEN** брошенная попытка обозначается частичным успехом либо неуспехом с сохранёнными результатами шагов, а новый обход начинается не ранее следующего часового срока от её начала + +### Requirement: Устаревшая публикация не влияет на актуальную попытку +Результат шага попытки, у которой появился преемник, MUST отклоняться как устаревший: он MUST NOT обновлять данные и MUST NOT учитываться в итоге актуальной попытки. Шаг устаревшей попытки MUST наблюдаться как устаревший относительно актуального преемника. Результат шага текущей попытки, полученный до фиксации её итога, MUST сохраняться и учитываться в этом итоге независимо от того, наступил ли он до или после истечения ограничения ожидания. + +#### Scenario: Поздняя публикация после появления преемника +- **WHEN** шаг старой попытки завершается после запуска следующей попытки +- **THEN** его результат отклоняется как устаревший, данные не обновляются, а итог актуальной попытки не меняется + +#### Scenario: Поздняя публикация до фиксации итога +- **WHEN** шаг завершился после истечения ограничения ожидания, но итог его попытки ещё не зафиксирован +- **THEN** его результат сохраняется и учитывается при выводе итога этой попытки diff --git a/openspec/specs/manual-bank-cards/spec.md b/openspec/specs/manual-bank-cards/spec.md new file mode 100644 index 0000000..4ad4c2b --- /dev/null +++ b/openspec/specs/manual-bank-cards/spec.md @@ -0,0 +1,680 @@ +# manual-bank-cards Specification + +## Purpose +Позволяет пользователю вручную вести дебетовые банковские карты в рублях: +сохранять операции, видеть независимые балансы, исправлять записи и оценивать +месячные расходы по категориям с учётом возвратов. + +## Requirements + +### Requirement: Отдельный раздел ручного учёта банковских карт + +Система SHALL предоставлять авторизованному пользователю раздел банковских +карт на `/cards` и просмотр отдельной своей карты на `/cards/:id`. +Раздел MUST работать без подключения T-Bank и не зависеть от доступности +инвестиционных данных. Банковские карты MUST NOT становиться брокерскими +счетами, а банковские суммы и операции MUST NOT включаться в инвестиционный +портфель, пассивный доход или прогноз выплат. + +Первая версия SHALL поддерживать только ручной учёт дебетовых карт в RUB. +Автоматическая загрузка из банков, кредитные карты, другие валюты, общий +денежный баланс нескольких карт, прогнозы и бюджеты не входят в возможность. + +#### Scenario: Работа без инвестиционного подключения + +- **WHEN** пользователь с действующей локальной сессией, но без подключения T-Bank, открывает раздел карт +- **THEN** он может просматривать и вести свои банковские карты без перехода к подключению брокера + +#### Scenario: Независимость от инвестиционных данных + +- **WHEN** пользователь сохраняет банковское пополнение или расход, а инвестиционные данные временно недоступны +- **THEN** банковская операция сохраняется по правилам ручного учёта и не меняет инвестиционные суммы или пассивный доход + +### Requirement: Ручное добавление независимых банковских карт + +Система SHALL позволять пользователю добавить карту с названием, начальным +остатком и датой начала учёта. Название MUST содержать от 1 до 100 символов +после удаления внешних пробелов. Название банка необязательно и MUST содержать +не более 100 символов после удаления внешних пробелов. Последние четыре цифры +карты необязательны; указанное значение MUST состоять ровно из четырёх цифр. +Полный номер карты, срок действия, PIN и код безопасности не запрашиваются +и не сохраняются как реквизиты этой возможности. + +Каждая карта MUST иметь собственную идентичность, баланс и историю независимо +от совпадения банка, названия или последних цифр. Пользователь SHALL иметь +возможность изменить указанные сведения о своей карте с теми же ограничениями. +Финансовые изменения начального остатка и даты начала подчиняются отдельным +требованиям исправления и архива. + +#### Scenario: Добавление карты с начальным остатком + +- **WHEN** пользователь сохраняет карту с допустимым названием, датой начала учёта и начальным остатком 10 000 ₽ +- **THEN** карта появляется в его списке с балансом 10 000 ₽ и без вручную внесённых операций + +#### Scenario: Необязательные сведения не заполнены + +- **WHEN** пользователь добавляет карту без названия банка и последних четырёх цифр +- **THEN** отсутствие этих сведений не препятствует сохранению карты при корректности обязательных значений + +#### Scenario: Несколько карт одного банка + +- **WHEN** пользователь добавляет две карты одного банка с одинаковым названием и начальными остатками 1 000 ₽ и 2 000 ₽ +- **THEN** система сохраняет две самостоятельные карты с раздельными балансами и историей + +#### Scenario: Некорректные сведения карты + +- **WHEN** название после удаления внешних пробелов пустое или длиннее 100 символов, название банка длиннее 100 символов либо указанные последние цифры не состоят ровно из четырёх цифр +- **THEN** система отклоняет сохранение, объясняет ошибку соответствующего поля и не создаёт или не изменяет карту + +### Requirement: Точные денежные значения и проверка до сохранения + +Система MUST принимать денежные значения только в RUB, с точностью не более +двух знаков после десятичного разделителя, без округления введённой суммы. +Сумма расхода, пополнения, перевода или возврата MUST быть строго положительной +и не превышать 92 233 720 368 547 758,07 ₽. Начальный остаток MUST находиться +в диапазоне от −92 233 720 368 547 758,08 ₽ до 92 233 720 368 547 758,07 ₽ +включительно; ноль и отрицательный начальный остаток допустимы. + +Значение, не представляющее корректную денежную сумму, неподдерживаемая +валюта, лишние десятичные знаки или выход за диапазон MUST приводить +к понятной ошибке без частичного сохранения. Применение ограничения +к каждому денежному вводу MUST NOT приводить к округлению, переполнению +или ограничению точного результата сложения уже сохранённых сумм. + +#### Scenario: Нулевой и отрицательный начальный остаток + +- **WHEN** пользователь сохраняет отдельные карты с начальными остатками 0 ₽ и −100 ₽ +- **THEN** система сохраняет обе карты и показывает соответствующие известные балансы + +#### Scenario: Неположительная сумма операции + +- **WHEN** пользователь пытается внести операцию с суммой 0 ₽ или −1 ₽ +- **THEN** система отклоняет операцию, сохраняя прежние историю, балансы и сводки + +#### Scenario: Точность не исправляется округлением + +- **WHEN** пользователь пытается сохранить начальный остаток или операцию со значением 1,001 ₽ +- **THEN** система сообщает о допустимых двух десятичных знаках и не заменяет введённое значение на 1 ₽ + +#### Scenario: Границы денежных значений + +- **WHEN** пользователь указывает верхнюю допустимую сумму операции или нижний допустимый начальный остаток +- **THEN** сами эти граничные значения принимаются, а выход за соответствующую границу на одну копейку отклоняется без изменения данных + +#### Scenario: Некорректная сумма или валюта + +- **WHEN** поступает попытка сохранить нечисловую сумму либо сумму в валюте, отличной от RUB +- **THEN** система объясняет ошибку и не сохраняет карту, операцию или изменение существующих данных + +### Requirement: Даты начала учёта и ручных операций + +Дата начала учёта и дата операции MUST быть существующими календарными датами +без времени. Сегодня определяется по Europe/Moscow. Дата начала учёта +MUST NOT быть позже сегодняшней даты. Начальный остаток означает деньги +до всех вручную внесённых операций начиная с указанной даты включительно. + +Дата операции MUST находиться между датой начала учёта затронутой карты +и сегодняшним днём включительно. Для перевода ограничение MUST выполняться +для обеих карт. Ввод прошедших операций внутри этого интервала SHALL быть +доступен. Необязательное описание операции MUST содержать не более +500 символов. Сохранённая календарная дата MUST NOT сдвигаться при смене +часового пояса устройства. + +#### Scenario: Операция в первый день учёта + +- **WHEN** при начальном остатке 1 000 ₽ на 1 сентября пользователь вносит расход 100 ₽ с датой 1 сентября +- **THEN** расход учитывается после начального остатка и баланс становится 900 ₽ + +#### Scenario: Прошедшая операция + +- **WHEN** пользователь вносит операцию задним числом не раньше начала учёта и не позже сегодняшнего дня +- **THEN** операция сохраняется с указанной датой и изменяет текущий баланс и сводку соответствующего месяца + +#### Scenario: Недопустимая дата или описание + +- **WHEN** дата не существует, находится до начала учёта или в будущем либо описание длиннее 500 символов +- **THEN** система объясняет ошибку и не сохраняет изменение + +#### Scenario: Перевод до начала учёта получателя + +- **WHEN** дата перевода допустима для карты-источника, но предшествует началу учёта карты-получателя +- **THEN** перевод отклоняется без изменения обеих карт + +#### Scenario: Московская календарная граница + +- **WHEN** на устройстве ещё предыдущий день, а в Europe/Moscow уже наступил следующий +- **THEN** допустимая сегодняшняя дата и текущий месяц определяются по Europe/Moscow, а сохранённые даты операций остаются прежними + +### Requirement: Баланс определяется начальным остатком и внесёнными операциями + +Баланс карты MUST равняться начальному остатку плюс денежное влияние всех +её сохранённых и не удалённых операций: пополнение и возврат увеличивают +баланс, расход уменьшает, перевод уменьшает баланс источника и увеличивает +баланс получателя. Начальный остаток MUST учитываться ровно один раз +и MUST NOT считаться расходом, возвратом или пополнением. + +Баланс MUST рассчитываться точно и может быть отрицательным. Недостаточный +учётный остаток MUST NOT препятствовать сохранению корректного расхода +или перевода. Баланс MUST NOT зависеть от выбранного месяца, категории, +показанной страницы истории или нахождения карты в архиве. + +Система SHALL представлять баланс как результат ручного учёта и MUST NOT +выдавать его за подтверждённый банком остаток или обещать полноту невнесённых +операций. Отдельной произвольной операции корректировки баланса SHALL NOT быть. + +#### Scenario: Последовательные расходы с копейками + +- **WHEN** при начальном остатке 1 ₽ сохраняются расходы 0,10 ₽ и 0,20 ₽ +- **THEN** баланс составляет ровно 0,70 ₽ + +#### Scenario: Полное расходование и отрицательный остаток + +- **WHEN** при балансе 100 ₽ пользователь сохраняет расход 100 ₽, затем расход 50 ₽ +- **THEN** первая операция даёт известный баланс 0 ₽, вторая сохраняется и даёт баланс −50 ₽ + +#### Scenario: Начальный остаток и страница истории + +- **WHEN** при начальном остатке 1 000 ₽ и расходах 100 ₽ и 200 ₽ пользователь повторно открывает карту и видит только расход 200 ₽ +- **THEN** текущий баланс остаётся 700 ₽: учитываются обе операции, а начальный остаток не добавляется повторно + +#### Scenario: Итог превышает границу отдельного ввода + +- **WHEN** на карту с начальным остатком 92 233 720 368 547 758,07 ₽ сохраняется пополнение 0,01 ₽ +- **THEN** баланс составляет точно 92 233 720 368 547 758,08 ₽ без переполнения, округления или ограничения верхней границей ввода + +### Requirement: Ручное внесение расходов и пополнений + +Система SHALL позволять вручную внести расход или пополнение по своей активной +карте с суммой, датой и необязательным описанием. Расход MUST иметь категорию; +если пользователь не выбирает другую, применяется его категория по умолчанию. +После успешного сохранения операция MUST появиться в истории соответствующей +карты и ровно один раз изменить её баланс. + +Расход SHALL уменьшать баланс на свою сумму, пополнение SHALL увеличивать. +Категория MUST NOT изменять денежное влияние расхода. Пополнение MUST NOT +включаться в расходы, возвраты или пассивный инвестиционный доход. + +#### Scenario: Расход с категорией + +- **WHEN** при балансе 10 000 ₽ пользователь сохраняет расход 500 ₽ с категорией «Кафе» +- **THEN** история показывает расход 500 ₽ с этой категорией, а баланс составляет 9 500 ₽ + +#### Scenario: Категория по умолчанию + +- **WHEN** пользователь сохраняет расход, не выбрав другую категорию +- **THEN** расход сохраняется с принадлежащей пользователю категорией по умолчанию и отображается под её текущим названием + +#### Scenario: Пополнение карты + +- **WHEN** при балансе −500 ₽ пользователь сохраняет пополнение 2 000 ₽ +- **THEN** баланс становится 1 500 ₽, история содержит пополнение, а расходы и возвраты не меняются + +#### Scenario: Независимость другой карты + +- **WHEN** при балансах A 1 000 ₽ и B 2 000 ₽ пользователь сохраняет расход 125,50 ₽ по A +- **THEN** баланс A становится 874,50 ₽, а баланс и история B остаются прежними + +### Requirement: Перевод между своими картами как одно действие + +Система SHALL позволять одним действием внести перевод между двумя разными +активными картами одного пользователя. Сумма перевода MUST одинаково уменьшать +баланс источника и увеличивать баланс получателя. История каждой стороны +MUST показывать направление и другую карту этого перевода. + +Перевод MUST сохраняться, исправляться и удаляться целиком для обеих сторон. +Нельзя сохранять только одну сторону, переводить на ту же или чужую карту. +Перевод MUST NOT увеличивать расходы или возвраты в сводках одной или всех карт. + +#### Scenario: Перевод между картами + +- **WHEN** при балансах A 5 000 ₽ и B 1 000 ₽ пользователь сохраняет перевод 2 000 ₽ из A в B +- **THEN** баланс A становится 3 000 ₽, баланс B — 3 000 ₽, обе истории показывают стороны одного перевода, а расходы не меняются + +#### Scenario: Недостаточный остаток источника + +- **WHEN** при балансах A 100 ₽ и B 0 ₽ пользователь сохраняет перевод 150 ₽ из A в B +- **THEN** перевод сохраняется целиком, баланс A становится −50 ₽, а баланс B — 150 ₽ + +#### Scenario: Недопустимый получатель + +- **WHEN** получателем указана та же карта или карта другого пользователя +- **THEN** система отклоняет перевод без изменения историй, балансов и сводок + +#### Scenario: Сбой сохранения перевода + +- **WHEN** перевод не удаётся сохранить +- **THEN** система не подтверждает успех и не оставляет сохранённой только одну его сторону + +### Requirement: Возврат связан с исходным расходом + +Возврат SHALL ссылаться на существующий не удалённый расход той же карты. +При внесении возврата пользователь SHALL иметь возможность выбрать исходный +расход карты за любой период учёта, независимо от месяца открытого журнала. +Частичные и несколько возвратов допустимы; сумма всех не удалённых возвратов +MUST NOT превышать текущую сумму исходного расхода. Дата возврата MUST быть +не раньше даты расхода и соответствовать общим ограничениям даты карты. +При редактировании возврата проверка общей возвращённой суммы MUST заменять +его прежнюю сумму новой, не учитывая обе версии одновременно. + +Возврат MUST увеличивать баланс карты и наследовать категорию исходного +расхода. Отдельный выбор категории возврата SHALL NOT предоставляться. +Изменение категории исходного расхода MUST менять категорию связанных +возвратов и результаты затронутых месячных сводок, сохраняя их суммы и даты. + +#### Scenario: Частичные возвраты + +- **WHEN** по расходу 1 000 ₽ пользователь последовательно сохраняет возвраты 300 ₽ и 700 ₽ +- **THEN** оба возврата связаны с расходом, увеличивают баланс суммарно на 1 000 ₽ и используют его категорию + +#### Scenario: Выбор покупки прошлого месяца + +- **WHEN** пользователь открывает сентябрьский журнал и вносит возврат по августовской покупке +- **THEN** форма позволяет найти и выбрать исходный августовский расход этой карты без ограничения сентябрьским журналом + +#### Scenario: Исправление полностью возвращённой покупки + +- **WHEN** по расходу 500 ₽ сохранён единственный возврат 500 ₽ и пользователь изменяет только описание возврата +- **THEN** исправление сохраняется: общая возвращённая сумма остаётся 500 ₽, а прежняя и новая версии не складываются как два возврата + +#### Scenario: Превышение возвращаемой суммы + +- **WHEN** при расходе 1 000 ₽ и сохранённом возврате 800 ₽ пользователь пытается сохранить ещё 300 ₽ +- **THEN** система отклоняет новый возврат без изменения существующих записей и сумм + +#### Scenario: Недопустимая связь или дата возврата + +- **WHEN** возврат ссылается на удалённую операцию, операцию другого вида или другой карты либо датирован раньше расхода +- **THEN** система отклоняет возврат без изменения истории, баланса и сводок + +#### Scenario: Изменение категории покупки + +- **WHEN** пользователь меняет категорию расхода с «Покупки» на «Здоровье», а возврат по нему произошёл в другом месяце +- **THEN** расход и возврат учитываются в категории «Здоровье» в своих месяцах с прежними денежными суммами + +### Requirement: Общие для пользователя категории расходов + +Система SHALL предоставлять каждому пользователю собственный общий для всех +его карт список категорий: «Продукты», «Кафе», «Такси», «Транспорт», «Жильё», +«Здоровье», «Покупки», «Развлечения», «Другое». «Другое» является начальной +категорией по умолчанию. Категории MUST быть доступны до первого расхода. +Начальный набор MUST создаваться при успешном добавлении первой карты +пользователя и не дублироваться при добавлении следующих карт. + +Пользователь SHALL иметь возможность добавлять и переименовывать любые свои +категории, включая начальные. Название MUST содержать от 1 до 80 символов +после удаления внешних пробелов и быть уникальным среди категорий пользователя +без учёта регистра. Переименование MUST сохранять идентичность категории: +все старые расходы, возвраты и сводки показывают её текущее название. +Переименование категории по умолчанию MUST сохранять её роль. +Удаление категорий и вложенные подкатегории SHALL NOT предоставляться в V1. + +#### Scenario: Начальный набор и повторный вход + +- **WHEN** пользователь сохраняет первую карту, затем добавляет следующую карту и повторно входит в приложение +- **THEN** после первой карты ему доступен начальный набор категорий до первого расхода, а дальнейшие действия не создают дубликатов + +#### Scenario: Категория доступна на всех своих картах + +- **WHEN** пользователь создаёт категорию «Питомец» +- **THEN** она доступна для расходов по всем его картам и не появляется у другого пользователя + +#### Scenario: Переименование используемой начальной категории + +- **WHEN** пользователь переименовывает категорию «Другое» в «Остальное» +- **THEN** прежние записи отображаются как «Остальное», суммы не меняются, а новые расходы без другого выбора используют ту же категорию «Остальное» + +#### Scenario: Дублирующееся или недопустимое название + +- **WHEN** пользователь пытается добавить « кафе » при существующей «Кафе», сохранить пустое после удаления пробелов название или название длиннее 80 символов +- **THEN** система объясняет ошибку и не изменяет список категорий + +### Requirement: Исправление и удаление операций с согласованным пересчётом + +Пользователь SHALL иметь возможность изменить сумму, дату, описание +и применимые реквизиты сохранённой операции либо удалить её. Вид операции +и связь возврата с исходным расходом MUST быть неизменны после создания. +Все затронутые карты MUST быть активны; каждое исправление MUST соблюдать +те же денежные, календарные и пользовательские ограничения, что и создание. + +Обычный расход или пополнение SHALL быть переносимы на другую свою активную +карту. Расход с не удалёнными возвратами MUST NOT переноситься на другую карту, +удаляться, уменьшаться ниже суммы возвратов или сдвигаться на дату позже +любого связанного возврата. Для такого действия сначала исправляются +или удаляются мешающие возвраты. Возврат остаётся на карте исходного расхода. + +Исправление или удаление MUST пересчитать балансы, историю и все затронутые +месячные сводки; изменение даты переносит влияние между соответствующими +месяцами. Исправление сторон перевода MUST учитывать его прежние и новые +карты. Удаление перевода MUST удалять обе стороны одним действием. +Повторное удаление уже удалённой собственной операции SHALL быть +безрезультатным успешным действием без повторного денежного влияния. + +#### Scenario: Изменение суммы и месяца расхода + +- **WHEN** пользователь меняет расход 100 ₽ за август на 150 ₽ за сентябрь +- **THEN** баланс дополнительно уменьшается на 50 ₽, август перестаёт учитывать 100 ₽, а сентябрь учитывает 150 ₽ + +#### Scenario: Перенос пополнения на другую карту + +- **WHEN** пользователь переносит пополнение 500 ₽ с A на свою активную B с допустимой датой +- **THEN** баланс A уменьшается на 500 ₽, баланс B увеличивается на 500 ₽, а запись относится к B + +#### Scenario: Изменение получателя перевода + +- **WHEN** перевод 200 ₽ из A в B исправляется на перевод 300 ₽ из A в C и все три карты активны +- **THEN** прежнее влияние на B отменяется, A уменьшается дополнительно на 100 ₽, C увеличивается на 300 ₽, а обе стороны исправленного перевода сохраняются согласованно + +#### Scenario: Исправление расхода ограничено возвратами + +- **WHEN** для расхода 1 000 ₽ существует возврат 600 ₽ и пользователь пытается удалить расход, перенести его на другую карту, уменьшить до 500 ₽ или сдвинуть позже даты возврата +- **THEN** система отклоняет действие и объясняет необходимость сначала исправить связанные возвраты + +#### Scenario: Удаление возврата и затем расхода + +- **WHEN** пользователь удаляет все возвраты расхода, а затем сам расход +- **THEN** влияние каждой удалённой записи отменяется в балансе и в её месячной сводке, а ограничение удаления исходного расхода больше не действует + +#### Scenario: Удаление перевода из любой стороны + +- **WHEN** пользователь удаляет перевод из истории получателя и затем повторяет удаление +- **THEN** обе стороны удаляются при первом действии, денежное влияние отменяется один раз, а повторное удаление ничего не меняет + +#### Scenario: Смена вида или исходного расхода + +- **WHEN** пользователь пытается превратить расход в пополнение либо связать существующий возврат с другим расходом +- **THEN** изменение отклоняется без подмены вида операции или её исходной связи + +### Requirement: Исправление начала ручного учёта + +Пользователь SHALL иметь возможность изменить начальный остаток и дату начала +учёта активной карты. Изменение MUST проверять всю затронутую сохранённую +историю, включая стороны переводов и связанные возвраты, и отклоняться +без частичного результата, если после него какая-либо операция нарушает +действующие ограничения. Удалённые операции не ограничивают новую дату. + +Изменение начального остатка SHALL пересчитывать текущий баланс, сохраняя +суммы, даты и категории операций. Оно MUST NOT создавать отдельную операцию +или изменять месячные расходы и возвраты. Пропущенное движение SHALL +исправляться внесением реальной операции с допустимой датой. + +#### Scenario: Исправление начального остатка + +- **WHEN** при начальном остатке 1 000 ₽ и расходах 300 ₽ пользователь меняет начальный остаток на 1 200 ₽ +- **THEN** баланс становится 900 ₽, история операций не пополняется корректировкой, а месячные расходы остаются 300 ₽ + +#### Scenario: Новое начало противоречит истории + +- **WHEN** пользователь переносит дату начала учёта с 1 на 10 сентября, а по карте сохранён расход или перевод от 5 сентября +- **THEN** система отклоняет изменение и сохраняет прежние дату, баланс и всю историю + +### Requirement: Архив карт сохраняет историю + +Пользователь SHALL иметь возможность архивировать свою карту при любом +балансе, включая положительный, нулевой и отрицательный, и вернуть её +из архива. Архивация MUST NOT менять деньги или операции. Обычный список +SHALL показывать активные карты; переключатель архива SHALL открывать +архивные карты и их историю. + +Пока карта архивна, система MUST запрещать создание, редактирование +и удаление затрагивающих её операций, а также изменение начального остатка +и даты начала учёта. Для исправления или новой операции карту сначала +возвращают из архива. Перевод или возврат MUST требовать активного состояния +всех затронутых карт. Изменение описательных сведений карты само по себе +не является финансовой операцией. + +Архивная карта MUST продолжать участвовать в исторических сводках «Все карты». +Безвозвратное удаление карты SHALL NOT предоставляться в первой версии. + +#### Scenario: Архивирование с ненулевым остатком + +- **WHEN** пользователь архивирует карту с балансом −500 ₽ и сохранённой историей +- **THEN** карта скрывается из обычного списка, остаётся доступной через переключатель архива с теми же балансом и историей и сохраняет вклад в сводки всех карт + +#### Scenario: Финансовое действие по архивной карте + +- **WHEN** пользователь пытается внести, исправить или удалить операцию либо изменить начало учёта архивной карты +- **THEN** система отклоняет действие без изменения данных и объясняет необходимость вернуть карту из архива + +#### Scenario: Возврат или перевод затрагивает архив + +- **WHEN** возврат относится к расходу архивной карты либо хотя бы одна прежняя или новая сторона изменяемого перевода архивна +- **THEN** финансовое действие отклоняется до возвращения всех затронутых карт из архива + +#### Scenario: Возвращение карты + +- **WHEN** пользователь возвращает карту из архива +- **THEN** она снова видна в обычном списке с сохранёнными данными и допускает финансовые действия по общим правилам + +### Requirement: Месячный журнал и сводка расходов по категориям + +Система SHALL показывать историю и месячную сводку выбранной карты, а также +месячную сводку всех банковских карт пользователя. По умолчанию выбирается +текущий месяц Europe/Moscow; пользователь SHALL иметь возможность явно выбрать +другой месяц. История карты и её сводка MUST использовать один выбранный месяц. +Операции SHALL отображаться по убыванию календарной даты со стабильным порядком +внутри даты при повторном чтении неизменившихся данных и доступом к следующим +страницам истории. + +Сводка MUST показывать расходы, возвраты и итог после возвратов, а также +соответствующие суммы по категориям. Расход входит в месяц собственной даты, +возврат — в месяц даты возврата, даже если покупка была раньше. +Итог MUST равняться расходам минус возвраты и может быть отрицательным. +Начальные остатки, пополнения и переводы MUST NOT включаться в эти суммы. + +Сводка MUST учитывать все подходящие не удалённые записи, а не только +текущую страницу истории. «Все карты» MUST включать активные и архивные карты +владельца. Выбор месяца и охвата MUST NOT изменять текущие балансы. +При отсутствии расходов и возвратов SHALL показываться известные нули. + +#### Scenario: Месяц по умолчанию и переключение + +- **WHEN** пользователь открывает карту в сентябре по Europe/Moscow и затем выбирает август +- **THEN** сначала история и сводка относятся к сентябрю, после выбора обе относятся к августу, а текущий баланс остаётся прежним + +#### Scenario: Сводка расходов и переводов + +- **WHEN** в месяце сохранены расходы «Кафе» 500 ₽ и «Такси» 300 ₽, пополнение 2 000 ₽ и перевод 1 000 ₽ без возвратов +- **THEN** сводка показывает расходы 800 ₽, возвраты 0 ₽, итог 800 ₽ и категории 500 ₽ и 300 ₽ без включения пополнения или перевода + +#### Scenario: Возврат покупки прошлого месяца + +- **WHEN** расход «Покупки» 1 000 ₽ совершён в августе, возврат 600 ₽ — в сентябре и других операций этих видов нет +- **THEN** август показывает расходы 1 000 ₽, возвраты 0 ₽ и итог 1 000 ₽, а сентябрь — расходы 0 ₽, возвраты 600 ₽ и итог −600 ₽ в той же категории + +#### Scenario: Все карты включают архивные + +- **WHEN** расходы активной A за месяц составили 800 ₽, архивной B — 200 ₽, а между ними ранее был перевод 1 000 ₽ +- **THEN** общая сводка показывает расходы 1 000 ₽ и не добавляет перевод, независимо от переключателя архива в списке карт + +#### Scenario: Несколько страниц и равные даты + +- **WHEN** операции выбранного месяца занимают несколько страниц и часть операций имеет одинаковую дату +- **THEN** пользователь может просмотреть все страницы в стабильном порядке, а сводка и баланс учитывают все соответствующие записи независимо от открытой страницы + +### Requirement: Изоляция пользовательских финансовых данных + +Карты, категории, операции и связи между ними MUST принадлежать локальному +пользователю WealthLedger. Система MUST предоставлять чтение, создание, +изменение, удаление, архив и восстановление только владельцу с действующей +сессией. Идентификатор сам по себе MUST NOT подтверждать доступ. + +Проверка владельца MUST распространяться на каждую упомянутую карту, +категорию и операцию, включая обе стороны перевода, исходный расход возврата +и новые связи при редактировании. Отказ MUST NOT раскрывать чужие финансовые +сведения или менять данные любой стороны. Сводки MUST включать только +данные текущего пользователя. + +#### Scenario: Нет действующей сессии + +- **WHEN** пользователь без действующей сессии пытается прочитать карты, сводку или изменить финансовые данные +- **THEN** система отказывает в доступе без раскрытия и изменения сохранённых данных + +#### Scenario: Чужая карта или категория + +- **WHEN** пользователь обращается к чужой карте или пытается назначить своей операции чужую категорию +- **THEN** система отклоняет действие, не раскрывает чужие сведения и не меняет баланс, историю или категории + +#### Scenario: Чужая связь перевода или возврата + +- **WHEN** пользователь подставляет чужую карту в перевод или чужой расход в возврат при создании либо редактировании +- **THEN** система отклоняет всё действие без сохранения частичного результата и без раскрытия данных другого пользователя + +#### Scenario: Чужие исправления и архив + +- **WHEN** пользователь пытается изменить или удалить чужую операцию, переименовать чужую категорию либо архивировать или восстановить чужую карту +- **THEN** система отказывает и сохраняет все данные владельца без изменений + +### Requirement: Согласованные изменения и защита повторной отправки + +Успешное финансовое изменение MUST согласованно сохранять операции, связи, +денежное влияние и пересчитанные результаты для всех затронутых карт. +Система MUST NOT подтверждать успех несохранённого действия или оставлять +частичный результат. Независимые одновременные операции MUST сохранять +влияние каждой успешной записи, не затирая друг друга. + +Повторная отправка одного намерения создать карту или операцию с тем же +содержанием MUST приводить не более чем к одному соответствующему результату +и однократному учёту начального остатка или денежного влияния операции. +Иное содержание при повторном использовании того же намерения MUST приводить +к конфликту без изменения данных. После исправления или удаления результата +поздняя повторная отправка исходного создания MUST NOT восстанавливать +старые значения, удалённую запись или её денежное влияние. +Ответ на повтор MUST показывать актуальное состояние ранее созданной карты +или операции либо сообщать, что операция удалена, не выдавая старые данные +за текущий результат. Уже сохранённое намерение MUST NOT проверяться +как новая финансовая запись и повторно расходовать доступную сумму возврата. +Архивирование ранее созданной карты MUST NOT превращать повтор её создания +в создание другой карты или возвращение из архива. + +Редактирование или удаление существующей операции и изменение карты, +основанные на её устаревшем состоянии, MUST завершаться понятным конфликтом +без перезаписи более свежих изменений. Повторное удаление уже удалённой +операции не считается перезаписью и остаётся безрезультатным действием. +Проверки суммы возвратов, состояния архива, дат и других ограничений +MUST сохраняться при одновременных действиях. + +#### Scenario: Повтор после потерянного ответа + +- **WHEN** создание расхода 100 ₽ успешно сохранено, ответ потерян и пользователь повторяет то же намерение с тем же содержанием +- **THEN** история содержит один расход, баланс изменён один раз, а повтор не создаёт дубликат + +#### Scenario: Повтор создания карты после архивации + +- **WHEN** карта с начальным остатком 1 000 ₽ создана, ответ потерян, карта затем архивирована и приходит повтор её исходного создания +- **THEN** система показывает актуальную архивную карту, не создаёт вторую карту, не дублирует начальный остаток и не возвращает карту из архива + +#### Scenario: Повтор полного возврата + +- **WHEN** возврат 500 ₽ полностью покрывает расход 500 ₽, успешно сохранён, но ответ потерян и пользователь повторяет то же намерение создания возврата +- **THEN** система возвращает сохранённый возврат без новой записи, повторного изменения баланса или ошибки о превышении суммы покупки + +#### Scenario: Изменённое содержание того же намерения + +- **WHEN** после сохранения расхода 100 ₽ повтор того же намерения содержит сумму 200 ₽ +- **THEN** система сообщает о конфликте и сохраняет исходный результат без дополнительной операции + +#### Scenario: Позднее создание после исправления или удаления + +- **WHEN** сохранённый расход 100 ₽ исправлен на 150 ₽ или удалён, после чего приходит повтор его исходного создания +- **THEN** система показывает актуальный расход 150 ₽ либо сообщает об удалении; прежняя сумма или удалённая запись не восстанавливаются и дополнительного денежного влияния нет + +#### Scenario: Два независимых расхода + +- **WHEN** при балансе 1 000 ₽ расходы 100 ₽ и 200 ₽ успешно сохраняются одновременно как разные намерения +- **THEN** история содержит оба расхода, а баланс составляет 700 ₽ + +#### Scenario: Два возврата превышают покупку вместе + +- **WHEN** для расхода 1 000 ₽ одновременно сохраняются независимые возвраты 600 ₽ и 600 ₽ +- **THEN** система не допускает общей сохранённой суммы 1 200 ₽: конфликтующее действие отклоняется без частичного результата + +#### Scenario: Исправление по устаревшим данным + +- **WHEN** операция или карта изменилась после того, как пользователь начал её редактирование, а он отправляет изменение прежнего состояния +- **THEN** система сообщает о конфликте, сохраняет более свежие данные и позволяет получить актуальное состояние для нового исправления + +#### Scenario: Удаление по устаревшим данным + +- **WHEN** пользователь пытается удалить существующую операцию, которую другой успешный запрос уже изменил +- **THEN** система отклоняет устаревшее удаление без потери более свежего исправления + +#### Scenario: Архивирование одновременно с финансовым изменением + +- **WHEN** архивирование карты пересекается с созданием или исправлением затрагивающей её операции +- **THEN** результаты соответствуют последовательному выполнению допустимых действий и не допускают финансовую запись, выполненную после вступившего в силу архива + +### Requirement: Сохранённый ручной учёт переживает повторный вход и перезапуск + +Успешно сохранённые карты, начальные остатки, даты начала, операции, +категории, архивное состояние и связи возвратов и переводов MUST оставаться +доступными владельцу после обновления страницы, выхода, повторного входа +и обычного перезапуска приложения. Эти действия MUST NOT создавать +дубликаты, восстанавливать удалённые операции или сбрасывать балансы. +Защита повторной отправки сохранённых намерений MUST сохранять действие +после этих переходов. + +#### Scenario: Повторный вход + +- **WHEN** пользователь выходит и снова входит после сохранения карты с начальным остатком 1 000 ₽ и расходом 100 ₽ +- **THEN** он видит ту же карту, дату начала, расход с его текущей категорией и баланс 900 ₽ + +#### Scenario: Перезапуск с переводом, возвратом и архивом + +- **WHEN** приложение перезапускается после сохранения переводов, связанных возвратов, переименования категорий и архивирования карты +- **THEN** владелец получает прежние связи, названия, архивное состояние и рассчитанные суммы без дубликатов или потери истории + +#### Scenario: Повтор создания после перезапуска + +- **WHEN** после перезапуска повторяется намерение создания ранее сохранённой, а затем удалённой операции +- **THEN** удалённая операция и её денежное влияние не восстанавливаются + +### Requirement: Доступные формы и различимые состояния без ложного успеха + +Раздел SHALL различать отсутствие карт, отсутствие операций за месяц, +известный нулевой баланс, загрузку и ошибку получения данных. Ошибка +MUST NOT подменяться пустым списком или нулевой суммой. Пользователь SHALL +иметь возможность повторить неуспешную загрузку. + +Формы карт, операций, исправлений и категорий MUST иметь доступные названия +полей и действий, управление с клавиатуры и понятные ошибки, связанные +с соответствующим полем или действием. Обязательность, направление денег +и ошибки MUST NOT передаваться только цветом. + +Во время сохранения, ошибки и конфликта введённый черновик MUST оставаться +доступным до явного закрытия или успешного завершения. Пока результат +сохранения не подтверждён, система MUST NOT показывать введённую сумму +как подтверждённый новый баланс или выдавать действие за успешное. +При неопределённом результате после потери ответа повтор SHALL использовать +защиту того же намерения создания. После подтверждённого изменения +отображаемые история, балансы и сводки MUST согласовываться с сохранёнными +данными всех затронутых карт. + +#### Scenario: Пустой список + +- **WHEN** раздел успешно загружен и у пользователя ещё нет карт +- **THEN** система показывает штатное пустое состояние с доступным действием добавления карты + +#### Scenario: Нет операций при ненулевом балансе + +- **WHEN** в выбранном месяце нет операций, но текущий баланс карты составляет 10 000 ₽ +- **THEN** история сообщает об отсутствии операций за месяц, сводка содержит нули, а текущий баланс остаётся 10 000 ₽ + +#### Scenario: Ошибка загрузки + +- **WHEN** загрузка карт, истории или сводки завершается ошибкой +- **THEN** система показывает ошибку и действие повтора, не выдавая её за отсутствие данных или нулевой результат + +#### Scenario: Доступный ввод и проверка + +- **WHEN** пользователь управляет формой с клавиатуры или средством чтения экрана и вводит некорректную сумму +- **THEN** названия полей и действий доступны, ошибка суммы определима без цвета, а введённые значения остаются для исправления + +#### Scenario: Ожидание и неопределённый результат + +- **WHEN** пользователь отправляет перевод и ответ ещё не получен либо потерян +- **THEN** черновик сохраняется, неподтверждённые балансы не выдаются за сохранённые, а повтор того же создания не порождает второй перевод + +#### Scenario: Ошибка или конфликт исправления + +- **WHEN** сохранение исправления отклоняется из-за ошибки или устаревшего состояния +- **THEN** пользователь видит причину, сохраняет черновик и может получить актуальные данные для следующего действия без перезаписи чужого успешного изменения diff --git a/openspec/specs/multi-repository-delivery/spec.md b/openspec/specs/multi-repository-delivery/spec.md index caeb6aa..ac2766a 100644 --- a/openspec/specs/multi-repository-delivery/spec.md +++ b/openspec/specs/multi-repository-delivery/spec.md @@ -8,21 +8,21 @@ ### Requirement: Каждый change содержит полный Delivery Map -Каждый versioned change MUST оценивать delivery areas `frontend`, `backend`, `specs` и `docs` и для каждой области указывать `affected` либо `N/A` с конкретной причиной. `specs` SHALL быть affected для каждого versioned SDD change. `docs` SHALL быть affected для каждой поставленной пользовательской фичи; для технического или процессного change она MAY быть `N/A` только когда фактическая устойчивая документация не изменяется. `frontend` и `backend` SHALL быть affected только когда меняется их поведение, контракт, реализация или обязательный локальный gate. Пустой коммит MUST NOT использоваться для имитации участия незатронутого репозитория. +Каждый legacy `wealth-ledger-sdd` change MUST оценивать delivery areas `frontend`, `backend`, `specs` и `docs` в Design Delivery Map и для каждой области указывать `affected` либо `N/A` с конкретной причиной. Для `wealth-ledger-sdd-v2` Design оценивает implementation impact `main`, `frontend`, `backend` (и `qa`, только если существует repo), а Docs Impact отдельно оценивает `docs`; `specs` владеет SDD artifacts и native sync/archive без фиктивной implementation task. Для обоих workflows `docs` SHALL быть affected для каждой поставленной пользовательской фичи; для технического или процессного change она MAY быть `N/A` только когда фактическая устойчивая документация не изменяется. `frontend` и `backend` SHALL быть affected только когда меняется их поведение, контракт, реализация или обязательный локальный gate. Пустой коммит MUST NOT использоваться для имитации участия незатронутого репозитория. #### Scenario: Фича затрагивает клиент и API - **WHEN** change меняет пользовательский поток и соответствующее API-поведение -- **THEN** Delivery Map отмечает frontend, backend, specs и docs как affected и описывает ожидаемый результат каждой области без реализации по файлам +- **THEN** legacy Delivery Map отмечает frontend, backend, specs и docs как affected; v2 Design отмечает frontend/backend, а Docs Impact — docs и отдельный specs artifact не превращается в пустую task #### Scenario: Область не затронута - **WHEN** поведение, контракт, реализация, guardrails и фактическая документация области не меняются -- **THEN** Delivery Map содержит проверяемую причину `N/A`, а delivery не требует пустого коммита этого репозитория +- **THEN** соответствующий impact artifact содержит проверяемую причину `N/A`, а delivery не требует пустого коммита этого репозитория ### Requirement: Частичная multi-repository delivery не считается завершённой -Change MUST оставаться active, пока каждая affected область не реализована и не прошла свои обязательные локальные gates. Успех одной области MUST NOT позволять архивировать change, если другая affected область отсутствует, не проверена либо не соответствует согласованному поведению. После archive change MUST NOT получить terminal status `done`, пока фактические результаты всех четырёх delivery areas не представлены в Completion Evidence. +Change MUST оставаться active, пока каждая affected область не реализована и не прошла свои обязательные локальные gates. Успех одной области MUST NOT позволять архивировать change, если другая affected область отсутствует, не проверена либо не соответствует согласованному поведению. После archive change MUST NOT получить terminal status `done`, пока фактические результаты всех применимых delivery areas выбранной schema не представлены в Completion Evidence. #### Scenario: Backend готов, а frontend ещё не реализован @@ -36,12 +36,12 @@ Change MUST оставаться active, пока каждая affected обла ### Requirement: Completion Evidence содержит только наблюдённые результаты -Completion Evidence MUST создаваться после выполнения работы и MUST связывать canonical change с commit identifier каждого изменённого Git-репозитория, точными выполненными non-mutating verification commands и их наблюдёнными результатами. Для каждой области с `N/A` evidence SHALL сохранять согласованную причину отсутствия изменения. Planned commands, ожидаемые результаты, незавершённые checklist items и предполагаемые commit identifiers MUST NOT представляться как completion evidence. +Completion Evidence MUST создаваться после выполнения работы и MUST связывать canonical change с commit identifier каждого изменённого Git-репозитория, точными выполненными non-mutating verification commands и их наблюдёнными результатами. Для каждой оценённой области с `N/A` evidence SHALL сохранять согласованную причину отсутствия изменения. Planned commands, ожидаемые результаты, незавершённые checklist items и предполагаемые commit identifiers MUST NOT представляться как completion evidence. #### Scenario: Change готовится к terminal status - **WHEN** implementer заявляет, что multi-repository change завершён -- **THEN** reviewer может сопоставить Delivery Map с фактическими commits, результатами gates и причинами `N/A` для всех четырёх областей +- **THEN** reviewer может сопоставить impact artifacts выбранной schema с фактическими commits, результатами gates и причинами `N/A` для всех оценённых областей #### Scenario: Команда ещё не была выполнена @@ -64,7 +64,7 @@ Workspace SHALL предоставлять один non-mutating локальн ### Requirement: Каждая задача объявляет единственную delivery area -Каждый checkbox Tasks MUST объявлять ровно одного owner из `frontend`, `backend`, `specs`, `docs` или `workspace`. Implementation work, изменяющая несколько Git-репозиториев, MUST быть разделена на repository-owned tasks; `workspace` SHALL использоваться только для cross-area orchestration или verification без владения repository implementation. Для каждой `affected` area Tasks MUST содержать её implementation work и canonical local gate, а task ownership MUST соответствовать утверждённому Delivery Map. +Каждый checkbox legacy Tasks MUST объявлять ровно одного owner из `frontend`, `backend`, `specs`, `docs` или `workspace`; `workspace` SHALL использоваться только для cross-area orchestration или verification без владения repository implementation. Каждый checkbox v2 Tasks MUST объявлять ровно одного repository owner из `main`, `frontend`, `backend`, `docs`, `specs` или существующего `qa`; `main` владеет только workspace/Compose/release/configuration, `specs` — genuine repository maintenance, `docs` — approved Docs Impact, `qa` — только реальная automation delivery, но не manual acceptance checkbox. Implementation work, изменяющая несколько Git-репозиториев, MUST быть разделена на repository-owned tasks. Для каждой `affected` implementation area Tasks MUST содержать её implementation work и canonical local gate, а task ownership MUST соответствовать утверждённым impact artifacts выбранной schema. #### Scenario: Change затрагивает frontend и backend @@ -74,16 +74,16 @@ Workspace SHALL предоставлять один non-mutating локальн #### Scenario: Выполняется общий verification gate - **WHEN** задача только оркестрирует проверку нескольких affected areas и не изменяет их implementation -- **THEN** task использует owner `workspace` и не подменяет repository-owned implementation или canonical gates +- **THEN** legacy task MAY использовать `workspace`; v2 использует `main` только при реальном workspace-owned outcome, не маскируя независимую Validation под implementation checkbox #### Scenario: Task ссылается на N/A area - **WHEN** checkbox объявляет owner области, которая имеет статус `N/A` в Delivery Map -- **THEN** planning validation завершается ошибкой и указывает несогласованную delivery area +- **THEN** planning validation завершается ошибкой и указывает несогласованную delivery area выбранной schema ### Requirement: Tasks трассируются к утверждённым требованиям -Каждый checkbox Tasks MUST содержать coverage reference: одну или несколько пар из точного capability path и точного Requirement heading либо `N/A` с конкретной причиной для работы, которая не реализует spec-level behavior. Причина `N/A` MUST объяснять отсутствие самостоятельного observable behavior, техническую необходимость задачи и её связь с утверждённым Design или delivery; однословная категория или неразрешённый placeholder не являются достаточной причиной. Каждый Requirement из Delta Specs MUST быть покрыт хотя бы одной task. Planning validation MUST отклонять отсутствующее покрытие, ссылку на несуществующий capability или Requirement, пустую причину `N/A` и неразрешённый placeholder. Содержательная достаточность непустой причины SHALL оцениваться semantic approval и independent review и MUST NOT выдаваться за детерминированный hard guardrail. Traceability SHALL использовать нативную идентичность requirement и MUST NOT вводить параллельный namespace собственных requirement IDs. +Каждый checkbox Tasks MUST содержать coverage reference: одну или несколько пар из точного capability path и точного Requirement heading либо `N/A` с конкретной причиной для работы, которая не реализует spec-level behavior. Причина `N/A` MUST объяснять отсутствие самостоятельного observable behavior, техническую необходимость задачи и её связь с утверждённым Design или delivery; для v2 также допустима связь с System Analysis или Docs Impact. Однословная категория или неразрешённый placeholder не являются достаточной причиной. Каждый Requirement из Delta Specs MUST быть покрыт хотя бы одной task; для v2 допустимо явное объяснение отсутствия implementation work. Planning validation MUST отклонять отсутствующее покрытие, ссылку на несуществующий capability или Requirement, пустую причину `N/A` и неразрешённый placeholder. Содержательная достаточность непустой причины SHALL оцениваться semantic approval и independent review и MUST NOT выдаваться за детерминированный hard guardrail. Traceability SHALL использовать нативную идентичность requirement и MUST NOT вводить параллельный namespace собственных requirement IDs. #### Scenario: Delta requirement покрыт реализацией diff --git a/openspec/specs/online-instrument-information/spec.md b/openspec/specs/online-instrument-information/spec.md new file mode 100644 index 0000000..ab24573 --- /dev/null +++ b/openspec/specs/online-instrument-information/spec.md @@ -0,0 +1,17 @@ +# online-instrument-information Specification + +## Purpose +Определяет online-only получение справочных сведений и результатов поиска инструментов отдельно от сохранённых личных финансовых данных. + +## Requirements + +### Requirement: Справочные сведения и поиск инструментов получают онлайн +Система SHALL получать подробные справочные сведения об Instrument и результаты поиска онлайн при их запросе. Такие сведения MUST NOT становиться самостоятельной долговременной копией справочника. Недоступность либо отсутствие онлайн-сведений MUST NOT задерживать сохранённый финансовый ответ, изменять его свежесть или подтверждать отсутствие Instrument. + +#### Scenario: Медленный online lookup +- **WHEN** подробные сведения об Instrument ещё не получены онлайн при чтении сохранённого Portfolio +- **THEN** система возвращает доступный сохранённый Portfolio без ожидания lookup + +#### Scenario: Online lookup не найден +- **WHEN** online lookup не возвращает Instrument +- **THEN** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных diff --git a/openspec/specs/openspec-authority-and-lifecycle/spec.md b/openspec/specs/openspec-authority-and-lifecycle/spec.md index ef5b3ae..c2ae08e 100644 --- a/openspec/specs/openspec-authority-and-lifecycle/spec.md +++ b/openspec/specs/openspec-authority-and-lifecycle/spec.md @@ -7,7 +7,7 @@ ### Requirement: SDD-артефакты имеют непересекающиеся полномочия -Proposal SHALL выражать причину, намерение, границы и ожидаемый эффект change языком владельца и MUST NOT содержать решения по технологиям, структуре кода, файлам, API или зависимостям. Delta Specs SHALL определять только добавленное, изменённое или удалённое наблюдаемое поведение и инварианты системы без описания изменений кода. Design SHALL объяснять архитектурный способ выполнения утверждённых Specs, существенные решения, риски и альтернативы крупными частями и MUST NOT становиться построчной инструкцией реализации. Применимые UI/UX-решения и визуальные материалы SHALL входить в единый canonical Design и MUST NOT образовывать отдельный planning authority или отдельный UI/UX approval. Tasks SHALL быть implementation checklist, достаточным для выполнения Design и Specs, и MUST NOT содержать ещё не наблюдённые completion results, commit identifiers или post-archive bookkeeping. Completion Evidence SHALL фиксироваться только после фактической реализации и верификации и MUST NOT подменять planning artifact. Концептуальные материалы и Discovery SHALL оставаться входным контекстом и MUST NOT считаться утверждёнными OpenSpec artifacts. +Proposal SHALL выражать причину, намерение, границы и ожидаемый эффект change языком владельца и MUST NOT содержать решения по технологиям, структуре кода, файлам, API или зависимостям. Delta Specs SHALL определять только добавленное, изменённое или удалённое наблюдаемое поведение и инварианты системы без описания изменений кода. В `wealth-ledger-sdd-v2` System Analysis SHALL определять межсистемные потоки, contracts и semantics до Design; Design SHALL объяснять внутреннюю реализацию approved behavior/contracts, риски и альтернативы, не становясь построчной инструкцией. QA Analysis SHALL определять независимую system verification, не предписывая разработчикам internal tests. Docs Impact SHALL планировать только durable AS-IS knowledge для `docs/`. Применимые UI/UX-решения и визуальные материалы SHALL входить в единый canonical Design и MUST NOT образовывать отдельный planning authority или отдельный UI/UX approval. Tasks SHALL быть implementation checklist из утверждённых upstream artifacts и MUST NOT содержать ещё не наблюдённые completion results, commit identifiers или post-archive bookkeeping. Completion Evidence SHALL фиксироваться только после фактической реализации и верификации и MUST NOT подменять planning artifact. Концептуальные материалы и Discovery SHALL оставаться входным контекстом и MUST NOT считаться утверждёнными OpenSpec artifacts. #### Scenario: Product Owner формулирует Proposal @@ -16,7 +16,7 @@ Proposal SHALL выражать причину, намерение, границ #### Scenario: Агент подготавливает Tasks -- **WHEN** Proposal, Delta Specs и Design уже утверждены +- **WHEN** upstream artifacts выбранной schema утверждены - **THEN** Tasks перечисляет конкретные действия реализации и проверки, но не заявляет будущие результаты уже выполненными #### Scenario: Владелец согласует визуальный результат @@ -33,26 +33,31 @@ Proposal SHALL выражать причину, намерение, границ ### Requirement: Artifact graph применяет последовательные approval gates -Versioned SDD change SHALL проходить последовательность `Proposal → Delta Specs → Design → Tasks → Apply`. AI-агент MAY подготовить черновик текущего разрешённого артефакта, но MUST NOT считать его утверждённым самостоятельно или создавать downstream artifact до явного решения Change Owner. Для change, определяющего продуктовое поведение, Change Owner SHALL быть Product Owner; для технического или процессного change — Engineering Owner. Каждый из четырёх planning artifacts MUST существовать и быть утверждён до Apply. +Каждый change SHALL следовать schema из своего `.openspec.yaml`: существующие active `wealth-ledger-sdd` сохраняют `Proposal → Delta Specs → Design → Tasks → Apply`, а новые `wealth-ledger-sdd-v2` используют `Proposal → Delta Specs → System Analysis → (Design + QA Analysis) → Docs Impact → Tasks → Apply`. В v2 Design и QA Analysis MAY подготавливаться параллельно после approval System Analysis; Docs Impact требует approvals обоих. `skip_specs: true` явно обозначает отсутствие behavior delta, а не фиктивный spec. AI-агент MAY подготовить черновик текущего разрешённого артефакта, но MUST NOT считать его утверждённым самостоятельно или создавать downstream artifact до явного решения Change Owner. Для change, определяющего продуктовое поведение, Change Owner SHALL быть Product Owner; для технического или процессного change — Engineering Owner. Каждый применимый planning artifact выбранной schema MUST существовать и быть явно утверждён до Apply; существующие approvals legacy changes MUST NOT инвалидироваться только из-за смены default schema. #### Scenario: Proposal ещё не утверждён - **WHEN** AI-агент завершил черновик Proposal, но Change Owner не дал явного согласия -- **THEN** Delta Specs, Design и Tasks остаются недоступны для создания +- **THEN** downstream artifacts выбранной schema остаются недоступны для создания #### Scenario: Delta Specs утверждены - **WHEN** Change Owner явно подтверждает Delta Specs после утверждённого Proposal -- **THEN** Design становится следующим разрешённым planning artifact, а Tasks и Apply остаются заблокированы +- **THEN** для legacy разрешён Design, для v2 — System Analysis; Tasks и Apply остаются заблокированы + +#### Scenario: Active change переживает смену default schema + +- **WHEN** active change закреплён за `wealth-ledger-sdd` и default меняется на `wealth-ledger-sdd-v2` +- **THEN** он сохраняет прежний artifact graph и approvals без создания фиктивных v2 artifacts ### Requirement: Изменение upstream artifact требует каскадной проверки -Изменение утверждённого Proposal MUST инициировать повторную проверку Delta Specs, Design и Tasks; изменение утверждённых Delta Specs MUST инициировать повторную проверку Design и Tasks; изменение утверждённого Design MUST инициировать повторную проверку Tasks. Изменение Tasks MUST NOT неявно менять scope, наблюдаемое поведение или архитектурные решения upstream artifacts; при таком обнаружении workflow SHALL вернуть работу к самому раннему затронутому upstream artifact и его approval gate. Если несоответствие обнаружено во время Design или Apply, downstream-работа MUST остановиться, а Change Owner MUST получить наблюдаемое основание возврата и решения, требуемые на выбранном уровне. Механическое исправление синтаксиса или формата, которое не меняет утверждённые намерение, поведение, архитектурные решения или состав задач, MUST пройти повторную structural validation и MUST NOT само по себе инвалидировать approvals. Исправление Markdown heading, опечатки без изменения нормативного смысла или выравнивания таблицы MAY считаться механическим; изменение формулировки Requirement, добавление или удаление Scenario, изменение выбранного архитектурного решения либо planned Verification задачи MUST NOT считаться механическим. +Семантическое изменение утверждённого artifact MUST возвращаться к самой ранней затронутой planning boundary и инициировать повторную проверку и approval только её зависимых downstream artifacts согласно выбранной schema. Для legacy Proposal → Delta Specs → Design → Tasks; для v2 intent/scope → Proposal, observable behavior → Specs, systems/sequences/contracts/mappings/inter-system errors/semantic observability → System Analysis, internal implementation/storage/technical observability/developer tests → Design, system verification/test data/automation → QA Analysis, durable AS-IS/ADR → Docs Impact, decomposition/ownership/dependencies → Tasks. Изменение Tasks MUST NOT неявно менять upstream semantics. Если несоответствие обнаружено во время Design или Apply, downstream-работа MUST остановиться, а Change Owner MUST получить наблюдаемое основание возврата. Механическое исправление синтаксиса или формата без изменения смысла MUST пройти повторную structural validation и MUST NOT само по себе инвалидировать approvals. Исправление Markdown heading, опечатки без изменения нормативного смысла или выравнивания таблицы MAY считаться механическим; изменение Requirement/Scenario, contract, sequence, архитектурного решения, QA expected result, docs target либо planned Verification задачи MUST NOT считаться механическим. #### Scenario: После Design изменилось намерение - **WHEN** владелец меняет scope или ожидаемый эффект Proposal -- **THEN** зависимые Delta Specs, Design и Tasks теряют прежнее подтверждение до повторной согласованной проверки +- **THEN** зависимые artifacts выбранной schema теряют прежнее подтверждение до повторной согласованной проверки #### Scenario: Task вводит новое поведение @@ -62,7 +67,7 @@ Versioned SDD change SHALL проходить последовательност #### Scenario: Apply выявляет архитектурное расхождение - **WHEN** во время реализации обнаружено, что утверждённый Design не позволяет выполнить Specs без другого архитектурного решения -- **THEN** Apply останавливается, Tasks теряет прежнее подтверждение, а change возвращается к Design с наблюдаемым основанием и вариантами решения +- **THEN** Apply останавливается, затронутые downstream approvals теряют силу, а change возвращается к Design с наблюдаемым основанием и вариантами решения #### Scenario: Исправляется только структурный формат @@ -107,33 +112,28 @@ Canonical store и policy расположения artifacts SHALL иметь do - **WHEN** project-owned OpenSpec entrypoint допускает выбор nearest-root или global-default - **THEN** verifier завершается ошибкой и указывает non-compliant entrypoint -### Requirement: OpenSpec tooling имеет проверяемый version contract +### Requirement: OpenSpec tooling проверяется по совместимости -SDD workflow MUST объявлять одну текущую поддерживаемую версию OpenSpec CLI в canonical version contract и проверять фактическую версию до команд, читающих или изменяющих canonical change. Normative checks и проверяемые generated markers MUST получать ожидаемую версию из этого контракта и MUST NOT независимо закреплять конкретный version literal. Несовместимая либо неопределённая версия MUST давать actionable blocking result и MUST NOT приводить к молчаливому изменению artifact graph, templates или project-owned entrypoints. Публикация новой stable версии MUST NOT автоматически менять поддерживаемую версию проекта. Upgrade SHALL изменять version contract только после изолированной проверки совместимости schema, agent workflow, canonical store и hard blockers и SHALL сохранять историческое evidence с фактически использованными ранее версиями. +SDD workflow MUST проверять доступный OpenSpec CLI на совместимость с выбранной schema и canonical store без закрепления одной версии CLI. Перед изменением workflow или обновлением CLI SHALL выполняться native validation обеих project-local schemas, затронутых changes и canonical specs; artifact graph, agent workflow, store selection и blocking gates MUST сохранять согласованную семантику. Несовместимый CLI MUST давать actionable blocking result и MUST NOT приводить к молчаливому изменению artifacts или project-owned entrypoints. Фактически использованная версия CLI SHALL сохраняться в observed verification evidence, а не в отдельном version contract. -#### Scenario: Версия CLI соответствует контракту +#### Scenario: Доступный CLI совместим -- **WHEN** агент начинает работу с canonical change поддерживаемой версией OpenSpec -- **THEN** workflow продолжает работу с объявленной схемой и canonical store +- **WHEN** проверки выбранной schema и canonical store проходят на установленном OpenSpec CLI +- **THEN** workflow продолжает работу без требования совпадения с закреплённым номером версии -#### Scenario: Версия CLI не соответствует контракту +#### Scenario: CLI нарушает совместимость -- **WHEN** доступная версия OpenSpec не совпадает с объявленным совместимым контрактом -- **THEN** локальная preflight-проверка блокирует SDD-операцию и сообщает ожидаемую и фактическую версии +- **WHEN** после обновления CLI schema validation, разрешение store или blocking gates перестают работать +- **THEN** зависимая SDD-операция останавливается с диагностикой нарушения до исправления несовместимости -#### Scenario: Опубликована новая stable версия +#### Scenario: CLI обновлён -- **WHEN** upstream публикует новую stable версию, которая ещё не прошла project compatibility assessment -- **THEN** текущий version contract остаётся действующим и новая версия не становится неявным требованием для разработки - -#### Scenario: Подготовлен upgrade candidate - -- **WHEN** Engineering Owner рассматривает смену поддерживаемой версии -- **THEN** изолированная проверка подтверждает schema, agent workflow, store selection и hard blockers до изменения version contract +- **WHEN** Engineering Owner обновляет OpenSpec CLI +- **THEN** проверяются обе project-local schemas, затронутые changes, canonical specs и workflow, а фактическая версия и результаты сохраняются в verification evidence ### Requirement: Structural и semantic verification являются разными gates -OpenSpec structural validation SHALL проверять schema, artifact graph и формальную корректность specs, но MUST NOT считаться доказательством соответствия реализации. До terminal status change MUST пройти semantic verification реализации против Proposal, Delta Specs, Design и Tasks, а затем независимый read-only review. Если основной agent entrypoint semantic verification недоступен, workflow SHALL использовать versioned эквивалентную процедуру с теми же входными артефактами, категориями проверки и наблюдаемым evidence. +OpenSpec structural validation SHALL проверять schema, artifact graph и формальную корректность specs, но MUST NOT считаться доказательством соответствия реализации или approval. До terminal status change MUST пройти semantic verification реализации против применимых approved artifacts выбранной schema, включая v2 System Analysis, QA Analysis и Docs Impact, а затем независимый read-only review. Если основной agent entrypoint semantic verification недоступен, workflow SHALL использовать versioned эквивалентную процедуру с теми же входными артефактами, категориями проверки и наблюдаемым evidence. #### Scenario: Structural validation прошла без ошибок @@ -146,7 +146,7 @@ OpenSpec structural validation SHALL проверять schema, artifact graph - **THEN** агент выполняет документированную эквивалентную read-only проверку и сохраняет её результат вместо пропуска gate ### Requirement: Archive блокируется незавершёнными tasks -Project-owned archive workflow MUST отклонять archive, если отсутствует требуемый planning artifact, любой обязательный artifact не утверждён, `tasks.md` отсутствует или содержит unchecked task, semantic verification либо независимый read-only review не завершены успешно, или любая affected delivery area не реализована и не прошла обязательные pre-archive gates. Workflow MUST NOT предлагать confirmation override для этих blocking conditions. Post-archive bookkeeping MUST NOT появляться как unchecked task; он SHALL фиксироваться через детерминированное Completion Evidence после успешного archive, после чего связанная delivery может получить terminal status. +Project-owned archive workflow MUST отклонять archive, если отсутствует требуемый выбранной schema planning artifact, любой обязательный artifact не утверждён, `tasks.md` отсутствует или содержит unchecked task, semantic verification либо независимый read-only review не завершены успешно, или любая affected delivery area не реализована и не прошла обязательные pre-archive gates. Для v2 также MUST быть подтверждены developer checks, независимая QA/system verification, semantic compatibility backend-generated OpenAPI/runtime interface с approved target contract при изменении API и применение Docs Impact так, чтобы canonical `docs/` отражали resulting AS-IS. Native OpenSpec sync/archive SHALL обновлять canonical behavior specs, не переписывая их implementation details; System Analysis и target contracts остаются в архивном change, а не становятся второй вечной копией current OpenAPI. Workflow MUST NOT предлагать confirmation override для этих blocking conditions. Post-archive bookkeeping MUST NOT появляться как unchecked task; он SHALL фиксироваться через детерминированное Completion Evidence после успешного archive, после чего связанная delivery может получить terminal status. #### Scenario: Change имеет unchecked task - **WHEN** implementer запрашивает archive и остаётся хотя бы один unchecked task @@ -181,7 +181,7 @@ Project-owned archive workflow MUST отклонять archive, если отс #### Scenario: Downstream artifacts ещё отсутствуют -- **WHEN** Proposal создан, а Delta Specs, Design и Tasks ещё не существуют +- **WHEN** Proposal создан, а downstream artifacts выбранной schema ещё не существуют - **THEN** workflow сообщает результат доступных Proposal checks и не представляет отсутствующую full-change validation как успешную ### Requirement: Project schema сохраняет заметные самодостаточные правила diff --git a/openspec/specs/portfolio-value-history/spec.md b/openspec/specs/portfolio-value-history/spec.md new file mode 100644 index 0000000..b3eb22d --- /dev/null +++ b/openspec/specs/portfolio-value-history/spec.md @@ -0,0 +1,28 @@ +# portfolio-value-history Specification + +## Purpose +Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек. + +## Requirements + +### Requirement: Полные оценки Portfolio сохраняются как история +Система SHALL сохранять для каждого доступного Brokerage Account точку оценки по каждой исходной валюте только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение без валютной конвертации и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки. + +#### Scenario: Полная мультивалютная оценка +- **WHEN** успешный полный Portfolio содержит вычислимые итоги RUB и USD +- **THEN** система сохраняет две отдельные точки с исходными значениями и валютами + +#### Scenario: Неполная оценка +- **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость +- **THEN** система сохраняет новый Portfolio без точки истории оценки + +### Requirement: История оценки скрыта вне подтверждённого доступа +Система MUST сохранять историю оценки при замене token или удалении подключения, но MUST NOT раскрывать её без текущего подтверждённого доступа User к тому же Brokerage Account. Успешное новое подключение, подтверждающее доступ к счёту, MUST снова делать его сохранённую историю доступной для будущих поддерживаемых представлений. Система MUST NOT предоставлять отдельное публичное чтение точек истории этим требованием. + +#### Scenario: Удалённое подключение +- **WHEN** User удаляет T-Bank connection +- **THEN** сохранённые точки оценки этого User не доступны для чтения + +#### Scenario: Повторно подтверждённый счёт +- **WHEN** новое успешное подключение подтверждает доступ User к тому же Brokerage Account +- **THEN** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений diff --git a/openspec/specs/portfolio-view/spec.md b/openspec/specs/portfolio-view/spec.md index 92646ae..5939832 100644 --- a/openspec/specs/portfolio-view/spec.md +++ b/openspec/specs/portfolio-view/spec.md @@ -7,13 +7,13 @@ ## Requirements ### Requirement: Получение Portfolio выбранного Brokerage Account -Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`. +Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio. Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection. #### Scenario: Получение текущего Portfolio -- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а обязательные upstream-данные успешно получены -- **THEN** система отвечает `200` полным нормализованным Portfolio Snapshot с `isStale: false` и временем успешного получения финансового снимка +- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён +- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta` #### Scenario: Недоступный Brokerage Account - **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection @@ -94,144 +94,72 @@ Portfolio Snapshot является результатом наблюдения - **THEN** система передаёт её как строку `"-0.5"` ### Requirement: Свежесть и fallback полного Portfolio Snapshot -Система MUST хранить последний полный успешный Portfolio Snapshot в SQLite -с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный -GET MUST читать сохранённые данные без ожидания T-Bank; missing/stale источники -обновляются в фоне по пользовательскому запросу. Portfolio source свеж 60 секунд. -Ошибка обновления MUST сохранять прежний полный Snapshot и его время без -пятиминутного лимита fallback. Metadata MUST NOT определять успешность или -свежесть финансового Snapshot; при её отсутствии используется доступный fallback. +Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST возвращать `{ data, meta }`, где `data` является сохранённым Snapshot, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot. Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника. -Система MUST делать прежние личные datasets недоступными после успешной замены -или удаления подключения. Поздняя работа прежнего подключения MUST NOT -публиковаться в новом. Общие metadata и подтверждённое отсутствие инструмента -сохраняются по UID/FIGI на 24 часа; ошибка lookup не подтверждает отсутствие. +Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента. #### Scenario: Свежий Snapshot -- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 секунд после успешного получения +- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения - **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank #### Scenario: Медленный lookup инструмента -- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился -- **THEN** система не задерживает локальный ответ ради lookup и показывает доступную identity; сохранённая успешная metadata используется последующим чтением +- **WHEN** сведения об инструменте ещё запрашиваются онлайн +- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity #### Scenario: Fallback после временной upstream-ошибки -- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot +- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot - **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt` #### Scenario: Недействительный token не маскируется fallback-данными -- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio +- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при часовом обходе Portfolio - **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` +#### Scenario: Неполная оценка при новом составе +- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены +- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля + ### Requirement: Состояния и ошибки Portfolio на клиенте +Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд без automatic retry или refetch при возврате фокуса. Открытый экран MUST перечитывать сохранённый результат backend каждые 30 минут без запуска синхронизации T-Bank. Клиент MUST показывать freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность из `meta` текстом или доступно именованным элементом, а не только цветом. Клиент MUST NOT предоставлять действие ручной синхронизации Portfolio. При отсутствии ранее успешного ответа он MUST показывать структурный skeleton с доступным обозначением занятости без вымышленных сумм; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус. -Клиент SHALL загружать Portfolio при открытии защищённого маршрута -`/accounts/:accountId`. Он MUST использовать успешный ответ в течение -30 секунд, без automatic retry или refetch при возврате фокуса. -Polling допускается только для известной работы открытого экрана; -data GET наблюдения использует `sync=observe` и MUST NOT запускать новый импорт. -Status сообщает state/errorCode без hash notifications и численных progress counters. -После ухода или завершения работы polling MUST прекращаться. +Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями. -Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться. -Новый диапазон свечей загружается при его открытии; initial loading допустим. -При загрузке без ранее полученного успешного ответа для этого же 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-состояние только при пустых `positions` -и `cashBalances`, без нулевых валютных карточек. Для показываемого состава -Portfolio клиент MUST сохранять время успешного получения снимка -`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST -показывать Snapshot, его исходное время и нейтральное предупреждение -о временной недоступности T-Bank. Это предупреждение само по себе MUST NOT -добавлять действие ручного обновления. Server-side stale и ошибка фонового -обновления Client Cached Snapshot MUST оставаться раздельными состояниями: -ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения -и действие повтора клиентского обновления MUST оставаться доступными при -одновременном наличии этих состояний. - -При отсутствии ранее успешно полученного Client Cached Snapshot этого же -Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта -с действием возврата к списку счетов без действия повтора. При отсутствии -Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` -или иной ошибке клиент MUST показать безопасное error-состояние с доступными -для ошибки действиями; действие повтора разрешено только для временной -и неизвестной ошибки. +При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки. #### Scenario: Показ устаревшего Portfolio - - **WHEN** клиент получает успешный ответ с `isStale: true` -- **THEN** клиент показывает позиции, остатки, валютные итоги, время - последнего успешного снимка и нейтральное предупреждение; само это - предупреждение не добавляет действие ручного обновления - -#### Scenario: Недоступный счёт на клиенте - -- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный - Client Cached Snapshot и клиент получает `404` -- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку - Brokerage Account без действия повтора +- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации #### Scenario: Первичная загрузка Portfolio +- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа +- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders -- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее - полученного успешного ответа -- **THEN** он показывает структурный skeleton, включая место сводки, - с доступным обозначением загрузки, без вымышленных или нулевых сумм - и без фокусируемых placeholders +#### Scenario: Неуспешная последняя попытка сохраняет Portfolio +- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки +- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки + +#### Scenario: Недоступный счёт на клиенте +- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404` +- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора #### Scenario: Обновление после истечения срока свежести - -- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое - обновление данных того же Brokerage Account -- **THEN** клиент сохраняет прежние валютные итоги и время их успешного - получения, показывает неблокирующую индикацию обновления и не заменяет - доступный состав и сводку skeleton-состоянием +- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание +- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием #### Scenario: Ошибка обновления после успешного ответа - -- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, - а обновление этих же данных завершается ошибкой -- **THEN** клиент сохраняет данные, суммы и время прежнего снимка, - показывает неблокирующее предупреждение и действие повтора; - ошибка не делает сохранённый снимок свежим и не изменяет `isStale` +- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой +- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения #### Scenario: Ошибка клиентского обновления при server-side stale - -- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое - обновление этого же Portfolio завершается ошибкой -- **THEN** прежние суммы и время остаются видимыми одновременно - с нейтральным server-side предупреждением, отдельным предупреждением - об ошибке клиентского обновления и его действием повтора +- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой +- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению #### Scenario: Повторное обновление успешно - -- **WHEN** после ошибки фонового обновления User запускает повторную - попытку и получает успешный Portfolio Snapshot -- **THEN** до ответа прежние суммы и время остаются видимыми, затем - клиент показывает значения и `lastSuccessfulAt` успешного ответа, - убирает предупреждение об ошибке клиентского обновления и показывает - server-side предупреждение только в соответствии с новым `isStale` +- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot +- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale` #### Scenario: Штатный пустой Portfolio на клиенте - -- **WHEN** успешно полученный Portfolio содержит пустые `positions` - и `cashBalances` -- **THEN** клиент показывает существующее empty-состояние без валютных - карточек с вымышленными нулевыми итогами +- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances` +- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами ### Requirement: Представление карточек позиций на клиенте Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции. diff --git a/openspec/specs/tbank-connection/spec.md b/openspec/specs/tbank-connection/spec.md index eaba473..567d8a0 100644 --- a/openspec/specs/tbank-connection/spec.md +++ b/openspec/specs/tbank-connection/spec.md @@ -18,15 +18,15 @@ - **THEN** система отвечает `200` с `{ connected: true, connectedAt: }` без token ### Requirement: Создание и замена подключения T-Bank -Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. +Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первый часовой обход без команды frontend. История стоимости прежних счетов MUST сохраняться, но MUST быть доступна только для счетов, доступ к которым подтверждён новым подключением. #### Scenario: Успешное создание подключения без счетов - **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов -- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: }` +- **THEN** система сохраняет подключение, отвечает `200` с `{ connected: true, connectedAt: }` и не создаёт фиктивные счета #### Scenario: Успешная замена подключения - **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет -- **THEN** система заменяет token подключения, обновляет `connectedAt` и отвечает текущим статусом без token +- **THEN** система заменяет token, обновляет `connectedAt`, отвечает текущим статусом без token и начинает первый часовой обход нового подключения #### Scenario: Невалидный token не заменяет существующее подключение - **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав @@ -37,15 +37,15 @@ - **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение ### Requirement: Удаление подключения T-Bank -Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные данные T-Bank. +Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и сделать относящиеся к User текущие личные данные недоступными. История стоимости счёта MUST сохраняться, но MUST NOT раскрываться до подтверждения нового доступа к тому же счёту. #### Scenario: Удаление существующего подключения - **WHEN** User удаляет существующее подключение -- **THEN** система удаляет token и связанные кэшированные данные User, затем отвечает `204` +- **THEN** система удаляет token, скрывает текущие личные данные и историю стоимости и отвечает `204` #### Scenario: Повторное удаление подключения - **WHEN** User удаляет уже отсутствующее подключение -- **THEN** система отвечает `204` +- **THEN** система отвечает `204`, не раскрывая сохранённую историю ### Requirement: Конфиденциальность token T-Bank Система MUST хранить не более одного token T-Bank для каждого User только в зашифрованном виде. Token MUST расшифровываться только для серверного вызова T-Bank и MUST NOT передаваться в frontend, HTTP-ответах, OpenAPI examples, сообщениях ошибок или application logs. diff --git a/openspec/specs/tbank-data-sync/spec.md b/openspec/specs/tbank-data-sync/spec.md new file mode 100644 index 0000000..fe365bc --- /dev/null +++ b/openspec/specs/tbank-data-sync/spec.md @@ -0,0 +1,37 @@ +# tbank-data-sync Specification + +## Purpose +Определяет единственного владельца обновления личных данных T-Bank и узкие +исключения первой загрузки данных, которых ещё нет в локальном результате. + +## Requirements + +### Requirement: Часовой обход владеет обновлением личных данных T-Bank +Система MUST обновлять личные инвестиционные данные T-Bank только часовым обходом для текущего подключения User. Обычные resource reads, действия повтора и открытие экранов MUST NOT запускать импорт, ручную личную синхронизацию или обращение к T-Bank. Единственными исключениями являются первая загрузка отсутствующего интервала Account Ledger и впервые открытого диапазона свечей: они MAY начать получение только этого отсутствующего интервала либо диапазона, когда для него нет подтверждённого локального результата. Эти исключения MUST NOT обновлять Accounts, Portfolio, иные интервалы операций, прогноз будущих событий или другой диапазон свечей. После подтверждения полноты запрошенного прошлого интервала или диапазона его повторное чтение MUST оставаться локальным. + +#### Scenario: Обычное чтение не запускает импорт +- **WHEN** User открывает Accounts, Portfolio, Account Ledger, будущие события либо повторяет неуспешное HTTP-чтение +- **THEN** система читает локальный результат и не начинает личную синхронизацию T-Bank + +#### Scenario: Первый отсутствующий интервал операций +- **WHEN** User впервые открывает интервал Account Ledger без подтверждённого локального результата +- **THEN** система MAY начинает получение только этого интервала и возвращает его локальное состояние неполноты до подтверждения результата + +#### Scenario: Впервые открытый диапазон свечей +- **WHEN** User впервые открывает диапазон свечей, для которого нет подтверждённого локального результата +- **THEN** система MAY начинает получение только этого диапазона и не загружает другие диапазоны или полную историю свечей + +### Requirement: Результат личного обновления остаётся наблюдаемым +Каждый response resource read личных данных текущего подключения MUST возвращать `{ data, meta }`. `meta` MUST отдельно делать наблюдаемыми freshness и полноту именно `data`, момент последнего успешного полного результата resource, момент и состояние последней часовой попытки, затронувшей resource, и доступность resource. Состояние последней попытки MUST различать отсутствие попытки, выполнение, полный успех, частичный успех и неуспех. Доступность MUST различать доступный полный результат, первую загрузку без полного результата, временную недоступность без полного результата и недоступность из-за отсутствующего либо недействительного подключения. Неполный, продолжающийся или неуспешный результат MUST NOT обозначаться полным или свежим. Успешный пустой Accounts либо иной пустой полный результат MUST обозначаться доступным и полным, а не отсутствующим. При неуспешном обновлении последний подтверждённый полный результат и его время MUST оставаться доступными, когда отдельное требование capability не предписывает недоступность данных; его freshness MUST определяться исходным временем, а `meta` MUST отдельно обозначать неуспех последней попытки. Наблюдение этих состояний MUST NOT предоставлять способ начать синхронизацию. + +#### Scenario: Ошибка после полного результата +- **WHEN** очередное обновление набора личных данных завершается ошибкой после ранее подтверждённого полного результата +- **THEN** система возвращает предыдущий полный `data`, его исходное время и freshness, а `meta` отдельно обозначает неуспех последней попытки + +#### Scenario: Первая загрузка ещё не завершена +- **WHEN** для набора личных данных ещё нет подтверждённого полного результата +- **THEN** система возвращает `data: null` и в `meta` обозначает выполнение первой загрузки либо её неуспех без частичного результата, выданного за полный + +#### Scenario: Успешный пустой список счетов +- **WHEN** последний полный результат Accounts подтверждает отсутствие открытых поддерживаемых счетов +- **THEN** система возвращает `data: []`, а `meta` обозначает доступный полный результат, его время и состояние последней часовой попытки diff --git a/package-lock.json b/package-lock.json index 780a527..b86dcb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-specs", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-specs", - "version": "0.1.3", + "version": "0.1.4", "devDependencies": { "@commitlint/cli": "21.2.2", "@commitlint/config-conventional": "21.2.2", diff --git a/package.json b/package.json index 7ad2ecd..0c0e5af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wealth-ledger-specs", - "version": "0.1.3", + "version": "0.1.4", "private": true, "engines": { "node": "24.21.0"