<!-- task=yaml_validation_contract/01_core_contract plan=0 tag=VALIDATE_CORE -->

# YAML Validation Core Contract Plan

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증 명령을 실행하고 실제 출력과 구현 메모를 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, 로그 변환, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

마일스톤의 다음 필수 기능은 YAML validation의 입력, 출력, 실패 기준을 외부 자동화용 계약으로 정의하는 것이다. 현재 validation은 `Application.build()` 내부에서 실행되며 실패는 예외와 사람용 로그로만 드러난다. 먼저 core에 빌드 실행 없이 호출 가능한 validation result 계약을 만들고, CLI 작업은 그 계약 위에 얹는다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/pipeline/rules.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `bin/main.dart`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_exe.dart`
- `lib/cli/commands/command_catalog.dart`
- `lib/oto/application.dart`
- `lib/oto/pipeline/pipeline.dart`
- `lib/oto/core/build_result.dart`
- `lib/oto/data/command_data.dart`
- `test/oto_application_test.dart`
- `test/oto_core_test.dart`
- `test/oto_catalog_cli_test.dart`
- `pubspec.yaml`
- `analysis_options.yaml`

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

- Public core validation contract: 기존 테스트 없음. `Application.build()` 실패 테스트는 있으나 build 실행 경로와 로그 출력까지 포함한다.
- JSON shape: `catalog --json` 테스트는 있으나 YAML validation result shape는 없음.
- Duplicate command id: `Application.build()` 구현에는 검사가 있으나 독립 validation 계약 테스트는 없음.
- YAML parse/root/build-map/pipeline 실패 기준: 기존 테스트는 내부 실패 메시지만 확인한다. 새 public result의 `valid`, `phase`, `message`, `exitCode`, `toJson()`을 추가로 검증해야 한다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 public API 후보: `Application.validateYamlContent(String yamlContent)`와 `YamlValidationResult`.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- shared task group: `yaml_validation_contract`
- `01_core_contract`: core validation result와 실패 기준을 만든다. 선행 의존성 없음.
- `02+01_validate_cli`: CLI 노출과 bin smoke 검증을 만든다. `01_core_contract`의 public API 완료가 필요하다.
- API/foundation 변경과 CLI rollout이 분리되며, 실패 원인도 core 계약과 CLI 인자/출력 처리로 나뉘므로 split이 필요하다.

### 범위 결정 근거

- 이 작업은 `Application`의 YAML validation 계약과 관련 테스트만 다룬다.
- `bin/main.dart`, 새 CLI 커맨드, CLI 통합 테스트는 `02+01_validate_cli`에서 처리한다.
- step event, 실행 결과 envelope, scheduler, command catalog 확장은 이번 하위 작업에서 제외한다.
- 새 패키지는 필요 없다. `yaml`, `test`는 이미 `pubspec.yaml`에 있다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`. Public validation schema와 실패 기준 계약을 만들고 이후 CLI가 의존하므로 schema/API 판단과 테스트 의미 검토가 필요하다.

## 구현 체크리스트

- [ ] [VALIDATE_CORE-1] `Application`에 실행 없는 YAML validation result 계약을 추가한다.
- [ ] [VALIDATE_CORE-2] core validation 계약 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. `VALIDATE_CORE-1`을 먼저 구현한다.
2. `VALIDATE_CORE-2`로 public contract를 고정한다.

### [VALIDATE_CORE-1] Core Validation Result Contract

#### 문제

`Application.build()` 내부 validation은 build 실행 중 예외로 변환된다. 외부 자동화는 build 실행 없이 동일 실패 기준을 받을 수 없다.

Before, `lib/oto/application.dart:100`:

```dart
final buildMap = getMapFromYamlA(composer.buildYaml);
final mapValidate = _validateBuildMap(buildMap);
if (!mapValidate.enable) {
  final ex = ExceptionData()
    ..phase = 'Validate build yaml'
    ..message = mapValidate.message;
  throw Exception(ex);
}
final commandValidate =
    _validateCommandList(buildMap!['commands'] as List);
```

Before, `lib/oto/application.dart:131`:

```dart
// Register commands; filter out duplicate command IDs
for (var command in build.commands) {
  if (dataCommandMap.containsKey(command.id)) {
    var ex = ExceptionData();
    ex.phase = 'Validate command list';
    ex.message = 'Duplicate command id exists: "${command.id}"';
    throw Exception(ex);
  } else {
    dataCommandMap[command.id] = command;
  }
}
```

Before, `lib/oto/pipeline/pipeline.dart:31`:

```dart
static PipelineValidateResult pipelineInitialize(List<dynamic> list,
    {ExecutionContext? context}) {
```

#### 해결 방법

`lib/oto/application.dart` 안에 public `YamlValidationResult`를 추가하고, `Application.validateYamlContent(String yamlContent)`를 추가한다. 기존 private `_validateBuildMap`, `_validateCommandList`, `Pipeline.pipelineInitialize()`를 그대로 재사용한다. duplicate command id 검사는 private helper로 추출해 build 경로와 validation 경로가 같은 기준을 쓰게 한다.

After:

