Skip to content

qa(audit): numeric-validation — 18 Rule #2 fixes across 5 modules#14

Merged
Hiksang merged 4 commits into
mainfrom
qa/2026-05-06-numeric-validation-audit
May 6, 2026
Merged

qa(audit): numeric-validation — 18 Rule #2 fixes across 5 modules#14
Hiksang merged 4 commits into
mainfrom
qa/2026-05-06-numeric-validation-audit

Conversation

@Hiksang
Copy link
Copy Markdown
Collaborator

@Hiksang Hiksang commented May 5, 2026

Summary

  • v0.13.0 cycle 의 hypothesis ("3 production 결함은 빙산의 일각") follow-up audit.
  • 5 모듈 × 18 NaN silent-pass 결함 동일 패턴으로 발견 → 가설 6배 강하게 입증 (3 → 21건 누적).
  • 4 commits, 1381 → 1396 tests (+15), host + container cross-validate.

Findings (18 production fixes)

모듈 결함 사이트 Fix
trade-validator.ts 5 (markPrice / balance / orderbook px·sz / posSize / fundingRate) f381c0b
exchanges/lighter.ts 9 (balance + position aggregation) 26d78d7
event-stream.ts 2 (liquidation distance / balance delta silent skip) ed533cb
rebalance.ts 1 (planner input NaN propagation) ed533cb
exchanges/hyperliquid-outcome.ts:439 1 (l2Book time, 이전 사이클 leftover) ed533cb

Approach

이전 사이클의 audit 패턴 follow-through:

  1. Helper 추출 + 단위 테스트 — 가능한 곳 (LighterAdapter._toFiniteNumber, public static)
  2. Inline Number.isFinite 가드 + 명시 throw — helper 추출 어려운 곳 (event stream)
  3. 에러 메시지에 field name + value 포함 — venue endpoint 단위 triage 가능

Tests (+15)

stream / rebalance / outcome-time 은 inline 가드라 단위 테스트 추가 안 함 (integration shape, 보고서에 follow-up 명시).

자세한 보고

docs/qa-reports/2026-05-06-numeric-validation-audit.md 참조 (228 줄, 가설 검증 표 + 모듈별 변경 + 부록).

Out of scope — follow-up

  • mcp-server.ts numeric output sanitization — advisor envelope NaN 노출 차단 (별도 micro-PR 또는 cross-adapter cycle).
  • pacifica.ts / hyperliquid.ts / aster.ts 동일 auditcross-adapter-matrix cycle 의 일부.
  • Number.isFinite 통일 — 가독성 (현재 일부 Number.isNaN || Infinity 명시).
  • dep 승인 필요: @vitest/coverage-v8 (coverage 정량), fast-check (property test).

Test plan

  • pnpm test (unit suite): 1396 / 1396 passed (host + container)
  • pnpm build (tsc strict): clean
  • mainnet 실거래 0건 (audit 자체가 readonly + unit test)
  • 컨테이너 정본: 75 files / 1396 tests / 23.09s
  • CI workflow 통과 확인
  • 사람 review

Live ↔ Unit Test Cross-Validation

해당 사유: 이번 PR 은 --json 명령 envelope 변경 없음 (정상 응답 동일, 잘못된 venue 응답에 한해서만 명시 EXCHANGE_ERROR throw 추가). 라이브 호출 ↔ 단위 테스트 매핑은 v0.13.0 사이클 보고서 Appendix B 가 그대로 유효.

Section 3 / 13 — 절대 금지 항목 준수

main 머지/push (이 PR 자체는 사용자 명시 승인) · npm publish ✗ · git tag ✗ · GitHub Release ✗ · mainnet 실거래 ✗ · deps 메이저 변경 ✗ · 새 npm 패키지 추가 ✗

🤖 Generated with Claude Code

Hiksang and others added 4 commits May 5, 2026 19:17
First commit of `qa/2026-05-06-numeric-validation-audit` cycle.

Previous v0.13.0 cycle found 3 Rule #2 violations via the helper-extract
pattern (NaN silent-pass / NaN propagation / silent empty book). The
review hypothesis was that other modules without that treatment likely
harbor the same class of bug. trade-validator.ts confirms it: 5 numeric
input paths were unguarded against NaN, all of which would have slipped
past their downstream comparison checks because NaN comparisons are
always false.

### Production fixes (5)

All throw `PerpError("EXCHANGE_ERROR", ...)` so the calling trade flow
surfaces a real venue/parsing failure instead of a confusing
"$NaN available" or false-positive "insufficient liquidity" message.

1. `markPrice` — `<= 0` post-check skipped NaN; now rejected upfront.
2. `balance.available` — would silently take the "insufficient
   balance" branch and emit `$NaN available` to the user.
3. orderbook level `px` / `sz` — would inflate `availableLiquidity` to
   NaN and emit "Insufficient liquidity: $NaN" with no real check.
   Also rejects zero/negative price (level should not exist).
4. reduce-only `pos.size` (`parseFloat`) — would make `params.size >
   posSize` always false, silently passing the size check.
