<!-- task=m-provider-resource-admission-ownership-alignment/14+10_offline_provider_tuple plan=0 tag=REFACTOR -->

# Code Review Reference - REFACTOR

> **[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 every implementation-owned section, keep active files in place, then end the implementation execution. The filled review-without-verdict state plus agent completion is the `review-ready` signal; do not create a handoff file/message, invoke code-review, or switch to the review role in place.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence, keep the active pair in place, and end the implementation execution; that file state is the same `review-ready` signal. Only the distinct review-agent instance decides whether to write `USER_REVIEW.md`.
> Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, call `request_user_input`, report review readiness, or wait for another user instruction. The runtime immediately assigns a distinct review-agent instance; a new turn, resume, compaction, or role change of this implementing agent is not distinct.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, follow-up pair creation, `코드리뷰 전용 체크리스트`) is distinct-review-agent-only, even after compaction/resume. The implementing agent must never perform it.
> `USER_REVIEW.md` and `complete.log` are the only overall loop terminal states. `review-ready`, `implementation-ready`, a verdict, an archive, or a WARN/FAIL pair is internal transitional state and must not trigger a user-facing question or intermediate report.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-07-21
task=m-provider-resource-admission-ownership-alignment/14+10_offline_provider_tuple, plan=0, tag=REFACTOR

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

> **[HARD ACTOR BOUNDARY]** 리뷰 에이전트는 구현 에이전트와 별도 agent instance여야 한다. 새 turn·resume·compaction·역할 전환으로 같은 agent를 재사용할 수 없다. product source/test를 수정하거나 현재·후속 plan을 구현하거나 구현 에이전트 소유 섹션을 채우지 않는다. WARN/FAIL 후속 pair를 만든 뒤 review 실행을 종료하면 archived current pair와 fresh pair가 runtime의 `implementation-ready` 신호가 된다. 별도 handoff 파일·사용자 메시지를 만들지 않는다.
> **[FOLLOW-UP MANDATE]** 미해결 Required는 별도 후속 지시가 없어도 반드시 FAIL과 정확히 하나의 next state를 만든다. 기본 next state는 follow-up pair이고, 유효한 Milestone-lock gate만 그 pair 대신 `USER_REVIEW.md`를 만든다. 유효한 gate는 Required/FAIL로 분류한다. Suggested는 WARN follow-up을 만들고, Nit-only는 모든 차원이 Pass일 때만 PASS할 수 있다. reviewer는 후속 pair까지만 materialize하고 직접 구현하지 않는다.

> **[REVIEW AGENT ONLY]** 실제 소스와 출력을 대조해 판정한다. archive 이름은 `code_review_local_G06_0.log`, `plan_local_G06_0.log`다. PASS에서만 `complete.log`와 월별 task archive를 수행한다.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| REFACTOR-1 Connectivity-aware provider snapshot | [x] |

## 구현 체크리스트

- [x] offline provider를 unavailable/offline/effective counter 0으로 노출하며 disabled와 offline 상태를 구분하고 catalog entry를 유지한다.
- [x] disconnect→offline→same identity reconnect→available/new admission service 흐름을 검증한다.
- [x] service race 결과 또는 blocker를 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 review-ready 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않는다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

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

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. 구현 에이전트는 수정하거나 체크하지 않는다.

