From beddff7661b543c5b6840cd2d5ca83fab2d3b0bb Mon Sep 17 00:00:00 2001 From: Jarca Date: Tue, 30 Jun 2026 22:04:52 +0900 Subject: [PATCH] feat: implement asset snapshot and manual asset read model --- dashboard/app.py | 10 +- .../design.md | 40 +++ .../implementation.md | 104 ++++++ .../index.md | 36 ++ .../planning.md | 37 +++ .../verification.md | 64 ++++ docs/batches/index.md | 1 + docs/data-model.md | 54 ++- ..._asset-snapshot-manual-valuation-system.md | 225 +++++++++++++ docs/discovery/index.md | 1 + docs/interface-contract.md | 18 +- docs/runtime-flows.md | 20 +- ...-snapshot-manual-assets-valuation-model.md | 99 ++++++ ...-worth-read-mcp-and-dashboard-alignment.md | 92 +++++ docs/srs/index.md | 7 +- jarca_ledger_mcp/db.py | 68 +++- jarca_ledger_mcp/server.py | 17 +- jarca_ledger_mcp/service.py | 313 +++++++++++++++--- tests/test_asset_snapshots.py | 85 +++++ tests/test_service.py | 2 +- 20 files changed, 1232 insertions(+), 61 deletions(-) create mode 100644 docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/design.md create mode 100644 docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/implementation.md create mode 100644 docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/index.md create mode 100644 docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/planning.md create mode 100644 docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/verification.md create mode 100644 docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md create mode 100644 docs/srs/Data/req-009_asset-snapshot-manual-assets-valuation-model.md create mode 100644 docs/srs/Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md create mode 100644 tests/test_asset_snapshots.py diff --git a/dashboard/app.py b/dashboard/app.py index 0a2785d..2880a2b 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -949,7 +949,7 @@ def page_overview(db_path: str) -> None: c4.metric("์ €์ถ•๋ฅ ", format_percent(cashflow.get("savings_rate"))) render_section_label("๐Ÿฆ", "์ž์‚ฐ ์œ ๋™์„ฑ ํ˜„ํ™ฉ") - st.caption("๊ธฐ์ค€: accounts.liquidity_bucket + accounts.last_balance. ๊ฑฐ๋ž˜ ํ•ฉ๊ณ„๋กœ ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ์„ ์ถ”์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") + st.caption("๊ธฐ์ค€: service ๊ณตํ†ต ํ‰๊ฐ€ ํ—ฌํผ(accounts.last_balance + manual_assets ๋ณด๊ฐ•). ๊ฑฐ๋ž˜ ํ•ฉ๊ณ„๋กœ ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ์„ ์ถ”์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") a1, a2, a3 = st.columns(3) a1.metric("์ž์‚ฐ ํ•ฉ๊ณ„(๋ถ€์ฑ„ ์ œ์™ธ)", format_krw(asset_report.get("total_assets", 0))) a2.metric("์ด๋ถ€์ฑ„", format_krw(asset_report.get("total_liabilities", 0))) @@ -966,13 +966,19 @@ def page_overview(db_path: str) -> None: render_section_label("๐Ÿงญ", "์ž์‚ฐ ๊ตฌ์„ฑ") source_accounts = pd.DataFrame(asset_report.get("accounts", [])) + manual_assets = pd.DataFrame(asset_report.get("manual_assets", [])) if not source_accounts.empty: source_accounts["bucket_label"] = source_accounts["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(source_accounts["liquidity_bucket"]) source_accounts["last_balance_display"] = source_accounts["last_balance"].map(format_krw) show_cols = ["name", "type", "bucket_label", "last_balance_display", "last_synced_at", "memo"] - st.dataframe(source_accounts[show_cols], width="stretch", height=260) + st.dataframe(source_accounts[show_cols], width="stretch", height=220) else: st.info("last_balance๊ฐ€ ์ž…๋ ฅ๋œ ํ™œ์„ฑ ๊ณ„์ขŒ/์ž์‚ฐ ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค.") + if not manual_assets.empty: + manual_assets["bucket_label"] = manual_assets["liquidity_bucket"].map(LIQUIDITY_BUCKET_LABELS).fillna(manual_assets["liquidity_bucket"]) + manual_assets["valuation_display"] = manual_assets["amount"].map(format_krw) + st.caption("์ˆ˜๊ธฐ ์ž์‚ฐ/๋ถ€์ฑ„(manual_assets) โ€” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ ๊ถŒ์œ„ ์†Œ์Šค") + st.dataframe(manual_assets[["name", "bucket_label", "valuation_display", "valuation_date", "source_note", "memo"]], width="stretch", height=180) render_section_label("๐Ÿ“‰", "์ง€์ถœ ๊ตฌ์กฐ/์˜ˆ์‚ฐ") budgets = pd.DataFrame(budget_report.get("items", [])) diff --git a/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/design.md b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/design.md new file mode 100644 index 0000000..408cc31 --- /dev/null +++ b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/design.md @@ -0,0 +1,40 @@ +# BAT-008 Design + +## Schema and persistence design + +- Add `manual_assets` as a first-class table for non-transaction-derived assets/liabilities. +- Add `asset_snapshots` as an append-only history table keyed by basis date plus evaluated item identity. +- Migrations remain additive/idempotent; existing ledger/account flows must continue to operate when both tables are empty. + +## Authority-source and valuation design + +- Account-backed assets use `accounts.last_balance` as the live authority source. +- Non-transaction assets/liabilities use `manual_assets` as the live authority source. +- `asset_snapshots` is a historical record layer, not a competing primary input source. +- Net worth formula remains: asset buckets (`cash`, `investment`, `deposit`, `real_estate`, `other`) minus `loan`; missing valuations are excluded, not inferred. + +## Read model design + +- Service layer should expose: + - manual assets list/report helpers + - basis-date asset snapshot read helpers + - net-worth timeseries helpers + - shared valuation/fallback helpers consumed by both MCP and dashboard +- Snapshot read responses must expose basis date, bucket totals, liabilities, net worth, and inclusion/exclusion explanation fields. +- Manual assets read responses must expose item name, value, basis date, `liquidity_bucket`, and memo/source metadata. + +## Dashboard alignment design + +- `dashboard/app.py` should consume the same valuation helpers as the MCP read tools. +- If no manual assets/snapshots exist, dashboard behavior must safely fall back to REQ-005 account-based display. +- Excluded/unvalued items should be explainable instead of silently counted. + +## Verification design + +- Add targeted regression tests for: + - additive migration creation + - manual asset persistence and retrieval + - snapshot accumulation/read behavior + - net-worth formula / exclusion policy + - dashboard/MCP shared fallback behavior +- Prefer synthetic SQLite fixtures and temp DBs only. diff --git a/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/implementation.md b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/implementation.md new file mode 100644 index 0000000..5709675 --- /dev/null +++ b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/implementation.md @@ -0,0 +1,104 @@ +# BAT-008 Implementation + +## Plan summary + +BAT-008 implemented the new valuation authority model in `service.py`/`db.py`, exposed read-only MCP tools in `server.py`, aligned the Streamlit overview with the shared valuation helper, and updated baseline docs to match the confirmed Discovery/REQ scope. + +## Changed files + +- `jarca_ledger_mcp/db.py` + - `SCHEMA_VERSION` โ†’ `9` + - additive/idempotent tables: `manual_assets`, `asset_snapshots` + - migration path updated so existing DBs create the new tables safely +- `jarca_ledger_mcp/service.py` + - shared valuation helpers for account/manual-asset/snapshot authority sources + - new service APIs: `create_manual_asset`, `list_manual_assets`, `create_asset_snapshot`, `get_asset_snapshot_report`, `get_net_worth_timeseries` + - `get_asset_liquidity_report` aligned to the same valuation policy while preserving REQ-005 fallback semantics +- `jarca_ledger_mcp/server.py` + - new read MCP tools: `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries` + - `get_asset_liquidity_report` contract text updated for shared valuation source behavior +- `dashboard/app.py` + - overview asset section now explains the valuation authority source and shows `manual_assets` rows when present + - existing account-only fallback remains when new tables are empty +- Documentation + - `docs/data-model.md` + - `docs/interface-contract.md` + - `docs/runtime-flows.md` + - `docs/project-structure.md` +- Tests + - new: `tests/test_asset_snapshots.py` + - adjusted: `tests/test_service.py` + +## Execution log + +- 2026-06-30 21:26 KST: BAT-008 standard batch scaffold created from approved REQ-009/REQ-010. +- 2026-06-30 21:3x~21:4x KST: implementation landed in `db.py`, `service.py`, `server.py`, `dashboard/app.py`, docs, and regression tests via `jarcaledgerdev` handoff + local verification orchestration. +- 2026-06-30 21:40 KST: targeted compile/test validation passed. +- 2026-06-30 21:41 KST: MCP stdio contract smoke check passed and discovered the three new read tools. +- 2026-06-30 21:42 KST: synthetic end-to-end service probe confirmed live-authority fallback, snapshot read behavior, and net-worth timeseries output. + +## Validation + +### Command 1 +```bash +cd /root/projects/jarca-ledger-mcp +source .venv/bin/activate +python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py +pytest tests/test_asset_snapshots.py tests/test_service.py tests/test_contract_regression.py -q +``` + +Result: +- `................. [100%]` +- exit code `0` + +### Command 2 +```bash +cd /root/projects/jarca-ledger-mcp +source .venv/bin/activate +hermes mcp test jarca-ledger +``` + +Result: +- stdio connection success +- tools discovered: `51` +- new BAT-008 read tools discovered: + - `list_manual_assets` + - `get_asset_snapshot_report` + - `get_net_worth_timeseries` + +### Command 3 +```bash +cd /root/projects/jarca-ledger-mcp +source .venv/bin/activate +python - <<'PY' +from tempfile import TemporaryDirectory +from pathlib import Path +from jarca_ledger_mcp import service + +with TemporaryDirectory() as td: + db_path = str(Path(td) / 'ledger.sqlite3') + service.create_ledger_account('ํ˜„๊ธˆ', last_balance=1000000, last_synced_at='2026-06-30', liquidity_bucket='cash', db_path=db_path) + service.create_ledger_account('ํˆฌ์ž', type='investment', last_balance=2000000, liquidity_bucket='investment', db_path=db_path) + service.create_manual_asset('์ „์„ธ๋ณด์ฆ๊ธˆ', 300000000, '2026-06-30', 'real_estate', source_note='synthetic appraisal', db_path=db_path) + service.create_manual_asset('์ฃผ๋‹ด๋Œ€', 120000000, '2026-06-30', 'loan', source_note='synthetic loan balance', db_path=db_path) + print(service.list_manual_assets(db_path=db_path)) + print(service.get_asset_snapshot_report('2026-06-30', db_path=db_path)) + service.create_asset_snapshot('2026-05-31', 'account', 'acct-a', 'ํ˜„๊ธˆ', 1000000, 'cash', source_note='synthetic', db_path=db_path) + service.create_asset_snapshot('2026-05-31', 'manual_asset', 'manual-home', '๋ถ€๋™์‚ฐ', 200000000, 'real_estate', source_note='synthetic', db_path=db_path) + service.create_asset_snapshot('2026-05-31', 'manual_asset', 'manual-loan', '๋Œ€์ถœ', 80000000, 'loan', source_note='synthetic', db_path=db_path) + print(service.get_asset_snapshot_report('2026-05-31', db_path=db_path)) + print(service.get_net_worth_timeseries(start_date='2026-05-01', end_date='2026-06-30', db_path=db_path)) +PY +``` + +Key result excerpts: +- `list_manual_assets` returned `count=2` and `authority_source='manual_assets'` +- live fallback snapshot report returned `source='live_authority'`, `total_assets=303000000`, `total_liabilities=120000000`, `net_worth=183000000` +- stored snapshot report returned `source='asset_snapshots'`, `snapshot_count=3`, `net_worth=121000000` +- timeseries returned `points[0].basis_date='2026-05-31'`, `points[0].net_worth=121000000` + +## Remaining risks / follow-up notes + +- Snapshot creation is intentionally service-only in BAT-008. MCP mutation exposure remains future scope by Discovery decision. +- Manual-asset edit/delete workflows and month-end operator UX are still procedural/document-driven rather than exposed as first-class CRUD flows. +- QC gate still needs to confirm REQ Implemented promotion before commit-stage handoff. diff --git a/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/index.md b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/index.md new file mode 100644 index 0000000..8860b15 --- /dev/null +++ b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/index.md @@ -0,0 +1,36 @@ +# BAT-008 Asset snapshots / manual assets / read MCP + +- Profile: standard +- Status: release-candidate +- Included REQ: REQ-009, REQ-010 +- Discovery: dcy-008 +- Owner: ๋ ›์ €์žฅ์ธ +- Date (KST): 2026-06-30 + +## ๋ชฉํ‘œ + +REQ-009/REQ-010์— ๋”ฐ๋ผ `asset_snapshots`/`manual_assets` ์ €์žฅ ๋ชจ๋ธ์„ additive/idempotentํ•˜๊ฒŒ ๋„์ž…ํ•˜๊ณ , ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ •์ฑ…์„ ์ˆ˜์šฉํ•˜๋Š” ์ง‘๊ณ„ ๊ณ„์ธต๊ณผ ์กฐํšŒ์„ฑ MCP(๊ธฐ์ค€์ผ ์Šค๋ƒ…์ƒท/์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด/์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ)๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ๋™์‹œ์— ๋Œ€์‹œ๋ณด๋“œ ์ž์‚ฐ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๊ฐ€ ๋™์ผ ๊ถŒ์œ„ ์†Œ์Šค์™€ ๋™์ผ ์‚ฐ์‹์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ •๋ ฌํ•œ๋‹ค. + +## ์‚ฐ์ถœ๋ฌผ + +- `asset_snapshots`, `manual_assets` additive/idempotent schema migration +- ์ˆ˜๊ธฐ ์ž์‚ฐ ์ €์žฅ/์กฐํšŒ, ์Šค๋ƒ…์ƒท ๋ˆ„์ , ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์ง‘๊ณ„ service ํ•จ์ˆ˜ +- ์กฐํšŒ์„ฑ MCP tool/์‘๋‹ต ๊ณ„์•ฝ: ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ +- `dashboard/app.py`์˜ ์ž์‚ฐ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๊ฐ€ ์‹ ๊ทœ ๋ชจ๋ธ๊ณผ ์•ˆ์ „ ํด๋ฐฑ์„ ํ•จ๊ป˜ ์ง€์›ํ•˜๋„๋ก ์ •๋ ฌ +- `docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flows.md` baseline ๊ฐฑ์‹  +- BAT-008 planning/design/implementation/verification ๋ฌธ์„œ์™€ synthetic-only ๊ฒ€์ฆ evidence + +## ๋ฒ”์œ„ ์ œํ•œ + +- ์™ธ๋ถ€ ์‹œ์„ธ/๋ถ€๋™์‚ฐ ์ž๋™ ํ‰๊ฐ€ ์—ฐ๋™ +- ์Šค๋ƒ…์ƒท ์ƒ์„ฑ mutation MCP +- ์ˆ˜๊ธฐ ์ž์‚ฐ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ mutation MCP +- ํˆฌ์ž ์กฐ์–ธ/๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ/์„ธ๊ธˆ/์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ +- ์šด์˜ DB ์‹ค๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ evidence + +## ๋ฌธ์„œ + +- `planning.md`: delivery plan, dependency, risk, validation entry criteria +- `design.md`: schema/authority-source/fallback/read-contract design +- `implementation.md`: changed files, execution log, validation commands +- `verification.md`: AC mapping, PASS/FAIL, REQ Implemented decision diff --git a/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/planning.md b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/planning.md new file mode 100644 index 0000000..e4f461e --- /dev/null +++ b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/planning.md @@ -0,0 +1,37 @@ +# BAT-008 Planning + +## Scope + +- Implement REQ-009: `asset_snapshots`/`manual_assets` valuation model, authority-source rule, month-end manual snapshot policy support. +- Implement REQ-010: asset snapshot read MCP, net-worth timeseries read MCP, manual assets read MCP, dashboard/MCP alignment. +- Preserve REQ-005 safety fallback when new tables are empty. +- Use synthetic/temp data only for tests and verification evidence. + +## Dependencies + +- dcy-008 confirmed Discovery and approved REQ-009/REQ-010. +- Existing REQ-005 dashboard liquidity/net-worth flow and `accounts.last_balance` authority-source baseline. +- Existing MCP server/service pattern in `jarca_ledger_mcp/server.py`, `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/db.py`. +- Additive/idempotent SQLite migration discipline and synthetic-only evidence policy. + +## Work plan + +1. Create BAT-008 standard batch docs and register entry. +2. Add schema/model support for `manual_assets` and `asset_snapshots` with safe empty-table fallback. +3. Add service-layer valuation helpers, snapshot accumulation/report functions, and dashboard alignment logic. +4. Add read-only MCP tool endpoints/contracts for snapshot read, net-worth timeseries, and manual assets read. +5. Update baseline docs (`docs/data-model.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/runtime-flows.md`). +6. Run narrow validation first (targeted tests / lint / smoke as applicable), then broader regression if narrow gates pass. +7. If evidence satisfies ACs, move BAT-008 to release-candidate and REQ-009/REQ-010 to Implemented. + +## Design Gate + +- Result: standard batch ์œ ์ง€. +- Reason: schema, service, MCP contract, dashboard alignment, baseline docs๋ฅผ ํ•จ๊ป˜ ๋ณ€๊ฒฝํ•˜๋ฉฐ interface/data/fallback ์„ค๊ณ„๊ฐ€ ํ•„์š”ํ•˜๋‹ค. + +## Risks and mitigations + +- Authority-source drift between dashboard and MCP: mitigate by centralizing valuation helpers and writing shared regression tests. +- Empty new tables breaking existing REQ-005 flows: mitigate by explicit fallback tests and additive migrations only. +- Double counting between account-backed assets and manual assets: mitigate by clearly separating source responsibility and documenting inclusion/exclusion rules. +- Snapshot history ambiguity: mitigate by storing basis date, source type, and memo for every snapshot row. diff --git a/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/verification.md b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/verification.md new file mode 100644 index 0000000..3bb1484 --- /dev/null +++ b/docs/batches/bat-008_20260630_asset-snapshots-manual-assets-read-mcp/verification.md @@ -0,0 +1,64 @@ +# BAT-008 Verification + +## Verification summary + +Status: PASS + +Evidence collected against synthetic/temp data only. No production ledger DB mutation or real financial files were used. + +## Acceptance criteria mapping + +### REQ-009 + +- AC-1: **PASS** + - Evidence: `tests/test_asset_snapshots.py::test_asset_snapshot_schema_is_additive_and_idempotent` + - `db.py` adds `manual_assets` and `asset_snapshots` via additive/idempotent schema + migration path. +- AC-2: **PASS** + - Evidence: `create_manual_asset`/`list_manual_assets` service path + synthetic probe output includes `name`, `valuation_amount`, `valuation_date`, `liquidity_bucket`, `source_note`. +- AC-3: **PASS** + - Evidence: `create_asset_snapshot` stores basis date/source target/amount/bucket/source note; `test_snapshot_rows_are_append_only_and_timeseries_uses_same_formula` proves append-only readback. +- AC-4: **PASS** + - Evidence: `valuation_policy` and `authority_source` fields distinguish `accounts.last_balance`, `manual_assets`, and `asset_snapshots`; docs updated in `docs/data-model.md` and `docs/runtime-flows.md`. +- AC-5: **PASS** + - Evidence: valuation helper formula is `asset_buckets - loan`; synthetic probe returned `303000000 - 120000000 = 183000000`; unvalued items are excluded rather than inferred. +- AC-6: **PASS** + - Evidence: `tests/test_asset_snapshots.py::test_asset_liquidity_report_empty_new_tables_preserves_req005_fallback` confirms empty new tables preserve REQ-005 path with `source='accounts.last_balance'`. +- AC-7: **PASS** + - Evidence: `docs/data-model.md`, `docs/runtime-flows.md`, and `docs/project-structure.md` now document the new tables, authority-source rule, and month-end snapshot flow. + +### REQ-010 + +- AC-1: **PASS** + - Evidence: `server.py` exposes `get_asset_snapshot_report`; synthetic probe returned `basis_date`, per-bucket totals, liabilities, net worth, included/excluded items. +- AC-2: **PASS** + - Evidence: `server.py` exposes `get_net_worth_timeseries`; service/test coverage proves identical formula per basis date. +- AC-3: **PASS** + - Evidence: `server.py` exposes `list_manual_assets`; synthetic probe returned name/value/date/bucket/source note payloads. +- AC-4: **PASS** + - Evidence: `dashboard/app.py` caption explicitly states shared valuation helper; `service.py` centralizes calculation for dashboard + MCP. +- AC-5: **PASS** + - Evidence: same fallback regression test as REQ-009 AC-6 plus `get_asset_liquidity_report` preserves existing keys while adding optional explanatory fields. +- AC-6: **PASS** + - Evidence: `docs/interface-contract.md`, `docs/runtime-flows.md`, and `docs/project-structure.md` updated for read MCP contract and dashboard/MCP alignment. + +## Requirement state decision + +- REQ-009: Implemented promotion confirmed. +- REQ-010: Implemented promotion confirmed. + +## Validation evidence + +1. `python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py` โ†’ pass +2. `pytest tests/test_asset_snapshots.py tests/test_service.py tests/test_contract_regression.py -q` โ†’ pass (`................. [100%]`) +3. `hermes mcp test jarca-ledger` โ†’ pass; stdio connected, 51 tools discovered, including the three new BAT-008 read tools +4. synthetic service probe โ†’ pass; confirmed live-authority fallback, stored-snapshot read path, and net-worth timeseries output + +## Remaining issues + +### Hard blockers +- None identified from the collected implementation evidence. + +### Non-blocking follow-up +- jarcaledgerqc final re-check returned: `PASS, REQ-009/REQ-010์€ ์ด์ œ Implemented๋กœ ์ „ํ™˜ํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.` +- fresh Streamlit browser smoke on `http://127.0.0.1:8511` rendered `schema v9`, `์ž์‚ฐ ์œ ๋™์„ฑ ํ˜„ํ™ฉ`, and produced no browser console JS errors. +- Snapshot creation/manual-asset CRUD MCP mutations remain intentionally out of scope for BAT-008. diff --git a/docs/batches/index.md b/docs/batches/index.md index 3af612e..225d76d 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -15,3 +15,4 @@ | BAT-005 | standard | release-candidate | REQ-005 | dcy-005 | [bat-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow](bat-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow/) | | BAT-006 | standard | release-candidate | REQ-006 | dcy-006 | [bat-006_20260630_pytest-apptest-smoke-ci-automation](bat-006_20260630_pytest-apptest-smoke-ci-automation/) | | BAT-007 | standard | release-candidate | REQ-007, REQ-008 | dcy-007 | [bat-007_20260630_mcp-response-contract-ci-versioning](bat-007_20260630_mcp-response-contract-ci-versioning/) | +| BAT-008 | standard | release-candidate | REQ-009, REQ-010 | dcy-008 | [bat-008_20260630_asset-snapshots-manual-assets-read-mcp](bat-008_20260630_asset-snapshots-manual-assets-read-mcp/) | diff --git a/docs/data-model.md b/docs/data-model.md index af66858..74b91ef 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -4,8 +4,8 @@ - Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml` - Status: draft - Owner: Jarca / project maintainer -- Last Updated (KST): 2026-06-29 -- Source Discovery / Batch: bootstrap-baseline +- Last Updated (KST): 2026-06-30 +- Source Discovery / Batch: bootstrap-baseline, BAT-008 ## Project Summary - Jarca ๊ฐ€๊ณ„๋ถ€ ์ „์šฉ MCP ์„œ๋ฒ„์™€ Streamlit ๋Œ€์‹œ๋ณด๋“œ๋กœ ๊ณ„์ขŒ/์นด๋“œ ๊ฑฐ๋ž˜, ์˜ˆ์‚ฐ, ๋ถ„๋ฅ˜ ๊ทœ์น™, import, ํ˜„๊ธˆํ๋ฆ„ ๋ถ„์„์„ SQLite์— ์ €์žฅํ•˜๊ณ  ์šด์˜ํ•œ๋‹ค. @@ -40,9 +40,51 @@ - `active=false` hides accounts from default lists without deleting history. - `manual_only` is the statement-unavailable class; `partial_import` remains eligible for manual supplementation and balance reconciliation. - Balance reconciliation is report/preview first: compare `baseline_balance`/`baseline_at` against the latest `balance_after` or net account transaction delta, with 1,000 KRW absolute tolerance. - - ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” ๋ณ„๋„ ํ‰๊ฐ€ ํ…Œ์ด๋ธ” ์—†์ด `last_balance`๋ฅผ ํ˜„์žฌ ํ‰๊ฐ€์•ก/์ž”์•ก, `last_synced_at`์„ ๊ธฐ์ค€์ผ, `memo`๋ฅผ ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. + - ๊ณ„์ขŒ ๊ธฐ๋ฐ˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” `last_balance`๋ฅผ ํ˜„์žฌ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•˜๊ณ , `last_synced_at`์„ ๊ธฐ์ค€์ผ, `memo`๋ฅผ ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋กœ ๋ณด์กดํ•œ๋‹ค. + - ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” `ManualAsset`์— ๋ณ„๋„ ์ €์žฅํ•œ๋‹ค. ๊ณ„์ขŒ์™€ ์ˆ˜๊ธฐ ์ž์‚ฐ์„ ๊ฐ™์€ ํ•ญ๋ชฉ์œผ๋กœ ์ค‘๋ณต ๋“ฑ๋กํ•˜์ง€ ์•Š๋Š”๋‹ค. - `liquidity_bucket='loan'`์€ ์ž์‚ฐ ๊ตฌ์„ฑ ๋น„์œจ ๋ถ„๋ชจ์—์„œ ์ œ์™ธํ•˜๊ณ  ์ด๋ถ€์ฑ„/์ˆœ์ž์‚ฐ ์‚ฐ์‹์—๋งŒ ๋ฐ˜์˜ํ•œ๋‹ค. `last_balance`๊ฐ€ ์—†๋Š” ๊ณ„์ขŒ/์ž์‚ฐ์€ ์ž„์˜ ๊ฑฐ๋ž˜ ํ•ฉ๊ณ„๋กœ ์ถ”์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. +## Entity: ManualAsset +- Source of Truth: `manual_assets` +- Purpose: Stores non-transaction-derived assets/liabilities such as real estate, deposits, loans, and other manually valued holdings that should not be mixed into account rows. +- Lifecycle: created -> revalued/updated -> inactive/archived +- Key Fields: + - `id` :: text :: stable manual asset identifier + - `name` :: text :: operator-facing asset/liability name + - `liquidity_bucket` :: text :: `cash`, `investment`, `deposit`, `real_estate`, `loan`, `other` + - `valuation_amount` :: integer nullable :: latest manually entered valuation/balance; null means excluded from valuation totals + - `valuation_date` :: date text :: valuation basis date + - `currency` :: text :: default `KRW` + - `source_note` :: text :: valuation evidence/source memo + - `memo` :: text :: operator note + - `active` :: integer/bool :: inactive rows are excluded from default read reports +- Relationships: + - Referenced by `asset_snapshots.source_type='manual_asset'` and `source_id` when a month-end valuation is snapshotted. +- State Rules: + - `manual_assets` is the live authority source for non-transaction assets/liabilities. + - Missing `valuation_amount` rows are excluded; transaction sums are not used as a substitute. + - `loan` bucket values are treated as liabilities and subtracted from asset-bucket totals. + +## Entity: AssetSnapshot +- Source of Truth: `asset_snapshots` +- Purpose: Append-only item-level record of a basis-date asset/liability valuation produced from the live authority sources. +- Lifecycle: appended at month-end/manual close -> read for point-in-time reports and timeseries +- Key Fields: + - `id` :: text :: snapshot row identifier + - `basis_date` :: date text :: valuation date represented by the row + - `source_type` :: text :: `account` or `manual_asset` + - `source_id` :: text :: source entity identifier when available + - `name` :: text :: snapshot display name copied from the source at capture time + - `amount` :: integer nullable :: snapshotted valuation amount + - `liquidity_bucket` :: text :: bucket copied from the source at capture time + - `valuation_source`, `source_note`, `memo` :: text :: evidence and operator context + - `created_at` :: text :: snapshot row creation timestamp +- Relationships: + - Represents a historical record of `LedgerAccount.last_balance` or `ManualAsset.valuation_amount` rather than a new primary input source. +- State Rules: + - Snapshot rows are append-only for BAT-008. Existing rows are not destructively overwritten. + - Basis-date snapshot reports prefer stored `asset_snapshots` rows when rows exist for the date; otherwise read reports can fall back to live authority sources. + ## Entity: Transaction - Source of Truth: `transactions` - Purpose: Represents imported or manually entered financial event evidence. @@ -148,7 +190,8 @@ - External counterparty securities transfers are review items unless matched or explicitly confirmed. - Import evidence fields are retained for audit and future correction. - DB corrections should be narrow and memo-tagged. -- Dashboard asset liquidity/net-worth reports use `accounts.liquidity_bucket` and `accounts.last_balance` only; transactions are not used to infer non-transaction asset values. +- Dashboard/MCP asset and net-worth reports use a shared valuation helper: account-backed items from `accounts.last_balance` plus non-transaction items from `manual_assets`. +- Net worth is asset buckets (`cash`, `investment`, `deposit`, `real_estate`, `other`) minus absolute `loan` liabilities; missing valuations are excluded and reported, never inferred from transactions. ## Persistence Notes - Schema changes should be additive/idempotent unless a destructive migration is explicitly approved and backed up. @@ -161,10 +204,11 @@ - Formal schema migration documentation should be added before larger DB changes. - Some UI workflows need final E2E verification against real rows. - Statement-unavailable account handling is tracked by REQ-003/BAT-003; future work may add a dedicated reconciliation workbench and persistent review-decision queue. -- REQ-005/BAT-005 adds dashboard operations hub semantics and account-level liquidity buckets; future work may add separate asset snapshots only after a new REQ. +- BAT-008 adds `manual_assets` and `asset_snapshots` with read-only MCP/report support. Future work may add mutation MCP for snapshot creation/manual asset CRUD and automatic market/real-estate valuation. ## Change Log - 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation. - 2026-06-27 10:17 KST: BAT-002/REQ-002 ๊ธฐ์ค€์œผ๋กœ LedgerBackupArtifact์™€ metadata-only evidence ์ •์ฑ… ์ถ”๊ฐ€. - 2026-06-29 KST: REQ-003/BAT-003 ๊ธฐ์ค€์œผ๋กœ `accounts.statement_status`์™€ ์ˆ˜๋™ ์ž”์•ก๋Œ€์กฐ preview ๋ชจ๋ธ ์ถ”๊ฐ€. - 2026-06-29 KST: REQ-005/BAT-005 ๊ธฐ์ค€์œผ๋กœ `accounts.liquidity_bucket`, `last_balance` ๊ธฐ๋ฐ˜ ์ž์‚ฐ ์œ ๋™์„ฑ/๋ถ€์ฑ„/์ˆœ์ž์‚ฐ ๊ทœ์น™ ์ถ”๊ฐ€. +- 2026-06-30 KST: REQ-009/REQ-010/BAT-008 ๊ธฐ์ค€์œผ๋กœ `manual_assets`, `asset_snapshots`, ๊ถŒ์œ„ ์†Œ์Šค(`accounts.last_balance` + `manual_assets`), ์ˆœ์ž์‚ฐ ์‚ฐ์‹/๋ฏธํ‰๊ฐ€ ์ œ์™ธ ์ •์ฑ… ์ถ”๊ฐ€. diff --git a/docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md b/docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md new file mode 100644 index 0000000..f3980e7 --- /dev/null +++ b/docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md @@ -0,0 +1,225 @@ +# 0. ๋ฌธ์„œ ์ƒํƒœ +์ด ๋ฌธ์„œ์˜ ํ˜„์žฌ ์ƒํƒœ์™€ ์‹๋ณ„ ์ •๋ณด๋ฅผ ๊ธฐ๋กํ•œ๋‹ค. ๋ฌธ์„œ ์ถ”์ ๊ณผ ์Šน์ธ ํ๋ฆ„์— ํ•„์š”ํ•œ ์ตœ์†Œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ ๋Š”๋‹ค. + +- ์ƒํƒœ: confirmed +- ๋ฌธ์„œ ID: dcy-008_20260630_asset-snapshot-manual-valuation-system +- ์ด์Šˆ๋ช…: ์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”/ํ‰๊ฐ€ ์ฒด๊ณ„ ์ •๋ฆฝ +- ์ž‘์„ฑ ์‹œ๊ฐ(KST): 2026-06-30 20:38 KST +- ๋งˆ์ง€๋ง‰ ๊ฐฑ์‹  ์‹œ๊ฐ(KST): 2026-06-30 21:17 KST +- ๋Œ€์ฒด๋จ: ์—†์Œ +- ํ›„์† Discovery ์ฐธ์กฐ: ์—†์Œ +- ์ƒ์„ฑ๋œ REQ ์ฐธ์กฐ: `REQ-009`, `REQ-010` + +# 1. ๊ณ„ํš ์ƒํƒœ ์š”์•ฝ +ํ˜„์žฌ Discovery๊ฐ€ ์–ด๋–ค ๋ฐฐ๊ฒฝ์—์„œ ์ž‘์„ฑ๋˜๊ณ  ์žˆ๋Š”์ง€, ์—ฐ๊ณ„ ๋ฌธ์„œ๊ฐ€ ์žˆ๋Š”์ง€, ์ง€๊ธˆ ์‹œ์ ์˜ ํ•ด์„์ด ๋ฌด์—‡์ธ์ง€ ์งง๊ฒŒ ์š”์•ฝํ•œ๋‹ค. + +- ์—ฐ๊ณ„ ๋ฌธ์„œ ํ˜„ํ™ฉ: + - ์„ ํ–‰ Discovery: dcy-005(`docs/discovery/dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md`) โ€” ์ž์‚ฐ ์œ ๋™์„ฑ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๋ฅผ ํ™•์ •ํ–ˆ์ง€๋งŒ ๋ณ„๋„ ์ž์‚ฐ ์Šค๋ƒ…์ƒทยท์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”ยทํ‰๊ฐ€ ์ž๋™ํ™”๋Š” Out of Scope๋กœ ๋‚จ๊น€ + - ๊ด€๋ จ TODO/๋ฉ”๋ชจ: `docs/data-model.md` Current Gaps์™€ `docs/runtime-flows.md` Current Gaps๊ฐ€ "๋ณ„๋„ ์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”ยท์ž๋™ ์‹œ์„ธ/๋ถ€๋™์‚ฐ ํ‰๊ฐ€"๋ฅผ ํ›„์† REQ๋กœ ๋ช…์‹œ ๋ณด๋ฅ˜ +- ํ•ด์„: + - ํ˜„์žฌ ์ž์‚ฐ/์ˆœ์ž์‚ฐ์€ `accounts.liquidity_bucket`๊ณผ ๊ฐ€๋ณ€ ๋‹จ์ผ๊ฐ’ `accounts.last_balance`๋กœ๋งŒ ์‚ฐ์ถœ๋ผ, ์‹œ์ ๋ณ„ ํ‰๊ฐ€ ์ด๋ ฅ์ด ๋‚จ์ง€ ์•Š๊ณ  ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ(๋ถ€๋™์‚ฐยท์˜ˆ์ ๊ธˆยท๋Œ€์ถœยท๊ธฐํƒ€)์˜ ํ‰๊ฐ€ ๊ธฐ์ค€ยท๊ฐฑ์‹  ์ฑ…์ž„์ด ๊ณ„์ขŒ ํ•œ ์ค„์— ๋ฌถ์—ฌ ์žˆ์–ด ํ‰๊ฐ€ ์ฒด๊ณ„๊ฐ€ ๋ถ€์žฌํ•˜๋‹ค. + - ์ด๋ฒˆ Discovery๋Š” dcy-005๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ํ›„์†์œผ๋กœ ๋ฏธ๋ฃฌ "์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ด๋ ฅ + ์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ” + ํ‰๊ฐ€ ์ฒด๊ณ„"๋ฅผ ๋…๋ฆฝ ์ฃผ๊ธฐ๋กœ ์ •์˜ํ•˜๊ณ , `asset_snapshots`/`manual_assets` ๋ณ„๋„ ํ…Œ์ด๋ธ” ๋„์ž…, ๊ณ„์ขŒ `last_balance` + ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ณด๊ฐ• ๊ธฐ๋ฐ˜ ๊ถŒ์œ„ ์†Œ์Šค, ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ •์ฑ…, ์กฐํšŒ์„ฑ MCP ํฌํ•จ ๋ฒ”์œ„๋ฅผ ํ™•์ •ํ•œ๋‹ค. + +# 2. ์š”๊ตฌ์‚ฌํ•ญ ํŒ์ • ๊ฒฐ๊ณผ +์ด๋ฒˆ ์š”์ฒญ์ด ๊ธฐ์กด ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ๋น„๊ตํ•ด ์‹ ๊ทœ์ธ์ง€, ์œ ์‚ฌํ•œ์ง€, ์ค‘๋ณต์ธ์ง€ ํŒ๋‹จํ•˜๊ณ  ๊ทธ ๊ทผ๊ฑฐ๋ฅผ ๋‚จ๊ธด๋‹ค. + +- ํŒ์ •: ์‹ ๊ทœ +- ๊ทผ๊ฑฐ + - ๊ฐ€์žฅ ๊ทผ์ ‘ํ•œ dcy-005๋Š” ๋Œ€์‹œ๋ณด๋“œ ์šด์˜ ํ—ˆ๋ธŒ์™€ `accounts.liquidity_bucket`/`last_balance` ๊ธฐ๋ฐ˜ ์œ ๋™์„ฑยท์ˆœ์ž์‚ฐ "ํ‘œ์‹œ"๋ฅผ confirmed ๋ฒ”์œ„๋กœ ๋‹ค๋ฃจ๋ฉฐ, ๋ณ„๋„ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ํ…Œ์ด๋ธ”ยท์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”ยทํ‰๊ฐ€ ์ž๋™ํ™”๋ฅผ Out of Scope๋กœ ๋ช…์‹œ ์ œ์™ธํ–ˆ๋‹ค. ์ด๋ฒˆ ์š”์ฒญ์€ ๊ทธ ์ œ์™ธ๋œ ํ‰๊ฐ€ ์ด๋ ฅยท์ž์‚ฐ ํ…Œ์ด๋ธ”ยทํ‰๊ฐ€ ์ฒด๊ณ„๋ฅผ ์‹ ๊ทœ๋กœ ์ •์˜ํ•˜๋ฏ€๋กœ dcy-005์˜ ๋ฏธ์„ธ ์กฐ์ •์ด ์•„๋‹ˆ๋‹ค. + - dcy-005๋Š” confirmed ์ƒํƒœ๋กœ BAT-005(release-candidate)๊นŒ์ง€ ์ง„ํ–‰๋ผ ๋ณ€๊ฒฝ ์ „์ œ๊ฐ€ ๊ณ ์ •๋ผ ์žˆ์–ด, ํ‰๊ฐ€ ์ด๋ ฅยท์‹ ๊ทœ ์—”ํ‹ฐํ‹ฐ ๋„์ž…์„ ๊ฐ™์€ ๋ฌธ์„œ์— ๋ˆ„์ ํ•˜๋ฉด ์ฑ…์ž„ ๊ฒฝ๊ณ„๊ฐ€ ํ๋ ค์ง„๋‹ค. ํ›„์† ๋ฐ˜๋ณต์„ ๋…๋ฆฝ ์ฃผ๊ธฐ๋กœ ์ถ”์ ํ•˜๋Š” ํŽธ์ด ๋ช…ํ™•ํ•˜๋‹ค. + +# 3. ๋ฌธ์ œ์ ์˜ ์š”์•ฝ +๋ฌธ์ œ์˜ ํ˜„์žฌ ์ƒํƒœ, ๊ธฐ๋Œ€ ์ƒํƒœ, ๊ด€๋ จ ์ดํ•ด๊ด€๊ณ„์ž, ์ œ์•ฝ์‚ฌํ•ญ์„ ์ •๋ฆฌํ•œ๋‹ค. Discovery์˜ ํ•ต์‹ฌ ๋งฅ๋ฝ์„ ๊ฐ€์žฅ ๋จผ์ € ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. + +- ํ˜„์žฌ ์ƒํƒœ + - `dashboard/app.py`์˜ `get_asset_liquidity_report`๋Š” ํ™œ์„ฑ ๊ณ„์ขŒ๋ฅผ `accounts.liquidity_bucket`์œผ๋กœ ๋ฌถ๊ณ  ์ž์‚ฐ/๋ถ€์ฑ„ ๊ฐ’์œผ๋กœ `accounts.last_balance`๋งŒ ์‚ฌ์šฉํ•œ๋‹ค(`docs/runtime-flows.md` 51~53ํ–‰). ์ฆ‰ ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์ด ๊ณ„์ขŒ๋ณ„ ๋‹จ์ผ ๊ฐ€๋ณ€ ์ปฌ๋Ÿผ์—๋งŒ ์กด์žฌํ•œ๋‹ค. + - `docs/data-model.md`์˜ LedgerAccount๋Š” `last_balance`(์ตœ์‹  ์ž”์•ก), `last_synced_at`(๊ธฐ์ค€ ์Šค๋ƒ…์ƒท ์‹œ๊ฐ), `liquidity_bucket`๋งŒ ๊ฐ–๊ณ  ์žˆ๊ณ , ์‹œ์ ๋ณ„ ์ž์‚ฐ ํ‰๊ฐ€ ์ด๋ ฅ์„ ๋ณด์กดํ•˜๋Š” ๋ณ„๋„ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†๋‹ค. ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” "๋ณ„๋„ ํ‰๊ฐ€ ํ…Œ์ด๋ธ” ์—†์ด `last_balance`/`last_synced_at`/`memo`๋กœ ์ฒ˜๋ฆฌ"ํ•˜๋„๋ก ๊ทœ์ •๋ผ ์žˆ๋‹ค(data-model 43ํ–‰). + - `docs/data-model.md` Current Gaps(164ํ–‰)์™€ `docs/runtime-flows.md`(131ํ–‰)๋Š” "๋ณ„๋„ ์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”๊ณผ ์ž๋™ ์‹œ์„ธยท๋ถ€๋™์‚ฐ ํ‰๊ฐ€๋Š” REQ-005 ์ดํ›„๋กœ ๋ณด๋ฅ˜, ์‹ ๊ทœ REQ ํ•„์š”"๋ผ๊ณ  ๋ช…์‹œํ•œ๋‹ค. ์ฆ‰ ์ด๋ฒˆ ์ฃผ์ œ๋Š” ๊ธฐ์กด baseline์ด ์˜๋„์ ์œผ๋กœ ๋น„์›Œ ๋‘” ๊ฐญ์ด๋‹ค. + - `liquidity_bucket`/`last_balance` ๊ด€๋ จ ๊ตฌํ˜„์€ `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/db.py`, `jarca_ledger_mcp/server.py`, `dashboard/app.py`์— ์กด์žฌํ•˜๋‚˜, ์Šค๋ƒ…์ƒท ์ด๋ ฅ ํ…Œ์ด๋ธ”ยท์ˆ˜๊ธฐ ์ž์‚ฐ ์—”ํ‹ฐํ‹ฐยทํ‰๊ฐ€ ๊ทœ์น™ ๋ชจ๋“ˆ์€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค. + - `docs/project-structure.md`, `docs/runtime-flows.md` baseline ๋ฌธ์„œ๋Š” ์กด์žฌํ•˜๋‚˜ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ด๋ ฅยท์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”ยทํ‰๊ฐ€ ์ฒด๊ณ„ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์•„์ง ๋ฐ˜์˜ํ•˜์ง€ ์•Š๋Š”๋‹ค. +- ๊ธฐ๋Œ€ ์ƒํƒœ + - ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์ด ์‹œ์ ๋ณ„ ์Šค๋ƒ…์ƒท์œผ๋กœ ๋ณด์กด๋ผ "ํŠน์ • ๊ธฐ์ค€์ผ ์ˆœ์ž์‚ฐ"๊ณผ ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด(์ถ”์ด)์„ ์žฌํ˜„ยท์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. + - ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ(๋ถ€๋™์‚ฐยท์˜ˆ์ ๊ธˆยท๋Œ€์ถœยท๊ธฐํƒ€)์„ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์œผ๋กœ ๋ช…์‹œ ๋“ฑ๋กยท๊ฐฑ์‹ ํ•˜๊ณ , ํ‰๊ฐ€ ์ถœ์ฒ˜ยท๊ธฐ์ค€์ผยทํ‰๊ฐ€ ๋ฐฉ๋ฒ•์ด ์ผ๊ด€๋œ ํ‰๊ฐ€ ์ฒด๊ณ„๋กœ ๊ธฐ๋ก๋œ๋‹ค. + - ์ž์‚ฐ ํ‰๊ฐ€ ๊ทœ์น™(์ž์‚ฐ์„ฑ ํ•ญ๋ชฉ ํ•ฉ๊ณ„ โˆ’ ๋ถ€์ฑ„(`loan`) ์ฐจ๊ฐ, ๋ฏธ์ž…๋ ฅ ์ž์‚ฐ ์ œ์™ธ ํ‘œ๊ธฐ ๋“ฑ)์ด ๋‹จ์ผ ์ •์˜๋กœ ๋ฌธ์„œยท์ง‘๊ณ„ยทํ™”๋ฉด์—์„œ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋œ๋‹ค. +- ์ดํ•ด๊ด€๊ณ„์ž + - ๊ฐ€๊ณ„ ์žฌ๋ฌด๋ฅผ ์šด์˜ํ•˜๋ฉฐ ์ž์‚ฐ/์ˆœ์ž์‚ฐ ์ถ”์ด๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์‚ฌ์šฉ์ž(๋Œ€์‹œ๋ณด๋“œ 1์ฐจ ์‚ฌ์šฉ์ž) + - ๋ฌธ์„œ/์š”๊ตฌ์‚ฌํ•ญ ๋ฆฌ๋ทฐ์–ด ๋ฐ ์Šน์ธ์ž(Hermes/์‚ฌ์šฉ์ž) + - ๋ฐ์ดํ„ฐ ๋ชจ๋ธยท์„œ๋น„์ŠคยทDBยท๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„/์šด์˜ํ•˜๋Š” ๊ฐœ๋ฐœ ๋‹ด๋‹น์ž +- ์ œ์•ฝ์‚ฌํ•ญ + - DB ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์€ additive/idempotent ์›์น™์„ ๋”ฐ๋ฅด๋ฉฐ ํŒŒ๊ดด์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋ช…์‹œ ์Šน์ธยท๋ฐฑ์—… ํ›„์—๋งŒ ํ•œ๋‹ค(`docs/data-model.md` Persistence Notes). + - ๊ธฐ์กด dcy-005/REQ-005 confirmed ๋ฒ”์œ„(`accounts.liquidity_bucket`/`last_balance` ๊ธฐ๋ฐ˜ ํ‘œ์‹œ)์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๊ณ , ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ์ด์ค‘ํ™”๋กœ ๋น„์œจยท์ˆœ์ž์‚ฐ์ด ์–ด๊ธ‹๋‚˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. + - ๋ณธ Discovery ๋‹จ๊ณ„์—์„œ๋Š” ์†Œ์Šค ์ฝ”๋“œ/ํ…Œ์ŠคํŠธ/์Šคํ‚ค๋งˆ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์–‘๋งŒ ์ •์˜ํ•œ๋‹ค. + +## ์ฃผ์š” ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค +์‚ฌ์šฉ์ž ๋˜๋Š” ์šด์˜์ž๊ฐ€ ์‹ค์ œ๋กœ ์–ด๋–ค ์ƒํ™ฉ์—์„œ ์ด ๋ณ€๊ฒฝ์„ ํ•„์š”๋กœ ํ•˜๋Š”์ง€ ๋Œ€ํ‘œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ ๋Š”๋‹ค. + +- ์‚ฌ์šฉ์ž๊ฐ€ ์›”๋ง์— ๋ถ€๋™์‚ฐยท์˜ˆ์ ๊ธˆยทํˆฌ์žยท๋Œ€์ถœ ์ž”์•ก์„ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์œผ๋กœ ์ž…๋ ฅ/๊ฐฑ์‹ ํ•˜๋ฉด, ํ•ด๋‹น ์‹œ์ ์˜ ์ž์‚ฐ ํ‰๊ฐ€ ์Šค๋ƒ…์ƒท์ด ๊ธฐ์ค€์ผ๊ณผ ํ•จ๊ป˜ ๋ณด์กด๋œ๋‹ค. +- ์‚ฌ์šฉ์ž๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ์—์„œ "์ด๋ฒˆ ๋‹ฌ ๋ง ์ˆœ์ž์‚ฐ"๊ณผ "์ตœ๊ทผ N๊ฐœ์›” ์ˆœ์ž์‚ฐ ์ถ”์ด"๋ฅผ ์กฐํšŒํ•ด ์ž์‚ฐ์ด ๋Š˜์—ˆ๋Š”์ง€ ์ค„์—ˆ๋Š”์ง€ ์‹œ์  ๋น„๊ต๋กœ ํ™•์ธํ•œ๋‹ค. +- ์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ์ž์‚ฐ(์˜ˆ: ์ „์„ธ๋ณด์ฆ๊ธˆ, ์ฐจ๋Ÿ‰)์˜ ํ‰๊ฐ€๊ฐ’์„ ๊ฐฑ์‹ ํ•  ๋•Œ, ๊ณผ๊ฑฐ ํ‰๊ฐ€๊ฐ’์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ์ƒˆ ์Šค๋ƒ…์ƒท์œผ๋กœ ๋ˆ„์ ํ•ด ํ‰๊ฐ€ ์ด๋ ฅ์„ ์ถ”์ ํ•œ๋‹ค. + +# 4. ์ด๋ฒˆ์— ์ •์˜ํ•  ๋ณ€๊ฒฝ +์ด๋ฒˆ Discovery์—์„œ ๋‹ค๋ฃจ๋Š” ํ•ต์‹ฌ ๋ณ€๊ฒฝ ํ•ญ๋ชฉ์„ ๋ชฉ๋ก์œผ๋กœ ์ ๋Š”๋‹ค. ๊ธฐ๋Šฅ, ๊ตฌ์กฐ, ๊ทœ์น™ ๋ณ€๊ฒฝ์„ ํ•จ๊ป˜ ํฌํ•จํ•ด๋„ ๋œ๋‹ค. + +- ์ž์‚ฐ ํ‰๊ฐ€ ์Šค๋ƒ…์ƒท ์ด๋ ฅ ๋ชจ๋ธ ์ •์˜: ์‹œ์ ๋ณ„ ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์„ ๋ณด์กดํ•˜๋Š” ์Šค๋ƒ…์ƒท ์—”ํ‹ฐํ‹ฐ(`asset_snapshots` ๊ถŒ์žฅ)์™€ ๊ธฐ์ค€์ผยทํ‰๊ฐ€ ์ถœ์ฒ˜ยท์Šค๋ƒ…์ƒท ๋‹จ์œ„(๊ณ„์ขŒ/์ž์‚ฐ ํ•ญ๋ชฉ)ยท์ƒ์„ฑ ์ •์ฑ…(์›”๋ง ์ž๋™ vs ์ˆ˜๋™)์„ ์‚ฌ์–‘์œผ๋กœ ์ •์˜ํ•œ๋‹ค. +- ์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ” ์ •์˜: ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„(๋ถ€๋™์‚ฐยท์˜ˆ์ ๊ธˆยท๋Œ€์ถœยท๊ธฐํƒ€)๋ฅผ ๊ณ„์ขŒ์™€ ๋ถ„๋ฆฌํ•ด ๋ช…์‹œ ๋“ฑ๋กํ•˜๋Š” ์ˆ˜๊ธฐ ์ž์‚ฐ ์—”ํ‹ฐํ‹ฐ(`manual_assets` ๊ถŒ์žฅ)์™€ ํ‰๊ฐ€๊ฐ’ยท๊ธฐ์ค€์ผยท`liquidity_bucket`ยท์ถœ์ฒ˜ ๋ฉ”๋ชจ ํ•„๋“œ๋ฅผ ์ •์˜ํ•œ๋‹ค. +- ํ‰๊ฐ€ ์ฒด๊ณ„ ๊ทœ์น™ ์ •์˜: ์ž์‚ฐ์„ฑ ํ•ญ๋ชฉ ํ•ฉ๊ณ„์—์„œ ๋ถ€์ฑ„(`loan`)๋ฅผ ์ฐจ๊ฐํ•˜๋Š” ์ˆœ์ž์‚ฐ ์‚ฐ์‹, ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค(๊ณ„์ขŒ `last_balance` vs ์ˆ˜๊ธฐ ์ž์‚ฐ vs ์Šค๋ƒ…์ƒท)์˜ ๋‹จ์ผํ™”/์šฐ์„ ์ˆœ์œ„, ๋ฏธ์ž…๋ ฅยท๋ฏธํ‰๊ฐ€ ์ž์‚ฐ์˜ ์ œ์™ธยทํ‘œ๊ธฐ ๊ทœ์น™์„ ๋‹จ์ผ ์ •์˜๋กœ ํ™•์ •ํ•œ๋‹ค. +- ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„ ์ •์˜: ์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด/์ˆ˜๊ธฐ ์ž์‚ฐ์„ ๋Œ€์‹œ๋ณด๋“œ ๋‚ด๋ถ€ ๋ชจ๋ธ์—๋งŒ ๊ฐ€๋‘์ง€ ์•Š๊ณ , ๋™์ผํ•œ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค์™€ ์ˆœ์ž์‚ฐ ์‚ฐ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ์กฐํšŒ์„ฑ MCP ์‘๋‹ต ๊ณ„์•ฝ๊นŒ์ง€ ๋ฒ”์œ„์— ํฌํ•จํ•œ๋‹ค. ์ดˆ๊ธฐ ๋ฒ”์œ„๋Š” ์กฐํšŒ(read) ์ค‘์‹ฌ์œผ๋กœ ํ•œ์ •ํ•˜๊ณ  ์Šค๋ƒ…์ƒท ์ƒ์„ฑยท์ˆ˜๊ธฐ ์ž์‚ฐ CRUD ๊ฐ™์€ mutation MCP๋Š” ํ›„์† ๋ฒ”์œ„๋กœ ๋‘”๋‹ค. +- baseline ๋ฌธ์„œ ๋ฐ˜์˜: ์œ„ ์ž์‚ฐ ์Šค๋ƒ…์ƒทยท์ˆ˜๊ธฐ ์ž์‚ฐยทํ‰๊ฐ€ ์ฒด๊ณ„ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๊ณผ ํ‰๊ฐ€/์Šค๋ƒ…์ƒท ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md` ๊ฐฑ์‹  ์š”๊ตฌ๋กœ ๋“ฑ๋กํ•œ๋‹ค. + +## ์˜ํ–ฅ ๋ฒ”์œ„ +๋ณ€๊ฒฝ์ด ๋ฏธ์น  ์ฝ”๋“œ/๋ฌธ์„œ/์šด์˜ ๋ฒ”์œ„๋ฅผ ๋ช…์‹œํ•ด REQ ์ •์ œ์™€ batch planning์—์„œ ์ถ”์ • ๋ˆ„๋ฝ์„ ์ค„์ธ๋‹ค. + +- ์ฝ”๋“œ ์˜ํ–ฅ: + - `jarca_ledger_mcp/db.py` ์Šคํ‚ค๋งˆ(์Šค๋ƒ…์ƒทยท์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ” additive ์ถ”๊ฐ€), `jarca_ledger_mcp/service.py` ์ž์‚ฐ ํ‰๊ฐ€/์Šค๋ƒ…์ƒท ์ง‘๊ณ„ยท์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ํ•จ์ˆ˜, `jarca_ledger_mcp/server.py`์˜ ์กฐํšŒ์„ฑ MCP ์‘๋‹ต ๊ณ„์•ฝ(๊ธฐ์ค€์ผ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ). + - `dashboard/app.py`์˜ `get_asset_liquidity_report`/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๋ถ€๊ฐ€ ๋‹จ์ผ ๊ถŒ์œ„ ์†Œ์Šค(์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ)๋ฅผ ์ฐธ์กฐํ•˜๋„๋ก ์กฐ์ •. +- ๋ฌธ์„œ ์˜ํ–ฅ: + - `docs/data-model.md`(`asset_snapshots`/`manual_assets` ์—”ํ‹ฐํ‹ฐ, ์ˆœ์ž์‚ฐ ์‚ฐ์‹ยทํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ๊ทœ์น™), `docs/project-structure.md`, `docs/runtime-flows.md`(์Šค๋ƒ…์ƒท ์ƒ์„ฑยท์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ ํ๋ฆ„) ๊ฐฑ์‹ . + - dcy-005/REQ-005์™€์˜ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ์ •ํ•ฉ์„ฑ ๋ช…์‹œ. +- ์šด์˜ ์˜ํ–ฅ: + - ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ ํ‰๊ฐ€ ์ž…๋ ฅ/๊ฐฑ์‹ ๊ณผ ์›”๋ง ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์ ˆ์ฐจ๊ฐ€ ์‹ ์„ค๋ผ ์šด์˜ ์ž…๋ ฅ ๋ฃจํ‹ดยท์ž”์•ก๋Œ€์กฐ ํ๋ฆ„์— ์˜ํ–ฅ์„ ์ค€๋‹ค. + +# 5. ์š”๊ตฌ์‚ฌํ•ญ ๋ชฉ๋ก +์‹ค์ œ๋กœ ๊ตฌํ˜„๊ณผ ๊ฒ€์ฆ์œผ๋กœ ์ด์–ด์งˆ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ธฐ๋Šฅ/๋น„๊ธฐ๋Šฅ์œผ๋กœ ๋‚˜๋ˆ„์–ด ์ •๋ฆฌํ•˜๊ณ , ๋ฒ”์œ„ ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ์ ๋Š”๋‹ค. + +## ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ +- FR-1: ์‹œ์ ๋ณ„ ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์„ ๋ณด์กดํ•˜๋Š” ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ด๋ ฅ์„ ๋„์ž…ํ•œ๋‹ค. ๊ฐ ์Šค๋ƒ…์ƒท์€ ์ตœ์†Œํ•œ ๊ธฐ์ค€์ผ, ํ‰๊ฐ€ ๋Œ€์ƒ(๊ณ„์ขŒ ๋˜๋Š” ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ), ํ‰๊ฐ€ ๊ธˆ์•ก, `liquidity_bucket`, ํ‰๊ฐ€ ์ถœ์ฒ˜/๋ฉ”๋ชจ๋ฅผ ๋ณด์กดํ•˜๋ฉฐ, ๊ณผ๊ฑฐ ๊ฐ’์€ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ๋ˆ„์ ํ•œ๋‹ค. +- FR-2: ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋ฅผ ๊ณ„์ขŒ์™€ ๋ถ„๋ฆฌํ•ด ๋“ฑ๋กํ•˜๋Š” ์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”์„ ๋„์ž…ํ•œ๋‹ค. ๊ฐ ํ•ญ๋ชฉ์€ ์ž์‚ฐ๋ช…, ํ‰๊ฐ€ ๊ธˆ์•ก, ๊ธฐ์ค€์ผ, `liquidity_bucket`(`cash`/`investment`/`deposit`/`real_estate`/`loan`/`other`), ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ๊ฐ–๋Š”๋‹ค. +- FR-3: ์ˆœ์ž์‚ฐ ํ‰๊ฐ€ ๊ทœ์น™์„ ๋‹จ์ผ ์ •์˜๋กœ ํ™•์ •ํ•œ๋‹ค. ์ž์‚ฐ์„ฑ ํ•ญ๋ชฉ(`cash`/`investment`/`deposit`/`real_estate`/`other`) ํ‰๊ฐ€๊ฐ’ ํ•ฉ๊ณ„์—์„œ `loan` ๋ถ€์ฑ„๋ฅผ ์ฐจ๊ฐํ•˜๊ณ , ํ‰๊ฐ€๊ฐ’์ด ์—†๋Š” ํ•ญ๋ชฉ์€ ์ž„์˜ ๊ฑฐ๋ž˜ ํ•ฉ๊ณ„๋กœ ์ถ”์ •ํ•˜์ง€ ์•Š๊ณ  ์ œ์™ธํ•˜๋ฉฐ ํ™”๋ฉด์— ์ œ์™ธ ์‚ฌ์‹ค์„ ํ‘œ๊ธฐํ•œ๋‹ค. +- FR-4: ๋Œ€์‹œ๋ณด๋“œ/๋ฆฌํฌํŠธ์™€ ์กฐํšŒ์„ฑ MCP๊ฐ€ ์ž์‚ฐยท์ˆœ์ž์‚ฐ์„ ์‚ฐ์ถœํ•  ๋•Œ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๋ฅผ ๋‹จ์ผํ™”ํ•œ๋‹ค. ๊ณ„์ขŒ `last_balance`, ์ˆ˜๊ธฐ ์ž์‚ฐ, ์Šค๋ƒ…์ƒท์ด ๋™์‹œ์— ์กด์žฌํ•  ๋•Œ์˜ ์šฐ์„ ์ˆœ์œ„์™€ ํด๋ฐฑ์„ ๋ช…์‹œํ•˜๊ณ , dcy-005/REQ-005์˜ `accounts.liquidity_bucket` ๊ธฐ๋ฐ˜ ํ‘œ์‹œ์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๊ฒŒ ์ •ํ•ฉ์„ฑ ๊ทœ์น™์„ ์ •์˜ํ•œ๋‹ค. +- FR-5: ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„์— ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ๋ฅผ ํฌํ•จํ•œ๋‹ค. ๊ฐ ์‘๋‹ต์€ ์ตœ์†Œํ•œ ๊ธฐ์ค€์ผ/์ง‘๊ณ„ ๊ตฌ๊ฐ„, ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค, `liquidity_bucket`, ์ˆœ์ž์‚ฐ ์‚ฐ์‹ ์ ์šฉ ๊ธฐ์ค€, ํฌํ•จยท์ œ์™ธ ํ•ญ๋ชฉ์„ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. +- FR-6: `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md` baseline ๋ฌธ์„œ์— `asset_snapshots`/`manual_assets` ๋ชจ๋ธ๊ณผ ์Šค๋ƒ…์ƒท ์ƒ์„ฑยท์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ดยท์กฐํšŒ์„ฑ MCP ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ฐฑ์‹ ์„ ํฌํ•จํ•œ๋‹ค. + +## ๋น„๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ +์„ฑ๋Šฅ, ์šด์˜์„ฑ, ์ผ๊ด€์„ฑ, ์ž๋™ํ™” ์•ˆ์ •์„ฑ ๋“ฑ ๊ธฐ๋Šฅ ์™ธ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ ๋Š”๋‹ค. + +- NFR-1 (์ผ๊ด€์„ฑ): ์ˆœ์ž์‚ฐยท์ž์‚ฐ ๊ตฌ์„ฑ ๊ณ„์‚ฐ์€ ๋‹จ์ผ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค์™€ ๋‹จ์ผ ์‚ฐ์‹(์ž์‚ฐ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ)์„ ๋ชจ๋“  ์„น์…˜ยท์ง‘๊ณ„ยทMCP ์‘๋‹ต์—์„œ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•œ๋‹ค. +- NFR-2 (์ด๋ ฅ ๋ณด์กด์„ฑ): ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’ ๊ฐฑ์‹ ์€ ๊ธฐ์กด ์Šค๋ƒ…์ƒท์„ ํŒŒ๊ดด์ ์œผ๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ์‹ ๊ทœ ์Šค๋ƒ…์ƒท์œผ๋กœ ๋ˆ„์ ํ•ด ๊ธฐ์ค€์ผ๋ณ„ ์žฌํ˜„ ๊ฐ€๋Šฅ์„ฑ์„ ๋ณด์žฅํ•œ๋‹ค. +- NFR-3 (์•ˆ์ •์„ฑ): `asset_snapshots`/`manual_assets` ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์€ additive/idempotent๋กœ ์ ์šฉํ•˜๊ณ , ์‹ ๊ทœ ํ…Œ์ด๋ธ”์ด ๋น„์–ด ์žˆ์–ด๋„ ๊ธฐ์กด ๋Œ€์‹œ๋ณด๋“œยท`get_asset_liquidity_report` ๋™์ž‘์„ ๊นจ์ง€ ์•Š๋Š”๋‹ค. + +## ๋ฒ”์œ„ ๊ฒฝ๊ณ„ +์ด๋ฒˆ ๋ฐ˜๋ณต์—์„œ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•  ๋ฒ”์œ„์™€ ๋ช…์‹œ์ ์œผ๋กœ ์ œ์™ธํ•  ๋ฒ”์œ„๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค. + +- In Scope + - ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ด๋ ฅ ์—”ํ‹ฐํ‹ฐ, ์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ, ์ˆœ์ž์‚ฐ ํ‰๊ฐ€ ๊ทœ์น™ยทํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ๋‹จ์ผํ™” ์‚ฌ์–‘ ์ •์˜ ๋ฐ baseline ๋ฌธ์„œ ๊ฐฑ์‹  ์š”๊ตฌ. + - ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ๋ฅผ ํฌํ•จํ•œ ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„ ์ •์˜. + - dcy-005/REQ-005 ํ‘œ์‹œ ๋ฒ”์œ„์™€์˜ ํ‰๊ฐ€ ์ •ํ•ฉ์„ฑ ๊ทœ์น™ ์ •์˜. +- Out of Scope + - ์™ธ๋ถ€ ์‹œ์„ธ/๋ถ€๋™์‚ฐ ํ‰๊ฐ€ ์ž๋™ ์—ฐ๋™, ์‹ค์‹œ๊ฐ„ ์ž”์•ก ์—ฐ๋™. + - ์ž์‚ฐ ํ‰๊ฐ€ ์•Œ๋ฆผ/๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ์ถ”์ฒœ, ์„ธ๊ธˆยท์ˆ˜์ต๋ฅ  ์ž๋™ ๊ณ„์‚ฐ. + - ์Šค๋ƒ…์ƒท ์ƒ์„ฑ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ๊ฐ™์€ mutation MCP ๋ฒ”์œ„. + - ๋ณธ ๋‹จ๊ณ„์—์„œ์˜ ์‹ค์ œ ์Šคํ‚ค๋งˆ/์ฝ”๋“œ/ํ…Œ์ŠคํŠธ ๋ณ€๊ฒฝ(์‚ฌ์–‘ ์ •์˜๋งŒ ์ˆ˜ํ–‰). + +# 6. ๋ฆฌ์Šคํฌ/๊ฐ€์ • ๋ชฉ๋ก +๋ถˆํ™•์‹ค์„ฑ๊ณผ ์ „์ œ ์กฐ๊ฑด์„ ๋ถ„๋ฆฌํ•ด์„œ ์ ๋Š”๋‹ค. ๋ฆฌ์Šคํฌ๋Š” ์˜ํ–ฅ๊ณผ ์™„ํ™” ๋ฐฉ์•ˆ์„, ๊ฐ€์ •์€ ํ˜„์žฌ ํŒ๋‹จ์˜ ์ „์ œ๋ฅผ ๊ธฐ๋กํ•œ๋‹ค. + +## ๋ฆฌ์Šคํฌ +- R-1 (High): ์ž์‚ฐ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๊ฐ€ ๊ณ„์ขŒ `last_balance`, ์ˆ˜๊ธฐ ์ž์‚ฐ, ์Šค๋ƒ…์ƒท์œผ๋กœ ์‚ผ์ค‘ํ™”๋˜๋ฉด ๋™์ผ ์ž์‚ฐ์ด ์ค‘๋ณต ์ง‘๊ณ„๋˜๊ฑฐ๋‚˜ ์ˆœ์ž์‚ฐ์ด ์–ด๊ธ‹๋‚  ์ˆ˜ ์žˆ๋‹ค. + - ์™„ํ™” ๋ฐฉ์•ˆ: FR-4์—์„œ ๋‹จ์ผ ๊ถŒ์œ„ ์†Œ์Šค์™€ ์šฐ์„ ์ˆœ์œ„/ํด๋ฐฑ์„ ๋จผ์ € ํ™•์ •ํ•˜๊ณ , dcy-005/REQ-005 ํ‘œ์‹œ ๊ฒฝ๋กœ์™€ ์กฐํšŒ์„ฑ MCP ์‘๋‹ต์ด ๋ชจ๋‘ ๊ฐ™์€ ๊ทœ์น™์„ ๋”ฐ๋ฅด๋„๋ก REQ ์ดˆ์•ˆ์— ๋ช…์‹œํ•œ๋‹ค. +- R-2 (Medium): ์‹ ๊ทœ ํ…Œ์ด๋ธ”(`asset_snapshots`/`manual_assets`) ๋„์ž…์ด ๊ธฐ์กด `get_asset_liquidity_report`/๋Œ€์‹œ๋ณด๋“œ ์ง‘๊ณ„์™€ ๋™์ž‘ ์ถฉ๋Œ์„ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. + - ์™„ํ™” ๋ฐฉ์•ˆ: additive/idempotent ์Šคํ‚ค๋งˆ์™€ ๋นˆ ํ…Œ์ด๋ธ” ์•ˆ์ „ ํด๋ฐฑ์„ NFR-3๋กœ ๊ณ ์ •ํ•˜๊ณ , ๋‹จ๊ณ„์  ์ „ํ™˜(ํ‘œ์‹œ๋Š” ๊ธฐ์กด ์œ ์ง€ โ†’ ๊ถŒ์œ„ ์†Œ์Šค ์ „ํ™˜) ๊ฒฝ๋กœ๋ฅผ ์„ค๊ณ„ ๋‹จ๊ณ„์—์„œ ๋ถ„๋ฆฌํ•œ๋‹ค. +- R-3 (Medium): ์ˆ˜๊ธฐ ์ž์‚ฐ ํ‰๊ฐ€๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์˜์กด๋„๊ฐ€ ๋†’์•„ ์ž…๋ ฅ ๋ˆ„๋ฝ/๊ธฐ์ค€์ผ ๋ถˆ์ผ์น˜ ์‹œ ์ˆœ์ž์‚ฐ ์ถ”์ด๊ฐ€ ์™œ๊ณก๋œ๋‹ค. + - ์™„ํ™” ๋ฐฉ์•ˆ: ๊ธฐ์ค€์ผยท์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ํ•„์ˆ˜ ํ•„๋“œ๋กœ ๋‘๊ณ , ๋ฏธํ‰๊ฐ€/์˜ค๋ž˜๋œ ํ‰๊ฐ€ ํ•ญ๋ชฉ์„ ํ™”๋ฉด ๊ฒฝ๊ณ ๋กœ ๋…ธ์ถœํ•˜๋Š” ๊ทœ์น™์„ ์š”๊ตฌ์‚ฌํ•ญ์— ํฌํ•จํ•œ๋‹ค. + +## ๊ฐ€์ • +์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜์ง€๋งŒ ํ˜„์žฌ ๋ฌธ์„œ ์ž‘์„ฑ์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ์ „์ œ๋ฅผ ์ ๋Š”๋‹ค. + +- A-1: `liquidity_bucket` ๋ถ„๋ฅ˜ ์ถ•๊ณผ ๊ทธ ํ—ˆ์šฉ๊ฐ’(`cash`/`investment`/`deposit`/`real_estate`/`loan`/`other`)์€ dcy-005/REQ-005 ์ •์˜๋ฅผ ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. +- A-2: ์ˆœ์ž์‚ฐ ์‚ฐ์‹์˜ ๊ธฐ๋ณธ ๊ณจ๊ฒฉ(์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ, ๋ฏธํ‰๊ฐ€ ์ œ์™ธ)์€ dcy-005์—์„œ ํ™•์ •๋œ ์ •์˜๋ฅผ ๊ณ„์Šนํ•ด ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ์œผ๋กœ ํ™•์žฅํ•œ๋‹ค. +- A-3: ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์€ ์ดˆ๊ธฐ์—๋Š” ์‚ฌ์šฉ์ž ์ˆ˜๊ธฐ ์ž…๋ ฅ์„ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ํ•˜๋ฉฐ ์™ธ๋ถ€ ์ž๋™ ์—ฐ๋™์€ ํ›„์† ๋ฒ”์œ„๋กœ ๋‘”๋‹ค. + +## ์˜คํ”ˆ ์งˆ๋ฌธ +๋‹ต์„ ์•„์ง ๋ชจ๋ฅด๋Š” ํ•ญ๋ชฉ์„ ์ ๋Š”๋‹ค. ์ถ”์ •์œผ๋กœ ๋ฎ์ง€ ๋ง๊ณ  REQ ์ดˆ์•ˆ ์ž‘์„ฑ ์ด์ „์— ํ™•์ธํ•  ์งˆ๋ฌธ์œผ๋กœ ๋‚จ๊ธด๋‹ค. + +- OQ-1: ์ž์‚ฐ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹จ์ผํ™”ํ• ์ง€(โ‘  ๊ณ„์ขŒ `last_balance` ์œ ์ง€ + ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ณด๊ฐ•, โ‘ก ๋ชจ๋“  ์ž์‚ฐ์„ ์ˆ˜๊ธฐ ์ž์‚ฐ/์Šค๋ƒ…์ƒท์œผ๋กœ ์ผ์›ํ™”) ๊ฒฐ์ •ํ•œ๋‹ค. + - ์ƒํƒœ: Answered + - ๋‹ต: ๊ณ„์ขŒ `last_balance`๋ฅผ ๊ธฐ์กด ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์œ ์ง€ํ•˜๋˜, ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” `manual_assets`๋กœ ๋ณด๊ฐ•ํ•œ๋‹ค. ์Šค๋ƒ…์ƒท์€ ์ด ๋‘˜์„ ๊ธฐ์ค€์ผ๋ณ„๋กœ ๊ณ ์ •ํ•˜๋Š” ๊ธฐ๋ก์ธต์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. + - ๋ฐ˜์˜ ์œ„์น˜: FR-4, In Scope, `docs/data-model.md`/์กฐํšŒ์„ฑ MCP ์‘๋‹ต ๊ทœ์น™ +- OQ-2: ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์ •์ฑ…(์›”๋ง ์ž๋™ ์ƒ์„ฑ vs ์‚ฌ์šฉ์ž ์ˆ˜๋™ ์ƒ์„ฑ vs ๋‘˜ ๋‹ค)๊ณผ ์Šค๋ƒ…์ƒท ๋‹จ์œ„(๊ณ„์ขŒ ๋‹จ์œ„ vs ์ž์‚ฐ ํ•ญ๋ชฉ ๋‹จ์œ„)๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค. + - ์ƒํƒœ: Answered + - ๋‹ต: ์ดˆ๊ธฐ ์šด์˜ ์ •์ฑ…์€ ์›”๋ง ์ˆ˜๋™ ์ƒ์„ฑ์œผ๋กœ ํ™•์ •ํ•œ๋‹ค. ์Šค๋ƒ…์ƒท ๊ตฌ์กฐ๋Š” ๊ณ„์ขŒ์™€ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์„ ๋ชจ๋‘ ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ ๋‹จ์œ„ ์ง‘๊ณ„๋ฅผ ์ˆ˜์šฉํ•˜๋˜, ์šด์˜ ์ž…๋ ฅ์€ ์›”๋ง ๋งˆ๊ฐ ๊ธฐ์ค€์œผ๋กœ ๋‹จ์ˆœํ™”ํ•œ๋‹ค. + - ๋ฐ˜์˜ ์œ„์น˜: FR-1, ์šด์˜ ์˜ํ–ฅ, runtime-flows ํ›„์† ๊ฐฑ์‹  + +- OQ-3: ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„๋ฅผ ์–ด๋””๊นŒ์ง€ ํฌํ•จํ• ์ง€(โ‘  ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ + ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ + ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ ํฌํ•จ, โ‘ก ์ผ๋ถ€๋งŒ ํฌํ•จ) ๊ฒฐ์ •ํ•œ๋‹ค. + - ์ƒํƒœ: Answered + - ๋‹ต: ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ๋ฅผ `dcy-008`์˜ ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„์— ํฌํ•จํ•œ๋‹ค. ์Šค๋ƒ…์ƒท ์ƒ์„ฑ๊ณผ ์ˆ˜๊ธฐ ์ž์‚ฐ CRUD ๊ฐ™์€ mutation MCP๋Š” ํ›„์† ๋ฒ”์œ„๋กœ ๋ถ„๋ฆฌํ•œ๋‹ค. + - ๋ฐ˜์˜ ์œ„์น˜: FR-5, In Scope/Out of Scope, `docs/interface-contract.md`/`docs/runtime-flows.md` ํ›„์† ๊ฐฑ์‹  + +### ์˜คํ”ˆ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ๊ธฐ์ค€ +- `Open`: ์•„์ง ๋‹ต์ด ์—†๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„ ์ง„ํ–‰์— ์˜ํ–ฅ์ด ์žˆ๋Š” ์ƒํƒœ๋‹ค. ๋ฌธ์„œ์— ์œ ์ง€ํ•œ๋‹ค. +- `Answered`: ๋‹ต์ด ํ™•์ •๋œ ์ƒํƒœ๋‹ค. ๊ด€๋ จ ๋ณธ๋ฌธ/์š”๊ตฌ์‚ฌํ•ญ์— ๋ฐ˜์˜ํ•œ ๋’ค ์˜คํ”ˆ ์งˆ๋ฌธ ๋ชฉ๋ก์—์„œ๋Š” ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ resolved ๊ธฐ๋ก์œผ๋กœ ์ด๋™ํ•œ๋‹ค. +- `Deferred`: ์ง€๊ธˆ ๋‹จ๊ณ„์—์„œ ๊ฒฐ์ •ํ•˜์ง€ ์•Š๊ณ  ๋‹ค์Œ ๋‹จ๊ณ„ ๋˜๋Š” ํ›„์† ์ด์Šˆ๋กœ ๋„˜๊ธฐ๋Š” ์ƒํƒœ๋‹ค. ๋„˜๊ธธ ๋Œ€์ƒ๊ณผ ์ด์œ ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ ๋Š”๋‹ค. +- `Dropped`: ๋” ์ด์ƒ ์œ ํšจํ•˜์ง€ ์•Š์€ ์งˆ๋ฌธ์ด๋‹ค. ์‚ญ์ œ ์‚ฌ์œ ๋ฅผ ์งง๊ฒŒ ๋‚จ๊ธฐ๊ณ  ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐํ•œ๋‹ค. +- ์ฒ˜๋ฆฌ ์›์น™ 1: ์งˆ๋ฌธ๋งŒ ๋‚จ๊ธฐ์ง€ ๋ง๊ณ  ๋ฐ˜๋“œ์‹œ `์ƒํƒœ`, `์ฒ˜๋ฆฌ ๋ฐฉ์•ˆ`, `์ข…๋ฃŒ ์กฐ๊ฑด`์„ ํ•จ๊ป˜ ์ ๋Š”๋‹ค. +- ์ฒ˜๋ฆฌ ์›์น™ 2: ๋‹ต์ด ๋‚˜์˜จ ์งˆ๋ฌธ์€ ๋ณธ๋ฌธ ๋ฐ˜์˜ ์—†์ด ๋‹จ์ˆœ ์‚ญ์ œํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ฐ˜์˜ ์œ„์น˜๋ฅผ ๋จผ์ € ๊ฐฑ์‹ ํ•œ๋‹ค. +- ์ฒ˜๋ฆฌ ์›์น™ 3: High ์˜ํ–ฅ ์งˆ๋ฌธ์€ `Deferred` ์ฒ˜๋ฆฌ ํ›„ ์ฒ˜๋ฆฌ ๋ฐฉ์•ˆ์— REQ ์ธ๊ณ„ ์ถ”์  ๋ฐฉ์‹์„ ๋ช…์‹œํ•œ๋‹ค. + +# 7. ์ดˆ๊ธฐ ์„ฑ๊ณต ๊ธฐ์ค€ +์ด ๋ณ€๊ฒฝ์ด ์„ฑ๊ณตํ–ˆ๋‹ค๊ณ  ํŒ๋‹จํ•  ๊ธฐ์ค€์„ ์ธก์ • ๊ฐ€๋Šฅํ•˜๊ฒŒ ์ •์˜ํ•œ๋‹ค. ๊ฐ€๋Šฅํ•˜๋ฉด ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜๊นŒ์ง€ ์—ฐ๊ฒฐํ•œ๋‹ค. + +- S-1: ๊ธฐ์ค€์ผ๋ณ„ ์ž์‚ฐ ํ‰๊ฐ€ ์Šค๋ƒ…์ƒท์ด ๋ณด์กด๋ผ "ํŠน์ • ๊ธฐ์ค€์ผ ์ˆœ์ž์‚ฐ"๊ณผ ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด์„ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์ด ์‚ฌ์–‘์œผ๋กœ ์ •์˜๋œ๋‹ค. +- S-2: ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋ฅผ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”๋กœ ๋“ฑ๋กยท๊ฐฑ์‹ ํ•˜๊ณ , ์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ ์ˆœ์ž์‚ฐ ์‚ฐ์‹์ด ๋‹จ์ผ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ๊ธฐ์ค€์œผ๋กœ ์ผ๊ด€๋˜๊ฒŒ ์ •์˜๋œ๋‹ค. +- S-3: `asset_snapshots`/`manual_assets` ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๊ณผ ์Šค๋ƒ…์ƒท ์ƒ์„ฑยท์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ดยท์กฐํšŒ์„ฑ MCP ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md`์— ๋ฐ˜์˜๋˜๊ณ , dcy-005/REQ-005 ํ‘œ์‹œ ๊ฒฝ๋กœ์™€์˜ ์ •ํ•ฉ์„ฑ์ด ๋ฌธ์„œ๋กœ ํ™•์ธ๋œ๋‹ค. + +## ์ธก์ • ๋ฐฉ์‹ ๋ฐ ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ +๊ฐ ์„ฑ๊ณต ๊ธฐ์ค€์„ ์–ด๋–ป๊ฒŒ ์ธก์ •ํ• ์ง€์™€ ์–ด๋–ค ๋ฐ์ดํ„ฐ๋‚˜ ๋ฌธ์„œ๋ฅผ ๊ทผ๊ฑฐ๋กœ ์“ธ์ง€ ์ ๋Š”๋‹ค. + +- S-1 ์ธก์ • ๋ฐฉ์‹: ํ‘œ๋ณธ ์ž์‚ฐ/๊ธฐ์ค€์ผ๋กœ ์Šค๋ƒ…์ƒท ๋ˆ„์ ยท๊ธฐ์ค€์ผ ์ˆœ์ž์‚ฐ ์žฌํ˜„์„ ์†๊ฒ€์ฆ๊ณผ ๋น„๊ต / ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜: `asset_snapshots` ๋ชจ๋ธ ์‚ฌ์–‘, `accounts.last_balance`/`last_synced_at` +- S-2 ์ธก์ • ๋ฐฉ์‹: ํ‘œ๋ณธ ์ˆ˜๊ธฐ ์ž์‚ฐยท๋ถ€์ฑ„๋กœ ์ˆœ์ž์‚ฐ ์‚ฐ์‹ ๊ฒฐ๊ณผ๋ฅผ ์†๊ฒ€์ฆ๊ณผ ๋น„๊ต / ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜: `manual_assets` ๋ชจ๋ธ ์‚ฌ์–‘, `liquidity_bucket` ๋ถ„๋ฅ˜ ๊ทœ์น™, dcy-005 ์ˆœ์ž์‚ฐ ์ •์˜ +- S-3 ์ธก์ • ๋ฐฉ์‹: baseline ๋ฌธ์„œ diff์—์„œ `asset_snapshots`/`manual_assets`ยท์Šค๋ƒ…์ƒท/์‹œ๊ณ„์—ด/์กฐํšŒ์„ฑ MCP ํ๋ฆ„ยทdcy-005 ์ •ํ•ฉ์„ฑ ๋ฐ˜์˜ ์—ฌ๋ถ€ ํ™•์ธ / ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜: `docs/data-model.md`, `docs/project-structure.md`, `docs/runtime-flows.md` + +# 8. REQ๋กœ ๋„˜๊ธฐ๊ธฐ ์ „ ํ™•์ธ ์ฒดํฌ +REQ ์ดˆ์•ˆ ์ž‘์„ฑ ๋‹จ๊ณ„๋กœ ๋„˜๊ธฐ๊ธฐ ์ „์— ๋ฌด์—‡์ด ํ™•์ •๋˜์–ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ๋ฏธ์™„๋ฃŒ ํ•ญ๋ชฉ์€ ํ›„์† ๊ฒฐ์ •์ด ํ•„์š”ํ•จ์„ ๋œปํ•œ๋‹ค. + +- [x] ๋ฒ”์œ„(In Scope/Out of Scope) ํ™•์ • + - ๊ธฐ์ค€: `asset_snapshots`/`manual_assets` ๋ณ„๋„ ํ…Œ์ด๋ธ”, ์กฐํšŒ์„ฑ MCP ํฌํ•จ ๋ฒ”์œ„, ์™ธ๋ถ€ ์‹œ์„ธ ์—ฐ๋™ ์ œ์™ธ๋ฅผ ์ด๋ฒˆ confirm์—์„œ ํ™•์ •ํ–ˆ๋‹ค. +- [x] ์„ฑ๊ณต ์ง€ํ‘œ์˜ ์ธก์ • ๋ฐฉ์‹/๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ ํ™•์ • + - ๊ธฐ์ค€: S-1~S-3 ๊ฒ€์ˆ˜ ๋ฐฉ์‹๊ณผ ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜๊ฐ€ REQ ์ดˆ์•ˆ์œผ๋กœ ๋‚ด๋ฆด ์ˆ˜ ์žˆ์„ ์ •๋„๋กœ ์ •์˜๋๋‹ค. +- [x] High ์˜ํ–ฅ ๋ฆฌ์Šคํฌ ์šฐ์„ ์ˆœ์œ„ ๋ฐ ํ•ด์†Œ ์ฑ…์ž„์ž ์ง€์ • + - ๊ธฐ์ค€: R-1์˜ ์ •์ฑ… ๋‹ต์€ ์ด๋ฒˆ confirm์—์„œ ํ™•์ •ํ–ˆ๊ณ , ๊ตฌํ˜„/๊ฒ€์ˆ˜ ์ฑ…์ž„์ž ์ง€์ •์€ REQยทbatch planning ๋‹จ๊ณ„์—์„œ ๋ฐฐ์ •ํ•œ๋‹ค. +- [x] ๋ชจ๋“  ์˜คํ”ˆ ์งˆ๋ฌธ(OQ) `Open` ์ƒํƒœ ์—†์Œ ํ™•์ธ + - ๊ธฐ์ค€: OQ-1~OQ-3์„ ๋ชจ๋‘ Answered ์ฒ˜๋ฆฌํ–ˆ๋‹ค. +- [x] Freeze ํ™•์ • ๋‹ด๋‹น์ž(CONFIRMED_BY) ์ง€์ • + - ๊ธฐ์ค€: ์‚ฌ์šฉ์ž๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค ๋‹จ์ผํ™”ยท์Šค๋ƒ…์ƒท ์ •์ฑ…ยท์กฐํšŒ์„ฑ MCP ํฌํ•จ ๊ถŒ์žฅ์•ˆ์„ ์Šน์ธํ•˜๊ณ  `dcy-008 confirm`์„ ์ง€์‹œํ–ˆ๋‹ค. + +# 9. ํŒŒ์ผ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ +์ด Discovery ๋ฌธ์„œ ์ƒ์„ฑ/๊ฐฑ์‹  ๊ฒฐ๊ณผ์™€ ์ฐธ์กฐ ๋ฌธ์„œ๋ฅผ ๊ธฐ๋กํ•œ๋‹ค. ์ดํ›„ ์ถ”์ ๊ณผ ๋ฆฌ๋ทฐ ์‹œ ๊ทผ๊ฑฐ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. + +- ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ: ์ƒ์„ฑ +- ์ƒ์„ฑ ๊ฒฝ๋กœ: docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md +- ์ฐธ์กฐ ๋ฌธ์„œ: + - docs/discovery/dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md (์ž์‚ฐ ์œ ๋™์„ฑยท์ˆœ์ž์‚ฐ ํ‘œ์‹œ ๋ฒ”์œ„ ๋ฐ ๋ณธ Discovery์˜ ํ›„์† ๋ณด๋ฅ˜ ์ถœ์ฒ˜) + - docs/data-model.md (LedgerAccount `last_balance`/`liquidity_bucket`, Current Gaps์˜ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ๋ณด๋ฅ˜ ๊ทผ๊ฑฐ; `asset_snapshots`/`manual_assets` ๋„์ž… ๋Œ€์ƒ) + - docs/runtime-flows.md, docs/project-structure.md, docs/interface-contract.md (์Šค๋ƒ…์ƒท ์ƒ์„ฑยท์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ดยท์กฐํšŒ์„ฑ MCP ํ๋ฆ„/์‘๋‹ต ๊ณ„์•ฝ ๊ฐฑ์‹  ๋Œ€์ƒ; stage-pilot/templates/runtime-flows.md, stage-pilot/templates/project-structure.md๋ฅผ ๋…ผ๋ฆฌ ํ…œํ”Œ๋ฆฟ ์ฐธ์กฐ๋กœ ์‚ฌ์šฉ) + - dashboard/app.py `get_asset_liquidity_report`, jarca_ledger_mcp/service.py, jarca_ledger_mcp/db.py (ํ˜„์žฌ ์ž์‚ฐ/์ˆœ์ž์‚ฐ ์ง‘๊ณ„ ๊ตฌํ˜„ ๊ทผ๊ฑฐ) + +# 10. ์‚ฌ์šฉ์ž ๊ฒฐ์ • ํ•„์š” ํ•ญ๋ชฉ ์š”์•ฝ +์‚ฌ์šฉ์ž๋‚˜ ์Šน์ธ์ž๊ฐ€ ๊ฒฐ์ •ํ•ด์•ผ ํ•˜๋Š” ํ•ญ๋ชฉ๋งŒ ๋ชจ์•„ ์ •๋ฆฌํ•œ๋‹ค. ๊ฒฐ์ •, ํ™•์ธ, ๋ฐ์ดํ„ฐ ์š”์ฒญ์„ ๊ตฌ๋ถ„ํ•ด์„œ ์ ๋Š”๋‹ค. + +## DECIDE +- DECIDE_AUTHORITY_SOURCE: โ‘  ๊ณ„์ขŒ `last_balance` ์œ ์ง€ + ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ณด๊ฐ•์œผ๋กœ ํ™•์ •. +- DECIDE_NEW_TABLES: `asset_snapshots`, `manual_assets` ๋ณ„๋„ ํ…Œ์ด๋ธ” ๋„์ž…์œผ๋กœ ํ™•์ •. + +## CONFIRM +- CONFIRM_SNAPSHOT_POLICY: ์›”๋ง ์ˆ˜๋™ ์ƒ์„ฑ์œผ๋กœ ํ™•์ •. ์Šค๋ƒ…์ƒท ๊ตฌ์กฐ๋Š” ๊ณ„์ขŒ์™€ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์„ ๋ชจ๋‘ ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ ๋‹จ์œ„๋ฅผ ์ˆ˜์šฉํ•œ๋‹ค. +- CONFIRM_NETWORTH_FORMULA: dcy-005 ์ •์˜(์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ, ๋ฏธํ‰๊ฐ€ ์ œ์™ธ)๋ฅผ ๊ทธ๋Œ€๋กœ ๊ณ„์Šนํ•ด ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ์œผ๋กœ ํ™•์žฅํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ํ™•์ •. +- CONFIRM_MCP_SNAPSHOT_READ_SCOPE: ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ๋ฅผ ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„์— ํฌํ•จํ•˜๊ณ , ์Šค๋ƒ…์ƒท ์ƒ์„ฑ/์ˆ˜๊ธฐ ์ž์‚ฐ CRUD ๊ฐ™์€ mutation MCP๋Š” ํ›„์† ๋ฒ”์œ„๋กœ ํ™•์ •. + +## DATA +- DATA_MANUAL_ASSET_INVENTORY: ์‹ค์ œ ์ดˆ๊ธฐ ์ˆ˜๊ธฐ ๋“ฑ๋ก ๋ชฉ๋ก(๋ถ€๋™์‚ฐยท์˜ˆ์ ๊ธˆยท๋Œ€์ถœยท๊ธฐํƒ€)๊ณผ ํ‰๊ฐ€๊ฐ’ยท๊ธฐ์ค€์ผยท์ถœ์ฒ˜๋Š” REQ/delivery ๋‹จ๊ณ„ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋กœ ์ˆ˜์ง‘ํ•œ๋‹ค. Discovery confirm์˜ ์ฐจ๋‹จ ํ•ญ๋ชฉ์œผ๋กœ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. + +# 11. Discovery Freeze +REQ ์ดˆ์•ˆ ์ž‘์„ฑ ์ธ๊ณ„ ์‹œ์ ์˜ ํ™•์ • ํ”Œ๋ž˜๊ทธ์™€ ์„น์…˜ ์ฐธ์กฐ๋งŒ ๊ธฐ๋กํ•œ๋‹ค. ๋‚ด์šฉ ์žฌ์ž‘์„ฑ ์—†์ด ์›๋ณธ ์„น์…˜์„ ๋งํฌ๋กœ ๋Œ€์‹ ํ•œ๋‹ค. + +## ์„น์…˜ ์ฐธ์กฐ +- ๋ฒ”์œ„: [# 5. ์š”๊ตฌ์‚ฌํ•ญ ๋ชฉ๋ก > ๋ฒ”์œ„ ๊ฒฝ๊ณ„](#5-์š”๊ตฌ์‚ฌํ•ญ-๋ชฉ๋ก) +- ๋ฆฌ์Šคํฌ/์˜คํ”ˆ ์งˆ๋ฌธ: [# 6. ๋ฆฌ์Šคํฌ/๊ฐ€์ • ๋ชฉ๋ก](#6-๋ฆฌ์Šคํฌ๊ฐ€์ •-๋ชฉ๋ก) +- ์„ฑ๊ณต ๊ธฐ์ค€: [# 7. ์ดˆ๊ธฐ ์„ฑ๊ณต ๊ธฐ์ค€](#7-์ดˆ๊ธฐ-์„ฑ๊ณต-๊ธฐ์ค€) +- ์ธ๊ณ„ ์ „ ํ™•์ธ: [# 8. REQ๋กœ ๋„˜๊ธฐ๊ธฐ ์ „ ํ™•์ธ ์ฒดํฌ](#8-req๋กœ-๋„˜๊ธฐ๊ธฐ-์ „-ํ™•์ธ-์ฒดํฌ) +- ์‚ฌ์šฉ์ž ๊ฒฐ์ • ํ•„์š”: [# 10. ์‚ฌ์šฉ์ž ๊ฒฐ์ • ํ•„์š” ํ•ญ๋ชฉ ์š”์•ฝ](#10-์‚ฌ์šฉ์ž-๊ฒฐ์ •-ํ•„์š”-ํ•ญ๋ชฉ-์š”์•ฝ) + +## Freeze ํ”Œ๋ž˜๊ทธ +- Handoff Decision: ์Šน์ธ +- Handoff Rationale: ๊ถŒ์žฅ์•ˆ ๊ธฐ์ค€์œผ๋กœ ๊ถŒ์œ„ ์†Œ์Šค, ์‹ ๊ทœ ํ…Œ์ด๋ธ”, ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ •์ฑ…, ์กฐํšŒ์„ฑ MCP ํฌํ•จ ๋ฒ”์œ„๋ฅผ ํ™•์ •ํ–ˆ๋‹ค. ๋‚จ์€ ์ˆ˜๊ธฐ ์ž์‚ฐ ์‹ค์ œ ๋ชฉ๋ก์€ REQ/delivery ๋‹จ๊ณ„ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์ด๋ฉฐ REQ ์ดˆ์•ˆ ์ž‘์„ฑ์„ ๋ง‰์ง€ ์•Š๋Š”๋‹ค. +- Ready for REQ Drafting: true +- Confirmed By: ์ง„ํ™˜ ๊ณ  +- Confirmed At (KST): 2026-06-30 21:15 KST diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 78f1b2b..6a96ed3 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -13,3 +13,4 @@ | dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow | 20260629 | Jarca Ledger ๋Œ€์‹œ๋ณด๋“œ ์šด์˜ ํ—ˆ๋ธŒ ์‚ฌ์–‘ ์ •๋ฆฝ (์ด๋ฒˆ ๋‹ฌ ํ˜„๊ธˆํ๋ฆ„ + ์ž์‚ฐ ์œ ๋™์„ฑ + ์šด์˜ ๋ฏธ์ฒ˜๋ฆฌ) | [Discovery](./dcy-005_20260629_dashboard-operations-hub-liquidity-monthly-cashflow.md) | confirmed | | dcy-006_20260630_pytest-apptest-smoke-ci-automation | 20260630 | pytest/AppTest/Smoke CI ์ž๋™ํ™” | [Discovery](./dcy-006_20260630_pytest-apptest-smoke-ci-automation.md) | confirmed | | dcy-007_20260630_mcp-response-contract-ci-contract-versioning | 20260630 | MCP response contract / CI contract versioning ์ •๋ฆฝ | [Discovery](./dcy-007_20260630_mcp-response-contract-ci-contract-versioning.md) | confirmed | +| dcy-008_20260630_asset-snapshot-manual-valuation-system | 20260630 | ์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ ํ…Œ์ด๋ธ”/ํ‰๊ฐ€ ์ฒด๊ณ„ ์ •๋ฆฝ | [Discovery](./dcy-008_20260630_asset-snapshot-manual-valuation-system.md) | confirmed | diff --git a/docs/interface-contract.md b/docs/interface-contract.md index a37e75d..c30a05f 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -5,7 +5,7 @@ - Status: governed - Owner: Jarca / project maintainer - Last Updated (KST): 2026-06-30 -- Source Discovery / Batch: bootstrap-baseline, dcy-007, BAT-007 +- Source Discovery / Batch: bootstrap-baseline, dcy-007, BAT-007, BAT-008 ## Project Summary - Jarca ๊ฐ€๊ณ„๋ถ€ ์ „์šฉ MCP ์„œ๋ฒ„์™€ Streamlit ๋Œ€์‹œ๋ณด๋“œ๋กœ ๊ณ„์ขŒ/์นด๋“œ ๊ฑฐ๋ž˜, ์˜ˆ์‚ฐ, ๋ถ„๋ฅ˜ ๊ทœ์น™, import, ํ˜„๊ธˆํ๋ฆ„ ๋ถ„์„์„ SQLite์— ์ €์žฅํ•˜๊ณ  ์šด์˜ํ•œ๋‹ค. @@ -26,6 +26,7 @@ - Accounts: `create_ledger_account`, `list_ledger_accounts`, `update_ledger_account`; account payloads include additive `liquidity_bucket` for asset-liquidity reporting. - Transactions: `create_ledger_transaction`, `list_ledger_transactions`, `update_ledger_transaction`, `delete_ledger_transaction` - Budgets/reports: `create_ledger_budget`, `list_ledger_budgets`, monthly/category/fixed/budget/investable reports, `get_asset_liquidity_report` +- Asset valuation reads: `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries`; these are read-only in BAT-008 and use the same valuation policy as the dashboard. - Imports: KB Bank PDF, Toss Bank PDF, Toss Securities PDF, SeoulPay XLSX, CSV preview/import where implemented - Categories/rules: category CRUD, rule CRUD, categorization preview/apply, uncategorized summaries - Reconciliation: cross-account preview/run/review queue, self-transfer candidates/linking, cancel-original candidates/linking @@ -72,6 +73,14 @@ | --- | --- | --- | --- | --- | | 1.0 | 2026-06-30 | Initial governed baseline | dcy-007 / REQ-007 / BAT-007 | Formalizes compatibility governance without exposing runtime version fields or changing success payload shapes. | +### Asset Valuation Read Tools +- `list_manual_assets(active_only=true)` returns `success`, `manual_assets`, `count`, and `valuation_policy`. Each item exposes `name`, `valuation_amount`, `valuation_date`, `liquidity_bucket`, `source_note`, `memo`, and `authority_source='manual_assets'`. +- `get_asset_snapshot_report(basis_date='')` returns `success`, `basis_date`, `source`, `snapshot_count`, `buckets`, `total_assets`, `total_liabilities`, `net_worth`, `included_items`, `excluded_items`, and `valuation_policy`. + - If `asset_snapshots` rows exist for `basis_date`, `source='asset_snapshots'` and values come from stored snapshot rows. + - If no snapshot rows exist, `source='live_authority'` and the report reads `accounts.last_balance` plus active `manual_assets`. +- `get_net_worth_timeseries(start_date='', end_date='', months=12)` returns `success`, requested date bounds, `points`, `count`, and `valuation_policy`; each point contains `basis_date`, `total_assets`, `total_liabilities`, `net_worth`, `source`, and `item_count`. +- Contract impact: additive minor. Existing success payloads are preserved; `get_asset_liquidity_report` gains optional explanatory fields (`manual_assets`, `manual_asset_count`, `included_items`, `excluded_items`, `valuation_policy`) while preserving existing keys. + ## Interface: CI Required-Check Contract - Required-check source of truth in code: `.github/workflows/ci.yml` job names are stable and must be mirrored by repository branch protection settings. - Required-check default set for PR/push merge blocking: `pytest-regression`, `dashboard-app-test`, `dashboard-http-smoke`, `db-safety-smoke`, `contract-regression`, `lint`. @@ -111,7 +120,8 @@ - Rule page should support create/edit/delete and condition fields `amount_min`, `amount_max`, `day_of_month`. - Dialog-based triage actions should avoid losing context where possible. - Account page should display/edit `statement_status` and show a minimal manual balance reconciliation report without adding an automatic schedule or dedicated workbench. -- Account page should display/edit `liquidity_bucket`; overview uses it with `last_balance` for asset liquidity/composition/debt/net-worth sections. +- Account page should display/edit `liquidity_bucket`; overview uses the shared service valuation report for asset liquidity/composition/debt/net-worth sections. +- Overview asset/net-worth display must keep the REQ-005 account-only fallback when `manual_assets`/`asset_snapshots` are empty, and may additionally show manual asset rows when present. - Overview top priority is KST current-month actual income/expense/net cashflow/savings rate based on `transaction_date` and `cashflow_kind`. - UI/dashboard-impacting batches require browser screenshot/snapshot evidence from synthetic/validation DBs. Non-UI batches may omit browser evidence, and docs-only batches do not require it. @@ -129,7 +139,7 @@ - Broad mutation workflows should capture metadata-only DB status and backup evidence before/after risky operations. - Restore execution is limited to script/runbook workflows and explicit approval, not MCP tools. - Manual balance reconciliation is preview/report-first. `REVIEW_NEEDED` suggests manual decision values (`๊ณ„์ขŒ์ด๋™`, `์‹ค์ง€์ถœ`, `์‹ค์ˆ˜์ž…`, `ํˆฌ์ž์ด๋™`, `๋ถ„์„์ œ์™ธ`, `ํ™•์ธํ•„์š”`) but does not auto-confirm mutations. -- Asset liquidity reporting is account-based: `liquidity_bucket='loan'` is debt/net-worth only, and missing `last_balance` rows must not be inferred from transaction totals. +- Asset liquidity reporting uses the shared valuation policy: account-backed values from `accounts.last_balance`, non-transaction assets/liabilities from `manual_assets`, `loan` as debt/net-worth only, and missing values excluded with explanation rather than inferred from transaction totals. - Browser evidence screenshots/snapshots must use synthetic or validation DBs only and must not include real account identifiers, source financial filenames, passwords, or raw payloads. - Financial data and passwords are sensitive runtime inputs and should not be committed. @@ -137,6 +147,7 @@ - Formal MCP response contract versioning is defined for version `1.0`; full tool-surface snapshot coverage remains a future expansion beyond the representative BAT-007 scope. - Browser-level UI evidence criteria are captured in REQ-004 and remain local evidence rather than a required CI gate unless a future REQ changes that policy. - CI tests/lint required-check contract is defined for the six stable jobs listed above; actual branch protection enforcement remains an operator-managed repository setting. +- BAT-008 adds read-only asset valuation MCP tools. Mutation MCP for manual asset CRUD or snapshot creation remains out of scope/future work. ## Change Log - 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation. @@ -145,3 +156,4 @@ - 2026-06-29 KST: REQ-004/BAT-004 ๊ธฐ์ค€์œผ๋กœ Streamlit browser evidence ์‚ฐ์ถœ๋ฌผ๊ณผ UI evidence gate ๊ทœ์น™ ์ถ”๊ฐ€. - 2026-06-29 KST: REQ-005/BAT-005 ๊ธฐ์ค€์œผ๋กœ account `liquidity_bucket`, `get_asset_liquidity_report`, dashboard operations hub contract ์ถ”๊ฐ€. - 2026-06-30 KST: REQ-007/REQ-008/BAT-007 ๊ธฐ์ค€์œผ๋กœ MCP response contract `1.0` governance, representative contract-regression scope, `ruff` lint, six-job required-check contract๋ฅผ ์ถ”๊ฐ€. +- 2026-06-30 KST: REQ-009/REQ-010/BAT-008 ๊ธฐ์ค€์œผ๋กœ `list_manual_assets`, `get_asset_snapshot_report`, `get_net_worth_timeseries` read MCP contract์™€ dashboard valuation alignment ๊ทœ์น™์„ ์ถ”๊ฐ€. diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 2c734ad..8ea266e 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -5,7 +5,7 @@ - Status: draft - Owner: Jarca / project maintainer - Last Updated (KST): 2026-06-30 -- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007 +- Source Discovery / Batch: bootstrap-baseline, BAT-006, BAT-007, BAT-008 - Primary Runtime: mixed ## Project Summary @@ -48,9 +48,10 @@ ## Flow: `Dashboard operations hub overview` 1. Operator opens the `์š”์•ฝ` page. 2. Dashboard resolves the KST current month and calls `get_monthly_cashflow_report(month)` using `transactions.transaction_date` and `ledger_categories.cashflow_kind` semantics. -3. Dashboard calls `get_asset_liquidity_report`, which groups active accounts by `accounts.liquidity_bucket` and uses only `accounts.last_balance` for asset/liability values. -4. Dashboard renders sections in REQ-005 priority order: this-month actual income/expense/net/savings rate, asset liquidity, asset composition, budget/spend, operational warnings. -5. Missing `last_balance` account rows are excluded from asset totals and surfaced as warnings; transaction sums are not used to estimate non-transaction assets. +3. Dashboard calls `get_asset_liquidity_report`, which uses the shared valuation helper: account-backed assets from `accounts.last_balance` plus active `manual_assets` rows when present. +4. If `manual_assets` and `asset_snapshots` are empty, the report keeps the REQ-005 account-only fallback and preserves existing totals/keys. +5. Dashboard renders sections in REQ-005 priority order: this-month actual income/expense/net/savings rate, asset liquidity, asset composition, budget/spend, operational warnings. +6. Missing `last_balance` or `manual_assets.valuation_amount` values are excluded from asset totals and surfaced as warnings; transaction sums are not used to estimate non-transaction assets. ## Flow: `CI validation gates` 1. GitHub Actions starts `.github/workflows/ci.yml` on `pull_request`, `push` to `main`, or `workflow_dispatch`. @@ -63,6 +64,14 @@ 8. Any required job failure fails the workflow. Stable required-check job names are `pytest-regression`, `dashboard-app-test`, `dashboard-http-smoke`, `db-safety-smoke`, `contract-regression`, and `lint`; actual branch protection toggle enforcement is configured outside the repository. 9. CI logs are status-oriented and synthetic-data-only; they must not contain real account identifiers, raw payloads, statement filenames, balances from the operating DB, or passwords. +## Flow: `Asset snapshot and net-worth read model` +1. Month-end manual close records item-level rows in `asset_snapshots` from the live authority sources: account `last_balance` rows and `manual_assets` valuations. BAT-008 stores the schema and service helper but does not expose snapshot creation as an MCP mutation. +2. `get_asset_snapshot_report(basis_date)` first checks for snapshot rows on the requested basis date. +3. If snapshot rows exist, the report computes bucket totals, total assets, total liabilities, and net worth from the stored rows. +4. If no snapshot rows exist, the report falls back to live authority sources (`accounts.last_balance` + active `manual_assets`) and marks `source='live_authority'`. +5. `get_net_worth_timeseries(start_date, end_date, months)` reads distinct stored snapshot basis dates and applies the same asset-buckets-minus-loan formula to every point. +6. `list_manual_assets` exposes current manual asset/liability rows for read-only MCP consumers. Manual asset CRUD and snapshot creation MCP tools remain future scope. + ## Flow: `Import preview and import` 1. Operator chooses or supplies a supported source file: KB Bank PDF, Toss Bank PDF, Toss Securities PDF, SeoulPay XLSX, or CSV. 2. Preview function parses the file and reports counts, sample rows, duplicate status, and classification preview. @@ -121,7 +130,8 @@ - CI gates must use synthetic/temp DBs only. The default operating DB path (`~/.local/share/jarca/ledger.sqlite3`) is forbidden for AppTest, HTTP smoke, and DB safety smoke wrappers. - Contract regression must use synthetic/temp DBs only and must not expose runtime contract-version fields in MCP success payloads unless a future approved REQ changes the policy. - Branch protection required-check drift should be audited against the six stable workflow job names documented in `docs/interface-contract.md`; changing a job name requires an explicit REQ/BAT note because it can break external repository settings. -- Dashboard asset liquidity calculations must use `accounts.liquidity_bucket` and `accounts.last_balance`; `loan` is excluded from the asset composition denominator and reflected as debt/net-worth only. +- Dashboard and MCP asset valuation reports must use the shared service valuation policy: `accounts.last_balance` plus `manual_assets`, asset buckets minus loan, and missing valuation exclusion. +- Snapshot read tools are read-only in BAT-008. Snapshot creation and manual asset CRUD MCP mutation tools require a future REQ. - Sensitive passwords are runtime inputs and must not be committed. ## Current Gaps / Planned Changes diff --git a/docs/srs/Data/req-009_asset-snapshot-manual-assets-valuation-model.md b/docs/srs/Data/req-009_asset-snapshot-manual-assets-valuation-model.md new file mode 100644 index 0000000..953d87e --- /dev/null +++ b/docs/srs/Data/req-009_asset-snapshot-manual-assets-valuation-model.md @@ -0,0 +1,99 @@ +# REQ-009 Asset snapshot/manual assets valuation model + +- Status: Implemented +- Type: Data +- Priority: High +- Owner: ๋ ›์ €์žฅ์ธ +- Source Discovery: `docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md` +- Batch: BAT-008 +- Created At (KST): 2026-06-30 21:17 KST +- Approved At (KST): 2026-06-30 21:24 KST +- Implemented At (KST): 2026-06-30 22:05 KST + +## 1. ๋ฐฐ๊ฒฝ + +REQ-005๋Š” `accounts.liquidity_bucket` + `last_balance` ๊ธฐ๋ฐ˜ ์ž์‚ฐ ์œ ๋™์„ฑ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๋ฅผ ๊ตฌํ˜„ํ–ˆ์ง€๋งŒ, ์‹œ์ ๋ณ„ ์ž์‚ฐ ํ‰๊ฐ€ ์ด๋ ฅ๊ณผ ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„์˜ ๋…๋ฆฝ ๋ชจ๋ธ์€ ์˜๋„์ ์œผ๋กœ ์ œ์™ธํ–ˆ๋‹ค. dcy-008 confirmed Discovery์— ๋”ฐ๋ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์ด๋ ฅ๊ณผ ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ชจ๋ธ์„ ๋ณ„๋„ ํ…Œ์ด๋ธ”๋กœ ๋„์ž…ํ•˜๊ณ , ๊ณ„์ขŒ `last_balance` + ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ณด๊ฐ•์„ ๋‹จ์ผ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์ •๋ฆฝํ•ด ๊ธฐ์ค€์ผ๋ณ„ ์ˆœ์ž์‚ฐ ์žฌํ˜„๊ณผ ํ›„์† MCP ์กฐํšŒ ๊ธฐ๋Šฅ์˜ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์„ ๋งˆ๋ จํ•ด์•ผ ํ•œ๋‹ค. + +## 2. Intent + +์ž์‚ฐ ํ‰๊ฐ€ ๋ชจ๋ธ์„ ๊ณ„์ขŒ ํ•œ ์ค„์˜ ์ตœ์‹  ๊ฐ’์—์„œ ๋ฒ—์–ด๋‚˜, ๊ธฐ์ค€์ผ๋ณ„ ์Šค๋ƒ…์ƒท๊ณผ ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„์˜ ๋ช…์‹œ์  ์—”ํ‹ฐํ‹ฐ๋กœ ๋ถ„๋ฆฌํ•œ๋‹ค. ์ด REQ์˜ ๋ชฉ์ ์€ `asset_snapshots`/`manual_assets` ๋ฐ์ดํ„ฐ ๋ชจ๋ธ, ์ˆœ์ž์‚ฐ ์‚ฐ์‹, ๊ถŒ์œ„ ์†Œ์Šค ์šฐ์„ ์ˆœ์œ„, ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ •์ฑ…์„ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ €์žฅ ๊ณ„์ธต ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ๊ณ ์ •ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. + +## 3. ๋ฒ”์œ„ + +### In Scope +- `asset_snapshots`์™€ `manual_assets`๋ฅผ ๋ณ„๋„ additive/idempotent ํ…Œ์ด๋ธ”๋กœ ๋„์ž…ํ•œ๋‹ค. +- ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„์˜ ํ•„์ˆ˜ ์†์„ฑ(์ด๋ฆ„, ํ‰๊ฐ€๊ฐ’, ๊ธฐ์ค€์ผ, `liquidity_bucket`, ์ถœ์ฒ˜ ๋ฉ”๋ชจ)์„ ์ •์˜ํ•œ๋‹ค. +- ์ž์‚ฐ ์Šค๋ƒ…์ƒท์˜ ํ•„์ˆ˜ ์†์„ฑ(๊ธฐ์ค€์ผ, ํ‰๊ฐ€ ๋Œ€์ƒ, ํ‰๊ฐ€ ๊ธˆ์•ก, `liquidity_bucket`, ์ถœ์ฒ˜/๋ฉ”๋ชจ)์„ ์ •์˜ํ•œ๋‹ค. +- ์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ, ๋ฏธํ‰๊ฐ€ ์ œ์™ธ ๊ทœ์น™์„ ์ €์žฅ/์ง‘๊ณ„ ๊ณ„์ธต ๊ธฐ์ค€์œผ๋กœ ๊ณ ์ •ํ•œ๋‹ค. +- ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์ •์ฑ…๊ณผ ํ•ญ๋ชฉ ๋‹จ์œ„ ์ˆ˜์šฉ ๊ตฌ์กฐ๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. +- `docs/data-model.md`์— ์œ„ ๋ชจ๋ธ๊ณผ ๊ถŒ์œ„ ์†Œ์Šค ๊ทœ์น™์„ ๋ฐ˜์˜ํ•œ๋‹ค. + +### Out of Scope +- ์™ธ๋ถ€ ์‹œ์„ธ/๋ถ€๋™์‚ฐ ์ž๋™ ํ‰๊ฐ€ ์—ฐ๋™. +- ์‹ค์‹œ๊ฐ„ ์ž๋™ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์Šค์ผ€์ค„๋Ÿฌ. +- ์ˆ˜์ต๋ฅ /์„ธ๊ธˆ/๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ๊ณ„์‚ฐ. +- ์กฐํšŒ์„ฑ MCP ์‘๋‹ต shape ์ •์˜ ์ž์ฒด(๋ณ„๋„ Interface REQ์—์„œ ๋‹ค๋ฃธ). + +## 4. Requirement + +### FR-1. Manual assets data model +- `manual_assets`๋Š” ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋ฅผ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค. +- ๊ฐ ํ•ญ๋ชฉ์€ ์ตœ์†Œํ•œ ์ด๋ฆ„, ์ž์‚ฐ/๋ถ€์ฑ„ ๊ตฌ๋ถ„ ๋˜๋Š” `liquidity_bucket`, ํ‰๊ฐ€๊ฐ’, ๊ธฐ์ค€์ผ, ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค. +- ํ—ˆ์šฉ `liquidity_bucket` ๊ฐ’์€ `cash`, `investment`, `deposit`, `real_estate`, `loan`, `other`๋‹ค. +- ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ธฐ๋ฐ˜ ํ‰๊ฐ€๊ฐ’์ด ๊ถŒ์œ„ ์†Œ์Šค๊ฐ€ ๋˜๋Š” ํ•ญ๋ชฉ์€ ๊ณ„์ขŒ ๋ชจ๋ธ๊ณผ ํ˜ผํ•ฉ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๋ณ„๋„ ์—”ํ‹ฐํ‹ฐ๋กœ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค. + +### FR-2. Asset snapshots history model +- `asset_snapshots`๋Š” ๊ธฐ์ค€์ผ ์‹œ์ ์˜ ์ž์‚ฐ ํ‰๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๋ˆ„์  ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค. +- ์Šค๋ƒ…์ƒท์€ ๊ณ„์ขŒ์™€ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์„ ๋ชจ๋‘ ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ํ•ญ๋ชฉ ๋‹จ์œ„ ๊ตฌ์กฐ๋ฅผ ์ˆ˜์šฉํ•ด์•ผ ํ•œ๋‹ค. +- ๊ธฐ์กด ์Šค๋ƒ…์ƒท์€ ํŒŒ๊ดด์ ์œผ๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ์‹ ๊ทœ ํ–‰์œผ๋กœ ๋ˆ„์ ํ•ด์•ผ ํ•œ๋‹ค. +- ๊ฐ ํ–‰์€ ์ตœ์†Œํ•œ ๊ธฐ์ค€์ผ, ํ‰๊ฐ€ ๋Œ€์ƒ ์‹๋ณ„์ž, ํ‰๊ฐ€ ๊ธˆ์•ก, `liquidity_bucket`, ํ‰๊ฐ€ ์ถœ์ฒ˜/๋ฉ”๋ชจ๋ฅผ ๋ณด์กดํ•ด์•ผ ํ•œ๋‹ค. + +### FR-3. Valuation authority source rule +- ๊ณ„์ขŒ ๊ธฐ๋ฐ˜ ์ž์‚ฐ์€ ๊ธฐ์กด `accounts.last_balance`๋ฅผ ๊ธฐ๋ณธ ํ‰๊ฐ€ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. +- ๊ฑฐ๋ž˜๋กœ ์‚ฐ์ถœ๋˜์ง€ ์•Š๋Š” ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” `manual_assets`๋ฅผ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. +- ์Šค๋ƒ…์ƒท์€ ์œ„ ๊ถŒ์œ„ ์†Œ์Šค๋ฅผ ๊ธฐ์ค€์ผ ์‹œ์ ์— ๊ณ ์ •ํ•œ ๊ธฐ๋ก์ธต์ด์–ด์•ผ ํ•˜๋ฉฐ, ์‹ค์‹œ๊ฐ„ ๊ถŒ์œ„ ์†Œ์Šค๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๋ณ„๋„ 1์ฐจ ์ž…๋ ฅ์›์ด ๋˜์–ด์„œ๋Š” ์•ˆ ๋œ๋‹ค. +- ๋™์ผ ์ž์‚ฐ์ด ์ค‘๋ณต ์ง‘๊ณ„๋˜์ง€ ์•Š๋„๋ก ๊ณ„์ขŒ ํ•ญ๋ชฉ๊ณผ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ•ญ๋ชฉ์˜ ์ฑ…์ž„ ๊ฒฝ๊ณ„๋ฅผ ์ •์˜ํ•ด์•ผ ํ•œ๋‹ค. + +### FR-4. Net worth formula and exclusion policy +- ์ˆœ์ž์‚ฐ์€ ์ž์‚ฐ์„ฑ ๋ฒ„ํ‚ท(`cash`, `investment`, `deposit`, `real_estate`, `other`) ํ•ฉ๊ณ„์—์„œ `loan` ๋ถ€์ฑ„๋ฅผ ์ฐจ๊ฐํ•ด ๊ณ„์‚ฐํ•ด์•ผ ํ•œ๋‹ค. +- ํ‰๊ฐ€๊ฐ’์ด ์—†๋Š” ํ•ญ๋ชฉ์€ ์ž„์˜ ๊ฑฐ๋ž˜ ํ•ฉ๊ณ„๋กœ ์ถ”์ •ํ•˜์ง€ ์•Š๊ณ  ์ œ์™ธํ•ด์•ผ ํ•œ๋‹ค. +- ์ œ์™ธ๋œ ํ•ญ๋ชฉ์€ ํ›„์† ํ™”๋ฉด/์‘๋‹ต ๊ณ„์ธต์—์„œ ์„ค๋ช… ๊ฐ€๋Šฅํ•˜๋„๋ก ์ƒํƒœ ๋˜๋Š” ๊ทผ๊ฑฐ๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. + +### FR-5. Manual month-end snapshot policy +- ์ดˆ๊ธฐ ์šด์˜ ์ •์ฑ…์€ ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ์ด์–ด์•ผ ํ•œ๋‹ค. +- ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ํ๋ฆ„์€ ์›”๋ง ๋งˆ๊ฐ ์‹œ์ ์— ๊ณ„์ขŒ `last_balance`์™€ ์ˆ˜๊ธฐ ์ž์‚ฐ ํ‰๊ฐ€๊ฐ’์„ ํ•จ๊ป˜ ๊ณ ์ •ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. +- ์ž๋™ ์ƒ์„ฑ/์Šค์ผ€์ค„๋ง์€ ํ›„์† ๋ฒ”์œ„๋กœ ๋‚จ๊ธด๋‹ค. + +### FR-6. Baseline documentation alignment +- `docs/data-model.md`๋Š” `asset_snapshots`, `manual_assets`, ๊ถŒ์œ„ ์†Œ์Šค ์šฐ์„ ์ˆœ์œ„, ์ˆœ์ž์‚ฐ ์‚ฐ์‹์„ ๋ฐ˜์˜ํ•ด์•ผ ํ•œ๋‹ค. +- ํ•„์š” ์‹œ `docs/project-structure.md`์™€ `docs/runtime-flows.md`์— ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ฑ…์ž„๊ณผ ์›”๋ง ์Šค๋ƒ…์ƒท ํ๋ฆ„์„ ์ฐธ์กฐ ์ˆ˜์ค€์œผ๋กœ ๋ฐ˜์˜ํ•ด์•ผ ํ•œ๋‹ค. + +## 5. Acceptance Criteria + +- AC-1: ์‹ ๊ทœ DB/๊ธฐ์กด DB ๋ชจ๋‘์—์„œ `asset_snapshots`, `manual_assets`๊ฐ€ additive/idempotent migration์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค. +- AC-2: `manual_assets`๋Š” ์ด๋ฆ„, ํ‰๊ฐ€๊ฐ’, ๊ธฐ์ค€์ผ, `liquidity_bucket`, ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. +- AC-3: `asset_snapshots`๋Š” ๊ธฐ์ค€์ผ, ํ‰๊ฐ€ ๋Œ€์ƒ, ํ‰๊ฐ€ ๊ธˆ์•ก, `liquidity_bucket`, ์ถœ์ฒ˜/๋ฉ”๋ชจ๋ฅผ ๋ˆ„์  ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. +- AC-4: ๊ณ„์ขŒ ๊ธฐ๋ฐ˜ ์ž์‚ฐ์€ `last_balance`, ๋น„๊ฑฐ๋ž˜ ์ž์‚ฐ/๋ถ€์ฑ„๋Š” `manual_assets`๋ฅผ ๊ถŒ์œ„ ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ทœ์น™์ด ๊ตฌํ˜„/๋ฌธ์„œ์— ๋ฐ˜์˜๋œ๋‹ค. +- AC-5: ์ˆœ์ž์‚ฐ ๊ณ„์‚ฐ์ด ์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan` ์ฐจ๊ฐ, ๋ฏธํ‰๊ฐ€ ์ œ์™ธ ๊ทœ์น™์„ ๋”ฐ๋ฅธ๋‹ค. +- AC-6: ๋นˆ ์‹ ๊ทœ ํ…Œ์ด๋ธ” ์ƒํƒœ์—์„œ๋„ ๊ธฐ์กด `REQ-005` ๋Œ€์‹œ๋ณด๋“œ ํ๋ฆ„์ด ๊นจ์ง€์ง€ ์•Š๋Š”๋‹ค. +- AC-7: `docs/data-model.md`์™€ ํ•„์š” baseline ๋ฌธ์„œ๊ฐ€ ๋ณธ REQ ๋ฒ”์œ„๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. + +## 6. Impacted Area + +- `jarca_ledger_mcp/db.py` +- `jarca_ledger_mcp/service.py` +- `docs/data-model.md` +- ํ•„์š” ์‹œ `docs/project-structure.md` +- ํ•„์š” ์‹œ `docs/runtime-flows.md` + +## 7. Notes + +- ์ด REQ๋Š” ์ €์žฅ ๊ตฌ์กฐ/๊ถŒ์œ„ ์†Œ์Šค/์ˆœ์ž์‚ฐ ์‚ฐ์‹ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ฑ…์ž„์„ ๋‹ค๋ฃจ๋ฏ€๋กœ `Data` ํƒ€์ž…์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. +- ์กฐํšŒ์„ฑ MCP ์‘๋‹ต ๊ณ„์•ฝ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ๋…ธ์ถœ shape๋Š” `REQ-010`์—์„œ ๋‹ค๋ฃฌ๋‹ค. +- ์‹ค์ œ ์ดˆ๊ธฐ ์ˆ˜๊ธฐ ์ž์‚ฐ ๋ชฉ๋ก์€ ๊ตฌํ˜„ ์ „ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•ด์•ผ ํ•˜๋ฉฐ, ๋ณธ REQ๋Š” ๊ทธ ์ž…๋ ฅ ํฌ๋งท๊ณผ ์ €์žฅ ๊ทœ์น™์„ ๋‹ค๋ฃจ๋Š” ์ˆ˜์ค€์œผ๋กœ ์ œํ•œํ•œ๋‹ค. + +## 8. Change Log + +- 2026-06-30 21:17 KST: dcy-008 confirmed ๋ฒ”์œ„๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Proposed ์ดˆ์•ˆ์„ ์ƒ์„ฑ. +- 2026-06-30 21:24 KST: dcy-008 confirmed ๊ฒฐ์ •์‚ฌํ•ญ๊ณผ Acceptance Criteria๋ฅผ ์žฌ๊ฒ€ํ† ํ•ด Approved๋กœ ์ „ํ™˜. +- 2026-06-30 22:05 KST: BAT-008 ๊ตฌํ˜„ยท๊ฒ€์ฆ evidence์™€ jarcaledgerqc PASS gate๋ฅผ ๊ทผ๊ฑฐ๋กœ Implemented๋กœ ์ „ํ™˜. diff --git a/docs/srs/Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md b/docs/srs/Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md new file mode 100644 index 0000000..3a64359 --- /dev/null +++ b/docs/srs/Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md @@ -0,0 +1,92 @@ +# REQ-010 Asset snapshot/net worth read MCP and dashboard alignment + +- Status: Implemented +- Type: Interface +- Priority: High +- Owner: ๋ ›์ €์žฅ์ธ +- Source Discovery: `docs/discovery/dcy-008_20260630_asset-snapshot-manual-valuation-system.md` +- Batch: BAT-008 +- Created At (KST): 2026-06-30 21:17 KST +- Approved At (KST): 2026-06-30 21:24 KST +- Implemented At (KST): 2026-06-30 22:05 KST + +## 1. ๋ฐฐ๊ฒฝ + +์ž์‚ฐ ์Šค๋ƒ…์ƒท/์ˆ˜๊ธฐ ์ž์‚ฐ/ํ‰๊ฐ€ ์ฒด๊ณ„๊ฐ€ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋กœ๋งŒ ์กด์žฌํ•˜๋ฉด ๋Œ€์‹œ๋ณด๋“œ์™€ MCP ์†Œ๋น„์ž๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ๊ทœ์น™์œผ๋กœ ์ž์‚ฐยท์ˆœ์ž์‚ฐ์„ ํ•ด์„ํ•  ์œ„ํ—˜์ด ์žˆ๋‹ค. dcy-008 confirmed Discovery๋Š” ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ๋ฅผ ์กฐํšŒ์„ฑ MCP ๋ฒ”์œ„์— ํฌํ•จํ•˜๊ณ , dcy-005 ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ ๊ฒฝ๋กœ์™€ ๋™์ผํ•œ ์‚ฐ์‹ยท๊ถŒ์œ„ ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์š”๊ตฌํ•œ๋‹ค. + +## 2. Intent + +์ž์‚ฐ ์Šค๋ƒ…์ƒท๊ณผ ์ˆœ์ž์‚ฐ ํ‰๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๋Œ€์‹œ๋ณด๋“œ ๋‚ด๋ถ€ ๊ณ„์‚ฐ์—๋งŒ ๊ฐ€๋‘์ง€ ์•Š๊ณ , ์กฐํšŒ์„ฑ MCP์™€ ํ™”๋ฉด ํ‘œ์‹œ๊ฐ€ ๊ฐ™์€ ๊ทœ์น™ยท๊ฐ™์€ ์„ค๋ช… ํ•„๋“œยท๊ฐ™์€ ์ œ์™ธ ์ •์ฑ…์„ ๋”ฐ๋ฅด๋„๋ก ์ธํ„ฐํŽ˜์ด์Šค ๊ณ„์•ฝ์„ ์ •๋ ฌํ•œ๋‹ค. ์ด REQ์˜ ๋ชฉ์ ์€ ๊ธฐ์ค€์ผ ์กฐํšŒ, ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ์˜ ์ตœ์†Œ ์‘๋‹ต ๊ณ„์•ฝ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์ •ํ•ฉ์„ฑ ์˜๋ฌด๋ฅผ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ธํ„ฐํŽ˜์ด์Šค ์š”๊ตฌ์‚ฌํ•ญ์œผ๋กœ ๊ณ ์ •ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. + +## 3. ๋ฒ”์œ„ + +### In Scope +- ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ MCP๋ฅผ ์ •์˜ํ•œ๋‹ค. +- ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ MCP๋ฅผ ์ •์˜ํ•œ๋‹ค. +- ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ MCP๋ฅผ ์ •์˜ํ•œ๋‹ค. +- ์‘๋‹ต์— ๊ธฐ์ค€์ผ/์ง‘๊ณ„ ๊ตฌ๊ฐ„, ๊ถŒ์œ„ ์†Œ์Šค, `liquidity_bucket`, ์‚ฐ์‹ ์ ์šฉ ๊ธฐ์ค€, ํฌํ•จ/์ œ์™ธ ํ•ญ๋ชฉ ์„ค๋ช…์„ ํฌํ•จํ•œ๋‹ค. +- ๋Œ€์‹œ๋ณด๋“œ ์ž์‚ฐ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ๊ฐ€ ๋™์ผ ๊ถŒ์œ„ ์†Œ์Šค์™€ ๋™์ผ ์‚ฐ์‹์„ ์‚ฌ์šฉํ•˜๋„๋ก ์ •๋ ฌํ•œ๋‹ค. +- `docs/interface-contract.md`, ํ•„์š” ์‹œ `docs/runtime-flows.md`, `docs/project-structure.md`๋ฅผ ๊ฐฑ์‹ ํ•œ๋‹ค. + +### Out of Scope +- ์Šค๋ƒ…์ƒท ์ƒ์„ฑ mutation MCP. +- ์ˆ˜๊ธฐ ์ž์‚ฐ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ mutation MCP. +- ์™ธ๋ถ€ ์‹œ์„ธ/๋ถ€๋™์‚ฐ ์ž๋™ ํ‰๊ฐ€๋ฅผ ๋…ธ์ถœํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค. +- ํˆฌ์ž ์กฐ์–ธ/๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ์ถ”์ฒœ ์‘๋‹ต. + +## 4. Requirement + +### FR-1. Asset snapshot read MCP +- ๊ธฐ์ค€์ผ๋ณ„ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ ๊ฒฝ๋กœ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค. +- ์‘๋‹ต์€ ์ตœ์†Œํ•œ ๊ธฐ์ค€์ผ, ์ž์‚ฐ ๋ฒ„ํ‚ท๋ณ„ ๊ธˆ์•ก, ์ด๋ถ€์ฑ„, ์ˆœ์ž์‚ฐ, ํฌํ•จ/์ œ์™ธ ํ•ญ๋ชฉ ์š”์•ฝ์„ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. +- ์‘๋‹ต์€ ์–ด๋–ค ๊ถŒ์œ„ ์†Œ์Šค(`last_balance`, `manual_assets`, snapshot record)๊ฐ€ ์‚ฌ์šฉ๋๋Š”์ง€ ์ถ”์  ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค. + +### FR-2. Net worth timeseries read MCP +- ์ตœ๊ทผ N๊ฐœ์›” ๋˜๋Š” ์ง€์ • ๊ตฌ๊ฐ„ ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ ๊ฒฝ๋กœ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค. +- ๊ฐ ์‹œ์ ์€ ๋™์ผ ์‚ฐ์‹(์ž์‚ฐ์„ฑ ํ•ฉ๊ณ„ โˆ’ `loan`)๊ณผ ๋™์ผ ์ œ์™ธ ์ •์ฑ…์„ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค. +- ์‹œ๊ณ„์—ด ์‘๋‹ต์€ ๊ธฐ์ค€์ผ ๋˜๋Š” ๊ตฌ๊ฐ„ ์ •์˜๋ฅผ ๋ช…ํ™•ํžˆ ํฌํ•จํ•ด์•ผ ํ•œ๋‹ค. + +### FR-3. Manual assets read MCP +- ์ˆ˜๊ธฐ ์ž์‚ฐ/๋ถ€์ฑ„ ๋ชฉ๋ก ๋˜๋Š” ์ง‘๊ณ„ ์กฐํšŒ ๊ฒฝ๋กœ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค. +- ์‘๋‹ต์€ ํ•ญ๋ชฉ๋ช…, ํ‰๊ฐ€๊ฐ’, ๊ธฐ์ค€์ผ, `liquidity_bucket`, ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. +- ๋ฏธํ‰๊ฐ€ ๋˜๋Š” ์˜ค๋ž˜๋œ ํ‰๊ฐ€ ํ•ญ๋ชฉ์€ ํ›„์† UX/์‘๋‹ต์—์„œ ๊ฒฝ๊ณ  ๊ฐ€๋Šฅํ•˜๋„๋ก ์ƒํƒœ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. + +### FR-4. Dashboard/MCP contract alignment +- `dashboard/app.py`์˜ ์ž์‚ฐ ์œ ๋™์„ฑ/์ˆœ์ž์‚ฐ ํ‘œ์‹œ์™€ ์กฐํšŒ์„ฑ MCP ์‘๋‹ต์€ ๋™์ผ ๊ถŒ์œ„ ์†Œ์Šค ์šฐ์„ ์ˆœ์œ„์™€ ๋™์ผ ์ˆœ์ž์‚ฐ ์‚ฐ์‹์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. +- `REQ-005`์˜ ๊ธฐ์กด ํ‘œ์‹œ ๊ฒฝ๋กœ์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก, ์‹ ๊ทœ ๋ชจ๋ธ์ด ๋น„์–ด ์žˆ๋Š” ๊ฒฝ์šฐ ๊ธฐ์กด ์•ˆ์ „ ํด๋ฐฑ์„ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค. +- ๋™์ผ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•ด ๋Œ€์‹œ๋ณด๋“œ์™€ MCP๊ฐ€ ์ˆซ์ž/๋ถ„๋ฅ˜ ๊ทœ์น™์ด ์–ด๊ธ‹๋‚˜์ง€ ์•Š๋„๋ก ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค. + +### FR-5. Baseline documentation alignment +- `docs/interface-contract.md`๋Š” ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ, ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ, ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ์˜ ์ตœ์†Œ ์‘๋‹ต ๊ณ„์•ฝ์„ ๋ฐ˜์˜ํ•ด์•ผ ํ•œ๋‹ค. +- `docs/runtime-flows.md`๋Š” ์›”๋ง ์Šค๋ƒ…์ƒท ์ƒ์„ฑ ์ดํ›„ ์กฐํšŒ์„ฑ MCP์™€ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ๋™์ผ ๋ฐ์ดํ„ฐ๋ฅผ ์†Œ๋น„ํ•˜๋Š” ํ๋ฆ„์„ ์„ค๋ช…ํ•ด์•ผ ํ•œ๋‹ค. +- `docs/project-structure.md`๋Š” `dashboard/app.py`, `jarca_ledger_mcp/service.py`, `jarca_ledger_mcp/server.py` ๊ฐ„ ์ฑ…์ž„ ๊ฒฝ๊ณ„๋ฅผ ๋ฐ˜์˜ํ•ด์•ผ ํ•œ๋‹ค. + +## 5. Acceptance Criteria + +- AC-1: ๊ธฐ์ค€์ผ ์ž์‚ฐ ์Šค๋ƒ…์ƒท ์กฐํšŒ MCP๊ฐ€ ์ •์˜๋˜๊ณ  ๊ธฐ์ค€์ผ, ๋ฒ„ํ‚ท๋ณ„ ๊ธˆ์•ก, ์ด๋ถ€์ฑ„, ์ˆœ์ž์‚ฐ, ํฌํ•จ/์ œ์™ธ ํ•ญ๋ชฉ์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค. +- AC-2: ์ˆœ์ž์‚ฐ ์‹œ๊ณ„์—ด ์กฐํšŒ MCP๊ฐ€ ์ •์˜๋˜๊ณ  ์ง€์ • ๊ตฌ๊ฐ„์˜ ๊ธฐ์ค€์ผ๋ณ„ ์ˆœ์ž์‚ฐ์„ ๋™์ผ ์‚ฐ์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค. +- AC-3: ์ˆ˜๊ธฐ ์ž์‚ฐ ์กฐํšŒ MCP๊ฐ€ ์ •์˜๋˜๊ณ  ํ•ญ๋ชฉ๋ช…, ํ‰๊ฐ€๊ฐ’, ๊ธฐ์ค€์ผ, `liquidity_bucket`, ์ถœ์ฒ˜ ๋ฉ”๋ชจ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค. +- AC-4: ๋Œ€์‹œ๋ณด๋“œ์™€ MCP๊ฐ€ ๋™์ผ ๊ถŒ์œ„ ์†Œ์Šค/๋™์ผ ์‚ฐ์‹/๋™์ผ ์ œ์™ธ ์ •์ฑ…์„ ์‚ฌ์šฉํ•œ๋‹ค. +- AC-5: ์‹ ๊ทœ ๋ชจ๋ธ์ด ๋น„์–ด ์žˆ์–ด๋„ ๊ธฐ์กด `REQ-005` ์ž์‚ฐ ํ‘œ์‹œ ๊ฒฝ๋กœ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ํด๋ฐฑํ•œ๋‹ค. +- AC-6: `docs/interface-contract.md`์™€ ํ•„์š” baseline ๋ฌธ์„œ๊ฐ€ ๋ณธ REQ ๋ฒ”์œ„๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. + +## 6. Impacted Area + +- `jarca_ledger_mcp/server.py` +- `jarca_ledger_mcp/service.py` +- `dashboard/app.py` +- `docs/interface-contract.md` +- ํ•„์š” ์‹œ `docs/runtime-flows.md` +- ํ•„์š” ์‹œ `docs/project-structure.md` + +## 7. Notes + +- ์ด REQ๋Š” ์กฐํšŒ์„ฑ MCP์™€ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ ๊ณ„์•ฝ์„ ๋‹ค๋ฃจ๋ฏ€๋กœ `Interface` ํƒ€์ž…์œผ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. +- mutation MCP๋Š” ๋ณธ REQ์—์„œ ์ œ์™ธํ•˜๋ฉฐ ํ›„์† Discovery/REQ๋กœ ๋ถ„๋ฆฌํ•œ๋‹ค. +- ๋ฐ์ดํ„ฐ ์ €์žฅ ๊ตฌ์กฐ์™€ ์›”๋ง ์ˆ˜๋™ ์Šค๋ƒ…์ƒท ์ •์ฑ…์˜ ๊ตฌํ˜„ ์ฑ…์ž„์€ `REQ-009`๋ฅผ ์„ ํ–‰ ์ž…๋ ฅ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. + +## 8. Change Log + +- 2026-06-30 21:17 KST: dcy-008 confirmed ๋ฒ”์œ„๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Proposed ์ดˆ์•ˆ์„ ์ƒ์„ฑ. +- 2026-06-30 21:24 KST: dcy-008 confirmed ๊ฒฐ์ •์‚ฌํ•ญ๊ณผ Acceptance Criteria๋ฅผ ์žฌ๊ฒ€ํ† ํ•ด Approved๋กœ ์ „ํ™˜. +- 2026-06-30 22:05 KST: BAT-008 ๊ตฌํ˜„ยท๊ฒ€์ฆ evidence์™€ jarcaledgerqc PASS gate๋ฅผ ๊ทผ๊ฑฐ๋กœ Implemented๋กœ ์ „ํ™˜. diff --git a/docs/srs/index.md b/docs/srs/index.md index 84cedbc..98d98a2 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -33,7 +33,7 @@ ## Next Requirement ID -- Current: `REQ-009` +- Current: `REQ-011` - Rule: `Requirement Register`์— ์žˆ๋Š” ๊ฐ€์žฅ ํฐ REQ ๋ฒˆํ˜ธ๋ณด๋‹ค ์ •ํ™•ํžˆ 1 ์ปค์•ผ ํ•œ๋‹ค. ## Requirement Register @@ -48,6 +48,8 @@ | REQ-006 | pytest/AppTest/Smoke CI ์ž๋™ํ™” | Testing | Implemented | High | ๋ ›์ €์žฅ์ธ | [req-006_pytest-apptest-smoke-ci-automation.md](Testing/req-006_pytest-apptest-smoke-ci-automation.md) | | REQ-007 | MCP response contract versioning governance | Interface | Implemented | High | ๋ ›์ €์žฅ์ธ | [req-007_mcp-response-contract-versioning-governance.md](Interface/req-007_mcp-response-contract-versioning-governance.md) | | REQ-008 | CI contract regression and lint required checks | Testing | Implemented | High | ๋ ›์ €์žฅ์ธ | [req-008_ci-contract-regression-and-lint-required-checks.md](Testing/req-008_ci-contract-regression-and-lint-required-checks.md) | +| REQ-009 | Asset snapshot/manual assets valuation model | Data | Implemented | High | ๋ ›์ €์žฅ์ธ | [req-009_asset-snapshot-manual-assets-valuation-model.md](Data/req-009_asset-snapshot-manual-assets-valuation-model.md) | +| REQ-010 | Asset snapshot/net worth read MCP and dashboard alignment | Interface | Implemented | High | ๋ ›์ €์žฅ์ธ | [req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md](Interface/req-010_asset-snapshot-net-worth-read-mcp-and-dashboard-alignment.md) | ## Recent Change Log Summary @@ -66,3 +68,6 @@ - 2026-06-30 18:35 KST: dcy-007 confirmed ๋ฒ”์œ„๋ฅผ `REQ-007`(MCP response contract versioning governance)๊ณผ `REQ-008`(CI contract regression and lint required checks) Proposed ์ƒํƒœ๋กœ ์ƒ์„ฑํ–ˆ๋‹ค. - 2026-06-30 18:56 KST: `REQ-007`, `REQ-008`์„ ์Šน์ธ ๊ฒŒ์ดํŠธ ์žฌ๊ฒ€ํ†  ํ›„ Approved๋กœ ์ „ํ™˜ํ–ˆ๋‹ค. - 2026-06-30 KST: BAT-007 ๊ตฌํ˜„๊ณผ contract regression/lint/smoke ๊ฒ€์ฆ evidence๋ฅผ ๊ทผ๊ฑฐ๋กœ `REQ-007`, `REQ-008`์„ Implemented๋กœ ์ „ํ™˜ํ–ˆ๋‹ค. +- 2026-06-30 21:17 KST: dcy-008 confirmed ๋ฒ”์œ„๋ฅผ `REQ-009`(asset snapshot/manual assets valuation model)๊ณผ `REQ-010`(asset snapshot/net worth read MCP and dashboard alignment) Proposed ์ƒํƒœ๋กœ ์ƒ์„ฑํ–ˆ๋‹ค. +- 2026-06-30 21:24 KST: `REQ-009`, `REQ-010`์„ ์Šน์ธ ๊ฒŒ์ดํŠธ ์žฌ๊ฒ€ํ†  ํ›„ Approved๋กœ ์ „ํ™˜ํ–ˆ๋‹ค. +- 2026-06-30 22:05 KST: BAT-008 ๊ตฌํ˜„ยท๊ฒ€์ฆ evidence, fresh Streamlit browser smoke, jarcaledgerqc PASS gate๋ฅผ ๊ทผ๊ฑฐ๋กœ `REQ-009`, `REQ-010`์„ Implemented๋กœ ์ „ํ™˜ํ–ˆ๋‹ค. diff --git a/jarca_ledger_mcp/db.py b/jarca_ledger_mcp/db.py index a145a86..9f5dba2 100644 --- a/jarca_ledger_mcp/db.py +++ b/jarca_ledger_mcp/db.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Iterator DEFAULT_DB_PATH = Path.home() / ".local" / "share" / "jarca" / "ledger.sqlite3" -SCHEMA_VERSION = "8" +SCHEMA_VERSION = "9" SCHEMA_SQL = """ PRAGMA journal_mode=WAL; @@ -137,7 +137,40 @@ CREATE TABLE IF NOT EXISTS ledger_categories ( CREATE INDEX IF NOT EXISTS idx_ledger_categories_active_sort ON ledger_categories(active, sort_order, category, subcategory); CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_categories_unique ON ledger_categories(category, subcategory); -INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', '7'); +CREATE TABLE IF NOT EXISTS manual_assets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + liquidity_bucket TEXT NOT NULL DEFAULT 'other', + valuation_amount INTEGER, + valuation_date TEXT NOT NULL DEFAULT '', + currency TEXT NOT NULL DEFAULT 'KRW', + source_note TEXT NOT NULL DEFAULT '', + memo TEXT NOT NULL DEFAULT '', + active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_manual_assets_active_bucket ON manual_assets(active, liquidity_bucket, name); +CREATE INDEX IF NOT EXISTS idx_manual_assets_valuation_date ON manual_assets(valuation_date); + +CREATE TABLE IF NOT EXISTS asset_snapshots ( + id TEXT PRIMARY KEY, + basis_date TEXT NOT NULL, + source_type TEXT NOT NULL, + source_id TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL, + amount INTEGER, + currency TEXT NOT NULL DEFAULT 'KRW', + liquidity_bucket TEXT NOT NULL DEFAULT 'other', + valuation_source TEXT NOT NULL DEFAULT '', + source_note TEXT NOT NULL DEFAULT '', + memo TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_asset_snapshots_basis_date ON asset_snapshots(basis_date); +CREATE INDEX IF NOT EXISTS idx_asset_snapshots_source ON asset_snapshots(source_type, source_id); + +INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', '9'); """ ACCOUNT_ADDITIVE_COLUMNS: dict[str, str] = { @@ -226,6 +259,37 @@ def migrate_db(conn: sqlite3.Connection) -> None: _add_missing_columns(conn, "ledger_categories", {"cashflow_kind": "TEXT NOT NULL DEFAULT 'expense'"}) conn.execute("CREATE INDEX IF NOT EXISTS idx_ledger_categories_active_sort ON ledger_categories(active, sort_order, category, subcategory)") conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_ledger_categories_unique ON ledger_categories(category, subcategory)") + conn.execute("""CREATE TABLE IF NOT EXISTS manual_assets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + liquidity_bucket TEXT NOT NULL DEFAULT 'other', + valuation_amount INTEGER, + valuation_date TEXT NOT NULL DEFAULT '', + currency TEXT NOT NULL DEFAULT 'KRW', + source_note TEXT NOT NULL DEFAULT '', + memo TEXT NOT NULL DEFAULT '', + active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )""") + conn.execute("CREATE INDEX IF NOT EXISTS idx_manual_assets_active_bucket ON manual_assets(active, liquidity_bucket, name)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_manual_assets_valuation_date ON manual_assets(valuation_date)") + conn.execute("""CREATE TABLE IF NOT EXISTS asset_snapshots ( + id TEXT PRIMARY KEY, + basis_date TEXT NOT NULL, + source_type TEXT NOT NULL, + source_id TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL, + amount INTEGER, + currency TEXT NOT NULL DEFAULT 'KRW', + liquidity_bucket TEXT NOT NULL DEFAULT 'other', + valuation_source TEXT NOT NULL DEFAULT '', + source_note TEXT NOT NULL DEFAULT '', + memo TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + )""") + conn.execute("CREATE INDEX IF NOT EXISTS idx_asset_snapshots_basis_date ON asset_snapshots(basis_date)") + conn.execute("CREATE INDEX IF NOT EXISTS idx_asset_snapshots_source ON asset_snapshots(source_type, source_id)") conn.execute("INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schema_version', ?)", (SCHEMA_VERSION,)) diff --git a/jarca_ledger_mcp/server.py b/jarca_ledger_mcp/server.py index 25e9c4e..96b343a 100644 --- a/jarca_ledger_mcp/server.py +++ b/jarca_ledger_mcp/server.py @@ -51,9 +51,24 @@ def update_ledger_account(account_id: str, name: str | None = None, type: str | @mcp.tool() def get_asset_liquidity_report() -> dict: - """Return asset liquidity, debt, and net worth from account liquidity buckets and last_balance.""" + """Return asset liquidity, debt, and net worth from shared account/manual valuation sources.""" return service.get_asset_liquidity_report() +@mcp.tool() +def list_manual_assets(active_only: bool = True) -> dict: + """List manual non-transaction assets/liabilities used for net-worth valuation.""" + return service.list_manual_assets(active_only=active_only) + +@mcp.tool() +def get_asset_snapshot_report(basis_date: str = "") -> dict: + """Return basis-date asset snapshot/net-worth report with live-authority fallback.""" + return service.get_asset_snapshot_report(basis_date=basis_date) + +@mcp.tool() +def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12) -> dict: + """Return net-worth timeseries from stored asset snapshot rows.""" + return service.get_net_worth_timeseries(start_date=start_date, end_date=end_date, months=months) + @mcp.tool() def get_balance_reconciliation_report(account_id: str = "", baseline_balance: int | None = None, baseline_at: str = "", source_note: str = "", memo: str = "", tolerance: int = 1000) -> dict: """Preview balance reconciliation for manual_only/partial_import accounts without mutating the DB.""" diff --git a/jarca_ledger_mcp/service.py b/jarca_ledger_mcp/service.py index 9ec735b..dc1a39d 100644 --- a/jarca_ledger_mcp/service.py +++ b/jarca_ledger_mcp/service.py @@ -21,6 +21,9 @@ VALID_TRANSACTION_TYPES = {"income", "expense", "transfer", "refund", "adjustmen VALID_ACCOUNT_TYPES = {"bank", "card", "cash", "investment", "loan", "other"} VALID_STATEMENT_STATUSES = {"unknown", "import_supported", "manual_only", "partial_import"} VALID_LIQUIDITY_BUCKETS = {"cash", "investment", "deposit", "real_estate", "loan", "other"} +ASSET_BUCKETS = ["cash", "investment", "deposit", "real_estate", "other"] +LIABILITY_BUCKETS = ["loan"] +ASSET_BUCKET_ORDER = [*ASSET_BUCKETS, *LIABILITY_BUCKETS] RECONCILIATION_TARGET_STATUSES = {"manual_only", "partial_import"} BALANCE_RECONCILIATION_TOLERANCE_KRW = 1_000 BALANCE_RECONCILIATION_REFERENCE_RATIO = 0.001 @@ -223,7 +226,7 @@ def _readonly_sqlite_connection(path: Path) -> sqlite3.Connection: def _safe_table_counts(conn: sqlite3.Connection) -> dict[str, int]: counts: dict[str, int] = {} - for table in ("accounts", "transactions", "budgets", "import_batches", "category_rules", "ledger_categories"): + for table in ("accounts", "transactions", "budgets", "import_batches", "category_rules", "ledger_categories", "manual_assets", "asset_snapshots"): exists = conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)).fetchone() if exists: counts[table] = int(conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]) @@ -374,73 +377,301 @@ def list_ledger_accounts(active_only: bool = True, db_path: str | None = None) - return ok(accounts=[row_to_dict(r) for r in rows], count=len(rows)) -def get_asset_liquidity_report(db_path: str | None = None) -> dict[str, Any]: - """Summarize assets/liabilities from account-level liquidity_bucket and last_balance only.""" - with db(db_path) as conn: - rows = conn.execute("SELECT * FROM accounts WHERE active=1 ORDER BY name").fetchall() - accounts = [row_to_dict(r) for r in rows] - bucket_order = ["cash", "investment", "deposit", "real_estate", "other", "loan"] - bucket_totals = {bucket: 0 for bucket in bucket_order} - bucket_counts = {bucket: 0 for bucket in bucket_order} - missing_balance_accounts: list[dict[str, Any]] = [] - invalid_bucket_accounts: list[dict[str, Any]] = [] - source_accounts: list[dict[str, Any]] = [] +def _valuation_policy() -> dict[str, Any]: + return { + "authority_sources": ["accounts.last_balance", "manual_assets"], + "snapshot_role": "historical_record_layer", + "formula": "asset_buckets - loan", + "asset_buckets": ASSET_BUCKETS, + "liability_buckets": LIABILITY_BUCKETS, + "missing_valuation_policy": "exclude_without_transaction_inference", + } - for account in accounts: + +def _account_valuation_items(conn: sqlite3.Connection) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]: + rows = conn.execute("SELECT * FROM accounts WHERE active=1 ORDER BY name").fetchall() + included: list[dict[str, Any]] = [] + excluded: list[dict[str, Any]] = [] + invalid: list[dict[str, Any]] = [] + for row in rows: + account = row_to_dict(row) raw_bucket = str(account.get("liquidity_bucket") or "other") try: bucket = normalize_liquidity_bucket(raw_bucket) except ValueError: bucket = "other" - invalid_bucket_accounts.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": raw_bucket}) - + invalid.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": raw_bucket}) + base = { + "id": account.get("id", ""), + "source_id": account.get("id", ""), + "source_type": "account", + "authority_source": "accounts.last_balance", + "name": account.get("name", ""), + "type": account.get("type", ""), + "liquidity_bucket": bucket, + "valuation_date": account.get("last_synced_at", ""), + "last_synced_at": account.get("last_synced_at", ""), + "source_note": account.get("memo", ""), + "memo": account.get("memo", ""), + } balance = account.get("last_balance") if balance is None: - missing_balance_accounts.append({"id": account.get("id", ""), "name": account.get("name", ""), "liquidity_bucket": bucket}) + excluded.append({**base, "reason": "missing_last_balance"}) continue - amount = int(balance) - bucket_counts[bucket] += 1 - bucket_totals[bucket] += amount - source_accounts.append( + included.append({**base, "amount": amount, "last_balance": amount}) + return included, excluded, invalid + + +def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool = True) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + where = "WHERE active=1" if active_only else "" + rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall() + included: list[dict[str, Any]] = [] + excluded: list[dict[str, Any]] = [] + for row in rows: + asset = row_to_dict(row) + try: + bucket = normalize_liquidity_bucket(asset.get("liquidity_bucket")) + except ValueError: + bucket = "other" + base = { + "id": asset.get("id", ""), + "source_id": asset.get("id", ""), + "source_type": "manual_asset", + "authority_source": "manual_assets", + "name": asset.get("name", ""), + "liquidity_bucket": bucket, + "valuation_date": asset.get("valuation_date", ""), + "source_note": asset.get("source_note", ""), + "memo": asset.get("memo", ""), + "active": asset.get("active", True), + } + value = asset.get("valuation_amount") + if value is None: + excluded.append({**base, "reason": "missing_valuation_amount"}) + continue + included.append({**base, "amount": int(value), "valuation_amount": int(value)}) + return included, excluded + + +def _snapshot_items(conn: sqlite3.Connection, basis_date: str) -> list[dict[str, Any]]: + rows = conn.execute( + "SELECT * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC", + (basis_date,), + ).fetchall() + items: list[dict[str, Any]] = [] + for row in rows: + snap = row_to_dict(row) + try: + bucket = normalize_liquidity_bucket(snap.get("liquidity_bucket")) + except ValueError: + bucket = "other" + items.append( { - "id": account.get("id", ""), - "name": account.get("name", ""), - "type": account.get("type", ""), + "id": snap.get("id", ""), + "source_id": snap.get("source_id", ""), + "source_type": snap.get("source_type", ""), + "authority_source": "asset_snapshots", + "valuation_source": snap.get("valuation_source", ""), + "name": snap.get("name", ""), "liquidity_bucket": bucket, - "last_balance": amount, - "last_synced_at": account.get("last_synced_at", ""), - "memo": account.get("memo", ""), + "amount": int(snap.get("amount") or 0), + "valuation_date": snap.get("basis_date", ""), + "source_note": snap.get("source_note", ""), + "memo": snap.get("memo", ""), } ) + return items - asset_buckets = ["cash", "investment", "deposit", "real_estate", "other"] - total_assets = sum(bucket_totals[bucket] for bucket in asset_buckets) - total_liabilities = abs(bucket_totals["loan"]) + +def _summarize_valuation_items(included_items: list[dict[str, Any]], excluded_items: list[dict[str, Any]] | None = None) -> dict[str, Any]: + bucket_totals = {bucket: 0 for bucket in ASSET_BUCKET_ORDER} + bucket_counts = {bucket: 0 for bucket in ASSET_BUCKET_ORDER} + for item in included_items: + bucket = normalize_liquidity_bucket(item.get("liquidity_bucket")) + amount = int(item.get("amount") or 0) + bucket_counts[bucket] += 1 + bucket_totals[bucket] += amount + total_assets = sum(bucket_totals[bucket] for bucket in ASSET_BUCKETS) + total_liabilities = sum(abs(bucket_totals[bucket]) for bucket in LIABILITY_BUCKETS) net_worth = total_assets - total_liabilities buckets = [] - for bucket in bucket_order: + for bucket in ASSET_BUCKET_ORDER: amount = bucket_totals[bucket] - denominator = total_liabilities if bucket == "loan" else total_assets + denominator = total_liabilities if bucket in LIABILITY_BUCKETS else total_assets buckets.append( { "liquidity_bucket": bucket, "amount": amount, - "account_count": bucket_counts[bucket], - "asset_share": round(amount / total_assets, 4) if bucket != "loan" and total_assets else None, - "liability_share": round(abs(amount) / total_liabilities, 4) if bucket == "loan" and total_liabilities else None, + "account_count": sum(1 for item in included_items if item.get("source_type") == "account" and item.get("liquidity_bucket") == bucket), + "manual_asset_count": sum(1 for item in included_items if item.get("source_type") == "manual_asset" and item.get("liquidity_bucket") == bucket), + "item_count": bucket_counts[bucket], + "asset_share": round(amount / total_assets, 4) if bucket not in LIABILITY_BUCKETS and total_assets else None, + "liability_share": round(abs(amount) / total_liabilities, 4) if bucket in LIABILITY_BUCKETS and total_liabilities else None, "share": round(abs(amount) / denominator, 4) if denominator else None, } ) + return { + "buckets": buckets, + "bucket_totals": bucket_totals, + "total_assets": total_assets, + "total_liabilities": total_liabilities, + "net_worth": net_worth, + "included_items": included_items, + "excluded_items": excluded_items or [], + "valuation_policy": _valuation_policy(), + } + + +def create_manual_asset(name: str, valuation_amount: int | None, valuation_date: str, liquidity_bucket: str, source_note: str = "", memo: str = "", currency: str = "KRW", active: bool = True, db_path: str | None = None) -> dict[str, Any]: + if not name.strip(): + return fail("validation_error", "manual asset name is required") + try: + bucket = normalize_liquidity_bucket(liquidity_bucket) + amount = int(valuation_amount) if valuation_amount is not None else None + vdate = normalize_date(valuation_date, default_today=True) + except Exception as e: + return fail("validation_error", str(e)) + ts = now_iso() + asset_id = new_id("asset") + with db(db_path) as conn: + conn.execute( + """INSERT INTO manual_assets(id,name,liquidity_bucket,valuation_amount,valuation_date,currency,source_note,memo,active,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?)""", + (asset_id, name.strip(), bucket, amount, vdate, currency, source_note, memo, int(active), ts, ts), + ) + row = conn.execute("SELECT * FROM manual_assets WHERE id=?", (asset_id,)).fetchone() + return ok(manual_asset={**row_to_dict(row), "authority_source": "manual_assets"}) + + +def list_manual_assets(active_only: bool = True, db_path: str | None = None) -> dict[str, Any]: + where = "WHERE active=1" if active_only else "" + with db(db_path) as conn: + rows = conn.execute(f"SELECT * FROM manual_assets {where} ORDER BY name").fetchall() + assets = [{**row_to_dict(row), "authority_source": "manual_assets"} for row in rows] + return ok(manual_assets=assets, count=len(assets), valuation_policy=_valuation_policy()) + + +def create_asset_snapshot(basis_date: str, source_type: str, source_id: str, name: str, amount: int | None, liquidity_bucket: str, valuation_source: str = "", source_note: str = "", memo: str = "", currency: str = "KRW", db_path: str | None = None) -> dict[str, Any]: + if source_type not in {"account", "manual_asset"}: + return fail("validation_error", "source_type must be one of ['account', 'manual_asset']") + if not name.strip(): + return fail("validation_error", "snapshot item name is required") + try: + basis = normalize_date(basis_date, default_today=True) + bucket = normalize_liquidity_bucket(liquidity_bucket) + amount_int = int(amount) if amount is not None else None + except Exception as e: + return fail("validation_error", str(e)) + ts = now_iso() + snapshot_id = new_id("snap") + with db(db_path) as conn: + conn.execute( + """INSERT INTO asset_snapshots(id,basis_date,source_type,source_id,name,amount,currency,liquidity_bucket,valuation_source,source_note,memo,created_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""", + (snapshot_id, basis, source_type, source_id, name.strip(), amount_int, currency, bucket, valuation_source or source_type, source_note, memo, ts), + ) + row = conn.execute("SELECT * FROM asset_snapshots WHERE id=?", (snapshot_id,)).fetchone() + return ok(asset_snapshot=row_to_dict(row)) + + +def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None) -> dict[str, Any]: + basis = normalize_date(basis_date, default_today=True) if basis_date else current_kst_date().isoformat() + with db(db_path) as conn: + snapshots = _snapshot_items(conn, basis) + if snapshots: + summary = _summarize_valuation_items(snapshots, []) + return ok( + basis_date=basis, + source="asset_snapshots", + snapshot_count=len(snapshots), + **summary, + ) + account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn) + manual_items, manual_excluded = _manual_asset_valuation_items(conn) + included = account_items + manual_items + excluded = account_excluded + manual_excluded + summary = _summarize_valuation_items(included, excluded) return ok( - buckets=buckets, - accounts=source_accounts, - total_assets=total_assets, - total_liabilities=total_liabilities, - net_worth=net_worth, - missing_balance_accounts=missing_balance_accounts, + basis_date=basis, + source="live_authority", + snapshot_count=0, invalid_bucket_accounts=invalid_bucket_accounts, - source="accounts.last_balance", + **summary, + ) + + +def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: int = 12, db_path: str | None = None) -> dict[str, Any]: + try: + months_int = max(1, min(int(months), 120)) + end_norm = normalize_date(end_date, default_today=True) if end_date else current_kst_date().isoformat() + if start_date: + start_norm = normalize_date(start_date) + else: + end_dt = date.fromisoformat(end_norm) + start_month = end_dt.month - months_int + 1 + year = end_dt.year + (start_month - 1) // 12 + month = (start_month - 1) % 12 + 1 + start_norm = date(year, month, 1).isoformat() + except Exception as e: + return fail("validation_error", str(e)) + with db(db_path) as conn: + basis_rows = conn.execute( + """SELECT DISTINCT basis_date FROM asset_snapshots + WHERE basis_date>=? AND basis_date<=? + ORDER BY basis_date ASC""", + (start_norm, end_norm), + ).fetchall() + points = [] + for row in basis_rows: + basis = row["basis_date"] + items = _snapshot_items(conn, basis) + summary = _summarize_valuation_items(items, []) + points.append( + { + "basis_date": basis, + "total_assets": summary["total_assets"], + "total_liabilities": summary["total_liabilities"], + "net_worth": summary["net_worth"], + "source": "asset_snapshots", + "item_count": len(items), + } + ) + return ok(start_date=start_norm, end_date=end_norm, months=months_int, points=points, count=len(points), valuation_policy=_valuation_policy()) + + +def get_asset_liquidity_report(db_path: str | None = None) -> dict[str, Any]: + """Summarize assets/liabilities from shared valuation authority sources. + + Existing REQ-005 keys are preserved. When manual_assets is empty, totals and + source remain equivalent to the original accounts.last_balance behavior. + """ + with db(db_path) as conn: + account_items, account_excluded, invalid_bucket_accounts = _account_valuation_items(conn) + manual_items, manual_excluded = _manual_asset_valuation_items(conn) + included = account_items + manual_items + excluded = account_excluded + manual_excluded + summary = _summarize_valuation_items(included, excluded) + source_accounts = [item for item in included if item.get("source_type") == "account"] + source = "accounts.last_balance" if not manual_items and not manual_excluded else "accounts.last_balance+manual_assets" + return ok( + buckets=summary["buckets"], + accounts=source_accounts, + manual_assets=manual_items, + total_assets=summary["total_assets"], + total_liabilities=summary["total_liabilities"], + net_worth=summary["net_worth"], + missing_balance_accounts=[ + {"id": item.get("id", ""), "name": item.get("name", ""), "liquidity_bucket": item.get("liquidity_bucket", "")} + for item in account_excluded + ], + missing_valuation_items=excluded, + invalid_bucket_accounts=invalid_bucket_accounts, + manual_asset_count=len(manual_items), + included_items=included, + excluded_items=excluded, + valuation_policy=summary["valuation_policy"], + source=source, ) diff --git a/tests/test_asset_snapshots.py b/tests/test_asset_snapshots.py new file mode 100644 index 0000000..1c99be0 --- /dev/null +++ b/tests/test_asset_snapshots.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import sqlite3 + +from jarca_ledger_mcp import service +from jarca_ledger_mcp.db import init_db + + +def test_asset_snapshot_schema_is_additive_and_idempotent(tmp_path): + db_path = str(tmp_path / "ledger.sqlite3") + init_db(db_path) + init_db(db_path) + + with sqlite3.connect(db_path) as conn: + manual_cols = {row[1] for row in conn.execute("PRAGMA table_info(manual_assets)")} + snapshot_cols = {row[1] for row in conn.execute("PRAGMA table_info(asset_snapshots)")} + + assert {"id", "name", "valuation_amount", "valuation_date", "liquidity_bucket", "source_note", "memo", "active"} <= manual_cols + assert {"id", "basis_date", "source_type", "source_id", "name", "amount", "liquidity_bucket", "valuation_source", "source_note", "memo"} <= snapshot_cols + + +def test_manual_assets_snapshot_report_and_net_worth_formula(tmp_path): + db_path = str(tmp_path / "ledger.sqlite3") + cash = service.create_ledger_account("ํ˜„๊ธˆ", last_balance=1_000_000, last_synced_at="2026-06-30", liquidity_bucket="cash", db_path=db_path) + stocks = service.create_ledger_account("ํˆฌ์ž", type="investment", last_balance=2_000_000, liquidity_bucket="investment", db_path=db_path) + missing = service.create_ledger_account("๋ฏธํ‰๊ฐ€ ๊ณ„์ขŒ", liquidity_bucket="deposit", db_path=db_path) + assert cash["success"] and stocks["success"] and missing["success"] + + house = service.create_manual_asset("์ „์„ธ๋ณด์ฆ๊ธˆ", 300_000_000, "2026-06-30", "real_estate", source_note="synthetic appraisal", db_path=db_path) + loan = service.create_manual_asset("์ฃผ๋‹ด๋Œ€", 120_000_000, "2026-06-30", "loan", source_note="synthetic loan balance", db_path=db_path) + unvalued = service.create_manual_asset("ํ‰๊ฐ€๋Œ€๊ธฐ ์ฐจ๋Ÿ‰", None, "2026-06-30", "other", source_note="pending", db_path=db_path) + assert house["success"] and loan["success"] and unvalued["success"] + + manual = service.list_manual_assets(db_path=db_path) + assert manual["success"] is True + assert manual["count"] == 3 + assert {item["authority_source"] for item in manual["manual_assets"]} == {"manual_assets"} + + report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path) + assert report["success"] is True + assert report["source"] == "live_authority" + assert report["valuation_policy"]["formula"] == "asset_buckets - loan" + assert report["total_assets"] == 303_000_000 + assert report["total_liabilities"] == 120_000_000 + assert report["net_worth"] == 183_000_000 + assert {item["source_type"] for item in report["included_items"]} == {"account", "manual_asset"} + assert any(item["source_type"] == "manual_asset" and item["name"] == "ํ‰๊ฐ€๋Œ€๊ธฐ ์ฐจ๋Ÿ‰" for item in report["excluded_items"]) + assert any(item["source_type"] == "account" and item["name"] == "๋ฏธํ‰๊ฐ€ ๊ณ„์ขŒ" for item in report["excluded_items"]) + + +def test_snapshot_rows_are_append_only_and_timeseries_uses_same_formula(tmp_path): + db_path = str(tmp_path / "ledger.sqlite3") + service.create_asset_snapshot("2026-05-31", "account", "acct-a", "ํ˜„๊ธˆ", 1_000_000, "cash", source_note="synthetic", db_path=db_path) + service.create_asset_snapshot("2026-05-31", "manual_asset", "manual-home", "๋ถ€๋™์‚ฐ", 200_000_000, "real_estate", source_note="synthetic", db_path=db_path) + service.create_asset_snapshot("2026-05-31", "manual_asset", "manual-loan", "๋Œ€์ถœ", 80_000_000, "loan", source_note="synthetic", db_path=db_path) + service.create_asset_snapshot("2026-06-30", "account", "acct-a", "ํ˜„๊ธˆ", 1_500_000, "cash", source_note="synthetic", db_path=db_path) + service.create_asset_snapshot("2026-06-30", "manual_asset", "manual-home", "๋ถ€๋™์‚ฐ", 205_000_000, "real_estate", source_note="synthetic", db_path=db_path) + service.create_asset_snapshot("2026-06-30", "manual_asset", "manual-loan", "๋Œ€์ถœ", 78_000_000, "loan", source_note="synthetic", db_path=db_path) + + may = service.get_asset_snapshot_report("2026-05-31", db_path=db_path) + assert may["success"] is True + assert may["source"] == "asset_snapshots" + assert may["snapshot_count"] == 3 + assert may["net_worth"] == 121_000_000 + + series = service.get_net_worth_timeseries(start_date="2026-05-01", end_date="2026-06-30", db_path=db_path) + assert series["success"] is True + assert [point["basis_date"] for point in series["points"]] == ["2026-05-31", "2026-06-30"] + assert [point["net_worth"] for point in series["points"]] == [121_000_000, 128_500_000] + + +def test_asset_liquidity_report_empty_new_tables_preserves_req005_fallback(tmp_path): + db_path = str(tmp_path / "ledger.sqlite3") + service.create_ledger_account("ํ˜„๊ธˆ์„ฑ ๊ณ„์ขŒ", last_balance=1_000_000, liquidity_bucket="cash", db_path=db_path) + service.create_ledger_account("ํˆฌ์ž ๊ณ„์ขŒ", last_balance=3_000_000, liquidity_bucket="investment", db_path=db_path) + service.create_ledger_account("๋Œ€์ถœ", last_balance=500_000, liquidity_bucket="loan", db_path=db_path) + + report = service.get_asset_liquidity_report(db_path=db_path) + assert report["success"] is True + assert report["total_assets"] == 4_000_000 + assert report["total_liabilities"] == 500_000 + assert report["net_worth"] == 3_500_000 + assert report["manual_asset_count"] == 0 + assert report["source"] == "accounts.last_balance" + assert [item["source_type"] for item in report["accounts"]] == ["account", "account", "account"] diff --git a/tests/test_service.py b/tests/test_service.py index d0de6cf..699294d 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -10,7 +10,7 @@ def test_health_check_initializes_db(tmp_path): db_path = tmp_path / "ledger.sqlite3" result = service.health_check(str(db_path)) assert result["success"] is True - assert result["schema_version"] == "8" + assert result["schema_version"] == "9" assert db_path.exists()