Compare commits
18
Commits
13e3e53d06
..
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae23d86f0 | ||
|
|
ebc9ca872c | ||
|
|
fe295aa73a | ||
|
|
e8e68a7ed6 | ||
|
|
77b211186c | ||
|
|
14174a7bb3 | ||
|
|
78f01066d2 | ||
|
|
7f12327c37 | ||
|
|
a29cbf6aae | ||
|
|
a441b08378 | ||
|
|
ea7beea863 | ||
|
|
18518e0d2d | ||
|
|
0b23fc3013 | ||
|
|
fcdc805f6c | ||
|
|
c132770fbb | ||
|
|
951f1a9a20 | ||
|
|
329d66e767 | ||
|
|
90edab6e33 |
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Неверное поведение
|
||||
---
|
||||
## Проблема и шаги воспроизведения
|
||||
|
||||
## Факт / ожидание
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Окружение / ссылки (если применимо)
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Engineering Task
|
||||
about: CI, тесты, документация, процесс и прочее
|
||||
---
|
||||
## Контекст и результат
|
||||
|
||||
## Scope / связанные Issues (если есть)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Новое наблюдаемое поведение
|
||||
---
|
||||
## Зачем и какой результат
|
||||
|
||||
## Ожидаемое поведение
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Зависимости / связанные Issues (если есть)
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Refactoring / Technical Debt
|
||||
about: Улучшение без нового поведения
|
||||
---
|
||||
## Проблема и результат
|
||||
|
||||
## Сохраняемые инварианты / границы
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Зависимости / ссылки (если есть)
|
||||
@@ -0,0 +1,11 @@
|
||||
## Summary
|
||||
|
||||
## Linked Issue
|
||||
Refs
|
||||
|
||||
## Changes
|
||||
|
||||
## Verification
|
||||
- Команда / результат / ограничения:
|
||||
|
||||
## Breaking Changes (если есть)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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; версии вручную не повышать.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>` от проверенного
|
||||
|
||||
@@ -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
|
||||
после теста).
|
||||
+59
@@ -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** его результат сохраняется и учитывается при выводе итога этой попытки
|
||||
+140
@@ -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.
|
||||
+23
@@ -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
|
||||
+24
@@ -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
|
||||
+23
@@ -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
|
||||
@@ -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; потеря результата остаётся ошибкой, а не молчаливой заменой поиска.
|
||||
+177
@@ -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
|
||||
@@ -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
|
||||
@@ -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** его результат сохраняется и учитывается при выводе итога этой попытки
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wealth-ledger-specs",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "24.21.0"
|
||||
|
||||
Reference in New Issue
Block a user