- [x] predecessor 10의 matching `complete.log`를 확인한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나를 append하고 차원별 평가와 Required/Suggested/Nit를 일치시킨다.
- [x] active review를 `code_review_local_G06_0.log`, plan을 `plan_local_G06_0.log`로 rename한다.
- [x] `.gitignore` Agent-Ops block의 `agent-task/**/*.md`, `agent-task/**/*.log` unignore와 `agent-roadmap/current.md` ignore를 확인한다.
- [ ] PASS이면 complete-log template으로 exact tuple/race evidence를 기록하고 active `.md`를 남기지 않는다. 이 중간 child에는 `Roadmap Completion`을 기록하지 않는다.
- [ ] PASS이면 task directory를 월별 archive로 이동하고 최종 `.log`에서 체크한다.
- [ ] PASS split 이동 후 active parent에 sibling/file이 남아 있어 유지했는지 확인한다.
- [x] WARN/FAIL이고 user-review gate가 아니면 plan skill/fresh routing follow-up을 만들고 `complete.log`를 쓰지 않는다.
- [x] WARN/FAIL follow-up을 만든 뒤 review 실행을 종료한다. archived current pair와 fresh pair 및 reviewer 실행 종료가 runtime의 `implementation-ready` 신호다. 리뷰 에이전트는 후속 구현이나 별도 handoff artifact 생성을 수행하지 않으며 runtime이 별도 구현 에이전트 instance를 즉시 배정한다.
- [ ] USER_REVIEW이면 template으로 `USER_REVIEW.md`를 만들고 active pair와 `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW의 해소 결정이 완료/PASS이면 문서를 해소 상태로 갱신하고 `complete.log` 작성 및 archive 이동을 수행한다.
- [ ] USER_REVIEW의 해소 결정이 새 구현을 요구하면 plan이 USER_REVIEW를 archive하고 fresh pair를 쓴 뒤 review 실행을 종료한다. 이 상태가 `implementation-ready`이며 runtime이 별도 구현-agent instance를 배정한다.

## 계획 대비 변경 사항

1. `apps/edge/internal/service/model_queue_snapshot.go`:
   - `getSnapshotForNodeLocked(nodeID, rec)` → `getSnapshotForNodeLocked(nodeID, rec, connected bool)` 시그니처 변경.
   - `getSnapshotForNode(nodeID, rec)` → `getSnapshotForNode(nodeID, rec, connected bool)` 시그니처 변경.
   - `effectiveStatus(connected)` 헬퍼 추가: connected=false → `"unavailable"`.
   - `effectiveHealth(connected, health)` 헬퍼 추가: connected=false → `"offline"`.
   - `effectiveCount(connected, val)` 헬퍼 추가: connected=false → `0`.
   - `buildDisabledProviderSnapshot(prov)` 헬퍼 추가: disabled 상태의 원본을 제거하고 재사용.
   - enabled provider 블록에서 `Status`/`Health`/`Capacity`/`InFlight`/`Queued`/`LongContextCapacity`/`LongInFlight`/`LongQueued`/`LoadRatio`를 effective 함수로 감싸 disconnected 시 영하/제로로 처리.

2. `apps/edge/internal/service/status_provider.go`:
   - `buildSnapshotLocked`: `s.queue.getSnapshotForNodeLocked(rec.ID, r)` → `s.queue.getSnapshotForNodeLocked(rec.ID, r, snap.Connected)`.
   - `buildSnapshotNoQueue`: `staticProviderCatalogSnapshots(r)` → `staticProviderCatalogSnapshots(r, snap.Connected)`.
   - `staticProviderCatalogSnapshots(rec)` → `staticProviderCatalogSnapshots(rec, connected bool)` 시그니처 변경.
   - enabled 블록에 effective 함수 적용.

3. `apps/edge/internal/service/provider_scheduling_advanced_test.go`:
   - `m.getSnapshotForNode(...)` 호출 4곳에 `true` connected 인자 추가.

4. `apps/edge/internal/service/status_provider_test.go`:
   - `staticProviderCatalogSnapshots(rec)` 호출에 `true` 인자 추가.
   - `TestListNodeSnapshotsIncludesConfiguredOfflineNodes/noQueue`: disconnected node-b 의 expected tuple 을 `unavailable/offline/0`으로 업데이트.
   - `TestListNodeSnapshotsIncludesConfiguredOfflineNodes/withQueue`: disconnected node-b 의 expected tuple 을 `unavailable/offline/0`으로 업데이트.
   - `TestProviderSnapshotOfflineVsDisabled` 테스트 신규 추가: disabled vs offline 구별 검증.
   - `TestProviderSnapshotReconnectRestoresCapacityAndAdmission` 테스트 신규 추가: disconnect→offline→reconnect 사이클 검증.

## 주요 설계 결정

1. **effective tuple 전략**: disconnected 노드의 provider 에 대해 catalog identity(id/adapter/type/category/models/lifecycleCaps) 를 유지하고, effective 값(status/health/capacity/counters/loadRatio) 을 영하/제로로 내린다. disabled provider 는 connectivity 와 무관하게 항상 disabled 유지.
2. **connectivity 스루핑**: `buildSnapshotLocked`/`buildSnapshotNoQueue` 에서 `snap.Connected` 를 `getSnapshotForNodeLocked`/`staticProviderCatalogSnapshots` 에 직접 전달. NodeRecord 에서 직접 registry look up 을 하지 않음 (중복 방지).
3. **reconnect 반성**: reconnect generation 에서 configured capacity/capacity/longCapacity 는 provider record 에서 재읽고, lease-backed counters(inFlight/longInFlight) 는 기존 resource map 에서 유지. orphan counters 는 노출하지 않음.
4. **헬퍼 함수**: `effectiveStatus`/`effectiveHealth`/`effectiveCount` 의 3개 작은 헬퍼로 disconnected 상태의 효과를 한곳에서 관리.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 채운 뒤 구현 에이전트는 code-review를 실행하거나 역할을 바꾸지 않고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이므로 별도 handoff 파일·사용자 메시지를 만들지 않는다. runtime은 사용자에게 묻지 않고 별도 review 에이전트 instance를 즉시 배정하며, review 에이전트가 `USER_REVIEW.md`를 실제로 만들기 전에는 terminal 상태가 아니다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 blocker evidence·결정적 해제 조건·재검증 명령을 `검증 결과`나 `계획 대비 변경 사항`에 기록한 뒤 code-review의 일반 follow-up plan으로 처리한다._

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

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

- offline provider catalog와 model/provider identity가 유지되는가.
- 모든 effective 일반·long/queue counter가 0인가.
- disabled와 offline 의미가 섞이지 않는가.
- reconnect generation에서 orphan counter 없이 configured capacity와 신규 admission이 복구되는가.

## 검증 결과

### gofmt

`gofmt -d apps/edge/internal/service/model_queue_snapshot.go apps/edge/internal/service/status_provider.go`

```
gofmt OK
```

### 대상과 race

`go test -count=1 ./apps/edge/internal/service -run 'Snapshot|Offline|Reconnect'`

```
ok  	iop/apps/edge/internal/service	0.613s
```

`go test -race -count=1 ./apps/edge/internal/service`

```
ok  	iop/apps/edge/internal/service	7.637s
```

`go test -race -count=1 ./apps/edge/...`

```
ok  	iop/apps/edge/cmd/edge	1.256s
ok  	iop/apps/edge/internal/bootstrap	2.056s
ok  	iop/apps/edge/internal/configrefresh	1.193s
ok  	iop/apps/edge/internal/controlplane	5.520s
ok  	iop/apps/edge/internal/edgecmd	1.123s
ok  	iop/apps/edge/internal/edgevalidate	1.018s
ok  	iop/apps/edge/internal/events	1.038s
ok  	iop/apps/edge/internal/input	1.052s
ok  	iop/apps/edge/internal/input/a2a	1.030s
ok  	iop/apps/edge/internal/node	1.052s
ok  	iop/apps/edge/internal/openai	8.481s
ok  	iop/apps/edge/internal/opsconsole	1.028s
ok  	iop/apps/edge/internal/service	7.455s
ok  	iop/apps/edge/internal/transport	5.609s
```

### 신규 테스트 출력

`TestProviderSnapshotOfflineVsDisabled`:

```
--- PASS: TestProviderSnapshotOfflineVsDisabled (0.00s)
```

`TestProviderSnapshotReconnectRestoresCapacityAndAdmission`:

```
--- PASS: TestProviderSnapshotReconnectRestoresCapacityAndAdmission (0.00s)
    status_provider_test.go:2112: connected: status=available cap=5 inflight=2 longCap=2 longInflight=1
    status_provider_test.go:2157: disconnected: status=unavailable health=offline cap=0 inflight=0 longCap=0
    status_provider_test.go:2187: reconnected: status=available cap=5 inflight=2 longCap=2 health=available