```dart
class YamlValidationResult {
  final bool valid;
  final String phase;
  final String message;
  final int exitCode;

  const YamlValidationResult.success()
      : valid = true,
        phase = 'Validate YAML',
        message = 'YAML is valid.',
        exitCode = 0;

  const YamlValidationResult.failure({
    required this.phase,
    required this.message,
    this.exitCode = 10,
  }) : valid = false;

  Map<String, dynamic> toJson() => {
        'schemaVersion': 1,
        'type': 'yamlValidation',
        'valid': valid,
        'phase': phase,
        'message': message,
        'exitCode': exitCode,
      };
}
```

After:

```dart
static YamlValidationResult validateYamlContent(String yamlContent) {
  registerAllCommands();
  Map<String, dynamic>? buildMap;
  try {
    buildMap = getMapFromYamlA(yamlContent);
  } catch (e) {
    return YamlValidationResult.failure(
      phase: 'Parse YAML',
      message: e.toString(),
    );
  }

  final mapValidate = _validateBuildMap(buildMap);
  if (!mapValidate.enable) {
    return YamlValidationResult.failure(
      phase: 'Validate build yaml',
      message: mapValidate.message,
    );
  }

  final commandValidate = _validateCommandList(buildMap!['commands'] as List);
  if (!commandValidate.enable) {
    return YamlValidationResult.failure(
      phase: 'Validate command list',
      message: commandValidate.message,
    );
  }

  final build = DataBuild.fromJson(buildMap);
  final validationContext = ExecutionContext();
  final commandMapValidate = _populateCommandMap(build, validationContext);
  if (!commandMapValidate.enable) {
    return YamlValidationResult.failure(
      phase: 'Validate command list',
      message: commandMapValidate.message,
    );
  }

  final pipelineValidate = Pipeline.pipelineInitialize(
    build.pipeline!.workflow,
    context: validationContext,
  );
  if (!pipelineValidate.enable) {
    return YamlValidationResult.failure(
      phase: 'Validate Pipeline',
      message: pipelineValidate.message ?? 'Pipeline validation failed.',
    );
  }
  return const YamlValidationResult.success();
}
```

`Application.build()`는 duplicate-id loop를 `_populateCommandMap(build, context)` 호출로 바꾸고, 실패 시 기존과 같은 `ExceptionData.phase/message`를 던진다.

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

- [ ] `lib/oto/application.dart`: `YamlValidationResult` 추가.
- [ ] `lib/oto/application.dart`: `Application.validateYamlContent` 추가.
- [ ] `lib/oto/application.dart`: duplicate command id 검사를 private helper로 추출하고 build 경로에서 재사용.
- [ ] `lib/oto/application.dart`: 기존 build 실패 메시지와 exit code를 유지.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- 테스트명:
  - `validateYamlContent returns success contract for valid yaml`
  - `validateYamlContent returns build yaml failure contract`
  - `validateYamlContent returns command list failure contract for duplicate id`
  - `validateYamlContent returns pipeline failure contract`
  - `validateYamlContent exposes stable json shape`
- assertion 목표: `valid`, `phase`, `message`, `exitCode`, `toJson()['schemaVersion']`, `toJson()['type']`를 검증한다.

#### 중간 검증

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

예상 결과: 모든 테스트 통과.

### [VALIDATE_CORE-2] Core Contract Regression Tests

#### 문제

기존 테스트는 build 실행 실패와 pipeline 내부 실패만 검증한다. 새 public validation API가 build를 실행하지 않고 같은 실패 기준을 유지하는지는 보장하지 않는다.

Before, `test/oto_application_test.dart:83`:

```dart
test('build fails with message when YAML root is not a map', () async {
  const yaml = '''
- command: Print
  id: hello
''';
  final result =
      await Application.instance.build(BuildType.file, yamlContent: yaml);
```

Before, `test/oto_application_test.dart:150`:

```dart
test('build fails with validation message when workflow key is not a string',
    () async {
```

#### 해결 방법

기존 build 테스트는 유지하고, 같은 파일에 `Application.validateYamlContent` 전용 테스트를 추가한다. 테스트 YAML은 파일 I/O 없이 문자열 fixture를 사용한다. 실패 케이스는 build yaml, command list, pipeline 세 phase를 모두 포함한다.

After:

```dart
test('validateYamlContent exposes stable json shape', () {
  final result = Application.validateYamlContent(validYaml);
  final json = result.toJson();

  expect(json['schemaVersion'], 1);
  expect(json['type'], 'yamlValidation');
  expect(json['valid'], isTrue);
  expect(json['exitCode'], 0);
});
```

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

- [ ] `test/oto_application_test.dart`: success contract 테스트 추가.
- [ ] `test/oto_application_test.dart`: build yaml failure contract 테스트 추가.
- [ ] `test/oto_application_test.dart`: duplicate id failure contract 테스트 추가.
- [ ] `test/oto_application_test.dart`: pipeline failure contract 테스트 추가.
- [ ] `test/oto_application_test.dart`: stable JSON shape 테스트 추가.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- fixture: 기존 print-only YAML 문자열을 재사용하거나 local const로 둔다.
- skip 없음. 새 public API이므로 normal + boundary 테스트가 필요하다.

#### 중간 검증

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

예상 결과: 모든 테스트 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/application.dart` | VALIDATE_CORE-1 |
| `test/oto_application_test.dart` | VALIDATE_CORE-2 |

## 최종 검증

```bash
dart analyze
```

예상 결과: `No issues found!`

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

예상 결과: 모든 테스트 통과. Dart test cache 개념은 적용하지 않는다.

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