Files

16 KiB

id, status
id status
dcy-0005 confirmed

Portfolio ledger source-independent return aggregates

Problem

GET /v1/portfolios와 GET /v1/portfolios/{portfolio_id}/returns/summary의 투자금 집계가 portfolio_ledger.source = 'manual'인 row만 포함해 broker 등 다른 producer가 기록한 유효한 원금·수익 event를 누락했다. 이 때문에 실제 ledger provenance와 API 집계값이 달라질 수 있으며, original source 제한만 제거하고 reversal source 제한을 남기면 cross-source reversal까지 잘못 집계할 수 있었다.

이 Discovery는 요청자가 OQ-1~OQ-5를 모두 결정하고 승인한 GitHub 이슈 #28을 canonical ProofLine 문서로 포착한다. 아래 Scope의 승인 이슈 본문은 canonical H2 충돌을 피하기 위해 heading만 한 단계 낮추고 나머지 내용을 최대한 원문 순서대로 보존한다.

Evidence

승인 원문과 provenance

  • 승인 원문: https://github.com/fourblocks2020/portfolio-hub/issues/28
  • 제목: Fix investment totals: remove source='manual' filter from ledger aggregates
  • 상태: closed
  • 최초 작성: 2026-08-18T01:53:37Z
  • 승인 본문 최종 갱신: 2026-08-19T08:39:49Z
  • 승인 본문 SHA-256: d5e5f65500d163bd5a9c9a97f47222e96373b3b6d0e09b021f1de3a2380a6014
  • Exact 승인 본문 snapshot: evidence/issue-28-approved-discovery.md
  • 승인 결과: APPROVED; OQ-1~OQ-5는 각각 A로 결정됐고 unresolved blocking Open Question은 없다.

구현·검증 evidence

  • 구현 기준 commit: 02bfa602995b8e1c2f410c9bd5762f062666f2d6
  • 구현 commit: 5eb8c042668b1c383ac59ca9be00aa1a5dfcb37a (fix: aggregate investment totals across ledger sources)
  • 구현 진행 comment: https://github.com/fourblocks2020/portfolio-hub/issues/28#issuecomment-5339488232; exact snapshot evidence/implementation-progress.md; SHA-256 ada9617f9fb106e890ab4b232f23cde3b457b429971b664ed9fcc34689d03c52
  • 구현 완료 comment: https://github.com/fourblocks2020/portfolio-hub/issues/28#issuecomment-5339596866; exact snapshot evidence/implementation-completion.md; SHA-256 3458db05561d843a7ecebab816f6aecc6120a6a2d62a4be151efd2876f0d3007
  • Focused repository/API/docs contract tests: 39 passed.
  • Disposable PostgreSQL 16 semantic integration: 1 passed; broker-only, mixed-source, cross-source reversal 양방향과 buffer_out 제외를 실제 production repository SQL로 검증했다.
  • 기존 temp/deleted_20260813/.worktrees/ artifact를 제외한 clean candidate full suite: 661 passed, 16 skipped.
  • 변경 test Ruff와 git diff --check: PASS.
  • 최초 독립 QC의 stale manual-only interface contract Major를 보완한 뒤 독립 재QC: PASS, Blocker 0 / Major 0 / Minor 0.

Lifecycle 경계

  • 현재 local main에 구현·검증·독립 재QC·commit까지 완료됐다.
  • Push, PR, 원격 main 반영, release, production deployment/restart, production read-only pre/post check와 production ledger mutation은 수행되지 않았다.
  • 이 confirmed Discovery와 사후 approved REQ/active AC는 normative scope와 현재 구현 평가를 기록하며 terminal lifecycle closure나 미수행 외부 반영을 주장하지 않는다.

Scope

승인된 이슈 본문 전체

Discovery: 투자 원금 집계의 source-independent 의미 복원

  • 상태: APPROVED
  • 대상 저장소: fourblocks2020/portfolio-hub
  • 대상 API: GET /v1/portfolios, GET /v1/portfolios/{portfolio_id}/returns/summary
  • 관련 이슈: #23의 manual-only cash-flow 집계 가정 중 Invest-total field 의미를 이 이슈가 대체한다.

Problem

get_return_summaries()는 현재 투자 원금 집계를 manual ledger row로 제한한다.

WHERE ledger.source = 'manual'
  AND ledger.side IN ('invest', 'withdraw', 'profit')

