<!-- task=yaml_validation_contract/02+01_validate_cli plan=0 tag=VALIDATE_CLI -->

# YAML Validate CLI Plan

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

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

## 배경

`01_core_contract`가 만든 YAML validation contract를 외부 자동화가 실제 CLI로 호출할 수 있어야 한다. 현재 `oto catalog --json`은 parseable JSON stdout 패턴을 제공하지만, YAML validation 전용 CLI는 없다. 이 하위 작업은 `oto validate -f <path>`와 `--json` 표면을 추가한다.

## 분석 결과

### 읽은 파일

- `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`

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

- Validate CLI command: 기존 테스트 없음.
- JSON stdout purity: `catalog --json` 테스트는 있으나 `validate --json`에는 없음.
- Missing file and invalid YAML file: `CommandExe -f` missing file smoke만 있고 validate command 계약은 없음.
- Actual bin registration: `catalog`는 bin smoke가 있으나 `validate`는 등록되어 있지 않다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 command class 후보: `CommandValidateCli`.

### 분할 판단

- split decision policy를 계획 파일 선택 전에 평가했다.
- shared task group: `yaml_validation_contract`
- 이 작업은 `02+01_validate_cli`이며 디렉터리명 기준으로 `01_core_contract` 완료에 의존한다.
- core result schema를 먼저 고정해야 CLI JSON shape와 exit code 테스트가 안정된다.

### 범위 결정 근거

- 이 작업은 CLI wrapper와 CLI 테스트만 다룬다.
- core validation 실패 기준 자체는 `01_core_contract`에서 완성되어야 한다.
- `exe --json`, 실행 결과 envelope, step event는 범위 밖이다.
- scheduler 명령과 install/start 명령은 건드리지 않는다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`. Terminal-agent work에 해당하는 CLI workflow, stdout JSON purity, exit-status contract가 포함된다.

## 구현 체크리스트

- [ ] [VALIDATE_CLI-1] `oto validate` CLI command를 추가하고 등록한다.
- [ ] [VALIDATE_CLI-2] validate CLI direct/unit 테스트와 bin smoke 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 subtask는 디렉터리명 `02+01_validate_cli` 기준으로 같은 task group의 `01_core_contract`가 `complete.log`를 만든 뒤 시작한다.

### [VALIDATE_CLI-1] Add Validate CLI Command

#### 문제

`bin/main.dart`는 `catalog`까지 등록하지만 validation command가 없다. 외부 자동화가 build를 실행하지 않고 YAML을 검증할 CLI 표면이 없다.

Before, `bin/main.dart:9`:

```dart
import 'package:oto/cli/commands/command_catalog.dart';
```

Before, `bin/main.dart:13`:

```dart
CLI.initialize(appName, arguments,
    [CommandTemplate(), CommandExe(), CommandScheduler(), CommandCatalogCli()]);
```

`catalog --json`은 stdout purity를 위해 실행 로그를 억제한다.

Before, `lib/cli/commands/command_catalog.dart:31`:

```dart
@override
bool shouldPrintExecuteLog(List<String> parameters) {
  return !parameters.contains('--json');
}
```

Before, `lib/cli/commands/command_manager.dart:41`:

```dart
if (commandItem.shouldPrintExecuteLog(parameters)) {
  await CLI.println('Execute command: $command$argStr',
      color: Color.green);
}
```

#### 해결 방법

새 파일 `lib/cli/commands/command_validate.dart`를 추가한다. CLI 레이어는 파일 읽기와 인자 파싱만 담당하고, 검증은 `Application.validateYamlContent`에 위임한다. `--json`이면 `JsonEncoder.withIndent('  ')`로 `YamlValidationResult.toJson()`을 stdout에 단독 출력하고, `exitCode`를 result exit code로 설정한다.

After:

```dart
class CommandValidateCli extends CommandBase {
  final Future<void> Function(String) _printString;

  CommandValidateCli({Future<void> Function(String)? printString})
      : _printString = printString ??
            ((value) async {
              stdout.writeln(value);
            });

  @override
  String get name => 'validate';

  @override
  Map<String, List<String>> get arguments => {
        '-f {file path}': ['Validate the yaml file without executing it.'],
        '--json': ['Output validation result as structured JSON format.'],
      };

