Skip to content

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-суда ПЕРЕД доверием статусу (владелец — skill verification-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:

  1. Подать на вход exact input, который feature заявляет обрабатывать (real WS frame, HMAC-signed POST, fill event, periodic tick, on-chain receipt, env-flag combination, …).
  2. Наблюдать конкретную observable boundary: MockOrderClient.place_calls / cancel_calls / merge_calls, journal row count, SSE EventEnvelope decoded из byte stream, MetricsTick.<counter> delta, captured tracing log line, axum HTTP status code, on-chain receipt watcher event, capture file bytes, fail-fast panic.
  3. 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_callsMutex<Vec<PlaceCall>> на MockOrderClienttests via mock.place_calls.lock().unwrap()
MOC.cancel_callsто же для canceltests
MOC.merge_callsто же для merge_positions / mergePositions / merge_positions_neg_risktests
MOC.list_orders etc8 trait-методов OrderClient (ADR-010)tests
journal.event_logSQLite row in event_log tableread_conn.query_row("SELECT seq, kind, ... ")
journal.<table>orders_history, fills, positions_history, basket_states, bot_sessions, capture_sessions, decisions, signalsrusqlite read-conn
sse.envelopeid: <seq>\nevent: <kind>\ndata: <json>\n decoded back to EventEnvelopereqwest::get(stream_url) + line parse
rest.<route>HTTP status + JSON body of /api/local/...axum_test_helper / reqwest
metrics.<counter>MetricsTick.<field> deltaMetrics::snapshot() projection
log.<level>tracing::Span line captured by tracing_subscriber::fmt::TestWritertest capture
capture.bytes*.jsonl.zst written to --capture-pathfilesystem assert
panic.fail_faststartup panic!() payload contains expected env-key list#[should_panic] test
onchain.receiptMergeReceiptRequest → MergeConfirmed / MergeReverted / MergeTimeouttests/receipt_watcher.rs
cli.exit_codepm-cli non-zero exit + stderr diagnosticassert_cmd
process.bindTCP listener exists on 127.0.0.1:<port>TcpStream::connect
subscription.messageoutbound WS frame on /ws/marketmock-server capture

2. Deep-dive волна (10 features × разное число sub-IDs) ​

LIV-1 — RTDS subscription + toxic-flow gate (ADR-004) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-1.1RTDS WebSocket connectADR-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 → ConnectedTCP stream open + metrics.rtds_connection_state == Connected + log line rtds.connectedTBDunmapped
LIV-1.2RTDS 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) сервер ackoutbound frame на mock WS = exact JSON shape; metrics.rtds_subscription_active_filters > 0 (метрика и клиентский фильтр, который она считала, сняты rtds-transport-remediation)TBDunmapped
LIV-1.3RTDS frame parse — activity/tradesADR-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 через mpscparsed event count > 0 (metrics.rtds_events_received_trade += 1); for ms+s payloads parametrisedcrates/pm-bot/src/io/rtds/parse.rs::tests::* (commit 33c49cc magnitude detection); end-to-end via process_messages covered by LIV-1.4 mapping belowpartial
LIV-1.4RTDS hub → ScannerHooks routingADR-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 conversioncrates/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.5Toxic-flow detection — sliding windowADR-004 §Toxic flow(1) on_rtds_trade обновляет ToxicFlowWindow (per-token) → (2) проверяет count >= N AND notional >= T within window W → (3) если активен — записывает Toxic-Flow signalToxicFlowWindow.is_toxic() == true после N seeded trades; metrics.rtds_toxic_signals_triggered += 1TBDunmapped
LIV-1.6Toxic-flow gate — Log-Only modeADR-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::CancelOrderlog line rtds.toxic_flow_detected captured by TestWriter; MOC.cancel_calls.is_empty()TBDunmapped
LIV-1.7Toxic-flow gate — Active Cancel modeADR-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_orderMOC.cancel_calls.lock().unwrap().len() >= 1 with reason matching toxic-flow taxonomy; metrics.cancel_decisions_emitted{reason="toxic_flow"} > 0crates/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.8RTDS metrics countersADR-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 payloadTBDunmapped
LIV-1.9RTDS 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 sinkVec<Alert> returned by engine содержит exact rulecrates/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.2partial