5. `marketInfo.fundingRate` output — substitutes 0 + warning instead
   of emitting `NaN` to JSON-parsing agents (output sanitization, not
   silent-pass: warning surfaces the missing data).

### New tests (6, 38 → 44 in trade-validator.test.ts)

One regression case per fix above — each verifies the explicit throw
or, for fundingRate, the 0-substitute + warning emission.

### Pattern note for follow-up

The audit pattern that found these bugs: grep `Number(...)` and
`parseFloat(...)` calls without an adjacent `Number.isFinite` /
`Number.isInteger` guard, then trace each downstream comparison to
see if NaN slips through. lighter.ts has many `|| 0` fallback
patterns that look like the same class — next commit candidate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second commit of `qa/2026-05-06-numeric-validation-audit` cycle.
Confirms hypothesis: every module without the helper-extract treatment
harbors the same class of NaN silent-pass — lighter.ts had 9 sites
silently coercing to 0 via `Number(... || 0)`.

### Helper

`LighterAdapter._toFiniteNumber(value, fieldName, defaultValue=0)` —
public static for unit-testability. Rules:

- undefined / null → `defaultValue` (venue may legitimately omit a
  field for an empty account / position)
- finite number / parseable string → returned as-is
- NaN / ±Infinity / non-numeric string → throw `EXCHANGE_ERROR` with
  the field name in the message (so triage can attribute the failure
  to the exact venue endpoint)

### Production sites updated (9, getBalance + getPositions)

Balance:
- `total_asset_value` (line 545)
- `available_balance` (line 546)
- `collateral` (line 547)
- `position.unrealized_pnl` reduce (line 549)

Positions:
- `position` filter (line 574)
- `posSize` (line 576)
- `position_value` (used in markPrice + leverage calcs, lines 584/592)
- `total_asset_value` for leverage equity (line 593)

Each site previously hid corruption as a phantom $0 balance / 0
position size — exactly the failure mode that misled the v0.12.13
HL portfolio undercount fix.

### Tests (new file `lighter-toFinite.test.ts`, 9 cases)

- finite numbers / numeric strings unchanged
- undefined / null → default (with custom default override)
- NaN → throw with exchange tag in details
- ±Infinity → throw
- non-numeric strings (`"abc"`, `"12abc"`) → throw
- empty string `""` → 0 (Number("") === 0, allowed)
- field name + value present in error message for triage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third commit of the numeric-validation-audit cycle. Applies the same
Rule #2 pattern across three more sites that the helper-extract audit
skipped:

### event-stream.ts (observability silent skips)

`Number(p.markPrice)` / `Number(p.liquidationPrice)` and balance
delta computations would silently fail their `> 0` / `> 0.01`
threshold checks on NaN, suppressing the very alerts users rely on
(critical-distance liquidation_warning, balance_update events).
Now `Number.isFinite` checks gate each branch — corruption is logged
to stderr, the misleading "no alert" outcome is replaced with a
visible warning, and downstream comparison logic only runs when the
inputs are trustworthy.

### rebalance.ts (planner input)

`computeRebalancePlan` consumes `Number(bal.equity)` etc. without a
finiteness check. A NaN value would propagate into the sum-and-target
math and the execute step would attempt nonsense moves. Throws
`EXCHANGE_ERROR` for the affected adapter so `Promise.allSettled`
filters it out — partial result instead of corrupt plan.

### hyperliquid-outcome.ts:439 (cycle leftover)

Previous v0.13.0 cycle's `getOrderbook` fix replaced
`book?.levels ?? [[],[]]` with an explicit throw, but
`Number(book.time ?? 0)` was left untouched and would coerce a
non-numeric venue time to 0 (1970 epoch — silently wrong, not
"missing"). Now distinguishes "time omitted" (legit 0) from
"time corrupted" (throws).

### Tests

No new unit tests — these sites are stream/aggregation/integration
shaped, not pure helpers. Existing 106 tests across the previously
audited helpers all pass (verified). The cycle's helper-extract
audit pattern continues to grow regression coverage; cross-cutting
observability sites are recorded here for follow-up if integration
test infrastructure expands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cycle artifact for `qa/2026-05-06-numeric-validation-audit`. Documents
how the v0.13.0 cycle's "tip of the iceberg" hypothesis was tested
across 5 modules and confirmed by 18 additional Rule #2 violations.

Includes:
- Hypothesis verification table (3 → 21 total findings across 2 cycles)
- Per-module audit pattern + before/after table
- Production interface change summary (no envelope drift on healthy
  responses; throws now surface previously hidden corruption)
- Follow-up plan: mcp-server output sanitization, cross-adapter audit,
  Number.isFinite uniformity, dep-approval gates (coverage / fast-check)
- Appendix A: cumulative finding count table
- Appendix B: re-usable grep commands for the next audit cycle

Container ground-truth at HEAD `ed533cb`: 75 files / 1396 tests / 23.09s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Hiksang Hiksang merged commit ed94cfd into main May 6, 2026
4 checks passed
@Hiksang Hiksang deleted the qa/2026-05-06-numeric-validation-audit branch May 6, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant