<!-- task=execution_result_envelope/01_result_contract plan=0 tag=RESULT_CONTRACT -->

# Execution Result Contract Plan

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

구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제 실행하고, 실제 출력과 구현 메모를 남긴 뒤 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 아카이브와 `complete.log` 작성은 code-review 스킬 전용이다.

## 배경

마일스톤의 다음 남은 항목은 실행 후 성공/실패, exit code, 에러 정보를 외부 자동화가 파싱할 수 있는 envelope로 만드는 것이다. 현재 `BuildResult`는 성공 여부와 exit code를 들고 있지만 JSON 계약이 없다. `exe --json` CLI 작업 전에 core 결과 계약과 로그 억제 기반을 먼저 고정한다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `lib/oto/core/build_result.dart`
- `lib/oto/application.dart`
- `lib/cli/cli.dart`
- `lib/oto/commands/util/print.dart`
- `test/oto_application_test.dart`
- `pubspec.yaml`

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

- `BuildResult` JSON shape: 기존 테스트는 `success`, `exitCode`, `error`만 확인하고 `toJson()` 계약을 확인하지 않는다.
- `logEnable: false`의 stdout 억제: 기존 테스트가 없다. `exe --json`이 순수 JSON stdout을 보장하려면 실패 출력과 `Application.log()` 출력 억제를 검증해야 한다.

### 심볼 참조

- renamed/removed symbols: none.
- `BuildResult` 참조: `lib/oto/application.dart`, `lib/cli/commands/command_exe.dart`, `test/oto_application_test.dart`.

### 분할 판단

분할 정책을 먼저 평가했다. 공유 task group은 `execution_result_envelope`이다.

- `01_result_contract`: core 계약과 `logEnable` 기반을 소유한다.
- `02+01_exe_json_cli`: `01_result_contract` 완료 후 CLI JSON 모드와 bin 스모크를 소유한다.

core API/foundation 변경과 CLI rollout이 분리되고 검증 방식도 달라서 split gate가 적용된다.

### 범위 결정 근거

- 이 작업은 `BuildResult` JSON 계약과 `Application` 출력 억제 기반만 다룬다.
- `CommandExe` 인자 파싱, isolate 결과 전달, actual bin smoke는 `02+01_exe_json_cli`에서 다룬다.
- step event 계약과 pipeline event 발생 지점은 이번 task group 밖이다.
- 새 패키지는 추가하지 않는다. `dart:convert`, `dart:async`는 SDK 라이브러리다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`.
- stdout/exit-status 계약이 후속 CLI 자동화 검증의 기반이며, 실제 bin 출력 신뢰성에 영향을 주므로 terminal-agent 성격으로 본다.

## 구현 체크리스트

- [ ] `BuildResult`에 안정적인 execution result JSON envelope를 추가한다.
- [ ] `Application.build()`와 `Application.log()`가 `logEnable: false`에서 stdout을 오염시키지 않도록 조정한다.
- [ ] `test/oto_application_test.dart`에 BuildResult envelope와 log suppression 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 하위 작업은 선행 의존성이 없다. 완료 후 `02+01_exe_json_cli`가 이 작업의 `complete.log`를 전제로 시작한다.

### [RESULT_CONTRACT-1] BuildResult JSON Envelope

#### 문제

[build_result.dart](/config/workspace/oto/lib/oto/core/build_result.dart:1)는 `success`, `exitCode`, `error`, `stackTrace`만 들고 있고 외부 자동화용 JSON 계약이 없다.

Before:

```dart
// lib/oto/core/build_result.dart:1
class BuildResult {
  final bool success;
  final int exitCode;
  final Object? error;
  final StackTrace? stackTrace;

  const BuildResult.success()
      : success = true,
        exitCode = 0,
        error = null,
        stackTrace = null;

  const BuildResult.failure(this.error, this.stackTrace, {this.exitCode = 10})
      : success = false;
}
```

#### 해결 방법

`BuildResult`에 `message` getter와 `toJson()`을 추가한다. schema는 다음 필드를 항상 유지한다.

```dart
Map<String, dynamic> toJson() => {
      'schemaVersion': 1,
      'type': 'executionResult',
      'success': success,
      'exitCode': exitCode,
      'message': message,
      'error': success
          ? null
          : {
              'type': error.runtimeType.toString(),
              'message': message,
            },
    };
