<!-- task=step_events/01_core_event_contract plan=0 tag=STEP_EVENT_CORE -->

# STEP_EVENT_CORE - 파이프라인 step event 수집 계약

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

구현 완료 전 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제로 실행하고 출력 원문을 붙이며, active 파일은 제자리에 둔 채 리뷰 준비 상태를 보고한다. 판정, 로그화, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

현재 로드맵의 남은 핵심은 step 단위 진행 상황을 구조화하는 것이다. `Pipeline.execute()`는 순차 실행만 하고, 실행 컨텍스트에는 command state 외에 외부 자동화가 소비할 step event 기록이 없다. 이 작업은 출력 연결 전 단계로, event 모델과 발생 지점만 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/pipeline/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/command/rules.md`
- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/structured-automation-surface.md`
- `pubspec.yaml`
- `lib/oto/core/execution_context.dart`
- `lib/oto/core/build_result.dart`
- `lib/oto/application.dart`
- `lib/cli/commands/command_exe.dart`
- `lib/oto/pipeline/pipeline.dart`
- `lib/oto/pipeline/pipeline_exe.dart`
- `lib/oto/pipeline/pipeline_exe_handle.dart`
- `lib/oto/pipeline/pipeline_condition.dart`
- `lib/oto/pipeline/pipeline_if.dart`
- `lib/oto/pipeline/pipeline_contain.dart`
- `lib/oto/pipeline/pipeline_foreach.dart`
- `lib/oto/pipeline/pipeline_while.dart`
- `lib/oto/pipeline/pipeline_switch.dart`
- `lib/oto/pipeline/pipeline_wait_until.dart`
- `test/oto_core_test.dart`
- `test/oto_application_test.dart`
- `test/oto_context_test.dart`

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

- 새 `StepEvent` 모델과 `ExecutionContext.stepEvents`: 기존 테스트 없음. `test/oto_context_test.dart` 또는 `test/oto_core_test.dart`에 직접 검증을 추가한다.
- `Pipeline.execute()`의 started/completed/failed event 기록: 기존 테스트 없음. `test/oto_core_test.dart`에 성공 1-step, 실패 custom executor 회귀 테스트를 추가한다.
- async step의 의미: 기존 테스트 없음. 이번 작업에서는 `PipelineAsync.execute()`가 반환되는 시점을 completed로 기록한다는 계약만 문서화하고 별도 async 완료 추적은 제외한다.

### 심볼 참조

- renamed/removed symbol 없음. `rg --sort path -n "BuildResult|PipelineExecutor|Pipeline\\(|pipelineInitialize|execute\\(\\)|CommandExe|executionResult" lib test bin`로 호출 지점을 확인했다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 작업 그룹: `step_events`
- `01_core_event_contract`: event 모델, context 저장소, pipeline 발생 지점. 선행 작업 없음.
- `02+01_execution_result_output`: `01_core_event_contract`의 `StepEvent`/context 결과를 `BuildResult`와 CLI JSON 계약에 노출한다. `01_core_event_contract` 완료 후 진행한다.
- 분할 이유: core/pipeline API 기반 작업과 실행 결과 출력 계약은 의존 관계와 테스트 전략이 다르다. 첫 작업은 `test/oto_core_test.dart` 중심, 둘째 작업은 `test/oto_application_test.dart`와 bin-level JSON 출력 중심으로 독립 리뷰가 가능하다.

### 범위 결정 근거

- CLI `--json` 출력 변경, `BuildResult.toJson()` 변경, 로드맵 체크 갱신은 후속 `02+01_execution_result_output`에서 처리한다.
- live streaming JSONL, Edge agent 네트워크 프로토콜, scheduler 로그 파일 포맷은 이번 작업에서 제외한다.
- 커맨드 구현체 내부는 수정하지 않는다. pipeline 도메인 규칙상 pipeline은 command dispatch와 흐름 제어만 담당한다.

### 빌드 등급

- build lane/grade: `cloud-G07`
- review lane/grade: `cloud-G07`
- 근거: 구조화 event schema와 pipeline/core 교차 변경이며, 후속 CLI 출력 계약의 기반이 된다.

## 구현 체크리스트

