<!-- task=m-proto-socket-infrastructure-communication-rail/01_client_transport plan=0 tag=API -->

# Plan - API Client Proto-Socket Transport

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

이 plan의 구현이 끝나면 active `CODE_REVIEW-*-G??.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채워야 한다. 최종 archive, `complete.log`, 리뷰 판정은 code-review-skill 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret 준비, 또는 안전한 진행을 막는 scope 충돌이 있으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정과 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 메울 수 있는 검증 공백만으로는 사용자 리뷰 요청을 만들지 않는다.

## 배경

`Proto-Socket Infrastructure Communication Rail`의 다음 큰 축은 Flutter client가 no-op connector를 실제 Core proto-socket transport로 바꾸는 것이다. Core는 이미 `google.protobuf.Struct` semantic envelope를 받아 task channel을 처리한다. 이 작업은 앱 내부 transport와 envelope 변환 기반만 추가하고, workspace 화면의 task 소비는 후속 split 작업으로 넘긴다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.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/mobile/rules.md`
- `agent-test/local/mobile-smoke.md`
- `apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart`
- `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart`
- `apps/client/lib/src/app/bootstrap.dart`
- `apps/client/lib/src/app/nomadcode_client_app.dart`
- `apps/client/test/integrations/proto_socket_endpoint_config_test.dart`
- `apps/client/test/integrations/proto_socket_lifecycle_test.dart`
- `apps/client/pubspec.yaml`
- `packages/contracts/notes/flutter-core-api-candidates.md`
- `services/core/internal/protosocket/envelope.go`
- `../proto-socket/dart/lib/proto_socket.dart`
- `../proto-socket/dart/lib/src/ws_protobuf_client_io.dart`
- `../proto-socket/dart/lib/src/ws_protobuf_client_web.dart`
- `../proto-socket/dart/lib/src/communicator.dart`
- `../proto-socket/dart/pubspec.yaml`

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

- Existing lifecycle tests cover fake connector state transitions, but not a real transport object returned by `connect`.
- No existing Flutter test covers `google.protobuf.Struct` envelope conversion.
- No existing Flutter test covers parser map registration for `Struct` or `sendRequest<Struct, Struct>`.

### 심볼 참조

- `NoopProtoSocketConnector`: referenced in `apps/client/lib/src/app/bootstrap.dart:19-21` and `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:74-80`; keep compatibility only where a test or disabled config needs it.
- `ProtoSocketConnector.connect`: referenced by lifecycle tests at `apps/client/test/integrations/proto_socket_lifecycle_test.dart:10-15` and lifecycle implementation at `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:36-44`; update all call sites if return type changes.

### 분할 판단

Split policy was evaluated before writing files. Use shared task group `m-proto-socket-infrastructure-communication-rail` with:

- `01_client_transport`: independent foundation; adds envelope/transport/lifecycle return type.
- `02+01_task_service`: depends on `01_client_transport`; builds task channel service on transport.
- `03+02_workspace_integration`: depends on `02+01_task_service`; wires UI to service.

This split is required because transport/protocol, service parsing, and UI state have different verification and risk profiles.

### 범위 결정 근거

This task does not modify workspace UI rendering, task domain models, or Core Go code. It only creates the Flutter transport boundary needed by later tasks. It does not add credentials, real endpoint defaults, Android platform permissions, or automatic app startup connection beyond the existing config switch.

### 빌드 등급

Build lane `cloud-G07`, review lane `cloud-G07`: protocol/schema/auth-adjacent transport work with cross-platform `dart:io`/web conditional behavior and weak existing coverage.

## 구현 체크리스트

- [ ] [API-1] `ProtoSocketEnvelope`와 `ProtoSocketEnvelopeError`를 추가하고 `google.protobuf.Struct` 변환 테스트를 작성한다.
- [ ] [API-2] `NomadProtoSocketClient`/transport wrapper와 `RealProtoSocketConnector`를 추가하고 lifecycle이 연결 transport를 보관하도록 바꾼다. 검증: 설정된 endpoint로 연결 성공 시 state가 `connected`까지 전이된다.
- [ ] [API-3] `bootstrap.dart`가 no-op 기본값 대신 concrete connector를 주입하되 config가 없거나 disabled이면 자동 연결하지 않는 경계를 유지한다.
- [ ] [API-4] `protobuf`를 직접 dependency로 선언하고 lockfile을 갱신한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [API-1] Envelope Struct Bridge

### 문제

