chore: restructure SRS for asset catalog and tag model redesign (dcy-001/002, REQ-001~008)
Architecture Redesign (May 19, 2026): - Separate admin-managed asset catalog from user portfolios - Eliminate portfolio as asset definition; clarify portfolio as user position - Introduce new asset_catalog + portfolio structure Discovery Documents: - dcy-001: asset-catalog-and-user-portfolio-model — admin asset management and user portfolio separation - dcy-002: tag-ownership-and-applicability-policy — tag model implementation strategy New Requirements (REQ-001~008): - REQ-001: tags master table introduction (Data, Proposed) - REQ-002: assets.tag_ids UUID[] column addition (Data, Proposed) - REQ-003: portfolios.tag_ids and portfolio_tags removal (Data, Proposed) - REQ-004: tags master HTTP API (Interface, Proposed) - REQ-005: tag apply/remove HTTP API (Interface, Proposed) - REQ-006: portfolio_tags data migration (Migration, Proposed) - REQ-007: pfhub tag CLI commands (Interface, Proposed) - REQ-008: tag model baseline docs update (Documentation, Proposed) Cleanup: - Remove archived SRS documents (REQ-001~029 old structure) - Remove archived batch documentation (BAT-001~010) - Remove archived discovery documents (dcy-001~006 old structure) - Remove archived release documentation (REL-001~006) Data Model Updates: - Add Deployment category to SRS structure - Add Data category for new data model requirements - Update app/cli/core/utils.py for new tag handling Documentation: - Create new SRS index with consolidated requirement structure - Update docs/discovery/index.md with new discovery references - Migrate from requirement-centric to domain-model-centric organization Rationale: - Existing single-portfolio model lacks admin catalog governance - New asset catalog enables central management of tradeable symbols - Tag system supports user-driven asset organization and discovery - Cleaner separation of concerns between admin operations and user trading
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
# BAT-001 Design
|
||||
|
||||
- Status: draft
|
||||
- Batch: BAT-001
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
trex-basic Consumer-Driven Contract를 충족하기 위해 4개 계층에 걸쳐 변경한다.
|
||||
|
||||
1. **DB 레이어** (REQ-009): Alembic migration 1개 추가로 4개 스키마 변경
|
||||
2. **보안 레이어** (REQ-010): `app/core/encryption.py` 신규 — Fernet 암복호화 유틸리티, `app/core/config.py` startup validation
|
||||
3. **인증 확장** (REQ-005): service-token 발급 시 UUID v4 runtime_id 동시 생성
|
||||
4. **v1 API 레이어** (REQ-004, 006, 007, 008): `/v1` prefix APIRouter 신규, 기존 `/health` 응답 확장
|
||||
|
||||
## Changed Areas
|
||||
|
||||
- Docs:
|
||||
- docs/project-structure.md (신규 모듈 app/core/encryption.py, app/api/v1_*.py 반영)
|
||||
- docs/runtime-flows.md (v1 API 요청 흐름, runtime_id 발급 흐름 추가)
|
||||
- Modules:
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (CREDENTIAL_ENCRYPTION_KEY 필드 + startup validation)
|
||||
- app/domain/models.py (AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가)
|
||||
- app/api/health.py (HealthResponse 스키마 적용, checked_at/service/version 필드 추가)
|
||||
- app/api/auth_routes.py (service-token 발급 응답에 runtime_id 포함)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/main.py (v1 router include 추가)
|
||||
- app/cli/commands/service_tokens.py (runtime_id 출력 추가)
|
||||
- app/services/auth_service.py (runtime_id 생성·저장 로직)
|
||||
- app/schemas/ (HealthResponse, v1 Assets/UserInfo/Ledger 스키마 추가)
|
||||
- app/repositories/postgres.py (broker_credentials, ledger_trades, runtime_id 쿼리 추가)
|
||||
- migrations/versions/ (신규 migration 파일)
|
||||
- Interfaces:
|
||||
- GET /health: 응답 필드 추가 (service, version, checked_at) — 기존 status 필드 유지로 하위 호환
|
||||
- POST /auth/service-tokens (또는 동등 경로): 응답에 runtime_id 추가
|
||||
- GET /v1/assets: 신규
|
||||
- GET /v1/user_info: 신규
|
||||
- POST /v1/assets/{asset_id}/ledger/trades: 신규
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **v1 라우터 구조**: `app/api/v1_*.py` 파일 3개를 각각 별도 모듈로 작성하고 `app/main.py`에서 `prefix="/v1"`로 일괄 등록한다. 기존 `/portfolios`, `/auth`, `/users` 라우터와 prefix가 겹치지 않는다.
|
||||
|
||||
2. **R-4 처리 (runtime_id 없는 구형 service-token)**: GET /v1/user_info 호출 시 해당 토큰에 runtime_id가 없으면 **HTTP 404** (`detail: "runtime_id not assigned to this token"`)를 반환한다. 즉시 생성 방식은 채택하지 않는다 — 기존 토큰 소유자에게 재발급을 요구하는 것이 명시적이고 안전하다.
|
||||
|
||||
3. **disabled_reason**: `users.is_active = false`인 경우 고정값 `"account_inactive"`를 반환한다. `is_active = true`인 경우 `disabled_reason`은 `null`로 반환한다.
|
||||
|
||||
4. **Fernet startup validation 위치**: `app/core/config.py`의 `Settings` 클래스 `model_post_init` 또는 FastAPI `lifespan` 시작 시 수행한다. `CREDENTIAL_ENCRYPTION_KEY`가 없으면 `RuntimeError`를 발생시켜 기동을 중단한다. 테스트 환경에서는 `CREDENTIAL_ENCRYPTION_KEY` 환경변수를 픽스처로 주입한다.
|
||||
|
||||
5. **runtime_id 포맷**: `str(uuid.uuid4())` — 하이픈 포함 UUID v4 문자열 (예: `"550e8400-e29b-41d4-a716-446655440000"`).
|
||||
|
||||
6. **broker_credentials 컬럼 타입**:
|
||||
- `user_id`: INTEGER NOT NULL (users.user_id FK)
|
||||
- `broker`: TEXT NOT NULL
|
||||
- `account_ref`: TEXT NOT NULL
|
||||
- `encrypted_payload`: TEXT NOT NULL
|
||||
- `credential_version`: INTEGER NOT NULL DEFAULT 1
|
||||
- `last_rotated_at`: TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
- PRIMARY KEY: `(user_id, broker)`
|
||||
|
||||
7. **ledger_trades 컬럼 타입**:
|
||||
- `ledger_entry_id`: BIGSERIAL PRIMARY KEY
|
||||
- `asset_id`: INTEGER NOT NULL (portfolios.portfolio_id 참조, FK 선택적)
|
||||
- `order_id`: TEXT NOT NULL
|
||||
- `execution_id`: TEXT NOT NULL
|
||||
- `side`: TEXT NOT NULL (buy/sell)
|
||||
- `quantity`: NUMERIC(20, 8) NOT NULL
|
||||
- `price`: NUMERIC(20, 8) NOT NULL
|
||||
- `fee_amount`: NUMERIC(20, 8) NOT NULL DEFAULT 0
|
||||
- `tax_amount`: NUMERIC(20, 8) NOT NULL DEFAULT 0
|
||||
- `executed_at`: TIMESTAMPTZ NOT NULL
|
||||
- `recorded_at`: TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
- UNIQUE: `(asset_id, order_id, execution_id)`
|
||||
|
||||
8. **앱 버전**: `app/core/config.py`의 `Settings.app_version` 값을 `GET /health` 응답의 `version` 필드로 사용한다. 현재 값: `"0.1.0"`.
|
||||
|
||||
9. **checked_at 타임존**: KST (UTC+9) ISO 8601 문자열로 반환한다. `datetime.now(timezone(timedelta(hours=9))).isoformat()`.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- `strategy_params` 파싱 실패(StrategySet.config가 null이거나 유효하지 않은 JSON): `{}` 폴백, 에러 전파 없음.
|
||||
- `ledger_trades` 중복 삽입 시 DB unique constraint 위반: psycopg `UniqueViolation` 캐치 → 기존 행 조회 → payload 비교 → 동일이면 200+duplicate, 다르면 409.
|
||||
- `broker_credentials`가 없는 사용자의 GET /v1/user_info: `brokers: []` 반환.
|
||||
- `CREDENTIAL_ENCRYPTION_KEY` 환경변수 없이 테스트 실행 시: 테스트 픽스처에서 `Fernet.generate_key()`로 임시 키 주입.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
- both (project-structure + runtime-flows)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
- project-structure.md: 신규 모듈(app/core/encryption.py, app/api/v1_assets.py, app/api/v1_user_info.py, app/api/v1_ledger.py) 반영 필요. implementation 완료 후 REQ-011에서 갱신.
|
||||
- runtime-flows.md: v1 API 요청 처리 흐름(service-token 인증 → 서비스 → DB), runtime_id 발급 흐름 추가 필요. implementation 완료 후 REQ-011에서 갱신.
|
||||
@@ -1,193 +0,0 @@
|
||||
# BAT-001 Implementation
|
||||
|
||||
- Status: implemented
|
||||
- Batch: BAT-001
|
||||
|
||||
## Plan Summary
|
||||
|
||||
design.md에서 확정된 결정 사항을 기반으로 아래 범위를 구현한다.
|
||||
|
||||
1. app/core/encryption.py 신규 — Fernet 암복호화 유틸리티
|
||||
2. app/core/config.py — CREDENTIAL_ENCRYPTION_KEY 필드 추가 및 require_credential_encryption_key() 메서드
|
||||
3. Alembic migration 신규 — 4개 스키마 변경 (IF NOT EXISTS 패턴)
|
||||
4. app/domain/models.py — AuthToken.runtime_id, TokenPrincipal.runtime_id, BrokerCredential, LedgerTrade 추가
|
||||
5. app/services/auth_service.py — service-token 발급 시 UUID v4 runtime_id 생성·저장
|
||||
6. app/cli/commands/service_tokens.py — 응답/출력에 runtime_id 포함
|
||||
7. app/core/auth.py — AuthContext.runtime_id 추가, _resolve_auth_context에서 전달
|
||||
8. app/api/health.py, app/schemas/v1.py — HealthResponse 스키마 적용
|
||||
9. app/api/v1_assets.py — GET /v1/assets 구현
|
||||
10. app/api/v1_user_info.py — GET /v1/user_info 구현 (R-4: 404 반환)
|
||||
11. app/api/v1_ledger.py — POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
12. app/repositories/postgres.py — broker_credentials, ledger_trades, runtime_id 쿼리 추가
|
||||
13. app/main.py — /v1 라우터 등록
|
||||
14. tests/ — 테스트 파일 작성 및 기존 테스트 확장
|
||||
|
||||
## Changed Files
|
||||
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (수정: credential_encryption_key 필드, require_credential_encryption_key 메서드, model_validator import)
|
||||
- app/core/auth.py (수정: AuthContext.runtime_id, _resolve_auth_context runtime_id 전달)
|
||||
- migrations/versions/20260502_0014_v1_api_schema.py (신규)
|
||||
- app/domain/models.py (수정: AuthToken.runtime_id, TokenPrincipal.runtime_id, Portfolio.market, BrokerCredential, LedgerTrade)
|
||||
- app/services/auth_service.py (수정: uuid import, runtime_id 생성·전달)
|
||||
- app/cli/commands/service_tokens.py (수정: ServiceTokenIssueCliResponse에 runtime_id 포함)
|
||||
- app/api/health.py (수정: HealthResponse 스키마 적용)
|
||||
- app/schemas/auth.py (수정: ServiceTokenIssueCliResponse.runtime_id 추가)
|
||||
- app/schemas/v1.py (신규: AssetsResponse, UserInfoResponse, LedgerTradeRequest/Response)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/repositories/postgres.py (수정: _SELECT_AUTH_TOKEN runtime_id, _SELECT_PORTFOLIO market, _row_to_auth_token runtime_id, _row_to_portfolio market, create_auth_token runtime_id, get_token_principal runtime_id, broker_credentials CRUD, ledger_trades CRUD, list_portfolios_with_strategies)
|
||||
- app/main.py (수정: v1 라우터 3개 등록)
|
||||
- tests/test_app.py (수정: test_healthcheck 응답 필드 검증 업데이트)
|
||||
- tests/test_auth.py (수정: _AuthRepo.create_auth_token runtime_id 파라미터 추가)
|
||||
- tests/test_cli.py (수정: FakeRepo.create_auth_token runtime_id 파라미터 추가)
|
||||
- tests/test_v1.py (신규: health/assets/user_info/ledger 9개 테스트)
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. [x] app/core/encryption.py 신규 구현
|
||||
2. [x] app/core/config.py CREDENTIAL_ENCRYPTION_KEY 추가 및 require_credential_encryption_key() 메서드
|
||||
3. [x] Alembic migration 파일 작성 (20260502_0014_v1_api_schema.py)
|
||||
4. [x] app/domain/models.py 모델 필드/클래스 추가
|
||||
5. [x] auth_service.py runtime_id 생성·저장 구현
|
||||
6. [x] service_tokens.py runtime_id 출력 추가
|
||||
7. [x] app/core/auth.py AuthContext.runtime_id 추가
|
||||
8. [x] app/api/health.py HealthResponse 스키마 적용
|
||||
9. [x] app/api/v1_assets.py 구현
|
||||
10. [x] app/api/v1_user_info.py 구현 (runtime_id 없는 구형 토큰 → HTTP 404)
|
||||
11. [x] app/api/v1_ledger.py 구현 (idempotent)
|
||||
12. [x] app/repositories/postgres.py 쿼리 추가
|
||||
13. [x] app/main.py /v1 라우터 등록
|
||||
14. [x] 테스트 작성 및 전체 테스트 스위트 통과 (98 passed, 2026-05-01 blocker 수정 후)
|
||||
|
||||
## Validation
|
||||
|
||||
- [x] pytest 98개 전체 통과 (2026-05-01, blocker B-1~B-12 수정 후)
|
||||
- [ ] alembic upgrade head 성공 (로컬 Docker Compose 환경 — DB 없이 코드 검증만 완료)
|
||||
- [x] GET /health 응답에 service/version/checked_at(+09:00 KST) 포함 확인 (테스트)
|
||||
- [x] GET /v1/assets 200 응답, 필드명 확인 (cash/quantity/broker_type/strategy_params)
|
||||
- [x] GET /v1/assets ?status 필터 확인 (테스트)
|
||||
- [x] GET /v1/user_info runtime_id 없는 토큰 → 404 확인 (테스트)
|
||||
- [x] GET /v1/user_info trading_enabled, brokers[] 확인 (테스트)
|
||||
- [x] GET /v1/user_info Fernet 복호화 → credential_payload 반환 확인 (테스트)
|
||||
- [x] POST /v1/assets/{asset_id}/ledger/trades: new → 201, duplicate same payload → 200, different payload → 409, unknown asset → 404 (테스트)
|
||||
- [x] POST /auth/service-tokens → 201 + runtime_id 확인 (테스트)
|
||||
- [x] startup: CREDENTIAL_ENCRYPTION_KEY 없으면 RuntimeError (테스트)
|
||||
- [x] Fernet encrypt/decrypt roundtrip 확인 (테스트)
|
||||
|
||||
## Blocker Fixes (2026-05-01)
|
||||
|
||||
- B-1: POST /auth/service-tokens 엔드포인트 추가 (app/api/auth_routes.py, app/schemas/auth.py)
|
||||
- B-2: GET /v1/assets ?status 필터 추가 (app/api/v1_assets.py)
|
||||
- B-3: 응답 필드명 수정 (cash/quantity/broker_type/strategy_id → 계약 정의명)
|
||||
- B-4: strategy_params {} 폴백 구현
|
||||
- B-5: UserInfoResponse trading_enabled 필드 추가
|
||||
- B-6: brokers[] 배열 구현 + Fernet 복호화
|
||||
- B-7: 중복 수신 → HTTP 200 + status="duplicate" (was created: bool)
|
||||
- B-8: 다른 payload → HTTP 409
|
||||
- B-9: 없는 asset_id → HTTP 404
|
||||
- B-10: lifespan startup validation (CREDENTIAL_ENCRYPTION_KEY 없으면 RuntimeError)
|
||||
- B-11: list_broker_credentials postgres 구현
|
||||
- B-12: docs/project-structure.md, docs/runtime-flows.md에 신규 모듈 및 v1 흐름 반영
|
||||
|
||||
## Changed Files (Blocker Fixes)
|
||||
|
||||
- app/api/auth_routes.py (수정: POST /auth/service-tokens 엔드포인트 추가)
|
||||
- app/schemas/auth.py (수정: IssueServiceTokenRequest, IssueServiceTokenApiResponse 추가)
|
||||
- app/schemas/v1.py (수정: 필드명 전면 수정, BrokerItem, trading_enabled, status)
|
||||
- app/api/v1_assets.py (수정: status 필터, strategy_params, 새 필드명)
|
||||
- app/api/v1_user_info.py (수정: trading_enabled, brokers[], Fernet 복호화, Depends(get_settings))
|
||||
- app/api/v1_ledger.py (수정: 200/201/404/409 분기, _payloads_match 비교)
|
||||
- app/repositories/postgres.py (수정: list_broker_credentials 추가)
|
||||
- app/main.py (수정: startup validation 추가)
|
||||
- tests/test_v1.py (전면 재작성: 19개 테스트로 확장)
|
||||
- tests/test_app.py (수정: db_enabled=True 테스트에 credential_encryption_key 추가)
|
||||
- docs/project-structure.md (수정: v1 모듈, encryption.py 반영)
|
||||
- docs/runtime-flows.md (수정: v1 API 흐름, startup 검증 흐름 추가)
|
||||
|
||||
## Design Decisions (confirmed during implementation)
|
||||
|
||||
- CREDENTIAL_ENCRYPTION_KEY: Settings 생성 시 validator 없음. `require_credential_encryption_key()` 메서드 호출 시 RuntimeError 발생 (lazy validation)
|
||||
- R-4 (구형 service-token runtime_id 없음): HTTP 404, detail="runtime_id not assigned to this token"
|
||||
- disabled_reason: is_active=False 시 "account_inactive" 고정값
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- (해소) R1: CREDENTIAL_ENCRYPTION_KEY — lazy validation으로 처리. 테스트 환경 unblocked.
|
||||
|
||||
|
||||
design.md에서 확정된 결정 사항을 기반으로 아래 범위를 구현한다.
|
||||
|
||||
1. app/core/encryption.py 신규 — Fernet 암복호화 유틸리티
|
||||
2. app/core/config.py — CREDENTIAL_ENCRYPTION_KEY 필드 추가 및 startup validation
|
||||
3. Alembic migration 신규 — 4개 스키마 변경 (IF NOT EXISTS 패턴)
|
||||
4. app/domain/models.py — AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가
|
||||
5. app/services/auth_service.py — service-token 발급 시 UUID v4 runtime_id 생성·저장
|
||||
6. app/api/auth_routes.py, app/cli/commands/service_tokens.py — 응답/출력에 runtime_id 포함
|
||||
7. app/api/health.py, app/schemas/ — HealthResponse 스키마 적용
|
||||
8. app/api/v1_assets.py — GET /v1/assets 구현
|
||||
9. app/api/v1_user_info.py — GET /v1/user_info 구현 (R-4: 404 반환)
|
||||
10. app/api/v1_ledger.py — POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
11. app/repositories/postgres.py — broker_credentials, ledger_trades, runtime_id 쿼리 추가
|
||||
12. app/main.py — /v1 라우터 등록
|
||||
13. 테스트 파일 작성 및 기존 테스트 확장
|
||||
|
||||
## Changed Files
|
||||
|
||||
- app/core/encryption.py (신규)
|
||||
- app/core/config.py (수정)
|
||||
- migrations/versions/20260502_0014_v1_api_schema.py (신규)
|
||||
- app/domain/models.py (수정)
|
||||
- app/services/auth_service.py (수정)
|
||||
- app/api/auth_routes.py (수정)
|
||||
- app/cli/commands/service_tokens.py (수정)
|
||||
- app/api/health.py (수정)
|
||||
- app/schemas/ (수정: HealthResponse 추가, v1 스키마 추가)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/repositories/postgres.py (수정)
|
||||
- app/main.py (수정)
|
||||
- tests/test_health.py (신규 또는 test_app.py 확장)
|
||||
- tests/test_auth.py (확장)
|
||||
- tests/test_v1_assets.py (신규)
|
||||
- tests/test_v1_user_info.py (신규)
|
||||
- tests/test_v1_ledger.py (신규)
|
||||
|
||||
## Execution Log
|
||||
|
||||
_(구현 진행 시 단계별 기록)_
|
||||
|
||||
1. [ ] app/core/encryption.py 신규 구현
|
||||
2. [ ] app/core/config.py CREDENTIAL_ENCRYPTION_KEY 추가 및 startup validation
|
||||
3. [ ] Alembic migration 파일 작성 및 로컬 upgrade head 성공 확인
|
||||
4. [ ] app/domain/models.py 모델 필드/클래스 추가
|
||||
5. [ ] auth_service.py runtime_id 생성·저장 구현
|
||||
6. [ ] auth_routes.py, service_tokens.py runtime_id 출력 추가
|
||||
7. [ ] app/api/health.py HealthResponse 스키마 적용
|
||||
8. [ ] app/api/v1_assets.py 구현
|
||||
9. [ ] app/api/v1_user_info.py 구현
|
||||
10. [ ] app/api/v1_ledger.py 구현
|
||||
11. [ ] app/repositories/postgres.py 쿼리 추가
|
||||
12. [ ] app/main.py /v1 라우터 등록
|
||||
13. [ ] 테스트 작성 및 전체 테스트 스위트 통과
|
||||
|
||||
## Validation
|
||||
|
||||
_(구현 완료 시 기록)_
|
||||
|
||||
- [ ] pytest 전체 통과
|
||||
- [ ] CREDENTIAL_ENCRYPTION_KEY 없이 기동 시 RuntimeError 확인
|
||||
- [ ] alembic upgrade head 성공 (로컬 Docker Compose 환경)
|
||||
- [ ] GET /health 응답에 service/version/checked_at 포함 확인
|
||||
- [ ] GET /v1/assets 200 응답 확인
|
||||
- [ ] GET /v1/user_info runtime_id 없는 토큰 → 404 확인
|
||||
- [ ] POST /v1/assets/{asset_id}/ledger/trades 중복 → 200+duplicate 확인
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
_(구현 전 상태 — 진행 중 업데이트)_
|
||||
|
||||
- R1: CREDENTIAL_ENCRYPTION_KEY 테스트 픽스처 처리 방식 확정 필요
|
||||
- R2: psycopg UniqueViolation import 경로 확인 필요 (psycopg v3 기준)
|
||||
@@ -1,67 +0,0 @@
|
||||
# BAT-001 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-001
|
||||
- Profile: standard
|
||||
- Created: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-004 | GET /health 응답 스키마 확장 | Interface | High |
|
||||
| REQ-005 | service-token 발급 시 runtime_id 동시 생성 | Interface | High |
|
||||
| REQ-006 | GET /v1/assets 엔드포인트 구현 | Interface | High |
|
||||
| REQ-007 | GET /v1/user_info 엔드포인트 구현 | Interface | High |
|
||||
| REQ-008 | POST /v1/assets/{asset_id}/ledger/trades 구현 | Interface | High |
|
||||
| REQ-009 | v1 API DB 스키마 migration 추가 | Migration | High |
|
||||
| REQ-010 | credential_payload Fernet 암호화 및 startup validation | Non-Functional | High |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- dcy-002: trex-basic Consumer-Driven Contract 반영
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- GET /health 응답 확장 (service, version, checked_at 필드 추가)
|
||||
- service-token 발급 시 runtime_id UUID v4 동시 생성 및 auth_tokens 저장
|
||||
- GET /v1/assets 엔드포인트 신규 구현 (portfolio → asset 뷰 변환)
|
||||
- GET /v1/user_info 엔드포인트 신규 구현 (runtime_id, trading_enabled, brokers[] 복호화 반환)
|
||||
- POST /v1/assets/{asset_id}/ledger/trades 엔드포인트 신규 구현 (idempotent 체결 기록)
|
||||
- Alembic migration: portfolios.market 컬럼, auth_tokens.runtime_id 컬럼, broker_credentials 테이블, ledger_trades 테이블 추가
|
||||
- Fernet 암복호화 유틸리티 신규 구현 및 startup validation
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs: docs/project-structure.md, docs/runtime-flows.md (design 완료 후 갱신)
|
||||
- Modules:
|
||||
- app/api/health.py (수정)
|
||||
- app/api/auth_routes.py (수정)
|
||||
- app/api/v1_assets.py (신규)
|
||||
- app/api/v1_user_info.py (신규)
|
||||
- app/api/v1_ledger.py (신규)
|
||||
- app/main.py (수정: /v1 라우터 등록)
|
||||
- app/cli/commands/service_tokens.py (수정)
|
||||
- app/core/config.py (수정: CREDENTIAL_ENCRYPTION_KEY 추가)
|
||||
- app/core/encryption.py (신규: Fernet 유틸리티)
|
||||
- app/domain/models.py (수정: AuthToken.runtime_id, BrokerCredential, LedgerTrade 추가)
|
||||
- app/services/auth_service.py (수정)
|
||||
- app/services/ (신규 서비스 메서드)
|
||||
- app/schemas/ (신규 v1 스키마)
|
||||
- app/repositories/postgres.py (수정: 신규 테이블 쿼리)
|
||||
- migrations/versions/ (신규 migration 파일)
|
||||
- Tests:
|
||||
- tests/test_app.py 또는 tests/test_health.py (신규/확장)
|
||||
- tests/test_auth.py (확장)
|
||||
- tests/test_v1_assets.py (신규)
|
||||
- tests/test_v1_user_info.py (신규)
|
||||
- tests/test_v1_ledger.py (신규)
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Design | [design.md](./design.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | re-verified |
|
||||
@@ -1,83 +0,0 @@
|
||||
# BAT-001 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-001
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-004: GET /health 응답 스키마 확장
|
||||
- REQ-005: service-token 발급 시 runtime_id 동시 생성
|
||||
- REQ-006: GET /v1/assets 엔드포인트 구현
|
||||
- REQ-007: GET /v1/user_info 엔드포인트 구현
|
||||
- REQ-008: POST /v1/assets/{asset_id}/ledger/trades 구현
|
||||
- REQ-009: v1 API DB 스키마 migration 추가
|
||||
- REQ-010: credential_payload Fernet 암호화 및 startup validation
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
구현 선행 조건과 의존성 방향을 기준으로 아래 순서로 진행한다.
|
||||
|
||||
### Out of Scope
|
||||
- REQ-001 (Testing — dcy-001 범위, 별도 batch 예정)
|
||||
- REQ-002 (Migration 정책 문서 — 기 반영됨)
|
||||
- REQ-003 (Documentation — dcy-001 runbook, 별도 처리)
|
||||
- REQ-011 (Documentation — 본 batch 구현 완료 후 별도 task)
|
||||
- broker_credentials 키 로테이션 절차
|
||||
- disabled_reason 동적 계산 (이번 반복은 고정값 "account_inactive")
|
||||
- GET /v1/assets 페이지네이션
|
||||
|
||||
### 1단계: 기반 레이어 (의존 없음)
|
||||
- REQ-010: app/core/encryption.py 신규 구현, app/core/config.py에 CREDENTIAL_ENCRYPTION_KEY startup validation 추가
|
||||
- REQ-009: Alembic migration 파일 작성 (portfolios.market, auth_tokens.runtime_id, broker_credentials, ledger_trades), app/domain/models.py 모델 필드/클래스 추가
|
||||
|
||||
### 2단계: 인증 확장 (REQ-009/010 완료 후)
|
||||
- REQ-005: app/cli/commands/service_tokens.py, app/api/auth_routes.py, app/services/auth_service.py에 runtime_id UUID v4 생성·저장 추가
|
||||
|
||||
### 3단계: v1 엔드포인트 (REQ-009/010/005 완료 후)
|
||||
- REQ-004: app/api/health.py 응답 스키마 확장 (service, version, checked_at 추가), HealthResponse 스키마 추가
|
||||
- REQ-006: app/api/v1_assets.py 신규, 서비스·스키마·레포지터리 메서드 추가, app/main.py /v1 라우터 등록
|
||||
- REQ-007: app/api/v1_user_info.py 신규, BrokerCredential 조회·복호화 서비스·스키마 추가
|
||||
- REQ-008: app/api/v1_ledger.py 신규, LedgerTrade 저장·중복 감지 서비스·스키마 추가
|
||||
|
||||
### 4단계: 테스트
|
||||
- 각 엔드포인트 단위 테스트 작성 및 기존 test_auth.py 확장
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- REQ-009 migration이 REQ-005, 006, 007, 008 구현의 선행 조건 (컬럼·테이블 존재 필요)
|
||||
- REQ-010 암호화 유틸리티가 REQ-007 credential_payload 복호화의 선행 조건
|
||||
- REQ-005 runtime_id 발급이 REQ-007 GET /v1/user_info runtime_id 반환의 선행 조건
|
||||
- app/main.py /v1 라우터 등록은 REQ-006/007/008 세 라우터를 한 번에 처리
|
||||
- External:
|
||||
- CREDENTIAL_ENCRYPTION_KEY 환경변수 (운영 환경 설정 필요)
|
||||
- Docker Compose local-test 환경에서 Alembic upgrade head 선행 필요
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason:
|
||||
- /v1 라우터 구조 및 app/main.py 등록 방식 결정 필요 (prefix, router 분리 또는 통합)
|
||||
- GET /v1/user_info의 R-4 처리 방식 결정 필요 (runtime_id 없는 구형 service-token: 404 또는 즉시 생성)
|
||||
- disabled_reason 고정값 방식 확정 필요 ("account_inactive" 등)
|
||||
- Fernet startup validation 위치 확정 필요 (FastAPI lifespan vs Settings 초기화)
|
||||
- broker_credentials 테이블 상세 컬럼 타입 확정 필요 (NUMERIC vs FLOAT, TIMESTAMPTZ 등)
|
||||
- runtime_id 포맷 확정 필요 (UUID v4 권장, 설계 단계 확정)
|
||||
|
||||
## Milestones
|
||||
|
||||
1. design.md confirmed — 모든 Design Gate 결정 사항 해소
|
||||
2. REQ-009 migration 구현 완료 및 로컬 DB upgrade head 성공
|
||||
3. REQ-010 Fernet 유틸리티 구현 완료 및 startup validation 동작 확인
|
||||
4. REQ-005 runtime_id 발급 구현 완료 및 단위 테스트 통과
|
||||
5. REQ-004/006/007/008 엔드포인트 구현 완료
|
||||
6. 전체 테스트 스위트 통과
|
||||
7. verification.md confirmed
|
||||
|
||||
## Risks
|
||||
|
||||
- R1: GET /v1/user_info R-4 처리(기존 runtime_id 없는 토큰) 미결 — 설계 단계에서 반드시 확정. 404 선택 시 기존 service-token 무효화 영향 범위 검토 필요.
|
||||
- R2: Fernet 키 없는 환경에서 테스트 실행 실패 가능 — 테스트 픽스처에서 환경변수 mock 처리 필요.
|
||||
- R3: broker_credentials 컬럼 타입(특히 numeric 정밀도)을 migration 작성 전 확정하지 않으면 후속 변경 migration이 늘어남.
|
||||
- R4: /v1 라우터 prefix 방식이 기존 /portfolios, /auth 등 라우터와 충돌하지 않는지 app/main.py 구조 확인 필요.
|
||||
@@ -1,361 +0,0 @@
|
||||
# BAT-001 Verification
|
||||
|
||||
- Status: re-verified
|
||||
- Batch: BAT-001
|
||||
- Last Updated (KST): 2026-05-11
|
||||
- Re-verification Trigger: CHG-20260503-01 (REQ-006 strategy_type 필드 추가), CHG-20260511-02 (REQ-006 owner scope 회귀 수정)
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-004: GET /health 응답 스키마 확장
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | status/service/version/checked_at 필드 모두 존재 | `tests/test_v1.py::test_health_returns_full_response` — 4개 필드 전체 assert | PASS |
|
||||
| AC2 | status 허용값 ok/degraded/failed | test에서 "ok" 값만 검증. "degraded"/"failed" 경로는 이번 반복 OOS로 REQ Notes에 명시 | PARTIAL (OOS 인정) |
|
||||
| AC3 | checked_at ISO 8601 형식 | `test_v1.py::test_health_returns_full_response` — `"+09:00" in body["checked_at"]` assert | PASS |
|
||||
| AC4 | service 고정값 "portfolio-hub" | `test_v1.py` — `body["service"] == "portfolio-hub"` | PASS |
|
||||
| AC5 | 비인증 접근 200 | `test_v1.py::test_health_returns_full_response` — 인증 헤더 없이 GET /health 호출, 200 응답 | PASS |
|
||||
|
||||
**REQ-004 판정: PASS** (AC2는 이번 반복 OOS — 릴리스 블로커 아님)
|
||||
|
||||
---
|
||||
|
||||
### REQ-005: service-token 발급 시 runtime_id 동시 생성
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | CLI 출력에 token + runtime_id | `tests/test_cli.py::test_cli_service_token_issue_supports_json` PASS | PASS |
|
||||
| AC2 | API 엔드포인트 응답에 token + runtime_id | `tests/test_v1.py::test_auth_issue_service_token_returns_runtime_id` — POST /auth/service-tokens → 201 + `runtime_id` PASS | PASS |
|
||||
| AC3 | GET /v1/user_info 응답 runtime_id 일치 | `tests/test_v1.py::test_v1_user_info_returns_user` — `body["runtime_id"] == "rt-abc"` PASS | PASS |
|
||||
| AC4 | 두 번 발급 시 다른 runtime_id | `auth_service.py`에서 매 발급마다 `str(uuid.uuid4())` 생성 — 코드 검토 PASS | PASS |
|
||||
| AC5 | auth_tokens 테이블 runtime_id 저장 | `postgres.py::create_auth_token` INSERT에 runtime_id 포함 — 코드 검토 PASS. DB 환경 실행 미확인(PARTIAL) | PARTIAL (DB 미확인) |
|
||||
|
||||
**REQ-005 판정: PASS** (AC5 DB 미확인은 REQ-009와 동일 제약으로 릴리스 블로커 아님)
|
||||
|
||||
---
|
||||
|
||||
### REQ-006: GET /v1/assets 엔드포인트
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | service-token 인증 → 200 + 해당 사용자 소유 portfolio 목록만 반환 | `tests/test_v1.py::test_v1_assets_returns_asset_list` — 200 응답 PASS, `mock_repo.list_portfolios_with_strategies.assert_called_once_with(owner_user_id="user-1", include_disabled=False)`로 owner scope 전달 검증 PASS | PASS |
|
||||
| AC2 | ?status=active 쿼리 파라미터 필터 | `tests/test_v1.py::test_v1_assets_status_filter` — ?status=active 필터 동작 확인 PASS | PASS |
|
||||
| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_type, strategy_params, updated_at | `test_v1_assets_returns_asset_list` — 필드명 assert PASS. `test_v1_assets_strategy_params` — strategy_type=="strategy" PASS. `test_v1_assets_returns_asset_list` — strategy_type is None PASS | PASS |
|
||||
| AC4 | strategy_params → StrategySet.config 파생 JSON | `tests/test_v1.py::test_v1_assets_strategy_params` — config JSON 파싱 결과 반환 PASS | PASS |
|
||||
| AC5 | strategy_params {} 폴백 | `tests/test_v1.py::test_v1_assets_strategy_params_fallback_on_error` — 조회 실패 시 {} 반환 PASS | PASS |
|
||||
| AC6 | portfolios.market NULL → 응답 null | `AssetItem.market: str | None = None`, `Portfolio.market: str | None = None` — NULL 전파 구조 검토 PASS | PASS |
|
||||
| AC7 | 미인증 401 | `tests/test_v1.py::test_v1_assets_requires_service_token` PASS | PASS |
|
||||
| AC8 | active_strategy_id → strategy_type = StrategySet.name | `tests/test_v1.py::test_v1_assets_strategy_params` — `strategy_type == "strategy"` assert PASS | PASS |
|
||||
| AC9 | active_strategy_id 없거나 조회 실패 → strategy_type = null | `tests/test_v1.py::test_v1_assets_returns_asset_list` — `strategy_type is None` PASS. `test_v1_assets_strategy_params_fallback_on_error` — `strategy_type is None` PASS | PASS |
|
||||
|
||||
**REQ-006 판정: PASS** (CHG-20260503-01 strategy_type 추가, CHG-20260511-02 owner scope 회귀 수정 재검증 완료)
|
||||
|
||||
---
|
||||
|
||||
### REQ-007: GET /v1/user_info 엔드포인트
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | 응답에 runtime_id, trading_enabled, brokers[] 포함 | `tests/test_v1.py::test_v1_user_info_returns_user` — 3개 필드 모두 assert PASS | PASS |
|
||||
| AC2 | environment 필드 없음 | `UserInfoResponse` 스키마 검토 — environment 필드 없음 PASS | PASS |
|
||||
| AC3 | is_active=true → trading_enabled=true | `test_v1_user_info_returns_user` — `body["trading_enabled"] is True` PASS | PASS |
|
||||
| AC4 | is_active=false → trading_enabled=false, disabled_reason 포함 | `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — `trading_enabled is False`, `disabled_reason == "account_inactive"` PASS | PASS |
|
||||
| AC5 | brokers[]에 credential_payload 복호화 JSON | `tests/test_v1.py::test_v1_user_info_returns_decrypted_broker_credentials` — Fernet 복호화 payload 반환 PASS | PASS |
|
||||
| AC6 | runtime_id 없는 구형 토큰 → HTTP 404 | `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — 404 + detail PASS | PASS |
|
||||
| AC7 | 미인증 401 | `_require_service_token` 의존성 + `test_v1_assets_requires_service_token` 패턴으로 구조 검토 PASS | PASS |
|
||||
|
||||
**REQ-007 판정: PASS**
|
||||
|
||||
---
|
||||
|
||||
### REQ-008: POST /v1/assets/{asset_id}/ledger/trades
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | 신규 체결 → HTTP 201 + ledger_entry_id | `tests/test_v1.py::test_v1_ledger_trade_created` — 201 + `status="new"` PASS | PASS |
|
||||
| AC2 | 동일 payload 재전송 → HTTP 200 + status="duplicate" | `tests/test_v1.py::test_v1_ledger_trade_idempotent_same_payload` — 200 + `status="duplicate"` PASS | PASS |
|
||||
| AC3 | 동일 키 다른 payload → HTTP 409 | `tests/test_v1.py::test_v1_ledger_trade_conflict_different_payload` — 409 PASS | PASS |
|
||||
| AC4 | 존재하지 않는 asset_id → 404 | `tests/test_v1.py::test_v1_ledger_trade_asset_not_found` — 404 PASS | PASS |
|
||||
| AC5 | 미인증 401 | `_require_service_token` 의존성 구조 검토 PASS | PASS |
|
||||
|
||||
**REQ-008 판정: PASS**
|
||||
|
||||
---
|
||||
|
||||
### REQ-009: v1 API DB 스키마 migration
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | portfolios.market 컬럼 존재 | `20260502_0014_v1_api_schema.py` — IF NOT EXISTS 패턴 구현. 코드 검토 PASS. DB 환경 미확인 | PARTIAL |
|
||||
| AC2 | auth_tokens.runtime_id 컬럼 존재 | 동일 migration — UNIQUE INDEX 포함. 코드 검토 PASS | PARTIAL |
|
||||
| AC3 | broker_credentials 테이블 존재 | 동일 migration — PK(user_id, broker). 코드 검토 PASS | PARTIAL |
|
||||
| AC4 | ledger_trades 테이블 존재 | 동일 migration — BIGSERIAL PK. 코드 검토 PASS | PARTIAL |
|
||||
| AC5 | (asset_id, order_id, execution_id) UniqueViolation | `uq_ledger_trades_idempotency` UniqueConstraint 정의 확인. DB 실행 미확인 | PARTIAL |
|
||||
| AC6 | 재실행 시 에러 없음 | IF NOT EXISTS + `_table_exists`/`_column_exists` 패턴. 코드 검토 PASS | PARTIAL |
|
||||
| AC7 | 기존 행 market NULL 유지 | ADD COLUMN NULLABLE, 기존 행 UPDATE 없음. 코드 검토 PASS | PARTIAL |
|
||||
|
||||
**REQ-009 판정: PARTIAL** — 코드 전체 PASS. 실제 DB 환경(Docker Compose) 실행 검증 미수행. 코드 전용 환경에서는 블로커 아님.
|
||||
|
||||
---
|
||||
|
||||
### REQ-010: credential_payload Fernet 암호화 및 startup validation
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | KEY 있음 → 정상 기동 | `tests/test_v1.py::test_startup_ok_when_encryption_key_present` PASS | PASS |
|
||||
| AC2 | KEY 없음 → 기동 실패 + 에러 메시지 | `tests/test_v1.py::test_startup_fails_when_encryption_key_missing` — db_enabled=True + KEY 없음 → RuntimeError("CREDENTIAL_ENCRYPTION_KEY") PASS | PASS |
|
||||
| AC3 | DB에 ciphertext 저장 | `tests/test_v1.py::test_encrypt_decrypt_roundtrip` — encrypt→decrypt roundtrip PASS. 저장 경로는 `upsert_broker_credential` 메서드에서 암호화된 payload 직접 수신 (호출자 책임). DB 환경 실행 미확인 | PARTIAL |
|
||||
| AC4 | GET /v1/user_info 복호화 JSON 반환 | `tests/test_v1.py::test_v1_user_info_returns_decrypted_broker_credentials` PASS | PASS |
|
||||
| AC5 | 잘못된 키 → InvalidToken 예외 처리 | `encryption.py`에서 `InvalidToken` re-export. `v1_user_info.py`에서 except 처리 후 `credential_payload: null` 반환 — 코드 검토 PASS | PASS |
|
||||
|
||||
**REQ-010 판정: PASS** (AC3 DB 미확인은 REQ-009와 동일 제약)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Impact & Baseline Docs Sync
|
||||
|
||||
- `docs/project-structure.md` — 갱신 완료: `app/core/encryption.py`, v1 라우터 모듈, v1 스키마 반영
|
||||
- `docs/runtime-flows.md` — 갱신 완료: v1 API 요청 흐름 5개, startup 검증 흐름 추가
|
||||
|
||||
**판정: PASS**
|
||||
|
||||
---
|
||||
|
||||
## Evidence
|
||||
|
||||
### Automated Tests (pytest)
|
||||
|
||||
- `tests/test_v1.py -k 'v1_assets'` — 6 passed, 13 deselected (2026-05-11 targeted rerun)
|
||||
- `test_v1_assets_returns_asset_list`에 owner scope 전달 assert 추가
|
||||
- `tests/test_v1.py` — 19개 전부 PASS
|
||||
- `test_health_returns_full_response`, `test_auth_issue_service_token_returns_runtime_id`
|
||||
- `test_v1_assets_requires_service_token`, `test_v1_assets_rejects_env_token`, `test_v1_assets_returns_asset_list`
|
||||
- `test_v1_assets_status_filter`, `test_v1_assets_strategy_params`(`strategy_type` assert 포함), `test_v1_assets_strategy_params_fallback_on_error`(`strategy_type is None` assert 포함)
|
||||
- `test_v1_user_info_404_when_no_runtime_id`, `test_v1_user_info_returns_user`, `test_v1_user_info_inactive_user_has_disabled_reason`
|
||||
- `test_v1_user_info_returns_decrypted_broker_credentials`
|
||||
- `test_v1_ledger_trade_created`, `test_v1_ledger_trade_idempotent_same_payload`, `test_v1_ledger_trade_conflict_different_payload`, `test_v1_ledger_trade_asset_not_found`
|
||||
- `test_startup_fails_when_encryption_key_missing`, `test_startup_ok_when_encryption_key_present`, `test_encrypt_decrypt_roundtrip`
|
||||
- `tests/test_app.py` — PASS (startup bootstrap 포함)
|
||||
- `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py` — PASS
|
||||
- `tests/test_portfolio_comments.py`, `tests/test_portfolio_ownership.py`, `tests/test_portfolio_tags.py`, `tests/test_users.py` — PASS
|
||||
- **전체 합계: 115 passed (2026-05-03, CHG-20260503-01 재검증 포함)**
|
||||
|
||||
### Code Review
|
||||
|
||||
- `app/core/encryption.py`: Fernet 암복호화 유틸리티 정상 구현
|
||||
- `app/api/auth_routes.py`: `POST /auth/service-tokens` 엔드포인트 추가
|
||||
- `app/api/v1_assets.py`: status 필터, strategy_params 조회, 새 필드명 매핑
|
||||
- `app/api/v1_user_info.py`: trading_enabled, brokers[], Fernet 복호화, Depends(get_settings)
|
||||
- `app/api/v1_ledger.py`: 200/201/404/409 분기, _payloads_match 비교
|
||||
- `app/repositories/postgres.py`: list_broker_credentials 추가
|
||||
- `app/main.py`: startup validation (CREDENTIAL_ENCRYPTION_KEY)
|
||||
|
||||
### DB 환경 실행
|
||||
|
||||
- 미수행 (Docker Compose 환경 미기동). 코드 전용 환경에서는 블로커 아님.
|
||||
|
||||
---
|
||||
|
||||
## Blocking Issues
|
||||
|
||||
없음.
|
||||
|
||||
## Non-blocking Notes
|
||||
|
||||
- REQ-009 AC1~AC7: 코드 검토 PASS, 실제 DB 환경 실행 미확인. 다음 릴리스 전 live DB 검증 권장.
|
||||
- REQ-004 AC2: "degraded"/"failed" 상태 경로는 이번 반복 OOS.
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
- **Ready for Release: true**
|
||||
|
||||
|
||||
## Historical Snapshot (2026-05-02 Initial Verification)
|
||||
|
||||
- 아래 섹션은 BAT-001의 초기 검증 기록이다. 현재 유효한 판정은 위 `Acceptance Mapping`, `Evidence`, `Result` 섹션을 따른다.
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-004: GET /health 응답 스키마 확장
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | status/service/version/checked_at 필드 모두 존재 | `tests/test_v1.py::test_health_returns_full_response` — 4개 필드 전체 assert | PASS |
|
||||
| AC2 | status 허용값 ok/degraded/failed | test에서 "ok" 값만 검증. "degraded"/"failed" 경로 코드 미구현(Notes에 이번 반복 Out of Scope 명시) — 스키마에 허용값으로만 선언 | PARTIAL (OOS 인정) |
|
||||
| AC3 | checked_at ISO 8601 형식 | `test_v1.py::test_health_returns_full_response` — `"+09:00" in body["checked_at"]` assert | PASS |
|
||||
| AC4 | service 고정값 "portfolio-hub" | `test_v1.py` — `body["service"] == "portfolio-hub"` | PASS |
|
||||
| AC5 | 비인증 접근 200 | `test_v1.py::test_health_returns_full_response` — 인증 헤더 없이 GET /health 호출, 200 응답 | PASS |
|
||||
|
||||
**REQ-004 판정: PASS** (AC2는 REQ Notes에서 이번 반복 OOS로 명시 — 릴리스 블로커 아님)
|
||||
|
||||
---
|
||||
|
||||
### REQ-005: service-token 발급 시 runtime_id 동시 생성
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | CLI 출력에 token + runtime_id | `app/cli/commands/service_tokens.py` — `ServiceTokenIssueCliResponse(runtime_id=issued.auth_token.runtime_id, ...)` 구현, `tests/test_cli.py::test_cli_service_token_issue_supports_json` PASS (FakeRepo에 runtime_id 반영) | PASS |
|
||||
| AC2 | API 엔드포인트 응답에 token + runtime_id | service-token 발급 API 엔드포인트 미구현 (CLI 전용으로 구현). `app/api/auth_routes.py` 변경 없음. REQ-005 AC2는 "POST /auth/tokens 또는 동등 경로"를 명시함 | **BLOCKER** |
|
||||
| AC3 | GET /v1/user_info 응답 runtime_id 일치 | `tests/test_v1.py::test_v1_user_info_returns_user` — `body["runtime_id"] == "rt-abc"` | PASS |
|
||||
| AC4 | 두 번 발급 시 다른 runtime_id | `auth_service.py`에서 매 발급마다 `str(uuid.uuid4())` 생성 — 코드 검토 통과. 전용 테스트 없음 | PARTIAL |
|
||||
| AC5 | auth_tokens 테이블 runtime_id 저장 | `postgres.py::create_auth_token` INSERT에 runtime_id 포함, `_SELECT_AUTH_TOKEN`에 runtime_id 컬럼 추가 — 코드 검토 통과. DB 환경 실행 미확인 | PARTIAL (DB 미확인) |
|
||||
|
||||
**REQ-005 판정: BLOCKED** — AC2 서비스 토큰 발급 API 엔드포인트 누락
|
||||
|
||||
---
|
||||
|
||||
### REQ-006: GET /v1/assets 엔드포인트
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | service-token 인증 → 200 + portfolio 목록 | `tests/test_v1.py::test_v1_assets_returns_asset_list` PASS | PASS |
|
||||
| AC2 | ?status=active 쿼리 파라미터 필터 | `v1_assets.py`에 `status` 쿼리 파라미터 미구현. `list_portfolios_with_strategies(include_disabled=False)`만 호출 — active 필터 없음 | **BLOCKER** |
|
||||
| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at | `AssetItem` 스키마에 `cash_balance`(≠cash), `asset_quantity`(≠quantity), `broker`(≠broker_type), `active_strategy_id`(≠strategy_id) 사용. `strategy_params` 필드 아예 없음 — trex-basic contract 필드명 불일치 | **BLOCKER** |
|
||||
| AC4 | strategy_params → StrategySet.config 파생 JSON | `AssetItem`에 strategy_params 필드 없음. v1_assets.py에서 StrategySet 조회 로직 없음 | **BLOCKER** |
|
||||
| AC5 | strategy_params {} 폴백 | strategy_params 미구현으로 폴백 불가 | **BLOCKER** |
|
||||
| AC6 | portfolios.market NULL → 응답 null | `AssetItem.market: str | None = None`, `Portfolio.market: str | None = None` — NULL 전파 구조 PASS. NULL 케이스 전용 테스트 없음 | PARTIAL |
|
||||
| AC7 | 미인증 401 | `tests/test_v1.py::test_v1_assets_requires_service_token` — 401 반환 PASS | PASS |
|
||||
|
||||
**REQ-006 판정: BLOCKED** — AC2(status 필터), AC3(필드명 불일치), AC4/AC5(strategy_params 미구현)
|
||||
|
||||
---
|
||||
|
||||
### REQ-007: GET /v1/user_info 엔드포인트
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | 응답에 runtime_id, trading_enabled, brokers[] 포함 | `UserInfoResponse`에 `runtime_id` ✓, `is_active` (trading_enabled 아님) ✓, `brokers[]` 없음. Contract 필드명 `trading_enabled` 사용 안 함 | **BLOCKER** |
|
||||
| AC2 | environment 필드 없음 | `UserInfoResponse`에 environment 필드 없음 | PASS |
|
||||
| AC3 | is_active=true → trading_enabled=true | `trading_enabled` 필드 미구현 | **BLOCKER** |
|
||||
| AC4 | is_active=false → trading_enabled=false, disabled_reason 포함 | `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — `disabled_reason == "account_inactive"` PASS. `trading_enabled` 미구현 | **BLOCKER** |
|
||||
| AC5 | brokers[]에 credential_payload 복호화 JSON | `UserInfoResponse`에 brokers[] 없음. v1_user_info.py에서 broker_credentials 조회 및 복호화 없음 | **BLOCKER** |
|
||||
| AC6 | runtime_id 없는 구형 토큰 → HTTP 404 | `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — 404 + "runtime_id not assigned to this token" PASS | PASS |
|
||||
| AC7 | 미인증 401 | `_require_service_token` 의존성에서 강제. 명시적 테스트 없으나 구조 검토 통과 | PARTIAL |
|
||||
|
||||
**REQ-007 판정: BLOCKED** — trading_enabled 필드명, brokers[] 배열 전체 미구현
|
||||
|
||||
---
|
||||
|
||||
### REQ-008: POST /v1/assets/{asset_id}/ledger/trades
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | 신규 체결 → HTTP 201 + ledger_entry_id | `tests/test_v1.py::test_v1_ledger_trade_created` — 201 + created=True PASS | PASS |
|
||||
| AC2 | 동일 payload 재전송 → HTTP 200 + status="duplicate" | 현재 구현은 중복 시에도 **HTTP 201** 반환, 응답 필드가 `status="duplicate"`가 아닌 `created=false` 사용. `test_v1_ledger_trade_idempotent`는 201을 assert | **BLOCKER** |
|
||||
| AC3 | 동일 키 다른 payload → HTTP 409 | `insert_ledger_trade`에서 UniqueViolation 시 기존 행 반환만 수행, payload 비교 없음. 409 응답 미구현 | **BLOCKER** |
|
||||
| AC4 | 존재하지 않는 asset_id → 404 | `v1_ledger.py`에서 asset_id 존재 여부 확인 로직 없음 | **BLOCKER** |
|
||||
| AC5 | 미인증 401 | `_require_service_token` 의존성에서 강제. 명시적 테스트 없으나 구조 검토 통과 | PARTIAL |
|
||||
|
||||
**REQ-008 판정: BLOCKED** — AC2(HTTP 상태 코드 및 응답 필드), AC3(409 미구현), AC4(asset_id 존재 확인 없음)
|
||||
|
||||
---
|
||||
|
||||
### REQ-009: v1 API DB 스키마 migration
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | portfolios.market 컬럼 존재 | `20260502_0014_v1_api_schema.py` — `add_column("portfolios", market TEXT NULLABLE)` IF NOT EXISTS 패턴 구현 — 코드 검토 PASS. 실제 DB 실행 미확인 | PARTIAL (DB 미확인) |
|
||||
| AC2 | auth_tokens.runtime_id 컬럼 존재 | 동일 migration — `add_column("auth_tokens", runtime_id TEXT NULLABLE)` + UNIQUE INDEX — 코드 검토 PASS | PARTIAL (DB 미확인) |
|
||||
| AC3 | broker_credentials 테이블 존재 | 동일 migration — `create_table("broker_credentials", ...)` PK(user_id, broker) — 코드 검토 PASS | PARTIAL (DB 미확인) |
|
||||
| AC4 | ledger_trades 테이블 존재 | 동일 migration — `create_table("ledger_trades", ...)` BIGSERIAL PK — 코드 검토 PASS | PARTIAL (DB 미확인) |
|
||||
| AC5 | (asset_id, order_id, execution_id) UniqueViolation | `uq_ledger_trades_idempotency` UniqueConstraint 정의 확인. DB 실행 미확인 | PARTIAL (DB 미확인) |
|
||||
| AC6 | 재실행 시 에러 없음 | IF NOT EXISTS 패턴 + `_table_exists`/`_column_exists` 검사 구현 — 코드 검토 PASS | PARTIAL (DB 미확인) |
|
||||
| AC7 | 기존 행 market NULL 유지 | `ADD COLUMN ... NULLABLE` 추가만 수행, 기존 행 UPDATE 없음 — 코드 검토 PASS | PARTIAL (DB 미확인) |
|
||||
|
||||
**REQ-009 판정: PARTIAL** — 코드 검토 전체 PASS. 실제 DB 환경(Docker Compose) 실행 검증 필요. 코드 전용 환경에서는 블로커 아님으로 취급하나, live DB 검증 항목을 Blocking Issues에 포함.
|
||||
|
||||
---
|
||||
|
||||
### REQ-010: credential_payload Fernet 암호화 및 startup validation
|
||||
|
||||
| # | Acceptance Criterion | Evidence | Result |
|
||||
|---|---|---|---|
|
||||
| AC1 | KEY 있음 → 정상 기동 | `app/core/encryption.py` 신규 구현, `Settings.credential_encryption_key` 필드 추가. KEY 있을 때 정상 동작 — 코드 검토 PASS | PASS |
|
||||
| AC2 | KEY 없음 → 기동 실패 + 에러 메시지 | 구현에서 lazy validation으로 변경됨: `Settings` 생성 시 에러 없이 통과, `require_credential_encryption_key()` 호출 시에만 RuntimeError 발생. **startup 실패 동작이 AC2와 불일치** | **BLOCKER** |
|
||||
| AC3 | DB에 ciphertext 저장 | `upsert_broker_credential` 메서드 존재. 그러나 API에서 plaintext → encrypt → DB 저장 경로 미구현 (암호화 적용은 호출자 책임으로 처리됨). 전용 테스트 없음 | **BLOCKER** |
|
||||
| AC4 | GET /v1/user_info 복호화 JSON 반환 | `UserInfoResponse`에 brokers[] 없음 (REQ-007 BLOCKED에 포함) — 동일 blocker | **BLOCKER** |
|
||||
| AC5 | 잘못된 키 → InvalidToken 예외 | `encryption.py`에서 `cryptography.fernet.InvalidToken` re-export. 전용 테스트 없음 | PARTIAL |
|
||||
|
||||
**REQ-010 판정: BLOCKED** — AC2(startup 실패 동작 불일치), AC3(암호화 저장 경로 미연결), AC4(REQ-007 blocker와 연동)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Impact & Baseline Docs Sync
|
||||
|
||||
`design.md` Changed Areas에 아래 baseline 문서 갱신이 명시됨:
|
||||
|
||||
- `docs/project-structure.md` — app/core/encryption.py, app/api/v1_*.py 신규 모듈 반영
|
||||
- `docs/runtime-flows.md` — v1 API 요청 흐름, runtime_id 발급 흐름 추가
|
||||
|
||||
**현재 상태:** `implementation.md` Changed Files에 baseline 문서 갱신 없음. design의 Architecture Impact가 `none`이 아니므로 baseline 문서 동기화 evidence가 필요함.
|
||||
|
||||
**판정: BLOCKER** — docs/project-structure.md, docs/runtime-flows.md 미갱신
|
||||
|
||||
---
|
||||
|
||||
## Evidence
|
||||
|
||||
### Automated Tests (pytest)
|
||||
|
||||
- `tests/test_app.py::test_healthcheck` — PASS (health 필드 검증 포함)
|
||||
- `tests/test_v1.py::test_health_returns_full_response` — PASS (4개 필드 + KST checked_at)
|
||||
- `tests/test_v1.py::test_v1_assets_requires_service_token` — PASS
|
||||
- `tests/test_v1.py::test_v1_assets_rejects_env_token` — PASS
|
||||
- `tests/test_v1.py::test_v1_assets_returns_asset_list` — PASS (asset_id, market 기본 필드)
|
||||
- `tests/test_v1.py::test_v1_user_info_404_when_no_runtime_id` — PASS
|
||||
- `tests/test_v1.py::test_v1_user_info_returns_user` — PASS (runtime_id, disabled_reason=None)
|
||||
- `tests/test_v1.py::test_v1_user_info_inactive_user_has_disabled_reason` — PASS
|
||||
- `tests/test_v1.py::test_v1_ledger_trade_created` — PASS
|
||||
- `tests/test_v1.py::test_v1_ledger_trade_idempotent` — PASS
|
||||
- `tests/test_auth.py` 전체 — PASS (runtime_id 파라미터 추가 후)
|
||||
- `tests/test_cli.py::test_cli_service_token_issue_supports_json` — PASS
|
||||
- 전체 합계: 88 passed (2026-05-02)
|
||||
|
||||
### Code Review
|
||||
|
||||
- `app/core/encryption.py`: Fernet 암복호화 유틸리티 정상 구현
|
||||
- `app/core/config.py`: `credential_encryption_key` 필드 추가, lazy validation 전환
|
||||
- `app/core/auth.py`: `AuthContext.runtime_id`, `TokenPrincipal.runtime_id` 추가
|
||||
- `app/domain/models.py`: `AuthToken.runtime_id`, `Portfolio.market`, `BrokerCredential`, `LedgerTrade` 추가
|
||||
- `app/services/auth_service.py`: `uuid.uuid4()` runtime_id 생성·전달
|
||||
- `migrations/versions/20260502_0014_v1_api_schema.py`: IF NOT EXISTS 패턴 정상
|
||||
|
||||
### DB 환경 실행
|
||||
|
||||
- 미수행 (Docker Compose 환경 미기동)
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
- **Ready for Release: false**
|
||||
|
||||
### Blocking Issues
|
||||
|
||||
| # | REQ | Issue | 필요 조치 |
|
||||
|---|---|---|---|
|
||||
| B-1 | REQ-005 AC2 | service-token 발급 API 엔드포인트(POST /auth/service-tokens 또는 동등 경로) 미구현. CLI 전용으로 구현됨 | API 엔드포인트 추가 또는 REQ-005 AC2 범위 조정(change-req) |
|
||||
| B-2 | REQ-006 AC2 | ?status=active 쿼리 파라미터 필터 미구현 | v1_assets.py에 status 쿼리 파라미터 추가 |
|
||||
| B-3 | REQ-006 AC3 | 응답 필드명 불일치: cash_balance(≠cash), asset_quantity(≠quantity), broker(≠broker_type), active_strategy_id(≠strategy_id) | 계약 필드명으로 응답 스키마 수정 또는 alias 추가 |
|
||||
| B-4 | REQ-006 AC4/AC5 | strategy_params 필드 미구현. StrategySet.config 조회 및 {} 폴백 로직 없음 | v1_assets.py에 strategy_set 조회 및 strategy_params 파생 로직 추가 |
|
||||
| B-5 | REQ-007 AC1/AC3/AC4 | trading_enabled 필드 미구현(is_active로 대체됨). 계약 필드명 불일치 | UserInfoResponse에 trading_enabled 필드 추가 |
|
||||
| B-6 | REQ-007 AC5 | brokers[] 배열 전체 미구현. broker_credentials 조회 + Fernet 복호화 경로 없음 | v1_user_info.py에 broker_credentials 조회 + 복호화 + 응답 포함 구현 |
|
||||
| B-7 | REQ-008 AC2 | 중복 수신 시 HTTP 201 반환(AC는 HTTP 200 요구). 응답 필드 created(bool) vs status="duplicate" 불일치 | 중복 시 HTTP 200 반환, 응답 스키마에 status="duplicate" 필드 추가 |
|
||||
| B-8 | REQ-008 AC3 | 동일 키 다른 payload → HTTP 409 미구현 | insert_ledger_trade에 payload 비교 로직 및 409 응답 추가 |
|
||||
| B-9 | REQ-008 AC4 | 존재하지 않는 asset_id → HTTP 404 미구현 | v1_ledger.py에 asset_id(portfolio_id) 존재 여부 확인 후 404 반환 추가 |
|
||||
| B-10 | REQ-010 AC2 | startup validation 동작 불일치: Settings 생성 시 에러 없음(lazy), AC는 기동 실패 요구 | lifespan 또는 애플리케이션 시작 시 require_credential_encryption_key() 호출 추가 |
|
||||
| B-11 | REQ-010 AC3 | broker_credentials 저장 시 암호화 미적용. API 레이어에서 encrypt_payload 호출 없음 | upsert_broker_credential 호출부 또는 API 레이어에서 암호화 적용 |
|
||||
| B-12 | Baseline | docs/project-structure.md, docs/runtime-flows.md 미갱신. design.md Architecture Impact에 갱신 필요로 명시됨 | 두 baseline 문서에 신규 모듈 및 v1 API 흐름 반영 |
|
||||
|
||||
### Non-Blocking Observations
|
||||
|
||||
| # | REQ | Observation |
|
||||
|---|---|---|
|
||||
| N-1 | REQ-009 | migration 파일 코드 검토 PASS. DB 환경(Docker Compose) 실행 미검증 — 배포 전 반드시 확인 필요 |
|
||||
| N-2 | REQ-004 AC2 | degraded/failed 상태 경로 미구현 — REQ Notes에서 이번 반복 OOS로 명시되어 릴리스 블로커 아님 |
|
||||
| N-3 | REQ-005 AC4 | 두 번 발급 시 다른 runtime_id — 코드 검토 통과. 전용 테스트 없음 |
|
||||
| N-4 | REQ-010 AC5 | InvalidToken 경로 전용 테스트 없음 |
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# BAT-002 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-002
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-012 | service-token rotate CLI 명령 구현 | Interface | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- minor-change fast path (Discovery 없음)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- `pfhub service-token rotate <name> [--force]` CLI 명령 신규 추가
|
||||
- `auth_tokens` 행의 `token_hash`, `token_prefix`, `runtime_id` in-place UPDATE
|
||||
- 메타데이터(`id`, `permission`, `expires_at` 등) 유지
|
||||
- DB 스키마 변경 없음
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs: 없음 (구조/인터페이스/런타임 흐름 변경 없음)
|
||||
- Modules:
|
||||
- `app/repositories/portfolio_repository.py` (Protocol stub 추가)
|
||||
- `app/repositories/postgres.py` (UPDATE 메서드 추가)
|
||||
- `app/services/auth_service.py` (rotate_service_token 메서드 추가)
|
||||
- `app/cli/commands/service_tokens.py` (rotate 명령 추가)
|
||||
- Tests:
|
||||
- `tests/test_cli.py` (rotate 성공/없는이름/revoked 3개 케이스)
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Implementation | implementation.md | pending |
|
||||
| Verification | [verification.md](./verification.md) | verified |
|
||||
@@ -1,59 +0,0 @@
|
||||
# BAT-002 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-002
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-012: service-token rotate CLI 명령 구현
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
### Out of Scope
|
||||
- `expires_at` 변경 옵션 (이번 rotate는 secret 교체만)
|
||||
- HTTP API rotate 엔드포인트 (CLI 전용)
|
||||
|
||||
### 구현 순서
|
||||
|
||||
#### 1단계: Repository 레이어
|
||||
- `app/repositories/portfolio_repository.py` — `rotate_service_token(token_id, token_hash, token_prefix, runtime_id) -> AuthToken` Protocol stub 추가
|
||||
- `app/repositories/postgres.py` — `rotate_service_token` UPDATE 구현
|
||||
```sql
|
||||
UPDATE auth_tokens
|
||||
SET token_hash = %s, token_prefix = %s, runtime_id = %s
|
||||
WHERE id = %s AND revoked_at IS NULL
|
||||
RETURNING id, user_id, name, token_prefix, ...
|
||||
```
|
||||
- 대상 행이 없거나 revoked이면 ValueError 반환
|
||||
|
||||
#### 2단계: Service 레이어
|
||||
- `app/services/auth_service.py` — `rotate_service_token(name) -> IssuedServiceToken` 추가
|
||||
- `get_service_token_by_name(name)` 조회 → 없으면 ValueError
|
||||
- revoked 상태이면 ValueError
|
||||
- `expires_at is not None`이면 ValueError ("만료일이 설정된 토큰은 rotate할 수 없습니다")
|
||||
- `generate_token()` + `hash_token()` + `token_prefix()` + `uuid.uuid4()` 로 새 값 생성
|
||||
- `repository.rotate_service_token(...)` 호출
|
||||
- `IssuedServiceToken(token=raw_token, auth_token=updated_token)` 반환
|
||||
|
||||
#### 3단계: CLI 명령
|
||||
- `app/cli/commands/service_tokens.py` — `rotate` 명령 추가
|
||||
- `--force` 없으면 확인 프롬프트
|
||||
- issue 명령과 동일한 1회 원문 출력 패턴 적용
|
||||
|
||||
#### 4단계: 테스트
|
||||
- `tests/test_cli.py` — 인메모리 repo에 `rotate_service_token` stub 추가 및 케이스 추가
|
||||
- rotate 성공 (no-expiry 토큰, 새 token 출력 확인)
|
||||
- 없는 이름 → 오류
|
||||
- revoked 토큰 → 오류
|
||||
- expires_at가 설정된 토큰 → 오류
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal: 없음 (기존 `get_service_token_by_name`, `generate_token`, `hash_token` 활용)
|
||||
- External: 없음 (DB 스키마 변경 없음)
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: DB 스키마 변경 없음. 기존 패턴(issue/revoke) 동일하게 적용. 결정 사항 없음.
|
||||
@@ -1,90 +0,0 @@
|
||||
# BAT-002 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-002
|
||||
- Profile: batch-lite
|
||||
- Verified: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-012: service-token rotate CLI 명령 구현
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
- Design Gate (from planning.md): Design Required Before Implementation — **no**
|
||||
- Reason: DB 스키마 변경 없음. 기존 패턴(issue/revoke) 동일하게 적용. 결정 사항 없음.
|
||||
- Confirmed: 구조/인터페이스/런타임 흐름 변경 없음 — **no architecture impact confirmed**
|
||||
- Baseline Docs Update Required: no
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-012 AC-1
|
||||
> Given `expires_at IS NULL`인 유효한 service-token 이름, When `pfhub service-token rotate <name> --force`를 실행, Then DB 행의 `token_hash`, `token_prefix`, `runtime_id`가 새 값으로 변경되고 `id`, `permission`, `expires_at`(NULL)은 기존과 동일하다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_no_expiry_succeeds`
|
||||
- `rotated.token_prefix != "oldpref"` 검증
|
||||
- `rotated.runtime_id != "old-runtime-id"` 검증
|
||||
- `expires_at=None` 유지 검증 (rotate 후 기존 token 조회 → expires_at 변경 없음)
|
||||
- Result: **PASS**
|
||||
|
||||
### REQ-012 AC-2
|
||||
> Given rotate 성공, When CLI 출력, Then 새 token 원문이 화면에 1회 출력되고 "이 토큰은 지금 1회만 표시" 안내 메시지가 함께 표시된다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_no_expiry_succeeds`
|
||||
- `result.stdout`에 `"token:"` 포함 확인
|
||||
- CLI 출력 패턴: `⚠ 갱신된 토큰 원문은 지금 1회만 표시됩니다.` + `token: <raw>`
|
||||
- Result: **PASS**
|
||||
|
||||
### REQ-012 AC-3
|
||||
> Given 존재하지 않는 token 이름, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_unknown_name_exits_error`
|
||||
- `result.exit_code == 1` 검증
|
||||
- `"nonexistent" in result.stdout` 검증
|
||||
- Result: **PASS**
|
||||
|
||||
### REQ-012 AC-4
|
||||
> Given revoked 상태의 token, When `rotate`, Then 오류 메시지를 출력하고 종료 코드 1을 반환한다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_revoked_token_exits_error`
|
||||
- `result.exit_code == 1` 검증
|
||||
- `"old-token" in result.stdout` 검증
|
||||
- Result: **PASS**
|
||||
|
||||
### REQ-012 AC-5
|
||||
> Given `expires_at`이 NULL이 아닌(만료일이 설정된) token, When `rotate`, Then "만료일이 설정된 토큰은 rotate할 수 없습니다" 오류를 출력하고 종료 코드 1을 반환한다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_with_expiry_exits_error`
|
||||
- `result.exit_code == 1` 검증
|
||||
- `"expiring" in result.stdout` 검증
|
||||
- Result: **PASS**
|
||||
|
||||
### REQ-012 AC-6
|
||||
> Given `--force` 없이 실행, When CLI, Then 확인 프롬프트를 표시하고 거절 시 변경 없이 종료한다.
|
||||
|
||||
- Evidence: `test_cli_service_token_rotate_can_abort`
|
||||
- `input="n\n"` 입력으로 abort 시뮬레이션
|
||||
- `result.exit_code == 0` 검증
|
||||
- `token.token_prefix == "pref"` (변경 없음) 검증
|
||||
- Result: **PASS**
|
||||
|
||||
## Test Summary
|
||||
|
||||
```
|
||||
tests/test_cli.py — 37 passed (신규 5개 포함, 회귀 없음)
|
||||
|
||||
rotate 관련:
|
||||
test_cli_service_token_rotate_no_expiry_succeeds PASSED
|
||||
test_cli_service_token_rotate_unknown_name_exits_error PASSED
|
||||
test_cli_service_token_rotate_revoked_token_exits_error PASSED
|
||||
test_cli_service_token_rotate_with_expiry_exits_error PASSED
|
||||
test_cli_service_token_rotate_can_abort PASSED
|
||||
```
|
||||
|
||||
## Blocking Issues
|
||||
|
||||
없음.
|
||||
|
||||
## Ready for Release
|
||||
|
||||
- true
|
||||
@@ -1,58 +0,0 @@
|
||||
# BAT-003 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-003
|
||||
- Profile: batch-lite
|
||||
|
||||
## Plan Summary
|
||||
|
||||
- CLI `pfhub portfolio list`에 `--user <username>` 필수 옵션 추가
|
||||
- `_with_service_and_repo` 헬퍼로 repository 직접 접근 패턴 구현
|
||||
- `repository.get_user_by_username(user)` 호출 → None이면 ValueError → exit code 1
|
||||
- `service.list_portfolios(owner_user_id=owner.id, ...)` 소유자 필터 항상 전달
|
||||
- README 예제 라인 갱신, breaking change 반영
|
||||
- 기존 테스트 2개 수정 + 신규 4개 추가
|
||||
|
||||
## Changed Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `app/cli/commands/portfolio.py` | `_with_service_and_repo` 헬퍼 추가, `list_portfolios` `--user` 필수화 및 소유자 필터 적용 |
|
||||
| `tests/test_cli.py` | `test_cli_list_renders_table`, `test_cli_list_supports_tag_filters` 수정; AC 1~4 커버 4개 케이스 추가 |
|
||||
| `README.md` | `pfhub portfolio list` → `pfhub portfolio list --user <username>` 갱신 |
|
||||
|
||||
## Execution Log
|
||||
|
||||
### 구현 결정 사항
|
||||
|
||||
- `--user` 옵션: `typer.Option(None, ...)` + 수동 None 체크로 exit code 1 보장 (typer required 옵션은 exit code 2이므로 AC-1 불일치)
|
||||
- username → user_id 조회: `_with_service_and_repo` 콜백 내 `repository.get_user_by_username(user)` 직접 호출
|
||||
- 존재하지 않는 username: `ValueError` raise → `execute_command`가 exit code 1로 처리
|
||||
- `_with_service_and_repo`: 기존 `_with_service`와 동일 패턴, callback 인자에 `repository` 추가
|
||||
|
||||
### 기존 테스트 수정
|
||||
|
||||
- `test_cli_list_renders_table`: `FakeRepo()` → user alice 생성 + `portfolio.owner_user_id` 설정 + `--user alice` 추가
|
||||
- `test_cli_list_supports_tag_filters`: 동일 패턴 적용
|
||||
|
||||
## Validation
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -k "test_cli_list_renders_table or test_cli_list_supports_tag_filters or test_cli_portfolio_list" -v
|
||||
```
|
||||
|
||||
```
|
||||
6 passed
|
||||
```
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -v
|
||||
```
|
||||
|
||||
```
|
||||
41 passed in 1.22s (전체 회귀, 기존 테스트 전원 통과)
|
||||
```
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- 없음
|
||||
@@ -1,42 +0,0 @@
|
||||
# BAT-003 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-003
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-013 | CLI `portfolio list --user` 필수화 및 소유자 기반 조회 | Interface | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- [dcy-003_20260502_portfolio-list-user-required](../../discovery/dcy-003_20260502_portfolio-list-user-required.md)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- CLI `pfhub portfolio list` 명령에 `--user <username>` 필수 옵션 추가
|
||||
- username → owner_user_id(UUID) 변환 로직 추가 (repository.get_user_by_username 직접 호출)
|
||||
- `PortfolioService.list_portfolios(owner_user_id=...)` 호출 시 소유자 필터 항상 전달
|
||||
- `--user` 미입력 시 exit code 1 오류 종료, 존재하지 않는 username 시 오류 종료
|
||||
- README 예제 라인 갱신 (breaking change 안내 포함)
|
||||
- DB 스키마 변경 없음, HTTP API 변경 없음
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `README.md` — `pfhub portfolio list` 예제 라인 `--user <username>` 옵션 추가, breaking change 안내 추가
|
||||
- Modules:
|
||||
- `app/cli/commands/portfolio.py` — `list_portfolios` 명령 시그니처 및 서비스 호출부 수정
|
||||
- Tests:
|
||||
- `tests/test_cli.py` — AC 1~5 커버 테스트 케이스 4개 추가
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | verified |
|
||||
@@ -1,89 +0,0 @@
|
||||
# BAT-003 Planning
|
||||
|
||||
- Status: draft
|
||||
- Batch: BAT-003
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-013: CLI `portfolio list --user` 필수화 및 소유자 기반 조회
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- `--all` 관리자 전체 조회 옵션 (후속 Discovery로 분리)
|
||||
- HTTP API `GET /portfolios` 접근 통제 변경 (현행 유지)
|
||||
- CLI에 `_ownership_required` 설정 플래그 도입
|
||||
|
||||
## 구현 순서
|
||||
|
||||
### 1단계: CLI 명령 수정
|
||||
|
||||
- `app/cli/commands/portfolio.py` — `list_portfolios` 명령 수정
|
||||
- 현재 시그니처:
|
||||
```python
|
||||
@portfolio_app.command("list")
|
||||
def list_portfolios(
|
||||
ctx: typer.Context,
|
||||
broker: str | None = ...,
|
||||
status: str | None = ...,
|
||||
include_tags: ...,
|
||||
exclude_tags: ...,
|
||||
match_mode: ...,
|
||||
)
|
||||
```
|
||||
- 변경 내용:
|
||||
- `user: str = typer.Option(..., "--user", help="Filter by username (required).")` 필수 옵션 추가
|
||||
- `_with_service` callback 안에서 `repository.get_user_by_username(user)` 호출
|
||||
- user가 `None`이면 오류 출력 + `raise typer.Exit(code=1)` (typer의 `required=True` 또는 직접 검증)
|
||||
- 조회 결과가 `None`이면 "사용자를 찾을 수 없습니다" 메시지 + `raise typer.Exit(code=1)`
|
||||
- `service.list_portfolios(owner_user_id=found_user.id, ...)` 로 호출
|
||||
|
||||
- 참고: `_with_service` 헬퍼는 `PortfolioService`만 생성하므로, username → user_id 조회는 callback 내부에서 `repository.get_user_by_username(user)`를 직접 호출한다. callback 인자로 `(service, app_context)` 대신 repository도 사용 가능 — 기존 `_with_service`에서 `repository = app_context.repository_factory(settings)` 반환 객체를 callback에 노출하는 방식으로 처리한다.
|
||||
|
||||
구현 패턴 (예시):
|
||||
```python
|
||||
def _with_service_and_repo(ctx, callback):
|
||||
app_context = ctx.obj
|
||||
repository = app_context.repository_factory(app_context.settings)
|
||||
service = PortfolioService(repository)
|
||||
try:
|
||||
result = execute_command(
|
||||
app_context.console,
|
||||
callback=lambda: callback(service, repository, app_context),
|
||||
)
|
||||
return result
|
||||
finally:
|
||||
if callable(getattr(type(repository), "close", None)):
|
||||
repository.close()
|
||||
```
|
||||
또는 기존 `_with_service` callback 안에서 `service._repository`를 통해 접근하는 방법도 검토한다. 구현 시 결정.
|
||||
|
||||
### 2단계: README 수정
|
||||
|
||||
- `README.md` — CLI 예제 섹션의 `pfhub portfolio list` 라인을 `pfhub portfolio list --user <username>`으로 변경
|
||||
- breaking change 안내 문구 추가 (Service token issue policy 아래 또는 CLI 섹션 내)
|
||||
|
||||
### 3단계: 테스트 추가
|
||||
|
||||
- `tests/test_cli.py` — 4개 테스트 케이스 추가:
|
||||
1. `test_cli_portfolio_list_without_user_exits_error`
|
||||
- FakeRepo 준비, `--user` 없이 호출 → `exit_code == 1`, 오류 메시지 포함 확인
|
||||
2. `test_cli_portfolio_list_with_valid_user_filters_by_owner`
|
||||
- FakeRepo에 복수 사용자 + 포트폴리오 준비, `--user alice` 호출 → alice 소유 항목만 반환
|
||||
3. `test_cli_portfolio_list_with_unknown_user_exits_error`
|
||||
- 존재하지 않는 username으로 호출 → `exit_code == 1`, "찾을 수 없습니다" 포함 확인
|
||||
4. `test_cli_portfolio_list_in_memory_mode_filters_owner`
|
||||
- in-memory 모드에서도 동일하게 소유자 필터 적용 확인 (AC 4)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- `app/repositories/portfolio_repository.py` — `get_user_by_username` Protocol stub 존재 확인 완료 (line 16)
|
||||
- `app/repositories/postgres.py` — `get_user_by_username` 구현 존재 확인 완료 (line 419)
|
||||
- `tests/test_cli.py` — `FakeRepo.get_user_by_username` 존재 확인 완료 (line 109)
|
||||
- External: 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: DB 스키마 변경 없음, HTTP API 변경 없음. CLI 옵션 추가 + repository 메서드 직접 호출 패턴은 기존 service-token issue 구현과 동일 수준. 구조적 결정 사항 없음.
|
||||
@@ -1,72 +0,0 @@
|
||||
# BAT-003 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-003
|
||||
- Profile: batch-lite
|
||||
|
||||
## Architecture Impact Check
|
||||
|
||||
- Design Gate (from planning.md): no — DB 스키마 변경 없음, HTTP API 변경 없음
|
||||
- Actual Architecture Impact: none confirmed — `app/cli/commands/portfolio.py` 내부 헬퍼 추가, Protocol stub/구현체 변경 없음
|
||||
- Baseline Document Sync Required: no
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### AC 1: `--user` 없이 실행 → exit code 1 + 오류 메시지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_list_without_user_exits_error` |
|
||||
| Verification | `runner.invoke(app, ["portfolio", "list"])` → `exit_code == 1`, `"--user" in result.stdout` |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 2: `--user alice` → alice 소유 포트폴리오만 반환
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_list_with_valid_user_filters_by_owner` |
|
||||
| Verification | alice 포트폴리오만 등록 → alice 조회 시 `005930:tv` 포함, bob 조회 시 미포함 확인 |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 3: 존재하지 않는 username → 오류 메시지 + exit code 1
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_list_with_unknown_user_exits_error` |
|
||||
| Verification | `--user nobody` → `exit_code == 1`, `"nobody" in result.stdout` |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 4: in-memory 모드에서도 동일 동작
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_list_in_memory_mode_filters_owner` |
|
||||
| Verification | FakeRepo(in-memory) 사용, `--user alice` → alice 소유 포트폴리오 반환 확인 |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 5: 오류 형식이 기존 CLI와 일관성 유지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | 코드 검토 — `execute_command`의 `ValueError` 핸들러(`console.print + raise typer.Exit(code=1)`)와 동일 패턴 사용 |
|
||||
| Verification | `--user` 미입력 → `ctx.obj.console.print(...)` + `raise typer.Exit(code=1)`, 미등록 user → `ValueError` → `execute_command` 핸들러 동일 경로 |
|
||||
| Result | ✅ 충족 |
|
||||
|
||||
## Test Execution
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -v
|
||||
41 passed in 1.22s
|
||||
```
|
||||
|
||||
- 신규 케이스 6개 (`test_cli_portfolio_list_*`, 수정된 `test_cli_list_renders_table`, `test_cli_list_supports_tag_filters`) 포함 전체 통과
|
||||
- 기존 37개 케이스 회귀 없음
|
||||
|
||||
## Blocking Issues
|
||||
|
||||
- 없음
|
||||
|
||||
## Result
|
||||
|
||||
- Release Ready: yes
|
||||
- Notes: batch-lite, design 없음, planning의 Design Gate = no, architecture impact = none confirmed
|
||||
@@ -1,59 +0,0 @@
|
||||
# BAT-004 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-004
|
||||
- Profile: batch-lite
|
||||
|
||||
## Plan Summary
|
||||
|
||||
- CLI `pfhub portfolio create`에 `--user <username>` 필수 옵션 추가
|
||||
- BAT-003에서 도입한 `_with_service_and_repo` 헬퍼 재사용
|
||||
- `repository.get_user_by_username(user)` 호출 → None이면 ValueError → exit code 1
|
||||
- `service.create_portfolio(payload, owner_user_id=owner.id)` 실제 user UUID 전달
|
||||
- README 예제 라인 갱신
|
||||
- 기존 테스트 3개 수정 + 신규 3개 추가
|
||||
|
||||
## Changed Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `app/cli/commands/portfolio.py` | `create_portfolio` 함수에 `--user` 옵션 추가, `_with_service` → `_with_service_and_repo` 교체, username→UUID 조회 후 `owner_user_id` 전달 |
|
||||
| `tests/test_cli.py` | `test_cli_create_buy_and_delete_flow`, `test_cli_write_command_prompts_before_create`, `test_cli_write_command_can_abort` 수정; AC 1~3 커버 신규 3개 케이스 추가 |
|
||||
| `README.md` | `pfhub portfolio create` → `pfhub portfolio create ... --user <username>` 갱신 |
|
||||
|
||||
## Execution Log
|
||||
|
||||
### 구현 결정 사항
|
||||
|
||||
- `--user` 옵션: `typer.Option(None, "--user", ...)` + 수동 None 체크로 exit code 1 보장 (BAT-003과 동일 패턴)
|
||||
- `_confirm` 호출 전에 None 체크를 배치하여 owner 미지정 시 확인 프롬프트 없이 즉시 종료
|
||||
- callback 내 username → user_id 조회: `repository.get_user_by_username(user)` → None이면 `ValueError`
|
||||
- `service.create_portfolio(payload, owner_user_id=owner.id)`: `portfolio_service.py` 시그니처 변경 불필요 (기존 키워드 인자 그대로 활용)
|
||||
|
||||
### 기존 테스트 수정
|
||||
|
||||
- `test_cli_create_buy_and_delete_flow`: alice user 생성 + `--user alice` 추가, `owner_user_id` 검증 BUY 전에 배치 (delete 후 `_portfolio`가 None이 되므로)
|
||||
- `test_cli_write_command_prompts_before_create`: alice user 생성 + `--user alice` 추가
|
||||
- `test_cli_write_command_can_abort`: alice user 생성 + `--user alice` 추가
|
||||
|
||||
## Validation
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -k "test_cli_portfolio_create or test_cli_create or test_cli_write_command" -v
|
||||
```
|
||||
|
||||
```
|
||||
6 passed
|
||||
```
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -v
|
||||
```
|
||||
|
||||
```
|
||||
44 passed in 1.37s (전체 회귀, 기존 테스트 전원 통과)
|
||||
```
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- 없음
|
||||
@@ -1,42 +0,0 @@
|
||||
# BAT-004 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-004
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-02
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-014 | CLI `portfolio create --user` 필수화 및 소유자 설정 | Interface | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- minor-change fast path (Discovery 없음)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- CLI `pfhub portfolio create` 명령에 `--user <username>` 필수 옵션 추가
|
||||
- `_with_service_and_repo` 헬퍼(REQ-013 도입) 재사용, `repository.get_user_by_username(user)` 호출
|
||||
- `PortfolioService.create_portfolio(owner_user_id=owner.id, ...)` 실제 UUID 전달
|
||||
- `--user` 미입력 시 exit code 1, 미존재 username 시 exit code 1
|
||||
- README 예제 갱신
|
||||
- DB 스키마 변경 없음, HTTP API 변경 없음
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `README.md` — `pfhub portfolio create` 예제 라인 `--user <username>` 추가
|
||||
- Modules:
|
||||
- `app/cli/commands/portfolio.py` — `create_portfolio` 명령 수정
|
||||
- Tests:
|
||||
- `tests/test_cli.py` — 기존 `test_cli_create_buy_and_delete_flow` 수정 + AC 1~4 커버 케이스 추가
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | verified |
|
||||
@@ -1,50 +0,0 @@
|
||||
# BAT-004 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-004
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-014: CLI `portfolio create --user` 필수화 및 소유자 설정
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- HTTP API `POST /portfolios` owner 설정 변경
|
||||
- `owner_user_id` 기본값(`"system"`) 서비스 레이어 변경
|
||||
|
||||
## 구현 순서
|
||||
|
||||
### 1단계: CLI 명령 수정
|
||||
|
||||
- `app/cli/commands/portfolio.py` — `create_portfolio` 명령 수정
|
||||
- `user: str | None = typer.Option(None, "--user", help="Owner username (required).")` 추가
|
||||
- `_with_service_and_repo` 콜백으로 교체
|
||||
- callback 내부:
|
||||
- `user is None` → `console.print(...)` + `raise typer.Exit(code=1)`
|
||||
- `repository.get_user_by_username(user)` → `None`이면 `raise ValueError(...)`
|
||||
- `service.create_portfolio(payload, owner_user_id=owner.id)` 호출
|
||||
|
||||
### 2단계: README 수정
|
||||
|
||||
- `README.md` — `pfhub portfolio create` 예제 라인에 `--user alice` 추가
|
||||
|
||||
### 3단계: 테스트
|
||||
|
||||
- `tests/test_cli.py`
|
||||
- `test_cli_create_buy_and_delete_flow`: `--user alice` 추가, repo에 alice 생성
|
||||
- `test_cli_write_command_prompts_before_create`: 동일 수정
|
||||
- `test_cli_write_command_can_abort`: 동일 수정
|
||||
- `test_cli_portfolio_create_without_user_exits_error` (신규)
|
||||
- `test_cli_portfolio_create_with_valid_user_sets_owner` (신규)
|
||||
- `test_cli_portfolio_create_with_unknown_user_exits_error` (신규)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal: `_with_service_and_repo` (BAT-003 도입, 이미 존재)
|
||||
- External: 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: 기존 `_with_service_and_repo` 패턴 재사용. 구조/스키마 변경 없음.
|
||||
@@ -1,65 +0,0 @@
|
||||
# BAT-004 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-004
|
||||
- Profile: batch-lite
|
||||
|
||||
## Architecture Impact Check
|
||||
|
||||
- Design Gate (from planning.md): no — DB 스키마 변경 없음, HTTP API 변경 없음
|
||||
- Actual Architecture Impact: none confirmed — `app/cli/commands/portfolio.py` 내 기존 `_with_service_and_repo` 헬퍼 재사용, Protocol stub/구현체 변경 없음
|
||||
- Baseline Document Sync Required: no
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### AC 1: `--user` 없이 실행 → exit code 1 + 오류 메시지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_create_without_user_exits_error` |
|
||||
| Verification | `runner.invoke(app, ["portfolio", "create", "005930:tv", "--symbol", "005930", "--force"])` → `exit_code == 1`, `"--user" in result.stdout` |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 2: `--user alice` → 생성된 포트폴리오의 `owner_user_id`가 alice UUID로 설정됨
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_create_with_valid_user_sets_owner` |
|
||||
| Verification | alice user 생성 → `--user alice --force` 실행 → `repo._portfolio.owner_user_id == user.id` 확인 |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 3: 존재하지 않는 username → 오류 메시지 + exit code 1
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_create_with_unknown_user_exits_error` |
|
||||
| Verification | `--user nobody` → `exit_code == 1`, `"nobody" in result.stdout` |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 4: in-memory 모드에서도 동일 동작
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | `test_cli_portfolio_create_with_valid_user_sets_owner` (FakeRepo 사용) |
|
||||
| Verification | FakeRepo(in-memory) 환경에서 alice UUID가 `owner_user_id`로 설정됨 확인 |
|
||||
| Result | ✅ PASSED |
|
||||
|
||||
### AC 5: 오류 형식이 기존 CLI와 일관성 유지
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| Evidence | 코드 검토 — BAT-003 `list_portfolios` 구현과 동일한 패턴 사용 |
|
||||
| Verification | `--user` 미입력 → `ctx.obj.console.print(...)` + `raise typer.Exit(code=1)`, 미등록 user → `ValueError` → `execute_command` 핸들러 동일 경로 |
|
||||
| Result | ✅ 충족 |
|
||||
|
||||
## Test Execution
|
||||
|
||||
```
|
||||
pytest tests/test_cli.py -v
|
||||
44 passed in 1.37s
|
||||
```
|
||||
|
||||
## Regression
|
||||
|
||||
- BAT-003 추가 테스트(portfolio list 관련 4개) 전원 PASSED — 회귀 없음
|
||||
- 기존 `test_cli_create_buy_and_delete_flow`, `test_cli_write_command_prompts_before_create`, `test_cli_write_command_can_abort` 수정 후 전원 PASSED
|
||||
@@ -1,53 +0,0 @@
|
||||
# BAT-005 Implementation Log
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-005
|
||||
- Profile: batch-lite
|
||||
- Completed: 2026-05-03
|
||||
|
||||
## 구현 내역
|
||||
|
||||
### 1. `app/repositories/postgres.py` — `delete_broker_credential` 추가
|
||||
|
||||
```python
|
||||
def delete_broker_credential(self, user_id: str, broker: str) -> bool:
|
||||
with self._connect().cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM broker_credentials WHERE user_id = %s AND broker = %s",
|
||||
(user_id, broker),
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
```
|
||||
|
||||
### 2. `app/cli/commands/credential.py` — 신규 생성
|
||||
|
||||
- `credential_app = typer.Typer(help="Broker credential management commands.")`
|
||||
- `_with_service(ctx, callback)`: repository 생성 → execute_command → finally close. RuntimeError도 캐치해 stdout 출력 후 exit 1.
|
||||
- `set` 명령: `--user`, `--broker` (기본값 `kis`), `--account-ref`, `--app-key`, `--app-secret`, `--force`
|
||||
- `list` 명령: `--user` — encrypted_payload 제외 dict 리스트 출력
|
||||
- `remove` 명령: `--user`, `--broker`, `--force` — delete_broker_credential 호출
|
||||
|
||||
### 3. `app/cli/main.py` — `credential_app` 등록
|
||||
|
||||
```python
|
||||
from app.cli.commands.credential import credential_app
|
||||
...
|
||||
app.add_typer(credential_app, name="credential")
|
||||
```
|
||||
|
||||
### 4. `tests/test_cli.py`
|
||||
|
||||
- `FakeRepo.__init__`: `_broker_credentials: dict[tuple[str, str], BrokerCredential] = {}` 추가
|
||||
- `FakeRepo.upsert/list/get/delete_broker_credential` 메서드 추가
|
||||
- `_settings_with_enc_key()`, `build_runner_with_enc_key()` 헬퍼 추가
|
||||
- 신규 테스트 5개 추가
|
||||
|
||||
### 5. `docs/project-structure.md` — CLI 모듈 목록 갱신
|
||||
|
||||
- `app/cli/` 섹션에 `credential` 명령 그룹 추가
|
||||
- primary runtime 설명에 `broker credentials` 추가
|
||||
|
||||
## 설계 결정
|
||||
|
||||
- dict 리스트 출력은 `render_output`의 타입 매칭을 우회해 `console.print(item)` 직접 호출 (기존 render_output이 등록된 타입만 처리함)
|
||||
- `RuntimeError`(`CREDENTIAL_ENCRYPTION_KEY` 미설정)는 `_with_service` 헬퍼에서 캐치해 stdout으로 출력 후 exit 1 처리
|
||||
@@ -1,45 +0,0 @@
|
||||
# BAT-005 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-005
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-03
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-015 | CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리) | Interface | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- minor-change fast path (Discovery 없음)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- CLI `pfhub credential set/list/remove` 명령 그룹 신규 추가
|
||||
- `app/cli/commands/credential.py` 신규 생성 (`credential_app`)
|
||||
- `app/cli/main.py`에 `credential_app` 등록
|
||||
- `app/repositories/postgres.py`에 `delete_broker_credential` 메서드 추가
|
||||
- 기존 `broker_credentials` 테이블 / `upsert_broker_credential` / `list_broker_credentials` 재사용
|
||||
- 기존 Fernet 암호화 인프라 (`app/core/encryption.py`) 재사용
|
||||
- DB 스키마 변경 없음, HTTP API 변경 없음
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/project-structure.md` — CLI 모듈 목록에 `credential.py` 추가
|
||||
- Modules:
|
||||
- `app/cli/commands/credential.py` (신규)
|
||||
- `app/cli/main.py` (`credential_app` 등록)
|
||||
- `app/repositories/postgres.py` (`delete_broker_credential` 추가)
|
||||
- Tests:
|
||||
- `tests/test_cli.py` — credential 명령 통합 테스트 추가
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | passed |
|
||||
@@ -1,73 +0,0 @@
|
||||
# BAT-005 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-005
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-015: CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- HTTP API 엔드포인트 추가 (별도 REQ 대상)
|
||||
- 브로커 인증 정보 복호화 출력 (보안 정책상 CLI에서 plain text 노출 금지)
|
||||
- 브로커 종류 확장 (현재는 `kis` 기본값만 처리)
|
||||
|
||||
## 구현 순서
|
||||
|
||||
### 1단계: repository 메서드 추가
|
||||
|
||||
- `app/repositories/postgres.py`
|
||||
- `delete_broker_credential(user_id: str, broker: str) -> bool` 추가
|
||||
- 레코드 존재 시 삭제 후 `True`, 미존재 시 `False` 반환
|
||||
|
||||
### 2단계: CLI 명령 모듈 신규 생성
|
||||
|
||||
- `app/cli/commands/credential.py`
|
||||
- `credential_app = typer.Typer(help="Broker credential management commands.")`
|
||||
- `_with_service(ctx, callback)` 헬퍼: repository 생성 → callback 실행 → finally close
|
||||
- `set` 명령: `--user`, `--broker` (기본값 `kis`), `--account-ref`, `--app-key`, `--app-secret`
|
||||
- `get_user_by_username` → `None`이면 `ValueError`
|
||||
- `settings.require_credential_encryption_key()` → Fernet 암호화
|
||||
- `upsert_broker_credential` 호출
|
||||
- `list` 명령: `--user`
|
||||
- `get_user_by_username` → `None`이면 `ValueError`
|
||||
- `list_broker_credentials` 호출, `encrypted_payload` 제외 출력
|
||||
- `remove` 명령: `--user`, `--broker`, `--force`
|
||||
- `get_user_by_username` → `None`이면 `ValueError`
|
||||
- `_confirm(force, ...)` 확인 후 `delete_broker_credential` 호출
|
||||
|
||||
### 3단계: CLI 등록
|
||||
|
||||
- `app/cli/main.py`
|
||||
- `from app.cli.commands.credential import credential_app` import 추가
|
||||
- `app.add_typer(credential_app, name="credential")` 등록
|
||||
|
||||
### 4단계: 문서 갱신
|
||||
|
||||
- `docs/project-structure.md` — CLI 모듈 목록에 `credential.py` 항목 추가
|
||||
|
||||
### 5단계: 테스트
|
||||
|
||||
- `tests/test_cli.py`
|
||||
- `test_cli_credential_set_creates_record` (신규): alice 생성 후 set 실행 → list에 표시
|
||||
- `test_cli_credential_list_hides_payload` (신규): list 출력에 `encrypted_payload` 미포함 확인
|
||||
- `test_cli_credential_set_with_unknown_user_exits_error` (신규): 미존재 user → exit code 1
|
||||
- `test_cli_credential_remove_deletes_record` (신규): set 후 remove → list에서 사라짐
|
||||
- `test_cli_credential_set_requires_encryption_key` (신규): CREDENTIAL_ENCRYPTION_KEY 미설정 → exit code 1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- `broker_credentials` 테이블 (migration 0014 — 이미 존재)
|
||||
- `upsert_broker_credential`, `list_broker_credentials`, `get_broker_credential` (이미 존재)
|
||||
- `app/core/encryption.py` — `encrypt_payload` (이미 존재)
|
||||
- `app/core/config.py` — `settings.require_credential_encryption_key()` (이미 존재)
|
||||
- `_confirm`, `execute_command`, `render_output` (기존 CLI 헬퍼 — 이미 존재)
|
||||
- External: 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: 기존 `_with_service` 헬퍼 패턴 재사용 (`user.py` 동일 구조). 구조/런타임 흐름 변경 없음.
|
||||
@@ -1,61 +0,0 @@
|
||||
# BAT-005 Verification
|
||||
|
||||
- Status: passed
|
||||
- Batch: BAT-005
|
||||
- Profile: batch-lite
|
||||
- Verified: 2026-05-03
|
||||
|
||||
## REQ Coverage
|
||||
|
||||
### REQ-015: CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리)
|
||||
|
||||
| AC # | 기준 | 검증 방법 | 결과 |
|
||||
|------|------|-----------|------|
|
||||
| AC-1 | alice 사용자 + 유효 enc key → `credential set` 실행 시 broker_credentials 레코드 생성(upsert) | `test_cli_credential_set_creates_record` | ✅ passed |
|
||||
| AC-2 | `credential list` 출력에 broker, account_ref, credential_version 포함 / encrypted_payload 미포함 | `test_cli_credential_list_hides_payload` | ✅ passed |
|
||||
| AC-3 | 존재하지 않는 username → exit code 1 + 에러 메시지 | `test_cli_credential_set_with_unknown_user_exits_error` | ✅ passed |
|
||||
| AC-4 | `credential remove --force` 후 list에서 사라짐 | `test_cli_credential_remove_deletes_record` | ✅ passed |
|
||||
| AC-5 | CREDENTIAL_ENCRYPTION_KEY 미설정 → exit code 1 + 에러 메시지 포함 | `test_cli_credential_set_requires_encryption_key` | ✅ passed |
|
||||
|
||||
## Test Evidence
|
||||
|
||||
### 실행 명령
|
||||
```bash
|
||||
python -m pytest tests/test_cli.py -q
|
||||
```
|
||||
|
||||
### 결과
|
||||
```
|
||||
49 passed in 1.49s
|
||||
```
|
||||
|
||||
- 신규 테스트 5개 모두 passed
|
||||
- 기존 테스트 44개 regression 없음
|
||||
|
||||
### 신규 테스트 목록
|
||||
| 테스트 | AC |
|
||||
|--------|----|
|
||||
| `test_cli_credential_set_creates_record` | AC-1 |
|
||||
| `test_cli_credential_list_hides_payload` | AC-2 |
|
||||
| `test_cli_credential_set_with_unknown_user_exits_error` | AC-3 |
|
||||
| `test_cli_credential_remove_deletes_record` | AC-4 |
|
||||
| `test_cli_credential_set_requires_encryption_key` | AC-5 |
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| `app/cli/commands/credential.py` | 신규 생성 — `credential_app`, `set`/`list`/`remove` 명령 |
|
||||
| `app/cli/main.py` | `credential_app` import 및 `app.add_typer(credential_app, name="credential")` 등록 |
|
||||
| `app/repositories/postgres.py` | `delete_broker_credential(user_id, broker) -> bool` 메서드 추가 |
|
||||
| `tests/test_cli.py` | `FakeRepo.upsert/list/get/delete_broker_credential`, 신규 테스트 5개 추가 |
|
||||
| `docs/project-structure.md` | CLI 명령 그룹 목록에 `credential` 추가 |
|
||||
|
||||
## Baseline Document Sync
|
||||
|
||||
- `docs/project-structure.md`: `app/cli/` 섹션에 `credential` 명령 그룹 추가 완료
|
||||
- 구조/런타임 흐름/인터페이스 계약 변경 없음 → `runtime-flows.md`, `interface-contract.md` 갱신 불필요
|
||||
|
||||
## Residual Risk
|
||||
|
||||
- 없음
|
||||
@@ -1,92 +0,0 @@
|
||||
# BAT-006 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-006
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
`portfolios.portfolio_id`를 사용자 지정 TEXT PK에서 서버 생성 UUID(TEXT)로 교체한다. 기존 `portfolio_id` 문자열은 `name` 컬럼으로 이전하고, `(owner_user_id, name)` 복합 UNIQUE 제약을 DB 레벨에서 도입해 사용자 범위 내 이름 유일성을 보장한다. CLI는 `<name>` + `--user` 조합으로 portfolio를 식별하고 내부에서 `get_portfolio_by_name_and_user()`를 통해 UUID를 조회한다. HTTP API path parameter와 `GET /v1/assets` `asset_id` 반환은 UUID 기반으로 전환된다.
|
||||
|
||||
## Baseline vs Batch 변경점
|
||||
|
||||
### docs/project-structure.md (현재 baseline)
|
||||
- `app/repositories/postgres.py`: SQL 세부사항 담당으로만 기술 — `get_portfolio_by_name_and_user()` 미기재
|
||||
- `app/domain/models.py`: `portfolio_id` 필드 의미 변경 미기재
|
||||
|
||||
### docs/runtime-flows.md (현재 baseline)
|
||||
- `CLI Operational Command Execution` flow: name+user 식별 방식 미기재
|
||||
- `v1 API — GET /v1/assets` flow: `asset_id` 필드 언급 없음
|
||||
|
||||
### docs/data-model.md (현재 baseline)
|
||||
- `portfolio_id :: text :: stable portfolio primary key` — UUID 의미 변경 미기재
|
||||
- `name` 컬럼 별도 기재 없음
|
||||
- `(owner_user_id, name)` UNIQUE 제약 미기재
|
||||
|
||||
### docs/interface-contract.md (현재 baseline)
|
||||
- `GET /v1/assets` 응답 명세 없음 (bat-001에서 추가 예정이었으나 아직 반영 안 됨)
|
||||
- trex-basic 동시 배포 계획 없음
|
||||
|
||||
## Changed Areas
|
||||
|
||||
- Docs:
|
||||
- `docs/project-structure.md`
|
||||
- `docs/runtime-flows.md`
|
||||
- `docs/data-model.md`
|
||||
- `docs/interface-contract.md`
|
||||
- `docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md` (A-3 철회 주석)
|
||||
- Modules:
|
||||
- `migrations/versions/20260511_0015_portfolio_uuid_migration.py` (신규 Alembic revision)
|
||||
- `app/domain/models.py` (주석 없음, Portfolio dataclass 구조 변경 없음 — 기존 `portfolio_id` + `name` 필드 유지)
|
||||
- `app/repositories/postgres.py`:
|
||||
- `create_portfolio()`: `portfolio_id` 파라미터 제거, 서버 UUID 생성
|
||||
- `get_portfolio_by_name_and_user()` 신규 메서드
|
||||
- `ensure_schema()`: `(owner_user_id, name)` UNIQUE 제약 추가
|
||||
- `update_portfolio()`: UniqueViolation 시 ValueError("이미 사용 중인 이름입니다") 반환
|
||||
- `app/domain/services.py`: `PortfolioDomainRepository` Protocol 업데이트
|
||||
- `app/schemas/portfolio.py`:
|
||||
- `CreatePortfolioRequest`: `portfolio_id` 필드 제거, `name: str` 필수로 변경, name 유효성 validator 추가
|
||||
- `app/services/portfolio_service.py`: `create_portfolio()` 호출부 업데이트
|
||||
- `app/cli/commands/portfolio.py`:
|
||||
- `create`: `portfolio_id` Argument → `name` Argument (필수), `--name` Option 제거
|
||||
- `status`, `buy`, `sell`, `invest`, `transfer`, `reset`, `delete`: `portfolio_id` → `name` Argument + `--user` Option
|
||||
- `update`: `portfolio_id` → `name` Argument + `--user` Option + `--new-name` Option (이름 변경용)
|
||||
- `_resolve_portfolio_id()` helper 추가
|
||||
- `_validate_portfolio_name()` helper 추가
|
||||
- Interfaces:
|
||||
- CLI: `portfolio <subcommand> <portfolio_id>` → `portfolio <subcommand> <name> --user <username>`
|
||||
- HTTP API: `POST /portfolios` request body: `portfolio_id` 필드 제거, `name` 필수화
|
||||
- HTTP API: `GET /v1/assets` `assets[].asset_id` → UUID 형식 반환
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **portfolio_id 컬럼 타입 TEXT 유지**: PostgreSQL UUID 타입 변환은 이번 범위 밖. `uuid.uuid4()` 문자열을 TEXT로 저장.
|
||||
2. **get_portfolio_by_name_and_user() 공유**: CLI와 HTTP API 식별 모두 이 단일 repository 메서드 사용. Protocol에도 추가.
|
||||
3. **이름 대소문자 구분**: `TV`와 `tv`는 다른 이름. DB UNIQUE 제약도 case-sensitive.
|
||||
4. **단일 Alembic revision**: name 컬럼 추가 (IF NOT EXISTS), UUID 교체, FK 4개 테이블 갱신, UNIQUE 제약을 하나의 revision에서 수행. IF NOT EXISTS 가드로 재실행 안전성 보장.
|
||||
5. **update 명령 name 변경**: CLI `update` 명령은 `--new-name` Option으로 이름 변경. `--force` 없으면 confirm 프롬프트. DB UNIQUE 위반 시 `이미 사용 중인 이름입니다: {name}` 오류.
|
||||
6. **name 유효성**: `[a-zA-Z0-9._:-]`, 1~100자. Pydantic validator + CLI 레이어에서 이중 검사.
|
||||
7. **comment/tag sub-commands**: REQ-017 명시 범위 밖. 이번 batch에서 변경하지 않음 (UUID 직접 전달 필요).
|
||||
8. **dcy-002 A-3 공식 철회**: REQ-018 구현 시 해당 discovery 문서에 주석 추가.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **기존 portfolio_id 값이 UUID 형식인 경우**: migration이 이미 적용된 것으로 판단하는 guard (UNIQUE constraint 존재 여부로 판단). 재실행 시 skip.
|
||||
- **FK 고아 레코드**: migration revision 내 트랜잭션 후 orphan 검증 쿼리. 고아 발견 시 RuntimeError 발생.
|
||||
- **name 허용 문자 범위 외 입력**: CLI validation + Pydantic validator에서 조기 거부.
|
||||
- **동일 owner_user_id + 동일 name**: DB UNIQUE 위반 → `create_portfolio()`에서 ValueError, `update_portfolio()`에서 UniqueViolation catch → ValueError.
|
||||
- **존재하지 않는 name+user 조합**: `get_portfolio_by_name_and_user()` None 반환 → `포트폴리오를 찾을 수 없습니다: {name} (user: {username})`.
|
||||
- **revision 재실행**: `already_migrated` guard (UNIQUE constraint 유무 확인). 이미 적용됐으면 UUID 교체 블록 전체 skip.
|
||||
- **migration 중 portfolio_tags PK 업데이트**: PostgreSQL은 PK 값 UPDATE 허용. FK 제약 없으므로 안전.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
both (project-structure + runtime-flows)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
- project-structure.md:
|
||||
- `app/repositories/postgres.py` 항목: `get_portfolio_by_name_and_user(owner_user_id, name) → Portfolio | None` 추가
|
||||
- `app/domain/models.py` 항목: `Portfolio.portfolio_id`가 서버 생성 UUID임을 명시
|
||||
- runtime-flows.md:
|
||||
- `CLI Operational Command Execution` flow: `<name>` + `--user` → `get_portfolio_by_name_and_user()` → UUID 조회 흐름 추가
|
||||
- `v1 API — GET /v1/assets` flow: `asset_id = portfolio_id (UUID)` 반환 명시
|
||||
@@ -1,63 +0,0 @@
|
||||
# BAT-006 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-006
|
||||
|
||||
## Plan Summary
|
||||
|
||||
- REQ-016: 신규 Alembic revision(name 컬럼 추가, portfolio_id UUID 교체, FK 4개 테이블 atomic 갱신, UNIQUE 제약 추가), Portfolio dataclass `name` 필드 추가, `create_portfolio()` UUID 생성, `get_portfolio_by_name_and_user()` 신규 메서드
|
||||
- REQ-017: CLI portfolio 명령 argument/option 전면 전환(`portfolio_id` → `<name> --user`), `(user_id, name)` → UUID 서비스 레이어, name 유효성 검사, update name guard
|
||||
- REQ-018: HTTP API path parameter UUID 전환, `GET /v1/assets` `asset_id` UUID 직렬화, dcy-002 A-3 철회 주석
|
||||
- REQ-019: baseline 문서 4개 갱신(project-structure.md, runtime-flows.md, data-model.md, interface-contract.md)
|
||||
|
||||
## Changed Files
|
||||
|
||||
### New Files
|
||||
- `migrations/versions/20260511_0015_portfolio_uuid_migration.py` — Alembic revision; idempotency guard via uq_portfolios_owner_name constraint presence check; atomic per-portfolio FK update across portfolio_ledger/portfolio_comments/portfolio_tags/ledger_trades
|
||||
- `tests/test_portfolio_uuid.py` — 14 tests covering UUID creation, name+user resolution, alice/bob isolation, buy/delete flow, name validation, unknown user/portfolio errors, update rename
|
||||
|
||||
### Modified Files
|
||||
- `app/repositories/postgres.py`
|
||||
- `ensure_schema()`: replaced `ADD CONSTRAINT IF NOT EXISTS` (not valid PostgreSQL syntax) with `DO $$ IF NOT EXISTS ... ALTER TABLE ADD CONSTRAINT $$` pattern for uq_portfolios_owner_name
|
||||
- `create_portfolio()`: signature changed from `(portfolio_id: str, *, ..., name: str, ...)` to `(name: str, *, ...)` with server-generated UUID; catches psycopg UniqueViolation → ValueError("이미 사용 중인 이름입니다: {name}")
|
||||
- Added `get_portfolio_by_name_and_user(owner_user_id, name) -> Portfolio | None`
|
||||
- `update_portfolio()`: catches UniqueViolation on name update → ValueError("이미 사용 중인 이름입니다: {new_name}")
|
||||
- `app/domain/services.py` — `PortfolioDomainRepository` Protocol updated: `create_portfolio` new signature, `get_portfolio_by_name_and_user` added, `list_portfolios` Protocol params aligned
|
||||
- `app/schemas/portfolio.py` — `CreatePortfolioRequest`: removed `portfolio_id` field; `name: str` now required with regex validation `[a-zA-Z0-9._:-]{1,100}`
|
||||
- `app/services/portfolio_service.py` — `create_portfolio()`: call site updated to `self._repository.create_portfolio(payload.name, owner_user_id=...)`
|
||||
- `app/cli/commands/portfolio.py` — Full rewrite for REQ-017: `_NAME_RE`, `_validate_portfolio_name()`, `_resolve_portfolio_id()` helpers; all 8 mutation commands switched to `<name> --user`; `update` uses `--new-name` with `--force` guard; `comment`/`tag` sub-commands keep `portfolio_id` (UUID) directly
|
||||
- `docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md` — A-3 철회 주석 추가
|
||||
- `docs/data-model.md` — portfolios.portfolio_id → UUID, name column, uq_portfolios_owner_name constraint documented
|
||||
- `docs/project-structure.md` — Added `get_portfolio_by_name_and_user()` to repositories section
|
||||
- `docs/runtime-flows.md` — Added CLI portfolio name+user resolution flow; GET /v1/assets asset_id UUID note
|
||||
- `docs/interface-contract.md` — Added v1 consumer API section with simultaneous deployment constraint; CLI portfolio inputs updated
|
||||
- `tests/test_cli.py` — FakeRepo: `create_portfolio` new signature (generates UUID), `get_portfolio_by_name_and_user` added; `_portfolio()`: name changed to "005930:tv"; 5 tests updated to pass --user and adjust assertions
|
||||
- `tests/test_portfolio_ownership.py` — Fixed POST body to use `name` instead of `portfolio_id`
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. Created Alembic migration `20260511_0015_portfolio_uuid_migration.py` (REQ-016)
|
||||
2. Updated `app/repositories/postgres.py`: ensure_schema UNIQUE constraint (DO $$ pattern), create_portfolio UUID generation, get_portfolio_by_name_and_user, update_portfolio UniqueViolation catch (REQ-016)
|
||||
3. Updated `app/domain/services.py` Protocol (REQ-016)
|
||||
4. Updated `app/schemas/portfolio.py`: removed portfolio_id, name required + validated (REQ-016, REQ-017)
|
||||
5. Updated `app/services/portfolio_service.py`: create_portfolio call site (REQ-016)
|
||||
6. Rewrote `app/cli/commands/portfolio.py`: name+user pattern for all mutation commands (REQ-017)
|
||||
7. Added dcy-002 A-3 철회 주석 (REQ-018)
|
||||
8. Updated 4 baseline docs (REQ-019)
|
||||
9. Created `tests/test_portfolio_uuid.py` with 14 tests
|
||||
10. Fixed `tests/test_cli.py` FakeRepo + 5 test cases
|
||||
11. Fixed `tests/test_portfolio_ownership.py` POST body field
|
||||
12. Fixed `ensure_schema()` syntax: `ADD CONSTRAINT IF NOT EXISTS` → DO $$ block (PostgreSQL compatibility)
|
||||
|
||||
## Validation
|
||||
|
||||
- Full test suite: **129 passed, 0 failed**
|
||||
- `tests/test_portfolio_uuid.py`: 14/14 passed (UUID generation, name+user resolution, alice/bob isolation, name validation, error messages)
|
||||
- `tests/test_cli.py`: 49/49 passed (including updated tests for new CLI interface)
|
||||
- `tests/test_portfolio_ownership.py`: 4/4 passed
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- Alembic migration `20260511_0015` not verified against real PostgreSQL instance (unit tests use FakeRepo only)
|
||||
- trex-basic simultaneous deployment required: GET /v1/assets asset_id changes from TEXT to UUID format
|
||||
- Existing `comment` and `tag` sub-commands still take `portfolio_id` (UUID) directly; operators must obtain UUID from `portfolio list --user` output
|
||||
@@ -1,36 +0,0 @@
|
||||
# BAT-006 Index
|
||||
|
||||
- BAT ID: BAT-006
|
||||
- Profile: standard
|
||||
- Status: release-candidate
|
||||
- Source Discovery: dcy-004
|
||||
- Date Created: 2026-05-11
|
||||
- Folder: docs/batches/bat-006_20260511_portfolio-uuid-migration/
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | Title | Type | Priority |
|
||||
| --- | --- | --- | --- |
|
||||
| REQ-016 | portfolios 테이블 UUID 전환 및 데이터 마이그레이션 | Migration | High |
|
||||
| REQ-017 | CLI portfolio 명령 — name+user 조합 식별 방식 전환 | Interface | High |
|
||||
| REQ-018 | HTTP API portfolio path parameter UUID 전환 및 v1 asset_id UUID 반환 | Interface | High |
|
||||
| REQ-019 | portfolio UUID 전환 관련 baseline 문서 갱신 | Documentation | Medium |
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- `portfolios.portfolio_id`를 사용자 지정 TEXT에서 서버 생성 UUID(TEXT)로 교체하고, 기존 `portfolio_id` 문자열을 `name` 컬럼으로 이전한다.
|
||||
- `(owner_user_id, name)` 복합 UNIQUE 제약을 DB 레벨에서 도입해 사용자 범위 내 이름 유일성을 보장한다.
|
||||
- 기존 데이터와 FK 참조 테이블(portfolio_ledger, portfolio_comments, portfolio_tags, ledger_trades)을 단일 Alembic revision으로 atomic하게 마이그레이션한다.
|
||||
- CLI portfolio 뮤테이션 명령을 `<name>` + `--user <username>` 조합 식별 방식으로 전환하고, 내부에서 `(owner_user_id, name)` → UUID 단일 쿼리 조회로 처리한다.
|
||||
- HTTP API path parameter 및 `GET /v1/assets` `asset_id` 반환을 UUID 기반으로 전환하고, dcy-002 A-3 조항을 공식 철회한다.
|
||||
- 위 변경 사항을 baseline 문서(docs/project-structure.md, docs/runtime-flows.md, docs/data-model.md, docs/interface-contract.md)에 반영한다.
|
||||
|
||||
## Delivery Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
| --- | --- | --- |
|
||||
| Index | [index.md](./index.md) | release-candidate |
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Design | [design.md](./design.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | passed |
|
||||
@@ -1,71 +0,0 @@
|
||||
# BAT-006 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-006
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-016: portfolios 테이블 UUID 전환 및 데이터 마이그레이션
|
||||
- REQ-017: CLI portfolio 명령 — name+user 조합 식별 방식 전환
|
||||
- REQ-018: HTTP API portfolio path parameter UUID 전환 및 v1 asset_id UUID 반환
|
||||
- REQ-019: portfolio UUID 전환 관련 baseline 문서 갱신
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
### 구현 순서
|
||||
|
||||
1. **REQ-016** — Alembic revision 추가 및 도메인/레포지터리 수정
|
||||
- `migrations/versions/`: 신규 revision (name 컬럼 추가, 기존 portfolio_id → name 복사, UUID 교체, FK 갱신, UNIQUE 제약 추가, 고아 레코드 검증 쿼리 포함)
|
||||
- `app/domain/models.py`: Portfolio dataclass `name` 필드 명확 선언, `portfolio_id` UUID 문자열 의미 변경
|
||||
- `app/repositories/postgres.py`: `create_portfolio()` 서버 UUID 생성, `get_portfolio_by_name_and_user()` 신규 메서드 추가
|
||||
|
||||
2. **REQ-017** — CLI 명령 식별 방식 전환 (REQ-016의 repository 메서드 선행 필요)
|
||||
- `app/cli/commands/portfolio.py`: `create`, `status`, `buy`, `sell`, `invest`, `transfer`, `reset`, `delete`, `update` Argument/Option 전환 (`portfolio_id` argument → `<name>` + `--user`)
|
||||
- `app/domain/services.py`: `(user_id, name)` → UUID 해석 로직 추가
|
||||
- name 유효성 검사(`[a-zA-Z0-9._:-]`, 1~100자), `update` name 변경 시 `--force` 또는 confirm 프롬프트, 중복 name 오류 메시지 처리 포함
|
||||
|
||||
3. **REQ-018** — HTTP API 전환 (REQ-016의 UUID 스키마 전제)
|
||||
- `app/api/portfolios.py`: path parameter UUID 기반 전환
|
||||
- `app/api/v1_assets.py`: `asset_id` 직렬화에서 `portfolio_id`(UUID) 사용
|
||||
- `app/schemas/portfolio.py`, `app/schemas/v1.py`: asset_id/portfolio_id 필드 타입 문서화
|
||||
- `docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md`: A-3 철회 주석 추가
|
||||
|
||||
4. **REQ-019** — 문서 갱신 (REQ-016~018 구현 흐름 내 또는 완료 후)
|
||||
- `docs/project-structure.md`: `get_portfolio_by_name_and_user()` 메서드 및 `portfolio_id` UUID 의미 반영
|
||||
- `docs/runtime-flows.md`: CLI `<name>` + `--user` 식별 흐름, `GET /v1/assets` `asset_id` UUID 반환 흐름 반영
|
||||
- `docs/data-model.md`: `portfolios` 테이블 `portfolio_id`(UUID TEXT PK), `name`(TEXT), `(owner_user_id, name)` UNIQUE 제약 반영
|
||||
- `docs/interface-contract.md`: `GET /v1/assets` `asset_id` UUID 반환 명시, trex-basic 동시 배포 계획 기록
|
||||
|
||||
### 선행 조건
|
||||
|
||||
- 로컬 docker compose 기반 PostgreSQL 환경이 실행 가능해야 한다.
|
||||
- `alembic upgrade head`가 현재 revision까지 적용된 상태에서 신규 revision을 추가 적용할 수 있어야 한다.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- REQ-017과 REQ-018은 REQ-016의 `get_portfolio_by_name_and_user()` 메서드와 UUID schema 전환 전제
|
||||
- REQ-019는 REQ-016~018 구현 내용을 기준으로 문서화
|
||||
- External:
|
||||
- trex-basic과 portfolio-hub의 동시 배포 필요 (REQ-018 NFR-3). trex-basic 측 코드 변경은 이번 범위 밖이나 배포 시점 조율 필요. 계획은 REQ-019에서 `docs/interface-contract.md`에 기록.
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: DB schema migration, CLI 인터페이스 전면 전환, HTTP API path parameter 변경이 포함되어 구조/인터페이스/런타임 흐름 영향이 크다. design.md에서 변경 경계, 공유 메서드 설계(`get_portfolio_by_name_and_user()`), edge case(name 유효성, FK 고아, IF NOT EXISTS)를 먼저 확정해야 구현 혼선을 방지할 수 있다.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. REQ-016 구현 완료 및 pytest 기반 UUID 형식·FK 고아 0건·UNIQUE 제약·IF NOT EXISTS 재실행 검증 통과
|
||||
2. REQ-017 구현 완료 및 CLI 소유권 검증·name 유효성·중복 변경 거부·alice/bob 독립 portfolio 테스트 통과
|
||||
3. REQ-018 구현 완료 및 `GET /v1/assets` asset_id UUID 형식·`POST /v1/assets/{asset_id}/ledger/trades` UUID 호출·HTTP 404 검증 통과
|
||||
4. REQ-019 문서 갱신 완료 및 코드 리뷰 기반 검증 (4개 baseline 문서 내용 확인)
|
||||
5. 전체 acceptance criteria mapping 완료, verification.md 확정, release 준비 상태 확인
|
||||
|
||||
## Risks
|
||||
|
||||
- **DB migration 안전성**: FK 4개 테이블 atomic 갱신 실패 시 데이터 불일치 위험. 대응: 단일 트랜잭션 + revision 내 고아 레코드 검증 쿼리 + 로컬 환경 rollback 테스트 포함.
|
||||
- **CLI 하위 호환 단절**: 기존 `portfolio_id` argument 기반 사용 흐름이 즉시 중단됨. 대응: migration 완료 후 CLI 명령 end-to-end 검증 suite 실행.
|
||||
- **trex-basic 동시 배포 조율 실패**: portfolio-hub만 배포 시 `asset_id` 형식 불일치로 trex-basic 연동 오류 가능. 대응: 배포 조율 계획을 `docs/interface-contract.md`에 명시(REQ-019), 배포 전 trex-basic 팀 협의.
|
||||
- **IF NOT EXISTS 재실행 안전성 누락**: revision 재실행 오류 발생 가능. 대응: REQ-016 AC-5 검증 포함(단순 재실행 확인).
|
||||
@@ -1,94 +0,0 @@
|
||||
# BAT-006 Verification
|
||||
|
||||
- Status: passed
|
||||
- Batch: BAT-006
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-016:
|
||||
- AC-1: 기존 portfolio_id → name 복사 및 portfolio_id UUID 형식 교체 검증 (`re.match(r'^[0-9a-f-]{36}$', ...)`)
|
||||
- Evidence: `test_create_portfolio_assigns_uuid_id` — `uuid.UUID(portfolio.portfolio_id)` 성공 확인 ✓
|
||||
- AC-2: portfolio_ledger, portfolio_comments, portfolio_tags, ledger_trades FK 고아 레코드 0건 확인
|
||||
- Evidence: `migrations/versions/20260511_0015_portfolio_uuid_migration.py` — 각 portfolio에 대해 FK 4개 테이블 UPDATE 후 orphan 레코드 zero-check RuntimeError 포함 ✓
|
||||
- AC-3: 동일 owner_user_id + 동일 name 중복 삽입 시 DB UNIQUE 제약 위반 오류 발생 확인
|
||||
- Evidence: `postgres.py` `create_portfolio()` psycopg UniqueViolation catch → ValueError("이미 사용 중인 이름입니다: {name}"); FakeRepo 동일 동작 ✓
|
||||
- AC-4: 다른 owner_user_id + 동일 name 정상 삽입 확인
|
||||
- Evidence: `test_alice_and_bob_can_have_same_portfolio_name` — alice, bob 각각 "SAMSUNG" 이름으로 독립 portfolio 생성 성공; portfolio_ids 2개 모두 유효한 UUID ✓
|
||||
- AC-5: 신규 revision 재실행 시 오류 없음 확인 (IF NOT EXISTS 준수)
|
||||
- Evidence: Alembic migration — uq_portfolios_owner_name constraint 존재 여부를 information_schema 쿼리로 확인 후 조건부 ADD CONSTRAINT ✓
|
||||
- AC-6: `create_portfolio()` 실행 시 서버 `uuid.uuid4()` 생성 UUID가 portfolio_id로 저장됨 확인
|
||||
- Evidence: `test_create_returns_uuid_in_response` — JSON 응답에서 `uuid.UUID(portfolio_id)` 파싱 성공 ✓
|
||||
|
||||
- REQ-017:
|
||||
- AC-1: `portfolio create <name> --user <username>` 실행 시 portfolio_id를 사용자가 직접 지정하지 않고 UUID가 DB에 저장됨
|
||||
- Evidence: `test_create_portfolio_assigns_uuid_id` — create 후 `uuid.UUID(portfolio.portfolio_id)` 검증 ✓
|
||||
- AC-2: `portfolio buy <name> --user alice` 실행 시 `(alice, <name>)` 단일 쿼리 UUID 조회 후 처리 확인
|
||||
- Evidence: `test_buy_resolves_portfolio_by_name_and_user` — `get_portfolio_by_name_and_user()` 경유 BUY 처리, `repo._entries[0].action == "BUY"` ✓
|
||||
- AC-3: `--user bob`으로 alice 소유 portfolio 접근 시 `포트폴리오를 찾을 수 없습니다: {name} (user: bob)` 출력 및 명령 중단 확인
|
||||
- Evidence: `test_alice_cannot_delete_bobs_portfolio` — alice가 bob 소유 "SAMSUNG" 삭제 시도 → exit_code=1, portfolio 잔존 ✓; `test_status_fails_when_portfolio_name_not_found_for_user` — 오류 메시지에 name 포함 ✓
|
||||
- AC-4: alice/bob 동일 name으로 독립 portfolio 생성 및 각자 조작 가능 확인
|
||||
- Evidence: `test_alice_and_bob_can_have_same_portfolio_name` ✓
|
||||
- AC-5: alice의 portfolio name을 이미 존재하는 name으로 `update` 시 `이미 사용 중인 이름입니다: {name}` 오류 출력 확인
|
||||
- Evidence: `postgres.py` `update_portfolio()` UniqueViolation catch → ValueError; `ensure_schema()` UNIQUE 제약으로 DB 레벨 보장 ✓
|
||||
- AC-6: `--force` 없는 `update` name 변경 시 명령 중단 또는 confirm 프롬프트 표시 확인
|
||||
- Evidence: `update_portfolio` command — `if new_name is not None: _confirm(force, ...)` 로직; `test_update_new_name_renames_portfolio`는 `--force` 있을 때만 성공 ✓
|
||||
- AC-7: `[a-zA-Z0-9._:-]` 범위 외 문자 포함 name `create` 시 유효성 검사 오류 반환 확인
|
||||
- Evidence: `test_name_validation_rejects_invalid_characters` — "invalid name!" → exit_code=1 ✓
|
||||
- AC-8: name 길이 0 또는 100자 초과 시 유효성 검사 오류 반환 확인
|
||||
- Evidence: `test_name_validation_rejects_empty_name` — 빈 문자열 → exit_code != 0; `_NAME_RE = re.compile(r"^[a-zA-Z0-9._:\-]{1,100}$")` regex로 1~100자 강제 ✓
|
||||
|
||||
- REQ-018:
|
||||
- AC-1: `GET /v1/assets` 응답 `assets[].asset_id`가 UUID 형식 확인
|
||||
- Evidence: `v1_assets.py` — `asset_id=p.portfolio_id`; REQ-016 이후 portfolio_id는 UUID; full test suite 129 passed ✓
|
||||
- AC-2: UUID `asset_id`로 `POST /v1/assets/{asset_id}/ledger/trades` 호출 시 정상 처리 확인
|
||||
- Evidence: `v1_ledger.py` — `repo.get(asset_id)` 로 포트폴리오 존재 확인; UUID portfolio_id로 정상 동작 ✓
|
||||
- AC-3: 존재하지 않는 UUID `asset_id`로 ledger/trades 호출 시 HTTP 404 반환 확인
|
||||
- Evidence: `repo.get()` 실패 → ValueError → HTTP 404 처리 (기존 패턴 유지) ✓
|
||||
- AC-4: `/portfolios/{portfolio_id}` UUID 호출 시 정상 응답 반환 확인
|
||||
- Evidence: HTTP API path는 `str` 수용; UUID 형식도 통과; `tests/test_portfolio_ownership.py` 4/4 passed ✓
|
||||
- AC-5: `docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md` A-3 항목에 철회 주석 존재 확인
|
||||
- Evidence: dcy-002 A-3 항목 아래 **[철회 — 2026-05-11 REQ-018]** 주석 추가 완료 ✓
|
||||
|
||||
- REQ-019:
|
||||
- AC-1: `docs/project-structure.md`에 `get_portfolio_by_name_and_user()` 메서드 및 `portfolio_id` UUID 의미 반영 확인
|
||||
- Evidence: app/repositories/ 섹션에 "Key methods added in bat-006: `get_portfolio_by_name_and_user()`" 추가 ✓
|
||||
- AC-2: `docs/runtime-flows.md`에 CLI `<name>` + `--user` 식별 흐름 및 `GET /v1/assets` `asset_id` UUID 반환 반영 확인
|
||||
- Evidence: "Flow: CLI Portfolio Command — Name+User Resolution" 섹션 신규 추가; GET /v1/assets flow step 6 UUID 명시 ✓
|
||||
- AC-3: `docs/data-model.md`에 `portfolios` 테이블 `portfolio_id`(UUID PK), `name`(TEXT), `(owner_user_id, name)` UNIQUE 제약 반영 확인
|
||||
- Evidence: Key Fields에 `portfolio_id :: uuid`, `name :: text` 추가; Uniqueness Constraints 섹션 신규 추가 ✓
|
||||
- AC-4: `docs/interface-contract.md`에 `GET /v1/assets` `asset_id` UUID 반환 명시 및 trex-basic 동시 배포 계획 기록 확인
|
||||
- Evidence: "Interface: v1 Consumer API (trex-basic)" 섹션 신규 추가; Deployment Constraint (bat-006) 명시 ✓
|
||||
|
||||
## Evidence
|
||||
|
||||
- Test:
|
||||
- `tests/test_portfolio_uuid.py`: 14/14 passed
|
||||
- `test_create_portfolio_assigns_uuid_id` — UUID PK 생성 검증
|
||||
- `test_create_returns_uuid_in_response` — API 응답 UUID 파싱 검증
|
||||
- `test_status_resolves_by_name_and_user` — name+user 조회 성공
|
||||
- `test_status_fails_when_user_not_found` — 없는 user 오류
|
||||
- `test_status_fails_when_portfolio_name_not_found_for_user` — 없는 name 오류
|
||||
- `test_create_without_user_fails` — --user 누락 오류
|
||||
- `test_create_with_unknown_user_fails` — 없는 user 오류
|
||||
- `test_name_validation_rejects_invalid_characters` — 특수문자 거부
|
||||
- `test_name_validation_rejects_empty_name` — 빈 문자열 거부
|
||||
- `test_alice_and_bob_can_have_same_portfolio_name` — 동일 name 독립 UUID
|
||||
- `test_buy_resolves_portfolio_by_name_and_user` — name+user BUY 처리
|
||||
- `test_delete_resolves_portfolio_by_name_and_user` — name+user DELETE 처리
|
||||
- `test_alice_cannot_delete_bobs_portfolio` — 소유권 격리
|
||||
- `test_update_new_name_renames_portfolio` — --new-name rename
|
||||
- `tests/test_cli.py`: 49/49 passed (FakeRepo 및 5개 테스트 업데이트 포함)
|
||||
- `tests/test_portfolio_ownership.py`: 4/4 passed
|
||||
- Full suite: **129/129 passed**
|
||||
- Manual:
|
||||
- Alembic migration SQL 코드 리뷰: name 컬럼 추가, FK UPDATE, UUID PK 교체, UNIQUE 제약 추가, orphan check 포함
|
||||
- dcy-002 A-3 철회 주석 문서 내 직접 확인
|
||||
- baseline 4개 문서 항목별 내용 확인
|
||||
|
||||
## Result
|
||||
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
- Notes:
|
||||
- Alembic migration은 FakeRepo 기반 단위 테스트만 수행됨; 실제 PostgreSQL 인스턴스 검증은 release 시 수행 권장
|
||||
- trex-basic 동시 배포 조정 필요 (asset_id TEXT → UUID 형식 변경)
|
||||
@@ -1,115 +0,0 @@
|
||||
# BAT-007 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-007
|
||||
- Profile: batch-lite
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
이 batch는 코드 구조 변경 없이 분석 및 문서화만 수행한다. 주요 설계 작업은 `ensure_schema()`와 Alembic revision 간 schema ownership 경계를 코드에서 직접 분석하고 그 결과를 baseline 문서에 반영하는 것이다.
|
||||
|
||||
코드 변경이 필요한 경우는 분석 결과 ownership 충돌이 발견될 때뿐이며, 이 batch 범위에서는 해당 충돌이 기존 IF NOT EXISTS/stamp 메커니즘으로 이미 처리되고 있음이 확인되었다.
|
||||
|
||||
## Changed Areas
|
||||
|
||||
| 대상 | 변경 유형 | REQ |
|
||||
|------|-----------|-----|
|
||||
| `README.md` | 로컬 DB 검증 절차, runbook 추가 | REQ-001, REQ-003 |
|
||||
| `docs/runtime-flows.md` | Local DB 초기화 흐름, Alembic/ensure_schema() 경계 흐름 추가 | REQ-001, REQ-002, REQ-003 |
|
||||
| `docs/project-structure.md` | Schema ownership 경계 섹션 추가 | REQ-002, REQ-003 |
|
||||
| `docker-compose.local-test.yml` | 변경 없음 — 현행 구성이 검증 시나리오 요건 충족 | — |
|
||||
| `app/cli/commands/health.py` | 변경 없음 — init-db/migrate-db/health-check 출력 구조 확인 완료 | — |
|
||||
| `app/db/migrations.py` | 변경 없음 — bootstrap_or_upgrade_to_head() 흐름 분석으로 coverage 충족 | — |
|
||||
| `app/repositories/postgres.py` | 변경 없음 — ensure_schema() ownership 분석으로 coverage 충족 | — |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### D-1: design gate = no 유지, 최소 design.md 작성
|
||||
|
||||
Planning의 `Design Gate: no` 결정을 유지한다. 사용자가 draft-batch-design을 명시적으로 요청했으므로 이 문서를 생성하되, 신규 인터페이스/코드 구조 결정 사항이 없음을 확인하고 기록하는 용도로 사용한다.
|
||||
|
||||
### D-2: ensure_schema() ownership 유지 — Alembic으로 이전 불필요
|
||||
|
||||
`ensure_schema()`는 runtime bootstrap fallback으로 유지한다. fresh DB → app 시작 → migrate-db 순서에서 `bootstrap_or_upgrade_to_head()`의 legacy stamp 경로가 이 충돌을 처리한다. 이번 batch에서 ensure_schema()를 제거하거나 Alembic으로 완전 이전하는 작업은 범위 밖이다.
|
||||
|
||||
### D-3: 검증 시나리오를 README.md + docs/runtime-flows.md에 배분
|
||||
|
||||
검증 매트릭스(REQ-001)를 별도 파일로 생성하지 않고, 운영자가 참조하는 README.md와 기술적 흐름을 담는 docs/runtime-flows.md에 나누어 반영한다. 이 방식이 docs/project-structure.md 정책(docs/ = SDLC source-of-truth)과 일관된다.
|
||||
|
||||
## Schema Ownership Analysis
|
||||
|
||||
### ensure_schema()가 생성하는 객체
|
||||
|
||||
| 객체 | 타입 | IF NOT EXISTS 보호 |
|
||||
|------|------|-------------------|
|
||||
| `users` | TABLE | ✓ |
|
||||
| `auth_tokens` | TABLE | ✓ |
|
||||
| `idx_auth_tokens_user_created` | INDEX | ✓ |
|
||||
| `idx_auth_tokens_hash_active` | INDEX | ✓ |
|
||||
| `idx_auth_tokens_service_name_unique` | UNIQUE INDEX (partial) | ✓ |
|
||||
| `strategy_sets` | TABLE | ✓ |
|
||||
| `portfolios` | TABLE | ✓ |
|
||||
| `portfolios.name` | COLUMN | ADD COLUMN IF NOT EXISTS |
|
||||
| `portfolios.owner_user_id` | COLUMN | ADD COLUMN IF NOT EXISTS |
|
||||
| `uq_portfolios_owner_name` | UNIQUE CONSTRAINT | DO $$...IF NOT EXISTS$$ |
|
||||
| `portfolio_ledger` | TABLE | ✓ |
|
||||
| `idx_portfolio_ledger_portfolio_time` | INDEX | ✓ |
|
||||
| `idx_portfolios_broker_status_updated` | INDEX | ✓ |
|
||||
| `portfolio_comments` | TABLE | ✓ |
|
||||
| `idx_portfolio_comments_portfolio_created` | INDEX | ✓ |
|
||||
| `portfolio_tags` | TABLE | ✓ |
|
||||
| `idx_portfolio_tags_tag_portfolio` | INDEX | ✓ |
|
||||
| TimescaleDB hypertable | HYPERTABLE | if_not_exists=TRUE (조건부) |
|
||||
|
||||
### Alembic만 소유하는 객체 (ensure_schema()에 없음)
|
||||
|
||||
| 객체 | 타입 | Revision |
|
||||
|------|------|----------|
|
||||
| `portfolios.market` | COLUMN | 0014 |
|
||||
| `auth_tokens.runtime_id` | COLUMN | 0014 |
|
||||
| `idx_auth_tokens_runtime_id_unique` | UNIQUE INDEX (partial) | 0014 |
|
||||
| `broker_credentials` | TABLE | 0014 |
|
||||
| `ledger_trades` | TABLE | 0014 |
|
||||
| `uq_portfolios_owner_name` | UNIQUE CONSTRAINT | 0015 (Alembic + ensure_schema 공유) |
|
||||
|
||||
### 충돌 지점 및 해결 메커니즘
|
||||
|
||||
| 충돌 지점 | 충돌 조건 | 해결 방식 |
|
||||
|-----------|-----------|-----------|
|
||||
| 핵심 테이블(users, auth_tokens 등) 중복 생성 | ensure_schema() 선행 후 Alembic 실행 | `bootstrap_or_upgrade_to_head()` legacy stamp → 이미 존재하면 head로 stamp |
|
||||
| `portfolios.name` / `owner_user_id` 컬럼 중복 | ensure_schema() 선행 후 Alembic 0008/0010 실행 | Alembic revision이 `_column_exists()` guard 사용 |
|
||||
| `uq_portfolios_owner_name` 중복 | ensure_schema() 선행 후 Alembic 0015 실행 | Alembic 0015의 `_constraint_exists()` guard 사용 |
|
||||
| v1 API 전용 스키마 누락 | ensure_schema()만 실행한 환경 | `migrate-db` 실행 필요 — ensure_schema()는 v1 API 스키마를 포함하지 않음 |
|
||||
|
||||
### 허용/금지 실행 순서
|
||||
|
||||
**허용 (safe):**
|
||||
1. `init-db` → App 시작 → 정상 운영 (Alembic이 완전 스키마 생성, ensure_schema()는 no-op)
|
||||
2. App 시작 (ensure_schema() 선행) → `init-db` 또는 `migrate-db` (legacy stamp + upgrade 경로)
|
||||
3. 이미 Alembic 관리 DB → `migrate-db` (증분 upgrade)
|
||||
4. 검증 후 `migrate-db` 재실행 (모든 revision이 IF NOT EXISTS 보호)
|
||||
|
||||
**주의 (caution):**
|
||||
1. ensure_schema()만 실행한 환경 → v1 API(`broker_credentials`, `ledger_trades`, `runtime_id`) 미동작 → 반드시 `migrate-db` 필요
|
||||
2. `migrate-db` 단독 실행 → DB 미존재 시 실패 → `init-db` 우선 실행 필요
|
||||
|
||||
**금지 (forbidden):**
|
||||
1. `alembic_version` 테이블 수동 삭제 후 `migrate-db` 재실행 → legacy stamp 오판 위험
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **TimescaleDB 미설치 환경**: ensure_schema()의 TimescaleDB hypertable 생성은 `pg_extension`에서 timescaledb 존재 여부를 먼저 확인한다. 미설치 시 포트폴리오 운영은 가능하나 시계열 최적화 없이 작동한다.
|
||||
- **ensure_schema()의 v1 스키마 미포함**: `broker_credentials`, `ledger_trades`, `runtime_id` 컬럼은 ensure_schema()에 없다. ensure_schema()만 실행한 환경에서 GET /v1/user_info 또는 POST .../ledger/trades를 호출하면 DB 오류가 발생한다. `migrate-db`로 해결한다.
|
||||
- **비어있는 alembic_version**: `bootstrap_or_upgrade_to_head()`는 먼저 `alembic_version` 테이블 존재를 확인한 뒤, 없으면 legacy 테이블 존재를 확인한다. fresh DB이면서 alembic_version 없으면 full upgrade 경로를 탄다.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
`both` — docs/project-structure.md와 docs/runtime-flows.md 모두 갱신
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
| 문서 | 추가/수정 내용 | REQ |
|
||||
|------|----------------|-----|
|
||||
| `README.md` | Local DB 검증 절차 섹션 추가 (준비, init-db, health-check, smoke flow, 반복 실행 기준) | REQ-001, REQ-003 |
|
||||
| `docs/runtime-flows.md` | "Flow: Local DB Initialization and Validation" 추가 (init-db/migrate-db 흐름, Alembic/ensure_schema() 경계) | REQ-001, REQ-002, REQ-003 |
|
||||
| `docs/project-structure.md` | "Schema Ownership Boundary" 섹션 추가 (객체별 소유 경로, 허용/금지 순서, 범위 제외 명시) | REQ-002, REQ-003 |
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
# BAT-007 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-007
|
||||
|
||||
## Plan Summary
|
||||
|
||||
- REQ-002: `app/repositories/postgres.py`와 `migrations/versions/` 코드를 직접 분석해 ensure_schema() vs Alembic ownership 경계를 확인하고 `docs/project-structure.md`에 반영
|
||||
- REQ-001: 로컬 docker compose 기반 검증 시나리오(Scenario A/B, 반복 기준, release gate)를 `README.md`와 `docs/runtime-flows.md`에 반영
|
||||
- REQ-003: README runbook + docs/project-structure.md, docs/runtime-flows.md baseline 갱신
|
||||
|
||||
코드 변경 없음. 분석 결과 기존 IF NOT EXISTS guard와 `bootstrap_or_upgrade_to_head()` 메커니즘이 ownership 충돌을 이미 처리하고 있어 코드 수정 불필요.
|
||||
|
||||
## Changed Files
|
||||
|
||||
| 파일 | 변경 내용 | REQ |
|
||||
|------|-----------|-----|
|
||||
| `README.md` | "Local DB Validation Runbook" 섹션 추가 — Scenario A/B, 반복 실행 기준, rollback 판단 기준, release gate criteria | REQ-001, REQ-003 |
|
||||
| `docs/runtime-flows.md` | "Flow: Local DB Initialization and Validation" 추가 (Scenario A/B, repeatable run); "Flow: Alembic vs ensure_schema() Schema Ownership Boundary" 추가; bat-007 Change Log 기록 | REQ-001, REQ-002, REQ-003 |
|
||||
| `docs/project-structure.md` | "Schema Ownership Boundary" 섹션 추가 (소유 경계 표, 허용 순서, 운영 제약, legacy 범위 제외); bat-007 Change Log 기록 | REQ-002, REQ-003 |
|
||||
|
||||
## Analysis Results (REQ-002)
|
||||
|
||||
### ensure_schema() 전용 객체 (Alembic에 없음)
|
||||
- `idx_auth_tokens_service_name_unique` (partial unique index, WHERE token_type='service')
|
||||
|
||||
### Alembic 전용 객체 (ensure_schema()에 없음)
|
||||
- `portfolios.market` (revision 0014)
|
||||
- `auth_tokens.runtime_id` + `idx_auth_tokens_runtime_id_unique` (revision 0014)
|
||||
- `broker_credentials` table (revision 0014)
|
||||
- `ledger_trades` table (revision 0014)
|
||||
|
||||
### 공유 객체 (양쪽 모두 관리)
|
||||
- users, auth_tokens, strategy_sets, portfolios, portfolio_ledger, portfolio_comments, portfolio_tags + 대부분의 인덱스
|
||||
- 모두 IF NOT EXISTS 또는 inspector guard로 보호됨
|
||||
|
||||
### 충돌 해결 메커니즘 확인
|
||||
- `bootstrap_or_upgrade_to_head()` (app/db/migrations.py): alembic_version 테이블 여부 → legacy 테이블 여부로 stamp 분기. 코드 확인 완료.
|
||||
- Alembic revisions 0005, 0008, 0009, 0010, 0012, 0014, 0015 모두 `_column_exists()` / `_table_exists()` / `_constraint_exists()` inspector guard 사용. 확인 완료.
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. `app/repositories/postgres.py` ensure_schema() 전체 분석 완료 — 생성 객체 18종 목록화
|
||||
2. `migrations/versions/` 15개 revision 분석 완료 — 소유 경계 분류
|
||||
3. `bootstrap_or_upgrade_to_head()` 로직 확인 — legacy stamp 경로 정상 동작 확인
|
||||
4. `docker-compose.local-test.yml` 확인 — 검증 시나리오 실행 환경 충족 (TimescaleDB healthcheck 설정 완비)
|
||||
5. `app/cli/commands/health.py` 확인 — init-db/migrate-db/health-check 출력 필드 고정 확인
|
||||
6. README.md, docs/runtime-flows.md, docs/project-structure.md 갱신 완료
|
||||
|
||||
## Validation
|
||||
|
||||
코드 변경 없음 → pytest 재실행 불필요. 문서 변경 only.
|
||||
|
||||
```
|
||||
# 문서 변경 범위 확인
|
||||
git diff --name-only
|
||||
# 기대: README.md, docs/runtime-flows.md, docs/project-structure.md,
|
||||
# docs/batches/bat-007_.../design.md, planning.md, implementation.md
|
||||
```
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- Scenario B (ensure_schema() 선행) 흐름은 실제 로컬 환경에서 수동 검증이 필요하다. 이 batch는 코드 분석 + 문서화 기반 evidence이며 실제 DB 기반 실행 검증은 release 전 수행 권장.
|
||||
- `idx_auth_tokens_service_name_unique`가 ensure_schema()에만 있고 Alembic에 없어 fresh Alembic 설치 환경에서 해당 인덱스가 생성되지 않는다. 기능 영향: service token name unique 보장 불완전. 후속 batch 검토 필요.
|
||||
@@ -1,52 +0,0 @@
|
||||
# BAT-007 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-007
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-11
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-001 | 로컬 DB 초기화 및 마이그레이션 검증 시나리오 정의 | Testing | High |
|
||||
| REQ-002 | Alembic와 runtime schema ownership 경계 정리 | Migration | High |
|
||||
| REQ-003 | 로컬 DB 운영 runbook 및 baseline 문서 갱신 | Documentation | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- [dcy-001_20260430_real-db-migration-validation-and-operations-readiness](../../discovery/dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- 로컬 docker compose 기반 DB 환경에서 init-db, migrate-db, health-check, smoke flow 검증 매트릭스 정의 (REQ-001)
|
||||
- Alembic migration 경로와 runtime `ensure_schema()` 경로의 schema ownership 경계 분석 및 문서화 (REQ-002)
|
||||
- README, docs/runtime-flows.md, docs/project-structure.md에 로컬 DB 검증 절차, release gate 기준, rollback 판단 기준 반영 (REQ-003)
|
||||
- 구조/인터페이스/런타임 흐름 신규 추가 없음 — 분석 결과 기반 문서화 및 기존 코드 경계 명시
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `README.md` — 로컬 DB 준비 절차, init-db/migrate-db 실행 순서, smoke flow 검증 기준 추가
|
||||
- `docs/runtime-flows.md` — Alembic/ensure_schema() ownership 경계 흐름, CLI health-check flow 갱신
|
||||
- `docs/project-structure.md` — schema ownership 경계 및 local DB 운영 제약 반영
|
||||
- `docs/data-model.md` — Alembic ownership 객체 목록 명시 (필요 시)
|
||||
- `docs/discovery/dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md` — 검증 시나리오 결과 링크 추가 (필요 시)
|
||||
- Modules:
|
||||
- `docker-compose.local-test.yml` — 검증 시나리오 실행 환경 확인 (변경 필요 시 반영)
|
||||
- `app/cli/commands/health.py` — health-check 출력 형식 확인 (변경 필요 시 반영)
|
||||
- `app/db/migrations.py` — ownership 경계 확인 (변경 필요 시 반영)
|
||||
- `app/repositories/postgres.py` — ensure_schema() ownership 확인 (변경 필요 시 반영)
|
||||
- `migrations/versions/*.py` — Alembic revision 대상 객체 확인
|
||||
- Tests:
|
||||
- 로컬 docker compose 기반 수동 검증 체크리스트 (REQ-001 AC-1~3)
|
||||
- Alembic/ensure_schema() 충돌 시나리오 검증 절차 문서 (REQ-002 AC-1~3)
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Design | [design.md](./design.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | passed |
|
||||
@@ -1,72 +0,0 @@
|
||||
# BAT-007 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-007
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-001: 로컬 DB 초기화 및 마이그레이션 검증 시나리오 정의
|
||||
- REQ-002: Alembic와 runtime schema ownership 경계 정리
|
||||
- REQ-003: 로컬 DB 운영 runbook 및 baseline 문서 갱신
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- shared dev DB, staging, production-like 환경 검증 (REQ-001 Notes 참고)
|
||||
- legacy DB 지원 및 legacy stamp 경로 검증 (REQ-002 Notes 참고)
|
||||
- baseline 문서 전체 복제 또는 로컬 DB 검증과 무관한 구조 변경 (REQ-003 Notes 참고)
|
||||
- trex-basic 연동 환경 검증 (이번 batch 범위 외)
|
||||
|
||||
## 구현 순서
|
||||
|
||||
### 1단계: 현황 파악 (REQ-002 선행)
|
||||
|
||||
- `app/repositories/postgres.py`의 `ensure_schema()` 메서드가 다루는 테이블/인덱스 목록 확인
|
||||
- `migrations/versions/*.py` revision들이 다루는 객체 목록 확인
|
||||
- 충돌 가능 지점 식별: 동일 객체를 두 경로가 모두 CREATE/ALTER하는 경우
|
||||
- 허용 순서와 금지 순서, 사전 점검 규칙 초안 작성
|
||||
|
||||
### 2단계: 검증 시나리오 정의 (REQ-001)
|
||||
|
||||
- fresh DB 시나리오: DB 컨테이너 초기화 후 `init-db` → `migrate-db` → `health-check` → smoke flow 실행 순서와 기대 결과 정의
|
||||
- runtime-precreated schema 시나리오: app 실행 후 `ensure_schema()` 적용 상태에서 `migrate-db` 실행 기대 결과 정의
|
||||
- 각 시나리오별 pass/fail 기준, 남겨야 할 로그, 증거 수집 위치 명시
|
||||
- 반복 실행 시 출력 형식 일관성 기준 정의
|
||||
|
||||
### 3단계: 문서 갱신 (REQ-003 + REQ-001, REQ-002 산출물 통합)
|
||||
|
||||
- `README.md` — docker compose 기반 준비 절차, init-db/migrate-db 실행 순서, health-check 및 smoke flow 검증 기준 추가
|
||||
- `docs/runtime-flows.md` — Alembic/ensure_schema() ownership 경계 흐름, local DB 검증 흐름 갱신
|
||||
- `docs/project-structure.md` — schema ownership 경계 및 local DB 운영 제약 반영
|
||||
- rollback 또는 재시도 판단 기준과 검증 로그 수집 위치 runbook에 명시
|
||||
|
||||
### 4단계: 코드/설정 확인 및 필요 시 최소 수정
|
||||
|
||||
- `docker-compose.local-test.yml` — 검증 시나리오 실행 환경 점검 (변경 필요 시 최소 반영)
|
||||
- `app/cli/commands/health.py` — health-check 출력 형식이 검증 기준과 일치하는지 확인
|
||||
- `app/db/migrations.py` — IF NOT EXISTS 패턴 및 ownership 경계 준수 확인
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- `app/repositories/postgres.py` — `ensure_schema()` ownership 분석 대상
|
||||
- `app/db/migrations.py` — migration 실행 entry point 확인
|
||||
- `migrations/versions/*.py` — Alembic revision 대상 객체 목록
|
||||
- `docker-compose.local-test.yml` — 로컬 검증 환경
|
||||
- External: 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: 신규 인터페이스, DB 스키마, 런타임 흐름 변경 없음. 기존 코드 분석 → 문서 작성 중심 batch. 구조적 결정 사항이 분석 과정에서 발견되면 planning을 갱신하고 필요 시 design.md를 추가한다.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. REQ-002: Alembic vs ensure_schema() ownership 경계 분석 완료 + 문서 초안
|
||||
2. REQ-001: 검증 시나리오 매트릭스 정의 완료
|
||||
3. REQ-003: README, docs/runtime-flows.md, docs/project-structure.md 갱신 완료
|
||||
|
||||
## Risks
|
||||
|
||||
- `ensure_schema()`와 Alembic revision 간 실제 충돌 지점이 분석 후 발견되면 코드 수정이 필요해질 수 있음 — 이 경우 scope를 확대해 planning 갱신 후 진행
|
||||
- `docker-compose.local-test.yml`이 현행 환경(PostgreSQL + TimescaleDB)과 맞지 않으면 수동 검증 절차 수정 필요
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
# BAT-007 Verification
|
||||
|
||||
- Status: passed
|
||||
- Batch: BAT-007
|
||||
- Profile: batch-lite
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-001: 로컬 DB 초기화 및 마이그레이션 검증 시나리오 정의
|
||||
|
||||
| AC | 기준 | Evidence | 결과 |
|
||||
|----|------|----------|------|
|
||||
| AC-1 | init-db, health-check, smoke flow의 명령 순서, 기대 출력, pass/fail 기준, 로그 수집 위치가 문서로 정의됨 | README.md "Local DB Validation Runbook" — Scenario A 섹션: 명령 3단계, 기대 출력, pass/fail 기준, 로그 수집 위치 명시 | pass |
|
||||
| AC-2 | fresh DB 및 runtime-precreated schema 시나리오별 기대 결과, 실패 조건, 확인 증거가 구분되어 문서화됨 | README.md Scenario A (fresh DB) 및 Scenario B (App 선행 기동), docs/runtime-flows.md "Flow: Local DB Initialization and Validation" — 두 시나리오 별도 절차 기재 | pass |
|
||||
| AC-3 | 출력 형식과 로그 수집 위치가 반복 실행 시에도 일관되게 정의됨 | README.md "반복 실행 (재현성 확인)" 섹션 — `down -v` 후 재실행 절차, JSON 출력 수집 방법, `docker compose logs` 위치 명시 | pass |
|
||||
|
||||
### REQ-002: Alembic와 runtime schema ownership 경계 정리
|
||||
|
||||
| AC | 기준 | Evidence | 결과 |
|
||||
|----|------|----------|------|
|
||||
| AC-1 | Alembic과 ensure_schema()가 다루는 주요 객체와 충돌 가능 지점이 식별됨 | `docs/project-structure.md` "Schema Ownership Boundary" 섹션 — 소유 경계 표(ensure_schema() 전용, Alembic 전용, 공유), 충돌 지점 명시; `docs/batches/bat-007.../design.md` "Schema Ownership Analysis" 상세 표 | pass |
|
||||
| AC-2 | 허용 순서, 금지 순서, 사전 점검, 보정 규칙이 문서로 정리됨 | `docs/project-structure.md` "허용 실행 순서" 및 "운영 제약" 항목; `docs/runtime-flows.md` "Alembic vs ensure_schema() Schema Ownership Boundary" 흐름 (경계 규칙 4항) | pass |
|
||||
| AC-3 | legacy DB 지원 제외 명시 | `docs/project-structure.md` "운영 제약" 마지막 항목: "legacy DB(bot_principals 등 구 revision 0006~0013 구조)와 신규 revision 간 호환성 검증은 이번 batch 범위 밖이다." | pass |
|
||||
|
||||
### REQ-003: 로컬 DB 운영 runbook 및 baseline 문서 갱신
|
||||
|
||||
| AC | 기준 | Evidence | 결과 |
|
||||
|----|------|----------|------|
|
||||
| AC-1 | README에 docker compose 기반 준비 절차, init-db/migrate-db 실행 순서, health-check 및 smoke flow 검증 기준이 포함됨 | README.md "Local DB Validation Runbook" 섹션 — 준비, Scenario A (init-db + health-check + smoke flow), Scenario B (migrate-db), 반복 실행, rollback 판단 기준, release gate criteria | pass |
|
||||
| AC-2 | docs/project-structure.md와 docs/runtime-flows.md에 구조 제약과 실행 흐름 제약이 반영됨 | docs/project-structure.md "Schema Ownership Boundary" 섹션; docs/runtime-flows.md "Flow: Local DB Initialization and Validation" 및 "Flow: Alembic vs ensure_schema() Schema Ownership Boundary" | pass |
|
||||
| AC-3 | rollback/재시도 판단 기준과 검증 로그 수집 위치가 runbook에 명시됨 | README.md "Rollback / 재시도 판단 기준" 표 (4개 증상별 조치), "Release Gate Criteria" (3개 필수 조건), 로그 수집: `docker compose logs portfolio-hub-timescaledb` | pass |
|
||||
|
||||
## Architecture Impact Confirmation
|
||||
|
||||
- batch-lite profile, Design Gate: no
|
||||
- Architecture Impact: `both` — docs/project-structure.md, docs/runtime-flows.md 갱신
|
||||
- 신규 코드 인터페이스/모듈 경계 변경 없음 확인
|
||||
- baseline 문서 동기화 evidence: 위 AC mapping에서 docs/project-structure.md, docs/runtime-flows.md, README.md 변경 내용 직접 확인
|
||||
|
||||
## Evidence Summary
|
||||
|
||||
| 문서/항목 | 변경 여부 | 확인 방법 |
|
||||
|-----------|-----------|-----------|
|
||||
| `README.md` — "Local DB Validation Runbook" 섹션 | 신규 추가 | 직접 확인 완료 |
|
||||
| `docs/runtime-flows.md` — "Flow: Local DB Initialization and Validation" | 신규 추가 | 직접 확인 완료 |
|
||||
| `docs/runtime-flows.md` — "Flow: Alembic vs ensure_schema() Schema Ownership Boundary" | 신규 추가 | 직접 확인 완료 |
|
||||
| `docs/project-structure.md` — "Schema Ownership Boundary" | 신규 추가 | 직접 확인 완료 |
|
||||
| `app/repositories/postgres.py` | 변경 없음 | 코드 분석으로 확인 — ensure_schema() IF NOT EXISTS 보호 완비 |
|
||||
| `app/db/migrations.py` | 변경 없음 | 코드 분석으로 확인 — bootstrap_or_upgrade_to_head() 정상 동작 |
|
||||
| `docker-compose.local-test.yml` | 변경 없음 | 검증 시나리오 요건 충족 확인 |
|
||||
|
||||
## Blocking Issues
|
||||
|
||||
없음.
|
||||
|
||||
## Remaining Risks (Post-verification)
|
||||
|
||||
- `idx_auth_tokens_service_name_unique` (partial index)가 ensure_schema()에만 존재하고 Alembic revision에는 없다. Fresh Alembic 설치 환경에서 이 인덱스가 생성되지 않을 수 있다. 기능 영향: service token name uniqueness 보장 불완전. 후속 batch에서 Alembic revision 추가 검토 권장.
|
||||
- Scenario B (App 선행 기동 → migrate-db) 흐름은 실제 DB 연결 환경에서의 수동 검증 없이 코드 분석 기반 evidence로 처리되었다. Release 전 실제 docker compose 환경에서 확인 권장.
|
||||
@@ -1,81 +0,0 @@
|
||||
# BAT-008 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-008
|
||||
- Profile: batch-lite
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
BAT-008은 신규 코드/스키마/런타임 변경이 없는 doc-only batch다. REQ-004~010 구현(BAT-001)과 BAT-006(UUID migration) 완료 후 미반영 상태인 baseline 문서 3개를 실제 구현 결과와 동기화한다.
|
||||
|
||||
- **Architecture Impact: none** (코드, DB 스키마, API 인터페이스 변경 없음)
|
||||
- 설계 결정 사항 없음 — 실제 구현 코드와 migration revision을 기준으로 문서 작성
|
||||
|
||||
## Changed Areas
|
||||
|
||||
### docs/interface-contract.md (AC-1)
|
||||
|
||||
현재 상태:
|
||||
- `v1 Consumer API (trex-basic)` 섹션에 Key Endpoints 3개와 Deployment Constraint(bat-006)만 있음
|
||||
- GET /health extended schema 미반영 (REQ-004)
|
||||
- 각 엔드포인트 요청/응답 스키마, HTTP 코드, 에러 계약 미반영
|
||||
|
||||
추가 필요:
|
||||
- GET /health 응답 스키마: `{status, service, version, checked_at}` (HealthResponse from app/api/health.py)
|
||||
- GET /v1/assets 상세 명세: query param, 응답 스키마(AssetsResponse/AssetItem), HTTP 200
|
||||
- GET /v1/user_info 상세 명세: 응답 스키마(UserInfoResponse/BrokerItem), HTTP 200/404
|
||||
- POST /v1/assets/{asset_id}/ledger/trades 상세 명세: 요청 스키마(LedgerTradeRequest), 응답 스키마(LedgerTradeResponse), HTTP 201/200/409/404
|
||||
- service-token bearer 인증 방식 명시 (v1 전용)
|
||||
|
||||
### docs/data-model.md (AC-2)
|
||||
|
||||
현재 상태:
|
||||
- Entity: Portfolio — `market` 컬럼 누락
|
||||
- Entity: User — `auth_tokens` 관련 AuthToken entity 없음
|
||||
- broker_credentials 테이블 미문서화
|
||||
- ledger_trades 테이블 미문서화
|
||||
|
||||
추가 필요:
|
||||
- Portfolio Key Fields에 `market :: text|null` 추가
|
||||
- Entity: `AuthToken` 추가 — `id`, `user_id`, `token_type`, `runtime_id`, `permission`, `expires_at` 등 핵심 필드
|
||||
- Entity: `BrokerCredential` 추가 — 스키마 기준: `(user_id, broker)` PK, `account_ref`, `encrypted_payload`, `credential_version`, `last_rotated_at`
|
||||
- Entity: `LedgerTrade` 추가 — 스키마 기준: `ledger_entry_id` BigInt Identity PK, `asset_id`, `order_id`, `execution_id`, `side`, `quantity`, `price`, `fee_amount`, `tax_amount`, `executed_at`, `recorded_at`; `uq_ledger_trades_idempotency` unique constraint
|
||||
|
||||
### docs/runtime-flows.md (AC-3)
|
||||
|
||||
현재 상태 (bat-001에서 이미 갱신):
|
||||
- Flow: v1 API — Service-Token Authentication ✓
|
||||
- Flow: v1 API — GET /v1/assets ✓
|
||||
- Flow: v1 API — GET /v1/user_info ✓
|
||||
- Flow: v1 API — POST /v1/assets/{asset_id}/ledger/trades ✓
|
||||
- Flow: v1 API — POST /auth/service-tokens (runtime_id 발급 흐름 포함) ✓
|
||||
- Flow: Application Startup Validation ✓
|
||||
|
||||
추가 필요: Source Discovery/Batch 헤더에 bat-008 추가 + Change Log 항목 추가 (AC-3 충족 확인 기록)
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| 결정 | 내용 | 근거 |
|
||||
|------|------|------|
|
||||
| runtime-flows.md 내용 보완 불필요 | bat-001에서 v1 API 처리 흐름과 runtime_id 발급 흐름이 이미 추가됨 | 현행 파일 확인 |
|
||||
| interface-contract.md 갱신 방식 | `v1 Consumer API (trex-basic)` 섹션 내에 endpoint별 상세 명세 subsection 추가 | 기존 섹션 구조 유지 |
|
||||
| data-model.md 갱신 방식 | 기존 Portfolio entity에 컬럼 추가; 신규 entity(BrokerCredential, LedgerTrade) 추가; AuthToken entity 신규 추가 | migration 20260502_0014 기준 |
|
||||
| broker_credentials encrypted_payload | 문서에 "Fernet-encrypted ciphertext (TEXT)" 으로 명시 — 복호화는 REQ-010 참조 | 민감정보 취급 기준 |
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- `docs/runtime-flows.md`의 bat-001 추가 내용과 bat-007 추가 내용이 공존 — 섹션 충돌 없음. bat-008은 Change Log와 Source Discovery만 갱신.
|
||||
- `auth_tokens` 테이블은 bootstrap baseline에 Users entity와 함께 암묵적으로 존재하나 명시적 entity 섹션이 없음 — bat-008에서 AuthToken entity 추가.
|
||||
- `ledger_trades.asset_id`는 BAT-006 이후 UUID portfolio_id를 참조하나 TEXT 타입 — 문서에 "TEXT (UUID-format after BAT-006)" 으로 명시.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
- none
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
| 문서 | 변경 유형 | 적용 REQ AC |
|
||||
|------|----------|-------------|
|
||||
| docs/interface-contract.md | 기존 섹션 내 endpoint 상세 명세 추가 | REQ-011 AC-1 |
|
||||
| docs/data-model.md | Portfolio entity 컬럼 추가, 신규 entity 3개 추가 | REQ-011 AC-2 |
|
||||
| docs/runtime-flows.md | Source Discovery/Batch 헤더 + Change Log만 갱신 (내용은 bat-001 기준 이미 충족) | REQ-011 AC-3 |
|
||||
@@ -1,68 +0,0 @@
|
||||
# BAT-008 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-008
|
||||
- Profile: batch-lite
|
||||
|
||||
## Plan Summary
|
||||
|
||||
REQ-011 AC-1~3에 대응하는 3개 baseline 문서 갱신. 코드/스키마/런타임 변경 없음.
|
||||
|
||||
## Changed Files
|
||||
|
||||
| 파일 | 변경 유형 | 대응 AC |
|
||||
|------|----------|---------|
|
||||
| `docs/interface-contract.md` | 기존 섹션 내 endpoint 상세 명세 추가 | REQ-011 AC-1 |
|
||||
| `docs/data-model.md` | portfolios.market 컬럼 추가; AuthToken, BrokerCredential, LedgerTrade entity 추가 | REQ-011 AC-2 |
|
||||
| `docs/runtime-flows.md` | Source Discovery/Batch 헤더 + Change Log 갱신 (내용은 bat-001에서 이미 충족) | REQ-011 AC-3 |
|
||||
|
||||
## Execution Log
|
||||
|
||||
### docs/interface-contract.md
|
||||
|
||||
- Source Discovery/Batch 헤더: `bootstrap-baseline, bat-006` → `bootstrap-baseline, bat-001, bat-006, bat-008`
|
||||
- GET /health 응답 스키마 갱신: `{"status": "ok"}` 단순 설명 → `{status, service, version, checked_at}` 명시 (HealthResponse 기준)
|
||||
- `v1 Consumer API (trex-basic)` 섹션 확장:
|
||||
- `### Authentication` 추가: service-token bearer, DB-issued 조건, trade-write 권한 명시
|
||||
- `### Endpoint: GET /v1/assets` 추가: query param, AssetsResponse/AssetItem 스키마, HTTP 200/401/403
|
||||
- `### Endpoint: GET /v1/user_info` 추가: UserInfoResponse/BrokerItem 스키마, HTTP 200/401/403/404
|
||||
- `### Endpoint: POST /v1/assets/{asset_id}/ledger/trades` 추가: LedgerTradeRequest/LedgerTradeResponse 스키마, HTTP 201/200/409/404/401/403
|
||||
- `### Idempotency Contract` 추가: deduplication key, duplicate/conflict 처리 방식
|
||||
- Change Log 항목 추가
|
||||
|
||||
### docs/data-model.md
|
||||
|
||||
- Source Discovery/Batch 헤더: `bootstrap-baseline, bat-006` → `bootstrap-baseline, bat-001, bat-006, bat-008`
|
||||
- Model Summary: AuthToken, BrokerCredential, LedgerTrade 추가
|
||||
- Entity: Portfolio — `market :: text|null` 필드 추가 (migration 20260502_0014 기준)
|
||||
- Entity: `AuthToken` 신규 추가:
|
||||
- Key Fields: id, user_id, token_type, token_source, permission, service_name, runtime_id, expires_at, created_at
|
||||
- Uniqueness Constraints: idx_auth_tokens_service_name_unique, idx_auth_tokens_runtime_id_unique
|
||||
- State Rules: runtime_id 없는 토큰 v1 거부 명시
|
||||
- Entity: `BrokerCredential` 신규 추가:
|
||||
- Key Fields: user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at
|
||||
- PK: (user_id, broker)
|
||||
- State Rules: 평문 저장 금지, 복호화 실패 처리 명시
|
||||
- Entity: `LedgerTrade` 신규 추가:
|
||||
- Key Fields: ledger_entry_id, asset_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at, recorded_at
|
||||
- Unique Constraint: uq_ledger_trades_idempotency
|
||||
- Index: idx_ledger_trades_asset_executed
|
||||
- State Rules: immutable rows, payload conflict → 409
|
||||
- Change Log 항목 추가
|
||||
|
||||
### docs/runtime-flows.md
|
||||
|
||||
- Source Discovery/Batch 헤더: `bat-007` 뒤에 `bat-008` 추가
|
||||
- Change Log 항목 추가: "REQ-011 AC-3 confirmed: v1 API flows and runtime_id issuance flow already present from bat-001"
|
||||
- 내용 변경 없음 — bat-001에서 추가된 v1 API 처리 흐름 5개, Application Startup Validation, bat-007에서 추가된 Local DB 흐름 2개 모두 현행 유지
|
||||
|
||||
## Validation
|
||||
|
||||
- `docs/interface-contract.md`: REQ-011 AC-1 대상 4개 엔드포인트 명세 + 인증 방식 포함 확인 ✓
|
||||
- `docs/data-model.md`: broker_credentials, ledger_trades, portfolios.market, auth_tokens.runtime_id 모두 반영 확인 ✓
|
||||
- `docs/runtime-flows.md`: v1 API 처리 흐름(4개) + runtime_id 발급 흐름(POST /auth/service-tokens) 존재 확인 ✓
|
||||
- 코드 변경 없음 — lint/typecheck 불필요
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- `docs/runtime-flows.md` v1 API flows는 bat-001에서 최초 작성됨. 이후 구현 변경 시 해당 흐름도 갱신 필요 (현재 기준 코드와 일치 확인됨).
|
||||
@@ -1,41 +0,0 @@
|
||||
# BAT-008 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-008
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-11
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-011 | trex-basic 계약 반영을 위한 baseline 문서 갱신 | Documentation | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- [dcy-002](../../discovery/) (trex-basic v1 API contract — FR/NFR 구현 완료 후 documentation closure)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- docs/interface-contract.md: GET /health, GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades 엔드포인트 명세 및 service-token bearer 인증 방식 추가 (REQ-011 AC-1)
|
||||
- docs/data-model.md: broker_credentials, ledger_trades 테이블, portfolios.market, auth_tokens.runtime_id 컬럼 추가 (REQ-011 AC-2)
|
||||
- docs/runtime-flows.md: v1 API 처리 흐름, runtime_id 발급 흐름 추가 (REQ-011 AC-3)
|
||||
- 신규 코드/모듈/스키마 변경 없음 — 구현 완료(REQ-004~010 Implemented) 후 문서 동기화
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — v1 API 엔드포인트 명세, service-token 인증 방식 추가
|
||||
- `docs/data-model.md` — broker_credentials, ledger_trades 테이블 및 컬럼 추가
|
||||
- `docs/runtime-flows.md` — v1 API 처리 흐름 및 runtime_id 발급 흐름 추가
|
||||
- Modules: 없음
|
||||
- Tests: 없음
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Design | [design.md](./design.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | passed |
|
||||
@@ -1,70 +0,0 @@
|
||||
# BAT-008 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-008
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-011: trex-basic 계약 반영을 위한 baseline 문서 갱신
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- REQ-004~010 구현 자체 변경 (이미 Implemented 상태)
|
||||
- 신규 API 엔드포인트 또는 스키마 추가
|
||||
- runtime-flows.md 중 BAT-007에서 이미 갱신한 Alembic/ensure_schema() 경계 흐름 재작성
|
||||
- docs/project-structure.md (BAT-007 범위)
|
||||
|
||||
## 구현 순서
|
||||
|
||||
### 1단계: 현황 파악
|
||||
|
||||
- `docs/interface-contract.md` 현재 상태 확인 — v1 API 엔드포인트 명세 누락 여부 점검
|
||||
- `docs/data-model.md` 현재 상태 확인 — broker_credentials, ledger_trades, portfolios.market, auth_tokens.runtime_id 누락 여부 점검
|
||||
- `docs/runtime-flows.md` 현재 상태 확인 — v1 API 처리 흐름 및 runtime_id 발급 흐름 누락 여부 점검
|
||||
|
||||
### 2단계: docs/interface-contract.md 갱신 (REQ-011 AC-1)
|
||||
|
||||
- GET /health 응답 스키마 (extended) 추가
|
||||
- GET /v1/assets 엔드포인트 명세 추가
|
||||
- GET /v1/user_info 엔드포인트 명세 추가
|
||||
- POST /v1/assets/{asset_id}/ledger/trades 엔드포인트 명세 추가
|
||||
- service-token bearer 인증 방식 섹션 추가 또는 갱신
|
||||
|
||||
### 3단계: docs/data-model.md 갱신 (REQ-011 AC-2)
|
||||
|
||||
- broker_credentials 테이블 스키마 추가 (user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at)
|
||||
- ledger_trades 테이블 스키마 추가 (ledger_entry_id, asset_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at, recorded_at; unique constraint 포함)
|
||||
- portfolios.market 컬럼 추가
|
||||
- auth_tokens.runtime_id 컬럼 추가
|
||||
|
||||
### 4단계: docs/runtime-flows.md 갱신 (REQ-011 AC-3)
|
||||
|
||||
- v1 API 요청 처리 흐름(인증 → 서비스 → DB) 추가 (GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades)
|
||||
- service-token 발급 시 runtime_id 동시 생성 흐름 추가 (REQ-005 반영)
|
||||
- BAT-007에서 추가된 기존 섹션과 충돌 없이 통합
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- REQ-004~010: 모두 Implemented 상태 확인됨 (BAT-001 release-candidate) — 선행 조건 충족
|
||||
- `app/api/v1_assets.py`, `app/api/v1_user_info.py`, `app/api/v1_ledger.py` — 실제 구현 기준으로 명세 작성
|
||||
- `app/domain/models.py` — broker_credentials, ledger_trades 모델 기준으로 data-model 작성
|
||||
- `migrations/versions/` — Alembic revision 기준으로 컬럼/테이블 정의 확인
|
||||
- External: 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: 신규 코드/인터페이스/DB 스키마/런타임 흐름 변경 없음. 구현 완료된 REQ-004~010 결과를 기존 3개 문서에 반영하는 doc-only 작업. 구조적 결정 사항 없음.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. docs/interface-contract.md 갱신 완료 (v1 API 엔드포인트 4개 + 인증 방식)
|
||||
2. docs/data-model.md 갱신 완료 (테이블 2개 + 컬럼 2개)
|
||||
3. docs/runtime-flows.md 갱신 완료 (v1 API 처리 흐름 + runtime_id 발급 흐름)
|
||||
|
||||
## Risks
|
||||
|
||||
- `docs/runtime-flows.md`에 BAT-007에서 추가된 섹션과 중복 또는 충돌이 생길 수 있음 — 1단계 현황 파악에서 확인 후 통합 방식 결정
|
||||
- 실제 구현 코드와 문서 내용 불일치 시 코드 기준으로 작성
|
||||
@@ -1,59 +0,0 @@
|
||||
# BAT-008 Verification
|
||||
|
||||
- Status: passed
|
||||
- Batch: BAT-008
|
||||
- Profile: batch-lite
|
||||
- Architecture Impact: none (confirmed — doc-only, no code/schema/runtime changes)
|
||||
- Design Gate: no (planning.md confirmed — no structural decisions required)
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-011 AC-1: docs/interface-contract.md에 4개 엔드포인트 명세 포함
|
||||
|
||||
| Criterion | Evidence | Result |
|
||||
|-----------|----------|--------|
|
||||
| GET /health 응답 스키마 포함 | `docs/interface-contract.md` Interface: Primary HTTP API — Outputs 섹션에 `{status, service, version, checked_at}` 명시 (HealthResponse from app/api/health.py 기준) | PASS |
|
||||
| GET /v1/assets 요청/응답 명세 포함 | `docs/interface-contract.md` Endpoint: GET /v1/assets — query param, AssetsResponse/AssetItem 스키마, HTTP 200/401/403 명시 | PASS |
|
||||
| GET /v1/user_info 요청/응답 명세 포함 | `docs/interface-contract.md` Endpoint: GET /v1/user_info — UserInfoResponse/BrokerItem 스키마, HTTP 200/401/403/404 명시 | PASS |
|
||||
| POST /v1/assets/{asset_id}/ledger/trades 요청/응답 명세 포함 | `docs/interface-contract.md` Endpoint: POST /v1/assets/{asset_id}/ledger/trades — LedgerTradeRequest/LedgerTradeResponse 스키마, HTTP 201/200/409/404/401/403 명시 | PASS |
|
||||
| service-token bearer 인증 방식 포함 | `docs/interface-contract.md` Authentication 섹션 — DB-issued service token 조건, trade-write 권한 명시 | PASS |
|
||||
|
||||
**REQ-011 AC-1: PASS**
|
||||
|
||||
### REQ-011 AC-2: docs/data-model.md에 신규 테이블/컬럼 문서화
|
||||
|
||||
| Criterion | Evidence | Result |
|
||||
|-----------|----------|--------|
|
||||
| broker_credentials 테이블 문서화 | `docs/data-model.md` Entity: BrokerCredential — (user_id, broker) PK, account_ref, encrypted_payload, credential_version, last_rotated_at; migration 20260502_0014 기준 | PASS |
|
||||
| ledger_trades 테이블 문서화 | `docs/data-model.md` Entity: LedgerTrade — ledger_entry_id (BigInt Identity), asset_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at, recorded_at; uq_ledger_trades_idempotency unique constraint 명시 | PASS |
|
||||
| portfolios.market 컬럼 문서화 | `docs/data-model.md` Entity: Portfolio Key Fields — `market :: text\|null` 추가 | PASS |
|
||||
| auth_tokens.runtime_id 컬럼 문서화 | `docs/data-model.md` Entity: AuthToken — `runtime_id :: text\|null` 및 idx_auth_tokens_runtime_id_unique 명시 | PASS |
|
||||
|
||||
**REQ-011 AC-2: PASS**
|
||||
|
||||
### REQ-011 AC-3: docs/runtime-flows.md에 v1 API 처리 흐름과 runtime_id 발급 흐름 포함
|
||||
|
||||
| Criterion | Evidence | Result |
|
||||
|-----------|----------|--------|
|
||||
| v1 API 처리 흐름 포함 | `docs/runtime-flows.md` — Flow: v1 API — Service-Token Authentication, GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades (bat-001에서 추가, 현행 코드와 일치) | PASS |
|
||||
| runtime_id 발급 흐름 포함 | `docs/runtime-flows.md` — Flow: v1 API — POST /auth/service-tokens: "Calls AuthService.issue_service_token() which creates a new DB token with runtime_id" (bat-001에서 추가) | PASS |
|
||||
|
||||
**REQ-011 AC-3: PASS**
|
||||
|
||||
## Architecture Impact Confirmation
|
||||
|
||||
- batch-lite, Design Gate = no
|
||||
- 코드/DB 스키마/런타임 흐름 변경 없음
|
||||
- architecture impact: none ✓
|
||||
|
||||
## Blocking Issues
|
||||
|
||||
- 없음
|
||||
|
||||
## Ready for Release
|
||||
|
||||
- true
|
||||
|
||||
## Notes
|
||||
|
||||
- docs/runtime-flows.md의 v1 API flows는 bat-001 (BAT-001 scope) 에서 이미 작성됨. REQ-011이 명시한 "v1 API 처리 흐름과 runtime_id 발급 흐름" 조건은 bat-001 결과로 충족되어 있었으며, bat-008은 이를 공식 확인하고 Source Discovery를 갱신함.
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
# BAT-009 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-009
|
||||
|
||||
## Plan Summary
|
||||
|
||||
README.md `Production Operator Runbook` 섹션에서 REQ-020/021/022의 AC gap 3개 항목을 추가. 신규 코드 변경 없음.
|
||||
|
||||
## Changed Files
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|---|---|
|
||||
| `README.md` | Section 2에 deployment secret custodian / production operator/release approver 역할 레이블 추가 (REQ-020 AC-3) |
|
||||
| `README.md` | Section 3 (신규) release credential matrix 테이블 추가 — public/read/trade-write/admin 4개 credential 용도/발급/보관/소유/폐기 구분 (REQ-021 AC-1) |
|
||||
| `README.md` | Section 6 (구 Section 5) 확장 — canonical smoke 환경 명시(production canonical, staging optional preflight), 401/403/404 오류 구분 판독 추가, release evidence 기록 필드 명시 (REQ-022 AC-2, AC-4) |
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. README.md 현재 Production Operator Runbook 섹션 내용 파악 (Section 1~5 확인)
|
||||
2. `app/main.py` `_bootstrap_initial_user()` — `if repository.list_users(): return` 코드로 bootstrap idempotency 확인 (REQ-020 AC-2)
|
||||
3. Gap 1: Section 2에 역할 레이블(deployment secret custodian, production operator/release approver) 추가
|
||||
4. Gap 2: Section 3을 신규 삽입해 credential matrix 테이블 작성 (기존 Section 3~5는 4~6으로 번호 이동)
|
||||
5. Gap 3: Section 6(구 Section 5)에 canonical smoke 환경 문장, 401/403/404 구분 판독, release evidence 기록 필드 명시
|
||||
|
||||
## Validation
|
||||
|
||||
- README.md Section 1: bootstrap path / steady-state canonical path / break-glass fallback 3-tier 구분 존재 확인 ✓
|
||||
- README.md Section 2: deployment secret custodian / production operator/release approver 역할 레이블 존재 확인 ✓
|
||||
- README.md Section 3: credential matrix 테이블 4개 행(public/read/trade-write/admin) 존재 확인 ✓
|
||||
- README.md Section 4: /auth/login → POST /auth/service-tokens 흐름 존재 확인 ✓
|
||||
- README.md Section 5: service-token list / rotate / user get / GET /v1/user_info provenance 절차 존재 확인 ✓
|
||||
- README.md Section 6: journalctl 명령 존재 확인 ✓, 401/403/404 구분 추가 확인 ✓, canonical production / optional staging 명시 확인 ✓, evidence 필드 명시 확인 ✓
|
||||
- app/main.py bootstrap idempotency: `if repository.list_users(): return` 확인 ✓
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- `/auth/login` 엔드포인트가 현재 저장소에 구현되어 있는지 확인 필요. steady-state canonical path(REQ-020 AC-4)의 smoke 재현성은 해당 엔드포인트 구현 여부에 의존한다. 현재는 문서화만 가능한 상태로 처리.
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
# BAT-009 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-009
|
||||
- Profile: batch-lite
|
||||
- Created: 2026-05-12
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-020 | 실배포 admin bootstrap 및 steady-state operator access runbook | Deployment | High |
|
||||
| REQ-021 | release 검증 credential matrix 및 token provenance 운영 절차 | Documentation | High |
|
||||
| REQ-022 | 운영 로그 접근 및 release evidence 기록 절차 | Documentation | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- [dcy-005](../../discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md) (실배포 admin bootstrap 및 operator access 경로 정립)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- README.md `Production Operator Runbook` 섹션 보강
|
||||
- admin bootstrap path / steady-state canonical path / break-glass fallback 3-tier 경로 구분 및 조건 명시 (REQ-020)
|
||||
- public/read/trade-write/admin credential matrix, token provenance CLI 절차, release evidence 기록 필드 정의 (REQ-021)
|
||||
- journalctl 기반 로그 접근 절차, 401/403/404 구분 판독, canonical smoke 환경 명시 (REQ-022)
|
||||
- 신규 코드/모듈/스키마 변경 없음 — 운영 문서화 전용
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Docs:
|
||||
- `README.md` — Production Operator Runbook 섹션 (bootstrap/credential/log 절차 보강)
|
||||
- Modules: 없음 (app/main.py bootstrap idempotency는 기존 구현 확인으로 대체)
|
||||
- Tests: 없음
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
# BAT-009 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-009
|
||||
- Profile: batch-lite
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-020: 실배포 admin bootstrap 및 steady-state operator access runbook
|
||||
- REQ-021: release 검증 credential matrix 및 token provenance 운영 절차
|
||||
- REQ-022: 운영 로그 접근 및 release evidence 기록 절차
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
1. README.md 현재 `Production Operator Runbook` 섹션 내용 확인
|
||||
2. REQ-020: bootstrap path / steady-state canonical path / break-glass fallback 3-tier 구분이 명시되어 있는지 확인 및 보강
|
||||
3. REQ-021: credential matrix (public/read/trade-write/admin), token provenance CLI 절차 (`pfhub service-token list` 등), release evidence 기록 필드가 명시되어 있는지 확인 및 보강
|
||||
4. REQ-022: `journalctl -u portfoliohub`, `journalctl -u caddy` 명령, 401/403/404 구분 판독, canonical acceptance 환경(production) 명시가 있는지 확인 및 보강
|
||||
5. app/main.py bootstrap idempotency 코드 리뷰로 REQ-020 AC-2 검증
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- `README.md` — 현재 Production Operator Runbook 섹션 존재 (REL-001 이후 초안 추가됨)
|
||||
- `app/main.py` — bootstrap idempotency 로직 확인 대상
|
||||
- `app/core/config.py` — `PORTFOLIOHUB_BOOTSTRAP_*` 환경 변수 확인 대상
|
||||
- External:
|
||||
- 없음
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: no
|
||||
- Reason: README.md 단일 문서 보강 작업이며 구조/인터페이스/런타임 흐름 변경이 없음. 현재 섹션 내용 확인 후 누락 항목만 추가하는 방식으로 진행 가능.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. README.md 현재 Operator Runbook 섹션 내용 파악 및 gap 식별
|
||||
2. REQ-020/021/022 AC 기준 누락 항목 보강 완료
|
||||
3. verification.md 작성 및 AC 매핑 완료
|
||||
|
||||
## Risks
|
||||
|
||||
- README.md에 이미 대부분의 내용이 존재할 수 있음 — 현재 내용 확인 후 실제 gap이 작으면 구현 범위 축소 가능
|
||||
- `/auth/login` 엔드포인트가 실제 구현되어 있지 않으면 REQ-020 AC-4(steady-state canonical path rehearsal)는 문서화만 가능하고 실제 smoke 재현 불가 — Notes에 전제 명시로 대응
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
# BAT-009 Verification
|
||||
|
||||
- Status: passed
|
||||
- Batch: BAT-009
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-020:
|
||||
- AC-1: Given README.md Production Operator Runbook이 존재할 때, bootstrap path / steady-state canonical path / break-glass fallback이 각각 사용 조건과 함께 구분되어 기술되어 있어야 한다.
|
||||
- Evidence: README.md Section 1 — steady-state(line 351), bootstrap path(line 353), break-glass fallback(line 354) 각각 사용 조건과 함께 존재 ✓
|
||||
- AC-2: users 테이블이 비어 있지 않은 환경에서 bootstrap 로직이 기존 사용자를 덮어쓰지 않아야 한다.
|
||||
- Evidence: `app/main.py` `_bootstrap_initial_user()` — `if repository.list_users(): return` (line 33) — 사용자 존재 시 즉시 반환, idempotency 보장 ✓
|
||||
- AC-3: deployment secret custodian과 production operator/release approver 역할이 구분되어 있고 secret 보관 위치가 명시되어 있어야 한다.
|
||||
- Evidence: README.md Section 2 — **deployment secret custodian** 역할 레이블(line 360), **production operator / release approver** 역할 레이블(line 361), 보관 위치 `/opt/portfoliohub/.env` 명시 ✓
|
||||
- AC-4: steady-state canonical path `/auth/login` → `POST /auth/service-tokens` 흐름이 runbook에 기술되어 있어야 한다.
|
||||
- Evidence: README.md Section 4 — 5단계 smoke flow에 admin session token 확보 → service-token 발급 흐름 및 curl 예시 포함 ✓
|
||||
|
||||
- REQ-021:
|
||||
- AC-1: public/read/trade-write/admin 4개 credential의 용도, 발급 주체, 보관 위치, 공동 소유 역할이 구분되어 기술되어 있어야 한다.
|
||||
- Evidence: README.md Section 3 credential matrix 테이블 — 4개 credential 행(public/no-auth, read service-token, trade-write service-token, admin auth context), 용도/발급/보관/소유/폐기 열 구분 ✓
|
||||
- AC-2: release 문서에 name/token_prefix/permission/runtime_id/issued_at/revoked_at만 기록하고 raw token 값은 포함되지 않아야 한다.
|
||||
- Evidence: README.md Section 2 — "release 문서와 운영 메모에는 token name, token_prefix, permission, runtime_id, issued_at, revoked_at만 남기고 raw secret은 남기지 않는다"(line 368) ✓; Section 6 evidence 기록 필드에도 동일 제약 명시 ✓
|
||||
- AC-3: `pfhub service-token list` 실행 시 필드가 출력되고 runtime_id 대조 가능해야 한다.
|
||||
- Evidence: README.md Section 5 — `pfhub service-token list --user <username>`으로 name/token_prefix/token_id/user_id/permission/created_at/last_used_at/revoked_at 확인, `GET /v1/user_info`로 runtime_id 대조 절차 명시 ✓
|
||||
- AC-4: admin context는 개인 소유, read/trade-write는 release approver 역할과 공동 소유임이 문서에 명시되어 있어야 한다.
|
||||
- Evidence: README.md Section 2 — admin 개인 소유(line 363), read/trade-write 공동 소유(release approver/repository-maintainers)(line 364) ✓; Section 3 credential matrix 소유 형태 열 ✓
|
||||
|
||||
- REQ-022:
|
||||
- AC-1: `journalctl -u portfoliohub`와 `journalctl -u caddy` 각각의 용도가 명시되어 있어야 한다.
|
||||
- Evidence: README.md Section 6 — portfoliohub: "startup 및 애플리케이션 로그"(line 418), caddy: "reverse proxy 또는 access 수준 로그"(line 419) ✓
|
||||
- AC-2: 401/403/404 의미가 분리되어 재현 가능하게 설명되어 있어야 한다.
|
||||
- Evidence: README.md Section 6 HTTP 오류 분류 — 401(인증 정보 없음/만료), 403(권한 부족), 404(리소스 없음) 각각 정의 및 예시 포함(line 424~426) ✓
|
||||
- AC-3: release evidence에 시각/endpoint/HTTP status code/source system 필드가 포함되고 raw secret이 제외되어야 한다.
|
||||
- Evidence: README.md Section 6 Release evidence 기록 필드 — 포함 필드(시각, endpoint, HTTP status code, source system, token name, token_prefix, runtime_id) 및 제외(raw secret, raw token) 명시 ✓
|
||||
- AC-4: production이 canonical acceptance이고 staging은 optional preflight임이 명시되어 있어야 한다.
|
||||
- Evidence: README.md Section 6 — "production을 canonical acceptance 환경으로 사용한다. staging smoke는 optional preflight로만 사용하며 release acceptance 기준이 되지 않는다"(line 414) ✓
|
||||
|
||||
## Evidence
|
||||
|
||||
- Manual:
|
||||
- README.md Section 1~6 항목별 내용 직접 확인
|
||||
- `app/main.py` `_bootstrap_initial_user()` 코드 리뷰 — `if repository.list_users(): return` idempotency 확인
|
||||
- credential matrix 테이블 4개 행 및 6개 열 구조 확인
|
||||
- 401/403/404 구분 판독 텍스트 존재 확인
|
||||
- canonical smoke environment 및 staging optional preflight 문장 존재 확인
|
||||
|
||||
## Result
|
||||
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
- Notes:
|
||||
- `/auth/login` 엔드포인트 구현 여부 미확인. REQ-020 AC-4는 runbook에 문서화되어 있으나, steady-state path의 실제 smoke 재현은 해당 엔드포인트 구현 후 가능. release gate blocker는 아니며 후속 운영 환경에서 확인 권장.
|
||||
@@ -1,55 +0,0 @@
|
||||
# BAT-010 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-010
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
daemon identity 모델을 도입해 service-token과 asset에 stable `daemon_id`를 부여하고, `/v1/assets`와 `/v1/assets/{asset_id}/ledger/trades`의 authorization 축을 user-scope에서 daemon-scope로 전환한다. 기존 service-token bearer auth와 permission 모델은 유지하고, daemon assignment는 그 위에 추가되는 두 번째 authorization 축이다. 전환 구간에서는 NULL backfill을 허용하고 fail-closed로 처리한다.
|
||||
|
||||
## Changed Areas
|
||||
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — daemon binding, daemon-scoped contract, 403 mismatch policy
|
||||
- `docs/data-model.md` — daemons 테이블, auth_tokens.daemon_id, portfolios.managed_by_daemon_id
|
||||
- `docs/runtime-flows.md` — daemon registry binding 흐름, rotate invariant, daemon-scoped v1 API 흐름
|
||||
- `README.md` — daemon provisioning runbook, enforcement enablement gate
|
||||
- Modules:
|
||||
- `migrations/versions/` — daemons, auth_tokens.daemon_id, portfolios.managed_by_daemon_id migration
|
||||
- `app/domain/models.py` — Daemon entity 추가
|
||||
- `app/repositories/postgres.py` — daemon CRUD methods
|
||||
- `app/repositories/portfolio_repository.py` — daemon-scoped asset query
|
||||
- `app/services/auth_service.py` — daemon binding at issue/rotate
|
||||
- `app/api/auth_routes.py` (또는 신규 daemon route) — daemon registry management endpoint
|
||||
- `app/api/v1_assets.py` — daemon-scoped GET, daemon assignment auth for ledger write
|
||||
- `app/api/v1_user_info.py` — daemon_id 필드 노출
|
||||
- `app/schemas/v1.py` — daemon_id 필드 추가
|
||||
- `app/cli/commands/service_tokens.py` — daemon_id binding at issue, daemon_id invariant at rotate, daemon_id in output
|
||||
- Interfaces:
|
||||
- Daemon registry management: HTTP endpoint(`POST /daemons`, `GET /daemons`, `PATCH /daemons/{daemon_id}`) 또는 CLI subcommand(`pfhub daemon create/list/update`) — **아래 Key Decisions 참조**
|
||||
- `POST /auth/service-tokens` request body: `daemon_id` 필드 추가
|
||||
- `GET /v1/assets` response: daemon-scoped 필터
|
||||
- `GET /v1/user_info` response: `daemon_id` 필드 추가
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades`: daemon assignment 검증 추가
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **Daemon registry 관리 경로 형태**: CLI subcommand(`pfhub daemon create/list/update`)를 1차 관리 경로로 채택. 운영자 중심 도구이고, HTTP endpoint로 노출 시 별도 admin-only auth guard 설계가 필요하다. HTTP endpoint는 향후 필요 시 추가한다.
|
||||
2. **daemon_slug uniqueness**: owner user 내에서 unique. DB unique constraint(`daemons.owner_user_id + daemons.daemon_slug` composite unique)로 강제.
|
||||
3. **enforcement enablement gate**: migration level에서 별도 flag 없이 진행. NULL backfill token/asset에 대한 fail-closed는 코드 레벨에서 처리. production cutover는 inventory + dry-run + release approver 승인 후 수동으로 진행.
|
||||
4. **NULL backfill fail-closed boundary**: `daemon_id = NULL`인 token이 daemon-scoped endpoint(`GET /v1/assets`, `POST /v1/assets/{asset_id}/ledger/trades`)를 호출하면 403 Forbidden. `GET /v1/user_info`는 NULL이어도 200 OK + `daemon_id: null` 진단 노출.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- 동일 user 아래 daemon이 없는 상태에서 service-token 발급 시: daemon registry에 daemon이 없으면 발급 불가. 운영자가 먼저 daemon create를 수행해야 한다.
|
||||
- rotate 중 daemon이 삭제된 경우: daemon FK 참조 무결성 위반으로 rotate 거부. 소프트 삭제(is_active flag) 방식으로 daemon을 비활성화하는 것이 안전하다.
|
||||
- managed_by_daemon_id가 설정된 asset을 다른 daemon으로 재배정: 명시적 reassignment API/CLI 또는 migration-level 절차로만 허용. 자동 재배정 없음.
|
||||
|
||||
## Architecture Impact
|
||||
|
||||
- runtime-flows
|
||||
|
||||
## Reference Doc Update Plan
|
||||
|
||||
- project-structure.md: none (파일 구조 변경 없음)
|
||||
- runtime-flows.md: 갱신 필요 — daemon registry binding 흐름, daemon-scoped v1 API 처리 흐름, rotate `runtime_id`/`daemon_id` 역할 차이 반영 (REQ-011 범위)
|
||||
@@ -1,72 +0,0 @@
|
||||
# BAT-010 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-010
|
||||
|
||||
## Plan Summary
|
||||
|
||||
Implemented the full daemon model: Daemon entity (registry CRUD), daemon_id binding on service-token issuance, daemon_id rotate-invariant on token rotation, daemon-scoped authorization on GET /v1/assets and POST /v1/assets/{asset_id}/ledger/trades (fail-closed for NULL daemon_id), daemon_id diagnostic on GET /v1/user_info (NULL → 200+null), CLI daemon subcommand (create/list/update), CLI service-token rotate command, Alembic migration 20260513_0016, and docs updates across data-model.md, interface-contract.md, runtime-flows.md, and README.md.
|
||||
|
||||
## Changed Files
|
||||
|
||||
### New Files
|
||||
- `migrations/versions/20260513_0016_daemon_model.py` — Alembic revision; creates daemons table, adds auth_tokens.daemon_id and portfolios.managed_by_daemon_id
|
||||
- `app/cli/commands/daemon.py` — CLI daemon subcommand (create, list, update)
|
||||
- `tests/test_daemon_registry.py` — 12 unit tests (REQ-023, REQ-005, REQ-012)
|
||||
- `tests/test_daemon_assignment.py` — 8 integration tests (REQ-006, REQ-007, REQ-008)
|
||||
|
||||
### Modified Files
|
||||
- `app/domain/models.py` — Added Daemon dataclass; added daemon_id to AuthToken, TokenPrincipal, Portfolio
|
||||
- `app/core/auth.py` — Added daemon_id to AuthContext; propagated from principal
|
||||
- `app/schemas/v1.py` — UserInfoResponse: runtime_id optional, daemon_id added
|
||||
- `app/schemas/auth.py` — IssueServiceTokenRequest/ApiResponse/CliResponse: daemon_id added
|
||||
- `app/repositories/portfolio_repository.py` — Protocol: create_auth_token(daemon_id), list_portfolios(managed_by_daemon_id), list_portfolios_with_strategies, rotate_service_token, daemon CRUD methods
|
||||
- `app/repositories/postgres.py` — _SELECT_AUTH_TOKEN/TOKEN_PRINCIPAL/PORTFOLIO queries updated; create_auth_token(daemon_id), get_token_principal(daemon_id), list_portfolios(managed_by_daemon_id), list_portfolios_with_strategies, rotate_service_token, ensure_schema (daemons table + daemon_id columns), _row_to_auth_token/portfolio/daemon, daemon CRUD
|
||||
- `app/services/auth_service.py` — issue_service_token(daemon_id with validation), rotate_service_token
|
||||
- `app/api/v1_assets.py` — NULL daemon_id guard (HTTP 403); daemon-scoped list_portfolios_with_strategies
|
||||
- `app/api/v1_user_info.py` — Removed 404 for null runtime_id; added daemon_id to response
|
||||
- `app/api/v1_ledger.py` — 3-step daemon assignment authorization (NULL token, unassigned asset, mismatch)
|
||||
- `app/cli/commands/service_tokens.py` — issue: added --daemon-id option; added rotate command
|
||||
- `app/cli/main.py` — Registered daemon_app typer
|
||||
- `tests/test_auth.py` — FakeRepo.create_auth_token: added daemon_id param
|
||||
- `tests/test_v1.py` — Mock repos: added daemon_id; all asset/ledger TokenPrincipal: added daemon_id="d-1"; Portfolio mocks: added managed_by_daemon_id="d-1"; user_info null-runtime_id tests updated (404→200+null)
|
||||
- `tests/test_cli.py` — FakeRepo.create_auth_token: added daemon_id param
|
||||
- `docs/data-model.md` — Added Daemon entity; updated Portfolio/AuthToken fields; updated Change Log
|
||||
- `docs/interface-contract.md` — Added Daemon Authorization Model; updated all v1 endpoints; updated CLI surface; updated Change Log
|
||||
- `docs/runtime-flows.md` — Added daemon registry and rotate invariant flows; updated v1 endpoint flows; updated Alembic boundary; updated Change Log
|
||||
- `docs/batches/bat-010_20260513_daemon-model-implementation/design.md` — Status: confirmed
|
||||
|
||||
## Execution Log
|
||||
|
||||
1. Added `Daemon` dataclass to `app/domain/models.py`; added `daemon_id` fields to `AuthToken`, `TokenPrincipal`, `Portfolio`.
|
||||
2. Updated `AuthContext` in `app/core/auth.py` to carry `daemon_id`; propagated from `_resolve_auth_context`.
|
||||
3. Extended `UserInfoResponse` in `app/schemas/v1.py`: `runtime_id` optional, `daemon_id` added.
|
||||
4. Extended auth schemas in `app/schemas/auth.py`: `daemon_id` on issue request and responses.
|
||||
5. Extended `PortfolioRepository` protocol with daemon CRUD, `rotate_service_token`, and updated signatures.
|
||||
6. Implemented full daemon support in `app/repositories/postgres.py`: updated SELECT queries, ensure_schema DDL, row mappers, create_auth_token, get_token_principal, list_portfolios, list_portfolios_with_strategies, rotate_service_token, and all daemon CRUD methods.
|
||||
7. Updated `AuthService.issue_service_token` to accept and validate `daemon_id`; added `rotate_service_token`.
|
||||
8. Updated `v1_assets.py`: NULL daemon_id → 403; `list_portfolios_with_strategies` with `managed_by_daemon_id=auth.daemon_id`.
|
||||
9. Updated `v1_user_info.py`: removed 404 for null runtime_id; added `daemon_id=auth.daemon_id` to response.
|
||||
10. Updated `v1_ledger.py`: 3-step daemon assignment authorization before `insert_ledger_trade`.
|
||||
11. Created `app/cli/commands/daemon.py` with `create`, `list`, `update` subcommands.
|
||||
12. Updated `app/cli/commands/service_tokens.py`: `--daemon-id` option on issue; added `rotate` command.
|
||||
13. Registered `daemon_app` in `app/cli/main.py`.
|
||||
14. Created `migrations/versions/20260513_0016_daemon_model.py` with idempotent DDL guards.
|
||||
15. Fixed `tests/test_auth.py` and `tests/test_cli.py` FakeRepo.create_auth_token signatures.
|
||||
16. Fixed `tests/test_v1.py`: all asset/ledger TokenPrincipal + Portfolio mocks updated; null-runtime_id tests renamed and corrected.
|
||||
17. Created `tests/test_daemon_registry.py` (12 tests: daemon CRUD + token binding + rotate invariant).
|
||||
18. Created `tests/test_daemon_assignment.py` (8 tests: v1 daemon-scoped auth).
|
||||
19. Updated `docs/data-model.md`, `docs/interface-contract.md`, `docs/runtime-flows.md`, `README.md`.
|
||||
|
||||
## Validation
|
||||
|
||||
- `pytest` run: **128 passed, 7 failed** (7 failures are pre-existing in test_cli.py; confirmed via git stash baseline check before BAT-010 changes).
|
||||
- All 20 new daemon tests pass (test_daemon_registry.py: 12, test_daemon_assignment.py: 8).
|
||||
- All existing tests in test_auth.py, test_v1.py, test_portfolios.py, test_strategy_sets.py continue to pass.
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- Pre-existing 7 test failures in test_cli.py (out of BAT-010 scope; tracked separately).
|
||||
- Production enforcement gate requires manual operator action: NULL token inventory + dry-run + release approver approval before deployment (documented in README Daemon Provisioning Runbook).
|
||||
- `managed_by_daemon_id` assignment to existing portfolios requires DB migration or manual update; no CLI path yet for daemon assignment on existing portfolios.
|
||||
- Real DB integration test against TimescaleDB with migration 20260513_0016 not yet run (env not available in this batch).
|
||||
@@ -1,55 +0,0 @@
|
||||
# BAT-010 Index
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-010
|
||||
- Profile: standard
|
||||
- Created: 2026-05-13
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | 제목 | Type | Priority |
|
||||
|-----|------|------|----------|
|
||||
| REQ-009 | v1 API 지원을 위한 DB 스키마 migration 추가 (daemon schema 포함) | Migration | High |
|
||||
| REQ-023 | Daemon Registry 관리 경로 | Interface | High |
|
||||
| REQ-005 | service-token 발급 시 runtime_id + daemon_id binding | Interface | High |
|
||||
| REQ-012 | service-token rotate CLI — daemon_id 불변식 및 출력 | Interface | Medium |
|
||||
| REQ-006 | GET /v1/assets daemon-scoped 엔드포인트 | Interface | High |
|
||||
| REQ-007 | GET /v1/user_info daemon_id 필드 및 NULL diagnostic | Interface | High |
|
||||
| REQ-008 | POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization | Interface | High |
|
||||
| REQ-011 | baseline docs daemon-aware 갱신 (interface-contract, data-model, runtime-flows) | Documentation | Medium |
|
||||
| REQ-028 | README.md Daemon Provisioning Runbook 반영 | Documentation | Medium |
|
||||
|
||||
## Source Discovery
|
||||
|
||||
- [dcy-006](../../discovery/dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment.md) (daemon_id 도입 및 1자산 1데몬 자산 배정 모델 정립) — 주 출처
|
||||
- dcy-002 (REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-012 원출처 — CHG-20260513-01로 범위 재정의)
|
||||
|
||||
## Scope Summary
|
||||
|
||||
- Daemon 모델 전체를 한 번에 구현·검증·배포하는 단일 batch
|
||||
- `daemons` 테이블, `auth_tokens.daemon_id`, `portfolios.managed_by_daemon_id` Alembic migration (REQ-009)
|
||||
- daemon registry CRUD 관리 경로 — daemon create/update/list (REQ-023)
|
||||
- service-token 발급 daemon binding + unknown daemon 거부 (REQ-005)
|
||||
- service-token rotate daemon_id 불변식 + CLI daemon_id 출력 (REQ-012)
|
||||
- GET /v1/assets daemon-scoped 응답 + NULL token fail-closed (REQ-006)
|
||||
- GET /v1/user_info daemon_id 필드 + NULL diagnostic 노출 (REQ-007)
|
||||
- POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization (REQ-008)
|
||||
- interface-contract, data-model, runtime-flows daemon-aware 갱신 (REQ-011)
|
||||
- README.md daemon provisioning runbook + enforcement enablement gate (REQ-028)
|
||||
|
||||
## Impacted Area
|
||||
|
||||
- Migrations: `migrations/versions/` (daemons table, auth_tokens.daemon_id, portfolios.managed_by_daemon_id)
|
||||
- Modules: `app/domain/models.py`, `app/repositories/postgres.py`, `app/repositories/portfolio_repository.py`, `app/services/auth_service.py`, `app/api/auth_routes.py`, `app/api/v1_assets.py`, `app/api/v1_user_info.py`, `app/cli/commands/service_tokens.py`
|
||||
- Schemas: `app/schemas/v1.py`
|
||||
- Docs: `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`, `README.md`
|
||||
- Tests: `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_v1.py`, 신규 `tests/test_daemon_registry.py`, 신규 `tests/test_daemon_assignment.py`
|
||||
|
||||
## Stage Documents
|
||||
|
||||
| Stage | File | Status |
|
||||
|-------|------|--------|
|
||||
| Planning | [planning.md](./planning.md) | confirmed |
|
||||
| Design | [design.md](./design.md) | confirmed |
|
||||
| Implementation | [implementation.md](./implementation.md) | completed |
|
||||
| Verification | [verification.md](./verification.md) | completed |
|
||||
@@ -1,62 +0,0 @@
|
||||
# BAT-010 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-010
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-009: v1 API DB 스키마 migration (daemon schema 포함)
|
||||
- REQ-023: Daemon Registry 관리 경로
|
||||
- REQ-005: service-token 발급 daemon binding
|
||||
- REQ-012: service-token rotate daemon_id 불변식
|
||||
- REQ-006: GET /v1/assets daemon-scoped
|
||||
- REQ-007: GET /v1/user_info daemon_id 필드
|
||||
- REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization
|
||||
- REQ-011: baseline docs daemon-aware 갱신
|
||||
- REQ-028: README.md daemon provisioning runbook
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
의존 순서에 따라 아래 순서로 구현한다.
|
||||
|
||||
1. **REQ-009** — Alembic migration: `daemons` 테이블 신규 생성, `auth_tokens.daemon_id` FK 컬럼 추가, `portfolios.managed_by_daemon_id` FK 컬럼 추가, 기존 레코드 NULL backfill. 기존 revision chain 및 `IF NOT EXISTS` 정책 준수.
|
||||
2. **REQ-023** — Daemon registry CRUD: domain model(`Daemon` entity), repository(`create_daemon`, `get_daemon_by_slug`, `list_daemons_for_user`, `update_daemon`), management route 또는 CLI subcommand. daemon_slug owner-unique validation.
|
||||
3. **REQ-005** — service-token issuance daemon binding: `POST /auth/service-tokens`와 `pfhub service-token issue`가 existing daemon registry entry에 1:1 binding. unknown daemon 입력 명시적 거부.
|
||||
4. **REQ-012** — service-token rotate daemon_id 불변식: rotate 시 `token_hash`, `token_prefix`, `runtime_id`만 갱신, `daemon_id` 유지. CLI rotate 출력에 `daemon_id` 포함.
|
||||
5. **REQ-006** — GET /v1/assets daemon-scoped: `managed_by_daemon_id = token.daemon_id` 필터 적용. NULL token 403 fail-closed.
|
||||
6. **REQ-007** — GET /v1/user_info daemon_id 필드: 응답 스키마에 `daemon_id` 추가. NULL backfill token은 `daemon_id: null` 진단 노출.
|
||||
7. **REQ-008** — POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization: `managed_by_daemon_id = token.daemon_id` 검증. mismatch/미배정/NULL token 403 fail-closed.
|
||||
8. **REQ-011** — baseline docs 갱신: `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`에 daemon binding/assignment/403 policy/rotate invariant 반영.
|
||||
9. **REQ-028** — README.md 갱신: daemon provisioning 절차, enforcement enablement gate 4종 evidence, runtime_id/daemon_id 역할 구분 반영.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Internal:
|
||||
- REQ-009 migration은 REQ-023~028 구현 전에 schema가 존재해야 한다.
|
||||
- REQ-023 daemon registry가 구축된 후 REQ-005/012 binding 구현이 가능하다.
|
||||
- REQ-005/012가 daemon binding을 제공한 후 REQ-006/007/008 daemon-scoped authorization이 가능하다.
|
||||
- REQ-011/028 문서화는 구현 단계 마지막 또는 구현 완료 직후 수행한다.
|
||||
- External:
|
||||
- 기존 Alembic revision chain (최신 head revision과 연결 필요)
|
||||
- 기존 service-token bearer auth 모델 유지 (read/trade-write/admin permission 불변)
|
||||
|
||||
## Design Gate
|
||||
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 신규 `Daemon` 엔티티의 API transport 형태(HTTP endpoint vs CLI subcommand vs 혼합), daemon_slug owner-unique constraint 설계, enforcement enablement gate 활성화 방식, NULL backfill 기간 동안의 fail-closed boundary 정의 등 구현 착수 전 확정이 필요한 설계 결정이 있다.
|
||||
|
||||
## Milestones
|
||||
|
||||
1. Design confirmed — daemon registry interface 형태, enforcement gate 활성화 방식 확정
|
||||
2. REQ-009 migration 적용 + pytest 통과
|
||||
3. REQ-023 daemon registry CRUD 구현 + targeted test 통과
|
||||
4. REQ-005/012 daemon binding/rotate 구현 + targeted test 통과
|
||||
5. REQ-006/007/008 daemon-scoped API 구현 + daemon A/B 분리 시나리오 테스트 통과
|
||||
6. REQ-011/028 문서 갱신 완료 + 전체 pytest 통과
|
||||
7. Verification — 전체 AC evidence 매핑 완료, ready for release
|
||||
|
||||
## Risks
|
||||
|
||||
- R-1 (High): NULL backfill 상태에서 daemon-scoped enforcement를 켜면 기존 service-token/asset이 연쇄 403에 직면해 운영 중단 발생 가능. 완화: enforcement는 inventory 완료 + dry-run zero-active-NULL/zero-mismatch + release approver 승인 이후에만 활성화.
|
||||
- R-2 (Medium): daemon registry interface 형태 결정이 늦어지면 REQ-005/023 구현이 지연됨. 완화: design 단계에서 먼저 확정.
|
||||
- R-3 (Medium): 기존 Alembic chain과 migration 충돌. 완화: 신규 revision을 기존 head에 선형으로 연결, `alembic history` 정상 여부 migration test에서 확인.
|
||||
@@ -1,132 +0,0 @@
|
||||
# BAT-010 Verification
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-010
|
||||
|
||||
## Test Suite Results
|
||||
|
||||
```
|
||||
7 failed, 128 passed in 4.88s
|
||||
```
|
||||
|
||||
- 7 failures: all pre-existing in `tests/test_cli.py` (confirmed via git stash baseline run before BAT-010 changes; not introduced by this batch)
|
||||
- 20 new daemon tests: all pass (test_daemon_registry.py: 12, test_daemon_assignment.py: 8)
|
||||
- All prior tests in test_auth.py, test_v1.py, test_portfolios.py, test_strategy_sets.py: pass
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-009: v1 API DB 스키마 migration (daemon schema 포함)
|
||||
|
||||
- AC-1: `daemons` 테이블이 신규 생성된다.
|
||||
- Evidence: `migrations/versions/20260513_0016_daemon_model.py` — `op.execute("CREATE TABLE IF NOT EXISTS daemons ...")`; `app/repositories/postgres.py` ensure_schema() DDL guard
|
||||
- AC-2: `auth_tokens.daemon_id` TEXT 컬럼이 추가된다.
|
||||
- Evidence: migration `ADD COLUMN IF NOT EXISTS daemon_id TEXT`; ensure_schema() `ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS daemon_id TEXT`
|
||||
- AC-3: `portfolios.managed_by_daemon_id` TEXT 컬럼이 추가된다.
|
||||
- Evidence: migration `ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT`; ensure_schema() `ALTER TABLE portfolios ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT`
|
||||
- AC-4: 기존 레코드는 NULL backfill로 처리된다 (명시적 UPDATE 불필요 — nullable column default).
|
||||
- Evidence: 컬럼 DDL에 DEFAULT 없음 → 기존 레코드 NULL (PostgreSQL nullable column behavior)
|
||||
- AC-5: migration revision이 기존 chain에 선형 연결된다.
|
||||
- Evidence: `down_revision = "20260511_0015"` in 20260513_0016_daemon_model.py
|
||||
|
||||
### REQ-023: Daemon Registry 관리 경로
|
||||
|
||||
- AC-1: `pfhub daemon create <slug> --user <username>` 가 daemon을 생성하고 daemon_id를 반환한다.
|
||||
- Evidence: `app/cli/commands/daemon.py` create_daemon(); `tests/test_daemon_registry.py::test_create_daemon_returns_daemon_id` ✓
|
||||
- AC-2: daemon_slug는 동일 owner_user_id 내에서 unique하다.
|
||||
- Evidence: `uq_daemons_owner_slug` UNIQUE (owner_user_id, daemon_slug) in migration + ensure_schema; `tests/test_daemon_registry.py::test_create_daemon_duplicate_slug_raises` ✓
|
||||
- AC-3: `pfhub daemon list --user <username>` 가 해당 user 소유 daemon만 반환한다.
|
||||
- Evidence: `app/repositories/postgres.py` list_daemons_for_user(); `tests/test_daemon_registry.py::test_list_daemons_scoped_to_user` ✓
|
||||
- AC-4: `pfhub daemon update <daemon_id> --display-name <name>` 가 display_name을 업데이트한다.
|
||||
- Evidence: `app/cli/commands/daemon.py` update_daemon(); `tests/test_daemon_registry.py::test_update_daemon_display_name` ✓
|
||||
- AC-5: `--deactivate` 로 daemon을 비활성화할 수 있다.
|
||||
- Evidence: `app/cli/commands/daemon.py` update_daemon `--deactivate` option → `update_daemon(daemon_id, is_active=False)`
|
||||
|
||||
### REQ-005: service-token 발급 daemon binding
|
||||
|
||||
- AC-1: `pfhub service-token issue <name> --daemon-id <daemon_id>` 가 daemon_id를 token에 binding한다.
|
||||
- Evidence: `app/cli/commands/service_tokens.py` issue() `--daemon-id` option; `app/services/auth_service.py` issue_service_token(daemon_id=...); `tests/test_daemon_registry.py::test_issue_service_token_with_daemon_id` ✓
|
||||
- AC-2: 존재하지 않는 daemon_id를 지정하면 ValueError로 거부된다.
|
||||
- Evidence: `AuthService.issue_service_token` — `get_daemon_by_id(daemon_id)` None → raise ValueError("존재하지 않거나 비활성"); `tests/test_daemon_registry.py::test_issue_service_token_unknown_daemon_raises` ✓
|
||||
- AC-3: 비활성(`is_active=False`) daemon에 대한 발급은 거부된다.
|
||||
- Evidence: `AuthService.issue_service_token` — `not daemon.is_active` → raise ValueError; `tests/test_daemon_registry.py::test_issue_service_token_inactive_daemon_raises` ✓
|
||||
- AC-4: `--daemon-id` 없이 발급하면 daemon_id=None 토큰이 정상 발급된다.
|
||||
- Evidence: `tests/test_daemon_registry.py::test_issue_service_token_without_daemon_id_succeeds` ✓
|
||||
|
||||
### REQ-012: service-token rotate daemon_id 불변식
|
||||
|
||||
- AC-1: `pfhub service-token rotate <name>` 시 daemon_id가 유지된다.
|
||||
- Evidence: `app/repositories/postgres.py` rotate_service_token() — new token has same daemon_id as old; `tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id` ✓
|
||||
- AC-2: rotate 시 runtime_id는 새로 발급된다.
|
||||
- Evidence: rotate_service_token() — new `runtime_id = str(uuid4())`; `tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id` — asserts `rotated.auth_token.runtime_id != original_runtime_id` ✓
|
||||
- AC-3: rotate 시 raw token 값이 변경된다.
|
||||
- Evidence: `tests/test_daemon_registry.py::test_rotate_service_token_generates_new_raw_token` ✓
|
||||
- AC-4: 존재하지 않는 token name rotate는 ValueError로 거부된다.
|
||||
- Evidence: `tests/test_daemon_registry.py::test_rotate_nonexistent_token_raises` ✓
|
||||
|
||||
### REQ-006: GET /v1/assets daemon-scoped
|
||||
|
||||
- AC-1: daemon_id=NULL 토큰으로 GET /v1/assets 호출 시 HTTP 403 반환.
|
||||
- Evidence: `app/api/v1_assets.py` — `if auth.daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_assets_403_when_token_has_no_daemon_id` ✓
|
||||
- AC-2: daemon_id가 설정된 토큰으로 호출 시 `managed_by_daemon_id = token.daemon_id` 필터가 적용된 asset만 반환.
|
||||
- Evidence: `app/api/v1_assets.py` — `list_portfolios_with_strategies(managed_by_daemon_id=auth.daemon_id)`; `tests/test_daemon_assignment.py::test_assets_200_with_daemon_scoped_filter` ✓ (mock assertion on called args)
|
||||
|
||||
### REQ-007: GET /v1/user_info daemon_id 필드
|
||||
|
||||
- AC-1: 응답에 `daemon_id` 필드가 포함된다 (bound token).
|
||||
- Evidence: `app/schemas/v1.py` UserInfoResponse.daemon_id; `app/api/v1_user_info.py` — `daemon_id=auth.daemon_id`; `tests/test_daemon_assignment.py::test_user_info_returns_daemon_id_in_response` ✓
|
||||
- AC-2: NULL daemon_id 토큰은 HTTP 200 + `daemon_id: null` 반환 (진단, 오류 아님).
|
||||
- Evidence: `app/api/v1_user_info.py` — no guard on daemon_id; `tests/test_daemon_assignment.py::test_user_info_null_daemon_id_returns_200_with_null` ✓
|
||||
- AC-3: NULL runtime_id 토큰도 HTTP 200 + `runtime_id: null` 반환 (기존 404 제거).
|
||||
- Evidence: `app/api/v1_user_info.py` — 404 guard removed; `tests/test_v1.py::test_v1_user_info_null_runtime_id_returns_200_with_null` ✓
|
||||
|
||||
### REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization
|
||||
|
||||
- AC-1: daemon_id=NULL 토큰으로 호출 시 HTTP 403.
|
||||
- Evidence: `app/api/v1_ledger.py` — check 1: `if auth.daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_token_has_no_daemon_id` ✓
|
||||
- AC-2: asset의 `managed_by_daemon_id=NULL` (미배정)이면 HTTP 403.
|
||||
- Evidence: `app/api/v1_ledger.py` — check 2: `if portfolio.managed_by_daemon_id is None: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_asset_has_no_daemon_assignment` ✓
|
||||
- AC-3: token.daemon_id ≠ asset.managed_by_daemon_id (mismatch)이면 HTTP 403.
|
||||
- Evidence: `app/api/v1_ledger.py` — check 3: `if portfolio.managed_by_daemon_id != auth.daemon_id: raise HTTPException(403, ...)`; `tests/test_daemon_assignment.py::test_ledger_trade_403_when_daemon_mismatch` ✓
|
||||
- AC-4: token.daemon_id == asset.managed_by_daemon_id이면 HTTP 201 + status="new".
|
||||
- Evidence: `tests/test_daemon_assignment.py::test_ledger_trade_201_when_daemon_matches` ✓
|
||||
|
||||
### REQ-011: baseline docs daemon-aware 갱신
|
||||
|
||||
- AC-1: `docs/interface-contract.md`에 Daemon Authorization Model, 각 v1 엔드포인트 daemon 조건, CLI daemon subcommand가 반영된다.
|
||||
- Evidence: `docs/interface-contract.md` — "Daemon Authorization Model" 섹션, GET /v1/assets / GET /v1/user_info / POST ledger/trades 엔드포인트 갱신, CLI surface에 daemon subcommand 추가
|
||||
- AC-2: `docs/data-model.md`에 Daemon entity, portfolios.managed_by_daemon_id, auth_tokens.daemon_id가 문서화된다.
|
||||
- Evidence: `docs/data-model.md` — "Entity: Daemon" 섹션 신규, Portfolio Key Fields에 managed_by_daemon_id, AuthToken Key Fields에 daemon_id + State Rules 갱신
|
||||
- AC-3: `docs/runtime-flows.md`에 daemon registry provisioning flow, rotate invariant flow, 갱신된 v1 API flows가 추가된다.
|
||||
- Evidence: `docs/runtime-flows.md` — "Daemon Registry — Create and Bind Service Token", "Service-Token Rotate — daemon_id Invariant" 신규; GET /v1/assets / GET /v1/user_info / POST ledger/trades flows 갱신
|
||||
|
||||
### REQ-028: README.md daemon provisioning runbook
|
||||
|
||||
- AC-1: README에 daemon 등록, service-token 바인딩, asset 할당 절차가 포함된다.
|
||||
- Evidence: `README.md` — "Daemon Provisioning Runbook" 섹션 1~3 (daemon create, service-token issue --daemon-id, managed_by_daemon_id SQL)
|
||||
- AC-2: enforcement enablement gate 4종 evidence 항목이 명시된다.
|
||||
- Evidence: `README.md` — 섹션 4: NULL token inventory, dry-run zero-active-NULL, dry-run zero-mismatch, release approver 승인
|
||||
- AC-3: runtime_id와 daemon_id 역할 구분이 명시된다.
|
||||
- Evidence: `README.md` — "runtime_id vs daemon_id" 섹션
|
||||
|
||||
## Evidence
|
||||
|
||||
### Test
|
||||
- `pytest tests/ -q` 결과: **128 passed, 7 failed** (2026-05-13)
|
||||
- 7 failures: `tests/test_cli.py` — pre-existing (test_cli_list_renders_table, test_cli_status_supports_json_output, test_cli_create_buy_and_delete_flow, test_cli_write_command_prompts_before_create, test_cli_write_command_can_abort, test_cli_user_update_and_delete_flow, test_cli_list_supports_tag_filters) — not introduced by BAT-010
|
||||
- New daemon tests: `tests/test_daemon_registry.py` (12), `tests/test_daemon_assignment.py` (8) — all pass
|
||||
|
||||
### Manual
|
||||
- 실제 TimescaleDB 인스턴스에 대한 통합 테스트: 미수행 (환경 미구성 — 잔여 리스크로 기록)
|
||||
- ensure_schema() 및 migration 20260513_0016 DDL: `IF NOT EXISTS` guard 확인 (코드 리뷰)
|
||||
|
||||
### Logs
|
||||
- N/A (unit test 기반 검증; DB integration 미수행)
|
||||
|
||||
## Result
|
||||
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
- Notes:
|
||||
- Pre-existing 7 test failures in test_cli.py는 이 batch scope 밖이며 별도 배치에서 처리한다.
|
||||
- 프로덕션 enforcement 활성화는 배포 전 README 섹션 4의 4종 evidence 확보 후 수행한다.
|
||||
- migration 20260513_0016 실제 DB 적용 검증은 배포 시 수행한다.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Batch Index
|
||||
|
||||
## 목적
|
||||
- 이 문서는 batch delivery 단위의 전역 인덱스다.
|
||||
- 각 batch는 `docs/batches/<BAT_ID>/` 아래에서 profile(`standard` | `batch-lite`)에 맞는 산출물을 관리한다.
|
||||
|
||||
## Batch Register
|
||||
|
||||
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| BAT-001 | standard | release-candidate | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
|
||||
| BAT-002 | batch-lite | release-candidate | REQ-012 | minor-change | [bat-002_20260502_service-token-rotate-cli](./bat-002_20260502_service-token-rotate-cli/) |
|
||||
| BAT-003 | batch-lite | release-candidate | REQ-013 | dcy-003 | [bat-003_20260502_portfolio-list-user-required](./bat-003_20260502_portfolio-list-user-required/) |
|
||||
| BAT-004 | batch-lite | release-candidate | REQ-014 | minor-change | [bat-004_20260502_portfolio-create-user-required](./bat-004_20260502_portfolio-create-user-required/) |
|
||||
| BAT-005 | batch-lite | release-candidate | REQ-015 | minor-change | [bat-005_20260503_broker-credential-cli](./bat-005_20260503_broker-credential-cli/) |
|
||||
| BAT-006 | standard | release-candidate | REQ-016, REQ-017, REQ-018, REQ-019 | dcy-004 | [bat-006_20260511_portfolio-uuid-migration](./bat-006_20260511_portfolio-uuid-migration/) |
|
||||
| BAT-007 | batch-lite | release-candidate | REQ-001, REQ-002, REQ-003 | dcy-001 | [bat-007_20260511_local-db-validation-and-operations-runbook](./bat-007_20260511_local-db-validation-and-operations-runbook/) |
|
||||
| BAT-008 | batch-lite | release-candidate | REQ-011 | dcy-002 | [bat-008_20260511_trex-basic-baseline-docs-update](./bat-008_20260511_trex-basic-baseline-docs-update/) |
|
||||
| BAT-009 | batch-lite | release-candidate | REQ-020, REQ-021, REQ-022 | dcy-005 | [bat-009_20260512_production-operator-runbook-and-credential-matrix](./bat-009_20260512_production-operator-runbook-and-credential-matrix/) |
|
||||
| BAT-010 | standard | release-candidate | REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-011, REQ-012, REQ-023, REQ-028 | dcy-006 | [bat-010_20260513_daemon-model-implementation](./bat-010_20260513_daemon-model-implementation/) |
|
||||
Reference in New Issue
Block a user