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

# Plan - REVIEW_REFACTOR

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

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

## 배경

이 follow-up은 첫 리뷰 FAIL에서 발견된 canonical id foundation 결함만 수정한다. S09의 핵심은 filename slug가 canonical id이고 path/provider work item id는 갱신 가능한 속성이라는 점이다. 현재 구현은 changed provider work item id를 candidate identity에 반영하지 못하고, canonical row를 찾은 뒤 revision mismatch를 reconcile 전에 upsert로 덮을 수 있다.

## 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

## Archive Evidence Snapshot

- Archived plan: `agent-task/m-plane-origin-authoring-roundtrip-sync/05_canonical_id/plan_cloud_G06_0.log`
- Archived review: `agent-task/m-plane-origin-authoring-roundtrip-sync/05_canonical_id/code_review_cloud_G06_0.log`
- Verdict: FAIL
- Required issues:
  - `services/core/internal/roadmapsync/develop_match.go:93` returns `expected` with only path replaced, so a scanned same-slug identity with a changed provider work item id is not propagated to storage.
  - `services/core/internal/roadmapsyncpipeline/service.go:210`-`218` upserts an existing canonical row before `ReconcileCreationCycle`, so stored provider/roadmap revision mismatches can be overwritten before conflict detection.
- Suggested issue:
  - `services/core/internal/db/*.go` generated headers were downgraded from `sqlc v1.31.1` to `sqlc v1.26.0` even where query output did not otherwise change.
- Verification evidence from failed loop:
  - `cd services/core && go test -count=1 ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline` passed.
  - `cd services/core && go test ./...` passed.
  - `git diff --check` passed.
- Allowed archive reread:
  - The next implementer may read only the two archived files listed above if exact prior review wording is needed. Do not search `agent-task/archive/**`.

## 범위 결정 근거

후속 범위는 canonical-id task의 Required 결함 2건으로 제한한다. Plane missing-create 실행, active index, live smoke, slot finalizer는 이 follow-up 범위가 아니다. SQLC header churn은 불필요한 artifact drift이므로, SQL/query를 다시 바꾸지 않는다면 관련 없는 generated version-only diffs를 제거한다.

## 구현 체크리스트

- [ ] `MatchDevelopMilestone`가 same slug로 matched된 scanned identity의 provider/work item 속성을 candidate identity에 반영하고, 누락된 기존 속성만 명시적으로 보존하도록 수정한다.
- [ ] `SyncCreation` identity load/upsert 순서를 조정해 기존 canonical row의 revision mismatch를 upsert 전에 conflict로 판정하고, 이를 검증하는 pipeline 테스트를 추가한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Scanned Provider Attribute Propagation

문제: `services/core/internal/roadmapsync/develop_match.go`는 `expected.Matches(smIdentity)`로 same slug를 찾지만, 반환 identity는 `updatedExpected := expected`에 path만 교체한다. 따라서 scanned Milestone provider identity block에 새 `work item id`가 있어도 pipeline/storage로 전달되지 않는다.

해결 방법: loop 안에서 normalized scanned identity를 보존하고, candidate identity는 scanned identity를 기준으로 만든다. 단, scanned identity가 tenant/project/parent 등 선택 속성을 비워 둔 경우 기존 expected 값을 유지할지 명시적으로 결정해 테스트로 고정한다. provider가 다르면 기존처럼 mismatch여야 한다.

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

테스트 작성: 작성한다. `TestMatchDevelopMilestoneReadyWithWorkItemIDDifference`가 `got.Identity.WorkItemID == "NOMAD-99"`까지 검증하도록 강화하고, scanned identity가 tenant/project를 비웠을 때 기대 동작을 테스트로 고정한다.

중간 검증: `cd services/core && go test -count=1 ./internal/roadmapsync`

### [REVIEW_REFACTOR-2] Reconcile Before Existing Row Mutation

문제: `services/core/internal/roadmapsyncpipeline/service.go`가 existing canonical row를 찾은 뒤 path/work item/revision 차이를 모두 upsert하고 나서 `ReconcileCreationCycle`을 호출한다. 이 때문에 기존 row의 revision mismatch가 사라진 상태로 reconcile되어 conflict guard가 동작하지 않을 수 있다.

해결 방법: existing row 조회와 completed step 조회를 먼저 수행하고, 기존 row 그대로 `ReconcileCreationCycle`을 실행한다. `ActionConflict`, `ActionSkipSelfMutation`, `ActionComplete`에서는 provider call과 mutation을 하지 않는다. `ActionResume`으로 판정된 뒤에만 허용된 path/work item/revision 속성 갱신이 필요하면 upsert하고, 그 row id로 나머지 step을 실행한다. 신규 row 또는 provider work item fallback row의 canonical id backfill은 기존 conflict guard를 우회하지 않게 분리한다.

수정 파일 및 체크리스트:
- [ ] `services/core/internal/roadmapsyncpipeline/service.go`
- [ ] `services/core/internal/roadmapsyncpipeline/service_test.go`
- [ ] `services/core/internal/db/*.go` version-only churn cleanup, 필요한 경우만

테스트 작성: 작성한다. existing canonical row가 `ProviderRevision` 또는 `RoadmapRevision`을 다르게 갖는 경우 `SyncCreation`이 `SyncActionConflict`를 반환하고 `store.upserted == false`, provider calls 없음, ledger marks 없음임을 검증한다. path 또는 provider work item id만 바뀌고 revision이 같은 경우에는 allowed attribute update가 수행되는 테스트도 추가한다.

중간 검증: `cd services/core && go test -count=1 ./internal/roadmapsyncpipeline`

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/roadmapsync/develop_match.go` | REVIEW_REFACTOR-1 |
| `services/core/internal/roadmapsync/develop_match_test.go` | REVIEW_REFACTOR-1 |
| `services/core/internal/roadmapsyncpipeline/service.go` | REVIEW_REFACTOR-2 |
| `services/core/internal/roadmapsyncpipeline/service_test.go` | REVIEW_REFACTOR-2 |
| `services/core/internal/db/*.go` | REVIEW_REFACTOR-2, version-only churn cleanup only if needed |

## 최종 검증

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

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

`git diff --check`

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