|
| 1 | +# QA Report — Numeric Validation Audit |
| 2 | + |
| 3 | +> 본 보고서는 `docs/QA_WORKFLOW.md` Section 11 의 한국어 구조화 포맷에 |
| 4 | +> 따라 작성된다. 이전 사이클 (`qa/2026-05-05-v0.13.0-validation`) 의 |
| 5 | +> production 결함 3건 (NaN silent-pass / NaN propagation / silent empty |
| 6 | +> book) 이 같은 audit pass 에서 발견된 것을 확인하고, **사용자 가설 |
| 7 | +> "빙산의 일각"** 을 다른 모듈 전반에 검증한 follow-up. |
| 8 | +
|
| 9 | +## QA 결과 요약 |
| 10 | + |
| 11 | +- **브랜치:** `qa/2026-05-06-numeric-validation-audit` (origin push 완료) |
| 12 | +- **베이스 커밋:** `1d0e961` — `Merge pull request #13 from hypurrquant/qa/2026-05-05-v0.13.0-validation` |
| 13 | +- **베이스 버전:** `0.13.0` |
| 14 | +- **추가 커밋 수:** 3개 |
| 15 | + |
| 16 | +| # | 해시 | 제목 | |
| 17 | +|---|------|------| |
| 18 | +| 1 | `f381c0b` | `fix(validator): reject non-finite numeric inputs (Rule #2 audit)` | |
| 19 | +| 2 | `26d78d7` | `fix(lighter): reject non-finite numeric venue payloads (Rule #2 audit)` | |
| 20 | +| 3 | `ed533cb` | `fix(numeric-audit): observability + rebalance + outcome-time NaN guards` | |
| 21 | + |
| 22 | +PR URL 후보: |
| 23 | +`https://github.com/hypurrquant/perp-cli/pull/new/qa/2026-05-06-numeric-validation-audit` |
| 24 | + |
| 25 | +## 가설 검증 결과 |
| 26 | + |
| 27 | +이전 사이클 사용자 review 의 핵심 가설: |
| 28 | + |
| 29 | +> "production 버그 2건 = 빙산의 일각일 가능성. 같은 treatment 안 받은 |
| 30 | +> 다른 모듈에도 같은 패턴의 버그가 있을 가능성이 높음." |
| 31 | +
|
| 32 | +이번 audit 가 **18개의 추가 사이트** 에서 동일 패턴의 NaN silent-pass / |
| 33 | +NaN propagation 결함을 확인. 가설 강하게 입증됨. |
| 34 | + |
| 35 | +| 모듈 | 결함 사이트 | Fix commit | |
| 36 | +|------|----------|-----------| |
| 37 | +| `trade-validator.ts` | 5 (markPrice / balance / orderbook px·sz / posSize / fundingRate) | `f381c0b` | |
| 38 | +| `exchanges/lighter.ts` | 9 (balance + position aggregation 9곳) | `26d78d7` | |
| 39 | +| `event-stream.ts` | 2 (liquidation distance / balance delta) | `ed533cb` | |
| 40 | +| `rebalance.ts` | 1 (planner input) | `ed533cb` | |
| 41 | +| `exchanges/hyperliquid-outcome.ts:439` | 1 (l2Book time, 이전 사이클 leftover) | `ed533cb` | |
| 42 | + |
| 43 | +**이전 사이클 (3건) + 이번 사이클 (18건) = 합계 21건** 의 같은 클래스 |
| 44 | +Rule #2 위반이 **2 audit pass** 에서 발견됨. |
| 45 | + |
| 46 | +## 환경 / Pre-flight (Section 4) |
| 47 | + |
| 48 | +- **호스트:** macOS (Darwin 25.4.0), pnpm 10, Node 20·22·24 매트릭스 |
| 49 | +- **컨테이너:** `perp-qa` Docker, `~/.ows`·`~/.perp` 마운트 |
| 50 | +- working tree clean, base `1d0e961` = origin/main HEAD |
| 51 | +- mainnet 거래 실행 0건 (audit 자체가 readonly + 단위 테스트 영역) |
| 52 | + |
| 53 | +## 실행 내역 |
| 54 | + |
| 55 | +### 적용된 audit 패턴 |
| 56 | + |
| 57 | +이전 사이클의 `_computeUnderlying` / `_computeMidSum` / |
| 58 | +`_assertOutcomeRange` 패턴 follow-through: |
| 59 | + |
| 60 | +1. **Helper 추출 + 단위 테스트** (가능한 곳) — 같은 가드 로직을 한 곳에 |
| 61 | + 두고 테스트로 freeze. |
| 62 | +2. **Inline 가드 + 명시 throw** (helper 추출이 어려운 곳, e.g. event |
| 63 | + stream 에서 venue 응답이 일회용으로 해석됨). |
| 64 | +3. **에러 메시지에 field name + 원본 value** 포함 — triage 시 어느 venue |
| 65 | + endpoint 의 어떤 field 가 깨졌는지 즉시 식별. |
| 66 | + |
| 67 | +### 모듈별 변경 |
| 68 | + |
| 69 | +#### `trade-validator.ts` (`f381c0b`, 5 sites + 6 tests) |
| 70 | + |
| 71 | +| Site | 이전 동작 | 변경 후 | |
| 72 | +|------|---------|---------| |
| 73 | +| `markPrice` | `<= 0` 검사가 NaN 통과시킴 | `Number.isNaN` / Infinity pre-check throw | |
| 74 | +| `balance.available` | NaN 면 "insufficient" branch 거짓 진입 + `$NaN available` 노출 | throw EXCHANGE_ERROR | |
| 75 | +| 주문북 level `px`·`sz` | NaN 누적 시 `availableLiquidity` NaN, `>= notional` 항상 false | level 단위 finite + positive 검사 throw | |
| 76 | +| reduce-only `pos.size` | NaN 면 size 비교 거짓 통과 | parseFloat 후 finite 검사 throw | |
| 77 | +| `marketInfo.fundingRate` (output) | envelope 에 `NaN` JSON 노출 | 0 substitute + warning push (output sanitization) | |
| 78 | + |
| 79 | +#### `exchanges/lighter.ts` (`26d78d7`, helper + 9 sites + 9 tests) |
| 80 | + |
| 81 | +`LighterAdapter._toFiniteNumber(value, fieldName, defaultValue=0)` 추출: |
| 82 | +- undefined / null → defaultValue (venue 가 빈 계정 / position 에서 omit 가능) |
| 83 | +- finite number / parseable string → 그대로 |
| 84 | +- NaN / ±Infinity / 비숫자 string → throw EXCHANGE_ERROR (field name 포함) |
| 85 | + |
| 86 | +Sites: |
| 87 | +- `total_asset_value`, `available_balance`, `collateral`, `position.unrealized_pnl` (balance) |
| 88 | +- `position`, `posSize`, `position_value` (positions, markPrice/leverage 계산) |
| 89 | + |
| 90 | +#### `event-stream.ts` (`ed533cb`, 2 sites) |
| 91 | + |
| 92 | +- liquidation distance (`mark`/`liq` NaN → critical alert silent skip) |
| 93 | +- balance delta (NaN → balance_update emit 누락) |
| 94 | + |
| 95 | +둘 다 `console.warn` + 분기 가드로 변경. observability 영역이라 |
| 96 | +throw 가 stream 흐름 깨므로 silent → noisy 로 격상 (warn 로그가 사용자 |
| 97 | +디버깅 단서). |
| 98 | + |
| 99 | +#### `rebalance.ts` (`ed533cb`, 1 site) |
| 100 | + |
| 101 | +`Number(bal.equity)` 등 NaN propagation → `Promise.allSettled` 의 reject |
| 102 | +경로 유도 (어댑터 단위로 plan 에서 제외, 다른 어댑터 영향 없음). |
| 103 | + |
| 104 | +#### `exchanges/hyperliquid-outcome.ts:439` (`ed533cb`, 1 site) |
| 105 | + |
| 106 | +이전 사이클 v0.13.0 의 `getOrderbook` 가드 강화에서 leftover. 이제 |
| 107 | +`Number(book.time ?? 0)` 도 nullish 와 NaN 분리: |
| 108 | +- nullish → 0 (legit "time 정보 없음") |
| 109 | +- non-finite → throw (corrupt) |
| 110 | + |
| 111 | +## 테스트 결과 |
| 112 | + |
| 113 | +- **passed: 1396 / failed: 0 / added: 15** (host + container cross-validate) |
| 114 | +- **이전 (베이스 = main HEAD `1d0e961`):** 1381 / 74 files |
| 115 | +- **이후 (QA 브랜치 HEAD = `ed533cb`):** 1396 / 75 files |
| 116 | +- **신규 test files (1):** `exchanges/lighter-toFinite.test.ts` |
| 117 | +- **확장된 test files (1):** `trade-validator.test.ts` (38 → 44) |
| 118 | +- **새 helper 단위 테스트:** 9 case (`_toFiniteNumber`) |
| 119 | +- **NaN edge case 테스트 추가:** trade-validator 의 5 production fix 마다 1 case |
| 120 | + |
| 121 | +## 변경된 공개 인터페이스 |
| 122 | + |
| 123 | +- **CLI / JSON envelope: 변경 없음.** 정상 동작 시 출력 동일. |
| 124 | +- **에러 행동 변경:** NaN/Infinity venue 응답 시 이제 명시 EXCHANGE_ERROR. |
| 125 | + - 이전: silent 0 substitution → "$0 balance" / "Insufficient liquidity: $NaN" / "no alert" 같은 false-positive 분기 |
| 126 | + - 이후: throw with field name + value (envelope `error` 필드로 surface) |
| 127 | +- **Internal 신규 helpers (모두 underscore prefix, 기존 컨벤션):** |
| 128 | + - `LighterAdapter._toFiniteNumber` (public static, 단위 테스트 가능) |
| 129 | + |
| 130 | +## 테스트 작성 중 발견된 production 결함 |
| 131 | + |
| 132 | +이전 사이클은 helper-extract pattern 으로 3건 발견. 이번 audit pass 는 |
| 133 | +같은 pattern 으로 **18건의 추가 결함**. 모두 NaN propagation |
| 134 | +(silent-pass, false-positive 분기, envelope NaN 노출) 의 동일 클래스. |
| 135 | + |
| 136 | +| 분류 | 건수 | 영향 | |
| 137 | +|------|------|------| |
| 138 | +| 자금 계산 (balance / position / margin) | 13 | "$0" 으로 corrupt 가림, false 충분 잔고 판정, plan 입력 NaN | |
| 139 | +| 시세 / 주문북 (markPrice / level px·sz / time) | 4 | NaN 비교 false → silent skip | |
| 140 | +| Output sanitization (envelope output) | 1 | agent JSON 파싱 깨짐 | |
| 141 | + |
| 142 | +이전 사이클의 가설 — "같은 treatment 안 받은 모듈에 같은 패턴 더 있음" |
| 143 | +— 이 audit 가 18건으로 **6배 강화**된 형태로 검증. |
| 144 | + |
| 145 | +## 사람 검토 필요 항목 |
| 146 | + |
| 147 | +1. **`Number.isNaN` vs `!Number.isFinite` 사용** — `markPrice` 가드는 |
| 148 | + `Number.isNaN(x) || x === Infinity || x === -Infinity` 명시. 다른 |
| 149 | + 사이트는 `!Number.isFinite(x)`. 동등하지만 가독성 차이. 통일 |
| 150 | + 권장 (`!Number.isFinite` 가 더 짧음). |
| 151 | +2. **mcp-server.ts 의 advisor numeric output sanitization** — 이번 |
| 152 | + 사이클 범위에서 분리. portfolio aggregation 의 `Number(snap.balance.equity)` 등 |
| 153 | + 가드 부재 영역 follow-up. envelope 영향 작지만 lighter.ts cascade |
| 154 | + 덕에 부분 보호. 별도 micro-PR 가치. |
| 155 | +3. **다른 어댑터 (`pacifica.ts` / `hyperliquid.ts` / `aster.ts`) 의 |
| 156 | + 동일 audit** — `cross-adapter-matrix` 사이클 (P1) 의 일부로 |
| 157 | + 처리 권장. lighter.ts 가 가장 많은 silent fallback 을 가졌지만 |
| 158 | + 다른 어댑터도 부분 패턴 가능. |
| 159 | +4. **event-stream / rebalance 의 단위 테스트 부재** — stream/aggregation |
| 160 | + shape 라 단위 테스트 비용 큼. integration test 인프라 (mock signer |
| 161 | + matrix 사이클) 와 함께 생기면 추가 가치. |
| 162 | + |
| 163 | +## 다음 권장 액션 |
| 164 | + |
| 165 | +### 즉시 (이번 PR) |
| 166 | + |
| 167 | +- [ ] **PR 생성** — `qa/2026-05-06-numeric-validation-audit` → `main`. |
| 168 | + 3 commits, +18 production fixes, +15 tests. |
| 169 | + |
| 170 | +### Follow-up micro-PRs |
| 171 | + |
| 172 | +- [ ] **mcp-server.ts numeric output sanitization** — portfolio aggregation |
| 173 | + `Number(...)` 사이트들 (`extractNumber || "<size>"` 패턴 + balance |
| 174 | + reduce). envelope NaN 노출 차단. |
| 175 | +- [ ] **`pacifica.ts` / `hyperliquid.ts` / `aster.ts` 동일 audit** — |
| 176 | + 이번 사이클의 cross-adapter follow-up. cross-adapter-matrix 와 |
| 177 | + 병합 가능. |
| 178 | +- [ ] **`Number.isFinite` 통일** — 사람 검토 #1 의 가독성 통일. |
| 179 | +- [ ] **`@vitest/coverage-v8` dep 추가 (사용자 승인 필요)** — 정량 |
| 180 | + coverage. 이번 audit 가 18건 추가했는데 0% → 미커버 모듈 식별. |
| 181 | +- [ ] **`fast-check` property test (사용자 승인 필요)** — `--json` numeric |
| 182 | + 필드 finite 강제. 1000+ 자동 case 로 audit 패턴 영구화. |
| 183 | + |
| 184 | +### 다른 사이클로 이미 분리된 항목 (이전 보고서 plan 그대로) |
| 185 | + |
| 186 | +- `qa/2026-05-XX-aster-signer-regression` (P0 격상) |
| 187 | +- `commander program-builder factory` (P1) |
| 188 | +- `qa/2026-05-XX-cross-adapter-matrix` (P1, mcp-server / 다른 어댑터 audit 묶기 가능) |
| 189 | +- `qa/2026-05-XX-failure-modes` (P2) |
| 190 | + |
| 191 | +## Section 3 / Section 13 — 절대 금지 항목 준수 |
| 192 | + |
| 193 | +| 항목 | 수행 여부 | |
| 194 | +|------|----------| |
| 195 | +| `main` 머지 / push | ✗ | |
| 196 | +| `npm publish` | ✗ | |
| 197 | +| `git tag` | ✗ | |
| 198 | +| GitHub Release 생성 | ✗ | |
| 199 | +| mainnet 실거래 | ✗ | |
| 200 | +| 의존성 메이저 업데이트 | ✗ | |
| 201 | +| 새 npm 패키지 추가 | ✗ | |
| 202 | + |
| 203 | +## 부록 A — 가설 강화 데이터 |
| 204 | + |
| 205 | +| 사이클 | audit 통과 모듈 | 발견 production 결함 | |
| 206 | +|--------|--------------|------------------| |
| 207 | +| v0.13.0 (`qa/2026-05-05`) | `hyperliquid-outcome.ts` (helper 5개) | 3 | |
| 208 | +| Numeric audit (`qa/2026-05-06`, 본 보고서) | `trade-validator` / `lighter` / `event-stream` / `rebalance` / `outcome-time` | 18 | |
| 209 | +| **합계** | 5 영역 | **21** | |
| 210 | + |
| 211 | +이 시점에서 **다음 사이클이 같은 패턴으로 더 발견할 결함의 lower bound** |
| 212 | +는 0 이 아님. 추가 audit 사이클의 가성비는 여전히 높음 — 본 보고서의 |
| 213 | +follow-up plan 이 그 우선순위 정렬. |
| 214 | + |
| 215 | +## 부록 B — audit 검색 명령 |
| 216 | + |
| 217 | +다음 사이클이 동일 패턴 적용 시 사용: |
| 218 | + |
| 219 | +```bash |
| 220 | +# 1. Number()/parseFloat()/parseInt() 후 isFinite 가드 부재 사이트 |
| 221 | +grep -rnE '(Number\(|parseFloat\(|parseInt\()' src --include='*.ts' \ |
| 222 | + --exclude-dir=__tests__ --exclude-dir=dist \ |
| 223 | + | grep -vE '(Number\.is(Finite|Integer)|\.\s*toString)' |
| 224 | + |
| 225 | +# 2. `|| 0` 또는 `?? 0` 같은 numeric default fallback |
| 226 | +grep -rnE '\?\? \d|\|\| \d' src/exchanges src/strategies src/arb \ |
| 227 | + --include='*.ts' |
| 228 | +``` |
0 commit comments