LIV-2 — Webhook signal server (ADR-003) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-2.1Webhook server bindADR-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:31338TcpStream::connect("127.0.0.1:31338") succeeds; GET /v1/health → 200TBDpartial (есть t_e2e_signal_dry_run_smoke)
LIV-2.2HMAC verificationADR-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 += 1TBDpartial
LIV-2.3Idempotency dedup signal_idADR-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 не растётTBDunmapped
LIV-2.4IP allowlist enforcementADR-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 TBDunmapped
LIV-2.5Signal routing — strategy scope matchADR-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 enqueue422 + metrics.webhook_signals_rejected_total{reason="strategy_scope_mismatch"} += 1TBDunmapped
LIV-2.6on_external_signal hook → DecisionADR-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_orderMOC.place_calls.len() >= 1 after one accepted signal под BOT_STRATEGY=directional с прохождением thresholds; metrics.webhook_signals_processed += 1TBD (audit gap §3 ADR-003 LM-2)unmapped

LIV-3 — Directional/Harvester evaluate_periodic (memory directional_periodic_scan_implemented) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-3.1Periodic timer firesdocs/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 marketlog scanner.periodic_tick per interval; metrics.scanner_ticks_total += 1TBDunmapped
LIV-3.2MarketCache iteration с binary-filterdocs/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 + ClosureWatcherinvocation count = admitted markets in cache; metrics.directional_admitted_markets gauge reflectsTBDunmapped
LIV-3.3Entry signal emit uses explicit execution.initial_entrydocs/strategies/directional-harvester.md + capability directional-order-execution-policyadmitted market → sizing/price → pure policy resolver → OrderIntent with configured type/TIF/GTD/post-only and BUY fee cap → routerpersisted order plus trace identify role initial_entry and normalized policy; WatchArm and Direct use the same resolverdirectional_watch_arm_fak_lifecycle, config resolver testsmapped
LIV-3.4Redeem backstopdocs/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 минут после resolutionTBDunmapped

LIV-5 — Directional neg_risk auto-redeem routing (G-17 / G-18 / G-19 / memory neg_risk_merge_path) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-5.1Binary market admission с per-strategy neg_risk gatedocs/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 configTBDunmapped
LIV-5.2Auto-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 pathTBDunmapped
LIV-5.3Auto-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 pathTBDunmapped
LIV-5.4Redeem backstop dispatches через корректный adapterdocs/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 adapterTBDunmapped

LIV-6 — Recovery после рестарта (G-25 / G-26) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-6.1CLOB GET /orders restore live ordersdirectional FSD recovery + ADR-007 §recover() hook(1) startup → (2) Strategy::recover invoked → (3) OrderClient::list_orders(status=Open) → (4) populate OrderManager DashMapMOC.list_orders invoked count==1 (или per-condition_id); OrderManager.orders.len() > 0 after recoveryTBD (covered by tests/restart_attempt_recovery.rs, tests/crash_recovery.rs)partial
LIV-6.2On-chain CTF balance checkdirectional FSD recovery section(1) recover → (2) pm_sdk::onchain::ctf::balance_of(token_id, owner) per leg → (3) populate positions_historyper-token balance read; journal.positions_history rows for each non-zero balanceTBD unmapped
LIV-6.3FSM reconstruction из event_logADR-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 statepost-recovery pairs.get(cid).state == BothFilled if events showed BothFilledTBDunmapped
LIV-6.4No duplicate placement в первый scan tickdirectional FSD idempotency(1) recovery complete → (2) первый periodic tick → (3) evaluate_periodic видит open order для cid → (4) NO new Decision::PlaceOrderMOC.place_calls.len() == 0 за первые N ticks после recovery с уже-active ordersTBDunmapped
LIV-6.5Recovery latency budgetproduction-readiness-2026-05-03.md (implicit)(1) startup → (2) recover total timeInstant::now() measurement < 30s (budget configurable)TBD unmapped

