<!-- task=m-architecture-refactor-foundation/09_proto_legacy_contract plan=0 tag=API -->

# Plan - API

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

이 계획을 구현한 뒤에는 반드시 같은 디렉터리의 `CODE_REVIEW-*-G??.md`에서 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채운다. 검증 명령을 실행하고 실제 stdout/stderr를 붙이며, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log archive, `complete.log` 작성, task archive 이동은 code-review-skill 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 충돌이 없이는 진행할 수 없으면 `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 기록하고 중단한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 증거 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

`control.proto`와 `node.proto`에 legacy scheduling/direct node registration placeholder가 아직 active message/field로 생성된다. 실제 운영 방향은 Edge-owned status/operation과 `runtime.proto`의 Node-Edge registration이므로, legacy placeholder를 reserved tombstone으로 고정해 새 호출자가 실수로 활성 계약처럼 사용하지 못하게 한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 조건은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 prompt는 금지이며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- Task ids:
  - `proto-legacy`: legacy scheduling/node registration proto placeholder를 deprecated/reserved/remove 중 하나로 정리하고 생성 코드와 call-site를 맞춘다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/client/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/client-smoke.md`
- `Makefile`
- `proto/iop/control.proto`
- `proto/iop/node.proto`
- `proto/iop/runtime.proto`
- `proto/gen/iop/control.pb.go`
- `proto/gen/iop/node.pb.go`
- `apps/control-plane/internal/wire/edge.go`
- `apps/control-plane/internal/wire/edge_test.go`
- `apps/control-plane/internal/wire/edge_server_test.go`
- `apps/client/lib/iop_wire/parser_map.dart`
- `apps/client/lib/gen/proto/iop/control.pb.dart`
- `apps/client/lib/gen/proto/iop/control.pbjson.dart`
- `apps/client/lib/gen/proto/iop/node.pb.dart`
- `apps/client/lib/gen/proto/iop/node.pbenum.dart`
- `apps/client/lib/gen/proto/iop/node.pbjson.dart`
- `apps/client/test/iop_wire/generated_proto_import_test.dart`
- `apps/client/test/iop_wire/parser_map_test.dart`

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`를 읽었다.
- 매칭 프로필: `platform-common-smoke`, `control-plane-smoke`, `client-smoke`.
- 적용 명령: proto 원본 변경은 `make proto`, Dart binding 변경은 `make proto-dart`, Go side는 `go test -count=1 ./apps/control-plane/... ./packages/go/... ./proto/gen/...`, focused client guard는 `cd apps/client && flutter test test/iop_wire/generated_proto_import_test.dart test/iop_wire/parser_map_test.dart`.
- 도구 확인: `protoc`, `protoc-gen-go`, `protoc-gen-dart`, `flutter`가 현재 checkout에서 발견됐다.
- client 프로필은 Flutter 검증을 원격 runner 기준으로 둔다. 현재 checkout에서 focused Flutter test는 실행 가능하지만, 실제 원격 runner 검증을 수행하려면 변경분 동기화 여부를 구현 에이전트가 기록한다.
- `<확인 필요>` 값은 없다.

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

- `ScheduleRequest`/`ScheduleResponse`가 active fields를 생성하지 않는다는 guard가 없다. 새 Go descriptor test가 필요하다.
- `NodeInfo`/`NodeRegisterRequest`/`NodeRegisterResponse`와 `NodeStatus`가 direct registration contract로 보이지 않도록 비우는 guard가 없다. 새 Go descriptor test가 필요하다.
- Dart compile guard가 `NodeInfo.address`를 직접 사용한다. reserved tombstone 전환 후 active `runtime.RegisterRequest` 중심으로 업데이트해야 한다.

### 심볼 참조

- 제거/비활성화 대상: `ScheduleRequest`, `ScheduleResponse`, `NodeInfo`, `NodeRegisterRequest`, `NodeRegisterResponse`, `NodeStatus`의 active fields.
- `rg --sort path` 결과, source call site는 `apps/client/test/iop_wire/generated_proto_import_test.dart`의 `NodeInfo` fixture뿐이다. 나머지는 proto 원본, Go/Dart 생성물, control-plane domain rule의 설명이다.
- `packages/go/config.NodeInfo`는 별도 config struct로 이름만 같고 proto symbol이 아니므로 수정하지 않는다.

### 분할 판단

- split 정책을 먼저 평가했다. Milestone task group은 이미 split 구조이며, 이 작업은 `agent-task/m-architecture-refactor-foundation/09_proto_legacy_contract/` 단일 active subtask로 둔다.
- schema 원본, generated Go/Dart, guard test는 같은 proto generation 단위라 분리하면 중간 상태가 깨진 생성물로 남는다.
- broad call-site rollout은 없다. source call site는 Dart compile guard 하나뿐이고 generated files는 `make proto`/`make proto-dart`의 산출물이다.
- `09_proto_legacy_contract`는 독립 subtask이며 directory name에 predecessor가 없으므로 active/archive predecessor `complete.log` 확인은 필요 없다. 이전 01-08 subtask는 archive 완료 상태로 확인됐다.

### 범위 결정 근거

- `runtime.proto`의 `RegisterRequest`/`RegisterResponse`는 active Node-Edge startup contract이므로 변경하지 않는다.
- `EdgeStatusRequest`, `EdgeStatusResponse`, `EdgeNodeSnapshot`, command/event proto는 현재 active Control Plane-Edge wire 계약이므로 legacy cleanup에 포함하지 않는다.
- `packages/go/config.NodeInfo`, Edge/Node runtime token handling, Mattermost token handling은 proto legacy placeholder와 무관하므로 제외한다.
- generated files는 직접 편집하지 않고 `make proto`/`make proto-dart`로만 갱신한다.

### 빌드 등급

- `cloud-G06`: protocol/schema와 Go/Dart generated binding을 동시에 다루며, descriptor regression과 client compile guard가 필요하다.

## 구현 체크리스트

- [ ] `proto/iop/control.proto`의 legacy scheduling placeholder를 reserved tombstone으로 전환한다.
- [ ] `proto/iop/node.proto`의 legacy node snapshot/direct registration placeholder와 enum을 reserved tombstone으로 전환한다.
- [ ] `make proto`와 `make proto-dart`로 Go/Dart 생성물을 갱신하고, generated files를 사람이 직접 편집하지 않는다.
- [ ] Go descriptor guard와 Dart generated import guard를 새 reserved 계약에 맞게 업데이트한다.
- [ ] 최종 검증 명령을 실행하고 결과를 `CODE_REVIEW-*-G??.md`에 붙인다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. `control.proto`와 `node.proto` 원본을 먼저 수정한다.
2. `make proto`, `make proto-dart`로 생성물을 갱신한다.
3. 생성물 변화에 맞춰 Go/Dart tests를 갱신한다.
4. 검증 명령과 deterministic search를 실행한다.

### [API-1] Control Scheduling Legacy Tombstone

#### 문제

`proto/iop/control.proto:16`-`31`의 `ScheduleRequest`/`ScheduleResponse`는 legacy placeholder라고 주석을 달았지만 `job_id`, `target`, `address`, `token` 같은 active field를 생성한다.

현재:

```proto
// proto/iop/control.proto:16
message ScheduleRequest {
  string job_id           = 1;
  string target           = 2;
  repeated PolicyRule policies = 3;
  map<string, string> metadata = 4;
}

