<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=0 tag=API -->

# Plan - API Core Proto-Socket Diagnostics

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 반드시 채운다. 구현 후 검증을 실행하고 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거와 재개 조건을 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

Core proto-socket server는 연결과 broadcast 실패를 로그로 남기지만 connection id, protocol version, channel, action, error code를 구조화된 속성으로 남기지 않는다. Client debug surface도 Core가 보내는 response/event meta를 소비해야 하므로 먼저 Core가 안정적인 diagnostics meta와 로그를 제공해야 한다. 이 subtask는 계약 문서 subtask 완료 후 진행한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 기준으로 하며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/workflow-core/PHASE.md`
- `agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/dispatcher.go`
- `services/core/internal/protosocket/envelope.go`
- `services/core/internal/protosocket/events.go`
- `services/core/internal/protosocket/tasks.go`
- `services/core/internal/http/router.go`
- `services/core/internal/protosocket/server_test.go`
- `services/core/internal/protosocket/tasks_test.go`
- `services/core/internal/protosocket/envelope_test.go`
- `packages/contracts/notes/flutter-core-api-candidates.md`

### 테스트 환경 규칙

`test_env=local`로 판단했고 `agent-test/local/rules.md`를 읽었다. 이 파일은 로컬 테스트 금지를 명시하므로 현 세션에서는 검증 명령을 실행하지 않는다. 적용 profile은 `core-smoke`이며 core 변경의 필수 검증은 `cd services/core && go test ./...`, 보조 검증은 `cd services/core && go vet ./...`다. 명령은 후속 구현/리뷰 에이전트가 허용된 원격/검증 환경에서 실행한다.

### 테스트 커버리지 공백

- connection id가 response/event meta에 들어가는 동작: 기존 테스트 없음. `server_test.go` 또는 `tasks_test.go`에 추가 필요.
- protocol/channel/action/error_code 구조화 로그: 기존 테스트 없음. `slog` buffer 기반 테스트 추가 필요.
- 기존 task action 의미 보존: `tasks_test.go`가 create/list/get/enqueue, auth, error code를 이미 검증한다.

### 심볼 참조

none. 기존 public symbol rename/remove 없이 server 내부 struct/helper를 추가한다.

### 분할 판단

split decision policy를 평가했다. 이 subtask는 `12+11_core_diagnostics`이며 predecessor `11_contracts_diagnostics`의 `complete.log`가 필요하다. 현재 active/archive predecessor completion은 없음으로 확인했다. 구현은 `11` PASS 후 시작해야 한다.

### 범위 결정 근거

Core proto-socket server, dispatcher/event helper, core tests만 수정한다. Flutter debug UI와 contracts note 수정은 각각 `13+11,12_client_diagnostics`, `11_contracts_diagnostics` 범위다. REST task API shape, auth policy, workflow service behavior, DB schema는 변경하지 않는다.

### 빌드 등급

`cloud-G07`: protocol diagnostics, websocket connection lifecycle, structured logging, concurrency-safe client registry를 건드리므로 cloud lane과 높은 등급이 필요하다.

## 구현 체크리스트

- [ ] `11_contracts_diagnostics`가 PASS되어 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다.
- [ ] Core proto-socket 연결마다 stable `connection_id`를 만들고 연결/해제/request/broadcast 로그에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 구조화 속성으로 남긴다.
- [ ] response/event envelope `meta`에 diagnostics 필드를 additive로 채운다.
- [ ] Core 테스트로 diagnostics meta와 구조화 로그를 검증한다.
- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행한다.
- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 directory name은 `12+11_core_diagnostics`이므로 predecessor는 같은 task group의 `11_*` subtask뿐이다. `agent-task/m-proto-socket-infrastructure-communication-rail/11_contracts_diagnostics/complete.log` 또는 matching archive `complete.log`가 생기기 전에는 구현을 시작하지 않는다.

### [API-1] Core Connection Diagnostics

#### 문제

[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:20)는 clients map에 client pointer만 저장하고 connection id가 없다. 연결/해제 로그도 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:58)와 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:102)에서 generic message만 남긴다.

Before:

```go
20	type Server struct {
21		cfg        Config
22		dispatcher *Dispatcher
23		logger     *slog.Logger
24		mu         sync.Mutex
25		clients    map[*toki.WsClient]struct{}
26	}
...
58		client.AddDisconnectListener(func(c *toki.WsClient) {
59			s.mu.Lock()
60			delete(s.clients, c)
61			s.mu.Unlock()
62			s.logger.Info("websocket client disconnected")
63		})
...
102		s.logger.Info("websocket client connected successfully")
```

#### 해결 방법

client registry 값을 diagnostics struct로 바꾸고 accept 시 `connection_id`를 생성한다. nil logger 가능성도 방어한다. 연결/해제/request 로그에는 contract subtask의 필드명을 사용한다.

After:

```go
type clientDiagnostics struct {
	connectionID string
}

