--- { "schema": "gitea-agent.requirement/v1", "id": "REQ-003", "title": "계좌 기준일 평가 이력과 자산 마감 연동", "revision": "1.0", "status": "approved", "depends_on": [], "applies_to": ["ledger", "dashboard", "mcp"], "language": "ko", "issue": "#12", "implementation_pr": "미정" } --- # REQ-003 — 계좌 기준일 평가 이력과 자산 마감 연동 ## 1. 목적 계좌의 기준일 평가 증거를 append-only 이력으로 저장·선택하고, 검증 가능한 자산만 자산/마감 및 월정산의 역사적 순자산에 반영한다. ## 2. 배경 구현은 SQLite schema 12를 schema 11에 additive하게 적용한다. 계좌 잔액·import 결과는 실제 증거가 없는 한 `unverified` 후보이고, 기존 거래·인증·메뉴 계약을 바꾸지 않는다. ## 3. 범위 ### 포함 - `account_valuations`, `asset_snapshot_runs`, `asset_snapshots` 확장과 exact/as_of 선택 - idempotency, append-only correction, 투자 optional metrics, loan 대표 부채 - read-only preview, explicit approval conditional close, immutable evidence, correction/re-close - 계좌·import 후보, reconciliation, MCP·Dashboard·월정산 공통 read model ### 제외 - credential/실제 금융 데이터, 자동 추정 거래 생성, fake investment transaction, 거래 원장 재작성 - 외부 자산 가격 API, 환율 변환, 자동 승인, unverified/future/unknown 값 승인 - Dashboard 인증·logout·cookie, MCP 인증의 신규 도입, 기존 메뉴 순서 변경 ## 4. 용어와 기준 - **verified leaf**: `supersedes_id`로 더 최신 child가 없는 `verification_status=verified` 행. - **exact**: 요청 `basis_date`와 일치하는 verified leaf만 선택한다. - **as_of**: 미래를 보지 않고 요청일 이전 최신 verified leaf를 선택하며 carry-forward를 표시한다. - **대표 부채**: `valuation_kind`가 `loan`/`loan_balance`이고 `representative_liability_definition`이 `principal_only` 또는 `principal_interest_fees`인 하나의 대표 금액만 합산한다. - **정본 구현**: `jarca_ledger_mcp/application/account_valuation_service.py`, `assets_service.py`, `reconciliation_service.py`, `migrations.py` 및 named tests. ## 5. 요구사항 ### REQ-003.01 — additive 이력과 legacy 보존 schema 11의 테이블·필드·거래를 삭제하거나 추측 이주하지 않고 schema 12의 `account_valuations`, `asset_snapshot_runs`를 추가하며 `asset_snapshots`에 필요한 필드를 additive하게 확장한다. migration은 idempotent다. **영향 범위** - 인터페이스: 기존 service/MCP 응답 envelope 유지, 신규 valuation/snapshot fields additive - 영속성: SQLite schema 11→12, 기존 rows 보존 - 후속 사용 주체: service, MCP tool, Dashboard gateway/page, 월정산 - 마이그레이션: `migrate_db()`의 `CREATE TABLE IF NOT EXISTS`/`ADD COLUMN` - 호환성: REQ-001 auth, cookie/logout, REQ-002 메뉴 10개/order, query/shell/browser evidence 유지 - 배포 전제 조건: SQLite write 권한과 기존 DB 백업 ### REQ-003.02 — verified leaf 선택과 범위 exact/as_of 선택은 account/currency/valuation_kind scope를 적용하고 future, unverified, rejected, unknown을 authority에서 제외한다. newer unverified candidate는 warning으로 남기며 investment는 `total_value`, `balance`, `cash_balance` 우선, loan은 `loan_balance`, `loan`, `balance` 우선이다. **영향 범위** - 인터페이스: `select_account_valuation()`, `_account_valuation_items()`, report warnings - 영속성: 상태·기준일·통화·kind와 candidate evidence 보존 - 후속 사용 주체: snapshot preview/close, Dashboard 계좌·자산, reconciliation - 마이그레이션: 없음 - 호환성: 기존 `last_balance` fallback은 basis-date 없는 legacy report에서만 유지 - 배포 전제 조건: ISO date와 허용 currency/kind ### REQ-003.03 — idempotency와 append-only correction 같은 idempotency payload는 기존 row/run을 재사용하고 다른 payload는 `idempotency_conflict`다. correction은 원본을 갱신하지 않고 단일 child를 append하며 다른 child는 `correction_conflict`다. **영향 범위** - 인터페이스: `register_account_valuation()`, `correct_account_valuation()`, `close_asset_snapshot()` - 영속성: unique idempotency 및 single-correction indexes - 후속 사용 주체: retry caller와 history/report - 마이그레이션: unique indexes additive - 호환성: 성공/실패 envelope와 기존 error_code 유지 - 배포 전제 조건: transaction-scoped SQLite write ### REQ-003.04 — 금액·옵션·대출 의미 amount와 principal/interest/fee/cost_basis/net_contributed/profit은 exact integer이고 NULL은 미제공, 0은 명시적 영(0)이다. 투자 optional metrics는 저장하되 total valuation을 다시 합산하지 않는다. loan은 대표 정의를 필수로 하고 principal/interest/fee의 unknown을 NULL로 보존하며 대표 금액만 총부채에 합산한다. **영향 범위** - 인터페이스: valuation input/output fields, validation errors - 영속성: INTEGER/REAL return_rate와 nullable optional columns - 후속 사용 주체: net-worth formula와 UI labels - 마이그레이션: 없음 - 호환성: 기존 integer ledger amounts와 double-sum 금지 - 배포 전제 조건: caller가 currency/definition을 명시 ### REQ-003.05 — current와 historical authority current latest display와 historical authority를 분리한다. 늦게 도착한 historical row는 current를 과거로 되돌리지 않으며 account/import writes는 real dated evidence가 있을 때만 dated `unverified` candidate를 만든다. **영향 범위** - 인터페이스: account update/import candidate, list/select/report - 영속성: `basis_date`/`recorded_at` 분리 - 후속 사용 주체: 계좌 화면, reconciliation, monthly settlement - 마이그레이션: 없음 - 호환성: account name-only update는 candidate를 만들지 않음 - 배포 전제 조건: source date와 source_ref/hash ### REQ-003.06 — preview·close·approval preview는 DB를 쓰지 않고 `complete_exact`, `incomplete_draft`, `conditional` 상태와 warnings/exclusions를 반환한다. close는 need-based explicit `approved_by`, `approval_reason`, `approval_reference`와 flags를 요구하며 immutable run evidence를 기록한다. unverified/future/unknown/ambiguous는 승인 없이 close할 수 없다. **영향 범위** - 인터페이스: `preview_asset_snapshot_close()`, `close_asset_snapshot()` 및 error codes - 영속성: run applied/excluded/carried evidence와 approval metadata - 후속 사용 주체: Dashboard 자산/마감, MCP operator - 마이그레이션: `asset_snapshot_runs` 신규 - 호환성: preview read-only, 기존 report response fields 유지 - 배포 전제 조건: operator approval identity와 reference ### REQ-003.07 — correction/re-close와 formula 같은 basis date close는 correction_reason을 요구하고 새 run/rows를 append한다. 최신 run만 historical report authority이며 순자산은 `asset_buckets - loan`, missing valuation은 transaction inference 없이 제외한다. **영향 범위** - 인터페이스: close/report/timeseries - 영속성: `correction_of_run_id`, `is_correction`, `closed_at` - 후속 사용 주체: timeseries와 월정산 - 마이그레이션: asset snapshot columns/indexes - 호환성: legacy `asset_snapshots` rows are readable - 배포 전제 조건: close transaction atomicity ### REQ-003.08 — reconciliation과 monthly propagation investment가 verified valuation으로 관리되면 reconciliation status는 `VALUATION_MANAGED`이고 fake transaction을 만들지 않는다. regular ledger calculation은 `calculation_basis`/`calculation_basis_at` provenance를 유지한다. monthly settlement는 read-only로 동일 snapshot/report를 전파한다. **영향 범위** - 인터페이스: `get_balance_reconciliation_report()`, `get_monthly_settlement_summary()` - 영속성: reconciliation session/transaction은 valuation 때문에 자동 변경하지 않음 - 후속 사용 주체: 월정산·overview·reconciliation UI - 마이그레이션: 없음 - 호환성: existing reconciliation statuses and reports additive - 배포 전제 조건: service read model available ### REQ-003.09 — shared application과 보안 경계 MCP와 Dashboard는 shared service/gateway를 사용한다. 기존 success/error envelope를 유지한다. Dashboard는 auth-before-DB이며 MCP auth를 claim하지 않는다. render는 read-only이고 implicit write를 일으키지 않는다. **영향 범위** - 인터페이스: MCP `@mcp.tool`, Streamlit gateways/pages - 영속성: render/preview에는 write 없음; explicit form/close만 write - 후속 사용 주체: browser/operator/automation - 마이그레이션: 없음 - 호환성: REQ-001 gate, cleanup/logout, MCP transport/auth scope 유지 - 배포 전제 조건: Dashboard auth config, MCP runtime config ### REQ-003.10 — privacy, compatibility, excluded scope 문서·tests·fixtures는 synthetic data만 사용하고 actual credential/account/financial data를 저장하지 않는다. Issue #12 밖의 OAuth, external prices, FX, automatic approval, transaction inference, redesign는 구현하지 않는다. **영향 범위** - 인터페이스: redacted evidence와 generic errors - 영속성: synthetic source_note/memo only - 후속 사용 주체: tests/operator - 마이그레이션: 없음 - 호환성: 기존 auth/menu/query/shell/browser evidence semantic inventory 0 missing - 배포 전제 조건: secret redaction과 fixture DB ## 6. 제약과 불변조건 - verified leaf만 authority이며 future/unverified/rejected/unknown은 합산하지 않는다. - 동일 idempotency payload는 같은 ID를 반환하고 다른 payload는 conflict다. - correction과 re-close는 append-only이고 parent당 child/run은 하나다. - NULL과 0을 구별하고 모든 금액은 integer precision이다. - loan representative만 liability sum에 들어가고 investment metrics는 double sum하지 않는다. - preview와 render는 DB write가 없고 close는 하나의 transaction 경계다. - current display와 historical basis authority는 분리된다. - Dashboard auth gate는 DB보다 먼저이며 MCP 인증은 이 요구사항이 주장하지 않는다. - 실제 credential·token·cookie·account data는 문서·로그·fixture에 없다. ## 7. 인수 기준 ### REQ-003.01-AC01 — migration and preservation **Cases (정상/경계/실패)**: 정상 schema 12 init twice; 경계 schema 11 legacy DB; 실패 malformed legacy DB. **State Invariants**: legacy tables/rows remain; migration repeats without duplicate columns. **판정 기준**: `schema_version=12`, required tables/columns/indexes exist and old rows remain. **검증**: `tests/test_account_valuations.py::test_account_valuation_schema_is_v12_and_idempotent`, `tests/test_issue_12_regressions.py::test_schema12_migration_preserves_legacy_rows_indexes_and_is_idempotent`. ### REQ-003.02-AC01 — selection policy **Cases (정상/경계/실패)**: exact verified; as_of carry-forward; future/unverified/ambiguous excluded/warned. **State Invariants**: no future/unverified row is selected. **판정 기준**: exact returns same-date verified leaf; as_of returns latest prior verified and age/warning. **검증**: `tests/test_account_valuations.py::test_select_exact_and_as_of_excludes_unverified_and_reports_carry_forward`, source `_account_valuation_items()`. ### REQ-003.03-AC01 — idempotent correction **Cases (정상/경계/실패)**: same payload reuse; different payload conflict; second correction fails. **State Invariants**: one row/child per operation and original immutable. **판정 기준**: exact error codes and count 2 after one correction. **검증**: `tests/test_account_valuations.py::test_register_same_idempotency_payload_reuses_row_and_changed_payload_conflicts`, `tests/test_account_valuations.py::test_correction_is_append_only_reasoned_and_single_child`, `tests/test_issue_12_regressions.py::test_empty_conditional_close_is_closed_and_second_close_requires_correction`. ### REQ-003.04-AC01 — precision and representative loan **Cases (정상/경계/실패)**: integer zero; NULL optional metrics; loan missing definition rejected. **State Invariants**: no float amount and no double sum. **판정 기준**: optional fields round-trip exactly and loan definition validation is enforced. **검증**: `test_correction_preserves_optional_fields_unless_overridden`, `_validate_common()` and schema static checks. ### REQ-003.05-AC01 — authority separation **Cases (정상/경계/실패)**: dated candidate; late historical candidate; name-only update. **State Invariants**: current/latest display cannot roll backward and candidate is unverified. **판정 기준**: dated account write creates only unverified candidate; name-only does not. **검증**: `test_account_balance_write_records_unverified_candidate_only_with_real_basis_date`, `test_account_name_only_update_does_not_create_candidate`. ### REQ-003.06-AC01 — preview and approval **Cases (정상/경계/실패)**: complete exact close; incomplete draft; missing approval rejected. **State Invariants**: preview row count unchanged; no unsafe evidence approved. **판정 기준**: preview is read-only and close returns `conditional_approval_required` without metadata. **검증**: `test_manual_asset_update_deactivate_preview_and_close_correction_policy`, `preview_asset_snapshot_close()`. ### REQ-003.07-AC01 — immutable close correction **Cases (정상/경계/실패)**: first close; same-date correction; second unreasoned close. **State Invariants**: old run/rows remain and latest run alone reports. **판정 기준**: `correction_of_run_id`, count, and net worth match. **검증**: `test_manual_asset_update_deactivate_preview_and_close_correction_policy`, `assets_service.py` close path. ### REQ-003.08-AC01 — reconciliation/monthly **Cases (정상/경계/실패)**: regular ledger calculation; verified investment; missing baseline. **State Invariants**: no fake transaction; provenance fields present; monthly read is non-mutating. **판정 기준**: status `VALUATION_MANAGED` for verified investment and monthly summary reads report. **검증**: `tests/test_service.py::test_statement_status_account_update_and_balance_reconciliation`, `tests/test_issue_12_regressions.py::test_reconciliation_uses_corrected_investment_leaf_without_transaction_count_change`, `tests/test_service.py::test_monthly_settlement_summary_read_model_foundation`. ### REQ-003.09-AC01 — shared adapters and auth **Cases (정상/경계/실패)**: MCP stdio/http config; authenticated Dashboard render; unauthenticated gate. **State Invariants**: auth precedes DB; render does not write; response envelope has `success`. **판정 기준**: actual tool/gateway names resolve and existing auth/menu tests pass. **검증**: `tests/test_mcp_http_runtime.py`, `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_menu_transition.py`, `server.py` and gateways. ### REQ-003.10-AC01 — privacy and compatibility **Cases (정상/경계/실패)**: synthetic fixture; redaction search; actual secret/data detection. **State Invariants**: no credential/token/cookie/real financial data in docs/tests/evidence. **판정 기준**: semantic inventory missing=0 and ambiguous=0; placeholder=0. **검증**: `tests/test_requirement_docs.py`, auth/menu evidence tests, repository redaction/static search. ## 8. 오류 및 충돌 계약 공통 envelope는 성공 시 `success: true`, 실패 시 `success: false, error_code, error`를 유지한다. 주요 codes는 `validation_error`, `not_found`, `idempotency_conflict`, `correction_conflict`, `ambiguous_valuation`, `conditional_approval_required`, `db_error`다. SQLite unique/transaction conflict는 추가 write 없이 실패한다. ## 9. 데이터 및 인터페이스 계약 서비스는 `jarca_ledger_mcp.service` re-export를 통해 호출되고 MCP는 `jarca_ledger_mcp.adapters.mcp.server` tools를 노출한다. Dashboard는 `adapters/streamlit/accounts_gateway.py`, `assets_gateway.py`와 `dashboard/page_views/accounts.py`, `assets.py`를 사용한다. 실제 fields/signatures는 `docs/interface-contract.md`에 고정한다. ## 10. 하위 호환성 REQ-001의 auth gate·cookie/cleanup/logout, REQ-002의 메뉴 10개와 order(`월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`), query/shell/background와 browser evidence 계약을 삭제·축약하지 않고 additive하게 유지한다. 기존 MCP stdio/streamable-http transport와 success/error envelope를 유지한다. ## 11. 추적성 - 근거 이슈: `#12` - 구현 PR: 미정 - 관련 설계 문서: `docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flow.md` - 관련 요구사항: `REQ-001`, `REQ-002`