fix: qualify corporate-action ledger reads (#36)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "ac-0037"
|
||||
status: active
|
||||
---
|
||||
|
||||
# 기업행동 원장이 포함된 joined ledger read 안정성
|
||||
|
||||
## Criterion
|
||||
|
||||
Portfolio owner의 holdings cost-basis와 unified ledger 조회는 같은 portfolio에 기업행동 원장과 일반 거래 원장이 함께 있어도 PostgreSQL column ambiguity 없이 기존 ledger·기업행동 사실을 읽어야 한다. Holdings path는 기존 WAC·실현손익·보유수량·원가 의미와 corporate-action replay mapping을 보존하고, unified ledger path는 기존 latest-first ordering·source filter·keyset cursor·canonical response 의미를 보존해야 한다.
|
||||
|
||||
## Verification
|
||||
|
||||
- 성공 조건: 실제 PostgreSQL에 일반 BUY/SELL과 `stock_consolidation` 기업행동을 함께 구성했을 때 `get_all_trades_for_pnl()`과 `list_ledger_entries()`가 모두 성공하고 기업행동 metadata를 해당 ledger entry에 결합한다. 같은 fixture의 `GET /v1/portfolios/{portfolio_id}/holdings/cost-basis`와 `GET /v1/portfolios/{portfolio_id}/ledger`가 HTTP 200을 반환하며 unified ledger의 기본 조회·source filter·다음 cursor page가 기존 정렬과 행 구성을 유지한다.
|
||||
- 실패·경계 조건: JOIN으로 `ledger_entry_id`·`recorded_at` 등 base column이 ambiguous해지거나, SELECT 순서·positional mapping이 바뀌거나, 기업행동이 없는 기존 거래 replay·회계 결과·authorization·API schema·pagination이 달라지면 FAIL이다.
|
||||
- 검사 방법: 실제 PostgreSQL repository integration과 holdings/unified-ledger API 회귀를 실행하고 `app/repositories/postgres.py`의 joined projection·predicate·ordering qualification 및 반환 mapping을 검토한다.
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"next_line_number": 7,
|
||||
"next_ac_number": 37
|
||||
"next_line_number": 8,
|
||||
"next_ac_number": 38
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: "dcy-0007"
|
||||
status: confirmed
|
||||
---
|
||||
|
||||
# 기업행동 원장이 포함된 보유 원가·전체 원장 조회 복구 (#19)
|
||||
|
||||
## Problem
|
||||
|
||||
기업행동 원장이 존재하는 포트폴리오에서 `GET /v1/portfolios/{portfolio_id}/holdings/cost-basis`와 `GET /v1/portfolios/{portfolio_id}/ledger?limit=50`가 PostgreSQL ambiguous-column 오류로 HTTP 500을 반환한다. 기업행동 metadata를 읽기 위해 `portfolio_ledger`와 `portfolio_corporate_actions`를 JOIN한 뒤 base ledger 컬럼 일부를 table alias 없이 참조한 것이 원인이다. 그 결과 보유 원가 replay와 전체 원장 조회라는 서로 다른 read path가 함께 중단되고, downstream인 trex-web 포트폴리오 상세에서 보유 정보와 원장 정보를 표시할 수 없다.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `E-001` Source issue `https://github.com/fourblocks2020/trex-web/issues/19`는 두 API의 HTTP 500과 `psycopg.errors.AmbiguousColumn: column reference "ledger_entry_id" is ambiguous` 운영 관찰을 기록한다. 확인 시각의 issue body SHA-256은 `8e382d87dd3358ac516cdd8e5c683f4fa673253c6eb7fc4d0699ed572378ad5b`, `updated_at`은 `2026-09-13T11:28:56Z`다.
|
||||
- `E-002` 기준 source는 `portfolio-hub` `c7d8235bdebfa8c2459b0b3300d8ed40a9f465e5`다. `app/repositories/postgres.py`의 `get_all_trades_for_pnl()`은 `portfolio_ledger AS ledger`와 `portfolio_corporate_actions ca`를 JOIN하지만 첫 18개 base projection의 `ledger_entry_id`, `portfolio_id`, `recorded_at` 등을 alias 없이 선택한다.
|
||||
- `E-003` 같은 파일의 `list_ledger_entries()`는 두 table을 JOIN하면서 base projection, 동적 `WHERE`의 `portfolio_id`·`source`·cursor tuple, `ORDER BY recorded_at, ledger_entry_id`를 alias 없이 참조한다. 현재 source에서 기업행동 table을 JOIN하는 repository query는 이 두 곳뿐이다.
|
||||
- `E-004` `.proofline/lines/line-0006/req-0006.md`는 approved 상태이며 unified ledger read를 소유한다. Active `AC-0032`는 source-neutral latest-first 조회, `AC-0034`는 source별 canonical representation, `AC-0036`은 PostgreSQL query 회귀를 요구한다. 보유 원가/PnL replay read는 현재 active AC가 직접 소유하지 않는다.
|
||||
- `E-005` `tests/test_ledger_consolidation_integration.py`는 실제 PostgreSQL repository path를 실행하지만 기업행동 행과 일반 거래 행을 함께 만든 뒤 `get_all_trades_for_pnl()`과 `list_ledger_entries()`를 호출하는 회귀는 없다. Mock 기반 테스트만으로는 PostgreSQL column resolution 오류를 검출할 수 없다.
|
||||
|
||||
## Scope
|
||||
|
||||
- `S-001` `get_all_trades_for_pnl()`의 모든 base ledger projection을 `ledger.<column>`으로 한정하여 기업행동 원장이 포함된 포트폴리오의 보유 원가 replay가 정상 동작하게 한다. 이 동작은 기존 active AC가 직접 소유하지 않으므로 지속적인 독립 검증이 가능한 AC를 `create`한다.
|
||||
- `S-002` `list_ledger_entries()`의 base ledger projection, dynamic predicate, keyset cursor tuple과 ordering을 `ledger.<column>`으로 한정하고 `portfolio_ledger AS ledger`를 일관되게 사용한다. Unified ledger의 응답 구조·정렬·cursor·권한 의미를 변경하지 않고 기존 `AC-0032`, `AC-0034`, `AC-0036`을 `satisfy`한다.
|
||||
- `S-003` 실제 PostgreSQL에서 기업행동 원장과 일반 거래 원장이 공존하는 fixture로 두 repository read를 실행한다. Holdings cost-basis API와 unified ledger API가 HTTP 200을 반환하고 기업행동 metadata 및 일반 거래 행을 기존 schema대로 보존하는 focused API/repository 회귀를 둔다.
|
||||
- `S-004` 변경은 ambiguous-column을 제거하는 query qualification과 직접 회귀에 한정한다. 회계 수치, WAC·replay 규칙, source/action 의미, authorization, pagination 또는 API schema를 바꾸지 않는다.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- `OS-001` 기업행동 원장의 생성·수정·삭제, reversal 또는 recovery 동작 변경.
|
||||
- `OS-002` broker 주문·취소·체결 처리, producer/outbox 변경 또는 기존 원장 row mutation/backfill.
|
||||
- `OS-003` holdings cost basis, PnL, corporate-action 회계 산식의 변경이나 임의 보정.
|
||||
- `OS-004` trex-web UI 변경, 배포·release·production mutation 및 특정 production portfolio 데이터 수정.
|
||||
|
||||
## Risks and Unknowns
|
||||
|
||||
### Risks
|
||||
|
||||
- `R-001` SELECT column 순서가 `_row_to_ledger_entry()`와 corporate-action metadata positional mapping에 결속돼 있다. Qualification 과정에서 순서를 바꾸면 값 mapping이 깨질 수 있으므로 기존 순서를 보존하고 반환 객체를 검증한다.
|
||||
- `R-002` mock cursor 테스트는 PostgreSQL의 ambiguous-column 판정을 재현하지 못한다. 실제 PostgreSQL 회귀가 없으면 같은 결함이 다시 유입될 수 있으므로 disposable/local integration을 필수 검증 경계로 둔다.
|
||||
- `R-003` unified ledger의 dynamic keyset predicate qualification이 누락되면 첫 page는 통과해도 source filter 또는 다음 page가 실패할 수 있다. 기본 조회, source filter와 cursor 경계를 함께 검증한다.
|
||||
|
||||
### Assumptions
|
||||
|
||||
- `A-001` Issue #19에 기록된 운영 오류와 현재 source의 unqualified JOIN query가 같은 결함이라는 판단은 SQL text와 PostgreSQL column-name 충돌에 근거한다. 실제 PostgreSQL pre-fix 회귀로 이를 확인한다.
|
||||
|
||||
### Open Questions
|
||||
|
||||
없음. Source issue가 query qualification, 유지할 계약, 회귀 범위와 제외 범위를 구체적으로 정했고 현재 source에서 추가 제품·정책 결정을 요구하는 항목은 발견되지 않았다.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Activity Log
|
||||
|
||||
- 2026-09-13T21:26:26+09:00 — 사용자가 `REQ-0007`, 신규 `AC-0037`, 기존 `AC-0032`·`AC-0034`·`AC-0036` satisfy 집합을 승인했다. `REQ-0007`은 `approved`, `AC-0037`은 `active`로 전환했고 `proofline validate`가 기존 line status warning만 남기고 통과했다.
|
||||
- 2026-09-13T21:26:26+09:00 — 직접 구현 branch `fix/issue-19-corporate-action-reads`를 생성했다. 계획: 실제 PostgreSQL 실패 회귀 추가 → joined query base column qualification → focused repository/API 회귀 → 전체 회귀·정적 검사 → 독립 QC → commit/PR.
|
||||
- 2026-09-13T21:40:00+09:00 — `test_joined_ledger_reads_qualify_base_columns` 실제 PostgreSQL 회귀를 추가하고 joined PnL/unified-ledger SQL의 ledger projection·predicate·cursor/order reference를 `ledger.`로 최소 한정했다. API holdings/ledger/PNL focused suite는 50 passed; Ruff는 기존 SIM117 4건만 보고했다.
|
||||
- 2026-09-13T21:44:31+09:00 — 로컬 TimescaleDB를 기동해 실제 PostgreSQL에서 unqualified JOIN이 SQLSTATE `42702` (`AmbiguousColumn`)를 내는 RED와 qualified JOIN 성공을 확인했다. Mixed BUY+`stock_consolidation` repository 회귀는 source filter·keyset cursor를 포함해 통과했고, 두 API corporate-action 회귀까지 합쳐 `3 passed`였다.
|
||||
@@ -0,0 +1,35 @@
|
||||
# AC-0037 Test Report
|
||||
|
||||
- 실행 시각: 2026-09-13T21:53:39+09:00
|
||||
- 기준 Git HEAD: `c7d8235bdebfa8c2459b0b3300d8ed40a9f465e5`
|
||||
- 검증 대상: 위 HEAD의 `fix/issue-19-corporate-action-reads` working-tree diff
|
||||
- 실제 backend: `timescale/timescaledb-ha:pg17` (`docker-compose.local-test.yml`, PostgreSQL DSN은 기록하지 않음)
|
||||
- 결과: PASS
|
||||
|
||||
## AC mapping
|
||||
|
||||
- Joined projection·metadata mapping·기존 BUY 동작: `tests/test_corporate_action_atomic_recovery_postgres.py::test_joined_ledger_reads_qualify_base_columns`
|
||||
- Unified ledger source filter·latest-first·keyset cursor: 같은 실제 PostgreSQL test의 `source="corporate_action"` 및 `before_recorded_at`/`before_ledger_entry_id` assertions
|
||||
- Holdings HTTP 200 및 기존 WAC/total cost basis: `tests/test_v1_portfolios_holdings.py::test_corporate_action_row_keeps_holdings_cost_basis_http_200`
|
||||
- Unified ledger HTTP 200 및 corporate-action response schema: `tests/test_v1_portfolios_manual_ledger.py::test_unified_ledger_includes_corporate_action_fields`
|
||||
- Positional mapping drift guard: `tests/test_issue34_regressions.py::test_runtime_schema_has_one_corporate_action_ddl_and_replay_mapping_starts_at_18`
|
||||
- Authorization·기존 API schema·기업행동 없는 경로: 전체 repository suite
|
||||
|
||||
## Executed evidence
|
||||
|
||||
1. Unqualified query probe on real PostgreSQL: SQLSTATE `42702`, `AmbiguousColumn` (RED).
|
||||
2. Same JOIN with `ledger.ledger_entry_id`: success (GREEN).
|
||||
3. Real PostgreSQL repository regression plus two API regressions: `3 passed, 1 warning`.
|
||||
4. Full suite with real-PostgreSQL selector enabled: `711 passed, 25 skipped, 17 warnings`.
|
||||
5. Python compile, `proofline validate`, and `git diff --check`: PASS. ProofLine emitted only pre-existing line-0001–0006 status warnings.
|
||||
6. Ruff: no finding was introduced by the changed lines; repository has four pre-existing `SIM117` findings in `app/repositories/postgres.py` and pre-existing `UP017` findings in `tests/test_v1_portfolios_holdings.py`.
|
||||
|
||||
## Artifact identities
|
||||
|
||||
- `app/repositories/postgres.py`: `2fa5ecbdc29a41bbc1379069b15e7cf43c7bf62cf8f00a3fa824ff8c182f410e`
|
||||
- `tests/test_corporate_action_atomic_recovery_postgres.py`: `0d0c436efe443b53c11b792885ca17e983320afe70d13122c7b89d3d73cf0cdd`
|
||||
- `tests/test_v1_portfolios_holdings.py`: `daff8ce2bec9f161e1a399d9bb3bea3cbf97707c6d8805cfedb66900b7b036e2`
|
||||
- `tests/test_v1_portfolios_manual_ledger.py`: `45bb77f66708b0b76e133031b3f35ac06714558c9603ec56c1b642906ca2e681`
|
||||
- `tests/test_issue34_regressions.py`: `661742e430522af58ebc15a8f97c8385e1f65126bf71a2783716c269a76ac45f`
|
||||
- `.proofline/lines/line-0007/req-0007.md`: `a3a6ae501f7635cc80a5a96416e61aa3ecce326a732126feba3b9ad1a7c8df73`
|
||||
- `.proofline/criteria/ac-0037.md`: `99dab6c2d70367c236dc08a65b24d6ddf94e131500e06cd52e9ab89784708d2d`
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
id: "line-0007"
|
||||
status: discovery
|
||||
---
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: "req-0007"
|
||||
status: approved
|
||||
discovery: "dcy-0007"
|
||||
criteria:
|
||||
create:
|
||||
- "ac-0037"
|
||||
update: []
|
||||
retire: []
|
||||
satisfy:
|
||||
- "ac-0032"
|
||||
- "ac-0034"
|
||||
- "ac-0036"
|
||||
---
|
||||
|
||||
# 기업행동 원장이 포함된 보유 원가·전체 원장 조회 복구 (#19)
|
||||
|
||||
## Objective
|
||||
|
||||
기업행동 metadata를 JOIN하는 포트폴리오 read query가 PostgreSQL column ambiguity 없이 기존 보유 원가 replay와 source-neutral 전체 원장 계약을 제공하도록 복구한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- 기업행동 원장이 있는 포트폴리오의 holdings cost-basis path가 모든 거래와 기업행동 metadata를 기존 순서·의미로 읽어 원가 replay 결과를 반환한다.
|
||||
- `GET /v1/portfolios/{portfolio_id}/ledger`가 기업행동 행과 일반 거래 행을 함께 기존 latest-first·source filter·keyset pagination 계약으로 반환한다.
|
||||
- Joined query의 base ledger column reference를 table alias로 한정하고 corporate-action metadata projection은 명시적으로 유지한다.
|
||||
- 실제 PostgreSQL repository 회귀와 API 회귀로 holdings cost-basis 및 unified ledger HTTP 200, 혼합 행, metadata mapping과 cursor 경계를 검증한다.
|
||||
|
||||
## Constraints
|
||||
|
||||
- SELECT column 순서와 `_row_to_ledger_entry()` positional mapping을 유지한다.
|
||||
- API endpoint, response schema, authorization, source/action 의미, ordering 및 cursor 형식을 변경하지 않는다.
|
||||
- Holdings cost basis, WAC, PnL 또는 corporate-action replay 회계 규칙을 변경하지 않는다.
|
||||
- 기존 active `AC-0032`, `AC-0034`, `AC-0036`의 의미를 변경하지 않고 satisfy binding으로 적용한다.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 기업행동 또는 일반 ledger row의 생성·수정·삭제·backfill.
|
||||
- broker 주문·취소·체결, producer/outbox 또는 reversal/recovery 동작 변경.
|
||||
- trex-web UI 구현, 배포, release, production mutation 또는 특정 production portfolio 데이터 보정.
|
||||
Reference in New Issue
Block a user