<!-- task=01_command_catalog_contract plan=0 tag=CATALOG_CONTRACT -->

# Command Catalog Contract Plan

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

**구현의 마지막 단계로 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채워야 한다. 이 파일을 채우기 전에는 작업이 완료된 것이 아니다.** `구현 체크리스트`를 순서대로 수행하고, 계획과 리뷰 스텁의 체크리스트를 모두 완료한 뒤, 중간/최종 검증을 실행하고 실제 출력으로 `CODE_REVIEW-*-G??.md`를 채운다. 리뷰 파일의 `이 파일을 읽는 리뷰 에이전트에게` 아카이브 지시는 실행하지 말고, `코드리뷰 전용 체크리스트`도 수정하거나 체크하지 않는다.

## 배경

`구조화된 자동화 표면` Milestone의 첫 항목은 command catalog를 안정된 조회 경로로 노출하는 것이다. 현재 내부 진실 소스는 `Command.specs`와 `Command.catalogRows`뿐이라 CLI 출력 계약이 바로 의존하기에는 sample 존재 여부와 JSON 필드 타입이 약하다. 먼저 command 도메인에 작은 catalog contract를 두고 CLI는 다음 작업에서 이 계약만 소비하게 한다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/sample/rules.md`
- `agent-ops/rules/project/domain/framework/rules.md`
- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `pubspec.yaml`
- `analysis_options.yaml`
- `lib/oto/commands/command.dart`
- `lib/oto/commands/command_registry.dart`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_template.dart`
- `lib/cli/cli.dart`
- `bin/main.dart`
- `test/oto_command_catalog_test.dart`
- `test/oto_application_test.dart`
- `test/oto_cli_runtime_test.dart`
- `test/oto_scheduler_runtime_test.dart`

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

- `Command.specs` 등록 누락, 빈 category/dataModel, samplePath 존재 여부는 `test/oto_command_catalog_test.dart`가 이미 커버한다.
- 새 catalog contract의 public JSON 필드(`command`, `category`, `dataModel`, `samplePath`, `hasSample`, `sampleExists`)와 필터링은 기존 테스트가 직접 커버하지 않는다. 이 작업에서 같은 테스트 파일에 contract 테스트를 추가한다.
- CLI 출력과 JSON 순수 stdout은 이 작업 범위가 아니다. `02+01_catalog_cli`에서 커버한다.

### 심볼 참조

- renamed/removed symbol: none.
- 새 public symbol 후보: `CommandCatalogEntry`, `CommandCatalog`. 기존 call site 없음.

### 분할 판단

- 분할 정책을 계획 파일 선택 전에 평가했다.
- 이 작업은 command catalog 내부 contract만 소유한다. `02+01_catalog_cli`가 CLI 노출을 맡고 이 작업의 `complete.log`에 의존한다.
- 분할 이유: shared command-domain contract와 terminal CLI workflow는 검증 방식과 위험이 다르다. contract가 먼저 통과하면 CLI 작업은 JSON stdout과 command routing에 집중할 수 있다.
- sibling task: `01_command_catalog_contract` has no dependency; `02+01_catalog_cli` depends on `01_command_catalog_contract`.

### 범위 결정 근거

- `lib/cli/**`, `bin/main.dart`, README, roadmap checkbox는 이 작업에서 수정하지 않는다. CLI 노출과 문서 완료 표시는 후속 task의 범위다.
- `CommandSpec` 필드를 늘리지 않는다. 현재 확인 가능한 필드와 sample 파일 상태만 contract로 만든다.
- `assets/yaml/sample/**`는 변경하지 않는다. sample 내용은 기존 테스트가 이미 검증한다.
- 현재 워킹 트리의 README/로드맵 변경은 이전 기준선 작업 산출물이므로 되돌리지 않는다.

### 빌드 등급

- build lane: `cloud-G06`, review lane: `cloud-G06`. 새 public contract와 JSON field schema가 생기지만 범위는 command 도메인과 focused tests로 제한된다.

## 구현 체크리스트

- [ ] [CATALOG_CONTRACT-1] command catalog contract를 추가한다.
- [ ] [CATALOG_CONTRACT-2] contract 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증을 모두 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [CATALOG_CONTRACT-1] Command Catalog Contract 추가

### 문제

`CommandSpec.toCatalogRow()`는 문자열 맵만 제공한다. `samplePath`는 빈 문자열로 변환되고 sample 존재 여부가 없어 외부 자동화용 JSON contract의 타입과 의미가 불명확하다.

Before: [command.dart](/config/workspace/oto/lib/oto/commands/command.dart:106)

```dart
  Map<String, String> toCatalogRow() => {
        'type': type.name,
        'category': category,
        'dataModel': dataModel,
        'samplePath': samplePath ?? '',
      };
```

### 해결 방법

새 파일 `lib/oto/commands/command_catalog.dart`를 추가한다. 기존 `CommandSpec`은 유지하고, 새 contract가 `Command.specs`를 읽어 typed entry를 만든다. `registerAllCommands()`를 호출해 빈 catalog가 나오지 않게 하되, 기존 register map은 같은 key를 덮어쓰므로 idempotent하게 동작한다.

