# Coding Conventions

> AI 에이전트는 코드를 작성하기 전 이 컨벤션을 확인합니다.

## 네이밍

### Python (서버)

| 대상 | 규칙 | 예시 |
|------|------|------|
| 변수/함수 | snake_case | `write_pending_approval()` |
| 클래스 | PascalCase | `GravityBot`, `WSHub`, `TokenManager` |
| 상수 | UPPER_SNAKE_CASE | `MAX_MSG_SIZE`, `HEARTBEAT_INTERVAL` |
| 파일명 | snake_case | `hub.py`, `ws_client.py` |
| 로거명 | 모듈명 | `logging.getLogger("hub")` |

### TypeScript (Extension)

| 대상 | 규칙 | 예시 |
|------|------|------|
| 변수/함수 | camelCase | `writePendingApproval()`, `setupMonitor()` |
| 클래스 | PascalCase | `WSBridgeClient` |
| 인터페이스 | PascalCase | `BridgeContext`, `WSPendingData` |
| 상수 | UPPER_SNAKE_CASE | `MAX_QUEUE_SIZE`, `AUTH_TIMEOUT` |
| 파일명 | kebab-case | `ws-client.ts`, `step-probe.ts` |
| export 함수 | camelCase | `initStepProbe()`, `generateApprovalObserverScript()` |

## 코드 스타일

| 항목 | Python | TypeScript |
|------|--------|-----------|
| 들여쓰기 | 4 spaces | 4 spaces |
| 따옴표 | 쌍따옴표 `"` (f-string 포함) | 작은따옴표 `'` |
| 세미콜론 | N/A | 사용 |
| 줄바꿈 | LF (Unix) | CRLF (Windows, git 자동 변환) |
| 최대 줄 길이 | 120자 권장 | 120자 권장 |
| 타입 힌트 | 적극 사용 (`-> list[str]`) | strict (`BridgeContext` 인터페이스) |

## 커밋 메시지

```
<type>(<scope>): <description>

type: feat|fix|refactor|test|docs|chore|ci|infra
scope: server|extension|hub|bot|gateway|bridge (선택)
```

**예시:**
- `feat(hub): WebSocket Hub 구현 + JWT 인증`
- `refactor(extension): 모듈 분리 (step-probe, observer-script)`
- `fix(bot): auto-approve 세션 간 초기화`
- `docs: architecture.md 전면 재작성`

관련 Vikunja 태스크가 있으면: `feat(hub): WS Hub 구현 #task-395`

## 주석

- 한국어/영어 혼용 가능
- TODO 주석: `// TODO: 설명` 형식
- 섹션 구분: `// ─── Section Name ───` (TypeScript), `# ─── Section ───` (Python)
- 복잡한 로직에는 반드시 WHY(왜) 주석 추가
- 함수 docstring: Python은 `"""..."""`, TypeScript는 `/** ... */`

## 모듈 분리 패턴

Extension 모듈 분리 시 사용하는 패턴:

| 패턴 | 용도 | 예시 |
|------|------|------|
| **순수 함수 추출** | 외부 상태 참조 없는 함수 | `step-utils.ts` |
| **독립 스크립트** | 문자열 반환 함수 | `observer-script.ts` |
| **Context 패턴** | 공유 상태가 많은 함수 그룹 | `step-probe.ts` (BridgeContext) |
| **클래스 추출** | 자체 상태 + 메서드 | `ws-client.ts` (WSBridgeClient) |

## 테스트

| 항목 | 위치 | 도구 |
|------|------|------|
| Python 구문 검사 | `tests/test_syntax.py` | `ast.parse` |
| WS Hub 연결 | `tests/test_ws_hub.py` | `websockets` |
| TypeScript 컴파일 | `npx tsc --noEmit` | TypeScript compiler |
| E2E | 수동 (Discord 버튼 클릭) | — |

## 로깅

| 측 | 방식 | 포맷 |
|----|------|------|
| Python | `logging.getLogger(name)` | `YYYY-MM-DD HH:MM:SS [name] LEVEL: message` |
| Extension | `logToFile(msg)` → bridge/log/ | `[HH:MM:SS] message` + `[WS]` prefix |
| Hub | `[HUB]` prefix | `[HUB] Auth OK: {conn_id} project={project}` |
| Gateway | `[GATEWAY]` prefix | `[GATEWAY] HTTP API started on {host}:{port}` |

## WS / File Bridge 상호 배타 패턴

> [!IMPORTANT]
> WS Hub과 파일 bridge는 **항상 상호 배타적**이어야 합니다.
> 양쪽에 동시에 쓰면 이중 전달 버그가 발생합니다. (known-issues-archive 참조)

**Extension (TypeScript):**
```typescript
// ✅ 올바른 패턴
if (ctx.wsBridge?.isConnected()) {
    ctx.wsBridge.sendPending(data);
    return;  // ← 반드시 return으로 파일 쓰기 건너뛰기
}
// File fallback
fs.writeFileSync(pendingPath, JSON.stringify(data));
```

**Bot (Python):**
```python
# ✅ 올바른 패턴
if self.hub:
    await self.hub.send_response(...)
else:
    bridge.write_response(...)
```

**금지 패턴:**
```python
# ❌ 이중 쓰기 — 절대 금지
if self.hub:
    await self.hub.send_response(...)
bridge.write_response(...)  # ← Hub 성공해도 파일에도 씀 → 이중 처리
```
