<!-- task=m-plane-origin-authoring-roundtrip-sync/05_canonical_id plan=0 tag=REFACTOR -->

# Plan - REFACTOR

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

구현 완료의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일은 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 직접 사용자에게 묻거나 `USER_REVIEW.md`를 만들지 않는다. 환경/secret/service 준비, 일반 범위 조정, 검증 공백은 사용자 리뷰가 아니라 검증 결과나 후속 plan으로 남긴다.

## 배경

현재 sync identity는 `roadmap_milestone_path`와 Plane work item id를 match key로 삼아 phase 이동, archive 후보 이동, provider work item 재생성 시 같은 Milestone을 잃는다. SDD S09는 filename slug를 canonical id로 두고 path/provider id를 속성으로 기록하라고 요구한다. 이 작업은 DB/query/storage/domain foundation만 바꾸고 Plane missing-create 실행은 후속 06에서 다룬다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 판단이 필요한 경우는 선택된 SDD 결정 또는 선택된 Milestone 잠금 결정뿐이다. 해당 경우 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 채팅 질문, 선택지 제시, `request_user_input`, `USER_REVIEW.md` 생성은 금지다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-origin-authoring-roundtrip-sync.md`
- Task ids:
  - `canonical-id`: `agent-roadmap/phase/*/milestones/<milestone-id>.md`의 `<milestone-id>`를 단일 canonical sync id로 정의하고, `roadmap_sync_identities`/parser/matcher/projection이 path 대신 이 id를 1차 판별값으로 사용한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/plane-origin-authoring-roundtrip-sync/SDD.md`
- Acceptance scenarios:
  - `S09`: task=`canonical-id`; evidence=`storage/parser/matcher tests showing filename slug is canonical and path/provider ids are attributes`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-origin-authoring-roundtrip-sync.md`
- `agent-roadmap/sdd/agent-ops-mcp-control-plane/plane-origin-authoring-roundtrip-sync/SDD.md`
- `services/core/internal/roadmapsync/identity.go`
- `services/core/internal/roadmapsync/identity_test.go`
- `services/core/internal/roadmapsync/milestone_parser.go`
- `services/core/internal/roadmapsync/milestone_parser_test.go`
- `services/core/internal/roadmapsync/develop_match.go`
- `services/core/internal/roadmapsync/develop_match_test.go`
- `services/core/internal/roadmapsync/retry.go`
- `services/core/internal/roadmapsync/retry_test.go`
- `services/core/internal/roadmapsync/plane_format.go`
- `services/core/internal/roadmapsync/plane_format_test.go`
- `services/core/internal/storage/roadmap_sync_identities.go`
- `services/core/internal/storage/roadmap_sync_identities_test.go`
- `services/core/internal/roadmapsyncpipeline/service.go`
- `services/core/internal/roadmapsyncpipeline/service_test.go`
- `services/core/queries/roadmap_sync_identities.sql`
- `services/core/migrations/00006_create_roadmap_sync_identities.sql`
- `services/core/internal/db/roadmap_sync_identities.sql.go`
- `services/core/internal/db/models.go`
- `services/core/sqlc.yaml`

### 테스트 환경 규칙

test_env=`local`. `agent-test/local/rules.md`와 `core-smoke`, `workspace-ops-smoke`를 읽었다. core 변경 최소 검증은 `cd services/core && go test ./...`다. SQLC schema/query 변경은 generated code 갱신이 필요하므로 `cd services/core && sqlc generate`를 사용한다. 현재 대화 workspace에서는 `command -v sqlc` 출력이 없었고 `go`는 `/config/.local/bin/go`에 있다. standard remote runner에서 `sqlc`가 없으면 `command -v sqlc` 출력과 함께 차단으로 기록한다.

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

- filename slug canonical id: 현재 없음. `identity`, `milestone_parser`, `storage`, `roadmapsyncpipeline` 테스트를 추가해야 한다.
- path/provider work item id 변경을 속성으로 갱신하는 DB upsert: 현재 path/work item key 중심 테스트만 있다. 신규 SQL/storage 테스트가 필요하다.
- phase 이동 후보: 현재 `Matches`/`MatchesStrict`가 path를 비교하므로 테스트가 실패해야 한다.

### 심볼 참조

- 변경 후보: `Identity.Normalize`, `Identity.Matches`, `Identity.MatchesStrict`, `GetRoadmapSyncIdentityByMilestonePath`, `UpsertRoadmapSyncIdentityByWorkItem`, `upsertParams`, `existingFromRow`.
- `roadmap_milestone_path` 참조는 `services/core/internal/db`, `services/core/internal/storage`, `services/core/internal/roadmapsync`, `services/core/internal/roadmapsyncpipeline`, `services/core/internal/gitosync`, `services/core/internal/scheduler`에 있다. 이 plan은 identity foundation에 필요한 storage/domain/pipeline 참조만 수정한다.

### 분할 판단

split decision policy를 적용했다. 공유 task group은 `m-plane-origin-authoring-roundtrip-sync`다. 이 subtask는 `05_canonical_id`로 선행 의존성이 없다. 후속 `06+05_plane_missing_create`와 `07+05,06_slug_reconcile`은 이 identity foundation의 generated query와 storage API가 있어야 구현 가능하다. DB/schema, domain matching, pipeline call-site가 한 묶음으로 리뷰되어야 하므로 단일 subtask 안에서 닫는다.

### 범위 결정 근거

이 subtask는 canonical id foundation만 다룬다. Plane work item 생성, identity-less active doc enqueue, live smoke, slot finalizer는 제외한다. `services/core/internal/gitosync/scanner.go`의 active-index 직접 변경은 별도 완료 후보이며 여기서 다시 확장하지 않는다.

### 빌드 등급

`cloud-G06`: DB migration/query/generated code와 storage/domain/pipeline semantics가 함께 바뀌며, path/provider id를 속성으로 낮추는 cross-module behavior 변경이다.

## 구현 체크리스트

- [ ] `roadmapsync.Identity`에 filename slug canonical id 정규화 경로를 추가하고 path/provider work item id는 속성으로 비교되도록 matcher/reconcile tests를 갱신한다.
- [ ] `roadmap_sync_identities` migration/query/generated/storage를 canonical id lookup/upsert 중심으로 바꾸고 path 변경과 provider work item id 변경 테스트를 추가한다.
- [ ] `roadmapsyncpipeline`이 provider work item lookup 전에 canonical id lookup/upsert를 사용하도록 조정하고 existing row 변환을 갱신한다.
- [ ] `cd services/core && sqlc generate`를 실행해 generated DB code를 갱신한다. `sqlc`가 없으면 `command -v sqlc` 결과를 기록하고 구현을 차단한다.
- [ ] `cd services/core && go test ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline`와 `cd services/core && go test ./...`를 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Canonical Milestone ID Foundation

문제: [identity.go](/config/workspace/nomadcode/services/core/internal/roadmapsync/identity.go:36)는 minimal key를 provider + work item id + Milestone path로 설명하고, [identity.go](/config/workspace/nomadcode/services/core/internal/roadmapsync/identity.go:77)는 path/provider/work item을 필수로 요구한다. [identity.go](/config/workspace/nomadcode/services/core/internal/roadmapsync/identity.go:100)와 [identity.go](/config/workspace/nomadcode/services/core/internal/roadmapsync/identity.go:122)는 path를 match key로 비교한다.

해결 방법: `MilestoneID` 또는 `RoadmapItemID` canonical helper를 추가해 `agent-roadmap/phase/*/milestones/<slug>.md`의 `<slug>`를 정규화한다. Milestone shape의 identity는 canonical id를 필수로 하고, path/provider/work item id는 저장 속성으로 둔다. 기존 `RoadmapItemID`는 Milestone shape에서 slug를 담는 field로 채우고, naming comment를 명확히 한다.

```go
// before identity.go:77
if normalized.RoadmapMilestonePath == "" ||
	normalized.Provider == "" ||
	normalized.WorkItemID == "" {
	return Identity{}, ErrInvalidIdentity
}
```

```go
// after
if normalized.MilestoneID() == "" || normalized.Provider == "" {
	return Identity{}, ErrInvalidIdentity
}
```

수정 파일 및 체크리스트:
- [ ] `services/core/internal/roadmapsync/identity.go`
- [ ] `services/core/internal/roadmapsync/identity_test.go`
- [ ] `services/core/internal/roadmapsync/milestone_parser.go`
- [ ] `services/core/internal/roadmapsync/milestone_parser_test.go`
- [ ] `services/core/internal/roadmapsync/develop_match.go`
- [ ] `services/core/internal/roadmapsync/develop_match_test.go`
- [ ] `services/core/internal/roadmapsync/retry.go`
- [ ] `services/core/internal/roadmapsync/retry_test.go`

테스트 작성: 작성한다. `TestIdentityMatchesByMilestoneIDAcrossPhaseMove`, `TestIdentityMatchesStrictAllowsProviderWorkItemAttributeChange`, parser가 path에서 slug를 채우는 테스트를 추가한다.

중간 검증: `cd services/core && go test ./internal/roadmapsync` 통과.

### [REFACTOR-2] Storage Key Migration And Pipeline Lookup

문제: [00006_create_roadmap_sync_identities.sql](/config/workspace/nomadcode/services/core/migrations/00006_create_roadmap_sync_identities.sql:23)는 provider work item unique index를, [00006_create_roadmap_sync_identities.sql](/config/workspace/nomadcode/services/core/migrations/00006_create_roadmap_sync_identities.sql:27)는 path unique index를 key로 둔다. [roadmap_sync_identities.sql](/config/workspace/nomadcode/services/core/queries/roadmap_sync_identities.sql:16)는 provider work item conflict로 upsert하고 [roadmap_sync_identities.sql](/config/workspace/nomadcode/services/core/queries/roadmap_sync_identities.sql:24)는 path가 같을 때만 update한다. [service.go](/config/workspace/nomadcode/services/core/internal/roadmapsyncpipeline/service.go:179)는 work item으로 먼저 lookup한다.

해결 방법: 새 migration을 추가해 기존 rows의 `roadmap_item_id`를 path basename으로 backfill하고 Milestone shape canonical id unique index를 추가한다. query는 `GetRoadmapSyncIdentityByMilestoneID`와 `UpsertRoadmapSyncIdentityByMilestoneID`를 중심으로 바꾸고, provider work item unique index는 foreign key 중복 방지용으로 유지한다. pipeline은 canonical id로 identity row를 찾고, 같은 slug 재처리에서 path/provider work item id/revision을 갱신한다.

```sql
-- before roadmap_sync_identities.sql:16
ON CONFLICT (provider, tenant, project, work_item_id)
...
WHERE roadmap_sync_identities.roadmap_milestone_path = EXCLUDED.roadmap_milestone_path
```

```sql
-- after
ON CONFLICT (shape, roadmap_item_id) WHERE shape = 'milestone' AND roadmap_item_id <> ''
DO UPDATE SET roadmap_milestone_path = EXCLUDED.roadmap_milestone_path, ...
```

수정 파일 및 체크리스트:
- [ ] `services/core/migrations/00009_add_roadmap_sync_milestone_id.sql`
- [ ] `services/core/queries/roadmap_sync_identities.sql`
- [ ] `services/core/internal/db/roadmap_sync_identities.sql.go`
- [ ] `services/core/internal/db/models.go`
- [ ] `services/core/internal/storage/roadmap_sync_identities.go`
- [ ] `services/core/internal/storage/roadmap_sync_identities_test.go`
- [ ] `services/core/internal/roadmapsyncpipeline/service.go`
- [ ] `services/core/internal/roadmapsyncpipeline/service_test.go`

테스트 작성: 작성한다. storage mock tests에서 same slug + moved path, same slug + changed work item, different slug + reused work item conflict를 검증한다. pipeline tests에서 canonical lookup이 work item lookup보다 먼저 쓰이는지 확인한다.

중간 검증: `cd services/core && sqlc generate && go test ./internal/storage ./internal/roadmapsyncpipeline`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/roadmapsync/identity.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/identity_test.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/milestone_parser.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/milestone_parser_test.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/develop_match.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/develop_match_test.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/retry.go` | REFACTOR-1 |
| `services/core/internal/roadmapsync/retry_test.go` | REFACTOR-1 |
| `services/core/migrations/00009_add_roadmap_sync_milestone_id.sql` | REFACTOR-2 |
| `services/core/queries/roadmap_sync_identities.sql` | REFACTOR-2 |
| `services/core/internal/db/roadmap_sync_identities.sql.go` | REFACTOR-2 |
| `services/core/internal/db/models.go` | REFACTOR-2 |
| `services/core/internal/storage/roadmap_sync_identities.go` | REFACTOR-2 |
| `services/core/internal/storage/roadmap_sync_identities_test.go` | REFACTOR-2 |
| `services/core/internal/roadmapsyncpipeline/service.go` | REFACTOR-2 |
| `services/core/internal/roadmapsyncpipeline/service_test.go` | REFACTOR-2 |

## 최종 검증

`command -v sqlc`

`cd services/core && sqlc generate`

`cd services/core && go test ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline`

`cd services/core && go test ./...`

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