LIV-7 — Webhook idempotency dedup (G-10) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-7.1HTTP-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"} += 1TBDunmapped
LIV-7.2Strategy-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"} += 1TBDunmapped

LIV-8 — Capture pipeline (ADR-009) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-8.1Capture taps emit envelopeADR-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 appendcrates/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 monotonicTBD (mapped по аудиту §9)mapped
LIV-8.2NullSink no-op в production hot loopADR-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 mpscfilesystem audit: 0 files в .tmp/captures/ после dry-run без --capture; metrics.capture_writes_total == 0TBDpartial (replay parity infers indirectly)
LIV-8.3Replay 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 stream10 SHA-256 строки идентичны bit-for-bit; CI workflow parity.yml enforceTBDmapped

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 (Directional evaluate_signal consumer) удалён — wiring TimezoneCache в hot path entry-gate отменён.

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-9.1Activation 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 invokedTBD (13 unit tests cover ADR-016, audit gap §11 fixes wiring)partial
LIV-9.2ICAO → local-day midnight deadlineADR-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)TBDpartial

LIV-10 — Strategy isolation invariant (memory feedback_strategy_isolation) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-10.1BOT_STRATEGY=directional ⇒ только Directional эмитит DecisionsADR-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-pointsMOC.place_calls все имеют client_id префиксированный directional_*TBDunmapped
LIV-10.2Cross-strategy if-branches absentADR-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 в CITBD partial (compile-time enforced via Strategy trait API)

3. Audit-discovered волна (≥ 20 LIV-IDs) ​

LIV-11 — Strategy Engine pipeline (ADR-001 / ADR-007) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-11.1Decision::PlaceOrder reaches place_limit_orderADR-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 Decisionmapped (tests/strategy_engine_integration.rs::engine_handle_full_fill_with_autosell_places_sell)mapped
LIV-11.2Decision::CancelOrder reaches cancel_orderADR-001 + ADR-010(1) hook returns Cancel Decision → (2) executor → OrderClient::cancel_order(&order_id)MOC.cancel_calls recordedmapped (tests/strategy_engine_integration.rs)mapped
LIV-11.3Strategy-tagged client_idADR-001 §Decision Drivers (isolation)(1) Strategy emits Decision → (2) client_id формируется как <strategy>_<basket_or_market>_<seq> → (3) reaches place_limit_orderMOC.place_calls.last().client_id.starts_with("directional_") или соответствующего prefixTBDpartial

LIV-12 — ScannerHooks pipeline (ADR-002 / phase-5) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-12.1on_ws_book → Decision end-to-endADR-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 → MockOrderClientMOC.place_calls.len() >= 1 после single seeded WsEvent::Book для tracked marketTBD (audit reco §4 «ScannerHooks per-hook E2E»)unmapped
LIV-12.2on_new_market → admissionADR-002 + Directional admission(1) WsEvent::NewMarket → (2) on_new_market → (3) если matches admission filter — добавление в MarketCacheMarketCache row для slugTBDpartial
LIV-12.3on_user_event (own-fill) routingADR-002 + ws_user.rs реальная подписка(1) WS user TradeMatched → (2) on_user_event → (3) Directional fill processingstrategy state mutation observable; journal fills row insertedTBDpartial
LIV-12.4on_market_resolved → Redeem decisionsADR-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 positionMOC.redeem_calls.len() == positions_filledTBDunmapped

