- QA-1 Backend API contract: APPROVE (minor type contract 2) - QA-2 Persona forget algorithm: APPROVE (math verified) - QA-3 Auth & security: REQUEST_CHANGES - email normalize missing / JWT revalidate / secret fallback / whitelist - QA-4 Frontend runtime: REQUEST_CHANGES - review double-submit race / subjects/[id] NaN / history error states / native alerts - QA-5 Prisma & transactions: REQUEST_CHANGES - reviews.submit snapshot outside tx / tag cascade data loss / iteration race - QA-6 Build & deploy: REQUEST_CHANGES - missing migrations, postinstall, lockfile, PM2 ecosystem Follow-up fix proposals saved to .claude/state/pending-fix-proposals.jsonl (18 tickets)
77 lines
3.1 KiB
Markdown
77 lines
3.1 KiB
Markdown
# QA-2 Report — Persona Forget Algorithm Verification
|
||
|
||
> Worker: Explore agent (ultrathink) · Lead review: 승인
|
||
|
||
## Summary
|
||
- **verdict**: APPROVE
|
||
- critical: 0, major: 0, minor: 2
|
||
|
||
## 상수 검증
|
||
| Constant | Expected | Actual | OK? |
|
||
|---|---|---|---|
|
||
| PERSONA_LAMBDA.senior | 0.1 | 0.1 | ✓ |
|
||
| PERSONA_LAMBDA.mid | 0.2 | 0.2 | ✓ |
|
||
| PERSONA_LAMBDA.junior | 0.4 | 0.4 | ✓ |
|
||
| PERSONA_LAMBDA.crammer | 0.6 | 0.6 | ✓ |
|
||
| INTENSITY_THRESHOLD.strict | 0.7 | 0.7 | ✓ |
|
||
| INTENSITY_THRESHOLD.moderate | 0.5 | 0.5 | ✓ |
|
||
| INTENSITY_THRESHOLD.relaxed | 0.35 | 0.35 | ✓ |
|
||
| DEFAULT_K | 4.0 | 4.0 | ✓ |
|
||
| DEFAULT_INITIAL_S0 | 0.3 | 0.3 | ✓ |
|
||
| MAX_INTERVAL_DAYS | 60 | 60 | ✓ |
|
||
|
||
## 수식 유도 검증
|
||
|
||
**목표**: σ(k·(S₀·e^(-λt) - D)) = P_threshold 를 t에 대해 풀기.
|
||
|
||
1. σ⁻¹ 적용: `k·(S₀·e^(-λt) - D) = logit(P_threshold)`
|
||
2. `S₀·e^(-λt) = D + logit(P_threshold)/k` (== target)
|
||
3. `e^(-λt) = target / S₀`
|
||
4. `t = -(1/λ)·ln(target / S₀)`
|
||
|
||
코드(`persona-forget.service.ts:131`):
|
||
```ts
|
||
const tDays = -(1 / lambda) * Math.log(target / s0);
|
||
```
|
||
**✓ 수식과 코드가 정확히 일치.**
|
||
|
||
## 경계 조건 검증
|
||
| Case | 예상 동작 | 코드 경로 | OK? |
|
||
|---|---|---|---|
|
||
| s0 ≤ 0 | 즉시 (1h) 스케줄 | 103-109 | ✓ |
|
||
| target ≤ 0 | 60일 cap | 114-120 | ✓ |
|
||
| target ≥ s0 | ln ≤ 0 → t ≤ 0 → 즉시 | 122-128 | ✓ |
|
||
| 0 < target < s0 | 정상 공식 | 131 | ✓ |
|
||
| t > 60 | `Math.min(tDays, MAX_INTERVAL_DAYS)` | 132 | ✓ |
|
||
| logit(0) / logit(1) | ε clamp | 192 | ✓ |
|
||
| predictP at t=0 | σ(k(s0 - D)) | 150-153 | ✓ |
|
||
|
||
모든 경계 조건에서 수학적 domain error 없음.
|
||
|
||
## updateS0 검증
|
||
| prev | result | formula | expected | OK? |
|
||
|---|---|---|---|---|
|
||
| 0.2 | correct | 0.2·0.5+0.6 | 0.70 | ✓ |
|
||
| 1.0 | correct | clamp(1.1) | 1.0 | ✓ |
|
||
| 0.8 | incorrect | 0.8·0.5 | 0.40 | ✓ |
|
||
| 0.4 | partial | 0.4·0.7+0.3 | 0.58 | ✓ |
|
||
| null | correct | 0.3·0.5+0.6 | 0.75 | ✓ (기본값 0.3에서 시작) |
|
||
|
||
## Findings
|
||
|
||
### [minor] 스펙 커버리지 — persona × intensity matrix 불완전
|
||
- **Location**: `backend/src/forget/persona-forget.service.spec.ts:40-130`
|
||
- **Evidence**: senior vs crammer 비교, 3개 intensity 개별 테스트는 있지만 4 persona × 3 intensity = 12 조합을 grid로 돌리는 테스트가 없음.
|
||
- **Impact**: 공식상 persona/intensity는 단순 lookup이라 회귀 가능성 낮음. 다만 CI 신뢰도 측면에서 보완 권장.
|
||
- **Suggested fix**: `describe.each([...])` 로 matrix 테스트 추가.
|
||
|
||
### [minor] study-logs / reviews 에서 `lastUpdatedAt: new Date()` 중복 생성
|
||
- **Location**: `backend/src/study-logs/study-logs.service.ts:96, 115`, `backend/src/reviews/reviews.service.ts:87, 106`
|
||
- **Evidence**: snapshot upsert 와 schedule() 호출이 각각 `new Date()`를 독립 생성. 수 ms 차이 발생 가능.
|
||
- **Impact**: 스케줄 시각이 수 ms 어긋남. 무시 가능 수준이지만 의미가 모호해짐.
|
||
- **Suggested fix**: `const now = new Date()` 를 함수 진입부에서 한 번만 생성.
|
||
|
||
## Verdict
|
||
|
||
**APPROVE**. 알고리즘의 수식·경계·consumer 호출 모두 정확. minor 2건만 follow-up 개선 권장.
|