```

### service race 결과

Race 검출 없음. 모든 Edge package race 테스트 통과.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING]** completion table, checklist, 변경 사항, 설계 결정, 검증 stdout/stderr를 모두 채우고 active 파일을 유지한 채 구현 실행을 종료한다. 이 파일 상태와 실행 종료가 runtime의 `review-ready` 신호이므로 별도 handoff 파일·사용자 메시지를 만들지 않는다. code-review 실행, 역할 전환, 판정, review 전용 섹션 수정, archive/terminal/follow-up 처리는 금지한다.

## 섹션 소유권

| Section | Owner | Note |
|---------|-------|------|
| Header, 개요, review 지시 | Fixed | 수정 금지 |
| 구현 표·체크리스트·변경 사항·설계 결정·검증 출력 | Implementing agent | 실제 내용 필수 |
| 코드리뷰 전용 체크리스트·코드리뷰 결과 | Review agent | 구현 에이전트 수정 금지 |
| 사용자 리뷰 요청 | Implementing agent | Milestone 잠금 결정만 사용 |

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 판정 | 근거 |
|------|------|------|
| correctness | Fail | reconnect 회귀가 disconnect generation settlement 뒤의 새 generation 상태 대신 수동으로 남긴 이전 counter를 정상 복구값으로 단정한다. |
| completeness | Fail | 계획한 disconnect→offline→same identity reconnect→new admission service 흐름을 실행하지 않았다. |
| test coverage | Fail | 신규 테스트가 `HandleNodeDisconnect`와 admission/lease API를 호출하지 않아 orphan 제거와 새 admission eligibility를 검증하지 못한다. |
| API contract | Pass | snapshot helper 시그니처와 현재 호출부는 함께 갱신됐고 wire schema 변경은 없다. |
| code quality | Pass | 대상 구현에 debug 출력, dead code, 미해결 TODO는 확인되지 않았다. |
| implementation deviation | Fail | 계획의 authoritative disconnect 및 신규 admission 검증이 registry map 조작과 snapshot 조회로 대체됐다. |
| verification trust | Fail | `TestProviderSnapshotReconnectRestoresCapacityAndAdmission`의 이름·주석과 달리 수동 counter는 lease-backed가 아니며 실제 출력도 reconnect 직후 이전 `inflight=2`를 그대로 노출한다. |

### 발견된 문제

- Required — `apps/edge/internal/service/status_provider_test.go:2072`: 테스트는 `resources`에 `inFlight=2`, `longInFlight=1`을 직접 넣은 뒤 `reg.Unregister`/`reg.Register`만 호출하고, `apps/edge/internal/service/status_provider_test.go:2173`에서 그 값을 새 generation의 정상 counter로 기대한다. 이 fixture에는 lease가 없고 `Service.HandleNodeDisconnect`도 호출되지 않으므로 disconnect settlement, orphan 제거, generation fencing, 신규 admission을 전혀 검증하지 않으며 SDD S14의 “이전 generation 상태가 새 generation을 막지 않고 새 admission 상태로 수렴” 조건과 반대인 stale-counter 기대를 고정한다. 첫 generation에서 실제 lease를 admission하고, authoritative unregister가 반환한 generation으로 `HandleNodeDisconnect`를 호출해 offline counter 0을 확인한 뒤, 더 높은 generation으로 reconnect하여 실제 신규 admission을 수행하고 snapshot이 configured capacity와 새 lease만 반영하는지 검증하도록 fixture와 assertion을 교체해야 한다.

### 다음 단계

- FAIL: plan skill의 isolated reassessment로 authoritative disconnect→reconnect→new admission 회귀를 보강하는 fresh follow-up pair를 만든다.
