docs(proofline): capture portfolio ledger discovery

This commit is contained in:
2026-08-20 12:36:24 +09:00
parent ac742b1150
commit f48a7c5cd7
12 changed files with 367 additions and 2 deletions
+15
View File
@@ -0,0 +1,15 @@
---
id: "ac-0032"
status: active
---
# Source-neutral portfolio ledger read
## Criterion
Portfolio owner는 `GET /v1/portfolios/{portfolio_id}/ledger`로 source 미지정 시 manual·broker를 포함한 모든 자기 portfolio 원장을 `recorded_at DESC, ledger_entry_id DESC` 순서로 조회할 수 있어야 한다.
## Verification
- mixed manual/broker 행이 최신순으로 반환되고 source 미지정이 모든 source를 포함한다.
- 구현 평가: `tests/test_v1_portfolios_manual_ledger.py` 및 전체 회귀 `664 passed, 16 skipped`에서 확인됐다.
+15
View File
@@ -0,0 +1,15 @@
---
id: "ac-0033"
status: active
---
# Stable keyset pagination and fail-closed cursor
## Criterion
Unified ledger 목록은 `(recorded_at, ledger_entry_id)` keyset cursor로 동시각 행의 중복·누락 없이 page를 이어야 하며 cursor는 portfolio/source에 결속되고 malformed·tampered·scope mismatch는 HTTP 422이어야 한다.
## Verification
- cursor 경계, 변조 및 다른 source cursor 재사용은 각각 정확한 next page 또는 422을 반환한다.
- 구현 평가: focused cursor 회귀와 전체 회귀 `664 passed, 16 skipped`에서 확인됐다.
+15
View File
@@ -0,0 +1,15 @@
---
id: "ac-0034"
status: active
---
# Canonical cross-source ledger representation
## Criterion
Unified ledger item은 canonical 대문자 `action`을 제공하고 internal `side`를 노출하지 않으며 source별로 없는 사실은 명시적 nullable schema 값으로 반환해야 한다.
## Verification
- manual/broker mixed 응답이 action casing과 nullable field 계약을 유지하고 OpenAPI가 해당 schema를 노출한다.
- 구현 평가: focused schema/API regression 및 OpenAPI inspection으로 확인됐다.
+15
View File
@@ -0,0 +1,15 @@
---
id: "ac-0035"
status: active
---
# Owner-only unified read and manual compatibility
## Criterion
Unified ledger read는 user/session portfolio owner만 허용하고 타 owner와 service token을 거부하며 기존 `/ledger/manual`의 manual-only·latest-first·limit 계약을 유지해야 한다.
## Verification
- 타 owner/service token 거부, owner 허용 및 manual endpoint 하위호환 회귀를 실행한다.
- 구현 평가: focused API/auth-surface `29 passed`, 전체 회귀 `664 passed, 16 skipped`에서 확인됐다.
+15
View File
@@ -0,0 +1,15 @@
---
id: "ac-0036"
status: active
---
# Unified ledger persistence and regression evidence
## Criterion
Unified ledger query는 portfolio/source keyset ordering에 맞는 PostgreSQL index migration을 제공하고 API/OpenAPI/repository 계약 회귀로 검증되어야 한다.
## Verification
- disposable PostgreSQL fresh upgrade가 head `20260820_0030`과 `idx_portfolio_ledger_portfolio_recorded_entry_desc` 생성을 확인하고 focused/full suite가 통과한다.
- 구현 평가: fresh migration 확인 및 전체 회귀 `664 passed, 16 skipped`가 있다. 운영 규모 `EXPLAIN ANALYZE` latency 관찰은 비차단 후속이다.
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"schema_version": 1, "schema_version": 1,
"next_line_number": 6, "next_line_number": 7,
"next_ac_number": 32 "next_ac_number": 37
} }
+41
View File
@@ -0,0 +1,41 @@
---
id: "dcy-0006"
status: confirmed
---
# [portfolio-hub] 포트폴리오 전체 원장 조회 API
## Problem
기존 `GET /v1/portfolios/{portfolio_id}/ledger/manual`는 manual source만 반환하므로, portfolio owner가 broker 체결/원장 이벤트를 포함한 전체 원장을 하나의 안정적인 read 계약으로 조회할 수 없었다. 소비자 UI는 owner가 제공한 원장 사실을 표시할 뿐 action·잔고 효과·PnL을 재계산해서는 안 된다.
## Evidence
- 승인 issue: `https://github.com/fourblocks2020/portfolio-hub/issues/29`; 전체 원문은 `evidence/issue-29-approved-discovery.md`에 body SHA-256 `833b5c8e2dd4bb6c90331bef58a0fdee48534fc1458a82b646f1581877428b66`으로 보존했다.
- 진행 comment: `evidence/implementation-progress.md`, body SHA-256 `e90881b5df47b17650169c0858e7f39c300682ab70d3a88032b6fe41d31f65c7`.
- 완료 comment: `evidence/implementation-completion.md`, body SHA-256 `ce25425fa95afad70221be88a8df88fe39b13f706bbc4f8508a0be34a8327187`.
- 구현 commit `ac742b1150832f6c1cf58744308b4f3ea6803fd4`에서 통합 endpoint, repository keyset query, `20260820_0030` index migration 및 회귀가 확인됐다.
- 실행 evidence: 전체 `pytest -q`는 `664 passed, 16 skipped`; disposable PostgreSQL fresh migration에서 head와 `idx_portfolio_ledger_portfolio_recorded_entry_desc` 생성을 확인했다.
## Scope
- owner-only `GET /v1/portfolios/{portfolio_id}/ledger`가 manual과 broker를 포함한 모든 source를 기본 최신순(`recorded_at DESC, ledger_entry_id DESC`)으로 반환한다.
- optional non-empty exact `source` filter, `(recorded_at, ledger_entry_id)` keyset pagination 및 `next_cursor` 종료 계약을 제공한다. cursor는 portfolio/source 조건에 결속되고 malformed·tampered·다른 조건 재사용은 HTTP 422다.
- 통합 item은 canonical 대문자 `action`을 노출하고 internal `side`는 노출하지 않는다. source별로 없는 사실은 0/추정값이 아닌 schema nullable 값이다.
- user/session owner만 허용한다. owner 밖 사용자와 service token은 기존 정책에 따라 거부한다.
- PostgreSQL index/migration, OpenAPI, mixed source/page/cursor/권한/manual 하위호환 회귀를 포함한다.
- AC admission: 기존 active AC는 aggregate 의미를 소유할 뿐 source-neutral unified read contract를 소유하지 않으므로 durable behavior를 `create`한다.
## Out of Scope
- ledger 생성·수정·삭제, REVERSAL 실행 또는 기존 원장 재해석.
- broker order/fill 생성, strategy signal, outbox/producer 변경.
- API/UI가 원장에 없는 경제 효과·잔고·PnL을 추론하는 동작.
- portfolio return aggregate 의미 변경(#28), trex-web UI, push/PR/merge, 배포·릴리스.
## Risks and Unknowns
### Risks
- 운영 cursor secret은 장기 랜덤 값으로 관리해야 하며 교체 시 기존 cursor의 만료·무효화 정책을 운영 문서에 명시해야 한다.
- 운영 규모 row가 있는 환경에서 source-filter query의 `EXPLAIN ANALYZE` index 사용과 latency 관찰이 필요하다.
@@ -0,0 +1,35 @@
<!-- Source URL: https://github.com/fourblocks2020/portfolio-hub/issues/29#issuecomment-5350993550
Source author: genichin
Source created_at: 2026-08-20T03:28:21Z
Body SHA-256: ce25425fa95afad70221be88a8df88fe39b13f706bbc4f8508a0be34a8327187
Snapshot: exact source body begins after this header.
-->
## 구현 완료
포트폴리오 소유자가 manual·broker를 포함한 모든 출처의 원장을 cursor 기반으로 조회하는 통합 API를 로컬 `main`에 구현·검증·커밋했습니다.
### 구현 내용
- `GET /v1/portfolios/{portfolio_id}/ledger`를 추가해 모든 source의 원장을 `recorded_at DESC, ledger_entry_id DESC` 순서로 반환합니다.
- optional exact `source` filter, HMAC-signed opaque cursor 및 portfolio/source 조건 결속을 구현했습니다. malformed·tampered·다른 조건의 cursor는 HTTP 422로 거부합니다.
- 통합 item은 canonical 대문자 `action`만 노출하며 `side`는 노출하지 않습니다. 출처별로 없는 사실은 추정값 대신 nullable field로 표현합니다.
- PostgreSQL repository keyset query와 `(portfolio_id, recorded_at DESC, ledger_entry_id DESC)` index migration (`20260820_0030`)을 추가했습니다.
- source filter, owner/service-token 권한 경계, cursor 오류, OpenAPI, release migration-head contract 및 `temp/` 제외 source-surface 검사를 회귀로 보완했습니다.
### 검증 결과
전체 회귀와 변경 범위 검증을 완료했습니다.
- `pytest -q` → `664 passed, 16 skipped`
- focused API·release·auth-surface 검증 → `29 passed`
- disposable PostgreSQL fresh migration → head `20260820_0030`, `idx_portfolio_ledger_portfolio_recorded_entry_desc` 생성 확인
- 변경 범위 lint 및 `git diff --check` → 통과
- 로컬 커밋 → `ac742b1150832f6c1cf58744308b4f3ea6803fd4` (`feat(portfolios): add unified ledger history API`)
### 비차단 후속 권고
- 운영 환경에서는 `PORTFOLIOHUB_LEDGER_CURSOR_SECRET`를 장기 랜덤 secret으로 설정하고, cursor secret 교체 시 기존 cursor의 유효기간·무효화 정책을 운영 문서에 명시하는 것을 권고합니다.
- 실제 운영 규모의 ledger row가 있는 환경에서 `EXPLAIN ANALYZE`로 source-filter 포함 query의 index 사용과 latency를 추가 관찰하는 것을 권고합니다.
> 현재 상태는 **로컬 구현·검증·커밋 완료**입니다. push·PR·배포·릴리스 완료를 의미하지 않습니다.
@@ -0,0 +1,36 @@
<!-- Source URL: https://github.com/fourblocks2020/portfolio-hub/issues/29#issuecomment-5349683566
Source author: genichin
Source created_at: 2026-08-20T00:30:07Z
Body SHA-256: e90881b5df47b17650169c0858e7f39c300682ab70d3a88032b6fe41d31f65c7
Snapshot: exact source body begins after this header.
-->
## 구현 진행 상황
- **대상:** [portfolio-hub] discovery: 포트폴리오 전체 원장 조회 API (#29)
- **브랜치:** `main`
- **기준:** `main` (`fe22be0b6686f346f4e6a5c9b64f7b3469e75810`)
- **상태:** 로컬 main 구현·검증·커밋 완료 — 미푸시, 미배포, 미릴리스
- **최종 업데이트:** 2026-08-20T11:05:00+09:00 (KST)
### 구현 계획 및 작업 목록
- [x] 통합 원장 조회 API의 응답·입력 계약을 추가했다.
- 모든 출처를 하나의 대문자 `action` 모델로 표현하고, 출처별로 없는 사실은 `null`로 명시한다.
- [x] source 필터와 cursor 기반 페이지 조회를 구현했다.
- cursor는 portfolio·source 조건과 정렬 기준에 결속하며, 잘못되거나 다른 조건의 cursor는 HTTP 422로 거부한다.
- [x] 소유자 권한과 기존 manual 전용 조회의 하위호환성을 검증했다.
- user/session owner만 허용하고 기존 `/ledger/manual` 계약을 변경하지 않았다.
- [x] API/OpenAPI 문서와 단위·통합 회귀 검증을 추가·실행했다.
- manual·broker 혼합, source filter, 권한 경계와 cursor 오류를 검증했다.
- [x] 실제 PostgreSQL query plan 및 index/migration을 검증했다.
- disposable PostgreSQL DB에서 `20260820_0030` migration head와 `idx_portfolio_ledger_portfolio_recorded_entry_desc` 생성을 확인했다.
### 진행 기록
- 2026-08-20T09:29:48+09:00 (KST): 승인된 Discovery(OQ-001~007)와 현재 `main` 기준 SHA를 확인했다. 별도 구현 worktree는 만들지 않고 `main`에서 구현·검증을 시작한다.
- 2026-08-20T10:40:54+09:00 (KST): source-중립 `GET /v1/portfolios/{portfolio_id}/ledger` vertical slice를 추가했다. focused `tests/test_v1_portfolios_manual_ledger.py`는 `14 passed`였다.
- 2026-08-20T10:55:00+09:00 (KST): disposable PostgreSQL fresh upgrade에서 migration head와 unified-ledger keyset index 생성을 확인했다.
- 2026-08-20T11:05:00+09:00 (KST): 권한/source/OpenAPI/release-contract 회귀를 보완했다. 전체 회귀 `664 passed, 16 skipped`, 변경 범위 lint와 `git diff --check`를 통과했고, 로컬 `main`에 `ac742b1`로 커밋했다. `temp/`는 후보 source가 아니므로 auth-surface 정적 검사의 재귀 대상에서 제외했다.
> 각 작업이 실제로 완료되고 검증될 때마다 이 댓글의 체크박스와 진행 기록을 갱신한다. 체크 표시는 구현 완료 또는 해당 검증의 실제 통과를 의미하며, 배포·릴리스 완료를 의미하지 않는다.
@@ -0,0 +1,140 @@
<!-- Source URL: https://github.com/fourblocks2020/portfolio-hub/issues/29
Source author: genichin
Source created_at: 2026-08-18T10:04:45Z
Body SHA-256: 833b5c8e2dd4bb6c90331bef58a0fdee48534fc1458a82b646f1581877428b66
Snapshot: exact source body begins after this header.
-->
## 배경
`trex-web` 포트폴리오 상세 화면은 현재 다음 manual 전용 계약만 사용합니다.
```http
GET /v1/portfolios/{portfolio_id}/ledger/manual?limit=50
```
이 계약은 수동으로 기록한 원장만 표시하므로, broker에서 유입된 체결/원장 이벤트를 포함한 포트폴리오의 통합 원장 화면을 제공할 수 없습니다.
현재 provider 확인 근거:
- `app/api/v1_portfolios.py`의 manual 목록 route는 `require_read` 뒤에 user/session token 및 portfolio owner를 확인한다.
- `app/repositories/postgres.py`의 `list_manual_ledger_entries()`는 `WHERE portfolio_id = %s AND source = 'manual'`로 필터하고 `recorded_at DESC, ledger_entry_id DESC`로 정렬한다.
- 도메인 `LedgerEntry`는 `source`를 보유하며 기본값은 `broker`; manual 기록은 `source='manual'`이다.
- 현재 응답의 `total`은 전체 일치 건수가 아니라 반환된 `items` 길이이다.
## 목표
포트폴리오 소유자가 manual과 broker를 포함한 **모든 출처의 원장 기록**을 하나의 안정적이고 page 가능한 read 계약으로 조회할 수 있게 한다. 소비자 UI는 owner가 제공한 원장 사실을 표시할 뿐, 원장 action이나 잔고 효과를 재계산하지 않는다.
## 제안 API 초안
```http
GET /v1/portfolios/{portfolio_id}/ledger?limit=50
GET /v1/portfolios/{portfolio_id}/ledger?limit=50&cursor=...
GET /v1/portfolios/{portfolio_id}/ledger?source=manual&limit=50
GET /v1/portfolios/{portfolio_id}/ledger?source=broker&limit=50
```
- `source` 미지정은 모든 출처를 최신순으로 반환한다.
- `source`가 지정되면 비어 있지 않은 provenance 문자열을 exact match로 사용한다. 현재 결과가 없는 값도 오류가 아니라 빈 page(`items=[]`, `next_cursor=null`)를 반환한다.
- 기본 정렬은 `recorded_at DESC, ledger_entry_id DESC`이며, cursor는 두 값을 함께 고정해 동률에서도 중복/누락 없이 다음 페이지를 반환해야 한다.
- 응답 `items`는 적어도 `ledger_entry_id`, `portfolio_id`, `source`, canonical 대문자 `action`, `recorded_at`, `executed_at`(있는 경우), order/execution 식별자(있는 경우), 수량·가격·수수료·세금·note, 기록 후 owner snapshot, reversal 참조와 confirmation evidence를 표현해야 한다.
- 새 통합 endpoint는 `side`를 노출하지 않고 canonical 대문자 `action`만 노출한다. 공통 필드와 출처별 필드는 response schema에 명시하며, 해당 사실에 값이 없을 때만 `null`을 반환한다. broker 원장과 manual 원장의 필드 공백을 0이나 추정값으로 채우지 않는다.
## Open Questions
각 OQ에서 하나의 선택지를 선택해 주세요. 선택은 Discovery 결정을 위한 것이며 구현·push·배포 승인을 뜻하지 않습니다.
### OQ-001 — pagination 계약
- [x] **A. Cursor pagination** — `recorded_at`과 `ledger_entry_id` 기반 opaque cursor, `next_cursor` 반환.
- [ ] **B. Offset pagination** — `offset`과 `limit`으로 페이지 이동.
- [ ] **C. 단일 무제한 응답** — 모든 원장을 한 응답에 반환.
**권장안: A** — 원장은 계속 증가하며, 최신순 목록에 새 행이 들어와도 cursor가 offset보다 중복·누락에 강하다. 무제한 응답은 응답 크기와 DB 부하 경계가 없다.
### OQ-002 — source 필터 정책
- [x] **A. 전체 출처가 기본값이며, 선택적 `source` filter를 제공한다.**
- [ ] **B. 전체 출처만 제공하며, source filter는 제공하지 않는다.**
- [ ] **C. manual/broker 전용 endpoint를 계속 분리하고 통합 endpoint는 만들지 않는다.**
**권장안: A** — 통합 운영 화면의 기본 요구를 충족하면서, manual-only 감사 화면 같은 소비자는 같은 안정적 계약에서 명시적으로 좁힐 수 있다. 기존 `/ledger/manual`은 하위호환성을 위해 유지한다.
### OQ-003 — read 권한 경계
- [x] **A. 현재 manual 조회와 동일하게 user/session token + portfolio owner만 허용한다.**
- [ ] **B. owner 외 admin도 명시적으로 허용한다.**
- [ ] **C. daemon service token에도 portfolio scope 내 조회를 허용한다.**
**권장안: A** — 최초 소비자인 trex-web의 사용자 상세 조회를 최소 권한으로 충족한다. admin/service 조회는 실제 운영 소비자와 audit 노출 범위를 별도로 확정한 뒤 additive하게 설계하는 편이 안전하다.
### OQ-004 — 전체 건수(`total`) 의미
- [x] **A. `next_cursor`만 반환하고 전체 건수는 제공하지 않는다.**
- [ ] **B. 정확한 전체 일치 건수를 `total`로 반환한다.**
- [ ] **C. 현재 page의 반환 건수를 `total`로 반환한다.**
**권장안: A** — 현재 manual endpoint의 `total=len(items)`와 전역 건수 의미를 혼동하지 않으며, 목록 UI에 필요한 다음 페이지 판단도 충족한다. 정확한 total은 필요성이 확인될 때 별도 비용·일관성 계약으로 추가한다.
### OQ-005 — 통합 원장 item의 정규 응답 계약
- [x] **A. 하나의 canonical `action` 필드를 대문자 enum으로 제공하고, 모든 공통·출처별 필드는 schema에 명시적으로 nullable로 선언한다. — 결정**
- 새 통합 endpoint는 `side`를 노출하지 않는다.
- `action`은 manual/broker 모두에서 동일한 대문자 vocabulary를 사용한다.
- 값이 없는 source별 사실은 0 또는 추정값으로 대체하지 않고 `null`로 표현한다.
- [ ] B. `action`과 `side`를 모두 노출하고 source별 field 존재·동치·casing 규칙을 명시한다.
- [ ] C. broker와 manual을 별도 item type/discriminator schema로 제공한다.
### OQ-006 — cursor scope 및 오류 계약
- [x] **A. opaque cursor는 `portfolio_id`, 선택된 `source` filter 및 `(recorded_at, ledger_entry_id)`를 결속한다. malformed·tampered·다른 portfolio/source에 재사용한 cursor는 HTTP 422로 거부한다. 마지막 page의 `next_cursor`는 `null`이다. (권장)**
- 다른 목록 조건의 cursor 혼용을 fail-closed하고, 소비자는 `next_cursor is null`로 종료를 판정한다.
- [ ] B. cursor에는 `(recorded_at, ledger_entry_id)`만 담고, portfolio/source 불일치는 구현이 현재 query 결과로 처리한다.
- cursor 재사용의 의미와 오류가 모호해진다.
- [ ] C. cursor pagination을 유지하되 cursor 오류·scope 동작은 구현 단계에서 정한다.
- 외부 API의 실패 동작을 구현자 재량으로 남긴다.
### OQ-007 — `source` parameter의 허용값 및 결과 없음 의미
- [x] **A. `source`는 비어 있지 않은 provenance 문자열을 exact match로 허용하며, 알 수 없는 값은 빈 page(`items=[]`, `next_cursor=null`)를 반환한다. — 결정**
- all-source 기본값과 미래 producer source를 별도 API 변경 없이 지원한다.
- source 값은 portfolio owner 권한 검증 뒤에만 평가한다.
- [ ] B. `source`는 현재 `manual`·`broker` enum만 허용하고 그 외 값은 HTTP 422로 거부한다.
- [ ] C. known source 조회는 허용하되, source 목록을 API metadata로 제공한 뒤 그 목록 밖 값의 처리 규칙을 별도 계약으로 정한다.
## 포함 범위
- source-중립 통합 원장 read endpoint, request/response schema, repository query와 cursor semantics.
- portfolio ownership 및 선택된 권한 모델의 명시적 검증.
- manual과 broker 행 혼합, 동일 `recorded_at` tie-breaker, page 경계, source filter, token/타인 portfolio 경계에 대한 계약 테스트.
- 필요한 DB index 및 migration 여부 검토. 실제 PostgreSQL에서 pagination query plan과 migration을 검증한다.
- OpenAPI/API interface 문서 갱신.
- 기존 `GET /v1/portfolios/{portfolio_id}/ledger/manual` 계약의 하위호환성 회귀 테스트.
## 제외 범위
- ledger 생성·수정·삭제, `REVERSAL` 실행 또는 기존 원장의 재해석.
- broker 주문/체결 생성, strategy signal, outbox/producer 동작 변경.
- 원장에 없는 경제적 효과·잔고·PnL을 API나 trex-web이 추론하는 기능.
- portfolio return aggregate 의미 변경(관련 이슈 #28의 별도 범위).
- trex-web UI 구현, push/PR/merge, 운영 배포·릴리스.
## 수용 기준 초안
- [ ] OQ-001~007 선택이 issue 본문에 결정으로 기록된다.
- [ ] 소유자가 자신의 portfolio에 대해 선택된 source 범위의 원장을 결정된 정렬·pagination 규칙으로 조회할 수 있다.
- [ ] 서로 다른 출처의 행이 누락 없이 공통 read model로 표현되며, 새 통합 endpoint는 대문자 canonical `action`만 노출한다. 없는 source별 사실은 추정값이 아닌 schema의 명시된 nullable 값이다.
- [ ] cursor 페이지 경계에서 동일 시각 행의 중복·누락이 없고, cursor의 portfolio/source scope 및 malformed·tampered cursor 거부 동작은 OQ-006 결정과 일치한다.
- [ ] 선택된 권한 범위 밖의 token 또는 타인 portfolio는 기존 정책에 맞는 거부 응답을 받는다.
- [ ] `source` 필터가 선택되면 지정 provenance와 exact match인 row만 반환하고, 결과 없는 non-empty 값은 빈 page(`items=[]`, `next_cursor=null`)를 반환한다. 미지정 기본 동작은 OQ-002·OQ-007 결정과 일치한다.
- [ ] `/ledger/manual`의 source=manual 전용·최신순·limit 계약은 하위호환으로 유지된다.
- [ ] API schema/OpenAPI와 실제 응답이 일치하고, focused 및 전체 회귀 테스트가 통과한다.
## 관련
- 소비자 맥락: `fourblocks2020/trex-web#15`
- 별도 집계 의미 변경: #28
- 작성 시각: 2026-08-18T19:04:00+09:00
> **Discovery 상태: APPROVED** — requester가 OQ-001~007을 모두 명시적으로 결정했고, 2026-08-20 KST에 현재 provider/data-flow/ownership 계약과 대조하여 승인했다. 이 승인은 구현·커밋·push·PR·merge·배포·릴리스 승인을 포함하지 않습니다.
+3
View File
@@ -0,0 +1,3 @@
---
id: "line-0006"
---
+35
View File
@@ -0,0 +1,35 @@
---
id: "req-0006"
status: approved
discovery: "dcy-0006"
criteria:
create: ["ac-0032", "ac-0033", "ac-0034", "ac-0035", "ac-0036"]
update: []
retire: []
satisfy: []
---
# 포트폴리오 전체 원장 조회 API
## Objective
Portfolio owner가 manual·broker를 포함한 모든 원장 source를 안정적이고 page 가능한 단일 read 계약으로 조회하도록 한다.
## Scope
- `GET /v1/portfolios/{portfolio_id}/ledger`의 source-neutral 목록, optional exact `source`, latest-first keyset pagination과 signed opaque cursor.
- canonical uppercase `action`, nullable source-specific facts, owner-only authorization, OpenAPI/repository/index migration 및 회귀.
## Constraints
- 기존 `/ledger/manual` 계약을 변경하지 않는다.
- cursor는 portfolio/source와 tuple ordering에 결속하며 잘못된 cursor는 fail-closed HTTP 422다.
- source에 없는 사실을 0 또는 추정값으로 만들지 않는다.
### 현재 구현 평가
구현 commit `ac742b1150832f6c1cf58744308b4f3ea6803fd4`에서 이 REQ의 모든 AC 동작이 구현됐다. `pytest -q`는 `664 passed, 16 skipped`; focused API/release/auth-surface는 `29 passed`; disposable PostgreSQL fresh migration에서 `20260820_0030`과 index 생성을 확인했다. local main은 origin/main보다 1 commit ahead이며 push·PR·배포·릴리스는 수행하지 않았다. 이 평가는 구현 evidence이며 approved/active status는 normative authority다.
## Non-Goals
Ledger write/reversal 실행, broker/strategy/outbox producer 변경, UI 구현, aggregate #28 변경, push·PR·배포·릴리스는 포함하지 않는다.