정리: 에이전트 작업 로그를 보관한다

This commit is contained in:
toki 2026-05-20 07:26:23 +09:00
parent 3a645f5f14
commit 9872ebac87
89 changed files with 1817 additions and 2780 deletions

View file

@ -1,101 +0,0 @@
<!-- task=api_consistency plan=0 tag=IMPROVE -->
# Code Review Reference - IMPROVE
## 개요
date=2026-04-24
task=api_consistency, plan=0, tag=IMPROVE
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_0.log` (기존 code_review_*.log 수 = 0)
2. `PLAN.md``plan_0.log` (기존 plan_*.log 수 = 0)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가 | [x] |
| [IMPROVE-2] TypeScript write queue maxsize 제한 | [x] |
## 계획 대비 변경 사항
- TypeScript write loop는 첫 packet을 즉시 in-flight로 소비하므로, 테스트는 `1개 in-flight + 64개 queued` 상태에서 66번째 `queuePacket()`이 waiter에 들어가는 방식으로 검증했다.
- Dart `_pendingRequests` 정리 확인은 public API를 늘리지 않기 위해 테스트에서만 reflection으로 확인했다.
## 주요 설계 결정
- Dart `sendRequest`는 기존 호출 호환성을 유지하도록 named parameter 기본값 `Duration(seconds: 30)`을 사용했다.
- Dart timeout 발생 시 해당 nonce를 `_pendingRequests`에서 제거하고 `TimeoutException`을 전파한다.
- TypeScript write queue 제한은 queued item 수 기준 64개로 적용하고, queue item이 소비될 때 waiter를 하나씩 깨운다.
- TypeScript `shutdown()`은 대기 중인 sender를 모두 깨워 재개 시 `NotConnectedError`로 종료되도록 했다.
## 리뷰어를 위한 체크포인트
- `dart/lib/src/communicator.dart``sendRequest` 시그니처에 `{Duration timeout = const Duration(seconds: 30)}` 파라미터 있는지
- `dart/lib/src/communicator.dart` — timeout 발생 시 `_pendingRequests.remove(requestNonce)` 호출 후 `TimeoutException` 전파하는지
- `dart/test/communicator_test.dart``TimeoutException` 테스트 추가 및 통과 여부
- `typescript/src/communicator.ts``MAX_WRITE_QUEUE_SIZE = 64` 상수 존재 여부
- `typescript/src/communicator.ts``queuePacket()`에서 queue full 시 대기 로직 구현 여부
- `typescript/src/communicator.ts``shutdown()`에서 waiters 플러시 여부
- `typescript/src/communicator.ts``initialize()`에서 `writeQueueWaiters = []` 초기화 여부
- `typescript/test/communicator.test.ts` — 큐 full 대기 테스트 추가 및 통과 여부
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### IMPROVE-1 중간 검증
```
$ cd dart && dart test test/communicator_test.dart
00:00 +0: Communicator protocol guards response typeName mismatch completes sendRequest with error
00:00 +1: Communicator protocol guards close 후 sendRequest는 StateError로 완료된다
00:00 +2: Communicator protocol guards sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다
00:00 +3: Communicator protocol guards cannot register addRequestListener for a type already using addListener
00:00 +4: Communicator protocol guards cannot register addListener for a type already using addRequestListener
00:00 +5: Communicator protocol guards queuePacket uses injected transport
00:00 +6: All tests passed!
$ cd dart && dart analyze
No issues found!
```
### IMPROVE-2 중간 검증
```
$ cd typescript && npx tsc --noEmit
(exit 0, no output)
$ cd typescript && npx vitest run test/communicator.test.ts
✓ test/communicator.test.ts (8 tests) 20ms
Test Files 1 passed (1)
Tests 8 passed (8)
```
### 최종 검증
```
$ cd dart && dart analyze
No issues found!
$ cd dart && dart test
00:15 +43: All tests passed!
$ cd typescript && npx tsc --noEmit
(exit 0, no output)
$ cd typescript && npx vitest run
✓ test/base_client.test.ts (7 tests) 9ms
✓ test/communicator.test.ts (8 tests) 19ms
✓ test/tcp.test.ts (2 tests) 13ms
✓ test/ws.test.ts (2 tests) 21ms
Test Files 4 passed (4)
Tests 19 passed (19)
```

View file

@ -1,225 +0,0 @@
<!-- task=api_consistency plan=0 tag=IMPROVE -->
# API 일관성 개선
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하면서 `[ ]``[x]`로 표시하라.
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
---
## 배경
프로젝트 전반 평가에서 발견된 두 가지 API 비일관성을 수정한다.
| 항목 | 문제 | 영향 |
|------|------|------|
| IMPROVE-1 | Dart `sendRequest`에 timeout 파라미터 없음 | 응답 없으면 무한 대기 |
| IMPROVE-2 | TypeScript write queue 크기 제한 없음 | await 없이 대량 send 시 무한 증가 가능 |
Go/Kotlin/Python/TypeScript는 `sendRequest`에 timeout을 지원한다. Dart만 없다.
Go/Kotlin/Python은 write queue를 64개로 제한한다. TypeScript와 Dart는 제한이 없다.
Dart write queue는 Promise chaining 방식이라 구조상 추가가 어려우므로 이번 범위에서 제외한다.
## 의존 관계 및 구현 순서
IMPROVE-1과 IMPROVE-2는 독립적이다. 이 순서로 진행한다.
---
### [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가
**문제**
[dart/lib/src/communicator.dart](../../dart/lib/src/communicator.dart) — `sendRequest`에 timeout 파라미터가 없다.
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data) async {
```
다른 언어 비교:
| 언어 | timeout 기본값 |
|------|--------------|
| Go | `time.Duration`, 기본 30s |
| Kotlin | `timeoutMs: Long = 30_000L` |
| Python | `timeout: float = 30.0` |
| TypeScript | `timeoutMs = 30000` |
| **Dart** | **없음 — 무한 대기** |
**해결 방법**
`sendRequest`에 named 파라미터 `{Duration timeout = const Duration(seconds: 30)}`를 추가한다.
응답 대기를 `completer.future.timeout(timeout, onTimeout: ...)` 으로 감싼다.
timeout 발생 시 `_pendingRequests`에서 해당 nonce를 제거하고 `TimeoutException`을 전파한다.
Before:
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data) async {
if (!isAlive) return Future.error(StateError('not connected'));
final requestNonce = ++nonce;
// ...
return completer.future;
}
```
After:
```dart
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
Req data, {Duration timeout = const Duration(seconds: 30)}) async {
if (!isAlive) return Future.error(StateError('not connected'));
final requestNonce = ++nonce;
// ...
return completer.future.timeout(timeout, onTimeout: () {
_pendingRequests.remove(requestNonce);
throw TimeoutException(
'sendRequest timeout for nonce $requestNonce', timeout);
});
}
```
`dart:async``TimeoutException`을 사용한다 (`import 'dart:async'`는 이미 있음).
**수정 파일 및 체크리스트**
- [x] `dart/lib/src/communicator.dart``sendRequest``{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가
- [x] `dart/lib/src/communicator.dart``completer.future.timeout(timeout, onTimeout: ...)` 적용, pending 정리 포함
- [x] `dart/test/communicator_test.dart``sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` 테스트 추가
**테스트 작성**
- 파일: `dart/test/communicator_test.dart`
- 테스트명: `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다`
- 시나리오: `FakeTransport`에 packet을 보내지만 응답을 보내지 않음, `Duration(milliseconds: 50)` timeout으로 호출, `throwsA(isA<TimeoutException>())` 단언, timeout 후 `_pendingRequests`가 비어있는지 확인
**중간 검증**
```bash
cd dart && dart test test/communicator_test.dart
# 새 timeout 테스트 포함 전체 PASS
cd dart && dart analyze
# No issues found
```
---
### [IMPROVE-2] TypeScript write queue maxsize 제한
**문제**
[typescript/src/communicator.ts](../../typescript/src/communicator.ts) — write queue가 크기 제한 없는 `Array`다.
```typescript
private writeQueue: QueueItem[] = [];
```
Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다.
TypeScript는 `await`하지 않고 대량 `send()`를 발행하면 큐가 무한 증가한다.
**해결 방법**
`MAX_WRITE_QUEUE_SIZE = 64` 상수를 추가한다.
큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 `asyncio.Queue`와 동일한 시맨틱).
`shutdown()` 시 모든 waiter를 깨워 `NotConnectedError`로 종료한다.
추가할 멤버:
```typescript
private static readonly MAX_WRITE_QUEUE_SIZE = 64;
private writeQueueWaiters: Array<() => void> = [];
```
`queuePacket` 변경:
```typescript
async queuePacket(base: PacketBase): Promise<void> {
if (!this.isAliveFlag || this.transport === null) {
throw new NotConnectedError();
}
// 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
await new Promise<void>((resolve) => {
this.writeQueueWaiters.push(resolve);
});
if (!this.isAliveFlag) throw new NotConnectedError();
}
// 이하 기존 코드 동일
...
}
```
`runWriteQueue`에서 항목 소비 후 waiter 해제:
```typescript
const item = this.writeQueue.shift();
if (this.writeQueueWaiters.length > 0) {
this.writeQueueWaiters.shift()?.();
}
```
`shutdown()`에서 waiters 플러시:
```typescript
while (this.writeQueueWaiters.length > 0) {
this.writeQueueWaiters.shift()?.();
}
```
`initialize()`에서 `writeQueueWaiters` 초기화 추가:
```typescript
this.writeQueueWaiters = [];
```
**수정 파일 및 체크리스트**
- [x] `typescript/src/communicator.ts``MAX_WRITE_QUEUE_SIZE = 64` 상수 추가
- [x] `typescript/src/communicator.ts``writeQueueWaiters` 멤버 추가 및 `initialize()` 초기화
- [x] `typescript/src/communicator.ts``queuePacket()`에 full 시 대기 로직 추가
- [x] `typescript/src/communicator.ts``runWriteQueue()`에서 waiter 해제
- [x] `typescript/src/communicator.ts``shutdown()`에서 waiters 플러시
- [x] `typescript/test/communicator.test.ts` — 큐 full 시 대기 후 처리 테스트 추가
**테스트 작성**
- 파일: `typescript/test/communicator.test.ts`
- 테스트명: `write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다`
- 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 `queuePacket` 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인
**중간 검증**
```bash
cd typescript && npx tsc --noEmit
# 타입 오류 없음
cd typescript && npx vitest run test/communicator.test.ts
# 새 큐 테스트 포함 PASS
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/lib/src/communicator.dart` | IMPROVE-1 |
| `dart/test/communicator_test.dart` | IMPROVE-1 |
| `typescript/src/communicator.ts` | IMPROVE-2 |
| `typescript/test/communicator.test.ts` | IMPROVE-2 |
## 최종 검증
```bash
# Dart
cd dart && dart analyze
# No issues found
cd dart && dart test
# 전체 PASS (신규 timeout 테스트 포함)
# TypeScript
cd typescript && npx tsc --noEmit
# 타입 오류 없음
cd typescript && npx vitest run
# 전체 PASS (신규 큐 테스트 포함)
```

View file

@ -0,0 +1,262 @@
<!-- task=01+browser_native_ws plan=0 tag=BNW -->
# Code Review Reference - BNW
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-20
task=01+browser_native_ws, plan=0, tag=BNW
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [BNW-1] 브라우저 native WebSocket client 구현 | [x] |
| [BNW-2] Browser-safe root export와 Node 전용 entrypoint 분리 | [x] |
| [BNW-3] 테스트와 crosstest 갱신 | [x] |
| [BNW-4] Browser boundary 최종 guard | [x] |
## 구현 체크리스트
- [x] [BNW-1] 브라우저 native WebSocket client를 Node 라이브러리 없이 구현한다.
- [x] [BNW-2] TypeScript entrypoint와 dependency manifest를 browser-safe root와 Node 전용 entrypoint로 분리한다.
- [x] [BNW-3] 브라우저 WS unit test, Node WS 회귀 테스트, crosstest, 정적 guard 검증을 추가/갱신한다.
- [x] [BNW-4] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- 계획의 `수정 파일 요약`에는 `typescript/test/ws.test.ts`, `typescript/crosstest/go_typescript_client.ts`, `typescript/crosstest/typescript_go.ts`만 import/name 갱신 대상으로 나열되어 있다. 그러나 `tsconfig.json`의 `include`가 모든 `crosstest/**/*.ts`를 포함하기 때문에 다른 crosstest 파일(`typescript_dart.ts`, `typescript_python.ts`, `typescript_kotlin.ts`, `dart_typescript_client.ts`, `python_typescript_client.ts`, `kotlin_typescript_client.ts`)도 동일한 import/name으로 갱신하지 않으면 `npm run check`가 실패한다. 따라서 동일 패턴으로 이 6개 파일도 같이 갱신했다.
- BNW-4 정적 guard 패턴 `Buffer`는 browser-safe 코드가 사용하는 globally-scoped `ArrayBuffer` 식별자에도 매치된다. 이는 의도된 매치가 아니므로, 동일한 명령을 그대로 실행하면 항상 `ArrayBuffer` 라인이 출력된다. 해당 출력을 그대로 검증 결과에 기록했고, 진짜 의도(Node `Buffer` 식별자 검출)는 `\bBuffer\b` 패턴으로 재확인했다. 추가 검증 결과는 검증 섹션에 함께 기록했다.
## 주요 설계 결정
- Browser native client는 `globalThis.WebSocket`을 기본으로 사용하되 테스트 주입용 `WebSocketCtor?: typeof WebSocket`만 옵션으로 노출한다. `connectBrowserWs(url, ...)`는 `ws://`/`wss://` 모두 지원하므로 별도 wss 함수를 두지 않는다.
- Node 전용 transport(`tcp_client`, `tcp_server`, `node_ws_client`, `node_ws_server`)는 `proto-socket/node` subpath로만 노출한다. root export(`proto-socket`)는 `base_client`, `communicator`, generated protobuf, `browser_ws_client`만 포함하여 번들러가 Node 모듈을 끌어오지 않도록 한다.
- `ws`는 `dependencies`에서 제거하고 `devDependencies`로 이동했다. 브라우저 사용자는 `ws` 패키지를 설치할 필요가 없고, Node 사용자는 자신의 환경에 `ws`를 함께 두면 된다(`@types/ws`는 그대로 devDep 유지).
- `WsClient`/`WsServer`/`connectWs`/`connectWss`는 Node 명칭으로 명확히 분리하기 위해 `NodeWsClient`/`NodeWsServer`/`connectNodeWs`/`connectNodeWss`로 이름을 바꾸고 파일을 `src/node_ws_client.ts`, `src/node_ws_server.ts`로 옮겼다. 기존 파일은 제거해 root export 경계가 흐려지는 것을 막았다.
- `tsconfig.json`에 `lib: ["ES2022", "DOM"]`을 추가해 browser native WebSocket 타입을 사용 가능하게 했다. `types: ["node"]`는 Node 전용 파일 컴파일을 위해 유지했다.
- Browser client는 `MessageEvent.data`로 `ArrayBuffer`, `Uint8Array`, `Blob`을 받아 `Uint8Array`로 정규화한다. text frame이나 알 수 없는 payload는 protocol 위반으로 처리해 즉시 disconnect 한다.
## 리뷰어를 위한 체크포인트
- root/browser entrypoint가 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
- native browser client가 WebSocket binary frame에 `PacketBase` protobuf만 싣고 TCP length header를 추가하지 않는지 확인한다.
- Node TCP/WS/server 기능은 browser root가 아니라 Node 전용 entrypoint로만 노출되는지 확인한다.
- browser unit test가 fake native WebSocket으로 send, receive, request-response, disconnect 경로를 검증하는지 확인한다.
- Go/TypeScript crosstest가 이름 통일 작업 이후 새 module path 상태에서 성공하는지 확인한다.
## 검증 결과
### BNW-1 중간 검증
```
$ cd typescript && npm test -- browser_ws_client
> proto-socket@1.0.5 test
> vitest run browser_ws_client
RUN v3.2.4 /config/workspace/proto-socket/typescript
✓ test/browser_ws_client.test.ts (6 tests) 17ms
Test Files 1 passed (1)
Tests 6 passed (6)
Start at 04:35:38
Duration 384ms (transform 90ms, setup 0ms, collect 156ms, tests 17ms, environment 0ms, prepare 85ms)
```
### BNW-2 중간 검증
```
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
```
(원문 패턴 `Buffer`가 browser-safe 식별자 `ArrayBuffer`에도 매치된 결과. 실제 Node `Buffer` 식별자 검출은 아래 word-boundary 패턴으로 재확인.)
```
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
(no output)
```
```
$ cd typescript && npm run check
> proto-socket@1.0.5 check
> tsc --noEmit
```
### BNW-3 중간 검증
```
$ cd typescript && npm test
> proto-socket@1.0.5 test
> vitest run
RUN v3.2.4 /config/workspace/proto-socket/typescript
✓ test/base_client.test.ts (7 tests) 7ms
✓ test/communicator.test.ts (10 tests) 20ms
✓ test/browser_ws_client.test.ts (6 tests) 16ms
✓ test/tcp.test.ts (5 tests) 49ms
✓ test/ws.test.ts (5 tests) 58ms
Test Files 5 passed (5)
Tests 33 passed (33)
Start at 04:35:41
Duration 532ms (transform 360ms, setup 0ms, collect 894ms, tests 149ms, environment 0ms, prepare 526ms)
```
```
$ cd go && go run ./crosstest/go_typescript.go
INFO typeName go=TestData
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all go-server/typescript-client crosstests passed
```
### BNW-4 중간 검증
```
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
```
(원문 패턴 `Buffer`가 browser-safe `ArrayBuffer`에 매치된 결과. 실제 의도된 검증은 word-boundary 패턴.)
```
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
(no output)
```
### 최종 검증
```
$ cd typescript && npm run check
> proto-socket@1.0.5 check
> tsc --noEmit
```
```
$ cd typescript && npm test
> proto-socket@1.0.5 test
> vitest run
RUN v3.2.4 /config/workspace/proto-socket/typescript
✓ test/base_client.test.ts (7 tests) 7ms
✓ test/communicator.test.ts (10 tests) 20ms
✓ test/browser_ws_client.test.ts (6 tests) 16ms
✓ test/tcp.test.ts (5 tests) 49ms
✓ test/ws.test.ts (5 tests) 58ms
Test Files 5 passed (5)
Tests 33 passed (33)
```
```
$ cd go && go run ./crosstest/go_typescript.go
PASS all go-server/typescript-client crosstests passed
```
(상단 BNW-3 출력에 동일한 명령의 전체 stdout이 포함되어 있다.)
```
$ rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
typescript/src/browser_ws_client.ts:97: if (data instanceof ArrayBuffer) {
typescript/src/browser_ws_client.ts:104: const buf = await data.arrayBuffer();
```
(원문 패턴 매치는 browser-safe `ArrayBuffer`. word-boundary 패턴 `\bBuffer\b`로 재실행시 출력 없음.)
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 2026-05-20 cloud-G08
- 종합 판정: FAIL
#### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | 브라우저 native WebSocket client는 `PacketBase` protobuf binary frame을 그대로 송수신하고, 재실행한 unit/crosstest가 통과했다. |
| Completeness | Pass | BNW-1~BNW-4 구현 항목과 구현 에이전트 소유 review stub 섹션은 채워져 있다. |
| Test Coverage | Pass | 브라우저 WS unit test, Node WS 회귀 테스트, Go/TypeScript crosstest, root guard가 재실행 기준 통과했다. |
| API Contract | Fail | `proto-socket/node`가 런타임에 `ws`를 import하지만 package manifest에는 devDependency로만 선언되어 일반 소비 환경에서 Node entrypoint가 깨질 수 있다. |
| Code Quality | Pass | Node/browser 경계 분리와 파일명/심볼 분리는 명확하다. |
| Plan Deviation | Pass | 구현 범위는 계획의 browser-safe root 및 Node 전용 entrypoint 분리 방향과 일치한다. |
| Verification Trust | Pass | 기록된 `npm run check`, `npm test`, `go run ./crosstest/go_typescript.go`, 정적 guard 결과를 재실행해 일치함을 확인했다. |
#### 발견된 문제
- Required — `typescript/package.json:21`: `typescript/src/node_ws_client.ts:1`과 `typescript/src/node_ws_server.ts:4`는 `ws`를 런타임 import하고, `package.json`은 `./node` subpath를 공개하지만 `ws`는 `devDependencies`에만 있다. 패키지 소비자가 devDependencies 없이 설치한 뒤 `proto-socket/node`를 import하면 `ERR_MODULE_NOT_FOUND`가 날 수 있다. `ws`를 Node entrypoint의 명시적 런타임 계약으로 선언해야 한다. 브라우저 root 설치 부담을 피하려면 `peerDependencies` + `peerDependenciesMeta.ws.optional=true`로 두고 devDependency는 테스트용으로 유지한 뒤, README에 Node WebSocket 사용자는 `ws`를 설치해야 한다고 적는 방식을 우선 검토한다.
#### 다음 단계
- FAIL: Required 문제를 수정하는 다음 routed plan/review를 작성한다.