  @override
  bool shouldPrintExecuteLog(List<String> parameters) {
    return !parameters.contains('--json');
  }
}
```

인자 파싱 기준:

- 지원: `-f path`, `-fpath`, `--file path`, `--file=path`, `--json`
- 파일 없음: `valid=false`, `phase='Read YAML file'`, `exitCode=10`
- 알 수 없는 option 또는 빠진 `-f` 값: 기존 CLI 패턴처럼 `Exception`을 던진다.
- 사람이 읽는 출력은 `Valid YAML.` 또는 `Invalid YAML: [phase] message` 정도로 제한한다.

`bin/main.dart`에는 import와 command 등록만 추가한다.

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

- [ ] `lib/cli/commands/command_validate.dart`: 새 command 추가.
- [ ] `bin/main.dart`: import 추가.
- [ ] `bin/main.dart`: `CommandValidateCli()` 등록.
- [ ] `lib/cli/commands/command_manager.dart`: 변경하지 않는다. 기존 `shouldPrintExecuteLog` hook을 사용한다.

#### 테스트 작성

- 작성: `test/oto_validate_cli_test.dart`
- 테스트명:
  - `CommandValidateCli shouldPrintExecuteLog behavior`
  - `CommandValidateCli json output for valid yaml`
  - `CommandValidateCli json output for invalid yaml`
  - `CommandValidateCli missing file returns validation json and exit code`
  - `actual bin validate --json output has parseable json and no execute logs`
- assertion 목표: `schemaVersion`, `type`, `valid`, `phase`, `exitCode`, stdout에 `Execute command:` 없음.

#### 중간 검증

```bash
dart test test/oto_validate_cli_test.dart
```

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

### [VALIDATE_CLI-2] CLI Integration Tests And Smoke

#### 문제

top-level CLI 등록은 `bin/main.dart`의 command list가 진실 소스다. direct command 테스트만으로는 실제 `dart run bin/main.dart validate ...`가 등록되어 있고 JSON stdout을 보장하는지 확인할 수 없다.

Before, `test/oto_catalog_cli_test.dart:97`:

```dart
test('actual bin execution with --json output has parseable json and no execute logs', () async {
  final result = await Process.run('dart', ['run', 'bin/main.dart', 'catalog', '--json']);
```

#### 해결 방법

`test/oto_validate_cli_test.dart`에 actual bin smoke를 포함한다. 임시 YAML 파일은 테스트 내부에서 `Directory.systemTemp`에 만들고 삭제한다. valid YAML은 exit code 0, invalid YAML은 exit code 10과 parseable JSON을 검증한다.

After:

```dart
test('actual bin validate --json output has parseable json and no execute logs', () async {
  final file = File('${Directory.systemTemp.path}/oto_validate_test.yaml');
  file.writeAsStringSync(validYaml);
  final result = await Process.run(
    'dart',
    ['run', 'bin/main.dart', 'validate', '-f', file.path, '--json'],
  );

  expect(result.exitCode, 0, reason: result.stderr.toString());
  final stdoutStr = result.stdout.toString().trim();
  final decoded = jsonDecode(stdoutStr) as Map<String, dynamic>;
  expect(decoded['type'], 'yamlValidation');
  expect(stdoutStr, isNot(contains('Execute command:')));
});
```

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

- [ ] `test/oto_validate_cli_test.dart`: direct command tests 추가.
- [ ] `test/oto_validate_cli_test.dart`: actual bin smoke tests 추가.
- [ ] `test/oto_validate_cli_test.dart`: temp file cleanup 보장.

#### 테스트 작성

- 작성: `test/oto_validate_cli_test.dart`
- skip 없음. 새 CLI public surface라 direct/unit과 bin smoke가 모두 필요하다.

#### 중간 검증

```bash
dart test test/oto_validate_cli_test.dart test/oto_catalog_cli_test.dart
```

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/cli/commands/command_validate.dart` | VALIDATE_CLI-1 |
| `bin/main.dart` | VALIDATE_CLI-1 |
| `test/oto_validate_cli_test.dart` | VALIDATE_CLI-2 |

## 최종 검증

```bash
dart analyze
```

예상 결과: `No issues found!`

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

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

```bash
tmp=$(mktemp /tmp/oto_validate_valid.XXXXXX.yaml); printf 'commands:\n  - command: Print\n    id: hello\n    param:\n      message: hi\npipeline:\n  id: main\n  workflow:\n    - exe: hello\n' > "$tmp"; dart run bin/main.dart validate -f "$tmp" --json; code=$?; rm -f "$tmp"; exit $code
```

예상 결과: exit code 0, stdout이 parseable JSON이며 `type`은 `yamlValidation`, `valid`는 `true`.

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