After:

```dart
import 'dart:io';

import 'package:oto/oto/commands/command.dart';
import 'package:oto/oto/commands/command_registry.dart';

class CommandCatalogEntry {
  final String command;
  final String category;
  final String dataModel;
  final String? samplePath;
  final bool hasSample;
  final bool sampleExists;

  const CommandCatalogEntry({
    required this.command,
    required this.category,
    required this.dataModel,
    required this.samplePath,
    required this.hasSample,
    required this.sampleExists,
  });

  Map<String, dynamic> toJson() => {
        'command': command,
        'category': category,
        'dataModel': dataModel,
        'samplePath': samplePath,
        'hasSample': hasSample,
        'sampleExists': sampleExists,
      };
}

class CommandCatalog {
  const CommandCatalog();

  List<CommandCatalogEntry> entries({String? category, String? command}) {
    registerAllCommands();
    final normalizedCategory = category?.toLowerCase();
    final normalizedCommand = command?.toLowerCase();
    final list = Command.specs.values.map(_entryFromSpec).where((entry) {
      final categoryMatches = normalizedCategory == null ||
          entry.category.toLowerCase() == normalizedCategory;
      final commandMatches = normalizedCommand == null ||
          entry.command.toLowerCase() == normalizedCommand;
      return categoryMatches && commandMatches;
    }).toList();
    list.sort((a, b) {
      final byCategory = a.category.compareTo(b.category);
      return byCategory == 0
          ? a.command.compareTo(b.command)
          : byCategory;
    });
    return List.unmodifiable(list);
  }

  CommandCatalogEntry _entryFromSpec(CommandSpec spec) {
    final sample = spec.samplePath;
    return CommandCatalogEntry(
      command: spec.type.name,
      category: spec.category,
      dataModel: spec.dataModel,
      samplePath: sample,
      hasSample: sample != null,
      sampleExists: sample != null && File(sample).existsSync(),
    );
  }
}
```

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

- [ ] `lib/oto/commands/command_catalog.dart` 새 파일 추가
- [ ] `CommandSpec` 기존 API와 `Command.catalogRows`는 변경하지 않음
- [ ] category/command 필터는 대소문자 무시 exact match로 구현
- [ ] samplePath가 없으면 `samplePath: null`, `hasSample: false`, `sampleExists: false`

### 테스트 작성

작성한다. `test/oto_command_catalog_test.dart`에 contract 테스트를 추가한다.

### 중간 검증

```bash
dart test test/oto_command_catalog_test.dart
```

예상 결과: all tests passed.

## [CATALOG_CONTRACT-2] Contract 테스트 추가

### 문제

기존 catalog 테스트는 registration과 sample consistency만 확인한다. 새 contract가 JSON에 노출할 필드와 필터 동작은 테스트가 없다.

Before: [oto_command_catalog_test.dart](/config/workspace/oto/test/oto_command_catalog_test.dart:82)

```dart
  test('spec category and dataModel are non-empty', () {
    final bad = <String>[];
    for (final entry in Command.specs.entries) {
      final spec = entry.value;
      if (spec.category.isEmpty || spec.dataModel.isEmpty) {
        bad.add('${entry.key}');
      }
    }
```

### 해결 방법

`package:oto/oto/commands/command_catalog.dart`를 import하고 아래 테스트를 추가한다.

After:

```dart
  test('command catalog contract exposes stable fields', () {
    final entries = const CommandCatalog().entries();
    expect(entries, isNotEmpty);

    final firstJson = entries.first.toJson();
    expect(firstJson.keys, containsAll([
      'command',
      'category',
      'dataModel',
      'samplePath',
      'hasSample',
      'sampleExists',
    ]));
    expect(firstJson['command'], isA<String>());
    expect(firstJson['category'], isA<String>());
    expect(firstJson['dataModel'], isA<String>());
    expect(firstJson['hasSample'], isA<bool>());
    expect(firstJson['sampleExists'], isA<bool>());
  });

  test('command catalog filters category and command case-insensitively', () {
    final catalog = const CommandCatalog();
    final buildEntries = catalog.entries(category: 'BUILD');
    expect(buildEntries, isNotEmpty);
    expect(buildEntries.every((entry) => entry.category == 'build'), isTrue);

    final printEntries = catalog.entries(command: 'print');
    expect(printEntries.map((entry) => entry.command), ['Print']);
  });
```

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

- [ ] `test/oto_command_catalog_test.dart` import 추가
- [ ] stable field 테스트 추가
- [ ] category/command 필터 테스트 추가
- [ ] samplePath null 가능성을 테스트가 허용하도록 작성

### 테스트 작성

작성한다. 새 public contract이므로 normal + boundary 필터 테스트가 필요하다.

### 중간 검증

```bash
dart test test/oto_command_catalog_test.dart
```

예상 결과: all tests passed.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/commands/command_catalog.dart` | CATALOG_CONTRACT-1 |
| `test/oto_command_catalog_test.dart` | CATALOG_CONTRACT-2 |

## 최종 검증

```bash
dart test test/oto_command_catalog_test.dart
dart analyze
```

예상 결과: 두 명령 모두 성공한다.

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