그 결과 total_invested와 net_contributed_capital은 portfolio ledger 전체의 Invest/net Invest가 아니라 manual source의 cash flow만 반영한다. Broker 등 다른 source가 기록한 유효한 INVEST가 제외되므로 사용자에게 표시되는 투자 원금 의미와 실제 집계가 불일치한다.

이 동작은 #23의 manual-cash-flow contract에서 유래했지만, Invest-total field가 모든 유효한 투자 원금을 나타내야 한다면 적합하지 않다.

Evidence

  • Production 관찰 사례인 portfolio 353200에는 broker-origin invest, buffer_out, buy ledger entry가 있고 manual INVEST/WITHDRAW가 없다.
  • 현재 portfolio API는 모든 source='broker' row를 집계에서 제외하므로 net_contributed_capital: 0.0을 반환한다.
  • 같은 사례에서 broker invest amount는 0, buffer_out은 800,000이다. 이 이슈의 source-filter 수정만으로 buffer_out을 INVEST로 간주하거나 zero-amount event를 교정하지는 않는다.

Target Semantics

승인된 목표 semantics는 다음과 같다.

total_invested              = sum(valid INVEST entries from every source)
total_withdrawn             = sum(valid WITHDRAW entries from every source)
net_contributed_capital     = total_invested - total_withdrawn
cumulative_profit_withdrawn = sum(valid PROFIT entries from every source)

valid는 원본 ledger entry가 REVERSAL.target_ledger_entry_id로 연결된 matching reversal에 의해 취소되지 않았음을 뜻한다.

total_invested는 gross value인 Σ INVEST로 유지한다. WITHDRAW는 total_withdrawn과 net_contributed_capital에 반영하고, PROFIT는 투자 원금 계산에서 제외하여 cumulative_profit_withdrawn으로만 별도 집계한다.

Scope

  • 투자 원금 aggregate에서 ledger.source = 'manual' predicate를 제거하고 모든 source의 유효한 INVEST·WITHDRAW·PROFIT을 side별로 집계한다.
  • INVEST, WITHDRAW, PROFIT accounting side 의미는 유지한다.
  • Original entry와 동일 portfolio에서 target_ledger_entry_id가 일치하는 REVERSAL이 있으면 reversal 자체의 source와 무관하게 original entry를 제외한다.
  • Portfolio list와 single-portfolio returns summary가 동일한 aggregate semantics를 사용하게 한다.
  • Non-manual broker INVEST와 mixed manual/broker INVEST·WITHDRAW·reversal 회귀를 검증한다.
  • API/interface 문서에서 total_invested와 net_contributed_capital의 manual-only 표현을 승인된 의미로 갱신한다.
  • 기존 owner/admin/service visibility와 authorization 경계를 변경하지 않는다.

Out of Scope

  • buffer_out, buy 등 non-INVEST event를 INVEST로 재분류하는 작업
  • Zero-amount broker invest를 non-zero event로 정규화하는 ledger-event modelling
  • Existing ledger row의 mutation, backfill 또는 rewrite
  • Ledger producer의 event emission 변경
  • Reversal model 또는 REVERSAL.target_ledger_entry_id 의미 변경
  • Owner/admin/service visibility 및 authorization 변경
  • 이 Discovery 승인만으로 배포·릴리스·production mutation을 수행하는 것

Risks and Constraints

  • Source predicate 제거는 과거 모든 source row에 즉시 적용되는 query semantic correction이므로 기존 응답 값이 증가하거나 달라질 수 있다.
  • Side label이 같다는 이유만으로 buffer_out·buy 같은 다른 accounting event를 합산하면 안 된다.
  • Reversal exclusion을 source-independent 집계와 함께 유지하지 않으면 reversed principal이 중복 반영될 수 있다.
  • List와 detail summary가 다른 query path를 사용한다면 동일 fixture로 semantic parity를 검증해야 한다.
  • total_invested의 의미 변경은 API field rename 없이 기존 field contract를 교정하는 선택인지 명시해야 한다.
  • Production 사례 353200은 source-filter defect와 separate ledger-event modelling gap을 동시에 포함하므로, source-filter 수정만으로 800,000이 투자 원금으로 표시된다고 보장해서는 안 된다.

Open Questions

