<!-- task=m-openai-compatible-raw-tunnel-sideband-passthrough/04_assembled_log plan=0 tag=OBSLOG -->

# Plan - OBSLOG

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

구현 후 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 반드시 실제 구현 내용과 검증 출력으로 채운다. 검증을 실행하고, active 파일은 그대로 두고, 리뷰 준비 상태를 보고한다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단하면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service blocker, 일반 범위 변경, evidence gap은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan으로 처리한다.

## 배경

현재 raw tunnel은 provider bytes를 passthrough하고 sideband assembler도 갖고 있지만, pure passthrough와 transformed/normalized path의 실행 로그에는 assembled output observation이 충분히 남지 않는다. Milestone S08은 raw chunk payload를 기본 저장하지 않으면서 readable assembled content, reasoning, tool call, sideband summary를 검증 가능한 fixture로 남기는 것을 요구한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision이 구현을 차단할 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. direct chat prompt는 금지이며, code-review가 이 요청을 검증하고 필요한 경우에만 실제 `USER_REVIEW.md`를 작성한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- Task ids:
  - `assembled-log`: 실행 로그에는 raw chunk payload 대신 assembled `content`, `reasoning`, `tool_calls`, sideband summary, transformed output을 남긴다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/log_safety_test.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/store/store.go`
- `apps/node/internal/store/store_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 없음.
- Target scenario: S08 -> Milestone Task `assembled-log`.
- Evidence Map row: execution log fixture에서 assembled content/reasoning/tool_calls 확인과 raw chunk 미저장 확인. 구현 체크리스트와 검증은 이 row를 그대로 따라 `apps/edge/internal/openai` log fixture와 raw payload negative test를 요구한다.

### 테스트 환경 규칙

- test_env: local.
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`.
- 적용 명령: local Go package tests 우선. `apps/edge/internal/openai` 변경이므로 `go test -count=1 ./apps/edge/internal/openai`를 필수로 둔다.
- 원격/field preflight: 이 plan은 local source/test 구현만 대상으로 하므로 원격 runner preflight는 사용하지 않는다. 외부 provider runtime smoke는 code-review가 남은 위험으로 보고할 수 있으나 PASS 조건은 S08 local fixture 충족이다.

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

- Pure passthrough: 현재 `TestChatCompletionsPassthroughDoesNotExposeSideband`는 sideband 부재와 byte identity를 검증하지만 assembled 실행 로그를 보지 않는다.
- Sideband: 현재 sideband response fixture는 assembled observation을 response body에서 확인하지만 실행 로그 fixture와 raw payload 미저장을 별도 검증하지 않는다.
- Transformed/normalized: 현재 `TestChatCompletionsTransformedModeLabelsIOPOutput`는 label/path만 확인하고 transformed output observation log를 보지 않는다.
- Log safety: 현재 `log_safety_test.go`는 preview field 부재를 검증하므로 새 assembled log는 raw chunk/full body leak 없이 구조화되어야 한다.

### 심볼 참조

- renamed/removed symbols: none.

### 분할 판단

- split policy 평가 완료.
- 공유 task group: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/`.
- sibling plan:
  - `04_assembled_log`: 이 plan, `assembled-log` 전용.
  - `05_backpressure_cancel`: 별도 plan, `backpressure-cancel` 전용.
- 의존 관계: 없음. 두 작업은 로그/관측과 flow-control 검증으로 소유 경계와 실패 양상이 다르다.

### 범위 결정 근거

- 포함: `apps/edge/internal/openai`의 assembled observation logging, log safety tests, focused OpenAI handler fixtures.
- 제외: Node SQLite schema migration. SDD S08은 raw chunk 장기 저장 금지가 핵심이고 현재 Node store는 output payload column이 없으므로 schema 변경 없이 negative evidence로 충분하다.
- 제외: provider runtime field smoke. 외부 provider endpoint가 없어도 S08은 deterministic tunnel frame fixture로 검증 가능하다.

### 빌드 등급

- `local-G06`: bounded edge/openai observability change이며 local unit/log fixtures로 검증 가능하지만 log-safety 정책과 passthrough/sideband/transformed 세 경로를 함께 다룬다.

## 구현 체크리스트

