<!-- task=m-workflow-core/05+02,03,04_notification_model plan=0 tag=WFC-EVENTS -->

# Workflow Core Notification Event Model Plan

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

이 계획은 `02+01_transition_ownership`, `03+01_failure_policy`, `04+03_retry_timeout` PASS 후 진행한다. 구현 완료는 active `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션과 검증 출력 작성까지다. 사용자 결정이나 외부 환경 준비가 필요한 blocker만 `사용자 리뷰 요청`으로 기록한다.

## 배경

Workflow 상태 변화는 notification service와 proto-socket `task.status.changed` event로 팬아웃된다. 현재 이벤트 payload는 상태와 메시지 중심이라 attempt, reason, 발생 시각 같은 workflow context가 부족하고, contracts note도 현재 shape만 기록한다. 이 작업은 내부 notification event 모델과 client-facing compatibility note를 additive하게 맞춘다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 USER_REVIEW 작성 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- Task ids:
  - `notification-model`: notification event 모델과 발행 지점
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/workflow-core/PHASE.md`
- `agent-roadmap/phase/workflow-core/milestones/workflow-core.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `services/core/internal/notification/model.go`
- `services/core/internal/notification/service.go`
- `services/core/internal/notification/service_test.go`
- `services/core/internal/scheduler/jobs.go`
- `services/core/internal/scheduler/jobs_test.go`
- `services/core/internal/protosocket/events.go`
- `services/core/internal/protosocket/tasks.go`
- `services/core/internal/protosocket/tasks_test.go`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/server_test.go`
- `services/core/cmd/server/main.go`
- `packages/contracts/notes/flutter-core-api-candidates.md`

### 테스트 환경 규칙

- test_env: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`
- 적용 명령: `cd services/core && go test ./...`
- contracts note 변경은 문서 변경이며 별도 contracts command가 없다. 검증은 deterministic grep으로 shape 기록을 확인한다.

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

- notification sink fanout tests exist.
- proto-socket event broadcast and diagnostics tests exist.
- Event payload lacks test coverage for new additive context fields because those fields do not yet exist.

### 심볼 참조

- planned changed symbols: `notification.TaskEvent` additive fields only; no removals.
- call sites: scheduler emits `TaskEvent`; notification tests construct events; proto-socket maps events; server wires broadcaster.

### 분할 판단

- task group: `m-workflow-core`
- this subtask: `05+02,03,04_notification_model`
- predecessors:
  - `02+01_transition_ownership`: active plan exists, `complete.log` pending.
  - `03+01_failure_policy`: active plan exists, `complete.log` pending.
  - `04+03_retry_timeout`: active plan exists, `complete.log` pending.
- split reason: event model must consume the final transition/failure/retry context and updates both core and contracts note.

### 범위 결정 근거

- Additive event payload only; no breaking removal of `id`, `type`, `status`, `title`, `message`.
- No Mattermost delivery expansion; completed Mattermost behavior remains existing `NotifyTaskEvent` branch.
- No Flutter client code changes in this milestone.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`
- 근거: shared proto-socket event payload and contracts compatibility note have cross-boundary impact.

## 구현 체크리스트

- [ ] [WFC-EVENTS-1] `notification.TaskEvent`에 workflow context fields를 additive하게 추가하고 scheduler emission point를 정리한다.
- [ ] [WFC-EVENTS-2] proto-socket `task.status.changed` payload와 contracts note를 additive fields와 맞춘다.
- [ ] 중간 및 최종 검증 명령을 실행하고 출력 원문을 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `02+01_transition_ownership`, `03+01_failure_policy`, `04+03_retry_timeout` complete.log가 필요하다.
- Runtime dependency source: directory name `05+02,03,04_notification_model`.

## [WFC-EVENTS-1] Notification TaskEvent Context

### 문제

- [services/core/internal/notification/model.go](/config/workspace/nomadcode/services/core/internal/notification/model.go:27)는 `Type`, `TaskID`, `Title`, `Status`, `Message`만 둔다.
- [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:56), [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:82), [services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:342)는 각 event payload를 수동 구성한다.

### 해결 방법

Before [services/core/internal/notification/model.go](/config/workspace/nomadcode/services/core/internal/notification/model.go:27):

