# DS App AI 라우팅 계약 (S2 인터페이스 동결) — v0.1

S1(데이터 fixture)이 앱의 **데이터 모양**을 동결했듯, 이 문서 + `Sources/AI/`는 앱의 **AI 호출 모양**을 동결한다.
S3 는 `AIProvider` 프로토콜 + `MockAIProvider` 로 AI 흐름을 끝까지 그리고, S2 가 실 provider 를 뒤에서 채운다.

- **Frozen**: 2026-06-04 · **typecheck PASS** (swift 6 strict concurrency) · **router 스모크 PASS**
- **위치**: `Sources/AI/{AIProvider,AIRouter,MockAIProvider}.swift` + `Sources/AI/Providers/*.swift`
- **파일 경계**: 이 `AI/` 디렉토리 = **S2 소유**(앱 나머지 = S3). 같은 Xcode repo 디렉토리 단위 분담 → 충돌 0.

## 1. Provider 티어 (= 디바이스 역할 표와 1:1)

| `AIProviderID` | 노드 | 용도 | 구현 경로 |
|---|---|---|---|
| `.onDevice` | 맥북·아이폰 | 즉답·오프라인·프라이버시 | Apple **FoundationModels** (`SystemLanguageModel`/`LanguageModelSession`) |
| `.localMLX` | 맥미니 허브 | 무거운 로컬 생성 | Gemma 4 26B — llm-router `:8890` / MLX `:8801` (OpenAI 호환) |
| `.remoteDS` | GPU(원격) | **코퍼스 RAG** | `GET /search/ask?backend=` (CONTRACT.md §4) |
| `.specialized` | GPU | rerank·embed·**vision**·OCR | 특화 모델 통로(온디맨드) |

## 2. 태스크 → 티어 정책 (`AIRoutingPolicy.default`)

| `AITask` | 선호 체인 | 근거 |
|---|---|---|
| `quickSummarize` | onDevice → localMLX | 빠르고 사적 |
| `memoAssist` | onDevice → localMLX | 짧은 보조 |
| `askSelection` | onDevice → localMLX → remoteDS | 로컬 컨텍스트, 부족 시 승급 |
| `corpusAsk` | **remoteDS only** | 코퍼스 필요 — 온디바이스로 폴백 불가 |
| `classify` | localMLX → remoteDS → onDevice | 분류 품질 우선 |
| `vision` | specialized → onDevice | GPU VLM 우선 |

## 3. 3단 fallback 규칙 (feedback_task_routing_hybrid + no_silent_fallback)

1. **explicit > rule > error.**
2. **명시 opt-in**(`request.explicitProvider`) → 그 provider 만. 불가 시 **에러**(`explicitProviderUnavailable`) — 자동 다른 티어 호출 **금지**.
3. **미지정** → 태스크 선호 체인 순회. 불가/실패는 다음으로 넘기되 **`routingNote`로 가시화**(silent skip 금지) + `log` 훅.
4. 전부 불가 → `noProviderAvailable`.

스모크 검증:
```
quickSummarize → onDevice         corpusAsk → remoteDS (citations=1)
vision → specialized              classify → localMLX
explicit onDevice 불가 → 에러(자동 fallback X)
rule fallback: onDevice 불가 → localMLX  note="fallback from onDevice → localMLX"
```

## 4. S1 계약과의 다리 (`RemoteDSProvider`)

`corpusAsk` 만 `.remoteDS` 책임. 매핑(고정):
```
GET /search/ask?q=<prompt>&backend=<map(explicitProvider)>   → AskResponse (CONTRACT.md §4)
AskResponse.ai_answer            → AICompletionResponse.text
AskResponse.citations[]          → AICitation[]  (n, doc_id, title, section_title, span_text)
AskResponse.synthesis_status     → AIFinishReason  (completed/timeout/no_evidence/backend_unavailable/…)
AskResponse.confidence           → AIConfidence
AskResponse.backend_used         → routingNote (어느 LLM 이 응답했는지)
```
`backend` 매핑: `nil`→`mac-mini-default` · `.localMLX`→`gemma-macmini` · (M5 Max Qwen 경로)→`qwen-macbook` · cloud→`claude-cloud`(503, 별 PR).

## 5. S2 가 다음에 채울 것 (인터페이스는 고정)
- `OnDeviceProvider`: `import FoundationModels` 가용성 프로브 + `LanguageModelSession` 호출.
- `LocalMLXProvider`: 맥미니 OpenAI 호환 호출(messages system/user 분리 call-shape 고정).
- `RemoteDSProvider`: S3 의 DS API client 주입 → 위 §4 매핑 결선.
- `SpecializedProvider`: GPU 비전/특화 통로(필요 태스크만).

## 동시 출발선 (S1 + S2 둘 다 동결 완료)
- **S3** = `MockAIProvider` 주입 + S1 `fixtures/` 디코딩 → 실 백엔드·실 LLM 대기 0 으로 앱 전체 빌드.
- **S1** = `[S1-ADD]` 구현 + 응답 shape 유지.
- **S2** = 위 §5 provider 결선. 인터페이스(`AIProvider`/`AIRouter`) 변경 시만 합의.
