<!-- task=m-milestone-work-item-creation-sync/03+02_develop_match plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API Develop Match Follow-up

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정, 사용자 소유 외부 환경 prerequisite, 또는 범위 충돌은 review stub의 `사용자 리뷰 요청`에 정확한 근거와 함께 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. `USER_REVIEW.md`, 로그 아카이브, `complete.log` 작성은 code-review 전용이다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

1차 구현은 `roadmapsync` 패키지와 테스트를 추가했지만, develop projection gate가 실제 pushed Milestone 문서의 provider identity를 expected Plane work item identity와 비교하지 않는다. 또한 bullet형 `Provider identity` parser가 indentation boundary를 보지 않아 unrelated sibling bullet을 identity field로 흡수할 수 있다. 이 follow-up은 두 Required만 닫는다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 요청 타당성을 검증하며, 구현 에이전트는 사용자에게 직접 질문하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- Task ids:
  - `cycle-develop-sync`: sync layer가 `branches/develop` 또는 remote `develop`에서 pushed Milestone을 감지하고 원래 Plane 티켓 identity와 매칭한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
- `packages/contracts/notes/flutter-core-api-candidates.md`
- `services/core/internal/workitem/provider.go`
- `services/core/internal/roadmapsync/identity.go`
- `services/core/internal/roadmapsync/milestone_parser.go`
- `services/core/internal/roadmapsync/develop_match.go`
- `services/core/internal/roadmapsync/identity_test.go`
- `services/core/internal/roadmapsync/milestone_parser_test.go`
- `services/core/internal/roadmapsync/develop_match_test.go`
- `agent-task/m-milestone-work-item-creation-sync/03+02_develop_match/plan_cloud_G07_0.log`
- `agent-task/m-milestone-work-item-creation-sync/03+02_develop_match/code_review_cloud_G07_0.log`
- `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/02+01_iop_authoring_bridge/complete.log`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`를 읽었다.
- core 코드 변경 필수 검증: `cd services/core && go test ./...`.
- 변경 패키지는 cache 신뢰를 피하기 위해 `cd services/core && go test -count=1 ./internal/roadmapsync`를 중간 검증으로 사용한다.
- whitespace 검증: `git diff --check`.

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

- `MatchDevelopMilestone`는 changed milestone path가 맞더라도 parsed provider identity가 missing/mismatched이면 not-ready가 되어야 한다. 현재 테스트 없음.
- bullet형 `Provider identity`는 nested child bullet만 field로 읽어야 한다. 현재 sibling/outdent bullet 오인 회귀 테스트 없음.

### 심볼 참조

- renamed/removed symbols: none.
- `rg -n "roadmapsync|MatchDevelopMilestone|ParseMilestoneIdentity|ProjectionCandidate|ScanResult" services/core` 결과, 신규 패키지 외 call site는 없다.

### 분할 판단

- 기존 split task `m-milestone-work-item-creation-sync/03+02_develop_match`의 review follow-up이다.
- predecessor index `02`는 `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/02+01_iop_authoring_bridge/complete.log`로 충족되었다.
- 새 sibling을 만들지 않고 같은 subtask loop에서 Required 2건만 보완한다. Plane Todo projection(04), idempotency/retry(05)는 그대로 제외한다.

### 범위 결정 근거

- Plane comment/body/status projection, identity map persistence, retry/idempotency DB 작업은 이 follow-up 범위가 아니다.
- 새 외부 API나 migration을 만들지 않는다.
- `roadmapsync` 패키지와 그 테스트만 수정한다.

### 빌드 등급

- build: `cloud-G07`. provider identity, roadmap parser, develop gate의 cross-domain correctness를 다시 맞춰야 하는 후속 수정이다.

## 구현 체크리스트

- [ ] `MatchDevelopMilestone`가 changed milestone path뿐 아니라 scanned Milestone의 parsed provider identity를 expected identity와 비교하도록 입력 모델과 gate를 보완한다.
- [ ] bullet형 `Provider identity` parser가 nested child bullet boundary만 field로 인정하도록 indentation/end-of-block 처리를 고친다.
- [ ] missing/mismatched scanned identity와 bullet outdent parser 회귀 테스트를 추가하고 `cd services/core && go test -count=1 ./internal/roadmapsync`, `cd services/core && go test ./...`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. Parser boundary를 먼저 고쳐 parser 결과 신뢰성을 확보한다.
2. Develop gate가 scanned identity를 입력받아 expected identity와 비교하게 한다.
3. 회귀 테스트와 core 검증을 실행한다.

### [REVIEW_API-1] Develop Gate Identity Match

#### 문제

`services/core/internal/roadmapsync/develop_match.go:38`의 `MatchDevelopMilestone`는 expected identity normalize, develop branch/revision, changed path만 확인한다.

```go
// services/core/internal/roadmapsync/develop_match.go:38
func MatchDevelopMilestone(in MatchInput) ProjectionCandidate {
	revision := in.Scan.Revision.Normalize()

	expected, err := in.Expected.Normalize()
	if err != nil {
		return notReady(in.Expected, revision, reasonInvalidIdentity)
	}

	if !revision.OnDevelop() {
		return notReady(expected, revision, reasonNotOnDevelop)
	}
	if revision.Revision == "" {
		return notReady(expected, revision, reasonNoRevision)
	}
	if !changedFilesContainMilestone(in.Scan.ChangedFiles, expected.RoadmapMilestonePath) {
		return notReady(expected, revision, reasonNoMilestoneFile)
	}
```

이 구조에서는 변경된 Milestone 파일에 provider identity가 없거나 다른 Plane work item id가 있어도 path만 맞으면 ready가 된다.

#### 해결 방법

`ScanResult`에 changed Milestone path와 parsed `Identity`를 함께 담는 typed shape를 추가한다. 예:

```go
type ScannedMilestone struct {
	Path     string   `json:"path"`
	Identity Identity `json:"identity"`
}

type ScanResult struct {
	Revision          Revision           `json:"revision"`
	ChangedFiles      []string           `json:"changed_files,omitempty"`
	ScannedMilestones []ScannedMilestone `json:"scanned_milestones,omitempty"`
}
```

`MatchDevelopMilestone`는 expected path와 같은 scanned milestone을 찾고, 그 `Identity.Normalize()`와 `expected.Matches(scanned)`를 통과할 때만 ready를 반환한다. path는 맞지만 scanned identity가 없거나 normalize되지 않으면 not-ready reason을 추가한다. provider/work item id mismatch도 not-ready reason으로 분리한다. 기존 `ChangedFiles` path guard는 유지하되, ready 조건은 scanned identity match까지 포함한다.

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

- [ ] `services/core/internal/roadmapsync/identity.go`
- [ ] `services/core/internal/roadmapsync/develop_match.go`
- [ ] `services/core/internal/roadmapsync/develop_match_test.go`

#### 테스트 작성

- `TestMatchDevelopMilestoneReady`는 scanned milestone identity까지 포함해 ready를 검증한다.
- changed path는 expected와 같지만 scanned identity가 비어 있으면 not-ready.
- changed path는 expected와 같지만 scanned work item id/provider가 다르면 not-ready.
- 기존 slot local/no revision, feature branch, non-milestone path 테스트는 유지한다.

#### 중간 검증

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

### [REVIEW_API-2] Provider Identity Bullet Boundary

#### 문제

`services/core/internal/roadmapsync/milestone_parser.go:95`의 parser는 raw line을 바로 trim한 뒤 block 상태를 관리한다.

```go
// services/core/internal/roadmapsync/milestone_parser.go:95
for scanner.Scan() {
	raw := scanner.Text()
	line := strings.TrimSpace(raw)
	if line == "" {
		continue
	}
```

`services/core/internal/roadmapsync/milestone_parser.go:122`에서 `- Provider identity:`를 만나면 `inBlock=true`가 되고, 이후 next heading 전까지 모든 `key: value` bullet을 field로 흡수한다.

```go
// services/core/internal/roadmapsync/milestone_parser.go:122
if !inBlock && strings.EqualFold(key, identityHeading) {
	inBlock = true
	found = true
	continue
}
```

따라서 nested child bullet이 아닌 뒤쪽 sibling bullet의 `provider` 또는 `work item id`도 provider identity field로 오인될 수 있다.

#### 해결 방법

heading block과 bullet block을 구분한다. bullet block에서는 `raw`의 leading space/tab indent를 보존하고, `- Provider identity:` parent indent보다 깊게 들여쓴 child bullet만 field로 읽는다. 빈 줄, outdent, 같은 indent의 sibling bullet, non-child line, 또는 다음 heading을 만나면 bullet block을 닫는다. heading block은 기존처럼 다음 heading까지 허용하되, 다른 heading에서 닫는다.

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

- [ ] `services/core/internal/roadmapsync/milestone_parser.go`
- [ ] `services/core/internal/roadmapsync/milestone_parser_test.go`

#### 테스트 작성

- `- Provider identity:` 아래 nested provider/work item id는 계속 성공한다.
- `- Provider identity:` 뒤에 nested field가 없고, 뒤쪽 top-level sibling bullet에 `provider`/`work item id`가 있어도 `ErrNoProviderIdentity`를 반환한다.
- heading block의 `## 완료 리뷰` 이후 provider line을 무시하는 기존 테스트는 유지한다.

#### 중간 검증

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

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/roadmapsync/identity.go` | REVIEW_API-1 |
| `services/core/internal/roadmapsync/develop_match.go` | REVIEW_API-1 |
| `services/core/internal/roadmapsync/develop_match_test.go` | REVIEW_API-1 |
| `services/core/internal/roadmapsync/milestone_parser.go` | REVIEW_API-2 |
| `services/core/internal/roadmapsync/milestone_parser_test.go` | REVIEW_API-2 |

## 최종 검증

```bash
cd services/core && go test -count=1 ./internal/roadmapsync
cd services/core && go test ./...
git diff --check
```

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