LIV-13 — Local-Bot Mode SSE+REST (ADR-005) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-13.1BOT_LOCAL_API_ENABLED=true server bindADR-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 succeedsTcpStream::connect("127.0.0.1:31339") succeeds; GET /health 200mapped (audit tests/local_api.rs 5 tests)mapped
LIV-13.2Disabled by default — bit-identical hot loopADR-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)TBDpartial
LIV-13.3Atomic batch — INV-AADR-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 broadcastcrash mid-batch (test) → no torn state: либо обе строки есть, либо обе нет; broadcast strictly post-commitmapped (tests/journal_writer.rs::single_event_atomic_commit, …_idempotent, …_under_load — 9 tests)mapped
LIV-13.4SSE wire-frame schemaADR-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 decodereqwest::get("http://127.0.0.1:31339/api/local/stream") → first frame parses back to EventEnvelope через sonic-rs::from_slice с identical fieldsTBD (audit reco §5)partial
LIV-13.5Last-Event-ID replayADR-005 §Поток событий §replay(1) Client отправляет Last-Event-ID: 42 → (2) handler reads event_log WHERE seq > 42 → (3) backfill streams → (4) затем live broadcast::recvclient получает frames с seq=43,44,…N (backfill) затем live; no gapsTBDpartial
LIV-13.6POST /rotate requires ROTATE tokenADR-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 rowmapped (audit tests/local_api.rs::rotate-confirm)mapped
LIV-13.7Hybrid PnL realized + unrealizedADR-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 totalGET /seed JSON: total_pnl_usd == realized + unrealized (tolerance ±0.01); per-strategy slicingTBDunmapped

LIV-14 — Dashboard cancel UI invariants (ADR-013) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-14.1Single-row cancel — REST POSTADR-013 §C1 + §C2(1) UI click → (2) cancelOrder(id, reason="manual") → (3) POST /api/local/orders/<id>/cancel body {reason} → (4) backend → OrderClient::cancel_orderrest./api/local/orders/X/cancel 200; MOC.cancel_calls.len() == 1TBDpartial
LIV-14.2Bulk cancel ≤ 5 — soft confirmADR-013 §C3(1) UI selects 5 rows → (2) confirm.require (PrimeVue) → (3) cancelMany(ids, "manual") → (4) Promise.allSettled 5 POSTsrest.cancel invoked 5×; MOC.cancel_calls.len() == 5TBDunmapped
LIV-14.3Bulk cancel > 5 — paranoid CANCEL tokenADR-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_callsTBD (audit reco §9)unmapped
LIV-14.45s soft-delete undoADR-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 callrow остаётся в store через t=2000ms; remove только через t=5500ms если не undo'нутоTBDunmapped
LIV-14.5Cancel-reason taxonomy chip severityADR-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'TBDunmapped

LIV-15 — Dashboard risk metrics (ADR-014) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-15.1Annualisation horizon = 31 536 000ADR-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 numberTBDunmapped
LIV-15.2Risk-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 reloadTBDunmapped
LIV-15.3Per-strategy slicingADR-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, allTBDunmapped

LIV-16 — Multi-wallet + strategy-config (ADR-015) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-16.1BOT_WALLET_LABEL unset ⇒ legacy .env pathADR-015 §D10 + INV-1 of migration runbook(1) BOT_WALLET_LABEL="" → (2) loader falls through to WALLET_PRIVATE_KEY env → (3) hot loop bit-identicalmetrics.wallet_source = "env"; legacy 1014-test baseline preserved (or 1071+ post-X2)TBDpartial (claimed 1071 baseline)
LIV-16.2BOT_WALLET_LABEL set + WALLETS_MASTER_PASSPHRASE empty ⇒ panicADR-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 keypanic.fail_fast payload contains WALLETS_MASTER_PASSPHRASETBD (audit reco §10)unmapped
LIV-16.3INV-3 cross-check strategy.wallet_label == BOT_WALLET_LABELADR-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 mismatchTBD (audit reco §10)unmapped
LIV-16.4wallets.db AES-256-GCM round-tripADR-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 для signinground-trip: encrypted blob != plaintext; decryption with wrong passphrase fails (auth tag mismatch)TBD (audit claims 57 wallets unit tests)partial
LIV-16.5Explicit budget envelope without wallet admissionADR-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/enablebudget above current wallet snapshot saves/enables; derived column equals definition / 1e6; runtime router still enforces canonical limitcrates/pm-engine-dashboard/src/routes/api/strategies.rsmapped
LIV-16.6config.db plaintext SELECTADR-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 JSONNO master-passphrase prompt; SELECT succeeds even with empty passphrase envTBDunmapped