- [ ] `providerChatAssembler` 또는 새 helper를 통해 pure passthrough, sideband, transformed/normalized output에서 content/reasoning/tool call summary를 일관된 observation으로 만들고 raw provider chunk/body를 zap field로 저장하지 않는다.
- [ ] `apps/edge/internal/openai/server_test.go`에 `assembled-log` fixture를 추가해 readable assembled summary, tool call names/count, sideband summary, transformed output label을 확인한다.
- [ ] `apps/edge/internal/openai/log_safety_test.go`를 갱신해 새 log line이 raw chunk payload, provider body, preview field를 남기지 않는다는 negative assertion을 추가한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'`를 통과시킨다.
- [ ] `go test -count=1 ./apps/edge/internal/openai`를 통과시킨다.
- [ ] `git diff --check`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [OBSLOG-1] Assembled Observation Logging

문제: [stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:414)의 pure passthrough close log는 `body_bytes`만 남기고, [chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:452)의 normalized output log도 content/reasoning length 중심이다. S08의 assembled output evidence가 없다.

해결 방법: provider response parsing은 기존 `providerChatAssembler`를 재사용하거나 작은 공통 observation helper로 분리한다. Pure passthrough에서는 body bytes를 caller에게 그대로 쓰면서 assembler에는 관찰만 시킨다. Transformed/normalized path는 `chatCompletionOutput`에서 assembled summary를 만들되 raw content 전문을 zap field로 저장하지 말고, readable-but-safe policy를 명시한다. 기존 log safety가 full preview를 금지하므로 full text 저장이 필요하다고 판단되면 code-review 전에 Milestone lock decision이 아니라 일반 follow-up risk로 기록한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/stream.go`: passthrough/sideband close log에 assembled summary fields 추가. raw `body`/`provider_body` payload field 금지.
- [ ] `apps/edge/internal/openai/chat_handler.go`: transformed/normalized output log에 response mode와 assembled/tool call observation 추가.
- [ ] `apps/edge/internal/openai/run_result.go`: 필요 시 observation DTO helper만 추가하고 public contract는 바꾸지 않는다.

테스트 작성: 작성. `server_test.go`에 pure passthrough, sideband, transformed fixture를 추가하거나 기존 fixture를 확장한다.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)'
```

### [OBSLOG-2] Raw Payload Negative Guard

문제: [log_safety_test.go](/config/workspace/iop/apps/edge/internal/openai/log_safety_test.go:16)는 preview field 부재를 검증하지만 raw provider chunk/body를 저장하지 않는다는 S08 전용 assertion이 없다.

해결 방법: 새 log messages와 fields를 observer 기반 fixture에 추가하고, `raw_body`, `provider_body`, `body`, `delta_preview`, `content_preview`, `reasoning_preview` 같은 payload/preview field 금지를 검증한다. `body_bytes`, `content_len`, `reasoning_len`, `tool_call_count`, `tool_call_names` 같은 operational fields는 허용한다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/log_safety_test.go`: 새 assembled log line fixture와 forbidden field 목록 추가.
- [ ] 필요하면 `rg --sort path` 검증 명령으로 production zap payload field를 보조 확인한다.

테스트 작성: 작성. Log safety regression test가 이 plan의 핵심 acceptance evidence다.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestLog'
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/stream.go` | OBSLOG-1 |
| `apps/edge/internal/openai/chat_handler.go` | OBSLOG-1 |
| `apps/edge/internal/openai/run_result.go` | OBSLOG-1 |
| `apps/edge/internal/openai/server_test.go` | OBSLOG-1 |
| `apps/edge/internal/openai/log_safety_test.go` | OBSLOG-2 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletions.*(Assembled|Passthrough|Sideband|Transformed)|TestLog'
go test -count=1 ./apps/edge/internal/openai
rg --sort path -n 'zap\.(String|ByteString|Any)\("(raw_body|provider_body|raw_chunk|body|content_preview|reasoning_preview|delta_preview)"' apps/edge/internal/openai
git diff --check
```

`rg` 명령은 forbidden raw payload zap field가 나오지 않아야 한다. 출력이 있으면 필드명을 operational summary로 바꾸거나 test exception을 명확히 해야 한다.

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