message ScheduleResponse {
  string node_id  = 1;
  string address  = 2;
  string token    = 3;
}
```

#### 해결 방법

메시지 이름은 tombstone으로 남기고 기존 field number/name을 reserved 처리한다.

```proto
message ScheduleRequest {
  reserved 1 to 4;
  reserved "job_id", "target", "policies", "metadata";
}

message ScheduleResponse {
  reserved 1 to 3;
  reserved "node_id", "address", "token";
}
```

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

- [ ] `proto/iop/control.proto`: reserved tombstone으로 변경한다.
- [ ] `proto/gen/iop/control.pb.go`: `make proto` 산출물로 갱신한다.
- [ ] `apps/client/lib/gen/proto/iop/control.pb.dart`: `make proto-dart` 산출물로 갱신한다.
- [ ] `apps/client/lib/gen/proto/iop/control.pbjson.dart`: `make proto-dart` 산출물로 갱신한다.

#### 테스트 작성

`apps/control-plane/internal/wire/edge_test.go`에 `ScheduleRequest`/`ScheduleResponse` descriptor fields가 0개임을 검증하는 regression test를 추가한다.

#### 중간 검증

```bash
make proto
make proto-dart
go test -count=1 ./apps/control-plane/internal/wire ./proto/gen/...
```

### [API-2] Node Registration Legacy Tombstone

#### 문제

`proto/iop/node.proto:7`-`38`은 legacy node snapshot/direct registration placeholder인데 `address`, `token`, `NodeStatus` 같은 active contract 필드를 생성한다. active startup contract는 `proto/iop/runtime.proto:132`-`143`의 `RegisterRequest`/`RegisterResponse`다.

현재:

```proto
// proto/iop/node.proto:10
message NodeInfo {
  string node_id   = 1;
  string name      = 2;
  string address   = 3;
  string version   = 4;
  NodeStatus status = 5;
  map<string, string> labels = 6;
}
```

#### 해결 방법

`NodeInfo`, `NodeRegisterRequest`, `NodeRegisterResponse`를 reserved tombstone으로 바꾸고, `NodeStatus`는 value 0만 남겨 inactive enum으로 둔다.

```proto
message NodeInfo {
  reserved 1 to 6;
  reserved "node_id", "name", "address", "version", "status", "labels";
}

