Тема
Feature Liveness Matrix
ИСТОРИЧЕСКИЙ ДОКУМЕНТ. Матрица фиксирует pre-cutover/pre-retirement тестовую архитектуру и содержит удалённые
pm-bot, replay, runtime simulation и standalone capture поверхности. Она не описывает текущие ворота. Действующие контракты находятся вopenspec/specs/; factual order trace реальныхlive/probeсессий остаётся обязательной телеметрией.
Реконсиляция cutover 2026-07-22. Test-file якоря в столбцах ниже, указывающие на
crates/pm-bot/tests/*/crates/pm-dashboard/*, ссылаются на УДАЛЁННУЮ тестовую базу pm-bot. Актуальная база живёт вcrates/pm-engine/tests/+crates/pm-engine/tests/it/(напр. RTDS toxic-flow —it/rtds_toxic_flow_active_cancel.rs; capture —capture_only_mode.rs; replay-детерминизм — parity-jobs в.github/workflows/parity.yml). Строки со статусомmapped/partial, чей anchor указывает на удалённый pm-bot-тест, СТАЛИ и требуют по-строчной ре-верификации против pm-engine-суда ПЕРЕД доверием статусу (владелец — skillverification-loop); не считать их автоматическиunmapped(ложная регрессия), но и не считать покрытыми по мёртвому якорю. CLI-строки с pm-bot-флагами (--smoke-rtds,--capture ... --dry-run) отражают pm-bot-поверхность; в pm-engine capture —--capture-only(standalone), отдельного--smoke-rtdsнет.
Перенесён из
docs/operations/вdocs/specs/в SSOT-конвергенции II (2026-05-20). Re-mapping LIV-IDs на новыйdocs/strategies/directional-harvester.md. Архивные стратегии (paired maker-rebate / merge-arb v2 / bucket-arb FSM / approve preconditions для merge-arb) удалены целиком per ADR-024 ACCEPTED 2026-05-13. Их LIV-IDs пересоберутся как layers поверх extension API (ADR-025 PROPOSED) после стабилизации pm-engine.
Source of truth для liveness тестов. Каждое feature, объявленное в ADR / spec / CLAUDE.md / memory / runbook, имеет уникальный LIV-ID + wiring chain звено за звеном + observable boundary. Liveness тест проверяет, что feature реально работает end-to-end, не просто что код компилируется.
0. Преамбула — класс «silent feature broken»
pm-engine это финансовый инструмент: каждое заявленное в ADR feature, которое silently no-op'ит, потенциально приводит к потере средств и/или к ложному ощущению защиты. Эта матрица фиксирует тот класс багов, который наша 1561-юниттестовая база не ловит по определению.
Прецедент-инициатор (RTDS, 2026-05-06): ADR-004 заявил «always-on RTDS subscription» с toxic-flow detection. WS-клиент подключался, ресабскрайбился, watchdog'ы крутили reconnect-цикл — но 100 % входящих событий молча отбрасывались парсером как stale, потому что Polymarket переключил единицу timestamp с миллисекунд на секунды. Парсер вычислял age_ms ≈ 1.78 × 10¹² ≈ 56 лет и каждый payload падал в guard stale. Дашборд показывал зелёный статус. Все 1561 тест зелёные. Feature тихо мёртвое минимум 4 дня в проде. См. incident-2026-05-06-rtds.md и liveness-audit-2026-05-06.md.
Концепция liveness теста — observable-boundary recording:
- Подать на вход exact input, который feature заявляет обрабатывать (real WS frame, HMAC-signed POST, fill event, periodic tick, on-chain receipt, env-flag combination, …).
- Наблюдать конкретную observable boundary:
MockOrderClient.place_calls / cancel_calls / merge_calls, journal row count, SSEEventEnvelopedecoded из byte stream,MetricsTick.<counter>delta, captured tracing log line, axum HTTP status code, on-chain receipt watcher event, capture file bytes, fail-fast panic. - Assert наличие эффекта на этой boundary. Absence = test fails = silent-broken caught до того как операционные средства уйдут на ринг.
Что НЕ liveness тест:
- Pure-evaluator unit test (
evaluate_signal,evaluate_rebate_pair,compute_sweep) — проверяет математику, не wiring. - Compile-only
#[tokio::test]без assert на observable boundary. - Static fixture replay (frozen-v1 Parquet) — проверяет внутренний invariant, но НЕ external-contract drift (Polymarket changes wire schema, replay continues passing на устаревшем capture).
Status column:
unmapped— liveness тест не идентифицирован вcrates/pm-engine/tests/(после Phase 1 audit ссылки появятся).partial— boundary наблюдается частично (например, hook вызван, но Decision не дошёл до executor).mapped— есть тест, наблюдающий заявленную boundary в production wiring.broken— тест есть, но именно эта feature упала в проде (RTDS).
1. Глоссарий observable boundaries
| Boundary | Артефакт | Откуда наблюдается |
|---|---|---|
MOC.place_calls | Mutex<Vec<PlaceCall>> на MockOrderClient | tests via mock.place_calls.lock().unwrap() |
MOC.cancel_calls | то же для cancel | tests |
MOC.merge_calls | то же для merge_positions / mergePositions / merge_positions_neg_risk | tests |
MOC.list_orders etc | 8 trait-методов OrderClient (ADR-010) | tests |
journal.event_log | SQLite row in event_log table | read_conn.query_row("SELECT seq, kind, ... ") |
journal.<table> | orders_history, fills, positions_history, basket_states, bot_sessions, capture_sessions, decisions, signals | rusqlite read-conn |
sse.envelope | id: <seq>\nevent: <kind>\ndata: <json>\n decoded back to EventEnvelope | reqwest::get(stream_url) + line parse |
rest.<route> | HTTP status + JSON body of /api/local/... | axum_test_helper / reqwest |
metrics.<counter> | MetricsTick.<field> delta | Metrics::snapshot() projection |
log.<level> | tracing::Span line captured by tracing_subscriber::fmt::TestWriter | test capture |
capture.bytes | *.jsonl.zst written to --capture-path | filesystem assert |
panic.fail_fast | startup panic!() payload contains expected env-key list | #[should_panic] test |
onchain.receipt | MergeReceiptRequest → MergeConfirmed / MergeReverted / MergeTimeout | tests/receipt_watcher.rs |
cli.exit_code | pm-cli non-zero exit + stderr diagnostic | assert_cmd |
process.bind | TCP listener exists on 127.0.0.1:<port> | TcpStream::connect |
subscription.message | outbound WS frame on /ws/market | mock-server capture |
2. Deep-dive волна (10 features × разное число sub-IDs)
LIV-1 — RTDS subscription + toxic-flow gate (ADR-004)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-1.1 | RTDS WebSocket connect | ADR-004 §Decision Option A | (1) startup читает RTDS_* env (всегда-on policy D3) → (2) RtdsClient::new(url) → (3) wss://ws-live-data.polymarket.com подключается через tokio-tungstenite → (4) connection state переходит в Connecting → Connected | TCP stream open + metrics.rtds_connection_state == Connected + log line rtds.connected | TBD | unmapped |
| LIV-1.2 | RTDS subscribe wire frame (pm-bot-era per-slug subscribe; pm-engine subscribes wildcard once per connection and never sends per-cid filters — client.rs module doc, 2026-05-17 pivot) | ADR-004 §Wire protocol | (1) scanner вычисляет collect_active_event_slugs → (2) RtdsClient::subscribe(filters) шлёт frame {"action":"subscribe","subscriptions":[{"topic":"activity","type":"trades","filters":"<JSON-string>"}]} → (3) сервер ack | outbound frame на mock WS = exact JSON shape; metrics.rtds_subscription_active_filters > 0 (метрика и клиентский фильтр, который она считала, сняты rtds-transport-remediation) | TBD | unmapped |
| LIV-1.3 | RTDS frame parse — activity/trades | ADR-004 §Activity payload + RCA §4.1 | (1) WS message arrives → (2) sonic-rs::from_slice → RawTradeFrame → (3) validate_and_convert нормализует timestamp (ms vs s detected by magnitude) → (4) emit RtdsEvent::Trade через mpsc | parsed event count > 0 (metrics.rtds_events_received_trade += 1); for ms+s payloads parametrised | crates/pm-bot/src/io/rtds/parse.rs::tests::* (commit 33c49cc magnitude detection); end-to-end via process_messages covered by LIV-1.4 mapping below | partial |
| LIV-1.4 | RTDS hub → ScannerHooks routing | ADR-004 §ScannerHooks D6 | (1) RtdsEvent::Trade пришёл в mpsc → (2) scanner tokio::select! рекомендует branch rtds_rx.recv() → (3) построен RtdsTradeContext → (4) hooks.on_rtds_trade(&ctx).await вызван | recorded invocation на synthetic ScannerHooks recorder; payload fields equal source modulo magnitude conversion | crates/pm-bot/tests/rtds_hook_e2e.rs::{t_wire_payload_drives_on_rtds_trade,t_seconds_magnitude_drives_hook,t_stale_payload_drops_at_parser} (TD-31 closed) | mapped |
| LIV-1.5 | Toxic-flow detection — sliding window | ADR-004 §Toxic flow | (1) on_rtds_trade обновляет ToxicFlowWindow (per-token) → (2) проверяет count >= N AND notional >= T within window W → (3) если активен — записывает Toxic-Flow signal | ToxicFlowWindow.is_toxic() == true после N seeded trades; metrics.rtds_toxic_signals_triggered += 1 | TBD | unmapped |
| LIV-1.6 | Toxic-flow gate — Log-Only mode | ADR-004 §Risk gate; CLAUDE.md RTDS_TOXIC_FLOW_LOG_ONLY | (1) toxic-flow detected → (2) check RTDS_TOXIC_FLOW_LOG_ONLY=true (default) → (3) emit tracing::warn! only, no Decision::CancelOrder | log line rtds.toxic_flow_detected captured by TestWriter; MOC.cancel_calls.is_empty() | TBD | unmapped |
| LIV-1.7 | Toxic-flow gate — Active Cancel mode | ADR-004 §Risk gate; RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED=true | (1) toxic-flow detected + LOG_ONLY=false + ACTIVE_CANCEL_ENABLED=true → (2) compute Vec<order_id> open orders на affected token → (3) emit Decision::CancelOrder { reason: ToxicFlow } per order_id → (4) Decision доходит до OrderClient::cancel_order | MOC.cancel_calls.lock().unwrap().len() >= 1 with reason matching toxic-flow taxonomy; metrics.cancel_decisions_emitted{reason="toxic_flow"} > 0 | crates/pm-bot/tests/rtds_active_cancel.rs::{t_toxic_flow_emits_cancel_when_active,t_toxic_flow_isolates_per_condition_id,t_log_only_emits_no_cancel,t_active_cancel_disabled_emits_no_cancel} (TD-32 closed) | mapped |
| LIV-1.8 | RTDS metrics counters | ADR-004 §Observability + RCA §6.2 | (1) parse path bumps rtds_events_received_* / rtds_events_dropped_* → (2) reconnect path bumps rtds_reconnect_total → (3) MetricsTick projection emits в SSE → (4) AlertRule::rtds_health потребляет | MetricsTick.rtds_events_received_trade > 0 после feed wire payload; MetricsTick.rtds_events_dropped_stale > 0 после feed expired payload | TBD | unmapped |
| LIV-1.9 | RTDS health alert (P0 corrective action) | RCA §6.2 + audit TD-33 | (1) MetricsTick series 5min window с rtds_events_dropped_stale ≥ 1 OR rtds_reconnect_total Δ > 1/60s OR rtds_events_received == 0 AND active_filters > 0 → (2) AlertRule::rtds_health срабатывает → (3) Alert{ severity: Critical, rule: "rtds_health" } поступает в notification sink | Vec<Alert> returned by engine содержит exact rule | crates/pm-bot/src/application/alerts/rule.rs::tests::t_rtds_health_* (8 tests, TD-33 unit-side closed); CI cron LIV-22.1 + Q3 hard-gate LIV-28.2 still open per RCA §6.2 | partial |
LIV-2 — Webhook signal server (ADR-003)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-2.1 | Webhook server bind | ADR-003 §Configuration; WEBHOOK_ENABLED=true + WEBHOOK_BIND_ADDR=127.0.0.1:31338 | (1) startup читает WebhookConfig.enabled=true → (2) fail-fast валидация secret >= 32 bytes → (3) axum router on tokio task → (4) bind на 127.0.0.1:31338 | TcpStream::connect("127.0.0.1:31338") succeeds; GET /v1/health → 200 | TBD | partial (есть t_e2e_signal_dry_run_smoke) |
| LIV-2.2 | HMAC verification | ADR-003 §Security 1 + Validation pipeline | (1) POST /v1/signals arrives → (2) middleware reads X-Webhook-Signature: sha256=<hex> → (3) Hmac::<Sha256>::verify_slice constant-time → (4) good → next, bad → 401 BAD_SIGNATURE | для valid HMAC: 202; для tampered: 401 + metrics.webhook_signature_failures_total += 1 | TBD | partial |
| LIV-2.3 | Idempotency dedup signal_id | ADR-003 Q1 + §Failure modes | (1) signal accepted → (2) LRU cache signal_id → ts (TTL 1h, cap 10_000) → (3) повтор того же signal_id → 409 DUPLICATE → (4) NO mpsc enqueue | первый POST 202; второй POST 409 + metrics.webhook_signals_rejected_total{reason="duplicate"} += 1; channel depth не растёт | TBD | unmapped |
| LIV-2.4 | IP allowlist enforcement | ADR-003 §Security 3 + WEBHOOK_IP_ALLOWLIST | (1) middleware reads ConnectInfo<SocketAddr> → (2) если allowlist=Some и peer ∉ list → 401/403 (status TBD by spec gap) → (3) если allowlist=None → pass | для allowed IP: 202; для denied IP: rejection status code | TBD | unmapped |
| LIV-2.5 | Signal routing — strategy scope match | ADR-003 §Strategy routing + §Failure modes 422 STRATEGY_SCOPE_MISMATCH | (1) signal_type=unknown arrived → (2) BOT_STRATEGY=directional → (3) IngestManager rejects с 422 STRATEGY_SCOPE_MISMATCH → (4) NO mpsc enqueue | 422 + metrics.webhook_signals_rejected_total{reason="strategy_scope_mismatch"} += 1 | TBD | unmapped |
| LIV-2.6 | on_external_signal hook → Decision | ADR-003 §Integration path + §Dispatch site | (1) accepted signal → mpsc → (2) scanner external_signal_rx.recv() → (3) hooks.on_external_signal(&ctx).await → (4) Directional evaluate → (5) Vec<Decision> → (6) executor.apply() → (7) OrderClient::place_limit_order | MOC.place_calls.len() >= 1 after one accepted signal под BOT_STRATEGY=directional с прохождением thresholds; metrics.webhook_signals_processed += 1 | TBD (audit gap §3 ADR-003 LM-2) | unmapped |
LIV-3 — Directional/Harvester evaluate_periodic (memory directional_periodic_scan_implemented)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-3.1 | Periodic timer fires | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) + memory directional_periodic_scan_implemented + commit b902234 | (1) scanner tokio::time::interval(BOT_FALLBACK_SCAN_SECS) → (2) tick → (3) hook on_periodic_tick (ScannerHooks) → (4) для directional → evaluate_periodic per market | log scanner.periodic_tick per interval; metrics.scanner_ticks_total += 1 | TBD | unmapped |
| LIV-3.2 | MarketCache iteration с binary-filter | docs/strategies/directional-harvester.md §Целевые рынки | (1) evaluate_periodic reads Arc<RwLock<MarketCache>> → (2) фильтрует по market.tokens.len() == 2 (binary) + per-strategy neg_risk gate + MARKET_TAG_FILTER → (3) для каждого market — synthesise MarketInput → (4) admission через capability outcome-tradability-detection + ClosureWatcher | invocation count = admitted markets in cache; metrics.directional_admitted_markets gauge reflects | TBD | unmapped |
| LIV-3.3 | Entry signal emit uses explicit execution.initial_entry | docs/strategies/directional-harvester.md + capability directional-order-execution-policy | admitted market → sizing/price → pure policy resolver → OrderIntent with configured type/TIF/GTD/post-only and BUY fee cap → router | persisted order plus trace identify role initial_entry and normalized policy; WatchArm and Direct use the same resolver | directional_watch_arm_fak_lifecycle, config resolver tests | mapped |
| LIV-3.4 | Redeem backstop | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) | (1) evaluate_periodic каждые REDEEM_BACKSTOP_SECS = 600 → (2) сканирует self.positions → (3) для cids отсутствующих в MarketCache::active_markets() с filled_at старше 600s → emit Decision::Redeem → (4) идемпотентно: позиция удаляется после emit | для seeded resolved position не покрытой WS-событием: backstop эмитит Redeem в первые 10 минут после resolution | TBD | unmapped |
LIV-5 — Directional neg_risk auto-redeem routing (G-17 / G-18 / G-19 / memory neg_risk_merge_path)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-5.1 | Binary market admission с per-strategy neg_risk gate | docs/strategies/directional-harvester.md §Целевые рынки | (1) DirectionalStrategy::evaluate* → (2) проверяет market.tokens.len() == 2 → (3) применяет конфигурируемый neg_risk gate (не hardcoded skip) → (4) admission совместимых рынков | seeded MarketCache: 1 binary standard + 1 binary neg_risk + 1 multi-outcome → multi-outcome не доходит до evaluation; binary admitted согласно gate config | TBD | unmapped |
| LIV-5.2 | Auto-redeem standard CTF (neg_risk=false) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) + memory project_neg_risk_merge_path | (1) Strategy::on_market_resolved для standard binary → (2) Decision::Redeem { condition_id, neg_risk: false } → (3) dispatch → (4) pm_sdk::onchain::ctf::redeem_positions(provider, condition_id) (стандартный CTF) | MOC.redeem_calls recorded with target=ConditionalTokens.redeemPositions(USDC, 0, cid, [1,2]); NOT NegRiskAdapter path | TBD | unmapped |
| LIV-5.3 | Auto-redeem NegRiskAdapter (neg_risk=true) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) + §Целевые рынки + memory project_neg_risk_merge_path | (1) Strategy::on_market_resolved для neg-risk-wrapped binary → (2) Decision::Redeem { condition_id, neg_risk: true } → (3) dispatch → (4) INegRiskAdapter.redeemPositions(bytes32, uint256) (load-bearing инверсия: стандартный CTF revert'нёт) | MOC.redeem_calls recorded with target=NegRiskAdapter.redeemPositions(cid, amount); NOT ConditionalTokens path | TBD | unmapped |
| LIV-5.4 | Redeem backstop dispatches через корректный adapter | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) | (1) 10-минутный periodic backstop эмитит Decision::Redeem { neg_risk: <market_flag> } → (2) dispatch уважает флаг → (3) adapter consistency с LIV-5.2 / LIV-5.3 | для seeded resolved позиции под каждым gate: MOC.redeem_calls matches expected adapter | TBD | unmapped |
LIV-6 — Recovery после рестарта (G-25 / G-26)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-6.1 | CLOB GET /orders restore live orders | directional FSD recovery + ADR-007 §recover() hook | (1) startup → (2) Strategy::recover invoked → (3) OrderClient::list_orders(status=Open) → (4) populate OrderManager DashMap | MOC.list_orders invoked count==1 (или per-condition_id); OrderManager.orders.len() > 0 after recovery | TBD (covered by tests/restart_attempt_recovery.rs, tests/crash_recovery.rs) | partial |
| LIV-6.2 | On-chain CTF balance check | directional FSD recovery section | (1) recover → (2) pm_sdk::onchain::ctf::balance_of(token_id, owner) per leg → (3) populate positions_history | per-token balance read; journal.positions_history rows for each non-zero balance | TBD | unmapped |
| LIV-6.3 | FSM reconstruction из event_log | ADR-005 §INV-A + ADR-009 §determinism | (1) recover reads journal.event_log ORDER BY seq → (2) replay BotEvent::* events → (3) reconstruct BasketState / RebatePair / InventoryFsm → (4) state = pre-restart state | post-recovery pairs.get(cid).state == BothFilled if events showed BothFilled | TBD | unmapped |
| LIV-6.4 | No duplicate placement в первый scan tick | directional FSD idempotency | (1) recovery complete → (2) первый periodic tick → (3) evaluate_periodic видит open order для cid → (4) NO new Decision::PlaceOrder | MOC.place_calls.len() == 0 за первые N ticks после recovery с уже-active orders | TBD | unmapped |
| LIV-6.5 | Recovery latency budget | production-readiness-2026-05-03.md (implicit) | (1) startup → (2) recover total time | Instant::now() measurement < 30s (budget configurable) | TBD | unmapped |
LIV-7 — Webhook idempotency dedup (G-10)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-7.1 | HTTP-level dedup (network retry) | ADR-003 §Q1 + §Failure modes 409 | (1) первый POST с signal_id=X → 202 + LRU.insert(X) → (2) повторный POST с signal_id=X в окне TTL → (3) LRU.contains(X)==true → (4) 409 DUPLICATE BEFORE schema parse → (5) NO mpsc enqueue, NO hook call | первый: 202; второй: 409; mpsc.recv() count == 1; metrics.webhook_signals_rejected_total{reason="duplicate"} += 1 | TBD | unmapped |
| LIV-7.2 | Strategy-level dedup (post-restart, новый signal_id, тот же cid) | ADR-003 Q1 + Directional on_external_signal impl | (1) HTTP-level cache empty (после restart или другой signal_id) → (2) сигнал accepted → (3) hook on_external_signal → (4) Directional: evaluate видит open orders → пустой Vec<Decision> | MOC.place_calls.len() == 0 после повторного signal с тем же cid; metrics.webhook_signals_deduped_by_strategy_total{strategy=…, reason="already_active"} += 1 | TBD | unmapped |
LIV-8 — Capture pipeline (ADR-009)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-8.1 | Capture taps emit envelope | ADR-009 §Decision 1 + §Capture flow | (1) --capture --capture-path .tmp/captures/<id> → (2) CaptureSink = JsonlZstdSink (NOT NullSink) → (3) WS Market frame arrives → (4) record(SourceTag::WS_MARKET, RawFrame, ts) → (5) mpsc unbounded → spawn_blocking writer → (6) JSONL.zstd append | crates/pm-bot/tests/capture_smoke.rs style: file captures/dt=<d>/cid=<c>/stream=ws_market/part-N.jsonl.zst exists; round-trip decode yields envelope с правильными schema_version=1, source_tag="WS_MARKET", monotonic_seq strictly monotonic | TBD (mapped по аудиту §9) | mapped |
| LIV-8.2 | NullSink no-op в production hot loop | ADR-009 §Decision 1 + audit gap §14 | (1) --capture НЕ задан → (2) CaptureSink = NullSink → (3) wire frame arrives → (4) record(...) = inline no-op (compile-time monomorphized) → (5) NO file writes, NO mpsc | filesystem audit: 0 files в .tmp/captures/ после dry-run без --capture; metrics.capture_writes_total == 0 | TBD | partial (replay parity infers indirectly) |
| LIV-8.3 | Replay determinism — 10× identical SHA-256 (СНЯТО 2026-08-21 вместе с бэктестом) | ADR-009 §Determinism Contract | (1) frozen-v1 Parquet fixture → (2) pm-analytics replay --session <id> 10× → (3) SymbolicClock + StrategyRng deterministic → (4) emitted Decision events serialised → (5) SHA-256 over stream | 10 SHA-256 строки идентичны bit-for-bit; CI workflow parity.yml enforce | TBD | mapped |
LIV-9 — TimezoneCache deadline (ADR-016) — display + auto-redeem TTL only
Per docs/strategies/directional-harvester.md §Decision rationale (ADR-016): TimezoneCache используется ИСКЛЮЧИТЕЛЬНО для display и auto-redeem TTL. Entry-gate driven by capability
outcome-tradability-detection(см. LIV-3 и AC-2 в acceptance-criteria.md). LIV-9.3 (Directionalevaluate_signalconsumer) удалён — wiring TimezoneCache в hot path entry-gate отменён.
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-9.1 | Activation predicate (wunderground.com) | ADR-016 §Decision rule 1 + §Detection rule canonical | (1) MarketSnapshot.resolution_source строка → (2) requires_timezone_resolution(market) = s.contains("wunderground.com") → (3) если true — invoke extract_icao → (4) если false — UTC end_date verbatim | для market с resolution_source = "https://wunderground.com/...": tz_cache.extract_icao invoked; для пустого/другого: NOT invoked | TBD (13 unit tests cover ADR-016, audit gap §11 fixes wiring) | partial |
| LIV-9.2 | ICAO → local-day midnight deadline | ADR-016 §Decision 1 + memory feedback_midnight_to_midnight | (1) extract_icao(url) → Some("KCYYZ") → (2) compute_deadline(icao, end_date_utc) → (3) lookup timezone_cache.json (disk-only, API key empty per main.rs:952) → (4) compute local midnight following resolution → (5) return as DateTime<Utc> | for known ICAO+date: deadline equals UTC instant of next local-tz midnight; for unknown ICAO: fallback raw UTC end_date (with warn) | TBD | partial |
LIV-10 — Strategy isolation invariant (memory feedback_strategy_isolation)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-10.1 | BOT_STRATEGY=directional ⇒ только Directional эмитит Decisions | ADR-001 (strategy-isolation) + ADR-007 §Phase 4 + ADR-024 | (1) startup → (2) parse BOT_STRATEGY=directional → (3) Dispatcher selects StrategyImpl::Directional enum variant → (4) только Directional registered в hooks → (5) archived strategy modules (per ADR-024) не имеют hook entry-points | MOC.place_calls все имеют client_id префиксированный directional_* | TBD | unmapped |
| LIV-10.2 | Cross-strategy if-branches absent | ADR-001 §Decision Drivers D1 + memory feedback_strategy_isolation | (1) source-level invariant: grep "matches!(.*StrategyType::" crates/pm-bot/src/ returns 0 (после Phase 4 cutover) | exit code != 0 ⇒ violation; integrate в CI | TBD | partial (compile-time enforced via Strategy trait API) |
3. Audit-discovered волна (≥ 20 LIV-IDs)
LIV-11 — Strategy Engine pipeline (ADR-001 / ADR-007)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-11.1 | Decision::PlaceOrder reaches place_limit_order | ADR-001 + ADR-007 §4-layer KISS + ADR-010 §required methods | (1) Strategy hook returns Vec<Decision> → (2) executor.apply iterates → (3) match Decision::PlaceOrder → (4) OrderClient::place_limit_order(token, price, size, side, GTC, expiration, post_only, condition_id_hint) → (5) Mock returns (order_id, _) | MOC.place_calls last entry имеет правильные fields per Decision | mapped (tests/strategy_engine_integration.rs::engine_handle_full_fill_with_autosell_places_sell) | mapped |
| LIV-11.2 | Decision::CancelOrder reaches cancel_order | ADR-001 + ADR-010 | (1) hook returns Cancel Decision → (2) executor → OrderClient::cancel_order(&order_id) | MOC.cancel_calls recorded | mapped (tests/strategy_engine_integration.rs) | mapped |
| LIV-11.3 | Strategy-tagged client_id | ADR-001 §Decision Drivers (isolation) | (1) Strategy emits Decision → (2) client_id формируется как <strategy>_<basket_or_market>_<seq> → (3) reaches place_limit_order | MOC.place_calls.last().client_id.starts_with("directional_") или соответствующего prefix | TBD | partial |
LIV-12 — ScannerHooks pipeline (ADR-002 / phase-5)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-12.1 | on_ws_book → Decision end-to-end | ADR-002 §Decision + audit reco §4 | (1) synthetic WsEvent::Book fed into application scanner → (2) Directional ScannerHooks registered → (3) hook builds OrderbookContext → (4) evaluate → (5) emit Decision → (6) executor → MockOrderClient | MOC.place_calls.len() >= 1 после single seeded WsEvent::Book для tracked market | TBD (audit reco §4 «ScannerHooks per-hook E2E») | unmapped |
| LIV-12.2 | on_new_market → admission | ADR-002 + Directional admission | (1) WsEvent::NewMarket → (2) on_new_market → (3) если matches admission filter — добавление в MarketCache | MarketCache row для slug | TBD | partial |
| LIV-12.3 | on_user_event (own-fill) routing | ADR-002 + ws_user.rs реальная подписка | (1) WS user TradeMatched → (2) on_user_event → (3) Directional fill processing | strategy state mutation observable; journal fills row inserted | TBD | partial |
| LIV-12.4 | on_market_resolved → Redeem decisions | ADR-002 + FSD directional §Outputs / FSD maker §Outputs | (1) WsEvent::MarketResolved → (2) hook iterates filled positions per cid → (3) emit Decision::Redeem { condition_id, neg_risk } per position | MOC.redeem_calls.len() == positions_filled | TBD | unmapped |
LIV-13 — Local-Bot Mode SSE+REST (ADR-005)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-13.1 | BOT_LOCAL_API_ENABLED=true server bind | ADR-005 §D1 + §Configuration | (1) BOT_LOCAL_API_ENABLED=true → (2) BOT_LOCAL_API_BIND_ADDR=127.0.0.1:31339 (default) → (3) axum on tokio task → (4) bind succeeds | TcpStream::connect("127.0.0.1:31339") succeeds; GET /health 200 | mapped (audit tests/local_api.rs 5 tests) | mapped |
| LIV-13.2 | Disabled by default — bit-identical hot loop | ADR-005 §D2 + §Backward compat | (1) BOT_LOCAL_API_ENABLED=false (default) → (2) NO axum, NO tokio::broadcast, NO JournalWriter → (3) executor никого не emit'ит | process.bind 31339: connection refused; metrics.bot_events_emitted_total == 0 (under no journal) | TBD | partial |
| LIV-13.3 | Atomic batch — INV-A | ADR-005 §INV-A | (1) executor.apply → (2) JournalWriter BEGIN IMMEDIATE → (3) UPDATE side_effect_table + INSERT event_log RETURNING seq → (4) COMMIT → (5) ONLY THEN broadcast | crash mid-batch (test) → no torn state: либо обе строки есть, либо обе нет; broadcast strictly post-commit | mapped (tests/journal_writer.rs::single_event_atomic_commit, …_idempotent, …_under_load — 9 tests) | mapped |
| LIV-13.4 | SSE wire-frame schema | ADR-005 §Поток событий + audit gap §5 | (1) BotEvent::OrderPlaced emitted → (2) JournalWriter persist → (3) tokio::broadcast → (4) SSE handler формирует id: <seq>\nevent: order_placed\ndata: <json>\n\n → (5) клиент EventSource decode | reqwest::get("http://127.0.0.1:31339/api/local/stream") → first frame parses back to EventEnvelope через sonic-rs::from_slice с identical fields | TBD (audit reco §5) | partial |
| LIV-13.5 | Last-Event-ID replay | ADR-005 §Поток событий §replay | (1) Client отправляет Last-Event-ID: 42 → (2) handler reads event_log WHERE seq > 42 → (3) backfill streams → (4) затем live broadcast::recv | client получает frames с seq=43,44,…N (backfill) затем live; no gaps | TBD | partial |
| LIV-13.6 | POST /rotate requires ROTATE token | ADR-005 §Session boundary + ADR-013 paranoid pattern | (1) POST /api/local/sessions/rotate без body или с confirm: "" → 400 BAD_TOKEN → (2) с confirm: "ROTATE" → 200 + Arc<SessionId> swap → (3) NEW bot_sessions row | без token: 400; with token: 200 + new bot_sessions.id row | mapped (audit tests/local_api.rs::rotate-confirm) | mapped |
| LIV-13.7 | Hybrid PnL realized + unrealized | ADR-005 §Decision 6 | (1) realized = SUM(fills.pnl_usd) per cid → (2) unrealized = positions × current_mid_price (live OrderManager) → (3) GET /api/local/seed returns hybrid total | GET /seed JSON: total_pnl_usd == realized + unrealized (tolerance ±0.01); per-strategy slicing | TBD | unmapped |
LIV-14 — Dashboard cancel UI invariants (ADR-013)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-14.1 | Single-row cancel — REST POST | ADR-013 §C1 + §C2 | (1) UI click → (2) cancelOrder(id, reason="manual") → (3) POST /api/local/orders/<id>/cancel body {reason} → (4) backend → OrderClient::cancel_order | rest./api/local/orders/X/cancel 200; MOC.cancel_calls.len() == 1 | TBD | partial |
| LIV-14.2 | Bulk cancel ≤ 5 — soft confirm | ADR-013 §C3 | (1) UI selects 5 rows → (2) confirm.require (PrimeVue) → (3) cancelMany(ids, "manual") → (4) Promise.allSettled 5 POSTs | rest.cancel invoked 5×; MOC.cancel_calls.len() == 5 | TBD | unmapped |
| LIV-14.3 | Bulk cancel > 5 — paranoid CANCEL token | ADR-013 §C4 + audit reco §9 | (1) UI selects 6 rows → (2) openParanoid dialog → (3) operator types literal CANCEL → (4) paranoidConfirm validates string equal → (5) если match — proceeds; если нет — abort + no REST | для cohort > 5 без token: 0 cancel_calls; with CANCEL: 6 cancel_calls | TBD (audit reco §9) | unmapped |
| LIV-14.4 | 5s soft-delete undo | ADR-013 §C5 + §C7 | (1) OrderCancelled SSE arrives → (2) store sets status='cancelled', pending_remove_until = ts+5000 → (3) Toast appears → (4) если undoRemove(id) в окне — flip back to open без backend call | row остаётся в store через t=2000ms; remove только через t=5500ms если не undo'нуто | TBD | unmapped |
| LIV-14.5 | Cancel-reason taxonomy chip severity | ADR-013 §C6 + Reason taxonomy table | (1) SSE OrderCancelled { reason: "emergency" } → (2) reasonSeverity("emergency") == "danger" → (3) chip rendering <Tag severity="danger"> | unit assert (Vitest или harness): reasonSeverity('strategy') === 'info', reasonSeverity('manual') === 'warn', reasonSeverity('unknown') === 'secondary' | TBD | unmapped |
LIV-15 — Dashboard risk metrics (ADR-014)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-15.1 | Annualisation horizon = 31 536 000 | ADR-014 §D1 | (1) computeRisk(fills) reads seconds_per_year = 31_536_000 constant → (2) annualisation = √(seconds_per_year / median_dt) | unit: computeRisk(fixedFills).sharpe == expected_with_365d_horizon; explicit assert that 252-day horizon yields different number | TBD | unmapped |
| LIV-15.2 | Risk-free rate override (localStorage) | ADR-014 §D2 | (1) operator types in RiskSettingsPanel.InputNumber → (2) localStorage[pmd:risk_free_rate] updated → (3) computeRisk reads from store → (4) Sharpe пересчитывается | для rf=0.0 vs rf=0.04: Sharpe values differ; restored on reload | TBD | unmapped |
| LIV-15.3 | Per-strategy slicing | ADR-014 §metricsByStrategy | (1) fills payload contains strategy: "directional" → (2) metricsByStrategy.directional computed → (3) если strategy отсутствует — fallback 'all' | seeded fills [s=A, s=A, s=B, missing] → 3 keys: A, B, all | TBD | unmapped |
LIV-16 — Multi-wallet + strategy-config (ADR-015)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-16.1 | BOT_WALLET_LABEL unset ⇒ legacy .env path | ADR-015 §D10 + INV-1 of migration runbook | (1) BOT_WALLET_LABEL="" → (2) loader falls through to WALLET_PRIVATE_KEY env → (3) hot loop bit-identical | metrics.wallet_source = "env"; legacy 1014-test baseline preserved (or 1071+ post-X2) | TBD | partial (claimed 1071 baseline) |
| LIV-16.2 | BOT_WALLET_LABEL set + WALLETS_MASTER_PASSPHRASE empty ⇒ panic | ADR-015 + INV-2 of migration runbook + crates/pm-bot/src/main.rs:112-116 | (1) BOT_WALLET_LABEL=foo + WALLETS_MASTER_PASSPHRASE="" → (2) startup panic with message listing missing key | panic.fail_fast payload contains WALLETS_MASTER_PASSPHRASE | TBD (audit reco §10) | unmapped |
| LIV-16.3 | INV-3 cross-check strategy.wallet_label == BOT_WALLET_LABEL | ADR-015 + INV-3 of migration runbook + main.rs:210-228 | (1) BOT_WALLET_LABEL=A + BOT_STRATEGY_LABEL=S где config.db.strategies[S].wallet_label = B → (2) cross-check → (3) panic | #[should_panic] test; panic payload contains wallet_label mismatch | TBD (audit reco §10) | unmapped |
| LIV-16.4 | wallets.db AES-256-GCM round-trip | ADR-015 §D1 + crypto crate | (1) pm-cli wallet add → encrypt with Argon2id+AES-GCM → INSERT → (2) pm-cli wallet list → decrypt → returns label+address (NOT private key) → (3) pm-bot startup decrypts private_key для signing | round-trip: encrypted blob != plaintext; decryption with wrong passphrase fails (auth tag mismatch) | TBD (audit claims 57 wallets unit tests) | partial |
| LIV-16.5 | Explicit budget envelope without wallet admission | ADR-015 config.db SSOT + canonical RISK-LIMIT 3 | (1) dashboard validates typed definition.total_budget_micro_usd → (2) persists it and derives budget_limit_usd read projection → (3) no wallet-balance RPC or aggregate-budget rejection on add/update/enable | budget above current wallet snapshot saves/enables; derived column equals definition / 1e6; runtime router still enforces canonical limit | crates/pm-engine-dashboard/src/routes/api/strategies.rs | mapped |
| LIV-16.6 | config.db plaintext SELECT | ADR-015 §D1 backup blast radius | (1) pm-cli strategy list → (2) plaintext SELECT (no passphrase) → (3) returns rows with strategy_type, budget_limit_usd, enabled, definition JSON | NO master-passphrase prompt; SELECT succeeds even with empty passphrase env | TBD | unmapped |
LIV-17 — Webhook config fail-fast (ADR-003 §Configuration)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-17.1 | WEBHOOK_ENABLED=true без WEBHOOK_SECRET ⇒ panic | ADR-003 §Configuration §Fail-fast | (1) parse env → (2) validation secret.len() < 32 → (3) panic!("WEBHOOK_SECRET must be >= 32 bytes") | panic.fail_fast payload contains WEBHOOK_SECRET; startup non-zero | TBD | partial (t_webhook_config_fail_fast_missing_secret mentioned в W.1) |
| LIV-17.2 | WEBHOOK_BIND_ADDR non-loopback ⇒ warn (NOT panic) | ADR-003 §Security 2 + Q5 | (1) WEBHOOK_BIND_ADDR=0.0.0.0:31338 + enabled=true → (2) bind_addr.ip().is_loopback() == false → (3) tracing::warn!("…non-loopback bind на вашу ответственность…") → (4) startup продолжается | startup succeeds; warn line captured by TestWriter; process.bind listens on 0.0.0.0:31338 | TBD | unmapped |
| LIV-17.3 | Backward-compat WEBHOOK_ENABLED=false ⇒ vacuous | ADR-003 §Backward compatibility + t_disabled_feature_is_vacuous | (1) WEBHOOK_ENABLED=false → (2) NO mpsc creation → (3) NO axum task → (4) NO on_external_signal registration | NO TCP listener on 31338; metrics.webhook_* все zero / absent | TBD | partial (test name mentioned) |
LIV-18 — Capture session lifecycle (ADR-009 §Session model)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-18.1 | --capture requires --dry-run | ADR-009 §Context user-approved decisions | (1) pm-bot --capture --capture-path X без --dry-run → (2) CLI fail-fast → (3) exit non-zero + diagnostic | cli.exit_code != 0; stderr contains --capture requires --dry-run | TBD | unmapped |
| LIV-18.2 | capture_sessions row inserted at start | ADR-009 §Session model + migration | (1) startup with --capture → (2) INSERT INTO capture_sessions(id, started_at, strategy, capture_path, schema_version) → (3) ID linked to bot_sessions.replay_of_capture_session_id (для replay) | journal.capture_sessions rows count == 1 после single capture session | TBD | partial |
| LIV-18.3 | bot_sessions.mode enum extension | ADR-009 §Session model | (1) startup mode='capture' (вместо 'dry-run' для ADR-005) → (2) row inserts with mode column | journal.bot_sessions.mode == 'capture' для capture session, 'replay' для pm-analytics replay | TBD | unmapped |
LIV-19 — Parameter sweep harness — СНЯТО 2026-08-21 вместе с бэктестом (строки ниже — историческое описание pm-bot-эры, исполнителя нет)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-19.1 | pm-analytics sweep --grid parallelism | ADR-009 §Phase 5 (документ снят) | (1) sweep CLI reads grid TOML → (2) cartesian product variants → (3) rayon CPU pool spawns N workers → (4) каждый writes .tmp/sweep-<id>/<variant>.sqlite | filesystem: N variant DBs; metrics: walltime less than N × single_replay_walltime (parallelism speedup) | TBD | partial |
| LIV-19.2 | DuckDB ATTACH aggregation | ADR-009 §Phase 5 | (1) post-sweep → (2) DuckDB ATTACH '<variant>.sqlite' AS v_i (READ_ONLY) × N → (3) SELECT … UNION ALL для каждого variant → (4) CSV report | output CSV rowcount == N variants × M metrics | TBD | unmapped |
| LIV-19.3 | What-if env-override | ADR-009 §Phase 6 | (1) whatif --markets <cids> --env-override KEY=VAL → (2) replay reads original capture → (3) BotConfig overlaid с override → (4) emit Decisions diff vs realized PnL → (5) CSV diff | output CSV содержит per-cid columns realized_pnl, whatif_pnl, delta; non-zero delta для overrides which actually shift gates | TBD | unmapped |
LIV-20 — RTDS connection state freshness (RCA §4.2 / §4.3)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-20.1 | Watchdog reset on outbound ping success | RCA §4.2 + commit 783aa05 | (1) tokio interval 5s sends "ping" text → (2) если flush succeeds — activity_deadline = now + 30s (NOT just on inbound) → (3) iff true silence → reconnect | seeded scenario: 60s without inbound + outbound ping every 5s → NO reconnect; only when ping send fails (TCP closed) → reconnect | TBD (already partially fixed) | partial |
| LIV-20.2 | Dashboard rtdsStatus() truthful indicator | RCA §4.3 + commit 15aa097 + audit reco §8 | (1) metric tape с rtds_events_received_trade=N (constant) → (2) sumDeltas('counter') over SPARKLINE_POINTS → (3) если delta == 0 за окно → severity='warn' (stuck) NOT 'success' | unit (Vitest): rtdsStatus(stuckTape) == 'warn'; rtdsStatus(healthyTape) == 'success'; rtdsStatus(stormTape) == 'danger' | TBD (indicator-truthfulness.test.ts per RCA §6.1, 26 tests committed но в worktree status uncommitted) | partial |
LIV-21 — Truthful-indicator AST-grep convention (RCA §6.3)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-21.1 | AST-grep rule blocks latest_tick.<counter> > 0 patterns | RCA §6.3 + .ast-grep/truthful-indicator.yml | (1) PR adds latest_tick.foo > 0 в Vue/TS file → (2) ast-grep scan --rule .ast-grep/truthful-indicator.yml → (3) violation reported в CI → (4) PR blocked | CI artifact: ast-grep scan exit code != 0 для violation; matches на explicit anti-pattern lines | TBD | unmapped |
LIV-22 — Live RTDS smoke probe (RCA §6.2 P0)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-22.1 | cargo run -p pm-bot -- --smoke-rtds <busy_slug> daily CI cron | RCA §6.2 + runbook.md §RTDS smoke | (1) GH Actions cron 06:00 UTC → (2) command connects to live wss://ws-live-data.polymarket.com с busy slug → (3) waits ≤ 30s → (4) parses ≥ 1 trade → (5) emits SmokeReport JSON | exit 0 iff timestamp_format == Milliseconds AND schema_parity == true AND trades_parsed >= 1; exit non-zero + Slack alert otherwise | TBD (P0 RCA action) | unmapped |
| LIV-22.2 | Polymarket API contract probe | RCA §6.4 P2 | (1) probe hits each REST surface (Gamma /events, Gamma /markets, CLOB /book, CLOB /orders, Data /trades) → (2) captures one envelope per surface → (3) diff vs golden fixtures → (4) fails on field-type drift или magnitude shift | cli.exit_code != 0 если proptest::magnitude_class shift detected; per-surface diff report | TBD (P2) | unmapped |
LIV-23 — Required env fail-fast (CLAUDE.md §Environment fail-fast policy)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-23.1 | DIRECTIONAL_* block missing ⇒ panic with full list | CLAUDE.md §Environment + BotConfig::validate_required_env_or_panic | (1) BOT_STRATEGY=directional + missing DIRECTIONAL_MIN_PROBABILITY (или иной REQUIRED knob) → (2) validate_required_env_or_panic collects all missing → (3) panic with comma-list | panic.fail_fast payload содержит ALL missing keys, not just first; exit non-zero | TBD | partial |
| LIV-23.2 | Strategy-conditional surfacing после BOT_STRATEGY (legacy env-path) | CLAUDE.md «strategy-conditional blocks surface lazily» | (1) BOT_STRATEGY parsed first → (2) затем нужный block validated lazily → (3) panic с relevant subset only. Note: при BOT_STRATEGY_LABEL set knobs читаются из config.db, env-validation skipped (ADR-015) | для BOT_STRATEGY=directional без LABEL: панcic только про DIRECTIONAL_*, не про другие | TBD | unmapped |
| LIV-23.3 | WEBHOOK_* lazy validation | CLAUDE.md + ADR-003 §Configuration | (1) WEBHOOK_ENABLED=false → (2) WEBHOOK_SECRET etc НЕ требуются → (3) startup succeeds | for disabled webhook: NO panic про missing WEBHOOK_SECRET | TBD | partial |
| LIV-23.4 | RTDS_* always-on + lazy gate knobs | ADR-004 D3 + CLAUDE.md | (1) RTDS_TOXIC_FLOW_LOG_ONLY + RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED обязательные при ADR-004 always-on policy → (2) panic если missing | panic payload contains both keys when missing; startup succeeds when set | TBD | unmapped |
LIV-24 — Optional defaults НЕ вызывают fail-fast (CLAUDE.md §Public optional defaults)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-24.1 | BOT_LOCAL_API_ENABLED unset ⇒ disabled | CLAUDE.md + ADR-005 | (1) env unset → (2) NO axum, NO journal writer mpsc → (3) executor hot path bit-identical | NO TCP 31339; metrics.local_api_* все zero | TBD | partial |
| LIV-24.2 | BOT_TOTAL_BUDGET unset ⇒ legacy default | CLAUDE.md §Public optional defaults | (1) env unset → (2) loader assigns predefined default | numerical default match, no panic | TBD | unmapped |
LIV-25 — Capture compact deferred (memory project_capture_compact_deferred)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-25.1 | pm-analytics compact is Phase 4 stub | memory + ADR-009 §Phase 4 | (1) pm-analytics compact --in --out → (2) ОЖИДАЕМОЕ поведение per ADR-009: JSONL.zstd → Parquet conversion → (3) ТЕКУЩЕЕ: stub returns success без работы (per memory) | exit 0 + warning log compact_deferred; OR (если promise-meet) — Parquet files in --out after conversion | TBD | unmapped |
| LIV-25.2 | Manual archival post-session | удалённый runbook ../operations/capture-runbook.md | (1) operator stops bot → (2) zstd archive .tmp/captures/<id>/ to .tmp/captures-archive/ manually | filesystem: archive exists; bytes count > 0 | RETIRED | historical |
LIV-26 — Encrypted-keys handling (memory feedback_encrypted_wallets + ADR-015)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-26.1 | pm-cli wallet rotate-master re-encrypts blobs | ADR-015 §D1 + pm-cli wallet rotate-master subcommand | (1) rotate-master --old-passphrase X --new-passphrase Y → (2) для каждого row: decrypt (X) → re-encrypt (Y) → UPDATE → (3) commit → (4) old passphrase больше не работает | post-rotate: pm-cli wallet list с new passphrase succeeds; с old passphrase fails (auth tag mismatch) | TBD | unmapped |
| LIV-26.2 | pm-cli wallet remove paranoid | ADR-015 §D1 (per memory feedback) | (1) remove --label foo → (2) require operator type literal DELETE token (ADR-013 paranoid pattern) → (3) DELETE row | без token: exit non-zero, NO row removed; with token: exit 0, row absent | TBD | unmapped |
LIV-27 — Toxic-flow recovery — Cancel chain proof (RCA §6.3)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-27.1 | Cancel reaches CLOB on toxic-flow active | ADR-004 §Risk gates + RCA §6.3 | (1) toxic-flow window saturated → (2) Decision::CancelOrder per active maker order → (3) executor → OrderClient::cancel_order → (4) live: pm-sdk POST /orders/{id}/cancel | MOC.cancel_calls count == affected open orders count; per-cancel reason="toxic_flow" | TBD | unmapped |
| LIV-27.2 | No cancel pile-up under repeated triggers | ADR-004 §Toxic flow detection | (1) sliding window остаётся toxic в течение M ticks → (2) уже-cancelled orders НЕ передаются повторно → (3) idempotency check on pairs[cid].state == Cancelled | MOC.cancel_calls count == ONE per affected order_id; не растёт в M раз | TBD | unmapped |
LIV-28 — Production-readiness pre-flight (runbook.md §Pre-flight checklist)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-28.1 | Pre-flight script runs all checks | RCA §6.4 + runbook.md §Pre-flight checklist | (1) scripts/preflight/all.sh → (2) iterates manual checklist items → (3) automates via curl / smoke / cargo run | exit 0 iff все items pass; report file written | TBD | unmapped |
| LIV-28.2 | Q3 timestamp invariant automated gate | RCA §5.1 + production-readiness checklist Q3 | (1) --smoke-rtds reads live wire → (2) SmokeReport.timestamp_format field → (3) если != Milliseconds → exit non-zero | exit non-zero для Seconds magnitude; exit 0 для Milliseconds | TBD (P0 corrective) | unmapped |
LIV-29 — Order client trait method coverage (ADR-010)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-29.1 | Required place_limit_order reachable through Arc<dyn OrderClient> | ADR-010 §2 | covered by LIV-11.1 | mapped (tests/strategy_engine_integration.rs) | mapped | mapped |
| LIV-29.2 | Required cancel_order reachable | ADR-010 §2 | covered by LIV-11.2 | mapped | mapped | mapped |
| LIV-29.3 | Provided cancel_all_orders(condition_id) reaches adapter | ADR-010 §2 + audit gap §10 | (1) Decision (TBD-which) emits cancel-all → (2) OrderClient::cancel_all_orders(Some(cid)) → (3) production: ClobExecutor::cancel_all REST | MOC.cancel_all_calls recorded with cid | TBD | unmapped |
| LIV-29.4 | Provided list_orders query reachable | ADR-010 §2 | covered by LIV-6.1 (recovery) | TBD | partial | partial |
| LIV-29.5 | Strategy-save balance lookup | Superseded 2026-08-01 | No strategy create/update/enable path may call wallet balance for budget admission; balance remains an informational/operator surface only | source test and route behavior prove save succeeds without configured RPC/balance | crates/pm-engine-dashboard/src/routes/api/strategies.rs | retired |
LIV-30 — Multi-strategy isolation enforcement (ADR-001 + memory feedback_strategy_isolation)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-30.1 | Process per strategy (single-strategy invariant) | ADR-001 + ADR-003 §Strategy routing «pm-bot per-process single-strategy» | (1) BOT_STRATEGY=directional → (2) только StrategyImpl::Directional constructed → (3) other strategy hooks НЕ registered | introspection of StrategyImpl enum variant matches; metrics.strategies_registered_count == 1 | TBD | partial |
| LIV-30.2 | Strategy-specific client_id namespacing | ADR-001 §Decision Drivers + memory | (1) Decision emit by Strategy X → (2) executor builds client_id = "X_<basket>_<seq>" → (3) reaches place_limit_order | grep on MOC.place_calls.last().client_id matches strategy prefix | TBD | partial |
LIV-31 — Capture envelope schema parity (ADR-009 §determinism)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-31.1 | Source priority deterministic ordering | ADR-009 §Determinism Contract 1 | (1) capture stream merged with multiple sources at same ts_ns → (2) tie-break (ts_ns, source_priority, monotonic_seq) per ADR — WS_USER > WS_MARKET > RTDS > REST_GAMMA > REST_CLOB > REST_DATA → (3) replay yields identical order across 10 runs | SHA-256 stable across runs; explicit assertion на ordering of synthetic same-ts events | mapped (tests/replay_determinism.rs) | mapped |
| LIV-31.2 | SymbolicClock замещает Instant::now на replay | ADR-009 §Determinism 2 | (1) replay path → (2) strategy code calls Clock::now() (NOT Instant::now()) → (3) SymbolicClock.advance(next_event.ts) drives time | ast-grep audit: 0 hits на Instant::now() / SystemTime::now() в strategy/application code; replay 10× identical SHA-256 | TBD (15 callsite migration claimed in plan) | partial |
| LIV-31.3 | StrategyRng seeded by (session_id, strategy_name) | ADR-009 §Determinism 3 | (1) strategy needs jitter / tiebreak → (2) StrategyRng::for(session, strategy) → (3) deterministic stream | identical RNG outputs across 10 replays of same session | TBD | partial |
LIV-32 — RTDS reconnect storm protection (audit RCA derived)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-32.1 | Exponential backoff 1s → 30s | ADR-004 §Existing pattern reuse from ws_client.rs | (1) connection error → (2) sleep min(2^attempts, 30) seconds → (3) reconnect attempt | per-attempt delay measured; cap at 30s after attempt 5 | TBD | unmapped |
| LIV-32.2 | Reconnect counter rate-limit | ADR-004 §D5 flood protection | (1) reconnect storm (>1 per 60s) → (2) metrics.rtds_reconnect_total Δ > threshold → (3) AlertRule fires | TBD covered by LIV-1.9 | TBD | unmapped |
LIV-33 — Webhook channel back-pressure (ADR-003 §Failure modes)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-33.1 | mpsc full → 503 CHANNEL_FULL | ADR-003 §Failure modes + t_full_channel_maps_to_503 | (1) channel capacity 256 → (2) accepted signals exceed capacity (executor stalled in test) → (3) try_send errors → (4) HTTP 503 + counter webhook_channel_full_total | 503 status; metrics.webhook_channel_full_total += 1; channel depth gauge near capacity | TBD | partial (test name mentioned) |
LIV-34 — Body-size cap (ADR-003 §Security 5)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-34.1 | 64 KB body cap → 413 (axum default) | ADR-003 §Security 5 | (1) POST с body > 64 KB → (2) axum middleware DefaultBodyLimit::max(64*1024) → (3) reject before sonic-rs parse | rest./v1/signals returns 413 для 70 KB body; signal NOT reaches mpsc | TBD | unmapped |
LIV-35 — Rate limit (ADR-003 §Security 4)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-35.1 | Token bucket 10 req/s burst 30 → 429 | ADR-003 §Security 4 + t_returns_429_when_burst_exhausted | (1) burst N HMAC-valid POSTs где N > 30 в 1 секунде → (2) bucket exhausted → (3) 429 RATE_LIMITED + Retry-After header | первые 30: 202; ост: 429; metrics.webhook_signals_rejected_total{reason="rate_limited"} += rejected count | TBD | partial |
LIV-36 — RTDS dedup vs CLOB last_trade_price (ADR-004 D7)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-36.1 | Idempotent consume by transactionHash | ADR-004 §D7 | (1) RTDS trade arrives → record seen_tx_hash.insert(tx) → (2) CLOB last_trade_price arrives с тем же tx → (3) dedup check → (4) NO double trigger | metrics.rtds_dedup_hits_total += 1; on_rtds_trade invocation count == 1 для same tx | TBD | unmapped |
LIV-37 — End-date deadline source-of-truth (memory project_end_date_issue + IMPORTANT in CLAUDE.md)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-37.1 | MarketSnapshot.end_date field absent | CLAUDE.md §IMPORTANT end_date | (1) compile-time assertion: struct definition does NOT contain end_date → (2) construction sites НЕ pass it | cargo check fails если field added; ast-grep field_definition[name="end_date"] inside struct[name="MarketSnapshot"] returns 0 hits | TBD | partial (compile-time enforced) |
| LIV-37.2 | TimezoneCache canonical computation | CLAUDE.md §IMPORTANT + ADR-016 | covered by LIV-9.* | TBD | partial | partial |
LIV-38 — Live test wallet hard-coded usage (memory feedback_live_test_wallet)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-38.1 | All live tests use canonical wallet 0xBA561B30…7F4 | memory feedback_live_test_wallet + multi-wallet-migration-runbook §Pre-flight | (1) live smoke / live test → (2) configured wallet address asserted в startup log == canonical → (3) (operator policy, not code-enforced) | log line wallet.loaded address=0xba561b30... matches канонический; integration test grep | TBD | unmapped |
LIV-42 — Cancel-all wallet reset (pm-cli cancel-all)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-42.1 | pm-cli cancel-all ⇒ POST per active order | crates/pm-cli/README.md + ADR-010 §provided cancel_all_orders | (1) pm-cli cancel-all --private-key X --rpc-url Y → (2) GET /orders open → (3) per order: POST cancel | per-order REST call recorded; non-zero exit if любой fails | TBD | unmapped |
| LIV-42.2 | pm-cli sell-all ⇒ market-sell per filled position | crates/pm-cli/README.md | (1) pm-cli sell-all → (2) iterate positions → (3) per position: POST FAK sell at best_bid | per-position place_calls(side=Sell, GTC=FAK); summary report at exit | TBD | unmapped |
LIV-43 — Redeem flow (pm-cli redeem)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-43.1 | Redeem standard CTF positions | pm-cli/README.md + memory project_neg_risk_audit | (1) pm-cli redeem --all → (2) iterate resolved positions → (3) pm_sdk::onchain::ctf::redeem_positions(condition_id) для standard | per-cid receipt event; on-chain USDC delta matches expected | TBD | unmapped |
| LIV-43.2 | Redeem neg-risk via NegRiskAdapter | memory project_neg_risk_merge_path + redeem_neg_risk helper | (1) resolved neg_risk position → (2) redeem_neg_risk(condition_id) → INegRiskAdapter.redeemPositions (NOT standard CTF) | NegRiskAdapter call recorded; NOT standard CTF | TBD | unmapped |
LIV-44 — pm-cli init bootstrap (pm-cli/README.md)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-44.1 | pm-cli init --chain-id 137 writes .env | pm-cli/README.md | (1) interactive flow → (2) derives L2 credential triple via clob.derive-api-key → (3) writes WALLET_PRIVATE_KEY, POLY_API_KEY, POLY_API_SECRET, POLY_API_PASSPHRASE, POLY_FUNDER_ADDRESS → (4) file mode 0600 | post-init: .env contains all 5 keys; mode -rw------- | TBD | unmapped |
LIV-45 — pm-cli wrap (pm-cli/README.md V2 wrap+approve)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-45.1 | pm-cli approve --with-wrap --wrap-all ⇒ on-chain wrap + approvals | pm-cli/README.md V2 + memory project_v2_live_done | (1) read USDC balance → (2) wrap to pUSD via on-chain → (3) approve V2 Exchange + CTF + NegRiskAdapter (5 approvals) | post-execution: wallet pUSD balance > 0; isApprovedForAll(owner, target) == true для all targets | TBD | unmapped (manual smoke 2026-05-01 commit 871f811) |
LIV-46 — RTDS schema parity probe (ADR-004 Q2)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-46.1 | --smoke-rtds validates schema parity | RTDS cutover runbook + Q2 resolution + RCA §6.2 | (1) live trades arrive → (2) parser logs all field types → (3) SmokeReport.schema_parity flag set if matches expected | SmokeReport.schema_parity == true для current Polymarket payload; false triggers exit non-zero + alert | TBD | unmapped |
LIV-47 — Local-Bot Mode 503 on offline (ADR-005 §Local-bot proxy)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-47.1 | pm-dashboard /api/local/* returns 503 если pm-bot offline | ADR-005 §Decision 2 + sequence diagram | (1) pm-bot процесс не работает → (2) pm-dashboard tries reverse-proxy → (3) connection refused → (4) 503 SERVICE_UNAVAILABLE с body {"error":"pm-bot offline"} | для curl на pm-dashboard /api/local/seed без pm-bot: 503 status; body matches | TBD (audit gap §12 (b) «proxy-only nature не asserted») | unmapped |
LIV-48 — Wallet-explorer mode independence (ADR-005 §D1 + ADR-012)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-48.1 | /wallet/{address} route works without pm-bot | ADR-005 §D1 + ADR-012 | (1) pm-bot offline → (2) GET /wallet/0xBA561...7F4 → (3) Vue SPA loads → (4) fetches Polymarket Data API directly → (5) renders portfolio | для pm-bot offline: /wallet/<addr> 200 + JSON portfolio populated | TBD | unmapped |
LIV-49 — RTDS subscription dynamic rebuild (ADR-004 §WS subscription rebuild)
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-49.1 | Diff-resubscribe на новые active slugs | ADR-004 §rebuild_rtds_subscription | (1) FSM tracks new event slug → (2) collect_active_event_slugs diff vs current subscription → (3) emit unsubscribe(old_only) + subscribe(new_only) без reconnect | outbound WS frames recorded: NO reconnect; precise diff size | TBD | unmapped |
| LIV-49.2 | Cleanup на slug terminal state (pm-bot-era client-side filter pool; removed by rtds-transport-remediation, no per-slug unsubscribe or filter pool in pm-engine's transport) | ADR-004 §Minuses + directional cleanup | (1) slug reaches terminal state → (2) cleanup hook → (3) RTDS unsubscribe(slug) | outbound unsubscribe frame for slug; metrics.rtds_subscription_active_filters -= 1 (сняты оба — метрика и механизм) | TBD | unmapped |
LIV-50 — Capture replay parity gate — СНЯТО 2026-08-21: три паритетных шага удалены из .github/workflows/parity.yml и scripts/gate.sh вместе с бэктестом
| LIV-ID | Feature | Заявлено в | Wiring chain | Observable boundary | Liveness test (file) | Status |
|---|---|---|---|---|---|---|
| LIV-50.1 | CI parity workflow runs per PR (directional/Harvester only per ADR-024) | ADR-009 §Phase 8 + Phase 9 + ADR-024 | (1) PR opened → (2) GH Actions parity.yml triggered → (3) runs cargo test -p pm-bot --test replay_determinism → (4) 10× SHA-256 contract на directional fixture (no live network) | CI status check parity required before merge; failure blocks merge | TBD | mapped |
4. Покрытие — суммарная таблица
После SSOT-конвергенции II (2026-05-20) удалены LIV-4 (paired maker-rebate, 6 sub-IDs), LIV-39 (approve preconditions, 2 sub-IDs), LIV-40 (merge-arb v2 inventory FSM, 5 sub-IDs), LIV-41 (bucket-arb FSM invariants, 3 sub-IDs), LIV-24.1 (MAKER_REBATE_PAIRED_ENABLED), LIV-9.3 (TimezoneCache в evaluate_signal) — всего убрано 18 strategy-specific sub-IDs ARCHIVED-BY-024. LIV-3 переписан (4 sub-IDs, было 4), LIV-5 переписан (4 sub-IDs, было 5).
Текущий counts ориентировочно: ~16 mapped + ~2 partial + ~107 strict unmapped = ~125 LIV-IDs (точный пересчёт — задача QA Lead после первой волны mapping post-pivot). Baseline в scripts/check-liveness-coverage.py обновить отдельным PR.
Высокоприоритетные (production-blocking) LIV-IDs, поднятые в audit-2026-05-06 (статус post-TD-31/32/33 closure):
- LIV-1.4 (RTDS hook E2E) — TD-31 CLOSED (
tests/rtds_hook_e2e.rs). - LIV-1.7 (RTDS active-cancel) — TD-32 CLOSED (
tests/rtds_active_cancel.rs). - LIV-1.9 (RTDS health alert) — TD-33 PARTIAL (
alerts/rule.rs::tests::t_rtds_health_*); LIV-22.1 (smoke probe daily cron) и LIV-28.2 (Q3 hard-gate) остаются открытыми P0 per RCA §6.2.
Средний приоритет (TD-34 bundle):
- LIV-12.* (ScannerHooks per-hook E2E)
- LIV-13.4 (SSE wire-frame schema)
- LIV-4.4 + LIV-4.5 (paired maker-rebate executor + escalation log)
- LIV-14.3 (paranoid bulk-cancel gate)
- LIV-16.2 + LIV-16.3 (multi-wallet INV-2/INV-3 panic assertions)
- LIV-9.3 (TZ used at evaluate_signal)
- LIV-5.3 + LIV-5.4 (neg-risk vs standard CTF dispatch)
- LIV-20.2 (dashboard truthfulness recurrence guard)
Известные spec gaps в исходных ADR/FSD (помечены <!-- spec gap: ... -->):
- LIV-2.4 — exact HTTP status для IP-deny не зафиксирован в ADR-003.
- LIV-3.4 — ADR-016 §D5 явно «Deferred: StrategyDeps wiring for periodic path».
- LIV-6.2 — точная on-chain call sequence — implementation-level.
- LIV-6.5 — production-readiness budget для recovery latency не зафиксирован численно.
- LIV-12.4 —
Decision::Redeemмаршрут к OrderClient (или RedeemClient?) уточнить. - LIV-25.1 — memory заявляет
pm-analytics compact= stub; ADR-009 §Phase 4 заявляет full Parquet pipeline. Inconsistency. - LIV-37.* — конструктивный compile-time check, runtime liveness ограничен.
- LIV-38.1 — operational invariant, не code-enforced.
- LIV-46.1 — runtime schema parity probe определён через
SmokeReport, но точные поля не зафиксированы в ADR-004.
5. Связь с tech-debt-backlog
Каждый LIV-ID без mapped теста должен быть промотирован в TD-XX ticket в docs/operations/tech-debt-backlog.md после согласования с оператором (memory feedback_no_autonomous_changes запрещает добавление тестов без явного approval).
Текущая привязка:
- TD-31..TD-33 — LIV-1.4 / LIV-1.7 / LIV-1.9 (RTDS High-risk).
- TD-34 (bundle) — Medium-risk gaps (см. выше).
- TD-35..TD-? — остальные unmapped LIV-IDs (audit-discovered волна), бить на блоки по 5..10.
6. Использование матрицы
- При добавлении нового feature в ADR/FSD: автор обязан зарегистрировать новый LIV-ID + wiring chain + observable boundary в этом файле, со статусом
unmappedи пометкой<!-- new feature: <date> -->. - При добавлении liveness теста: переключить status на
mappedи заполнитьLiveness test (file)с относительным путём. - При production-инциденте: пометить status
broken+ ссылка на post-mortem; не удалять запись. - Pre-merge gate: PR, добавляющий ADR-feature без LIV-ID, блокируется.
- Audit cadence: каждый квартал — full audit ADR/FSD vs матрицей, surface diff в новый ticket.