- [ ] `ExecutionContext`에 `StepEvent` 모델, 저장소, sequence 할당/reset API를 추가한다.
- [ ] `PipelineExecutor`와 command 실행 executor에 event metadata getter를 추가한다.
- [ ] `Pipeline.execute()`에서 started/completed/failed event를 기록하고 실패는 기존처럼 rethrow한다.
- [ ] `Application.build()` 시작 시 step event 상태를 reset한다.
- [ ] `test/oto_core_test.dart`와 필요 시 `test/oto_context_test.dart`에 event 수집 회귀 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [STEP_EVENT_CORE-1] ExecutionContext event 계약 추가

### 문제

[execution_context.dart](/config/workspace/oto/lib/oto/core/execution_context.dart:4)는 런타임 property, command state, command map만 보관한다. step event를 누적할 구조와 실행별 sequence reset 지점이 없다.

```dart
// lib/oto/core/execution_context.dart:4
class ExecutionContext {
  DataCommon? commonData;
  Map<String, dynamic> property = {};
  Map<String, CommandState> commandStates = {};
  Map<String, DataCommand> dataCommandMap = {};
}
```

### 해결 방법

`execution_context.dart` 안에 새 파일 없이 `StepEvent` 클래스를 추가하고, `ExecutionContext`가 event 목록과 sequence를 관리하게 한다. event 최소 필드는 `schemaVersion`, `type`, `event`, `stepId`, `workflowIndex`, `stepType`, `commandId`, `commandType`, `timestamp`, `error`다.

```dart
class StepEvent {
  final int stepId;
  final int workflowIndex;
  final String stepType;
  final String event;
  final String timestamp;
  final String? commandId;
  final String? commandType;
  final Map<String, String>? error;

  StepEvent({
    required this.stepId,
    required this.workflowIndex,
    required this.stepType,
    required this.event,
    required this.timestamp,
    this.commandId,
    this.commandType,
    this.error,
  });

  Map<String, dynamic> toJson() => {
        'schemaVersion': 1,
        'type': 'stepEvent',
        'event': event,
        'stepId': stepId,
        'workflowIndex': workflowIndex,
        'stepType': stepType,
        'commandId': commandId,
        'commandType': commandType,
        'timestamp': timestamp,
        'error': error,
      };
}
```

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

- [ ] `lib/oto/core/execution_context.dart`: `StepEvent` 클래스와 `stepEvents`, `allocateStepEventId()`, `addStepEvent()`, `resetStepEvents()` 추가.
- [ ] `lib/oto/application.dart`: build 시작 시 `context.resetStepEvents()` 호출.

### 테스트 작성

- 작성: `test/oto_context_test.dart`에 `execution context records and resets step events` 추가. `StepEvent.toJson()` shape와 reset 후 sequence 0 재시작을 검증한다.

### 중간 검증

```bash
dart test test/oto_context_test.dart
```

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

## [STEP_EVENT_CORE-2] PipelineExecutor event metadata 추가

### 문제

[pipeline_exe.dart](/config/workspace/oto/lib/oto/pipeline/pipeline_exe.dart:18)의 `PipelineExecutor`는 `initialize`와 `execute`만 노출한다. [pipeline.dart](/config/workspace/oto/lib/oto/pipeline/pipeline.dart:45)는 workflow key를 알고 있지만 executor에 저장하지 않아 event에서 `exe`, `if`, `foreach` 같은 step type을 안정적으로 기록할 수 없다.

```dart
// lib/oto/pipeline/pipeline_exe.dart:18
abstract class PipelineExecutor {
  ExecutionContext? context;

  ExecutionContext get runtimeContext => context ?? Application.instance.context;

  //set & validate data
  PipelineValidateResult initialize(Map<String, dynamic> set);
  Future execute();
```

### 해결 방법

`PipelineExecutor`에 `workflowKey`, `stepType`, `stepCommandId`, `stepCommandType` getter를 추가한다. `Pipeline.pipelineInitialize()`에서 key를 설정하고, `PipelineExe`와 `PipelineExeHandle`만 command metadata를 override한다.

```dart
abstract class PipelineExecutor {
  ExecutionContext? context;
  String workflowKey = '';

  ExecutionContext get runtimeContext => context ?? Application.instance.context;
  String get stepType => workflowKey;
  String? get stepCommandId => null;
  String? get stepCommandType => null;
```

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

