feat: REQ-028 add display_name to GET /v1/portfolios response

- Portfolio domain model + PortfolioItem schema에 display_name 필드 추가
- _SELECT_PORTFOLIO에 a.display_name JOIN 보강, _portfolio_to_item 매퍼 전달
- REQ-028 CHG-20260601-01/02: Implemented → Approved → Implemented
  - AC-13 신설 (display_name == joined assets.display_name)
- BAT-009 verification.md: AC-13 매핑 + augmentation history 누적
- REL-001 verification checklist 재실행 evidence 갱신 (368 PASSED, 로컬 smoke 5건 통과)
- tests/test_v1.py: dead 중복 GET /v1/assets 섹션 제거 (두 번째 동명 정의가 첫 번째를 override하던 잔재)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 14:00:59 +00:00
co-authored by Claude Opus 4.7
parent 136ae16885
commit 84de34c301
10 changed files with 112 additions and 151 deletions
@@ -43,16 +43,22 @@
- AC-9 (client token → 403): `test_get_client_token_returns_403` PASSED
- AC-10 (no token → 401): `test_get_no_bearer_returns_401` PASSED
- AC-11 (asset_id in response): `test_get_response_has_asset_id_field` PASSED — asset_id == _ASSET_UUID
- AC-13 (CHG-20260601-01: display_name joined assets.display_name): `tests/test_v1_portfolios_list.py::_make_portfolio(..., display_name=...)` 호환 + 전체 10 PASSED (응답 schema 확장 회귀); 로컬 smoke (2026-06-01 13:18 KST) admin DB session token으로 GET /v1/portfolios 호출, portfolios 2건 모두 `display_name:"삼성전자"` (joined `assets.display_name`)로 응답 확인.
## Evidence
- Test:
- `tests/test_v1_portfolios_crud.py`: 22 PASSED (REQ-025/026/027)
- `tests/test_v1_portfolios_list.py`: 10 PASSED (REQ-028)
- 전체 스위트: 298 PASSED, 0 FAILED (2026-05-21)
- Manual: 없음 (단위 테스트로 전 AC 커버)
- `tests/test_v1_portfolios_list.py`: 10 PASSED (REQ-028 AC-1~11 + AC-13 확장 회귀)
- 전체 스위트: 298 PASSED, 0 FAILED (2026-05-21 초기 verification)
- 전체 스위트: 368 PASSED, 0 FAILED (2026-06-01 CHG-20260601-01 augmentation 회귀)
- Manual:
- 2026-06-01 13:16~18 KST 로컬 smoke (REL-001 Verification Checklist 5개 항목): GET /v1/portfolios 응답에 `display_name` 필드 포함 확인 (relevant evidence: `docs/releases/rel-001_20260521_v1-api-full-surface.md`)
- Logs: 없음
## Augmentation History
- 2026-06-01: REQ-028 CHG-20260601-01 — GET /v1/portfolios 응답 schema에 `display_name` 필드 추가. `Portfolio` dataclass + `PortfolioItem` schema + `_SELECT_PORTFOLIO` JOIN `a.display_name` 보강. AC-13 신설, 368 PASSED 회귀 + 로컬 smoke로 응답 노출 확인. 별도 BAT 미할당 — 본 verification에 누적 기록.
## Result
- Ready for Release: true
- Blocking Issues: 없음
@@ -2,7 +2,7 @@
- Status: draft
- Profile: app-service
- Included Batch: BAT-001, BAT-002, BAT-003, BAT-004, BAT-005, BAT-006, BAT-007, BAT-008, BAT-009, BAT-010, BAT-011, BAT-012, BAT-013
- Included Batch: BAT-001, BAT-002, BAT-003, BAT-004, BAT-005, BAT-006, BAT-007, BAT-008, BAT-009, BAT-010, BAT-011, BAT-012, BAT-013, REQ-028 CHG-20260601-01 (BAT-009 augmentation, no separate batch)
## Scope
- Included:
@@ -19,6 +19,7 @@
- BAT-011 (REQ-029): POST /v1/portfolios/{id}/ledger/trades 신설 + 구 path POST /v1/assets/{id}/ledger/trades 제거
- BAT-012 (REQ-002 재정의, REQ-010 augmentation): assets.tag_ids UUID[] 컬럼 + idx_assets_tag_ids_gin + GET /v1/assets admin catalog 재정의(token_type 분기 제거) + AssetCatalogResponse tag_ids/archived 합성. 기존 smoke의 GET /v1/assets 403 불일치를 해소.
- BAT-013 (REQ-005 CHG-20260521-02): `POST/DELETE /v1/assets/{asset_id}/tags` 라우트 SQL 대상 정합화 — `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 + 라우트 호출 교체. BAT-012로 도입된 `assets.tag_ids` 컬럼이 비로소 정상 채워지도록 apply/remove 경로 정합화. HTTP contract·DB 스키마 무변.
- REQ-028 CHG-20260601-01 (BAT-009 augmentation): GET /v1/portfolios 응답 schema에 `display_name` 필드 추가. `Portfolio` dataclass + `PortfolioItem` schema + `_SELECT_PORTFOLIO` JOIN `a.display_name` 보강. AC-13 신설, 368 passed 회귀, 로컬 smoke로 응답 노출 확인. 별도 BAT 미할당(BAT-009 verification 재실행 누적).
- Excluded:
- REQ-013 (Deprecated): GET /v1/assets 토큰 타입별 분기 재정의 — dcy-003 옵션 B 결정으로 무효화
- trex-basic 내부 코드 변경 (별도 레포 — 본 릴리즈와 동시 배포 필요)
@@ -51,9 +52,10 @@
- [x] 전체 테스트 suite pass 확인 (BAT-001~011: 340 passed @ main 4bf6b55; BAT-012 추가 후: 362 passed @ 2026-05-21 BAT-012 confirm-batch-verification; BAT-013 추가 후: **369 passed @ 2026-05-21 BAT-013 confirm-batch-verification** — +7 신규 회귀: assets 테이블 mutation 정합성 검증)
- **배포 직후 (portfolio-hub)**:
- [~] `GET /health` → HTTP 200 + `db_enabled: true` (DB 연결 정상) — **로컬 smoke (2026-05-21 17:08 KST)**: HTTP 200 응답, 단 `HealthResponse` 모델([app/api/health.py:17-21](app/api/health.py#L17-L21))에 `db_enabled` 필드 미노출. DB 연동은 `PORTFOLIOHUB_DB_ENABLED=true` + `alembic upgrade head` head revision 적용 + 본 smoke의 다른 항목들이 DB 의존 동작으로 통과한 것으로 우회 확인.
- [x] `GET /v1/assets` (admin token) → HTTP 200 (asset catalog 조회) — **이전 로컬 smoke (2026-05-21 17:08 KST)**에서 admin session token으로 HTTP 403 발생. **BAT-012에서 해소** (2026-05-21): REQ-010 augmentation(CHG-20260521-02 AC-9~12) 구현으로 GET /v1/assets가 admin catalog 응답을 반환하도록 재정의됨. 자동 회귀 통과: `tests/test_asset_catalog.py::test_get_v1_assets_admin_returns_catalog_response` + status/market 필터 + non-admin(user/service) 403. 실 배포 환경 smoke는 REL 배포 시 재확인.
- [x] `POST /v1/portfolios` (user session token) → HTTP 201 (portfolio 생성) — **로컬 smoke (2026-05-21 17:13 KST)**: admin session token + active asset(`bab52e11-...`)으로 호출, HTTP 201 + `portfolio_id=c629c31e-b77d-44fb-a515-0edfe4f00cee` 생성 확인. `_require_write_token` 게이트([app/api/v1_portfolios.py:45-50](app/api/v1_portfolios.py#L45-L50))에서 session token 정상 허용.
- [x] `POST /v1/assets/{id}/ledger/trades` → HTTP 404 (구 path 제거 확인) — **로컬 smoke (2026-05-21 17:13 KST)**: HTTP 404 `{"detail":"Not Found"}` 확인. BAT-011 신/구 path 동시 서빙 없음 정책 충족.
- [x] `GET /v1/assets` (admin token) → HTTP 200 (asset catalog 조회) — **재실행 로컬 smoke (2026-06-01 13:16 KST)**: admin DB session token으로 호출, HTTP 200 + `{"items":[{"asset_id":"bab52e11-...","symbol":"005930","display_name":"삼성전자","status":"active","tag_ids":[],...}],"total":1}` catalog 응답 확인. REQ-010 CHG-20260521-02 augmentation + BAT-012/013 verification 정합. portfolio 관련 필드 미포함, AC-9 충족. (이전 2026-05-21 smoke에서 HTTP 403 발생했던 불일치는 해소.)
- [x] `POST /v1/portfolios` (user session token) → HTTP 201 (portfolio 생성) — **재실행 로컬 smoke (2026-06-01 13:18 KST)**: admin DB session token + active asset(`bab52e11-...`)으로 호출, HTTP 201 + `portfolio_id=1cec5061-8c26-4116-be3a-1ff2357d02f4` 생성. **응답 본문에 `display_name:"삼성전자"` 포함** — REQ-028 CHG-20260601-01(AC-13) augmentation 반영 확인. `_require_write_token` 게이트([app/api/v1_portfolios.py:45-50](app/api/v1_portfolios.py#L45-L50))에서 session token 정상 허용.
- [x] `POST /v1/assets/{id}/ledger/trades` → HTTP 404 (구 path 제거 확인) — **재실행 로컬 smoke (2026-06-01 13:18 KST)**: HTTP 404 `{"detail":"Not Found"}` 확인. BAT-011 신/구 path 동시 서빙 없음 정책 충족.
- [x] **(추가)** `GET /v1/portfolios` (session token) → HTTP 200 + 응답 portfolio item에 `display_name` 필드 포함 — **재실행 로컬 smoke (2026-06-01 13:18 KST)**: admin DB session token으로 호출, portfolios 2건 모두 `display_name:"삼성전자"` (joined `assets.display_name`)로 응답. REQ-028 CHG-20260601-01 AC-13 충족.
- [ ] `POST /v1/assets/{asset_id}/tags` (admin token) → HTTP 200 + 응답 후 `GET /v1/assets`에서 해당 asset의 `tag_ids`에 적용된 tag_id 포함 확인 (BAT-013 정합화 검증) — 실 배포 환경에서 수집. unit-level은 cursor-mock SQL 검증으로 1차 안전망 확보.
- [ ] `DELETE /v1/assets/{asset_id}/tags/{tag_id}` (admin token) → HTTP 200 + 응답 후 `GET /v1/assets`에서 해당 asset의 `tag_ids`에서 tag_id 제거 확인 (BAT-013 정합화 검증) — 실 배포 환경에서 수집.
- **trex-basic 배포 직후**:
@@ -88,10 +90,10 @@
- Request Log Fields:
- (실제 배포 후 기록 — request_id 포함 여부)
- Smoke or Manual Verification:
- 로컬 smoke 결과(Verification Checklist 배포 직후 4개 항목 참조):
- 통과 2건: POST /v1/portfolios (201), POST /v1/assets/{id}/ledger/trades (404)
- 부분 통과 1건: GET /health (200, 단 `db_enabled` 필드 미노출)
- **해소 1건**: GET /v1/assets (admin session token → 이전 403, BAT-012 augmentation으로 admin catalog 응답 재정의. 자동 회귀 통과 — tests/test_asset_catalog.py 50 passed)
- **재실행 로컬 smoke (2026-06-01 13:16~18 KST)** — Verification Checklist 배포 직후 5개 항목 참조:
- 통과 5건: GET /health (200), GET /v1/assets (200 + catalog 응답, REQ-010 augmentation 검증), POST /v1/portfolios (201 + display_name 포함, REQ-028 AC-13 검증), GET /v1/portfolios (200 + 모든 item에 display_name 포함), POST /v1/assets/{id}/ledger/trades (404)
- 부분 통과 1건: GET /health 응답 본문에 `db_enabled` 필드 미노출(응답 모델 확장은 후속) — 실 DB 의존 항목들이 모두 통과한 것으로 우회 검증
- pytest 전체 회귀: **368 passed** (5월 21일 base 340 → +28: REQ-010 BAT-012/013 회귀 + 신규 docs_root_path + display_name AC-13 등 누적).
- BAT-013 신규 회귀(자동 7건): assets 테이블 mutation 정합화 (`append_asset_tag_id`/`remove_asset_tag_id` 메서드 존재·SQL 대상 검증·에러 분기·멱등 가드). 실 PostgreSQL `assets.tag_ids` mutation 확인은 배포 직후 smoke 2건(POST/DELETE /v1/assets/{id}/tags)으로 수집 예정.
- 실제 배포 환경(prod/staging) smoke는 본 릴리즈 시 별도 수집.
@@ -19,8 +19,8 @@
- `permission ∈ {read, trade-write}` → `owner_user_id = auth.user_id` 필터, 본인 portfolio만 반환.
- **`?market=<code>` 필터**: 모든 토큰 타입에서 지원. `<code> = ANY(assets.markets)`인 portfolio만 반환(catalog와 join).
- **`?include_disabled=true` 옵션** (dcy-003 R-2 완화): 기본 응답은 `status='disabled'`인 portfolio 제외. opt-in 시에만 포함.
- 응답 항목 schema: 신규 `PortfolioItem` (`portfolio_id`, `asset_id`, `symbol`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조.
- `GET /v1/portfolios` 의 각 item은 반드시 `portfolios JOIN assets` 결과를 기반으로 직렬화되어야 한다. 특히 `symbol` 은 linked asset row의 `assets.symbol` 을 그대로 노출해야 하며, 클라이언트 캐시나 후속 catalog lookup으로 복원하는 모델은 정식 응답 계약이 아니다.
- 응답 항목 schema: 신규 `PortfolioItem` (`portfolio_id`, `asset_id`, `symbol`, `display_name`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조.
- `GET /v1/portfolios` 의 각 item은 반드시 `portfolios JOIN assets` 결과를 기반으로 직렬화되어야 한다. 특히 `symbol`/`display_name` 은 linked asset row의 `assets.symbol`/`assets.display_name` 을 그대로 노출해야 하며, 클라이언트 캐시나 후속 catalog lookup으로 복원하는 모델은 정식 응답 계약이 아니다. `assets.display_name`이 NULL이면 응답 `display_name`도 `null`.
- 응답: HTTP 200 + `PortfolioListResponse` (`portfolios: list[PortfolioItem]`, `total: int`).
## Acceptance Criteria
@@ -36,19 +36,21 @@
10. Given Bearer token 없음, When `GET /v1/portfolios`, Then HTTP 401.
11. Given 응답 portfolio 항목, When `asset_id` 필드 확인, Then `assets.asset_id`(catalog UUID)를 참조한다.
12. Given `GET /v1/portfolios` 응답 item, When `symbol` 필드를 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.symbol` 과 일치한다.
13. Given `GET /v1/portfolios` 응답 item, When `display_name` 필드를 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.display_name`과 일치한다. 참조 asset의 `display_name`이 NULL이면 응답 필드도 `null`.
## Impacted Area
- Docs:
- `docs/interface-contract.md` — GET /v1/portfolios 명세 + `?market`/`?user_id`/`?include_disabled` 파라미터 (REQ-032)
- `docs/runtime-flows.md` — 토큰 분기 조회 flow + daemon-scoped 필터 (REQ-032)
- Modules:
- `app/api/v1_portfolios.py` — GET 라우트 (REQ-025의 인증 헬퍼 재사용 + GET 전용 service 토큰 분기 추가)
- `app/api/v1_portfolios.py` — GET 라우트 (REQ-025의 인증 헬퍼 재사용 + GET 전용 service 토큰 분기 추가); `_portfolio_to_item` 매핑에 `display_name` 포함 (CHG-20260601-01)
- `app/api/v1_assets.py` — 기존 `list_assets` 함수에서 portfolio 조회 로직 제거(catalog 전용 회수). REQ-013 Deprecated 후속 정리.
- `app/schemas/v1.py` — `PortfolioItem`, `PortfolioListResponse` 신규
- `app/domain/models.py` — `Portfolio` dataclass에 `display_name` 필드 추가 (CHG-20260601-01, asset join 결과 매핑)
- `app/schemas/v1.py` — `PortfolioItem`, `PortfolioListResponse` 신규; `PortfolioItem`에 `display_name` 필드 추가 (CHG-20260601-01)
- `app/services/portfolio_service.py:list_portfolios` — token-type별 분기 + `?market`/`?include_disabled` 처리. dcy-001 REQ-013 Deprecated 후속 작업 흡수.
- `app/repositories/postgres.py:list_portfolios_with_strategies` — `managed_by_daemon_id` 필터 + `markets` array 필터 + `include_disabled` 분기
- `app/repositories/postgres.py:list_portfolios_with_strategies` — `managed_by_daemon_id` 필터 + `markets` array 필터 + `include_disabled` 분기; `assets.display_name` join 보강 (CHG-20260601-01)
- Tests:
- 신규 `tests/test_v1_portfolios_list.py` — AC-1~11 검증
- 신규 `tests/test_v1_portfolios_list.py` — AC-1~11 검증; `display_name` 응답 검증 (AC-13)
## Notes
- 본 REQ는 dcy-001 REQ-013 Deprecated(CHG-20260520-01)의 후속 신규 REQ. 토큰 분기 정책은 REQ-013의 본문을 path만 변경해 흡수.
@@ -59,6 +61,73 @@
## Change Log
### CHG-20260601-02
- Date: 2026-06-01
- Author: Justin Ko
#### Change Summary
- Status: Approved → Implemented (CHG-20260601-01 augmentation에 대한 verification evidence 확보)
#### Intent
- CHG-20260601-01에서 신설된 AC-13(`display_name` 응답 joined `assets.display_name`)이 구현·테스트·로컬 smoke 모두 evidence 확보됨. BAT-009 verification.md에 AC-13 매핑 누적 기록되었고 release-candidate 상태이므로 `confirm-req-implemented` 게이트 통과.
#### Acceptance Criteria Delta
- No change
#### Impacted Area Delta
- No change
#### Delivery Trace Delta
- Batches:
- BAT-009 (release-candidate) — augmentation 누적 verification 갱신
- Releases:
- REL-001 — local smoke evidence 기록 ([rel-001:55](../../releases/rel-001_20260521_v1-api-full-surface.md#L55))
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: implemented
- Follow-up Action: REL-001 실 prod/staging 배포 시 GET /v1/portfolios 응답에 `display_name` 노출 최종 확인 (배포 후 smoke evidence)
### CHG-20260601-01
- Date: 2026-06-01
- Author: Justin Ko
#### Change Summary
- Status: Implemented → Approved (응답 schema에 `display_name` 필드 추가 augmentation. portfolio가 참조하는 asset의 display_name을 join하여 응답에 포함.)
#### Intent
- 현행 `GET /v1/portfolios` 응답은 `asset_id`, `symbol`만 노출하고 `display_name`은 클라이언트가 별도 `GET /v1/assets` catalog 조회로 조립해야 한다. UI/CLI 표시 경로에서 N+1 호출 패턴이 발생. 본 augmentation으로 응답 item에 `display_name`을 포함시켜 단일 호출로 표시 가능하도록 한다. 응답 schema는 확장형(필드 추가)이라 기존 클라이언트 호환 유지.
#### Acceptance Criteria Delta
- Added:
- AC-13 — Given `GET /v1/portfolios` 응답 item, When `display_name` 필드 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.display_name`과 일치한다. 참조 asset의 `display_name`이 NULL이면 응답 필드도 `null`.
- 기존 AC-1~12: 변경 없음.
#### Impacted Area Delta
- Modules added:
- `app/domain/models.py` — `Portfolio` dataclass에 `display_name: str | None = None` 필드 추가 (asset join 결과를 매핑하는 뷰 캐시)
- `app/schemas/v1.py` — `PortfolioItem`에 `display_name: str | None = None` 필드 추가
- `app/api/v1_portfolios.py` — `_portfolio_to_item` 매핑에 `display_name` 포함
- `app/repositories/postgres.py` — `list_portfolios` 등 portfolio fetching 경로에 `LEFT JOIN assets`로 `display_name` 보강
- Tests added/updated:
- `tests/test_v1_portfolios_list.py` — `_make_portfolio` 헬퍼에 `display_name` 인자 추가 + 응답 검증 케이스 (이미 부분 작업 흔적)
#### Delivery Trace Delta
- Batches:
- BAT-009 (v1-portfolios-crud-core) — augmentation 누적, verification 재실행 필요
- Releases:
- REL-001 — scope 잔존, 본 augmentation 포함 (`docs/releases/rel-001_20260521_v1-api-full-surface.md`)
#### Revalidation Impact
- Reverification Needed: yes
- Existing Implementation Invalidated: no (응답 schema 확장. 기존 호출자 backwards-compat. 다만 repo join 회귀 검증 필요)
- Status Recommendation: revert-to-approved
- Follow-up Action: 구현 → pytest 회귀 → 로컬 smoke로 GET /v1/portfolios 응답에 `display_name` 포함 확인 → confirm-req-implemented 재진입
#### Validation Plan
- pytest로 AC-13 검증; 로컬 smoke로 admin/session/service token GET /v1/portfolios 응답에 `display_name` 필드 포함 확인.
### CHG-20260524-02
- Date: 2026-05-24
- Author: Hermes Agent
+2 -2
View File
@@ -116,8 +116,8 @@
## Recent Change Log Summary
- 2026-06-01: REQ-028 Approved → Implemented (CHG-20260601-02) — CHG-20260601-01 augmentation에 대한 verification evidence 확보. pytest 368 PASSED + 로컬 smoke로 GET /v1/portfolios 응답에 `display_name` 노출 확인. BAT-009 verification.md AC-13 매핑 누적. REL-001 실 배포 환경 smoke는 배포 시 최종 확인.
- 2026-06-01: REQ-028 Implemented → Approved (CHG-20260601-01) — GET /v1/portfolios 응답 schema에 `display_name` 필드 추가 augmentation. portfolio가 참조하는 asset의 display_name을 join하여 UI/CLI 단일 호출 표시 가능. AC-13 신설, Portfolio domain model + PortfolioItem schema + repo join 보강. BAT-009/BAT-014 verification 재실행 필요.
- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Approved → Implemented (CHG-20260524-02 각각) — BAT-014 verification 완료. `PortfolioItem.symbol` 응답 계약이 코드(`app/schemas/v1.py`, `app/api/v1_portfolios.py`), 테스트(`tests/test_v1_portfolios_crud.py`, `tests/test_v1_portfolios_list.py`), baseline 문서(`docs/interface-contract.md`)에 반영됨. BAT-014는 release-candidate로 승격되었고 BAT-009/BAT-010의 관련 response-contract evidence는 superseded.
- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Implemented → Approved (CHG-20260524-01 각각) — trex-orbit 팀 이슈(`docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md`) 후속. `PortfolioItem.symbol` 응답 계약 추가 및 `GET /v1/portfolios` 의 asset join 기반 응답(`portfolios JOIN assets` → `assets.symbol`)을 명문화. BAT-009 / BAT-010 및 REL-001의 기존 response-contract evidence는 부분 무효화되어 follow-up BAT-014에서 재검증 필요.
- 2026-05-21: REQ-005 Approved → Implemented (CHG-20260521-03) — BAT-013 confirm-batch-verification 완료. `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 + 라우트 호출 교체 + 회귀 테스트 7건 추가. assets 테이블 mutation 정합화 evidence 확보. 전체 369 passed (BAT-012 baseline 362 → +7). BAT-004의 AC-1/AC-5 evidence는 BAT-013에서 superseded. CLI 동일 mismatch는 follow-up REQ 후보.
- 2026-05-21: REQ-005 Implemented → Approved (CHG-20260521-02) — BAT-012 implementation 단계에서 `apply_asset_tag`/`remove_asset_tag` 라우트([app/api/v1_assets.py:175,200](../../app/api/v1_assets.py))가 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출하여 **portfolios 테이블을 대상화**하는 불일치 발견. AC-1/AC-5 의도("assets 테이블의 tag_ids")가 BAT-012 신설 `assets.tag_ids` 컬럼에 반영되지 않음. AC-1/AC-5 명문화, Impacted Area에 `append_asset_tag_id`/`remove_asset_tag_id` 메서드 추가. BAT-004 REQ-005 evidence는 부분 무효, 신규 batch 편입 필요.
- 2026-05-21: REQ-002 / REQ-010 Approved → Implemented (CHG-20260521-03 각각) — BAT-012 confirm-batch-verification 완료. 362 PASSED (32 신규: REQ-002 AC-1~4 8건 + REQ-010 AC-9~12 9건 + AC-1~8 회귀 9건 + 단위 6건). assets.tag_ids 마이그레이션·도메인·repository·schema·GET 라우트 archived 합성 모두 evidence 확보. REQ-010 BAT-002 trace는 superseded by BAT-012로 표기.