View file

@ -0,0 +1,137 @@
<!-- task=01+browser_native_ws plan=1 tag=REVIEW_BNW -->
# Code Review Reference - REVIEW_BNW
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-20
task=01+browser_native_ws, plan=1, tag=REVIEW_BNW
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
Review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다. | [ ] |
## 구현 체크리스트
- [ ] [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 리뷰어를 위한 체크포인트
- `typescript/package.json`과 `typescript/package-lock.json`이 `ws`를 Node WebSocket entrypoint의 명시적 소비자-facing 의존성 계약으로 선언하는지 확인한다.
- 브라우저 root 사용자가 불필요하게 `ws`를 설치하지 않도록 optional peer dependency 또는 그에 준하는 명확한 방식인지 확인한다.
- `typescript/README.md`가 Node WebSocket 사용자의 `ws` 설치 필요성을 설명하는지 확인한다.
- root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
- 기존 TypeScript unit test와 Go/TypeScript crosstest가 계속 통과하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_BNW-1 중간 검증
```
$ cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
(output)
```
### 최종 검증
```
$ cd typescript && npm run check
(output)
```
```
$ cd typescript && npm test
(output)
```
```
$ cd go && go run ./crosstest/go_typescript.go
(output)
```
```
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 2026-05-20 cloud-G08
- 종합 판정: FAIL
#### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | `typescript/package.json`은 `ws`를 root dependency에서 제거하고 optional peer + devDependency로 선언했다. README도 Node WebSocket 사용자만 `ws` 설치가 필요하다고 설명한다. |
| Completeness | Fail | active review stub의 구현 항목별 완료 여부와 구현 체크리스트가 미체크 상태이고, 구현 에이전트 소유 섹션이 placeholder로 남아 있다. |
| Test Coverage | Pass | 지정된 TypeScript check/test, Go/TypeScript crosstest, root/browser guard를 재실행해 통과 또는 기대된 no-output 상태를 확인했다. |
| API Contract | Pass | `proto-socket/node`의 `ws` 런타임 계약은 optional peer dependency와 README 안내로 명시됐고, root/browser entrypoint는 `ws`, `node:*`, `Buffer`를 끌지 않는다. |
| Code Quality | Pass | 이번 follow-up 범위의 manifest/README 변경은 좁고, lockfile도 manifest 상태와 일치한다. |
| Plan Deviation | Pass | 실제 소스 변경은 `REVIEW_BNW-1`의 해결 방향과 일치한다. |
| Verification Trust | Fail | `검증 결과` 섹션에 실제 stdout/stderr가 아니라 `(output)` placeholder가 남아 있어 기록된 검증 출력과 실제 실행 결과를 대조할 수 없다. |
#### 발견된 문제
- Required - `agent-task/01+browser_native_ws/CODE_REVIEW-cloud-G08.md:35`: 구현 항목 완료표, 구현 체크리스트, `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`가 구현 완료 상태로 채워지지 않았다. 이 파일의 지시와 code-review skill 계약상 구현 에이전트 소유 섹션과 실제 stdout/stderr 기록은 필수 완료 조건이다. 후속 구현에서는 이미 적용된 `REVIEW_BNW-1` 변경을 기준으로 해당 섹션을 채우고, 고정 검증 명령의 실제 출력을 붙여 넣어야 한다.
#### 다음 단계
- FAIL: 위 Required 항목을 처리하는 후속 plan/review를 작성한다.

View file

@ -0,0 +1,193 @@
<!-- task=01+browser_native_ws plan=2 tag=REVIEW_REVIEW_BNW -->
# Code Review Reference - REVIEW_REVIEW_BNW
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-20
task=01+browser_native_ws, plan=2, tag=REVIEW_REVIEW_BNW
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
Review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다. | [x] |
## 구현 체크리스트
- [x] [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 `agent-task/01+browser_native_ws/`를 `agent-task/archive/YYYY/MM/01+browser_native_ws/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
없음. 이번 후속 작업은 review 문서 회복 범위에 한정했고, `REVIEW_BNW-1`에서 적용된 소스 상태(`typescript/package.json`, `typescript/package-lock.json`, `typescript/README.md`)가 plan 요구 조건을 이미 만족함을 검증으로 확인하여 소스 추가 변경 없이 review 문서와 검증 출력만 기록했다.
## 주요 설계 결정
- `ws`를 root `dependencies`가 아닌 optional `peerDependencies`(+ `peerDependenciesMeta.ws.optional = true`)로 선언한다. Node WebSocket entrypoint(`proto-socket/node`) 소비자에게만 `ws` 설치 책임을 위임하고, browser root 및 Node TCP-only 소비자는 불필요한 `ws` 의존성을 끌어오지 않도록 한다.
- 로컬 테스트(`test/ws.test.ts` 등) 실행을 위해 `devDependencies.ws`는 `^8.18.1`로 유지한다.
- `typescript/README.md`의 `Runtime Dependencies` 섹션에 Node WebSocket 사용 시 `ws` 설치 필요성과 browser/TCP-only 사용자의 불필요성을 명시한다.
- root/browser entrypoint(`typescript/src/index.ts`, `typescript/src/browser_ws_client.ts`)는 `ws`, `node:*` 모듈, `Buffer`, `ClientOptions`, `RawData`를 끌지 않는 기존 분리 구조를 유지하고, `rg` guard 명령으로 회귀 여부를 검증한다.
## 리뷰어를 위한 체크포인트
- active `CODE_REVIEW-cloud-G08.md`의 구현 항목별 완료 여부와 구현 체크리스트가 실제 상태에 맞게 체크됐는지 확인한다.
- `계획 대비 변경 사항`과 `주요 설계 결정`에 placeholder가 남아 있지 않은지 확인한다.
- `검증 결과`에 고정 명령의 실제 stdout/stderr가 붙어 있고 코드 상태와 일치하는지 확인한다.
- `typescript/package.json`과 `typescript/package-lock.json`이 `ws`를 optional peer dependency와 테스트용 devDependency로 반영하는지 확인한다.
- `typescript/README.md`가 Node WebSocket 사용자의 `ws` 설치 필요성을 설명하고 browser/root 및 TCP-only 사용자의 불필요성을 설명하는지 확인한다.
- root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 import하지 않는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_REVIEW_BNW-1 중간 검증
```
$ cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
(no output; exit code 0)
```
### 최종 검증
```
$ cd typescript && npm run check
> proto-socket@1.0.5 check
> tsc --noEmit
(exit code 0)
```
```
$ cd typescript && npm test
> proto-socket@1.0.5 test
> vitest run
RUN v3.2.4 /config/workspace/proto-socket/typescript
✓ test/base_client.test.ts (7 tests) 8ms
✓ test/communicator.test.ts (10 tests) 19ms
✓ test/browser_ws_client.test.ts (6 tests) 17ms
✓ test/tcp.test.ts (5 tests) 49ms
✓ test/ws.test.ts (5 tests) 56ms
Test Files 5 passed (5)
Tests 33 passed (33)
Start at 06:10:16
Duration 540ms (transform 355ms, setup 0ms, collect 964ms, tests 149ms, environment 0ms, prepare 503ms)
(exit code 0)
```
```
$ cd go && go run ./crosstest/go_typescript.go
INFO typeName go=TestData
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all go-server/typescript-client crosstests passed
(exit code 0)
```
```
$ rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
(no output; exit code 1)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 2026-05-20 cloud-G08
- 종합 판정: PASS
#### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | 후속 범위인 review 문서 회복이 완료됐고, `ws` runtime 계약은 optional peer + devDependency + README 안내로 실제 소스와 일치한다. |
| Completeness | Pass | plan/review의 구현 체크리스트가 서로 일치하며 모두 완료됐고, 구현-owned 섹션이 placeholder 없이 채워졌다. |
| Test Coverage | Pass | 고정 검증 명령 `npm run check`, `npm test`, Go/TypeScript crosstest, root/browser guard를 재실행해 기대 결과를 확인했다. |
| API Contract | Pass | `typescript/package.json`/lockfile은 `ws`를 root dependency가 아닌 optional peer로 선언하고, README는 Node WebSocket 사용자 설치 책임과 browser/TCP-only 예외를 설명한다. |
| Code Quality | Pass | 후속 작업은 task 문서 기록 회복으로 좁게 수행됐고, 추가 소스 변경 없이 기존 구현 상태를 검증했다. |
| Plan Deviation | Pass | 계획된 문서 회복과 검증 출력 기록 범위를 벗어난 변경이 없다. |
| Verification Trust | Pass | active review 파일의 기록과 재실행한 명령 출력이 일치한다. |
#### 발견된 문제
- 없음
#### 다음 단계
- PASS: active plan/review를 아카이브하고 complete.log 작성 후 task 디렉터리를 archive로 이동한다.

View file

@ -0,0 +1,41 @@
# Complete - 01+browser_native_ws
## 완료 일시
2026-05-20
## 요약
Browser native WebSocket 및 Node WebSocket `ws` 런타임 계약 정리 작업을 3회 리뷰 루프로 완료했다. 최종 판정은 PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | FAIL | Browser/native split 자체는 통과했지만 `proto-socket/node`의 `ws` runtime 계약이 manifest/README에 명시되지 않았다. |
| `plan_cloud_G08_1.log` | `code_review_cloud_G08_1.log` | FAIL | `ws` 계약 소스 변경은 통과했지만 active review stub의 구현-owned 섹션과 검증 출력이 비어 있었다. |
| `plan_cloud_G08_2.log` | `code_review_cloud_G08_2.log` | PASS | Review 문서 회복과 검증 출력 기록이 완료됐고, 지정 검증을 재실행해 일치함을 확인했다. |
## 구현/정리 내용
- TypeScript root/browser entrypoint는 browser-safe API만 노출하고 Node 전용 API는 `proto-socket/node`로 분리했다.
- Browser native WebSocket client와 관련 unit test를 추가하고, Node WebSocket 명칭과 crosstest import를 갱신했다.
- `ws`를 root dependency에서 제거하고 optional peer dependency 및 테스트용 devDependency로 선언했다.
- README에 Node WebSocket 사용자의 `ws` 설치 필요성과 browser/TCP-only 사용자의 불필요성을 명시했다.
- 후속 review 문서의 구현 체크리스트, 설계 결정, 검증 출력 기록을 완료했다.
## 최종 검증
- `cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"` - PASS; no output, exit code 0.
- `cd typescript && npm run check` - PASS; `tsc --noEmit` exit code 0.
- `cd typescript && npm test` - PASS; 5 test files and 33 tests passed.
- `cd go && go run ./crosstest/go_typescript.go` - PASS; all go-server/typescript-client crosstests passed.
- `rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts` - PASS; no output, exit code 1.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,420 @@
<!-- task=01+browser_native_ws plan=0 tag=BNW -->
# Browser Native WebSocket 계획 - BNW
## 이 파일을 읽는 구현 에이전트에게
**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
## 배경
Web에서 control-plane으로 직접 연결하려면 브라우저가 제공하는 native `WebSocket` API만으로 protobuf binary frame을 송수신할 수 있어야 한다.
현재 TypeScript 구현은 package 이름은 `proto-socket`이지만 WebSocket client/server 경로가 `ws`, `node:https`, `Buffer`에 의존하고, root export도 Node 전용 모듈을 함께 노출한다.
프로토콜의 WebSocket wire format은 이미 binary frame 안에 `PacketBase` protobuf를 싣는 구조이므로, Node 라이브러리 없이 같은 frame 계약을 구현할 수 있다.
## 의존 관계 및 구현 순서
`agent-task/01_proto_socket_naming`이 PASS되어 `complete.log`까지 생성된 뒤 시작한다.
이 작업은 module/import path가 `proto-socket`으로 통일된 상태를 전제로 TS/Go crosstest를 실행한다.
## 분석 결과
### 읽은 파일
- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/protocol/rules.md`
- `agent-ops/rules/project/domain/tools/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `PROTOCOL.md`
- `typescript/package.json`
- `typescript/tsconfig.json`
- `typescript/README.md`
- `typescript/src/base_client.ts`
- `typescript/src/communicator.ts`
- `typescript/src/index.ts`
- `typescript/src/tcp_client.ts`
- `typescript/src/tcp_server.ts`
- `typescript/src/ws_client.ts`
- `typescript/src/ws_server.ts`
- `typescript/test/base_client.test.ts`
- `typescript/test/communicator.test.ts`
- `typescript/test/tcp.test.ts`
- `typescript/test/ws.test.ts`
- `typescript/crosstest/go_typescript_client.ts`
- `typescript/crosstest/typescript_go.ts`
- `go/ws_client.go`
- `go/ws_server.go`
### 테스트 커버리지 공백
- Browser native WebSocket client: 기존 테스트 없음. `typescript/test/ws.test.ts`는 Node `ws` server/client만 검증한다.
- Browser binary frame parsing: 기존 `Communicator` 테스트는 PacketBase routing을 검증하지만 native `MessageEvent.data`의 `ArrayBuffer`, `Uint8Array`, `Blob` 입력은 검증하지 않는다.
- Browser-safe export boundary: 기존 테스트 없음. 정적 `rg` guard로 browser/root entrypoint에 `ws`, `node:*`, `Buffer`가 남지 않는지 검증한다.
- Node WebSocket/TCP 회귀: 기존 `typescript/test/ws.test.ts`, `typescript/test/tcp.test.ts`, `go run ./crosstest/go_typescript.go`가 유지되어야 한다.
### 심볼 참조
변경 또는 분리 대상 public/import symbol:
- `WsClient`, `connectWs`, `connectWss`: `typescript/src/ws_client.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/go_typescript_client.ts`, `typescript/crosstest/typescript_go.ts`
- `WsServer`: `typescript/src/ws_server.ts`, `typescript/test/ws.test.ts`, `typescript/crosstest/typescript_go.ts`
- root export: `typescript/src/index.ts:4-7`에서 `tcp_client`, `tcp_server`, `ws_client`, `ws_server`를 함께 export한다.
- Node dependency strings: `typescript/src/ws_client.ts:1`, `typescript/src/ws_server.ts:1`, `typescript/src/ws_server.ts:4`, `typescript/src/tcp_client.ts:1`, `typescript/src/tcp_server.ts:1`, `typescript/package.json:13`, `typescript/tsconfig.json:12`
### 범위 결정 근거
- TCP transport는 browser runtime에서 사용할 수 없으므로 native browser 구현 대상이 아니다. Node 전용 entrypoint로 분리만 한다.
- WebSocket server는 브라우저가 제공하지 않으므로 Node 전용 entrypoint로 유지한다.
- 프로토콜 schema와 Go WebSocket wire format은 변경하지 않는다. `PROTOCOL.md:38-48`의 binary frame 계약을 그대로 따른다.
- 실제 브라우저 E2E 자동화는 이번 계획의 필수 범위로 넣지 않는다. 먼저 fake native WebSocket 기반 unit test와 export boundary 검증으로 Node 의존성 제거를 고정한다.
### 빌드 등급
build=`cloud-G08`, review=`cloud-G08`. TypeScript public entrypoint, dependency manifest, Node/browser runtime boundary, cross-language WebSocket 흐름에 영향이 있어 API/프로토콜 경계 리뷰가 필요하다.
## 구현 체크리스트
- [ ] [BNW-1] 브라우저 native WebSocket client를 Node 라이브러리 없이 구현한다.
- [ ] [BNW-2] TypeScript entrypoint와 dependency manifest를 browser-safe root와 Node 전용 entrypoint로 분리한다.
- [ ] [BNW-3] 브라우저 WS unit test, Node WS 회귀 테스트, crosstest, 정적 guard 검증을 추가/갱신한다.
- [ ] [BNW-4] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [BNW-1] 브라우저 native WebSocket client 구현
#### 문제
현재 WebSocket client는 Node `ws` 패키지와 Node buffer API에 묶여 있다.
```ts
// typescript/src/ws_client.ts:1
import WebSocket, { type ClientOptions, type RawData } from "ws";
```
```ts
// typescript/src/ws_client.ts:31
this.ws.send(data, { binary: true }, (err) => {
```
```ts
// typescript/src/ws_client.ts:109-116
function toUint8Array(data: RawData): Uint8Array {
if (Array.isArray(data)) {
return Buffer.concat(data.map((chunk) => Buffer.from(chunk)));
}
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
return new Uint8Array(data);
}
```
브라우저 WebSocket에는 `send` callback, `RawData`, `off`, `once`, `terminate`, `ClientOptions`가 없다.
#### 해결 방법
`typescript/src/browser_ws_client.ts`를 추가한다. class 이름은 `BrowserWsClient`, 연결 함수는 `connectBrowserWs(url, intervalSec, waitSec, parserMap, options?)`로 둔다.
`options`에는 테스트 주입용 `WebSocketCtor?: typeof WebSocket`만 둔다. WSS는 별도 함수가 아니라 `wss://` URL로 처리한다.
구현은 `globalThis.WebSocket` 또는 주입된 constructor를 사용하고, `binaryType = "arraybuffer"`를 설정한다.
```ts
// typescript/src/browser_ws_client.ts
import { fromBinary, toBinary } from "@bufbuild/protobuf";
import { BaseClient } from "./base_client.js";
import { type ParserMap } from "./communicator.js";
import { PacketBaseSchema, type PacketBase } from "./packets/message_common_pb.js";
export interface BrowserWsConnectOptions {
WebSocketCtor?: typeof WebSocket;
}
export class BrowserWsClient extends BaseClient {
private readonly ws: WebSocket;
constructor(ws: WebSocket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
super(intervalSec, waitSec, () => closeBrowserWebSocket(ws));
this.ws = ws;
this.ws.binaryType = "arraybuffer";
this.initBase(parserMap);
this.ws.addEventListener("message", (event) => {
void this.onMessage(event.data);
});
this.ws.addEventListener("close", () => {
void this.onDisconnected();
});
this.ws.addEventListener("error", () => {
void this.onDisconnected();
});
}
async writePacket(base: PacketBase): Promise<void> {
this.ws.send(toBinary(PacketBaseSchema, base));
}
}
```
`onMessage`는 `ArrayBuffer`, `Uint8Array`, `Blob`을 `Uint8Array`로 변환한다. 문자열 frame은 protocol 위반으로 처리해 disconnect한다.
#### 수정 파일 및 체크리스트
- [ ] `typescript/src/browser_ws_client.ts` 추가
- [ ] `ArrayBuffer`, `Uint8Array`, `Blob` 입력 변환 구현
- [ ] native `addEventListener`/`removeEventListener` 기반 open/error/close 처리 구현
- [ ] `send`가 동기 throw를 Promise rejection으로 전달하도록 처리
- [ ] `close`는 `WebSocket.CLOSED`/`CLOSING` 상태를 고려하고 `close(1000, "")`만 사용한다. `terminate`는 사용하지 않는다.
#### 테스트 작성
`typescript/test/browser_ws_client.test.ts`를 새로 작성한다.
테스트는 fake native WebSocket constructor를 사용하며, 다음을 검증한다.
- `connectBrowserWs`가 open event 뒤 `BrowserWsClient`를 resolve한다.
- `communicator.send(TestData)`가 binary `PacketBase` frame을 native `send`로 전달한다.
- `message` event의 `ArrayBuffer` frame이 `communicator` listener로 routing된다.
- `sendRequestTyped` pending request가 response frame으로 resolve된다.
- non-binary string frame 또는 protobuf parse 실패가 disconnect를 발생시킨다.
#### 중간 검증
```bash
cd typescript && npm test -- browser_ws_client
```
예상 결과: browser native WebSocket unit test 성공.
### [BNW-2] Browser-safe root export와 Node 전용 entrypoint 분리
#### 문제
root export가 Node 전용 transport를 모두 노출한다.
```ts
// typescript/src/index.ts:4-7
export * from "./tcp_client.js";
export * from "./tcp_server.js";
export * from "./ws_client.js";
export * from "./ws_server.js";
```
`typescript/src/ws_server.ts`는 Node server API와 `ws`를 import한다.
```ts
// typescript/src/ws_server.ts:1-4
import * as https from "node:https";
import WebSocket, { WebSocketServer } from "ws";
```
`typescript/package.json:13`은 `ws`를 runtime dependency로 강제한다.
```json
// typescript/package.json:11-14
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"ws": "^8.18.1"
}
```
#### 해결 방법
root `typescript/src/index.ts`는 browser-safe API만 export한다.
```ts
// typescript/src/index.ts
export * from "./base_client.js";
export * from "./communicator.js";
export * from "./packets/message_common_pb.js";
export * from "./browser_ws_client.js";
```
Node 전용 API는 `typescript/src/node.ts`를 추가해 export한다.
```ts
// typescript/src/node.ts
export * from "./base_client.js";
export * from "./communicator.js";
export * from "./packets/message_common_pb.js";
export * from "./tcp_client.js";
export * from "./tcp_server.js";
export * from "./node_ws_client.js";
export * from "./node_ws_server.js";
```
현재 `ws_client.ts`/`ws_server.ts`의 Node 구현은 `node_ws_client.ts`/`node_ws_server.ts`로 이동하거나 같은 내용을 새 파일에 보존한다. Node API 이름은 명확히 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`를 사용한다. 기존 TS 테스트와 crosstest import는 Node entrypoint 또는 Node 파일명으로 갱신한다.
`package.json`에는 root와 Node subpath export를 명시한다.
```json
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./node": {
"types": "./dist/src/node.d.ts",
"default": "./dist/src/node.js"
}
}
```
`ws`와 `@types/ws`는 Node test/build용 devDependency로 둔다. root/browser entrypoint가 `ws`를 import하지 않기 때문에 브라우저 사용자는 Node 라이브러리에 묶이지 않는다.
`tsconfig.json`에는 DOM WebSocket 타입을 위해 `lib: ["ES2022", "DOM"]`를 추가한다. Node 전용 파일 compile을 위해 기존 `types: ["node"]`는 유지한다.
#### 수정 파일 및 체크리스트
- [ ] `typescript/src/index.ts`를 browser-safe export로 축소
- [ ] `typescript/src/node.ts` 추가
- [ ] `typescript/src/ws_client.ts`의 Node 구현을 `typescript/src/node_ws_client.ts`로 이동 또는 복사 후 명확한 Node API명 적용
- [ ] `typescript/src/ws_server.ts`의 Node 구현을 `typescript/src/node_ws_server.ts`로 이동 또는 복사 후 명확한 Node API명 적용
- [ ] 기존 Node-only `ws_client.ts`/`ws_server.ts`가 root export로 노출되지 않도록 정리
- [ ] `typescript/package.json` exports와 dependency/devDependency 정리
- [ ] `typescript/package-lock.json` 동기화
- [ ] `typescript/tsconfig.json` DOM lib 추가
- [ ] `typescript/README.md`에서 browser native WebSocket이 지원됨을 문서화하고 Node entrypoint import를 구분
#### 테스트 작성
신규 테스트는 BNW-1에서 작성한다. 이 항목은 기존 Node WS 테스트와 crosstest import 갱신으로 검증한다.
#### 중간 검증
```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```
예상 결과: 출력 없음.
```bash
cd typescript && npm run check
```
예상 결과: TypeScript typecheck 성공.
### [BNW-3] 테스트와 crosstest 갱신
#### 문제
기존 TS WebSocket 테스트는 Node `ws` 기반 이름을 직접 import한다.
```ts
// typescript/test/ws.test.ts
import { connectWs, connectWss, WsClient } from "../src/ws_client.js";
import { WsServer } from "../src/ws_server.js";
```
TS/Go crosstest도 Node WS 구현을 전제로 한다.
```ts
// typescript/crosstest/go_typescript_client.ts
import { connectWs, connectWss } from "../src/ws_client.js";
```
#### 해결 방법
Node WS 테스트와 crosstest는 `../src/node.js` 또는 `../src/node_ws_client.js`, `../src/node_ws_server.js`를 import하도록 바꾼다.
Node class/function 이름은 `NodeWsClient`, `connectNodeWs`, `connectNodeWss`, `NodeWsServer`로 맞춘다.
새 browser unit test는 package root 또는 `browser_ws_client` 경로에서 native client를 import해 root export가 browser-safe임을 함께 검증한다.
#### 수정 파일 및 체크리스트
- [ ] `typescript/test/ws.test.ts` Node API import/name 갱신
- [ ] `typescript/crosstest/go_typescript_client.ts` Node API import/name 갱신
- [ ] `typescript/crosstest/typescript_go.ts` Node API import/name 갱신
- [ ] `typescript/test/browser_ws_client.test.ts` 추가
- [ ] `typescript/README.md` validation/import 예시 갱신
#### 테스트 작성
`typescript/test/browser_ws_client.test.ts` 신규 작성은 필수다. 기존 `typescript/test/ws.test.ts`는 삭제하지 않고 Node WebSocket 회귀 테스트로 유지한다.
#### 중간 검증
```bash
cd typescript && npm test
```
예상 결과: TypeScript 전체 테스트 성공.
```bash
cd go && go run ./crosstest/go_typescript.go
```
예상 결과: Go/TypeScript crosstest 성공.
### [BNW-4] Browser boundary 최종 guard
#### 문제
구현 후에도 root/browser entrypoint에 Node import가 다시 섞이면 web-control-plane 연결에서 bundling/runtime 문제가 재발한다.
#### 해결 방법
정적 검색으로 browser-safe 파일의 금지 문자열을 검증한다. Node 전용 파일은 검색 범위에 넣지 않는다.
#### 수정 파일 및 체크리스트
- [ ] root/browser 파일에 `from "ws"`, `node:`, `Buffer`, `ClientOptions`, `RawData`가 없는지 확인
- [ ] Node 전용 파일은 `src/node.ts`, `src/node_ws_client.ts`, `src/node_ws_server.ts`, `src/tcp_client.ts`, `src/tcp_server.ts`에만 모은다
#### 테스트 작성
정적 guard 검증만 수행한다. 별도 테스트 파일은 BNW-1에서 작성한 browser WS 테스트로 충분하다.
#### 중간 검증
```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```
예상 결과: 출력 없음.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `typescript/src/browser_ws_client.ts` | BNW-1 |
| `typescript/src/index.ts` | BNW-2, BNW-4 |
| `typescript/src/node.ts` | BNW-2 |
| `typescript/src/node_ws_client.ts` | BNW-2, BNW-3 |
| `typescript/src/node_ws_server.ts` | BNW-2, BNW-3 |
| `typescript/src/ws_client.ts` | BNW-2 |
| `typescript/src/ws_server.ts` | BNW-2 |
| `typescript/package.json` | BNW-2 |
| `typescript/package-lock.json` | BNW-2 |
| `typescript/tsconfig.json` | BNW-2 |
| `typescript/README.md` | BNW-2, BNW-3 |
| `typescript/test/browser_ws_client.test.ts` | BNW-1, BNW-3 |
| `typescript/test/ws.test.ts` | BNW-3 |
| `typescript/crosstest/go_typescript_client.ts` | BNW-3 |
| `typescript/crosstest/typescript_go.ts` | BNW-3 |
## 최종 검증
```bash
cd typescript && npm run check
```
예상 결과: TypeScript typecheck 성공.
```bash
cd typescript && npm test
```
예상 결과: TypeScript 전체 테스트 성공.
```bash
cd go && go run ./crosstest/go_typescript.go
```
예상 결과: Go/TypeScript crosstest 성공.
```bash
rg --sort path -n 'from "ws"|node:|Buffer|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```
예상 결과: 출력 없음.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,82 @@
<!-- task=01+browser_native_ws plan=1 tag=REVIEW_BNW -->
# Browser Native WebSocket 후속 계획 - REVIEW_BNW
## 배경
이전 리뷰(`code_review_cloud_G08_0.log`)에서 브라우저 native WebSocket 구현, root/browser export guard, Node WS 회귀 테스트, Go/TypeScript crosstest는 통과했다.
다만 `proto-socket/node` subpath가 `ws`를 런타임 import하면서 `package.json`에는 `ws`가 devDependency로만 남아 있어, 일반 소비자가 devDependencies 없이 설치한 환경에서 Node WebSocket entrypoint가 깨질 수 있다.
## 실패 사유
- Required: `typescript/src/node_ws_client.ts`와 `typescript/src/node_ws_server.ts`가 `ws`를 런타임 import하지만, `typescript/package.json`은 `ws`를 Node entrypoint의 소비자-facing 의존성 계약으로 선언하지 않는다.
## 구현 체크리스트
- [x] [REVIEW_BNW-1] `proto-socket/node`의 `ws` 런타임 의존성 계약을 manifest와 README에 명시한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_BNW-1] Node WebSocket 런타임 의존성 계약 정리
#### 문제
`package.json`은 `./node` subpath를 공개한다.
```json
"./node": {
"types": "./dist/src/node.d.ts",
"default": "./dist/src/node.js"
}
```
그러나 Node WebSocket 구현은 런타임에 `ws`를 import한다.
```ts
import WebSocket, { type ClientOptions, type RawData } from "ws";
```
현재 `ws`는 devDependency에만 있어 패키지 소비 환경에서 `proto-socket/node` import가 실패할 수 있다.
#### 해결 방법
브라우저 root 사용자가 불필요하게 `ws`를 설치하지 않도록 `ws`를 optional peer dependency로 선언하고, 로컬 테스트용 devDependency는 유지한다.
```json
"peerDependencies": {
"ws": "^8.18.1"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
}
}
```
`package-lock.json`도 같은 manifest 상태를 반영하도록 갱신한다.
`typescript/README.md`에는 Node WebSocket entrypoint를 사용할 때 `ws` 설치가 필요하다는 문장을 추가한다. TCP-only Node 사용자는 별도 `ws` 설치가 필요 없다는 점도 함께 적어 혼동을 줄인다.
#### 수정 파일 및 체크리스트
- [x] `typescript/package.json`에 `peerDependencies.ws`와 `peerDependenciesMeta.ws.optional`을 추가한다.
- [x] `typescript/package-lock.json`을 package manifest와 동기화한다.
- [x] `typescript/README.md`에 Node WebSocket 사용 시 `ws` 설치 필요성을 문서화한다.
- [x] root/browser entrypoint가 여전히 `ws`, `node:*`, `Buffer`를 끌지 않는지 확인한다.
#### 중간 검증
```bash
cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
```
예상 결과: 출력 없이 exit code 0.
## 최종 검증
```bash
cd typescript && npm run check
cd typescript && npm test
cd go && go run ./crosstest/go_typescript.go
rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```
마지막 `rg` 명령의 예상 결과는 출력 없음.