OQ-1 — Invest-total 집계의 source 범위
  • A. 모든 source의 유효한 원금 event를 포함하되 total_invested는 gross Σ INVEST로 유지한다. — 결정
    • total_invested = Σ valid INVEST, total_withdrawn = Σ valid WITHDRAW, net_contributed_capital = Σ valid INVEST - Σ valid WITHDRAW로 계산한다.
    • PROFIT는 원금 계산에서 제외하고 cumulative_profit_withdrawn = Σ valid PROFIT으로만 별도 집계한다.
    • ledger.source = 'manual' predicate를 제거하고 source와 무관하게 accounting side와 reversal을 적용한다.
  • B. manual과 broker만 포함하는 allowlist를 사용한다.
    • 현재 알려진 두 source만 포함하지만 새 source가 추가되면 다시 누락될 수 있다.
  • C. Manual-only 의미를 유지하고 별도 all-source field를 추가한다.
    • 기존 값은 보존하지만 API schema와 UI 소비 계약이 확장되며 본 이슈의 단순 semantic correction 범위를 넘어간다.
OQ-2 — buffer_out 및 zero-amount broker invest 처리
  • A. 이 이슈에서는 현행 accounting side를 그대로 사용하고 별도 ledger-event modelling 이슈로 분리한다. — 결정
    • buffer_out을 INVEST로 추론하지 않으며 source-filter defect만 교정한다.
    • Portfolio 353200의 800,000 표시 문제는 별도 결정 없이는 해결됐다고 주장하지 않는다.
  • B. buffer_out을 이 집계에서 INVEST로 간주한다.
    • 사용자 표시 값은 달라질 수 있지만 accounting event 의미를 query layer에서 재해석한다.
  • C. Broker producer가 non-zero INVEST를 기록하도록 이 이슈 범위를 확장한다.
    • Producer contract, historical data, deduplication과 migration 결정이 추가로 필요하다.
OQ-3 — 기존 ledger data 처리 방식
  • A. Query semantic만 교정하고 existing ledger row를 mutation/backfill하지 않는다. — 결정
    • 모든 기존 유효 row가 새 query에 자동 반영되며 원장 provenance를 보존한다.
  • B. Source 또는 side를 migration으로 backfill한다.
    • Historical ledger mutation 범위, 대상 식별과 rollback/audit 계약이 별도로 필요하다.
  • C. 별도 aggregate projection/table을 만들어 재계산한다.
    • 지속 materialization, replay, ordering 및 consistency ownership이 추가된다.
OQ-4 — API compatibility 방식
  • A. 기존 field 이름을 유지하고 의미를 all-source Invest-total로 교정한다. — 결정
    • total_invested, total_withdrawn, net_contributed_capital, cumulative_profit_withdrawn의 문서를 함께 갱신한다.
  • B. 새 field를 추가하고 기존 field는 manual-only로 유지한다.
    • 하위 호환은 보존하지만 response schema와 consumer 선택이 복잡해진다.
  • C. Versioned endpoint에서만 새 의미를 제공한다.
    • 가장 강한 contract 분리를 제공하지만 새 API version과 migration 계획이 필요하다.
OQ-5 — 배포 후 검증 경계
  • A. Repository tests에 더해 승인된 read-only production query/API pre/post check를 별도 배포 절차에 포함한다. — 결정
    • Source-independent 합계, reversal 제외와 list/detail parity를 확인하되 production row는 변경하지 않는다.
    • 353200은 source-filter 제거 여부를 확인하는 관찰 사례로만 사용하고 buffer_out=INVEST를 기대하지 않는다.
  • B. Repository tests만 수행하고 production 검증은 하지 않는다.
    • 구현 검증은 단순하지만 실제 source 분포와 응답 변화 확인이 남는다.
  • C. Synthetic staging data에서만 post-deploy 검증한다.
    • Production read를 피하지만 실제 historical source/reversal 조합을 완전히 대표하지 못할 수 있다.

Acceptance Criteria

