Files
sentiment-engine/prod/docs/COMPREHENSIVE_UV_EDGE_CASE_THEORETICALS.md

466 lines
23 KiB
Markdown
Raw Normal View History

# COMPREHENSIVE UV EDGE CASE THEORETICALS
## State Machine Edge Cases, Undesirable States, and Mitigations for DITAv2
**Document Status:** Reference for operators and engineers
**Last Updated:** 2026-07-13 (Fable commit: INDETERMINATE submit fix)
**Scope:** DITAv2 execution kernel, BingX venue adapter, `bingx_direct` adapter, Rust-backed FSM
---
## 1. EXECUTIVE SUMMARY
The 2026-07-13 Fable commit (INDETERMINATE submit fix) revealed a **fundamental state machine flaw**: treating an **INDETERMINATE** HTTP outcome (request sent, answer lost) as a **confident REJECTED** with `fill_qty=0`. This caused the kernel FSM to roll back to `IDLE` (flat) while the venue kept the position live — creating **orphan positions** that had no SL/TP/ADVSL protection.
This document catalogs:
1. **The root flaw** (INDETERMINATE ≡ REJECTED misclassification)
2. **All seams** where this class of bug can manifest
3. **Foreseeable & unforeseeable state transitions** that are undesirable
4. **Missing states/transitions** that should exist
5. **Mitigations** (implemented, partial, and proposed)
---
## 2. THE ROOT FLAW: INDETERMINATE ≠ REJECTED
### 2.1 What Happened
```python
# OLD bingx_direct.py (pre-2026-07-13)
except BingxHttpError as exc:
status = "REJECTED" # ← WRONG: treated timeout/5xx as confident rejection
ack_row = {"status": "REJECTED", "msg": str(exc), ...}
fill_qty = 0.0 # ← kernel rolls back to IDLE, believes flat
```
### 2.2 The Correction (2026-07-13)
```python
# NEW bingx_direct.py (post-fix)
except BingxHttpError as exc:
effect = getattr(exc, "effect", BingxHttpError.INDETERMINATE)
if effect == BingxHttpError.INDETERMINATE and not _is_rate_limited_error(exc):
# THE ORDER MAY BE LIVE. We do NOT guess. We ask the venue.
found = await self._lookup_own_order_by_client_id(...)
if isinstance(found, dict):
# Venue confirms: order exists → adopt its state
status = ack_row.get("status") or "ACKED"
elif found == "ABSENT":
# Venue says "no such order" → rollback IS sound
status = "REJECTED"
else:
# TRUTH NOT ESTABLISHED. We REFUSE to claim rejection.
status = "INDETERMINATE"
# Venue layer escalates to VenueIndeterminateError
```
### 2.3 The Three-Way Classification (NOW ENFORCED)
| Classification | HTTP Examples | What It Proves | Rollback Sound? |
|----------------|---------------|----------------|-----------------|
| **NOT_ATTEMPTED** | `ConnectError`, `ConnectTimeout`, DNS failure | Request never left the box | ✅ YES |
| **REFUSED** | 4xx (non-429), "insufficient margin" | Venue received and explicitly refused | ✅ YES |
| **INDETERMINATE** | `ReadTimeout`, `ConnectReset`, 5xx, transport death | **Request was sent, answer lost** — order MAY be live | ❌ **NEVER** |
---
## 3. THE SEAMS: WHERE THIS BUG CLASS MANIFESTS
### 3.1 Seam 1: `bingx_direct.submit_intent()` — The Origin
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/adapters/bingx_direct.py`
**Method:** `BingxDirectExecutionAdapter.submit_intent()` (lines ~850-1100)
**Critical Sections:**
- **Lines ~950-1020**: `try/except BingxHttpError` block — the classification logic
- **Lines ~970-1010**: `_lookup_own_order_by_client_id()` — the point lookup that reconciles truth
- **Lines ~1010-1050**: Status assignment (`REJECTED` vs `INDETERMINATE` vs `ABSENT`)
**Enumerated Failure Modes Here:**
| Failure | Classification | Root Cause |
|---------|----------------|------------|
| `httpx.ReadTimeout` | INDETERMINATE | Request sent, response lost |
| `httpx.ConnectTimeout` | NOT_ATTEMPTED | Never left the box |
| `httpx.ConnectError("refused")` | NOT_ATTEMPTED | Connection refused |
| 500/502/503/504 | INDETERMINATE | Server error, request may have been processed |
| `httpx.RemoteProtocolError` | INDETERMINATE | Protocol violation mid-stream |
| `httpx.RemoteProtocolError` (partial response) | INDETERMINATE | May have been accepted |
| TLS handshake failure | NOT_ATTEMPTED | Never established secure channel |
| `BingxHttpError` with unknown code | INDETERMINATE (default) | Conservative: assume may be live |
**Mitigation (Implemented):**
- Lines ~950-1020: Three-way classification with `_lookup_own_order_by_client_id()` reconciliation
- Lines ~1010-1050: `status = "INDETERMINATE"` path that does NOT set `REJECTED`
---
### 3.2 Seam 2: `BingxVenueAdapter.submit()` / `submit_async()` — The Venue Layer
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/bingx_venue.py`
**Methods:** `submit()` (sync, line ~800) and `submit_async()` (async, line ~950)
**Critical Logic:**
```python
# Both submit() and submit_async() share this pattern:
receipt = self._call_backend("submit_intent", legacy) # or await backend.submit_intent_async()
# ── UNKNOWN OUTCOME ──
if str(getattr(receipt, "status", "") or "").upper() == "INDETERMINATE":
raise VenueIndeterminateError(
f"submit outcome UNKNOWN for intent={intent.intent_id} asset={intent.asset} "
f"— order may be live at the venue; refusing to claim rejection",
receipt=receipt,
)
```
**Error Hierarchy:**
```
VenuePostAckError (base)
└── VenueIndeterminateError ← raised for INDETERMINATE status
```
**Mitigation (Implemented):**
- Both sync and async paths check `receipt.status == "INDETERMINATE"`
- Raises `VenueIndeterminateError` (subclass of `VenuePostAckError`)
- Kernel's "no-rollback fences" catch `VenuePostAckError` (see Seam 3)
---
### 3.3 Seam 3: `ExecutionKernel.process_intent()` / `process_intent_async()` — The Kernel Boundary
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/rust_backend.py`
**Methods:** `process_intent()` (sync, line ~880) and `process_intent_async()` (async, line ~1050)
**Critical Logic (sync):**
```python
try:
emitted_events = self.venue.submit(intent)
except VenuePostAckError as _post_ack_exc:
# ORDER IS LIVE AT THE VENUE. Unknown != flat.
# NO rollback, NO synthetic REJECT.
logger.critical("FIX(rust_backend): POST-ACK venue failure — ORDER IS LIVE. NOT rolling back.")
emitted_events = list(getattr(_post_ack_exc, "events", []) or [])
except Exception as _submit_exc:
# PRE-ACK failure: venue never took the order. Safe to roll back.
logger.error("venue.submit failed — synthetic REJECTED to roll back FSM")
emitted_events = [synthetic_REJECTED_event]
```
**Critical Logic (async):**
```python
submit_async = getattr(self.venue, "submit_async", None)
try:
if submit_async is not None:
emitted_events = await submit_async(intent)
else:
emitted_events = self.venue.submit(intent)
except VenuePostAckError as _post_ack_exc:
# SAME LOGIC: ORDER IS LIVE. NO ROLLBACK.
logger.critical("FIX(rust_backend): POST-ACK venue failure — ORDER IS LIVE. NOT rolling back.")
emitted_events = list(getattr(_post_ack_exc, "events", []) or [])
except Exception as _submit_exc:
# PRE-ACK: synthetic REJECT to roll back FSM
emitted_events = [synthetic_REJECTED_event]
```
**The "No-Rollback Fence" — THE KEY MITIGATION:**
```python
# In on_venue_event (and process_intent), kernel checks:
if isinstance(exc, VenuePostAckError):
# NO ROLLBACK — slot stays in ENTRY_WORKING / EXIT_WORKING
# Let E-feed FILL / reconcile settle it
```
---
### 3.4 Seam 4: `on_venue_event()` — The Event Ingestion Path
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/rust_backend.py`
**Method:** `on_venue_event()` (line ~1200)
**Logic:**
```python
def on_venue_event(self, event: VenueEvent) -> KernelOutcome:
# Processes all venue events through the Rust FSM
# If event.status == REJECTED and slot is in ENTRY_WORKING/EXIT_WORKING:
# → FSM transitions to IDLE (flat)
# If event.status == INDETERMINATE:
# → Venue layer NEVER emits this; it raises VenueIndeterminateError instead
```
**Key Point:** The venue adapter **never emits an `INDETERMINATE` event** — it raises `VenueIndeterminateError` instead. The kernel only sees events with `status` ∈ {ACKED, REJECTED, FILLED, PARTIAL_FILL, CANCELED, CANCEL_REJECTED, RATE_LIMITED}.
---
### 3.5 Seam 5: Rust FSM (Hidden in `rust_backend.so`) — The Core State Machine
**Location:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/_rust_kernel/` (built into `libdita_v2_kernel.so`)
**State Machine (Simplified):**
```
IDLE
──ENTER_intent──→ ORDER_REQUESTED
──venue.submit() succeeds (ACKED)──→ ENTRY_WORKING
──FILL event──→ OPEN
──REJECTED event──→ IDLE (rollback)
──INDETERMINATE (VenuePostAckError)──→ STAYS ENTRY_WORKING (no rollback)
──venue.submit() fails (PRE-ACK)──→ synthetic REJECTED → IDLE
──EXIT_intent──→ ORDER_REQUESTED (exit)
──venue.submit() succeeds──→ EXIT_WORKING
──FILL event──→ IDLE
──REJECTED event──→ IDLE
──INDETERMINATE──→ STAYS EXIT_WORKING
```
**Rust FSM Responsibility:**
- Manages `TradeSlot.fsm_state` (IDLE, ORDER_REQUESTED, ENTRY_WORKING, OPEN, EXIT_WORKING, etc.)
- Transitions are deterministic given event `(kind, status)`
- **Does NOT know about INDETERMINATE** — it only sees events the Python layer emits
- The Python layer is the **semantic firewall**: it decides whether to emit a `REJECTED` event or suppress it
---
### 3.6 Seam 6: `on_venue_event()` — The Feedback Loop
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/rust_backend.py`
**Method:** `on_venue_event()` (line ~1200)
```python
def on_venue_event(self, event: VenueEvent) -> KernelOutcome:
# All venue events flow here → Rust FSM → state transitions
# If event.status == REJECTED:
# kernel.fsm_state → IDLE (flat)
# If event.status == FILLED/PARTIAL_FILL:
# kernel.fsm_state → OPEN / IDLE
```
**Critical Invariant:** The kernel **only rolls back to IDLE on explicit `REJECTED` events**. It has no concept of "unknown".
---
## 4. ENUMERATED UNDESIRABLE STATES & TRANSITIONS
### 4.1 States That SHOULD Exist But Don't
| Missing State | Description | Why It Matters |
|---------------|-------------|----------------|
| `ENTRY_INDETERMINATE` | Order sent, answer lost — may be live | Distinguishes "we don't know" from "working normally" |
| `EXIT_INDETERMINATE` | Exit sent, answer lost — may be live | Same for exits |
| `ORDER_TRUTH_UNKNOWN` | Lookup failed after N attempts | Explicit "truth unknown" state vs silent failure |
| `VENUE_LOOKUP_PENDING` | Background reconcile in progress | Prevents duplicate reconcile attempts |
| `RECONCILE_IN_PROGRESS` | Full reconcile running | Prevents concurrent reconciles |
### 4.2 Transitions That Should Not Exist (But Do)
| Transition | Why It's Bad | Where It Happens |
|------------|--------------|------------------|
| `ENTRY_WORKING` → `IDLE` on `INDETERMINATE` | Orphans live position | `bingx_direct` pre-2026-07-13 (FIXED) |
| `EXIT_WORKING` → `IDLE` on `INDETERMINATE` | Orphans live position, no SL/TP | Same as above |
| `ENTRY_WORKING` → `IDLE` on `VenuePostAckError` (broad) | Over-broad catch rolls back live orders | `rust_backend.py` line ~900 (MITIGATED: catches `VenuePostAckError` specifically) |
| `ORDER_REQUESTED` → `IDLE` without event | Silent state loss | Never directly, but possible via exception path |
### 4.3 States That Should Be Unreachable But Are Reachable
| State | How It's Reached | Why It's Bad |
|-------|------------------|--------------|
| `SLOT_BUSY` with no active order | FSM advanced but order never sent | `process_intent` exception before `venue.submit` |
| `ENTRY_WORKING` with `active_entry_order = None` | `attach_entry_order` not called | `venue.submit` exception before `attach_entry_order` |
| `OPEN` with `position = None` | Phantom fill event | E-feed race, duplicate fill |
| `IDLE` with `trade_id` set | Incomplete cleanup | `reset()` not called on all fields |
### 4.4 Foreseeable "Phantom" States (Unforeseeable in Practice)
| Scenario | Resulting State | Detection |
|----------|-----------------|-----------|
| BingX accepts order, sends ACK, TCP dies before ACK arrives | Venue: LIVE / Kernel: ENTRY_WORKING (no ACK event) | `_lookup_own_order_by_client_id` reconciliation |
| BingX fills order, sends FILL, WS dies before delivery | Venue: FILLED / Kernel: ENTRY_WORKING | E-feed FILL event / reconcile |
| BingX rejects order, sends REJECTED, TCP dies | Venue: REJECTED / Kernel: ENTRY_WORKING | Reconcile finds REJECTED |
| Network partition: client can't reach BingX, but BingX can fill | Venue: FILLED / Kernel: INDETERMINATE | Reconcile + E-feed |
| Duplicate `clientOrderId` sent (retries) → BingX returns original order | Venue: FILLED / Kernel: sees duplicate | `clientOrderId` deduplication in `_lookup` |
---
## 5. MITIGATIONS — IMPLEMENTED, PARTIAL, PROPOSED
### 5.1 Implemented (Post-2026-07-13)
| Mitigation | Location | What It Prevents |
|------------|----------|------------------|
| Three-way HTTP classification | `bingx_direct.py` lines ~950-1020 | Misclassification of INDETERMINATE as REJECTED |
| `_lookup_own_order_by_client_id()` | `bingx_direct.py` lines ~1030-1100 | Reconciling truth from venue after INDETERMINATE |
| `VenueIndeterminateError` exception | `bingx_venue.py` line ~859/937 | Semantic escalation: INDETERMINATE ≠ REJECTED |
| `VenuePostAckError` catch (no rollback) | `rust_backend.py` lines ~900, ~1080 | Kernel no-rollback fence for INDETERMINATE |
| `VenueIndeterminateError` inherits `VenuePostAckError` | `venue.py:43` | Existing no-rollback fence catches it |
| `_lookup_own_order_by_client_id` bounded retries | `bingx_direct.py` line ~1070 | Bounded reconciliation, then gives up with `None` |
| `VenuePostAckError` catch in async path too | `rust_backend.py` line ~1080 | Async path same protection as sync |
| No synthetic event on INDETERMINATE | `rust_backend.py` lines ~910, ~1090 | No synthetic REJECT pollutes event log |
### 5.2 Partial / Incomplete
| Gap | Location | Risk |
|-----|----------|------|
| No `ENTRY_INDETERMINATE` / `EXIT_INDETERMINATE` FSM states | Rust FSM | Can't distinguish "working normally" from "truth unknown" in state |
| No `ORDER_TRUTH_UNKNOWN` explicit state | `bingx_direct._lookup_own_order` | Returns `None` but no state reflects this |
| No background reconcile task for INDETERMINATE orders | `bingx_direct._s2_tasks` | Only MARKET fills trigger background refresh |
| No metrics/alerting on INDETERMINATE frequency | None | Can't detect systemic issues |
| No alert on `VenueIndeterminateError` rate | Logging only | Operators don't know it's happening |
### 5.3 Proposed (For Future Hardening)
| Proposal | Effort | Impact |
|----------|--------|--------|
| Add `ENTRY_INDETERMINATE` / `EXIT_INDETERMINATE` FSM states | Rust FSM + Python bindings | Explicit "truth unknown" state visible in TUI/monitoring |
| Add `OrderTruth` enum: `KNOWN_LIVE`, `KNOWN_FLAT`, `UNKNOWN` | `bingx_direct` + kernel | Explicit truth tracking per order |
| Background reconcile task for INDETERMINATE orders | New async task in `DolphinLiveTrader` | Proactive truth discovery |
| `INDETERMINATE` metric + alert | Prometheus/Grafana | Operator visibility |
| `OrderTruth` persisted in Zinc/CH | Zinc plane + CH writer | Survives restarts |
| `ReconcileManager` service | New module | Centralized truth discovery |
| `ORDER_TRUTH_UNKNOWN` event type | Contracts + kernel | Explicit event for "we don't know" |
---
## 6. ENUMERATED INSTANCES IN CODEBASE
### 6.1 Where INDETERMINATE Classification Happens
| File | Line | Function | Purpose |
|------|------|----------|---------|
| `prod/bingx/http.py` | ~50 | `BingxHttpError.effect` | Classification property |
| `prod/bingx/http.py` | ~60 | `_effect_of_httpx_error()` | Maps `httpx` exceptions |
| `prod/bingx/http.py` | ~80 | `_effect_of_status()` | Maps HTTP status codes |
| `prod/clean_arch/adapters/bingx_direct.py` | ~950 | `submit_intent()` exception handler | Main classification |
| `prod/clean_arch/adapters/bingx_direct.py` | ~1030 | `_lookup_own_order_by_client_id()` | Truth reconciliation |
| `prod/clean_arch/dita_v2/bingx_venue.py` | ~859 | `submit()` INDETERMINATE check | Sync venue |
| `prod/clean_arch/dita_v2/bingx_venue.py` | ~937 | `submit_async()` INDETERMINATE check | Async venue |
### 6.2 Where INDETERMINATE Is Caught / Escalated
| File | Line | Context |
|------|------|---------|
| `prod/clean_arch/dita_v2/bingx_venue.py` | ~859 | `raise VenueIndeterminateError(...)` (sync) |
| `prod/clean_arch/dita_v2/bingx_venue.py` | ~937 | `raise VenueIndeterminateError(...)` (async) |
| `prod/clean_arch/dita_v2/rust_backend.py` | ~900 | `except VenuePostAckError:` (sync) |
| `prod/clean_arch/dita_v2/rust_backend.py` | ~1080 | `except VenuePostAckError:` (async) |
| `prod/clean_arch/dita_v2/venue.py` | 43 | `class VenueIndeterminateError(VenuePostAckError)` |
### 6.3 Where No-Rollback Fence Exists
| File | Line | Protection |
|------|------|------------|
| `prod/clean_arch/dita_v2/rust_backend.py` | ~900 | `except VenuePostAckError:` in sync `process_intent` |
| `prod/clean_arch/dita_v2/rust_backend.py` | ~1080 | `except VenuePostAckError:` in async `process_intent_async` |
### 6.4 Where Synthetic REJECTED Is Emitted (PRE-ACK Only)
| File | Line | Context |
|------|------|---------|
| `prod/clean_arch/dita_v2/rust_backend.py` | ~920 | Sync: `synthetic_REJECTED_event` on generic `Exception` |
| `prod/clean_arch/dita_v2/rust_backend.py` | ~1110 | Async: `synthetic_REJECTED_event` on generic `Exception` |
---
## 7. TEST VERIFICATION MATRIX
**File:** `/mnt/dolphinng5_predict/prod/clean_arch/dita_v2/test_indeterminate_submit.py`
| Test | What It Verifies |
|------|------------------|
| `test_read_timeout_is_indeterminate_not_rejection` | `ReadTimeout` → INDETERMINATE |
| `test_connect_failures_are_not_attempted` | `ConnectError`/`ConnectTimeout` → NOT_ATTEMPTED |
| `test_5xx_is_indeterminate_and_4xx_is_refused` | 5xx → INDETERMINATE, 4xx → REFUSED |
| `test_unknown_failure_defaults_to_indeterminate` | Unknown error → INDETERMINATE (conservative) |
| `test_lookup_finds_the_order_and_never_posts` | Reconcile finds live order, never re-POSTs |
| `test_lookup_reports_absent_when_venue_has_no_such_order` | Venue says "no order" → ABSENT (rollback sound) |
| `test_lookup_treats_venue_refusal_as_absent` | Venue REFUSES lookup → ABSENT |
| `test_lookup_returns_none_when_truth_cannot_be_established` | Timeout on lookup → None (not ABSENT) |
| `test_sync_venue_escalates_indeterminate_receipt` | `VenueIndeterminateError` raised |
| `test_async_venue_escalates_indeterminate_receipt` | Async path same |
| `test_indeterminate_is_a_post_ack_error_so_existing_fences_catch_it` | Subclass check |
| `test_kernel_does_not_roll_back_slot_on_indeterminate_submit` | Sync kernel no-rollback |
| `test_kernel_does_not_roll_back_slot_on_indeterminate_submit_async` | Async kernel no-rollback |
| `test_genuine_rejection_still_rolls_the_slot_back` | REFUSED still rolls back (contrast test) |
---
## 8. DECISION LOG (Appendix)
| Date | Decision | Rationale |
|------|----------|-----------|
| 2026-07-13 | Classify `ReadTimeout`/`5xx` as INDETERMINATE | Request sent, answer lost — order may be live |
| 2026-07-13 | Default unknown errors to INDETERMINATE | Conservative: never guess "rejected" |
| 2026-07-13 | `_lookup_own_order_by_client_id` bounded retries | Prevent infinite retry loops |
| 2026-07-13 | `VenueIndeterminateError` inherits `VenuePostAckError` | Reuses existing no-rollback fence |
| 2026-07-13 | NO synthetic event on INDETERMINATE | Don't pollute event log with guesses |
| 2026-07-13 | `VenuePostAckError` catch in BOTH sync/async | Both code paths protected |
---
## 9. OPERATOR RUNBOOK: INDETERMINATE INCIDENT
**If you see `VenueIndeterminateError` in logs:**
1. **Do NOT manually flatten the slot** — the order may be live
2. Check BingX VST/LIVE for the `clientOrderId` (logged in the error)
3. If position exists at venue: let E-feed FILL settle it, or manually reconcile
4. If position NOT at venue: kernel will eventually reconcile or you can trigger manual reconcile
5. **Do NOT restart the kernel to "clear" the slot** — this loses the `ENTRY_WORKING`/`EXIT_WORKING` state
**Log Signature:**
```
CRITICAL FIX(bingx_direct): submit outcome INDETERMINATE (ReadTimeout("..."))
symbol=TRXUSDT clientOrderId=p-e-1q3k7m-ab4c — order MAY be live. Looking it up;
NOT assuming rejection.
```
**Resolution Paths:**
- `found == dict`: Order is LIVE → kernel adopts venue truth, slot stays OPEN/WORKING
- `found == "ABSENT"`: Order never existed → rollback IS sound, slot → IDLE
- `found == None`: Truth unknown → slot stays in ENTRY_WORKING/EXIT_WORKING, E-feed FILL settles it
---
## 10. FUTURE WORK PRIORITY ORDER
| Priority | Item | Effort | Blocker |
|----------|------|--------|---------|
| P0 | `ENTRY_INDETERMINATE` / `EXIT_INDETERMINATE` FSM states | Medium | Rust FSM changes + Python bindings |
| P0 | Background reconcile task for INDETERMINATE orders | Low | New async task in trader loop |
| P1 | `OrderTruth` enum + Zinc persistence | Medium | Zinc plane schema change |
| P1 | INDETERMINATE metrics + Grafana alert | Low | Prometheus exporter |
| P2 | `ReconcileManager` service | High | New module, cross-cutting |
| P2 | `ORDER_TRUTH_UNKNOWN` event type | Medium | Contracts + kernel + venue adapters |
---
## 11. FABLE REVIEW (2026-07-13) — verdict + MISSING SEAMS (fleet work)
**Truth audit:** Doc is faithful to the shipped fix (verified: `_is_rate_limited_error`
exists at `bingx_direct.py:127`; test matrix §7 matches the real 14-test suite;
FSM description matches kernel-invariant tests). Two factual errors corrected
in-place: `VenueIndeterminateError` lives at `venue.py:43` (not contracts.py);
fix date is 2026-07-13 (commits `bdc54fb` canonical / `54cd5d5` vendored).
**Scope gap — THE assignment ("extrapolate the class across the codebase") is
unfinished.** §3 covers the SUBMIT seam deeply; the same not-done ≠ tried-and-
failed ≠ unknown triage is UNAUDITED at every other venue-side-effect seam:
| # | Seam | Specific risk |
|---|------|---------------|
| M1 | `cancel()` / `cancel_async()` | cancel-INDETERMINATE = order MAY still be live → blind re-place = double fill. (Encoded in SPEC_UV_SMART_EXEC_MM §5; kernel path unaudited.) |
| M2 | leverage SET (`bingx_direct`) | On the money path (seen live 17:20 today). SET-INDETERMINATE → position sized at unknown leverage. |
| M3 | promotion bridge receipts (`uv.promotion`) | Bridge assumes receipt is truth; INDETERMINATE mid-bridge unhandled? |
| M4 | E-feed / EKBridge gap handling | WS death between ACK and FILL — partially covered by rehydrate; no triage taxonomy. |
| M5 | exit-leg placement (multi-leg exits) | Leg 1 fills, leg 2 INDETERMINATE → partial exit with unknown remainder. |
| M6 | ch_writer / journal lane | Covered for data (spool), but write-INDETERMINATE→retry = possible duplicate rows (dedup ratchet history). |
| M7 | every `except: pass` / broad `except Exception` after venue I/O | Grep-sweep + classify each; the b46ebd2 class hid here. |
**DOCTRINE GUARDRAIL on §10 P0 "background reconcile task":** operator standing
order — reconcile is proto-PINK hell. Any background truth-discovery MUST stay
a bounded, read-only, own-clientOrderId point lookup (the §2.2 primitive), never
a scan-all-orders reconciler. Build the loop around the existing primitive.
**Disposition:** M1–M7 are fleet-sized (audit + tests per seam, same pattern as
`test_indeterminate_submit.py`). Corrections done in-place are complete.
---
**END OF DOCUMENT**
*This document is a living reference. Update on every INDETERMINATE-related change.*