type Server struct {
	...
	clients map[*toki.WsClient]clientDiagnostics
}

connectionID := "conn-" + generateID()
s.clients[client] = clientDiagnostics{connectionID: connectionID}
s.logInfo("proto-socket client connected", "connection_id", connectionID, "protocol_version", ProtocolVersion)
```

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/protosocket/server.go`: client diagnostics registry와 log helper 추가.
- [ ] `services/core/internal/protosocket/server_test.go`: connection id가 response meta 또는 logs에 남는지 검증.

#### 테스트 작성

`server_test.go`에 `slog.NewJSONHandler(&bytes.Buffer, nil)` 기반 테스트를 추가한다. 연결 성공/해제 또는 request 처리 후 JSON log에 `connection_id`, `protocol_version`이 포함되는지 assertion한다.

#### 중간 검증

```bash
cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1
```

기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.

### [API-2] Core Envelope Meta And Error Logs

#### 문제

[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:65)는 request를 dispatch하지만 request/response diagnostics를 남기지 않는다. [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:22)의 error response에는 code가 있지만 meta에는 diagnostics가 없다.

Before:

```go
65		toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
66			env, err := EnvelopeFromStruct(req)
...
82			resEnv := s.dispatcher.Dispatch(r.Context(), env)
83			resStruct, err := resEnv.ToStruct()
...
99			return resStruct, nil
100		})
```

#### 해결 방법

`withDiagnosticsMeta(env Envelope, connectionID string) Envelope`와 `errorCode(env Envelope) string` helper를 추가한다. 모든 response/event 직전 meta에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 additive로 넣고, request 처리 로그도 같은 값을 남긴다. Broadcast는 client별 clone에 connection id를 채운 뒤 전송한다.

After:

```go
resEnv := withDiagnosticsMeta(s.dispatcher.Dispatch(r.Context(), env), connectionID)
s.logInfo("proto-socket request handled",
	"connection_id", connectionID,
	"protocol_version", resEnv.ProtocolVersion,
	"channel", resEnv.Channel,
	"action", resEnv.Action,
	"error_code", errorCode(resEnv),
)
```

#### 수정 파일 및 체크리스트

- [ ] `services/core/internal/protosocket/server.go`: response/event meta 주입, request/broadcast log 추가.
- [ ] `services/core/internal/protosocket/events.go`: error/success response helper와 meta 주입 방식 충돌 여부 확인.
- [ ] `services/core/internal/protosocket/tasks_test.go`: task response/error envelope meta assertions 추가.
- [ ] `services/core/internal/protosocket/server_test.go`: broadcast meta/log assertions 추가.

#### 테스트 작성

`tasks_test.go` route-level tests에서 `task.list` response와 error response의 `Meta["connection_id"]`, `Meta["protocol_version"]`, `Meta["channel"]`, `Meta["action"]`, `Meta["error_code"]`를 검증한다. `server_test.go` broadcast test는 client별 event meta에 connection id가 들어가는지 검증한다.

#### 중간 검증

```bash
cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
```

기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/protosocket/server.go` | API-1, API-2 |
| `services/core/internal/protosocket/events.go` | API-2 |
| `services/core/internal/protosocket/server_test.go` | API-1, API-2 |
| `services/core/internal/protosocket/tasks_test.go` | API-2 |

## 최종 검증

```bash
cd services/core && go test ./...
cd services/core && go vet ./...
```

기대 결과: 두 명령 모두 실패 없이 종료한다. Go test cache는 diagnostics 변경 확인에는 충분하지 않으므로 필요한 targeted tests는 `-count=1`로 중간 검증에서 fresh 실행한다. 명령은 허용된 원격/검증 환경에서 실행한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
