<!-- task=02+yaml_validation plan=1 tag=REVIEW_REFACTOR -->

# YAML Validation Review Follow-up

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

**경고: 구현 완료의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 모두 채우는 것이다.**
구현 체크리스트를 기준으로 작업하고, plan과 review stub 양쪽의 체크리스트를 완료하며, 중간/최종 검증을 실행한 실제 출력으로 `CODE_REVIEW-*-G??.md`를 채운다.
리뷰 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시는 실행하지 말고, `코드리뷰 전용 체크리스트`는 수정하거나 체크하지 않는다.

## 배경

Review 0에서 정상 테스트와 샘플 smoke는 통과했지만, 일부 invalid YAML이 신규 validation layer를 통과해 generated parser/type cast 오류로 실패하는 문제가 확인됐다.
이번 follow-up은 REFACTOR-1/2의 "DataBuild.fromJson 전 구조 검증" 계약을 실제 오류 케이스까지 닫고, 그 케이스를 테스트로 고정한다.

## 분석 결과

### 읽은 파일

- `lib/oto/application.dart`
- `lib/oto/pipeline/pipeline.dart`
- `lib/oto/commands/command.dart`
- `lib/oto/data/command_data.dart`
- `test/oto_application_test.dart`
- `test/oto_core_test.dart`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/pipeline/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`

### 리뷰 실패 근거

- YAML root가 list이면 `Application.getMapFromYamlA()`에서 `YamlMap` cast가 먼저 발생해 `_validateBuildMap()`의 root guard가 실행되지 않는다.
- `property`가 list이면 `_validateBuildMap()`에서 걸러지지 않고 `DataBuild.fromJson()`의 generated cast 오류로 실패한다.
- `commands[].id`가 int이면 `_validateCommandList()`가 통과시키고 `DataCommand.fromJson()`의 generated cast 오류로 실패한다.
- 위 invalid YAML 케이스를 고정하는 테스트가 없어, 기존 검증 출력만으로 신규 validation contract를 신뢰하기 어렵다.

### 빌드 등급

- build lane: `cloud-G07`. YAML schema/protocol 검증 실패의 후속 작업이며, 실제 CLI smoke에서 parser/type-cast 오류가 재현되었으므로 기존 cloud-G07을 유지한다.

## 구현 체크리스트

- [ ] YAML root shape guard를 `YamlMap` cast 전에 추가한다.
- [ ] `property`가 존재할 때 map 타입인지 `DataBuild.fromJson()` 전에 검증한다.
- [ ] command entry의 `id`와 `command`를 non-empty `String` 기준으로 검증한다.
- [ ] 신규 invalid YAML 테스트와 기존 smoke 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] YAML root shape guard

#### 문제

`lib/oto/application.dart:255`의 `getMapFromYamlA()`는 `loadYaml(yamlStr)` 결과를 바로 `YamlMap` 변수에 할당한다.
따라서 root가 list/scalar인 YAML은 `_validateBuildMap()`에 도달하지 못하고 Dart type error로 실패한다.

#### 해결 방법

`loadYaml()` 결과를 `dynamic`으로 받은 뒤 `YamlMap`인지 확인한다.
root가 map이 아니면 `getMapFromYamlA()`가 `null`을 반환하거나 별도 validation result를 만들고, `Application.build()`가 `Validate build yaml` phase의 명시 메시지로 실패하게 한다.

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

- [ ] `lib/oto/application.dart`: `YamlMap` cast 전에 root 타입을 검사한다.
- [ ] `lib/oto/application.dart`: root non-map 실패 메시지가 generated type error가 아니라 validation message인지 확인한다.

#### 테스트 작성

- `test/oto_application_test.dart`에 root list YAML이 실패하고 `Build YAML root must be a map` 계열 메시지를 반환하는 테스트를 추가한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart
```

기대 결과: root non-map 테스트와 기존 Application 테스트가 모두 통과한다.