LIV-17 — Webhook config fail-fast (ADR-003 §Configuration) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-17.1WEBHOOK_ENABLED=true без WEBHOOK_SECRET ⇒ panicADR-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-zeroTBDpartial (t_webhook_config_fail_fast_missing_secret mentioned в W.1)
LIV-17.2WEBHOOK_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:31338TBDunmapped
LIV-17.3Backward-compat WEBHOOK_ENABLED=false ⇒ vacuousADR-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 registrationNO TCP listener on 31338; metrics.webhook_* все zero / absentTBDpartial (test name mentioned)

LIV-18 — Capture session lifecycle (ADR-009 §Session model) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-18.1--capture requires --dry-runADR-009 §Context user-approved decisions(1) pm-bot --capture --capture-path X без --dry-run → (2) CLI fail-fast → (3) exit non-zero + diagnosticcli.exit_code != 0; stderr contains --capture requires --dry-runTBDunmapped
LIV-18.2capture_sessions row inserted at startADR-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 sessionTBDpartial
LIV-18.3bot_sessions.mode enum extensionADR-009 §Session model(1) startup mode='capture' (вместо 'dry-run' для ADR-005) → (2) row inserts with mode columnjournal.bot_sessions.mode == 'capture' для capture session, 'replay' для pm-analytics replayTBDunmapped

LIV-19 — Parameter sweep harness — СНЯТО 2026-08-21 вместе с бэктестом (строки ниже — историческое описание pm-bot-эры, исполнителя нет) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-19.1pm-analytics sweep --grid parallelismADR-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>.sqlitefilesystem: N variant DBs; metrics: walltime less than N × single_replay_walltime (parallelism speedup)TBDpartial
LIV-19.2DuckDB ATTACH aggregationADR-009 §Phase 5(1) post-sweep → (2) DuckDB ATTACH '<variant>.sqlite' AS v_i (READ_ONLY) × N → (3) SELECT … UNION ALL для каждого variant → (4) CSV reportoutput CSV rowcount == N variants × M metricsTBDunmapped
LIV-19.3What-if env-overrideADR-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 diffoutput CSV содержит per-cid columns realized_pnl, whatif_pnl, delta; non-zero delta для overrides which actually shift gatesTBDunmapped

LIV-20 — RTDS connection state freshness (RCA §4.2 / §4.3) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-20.1Watchdog reset on outbound ping successRCA §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 → reconnectseeded scenario: 60s without inbound + outbound ping every 5s → NO reconnect; only when ping send fails (TCP closed) → reconnectTBD (already partially fixed)partial
LIV-20.2Dashboard rtdsStatus() truthful indicatorRCA §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-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-21.1AST-grep rule blocks latest_tick.<counter> > 0 patternsRCA §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 blockedCI artifact: ast-grep scan exit code != 0 для violation; matches на explicit anti-pattern linesTBDunmapped

LIV-22 — Live RTDS smoke probe (RCA §6.2 P0) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-22.1cargo run -p pm-bot -- --smoke-rtds <busy_slug> daily CI cronRCA §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 JSONexit 0 iff timestamp_format == Milliseconds AND schema_parity == true AND trades_parsed >= 1; exit non-zero + Slack alert otherwiseTBD (P0 RCA action)unmapped
LIV-22.2Polymarket API contract probeRCA §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 shiftcli.exit_code != 0 если proptest::magnitude_class shift detected; per-surface diff reportTBD (P2)unmapped

