feat: add ledger backup safety workflow

confirm dcy-002 and add REQ/BAT evidence

add ledger backup/status metadata workflow

document restore as runbook/script-only guarded operation

add synthetic DB backup/restore smoke tests
This commit is contained in:
Jarca
2026-06-27 10:40:06 +09:00
parent 0f881b245f
commit 67d8d4871b
19 changed files with 1035 additions and 8 deletions
@@ -0,0 +1,42 @@
# BAT-002 Design
## Architecture Summary
BAT-002 adds a small operational-safety layer around the existing SQLite ledger without changing transaction schemas. The service layer produces metadata-only DB evidence, creates backups with SQLite's online backup API, lists backup files, and validates one backup file. MCP exposes only non-destructive backup/status operations. Restore remains a repo script/runbook workflow requiring explicit user approval before overwriting a target DB.
## Changed Areas
- `jarca_ledger_mcp/service.py`
- New constants: default backup directory and restore policy.
- New helpers: sha256, file metadata, read-only SQLite status, safe table counts, aggregate totals.
- New public functions: `get_ledger_db_status`, `create_ledger_backup`, `list_ledger_backups`, `get_ledger_backup_status`.
- `jarca_ledger_mcp/server.py`
- New additive MCP tools for backup/status/list/hash only.
- No restore MCP tool.
- `scripts/ledger_restore_smoke.py`
- Script-only restore smoke helper with explicit overwrite approval token.
- Docs/tests
- Synthetic tests and operational runbook/baseline docs.
## Key Decisions
1. Backup creation uses SQLite online backup API rather than raw copy so WAL-mode DBs are copied consistently.
2. Backup evidence returns metadata only: path, size, mtime, sha256, integrity result, table row counts, and aggregate totals by cashflow kind.
3. Retention policy implements "recent 30 days OR recent 20 files" by pruning only files that are both older than the retention window and outside the newest count.
4. Restore is deliberately excluded from MCP. The script exists for fixture/validation DB restore smoke and requires `RESTORE_APPROVED` for overwriting any existing target.
5. Existing success response shapes are preserved; all new response fields are additive under new tools/functions.
## Edge Cases
- Missing source DB: backup/status returns `not_found` rather than initializing or mutating a new DB.
- Empty backup directory: list returns success with zero backups.
- Invalid/corrupt backup: backup status includes integrity error metadata and restore smoke fails before copying.
- Existing target restore path: restore smoke fails unless `--allow-overwrite --approval-token RESTORE_APPROVED` are supplied.
- Sensitive row data: metadata collection never selects merchant, counterparty, account number, raw_payload, or raw financial file content.
## Architecture Impact
- runtime-flows: updated with backup/status and restore-restricted operational flow.
- data-model: updated with backup artifact and evidence policy notes.
- interface-contract: updated with new MCP tools and explicit no-restore-MCP rule.
## Reference Doc Update Plan
- Update `docs/runtime-flows.md` with operational backup/validation/restore-smoke flow and high-risk flow constraints.
- Update `docs/data-model.md` with backup artifact/evidence persistence boundary.
- Update `docs/interface-contract.md` with additive backup/status tools and restore non-exposure.
- Add `docs/runbooks/ledger-backup-restore.md`.
@@ -0,0 +1,62 @@
# BAT-002 Implementation
## Plan Summary
Implemented REQ-002 as a staged additive operational-safety change. No transaction schema migration, operating DB restore, or broad data rewrite was performed.
## Changed Files
- `jarca_ledger_mcp/service.py`
- Added metadata-only DB status helpers.
- Added SQLite online backup creation with retention enforcement.
- Added backup list/status/hash functions.
- Added explicit restore policy metadata (`mcp_restore_exposed: false`).
- `jarca_ledger_mcp/server.py`
- Added MCP tools: `get_ledger_db_status`, `create_ledger_backup`, `list_ledger_backups`, `get_ledger_backup_status`.
- Did not add any restore MCP tool.
- `scripts/ledger_restore_smoke.py`
- Added script-only restore smoke helper with overwrite approval token.
- `tests/test_backup_workflow.py`
- Added synthetic DB tests for metadata-only status, backup create/list/status/hash, retention pruning, and sensitive evidence exclusion.
- `docs/runbooks/ledger-backup-restore.md`
- Added operator runbook and QC checklist.
- `docs/runtime-flows.md`
- Added operational backup/validation/restore smoke runtime flow.
- `docs/data-model.md`
- Added `LedgerBackupArtifact` and metadata-only persistence notes.
- `docs/interface-contract.md`
- Added backup/status MCP contract and restore non-exposure rule.
- StagePilot docs
- `docs/srs/Non-Functional/req-002_operational-safety-backup-restore-validation-db.md`
- `docs/srs/index.md`
- `docs/discovery/dcy-002_20260627_operational-safety-backup-restore-validation-db.md`
- `docs/batches/index.md`
- `docs/batches/bat-002_20260627_operational-safety-backup-restore-validation-db/*`
## Execution Log
- RED test first:
- Command: `source .venv/bin/activate && pytest tests/test_backup_workflow.py -q`
- Initial result: failed with missing `service.get_ledger_db_status`, `create_ledger_backup`, `list_ledger_backups`, `get_ledger_backup_status` attributes.
- Implemented service/server/script/docs changes.
- Targeted test:
- Command: `source .venv/bin/activate && pytest tests/test_backup_workflow.py -q`
- Result: `3 passed`.
- Full syntax/test:
- Command: `source .venv/bin/activate && python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py && pytest tests/test_backup_workflow.py -q && pytest -q`
- Result: backup tests passed, full suite passed with `...........ss.........ss [100%]`.
- Full pytest summary:
- Command: `source .venv/bin/activate && pytest -ra`
- Result: `20 passed, 4 skipped in 16.02s`; skips are sample KB/Toss PDF tests without sample files/passwords.
- Restore smoke:
- Command: created synthetic tmp DB, created backup via `service.create_ledger_backup`, then ran `python scripts/ledger_restore_smoke.py --backup <tmp backup> --target <tmp restored.sqlite3>`.
- Result: `success: true`, backup and restored target `integrity_check: ok`, table counts `{accounts: 1, transactions: 1, budgets: 0, import_batches: 0, category_rules: 0, ledger_categories: 0}`, health check `active_transactions: 1`.
- MCP surface check:
- Command: `source .venv/bin/activate && hermes mcp test jarca-ledger`
- Result: connected, `Tools discovered: 45`, including backup/status tools and no restore tool.
## Validation
- Synthetic DB tests confirmed no operating DB is needed for backup/status/list/hash verification.
- Restore smoke used `/tmp/...` source/target only, not `/root/.local/share/jarca/ledger.sqlite3`.
- Evidence captured only path, size, mtime, sha256, integrity, row counts, aggregate totals, and health/test results.
## Remaining Risks
- Real operating DB restore remains intentionally untested and requires separate explicit user approval.
- Browser/UI copy is not changed in this batch; operator safety guidance is runbook/MCP/script level only.
@@ -0,0 +1,33 @@
# BAT-002: 운영 안전 백업·복구·검증 DB 워크플로우
## Metadata
- BAT ID: BAT-002
- Profile: standard
- Status: release-candidate
- Source Discovery: dcy-002
- Included REQ: REQ-002
- Created At (KST): 2026-06-27 10:17
- Owner: 렛저장인
## Scope
- Add metadata-only DB status, backup create/list/status helpers.
- Expose backup/status/list/hash as additive MCP tools.
- Keep restore execution out of MCP and document script/runbook approval path.
- Verify with synthetic DB tests and safe restore smoke.
- Update runtime, data-model, and interface baseline docs.
## Out of Scope
- 실제 운영 DB restore 실행.
- 원본 운영 DB 직접 mutation.
- 파괴적 SQLite migration 또는 대량 data rewrite.
- 신규 금융기관 parser, CI/CD 구축, 배포 서비스화.
## Documents
- [planning.md](planning.md)
- [design.md](design.md)
- [implementation.md](implementation.md)
- [verification.md](verification.md)
## Notes
- Restore is intentionally script/runbook only; no `restore_*` MCP tool should be added.
- Evidence must remain metadata-only and exclude merchant/counterparty full dumps, raw_payload, account numbers, passwords, and source financial files.
@@ -0,0 +1,34 @@
# BAT-002 Planning
## Included REQ
- REQ-002: 운영 안전 백업·복구·검증 DB 워크플로우
## Delivery Plan
1. Add service helpers for non-sensitive DB status evidence and SQLite online backup creation.
2. Add additive MCP tools for backup creation, backup list, backup status/hash, and DB status.
3. Add a non-MCP restore smoke script requiring explicit approval for overwriting an existing target DB.
4. Add synthetic DB tests for status evidence, backup/list/status/hash, retention, and restore smoke policy.
5. Update runtime/data/interface docs and add a runbook.
6. Run targeted/full verification and request jarcaledgerqc QC gate.
## Dependencies
- Existing SQLite initialization and additive migration behavior in `jarca_ledger_mcp/db.py`.
- Existing service response convention: `{"success": True, ...}` and `fail(...)` error shape.
- Synthetic pytest tmp DBs for safe evidence.
## Design Gate
Design required: yes.
Reason: this batch changes the MCP interface surface and runtime/data safety baseline, and must explicitly preserve restore non-exposure and sensitive evidence restrictions.
## Milestones
- M1: REQ-002 Approved and BAT-002 scaffolded.
- M2: Backup/status/list/hash helpers and MCP tools implemented.
- M3: Restore smoke script and runbook documented without MCP restore surface.
- M4: Synthetic tests and docs evidence complete.
- M5: QC gate PASS/PASS_WITH_NOTES, then REQ-002 Implemented.
## Risks
- Accidentally exposing restore as MCP would violate dcy-002 policy.
- SQLite WAL copy must be consistent; implementation should use SQLite online backup API rather than raw file copy for backup creation.
- Evidence must not serialize sensitive row-level details.
@@ -0,0 +1,111 @@
# BAT-002 Verification
## Verification Status
- Status: approved
- Ready for Release: true
- Verified At (KST): 2026-06-27 10:33
- Verified By: 렛저장인
## Acceptance Mapping
### REQ-002 AC1
Backup creation must write timestamped SQLite files under `/root/.local/share/jarca/backups/ledger/` by default and return source/backup path, size, mtime, sha256, integrity, and table counts.
Evidence:
- `service.create_ledger_backup` uses default `DEFAULT_LEDGER_BACKUP_DIR = ~/.local/share/jarca/backups/ledger` and returns `source_db`, `backup`, `retention`, and `restore_policy`.
- `tests/test_backup_workflow.py::test_create_list_and_status_ledger_backup_with_retention` verifies backup file creation, sha256 presence, `integrity_check == "ok"`, and source/backup table count equality on synthetic DB.
Result: pass.
### REQ-002 AC2
Retention keeps recent 30 days or newest 20 backup files and reports pruned paths.
Evidence:
- `_apply_backup_retention` prunes only backups that are both outside newest count and older than day cutoff.
- `tests/test_backup_workflow.py::test_backup_retention_keeps_recent_count_and_prunes_older_extras` verifies retention with synthetic old backup files and checks `pruned_paths` length.
Result: pass.
### REQ-002 AC3
Backup list/status/hash is additive service/MCP surface and existing response shapes are preserved.
Evidence:
- New service functions: `list_ledger_backups`, `get_ledger_backup_status`, `get_ledger_db_status`.
- New MCP tools are additive in `server.py`.
- `hermes mcp test jarca-ledger` result: connected, `Tools discovered: 45`, including `get_ledger_db_status`, `create_ledger_backup`, `list_ledger_backups`, `get_ledger_backup_status`.
Result: pass.
### REQ-002 AC4
Restore execution is not MCP-exposed and is script/runbook only with explicit approval for overwrite.
Evidence:
- `server.py` contains no restore MCP tool.
- `hermes mcp test jarca-ledger` discovered backup/status tools and no restore tool.
- `scripts/ledger_restore_smoke.py` requires `--allow-overwrite --approval-token RESTORE_APPROVED` before overwriting an existing target.
- `docs/runbooks/ledger-backup-restore.md` documents restore as script-only and warns not to target the live operating DB without explicit user approval.
Result: pass.
### REQ-002 AC5
Validation uses synthetic fixture DB first; operating DB copy evidence is metadata-only if used.
Evidence:
- All tests use pytest `tmp_path` synthetic SQLite DBs.
- Restore smoke command used `/tmp/.../fixture.sqlite3`, `/tmp/.../backups/...sqlite3`, and `/tmp/.../restored.sqlite3`.
- No operating DB copy was needed.
Result: pass.
### REQ-002 AC6
Baseline docs reflect backup/status/list/hash/restore restrictions and sensitive evidence policy.
Evidence:
- `docs/runtime-flows.md` adds operational backup/validation/restore smoke flow and restore non-MCP constraint.
- `docs/data-model.md` adds `LedgerBackupArtifact` and metadata-only evidence notes.
- `docs/interface-contract.md` adds backup/status tools and restore non-exposure compatibility rule.
- `docs/runbooks/ledger-backup-restore.md` lists allowed/forbidden evidence and QC checklist.
Result: pass.
### REQ-002 AC7
Synthetic tests verify backup/status/list/hash/retention/restore smoke or health/status check without operating DB mutation.
Evidence:
- Command: `source .venv/bin/activate && pytest -ra`
- Result: `20 passed, 4 skipped in 16.02s`.
- Command: synthetic restore smoke with `scripts/ledger_restore_smoke.py`.
- Result: `success: true`, backup/target `integrity_check: ok`, matching table counts, `health_check.success: true`.
Result: pass.
### REQ-002 AC8
QC gate must verify operating DB mutation absence, restore MCP non-exposure, sensitive evidence absence, smoke evidence, tests/docs.
Evidence:
- This verification document and runbook include QC checklist.
- `jarcaledgerqc` QC gate returned `PASS`.
- QC summary confirmed no operating DB restore/direct mutation, backup/status/list/hash policy compliance, no restore MCP tool, synthetic restore smoke success, no real sensitive evidence exposure, and tests/docs evidence.
Result: pass.
## Evidence Summary
- Syntax: `python -m py_compile jarca_ledger_mcp/*.py dashboard/app.py` -> exit 0.
- Targeted tests: `pytest tests/test_backup_workflow.py -q` -> `3 passed`.
- Full tests: `pytest -ra` -> implementer `20 passed, 4 skipped in 16.02s`; QC rerun `20 passed, 4 skipped in 14.14s`.
- Restore smoke: synthetic tmp DB restore -> `success: true`, `integrity_check: ok`, table counts `{accounts: 1, transactions: 1, budgets: 0, import_batches: 0, category_rules: 0, ledger_categories: 0}`.
- MCP surface: `hermes mcp test jarca-ledger` -> connected, `Tools discovered: 45`, backup/status tools present, no restore tool listed.
## Sensitive Evidence Review
- No transaction row dumps, merchant/counterparty full dumps, account numbers, `raw_payload`, source financial files, or passwords were written into docs/tests/logs.
- Synthetic test data contains placeholder sentinel labels only and asserts those labels are not serialized in status evidence.
## QC Gate Result
- Verdict: PASS
- Reviewer: jarcaledgerqc
- Reviewed At (KST): 2026-06-27 10:33
- Summary: 검수관은 원본 운영 DB restore/direct mutation 없음, backup/status/list/hash 정책 정합성, restore MCP 비노출, synthetic restore smoke 성공, 민감정보 비노출, tests/docs evidence를 확인했다.
- Notes: 실제 backup MCP 호출은 운영 DB mutation을 피하기 위해 수행하지 않았고, service-level synthetic DB 테스트와 smoke로 대체 검증했다. 이는 비차단 참고사항이다.
## Blocking Issues
- None.
+1
View File
@@ -9,3 +9,4 @@
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
| --- | --- | --- | --- | --- | --- |
| BAT-001 | batch-lite | release-candidate | REQ-001 | dcy-001 | [bat-001_20260626_category-rule-ops](bat-001_20260626_category-rule-ops/) |
| BAT-002 | standard | release-candidate | REQ-002 | dcy-002 | [bat-002_20260627_operational-safety-backup-restore-validation-db](bat-002_20260627_operational-safety-backup-restore-validation-db/) |
+20 -1
View File
@@ -4,7 +4,7 @@
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
- Status: draft
- Owner: Jarca / project maintainer
- Last Updated (KST): 2026-06-26 22:24
- Last Updated (KST): 2026-06-27 10:17
- Source Discovery / Batch: bootstrap-baseline
## Project Summary
@@ -118,6 +118,22 @@
- Relationships:
- Compared against Transaction aggregates by report functions.
## Entity: LedgerBackupArtifact
- Source of Truth: filesystem under `/root/.local/share/jarca/backups/ledger/` by default
- Purpose: Timestamped SQLite backup artifact used for operational recovery and validation smoke checks.
- Lifecycle: created -> listed/status-checked -> retained/pruned by policy -> optionally restored by script/runbook after approval
- Key Evidence Fields:
- `path` :: text :: backup file path outside repository
- `size_bytes` :: integer :: file size metadata
- `mtime` :: text :: filesystem modification time
- `sha256` :: text :: file checksum
- `integrity_check` :: text :: SQLite integrity result
- `table_counts` / `aggregate_totals` :: metadata-only validation evidence
- State Rules:
- Keep backups from the most recent 30 days or newest 20 files.
- Backup artifacts and validation DB copies must not be committed to git.
- Restore execution is not a persisted MCP operation and requires script/runbook approval controls.
## Consistency Rules
- Actual cashflow reports include only categories whose `cashflow_kind` represents actual income or expense according to report semantics.
- Account movement summaries should be separate from actual household income/expense summaries.
@@ -128,6 +144,8 @@
## Persistence Notes
- Schema changes should be additive/idempotent unless a destructive migration is explicitly approved and backed up.
- Operational backup/status evidence may include only path, size, mtime, checksum, integrity, row counts, and aggregate totals.
- Validation DB copies should live outside the repository and should be selected explicitly via `JARCA_LEDGER_DB_PATH` when used.
- Passwords for encrypted imports are runtime-only inputs and are not part of the persisted model.
- Raw payloads may contain sensitive financial evidence and should not be committed to git.
@@ -138,3 +156,4 @@
## 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 정책 추가.
@@ -7,7 +7,7 @@
- 작성 시각(KST): 2026-06-26 22:38
- 마지막 갱신 시각(KST): 2026-06-26 23:31
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 후속 Discovery 참조: docs/discovery/dcy-002_20260627_operational-safety-backup-restore-validation-db.md
- 생성된 REQ 참조: REQ-001 ([docs/srs/Interface/req-001_category-rule-ops.md](../srs/Interface/req-001_category-rule-ops.md))
# 1. 계획 상태 요약
@@ -0,0 +1,228 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-002_20260627_operational-safety-backup-restore-validation-db
- 이슈명: Jarca Ledger 운영 안전장치 및 백업·복구·검증 DB 워크플로우 정립
- 작성 시각(KST): 2026-06-27 00:06
- 마지막 갱신 시각(KST): 2026-06-27 10:17
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-002 (`docs/srs/Non-Functional/req-002_operational-safety-backup-restore-validation-db.md`)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: 없음
- 해석:
- dcy-001의 BAT-001은 분류 규칙 운영 액션을 작은 단위로 안정화했고, 검증 과정에서 synthetic DB와 기본 DB mtime 확인을 대체 smoke evidence로 사용했다.
- 이번 Discovery는 이후 import, rollback, categorization, reconciliation 같은 위험 작업을 원본 운영 DB 훼손 없이 진행하기 위한 백업·복구·검증 DB 표준 워크플로우를 정의한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-001은 Streamlit 운영 UX/E2E와 분류·환급 workflow 안정화가 중심이고, 백업/복구/검증 DB 운용 체계 자체를 독립 성공 기준으로 다루지 않는다.
- `docs/runtime-flows.md`, `docs/data-model.md`에는 destructive migration 회피와 백업 승인 필요성이 gap/constraint로만 기록되어 있어 별도 Discovery로 운영 안전장치를 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 기본 SQLite DB는 `~/.local/share/jarca/ledger.sqlite3`이며 `JARCA_LEDGER_DB_PATH`로 대체 가능하다. `db.py`는 DB path resolution과 schema initialization을 담당하지만, 운영 DB 백업/복구 workflow는 아직 명시적 기능이나 runbook으로 정리되어 있지 않다.
- Streamlit Import 화면에는 import batch rollback UI가 있고, service/server에는 import rollback 도구가 있지만, rollback 전후 백업 생성, 검증용 DB copy, 원본 DB 미변경 증거, restore smoke check가 일관된 acceptance criteria로 묶여 있지 않다.
- 기대 상태
- 운영자는 import, rollback, 대량 categorization, reconciliation, schema 변경 전후에 표준화된 백업·검증 DB 절차를 따라 원본 운영 DB 훼손 위험을 낮출 수 있다.
- 렛저장인과 검수관은 REQ/BAT 수행 시 어떤 DB를 사용했는지, 원본 DB를 직접 건드리지 않았는지, 복구 가능한 백업이 있는지, smoke check가 통과했는지를 동일한 evidence 형식으로 검증할 수 있다.
- 이해관계자
- Jarca Ledger 운영자: 실제 가계부 운영 DB를 안전하게 유지해야 하는 사용자
- Jarca/Hermes Agent: DB 변경 작업과 검증을 보조하고 승인/중단 기준을 판단하는 자동화 주체
- 렛저장인/검수관: 구현과 verification gate를 분담하는 profile subagents
- 프로젝트 유지보수자: SQLite schema, service layer, Streamlit UI, 운영 문서를 유지하는 개발 주체
- 제약사항
- 실제 금융 DB, 원본 PDF/XLS/XLSX/CSV, 비밀번호, 전체 계좌식별정보는 테스트·문서·커밋에 노출하지 않는다.
- 파괴적 SQLite migration, 원본 운영 DB 직접 mutation, 대량 data rewrite는 사용자 명시 승인 없이는 진행하지 않는다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 새 KB/Toss/SeoulPay 자료를 import하기 전에 운영 DB 백업을 만들고, 검증 DB copy에서 preview/import/rollback smoke를 먼저 실행한다.
- 운영자가 분류 규칙 apply나 확인필요 큐 정리처럼 여러 거래를 바꿀 수 있는 작업을 하기 전에 원본 DB hash/mtime/row count를 기록하고, 변경 후 보고서와 DB 상태를 비교한다.
- 렛저장인이 batch 구현을 완료하면 검수관이 backup 존재 여부, 검증 DB 사용 여부, 원본 DB 미변경 증거, restore/health check 결과를 확인한 뒤 PASS/BLOCKED를 판단한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 운영 DB 변경 전 백업 생성, 검증 DB copy 생성, 복구 절차, smoke check를 하나의 표준 workflow로 정의한다.
- import, rollback, categorization, reconciliation, migration 같은 위험 작업의 사전/사후 체크리스트와 검수관 evidence 기준을 정의한다.
- `docs/runtime-flows.md`, `docs/data-model.md`, 필요 시 별도 runbook 또는 utility script를 갱신해 운영자가 재현 가능한 명령 단위 절차를 갖도록 한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `jarca_ledger_mcp/db.py`: 필요 시 백업/복사/검증 DB 경로 helper 또는 안전 검사 helper 추가 후보.
- `jarca_ledger_mcp/service.py` / `server.py`: 필요 시 백업/health/smoke 관련 additive tool 또는 service function 추가 후보.
- `dashboard/app.py`: 필요 시 운영 DB 위험 작업 전 안내, 검증 DB 사용 안내, import/rollback 전 안전 상태 표시 추가 후보.
- `scripts/` 또는 `tests/`: 백업/복구/검증 DB smoke를 재현하는 utility와 regression 추가 후보.
- 문서 영향:
- `docs/runtime-flows.md`: 운영 DB 백업·복구·검증 DB workflow와 위험 작업 flow constraint 갱신.
- `docs/data-model.md`: SQLite persistence/backup/restore policy와 destructive migration 승인 조건 갱신.
- `docs/interface-contract.md`: 새 MCP tool 또는 dashboard action이 생길 경우 additive interface contract 갱신.
- 후속 REQ/BAT 문서: backup, DB copy, restore smoke, QC evidence acceptance criteria 기록.
- 운영 영향:
- 실제 운영 DB를 대상으로 하는 작업은 백업 또는 검증 DB copy 확인 후 진행한다.
- 검수관은 batch verification에서 원본 DB 직접 mutation 여부와 복구 가능성을 evidence로 확인한다.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 운영 DB 변경 전 timestamped backup을 생성하고, 생성 경로·파일 크기·hash 또는 mtime 같은 식별 정보를 evidence로 남길 수 있어야 한다.
- FR-2: 검증용 DB copy 또는 synthetic fixture DB를 생성/선택하는 절차가 있어야 하며, Streamlit/MCP/pytest 검증이 해당 DB를 사용했음을 확인할 수 있어야 한다.
- FR-3: backup에서 restore하는 절차와 restore 후 `ledger_health_check` 또는 동등한 smoke check를 정의해야 한다.
- FR-4: import, rollback, categorization apply, reconciliation, migration 전후에 사용할 안전 체크리스트와 검수관 gate evidence 형식을 정의해야 한다.
- FR-5: 이번 Discovery가 확정하는 운영 flow는 `docs/runtime-flows.md`와 필요 시 `docs/data-model.md`에 반영되어야 한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 원본 운영 DB 직접 mutation은 기본 금지이며, 불가피한 경우 사용자 명시 승인과 백업 evidence가 필요하다.
- NFR-2: 백업/검증/복구 절차는 민감정보를 노출하지 않는 방식으로 문서화되어야 하며, 원본 금융 파일·비밀번호·전체 계좌식별정보를 커밋하지 않는다.
- NFR-3: 기존 MCP tool 성공 응답 shape와 `cashflow_kind` 의미론은 보존하고, 새 도구나 응답 필드는 additive 방식으로 설계한다.
- NFR-4: 절차는 렛저장인과 검수관이 자동화 또는 반자동으로 반복 실행할 수 있을 만큼 명령어 단위로 재현 가능해야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 운영 DB 백업 생성 정책, 검증 DB copy/synthetic fixture 정책, restore smoke check 정의.
- 위험 작업 전후 체크리스트와 검수관 QC evidence 기준 정의.
- 필요한 경우 작은 additive helper/script/tool 및 문서 갱신.
- dcy-001 후속 batch에서 사용할 수 있는 DB safety verification pattern 정립.
- Out of Scope
- 신규 금융기관 parser 추가.
- 파괴적 SQLite schema migration 또는 대량 데이터 rewrite 실행.
- CI/CD 전체 구축과 배포 서비스화.
- 원본 금융 파일 보관 정책 전체와 장기 암호화 저장소 설계.
- 운영 DB의 실제 복구 실행. 실제 복구는 별도 사용자 승인 후 수행한다.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 백업/복구 절차 자체가 잘못 설계되면 운영 DB 손상 시 복구할 수 없거나 잘못된 DB를 복구할 수 있다.
- 완화 방안: synthetic fixture 또는 운영 DB copy에서 먼저 backup/restore smoke를 검증하고, 원본 DB restore는 사용자 승인 전까지 실행하지 않는다.
- R-2 (High): 검증 과정에서 실수로 기본 운영 DB를 대상으로 import, rollback, apply 작업을 실행할 수 있다.
- 완화 방안: `JARCA_LEDGER_DB_PATH` 명시, DB path evidence, 원본 DB mtime/hash/row count 확인, 검수관 gate를 acceptance criteria에 포함한다.
- R-3 (Medium): 백업 파일이나 검증 DB copy에 민감 금융정보가 포함되어 커밋 또는 로그에 노출될 수 있다.
- 완화 방안: 백업/DB copy는 git ignored 경로에 두고, 보고서에는 파일 경로·크기·hash 등 최소 메타데이터만 남긴다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: Jarca Ledger는 현재 로컬 SQLite 중심 운영이며, 기본 운영 DB는 `/root/.local/share/jarca/ledger.sqlite3`이다.
- A-2: 운영 안전장치는 먼저 문서/스크립트/작은 additive helper 중심으로 구현하며, 파괴적 migration framework는 이번 범위에서 제외한다.
- A-3: 검수관은 실제 browser evidence가 불가능할 때도 AppTest, HTTP 200, service/DB smoke, hash/mtime evidence 같은 대체 증거를 평가할 수 있다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 운영 DB backup 파일의 기본 보관 위치와 보관 기간을 어디로 둘지 결정해야 한다.
- 상태: Answered
- 처리 방안: 기본 백업 위치는 `/root/.local/share/jarca/backups/ledger/`, 보관 정책은 최근 30일 또는 최근 20개 백업 유지로 확정한다.
- 종료 조건: REQ acceptance criteria 또는 runbook에 해당 위치와 retention 정책을 반영한다.
- OQ-2: 백업/복구 기능을 MCP tool로 노출할지, 로컬 script/runbook으로만 둘지 결정해야 한다.
- 상태: Answered
- 처리 방안: 혼합 방식으로 확정한다. Backup 생성과 backup 목록/status/hash 확인은 MCP tool 또는 script를 허용하되, restore 실행은 MCP tool로 노출하지 않고 script/runbook과 사용자 명시 승인으로 제한한다.
- 종료 조건: 후속 REQ 범위에 backup/status 노출과 restore 비노출 원칙을 명시한다.
- OQ-3: 운영 DB copy를 사용하는 검증에서 어떤 수준의 masking 또는 hash-only 보고를 요구할지 결정해야 한다.
- 상태: Answered
- 처리 방안: 검증 보고에는 DB 파일 경로, 파일 크기, mtime, sha256/checksum, 주요 table row count, category/report aggregate total, 테스트/health check 결과만 허용한다. 거래 원문, merchant/counterparty 전체 dump, 계좌번호, raw_payload, 원본 금융 파일 내용은 금지한다.
- 종료 조건: 후속 REQ 또는 verification template에 hash/mtime/row count/aggregate 중심 evidence 정책을 반영한다.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 운영 DB 백업 생성과 검증 DB copy 생성 절차가 명령어 또는 함수 단위로 재현 가능하고, 원본 DB를 직접 변경하지 않았다는 evidence를 남길 수 있다.
- S-2: backup restore 절차와 restore 후 health/smoke check가 synthetic fixture 또는 DB copy에서 성공한다.
- S-3: import, rollback, categorization apply, reconciliation, migration에 대한 위험 작업 체크리스트가 후속 REQ/BAT와 검수관 gate에서 사용할 수 있는 형식으로 정리된다.
- S-4: `docs/runtime-flows.md`와 필요 시 `docs/data-model.md`가 운영 안전장치 flow를 반영한다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: backup/copy command 실행 결과, 파일 존재·크기·hash/mtime, 원본 DB mtime/hash 비교 / 데이터 출처: synthetic fixture DB 또는 운영 DB copy metadata
- S-2 측정 방식: restore 후 `ledger_health_check`, sqlite integrity check, pytest 또는 service smoke 결과 확인 / 데이터 출처: 복구된 검증 DB와 command output
- S-3 측정 방식: 후속 REQ acceptance criteria와 BAT verification checklist에 위험 작업 evidence 항목 존재 여부 확인 / 데이터 출처: `docs/srs/`, `docs/batches/`, 검수관 gate report
- S-4 측정 방식: baseline 문서 diff와 change log 확인 / 데이터 출처: `docs/runtime-flows.md`, `docs/data-model.md`, 필요 시 `docs/interface-contract.md`
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 운영 안전장치/백업/복구/검증 DB에 집중하고 신규 parser, CI/CD, 배포 서비스화, 파괴적 migration은 제외하는지 확인한다.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: synthetic fixture 또는 DB copy 기반의 backup/copy/restore/smoke evidence 방식이 충분한지 확인한다.
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 원본 운영 DB mutation과 잘못된 restore 리스크에 대해 자르카/렛저장인/검수관의 책임 경계를 지정한다.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 사용자가 Discovery 내용을 확인하고 REQ draft 진행 가능 여부를 명시한다.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-002_20260627_operational-safety-backup-restore-validation-db.md
- 참조 문서:
- docs/discovery/dcy-001_20260626_streamlit-ux-e2e-categorization-refund-workflows.md
- docs/runtime-flows.md
- docs/data-model.md
- docs/interface-contract.md
- docs/batches/bat-001_20260626_category-rule-ops/verification.md
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- 운영 DB backup 기본 저장 위치는 `/root/.local/share/jarca/backups/ledger/`, 보관 정책은 최근 30일 또는 최근 20개 백업 유지로 확정한다.
- 백업/복구 인터페이스는 혼합 방식으로 확정한다. Backup 생성과 목록/status/hash 확인은 MCP tool 또는 script를 허용하고, restore 실행은 MCP tool로 노출하지 않으며 script/runbook과 사용자 명시 승인으로 제한한다.
- 운영 DB copy 검증 보고에는 파일 경로, 크기, mtime, checksum, 주요 table row count, category/report aggregate, 테스트/health check 결과만 허용하고 거래 상세·계좌번호·raw_payload·원본 금융 파일 내용은 금지한다.
## CONFIRM
- 이번 Discovery는 운영 안전장치·백업·복구·검증 DB workflow로 한정하고 신규 parser, CI/CD, 배포 서비스화, 파괴적 migration은 제외하는 것으로 확정한다.
- 사용자 승인에 따라 REQ draft로 넘긴다.
## DATA
- 1차 검증은 synthetic fixture DB를 우선 사용하고, 필요 시 운영 DB copy를 repo 밖 임시/백업 디렉터리에 생성해 사용한다. 운영 DB copy evidence는 hash, mtime, row count, aggregate만 기록한다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: 사용자 승인으로 백업 위치/보관 기간, 백업·복구 인터페이스 수준, 검증 DB evidence 수준, 범위 경계가 확정되었으므로 REQ 초안 작성으로 넘긴다.
- Ready for REQ Drafting: true
- Confirmed By: 사용자 + Jarca
- Confirmed At (KST): 2026-06-27 10:12
+1
View File
@@ -7,3 +7,4 @@
| ID | 날짜 | 이슈명 | 문서 | 상태 |
|---|---|---|---|---|
| dcy-001_20260626_streamlit-ux-e2e-categorization-refund-workflows | 20260626 | Streamlit 운영 UX/E2E 검증 및 분류·환급 워크플로우 안정화 | [Discovery](./dcy-001_20260626_streamlit-ux-e2e-categorization-refund-workflows.md) | approved |
| dcy-002_20260627_operational-safety-backup-restore-validation-db | 20260627 | Jarca Ledger 운영 안전장치 및 백업·복구·검증 DB 워크플로우 정립 | [Discovery](./dcy-002_20260627_operational-safety-backup-restore-validation-db.md) | confirmed |
+9 -3
View File
@@ -4,7 +4,7 @@
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
- Status: draft
- Owner: Jarca / project maintainer
- Last Updated (KST): 2026-06-26 22:24
- Last Updated (KST): 2026-06-27 10:17
- Source Discovery / Batch: bootstrap-baseline
## Project Summary
@@ -21,7 +21,8 @@
- Stability: internal but should preserve existing success response shapes for compatibility.
### Representative Tool Groups
- Health: `ledger_health_check`
- Health: `ledger_health_check`, `get_ledger_db_status`
- Operational backup: `create_ledger_backup`, `list_ledger_backups`, `get_ledger_backup_status` (restore is intentionally not exposed as MCP)
- Accounts: `create_ledger_account`, `list_ledger_accounts`
- 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
@@ -36,6 +37,7 @@
### Outputs
- Python dictionary payloads containing success data, counts, records, previews, reports, or error details.
- Backup/status tools return metadata-only DB evidence: path, size, mtime, sha256, integrity check, table row counts, and aggregate totals.
- Existing success shape compatibility is preferred; new fields should be additive where possible.
### Errors
@@ -46,7 +48,8 @@
### Compatibility Rules
- Preserve existing success response shapes unless a migration/REQ explicitly approves a breaking change.
- Do not write secrets or raw passwords into persistent output, skill files, or committed documentation.
- Additive backup/status/list/hash tools are allowed; restore execution must not be added as an MCP tool.
- Do not write secrets, raw passwords, raw_payload, full merchant/counterparty dumps, or full account identifiers into persistent output, skill files, or committed documentation.
- Source evidence should remain auditable through raw fields and import batch records.
## Interface: Streamlit Dashboard
@@ -90,6 +93,8 @@
- All import flows should support preview before mutation where implemented.
- Dedupe should use deterministic source/external IDs or same-account/date/type/amount/merchant evidence.
- Import batch metadata should support rollback and audit.
- 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.
- Financial data and passwords are sensitive runtime inputs and should not be committed.
## Current Gaps / Planned Changes
@@ -99,3 +104,4 @@
## 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 기준으로 backup/status/list/hash MCP contract와 restore 비노출 정책 추가.
+100
View File
@@ -0,0 +1,100 @@
# Ledger Backup / Restore / Validation DB Runbook
## Purpose
This runbook defines the approved operational safety workflow from dcy-002 / REQ-002. It is designed for Jarca Ledger local SQLite operation and for Hermes agents working under `jarcaledgerdev`/QC profiles.
## Policy Summary
- Default backup directory: `/root/.local/share/jarca/backups/ledger/`
- Retention: keep backups from the most recent 30 days OR the newest 20 backup files.
- Backup creation/list/status/hash may be exposed as additive MCP tools or scripts.
- Restore execution must not be exposed as an MCP tool.
- Restore execution requires script/runbook use and explicit user approval before touching a real operating DB.
- Preferred validation DB: synthetic fixture DB. If an operating DB copy is needed, keep it outside the repository and report only metadata evidence.
## Allowed Evidence
- DB path
- File size
- mtime
- sha256/checksum
- SQLite `PRAGMA integrity_check` result
- Major table row counts
- Category/report aggregate totals
- Test, health check, or smoke command result
## Forbidden Evidence
- Full transaction row dumps
- Full merchant/counterparty dumps
- Full account identifiers/account numbers
- `raw_payload`
- Original financial file content
- Passwords or encrypted file secrets
## Create Backup
From Python/service:
```bash
cd /root/projects/jarca-ledger-mcp
source .venv/bin/activate
python - <<'PY'
from jarca_ledger_mcp import service
print(service.create_ledger_backup())
PY
```
MCP tool equivalent is allowed:
- `create_ledger_backup`
- `list_ledger_backups`
- `get_ledger_backup_status`
- `get_ledger_db_status`
## Validation DB Smoke
Prefer synthetic DB:
```bash
cd /root/projects/jarca-ledger-mcp
source .venv/bin/activate
pytest tests/test_backup_workflow.py
```
For a repo-external validation DB copy, set `JARCA_LEDGER_DB_PATH` to the copy path before running smoke tests or service checks. Do not commit that DB.
## Restore Smoke Script
Restore is script-only and not an MCP operation.
```bash
cd /root/projects/jarca-ledger-mcp
source .venv/bin/activate
python scripts/ledger_restore_smoke.py \
--backup /root/.local/share/jarca/backups/ledger/<backup>.sqlite3 \
--target /tmp/jarca-ledger-restore-smoke.sqlite3
```
If the target already exists, overwriting requires explicit approval controls:
```bash
python scripts/ledger_restore_smoke.py \
--backup /root/.local/share/jarca/backups/ledger/<backup>.sqlite3 \
--target /tmp/jarca-ledger-restore-smoke.sqlite3 \
--allow-overwrite \
--approval-token RESTORE_APPROVED
```
Do not point `--target` at `/root/.local/share/jarca/ledger.sqlite3` unless the user has explicitly approved a real operating DB restore in the current task.
## High-Risk Work Checklist
Before import, rollback, categorization apply, reconciliation apply, migration, or any broad DB mutation:
1. Capture `get_ledger_db_status` for the selected DB.
2. Create a backup or confirm an acceptable recent backup.
3. Prefer running the risky operation against a synthetic fixture or repo-external validation copy first.
4. Capture post-operation status/report aggregates.
5. Record only allowed metadata evidence.
6. Ask QC to confirm no direct operating DB mutation occurred unless explicitly approved.
## QC Gate Checklist
QC should verify:
- No source operating DB direct mutation without approval.
- Backup/status/list/hash tools exist and restore MCP tool does not exist.
- Restore path is script/runbook only and approval-gated for overwrite.
- Synthetic fixture or repo-external DB copy smoke evidence exists.
- Docs/tests/logs do not expose forbidden sensitive data.
- Tests and baseline docs are updated.
+12 -1
View File
@@ -4,7 +4,7 @@
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
- Status: draft
- Owner: Jarca / project maintainer
- Last Updated (KST): 2026-06-26 22:24
- Last Updated (KST): 2026-06-27 10:17
- Source Discovery / Batch: bootstrap-baseline
- Primary Runtime: mixed
@@ -63,11 +63,21 @@
3. Matched self-transfers become `이체 / 본인계좌이동`; investment movements become `이체 / 투자이동`; ambiguous rows remain `확인필요`.
4. Cancel/refund rows can be linked to original expense rows; memo tags preserve manual link evidence.
## Flow: `Operational backup, validation DB, and restore smoke`
1. Operator captures non-sensitive DB status evidence with `get_ledger_db_status`: path, size, mtime, sha256, integrity check, major table row counts, and aggregate totals.
2. Operator creates a timestamped backup in `/root/.local/share/jarca/backups/ledger/` with `create_ledger_backup`; retention keeps the most recent 30 days or newest 20 files.
3. Risky import, rollback, categorization apply, reconciliation, or migration work is tested first against a synthetic fixture DB or repo-external validation DB copy selected through `JARCA_LEDGER_DB_PATH`.
4. Backup list/status/hash checks use `list_ledger_backups` and `get_ledger_backup_status`.
5. Restore execution is not an MCP flow. Restore smoke uses `scripts/ledger_restore_smoke.py` or the runbook and requires explicit approval before overwriting an existing target DB.
6. Verification evidence must not include full transaction dumps, full merchant/counterparty dumps, account identifiers, raw_payload, original financial file contents, or passwords.
## Flow Constraints
- Preview before broad import or categorization mutation where supported.
- Do not include `transfer`, `investment`, `exclude`, or unresolved rows in actual household spending totals unless a report explicitly asks for account movement.
- Preserve raw import evidence for audit and future reclassification.
- Avoid destructive migrations without a backup/approval plan.
- Before broad DB mutation, capture DB status evidence and create or confirm a recent backup.
- Restore execution is script/runbook only and must not be exposed as an MCP tool.
- Sensitive passwords are runtime inputs and must not be committed.
## Current Gaps / Planned Changes
@@ -82,3 +92,4 @@
## 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 기준으로 운영 backup/status/list/hash 및 restore 제한 flow 추가.
@@ -0,0 +1,45 @@
# REQ-002: 운영 안전 백업·복구·검증 DB 워크플로우
## Metadata
- ID: REQ-002
- Type: Non-Functional
- Status: Implemented
- Priority: High
- Owner: 렛저장인
- Source Discovery: dcy-002_20260627_operational-safety-backup-restore-validation-db
- Created At (KST): 2026-06-27 10:17
- Approved At (KST): 2026-06-27 10:17
## Intent
원본 Jarca Ledger 운영 DB를 직접 훼손하지 않고 import, rollback, categorization, reconciliation, migration 검증을 반복할 수 있도록 백업 생성, 검증 DB 선택, 복구 smoke, QC evidence 기준을 표준화한다.
## Requirement
시스템은 운영 DB의 timestamped backup을 생성·조회·상태 확인할 수 있는 additive service/MCP 인터페이스와, restore를 MCP로 노출하지 않는 script/runbook 기반 절차를 제공해야 한다. 모든 검증 evidence는 DB 파일 경로, 크기, mtime, sha256/checksum, 주요 table row count, aggregate total, health/test 결과처럼 민감정보가 없는 메타데이터로 제한해야 한다.
## Acceptance Criteria
1. Backup 생성은 기본 위치 `/root/.local/share/jarca/backups/ledger/`에 timestamped SQLite 파일을 만들고, source DB와 backup DB의 path, size, mtime, sha256, integrity_check, 주요 table row count를 반환한다.
2. Backup 보관 정책은 최근 30일 또는 최근 20개 backup 유지이며, retention 실행 결과로 pruned path 목록을 메타데이터로 확인할 수 있다.
3. Backup 목록/status/hash 확인은 additive service/MCP tool로 제공되며 기존 MCP success response shape를 깨지 않는다.
4. Restore 실행은 MCP tool로 노출하지 않고 `scripts/ledger_restore_smoke.py`와 runbook으로만 제공하며, 기존 target overwrite에는 명시 승인 token이 필요하다.
5. 검증은 synthetic fixture DB를 우선 사용하고, 운영 DB copy가 필요한 경우 repo 밖 경로에서 hash/mtime/row count/aggregate만 evidence로 남긴다.
6. `docs/runtime-flows.md`, `docs/data-model.md`, `docs/interface-contract.md`는 backup/status/list/hash/restore 제한 정책과 민감정보 금지 evidence 기준을 반영한다.
7. 테스트는 synthetic DB에서 backup 생성, list, status/hash, retention, restore smoke 또는 health/status check를 검증하고 원본 운영 DB mutation 없이 통과한다.
8. 검수관 QC gate는 원본 운영 DB 직접 mutation 여부, restore MCP 노출 금지, 민감정보 노출 금지, synthetic fixture 또는 DB copy 기반 smoke evidence를 확인해 PASS/PASS_WITH_NOTES/BLOCKED 판정을 기록한다.
## Impacted Area
- `jarca_ledger_mcp/service.py`: DB status, backup 생성, backup 목록/status helper 추가.
- `jarca_ledger_mcp/server.py`: restore 제외, backup/status/list/hash additive MCP tool 추가.
- `scripts/ledger_restore_smoke.py`: 사용자 승인 기반 restore smoke helper.
- `tests/test_backup_workflow.py`: synthetic DB 기반 regression.
- `docs/runtime-flows.md`, `docs/data-model.md`, `docs/interface-contract.md`, `docs/runbooks/ledger-backup-restore.md`: 운영 안전 flow와 interface 계약 갱신.
## Notes
- 원본 운영 DB 직접 mutation, 실제 운영 DB restore, 파괴적 migration, 대량 data rewrite는 사용자 명시 승인 전까지 금지한다.
- Restore는 intentionally non-MCP surface다. 자동화가 필요해도 MCP tool로 추가하지 않는다.
- 거래 원문, merchant/counterparty 전체 dump, 계좌번호, raw_payload, 원본 금융 파일 내용은 docs/tests/logs evidence에 포함하지 않는다.
- 사용자 기본안이 승인되었으므로 REQ 상태는 Proposed 생성 직후 자르카 대리 승인 원칙에 따라 Approved로 전환했다.
## Change Log
- 2026-06-27 10:17 KST: dcy-002에서 REQ-002 초안 생성.
- 2026-06-27 10:17 KST: 사용자 승인 기본안에 따라 REQ-002를 Approved로 전환.
- 2026-06-27 10:33 KST: BAT-002 verification 및 jarcaledgerqc PASS 근거로 REQ-002를 Implemented로 전환.
+4 -1
View File
@@ -33,7 +33,7 @@
## Next Requirement ID
- Current: `REQ-002`
- Current: `REQ-003`
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
## Requirement Register
@@ -41,8 +41,11 @@
| ID | Title | Type | Status | Priority | Owner | Link |
| --- | --- | --- | --- | --- | --- | --- |
| REQ-001 | 분류 규칙 운영 액션의 DB 경로 일관성 및 삭제 노출 | Interface | Implemented | High | 렛저장인 | [req-001_category-rule-ops.md](Interface/req-001_category-rule-ops.md) |
| REQ-002 | 운영 안전 백업·복구·검증 DB 워크플로우 | Non-Functional | Implemented | High | 렛저장인 | [req-002_operational-safety-backup-restore-validation-db.md](Non-Functional/req-002_operational-safety-backup-restore-validation-db.md) |
## Recent Change Log Summary
- 2026-06-26 23:31 KST: dcy-001 후속 첫 구현 범위로 REQ-001을 Approved 상태로 등록.
- 2026-06-26 23:41 KST: BAT-001 검증 및 검수관 PASS_WITH_NOTES 근거로 REQ-001을 Implemented 상태로 전환.
- 2026-06-27 10:17 KST: dcy-002 후속 운영 안전장치 범위로 REQ-002를 생성하고 사용자 승인 기본안에 따라 Approved 상태로 등록.
- 2026-06-27 10:33 KST: BAT-002 verification 및 jarcaledgerqc PASS 근거로 REQ-002를 Implemented 상태로 전환.
+20
View File
@@ -14,6 +14,26 @@ def ledger_health_check() -> dict:
"""Verify the Jarca Ledger MCP server and SQLite database are reachable."""
return service.health_check()
@mcp.tool()
def get_ledger_db_status(include_hash: bool = True, include_integrity: bool = True) -> dict:
"""Return non-sensitive DB status evidence: path, size, mtime, checksum, table row counts, and aggregate totals."""
return service.get_ledger_db_status(include_hash=include_hash, include_integrity=include_integrity)
@mcp.tool()
def create_ledger_backup(backup_dir: str = "", retention_days: int = 30, retention_count: int = 20) -> dict:
"""Create a timestamped ledger DB backup. Restore is intentionally not exposed as an MCP tool."""
return service.create_ledger_backup(backup_dir=backup_dir or None, retention_days=retention_days, retention_count=retention_count)
@mcp.tool()
def list_ledger_backups(backup_dir: str = "", include_hash: bool = False, limit: int = 50) -> dict:
"""List ledger DB backups with metadata-only evidence."""
return service.list_ledger_backups(backup_dir=backup_dir or None, include_hash=include_hash, limit=limit)
@mcp.tool()
def get_ledger_backup_status(file_path: str, include_hash: bool = True, include_integrity: bool = True) -> dict:
"""Return metadata-only status and integrity evidence for one backup file."""
return service.get_ledger_backup_status(file_path, include_hash=include_hash, include_integrity=include_integrity)
@mcp.tool()
def create_ledger_account(name: str, type: str = "bank", institution: str = "", currency: str = "KRW", active: bool = True, memo: str = "", account_number_masked: str = "", holder_name: str = "", last_balance: int | None = None, last_synced_at: str = "") -> dict:
"""Create an account/card/cash/investment/loan ledger account."""
+161 -1
View File
@@ -4,7 +4,8 @@ import csv
import hashlib
import json
import re
from datetime import date, datetime
import sqlite3
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any
from uuid import uuid4
@@ -31,6 +32,12 @@ END)"""
CROSS_TRANSFER_MATCH_TAG = "cross_transfer_match:"
CROSS_TRANSFER_REVIEW_TAG = "cross_transfer_review:"
DEFAULT_LEDGER_BACKUP_DIR = Path.home() / ".local" / "share" / "jarca" / "backups" / "ledger"
BACKUP_RESTORE_POLICY = {
"mcp_restore_exposed": False,
"restore_requires_explicit_user_approval": True,
"restore_entrypoint": "scripts/ledger_restore_smoke.py 또는 docs/runbooks/ledger-backup-restore.md",
}
def _normalize_counterparty_for_match(value: str) -> str:
@@ -134,6 +141,159 @@ def health_check(db_path: str | None = None) -> dict[str, Any]:
tx_count = conn.execute("SELECT COUNT(*) FROM transactions WHERE status='active'").fetchone()[0]
return ok(server="jarca-ledger-mcp", db_path=path, schema_version=schema_version, active_transactions=tx_count)
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _file_metadata(path: Path, include_hash: bool = True) -> dict[str, Any]:
stat = path.stat()
metadata: dict[str, Any] = {
"path": str(path.resolve()),
"size_bytes": stat.st_size,
"mtime": datetime.fromtimestamp(stat.st_mtime).replace(microsecond=0).isoformat(),
}
if include_hash:
metadata["sha256"] = _sha256_file(path)
return metadata
def _readonly_sqlite_connection(path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
return conn
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"):
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])
return counts
def _safe_monthly_aggregates(conn: sqlite3.Connection) -> dict[str, Any]:
current_month = date.today().strftime("%Y-%m")
start, next_month = _month_range(current_month)
rows = conn.execute(
f"""SELECT {CASHFLOW_KIND_SQL} AS cashflow_kind, SUM(t.amount) AS total, COUNT(*) AS count
FROM transactions t
LEFT JOIN ledger_categories lc ON lc.active=1 AND t.category=lc.category AND t.subcategory=lc.subcategory
WHERE t.status='active' AND t.transaction_date>=? AND t.transaction_date<?
GROUP BY {CASHFLOW_KIND_SQL}""",
(start, next_month),
).fetchall()
return {
"month": current_month,
"by_cashflow_kind": {
str(row["cashflow_kind"]): {"amount": int(row["total"] or 0), "count": int(row["count"] or 0)}
for row in rows
},
}
def _ledger_db_status_for_path(path: Path, include_hash: bool = True, include_integrity: bool = True) -> dict[str, Any]:
status: dict[str, Any] = {"db_path": str(path.resolve()), "exists": path.exists()}
if not path.exists():
return status
status.update(_file_metadata(path, include_hash=include_hash))
try:
with _readonly_sqlite_connection(path) as conn:
if include_integrity:
status["integrity_check"] = str(conn.execute("PRAGMA integrity_check").fetchone()[0])
status["table_counts"] = _safe_table_counts(conn)
status["aggregate_totals"] = _safe_monthly_aggregates(conn)
except Exception as e:
status["integrity_check"] = "error"
status["error"] = str(e)
return status
def get_ledger_db_status(db_path: str | None = None, include_hash: bool = True, include_integrity: bool = True) -> dict[str, Any]:
"""Return non-sensitive metadata evidence for the selected ledger DB."""
path = resolve_db_path(db_path)
if not path.exists():
return fail("not_found", f"ledger DB not found: {path}", db_path=str(path), exists=False)
return ok(**_ledger_db_status_for_path(path, include_hash=include_hash, include_integrity=include_integrity))
def _resolve_backup_dir(backup_dir: str | None = None) -> Path:
return Path(backup_dir).expanduser().resolve() if backup_dir else DEFAULT_LEDGER_BACKUP_DIR.resolve()
def _backup_file_metadata(path: Path, include_hash: bool = False) -> dict[str, Any]:
metadata = _file_metadata(path, include_hash=include_hash)
metadata["name"] = path.name
metadata["restore_policy"] = BACKUP_RESTORE_POLICY
return metadata
def _apply_backup_retention(backup_dir: Path, retention_days: int = 30, retention_count: int = 20) -> list[str]:
backups = sorted(backup_dir.glob("*.sqlite3"), key=lambda p: p.stat().st_mtime, reverse=True)
keep_count = max(1, int(retention_count))
cutoff = datetime.now() - timedelta(days=max(0, int(retention_days)))
pruned: list[str] = []
for idx, path in enumerate(backups):
mtime = datetime.fromtimestamp(path.stat().st_mtime)
should_keep = idx < keep_count or mtime >= cutoff
if should_keep:
continue
path.unlink()
pruned.append(str(path.resolve()))
return pruned
def create_ledger_backup(db_path: str | None = None, backup_dir: str | None = None, retention_days: int = 30, retention_count: int = 20) -> dict[str, Any]:
"""Create a timestamped SQLite backup and return metadata-only evidence."""
source_path = resolve_db_path(db_path)
if not source_path.exists():
return fail("not_found", f"ledger DB not found: {source_path}", db_path=str(source_path), exists=False)
backup_root = _resolve_backup_dir(backup_dir)
backup_root.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
backup_path = backup_root / f"ledger_{timestamp}.sqlite3"
try:
# Use SQLite's online backup API so WAL-mode databases are copied consistently.
with sqlite3.connect(f"file:{source_path}?mode=ro", uri=True) as source_conn:
with sqlite3.connect(backup_path) as backup_conn:
source_conn.backup(backup_conn)
except Exception:
if backup_path.exists():
backup_path.unlink()
raise
source_status = _ledger_db_status_for_path(source_path, include_hash=True, include_integrity=True)
backup_status = _ledger_db_status_for_path(backup_path, include_hash=True, include_integrity=True)
pruned_paths = _apply_backup_retention(backup_root, retention_days=retention_days, retention_count=retention_count)
return ok(
source_db=source_status,
backup=backup_status,
backup_dir=str(backup_root),
retention={"retention_days": int(retention_days), "retention_count": int(retention_count), "pruned_paths": pruned_paths},
restore_policy=BACKUP_RESTORE_POLICY,
)
def list_ledger_backups(backup_dir: str | None = None, include_hash: bool = False, limit: int = 50) -> dict[str, Any]:
backup_root = _resolve_backup_dir(backup_dir)
if not backup_root.exists():
return ok(backup_dir=str(backup_root), backups=[], count=0, restore_policy=BACKUP_RESTORE_POLICY)
max_items = max(1, min(int(limit), 500))
files = sorted(backup_root.glob("*.sqlite3"), key=lambda p: p.stat().st_mtime, reverse=True)[:max_items]
backups = [_backup_file_metadata(path, include_hash=include_hash) for path in files]
return ok(backup_dir=str(backup_root), backups=backups, count=len(backups), restore_policy=BACKUP_RESTORE_POLICY)
def get_ledger_backup_status(file_path: str, include_hash: bool = True, include_integrity: bool = True) -> dict[str, Any]:
path = Path(file_path).expanduser().resolve()
if not path.exists():
return fail("not_found", f"backup file not found: {path}", path=str(path), exists=False)
return ok(backup=_ledger_db_status_for_path(path, include_hash=include_hash, include_integrity=include_integrity), restore_policy=BACKUP_RESTORE_POLICY)
def create_ledger_account(name: str, type: str = "bank", institution: str = "", currency: str = "KRW", active: bool = True, memo: str = "", account_number_masked: str = "", holder_name: str = "", last_balance: int | None = None, last_synced_at: str = "", db_path: str | None = None) -> dict[str, Any]:
if not name.strip():
return fail("validation_error", "account name is required")
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Restore/smoke helper for Jarca Ledger backups.
This script is intentionally not exposed as an MCP tool. It restores a backup file
into an explicitly provided target DB path, then runs SQLite integrity and Jarca
ledger health/status checks. Use a synthetic fixture or repo-external validation
DB first. Do not point --target at the live operating DB unless the user has
explicitly approved the restore.
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from pathlib import Path
from jarca_ledger_mcp import service
APPROVAL_TEXT = "RESTORE_APPROVED"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Restore a ledger backup into a target DB and run smoke checks.")
parser.add_argument("--backup", required=True, help="Backup sqlite3 file to restore from")
parser.add_argument("--target", required=True, help="Target sqlite3 path to create/replace")
parser.add_argument(
"--approval-token",
default="",
help=f"Required token for overwrite/live restore. Must be {APPROVAL_TEXT!r}.",
)
parser.add_argument("--allow-overwrite", action="store_true", help="Allow replacing an existing target DB")
return parser.parse_args()
def main() -> int:
args = parse_args()
backup = Path(args.backup).expanduser().resolve()
target = Path(args.target).expanduser().resolve()
if not backup.exists():
print(json.dumps({"success": False, "error_code": "not_found", "error": f"backup not found: {backup}"}, ensure_ascii=False))
return 1
if target.exists() and not args.allow_overwrite:
print(json.dumps({"success": False, "error_code": "target_exists", "error": f"target exists: {target}"}, ensure_ascii=False))
return 2
if target.exists() and args.approval_token != APPROVAL_TEXT:
print(json.dumps({"success": False, "error_code": "approval_required", "error": f"overwrite requires --approval-token {APPROVAL_TEXT}"}, ensure_ascii=False))
return 3
before = service.get_ledger_backup_status(str(backup))
if not before.get("success") or before.get("backup", {}).get("integrity_check") != "ok":
print(json.dumps({"success": False, "error_code": "invalid_backup", "backup_status": before}, ensure_ascii=False))
return 4
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(backup, target)
health = service.health_check(str(target))
status = service.get_ledger_db_status(str(target))
result = {
"success": bool(health.get("success") and status.get("success") and status.get("integrity_check") == "ok"),
"backup": before.get("backup", {}),
"target": status,
"health_check": health,
"restore_policy": service.BACKUP_RESTORE_POLICY,
}
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["success"] else 5
if __name__ == "__main__":
sys.exit(main())
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
from pathlib import Path
from jarca_ledger_mcp import service
def test_db_status_reports_metadata_without_sensitive_rows(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
service.create_ledger_transaction(
"2026-06-01",
"expense",
12_000,
category="식비",
subcategory="카페",
merchant="SENTINEL_MERCHANT",
counterparty="SENTINEL_COUNTERPARTY",
raw_payload="SENTINEL_RAW_PAYLOAD",
db_path=db_path,
)
status = service.get_ledger_db_status(db_path=db_path)
assert status["success"] is True
assert status["db_path"] == str(Path(db_path).resolve())
assert status["exists"] is True
assert status["sha256"]
assert status["integrity_check"] == "ok"
assert status["table_counts"]["transactions"] == 1
dumped = repr(status)
assert "SENTINEL_MERCHANT" not in dumped
assert "SENTINEL_COUNTERPARTY" not in dumped
assert "SENTINEL_RAW_PAYLOAD" not in dumped
def test_create_list_and_status_ledger_backup_with_retention(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
backup_dir = str(tmp_path / "backups")
service.create_ledger_account("테스트 계좌", db_path=db_path)
before = service.get_ledger_db_status(db_path=db_path)
created = service.create_ledger_backup(db_path=db_path, backup_dir=backup_dir, retention_days=30, retention_count=20)
assert created["success"] is True
assert created["source_db"]["sha256"] == before["sha256"]
assert created["backup"]["path"].startswith(str(Path(backup_dir).resolve()))
assert Path(created["backup"]["path"]).exists()
assert created["backup"]["sha256"]
assert created["backup"]["integrity_check"] == "ok"
assert created["backup"]["table_counts"] == created["source_db"]["table_counts"]
assert created["restore_policy"]["mcp_restore_exposed"] is False
listed = service.list_ledger_backups(backup_dir=backup_dir, include_hash=True)
assert listed["success"] is True
assert listed["count"] == 1
assert listed["backups"][0]["sha256"] == created["backup"]["sha256"]
backup_status = service.get_ledger_backup_status(created["backup"]["path"])
assert backup_status["success"] is True
assert backup_status["backup"]["integrity_check"] == "ok"
assert backup_status["backup"]["table_counts"]["accounts"] == 1
def test_backup_retention_keeps_recent_count_and_prunes_older_extras(tmp_path):
db_path = str(tmp_path / "ledger.sqlite3")
backup_dir = tmp_path / "backups"
service.create_ledger_transaction("2026-06-01", "income", 1000, category="수입", db_path=db_path)
for i in range(3):
old = backup_dir / f"ledger_20200101000{i}000000.sqlite3"
old.parent.mkdir(parents=True, exist_ok=True)
old.write_text("old", encoding="utf-8")
created = service.create_ledger_backup(db_path=db_path, backup_dir=str(backup_dir), retention_days=0, retention_count=2)
assert created["success"] is True
remaining = sorted(p.name for p in backup_dir.glob("*.sqlite3"))
assert len(remaining) == 2
assert Path(created["backup"]["path"]).name in remaining
assert len(created["retention"]["pruned_paths"]) == 2