View file

@ -0,0 +1,75 @@
<!-- task=01+browser_native_ws plan=2 tag=REVIEW_REVIEW_BNW -->
# Browser Native WebSocket 리뷰 문서 회복 계획 - REVIEW_REVIEW_BNW
## 이 파일을 읽는 구현 에이전트에게
**이번 후속 작업은 소스 재설계가 아니라 active `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션과 검증 출력 회복이다.**
소스 변경은 검증 중 불일치가 발견될 때만 최소 범위로 수행한다.
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-cloud-G08.md`에 기록한다.
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
## 배경
이전 후속 구현은 `proto-socket/node`의 `ws` 런타임 계약을 `typescript/package.json`, `typescript/package-lock.json`, `typescript/README.md`에 반영했다.
리뷰어 재검증 기준으로 소스 계약과 지정 검증 명령은 통과했다.
하지만 active `CODE_REVIEW-cloud-G08.md`의 구현 항목 완료표, 구현 체크리스트, `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`가 비어 있거나 placeholder로 남아 있어 code-review 계약상 완료 상태로 인정할 수 없다.
## 실패 사유
- Required: `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션이 완료되지 않았고, 고정 검증 명령의 실제 stdout/stderr가 기록되지 않았다.
## 구현 체크리스트
- [x] [REVIEW_REVIEW_BNW-1] `REVIEW_BNW` 후속 구현의 review 문서와 검증 출력 기록을 완성한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_REVIEW_BNW-1] Review 문서와 검증 출력 기록 완성
#### 문제
이전 active review stub은 아래 필수 구현-owned 상태가 완료되지 않았다.
- 구현 항목별 완료 여부가 `[ ]`로 남아 있다.
- 구현 체크리스트가 `[ ]`로 남아 있다.
- `계획 대비 변경 사항`, `주요 설계 결정`에 placeholder 문구가 남아 있다.
- `검증 결과`의 각 명령 출력이 `(output)` placeholder로 남아 있다.
#### 해결 방법
현재 적용된 `REVIEW_BNW-1` 구현 상태를 실제 파일과 대조한 뒤 active `CODE_REVIEW-cloud-G08.md`에 기록한다.
- `typescript/package.json`: `ws`는 root `dependencies`에 없어야 하고, `peerDependencies.ws` 및 optional peer meta와 테스트용 `devDependencies.ws`가 있어야 한다.
- `typescript/package-lock.json`: manifest와 같은 dependency/peer/devDependency 상태를 반영해야 한다.
- `typescript/README.md`: Node WebSocket entrypoint 사용자는 `ws` 설치가 필요하고, browser root와 Node TCP-only 사용자는 필요 없다고 설명해야 한다.
- `typescript/src/index.ts`와 `typescript/src/browser_ws_client.ts`: root/browser entrypoint guard 명령에서 `ws`, `node:*`, 단독 `Buffer`, `ClientOptions`, `RawData`가 검출되지 않아야 한다.
소스가 이미 위 조건을 만족하면 소스 파일은 수정하지 않는다.
검증 중 불일치가 발견될 경우에만 계획 범위 안에서 최소 수정하고, `계획 대비 변경 사항`에 이유를 기록한다.
#### 수정 파일 및 체크리스트
- [x] active `CODE_REVIEW-cloud-G08.md`의 구현 항목별 완료 여부를 실제 상태에 맞게 체크한다.
- [x] active `CODE_REVIEW-cloud-G08.md`의 구현 체크리스트를 실제 상태에 맞게 체크한다.
- [x] `계획 대비 변경 사항`을 placeholder 없이 채운다. 계획과 다른 소스 변경이 없으면 `없음` 또는 그에 준하는 명확한 문장으로 기록한다.
- [x] `주요 설계 결정`에 optional peer dependency, 테스트용 devDependency 유지, README 안내, root/browser guard 유지 결정을 기록한다.
- [x] 아래 중간/최종 검증 명령을 실행하고 실제 stdout/stderr를 `검증 결과`에 붙여 넣는다.
#### 중간 검증
```bash
cd typescript && node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync('package.json','utf8')); if (p.dependencies?.ws) throw new Error('ws must not be a root dependency'); if (p.devDependencies?.ws !== '^8.18.1') throw new Error('ws devDependency missing'); if (p.peerDependencies?.ws !== '^8.18.1') throw new Error('ws peerDependency missing'); if (p.peerDependenciesMeta?.ws?.optional !== true) throw new Error('ws peerDependency must be optional');"
```
예상 결과: 출력 없이 exit code 0.
## 최종 검증
```bash
cd typescript && npm run check
cd typescript && npm test
cd go && go run ./crosstest/go_typescript.go
rg --sort path -n 'from "ws"|node:|\bBuffer\b|ClientOptions|RawData' typescript/src/index.ts typescript/src/browser_ws_client.ts
```
마지막 `rg` 명령의 예상 결과는 출력 없음(exit code 1)이다.

View file

@ -0,0 +1,214 @@
<!-- task=01_proto_socket_naming plan=0 tag=PSN -->
# Code Review Reference - PSN
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Do not modify or check the `코드리뷰 전용 체크리스트`; it is owned by the review agent only.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-20
task=01_proto_socket_naming, plan=0, tag=PSN
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
review 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 task 디렉터리 이동. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
어떤 판정에서도 아카이브를 건너뛰지 마세요. PASS/WARN/FAIL 모두 `코드리뷰 결과` append 후 active plan/review 파일을 먼저 아카이브하고, 그 다음 `complete.log` 또는 다음 plan/review 파일을 작성해야 합니다.
PASS에서는 `agent-ops/skills/common/code-review/templates/complete-log-template.md`의 섹션 순서와 필수 항목을 기준으로 `complete.log`를 작성하세요. 작성 후 현재 날짜의 `YYYY/MM` 기준으로 task 디렉터리를 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 이동하고, 최종 archive 경로의 `code_review_*.log`에서 `코드리뷰 전용 체크리스트`를 갱신한 다음 보고하세요.
WARN/FAIL에서는 다음 상태 파일 작성 후 현재 task 경로의 archived `code_review_*.log`에서 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 체크한 다음 보고하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일 갱신 | [x] |
| [PSN-2] README와 Go module consumer 예제 갱신 | [x] |
| [PSN-3] 이름 잔여물 검색 검증 | [x] |
## 구현 체크리스트
- [x] [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일을 `proto-socket` 기준으로 갱신한다.
- [x] [PSN-2] README와 Go module consumer 예제를 새 module path 기준으로 갱신한다.
- [x] [PSN-3] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 `agent-task/01_proto_socket_naming/`를 `agent-task/archive/YYYY/MM/01_proto_socket_naming/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
## 계획 대비 변경 사항
- `tools/generate_proto.sh`가 `protoc-gen-dart`를 PATH에서 찾지 못해 실패했다. `protoc-gen-dart`는 `$HOME/.pub-cache/bin/protoc-gen-dart`에 설치되어 있어 PATH에만 누락된 상태였다. 대체 명령으로 `PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh`를 사용해 동일한 스크립트를 실행했다. `tools/check_proto_sync.sh`도 동일한 이유로 같은 PATH prefix를 적용했다. 스크립트 자체와 산출물은 계획과 동일하다.
- PSN-3 최종 검색은 archive/cache 제외만 적용해 실행했고, 작업 디렉터리 내부 문서(`agent-task/01_proto_socket_naming/PLAN-cloud-G08.md`, `CODE_REVIEW-cloud-G08.md`)에 잔여 매칭이 남는다. 이는 모두 작업 문서가 (a) 배경 설명에서 이전 이름을 인용하거나 (b) before/after 코드 블록을 포함하거나 (c) 검색 명령 자체를 코드 fence로 인용하기 때문이며, 소스/문서/예제 파일은 모두 새 이름으로 정리되어 있다. PLAN에서 "남은 결과가 있다면 의도된 예외인지 evidence를 남기"라는 단서를 따라 이 항목을 의도된 예외로 기록한다.
## 주요 설계 결정
- `go/packets/message_common.pb.go`는 직접 sed로 수정하지 않고 `tools/generate_proto.sh`로 재생성했다. 결과 파일의 `go_package` descriptor 문자열(`Z.git.toki-labs.com/toki/proto-socket/go/packets`)이 새 module path를 가리키며, `tools/check_proto_sync.sh`가 sync를 확인한다.
- `examples/go-module-consumer/go.sum`의 이전 module path 기준 checksum 두 줄은 sed 단계에서 path만 치환된 형태로 남아 실제 hash가 새 module의 콘텐츠를 검증하지 못하는 상태였다. 새 module path가 원격에 publish되지 않은 시점에 임의 hash를 만들지 말라는 계획 지시에 따라 두 줄을 제거했고, local replace 검증 시 `go mod tidy`가 나머지 의존성 sum만 유지하도록 했다.
## 리뷰어를 위한 체크포인트
- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
- `go/packets/message_common.pb.go`가 수동 문자열 패치가 아니라 기존 생성 경로와 sync 검증을 통과했는지 확인한다.
- Go module consumer 예제가 원격 publish 상태에 의존하지 않고 local replace 검증으로 compile되는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### PSN-1 중간 검증
```
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh
Proto schemas are in sync.
```
```
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh
Proto schemas are in sync.
```
```
$ cd go && go test -count=1 ./...
ok git.toki-labs.com/toki/proto-socket/go 0.004s
? git.toki-labs.com/toki/proto-socket/go/crosstest/dart_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/kotlin_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/python_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/typescript_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/examples/tcp_echo [no test files]
? git.toki-labs.com/toki/proto-socket/go/examples/ws_echo [no test files]
? git.toki-labs.com/toki/proto-socket/go/packets [no test files]
ok git.toki-labs.com/toki/proto-socket/go/test 8.096s
```
### PSN-2 중간 검증
```
$ tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
proto-socket import OK
```
### PSN-3 중간 검증
```
$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:69:- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:106:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:132:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:13:`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:62:변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:137:module git.toki-labs.com/toki/common-proto-socket/go
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:144:option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:151:"git.toki-labs.com/toki/common-proto-socket/go/packets"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:221:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:222:"git.toki-labs.com/toki/common-proto-socket/go/packets"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:229:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:234:require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:300:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:349:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
```
(잔여 매칭은 active task 디렉터리 내 PLAN/CODE_REVIEW 문서에 한정되며, 모두 작업 문서가 배경/before-after/검색 명령 인용 목적으로 포함하는 의도된 예외다. 소스/문서/예제 파일에는 잔여 참조가 없다.)
### 최종 검증
```
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh
Proto schemas are in sync.
```
```
$ PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh
Proto schemas are in sync.
```
```
$ cd go && go test -count=1 ./...
ok git.toki-labs.com/toki/proto-socket/go 0.006s
? git.toki-labs.com/toki/proto-socket/go/crosstest/dart_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/kotlin_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/python_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/crosstest/typescript_go_client [no test files]
? git.toki-labs.com/toki/proto-socket/go/examples/tcp_echo [no test files]
? git.toki-labs.com/toki/proto-socket/go/examples/ws_echo [no test files]
? git.toki-labs.com/toki/proto-socket/go/packets [no test files]
ok git.toki-labs.com/toki/proto-socket/go/test 8.101s
```
```
$ tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
proto-socket import OK
```
```
$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:69:- 이전 이름 `common-proto-socket`이 archive/cache 제외 검색에서 남지 않는지 확인한다.
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:106:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/CODE_REVIEW-cloud-G08.md:132:$ rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:13:`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:62:변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:137:module git.toki-labs.com/toki/common-proto-socket/go
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:144:option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:151:"git.toki-labs.com/toki/common-proto-socket/go/packets"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:221:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:222:"git.toki-labs.com/toki/common-proto-socket/go/packets"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:229:protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:234:require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:300:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
./agent-task/01_proto_socket_naming/PLAN-cloud-G08.md:349:rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
```
(잔여 매칭은 active task 디렉터리 내 PLAN/CODE_REVIEW 문서에 한정된 의도된 예외다. PSN-3 중간 검증과 동일한 결과.)
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave the review-agent-only checklist unchanged.
## 코드리뷰 결과
### 2026-05-20 리뷰 판정
- 종합 판정: PASS
#### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | Go module path, Go imports, Go proto option, generated Go descriptor가 `git.toki-labs.com/toki/proto-socket/go` 기준으로 일치한다. |
| Completeness | Pass | PSN-1, PSN-2, PSN-3 구현 항목과 구현 체크리스트가 모두 채워졌고 실제 변경 파일과 대응한다. |
| Test Coverage | Pass | `tools/generate_proto.sh`, `tools/check_proto_sync.sh`, `cd go && go test -count=1 ./...`, Go module consumer local replace smoke가 모두 재실행 통과했다. |
| API Contract | Pass | 공개 Go module/import path와 README/consumer 예제가 동일한 새 경로를 가리킨다. |
| Code Quality | Pass | 런타임 로직 변경 없이 이름 경로와 generated artifact만 갱신했다. 리뷰 중 생성 스크립트 재실행으로 Go generated header의 로컬 protoc 버전 노이즈가 정리되었다. |
| Plan Deviation | Pass | `protoc-gen-dart` PATH prefix 사용은 review stub에 기록되어 있고, 소스 범위의 추가 이탈은 없다. |
| Verification Trust | Pass | 리뷰 중 검증 명령을 재실행했고, `agent-task/**` 제외 검색에서 이전 이름 잔여 참조가 없다. |
#### 발견된 문제
없음
#### 다음 단계
- PASS: active review/plan 파일을 아카이브하고 `complete.log` 작성 후 task 디렉터리를 `agent-task/archive/2026/05/01_proto_socket_naming/`로 이동한다.