- [ ] `lib/oto/pipeline/pipeline_exe.dart`: base getter와 `PipelineExe` override 추가.
- [ ] `lib/oto/pipeline/pipeline_exe_handle.dart`: `PipelineExeHandle` override 추가.
- [ ] `lib/oto/pipeline/pipeline.dart`: executor 생성 직후 `exe.workflowKey = key;` 설정.

### 테스트 작성

- 작성: `test/oto_core_test.dart`의 event 테스트에서 `exe` step이 `commandId: hello`, `commandType: Print`를 기록하는지 검증한다.

### 중간 검증

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

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

## [STEP_EVENT_CORE-3] Pipeline.execute event 발생

### 문제

[pipeline.dart](/config/workspace/oto/lib/oto/pipeline/pipeline.dart:72)는 executor를 순회하며 실행만 한다. 성공/실패 시점이 구조화된 event로 남지 않고, 실패 시 어느 step이 실패했는지도 외부 자동화가 알 수 없다.

```dart
// lib/oto/pipeline/pipeline.dart:72
Future execute() async {
  for (var exe in _exeList) {
    await exe.execute();
  }
  return simpleFuture;
}
```

### 해결 방법

각 executor 실행 전 `started`, 정상 반환 후 `completed`, 예외 발생 시 `failed` event를 `ExecutionContext.stepEvents`에 추가한다. `failed` 기록 뒤에는 기존 실패 흐름을 유지하기 위해 예외를 다시 던진다.

```dart
Future execute() async {
  for (var index = 0; index < _exeList.length; index++) {
    final exe = _exeList[index];
    final stepId = context.allocateStepEventId();
    context.addStepEvent(StepEvent.started(...));
    try {
      await exe.execute();
      context.addStepEvent(StepEvent.completed(...));
    } catch (e) {
      context.addStepEvent(StepEvent.failed(...));
      rethrow;
    }
  }
  return simpleFuture;
}
```

`PipelineAsync.execute()`는 async command 완료를 기다리지 않는 기존 의미를 유지한다. 따라서 `async` step의 `completed`는 "비동기 실행 예약 완료"를 의미한다.

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

- [ ] `lib/oto/pipeline/pipeline.dart`: index 기반 루프, event 기록, 실패 rethrow 구현.
- [ ] `test/oto_core_test.dart`: 성공 event 순서와 실패 event 기록 테스트 추가.

### 테스트 작성

- 작성: `test/oto_core_test.dart`
  - `pipeline execution records started and completed step events`: `Pipeline.pipelineInitialize`로 `exe: hello` 실행 후 event 2개, `stepId` 동일, event 순서 `started` -> `completed` 검증.
  - `pipeline execution records failed step event before rethrow`: test-local `PipelineExecutor`를 만들어 throw시키고 `failed.error.message` 검증.

### 중간 검증

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

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/oto/core/execution_context.dart` | STEP_EVENT_CORE-1 |
| `lib/oto/application.dart` | STEP_EVENT_CORE-1 |
| `lib/oto/pipeline/pipeline_exe.dart` | STEP_EVENT_CORE-2 |
| `lib/oto/pipeline/pipeline_exe_handle.dart` | STEP_EVENT_CORE-2 |
| `lib/oto/pipeline/pipeline.dart` | STEP_EVENT_CORE-2, STEP_EVENT_CORE-3 |
| `test/oto_context_test.dart` | STEP_EVENT_CORE-1 |
| `test/oto_core_test.dart` | STEP_EVENT_CORE-2, STEP_EVENT_CORE-3 |

## 최종 검증

```bash
dart analyze && dart test test/oto_context_test.dart test/oto_core_test.dart test/oto_application_test.dart
```

예상 결과: analyze issue 없음, 모든 테스트 통과. Dart test cache 출력은 허용한다.

```bash
rg --sort path -n "StepEvent|stepEvents|allocateStepEventId|resetStepEvents" lib test
```

예상 결과: 새 event 계약 참조가 `execution_context.dart`, `application.dart`, `pipeline.dart`, 관련 테스트에만 나타난다.

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