OQ가 확정되면 아래 criterion을 선택 결과에 맞춰 canonical requirement로 전환한다.

  • Investment aggregate에서 original ledger row의 source predicate가 제거되고 모든 source의 유효 row가 side별로 집계된다.
  • total_invested = Σ valid INVEST, total_withdrawn = Σ valid WITHDRAW, net_contributed_capital = total_invested - total_withdrawn이며 PROFIT는 원금 계산에서 제외되고 cumulative_profit_withdrawn으로만 집계된다.
  • 동일 portfolio에서 matching target_ledger_entry_id를 가진 REVERSAL이 존재하면 reversal source와 무관하게 original entry가 제외된다.
  • GET /v1/portfolios와 single-portfolio returns summary가 동일한 aggregate semantics를 사용한다.
  • Non-manual broker INVEST의 non-zero amount 회귀가 검증된다.
  • Mixed manual/broker INVEST·WITHDRAW, reversal과 source-independent total이 검증된다.
  • API/interface 문서가 승인된 field 의미를 정확히 설명한다.
  • Existing owner/admin/service visibility 경계가 유지된다.
  • Out-of-scope인 buffer_out 재분류나 historical ledger mutation이 암묵적으로 포함되지 않는다.

Approval Record

  • 승인 상태: APPROVED
  • 승인 결정: OQ-1-A, OQ-2-A, OQ-3-A, OQ-4-A, OQ-5-A
  • OQ-1: 모든 source의 유효한 row를 side별로 집계한다. total_invested는 gross Σ INVEST, net_contributed_capital은 Σ INVEST - Σ WITHDRAW이며 PROFIT는 원금 계산에서 제외한다.
  • OQ-2: buffer_out 재분류와 zero-amount broker invest 교정은 별도 ledger-event modelling으로 분리한다.
  • OQ-3: Query semantic만 교정하고 existing ledger row를 mutation/backfill하지 않는다.
  • OQ-4: 기존 field 이름을 유지하고 all-source Invest-total 의미로 문서와 contract를 교정한다.
  • OQ-5: Repository 검증에 더해 별도 배포 절차에서 승인된 read-only production query/API pre/post check를 수행한다. Production row mutation 권한은 포함하지 않는다.
  • Reversal 적용: all-source original 집계에서 matching reversal도 source와 무관하게 적용한다. reversal.source = 'manual' predicate를 보존하면 broker original reversal이 누락되므로 허용하지 않는다.
  • 추가 결정 검토: 현재 구현·data-flow 검토에서 승인을 중지할 추가 material OQ는 발견되지 않았다.
  • 권한 경계: 이 승인은 이 Discovery와 bounded implementation/verification scope를 승인한다. Push, PR, release, deployment, production mutation 또는 issue closure 권한을 자동으로 부여하지 않는다.

AC admission 분류

  • update: 기존 active ac-0001, ac-0002, ac-0003, ac-0005의 manual-only 의미를 source-independent 계약과 현재 검증 상태로 교정한다.
  • satisfy: 기존 active ac-0004의 목록 가시성·인증·필터 격리는 의미 변경 없이 이번 구현이 충족한다.
  • create: 없음. 동일 behavior를 새 AC로 중복하지 않는다.
  • retire: 없음.
  • Release evidence와 terminal closure: 없음.
  • Housekeeping: GitHub comment exact snapshot 두 건을 Line evidence로 보존한다.

Out of Scope

  • Existing ledger row mutation 또는 historical migration/backfill
  • buffer_out을 INVEST로 재분류하거나 zero-amount broker event를 교정하는 ledger-event modelling
  • 별도 aggregate projection/table 및 성능 근거 없는 schema 확장
  • API field rename 또는 breaking response change
  • Push, PR, 원격 main 반영, release, production deployment/restart
  • Production write/mutation과 credential 기록
  • 배포 절차의 승인된 read-only production pre/post check를 이미 완료했다고 주장하는 것

Risks and Unknowns

Risks

  • 모든 source를 집계하므로 producer가 accounting side를 잘못 기록하면 그 modelling 오류가 aggregate에 노출될 수 있다. 이번 범위는 side 재분류가 아니라 유효한 기존 side의 source filter 교정이다.
  • Cross-source reversal을 놓치면 original row가 잘못 남으므로 original과 reversal 양쪽의 source 제한을 제거하고 실제 PostgreSQL 테스트로 검증했다.
  • 배포 전 실제 production data parity는 아직 미확인이다. 승인된 read-only pre/post check는 별도 배포 절차에서 수행해야 하며 mutation 권한을 포함하지 않는다.

Open Questions

없음. OQ-1~OQ-5는 승인 이슈 본문에서 모두 결정됐으며 추가 blocking decision은 발견되지 않았다.