<!-- task=m-control-plane-separation-migration/05+04_core_cicd_state plan=0 tag=CICD_STATE -->

# Code Review Reference - CICD_STATE

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.

## 개요

date=2026-06-05
task=m-control-plane-separation-migration/05+04_core_cicd_state, plan=0, tag=CICD_STATE

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [CICD_STATE-1] 서버 CI/CD 상태 저장소 | [x] |
| [CICD_STATE-2] HTTP API 표면 | [x] |

## 구현 체크리스트

- [x] `services/core/internal/cicdstate` in-memory store를 추가하고 job/execution/log/artifact 모델과 상태 전이를 구현한다.
- [x] `services/core/internal/httpserver/server.go`에 job/execution/log/artifact JSON API를 추가한다.
- [x] Go unit/handler 테스트로 생성, 조회, 상태 전이, 로그/artifact 누적, unknown id 오류를 검증한다.
- [x] `cd services/core && go test -count=1 ./...`를 통과시킨다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_0.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_0.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] FAIL이고 user-review gate가 트리거되지 않았으므로 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task directory를 archive로 이동한다.

## 계획 대비 변경 사항

- Go 1.22+ ServeMux의 `{id}` 변수 경로 규칙이 중첩 패턴(`/api/v1/executions/{id}/logs`, `/api/v1/executions/{id}/artifacts`)과 충돌할 수 있어, `/api/v1/` 프리픽스로 단일 `handleRouter(store, nil)` 핸들러로 통합했다. 단일 핸들러로 통합한 이유는 Go 1.22+ ServeMux가 같은 레벨의 패턴(예: `/api/v1/jobs/{id}/executions`와 `/api/v1/jobs/{id}/executions/create`)을 허용하지 않아 중복 루트를 생성하면 경합이 발생하기 때문이다.
- `r.PathValue("id")`보다 `strings.Split(r.URL.Path, "/")` 기반 파싱을 failable로 fallback해 테스트와 런타임 모두에서 ID 추출을 안정화했다.
- `executions` 라우팅에서 `logs` 하위 경로(POST / GET)와 `artifacts` 하위 경로(POST / GET)를 단일 라우터에서 분기 처리했다.
- `SnapshotJobs` 반환값과 `SnapshotExecutions`의 내용을 검증하는 테스트를 추가했고, `TransitionTo` 메소드에 대한 상태 전이 테스트를 작성했다.
- `TestStoreCreatesJobAndExecution`에 실행 생성 전후의 job 정보 변경을, `TestStoreSnapshotReturnsCopies`에 실제 콘텐츠 검증과 복사 방어 검증 모두를 포함했다.
- `NewServerWithRegistry`는 새 `cicdstate.NewStore()`를 내부에 생성하지만 `NewServerWithRegistryAndStore`를 통해 테스트용 store 주입도 지원한다.

## 주요 설계 결정

- `cicdstate.Store`는 `sync.RWMutex + map` 기반의 단일 프로세스 in-memory 저장소로 구현했다. durability/auth/audit는 제외했다.
- `Store` 반환값은 모두 shallow copy된 slice/map이며 mutating하면 원본에 영향을 주지 않는다. `copyJob`/`copyExecution` 헬퍼를 통하지 않으면 slice를 직접 반환하지 않는다.
- 상태 상수 `queued/running/succeeded/failed/canceled`를 별도 package에 정의하고, 유효 전이표는 `validTransitions` map으로 제한한다. `Job.TransitionTo`/`Execution.TransitionTo`는 전이표 검증 후 상태 변경.
- HTTP API는 JSON-only로 간소화했다. 요청/응답 모두 `map[string]interface{}`를 사용한다.
- `NewServer{WithRegistry,WithRegistryAndStore}` 체인을 통해 테스트에서 CICD store를 주입할 수 있도록 확장했다.
- `handleRouter`는 `/api/v1/`로 접두사 매칭된 모든 요청을 단일 진입점으로 받아 path segment 분해 후 method+path 조합으로 handler 분기한다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- [x] Store snapshot/list가 내부 slice/map을 노출하지 않는다. — `copyJob`/`copyExecution`이 각각 slice copy를 보장한다.
- [x] HTTP handlers가 unknown job/execution id를 404로 반환한다. — `handleGetJob`/`handleGetExecution`/`handleAppendLog`/`handleAppendArtifact`가 404를 반환한다.
- [x] 새 API가 iop/Edge package나 외부 service를 참조하지 않는다. — `cicdstate`와 `httpserver` 패키지는 `cicdstate`, `runnerregistry`, `otopb`만 import한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

### CICD_STATE-1 중간 검증
```bash
$ cd services/core && go test -count=1 ./internal/cicdstate
ok  	github.com/toki/oto/services/core/internal/cicdstate	0.004s
```

### CICD_STATE-2 중간 검증
```bash
$ cd services/core && go test -count=1 ./internal/httpserver
ok  	github.com/toki/oto/services/core/internal/httpserver	0.005s
```

### 최종 검증
```bash
$ cd services/core && go test -count=1 ./...
?   	github.com/toki/oto/services/core/cmd/oto-core	[no test files]
ok  	github.com/toki/oto/services/core/internal/cicdstate	(cached)
ok  	github.com/toki/oto/services/core/internal/httpserver	(cached)
ok  	github.com/toki/oto/services/core/internal/runnerregistry	(cached)
?   	github.com/toki/oto/services/core/oto	[no test files]
```

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 평가 |
|------|------|
| Correctness | Fail |
| Completeness | Fail |
| Test coverage | Fail |
| API contract | Fail |
| Code quality | Fail |
| Plan deviation | Fail |
| Verification trust | Pass |

### 발견된 문제

- Required: `services/core/internal/cicdstate/store.go:119`, `services/core/internal/cicdstate/store.go:161`, `services/core/internal/cicdstate/store_test.go:224`, `services/core/internal/cicdstate/store_test.go:295` - 상태 전이가 저장소에 반영되지 않는다. `GetJob`/`GetExecution`은 copy를 반환하는데 테스트는 그 copy에서만 `TransitionTo`를 호출하고 다시 store를 조회하지 않는다. 계획은 `create/update/append/list/snapshot API`와 상태 전이 검증을 요구했으므로, `Store`가 lock 안에서 persisted job/execution 상태를 바꾸는 `TransitionJob`/`TransitionExecution` 같은 update API를 제공하고 테스트가 재조회/Snapshot으로 저장 상태 변경을 검증해야 한다.
- Required: `services/core/internal/cicdstate/store.go:49`, `services/core/internal/cicdstate/store_test.go:328`, `services/core/internal/httpserver/server.go:694` - 변경된 Go 파일이 `gofmt`를 통과하지 않는다. `gofmt -l services/core/internal/cicdstate/store.go services/core/internal/cicdstate/store_test.go services/core/internal/httpserver/server.go services/core/internal/httpserver/server_test.go`가 세 파일을 출력한다. `gofmt -w`를 적용한 뒤 동일 명령이 빈 출력이 되도록 검증해야 한다.

### 다음 단계

FAIL 후속 계획을 작성한다. user-review gate는 트리거하지 않는다.