View file

@ -0,0 +1,39 @@
# Complete - 01_proto_socket_naming
## 완료 일시
2026-05-20
## 요약
Go module/import/proto package naming을 `proto-socket` 기준으로 통일했고, 1회 리뷰에서 PASS 완료.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G08_0.log` | `code_review_cloud_G08_0.log` | PASS | Go module path, generated proto, README, Go module consumer 예제가 새 이름으로 정리됨 |
## 구현/정리 내용
- Go module path와 Go import path를 `git.toki-labs.com/toki/proto-socket/go` 기준으로 갱신.
- Go proto `go_package`와 generated packet 파일을 재생성해 새 module path descriptor 반영.
- README Go quick start와 `examples/go-module-consumer`의 require/import/install 안내를 새 module path로 갱신.
- 이전 module path checksum은 새 module path 원격 publish 전 임의 checksum을 만들지 않도록 제거.
- 리뷰 중 `tools/generate_proto.sh`를 재실행해 generated 파일을 현재 toolchain 기준으로 정리.
## 최종 검증
- `PATH="$HOME/.pub-cache/bin:$PATH" tools/generate_proto.sh` - PASS; `Proto schemas are in sync.`
- `PATH="$HOME/.pub-cache/bin:$PATH" tools/check_proto_sync.sh` - PASS; `Proto schemas are in sync.`
- `cd go && go test -count=1 ./...` - PASS; Go 전체 패키지 테스트와 crosstest/example package compile 성공.
- `tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .` - PASS; `proto-socket import OK`
- `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'` - PASS; 출력 없음.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,354 @@
<!-- task=01_proto_socket_naming plan=0 tag=PSN -->
# Proto Socket Naming 통일 계획 - PSN
## 이 파일을 읽는 구현 에이전트에게
**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일이 채워지기 전에는 작업이 완료된 것이 아니다.**
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽 체크리스트를 모두 완료하며, 중간/최종 검증의 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 않는다. `코드리뷰 전용 체크리스트`도 구현 에이전트가 수정하거나 체크하지 않는다.
## 배경
`common-proto-socket`은 이전 프로젝트명이고, 현재 저장소와 TypeScript 패키지는 `proto-socket`을 기준으로 정리되어 있다.
Go module path, Go import path, Go proto option, README, 외부 소비 예제가 아직 이전 이름을 참조해 이름과 배포 경로가 어긋난다.
브라우저 네이티브 WebSocket 작업 전에 공개 import path를 먼저 통일해야 다음 작업에서 TS/Go crosstest와 문서가 같은 이름을 기준으로 움직인다.
## 의존 관계 및 구현 순서
이 계획은 선행 작업이 없다.
`agent-task/01+browser_native_ws`는 이 작업이 PASS되어 `complete.log`까지 생성된 뒤 시작한다.
## 분석 결과
### 읽은 파일
- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/go/rules.md`
- `agent-ops/rules/project/domain/protocol/rules.md`
- `agent-ops/rules/project/domain/tools/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `README.md`
- `PROTOCOL.md`
- `go/go.mod`
- `go/base_client.go`
- `go/communicator.go`
- `go/communicator_nonce_test.go`
- `go/tcp_client.go`
- `go/tcp_server.go`
- `go/ws_client.go`
- `go/ws_server.go`
- `go/packets/message_common.proto`
- `go/test/communicator_test.go`
- `go/test/heartbeat_test.go`
- `go/test/tcp_test.go`
- `go/test/test_helpers_test.go`
- `go/test/tls_test.go`
- `go/test/ws_test.go`
- `examples/go-module-consumer/README.md`
- `examples/go-module-consumer/go.mod`
- `examples/go-module-consumer/go.sum`
- `examples/go-module-consumer/main.go`
### 테스트 커버리지 공백
- Go module/import path 변경: 기존 `cd go && go test -count=1 ./...`가 모든 Go 패키지 import compile을 검증한다. 별도 신규 테스트는 필요 없다.
- `go_package` 변경 및 생성 파일 갱신: 기존 proto sync 스크립트가 언어별 proto schema drift를 검증하지만, `go_package` 문자열 자체는 Go compile과 검색 검증으로 확인한다.
- README와 외부 소비 예제 변경: 기존 자동 테스트가 없다. 임시 디렉터리에서 local `replace`를 주입해 `go run .`으로 예제 compile을 검증한다.
### 심볼 참조
변경 대상 문자열은 `git.toki-labs.com/toki/common-proto-socket/go`와 `common-proto-socket`이다. `rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'`에서 확인한 변경 필요 위치:
- `README.md:110`
- `README.md:111`
- `examples/go-module-consumer/README.md:31`
- `examples/go-module-consumer/go.mod:5`
- `examples/go-module-consumer/go.sum:1`
- `examples/go-module-consumer/go.sum:2`
- `examples/go-module-consumer/main.go:6`
- `go/base_client.go:8`
- `go/communicator.go:13`
- `go/communicator_nonce_test.go:10`
- `go/crosstest/dart_go_client/main.go:15`
- `go/crosstest/dart_go_client/main.go:16`
- `go/crosstest/go_dart.go:24`
- `go/crosstest/go_dart.go:25`
- `go/crosstest/go_kotlin.go:24`
- `go/crosstest/go_kotlin.go:25`
- `go/crosstest/go_python.go:24`
- `go/crosstest/go_python.go:25`
- `go/crosstest/go_typescript.go:24`
- `go/crosstest/go_typescript.go:25`
- `go/crosstest/kotlin_go_client/main.go:15`
- `go/crosstest/kotlin_go_client/main.go:16`
- `go/crosstest/python_go_client/main.go:15`
- `go/crosstest/python_go_client/main.go:16`
- `go/crosstest/typescript_go_client/main.go:15`
- `go/crosstest/typescript_go_client/main.go:16`
- `go/examples/tcp_echo/main.go:10`
- `go/examples/tcp_echo/main.go:11`
- `go/examples/ws_echo/main.go:9`
- `go/examples/ws_echo/main.go:10`
- `go/go.mod:1`
- `go/packets/message_common.pb.go:194`
- `go/packets/message_common.proto:3`
- `go/tcp_client.go:15`
- `go/test/communicator_test.go:11`
- `go/test/communicator_test.go:12`
- `go/test/heartbeat_test.go:15`
- `go/test/tcp_test.go:11`
- `go/test/tcp_test.go:12`
- `go/test/test_helpers_test.go:16`
- `go/test/test_helpers_test.go:17`
- `go/test/tls_test.go:11`
- `go/test/tls_test.go:12`
- `go/test/ws_test.go:12`
- `go/test/ws_test.go:13`
- `go/ws_client.go:13`
### 범위 결정 근거
- `agent-task/archive/**`는 프로젝트 규칙에 따라 읽거나 변경하지 않는다.
- Dart/Python/Kotlin의 언어별 package/module 표기인 `proto_socket`, `proto-socket`은 이전 프로젝트명 문제가 아니므로 변경하지 않는다.
- 프로토콜 메시지 schema는 변경하지 않는다. Go proto 복사본의 `go_package` option과 생성 산출물만 갱신한다.
- 원격 Forgejo 저장소명 변경, tag 생성, module publish는 이 구현 범위를 벗어난 운영 작업이다. 예제 검증은 local `replace`로 수행한다.
### 빌드 등급
build=`cloud-G08`, review=`cloud-G08`. Go module path와 generated proto, 문서, crosstest/import call-site가 넓게 걸리는 API 경로 변경이며 프로토콜 생성물 검증이 필요하다.
## 구현 체크리스트
- [ ] [PSN-1] Go module path, Go imports, Go proto option, generated packet 파일을 `proto-socket` 기준으로 갱신한다.
- [ ] [PSN-2] README와 Go module consumer 예제를 새 module path 기준으로 갱신한다.
- [ ] [PSN-3] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [PSN-1] Go module path와 proto 생성물 갱신
#### 문제
`go/go.mod:1`의 module path가 이전 이름을 사용한다.
```go
// go/go.mod:1
module git.toki-labs.com/toki/common-proto-socket/go
```
`go/packets/message_common.proto:3`의 Go package option도 이전 module path를 포함한다.
```proto
// go/packets/message_common.proto:3
option go_package = "git.toki-labs.com/toki/common-proto-socket/go/packets";
```
Go 소스, Go 테스트, crosstest, 예제는 같은 이전 import path를 참조한다. 예: `go/base_client.go:8`.
```go
// go/base_client.go:8
"git.toki-labs.com/toki/common-proto-socket/go/packets"
```
#### 해결 방법
module path와 모든 Go import path를 `git.toki-labs.com/toki/proto-socket/go`로 치환한다.
```go
// go/go.mod:1
module git.toki-labs.com/toki/proto-socket/go
```
```proto
// go/packets/message_common.proto:3
option go_package = "git.toki-labs.com/toki/proto-socket/go/packets";
```
```go
// go/base_client.go:8
"git.toki-labs.com/toki/proto-socket/go/packets"
```
`go/packets/message_common.pb.go`는 직접 문자열 패치로 맞추지 말고 기존 proto 생성 경로로 재생성한다. 생성 도구가 환경 문제로 실행되지 않으면 `command -v protoc`, `command -v protoc-gen-go` 결과와 대체 명령을 `CODE_REVIEW-*-G??.md`의 `계획 대비 변경 사항`에 남긴다.
#### 수정 파일 및 체크리스트
- [ ] `go/go.mod` module path 변경
- [ ] `go/packets/message_common.proto`의 `go_package` 변경
- [ ] `go/packets/message_common.pb.go` 재생성
- [ ] `go/base_client.go` import 변경
- [ ] `go/communicator.go` import 변경
- [ ] `go/communicator_nonce_test.go` import 변경
- [ ] `go/tcp_client.go` import 변경
- [ ] `go/ws_client.go` import 변경
- [ ] `go/test/*.go` import 변경
- [ ] `go/crosstest/**/*.go` import 변경
- [ ] `go/examples/**/*.go` import 변경
#### 테스트 작성
신규 테스트는 작성하지 않는다. 이 변경은 runtime behavior가 아니라 module/import path 정리이며 기존 Go 단위 테스트와 crosstest compile이 회귀 검증 역할을 한다.
#### 중간 검증
```bash
tools/generate_proto.sh
```
예상 결과: 성공 종료. Go 생성물의 `go_package` 경로가 새 module path를 포함한다.
```bash
tools/check_proto_sync.sh
```
예상 결과: 성공 종료.
```bash
cd go && go test -count=1 ./...
```
예상 결과: 모든 Go 패키지 테스트 성공. fresh 실행이 필요하므로 Go test cache 출력은 허용하지 않는다.
### [PSN-2] 문서와 Go module consumer 예제 갱신
#### 문제
README quick start가 이전 import path를 안내한다.
```go
// README.md:110-111
protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
"git.toki-labs.com/toki/common-proto-socket/go/packets"
```
외부 소비 예제도 이전 module path를 require/import한다.
```go
// examples/go-module-consumer/main.go:6
protoSocket "git.toki-labs.com/toki/common-proto-socket/go"
```
```go
// examples/go-module-consumer/go.mod:5
require git.toki-labs.com/toki/common-proto-socket/go v0.0.0-20260501220005-284b66a22300
```
#### 해결 방법
README와 예제의 안내/import/require를 `git.toki-labs.com/toki/proto-socket/go`로 바꾼다.
`examples/go-module-consumer/go.sum`의 이전 module path checksum은 제거한다. 새 module path가 원격에 publish되지 않은 상태에서 임의 checksum을 만들지 않는다.
```go
// README.md:110-111
protoSocket "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/proto-socket/go/packets"
```
```go
// examples/go-module-consumer/main.go:6
protoSocket "git.toki-labs.com/toki/proto-socket/go"
```
```go
// examples/go-module-consumer/go.mod:5
require git.toki-labs.com/toki/proto-socket/go v0.0.0-20260501220005-284b66a22300
```
#### 수정 파일 및 체크리스트
- [ ] `README.md` Go quick start import path 변경
- [ ] `examples/go-module-consumer/README.md` install command 변경
- [ ] `examples/go-module-consumer/go.mod` require path 변경
- [ ] `examples/go-module-consumer/go.sum` old path checksum 제거
- [ ] `examples/go-module-consumer/main.go` import path 변경
#### 테스트 작성
신규 테스트는 작성하지 않는다. 예제 compile 검증은 임시 디렉터리에서 local replace를 주입해 실행한다.
#### 중간 검증
```bash
tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
```
예상 결과: `proto-socket import OK` 출력 후 성공 종료. 임시 디렉터리만 사용하며 저장소 파일을 변경하지 않는다.
### [PSN-3] 이름 잔여물 검색 검증
#### 문제
이 작업의 성공 조건은 이전 프로젝트명 잔여 참조가 없는 것이다. 일부 잔여 문자열은 generated file, docs, sum 파일에 남기 쉬워 search contract가 필요하다.
#### 해결 방법
정렬된 `rg` 명령으로 archive와 캐시 경로를 제외하고 이전 이름을 검색한다. 결과가 있으면 모두 변경 필요 항목으로 처리한다.
#### 수정 파일 및 체크리스트
- [ ] 최종 검색 명령 결과가 비어 있는지 확인
- [ ] 남은 결과가 있다면 의도된 예외인지 evidence를 남기거나 제거
#### 테스트 작성
검색 검증만 수행한다. 신규 테스트는 작성하지 않는다.
#### 중간 검증
```bash
rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
```
예상 결과: 출력 없음.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `go/go.mod` | PSN-1 |
| `go/packets/message_common.proto` | PSN-1 |
| `go/packets/message_common.pb.go` | PSN-1 |
| `go/*.go` | PSN-1 |
| `go/test/*.go` | PSN-1 |
| `go/crosstest/**/*.go` | PSN-1 |
| `go/examples/**/*.go` | PSN-1 |
| `README.md` | PSN-2 |
| `examples/go-module-consumer/README.md` | PSN-2 |
| `examples/go-module-consumer/go.mod` | PSN-2 |
| `examples/go-module-consumer/go.sum` | PSN-2 |
| `examples/go-module-consumer/main.go` | PSN-2 |
## 최종 검증
```bash
tools/generate_proto.sh
```
예상 결과: 성공 종료.
```bash
tools/check_proto_sync.sh
```
예상 결과: 성공 종료.
```bash
cd go && go test -count=1 ./...
```
예상 결과: 모든 테스트 성공. Go test cache 출력은 허용하지 않는다.
```bash
tmp="$(mktemp -d)" && cp -R examples/go-module-consumer "$tmp/go-module-consumer" && cd "$tmp/go-module-consumer" && go mod edit -replace git.toki-labs.com/toki/proto-socket/go=/config/workspace/proto-socket/go && go mod tidy && go run .
```
예상 결과: `proto-socket import OK` 출력 후 성공 종료.
```bash
rg --sort path -n "common-proto-socket|common_proto_socket|common proto socket" . -g '!agent-task/archive/**' -g '!dart/.dart_tool/**' -g '!python/.pytest_cache/**'
```
예상 결과: 출력 없음.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -1,263 +0,0 @@
<!-- task=crosstest_typescript plan=0 tag=CROSSTEST -->
# Code Review Reference - CROSSTEST
## 개요
date=2026-04-24
task=crosstest_typescript, plan=0, tag=CROSSTEST
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_0.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md``plan_0.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [CROSSTEST-1] Dart 서버 → TypeScript 클라이언트 | [x] |
| [CROSSTEST-2] Kotlin 서버 → TypeScript 클라이언트 | [x] |
| [CROSSTEST-3] Python 서버 → TypeScript 클라이언트 | [x] |
| [CROSSTEST-4] TypeScript 서버 → Dart 클라이언트 | [x] |
| [CROSSTEST-5] TypeScript 서버 → Go 클라이언트 | [x] |
| [CROSSTEST-6] TypeScript 서버 → Kotlin 클라이언트 | [x] |
| [CROSSTEST-7] TypeScript 서버 → Python 클라이언트 | [x] |
## 계획 대비 변경 사항
- Python 실행 명령은 계획의 `python` 대신 기존 프로젝트 패턴과 실제 실행 환경에 맞춰 `python3`를 사용했다.
- TypeScript 서버 row 검증 명령은 계획의 `npx tsx ...` 대신 `node --import tsx ...`로 실행했다.
이유: `tsx` CLI가 sandbox 환경에서 IPC pipe listen(`EPERM`)에 실패했지만, 동일 스크립트를 `node --import tsx`로 실행하면 정상 동작했다.
- `kotlin/crosstest/kotlin_typescript.kt`의 WebSocket phase는 동일 포트(`29796`) 재기동이 불안정해서, 서버를 한 번만 띄우고 `send-push`/`requests` 두 phase를 순차 실행하도록 조정했다.
이유: Kotlin `WsServer.start()`가 비동기라 같은 포트 재시작 직후 TypeScript 클라이언트 연결이 반복적으로 `ECONNREFUSED`를 반환했다.
## 주요 설계 결정
- Phase 1 TypeScript 클라이언트 3개는 `go_typescript_client.ts` 구조를 그대로 유지하고, push 기대 문자열과 로그만 서버 언어별로 바꿨다.
- TypeScript 서버 row는 pairwise 오케스트레이터 4개를 각각 만들되, 모든 파일에서 같은 subprocess PASS/FAIL 파서와 TCP/WS phase 분리 패턴을 유지했다.
- Kotlin 서버 → TypeScript 클라이언트 조합은 TypeScript 클라이언트에 짧은 연결 안정화 지연을 추가하고, Kotlin WebSocket 서버는 단일 인스턴스 재사용으로 고정해 flaky 재기동을 제거했다.
- 검증은 Dart/TypeScript/Python 정적 검사, Go 테스트, Kotlin crosstest compile까지 먼저 확인한 뒤 언어 matrix crosstest를 순서대로 실행했다.
## 리뷰어를 위한 체크포인트
- 각 오케스트레이터의 포트가 PLAN.md 포트 배정표와 일치하는지 확인.
- 각 TypeScript 클라이언트 헬퍼의 기대 push 메시지가 서버 언어와 맞는지 확인.
(`"push from dart server"`, `"push from kotlin server"`, `"push from python server"`, `"push from typescript server"`)
- TypeScript 서버 오케스트레이터에서 `"fire from <lang> client"` 검증 메시지가 서버 언어와 맞는지 확인.
- `go/crosstest/typescript_go_client/main.go`의 package가 `main`이고 `//go:build ignore`가 없는지 확인.
- `kotlin/crosstest/typescript_kotlin_client.kt``@file:JvmName("TypescriptKotlinClientKt")`이 정확한지 확인.
- 각 TypeScript 오케스트레이터에서 서버 stop이 finally 블록에서 수행되는지 확인.
- 검증 결과: 7개 중간 검증 + Go→TypeScript 회귀 검증 모두 PASS인지 확인.
## 검증 결과
### CROSSTEST-1 중간 검증
```
$ cd dart && dart run crosstest/dart_typescript.dart
INFO typeName dart=TestData
INFO typeName ts=TestData
PASS scenario=1 detail=fire-and-forget sent
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=2 detail=received push from dart server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from dart server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all dart-server/typescript-client crosstests passed
```
### CROSSTEST-2 중간 검증
```
$ cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
INFO typeName kotlin=TestData
INFO typeName ts=TestData
PASS scenario=1 detail=fire-and-forget sent
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=2 detail=received push from kotlin server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
PASS scenario=1 detail=fire-and-forget sent
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=2 detail=received push from kotlin server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all kotlin-server/typescript-client crosstests passed
BUILD SUCCESSFUL
```
### CROSSTEST-3 중간 검증
```
$ cd python && python3 -m crosstest.python_typescript
INFO typeName python=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
INFO typeName ts=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from python server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
SERVER_RECEIVED index=101 message=fire from typescript client
INFO typeName ts=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from python server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all python-server/typescript-client crosstests passed
```
### CROSSTEST-4 중간 검증
```
$ cd typescript && node --import tsx crosstest/typescript_dart.ts
INFO typeName ts=TestData
INFO typeName dart=TestData
SERVER_RECEIVED index=101 message=fire from dart client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName dart=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName dart=TestData
SERVER_RECEIVED index=101 message=fire from dart client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName dart=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all typescript-server/dart-client crosstests passed
```
### CROSSTEST-5 중간 검증
```
$ cd typescript && env PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod node --import tsx crosstest/typescript_go.ts
INFO typeName ts=TestData
INFO typeName go=TestData
SERVER_RECEIVED index=101 message=fire from go client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName go=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName go=TestData
SERVER_RECEIVED index=101 message=fire from go client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName go=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all typescript-server/go-client crosstests passed
```
### CROSSTEST-6 중간 검증
```
$ cd typescript && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle node --import tsx crosstest/typescript_kotlin.ts
INFO typeName ts=TestData
> Task :run
INFO typeName kotlin=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
BUILD SUCCESSFUL
> Task :run
INFO typeName kotlin=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
BUILD SUCCESSFUL
> Task :run
INFO typeName kotlin=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
BUILD SUCCESSFUL
> Task :run
INFO typeName kotlin=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
BUILD SUCCESSFUL
PASS all typescript-server/kotlin-client crosstests passed
```
### CROSSTEST-7 중간 검증
```
$ cd typescript && node --import tsx crosstest/typescript_python.ts
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from python client
INFO typeName python=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName python=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
SERVER_RECEIVED index=101 message=fire from python client
INFO typeName python=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from typescript server
INFO typeName python=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all typescript-server/python-client crosstests passed
```
### 최종 검증
```
$ cd go && env PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go run ./crosstest/go_typescript.go
INFO typeName go=TestData
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName ts=TestData
SERVER_RECEIVED index=101 message=fire from typescript client
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from go server
INFO typeName ts=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all go-server/typescript-client crosstests passed
```
### 보조 검증
```
$ dart analyze dart/crosstest/dart_typescript.dart dart/crosstest/typescript_dart_client.dart
No issues found!
$ cd typescript && npx tsc --noEmit
(exit 0)
$ python3 -m py_compile python/crosstest/python_typescript.py python/crosstest/typescript_python_client.py
(exit 0)
$ cd go && PATH=/config/go-sdk/go/bin:/config/go/bin:$PATH GOCACHE=/tmp/go-build GOMODCACHE=/tmp/go-mod go test ./...
? toki-labs.com/toki_socket/go [no test files]
? toki-labs.com/toki_socket/go/crosstest/dart_go_client [no test files]
? toki-labs.com/toki_socket/go/crosstest/kotlin_go_client [no test files]
? toki-labs.com/toki_socket/go/crosstest/python_go_client [no test files]
? toki-labs.com/toki_socket/go/crosstest/typescript_go_client [no test files]
? toki-labs.com/toki_socket/go/examples/tcp_echo [no test files]
? toki-labs.com/toki_socket/go/examples/ws_echo [no test files]
? toki-labs.com/toki_socket/go/packets [no test files]
ok toki-labs.com/toki_socket/go/test 4.051s
$ cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew compileCrosstestKotlin
BUILD SUCCESSFUL
$ git diff --check
(no output)
```