LIV-23 — Required env fail-fast (CLAUDE.md §Environment fail-fast policy) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-23.1DIRECTIONAL_* block missing ⇒ panic with full listCLAUDE.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-listpanic.fail_fast payload содержит ALL missing keys, not just first; exit non-zeroTBDpartial
LIV-23.2Strategy-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_*, не про другиеTBDunmapped
LIV-23.3WEBHOOK_* lazy validationCLAUDE.md + ADR-003 §Configuration(1) WEBHOOK_ENABLED=false → (2) WEBHOOK_SECRET etc НЕ требуются → (3) startup succeedsfor disabled webhook: NO panic про missing WEBHOOK_SECRETTBDpartial
LIV-23.4RTDS_* always-on + lazy gate knobsADR-004 D3 + CLAUDE.md(1) RTDS_TOXIC_FLOW_LOG_ONLY + RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED обязательные при ADR-004 always-on policy → (2) panic если missingpanic payload contains both keys when missing; startup succeeds when setTBDunmapped

LIV-24 — Optional defaults НЕ вызывают fail-fast (CLAUDE.md §Public optional defaults) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-24.1BOT_LOCAL_API_ENABLED unset ⇒ disabledCLAUDE.md + ADR-005(1) env unset → (2) NO axum, NO journal writer mpsc → (3) executor hot path bit-identicalNO TCP 31339; metrics.local_api_* все zeroTBDpartial
LIV-24.2BOT_TOTAL_BUDGET unset ⇒ legacy defaultCLAUDE.md §Public optional defaults(1) env unset → (2) loader assigns predefined defaultnumerical default match, no panicTBDunmapped

LIV-25 — Capture compact deferred (memory project_capture_compact_deferred) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-25.1pm-analytics compact is Phase 4 stubmemory + 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 conversionTBD unmapped
LIV-25.2Manual archival post-sessionудалённый runbook ../operations/capture-runbook.md(1) operator stops bot → (2) zstd archive .tmp/captures/<id>/ to .tmp/captures-archive/ manuallyfilesystem: archive exists; bytes count > 0RETIREDhistorical

LIV-26 — Encrypted-keys handling (memory feedback_encrypted_wallets + ADR-015) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-26.1pm-cli wallet rotate-master re-encrypts blobsADR-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)TBDunmapped
LIV-26.2pm-cli wallet remove paranoidADR-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 absentTBDunmapped

LIV-27 — Toxic-flow recovery — Cancel chain proof (RCA §6.3) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-27.1Cancel reaches CLOB on toxic-flow activeADR-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}/cancelMOC.cancel_calls count == affected open orders count; per-cancel reason="toxic_flow"TBDunmapped
LIV-27.2No cancel pile-up under repeated triggersADR-004 §Toxic flow detection(1) sliding window остаётся toxic в течение M ticks → (2) уже-cancelled orders НЕ передаются повторно → (3) idempotency check on pairs[cid].state == CancelledMOC.cancel_calls count == ONE per affected order_id; не растёт в M разTBDunmapped

LIV-28 — Production-readiness pre-flight (runbook.md §Pre-flight checklist) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-28.1Pre-flight script runs all checksRCA §6.4 + runbook.md §Pre-flight checklist(1) scripts/preflight/all.sh → (2) iterates manual checklist items → (3) automates via curl / smoke / cargo runexit 0 iff все items pass; report file writtenTBD unmapped
LIV-28.2Q3 timestamp invariant automated gateRCA §5.1 + production-readiness checklist Q3(1) --smoke-rtds reads live wire → (2) SmokeReport.timestamp_format field → (3) если != Milliseconds → exit non-zeroexit non-zero для Seconds magnitude; exit 0 для MillisecondsTBD (P0 corrective)unmapped