Core envelope는 `services/core/internal/protosocket/envelope.go:11-30`의 semantic fields를 `structpb.Struct`로 싣는다. Flutter에는 대응 타입이 없어 `packages/contracts/notes/flutter-core-api-candidates.md:85-89`의 `google.protobuf.Struct` wire message를 생성하거나 파싱할 수 없다.

### 해결 방법

Add `apps/client/lib/src/integrations/proto_socket/proto_socket_envelope.dart`.

Before (`services/core/internal/protosocket/envelope.go:11-30`):

```go
type Envelope struct {
	ProtocolVersion string         `json:"protocol_version"`
	ID              string         `json:"id"`
	CorrelationID   string         `json:"correlation_id"`
	Type            string         `json:"type"`
	Channel         string         `json:"channel"`
	Action          string         `json:"action"`
	Actor           map[string]any `json:"actor,omitempty"`
	Auth            map[string]any `json:"auth,omitempty"`
	Payload         map[string]any `json:"payload,omitempty"`
	Error           *EnvelopeError `json:"error,omitempty"`
	Meta            map[string]any `json:"meta,omitempty"`
}
```

After:

```dart
import 'package:protobuf/well_known_types/google/protobuf/struct.pb.dart';

const protoSocketProtocolVersion = 'nomadcode.proto-socket.v1';

class ProtoSocketEnvelope {
  final String protocolVersion;
  final String id;
  final String correlationId;
  final String type;
  final String channel;
  final String action;
  final Map<String, Object?> actor;
  final Map<String, Object?> auth;
  final Map<String, Object?> payload;
  final ProtoSocketEnvelopeError? error;
  final Map<String, Object?> meta;

  const ProtoSocketEnvelope({
    required this.id,
    required this.type,
    required this.channel,
    required this.action,
    this.protocolVersion = protoSocketProtocolVersion,
    this.correlationId = '',
    this.actor = const {},
    this.auth = const {},
    this.payload = const {},
    this.error,
    this.meta = const {},
  });

  Struct toStruct() => mapToStruct(toMap());
  factory ProtoSocketEnvelope.fromStruct(Struct value) =>
      ProtoSocketEnvelope.fromMap(structToMap(value));
}
```

Implement recursive `mapToStruct`, `structToMap`, `valueFromObject`, and `objectFromValue` for null, bool, num, String, Map, and List.

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

- [ ] Add `apps/client/lib/src/integrations/proto_socket/proto_socket_envelope.dart`.
- [ ] Add `apps/client/test/integrations/proto_socket_envelope_test.dart`.
- [ ] Keep key names identical to Core JSON tags: `protocol_version`, `correlation_id`, `retryable`.

### 테스트 작성

Write `ProtoSocketEnvelope round-trips through google.protobuf.Struct` and `error envelope preserves retryable and details` in `apps/client/test/integrations/proto_socket_envelope_test.dart`.

### 중간 검증

```bash
cd apps/client && flutter test test/integrations/proto_socket_envelope_test.dart
```

Expected: all tests pass.

## [API-2] Concrete Client Connector

### 문제

The current production connector is a no-op at `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:70-80`, and lifecycle drops the concrete connection because `connect` returns `void` at `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:10-12`.

### 해결 방법

Introduce a transport interface and make `ProtoSocketConnector.connect` return it.

Before (`apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart:10-12`):

```dart
abstract interface class ProtoSocketConnector {
  Future<void> connect(ProtoSocketEndpointConfig config);
  Future<void> disconnect();
}
```

After:

```dart
abstract interface class ProtoSocketTransport {
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  });
  Stream<ProtoSocketEnvelope> get events;
  Future<void> close();
}

abstract interface class ProtoSocketConnector {
  Future<ProtoSocketTransport> connect(ProtoSocketEndpointConfig config);
  Future<void> disconnect();
}
```

Create `apps/client/lib/src/integrations/proto_socket/proto_socket_client.dart` with:

- `class NomadProtoSocketClient extends WsProtobufClient`
- parser map for `Struct.fromBuffer`, keyed by both `Struct.toString()` and `Struct.getDefault().info_.qualifiedMessageName`
- `RealProtoSocketConnector` that selects `WsProtobufClient.connect` or `connectSecure` from `ProtoSocketEndpointConfig`
- `ProtoSocketTransport` wrapper that delegates `sendRequest<Struct, Struct>` and maps event `Struct` messages into `ProtoSocketEnvelope`

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

