Files
jarca-ledger-mcp/docs/interface-contract.md
T

465 lines
34 KiB
Markdown

# Interface Contract — Dashboard 관리자 인증
## 목적과 범위
Dashboard 인증 설정, login/cleanup/logout HTTP route, signed cookie와 Streamlit gate의 외부·내부 계약을 정의한다.
- 포함 범위: 환경변수, HTTPS form POST, cookie 발급·삭제, fail-closed gate
- 제외 범위: MCP 인증, OAuth/OIDC, 다중 사용자, DB session/token registry, per-token revocation
- 사용 주체: browser, Dashboard ASGI server, Streamlit app, 운영 배포
## 계약
### auth-config — 서버 인증 설정
- 책임: 현재 configured credential과 signing secret을 server-only로 제공한다.
- 종류·프로토콜: process environment
- 호출 대상·operation: `load_auth_settings()`
- 버전: 2.0
- 호출 주체: Dashboard server
- 인증 방식: 없음
- 필요 권한: process secret read
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `JARCA_DASHBOARD_ADMIN_ID` | environment | `string` | 필수 | 없음 | non-empty | configured admin identifier | 민감 |
| `JARCA_DASHBOARD_ADMIN_PASSWORD` | environment | `string` | 필수 | 없음 | non-empty | configured credential | 민감 |
| `JARCA_DASHBOARD_SIGNING_SECRET` | environment | `string` | 필수 | 없음 | UTF-8 32 byte 이상 | signing input | 민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `AuthSettings` | server memory | `object` | 성공 시 필수 | current configured values | verifier input; never serialized | 민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `configuration_unavailable` | configuration | missing/empty/read error/short secret | 수정 후 가능 | generic unavailable | no cookie, no DB |
### login — `POST /api/auth/login`
- 책임: same-origin HTTPS credential을 검증하고 signed cookie를 발급한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation: `/api/auth/login`
- 버전: 1.0
- 호출 주체: browser login form
- 인증 방식: submitted credential, server-only comparison
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `admin_id` | form body | `string` | 필수 | 없음 | bounded form value | submitted ID | 민감 |
| `admin_password` | form body | `string` | 필수 | 없음 | bounded form value | submitted password | 민감 |
| `remember_me` | form body | `string` | 선택 | off | `on` or absent | choose 30-day TTL | 비민감 |
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | success | hardened auth cookie, redacted value | persistent auth | 값은 민감 |
| `Location` | response header | `string` | success/failure | `/` or generic error path | browser navigation | 비민감 |
발급 token은 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti` claim만 가진다. `sub=dashboard-admin`은 관리자 ID가 아닌 고정 비민감 role이고, token 값과 서명은 민감하므로 browser의 hardened cookie에만 저장한다. 성공한 login 시각에 `iat`와 `exp`를 한 번 고정하며 기본 TTL은 24시간, `remember_me=on`이면 30일이다. 활동, 일반 요청, rerun 또는 인증 복원은 token을 재발급하거나 두 시각을 연장하지 않는다.
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `400` | request | malformed/oversized body | 수정 후 가능 | resubmit valid form | no cookie |
| `403` | origin | non-HTTPS or cross-origin | 수정 후 가능 | use same-origin HTTPS | no cookie |
| `invalid` | authentication | ID/password mismatch | 가능 | generic error and delay | no cookie |
| `unavailable` | configuration | invalid server configuration | config fix 후 | generic unavailable | no cookie |
### cleanup — `GET /api/auth/cleanup`
- 책임: invalid cookie를 matching scope로 삭제한 뒤 unauthenticated navigation을 완료한다.
- 종류·프로토콜: HTTPS navigation GET
- 호출 대상·operation: `/api/auth/cleanup`
- 버전: 1.0
- 호출 주체: same-origin browser/auth gate
- 인증 방식: same-origin HTTPS `Referer`; token validity 불필요
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `Referer` | request header | `URL` | 필수 | 없음 | request origin과 같은 HTTPS authority | navigation origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | always | same name/path/security, `Max-Age=0` | delete stale cookie | 값은 민감 |
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
### logout — `POST /api/auth/logout`
- 책임: explicit logout으로 browser cookie를 삭제한다.
- 종류·프로토콜: HTTPS form POST
- 호출 대상·operation: `/api/auth/logout`
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: same-origin HTTPS; server-side revocation 없음
- 필요 권한: same-origin HTTPS
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `none` | none | `none` | 선택 | 없음 | no credential accepted | explicit logout | 없음 |
| `Origin` | request header | `URL` | 필수 | 없음 | request `Host`와 scheme·hostname·effective port가 같은 HTTPS origin | CSRF origin 검증 | 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `Set-Cookie` | response header | `cookie` | always | matching deletion, `Max-Age=0` | remove browser cookie | 값은 민감 |
| `Location` | response header | `string` | success | `/` | unauthenticated page | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `403` | origin | non-HTTPS/cross-origin | 가능 | retry same-origin | no DB |
### gate — Dashboard 인증 경계
- 책임: config and cookie verification before any protected work.
- 종류·프로토콜: Python/Streamlit call
- 호출 대상·operation: `require_dashboard_authentication()`
- 버전: 2.0
- 호출 주체: `dashboard.app.main()`
- 인증 방식: current config plus signed cookie
- 필요 권한: valid token with `sub=dashboard-admin`
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `auth_cookie` | request cookie | `string` | success path | absent | current configuration으로 서명을 검증할 수 있고 claim이 정확히 `v`, `sub=dashboard-admin`, `iat`, `exp`, `jti`인 token | browser proof | 값과 서명은 민감; `sub`는 비민감 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `authenticated` | server result | `bool` | always | true/false | gate decision | 비민감 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `unauthenticated` | authentication | absent/invalid/expired/config failure | login 가능 | cleanup then show login | no protected work |
관리자 ID, password 또는 signing secret 변경은 server-only signing key binding을 바꾸므로 기존 token을 모두 무효화하고 cleanup으로 전이한다. DB session, token registry와 per-token revocation은 만들지 않는다.
## 호환성
- 유지할 기존 호출 방식: authenticated `page` query parameter, page dispatch, gateway와 MCP behavior
- 버전 변경 정책: REQ-001 revision 2.0; session-only boolean을 signed cookie로 대체
- 폐기 정책: client-side token storage, DB-backed session, per-token revocation 없음
- 재시도 계약: credential 실패는 최소 1초 지연; origin/request 오류는 수정 후 재시도
- 중복 처리 계약: login은 새 token 발급 가능; cleanup/logout deletion은 idempotent
- 순서 보장: config → cookie verify → cleanup if invalid → DB/page dispatch only if valid
- 만료 보장: login 성공 시 고정한 `iat`와 `exp`는 활동·일반 요청·rerun·복원에서 변경하거나 연장하지 않음
## 예시
### example-login — 정상 login
- 대상 인터페이스: `login`
#### 호출
```text
POST /api/auth/login
Origin: https://dashboard.example
admin_id=<submitted>&admin_password=<submitted>&remember_me=on
```
#### 결과
```text
303 Location: /
Set-Cookie: __Host-jarca_dashboard_auth=<redacted>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000
```
### example-cleanup — invalid cookie cleanup
- 대상 인터페이스: `cleanup`
#### 호출
```text
GET /api/auth/cleanup
Referer: https://dashboard.example/
```
#### 결과
```text
303 Location: /
Set-Cookie: __Host-jarca_dashboard_auth=; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Path=/
```
## 검증
- 정상 계약: authentication/server tests와 loopback HTTPS evidence
- 실패 계약: wrong credential, invalid configuration, cross-origin, malformed cookie tests
- 경계 계약: expiry, rotation, oversized token, same-host different-port origin tests
- 구현·스키마·예시 정합성: `tests/test_dashboard_auth_server.py`, `tests/test_dashboard_authentication_app.py`, `tests/test_dashboard_persistent_authentication.py`, source/document redaction search
## Dashboard 메뉴 전환 계약 — REQ-002
### dashboard-menu-transition
- 책임: authenticated Dashboard에서 공통 shell/background를 유지하고 선택된 page body만 교체한다.
- 종류·프로토콜: Streamlit rerun, `st.radio` callback, query parameter
- 호출 대상·operation: `dashboard.app.main()` → `render_sidebar_nav()` → `render_dashboard_body()`
- 버전: 1.0
- 호출 주체: authenticated browser
- 인증 방식: 기존 `gate` 계약 선행; 메뉴는 인증을 우회하지 않음
- 필요 권한: valid Dashboard auth cookie
#### 상태와 입력
정확한 menu options/order는 `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`이다. selected value는 `st.session_state["dashboard_page"]`에 둔다. 직접 query의 `page`가 known option이면 해당 page를, absent/unknown이면 `요약`을 사용한다.
#### 출력·불변조건
- callback은 `st.query_params["page"]`만 갱신하고 `month`, `account_id`, `filter` 등 sibling key를 보존한다.
- menu click은 document/path navigation이 아니며 stable `st.empty` body container 안에서 target renderer를 실행한다.
- 공통 sidebar, refresh control, CSS/background는 유지하고 body만 target content로 대체한다.
- 느린 target에는 `<target> 불러오는 중…`을 표시한 뒤 renderer 완료 시 placeholder를 제거한다.
- renderer 예외는 body 안의 `페이지를 불러오지 못했습니다. 다시 시도해 주세요.`와 retry control로 대체하며 예외 상세를 노출하지 않는다.
- serialized callback이 여러 번 실행되면 마지막 selection이 query와 body의 최종 상태가 된다.
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `page_fallback` | request | missing/unknown `page` | 가능 | `요약` 표시 | sibling query 유지 |
| `renderer_unavailable` | rendering | target renderer exception | 가능 | generic error의 retry 클릭 | previous body 제거, auth/shell 유지 |
#### 검증 경계
AppTest는 callback, query mapping, synthetic renderer output과 error/loading event를 검증하지만 실제 browser의 document navigation 부재, paint timing, focus/scroll, network request 또는 perceived shell continuity를 증명하지 않는다. 따라서 `REQ-002.03-AC01`은 authenticated real-browser evidence가 필수이며 AppTest는 companion evidence일 뿐이다. history/back-forward와 path URL은 계약에서 제외한다.
상세 acceptance target은 `docs/requirements/req-002_dashboard-menu-transition.md`의 `REQ-002.01-AC01`부터 `REQ-002.09-AC01`까지를 따른다.
## 계좌 평가·자산 마감 계약 — REQ-003
### valuation-service
- 책임: `register_account_valuation()`, `list_account_valuations()`, `select_account_valuation()`, `correct_account_valuation()`을 통해 exact/as_of verified leaf와 append-only history를 제공한다.
- 종류·프로토콜: Python service function; `jarca_ledger_mcp.service` re-export
- 호출 대상·operation: `jarca_ledger_mcp.application.account_valuation_service`
- 버전: additive v1
- 호출 주체: Dashboard accounts gateway, MCP tool, import/account write service
- 인증 방식: service layer 자체 MCP auth는 주장하지 않음; Dashboard는 기존 `gate` 선행
- 필요 권한: caller가 DB path와 approval context를 제공
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `account_id`/`basis_date`/`amount` | function/MCP | `string/string/int` | register 필수 | 없음 | account id, YYYY-MM-DD, exact integer | valuation identity/value | 아니오 |
| `currency`/`valuation_kind`/`source_type`/`verification_status` | function/MCP | `string` | 선택 | KRW/balance/manual/unverified | canonical enums listed below | 아니오 |
| `idempotency_key` | function/MCP | `string` | 선택 | `''` | stable caller key | retry identity | 아니오 |
| `as_of_at`/`timezone`, optional metrics, loan definition | function/MCP | nullable string/int/float | 선택 | NULL | source validation; loan definition required | evidence metrics | 아니오 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `success` | envelope | `bool` | 항상 | true/false | outcome | 아니오 |
| `valuation`/`valuations` | result | object/list | 성공 시 | persisted rows | history/evidence | source_ref may be sensitive |
| `reused`, `carried_forward`, `warning`, `unverified_candidates` | result | bool/string/list | 조건부 | additive | idempotency/selection evidence | 아니오 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `validation_error` | request | invalid date/type/currency/loan definition | 수정 후 가능 | payload 수정 | no write; no row or cache mutation |
| `not_found` | lookup | account/valuation missing | 수정 후 가능 | id 확인 | 없음 |
| `idempotency_conflict` | conflict | same key different payload | 아니오 | new key or inspect | 없음 |
| `correction_conflict`/`ambiguous_valuation` | conflict | second child/multiple leaves | 수동 검토 | correct evidence | 없음 |
### snapshot-close
- 책임: `preview_asset_snapshot_close()` is read-only; `close_asset_snapshot()` writes an immutable run and rows.
- 종류·프로토콜: Python service, Streamlit `assets_gateway`, MCP tools
- 호출 대상·operation: `jarca_ledger_mcp.application.assets_service`
- 버전: additive v1
- 호출 주체: authenticated Dashboard operator or MCP caller
- 인증 방식: Dashboard existing auth gate; MCP authentication is outside this contract
- 필요 권한: close conditional approval fields when warnings require it
#### 입력
| 필드 | 위치 | 타입 | 필수 여부 | 기본값 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|---|
| `basis_date`/`selection_policy` | function/MCP/form | `string/string` | basis optional | today/exact | YYYY-MM-DD, exact/as_of | close scope | 아니오 |
| `idempotency_key`/`correction_reason` | function/MCP/form | `string` | conditional | empty | stable key; reason on re-close | retry/correction | 아니오 |
| `allow_missing_exclusions`/`allow_as_of_carry_forward` | function/MCP | `bool` | optional | false | explicit flags | conditional approval need | 아니오 |
| `approved_by`/`approval_reason`/`approval_reference` | function/MCP/form | `string` | conditional | empty | all required together | approval evidence | 아니오 |
#### 출력
| 필드 | 위치 | 타입 | 필수 여부 | 허용 값·형식 | 의미 | 민감정보 |
|---|---|---|---|---|---|---|
| `source`/`is_closed_snapshot`/`snapshot_run_id` | result | string/bool/string | success | live_authority/snapshot | preview vs close | 아니오 |
| `close_status`/`selection_policy`/`warnings` | result | string/string/list | success | complete_exact/incomplete_draft/conditional | authority state | 아니오 |
| `total_assets`/`total_liabilities`/`net_worth` | result | integer | success | exact integer | formula summary | 아니오 |
#### 오류
| 코드 | 분류 | 발생 조건 | 재시도 가능 여부 | 호출자 대응 | 실패 시 상태 변경 |
|---|---|---|---|---|---|
| `conditional_approval_required` | policy | unsafe exclusions/carry-forward without explicit approval | 수정 후 가능 | preview, approve explicitly | 없음 |
| `idempotency_conflict` | conflict | close key payload differs | 아니오 | inspect run | 없음 |
| `validation_error` | request | invalid policy or unreasoned correction | 수정 후 가능 | provide reason | no partial rows |
#### Canonical valuation values and validation
The public contract explicitly supports these exact values: source_type: `manual`, `import`, `account_write`, `reconciliation`, `snapshot`; verification_status: `verified`, `unverified`, `rejected`; valuation_kind: `balance`, `cash_balance`, `total_value`, `loan`, `loan_balance`; representative_liability_definition: `principal_only`, `principal_interest_fees`. These are the currently supported enums, not a delegation to source code.
loan accounts require representative_liability_definition for `loan`, `loan_balance`, and `balance` aliases. Those are the only accepted valuation kinds for loan accounts; `cash_balance` and `total_value` do not represent a loan liability and are rejected. legacy valuation-kind aliases remain accepted and map to the canonical purpose; alias acceptance does not broaden the canonical enum set. Invalid values, missing loan definitions, and incompatible definitions return `validation_error` and perform no write. A validation failure returns `validation_error` and performs no write. Converting an existing non-loan account to `loan` is rejected with `validation_error` if any attached valuation lacks an explicit valid definition; the account and valuation history remain unchanged. Conversion with no valuations remains allowed.
loan valuations map to liabilities and non-loan valuations map to assets. The output mapping preserves the existing envelopes and signatures: `success` remains the outcome field, failures include `error_code` and `error`, and validation or conflict failures do not partially write.
Account current-cache writes keep `last_balance_basis_date` (optional `YYYY-MM-DD`) separate from `last_synced_at` (execution/sync timestamp). `create_ledger_account` keeps the established positional `db_path` argument and adds `last_balance_basis_date` after it as an optional keyword. `update_ledger_account` accepts the same field through its existing keyword-field boundary. A balance with no explicit basis advances only the current display cache, leaves the basis unknown, and creates no historical candidate. A dated older non-loan balance appends historical evidence but cannot roll back a newer current cache. loan cache writes without an explicit liability definition do not append a historical candidate; they preserve current-cache and import behavior without inferring a definition. Import and account writers use deterministic candidate keys.
### shared contract and adapter limits
All new interfaces retain the existing `success`/`error_code` envelope. `preview_asset_snapshot_close()` and Dashboard render/report are read-only; only explicit register/update/close forms or MCP calls write. MCP tools are `register_account_valuation`, `list_account_valuations`, `select_account_valuation`, `correct_account_valuation`, `preview_asset_snapshot_close`, `close_asset_snapshot`, `get_asset_snapshot_report`, and `get_net_worth_timeseries`; actual decorators are in `jarca_ledger_mcp/adapters/mcp/server.py`. Dashboard uses `accounts_gateway.py`, `assets_gateway.py`, and `page_views/accounts.py`/`assets.py`.
## 호환성 추가 규칙 — REQ-003
- 유지할 기존 호출 방식: REQ-001 auth config/login/cleanup/logout/gate, REQ-002 exact 10 menu labels/order, `page` query sibling preservation, stable shell/background and browser evidence; existing MCP stdio/streamable-http runtime.
- 버전 변경 정책: additive fields only; no rename/removal of existing response keys.
- 폐기 정책: 없음; guessed migration, fake transactions and MCP auth claim are excluded.
- 재시도 계약: same idempotency payload reuses; changed payload conflicts; DB conflict does not retry blindly.
- 중복 처리 계약: valuation/run idempotency keys and single correction indexes.
- 순서 보장: Dashboard auth → DB path → page/gateway; preview → explicit approval → atomic close; selection → report.
## 예시 추가
### example-valuation — synthetic verified valuation
- 대상 인터페이스: `valuation-service`
#### 호출
```text
register_account_valuation(account_id="acct-synthetic", basis_date="2026-06-30", amount=1000000, verification_status="verified", idempotency_key="fixture-v1")
```
#### 결과
```text
{"success": true, "reused": false, "valuation": {"basis_date": "2026-06-30", "amount": 1000000, "verification_status": "verified"}}
```
### example-preview — read-only close preview
- 대상 인터페이스: `snapshot-close`
#### 호출
```text
preview_asset_snapshot_close(basis_date="2026-06-30", selection_policy="exact")
```
#### 결과
```text
{"success": true, "source": "live_authority", "is_closed_snapshot": false, "close_status": "complete_exact", "warnings": []}
```
## 검증 추가
- 정상 계약: `tests/test_account_valuations.py`, `tests/test_asset_snapshots.py`, `tests/test_contract_regression.py`
- 실패 계약: idempotency/correction/conditional approval tests and source validation
- 경계 계약: exact/as_of, NULL/0, future/unverified candidate, correction/re-close
- 구현·스키마·예시 정합성: `tests/test_requirement_docs.py`, actual service/MCP/gateway symbols
### canonical implementation signatures and fields
The application contracts are these actual signatures (defaults included):
```python
register_account_valuation(account_id: str, basis_date: str, amount: int, currency: str = "KRW", valuation_kind: str = "balance", source_type: str = "manual", source_ref: str = "", verification_status: str = "unverified", memo: str = "", idempotency_key: str = "", as_of_at: str | None = None, timezone: str | None = None, cost_basis: int | None = None, net_contributed: int | None = None, reported_principal_kind: str | None = None, reported_principal_label: str | None = None, profit: int | None = None, return_rate: float | None = None, return_basis: str | None = None, principal: int | None = None, interest: int | None = None, fee: int | None = None, representative_liability_definition: str | None = None, db_path: str | None = None, supersedes_id: str | None = None, correction_reason: str = "") -> dict[str, Any]
list_account_valuations(account_id: str, include_rejected: bool = True, db_path: str | None = None) -> dict[str, Any]
select_account_valuation(account_id: str, basis_date: str, as_of: bool = False, currency: str | None = None, valuation_kind: str | None = None, db_path: str | None = None) -> dict[str, Any]
correct_account_valuation(valuation_id: str, amount: int, correction_reason: str, idempotency_key: str = "", db_path: str | None = None, **fields: Any) -> dict[str, Any]
preview_asset_snapshot_close(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str = "", correction_reason: str = "", db_path: str | None = None, selection_policy: str = "exact", idempotency_key: str = "", allow_missing_exclusions: bool = False, allow_as_of_carry_forward: bool = False, approved_by: str = "", approval_reason: str = "", approval_reference: str = "", preview_fingerprint: str = "") -> dict[str, Any]
get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None, selection_policy: str = "exact") -> dict[str, Any]
```
Every output is a dict with `success`; failures also have `error_code` and `error`. Successful close responses expose top-level `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`, plus `snapshot_run_id` and `close_status`. Their nested `provenance` contains `valuation_date`, `age_days`, `carried_forward`, `verification_status`, `evidence_kind`, `approval`, `correction`, `warnings`, and redacted `exclusions`. `last_balance` is an external current comparison, not opening evidence; absent balance_after/opening evidence returns `INCOMPLETE_CALCULATION`. `source_ref` is account-scoped evidence identity even with an explicit key; key/source mismatch is a no-write conflict. Correction requires same parent account/date/currency/purpose, rejects cyclic/self parent, permits one child, and requires reason.
### canonical read and provenance rules
`monthly_source_snapshot_id` is a read-model alias of `snapshot_run_id`, not a persistence field. The alias and actual valuation date/age/carry/verification/run/approval/correction/warnings/exclusions are exposed through each response's documented top-level fields, `included_items`, and nested `provenance` in close, live, closed, timeseries, and monthly read models. Reconciliation states investment unavailable, loan purpose/definition/ambiguity, and completeness without fake writes.
### C13 database boundary
Writer/bootstrap explicitly calls `init_db()`. `read_db()` uses SQLite `mode=ro`; missing database returns `db_not_initialized`, while initialized database read failures return `db_error`. Both failures retain the standard `success=false`, `error_code`, `error` envelope without raw SQLite details. Read paths perform no mkdir, no migrate, and no commit. Only first-use write initializes, and Dashboard auth is evaluated before DB access.
## D04 snapshot close output contract
Every successful **legacy direct close** output has these exact top-level fields (the transport envelope may additionally include `success`); this field list does not define the additive settlement-draft finalize response: `snapshot_run_id`, `close_status`, `approval`, `correction`, `warnings`, `excluded_items`, `carried_items`, `provenance`, `valuation_date`, `age_days`, `verification_status`, `evidence_kind`, and `monthly_source_snapshot_id`. `monthly_source_snapshot_id` is an alias of `snapshot_run_id`, not a persistence field.
The top-level `provenance` object has these exact nested `provenance` fields: `valuation_date`, `age_days`, `carried_forward`, `verification_status`, `evidence_kind`, `approval`, `correction`, `warnings`, `excluded_items`, and `carried_items`. `approval` and `correction` are objects or explicit empty values when not applicable; warnings and item collections are arrays. `valuation_date` is the actual selected evidence date, `age_days` is computed from the requested basis date, and verification/evidence status describe selected evidence. Live, closed, timeseries, and monthly read models preserve these fields and alias semantics.
The preview input includes basis_date, selection_policy, included/excluded evidence state hashes, warnings, and candidate_state_hashes. preview output includes preview_fingerprint. Close accepts the exact `preview_fingerprint: str = ""` parameter shown in the signature block and recomputes the same authority fingerprint before opening the write boundary. A mismatch returns `authority_drift` and no partial write.
Replay uses replay payload equality: an existing idempotency key replays only when the persisted request fingerprint is equal; a changed payload returns `idempotency_conflict`. The replay response is reconstructed from the persisted run and its evidence, with no replay-only reused fields.
`close_metadata` is a recursive close metadata allowlist: only run, approval, correction, warnings, exclusions, carried evidence, and allowlisted evidence fields (`source_type`, `source_id`, `authority_source`, `valuation_id`, dates/statuses and `state_hash`) cross the boundary. `applied_evidence`, `excluded_evidence`, and `carried_evidence` are redacted recursively; unknown nested keys are dropped, and credentials, tokens, cookies, raw account data, and arbitrary source-note payloads are never returned. This is the recursive close_metadata allowlist and redaction contract.
## settlement-draft-service — 월정산 초안 API (Issue #15)
Common application owner: `jarca_ledger_mcp/application/settlement_draft_service.py`; re-export through `jarca_ledger_mcp.service`; service and gateway signatures below include internal `db_path`, which is omitted from MCP tool inputSchema. All other MCP-exposed parameters retain their documented names, defaults, and requiredness. MCP exposes the same operation names, not the same signatures; Dashboard uses existing gateway layering. Draft finalize adds draft-specific response fields while preserving common aliases; it is not a claim that its response has the legacy direct-close shape. Each returns the existing `success`/`error_code`/`error` envelope. Dashboard auth gate precedes DB access; this adds no MCP authentication claim.
```python
create_settlement_draft(month: str, created_by: str, idempotency_key: str, basis_date: str = "", memo: str = "", correction_of_run_id: str = "", correction_reason: str = "", db_path: str | None = None)
list_settlement_drafts(month: str = "", db_path: str | None = None)
get_settlement_draft(draft_id: str, db_path: str | None = None)
update_settlement_draft(draft_id: str, expected_version: int, updated_by: str, memo: str, db_path: str | None = None)
refresh_settlement_draft_targets(draft_id: str, expected_version: int, updated_by: str, db_path: str | None = None)
set_settlement_draft_item(draft_id: str, source_type: str, source_id: str, expected_version: int, selected_by: str, valuation_id: str = "", selection_mode: str = "exact", selection_reason: str = "", included: bool = True, exclusion_reason: str = "", db_path: str | None = None)
review_settlement_draft(draft_id: str, db_path: str | None = None)
approve_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, approved_by: str, approval_reason: str, approval_reference: str, allow_missing_exclusions: bool = False, allow_carried_before: bool = False, allow_manual_substitute: bool = False, db_path: str | None = None)
finalize_settlement_draft(draft_id: str, expected_version: int, review_fingerprint: str, idempotency_key: str, db_path: str | None = None)
cancel_settlement_draft(draft_id: str, expected_version: int, cancelled_by: str, reason: str, db_path: str | None = None)
```
Create validates strict YYYY-MM and month-end date, nonempty actor/key and payload replay equality; same key+payload returns same draft, changed payload or active duplicate conflicts. Versions are positive strict integers (bool/fraction/None rejected); inclusion flags actual bool. `review` is read-only and returns draft/version/fingerprint, items and included/excluded/missing/warnings, approval_required, provisional totals, completeness, substitution and unregistered-liability flags. `candidate_amount` may be shown for unverified evidence but authority amount NULL. Approval binds fingerprint+version and explicit flags. Finalize requires approval even for exact complete data; same request replays the persisted response, conflicting key/payload errors, and failed transaction leaves no partial state.
`read_db()` remains SQLite `mode=ro`: missing DB returns `db_not_initialized`, no-create/no mkdir/no migrate/no commit; initialized read failures return `db_error`. Existing auth cookie/logout, MCP transport/scope, menu, query, shell and browser behavior are unchanged. Existing D04 close aliases and recursive metadata allowlist/redaction remain authoritative.
### Draft evidence public fields
`review.included_items`, finalized `items`, asset report `close_metadata.applied_evidence`, timeseries point `applied_evidence`, and monthly `source_coverage.assets.applied_evidence` preserve the same selected-evidence field names: `valuation_date` (actual valuation date), `recorded_at` (original recording timestamp, not the basis date), `date_difference_days = valuation_date - basis_date`, `date_difference_direction` (`after`, `before`, `same`, or `unknown`), and `age_days` (basis-minus-valuation days; negative for a future substitute). A loan additionally preserves `representative_liability_definition`; selection audit includes `selection_mode`, `selection_reason`, `selected_by`, `selected_at`, `item_version`, and draft/fingerprint lineage. These are derived or frozen response fields, not additional database columns.
Nested draft `approval` retains `allow_missing_exclusions`, `allow_carried_before`, and `allow_manual_substitute` alongside actor/time/reason/reference. The three approval flags and the three result flags (`contains_substitutions`, `contains_carried`, `has_approved_missing`) are independent. Unverified evidence can be visible as `candidate_amount` but cannot become applied monetary authority; only an explicitly approved missing/excluded row may persist with NULL amount. No lineage default keys are injected into legacy persisted evidence arrays. Recursive allowlisting excludes raw source notes, memos, and unknown nested fields.
Executable field-contract regression: `tests/test_settlement_draft_contract_scenarios.py::test_date_metadata_public_contract_names_survive_all_closed_consumers`. Related clause tests cover representative integer precision, transaction readiness independent of asset close, approved unresolved candidates, and the synthetic September pension/loan dates with stale, rollback, and concurrent idempotent retry.