### [REVIEW_REFACTOR-2] property 타입 검증

#### 문제

`lib/oto/application.dart:193`의 `_validateBuildMap()`가 `property` 타입을 검사하지 않는다.
`property`가 list인 YAML은 `DataBuild.fromJson()`에서 `type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>?'`로 실패한다.

#### 해결 방법

`property`는 기존 동작을 유지해 생략은 허용하되, 값이 존재하면 `Map`인지 검사한다.
실패 메시지는 `"property" must be a map`처럼 section 이름과 기대 타입을 포함한다.

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

- [ ] `lib/oto/application.dart`: `_validateBuildMap()`에 optional `property` 타입 검증을 추가한다.
- [ ] `lib/oto/application.dart`: `property` 생략 YAML의 기존 성공 동작은 유지한다.

#### 테스트 작성

- `test/oto_application_test.dart`에 non-map `property` YAML이 `Validate build yaml` 단계에서 실패하는 테스트를 추가한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart
```

기대 결과: property 타입 테스트와 기존 Application 테스트가 모두 통과한다.

### [REVIEW_REFACTOR-3] command id/type 문자열 검증

#### 문제

`lib/oto/application.dart:231`의 `_validateCommandList()`는 `id`와 `command`가 null/빈 문자열인지 위주로 검사한다.
`id: 12`처럼 String이 아닌 값은 통과 후 `DataCommand.fromJson()`에서 type cast 오류로 실패한다.

#### 해결 방법

각 command entry의 `id`와 `command`를 `String`으로 제한하고, `trim().isNotEmpty`를 검사한다.
`command`가 String인 경우에만 `CommandType` 이름 매칭과 `Command.registeredTypes` 검증을 수행한다.

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

- [ ] `lib/oto/application.dart`: `id is String && id.trim().isNotEmpty` 검증으로 바꾼다.
- [ ] `lib/oto/application.dart`: `command is String && command.trim().isNotEmpty` 검증으로 바꾼다.
- [ ] `lib/oto/application.dart`: 오류 메시지에 `commands[index]`와 가능한 경우 `id`를 포함한다.

#### 테스트 작성

- `test/oto_application_test.dart`에 non-string `id` YAML이 `Validate command list` 단계에서 실패하는 테스트를 추가한다.
- 가능하면 non-string `command` YAML도 같은 contract로 고정한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart
```

기대 결과: command entry 타입 테스트와 기존 Application 테스트가 모두 통과한다.

### [REVIEW_REFACTOR-4] 검증 신뢰도 회복

#### 문제

Review 0의 최종 검증은 정상 경로 회귀를 확인했지만, 신규 validation이 책임지는 invalid YAML 케이스를 고정하지 못했다.

#### 해결 방법

추가한 테스트로 invalid YAML contract를 고정하고, 기존 `dart analyze`, application/core 테스트, 샘플 smoke를 다시 실행한다.

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

- [ ] `test/oto_application_test.dart`: invalid YAML validation 테스트를 추가한다.
- [ ] `CODE_REVIEW-cloud-G07.md`: 추가 테스트 및 최종 검증의 실제 stdout/stderr를 기록한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart test/oto_core_test.dart
```

기대 결과: Application/Core 테스트가 모두 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/application.dart` | REVIEW_REFACTOR-1, REVIEW_REFACTOR-2, REVIEW_REFACTOR-3 |
| `test/oto_application_test.dart` | REVIEW_REFACTOR-1~4 |
| `test/oto_core_test.dart` | 검증 대상 |

## 최종 검증

```bash
dart analyze
dart test test/oto_application_test.dart test/oto_core_test.dart
dart run bin/main.dart exe -f assets/yaml/sample/01_basic.yaml
dart run bin/main.dart exe -f assets/yaml/sample/02_pipeline_if.yaml
```

기대 결과: analyze와 테스트가 통과하고, 두 샘플 실행이 `Build Successfully Complete`를 출력한다.

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