- [ ] Update `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart`.
- [ ] Add `apps/client/lib/src/integrations/proto_socket/proto_socket_client.dart`.
- [ ] Update `apps/client/test/integrations/proto_socket_lifecycle_test.dart` fake connector return type.
- [ ] Add connector/transport tests using an injectable fake `WsProtobufClient` factory or fake `ProtoSocketTransport`; do not open network sockets in unit tests.

### 테스트 작성

Update lifecycle tests to assert `lifecycle.transport` is set on connect and cleared on disconnect. Add tests for failed connect preserving `lastError` and not leaving a stale transport.

### 중간 검증

```bash
cd apps/client && flutter test test/integrations/proto_socket_lifecycle_test.dart test/integrations/proto_socket_envelope_test.dart
```

Expected: all tests pass.

## [API-3] Bootstrap Concrete Connector

### 문제

Bootstrap wires `NoopProtoSocketConnector` at `apps/client/lib/src/app/bootstrap.dart:16-21`, so even an enabled config cannot reach Core.

### 해결 방법

Import the concrete connector and instantiate it, while keeping no auto-connect when `_protoSocketConfig` is null or disabled.

Before (`apps/client/lib/src/app/bootstrap.dart:16-21`):

```dart
final _protoSocketLifecycle = ProtoSocketLifecycle(
  connector: NoopProtoSocketConnector(),
);
```

After:

```dart
final _protoSocketLifecycle = ProtoSocketLifecycle(
  connector: RealProtoSocketConnector(),
);
```

Do not call `connect` in `bootstrapNomadCodeClient`; UI wiring in `03+02_workspace_integration` owns config-enabled startup.

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

- [ ] Update `apps/client/lib/src/app/bootstrap.dart` imports and connector instantiation.
- [ ] Keep `NoopProtoSocketConnector` available for tests and disabled/fake scenarios.

### 테스트 작성

No separate bootstrap test in this subtask; lifecycle and app wiring are covered in adjacent tests. Do not add Firebase bootstrap tests.

### 중간 검증

```bash
cd apps/client && flutter analyze --no-fatal-infos
```

Expected: analyzer completes without fatal issues.

## [API-4] Direct Protobuf Dependency

### 문제

`apps/client/pubspec.yaml:46-48` depends on `proto_socket`, but importing `google.protobuf.Struct` directly from `package:protobuf` would rely on a transitive dependency.

### 해결 방법

Add a direct dependency matching `../proto-socket/dart/pubspec.yaml`, then refresh lockfile.

Before (`apps/client/pubspec.yaml:46-51`):

```yaml
  proto_socket:
    path: ../../../proto-socket/dart

  # HTTP client
  http: ^1.3.0
```

After:

```yaml
  proto_socket:
    path: ../../../proto-socket/dart

  protobuf: ^6.0.0

  # HTTP client
  http: ^1.3.0
```

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

- [ ] Update `apps/client/pubspec.yaml`.
- [ ] Run `cd apps/client && flutter pub get` to update `apps/client/pubspec.lock`.

### 테스트 작성

No separate test; dependency correctness is covered by `flutter pub get`, analyzer, and transport tests.

### 중간 검증

```bash
cd apps/client && flutter pub get
```

Expected: dependency resolution succeeds and lockfile records direct `protobuf`.

## 의존 관계 및 구현 순서

This subtask has no predecessor. Complete it before `02+01_task_service`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/integrations/proto_socket/proto_socket_envelope.dart` | API-1 |
| `apps/client/test/integrations/proto_socket_envelope_test.dart` | API-1 |
| `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart` | API-2 |
| `apps/client/lib/src/integrations/proto_socket/proto_socket_client.dart` | API-2 |
| `apps/client/test/integrations/proto_socket_lifecycle_test.dart` | API-2 |
| `apps/client/lib/src/app/bootstrap.dart` | API-3 |
| `apps/client/pubspec.yaml` | API-4 |
| `apps/client/pubspec.lock` | API-4 |

## 최종 검증

```bash
cd apps/client && flutter pub get
cd apps/client && flutter test test/integrations/proto_socket_endpoint_config_test.dart test/integrations/proto_socket_lifecycle_test.dart test/integrations/proto_socket_envelope_test.dart
cd apps/client && flutter analyze --no-fatal-infos
```

Expected: pub get succeeds; targeted tests pass; analyzer has no fatal issues. Cached Flutter test output is acceptable only if no implementation files changed after the run.

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