LIV-29 — Order client trait method coverage (ADR-010) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-29.1Required place_limit_order reachable through Arc<dyn OrderClient>ADR-010 §2covered by LIV-11.1mapped (tests/strategy_engine_integration.rs)mappedmapped
LIV-29.2Required cancel_order reachableADR-010 §2covered by LIV-11.2mappedmappedmapped
LIV-29.3Provided cancel_all_orders(condition_id) reaches adapterADR-010 §2 + audit gap §10(1) Decision (TBD-which) emits cancel-all → (2) OrderClient::cancel_all_orders(Some(cid)) → (3) production: ClobExecutor::cancel_all RESTMOC.cancel_all_calls recorded with cidTBDunmapped
LIV-29.4Provided list_orders query reachableADR-010 §2covered by LIV-6.1 (recovery)TBDpartialpartial
LIV-29.5Strategy-save balance lookupSuperseded 2026-08-01No strategy create/update/enable path may call wallet balance for budget admission; balance remains an informational/operator surface onlysource test and route behavior prove save succeeds without configured RPC/balancecrates/pm-engine-dashboard/src/routes/api/strategies.rsretired

LIV-30 — Multi-strategy isolation enforcement (ADR-001 + memory feedback_strategy_isolation) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-30.1Process 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 НЕ registeredintrospection of StrategyImpl enum variant matches; metrics.strategies_registered_count == 1TBDpartial
LIV-30.2Strategy-specific client_id namespacingADR-001 §Decision Drivers + memory(1) Decision emit by Strategy X → (2) executor builds client_id = "X_<basket>_<seq>" → (3) reaches place_limit_ordergrep on MOC.place_calls.last().client_id matches strategy prefixTBDpartial

LIV-31 — Capture envelope schema parity (ADR-009 §determinism) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-31.1Source priority deterministic orderingADR-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 runsSHA-256 stable across runs; explicit assertion на ordering of synthetic same-ts eventsmapped (tests/replay_determinism.rs)mapped
LIV-31.2SymbolicClock замещает Instant::now на replayADR-009 §Determinism 2(1) replay path → (2) strategy code calls Clock::now() (NOT Instant::now()) → (3) SymbolicClock.advance(next_event.ts) drives timeast-grep audit: 0 hits на Instant::now() / SystemTime::now() в strategy/application code; replay 10× identical SHA-256TBD (15 callsite migration claimed in plan)partial
LIV-31.3StrategyRng seeded by (session_id, strategy_name)ADR-009 §Determinism 3(1) strategy needs jitter / tiebreak → (2) StrategyRng::for(session, strategy) → (3) deterministic streamidentical RNG outputs across 10 replays of same sessionTBDpartial

LIV-32 — RTDS reconnect storm protection (audit RCA derived) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-32.1Exponential backoff 1s → 30sADR-004 §Existing pattern reuse from ws_client.rs(1) connection error → (2) sleep min(2^attempts, 30) seconds → (3) reconnect attemptper-attempt delay measured; cap at 30s after attempt 5TBDunmapped
LIV-32.2Reconnect counter rate-limitADR-004 §D5 flood protection(1) reconnect storm (>1 per 60s) → (2) metrics.rtds_reconnect_total Δ > threshold → (3) AlertRule firesTBD covered by LIV-1.9TBDunmapped

LIV-33 — Webhook channel back-pressure (ADR-003 §Failure modes) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-33.1mpsc full → 503 CHANNEL_FULLADR-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_total503 status; metrics.webhook_channel_full_total += 1; channel depth gauge near capacityTBDpartial (test name mentioned)

LIV-34 — Body-size cap (ADR-003 §Security 5) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-34.164 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 parserest./v1/signals returns 413 для 70 KB body; signal NOT reaches mpscTBDunmapped

LIV-35 — Rate limit (ADR-003 §Security 4) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-35.1Token bucket 10 req/s burst 30 → 429ADR-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 countTBDpartial

LIV-36 — RTDS dedup vs CLOB last_trade_price (ADR-004 D7) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-36.1Idempotent consume by transactionHashADR-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 triggermetrics.rtds_dedup_hits_total += 1; on_rtds_trade invocation count == 1 для same txTBDunmapped