```

성공 message는 `Build completed successfully.`로 고정하고, 실패 message는 `error?.toString() ?? 'Build failed.'`를 사용한다. stack trace는 자동화 계약의 기본 JSON에 넣지 않는다.

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

- [ ] `lib/oto/core/build_result.dart`: `message` getter 추가.
- [ ] `lib/oto/core/build_result.dart`: `toJson()` 추가.
- [ ] `test/oto_application_test.dart`: `BuildResult` import 추가.
- [ ] `test/oto_application_test.dart`: 성공/failure JSON shape 테스트 추가.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- 테스트명:
  - `BuildResult.toJson exposes success execution envelope`
  - `BuildResult.toJson exposes failure execution envelope`
- 검증 목표: `schemaVersion`, `type`, `success`, `exitCode`, `message`, `error` shape가 안정적인지 확인한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart --plain-name "BuildResult.toJson"
```

기대 결과: 추가한 `BuildResult.toJson` 테스트가 통과한다.

### [RESULT_CONTRACT-2] logEnable False Stdout Suppression

#### 문제

[application.dart](/config/workspace/oto/lib/oto/application.dart:153)는 `logEnable`과 무관하게 실패 정보를 stdout에 출력한다. [application.dart](/config/workspace/oto/lib/oto/application.dart:180)의 `Application.log()`도 `_logFile == null`이면 `_logEnable`을 보지 않고 `print(message)`를 호출한다. 후속 `exe --json`은 `Application.build(..., logEnable: false)`를 사용해도 stdout이 JSON 외 텍스트로 오염될 수 있다.

Before:

```dart
// lib/oto/application.dart:153
} catch (e, stacktrace) {
  await CLI.printString(e.toString(), color: Color.redStrong);
  await CLI.printString(stacktrace.toString(), color: Color.yellowStrong);
  await printBuildStep('Build Failed', Color.redStrong);
  return BuildResult.failure(e, stacktrace);
}
```

```dart
// lib/oto/application.dart:180
static void log(String message, {LogType logType = LogType.verbose}) {
  if (_logFile == null) {
    print(message);
  } else {
    if (_logEnable || logType == LogType.error) {
      _logFile?.writeAsStringSync('[${getDates()}] $message\n',
          mode: FileMode.append, flush: true);
    }
  }
}
```

#### 해결 방법

`Application.build()`의 catch 출력은 `_logEnable`이 true일 때만 수행한다. `Application.log()`의 stdout branch도 `_logEnable`이 true일 때만 `print(message)`를 호출한다. 파일 로그 branch의 기존 error logging 동작은 유지한다.

```dart
} catch (e, stacktrace) {
  if (_logEnable) {
    await CLI.printString(e.toString(), color: Color.redStrong);
    await CLI.printString(stacktrace.toString(), color: Color.yellowStrong);
    await printBuildStep('Build Failed', Color.redStrong);
  }
  return BuildResult.failure(e, stacktrace);
}
```

```dart
if (_logFile == null) {
  if (_logEnable) {
    print(message);
  }
} else {
  ...
}
```

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

- [ ] `lib/oto/application.dart`: catch 출력에 `_logEnable` 조건 추가.
- [ ] `lib/oto/application.dart`: stdout branch의 `Application.log()`가 `_logEnable`을 존중하도록 수정.
- [ ] `test/oto_application_test.dart`: `dart:async` import 추가.
- [ ] `test/oto_application_test.dart`: `runZoned`로 `print`를 캡처해 `logEnable: false`에서 출력이 없는지 테스트.

#### 테스트 작성

- 작성: `test/oto_application_test.dart`
- 테스트명:
  - `build with logEnable false suppresses failure stdout`
  - `build with logEnable false suppresses command stdout logs`
- 검증 목표: 실패 출력과 `PrintCommand`의 `Application.log()` 출력이 `logEnable: false`에서 stdout으로 나오지 않는지 확인한다.

#### 중간 검증

```bash
dart test test/oto_application_test.dart --plain-name "logEnable false"
```

기대 결과: `logEnable false` 관련 테스트가 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/core/build_result.dart` | RESULT_CONTRACT-1 |
| `lib/oto/application.dart` | RESULT_CONTRACT-2 |
| `test/oto_application_test.dart` | RESULT_CONTRACT-1, RESULT_CONTRACT-2 |

## 최종 검증

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

기대 결과: analyze issue 없이 통과하고 `test/oto_application_test.dart` 전체가 통과한다.

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