<!-- task=02+yaml_validation plan=0 tag=REFACTOR -->

# YAML Validation Refactor

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

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

## 배경

현재 YAML은 `DataBuild.fromJson()`과 pipeline 초기화까지 들어간 뒤에야 오류가 드러난다.
AI-first 관점에서는 어떤 YAML 섹션, command id, workflow node가 잘못됐는지 먼저 알 수 있어야 다음 수정이 빨라진다.
이 작업은 command-specific 파라미터 검증까지 확장하지 않고, build/pipeline 기본 구조와 등록 커맨드 참조를 명확히 검증한다.

## 의존 관계 및 구현 순서

- `agent-task/01_command_catalog`의 `complete.log`가 선행되어야 한다. `Command.specs` 또는 catalog helper를 validation 메시지의 기준으로 사용한다.
- 이 task가 완료된 뒤 `agent-task/03+execution_context_boundaries`를 진행한다.

## 분석 결과

### 읽은 파일

- `lib/oto/application.dart`
- `lib/oto/core/data_composer.dart`
- `lib/oto/data/base_data.dart`
- `lib/oto/data/command_data.dart`
- `lib/oto/data/pipeline_data.dart`
- `lib/oto/pipeline/pipeline.dart`
- `test/oto_application_test.dart`
- `test/oto_core_test.dart`
- `pubspec.yaml`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/pipeline/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`

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

- invalid workflow command id는 `test/oto_application_test.dart:14`와 `test/oto_core_test.dart:56`이 커버한다.
- YAML root 타입, missing `commands`, missing `pipeline.workflow`, workflow node가 Map이 아닌 경우는 기존 테스트가 없다.
- 사용자가 유닛 테스트를 이후 별도 작업한다고 했으므로 신규 테스트는 작성하지 않고, 기존 테스트와 샘플 smoke로 회귀를 확인한다.

### 심볼 참조

- renamed/removed symbol: none.
- YAML parse entry: `Application.getMapFromYamlA()` at `lib/oto/application.dart:177`.
- build parse entry: `DataBuild.fromJson(getMapFromYamlA(...))` at `lib/oto/application.dart:99`.
- pipeline validation entry: `Pipeline.pipelineInitialize()` at `lib/oto/pipeline/pipeline.dart:29`.
- command id validation call sites: `lib/oto/pipeline/pipeline_exe.dart:58`, `lib/oto/pipeline/pipeline_exe_handle.dart:47`.

### 범위 결정 근거

- 각 `DataParam` 세부 필드 검증은 제외한다. 모든 command data model을 읽고 손대야 하므로 별도 작업이다.
- scheduler daemon YAML 등록 검증은 제외한다. scheduler 도메인 변경 없이 `Application.build(BuildType.file/test)` 흐름만 다룬다.
- 샘플 파일 내용 재작성은 제외한다. 현재 샘플 정리는 앞선 작업에서 했고, 이번 task는 검증 레이어만 추가한다.

### 빌드 등급

- build lane: `cloud-G07`. YAML schema/protocol 성격의 검증이고 기존 테스트가 구조 오류 전체를 커버하지 않으므로 더 강한 리뷰가 필요하다.

## 구현 체크리스트

- [ ] YAML root/build 구조 검증을 `DataBuild.fromJson()` 전에 추가한다.
- [ ] command list의 id/type 기본 검증을 등록 catalog 기준으로 추가한다.
- [ ] workflow node shape guard를 `Pipeline.pipelineInitialize()`에 추가한다.
- [ ] 기존 테스트와 샘플 smoke 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] DataBuild.fromJson 전 구조 검증

#### 문제

`Application.build()`는 `lib/oto/application.dart:99`에서 YAML map을 바로 `DataBuild.fromJson()`에 넘긴다.
root가 Map이 아니거나 필수 섹션이 없으면 generated parser 오류가 그대로 노출되어 AI가 어느 섹션을 고쳐야 하는지 알기 어렵다.

Before (`lib/oto/application.dart:97`):

```dart
await composer.compose(yamlContent, printBuildStep);
commonData = composer.commonData;
build = DataBuild.fromJson(getMapFromYamlA(composer.buildYaml)!);
```

#### 해결 방법

`Application` 내부에 `validateBuildMap(Map<String, dynamic>? map)`를 추가하고, `property`, `commands`, `pipeline.workflow`의 타입과 존재를 먼저 검사한다.
검증 실패는 기존 `ExceptionData` 흐름으로 phase/message를 명시한다.

After:

```dart
final buildMap = getMapFromYamlA(composer.buildYaml);
final validateResult = validateBuildMap(buildMap);
if (!validateResult.enable) {
  final ex = ExceptionData()
    ..phase = 'Validate build yaml'
    ..message = validateResult.message;
  throw Exception(ex);
}
build = DataBuild.fromJson(buildMap!);
```

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

- [ ] `lib/oto/application.dart`: `validateBuildMap()` 추가.
- [ ] `lib/oto/application.dart`: `DataBuild.fromJson()` 호출 전 검증 적용.

#### 테스트 작성

- 신규 유닛 테스트는 작성하지 않는다. 사용자가 유닛 테스트를 별도 작업으로 분리했고, 기존 invalid YAML 테스트가 failure contract만 확인한다.

#### 중간 검증

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

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

### [REFACTOR-2] command list 기본 검증

#### 문제

중복 id 검증은 `lib/oto/application.dart:114`에 있지만, command id 누락이나 미등록 command type은 generated enum parsing 또는 나중 단계 오류에 의존한다.
`DataCommand.param`은 `lib/oto/data/command_data.dart:55`에서 dynamic이라 command-specific 검증으로 확장하기 전에 기본 shape 검증이 필요하다.

Before (`lib/oto/application.dart:114`):

```dart
// Register commands; filter out duplicate command IDs
for (var command in build.commands) {
  if (dataCommandMap.containsKey(command.id)) {
```

#### 해결 방법

`DataBuild.fromJson()` 전 raw `commands` 배열을 순회해 각 entry가 Map인지, `id`와 `command`가 비어 있지 않은지, `command`가 등록된 `CommandType`/catalog에 있는지 확인한다.
중복 id 검증은 기존 위치에 유지해 parsed `DataCommand` 기준 호환성을 지킨다.

After:

```dart
final commandValidateResult = validateCommandList(buildMap['commands']);
if (!commandValidateResult.enable) {
  final ex = ExceptionData()
    ..phase = 'Validate command list'
    ..message = commandValidateResult.message;
  throw Exception(ex);
}
```

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

- [ ] `lib/oto/application.dart`: raw command list 검증 helper 추가.
- [ ] `lib/oto/application.dart`: error message에 command index와 id/type 후보를 포함.
- [ ] `lib/oto/commands/command.dart`: 01 task에서 만든 catalog helper가 있다면 등록 타입 조회에 재사용.

#### 테스트 작성

- 신규 유닛 테스트는 작성하지 않는다. 테스트 공백은 review stub에 기록한다.

#### 중간 검증

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

기대 결과: 기존 invalid YAML failure와 정상 print-only build가 모두 통과한다.

### [REFACTOR-3] workflow node shape guard

#### 문제

`Pipeline.pipelineInitialize()`는 `lib/oto/pipeline/pipeline.dart:31`에서 workflow item을 `Map<String, dynamic>`으로 가정한다.
workflow node가 문자열, 빈 Map, 여러 key를 가진 Map이면 현재 오류가 Dart cast/first key 오류로 흐를 수 있다.

Before (`lib/oto/pipeline/pipeline.dart:29`):

```dart
static PipelineValidateResult pipelineInitialize(List<dynamic> list) {
  var result = PipelineValidateResult();
  for (Map<String, dynamic> item in list) {
    var key = item.keys.first;
```

#### 해결 방법

loop를 `for (var index = 0; index < list.length; index++)`로 바꾸고, item 타입/빈 Map/다중 key를 `PipelineValidateResult`로 반환한다.
메시지에는 `workflow[$index]`와 지원 task key 목록을 포함한다.

After:

```dart
for (var index = 0; index < list.length; index++) {
  final raw = list[index];
  if (raw is! Map || raw.length != 1) {
    result
      ..enable = false
      ..message = 'workflow[$index] must be a single-key task map.';
    return result;
  }
  final item = Map<String, dynamic>.from(raw);
  final key = item.keys.first;
```

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

- [ ] `lib/oto/pipeline/pipeline.dart`: workflow node guard 추가.
- [ ] `lib/oto/pipeline/pipeline.dart`: unsupported task 메시지를 지원 key 목록과 함께 유지.

#### 테스트 작성

- 신규 유닛 테스트는 작성하지 않는다. 기존 `pipeline validation rejects missing command id` 테스트로 기본 실패 contract를 확인한다.

#### 중간 검증

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

기대 결과: 기존 pipeline validation 테스트가 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/application.dart` | REFACTOR-1, REFACTOR-2 |
| `lib/oto/pipeline/pipeline.dart` | REFACTOR-3 |
| `test/oto_application_test.dart` | 검증 대상 |
| `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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