LIV-37 — End-date deadline source-of-truth (memory project_end_date_issue + IMPORTANT in CLAUDE.md) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-37.1MarketSnapshot.end_date field absentCLAUDE.md §IMPORTANT end_date(1) compile-time assertion: struct definition does NOT contain end_date → (2) construction sites НЕ pass itcargo check fails если field added; ast-grep field_definition[name="end_date"] inside struct[name="MarketSnapshot"] returns 0 hitsTBDpartial (compile-time enforced)
LIV-37.2TimezoneCache canonical computationCLAUDE.md §IMPORTANT + ADR-016covered by LIV-9.*TBDpartialpartial

LIV-38 — Live test wallet hard-coded usage (memory feedback_live_test_wallet) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-38.1All live tests use canonical wallet 0xBA561B30…7F4memory 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 grepTBD unmapped

LIV-42 — Cancel-all wallet reset (pm-cli cancel-all) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-42.1pm-cli cancel-all ⇒ POST per active ordercrates/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 cancelper-order REST call recorded; non-zero exit if любой failsTBDunmapped
LIV-42.2pm-cli sell-all ⇒ market-sell per filled positioncrates/pm-cli/README.md(1) pm-cli sell-all → (2) iterate positions → (3) per position: POST FAK sell at best_bidper-position place_calls(side=Sell, GTC=FAK); summary report at exitTBDunmapped

LIV-43 — Redeem flow (pm-cli redeem) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-43.1Redeem standard CTF positionspm-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) для standardper-cid receipt event; on-chain USDC delta matches expectedTBDunmapped
LIV-43.2Redeem neg-risk via NegRiskAdaptermemory 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 CTFTBDunmapped

LIV-44 — pm-cli init bootstrap (pm-cli/README.md) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-44.1pm-cli init --chain-id 137 writes .envpm-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 0600post-init: .env contains all 5 keys; mode -rw-------TBDunmapped

LIV-45 — pm-cli wrap (pm-cli/README.md V2 wrap+approve) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-45.1pm-cli approve --with-wrap --wrap-all ⇒ on-chain wrap + approvalspm-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 targetsTBDunmapped (manual smoke 2026-05-01 commit 871f811)

LIV-46 — RTDS schema parity probe (ADR-004 Q2) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-46.1--smoke-rtds validates schema parityRTDS 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 expectedSmokeReport.schema_parity == true для current Polymarket payload; false triggers exit non-zero + alertTBD unmapped

LIV-47 — Local-Bot Mode 503 on offline (ADR-005 §Local-bot proxy) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-47.1pm-dashboard /api/local/* returns 503 если pm-bot offlineADR-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 matchesTBD (audit gap §12 (b) «proxy-only nature не asserted»)unmapped

LIV-48 — Wallet-explorer mode independence (ADR-005 §D1 + ADR-012) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-48.1/wallet/{address} route works without pm-botADR-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 populatedTBDunmapped

LIV-49 — RTDS subscription dynamic rebuild (ADR-004 §WS subscription rebuild) ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-49.1Diff-resubscribe на новые active slugsADR-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) без reconnectoutbound WS frames recorded: NO reconnect; precise diff sizeTBDunmapped
LIV-49.2Cleanup на 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 (сняты оба — метрика и механизм)TBDunmapped

LIV-50 — Capture replay parity gate — СНЯТО 2026-08-21: три паритетных шага удалены из .github/workflows/parity.yml и scripts/gate.sh вместе с бэктестом ​

LIV-IDFeatureЗаявлено вWiring chainObservable boundaryLiveness test (file)Status
LIV-50.1CI 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 mergeTBDmapped

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. Использование матрицы ​

  1. При добавлении нового feature в ADR/FSD: автор обязан зарегистрировать новый LIV-ID + wiring chain + observable boundary в этом файле, со статусом unmapped и пометкой <!-- new feature: <date> -->.
  2. При добавлении liveness теста: переключить status на mapped и заполнить Liveness test (file) с относительным путём.
  3. При production-инциденте: пометить status broken + ссылка на post-mortem; не удалять запись.
  4. Pre-merge gate: PR, добавляющий ADR-feature без LIV-ID, блокируется.
  5. Audit cadence: каждый квартал — full audit ADR/FSD vs матрицей, surface diff в новый ticket.