Тема
Acceptance Criteria — Trading Logic Critical Paths
ИСТОРИЧЕСКИЙ ДОКУМЕНТ. Это снимок критериев до отказа от Backtest, replay, runtime simulation и standalone capture 2026-08-22. Он не задаёт текущее поведение и не является источником команд проверки. Действующие критерии находятся в
openspec/specs/и change-артефактах.
Перенесён из
docs/operations/вdocs/specs/в SSOT-конвергенции II (2026-05-20). Re-mapping AC-IDs / Gherkin scenarios на новыйdocs/strategies/directional-harvester.md. Архивные стратегии (bucket-arb / maker / maker-rebate / merge-arb) удалены целиком per ADR-024 ACCEPTED 2026-05-13. Их AC-IDs пересоберутся как layers поверх extension API (ADR-025 PROPOSED).
Spec-derived Gherkin scenarios для критических торговых paths. Каждый AC имеет уникальный AC-N ID. L3 integration test ОБЯЗАН ссылаться на AC-N в комментарии (
// covers AC-N) или в имени теста (fn ac_N_<slug>()). Источник — только ADR / spec / CLAUDE.md, не код.
Conventions
- Background block — env knobs + market state + wallet pre-conditions. Это инвариантные предусловия (Gherkin
Givenверхнего уровня). - Scenario block — happy path, английские keywords (Given / When / Then / And).
- Negative scenario block — adversarial / edge / failure path.
- Verification observable — конкретный артефакт (placement_log, Decision-stream snapshot, journal row, counter, log line, FSM state), который ассертится в L3 integration test. Это контракт liveness: если артефакт не наблюдаем — тест нельзя написать, AC возвращается на доработку.
<!-- spec gap: ... -->— точка, где ADR/FSD не дают однозначной спецификации. L3 author должен либо запросить уточнение у software-architect, либо assert минимальный наблюдаемый инвариант.
Source map
| AC | Primary spec | Secondary refs |
|---|---|---|
| AC-1 | docs/strategies/directional-harvester.md §Жизненный цикл сделки, §Anchor source | memory:harvester_semantics, ADR-024, ADR-027, ADR-030 |
| AC-2 | docs/strategies/directional-harvester.md §Migration path | ADR-007 §recovery |
| AC-3 | docs/integrations/webhook.md (ADR-003) | docs/strategies/directional-harvester.md §Decision rationale |
| AC-4 | docs/integrations/rtds.md (ADR-004) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (exit GTC) |
| AC-5 | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8 auto-redeem) + §Целевые рынки | memory project_neg_risk_merge_path |
| AC-6 | СНЯТ 2026-08-21 вместе с бэктестом | исполнителя нет; детерминизм решений держит tests/it/directional_decision_trace_golden.rs |
AC-1: Directional / Harvester periodic scan + entry band + DCA + stop-loss
Source: docs/strategies/directional-harvester.md §Жизненный цикл сделки (инварианты 1, 3, 5, 6, 7, 8, 9), §Anchor source и entry-window gate, §Reactive closure detection, §Decision rationale (ADR-024, ADR-027, ADR-030); memory harvester_semantics; memory:directional_periodic_scan_implemented.
Background (knobs live в config.db strategies.definition per ADR-015; env используется только для infrastructure):
BOT_STRATEGY=directional(единственная активная per ADR-024)strategies.definition.entry_price_base=best-ask+offset_cents=0DIRECTIONAL_MIN_PROBABILITY=0.95DIRECTIONAL_MAX_ENTRY_PRICE=0.98(внутренний cap полосы)budget_per_market=5.00(live Harvester)min_size_shares=5целых долей (at-rest в config.db — micro-shares5_000_000, × 10^6; оператор вводит/видит 5 через UI scaling-seam, какbudget_*_micro_usdпрячет μUSD за $)exit_price=0.99,recompute_exit_on_fill=trueexecution.initial_entry.policy={type:"limit_gtd",post_only:false,ttl_seconds:3600}и собственныйmax_taker_fee_bps(effective GTD 1 час; live wirenow+60+3600); каждая другая роль задаёт свою policy явноstop_loss_bid_threshold=0.80,stop_loss_cooldown_hours=5,stop_loss_debounce_secs=5(live с 2026-05-16)finish_typeсконфигурирован per market category (e.g.GstLocalMidnightдля weather daily через embedded GeoNamescities15000)BOT_FALLBACK_SCAN_SECSset (e.g. 60)- Market: binary (
tokens.len() == 2), per-strategyneg_riskgate совместим,accepting_orders=true,closed=falseчерез ClosureWatcher, probability winning outcome ≥ 0.95,best_ask ∈ [0.95, 0.98], entry-window gate допускает (capabilityoutcome-tradability-detectionвозвращаетAllowEntry)
Scenario: Periodic scan эмитит BUY на winning outcome в полосе [0.95, 0.98]
- Given
MarketCacheсодержит ≥ 1 admissible binary market matching все фильтры выше - And бот работает ≥
BOT_FALLBACK_SCAN_SECSс момента последнегоevaluate_periodic - When scanner fires periodic tick →
Strategy::evaluate_periodicнаDirectionalStrategy(Harvester profile) - And per-tick warmup buffer cheapest-first preference sorted ascending по
best_ask(capabilitydirectional-entry-buffer) - Then ровно один
Decision::PlaceOrder { side: Buy, outcome: winning_outcome, price: best_ask, post_only: false, order_type: Limit, time_in_force: Gtd, gtd_seconds: 3600 }эмитится для qualifying market - And
winning_outcomeопределён probability-моделью (single-side YES OR NO, не side_mode-knob) - And
price ∈ [0.95, 0.98](отказ еслиbest_ask > 0.98или< 0.95) - And
size ≥ min_size_shares = 5целых долей (enforcement сравнивает micro-vs-micro:sizeиmin_size_sharesоба at-rest× 10^6, поэтому5целых =5_000_000; бизнес-смысл — «≥ 5 целых долей») - And
size × price ≤ budget_per_market(greedy-budget-bounded) - And ClosureWatcher confirms
is_closed(asset_id) == false - And entry-window verdict =
AllowEntry(finish_type + seconds-bounds)
Scenario: First fill эмитит GTC SELL @ 0.99 для total accumulated position
- Given entry BUY filled полностью (или partial)
- When
Strategy::on_full_fillилиon_partial_fillfires для того order - Then ровно один
Decision::PlaceOrder { side: Sell, price: 0.99, order_type: GTC, size: total_accumulated_position }эмитится - And если был ранее выставленный exit GTC — он пересчитывается на новый total size (
recompute_exit_on_fill = true)
Scenario: DCA top-up — на каждый fill дополнительный BUY в пределах budget
- Given позиция накоплена ниже
budget_per_market - When fill arrives
- Then дополнительный BUY по собственной
execution.dca(в этом сценарии GTD, цена ∈ [0.95, 0.98]) эмитится если остаётся budget - And суммарная экспозиция после DCA ≤
budget_per_market - And exit GTC переразмещается на новый total size
Scenario: Stop-loss — bid < 0.80 непрерывно ≥ 5s → handoff + configured SELL
- Given позиция открыта, exit GTC @ 0.99 в книге
- When
best_bid < 0.80фиксируется первый sub-threshold tick →stop_loss_below_since = now - And continuous monitoring: на recovery (
bid >= 0.80) timer сбрасывается - When
now - stop_loss_below_since > stop_loss_debounce_secs = 5sнепрерывно - Then после one-owner handoff эмитится
Decision::PlaceOrder { side: Sell, execution: execution.stop_exit, price: current_best_bid, close_reason: "stop_loss" }] - And market →
StopLossBlacklistнаstop_loss_cooldown_hours = 5h - And конкретные fill/kill/rest semantics определяются выбранной
execution.stop_exit, не stop-trigger FSM
Negative scenario: best_ask вне полосы → no entry
- Given market matches все остальные admissions, но
best_ask = 0.99(выше cap) - When
evaluate_periodicruns - Then ZERO
Decision::PlaceOrderдля этого cid - And market остаётся в кэше для следующего тика
Negative scenario: probability < 0.95 → no entry
- Given market matches
best_ask ∈ [0.95, 0.98], но winning outcome probability = 0.92 - When
evaluate_periodicruns - Then ZERO
Decision::PlaceOrderдля этого cid
Negative scenario: ClosureWatcher reports closed → no entry, no DCA
- Given ClosureWatcher emitted
OutcomeClosedдля asset_id - When
evaluate_periodicruns или fill arrives - Then ZERO
Decision::PlaceOrderдля этого cid (entry skipped, DCA blocked) - And
closure_event_subscriberдиспатчитDecision::CancelByLifecycleдля existing open orders
Negative scenario: entry-window predictive gate refuses
- Given
late_entry_seconds = 3600сконфигурирован,secs_to_anchor = 60 - When entry-window gate evaluates
- Then verdict =
RefuseTooLate - And ZERO
Decision::PlaceOrder
Verification observable
- Исторический L3 driver: ReplayEngine с synthetic
MarketCache, capture fixture иSymbolicClock. Этот исполнитель и его runbook удалены 2026-08-22. - Assert (entry happy):
placement_logдля qualifying cid содержит ровно 1 BUY entry per periodic tick;price ∈ [0.95, 0.98];order_type == Gtd;gtd_seconds == 3600. - Assert (cheapest-first): два одновременно admitted cids — первый placement по cid с меньшим
best_ask. - Assert (exit on fill): после fill ровно 1
Decision::PlaceOrder { side: Sell, price: 0.99, order_type: GTC }; size equals total accumulated (включая DCA). - Assert (DCA recompute): после второго fill exit GTC переразмещается с увеличенным size; суммарная exposure ≤
budget_per_market. - Assert (stop-loss): seeded sequence
best_bid = 0.79для 5 непрерывных секунд →cancel_callsдля exit_GTC + 1 FOK SELL @ current best_bid; market вStopLossBlacklistдля следующих 5 часов. - Assert (band-out skip): market с
best_ask = 0.94или0.99— zero entries вplacement_log. - Assert (closure skip): emit
OutcomeClosed→ последующий periodic tick не эмититPlaceOrderдля этого asset_id.
AC-2: Recovery after process kill
Source: docs/strategies/directional-harvester.md §Migration path, §Жизненный цикл сделки (инвариант 7 shadow TTL + инвариант 8 redeem hybrid); ADR-007 §recovery (durable bot_sessions + capture_sessions).
Background:
- pm-engine is live (or capture mode) с активной стратегией
directional - 3 open orders POSTED на CLOB: 2 BUY GTD (entry/DCA) + 1 exit GTC @ 0.99
- 2 filled positions держатся on-chain (resolved-pending markets)
- Durable state в SQLite:
orders_history,fills,positions_history,event_logrows (per ADR-005 INV-A atomic journal) - StopLossBlacklist (если был triggered) persisted
Scenario: Hard-kill restart hydrates state from durable + CLOB + on-chain
- Given бот operational с state выше
- When operator посылает
kill -9 <pid>(no graceful shutdown) - And бот рестартует против тех же SQLite databases
- Then
Strategy::recoverвосстанавливает PendingEntry shadow map- accumulated position state + exit GTC awareness
- And
OrderClient::list_orders(status=Open)гидрирует open orders (вычисляетsize = original_size − size_matchedдля partial fills) - And filled positions гидрируются from on-chain ERC-1155 balances
- And первый
evaluate_periodicилиon_full_filltick после restart НЕ эмитит duplicateDecision::PlaceOrderдля cids которые уже представлены вorders_history ∪ positions_history - And StopLossBlacklist restored — cooldown продолжается с оригинального wall-clock timestamp
Negative scenario: Idempotent trade processing after restart
- Given restart sequence выше
- And во время kill window CLOB эмитнул
WsEvent::TradeMatchedдля одного из BUY ордеров, но локально не было ACK - When CLOB re-replays trade на новом WS connection
- Then durable journal idempotency key (trade_id / fill_id) предотвращает double-application
- And position size incremented ровно один раз
- And downstream exit GTC recompute эмитится ровно один раз
Verification observable
- L3 driver: multi-phase replay session — phase 1 captures state в SQLite, session interrupted; phase 2 invokes
Strategy::recoverhook over those DBs. - Assert (no dup): в первые 10 scanner ticks post-recovery,
placement_logсодержит zero new POST entries для cids уже представленных вorders_history ∪ positions_history. - Assert (idempotent trade): inject duplicate
TradeMatchedдля already-processed key; journal row count для того ключа stays at 1. - Assert (partial fill): hydrated order имеет
size = 6(еслиoriginal_size=10иsize_matched=4). - Assert (shadow TTL): seeded PendingEntry с
created_at = now - 700s→ после recovery in-memory state не содержит этого entry; CLOB order (если жив) остался безcancel_callsот shadow cleanup. - Assert (StopLossBlacklist): seeded blacklist entry с cooldown_until в будущем → market не получает entry decisions до истечения cooldown.
AC-3: Webhook signal triggers entry through Strategy pipeline
Source: docs/integrations/webhook.md (ADR-003) §§ "Decision", "Decision Drivers" D2 / D5 / D7, "Considered Options" Option A.
Background:
WEBHOOK_ENABLED=trueWEBHOOK_BIND_ADDR=127.0.0.1:31338(loopback default)WEBHOOK_SECRET=<32-byte hex>(HMAC-SHA256)WEBHOOK_IP_ALLOWLIST=127.0.0.1(optional, set for hardened scenario)BOT_STRATEGY=directionalwith valid Background of AC-1- Active scanner with 8th hook
on_external_signalregistered (ADR-003 § "Decision")
Scenario: Valid signal fans out to Strategy hook → Decision::Buy
- Given an external producer issues
POST /v1/signalsto127.0.0.1:31338with body{"signal_id":"X-001","condition_id":"<cid>","outcome":"Yes",...} - And the body carries a valid
X-Webhook-SignatureHMAC-SHA256 header keyed byWEBHOOK_SECRET - And the source IP is in
WEBHOOK_IP_ALLOWLIST - When the axum handler validates HMAC and dedup-checks
signal_idagainst the in-memory seen-set (D5 idempotency) - Then the signal is enqueued via
mpsc::Sender<ExternalSignal>to the scanner (Option A wire) - And
ScannerHooks::on_external_signalfires on the active strategy chain (DirectionalStrategy::on_external_signal) - And exactly one
Decision::PlaceOrder { side: Buy, .. }is emitted withsignal_source=webhookmetadata for the signal-specifiedcondition_id(D7 — single pipeline, no parallel path) - And the order respects ALL Phase 4 invariants (INV-1 neg-risk filter, max_entry_price, etc.) — webhook does not bypass strategy filters (D2 critical)
Negative scenario: Duplicate signal_id is no-op
- Given signal
X-001was just processed (entry placed) - When the same producer retries with
signal_id=X-001(network retry / at-least-once delivery) - Then the dedup check rejects the duplicate
- And ZERO new
Decision::PlaceOrderis emitted - And the HTTP response is 200 (idempotent ACK), not 4xx
Negative scenario: Bad HMAC → 401 unauthorized
- Given a request body with a tampered or wrong-key HMAC
- When the handler verifies the signature
- Then the response status is
401 Unauthorized - And ZERO downstream effects: no
mpsc::send, noon_external_signalhook fires, noDecisionemitted
Negative scenario: Non-allowlisted source IP → 403 forbidden
- Given
WEBHOOK_IP_ALLOWLIST=10.0.0.5and incoming connection from127.0.0.1 - When axum middleware checks the source IP
- Then the response is
403 Forbidden - And body verification (HMAC) is short-circuited before hash computation
- And no
Decisionemitted
Verification observable
- L3 driver: in-process axum test client (
TestServerover127.0.0.1ephemeral port) + ReplayEngine listening on the mpsc receiver. - Assert (happy): exactly 1 PlaceOrder Decision per unique signal_id AND
metadata.signal_source == "webhook"AND target cid matches payload. - Assert (dup): re-POST same signal_id; total Decision count for that cid stays at 1 across the second request.
- Assert (bad HMAC): response.status == 401 AND
mpsc.try_recv() == Emptyafter a 100ms quiesce. - Assert (IP block): response.status == 403; HMAC verifier counter increment is 0 (proves short-circuit).
AC-4: RTDS toxic-flow active cancel
Source: docs/integrations/rtds.md (ADR-004) §§ Context Pain points #2 (toxic-flow detection), CLAUDE.md § "Optional integrations" RTDS gate (RTDS_TOXIC_FLOW_LOG_ONLY → RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED).
Background:
- RTDS WS connection up to
wss://ws-live-data.polymarket.com RTDS_TOXIC_FLOW_ENABLED=trueRTDS_TOXIC_FLOW_LOG_ONLY=false(active mode)RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED=true(final cutover gate per CLAUDE.md)BOT_STRATEGY=directionalwith active exit GTC quote resting on at least 1 market (per AC-1 exit pathway)- Toxic-flow window thresholds (operator-defined, per ADR-004 Pain point #2): N trades within W seconds, all on the same side, total size ≥ T
Scenario: One-sided pressure burst triggers cancel of resting quotes
- Given an exit GTC SELL is RESTING on cid
Cfor outcome YES - And the rolling RTDS window for cid
Cis empty - When RTDS emits
Nconsecutiveactivity/tradesevents for cidCwithin windowW, allside: BUYand aggregatesize ≥ T(one-sided pressure) - Then the toxic-flow detector classifies the window as toxic
- And exactly one
Decision::CancelOrder(orDecision::CancelAll(condition_id=C)) is emitted for the resting order on cidC - And the Decision's
metadata.reasoncontains"toxic_flow"for operator audit
Negative scenario: LOG_ONLY=true → log entry, no cancel
- Given Background mutated with
RTDS_TOXIC_FLOW_LOG_ONLY=true(and either active-cancel flag false or irrelevant) - And the same toxic burst sequence as above
- When the detector classifies the window as toxic
- Then ZERO
Decision::CancelOrderis emitted for any cid - And a structured log line at level
warn/infois produced withevent="toxic_flow_detected",cid=C,window_size=N,total_size(CLAUDE.md gate semantics)
Negative scenario: ACTIVE_CANCEL_ENABLED=false short-circuits
- Given Background mutated with
RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED=false - When detector classifies the window as toxic
- Then ZERO Cancel emitted (final gate not opened) regardless of
LOG_ONLYvalue
Negative scenario: own-fills do NOT trigger toxic classification
- Given RTDS event has
proxyWallet == OUR_WALLET_ADDRESS - When the event would otherwise contribute to the toxic window
- Then the event is filtered out before window aggregation (ADR-004 § Context — own-fills come from CLOB
/ws/user, not RTDS) - And the window does NOT advance toward classification
Verification observable
- L3 driver: ReplayEngine fed with synthetic RTDS frames +
SimOrderClientwith one resting exit GTC pre-seeded. - Assert (happy): inject 10 RTDS BUY events on cid
Cwithin 5s, total size 50000; assert exactly 1 Cancel-class Decision for an order on cidCwithreason ~= "toxic_flow"within 1 scanner tick of the 10th event. - Assert (LOG_ONLY): same input, flag flipped; assert decision-stream Cancel count for cid
Cis 0; assert journal/log contains 1 entry matchingevent="toxic_flow_detected". - Assert (gate): with active-cancel flag false; same input; same zero-Cancel assertion as LOG_ONLY.
- Assert (own-wallet filter): inject toxic-shaped burst where every event has
proxyWallet == OUR_WALLET; assert detector window stays empty AND zero Cancel.
AC-5: Directional / Harvester auto-redeem on market resolution (hybrid WS + backstop)
Source: docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8 — auto-redeem hybrid), §Целевые рынки (standard CTF + neg-risk-wrapped), §Decision rationale (ADR-016 — TimezoneCache для auto-redeem TTL only); memory project_neg_risk_merge_path (NegRiskAdapter is canonical contract для neg-risk; standard CTF MUST NOT быть использован на neg-risk).
Background:
BOT_STRATEGY=directional- One filled позиция (e.g. 50 shares winning outcome filled @ 0.97 avg)
- Market type: либо standard binary CTF, либо neg-risk-wrapped binary
- Wallet has
setApprovalForAll(NEG_RISK_ADAPTER_ADDRESS, true)(для neg-risk) REDEEM_BACKSTOP_SECS = 600
Scenario: Resolution через WS-event → auto-redeem через корректный adapter
- Given 50-share filled позиция выше
- When CLOB эмитит
WsEvent::MarketResolved { condition_id, winning_outcome } - Then
Strategy::on_market_resolvedfires наDirectionalStrategy - And ровно один
Decision::Redeem { condition_id, neg_risk: <market_flag> }эмитится (primary path per инвариант 8) - And dispatcher маршрутизирует Redeem к корректному on-chain path:
neg_risk=false→CTF.redeemPositions(USDC, 0, cid, [1,2])neg_risk=true→NegRiskAdapter.redeemPositions(bytes32, uint256)
- And позиция удаляется из
self.positions(идемпотентно для backstop) - And на receipt confirmation, wallet pUSD balance увеличивается на approximately
50 × $1.00 = $50.00(modulo gas)
Scenario: Resolution missed WS → 10-min backstop эмитит Redeem
- Given rynok резолвится, но WS-event пропал (network drop / restart window) —
MarketCache::active_markets()больше не содержит cid - And позиция has
filled_atстарше 600s - When
evaluate_periodicruns (backstop branch) - Then ровно один
Decision::Redeemэмитится для того cid (с корректным neg_risk флагом из persisted state) - And позиция удаляется из
self.positions - And идемпотентность с WS-path: если WS приходит позже, on_market_resolved видит уже отсутствующий cid → no double Redeem
Negative scenario: Standard CTF redeem на neg-risk market forbidden
- Given позиция на neg-risk-wrapped binary
- When L3 test моки redeem dispatcher и assert какой контракт method вызывается
- Then call MUST идти на
NegRiskAdapter.redeemPositions, NOT наCTF.redeemPositions(memoryproject_neg_risk_merge_path— neg-risk collateral =wcol, standard CTF revert'нёт) - And L3 test FAILS если standard CTF method вызван для neg_risk=true cid
Negative scenario: Exit GTC @ 0.99 заполняется до resolution → no Redeem
- Given позиция exit'нула через GTC SELL @ 0.99 до on-chain resolve
- And
self.positionsуже не содержит cid (closed после exit fill) - When
WsEvent::MarketResolvedarrives - Then ZERO
Decision::Redeemэмитится — нечего redeem'ить - And backstop тоже molchit (cid отсутствует в
self.positions)
Verification observable
- L3 driver: ReplayEngine + on-chain mock (
MockProvidercapturing contract address + method selector tuples). - Assert (WS-path neg-risk): synthetic
MarketResolvedдля neg-risk cid → ровно 1 Redeem Decision; mock provider записал ровно 1 вызовNegRiskAdapter.redeemPositions(...)AND zero вызововCTF.redeemPositions(...)для того cid. - Assert (WS-path standard CTF): synthetic
MarketResolvedдля standard binary → ровно 1 Redeem Decision; mock записалCTF.redeemPositions(...)AND zeroNegRiskAdapterвызовов. - Assert (backstop): seeded resolved позиция без WS-event,
filled_at = now - 700s→ periodic tick эмитит ровно 1 Redeem; повторный tick не эмитит (idempotent). - Assert (forbid standard CTF on neg-risk): negative invariant —
mock_provider.call_count("CTF.redeemPositions", neg_risk_cid) == 0через все сессии. - Assert (no redeem after exit): seeded exit-GTC fill до resolution → на
MarketResolvedevent zero Redeem Decisions.
AC-6: Replay determinism contract — СНЯТ 2026-08-21
Что произошло. Критерий проверялся тремя паритетными работами CI над офлайн-реплеем записи. Офлайн-бэктест снят решением оператора (обоснование — openspec/changes/composable-portfolio-strategy-domain/design.md §D13: симулятор филлов систематически наливает охотнее биржи, а запись сохраняет один вход торгового цикла из шести). Вместе с ним сняты три шага детерминизма из scripts/gate.sh и .github/workflows/parity.yml, и у AC-6 не осталось исполнителя.
Что держит детерминизм теперь. Утверждение «поток решений стратегии воспроизводим и не зависит от инструментации» проверяет crates/pm-engine/tests/it/directional_decision_trace_golden.rs (C2 golden): он пинит DecisionKind и непустой input_digest и требует равенства прогонов «с эмиссией отчёта» и «без неё». Это тот же класс утверждения без реплея. Виртуальное время (SymbolicClock) сохранено в детерминированных тестовых двойниках; удалённый capture-runbook.md больше не является его контрактом.
Что НЕ покрыто после снятия и признано осознанно. Бит-паритет «живая сессия ↔ переигранная запись» больше не проверяется ничем: проверка перенесена на живые сессии и телеметрию.
Cross-cutting traceability
Every active AC above must have at least ONE L3 integration test in the pm-engine test suite that:
- Carries
// covers AC-Nin its source-level comment, OR has a test name starting withac_<N>_(e.g.fn ac_1_directional_periodic_scan()). - Runs the strategy's actual production hook chain (no in-test re-implementation of the pipeline).
- Asserts at least one observable from the AC's "Verification observable" block.
- Has both a positive AND at least one negative-scenario test (1:1 with the Negative scenario blocks above).
L3 tests для активных AC (AC-1, AC-2, AC-3, AC-4, AC-5) запускаются как часть cargo test -p pm-engine. AC-6 снят 2026-08-21 вместе с бэктестом — исполнителя у него больше нет. Архивные стратегии (bucket-arb / maker / maker-rebate / merge-arb) — ARCHIVED-BY-024, соответствующие L3 tests должны быть помечены #[ignore = "ARCHIVED-BY-024"] до восстановления как layers (ADR-025 PROPOSED).
Maintenance protocol
- When an ADR or FSD listed in the Source map changes, the corresponding AC MUST be reviewed in the same PR. A spec change without an AC review is rejected at gate.
- New strategies require a new AC slot here BEFORE their L3 test lands (AC-first, then test, then strategy gate sign-off —
quality-gatesskill). <!-- spec gap: ... -->markers must either be resolved (ADR amendment + AC update) or escalated via Observatoryconsensus.createto software-architect within 1 sprint of the test landing.