18 Commits
Author SHA1 Message Date
ksv741 aae23d86f0 ci: validate release PR commits independently (WealthLedger/main#30)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:39:30 +03:00
ksv741 ebc9ca872c chore(release): 0.1.4
Conventional commits / commits (pull_request) Failing after 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:19:12 +03:00
ksv741 fe295aa73a spec: archive harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-12 22:52:44 +03:00
ksv741 e8e68a7ed6 Merge pull request 'spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)' (#33) from fix/56-harden-hourly-sync-lifecycle into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
Reviewed-on: #33
2026-10-12 22:29:16 +03:00
ksv741 77b211186c spec: address hourly-sync review findings (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 14s
Specs security / security (pull_request) Successful in 9s
Specs verify / verify (pull_request) Successful in 11s
2026-10-12 22:19:03 +03:00
ksv741 14174a7bb3 spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 20:28:58 +03:00
codex-bot 78f01066d2 Merge pull request 'chore: align specs engineering workflow (WealthLedger/main#27)' (#32) from chore/27-engineering-workflow into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-10-12 11:00:37 +03:00
ksv741 7f12327c37 docs: pin specs workflow reference (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 10:22:13 +03:00
ksv741 a29cbf6aae fix: align specs commitlint workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Canceled after 0s
Specs security / security (pull_request) Canceled after 0s
Specs verify / verify (pull_request) Canceled after 0s
2026-10-12 10:21:32 +03:00
ksv741 a441b08378 chore: align specs engineering workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-12 09:21:12 +03:00
codex-bot ea7beea863 Merge pull request 'docs: clarify specs Node bootstrap' (#31) from codex/issue-24-process into develop
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-10-11 23:23:10 +03:00
ksv741 18518e0d2d docs: clarify specs Node bootstrap
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
2026-10-11 23:06:51 +03:00
codex-bot 0b23fc3013 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-11 11:45:09 +03:00
ksv741 fcdc805f6c ci: skip checks on PR metadata edits
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
2026-10-11 11:36:51 +03:00
ksv741 c132770fbb ci: avoid duplicate PR checks
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 9s
2026-10-11 11:24:57 +03:00
ksv741 951f1a9a20 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-11 11:18:21 +03:00
ksv741 329d66e767 spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-10-09 21:14:22 +03:00
codex-bot 90edab6e33 Merge pull request 'spec: define hourly portfolio sync history' (#25) from release/0.1.3 into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Merge hourly portfolio sync history specs
2026-10-09 09:19:38 +03:00
36 changed files with 1680 additions and 18 deletions
+12
View File
@@ -0,0 +1,12 @@
---
name: Bug Report
about: Неверное поведение
---
## Проблема и шаги воспроизведения
## Факт / ожидание
## Acceptance Criteria
- [ ]
## Окружение / ссылки (если применимо)
+10
View File
@@ -0,0 +1,10 @@
---
name: Engineering Task
about: CI, тесты, документация, процесс и прочее
---
## Контекст и результат
## Scope / связанные Issues (если есть)
## Acceptance Criteria
- [ ]
+12
View File
@@ -0,0 +1,12 @@
---
name: Feature Request
about: Новое наблюдаемое поведение
---
## Зачем и какой результат
## Ожидаемое поведение
## Acceptance Criteria
- [ ]
## Зависимости / связанные Issues (если есть)
+12
View File
@@ -0,0 +1,12 @@
---
name: Refactoring / Technical Debt
about: Улучшение без нового поведения
---
## Проблема и результат
## Сохраняемые инварианты / границы
## Acceptance Criteria
- [ ]
## Зависимости / ссылки (если есть)
+11
View File
@@ -0,0 +1,11 @@
## Summary
## Linked Issue
Refs
## Changes
## Verification
- Команда / результат / ограничения:
## Breaking Changes (если есть)
+10 -2
View File
@@ -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
+6 -2
View File
@@ -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
+6 -1
View File
@@ -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:
+11 -5
View File
@@ -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; версии вручную не повышать.
+29
View File
@@ -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
+5 -3
View File
@@ -4,7 +4,7 @@
## Начало работы
OpenSpec применяется по явному запросу согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
| Change | Lifecycle |
| --- | --- |
@@ -37,9 +37,11 @@ Node задаётся собственной `.nvmrc`; после `nvm use --sil
## Релизы
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Работа: `codex/<task>` от актуального `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/<name>` от проверенного
+9 -1
View File
@@ -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"],
},
};
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-12
@@ -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<string>` (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 не даёт выгоды в
одно-процессной топологии, а стоимость нового компонента выше фикса.
@@ -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
и словарь состояний не изменились.
@@ -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` уже запрещают выдавать неполный или
продолжающийся результат за полный, поэтому их изменение не планируется.
@@ -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
после теста).
@@ -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** его результат сохраняется и учитывается при выводе итога этой попытки
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-10
+93
View File
@@ -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 модели страниц, а не молчаливое изменение контракта.
@@ -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 не создаётся.
@@ -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, необходимые характеристики акций и время получения сведений. Отсутствие или недоступность онлайн-сведений не означает подтверждённого отсутствия акций.
@@ -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.
@@ -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** система сообщает об ошибке получения результата, не утверждая, что подходящих акций нет
@@ -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; потеря результата остаётся ошибкой, а не молчаливой заменой поиска.
@@ -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
@@ -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
+57
View File
@@ -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
+42 -1
View File
@@ -25,7 +25,7 @@
- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00
### Requirement: Последний результат обхода доступен только для наблюдения
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
#### Scenario: Частичный успех
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
@@ -38,3 +38,44 @@
#### 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** его результат сохраняется и учитывается при выводе итога этой попытки
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wealth-ledger-specs",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"engines": {
"node": "24.21.0"