Аудит main/audits/2026-10-19-code-quality-architecture/02-findings.md, CQ-004/CQ-005 и 03-overengineering.md, OE-01: протокол запуска/reservation/drain повторён в шести sync-сервисах; claim к одной таблице tbankSyncWork реализован тремя способами. Изменение инварианта приходится согласовывать в нескольких местах. Фактическое расхождение поведения трёх claim-реализаций аудит не подтвердил — сначала проверить их семантику и транзакционные границы.
Результат и scope
Устранить доказанное дублирование протокола у существующего владельца, мигрируя потребителей по одному с проверкой поведения. Не навязывать всем сервисам одну семантику: Map и Set сейчас по-разному обрабатывают повторный вызов; внешние транзакции и lifecycle каждого run должны сохраняться. ReservationRunner из отчёта — гипотеза реализации, не обязательный результат.
Acceptance Criteria
Для шести runners и трёх claim-путей перечислены сохраняемые инварианты (join/dedup, reserve/finish, failure/drain, restart и границы транзакций); подтверждено, что объединяется только общий протокол.
Claim/reservation больше не поддерживается несколькими независимыми реализациями одного инварианта; ответственное место и оставшиеся намеренные различия явно видны.
Переход каждого потребителя проверен релевантными concurrency/recovery тестами; проверяется сохранение результата при параллельных запросах, сбое и рестарте, без ложной зелёности теста.
Сборка, архитектурные проверки и релевантные тесты backend проходят.
Связано с main#22 (часовая синхронизация), backend#104 (барьеры в тестах операций) и backend#118 (внутренняя граница tbank-sync); их разные AC не заменяют эту задачу. Источник: аудит 2026-10-19, план, пункт 8.
## Проблема
Аудит `main/audits/2026-10-19-code-quality-architecture/02-findings.md`, CQ-004/CQ-005 и `03-overengineering.md`, OE-01: протокол запуска/reservation/drain повторён в шести sync-сервисах; claim к одной таблице `tbankSyncWork` реализован тремя способами. Изменение инварианта приходится согласовывать в нескольких местах. Фактическое расхождение поведения трёх claim-реализаций аудит не подтвердил — сначала проверить их семантику и транзакционные границы.
## Результат и scope
Устранить доказанное дублирование протокола у существующего владельца, мигрируя потребителей по одному с проверкой поведения. Не навязывать всем сервисам одну семантику: `Map` и `Set` сейчас по-разному обрабатывают повторный вызов; внешние транзакции и lifecycle каждого run должны сохраняться. `ReservationRunner` из отчёта — гипотеза реализации, не обязательный результат.
## Acceptance Criteria
- [ ] Для шести runners и трёх claim-путей перечислены сохраняемые инварианты (join/dedup, reserve/finish, failure/drain, restart и границы транзакций); подтверждено, что объединяется только общий протокол.
- [ ] Claim/reservation больше не поддерживается несколькими независимыми реализациями одного инварианта; ответственное место и оставшиеся намеренные различия явно видны.
- [ ] Переход каждого потребителя проверен релевантными concurrency/recovery тестами; проверяется сохранение результата при параллельных запросах, сбое и рестарте, без ложной зелёности теста.
- [ ] Сборка, архитектурные проверки и релевантные тесты backend проходят.
Связано с [main#22](https://git.personal.ksv741.ru/WealthLedger/main/issues/22) (часовая синхронизация), [backend#104](https://git.personal.ksv741.ru/WealthLedger/backend/issues/104) (барьеры в тестах операций) и [backend#118](https://git.personal.ksv741.ru/WealthLedger/backend/issues/118) (внутренняя граница `tbank-sync`); их разные AC не заменяют эту задачу. Источник: аудит 2026-10-19, план, пункт 8.
PR backend#162 (refactor/119-sync-reservation-skeleton, commit 1bc254a) is ready for review; CI verify/commits/security are green.
What changed
The duplicated join predicate of sync reservations is now one function, isJoinableRunningAttempt (type guard next to TbankSyncAttempt, re-exported from tbank.api.ts).
Used by: tbank-access.service.ts (membership), instrument-metadata.repository.ts, catalog-discovery.repository.ts.
Works with Set and Map (only .has is used), so portfolio's Map can adopt it later without an adapter.
Scope decision (differs from the Issue's suggestion)
The Issue suggested a shared reserve skeleton; only the join predicate is shared. The reserve blocks differ in scope key, intent, priority, membership TTL pre-check, findUniqueOrThrow, and the active structure. A generic runner would take 6–7 parameters and be bigger than the duplicates. This follows the Issue's own note that ReservationRunner is a hypothesis, not a requirement.
Not changed (different rules, out of path A): candle join (raises priority, rewrites intent), screen join (checks progress.dispatched and children), portfolio (left as-is for now: it uses pending Map and the same rule could be adopted in a follow-up).
Characterization
New test/tbank-membership-reservation.e2e-spec.ts (2 tests, real SQLite): same-process join returns the same id/attempt; relaunch after the attempt leaves the active set; a foreign process never joins.
Mutation check: removing the active-set check makes the test fail.
Verification
Before changes (develop): unit 310/310, e2e 322/322, openapi OK.
After: tsc app+test 0 errors; unit 310/310; e2e 324/324; openapi OK; lint OK.
Not verified
No production runtime check; SQLite harness only.
Full npm run verify was not run as one command; its steps were run separately.
Sync-state logic in TbankSyncService.status and the hourly scheduler is unchanged.
Not closed yet: waiting for merge approval. Closing via Closes in the PR when merged.
PR backend#162 (`refactor/119-sync-reservation-skeleton`, commit `1bc254a`) is ready for review; CI verify/commits/security are green.
## What changed
- The duplicated join predicate of sync reservations is now one function, `isJoinableRunningAttempt` (type guard next to `TbankSyncAttempt`, re-exported from `tbank.api.ts`).
- Used by: `tbank-access.service.ts` (membership), `instrument-metadata.repository.ts`, `catalog-discovery.repository.ts`.
- Works with `Set` and `Map` (only `.has` is used), so portfolio's `Map` can adopt it later without an adapter.
## Scope decision (differs from the Issue's suggestion)
The Issue suggested a shared reserve skeleton; only the join predicate is shared. The reserve blocks differ in scope key, intent, priority, membership TTL pre-check, `findUniqueOrThrow`, and the active structure. A generic runner would take 6–7 parameters and be bigger than the duplicates. This follows the Issue's own note that `ReservationRunner` is a hypothesis, not a requirement.
- Not changed (different rules, out of path A): candle join (raises priority, rewrites intent), screen join (checks `progress.dispatched` and children), portfolio (left as-is for now: it uses `pending` Map and the same rule could be adopted in a follow-up).
## Characterization
- New `test/tbank-membership-reservation.e2e-spec.ts` (2 tests, real SQLite): same-process join returns the same `id`/attempt; relaunch after the attempt leaves the active set; a foreign process never joins.
- Mutation check: removing the active-set check makes the test fail.
## Verification
- Before changes (develop): unit 310/310, e2e 322/322, openapi OK.
- After: `tsc` app+test 0 errors; unit 310/310; e2e 324/324; openapi OK; lint OK.
## Not verified
- No production runtime check; SQLite harness only.
- Full `npm run verify` was not run as one command; its steps were run separately.
- Sync-state logic in `TbankSyncService.status` and the hourly scheduler is unchanged.
Not closed yet: waiting for merge approval. Closing via `Closes` in the PR when merged.
Merged as fa0a6e5 (squash of PR backend#162). Closed by Closes.
Acceptance criteria
Invariants for the reserve paths are listed and characterized. Membership join/relaunch/foreign-process is covered by the new test/tbank-membership-reservation.e2e-spec.ts; metadata coalescing, abandoned-work recovery and superseded-attempt fencing are covered by the existing tbank-metadata-persistence e2e; catalog and hourly lifecycle by their existing e2e.
The join rule is in one place (isJoinableRunningAttempt, tbank-access.service.ts) and used by membership, metadata and catalog-discovery. Candle, screen and portfolio keep their own rules on purpose (documented in the PR).
Concurrency and restart behaviour verified by the characterization tests and the existing e2e; a mutation that removes the active-set check fails the new test.
Build, types and relevant backend tests pass (see below).
Scope note (differs from the Issue body)
The full reserve skeleton (scope, intent, priority, TTL pre-check, findUniqueOrThrow, Set vs Map) was not extracted. Only the join predicate was shared, because the rest differs by design and a generic runner would add parameters without removing logic. The Issue itself called ReservationRunner a hypothesis, not a requirement.
Verification (on the PR branch; develop identical after squash)
Before changes: unit 310/310, e2e 322/322, openapi:check OK.
After: tsc app and test configs 0 errors; unit 310/310; e2e 324/324 (322 + 2 new); openapi:check OK; npm run lint OK.
CI on PR backend#162: commits, security, verify — success.
Not verified
Production runtime; only the SQLite e2e harness.
npm run verify was not run as one command locally; its steps were run separately (CI verify on the PR was green).
Portfolio reserve (Map-based) still has its own copy of the join rule; can adopt isJoinableRunningAttempt in a follow-up.
TbankSyncService.status and the hourly scheduler behaviour are unchanged.
API contract
No API or OpenAPI change.
Merged as `fa0a6e5` (squash of PR backend#162). Closed by `Closes`.
## Acceptance criteria
- [x] Invariants for the reserve paths are listed and characterized. Membership join/relaunch/foreign-process is covered by the new `test/tbank-membership-reservation.e2e-spec.ts`; metadata coalescing, abandoned-work recovery and superseded-attempt fencing are covered by the existing `tbank-metadata-persistence` e2e; catalog and hourly lifecycle by their existing e2e.
- [x] The join rule is in one place (`isJoinableRunningAttempt`, `tbank-access.service.ts`) and used by membership, metadata and catalog-discovery. Candle, screen and portfolio keep their own rules on purpose (documented in the PR).
- [x] Concurrency and restart behaviour verified by the characterization tests and the existing e2e; a mutation that removes the active-set check fails the new test.
- [x] Build, types and relevant backend tests pass (see below).
## Scope note (differs from the Issue body)
The full reserve skeleton (scope, intent, priority, TTL pre-check, `findUniqueOrThrow`, Set vs Map) was not extracted. Only the join predicate was shared, because the rest differs by design and a generic runner would add parameters without removing logic. The Issue itself called `ReservationRunner` a hypothesis, not a requirement.
## Verification (on the PR branch; develop identical after squash)
- Before changes: unit 310/310, e2e 322/322, `openapi:check` OK.
- After: `tsc` app and test configs 0 errors; unit 310/310; e2e 324/324 (322 + 2 new); `openapi:check` OK; `npm run lint` OK.
- CI on PR backend#162: commits, security, verify — success.
## Not verified
- Production runtime; only the SQLite e2e harness.
- `npm run verify` was not run as one command locally; its steps were run separately (CI `verify` on the PR was green).
- Portfolio reserve (`Map`-based) still has its own copy of the join rule; can adopt `isJoinableRunningAttempt` in a follow-up.
- `TbankSyncService.status` and the hourly scheduler behaviour are unchanged.
## API contract
No API or OpenAPI change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Проблема
Аудит
main/audits/2026-10-19-code-quality-architecture/02-findings.md, CQ-004/CQ-005 и03-overengineering.md, OE-01: протокол запуска/reservation/drain повторён в шести sync-сервисах; claim к одной таблицеtbankSyncWorkреализован тремя способами. Изменение инварианта приходится согласовывать в нескольких местах. Фактическое расхождение поведения трёх claim-реализаций аудит не подтвердил — сначала проверить их семантику и транзакционные границы.Результат и scope
Устранить доказанное дублирование протокола у существующего владельца, мигрируя потребителей по одному с проверкой поведения. Не навязывать всем сервисам одну семантику:
MapиSetсейчас по-разному обрабатывают повторный вызов; внешние транзакции и lifecycle каждого run должны сохраняться.ReservationRunnerиз отчёта — гипотеза реализации, не обязательный результат.Acceptance Criteria
Связано с main#22 (часовая синхронизация), backend#104 (барьеры в тестах операций) и backend#118 (внутренняя граница
tbank-sync); их разные AC не заменяют эту задачу. Источник: аудит 2026-10-19, план, пункт 8.PR backend#162 (
refactor/119-sync-reservation-skeleton, commit1bc254a) is ready for review; CI verify/commits/security are green.What changed
isJoinableRunningAttempt(type guard next toTbankSyncAttempt, re-exported fromtbank.api.ts).tbank-access.service.ts(membership),instrument-metadata.repository.ts,catalog-discovery.repository.ts.SetandMap(only.hasis used), so portfolio'sMapcan adopt it later without an adapter.Scope decision (differs from the Issue's suggestion)
The Issue suggested a shared reserve skeleton; only the join predicate is shared. The reserve blocks differ in scope key, intent, priority, membership TTL pre-check,
findUniqueOrThrow, and the active structure. A generic runner would take 6–7 parameters and be bigger than the duplicates. This follows the Issue's own note thatReservationRunneris a hypothesis, not a requirement.progress.dispatchedand children), portfolio (left as-is for now: it usespendingMap and the same rule could be adopted in a follow-up).Characterization
test/tbank-membership-reservation.e2e-spec.ts(2 tests, real SQLite): same-process join returns the sameid/attempt; relaunch after the attempt leaves the active set; a foreign process never joins.Verification
tscapp+test 0 errors; unit 310/310; e2e 324/324; openapi OK; lint OK.Not verified
npm run verifywas not run as one command; its steps were run separately.TbankSyncService.statusand the hourly scheduler is unchanged.Not closed yet: waiting for merge approval. Closing via
Closesin the PR when merged.Merged as
fa0a6e5(squash of PR backend#162). Closed byCloses.Acceptance criteria
test/tbank-membership-reservation.e2e-spec.ts; metadata coalescing, abandoned-work recovery and superseded-attempt fencing are covered by the existingtbank-metadata-persistencee2e; catalog and hourly lifecycle by their existing e2e.isJoinableRunningAttempt,tbank-access.service.ts) and used by membership, metadata and catalog-discovery. Candle, screen and portfolio keep their own rules on purpose (documented in the PR).Scope note (differs from the Issue body)
The full reserve skeleton (scope, intent, priority, TTL pre-check,
findUniqueOrThrow, Set vs Map) was not extracted. Only the join predicate was shared, because the rest differs by design and a generic runner would add parameters without removing logic. The Issue itself calledReservationRunnera hypothesis, not a requirement.Verification (on the PR branch; develop identical after squash)
openapi:checkOK.tscapp and test configs 0 errors; unit 310/310; e2e 324/324 (322 + 2 new);openapi:checkOK;npm run lintOK.Not verified
npm run verifywas not run as one command locally; its steps were run separately (CIverifyon the PR was green).Map-based) still has its own copy of the join rule; can adoptisJoinableRunningAttemptin a follow-up.TbankSyncService.statusand the hourly scheduler behaviour are unchanged.API contract
No API or OpenAPI change.