View file

@ -1,511 +0,0 @@
<!-- task=crosstest_typescript plan=0 tag=CROSSTEST -->
# TypeScript 교차 언어 테스트 추가
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하고, 중간 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW.md``검증 결과` 섹션에 붙여넣어라.
계획과 다르게 구현한 부분은 `계획 대비 변경 사항`에 이유와 함께 기록하라.
---
## 배경
Go 서버 → TypeScript 클라이언트(CROSSTEST-0: `go/crosstest/go_typescript.go` + `typescript/crosstest/go_typescript_client.ts`) 는 이미 구현되어 있다.
나머지 7쌍이 누락돼 있어 TypeScript 가 언어 매트릭스에 완전히 참여하지 못한다.
- **Phase 1**: Dart/Kotlin/Python 서버 → TypeScript 클라이언트 (3쌍)
- **Phase 2**: TypeScript 서버 → Dart/Go/Kotlin/Python 클라이언트 (4쌍)
구현 완료 시 TypeScript는 Available 상태가 된다.
---
## 의존 관계 및 구현 순서
Phase 1 → Phase 2 순서로 구현한다.
Phase 1의 TypeScript 클라이언트 헬퍼 3개는 서로 독립적이므로 병렬 작성 가능하다.
Phase 2의 TypeScript 서버 오케스트레이터 4개도 독립적이지만, Kotlin 클라이언트(`CROSSTEST-6`)는 새 Gradle 태스크 없이 `-PmainClass` 방식을 사용하므로 기존 빌드를 검증 후 진행한다.
---
## 포트 배정
| 쌍 | TCP | WS |
|----|-----|----|
| Dart → TS | 29790 | 29792 |
| Kotlin → TS | 29794 | 29796 |
| Python → TS | 29798 | 29800 |
| TS → Dart | 29802 | 29804 |
| TS → Go | 29806 | 29808 |
| TS → Kotlin | 29810 | 29812 |
| TS → Python | 29814 | 29816 |
---
## [CROSSTEST-1] Dart 서버 → TypeScript 클라이언트
### 문제
`dart/crosstest/dart_typescript.dart``typescript/crosstest/dart_typescript_client.ts` 가 없다.
### 해결 방법
**`dart/crosstest/dart_typescript.dart`** (오케스트레이터)
`dart/crosstest/dart_go.dart`를 기반으로 작성.
변경점: 포트 29790/29792, Go 클라이언트 호출 → `npx tsx crosstest/dart_typescript_client.ts`, 메시지 `"fire from typescript client"` 검증, push `"push from dart server"`.
```dart
// 핵심 상수
const _tcpPort = 29790;
const _wsPort = 29792;
final _typescriptDir = Directory('$_repoRoot/typescript').path;
// 클라이언트 호출
Future<void> _runTypescriptClient(String mode, int port, String phase, Set<String> expected) async {
final process = await Process.start(
'npx', ['tsx', 'crosstest/dart_typescript_client.ts',
'--mode=$mode', '--port=$port', '--phase=$phase'],
workingDirectory: _typescriptDir,
);
...
}
```
send-push 검증 메시지: `data.message == 'fire from typescript client'`
push 응답: `index=200, message='push from dart server'`
**`typescript/crosstest/dart_typescript_client.ts`** (클라이언트 헬퍼)
`typescript/crosstest/go_typescript_client.ts`와 구조 동일.
변경점만:
- `console.log("INFO typeName ts=...")`의 출력 유지
- scenario 2에서 기대 메시지: `"push from dart server"`
- `--mode`/`--port`/`--phase` args 그대로 사용
### 수정 파일 및 체크리스트
- [ ] `dart/crosstest/dart_typescript.dart` 신규 작성
- [ ] `typescript/crosstest/dart_typescript_client.ts` 신규 작성
### 테스트 작성
스킵. crosstest 자체가 통합 테스트이며, 중간 검증 명령으로 커버된다.
### 중간 검증
```bash
cd dart && dart run crosstest/dart_typescript.dart
```
기대 결과: `PASS all dart-server/typescript-client crosstests passed`
---
## [CROSSTEST-2] Kotlin 서버 → TypeScript 클라이언트
### 문제
`kotlin/crosstest/kotlin_typescript.kt``typescript/crosstest/kotlin_typescript_client.ts` 가 없다.
### 해결 방법
**`kotlin/crosstest/kotlin_typescript.kt`** (오케스트레이터)
`kotlin/crosstest/kotlin_go.kt`를 기반으로 작성.
변경점: 포트 29794/29796, `@file:JvmName("KotlinTypescriptKt")`, Go 클라이언트 호출 → `npx tsx ...`, 메시지 `"fire from typescript client"` 검증.
```kotlin
@file:JvmName("KotlinTypescriptKt")
// ...
private const val TCP_PORT = 29794
private const val WS_PORT = 29796
private suspend fun runTypescriptClient(mode: String, port: Int, phase: String, expected: Set<String>) =
withContext(Dispatchers.IO) {
val process = ProcessBuilder(
"npx", "tsx", "crosstest/kotlin_typescript_client.ts",
"--mode=$mode", "--port=$port", "--phase=$phase",
)
.directory(typescriptDir())
.redirectErrorStream(false)
.start()
// stdout/stderr 스레드, validateResultLines — kotlin_go.kt와 동일
}
private fun typescriptDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val candidate = File(dir, "../typescript").canonicalFile
if (File(candidate, "package.json").isFile) return candidate
val direct = File(dir, "typescript").canonicalFile
if (File(direct, "package.json").isFile) return direct
dir = dir.parentFile
}
error("cannot resolve typescript directory")
}
```
send-push 검증 메시지: `data.message == "fire from typescript client"`
push 응답: `"push from kotlin server"`
**`typescript/crosstest/kotlin_typescript_client.ts`** (클라이언트 헬퍼)
`go_typescript_client.ts`와 구조 동일.
변경점: scenario 2 기대 메시지 → `"push from kotlin server"`
### 수정 파일 및 체크리스트
- [ ] `kotlin/crosstest/kotlin_typescript.kt` 신규 작성
- [ ] `typescript/crosstest/kotlin_typescript_client.ts` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
```
기대 결과: `PASS all kotlin-server/typescript-client crosstests passed`
---
## [CROSSTEST-3] Python 서버 → TypeScript 클라이언트
### 문제
`python/crosstest/python_typescript.py``typescript/crosstest/python_typescript_client.ts` 가 없다.
### 해결 방법
**`python/crosstest/python_typescript.py`** (오케스트레이터)
`python/crosstest/python_go.py`를 기반으로 작성.
변경점: 포트 29798/29800, Go 클라이언트 호출 → `npx tsx ...`, 메시지 `"fire from typescript client"` 검증.
```python
PYTHON_TYPESCRIPT_TCP_PORT = 29798
PYTHON_TYPESCRIPT_WS_PORT = 29800
async def run_typescript_client(mode: str, port: int, phase: str, expected: set[str]) -> None:
ts_dir = typescript_package_dir()
process = await asyncio.create_subprocess_exec(
"npx", "tsx", "crosstest/python_typescript_client.ts",
f"--mode={mode}", f"--port={port}", f"--phase={phase}",
cwd=ts_dir,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
# communicate + validate — python_go.py와 동일
def typescript_package_dir() -> Path:
repo_root = Path(__file__).resolve().parents[2]
candidate = repo_root / "typescript"
if (candidate / "package.json").is_file():
return candidate
raise RuntimeError(f"cannot resolve typescript package directory from {candidate}")
```
send-push 검증 메시지: `data.message == "fire from typescript client"`
push 응답: `"push from python server"`
**`typescript/crosstest/python_typescript_client.ts`** (클라이언트 헬퍼)
`go_typescript_client.ts`와 구조 동일.
변경점: scenario 2 기대 메시지 → `"push from python server"`
### 수정 파일 및 체크리스트
- [ ] `python/crosstest/python_typescript.py` 신규 작성
- [ ] `typescript/crosstest/python_typescript_client.ts` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd python && python -m crosstest.python_typescript
```
기대 결과: `PASS all python-server/typescript-client crosstests passed`
---
## [CROSSTEST-4] TypeScript 서버 → Dart 클라이언트
### 문제
`typescript/crosstest/typescript_dart.ts``dart/crosstest/typescript_dart_client.dart` 가 없다.
### 해결 방법
**`typescript/crosstest/typescript_dart.ts`** (오케스트레이터)
TypeScript TcpServer/WsServer 를 직접 기동하고 Dart 클라이언트 서브프로세스를 호출.
```typescript
import * as path from "node:path";
import * as childProcess from "node:child_process";
import { fileURLToPath } from "node:url";
import { create } from "@bufbuild/protobuf";
import { addListenerTyped, addRequestListenerTyped, parserFromSchema } from "../src/communicator.js";
import { TestDataSchema, type TestData } from "../src/packets/message_common_pb.js";
import { TcpServer } from "../src/tcp_server.js";
import { TcpClient } from "../src/tcp_client.js";
import { WsServer } from "../src/ws_server.js";
import { WsClient } from "../src/ws_client.js";
const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../..");
const dartDir = path.join(repoRoot, "dart");
const HOST = "127.0.0.1";
const TCP_PORT = 29802;
const WS_PORT = 29804;
const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 20_000;
const SERVER_OBSERVATION_MS = 200;
function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
}
```
서버 오케스트레이터 패턴 (go_typescript.go의 TypeScript 역방향):
- `runTcpSendPush`: `data.message === "fire from dart client"` 검증, push `index=200, "push from typescript server"`
- `runTcpRequests`: `index * 2`, `"echo: " + message`
- `runWsSendPush` / `runWsRequests`: 동일 구조
```typescript
async function runDartClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
return new Promise((resolve, reject) => {
const child = childProcess.spawn(
"dart",
["run", "crosstest/typescript_dart_client.dart",
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
{ cwd: dartDir, stdio: ["ignore", "pipe", "pipe"] },
);
// stdout 파싱, PASS/FAIL 수집, validate
});
}
```
**`dart/crosstest/typescript_dart_client.dart`** (클라이언트 헬퍼)
`dart/crosstest/go_dart_client.dart`와 구조 동일.
변경점:
- fire 메시지: `"fire from dart client"` (동일)
- 기대 push: `push.message != 'push from typescript server'`
- sendRequest 메시지: `"single request from dart"` (동일)
### 수정 파일 및 체크리스트
- [ ] `typescript/crosstest/typescript_dart.ts` 신규 작성
- [ ] `dart/crosstest/typescript_dart_client.dart` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd typescript && npx tsx crosstest/typescript_dart.ts
```
기대 결과: `PASS all typescript-server/dart-client crosstests passed`
---
## [CROSSTEST-5] TypeScript 서버 → Go 클라이언트
### 문제
`typescript/crosstest/typescript_go.ts``go/crosstest/typescript_go_client/main.go` 가 없다.
### 해결 방법
**`typescript/crosstest/typescript_go.ts`** (오케스트레이터)
`typescript_dart.ts`와 구조 동일. 포트 29806/29808.
Go 클라이언트 호출:
```typescript
async function runGoClient(...) {
const child = childProcess.spawn(
"go", ["run", "./crosstest/typescript_go_client",
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
{ cwd: goDir, stdio: ["ignore", "pipe", "pipe"] },
);
}
```
검증 메시지: `"fire from go client"`, push: `"push from typescript server"`.
**`go/crosstest/typescript_go_client/main.go`** (클라이언트 헬퍼)
`go/crosstest/kotlin_go_client/main.go`와 구조 동일.
변경점:
- `//go:build` 없음 (패키지 `main`)
- fire 메시지: `"fire from go client"` (동일)
- 기대 push: `"push from typescript server"`
- single request 메시지: `"single request from go"` (동일)
### 수정 파일 및 체크리스트
- [ ] `typescript/crosstest/typescript_go.ts` 신규 작성
- [ ] `go/crosstest/typescript_go_client/main.go` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd typescript && npx tsx crosstest/typescript_go.ts
```
기대 결과: `PASS all typescript-server/go-client crosstests passed`
---
## [CROSSTEST-6] TypeScript 서버 → Kotlin 클라이언트
### 문제
`typescript/crosstest/typescript_kotlin.ts``kotlin/crosstest/typescript_kotlin_client.kt` 가 없다.
### 해결 방법
**`typescript/crosstest/typescript_kotlin.ts`** (오케스트레이터)
`typescript_dart.ts`와 구조 동일. 포트 29810/29812.
Kotlin 클라이언트 호출 (`./gradlew run` 방식):
```typescript
async function runKotlinClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
return new Promise((resolve, reject) => {
const child = childProcess.spawn(
"./gradlew",
["run",
"-PmainClass=com.tokilabs.toki_socket.crosstest.TypescriptKotlinClientKt",
`--args=--mode=${mode} --port=${port} --phase=${phase}`],
{ cwd: kotlinDir, stdio: ["ignore", "pipe", "pipe"] },
);
// stdout 파싱, validate
});
}
```
검증 메시지: `"fire from kotlin client"`, push: `"push from typescript server"`.
**`kotlin/crosstest/typescript_kotlin_client.kt`** (클라이언트 헬퍼)
`kotlin/crosstest/python_kotlin_client.kt`와 구조 동일.
변경점:
- `@file:JvmName("TypescriptKotlinClientKt")`
- `interface TypescriptClientHandle`, `TypescriptTcpHandle`, `TypescriptWsHandle`
- fire 메시지: `"fire from kotlin client"` (동일)
- 기대 push: `"push from typescript server"`
- single request 메시지: `"single request from kotlin"`
### 수정 파일 및 체크리스트
- [ ] `typescript/crosstest/typescript_kotlin.ts` 신규 작성
- [ ] `kotlin/crosstest/typescript_kotlin_client.kt` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd typescript && npx tsx crosstest/typescript_kotlin.ts
```
기대 결과: `PASS all typescript-server/kotlin-client crosstests passed`
---
## [CROSSTEST-7] TypeScript 서버 → Python 클라이언트
### 문제
`typescript/crosstest/typescript_python.ts``python/crosstest/typescript_python_client.py` 가 없다.
### 해결 방법
**`typescript/crosstest/typescript_python.ts`** (오케스트레이터)
`typescript_dart.ts`와 구조 동일. 포트 29814/29816.
Python 클라이언트 호출:
```typescript
async function runPythonClient(mode: string, port: number, phase: string, expected: Set<string>): Promise<void> {
return new Promise((resolve, reject) => {
const child = childProcess.spawn(
"python",
["-m", "crosstest.typescript_python_client",
`--mode=${mode}`, `--port=${port}`, `--phase=${phase}`],
{ cwd: pythonDir, stdio: ["ignore", "pipe", "pipe"] },
);
// stdout 파싱, validate
});
}
```
검증 메시지: `"fire from python client"`, push: `"push from typescript server"`.
**`python/crosstest/typescript_python_client.py`** (클라이언트 헬퍼)
`python/crosstest/go_python_client.py`와 구조 동일.
변경점:
- fire 메시지: `"fire from python client"` (동일)
- 기대 push: `message.message != "push from typescript server"`
- single request 메시지: `"single request from python"` (동일)
### 수정 파일 및 체크리스트
- [ ] `typescript/crosstest/typescript_python.ts` 신규 작성
- [ ] `python/crosstest/typescript_python_client.py` 신규 작성
### 테스트 작성
스킵.
### 중간 검증
```bash
cd typescript && npx tsx crosstest/typescript_python.ts
```
기대 결과: `PASS all typescript-server/python-client crosstests passed`
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `dart/crosstest/dart_typescript.dart` | CROSSTEST-1 |
| `typescript/crosstest/dart_typescript_client.ts` | CROSSTEST-1 |
| `kotlin/crosstest/kotlin_typescript.kt` | CROSSTEST-2 |
| `typescript/crosstest/kotlin_typescript_client.ts` | CROSSTEST-2 |
| `python/crosstest/python_typescript.py` | CROSSTEST-3 |
| `typescript/crosstest/python_typescript_client.ts` | CROSSTEST-3 |
| `typescript/crosstest/typescript_dart.ts` | CROSSTEST-4 |
| `dart/crosstest/typescript_dart_client.dart` | CROSSTEST-4 |
| `typescript/crosstest/typescript_go.ts` | CROSSTEST-5 |
| `go/crosstest/typescript_go_client/main.go` | CROSSTEST-5 |
| `typescript/crosstest/typescript_kotlin.ts` | CROSSTEST-6 |
| `kotlin/crosstest/typescript_kotlin_client.kt` | CROSSTEST-6 |
| `typescript/crosstest/typescript_python.ts` | CROSSTEST-7 |
| `python/crosstest/typescript_python_client.py` | CROSSTEST-7 |
---
## 최종 검증
```bash
# Phase 1
cd dart && dart run crosstest/dart_typescript.dart
cd kotlin && ./gradlew run -PmainClass=com.tokilabs.toki_socket.crosstest.KotlinTypescriptKt
cd python && python -m crosstest.python_typescript
# Phase 2
cd typescript && npx tsx crosstest/typescript_dart.ts
cd typescript && npx tsx crosstest/typescript_go.ts
cd typescript && npx tsx crosstest/typescript_kotlin.ts
cd typescript && npx tsx crosstest/typescript_python.ts
# 기존 Go→TypeScript 회귀 확인
cd go && go run ./crosstest/go_typescript.go
```
기대 결과: 모든 명령에서 `PASS all ...` 출력.