enum NodeStatus {
  NODE_STATUS_UNSPECIFIED = 0;
  reserved 1 to 3;
  reserved "NODE_STATUS_ONLINE", "NODE_STATUS_OFFLINE", "NODE_STATUS_DRAINING";
}
```

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

- [ ] `proto/iop/node.proto`: legacy message/enum active fields를 reserved 처리한다.
- [ ] `proto/gen/iop/node.pb.go`: `make proto` 산출물로 갱신한다.
- [ ] `apps/client/lib/gen/proto/iop/node.pb.dart`: `make proto-dart` 산출물로 갱신한다.
- [ ] `apps/client/lib/gen/proto/iop/node.pbenum.dart`: `make proto-dart` 산출물로 갱신한다.
- [ ] `apps/client/lib/gen/proto/iop/node.pbjson.dart`: `make proto-dart` 산출물로 갱신한다.

#### 테스트 작성

`apps/control-plane/internal/wire/edge_test.go`에 `NodeInfo`/`NodeRegisterRequest`/`NodeRegisterResponse` descriptor fields가 0개이고 `NodeStatus`가 only-unspecified enum임을 검증하는 regression test를 추가한다.

#### 중간 검증

```bash
make proto
make proto-dart
go test -count=1 ./apps/control-plane/internal/wire ./proto/gen/...
```

### [API-3] Generated Binding Guards And Call-Site Cleanup

#### 문제

`apps/client/test/iop_wire/generated_proto_import_test.dart:24`-`34`가 generated `NodeInfo.address`를 active field처럼 사용한다. reserved tombstone 전환 후 이 테스트는 깨지며, active `runtime.RegisterRequest`/`RegisterResponse` guard로 바뀌어야 한다.

현재:

```dart
// apps/client/test/iop_wire/generated_proto_import_test.dart:24
final nodeInfo = NodeInfo()
  ..nodeId = 'node-1'
  ..name = 'test-node'
  ..address = '127.0.0.1:8080'
  ..version = '1.0.0';
```

#### 해결 방법

`NodeInfo` direct field fixture를 제거하고 `runtime.RegisterRequest`/`RegisterResponse` 또는 empty tombstone serialization guard로 바꾼다. Control Plane wire parser map은 Client hello만 사용하므로 변경하지 않는다.

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

- [ ] `apps/client/test/iop_wire/generated_proto_import_test.dart`: active node startup guard를 `runtime.pb.dart` 중심으로 갱신한다.
- [ ] `apps/control-plane/internal/wire/edge_test.go`: legacy tombstone descriptor guard를 추가하고 기존 active Edge wire tests를 유지한다.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: generated fixture가 legacy 제거 후에도 깨지지 않는지 확인하고, 필요 시 DGX test fixture를 generic 이름으로만 정리한다.

#### 테스트 작성

Go descriptor regression과 Dart generated import guard를 작성한다.

#### 중간 검증

```bash
go test -count=1 ./apps/control-plane/internal/wire ./proto/gen/...
cd apps/client && flutter test test/iop_wire/generated_proto_import_test.dart test/iop_wire/parser_map_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `proto/iop/control.proto` | API-1 |
| `proto/iop/node.proto` | API-2 |
| `proto/gen/iop/control.pb.go` | API-1 |
| `proto/gen/iop/node.pb.go` | API-2 |
| `apps/client/lib/gen/proto/iop/control.pb.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/control.pbjson.dart` | API-1 |
| `apps/client/lib/gen/proto/iop/node.pb.dart` | API-2 |
| `apps/client/lib/gen/proto/iop/node.pbenum.dart` | API-2 |
| `apps/client/lib/gen/proto/iop/node.pbjson.dart` | API-2 |
| `apps/control-plane/internal/wire/edge_test.go` | API-1, API-2, API-3 |
| `apps/control-plane/internal/wire/edge_server_test.go` | API-3 |
| `apps/client/test/iop_wire/generated_proto_import_test.dart` | API-3 |

## 최종 검증

```bash
make proto
make proto-dart
go test -count=1 ./apps/control-plane/... ./packages/go/... ./proto/gen/...
cd apps/client && flutter test test/iop_wire/generated_proto_import_test.dart test/iop_wire/parser_map_test.dart
rg --sort path -n "GetAddress|GetToken|hasAddress|hasToken|set address|set token|NODE_STATUS_ONLINE|NODE_STATUS_OFFLINE|NODE_STATUS_DRAINING" proto/gen/iop/control.pb.go proto/gen/iop/node.pb.go apps/client/lib/gen/proto/iop/control.pb.dart apps/client/lib/gen/proto/iop/control.pbjson.dart apps/client/lib/gen/proto/iop/node.pb.dart apps/client/lib/gen/proto/iop/node.pbenum.dart apps/client/lib/gen/proto/iop/node.pbjson.dart
```

기대 결과: codegen 명령은 성공하고 generated diff만 남는다. Go/Flutter focused tests는 통과한다. 마지막 `rg --sort path`는 출력이 없어야 하며, 출력이 있으면 intentional exception이 아니라 실패다. Go test cache output은 schema 변경 검증에 부적합하므로 `-count=1`을 유지한다.

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