## 구현 진행 상황 - **대상:** 포트폴리오 추가 시 연결 데몬을 설정해도 지정되지 않음 (GitHub Issue #27) - **브랜치:** `line/line-0004-implementation` - **기준:** `main` (`bb3bb5d3f0ce57358f7385084fba1343f7ff06e1`) - **상태:** 2차 QC 반려 수정 구현·검증·로컬 커밋 완료 (QC 재검증 대기) - **최종 업데이트:** 2026-08-15 13:18 KST ### 구현 계획 및 작업 목록 - [x] 공유 daemon validation 도메인 오류 정의 - `DaemonNotFoundError`(HTTP 404)·`DaemonInactiveError`(HTTP 409)를 `app/domain/errors.py`에 추가해 전 계층에서 같은 오류 계약을 사용한다. - [x] `create_portfolio` 저장소 계약·구현에 `managed_by_daemon_id` 전달 - 프로토콜 시그니처(`portfolio_repository.py`)와 PostgreSQL 구현(`postgres.py`)에 `managed_by_daemon_id` 파라미터를 추가하고 INSERT 컬럼까지 반영해 생성 persistence 경계에 누락을 제거한다. - [x] 생성·수정 트랜잭션 안의 원자적 daemon 검증 경계 - 기존 `_lock_active_strategy` 패턴처럼 daemon row를 `FOR SHARE` lock하고 존재·소유권·active를 같은 트랜잭션에서 검증해, portfolio write와 하나의 원자적 판정으로 묶어 TOCTOU를 차단한다. - [x] `POST /v1/portfolios` 생성 handler에서 비-null daemon 전달·오류 매핑 - UUID 형식이 아니면 422, 미존재·foreign(타인 소유)이면 404, inactive면 409로 매핑하고 admin의 `user_id` override 생성은 최종 target owner 기준으로 검증한다. - [x] `PATCH /v1/portfolios/{portfolio_id}` 비-null 지정·재지정에도 동일 검증 적용 - 기존 비-null PATCH 지정·재지정 경로에 생성과 동일한 daemon 검증·오류 계약을 적용하되, 포트폴리오 소유권·disabled 보호·service/client 거부 등 기존 mutation 계약은 유지한다. - [x] 단위/API 테스트 추가 - 성공 생성 응답의 `managed_by_daemon_id` 일치, 미선택 생성 시 null 유지, malformed 422, 미존재·foreign 404, inactive 409, admin target-owner 허용/거부, PATCH 지정·재지정을 검증한다. - [x] 실제 PostgreSQL 통합 테스트 추가 - `POST → GET` persistence 보존, 검증 실패 시 portfolio 무변경, assignment와 daemon 비활성화가 경합해도 inactive daemon에 새 assignment가 확정되지 않는 동시성 race를 검증한다. - [x] 문서 갱신 - `docs/interface-contract.md`, `docs/data-model.md`에 daemon 배정 검증 계약·오류 코드·원자적 판정 경계를 반영한다. - [x] 회귀·정적 검사 및 진행 상황 업데이트 - 기존 portfolio 생성·수정의 인증·소유권·strategy·asset·disabled 계약 회귀, 전체 관련 테스트, compile·`git diff --check`·Ruff 검사를 실행하고 진행 상황 파일을 갱신한다. - [x] 참조 중 daemon owner transfer 차단 및 409 계약 - `update_daemon_owner()`가 daemon row를 `FOR UPDATE`로 잠근 뒤 참조 portfolio를 검사하고, 참조가 있으면 credential 삭제·owner 변경 전에 `DaemonInUseError`를 발생시킨다. admin PATCH는 이를 HTTP 409으로 반환한다. - [x] assignment/owner-transfer 직렬화 및 실제 PostgreSQL 검증 - assignment의 `FOR SHARE`와 transfer의 `FOR UPDATE`를 동일 daemon row에 적용해 transfer-first 또는 assignment-first의 허용 결과만 남기고, 순차·경합·credential·token-revoke 회귀를 실제 PostgreSQL로 검증한다. ### 진행 기록 - 2026-08-15 13:00 KST — Issue #27의 승인된 Discovery(`dcy-0004`) 범위를 기준으로 구현 계획을 확정했다. 근본 원인(`v1_portfolios.py` 생성 handler가 `repo.create_portfolio(...)`에 `managed_by_daemon_id`를 전달하지 않음)과 승인된 결정(존재·active·동일 소유자, 404/409/422 오류 계약, 원자적 판정 경계)을 반영해 작업 목록을 구성했다. - 2026-08-15 15:10 KST — `app/domain/errors.py`에 `DaemonNotFoundError`·`DaemonInactiveError`를 추가하고, `create_portfolio` 프로토콜·구현과 INSERT 컬럼에 `managed_by_daemon_id`를 반영했다. `postgres.py`에 `_lock_active_daemon`(daemon row `FOR SHARE` + 존재·동일 소유자·active 검증)을 추가해 생성·수정 트랜잭션 안에서 원자적 판정 경계를 구성했다. - 2026-08-15 15:10 KST — `POST /v1/portfolios`와 `PATCH /v1/portfolios/{id}` handler에 UUID 형식(422)·미존재/foreign(404)·inactive(409) 검증과 오류 매핑을 적용했다. admin target-user 생성은 최종 target owner 기준으로 daemon 소유권을 검증한다. - 2026-08-15 15:12 KST — 기존 `test_v1_portfolios_crud.py`의 `_FakeRepo.create_portfolio` 시그니처를 새 파라미터에 맞춰 보완했다. - 2026-08-15 15:15 KST — 전용 API 테스트 `tests/test_v1_portfolios_daemon_assignment.py`(13개)를 추가했다. POST 성공·null 유지·malformed 422·미존재/foreign 404·inactive 409·admin target-owner 허용/거부, PATCH 지정·재지정·malformed·foreign·inactive를 검증해 통과했다. - 2026-08-15 15:18 KST — 실제 PostgreSQL 통합 테스트 `tests/test_v1_portfolios_daemon_postgres_integration.py`(4개)를 추가했다. HTTP `POST → GET` persistence, 검증 실패 시 portfolio 무변경, assignment vs deactivation race의 허용 outcome, PATCH 재배정 persistence를 실제 DB에서 검증했다. - 2026-08-15 15:20 KST — 회귀 검증: 전체 단위 스위트 `625 passed, 11 skipped`, 실제 PostgreSQL 통합(daemon 4 + strategy 4) `8 passed`. Python compile, `git diff --check`, 변경 핵심 파일 Ruff 검사 통과(Ruff 이슈 44건은 기준 커밋과 동일한 기존 항목이며 신규 도입 없음). - 2026-08-15 13:10 KST — 1차 QC가 `update_daemon_owner()`가 참조 중 daemon의 소유자를 변경해 `daemon.owner_user_id == portfolio.owner_user_id` 불변식을 깨뜨리는 blocker를 재현해 FAIL 판정했다. `.proofline/lines/line-0004/evidence/implementation-fix-rerequest.md`의 명시 요구에 따라 2차 보완 계획을 적용했다. - 2026-08-15 13:18 KST — `DaemonInUseError`를 추가하고 `update_daemon_owner()`를 daemon `FOR UPDATE` → 참조 portfolio 검사 → credential 삭제 → owner UPDATE 순서의 단일 트랜잭션으로 변경했다. assignment는 기존 daemon `FOR SHARE`를 먼저 획득하므로 transfer-first면 이전 owner assignment가 foreign으로 실패하고, assignment-first면 transfer가 409으로 실패한다. 자동 assignment 해제·portfolio owner 이전·재배정·backfill은 추가하지 않았다. - 2026-08-15 13:18 KST — admin daemon PATCH 409 매핑, PATCH missing daemon 404·무변경, 참조 transfer의 owner/assignment/credential 무변경, 미참조 transfer의 credential invalidation, assignment/transfer race, deactivation 후 assignment reference 보존과 service-token revoke를 테스트로 보강했다. - 2026-08-15 13:18 KST — 수정 커밋 `e90519a fix(daemons): block owner transfer while assigned`를 생성했다. 집중 단위/API `62 passed`, 전체 스위트 `627 passed, 15 skipped`, 실제 PostgreSQL daemon integration `8 passed`, `compileall`, `git diff --check`, `proofline validate`를 통과했다. 지정 Ruff 검사는 기준 `e2102b6`의 기존 `76`건에서 현재 `74`건으로 감소했으며 새 lint 항목은 없다. > 각 작업이 실제로 완료되고 검증될 때마다 이 댓글의 체크박스와 진행 기록을 갱신한다. 체크 표시는 구현 완료 또는 해당 검증의 실제 통과를 의미하며, 배포·릴리스 완료를 의미하지 않는다. 1차 구현은 `e2102b6`, 2차 QC 반려 수정은 `e90519a`에 로컬 커밋되었다. push·PR·main 반영·배포·release는 수행하지 않았으며, 이 기록은 QC PASS 또는 lifecycle 완료를 뜻하지 않는다.