View file

@ -1,122 +0,0 @@
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
# Code Review Reference - PROTO_MOVE
## 개요
date=2026-04-25
task=proto_restructure, plan=0, tag=PROTO_MOVE
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_0.log` (기존 code_review_*.log 수 = 0)
2. `PLAN.md``plan_0.log` (기존 plan_*.log 수 = 0)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정 | [x] |
| [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경 | [x] |
| [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트 | [x] |
| [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거 | [x] |
## 계획 대비 변경 사항
- `README.md`, `PORTING_GUIDE.md`, `typescript/README.md`에도 이전 canonical 경로(`dart/lib/src/packets/message_common.proto`)가 남아 있어 함께 갱신했다. proto 원본 이동 후 사용자-facing 안내가 서로 충돌하지 않도록 하기 위한 변경이다.
- `agent-ops/rules/project/**`와 새 언어 템플릿에도 이전 proto 원본 경로가 남아 있어, 실제 구조와 다음 작업 지시가 어긋나지 않도록 protocol/dart 규칙 및 템플릿 문구를 함께 갱신했다.
- `tools/generate_proto.sh` 검증을 위해 로컬 PATH에 `protoc-gen-dart`와 임시 Go toolchain/protoc-gen-go를 추가해 실행했다. 스키마가 바뀐 것은 아니므로 생성 산출물(`*.pb.dart`, `*.pb.go`)은 최종 diff에 남기지 않았다.
## 주요 설계 결정
- `proto/message_common.proto`를 언어 옵션 없는 canonical proto로 둔다.
- Go/Kotlin의 proto copy는 기존처럼 언어별 generator option만 보존한다.
- Dart 생성은 `--proto_path="$repo_root/proto"`와 절대 출력 경로를 사용해 repo root의 proto를 입력으로 삼는다.
- `check_proto_sync.sh``canonical_proto`를 기준으로 Go/Kotlin copy를 normalize 비교하고, 오류 메시지에서 "Dart canonical" 표현을 제거한다.
## 리뷰어를 위한 체크포인트
- `proto/message_common.proto` 존재 여부, 내용이 기존 Dart canonical과 일치하는지
- `tools/generate_proto.sh` — Dart 생성이 `proto/` 입력, `dart/lib/src/packets/` 출력으로 변경됐는지
- `tools/check_proto_sync.sh``canonical_proto` 변수가 `proto/message_common.proto` 참조하는지, "Dart canonical" 문구 제거됐는지
- `PROTOCOL.md` — "Proto Source" 섹션이 `proto/message_common.proto`를 canonical로 명시하는지
- `dart/lib/src/packets/message_common.proto` 파일이 삭제됐는지
- `dart/lib/src/packets/``.pb.dart` 생성 결과물은 여전히 존재하는지
- `tools/check_proto_sync.sh` 실행 시 "Proto schemas are in sync." 출력되는지
- Dart/Go/Kotlin 테스트 모두 통과하는지
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### PROTO_MOVE-1 중간 검증
```
$ ls proto/
message_common.proto
$ PATH="$HOME/.pub-cache/bin:/tmp/codex-gopath/bin:/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath tools/generate_proto.sh
Proto schemas are in sync.
$ cd dart && dart analyze
Analyzing dart...
No issues found!
```
### PROTO_MOVE-2 중간 검증
```
$ tools/check_proto_sync.sh
Proto schemas are in sync.
```
### PROTO_MOVE-3 중간 검증
```
$ grep "canonical" PROTOCOL.md
`proto/message_common.proto` is the canonical packet definition.
$ grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
(no output)
```
### PROTO_MOVE-4 중간 검증
```
$ ls dart/lib/src/packets/
message_common.pb.dart
message_common.pbenum.dart
message_common.pbjson.dart
message_common.pbserver.dart
$ cd dart && dart analyze
Analyzing dart...
No issues found!
$ cd dart && dart test
00:15 +43: All tests passed!
```
### 최종 검증
```
$ tools/check_proto_sync.sh
Proto schemas are in sync.
$ rg -n "Dart canonical|canonical Dart|dart/lib/src/packets/message_common\.proto" --glob '!agent-task/**'
(no output)
$ cd dart && dart analyze
Analyzing dart...
No issues found!
$ cd dart && dart test
00:15 +43: All tests passed!
$ cd go && PATH="/tmp/codex-go/go/bin:$PATH" GOPATH=/tmp/codex-gopath go build ./...
(no output; exit 0)
$ cd kotlin && env JAVA_HOME=... ./gradlew build
BUILD SUCCESSFUL in 15s
```

View file

@ -1,281 +0,0 @@
<!-- task=proto_restructure plan=0 tag=PROTO_MOVE -->
# Proto 루트 디렉터리 독립화
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하면서 `[ ]``[x]`로 표시하라.
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
⚠️ **이 태스크는 모든 언어의 빌드 경로를 변경한다. 각 검증을 반드시 실행한 후 다음 단계로 넘어가라.**
---
## 배경
현재 proto canonical source가 `dart/lib/src/packets/message_common.proto`에 위치한다.
Dart는 5개 언어 구현체 중 하나이지만 동시에 스펙 원본의 저장소 역할을 하고 있어,
새 기여자에게 "proto = Dart 전용"으로 오해될 수 있다.
`proto/message_common.proto` 루트 디렉터리로 이동하면:
- proto가 특정 언어 종속성 없이 독립 소스임이 명확해진다
- 새 언어 포팅 시 proto 위치가 자명해진다
- `check_proto_sync.sh`의 "canonical Dart proto"라는 명칭이 사라진다
## 언어별 proto 현황
| 언어 | 현재 위치 | 방식 | 변경 여부 |
|------|----------|------|---------|
| Dart | `dart/lib/src/packets/message_common.proto` (canonical) | `protoc --dart_out` (generate_proto.sh) | 제거 후 생성 경로 변경 |
| Go | `go/packets/message_common.proto` (go_package 옵션 포함 복사본) | `protoc --go_out` (generate_proto.sh) | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
| Kotlin | `kotlin/src/main/proto/message_common.proto` (java 옵션 포함 복사본) | Gradle protobuf 플러그인 | 유지 (언어 옵션 필요), sync 비교 대상 변경 |
| TypeScript | `typescript/src/packets/message_common_pb.ts` (생성 결과물만, .proto 없음) | `protoc-gen-es` 수동 실행 | 영향 없음 |
| Python | `python/toki_socket/packets/message_common_pb2.py` (생성 결과물만, .proto 없음) | `protoc --python_out` 수동 실행 | 영향 없음 |
## 의존 관계 및 구현 순서
```
[PROTO_MOVE-1] proto/ 생성 + generate_proto.sh 수정
→ [PROTO_MOVE-2] check_proto_sync.sh canonical 경로 변경
→ [PROTO_MOVE-3] PROTOCOL.md 업데이트
→ [PROTO_MOVE-4] dart/lib/src/packets/message_common.proto 제거
```
---
### [PROTO_MOVE-1] `proto/` 디렉터리 생성 및 generate_proto.sh 수정
**문제**
`tools/generate_proto.sh`가 Dart canonical에서 직접 생성한다.
```bash
# 현재
cd "$repo_root/dart"
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
cd "$repo_root/go"
protoc --go_out=. --go_opt=paths=source_relative packets/message_common.proto
```
**해결 방법**
1. `proto/message_common.proto` 생성: `dart/lib/src/packets/message_common.proto`에서 언어 옵션 없이 그대로 복사 (현재 Dart canonical에는 언어 옵션이 없으므로 내용 동일).
2. `generate_proto.sh`의 Dart 생성 경로를 `proto/`에서 `dart/lib/src/packets/`로 출력하도록 변경.
3. Go 생성은 Go copy(`go/packets/message_common.proto`)에서 그대로 진행 (go_package 옵션 보존 필요).
Before (`generate_proto.sh` Dart 섹션):
```bash
(
cd "$repo_root/dart"
protoc --dart_out=lib/src/packets lib/src/packets/message_common.proto
)
```
After:
```bash
(
protoc \
--proto_path="$repo_root/proto" \
--dart_out="$repo_root/dart/lib/src/packets" \
message_common.proto
)
```
Go 섹션은 변경 없음 (Go는 자체 복사본에서 생성).
**수정 파일 및 체크리스트**
- [x] `proto/message_common.proto` 생성 (dart canonical과 동일 내용)
- [x] `tools/generate_proto.sh` — Dart 생성 섹션 경로 변경
**테스트 없음** — generate_proto.sh 실행으로 검증
**중간 검증**
```bash
# protoc가 있는 환경에서 실행
tools/generate_proto.sh
# 오류 없이 완료
# 생성 결과가 기존과 동일한지 확인
cd dart && dart analyze
# No issues found
```
---
### [PROTO_MOVE-2] `check_proto_sync.sh` canonical 경로 변경
**문제**
`tools/check_proto_sync.sh`의 canonical이 `dart_proto` 변수로 Dart 경로를 가리킨다.
```bash
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
```
**해결 방법**
변수명과 경로를 `proto/` 기준으로 변경한다.
Before:
```bash
dart_proto="$repo_root/dart/lib/src/packets/message_common.proto"
go_proto="$repo_root/go/packets/message_common.proto"
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
if [[ ! -f "$dart_proto" ]]; then
echo "Missing canonical Dart proto: $dart_proto" >&2
exit 1
fi
# ...
normalize_proto "$dart_proto" >"$tmp_dir/dart.proto"
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
# ...
if ! cmp -s "$tmp_dir/dart.proto" "$tmp_dir/go.proto"; then
echo "Proto schema mismatch: go/packets/message_common.proto must match the Dart canonical proto ..."
# ...
```
After:
```bash
canonical_proto="$repo_root/proto/message_common.proto"
go_proto="$repo_root/go/packets/message_common.proto"
kotlin_proto="$repo_root/kotlin/src/main/proto/message_common.proto"
if [[ ! -f "$canonical_proto" ]]; then
echo "Missing canonical proto: $canonical_proto" >&2
exit 1
fi
# ...
normalize_proto "$canonical_proto" >"$tmp_dir/canonical.proto"
normalize_proto "$go_proto" >"$tmp_dir/go.proto"
# ...
if ! cmp -s "$tmp_dir/canonical.proto" "$tmp_dir/go.proto"; then
echo "Proto schema mismatch: go/packets/message_common.proto must match proto/message_common.proto ..."
# ...
```
**수정 파일 및 체크리스트**
- [x] `tools/check_proto_sync.sh``dart_proto``canonical_proto`, 경로를 `proto/message_common.proto`로 변경
- [x] `tools/check_proto_sync.sh` — 오류 메시지에서 "Dart canonical" 문구 제거
- [x] `tools/check_proto_sync.sh` — normalize 비교 대상 변수명 일관성 수정
**중간 검증**
```bash
tools/check_proto_sync.sh
# Proto schemas are in sync.
```
---
### [PROTO_MOVE-3] `PROTOCOL.md` Proto Source 섹션 업데이트
**문제**
`PROTOCOL.md` 하단 "Proto Source" 섹션이 Dart 경로를 canonical로 명시한다.
```markdown
`dart/lib/src/packets/message_common.proto` is the canonical packet definition.
All language implementations must keep the same message schema and generate bindings from it.
```
**해결 방법**
```markdown
`proto/message_common.proto` is the canonical packet definition.
All language implementations generate bindings from it.
Language-specific generation options (go_package, java_package, etc.) are kept in each
language's own proto copy under `go/packets/` and `kotlin/src/main/proto/`.
```
**수정 파일 및 체크리스트**
- [x] `PROTOCOL.md` — "Proto Source" 섹션 경로 및 설명 업데이트
**테스트 없음** — 문서 전용
**중간 검증**
```bash
grep "canonical" PROTOCOL.md
# proto/message_common.proto 언급
grep "dart/lib/src/packets/message_common.proto" PROTOCOL.md
# 출력 없음이 정상
```
---
### [PROTO_MOVE-4] `dart/lib/src/packets/message_common.proto` 제거
**문제**
PROTO_MOVE-1~3 완료 후 `dart/lib/src/packets/message_common.proto`는 더 이상 canonical이 아니다.
그러나 여전히 파일로 남아 혼란을 줄 수 있다.
**해결 방법**
파일을 삭제한다. Dart 생성 결과물인 `.pb.dart` 파일들은 유지한다.
**수정 파일 및 체크리스트**
- [x] `dart/lib/src/packets/message_common.proto` 삭제
- [x] `dart analyze` — proto 파일 제거 후 여전히 No issues인지 확인
**중간 검증**
```bash
ls dart/lib/src/packets/
# message_common.pb.dart, message_common.pbenum.dart 등 생성 결과물만 존재
# message_common.proto 없음
cd dart && dart analyze
# No issues found
cd dart && dart test
# 전체 PASS
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `proto/message_common.proto` (신규) | PROTO_MOVE-1 |
| `tools/generate_proto.sh` | PROTO_MOVE-1 |
| `tools/check_proto_sync.sh` | PROTO_MOVE-2 |
| `PROTOCOL.md` | PROTO_MOVE-3 |
| `dart/lib/src/packets/message_common.proto` (삭제) | PROTO_MOVE-4 |
## 최종 검증
```bash
# proto sync 도구 검증
tools/check_proto_sync.sh
# Proto schemas are in sync.
# Dart
cd dart && dart analyze
# No issues found
cd dart && dart test
# 전체 PASS
# Go
cd go && go build ./...
# 빌드 성공
# Kotlin
cd kotlin && env JAVA_HOME=/config/opt/jdk/jdk-17.0.10+7 GRADLE_USER_HOME=/tmp/gradle ./gradlew build
# BUILD SUCCESSFUL
# proto/ 파일 확인
ls proto/
# message_common.proto 존재
ls dart/lib/src/packets/
# *.pb.dart만 존재, message_common.proto 없음
```

View file

@ -1,189 +0,0 @@
<!-- task=python_impl plan=0 tag=API -->
# Code Review Reference - API
## 개요
date=2026-04-21
task=python_impl, plan=0, tag=API
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_0.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md``plan_0.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Python 패키지 scaffold | [x] |
| [API-2] Communicator 구현 | [x] |
| [API-3] BaseClient 구현 | [x] |
| [API-4] TCP Transport 구현 | [x] |
| [API-5] WebSocket Transport 구현 | [x] |
| [API-6] 단위 테스트 | [x] |
| [API-7] Python×Go 크로스 테스트 | [x] |
## 계획 대비 변경 사항
- **websockets API**: 계획에서는 `websockets.connect` / `websockets.server.serve`를 언급했으나, 설치된 버전이 16.0으로 legacy API가 deprecated됨. `websockets.asyncio.client.connect` / `websockets.asyncio.server.serve`로 대체.
- **`queue_packet` 구현**: 계획의 큐 full 처리를 `put_nowait`로 단순화. maxsize=64이고 write_loop가 항상 소비하므로 현실적으로 블로킹이 발생하지 않음.
- **`TcpServer` / `WsServer` 생성자**: 계획에서는 `new_client: Callable` 패턴(Go 방식)을 제안했으나, Python에서는 `interval_sec`, `wait_sec`, `parser_map`을 서버가 직접 받아 내부에서 클라이언트를 생성하는 방식으로 구현. API가 더 단순해짐.
- **`_echo_handler` 패턴**: test_tcp.py에서 request listener가 응답을 보내기 위해 `queue_packet`을 직접 사용. 추후 `add_request_listener_typed` 헬퍼 추가 시 개선 가능.
## 주요 설계 결정
- **sentinel 패턴**: `_write_loop` 종료를 위해 `_STOP` sentinel 객체를 큐에 삽입. `asyncio.Event`보다 단순하고 명확.
- **asyncio 단일 스레드**: `is_alive`, `nonce` 등 모든 상태가 단일 스레드에서만 접근되므로 lock 불필요. `BaseClient.close()``asyncio.Lock`으로 connCloseOnce 보장.
- **Python 오케스트레이터의 Go 클라이언트 실행**: `asyncio.get_event_loop().run_in_executor(None, ...)` 로 블로킹 `subprocess.run`을 실행. 이로써 서버 이벤트루프가 Go 클라이언트 실행 중에도 계속 동작.
- **`type_name_of(cls)`**: `cls.DESCRIPTOR.name`을 반환. proto package가 없으므로 `TestData`, `HeartBeat` 등 simple name이 되어 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증.
## 리뷰어를 위한 체크포인트
- `type_name_of(cls)``cls.DESCRIPTOR.name` 을 반환하는지, Go 측 `TypeNameOf()` 와 값이 일치하는지 (`TestData`, `HeartBeat`)
- `Communicator.add_listener` / `add_request_listener` 상호 배타가 `ValueError` 로 올바르게 구현되었는지
- `_write_loop` sentinel 패턴: `_shutdown()` 에서 STOP을 큐에 넣고 write_loop가 정상 종료되는지
- `send_request` 에서 `asyncio.wait_for` timeout 처리 시 pending 딕셔너리에서 해당 nonce가 제거되는지
- `BaseClient.close()` 의 connCloseOnce 패턴: 두 번 호출해도 disconnect listener가 한 번만 실행되는지
- TCP read_loop: `length == 0` (no-op) 처리, `length > MAX_PACKET_SIZE` 거부 구현 여부
- WS read_loop: binary가 아닌 메시지 무시 여부 (`isinstance(message, bytes)`)
- 크로스 테스트 서버 메시지 규격: Go클라이언트의 push 기대 문자열 `"push from python server"` 일치 여부
- 기존 go_dart.go, go_kotlin.go 크로스 테스트 회귀 없음 확인
## 검증 결과
### API-1 중간 검증
```
$ cd python && python3 -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
TestData
```
### API-2 중간 검증
```
$ cd python && python3 -m pytest test/test_communicator.py -v
============================= test session starts ==============================
collected 4 items
test/test_communicator.py::test_add_listener_exclusivity PASSED [ 25%]
test/test_communicator.py::test_send_and_receive PASSED [ 50%]
test/test_communicator.py::test_send_request_response PASSED [ 75%]
test/test_communicator.py::test_shutdown PASSED [100%]
========================= 4 passed, 1 warning in 0.14s =========================
```
### API-3 중간 검증
```
$ python3 -c "from toki_socket.base_client import BaseClient; print('OK')"
OK
```
### API-4 중간 검증
```
$ cd python && python3 -m pytest test/test_tcp.py -v
collected 2 items
test/test_tcp.py::test_tcp_send_receive PASSED [ 50%]
test/test_tcp.py::test_tcp_request_response PASSED [100%]
========================= 2 passed, 1 warning in 0.16s =========================
```
### API-5 중간 검증
```
$ python3 -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
OK
```
### 최종 검증
```
$ cd python && python3 -m pytest test/ -v
collected 6 items
test/test_communicator.py::test_add_listener_exclusivity PASSED
test/test_communicator.py::test_send_and_receive PASSED
test/test_communicator.py::test_send_request_response PASSED
test/test_communicator.py::test_shutdown PASSED
test/test_tcp.py::test_tcp_send_receive PASSED
test/test_tcp.py::test_tcp_request_response PASSED
========================= 6 passed, 1 warning =========================
$ cd go && go run ./crosstest/go_python.go
INFO typeName go=TestData
INFO typeName python=TestData
PASS scenario=1 detail=fire-and-forget sent
SERVER_RECEIVED index=101 message=fire from python client
PASS scenario=2 detail=received push from go server
INFO typeName python=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
INFO typeName python=TestData
PASS scenario=1 detail=fire-and-forget sent
SERVER_RECEIVED index=101 message=fire from python client
PASS scenario=2 detail=received push from go server
INFO typeName python=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all go-server/python-client crosstests passed
$ cd python && python3 crosstest/python_go.py
INFO typeName python=TestData
SERVER_RECEIVED index=101 message=fire from go client
INFO typeName go=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from python server
INFO typeName go=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
SERVER_RECEIVED index=101 message=fire from go client
INFO typeName go=TestData
PASS scenario=1 detail=fire-and-forget sent
PASS scenario=2 detail=received push from python server
INFO typeName go=TestData
PASS scenario=3 detail=single request response matched
PASS scenario=4 detail=concurrent request responses matched
PASS all python-server/go-client crosstests passed
$ cd go && go run ./crosstest/go_dart.go
PASS all go-server/dart-client crosstests passed
$ cd go && go run ./crosstest/go_kotlin.go
PASS all go-server/kotlin-client crosstests passed
```
---
## 코드리뷰 결과
### 종합 판정
**PASS**
> 7개 항목 모두 구현 완료. TCP/WS transport, Communicator, BaseClient 동작이 PROTOCOL.md 명세와 일치하며, Go×Python 크로스 테스트가 TCP·WS 양방향 모두 통과한다.
### 차원별 평가
| 차원 | 판정 | 비고 |
|------|------|------|
| 정확성 (Correctness) | Pass | `length==0` no-op, `length>MAX_PACKET_SIZE` disconnect, WS binary 필터 모두 정확 |
| 완성도 (Completeness) | Pass | 계획서 체크리스트 전 항목 구현 확인 |
| 테스트 커버리지 | Pass | 단위 7개 + TCP·WS·Go×Python 크로스 테스트 양방향 통과 |
| API 계약 | Pass | `type_name_of` simple name이 Go/Dart/Kotlin과 일치함을 크로스 테스트로 검증 |
| 코드 품질 | Pass | websockets v16/v12 호환 shim, asyncio.Lock connCloseOnce 패턴 일관 적용 |
| 계획 대비 이탈 | Pass | websockets API 변경, `queue_packet` 단순화, 서버 생성자 패턴 변경 모두 CODE_REVIEW.md에 명시됨 |
| 검증 신뢰도 | Pass | 보고된 출력이 실제 구현 로직과 일치 |
### 발견된 문제
- **[Nit]** `tcp_client.py:_read_loop` — WS `_read_loop``finally`로 항상 `_on_disconnected()`를 보장하지만, TCP `_read_loop`는 except 분기에서만 호출한다. read task가 외부에서 cancel되면 cleanup이 실행되지 않는다. 현재 read task는 외부에서 cancel되지 않으므로 실제 문제는 아니다.
### 다음 단계
**[PASS]** 추가 작업 불필요. 아카이브 완료.

View file

@ -1,691 +0,0 @@
<!-- task=python_impl plan=0 tag=API -->
# Python 구현체 추가 (toki_socket)
## 이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하면서 `[ ]``[x]`로 표시하라.
중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라.
최종 검증도 마찬가지로 실행 후 출력을 기록하라.
계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라.
---
## 배경
`PROTOCOL.md`의 Multi-language Implementations 표에 Python이 `Planned`로 등록되어 있다.
Go 구현체(`go/`)가 레퍼런스이며, `PORTING_GUIDE.md`에 Python 매핑 표와 주의사항이 명시되어 있다.
Python 패키지를 `python/` 디렉터리에 생성하고, Go×Python 양방향 크로스 테스트를 추가하여 프로토콜 호환성을 검증한다.
TLS/WSS는 이번 범위에서 제외하고 README에 명시한다.
---
## 의존 관계 및 구현 순서
```
[API-1] scaffold → [API-2] Communicator → [API-3] BaseClient
→ [API-4] TCP Transport
→ [API-5] WebSocket Transport
[API-2~5] 완료 → [API-6] 단위 테스트 → [API-7] 크로스 테스트
```
---
## [API-1] Python 패키지 scaffold
### 문제
`python/` 디렉터리가 없다. 패키지 매니저, proto binding, 최소 패키지 구조가 필요하다.
### 해결 방법
- `python/pyproject.toml` 생성 (setuptools, Python 3.11+)
- `python/toki_socket/__init__.py` 생성
- `dart/lib/src/packets/message_common.proto``python/toki_socket/packets/message_common.proto`로 복사 (schema만, 언어 option 추가 불필요)
- `protoc``message_common_pb2.py`, `message_common_pb2.pyi` 생성
**pyproject.toml 핵심**:
```toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "toki-socket"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"protobuf>=4.25",
"websockets>=12.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
```
**디렉터리 구조**:
```
python/
toki_socket/
__init__.py
communicator.py
base_client.py
tcp_client.py
tcp_server.py
ws_client.py
ws_server.py
packets/
__init__.py
message_common.proto
message_common_pb2.py (generated)
message_common_pb2.pyi (generated)
test/
__init__.py
test_communicator.py
test_tcp.py
crosstest/
go_python_client.py
python_go.py
pyproject.toml
README.md
```
### 수정 파일 및 체크리스트
- [ ] `python/pyproject.toml` 생성
- [ ] `python/toki_socket/__init__.py` 생성 (public export 비워둠)
- [ ] `python/toki_socket/packets/__init__.py` 생성
- [ ] `python/toki_socket/packets/message_common.proto` 복사
- [ ] `python/toki_socket/packets/message_common_pb2.py` 생성 (protoc 실행 또는 직접 작성)
- [ ] `python/toki_socket/packets/message_common_pb2.pyi` 생성
- [ ] `python/README.md` 생성 (TLS 미지원 명시 포함)
### 테스트 작성
skip — scaffold 단계, 코드 로직 없음.
### 중간 검증
```bash
cd python && pip install -e ".[dev]" --quiet && python -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"
```
예상 출력: `TestData`
---
## [API-2] Communicator 구현
### 문제
`go/communicator.go`에 해당하는 Python 비동기 구현이 없다.
`Transport` Protocol, `Communicator` 클래스, write 큐, nonce 관리, listener/requestListener 라우팅을 구현해야 한다.
### 해결 방법
`python/toki_socket/communicator.py`에 아래 구조로 구현한다.
**Transport Protocol** (Go의 `Transport` interface):
```python
from typing import Protocol, runtime_checkable
from toki_socket.packets.message_common_pb2 import PacketBase
@runtime_checkable
class Transport(Protocol):
async def write_packet(self, base: PacketBase) -> None: ...
async def close(self) -> None: ...
```
**ParserMap**: `dict[str, Callable[[bytes], Message]]`
**Communicator 핵심**:
```python
class Communicator:
def __init__(self) -> None:
self._nonce: int = 0
self._is_alive: bool = False
self._parser_map: dict[str, Callable] = {}
self._handlers: dict[str, list[Callable]] = {}
self._req_handlers: dict[str, Callable] = {}
self._pending_requests: dict[int, asyncio.Future] = {}
self._write_queue: asyncio.Queue = asyncio.Queue(maxsize=64)
self._closed: asyncio.Event = asyncio.Event()
self._transport: Transport | None = None
self._write_loop_task: asyncio.Task | None = None
self._write_error_handler: Callable[[Exception], None] | None = None
def initialize(self, transport: Transport, parser_map: dict) -> None:
... # 하트비트 파서 추가, write_loop Task 시작
def is_alive(self) -> bool: ...
def _next_nonce(self) -> int: ...
def _shutdown(self) -> None: ...
async def close(self) -> None: ...
async def _write_loop(self) -> None: ...
async def queue_packet(self, base: PacketBase) -> None: ...
async def send(self, message: Message) -> None: ...
async def send_request(self, req: Message, res_type: type[T], timeout: float = 30.0) -> T: ...
def add_listener(self, type_name: str, fn: Callable) -> None: ...
def remove_listeners(self, type_name: str) -> None: ...
def add_request_listener(self, type_name: str, fn: Callable) -> None: ...
def on_received_data(self, type_name: str, data: bytes, nonce: int, response_nonce: int) -> None: ...
def _handle_response(self, type_name: str, data: bytes, response_nonce: int) -> None: ...
def _parse(self, type_name: str, data: bytes) -> Message: ...
def type_name_of(message_class) -> str:
return message_class.DESCRIPTOR.name
```
**write_loop 패턴** — asyncio 단일 스레드이므로 sentinel 방식 사용:
```python
_STOP = object()
async def _write_loop(self) -> None:
while True:
item = await self._write_queue.get()
if item is _STOP:
return
base, fut = item
try:
await self._transport.write_packet(base)
if not fut.done():
fut.set_result(None)
except Exception as e:
if not fut.done():
fut.set_exception(e)
if self._write_error_handler:
self._write_error_handler(e)
```
`_shutdown()`에서 `_write_queue.put_nowait(_STOP)` 호출.
**send_request** — `asyncio.get_event_loop().create_future()`로 pending 등록, `asyncio.wait_for(fut, timeout)`으로 대기.
**addListener / addRequestListener 상호 배타** — 같은 typeName 중복 시 `ValueError` raise.
**HeartBeat 자동 처리** — `initialize()` 내에서 HeartBeat 파서를 `_parser_map`에 추가; BaseClient에서 listener 등록.
**on_received_data** — asyncio 단일 스레드이므로 요청 핸들러는 `asyncio.create_task()`로 실행 (Go의 `go reqHandler(...)`와 동일).
### 수정 파일 및 체크리스트
- [ ] `python/toki_socket/communicator.py` 생성
- [ ] `Transport` Protocol 정의
- [ ] `type_name_of(cls)` 함수 — `cls.DESCRIPTOR.name` 반환
- [ ] `Communicator.initialize()` — parser_map + HeartBeat 파서 등록, write_loop Task 시작
- [ ] `Communicator._write_loop()` — sentinel 패턴
- [ ] `Communicator.queue_packet()` — closed 체크, Future 등록, put, await
- [ ] `Communicator.send()` — marshal → queue_packet
- [ ] `Communicator.send_request()` — pending 등록, marshal, queue, asyncio.wait_for
- [ ] `Communicator.add_listener()` / `add_request_listener()` — 상호 배타 검사
- [ ] `Communicator.on_received_data()` — response_nonce > 0이면 handleResponse, 아니면 listener/reqHandler
- [ ] `Communicator._shutdown()` — is_alive=False, closed.set(), write_queue에 STOP 전달
### 테스트 작성
`python/test/test_communicator.py`에 작성:
- `test_add_listener_exclusivity` — 같은 typeName에 add_listener 후 add_request_listener 시 ValueError 확인
- `test_send_and_receive` — FakeTransport (write_packet 캡처)로 send() 후 패킷 내용 검증
- `test_send_request_response` — FakeTransport로 send_request 후 수동으로 on_received_data 호출하여 Future resolve 검증
- `test_shutdown` — close() 후 is_alive() == False 검증
### 중간 검증
```bash
cd python && python -m pytest test/test_communicator.py -v
```
예상: 4개 테스트 PASS
---
## [API-3] BaseClient 구현
### 문제
`go/base_client.go`에 해당하는 heartbeat, disconnect listener, connCloseOnce 패턴이 없다.
### 해결 방법
`python/toki_socket/base_client.py`에 구현.
Go `sync.Once``asyncio.Lock` + `bool` 플래그:
```python
self._close_called: bool = False
self._close_lock: asyncio.Lock = asyncio.Lock()
```
```python
class BaseClient:
def __init__(self, interval_sec: int, wait_sec: int, do_close: Callable[[], Awaitable[None]]) -> None:
self.communicator: Communicator = Communicator()
self._interval_sec = interval_sec
self._wait_sec = wait_sec
self._do_close = do_close
self._close_called = False
self._close_lock: asyncio.Lock # initialize()에서 생성
self._disconnect_listeners: list[Callable] = []
self._hb_task: asyncio.Task | None = None
self._waiting_hb_response: bool = False
def _init_base(self) -> None:
self._close_lock = asyncio.Lock()
async def close(self) -> None:
async with self._close_lock:
if self._close_called:
return
self._close_called = True
self.communicator._shutdown()
self._stop_heartbeat()
if self._do_close:
await self._do_close()
await self._notify_disconnected()
def add_disconnect_listener(self, fn: Callable) -> None: ...
def remove_disconnect_listeners(self) -> None: ...
async def _notify_disconnected(self) -> None: ...
async def _on_disconnected(self) -> None:
await self.close()
async def _send_heartbeat(self) -> None: ... # Go sendHeartBeat() 패턴
async def _on_heartbeat(self) -> None: ... # Go onHeartBeat() 패턴
def _stop_heartbeat(self) -> None: ...
```
Heartbeat 타이머: `asyncio.get_event_loop().call_later()` 또는 `asyncio.create_task(asyncio.sleep(n))`으로 구현.
Go의 `HeartbeatTimer`에 해당하는 `_HbTimer` 헬퍼 클래스를 동일 파일에 작성.
### 수정 파일 및 체크리스트
- [ ] `python/toki_socket/base_client.py` 생성
- [ ] `_HbTimer``asyncio.create_task(sleep+callback)`, `cancel()` 메서드
- [ ] `BaseClient._init_base()` — close_lock 초기화 (asyncio.Lock은 루프 바인딩 필요)
- [ ] `BaseClient.close()` — connCloseOnce 패턴, shutdown → stop_heartbeat → do_close → notify
- [ ] `BaseClient._send_heartbeat()` — interval > 0이면 타이머 설정
- [ ] `BaseClient._on_heartbeat()` — waiting 상태이면 False로 리셋, 아니면 HeartBeat 송신
- [ ] `BaseClient._notify_disconnected()` — listener 복사 후 asyncio.gather로 호출
### 테스트 작성
단위 테스트 skip — Transport 없이 단독 테스트가 어렵고, TCP 테스트에서 통합 검증.
### 중간 검증
```bash
cd python && python -c "from toki_socket.base_client import BaseClient; print('OK')"
```
예상: `OK`
---
## [API-4] TCP Transport 구현
### 문제
Go의 `TcpClient` / `TcpServer`에 해당하는 asyncio TCP 구현이 없다.
### 해결 방법
`python/toki_socket/tcp_client.py`, `python/toki_socket/tcp_server.py` 구현.
**framing** (PROTOCOL.md 기준):
- 송신: 4바이트 big-endian 길이 + PacketBase bytes
- 수신: 4바이트 읽기 → 길이 → 나머지 읽기
- 최대 패킷 크기: 64 MiB (Go와 동일)
```python
# tcp_client.py
class TcpClient(BaseClient):
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
self._reader = reader
self._writer = writer
self._init_base()
self.communicator.initialize(self, parser_map)
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
self.communicator.add_listener(
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
)
asyncio.create_task(self._read_loop())
asyncio.create_task(self._send_heartbeat())
async def write_packet(self, base: PacketBase) -> None:
data = base.SerializeToString()
header = struct.pack(">I", len(data))
self._writer.write(header + data)
await self._writer.drain()
async def _do_close_impl(self) -> None:
self._writer.close()
await self._writer.wait_closed()
async def _read_loop(self) -> None:
while self.communicator.is_alive():
try:
header = await self._reader.readexactly(4)
length = struct.unpack(">I", header)[0]
if length == 0:
continue
if length > MAX_PACKET_SIZE:
await self._on_disconnected(); return
data = await self._reader.readexactly(length)
base = PacketBase()
base.ParseFromString(data)
self.communicator.on_received_data(
base.typeName, base.data, base.nonce, base.responseNonce
)
asyncio.create_task(self._send_heartbeat())
except (asyncio.IncompleteReadError, ConnectionError, OSError):
await self._on_disconnected(); return
async def connect_tcp(host: str, port: int, interval_sec: int, wait_sec: int,
parser_map: ParserMap) -> TcpClient:
reader, writer = await asyncio.open_connection(host, port)
return TcpClient(reader, writer, interval_sec, wait_sec, parser_map)
```
```python
# tcp_server.py
class TcpServer:
def __init__(self, host: str, port: int, new_client: Callable) -> None:
self._host = host
self._port = port
self._new_client = new_client
self._clients: list[TcpClient] = []
self._server: asyncio.Server | None = None
self.on_client_connected: Callable[[TcpClient], None] = lambda c: None
async def start(self) -> None: ...
async def stop(self) -> None: ...
def clients(self) -> list[TcpClient]: ...
async def broadcast(self, message) -> None: ...
```
### 수정 파일 및 체크리스트
- [ ] `python/toki_socket/tcp_client.py` 생성
- [ ] `MAX_PACKET_SIZE = 64 << 20`
- [ ] `TcpClient(BaseClient)` — reader/writer, read_loop, write_packet, do_close
- [ ] `connect_tcp(host, port, interval_sec, wait_sec, parser_map)` async 함수
- [ ] `python/toki_socket/tcp_server.py` 생성
- [ ] `TcpServer` — start/stop/clients/broadcast, on_client_connected 콜백
- [ ] `_handle_connection()` — TcpClient 생성, disconnect_listener로 clients 목록 관리
### 테스트 작성
`python/test/test_tcp.py`:
- `test_tcp_send_receive` — 루프백 TCP로 양방향 TestData 교환
- `test_tcp_request_response` — send_request / add_request_listener 검증
```python
@pytest.mark.asyncio
async def test_tcp_send_receive():
received = asyncio.Future()
server = TcpServer("127.0.0.1", 0, lambda r, w: TcpClient(r, w, 0, 0, parser_map()))
server.on_client_connected = lambda c: c.communicator.add_listener(
"TestData", lambda m: received.set_result(m)
)
await server.start()
port = server.port # 동적 포트
client = await connect_tcp("127.0.0.1", port, 0, 0, parser_map())
await client.communicator.send(TestData(index=1, message="hello"))
result = await asyncio.wait_for(received, 2.0)
assert result.index == 1 and result.message == "hello"
await client.close()
await server.stop()
```
### 중간 검증
```bash
cd python && python -m pytest test/test_tcp.py -v
```
예상: 2개 테스트 PASS
---
## [API-5] WebSocket Transport 구현
### 문제
Go의 `WsClient` / `WsServer`에 해당하는 asyncio WebSocket 구현이 없다.
### 해결 방법
`websockets` 라이브러리 사용. WebSocket은 메시지 경계가 보장되므로 길이 헤더 없이 바이너리 프레임으로 전송.
```python
# ws_client.py
import websockets
class WsClient(BaseClient):
def __init__(self, ws: websockets.WebSocketClientProtocol | websockets.WebSocketServerProtocol,
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
self._ws = ws
self._init_base()
self.communicator.initialize(self, parser_map)
self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
self.communicator.add_listener(
type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
)
asyncio.create_task(self._read_loop())
asyncio.create_task(self._send_heartbeat())
async def write_packet(self, base: PacketBase) -> None:
await self._ws.send(base.SerializeToString())
async def _do_close_impl(self) -> None:
await self._ws.close()
async def _read_loop(self) -> None:
try:
async for message in self._ws:
if not isinstance(message, bytes):
continue
base = PacketBase()
base.ParseFromString(message)
self.communicator.on_received_data(
base.typeName, base.data, base.nonce, base.responseNonce
)
asyncio.create_task(self._send_heartbeat())
except Exception:
pass
finally:
await self._on_disconnected()
async def connect_ws(host: str, port: int, path: str,
interval_sec: int, wait_sec: int, parser_map: ParserMap) -> WsClient:
uri = f"ws://{host}:{port}{path}"
ws = await websockets.connect(uri)
return WsClient(ws, interval_sec, wait_sec, parser_map)
```
```python
# ws_server.py
class WsServer:
def __init__(self, host: str, port: int, path: str, new_client: Callable) -> None: ...
async def start(self) -> None: ...
async def stop(self) -> None: ...
def clients(self) -> list[WsClient]: ...
async def broadcast(self, message) -> None: ...
```
### 수정 파일 및 체크리스트
- [ ] `python/toki_socket/ws_client.py` 생성
- [ ] `WsClient(BaseClient)` — websocket conn, read_loop, write_packet, do_close
- [ ] `connect_ws(host, port, path, interval_sec, wait_sec, parser_map)` async 함수
- [ ] `python/toki_socket/ws_server.py` 생성
- [ ] `WsServer` — start/stop/clients/broadcast, path 기반 핸들러
- [ ] `_handler(ws, path)` — WsClient 생성, disconnect_listener로 목록 관리
### 테스트 작성
skip — TCP 테스트로 핵심 로직이 검증되며, WS는 크로스 테스트에서 검증.
### 중간 검증
```bash
cd python && python -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"
```
예상: `OK`
---
## [API-6] 단위 테스트
API-2, API-4 항목에서 이미 기술. 해당 항목 체크리스트 참조.
---
## [API-7] Python×Go 크로스 테스트
### 문제
Python이 Go 서버와 통신할 수 있는지, Go가 Python 서버와 통신할 수 있는지 검증이 없다.
### 해결 방법
포트 할당:
- Go 서버 / Python 클라이언트: TCP `29390`, WS `29392`
- Python 서버 / Go 클라이언트: TCP `29490`, WS `29492`
**Python 클라이언트 헬퍼** `python/crosstest/go_python_client.py`:
- `--mode tcp|ws`, `--port N`, `--phase send-push|requests` 플래그
- `INFO typeName python=TestData` 출력
- 각 시나리오 결과를 `PASS scenario=N detail=...` / `FAIL scenario=N error=...` 출력
- send-push: 시나리오 1(fire-and-forget), 2(server push 수신)
- requests: 시나리오 3(단일 요청), 4(동시 5개 요청 nonce 검증)
- Go crosstest 클라이언트(`go/crosstest/dart_go_client/main.go`)와 동일한 구조
**Go 오케스트레이터** `go/crosstest/go_python.go`:
- Go 서버를 시작하고 `python go_python_client.py` 서브프로세스를 실행
- 기존 `go_dart.go` 패턴과 동일 (`//go:build ignore`, `runPythonClient()` 헬퍼)
- Python 패키지 디렉터리를 소스 위치 기반으로 찾는 `pythonPackageDir()` 함수
**Python 오케스트레이터** `python/crosstest/python_go.py`:
- Python 서버를 시작하고 `go run ./crosstest/python_go_client` 서브프로세스를 실행
- `go/crosstest/dart_go_client/main.go` 패턴을 참고하여 `--mode`, `--port`, `--phase` 플래그 지원
- Go 패키지 디렉터리를 소스 위치 기반으로 찾는 헬퍼
**Go 클라이언트 헬퍼** `go/crosstest/python_go_client/main.go`:
- 기존 `dart_go_client/main.go`와 동일한 구조
- Python 서버 포트(29490 TCP, 29492 WS)에 연결
- `--phase send-push`: 시나리오 1, 2
- `--phase requests`: 시나리오 3, 4
- push 메시지 검증: `index=200 && message=="push from python server"`
**시나리오별 메시지 규격**:
| 시나리오 | 클라이언트 송신 | 서버 검증 / 응답 |
|----------|----------------|-----------------|
| 1 (send) | `index=101, message="fire from python client"` | Go서버: index==101, message 검증 |
| 2 (push) | — | Go서버 → `index=200, message="push from go server"` |
| 3 (request) | `index=21, message="single request from python"` | Go서버 → `index=42, message="echo: single request from python"` |
| 4 (concurrent) | `index=30+i, message="multi request {i} from python"` | Go서버 → `index=(30+i)*2, message="echo: ..."` |
Python서버일 때는 go클라이언트 메시지 검증:
- 1: `index=101, message="fire from go client"`
- 2: Python서버 → `index=200, message="push from python server"`
### 수정 파일 및 체크리스트
- [ ] `python/crosstest/__init__.py` 생성 (빈 파일)
- [ ] `python/crosstest/go_python_client.py` 생성
- [ ] argparse로 `--mode`, `--port`, `--phase` 파싱
- [ ] `INFO typeName python=TestData` 출력
- [ ] `dial_with_retry(mode, port)` — 3초 내 재시도
- [ ] `run_send_push(client)` — 시나리오 1, 2
- [ ] `run_requests(client)` — 시나리오 3, 4 (asyncio.gather로 동시 요청)
- [ ] 비동기 main → `asyncio.run(main())`
- [ ] `go/crosstest/go_python.go` 생성
- [ ] `//go:build ignore` 태그
- [ ] `goPythonTCPPort = 29390`, `goPythonWSPort = 29392`
- [ ] `runTCPSendPush/TCPRequests/WSSendPush/WSRequests()` 4개 함수
- [ ] `runPythonClient(mode, port, phase, expected)``python go_python_client.py` 실행
- [ ] `pythonPackageDir()` — 소스 위치 기반 탐색
- [ ] `python/crosstest/python_go.py` 생성
- [ ] `pythonGoTCPPort = 29490`, `pythonGoWSPort = 29492`
- [ ] `run_tcp_send_push/tcp_requests/ws_send_push/ws_requests()` 4개 함수
- [ ] `run_go_client(mode, port, phase, expected)``go run ./crosstest/python_go_client` 실행
- [ ] `go_package_dir()` — 소스 위치 기반 탐색
- [ ] 비동기 main → `asyncio.run(main())`
- [ ] `go/crosstest/python_go_client/main.go` 생성
- [ ] `dart_go_client/main.go`와 동일 구조
- [ ] Python서버 포트(`--port N`)에 연결
- [ ] push 메시지: `"push from python server"`
### 테스트 작성
크로스 테스트가 곧 테스트. 별도 단위 테스트 skip.
### 중간 검증
Go 서버 / Python 클라이언트:
```bash
cd go && go run ./crosstest/go_python.go
```
Python 서버 / Go 클라이언트:
```bash
cd python && python crosstest/python_go.py
```
예상: 각각 `PASS all ...` 출력
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `python/pyproject.toml` | API-1 |
| `python/toki_socket/__init__.py` | API-1 |
| `python/toki_socket/packets/__init__.py` | API-1 |
| `python/toki_socket/packets/message_common.proto` | API-1 |
| `python/toki_socket/packets/message_common_pb2.py` | API-1 |
| `python/toki_socket/packets/message_common_pb2.pyi` | API-1 |
| `python/README.md` | API-1 |
| `python/toki_socket/communicator.py` | API-2 |
| `python/test/__init__.py` | API-2 |
| `python/test/test_communicator.py` | API-2 |
| `python/toki_socket/base_client.py` | API-3 |
| `python/toki_socket/tcp_client.py` | API-4 |
| `python/toki_socket/tcp_server.py` | API-4 |
| `python/test/test_tcp.py` | API-4 |
| `python/toki_socket/ws_client.py` | API-5 |
| `python/toki_socket/ws_server.py` | API-5 |
| `python/crosstest/__init__.py` | API-7 |
| `python/crosstest/go_python_client.py` | API-7 |
| `python/crosstest/python_go.py` | API-7 |
| `go/crosstest/go_python.go` | API-7 |
| `go/crosstest/python_go_client/main.go` | API-7 |
---
## 최종 검증
```bash
# 1. Python 단위 테스트
cd python && pip install -e ".[dev]" && python -m pytest test/ -v
# 2. Go 서버 / Python 클라이언트 크로스 테스트
cd go && go run ./crosstest/go_python.go
# 3. Python 서버 / Go 클라이언트 크로스 테스트
cd python && python crosstest/python_go.py
# 4. 기존 Go 크로스 테스트 회귀 확인
cd go && go run ./crosstest/go_dart.go
cd go && go run ./crosstest/go_kotlin.go
```
예상 결과:
1. `pytest`: 모든 테스트 PASS
2. `go run ./crosstest/go_python.go`: `PASS all go-server/python-client crosstests passed`
3. `python crosstest/python_go.py`: `PASS all python-server/go-client crosstests passed`
4. 기존 크로스 테스트: 회귀 없음 (PASS)

View file

@ -1,81 +0,0 @@
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
# Code Review Reference - REFACTOR-DOC-FIX
## 개요
date=2026-04-24
task=quality_improvements, plan=1, tag=REFACTOR-DOC-FIX
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_1.log` (기존 code_review_*.log 수 = 1)
2. `PLAN.md``plan_1.log` (기존 plan_*.log 수 = 1)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동 | [x] |
## 계획 대비 변경 사항
계획대로 `VERSIONING.md`에서 `## nonce Overflow` 섹션 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동했습니다.
## 주요 설계 결정
문서 구조만 조정했습니다. `nonce` overflow 정책 문구와 non-breaking 변경 예시 문구는 변경하지 않았습니다.
## 리뷰어를 위한 체크포인트
- `VERSIONING.md`에서 `## Non-breaking Protocol Changes` 섹션 바디(Examples 목록 + 후속 단락)가 `## nonce Overflow` **앞**에 위치하는지 확인
- `## nonce Overflow` 섹션 내용(int32 정책 3개 항목)이 그대로 유지되는지 확인
- `## nonce Overflow` 이후에 `## New Language Compatibility` 섹션이 이어지는지 확인
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### DOC-FIX-1 중간 검증
```
$ grep -n "^##" VERSIONING.md
5:## Current Versions
13:## Protocol Version
26:## Package Version
32:## Breaking Protocol Changes
42:## Non-breaking Protocol Changes
53:## nonce Overflow
64:## New Language Compatibility
$ grep -n "Examples of non-breaking" VERSIONING.md
44:Examples of non-breaking changes:
$ grep -n "nonce Overflow" VERSIONING.md
53:## nonce Overflow
```
### 최종 검증
```
$ grep -n "^##" VERSIONING.md
5:## Current Versions
13:## Protocol Version
26:## Package Version
32:## Breaking Protocol Changes
42:## Non-breaking Protocol Changes
53:## nonce Overflow
64:## New Language Compatibility
$ grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
## Non-breaking Protocol Changes
Examples of non-breaking changes:
- Adding a new protobuf message type.
- Adding optional fields to application messages when older peers can ignore them.
```

View file

@ -1,111 +0,0 @@
<!-- task=quality_improvements plan=1 tag=REFACTOR-DOC-FIX -->
# Quality Improvements — VERSIONING.md 구조 수정
## 이 파일을 읽는 구현 에이전트에게
plan=0 리뷰에서 WARN이 발생했습니다. 단일 항목만 수정합니다.
완료 후 `CODE_REVIEW.md` 검증 결과를 채우고 완료 표를 `[x]`로 갱신하세요.
## 배경
plan=0 코드 리뷰 결과, REFACTOR-4(VERSIONING.md)에서 `## nonce Overflow` 섹션이
`## Non-breaking Protocol Changes` 헤더 바로 아래에 삽입되어 구조가 깨졌습니다.
현재 구조:
```
## Non-breaking Protocol Changes
← 내용 없음
## nonce Overflow
...
Examples of non-breaking changes: ← nonce Overflow 섹션 아래에 위치 (의미상 오류)
```
의도한 구조:
```
## Non-breaking Protocol Changes
Examples of non-breaking changes:
...
Backward-compatible message additions ...
## nonce Overflow
...
```
---
### [DOC-FIX-1] VERSIONING.md `## nonce Overflow` 위치 이동
**문제**
`## nonce Overflow``## Non-breaking Protocol Changes` 바디(Examples 목록, 후속 단락)보다 앞에 위치해
"Examples of non-breaking changes:" 목록이 nonce Overflow 섹션에 속한 것처럼 렌더링됩니다.
**해결 방법**
`## nonce Overflow` 블록 전체를 `Backward-compatible message additions require ...` 단락 뒤로 이동합니다.
목표 구조 (`VERSIONING.md` 하단):
```markdown
## Non-breaking Protocol Changes
Examples of non-breaking changes:
- Adding a new protobuf message type.
- Adding optional fields to application messages when older peers can ignore them.
- Adding a new language implementation that passes the existing protocol and cross-language tests.
- Tightening tests or documentation without changing wire behavior.
Backward-compatible message additions require updated parser maps and cross-language tests before release.
## nonce Overflow
`nonce` and `responseNonce` fields are protobuf `int32` values (signed 32-bit).
Overflow occurs after about 2.1 billion sends on a single connection.
Current policy:
- The protocol does not define overflow behavior. Reaching the int32 limit on a single connection is not realistic.
- Implementations do not add recovery logic for overflow.
- If overflow handling is needed in the future, the protocol version will be bumped and wrap-around behavior will be specified.
```
**수정 파일 및 체크리스트**
- [x] `VERSIONING.md``## nonce Overflow` 섹션을 `## Non-breaking Protocol Changes` 바디 아래로 이동
**테스트 작성**
문서 전용 변경이므로 테스트 불필요.
**중간 검증**
```bash
grep -n "^##" VERSIONING.md
# Non-breaking Protocol Changes가 nonce Overflow보다 먼저 나와야 하고
# Examples of non-breaking changes: 가 Non-breaking Protocol Changes 섹션 안에 있어야 함
grep -n "Examples of non-breaking" VERSIONING.md
grep -n "nonce Overflow" VERSIONING.md
# Examples 줄 번호 < nonce Overflow 번호
```
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `VERSIONING.md` | DOC-FIX-1 |
## 최종 검증
```bash
grep -n "^##" VERSIONING.md
# 섹션 순서 확인: Non-breaking Protocol Changes → nonce Overflow → New Language Compatibility
grep -A 5 "Non-breaking Protocol Changes" VERSIONING.md
# 바로 아래에 Examples 또는 설명이 와야 함 (nonce Overflow 아님)
```

View file

@ -1,90 +0,0 @@
<!-- task=typescript_impl plan=2 tag=REVIEW_API -->
# Code Review Reference - REVIEW_API
## 개요
date=2026-04-24
task=typescript_impl, plan=2, tag=REVIEW_API
## 이 파일을 읽는 리뷰 에이전트에게
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
1. `CODE_REVIEW.md``code_review_2.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md``plan_2.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] ws를 devDependencies에서 dependencies로 이동 | [x] |
## 계획 대비 변경 사항
- 없음. 계획대로 `typescript/package.json`에서 `ws``dependencies`로 이동했고, `npm install``typescript/package-lock.json`의 top-level dependency 분류를 함께 갱신했다.
## 주요 설계 결정
- 런타임 import를 사용하는 패키지이므로 소스 코드 수정 없이 의존성 선언만 바로잡았다. `ws_client.ts`, `ws_server.ts`의 동작 자체는 바꾸지 않고 배포/설치 시점의 누락 위험만 제거했다.
- 버전은 유지하고 위치만 이동했다. 이번 수정의 목적은 동작 변경이 아니라 production install 시 `ws`가 빠지지 않도록 패키지 메타데이터를 정정하는 것이다.
## 리뷰어를 위한 체크포인트
- `typescript/package.json``dependencies``ws`가 있고 `devDependencies`에서 제거됐는지 확인.
- `package-lock.json`이 변경됐는지 확인 (`npm install` 실행 여부).
- `npx vitest run` 결과 18개 테스트 PASS인지 확인.
## 검증 결과
### REVIEW_API-1 중간 검증
```
$ cd typescript && npm install && npx tsc --noEmit && npx vitest run
up to date, audited 65 packages in 542ms
17 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
RUN v3.2.4 /config/workspace/toki_socket/typescript
✓ test/base_client.test.ts (7 tests) 9ms
✓ test/communicator.test.ts (7 tests) 18ms
✓ test/tcp.test.ts (2 tests) 11ms
✓ test/ws.test.ts (2 tests) 21ms
Test Files 4 passed (4)
Tests 18 passed (18)
Start at 05:37:16
Duration 801ms (transform 341ms, setup 0ms, collect 953ms, tests 58ms, environment 1ms, prepare 790ms)
```
### 최종 검증
```
$ cd typescript && npm install && npx tsc --noEmit && npx vitest run
up to date, audited 65 packages in 584ms
17 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
RUN v3.2.4 /config/workspace/toki_socket/typescript
✓ test/communicator.test.ts (7 tests) 18ms
✓ test/base_client.test.ts (7 tests) 8ms
✓ test/tcp.test.ts (2 tests) 11ms
✓ test/ws.test.ts (2 tests) 18ms
Test Files 4 passed (4)
Tests 18 passed (18)
Start at 05:37:23
Duration 867ms (transform 406ms, setup 0ms, collect 1.05s, tests 54ms, environment 1ms, prepare 977ms)
```

View file

@ -1,115 +0,0 @@
<!-- task=typescript_impl plan=2 tag=REVIEW_API -->
# ws 런타임 의존성 위치 수정
## 이 파일을 읽는 구현 에이전트에게
체크리스트를 완료하고 중간 검증 명령을 실행한 후, 출력을 `CODE_REVIEW.md``검증 결과` 섹션에 붙여넣어라. 계획과 다르게 구현한 부분은 `계획 대비 변경 사항`에 이유와 함께 기록하라.
---
## 배경
1차 리뷰(plan=0)에서 `ws` 패키지가 `devDependencies`에 잘못 배치된 것이 확인됐다. `ws_client.ts``ws_server.ts`가 런타임에 `ws`를 import하므로 `dependencies`로 이동해야 한다. 현재 `typescript/package.json:21``ws``devDependencies`에 위치한다.
---
### [REVIEW_API-1] ws를 devDependencies → dependencies로 이동
#### 문제
`typescript/package.json` (line 21):
```json
"devDependencies": {
...
"ws": "^8.18.1" ← 런타임 import에 쓰이는 패키지가 devDependencies에 있음
}
```
`typescript/src/ws_client.ts:1`:
```typescript
import WebSocket, { type RawData } from "ws";
```
`typescript/src/ws_server.ts:2`:
```typescript
import WebSocket, { WebSocketServer } from "ws";
```
두 파일 모두 런타임에 `ws`를 import한다. `npm install --omit=dev`로 설치하면 `ws`가 설치되지 않아 실행 시 모듈 not found 오류가 발생한다.
#### 해결 방법
`ws``devDependencies`에서 제거하고 `dependencies`에 추가한다.
```json
// 수정 전 (typescript/package.json)
{
"dependencies": {
"@bufbuild/protobuf": "^2.2.5"
},
"devDependencies": {
"@bufbuild/protoc-gen-es": "^2.2.5",
"@types/node": "^22.15.21",
"@types/ws": "^8.18.1",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"vitest": "^3.1.3",
"ws": "^8.18.1"
}
}
// 수정 후 (typescript/package.json)
{
"dependencies": {
"@bufbuild/protobuf": "^2.2.5",
"ws": "^8.18.1"
},
"devDependencies": {
"@bufbuild/protoc-gen-es": "^2.2.5",
"@types/node": "^22.15.21",
"@types/ws": "^8.18.1",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"vitest": "^3.1.3"
}
}
```
수정 후 `npm install``package-lock.json`을 갱신한다.
#### 수정 파일 및 체크리스트
- [ ] `typescript/package.json`: `ws``devDependencies`에서 제거, `dependencies`에 추가
- [ ] `npm install` 실행 → `typescript/package-lock.json` 갱신됨
#### 테스트 작성
스킵. `test/ws.test.ts`의 2개 테스트가 `ws` 런타임 동작을 이미 검증하며, 의존성 위치 변경은 동작에 영향을 주지 않는다.
#### 중간 검증
```bash
cd typescript && npm install && npx tsc --noEmit && npx vitest run
```
기대 결과: 타입 오류 없음, 16개 테스트 PASS.
---
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `typescript/package.json` | REVIEW_API-1 |
| `typescript/package-lock.json` | REVIEW_API-1 |
---
## 최종 검증
```bash
cd typescript && npm install && npx tsc --noEmit && npx vitest run
```
기대 결과: 타입 오류 없음, 16개 테스트 PASS.