```go
type TaskEvent struct {
	Type    TaskEventType
	TaskID  string
	Title   string
	Status  string
	Message string
}
```

After:

```go
type TaskEvent struct {
	Type       TaskEventType
	TaskID     string
	Title      string
	Status     string
	Message    string
	Attempt    int
	Reason     string
	OccurredAt time.Time
}
```

Use zero values safely: old tests and callers can omit new fields. Add a helper such as `notification.NewTaskEvent(eventType, task, message)` only if it avoids duplicated parsing of `task.Metadata`.

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

- [ ] `services/core/internal/notification/model.go`: add additive fields and import `time` if needed.
- [ ] `services/core/internal/scheduler/jobs.go`: populate event context from returned task metadata and updated_at.
- [ ] `services/core/internal/scheduler/jobs_test.go`: assert running/completed/failed events include expected status and attempt/reason when available.
- [ ] `services/core/internal/notification/service_test.go`: keep sink fanout tests passing with additive fields.

### 테스트 작성

- Required:
  - Extend `TestWorkEmitsRunningAndCompletedEvents`.
  - Extend `TestWorkEmitsFailedEvent`.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/notification ./internal/scheduler
```

Expected: notification and worker event tests pass.

## [WFC-EVENTS-2] Proto-Socket Event Payload Contract

### 문제

- [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:67) maps only `id`, `type`, `status`, `title`, `message`.
- [packages/contracts/notes/flutter-core-api-candidates.md](/config/workspace/nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:108) records the same limited shape.

### 해결 방법

Before [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:67):

```go
Payload: map[string]any{
	"id":      event.TaskID,
	"type":    string(event.Type),
	"status":  event.Status,
	"title":   event.Title,
	"message": event.Message,
},
```

After:

```go
Payload: map[string]any{
	"id":          event.TaskID,
	"type":        string(event.Type),
	"status":      event.Status,
	"title":       event.Title,
	"message":     event.Message,
	"attempt":     event.Attempt,
	"reason":      event.Reason,
	"occurred_at": event.OccurredAt.UTC().Format(time.RFC3339Nano),
},
```

Only include `occurred_at` when non-zero if that keeps payload cleaner. Update contracts note as Candidate/additive fields, not Stable schema.

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

- [ ] `services/core/internal/protosocket/events.go`: map additive fields.
- [ ] `services/core/internal/protosocket/tasks_test.go`: assert event envelope includes additive fields.
- [ ] `services/core/internal/protosocket/server_test.go`: keep broadcast diagnostics test passing.
- [ ] `packages/contracts/notes/flutter-core-api-candidates.md`: update `task.status.changed` payload row and note additive context fields.

### 테스트 작성

- Required:
  - Update `TestHandleTaskEventBroadcastsStatusChangedEnvelope`.
  - Existing server broadcast test should continue to pass.

### 중간 검증

```bash
cd services/core && go test -count=1 ./internal/protosocket ./internal/notification ./internal/scheduler
rg --sort path -n 'attempt|occurred_at|task.status.changed' packages/contracts/notes/flutter-core-api-candidates.md
```

Expected: tests pass and contracts note contains the additive event fields.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/notification/model.go` | WFC-EVENTS-1 |
| `services/core/internal/notification/service.go` | WFC-EVENTS-1 |
| `services/core/internal/notification/service_test.go` | WFC-EVENTS-1 |
| `services/core/internal/scheduler/jobs.go` | WFC-EVENTS-1 |
| `services/core/internal/scheduler/jobs_test.go` | WFC-EVENTS-1 |
| `services/core/internal/protosocket/events.go` | WFC-EVENTS-2 |
| `services/core/internal/protosocket/tasks_test.go` | WFC-EVENTS-2 |
| `services/core/internal/protosocket/server_test.go` | WFC-EVENTS-2 |
| `packages/contracts/notes/flutter-core-api-candidates.md` | WFC-EVENTS-2 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/protosocket ./internal/notification ./internal/scheduler
rg --sort path -n 'attempt|occurred_at|task.status.changed' packages/contracts/notes/flutter-core-api-candidates.md
cd services/core && go test ./...
```

Expected: focused tests pass, deterministic `rg` shows contracts note event fields, full core tests pass. Fresh `-count=1` is required for focused packages; final full run may use cache.

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