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

46 KiB

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

호출

POST /api/auth/login
Origin: https://dashboard.example
admin_id=<submitted>&admin_password=<submitted>&remember_me=on

결과

303 Location: /
Set-Cookie: __Host-jarca_dashboard_auth=<redacted>; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=2592000
  • 대상 인터페이스: cleanup

호출

GET /api/auth/cleanup
Referer: https://dashboard.example/

결과

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

호출

register_account_valuation(account_id="acct-synthetic", basis_date="2026-06-30", amount=1000000, verification_status="verified", idempotency_key="fixture-v1")

결과

{"success": true, "reused": false, "valuation": {"basis_date": "2026-06-30", "amount": 1000000, "verification_status": "verified"}}

example-preview — read-only close preview

  • 대상 인터페이스: snapshot-close

호출

preview_asset_snapshot_close(basis_date="2026-06-30", selection_policy="exact")

결과

{"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):

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 = "", *, monthly_settlement_id: str | None = None, liquidity_bucket: str | None = None, selection_policy: str = "manual", valuation_source: str = "manual", source_note: 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 retains these required top-level fields as required top-level metadata fields (the transport envelope may additionally include success): 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. This is a required retained metadata-field list, not an exhaustive inventory of all keys in a successful legacy response; additional legacy response keys remain part of the legacy output. The list does not define the additive settlement-draft finalize response. monthly_source_snapshot_id is an alias of snapshot_run_id, not a persistence field.

The top-level provenance object retains these required nested provenance fields (metadata, not an exhaustive inventory of all nested response keys): 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.

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

unregistered_liabilities is true when the frozen target set contains no registered liability source. It does not assert unknown real-world debt or prove the operator is debt-free. Exclusion and missing evidence remain separate completeness states; neither changes the registration meaning of this flag.

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.

Issue #18 additive public contracts (schema 14)

Shared service setter: set_settlement_cashflow_policy(draft_id,expected_version,updated_by,cashflow_policy,db_path=None); stored run getter: get_monthly_settlement_snapshot(month='',run_id='',db_path=None). MCP signatures omit internal db_path. Draft policy is draft.cashflow_policy; policy edits increment version and invalidate approval. Review/finalize results expose fields at response root; active get nests review, completed get has root final_response. Stored grouped details use cashflow_kind (income/expense/excluded), not kind.

The stored reader recognizes snapshot_schema_version=1 only with aggregation_version=comprehensive-v1. Any non-NULL version metadata requires both recognized versions; a partial pair, unsupported snapshot schema, or absent/unknown aggregation version returns the existing success=false, error_code=db_error, error envelope without authoritative totals/details or DB writes. Legacy NULL schema headers remain legacy reads with unrecorded totals NULL; this does not reinterpret unknown comprehensive versions as legacy.

Getter distinguishes immutable stored run/evidence (exact run ID, schema/aggregation version, legacy/partial/missing/excluded indicators) from live current-ledger report. Existing asset-only API inputs/responses/errors/aliases and direct-close meaning remain unchanged and are not comprehensive close. Reads/render are readonly/no-create; authenticated Dashboard gate precedes DB. Existing envelope, stale approval/drift rejection, idempotent persisted replay, conflict, atomic rollback, append-only correction, and privacy contracts remain.

Comprehensive cashflow policy input

set_settlement_cashflow_policy(draft_id, expected_version, updated_by, cashflow_policy, db_path=None) accepts this normalized input (omitted lists normalize to empty lists):

{"savings_categories":[{"category":"Savings","subcategory":"Deposit"}],"category_decisions":[{"category":"Food","subcategory":"Dining","cashflow_kind":"expense","reason":"Reviewed classification"}]}

Only savings_categories and category_decisions are allowed top-level keys; each is a list. Savings entries have exactly category and subcategory; decision entries have exactly those keys plus cashflow_kind and reason. Category fields are strings and blank strings are valid. Kinds are income, expense, or excluded; reason must be a nonempty string. Within each list, duplicate category/subcategory pairs are rejected. Normalization sorts each list and supplies omitted lists as empty.

The setter requires matching expected_version; successful mutation increments version and clears prior approval/review fingerprint. Review computes a new fingerprint; approval applies only to the reviewed version/fingerprint. Changed policy or transactions require re-review and re-approval. aggregation_version is comprehensive-v1; policy editing increments the draft version, clears approval fields and invalidates the prior review_fingerprint for approval.

Comprehensive settlement response contract

The six totals are integer root fields in comprehensive review and saved-run reader responses. Legacy readers return NULL for totals they never stored.

Field Type Presence Legacy
income_total integer required null
expense_total integer required null
net_cashflow integer required null
total_assets integer required null
total_liabilities integer required null
net_worth integer required null

A review response has root-level review_fingerprint, cashflow_policy, cashflow_items, cashflow_blockers, and source_snapshot. The active draft getter nests its current review under review. For a completed draft, get_settlement_draft exposes the persisted close response under final_response; finalize_settlement_draft returns that close response at root, including on idempotent replay. A saved-run reader instead returns the stored snapshot at root with source: settlement_snapshot, run_id, and six totals. cashflow_items is a required list in comprehensive review, close and saved-reader results (empty is valid): each entry has cashflow_kind (income/expense/excluded string), category and subcategory (strings, including blanks), signed integer amount, positive integer transaction_count, and string currency. cashflow_blockers is a required review/close list with string transaction_id and reason entries; successful close requires it to be empty. The saved reader does not synthesize a blocker list from old records. cashflow_policy is the normalized policy object for comprehensive responses, but null for the legacy saved reader. source_snapshot is the preserved transaction/asset input object for comprehensive responses and an empty object for legacy reads. Version metadata is preserved, not inferred from today's ledger. The following JSON examples show selected fields rather than exhaustive response payloads.

Saved-reader additive asset metadata

get_monthly_settlement_snapshot(month="", run_id="", db_path=None) adds the following four root fields to a successful saved-run response, using the same strict public evidence projection as the asset close reader:

Field Type Meaning / privacy
excluded_items list of objects Persisted excluded evidence projected through the public evidence allowlist; may include source identity, reason/status/date, candidate metadata and state hash when present.
carried_items list of objects Persisted carried evidence projected through the same allowlist. This is the persisted carried-evidence collection, not a recomputation from current valuations.
applied_evidence list of objects Persisted applied evidence projected through the same allowlist, including only allowlisted evidence attributes when present.
approval object Approval projection: approved_by, approved_at, approval_reason, approval_reference; legacy row-only records without run-header approval metadata return {}.

Evidence objects are not a fixed full-row schema: keys are optional. _closed_run_metadata uses the same key allowlist recursively for nested dictionaries and lists; unknown keys (including source_note, memo, credential and raw-payload keys) are dropped, while allowlisted scalar values are preserved. Candidate metadata and source attributes may therefore retain persisted values under allowlisted keys. This is not a content-based secret redactor: callers must not persist sensitive values under public keys such as name or reason. Do not treat these fields as permission to expose a complete database row. The approval object is limited to approved_by, approved_at, approval_reason, and approval_reference; row-only legacy records return {}, while run headers preserve the stored approval values, including empty or null values.

For run-header-backed records, these collections and approval are projected from the immutable run metadata, rather than inferred from snapshot rows. Thus rows are not the origin of applied_evidence, excluded_items, or carried_items. Exact run_id and latest-month reads use the saved run/rows and stored metadata only: they do not write, initialize/migrate the database, consult live ledger authority, or recompute current selections. Legacy schema-13 snapshots with row-only evidence and no run header remain readable: the additive four fields use sensible empty arrays (excluded_items, carried_items, applied_evidence) and empty approval object, while preserving legacy status/version behavior and the six stored-total fields as null (they were not persisted). This additive metadata does not change the asset-only get_asset_snapshot_report result or its existing schema/version contract.

Successful root review example, including a negative signed expense (refund):

{"success":true,"income_total":120000,"expense_total":-5000,"net_cashflow":125000,"total_assets":900000,"total_liabilities":200000,"net_worth":700000,"review_fingerprint":"<fingerprint>","cashflow_policy":{"savings_categories":[],"category_decisions":[]},"cashflow_items":[{"cashflow_kind":"expense","category":"Food","subcategory":"Dining","amount":-5000,"transaction_count":1,"currency":"KRW"}],"cashflow_blockers":[{"transaction_id":"txn-example","reason":"unknown cashflow classification requires an explicit decision"}],"source_snapshot":{"transactions":[],"assets":[]}}

Saved reader root example:

{"success":true,"source":"settlement_snapshot","run_id":"run-example","income_total":120000,"expense_total":-5000,"net_cashflow":125000,"total_assets":900000,"total_liabilities":200000,"net_worth":700000,"cashflow_policy":{"savings_categories":[],"category_decisions":[]},"source_snapshot":{"transactions":[],"assets":[]}}

A validation_error results when cashflow review blockers must be resolved before approval/finalize. Remediate policy or transaction classification, then re-review and re-approve. Examples use synthetic identifiers, not raw secrets.

Schema 15 preserves the positional v0.1.7 valuation API and manual asset and snapshot run APIs. register_account_valuation(..., monthly_settlement_id=...) explicitly selects canonical unverified account snapshot registration; omitted monthly identity uses legacy valuation evidence and correction semantics. Canonical monthly review/approval/close APIs remain separate from legacy draft finalization. Neither finalizer rewrites the other's frozen records.

Public immutable monthly settlement history

The read-only application contracts are:

list_monthly_settlement_history(month: str = "", db_path: str | None = None) -> dict[str, Any]
get_monthly_settlement_history_record(run_id: str, db_path: str | None = None) -> dict[str, Any]

Both operations are public MCP tools. MCP signatures omit internal db_path: the list schema exposes optional month, while the exact-record schema requires run_id. Successful list responses contain history, normalized month, and read_only=true; successful exact reads contain one history record and read_only=true. The list is newest-first and exposes run/version, close time, close status, and correction linkage without consulting or rewriting live data.

The authenticated Dashboard wrapper is unchanged. In the completed/cancelled draft panel, 마감내역보기 opens read-only history state, lists immutable runs for the draft month, lets the operator select a run, and fetches that exact record. Opening or changing this selection performs no lifecycle mutation.