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:
+42
@@ -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`.
|
||||||
+62
@@ -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.
|
||||||
+33
@@ -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.
|
||||||
+34
@@ -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.
|
||||||
+111
@@ -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.
|
||||||
@@ -9,3 +9,4 @@
|
|||||||
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
|
| 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-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
@@ -4,7 +4,7 @@
|
|||||||
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
||||||
- Status: draft
|
- Status: draft
|
||||||
- Owner: Jarca / project maintainer
|
- 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
|
- Source Discovery / Batch: bootstrap-baseline
|
||||||
|
|
||||||
## Project Summary
|
## Project Summary
|
||||||
@@ -118,6 +118,22 @@
|
|||||||
- Relationships:
|
- Relationships:
|
||||||
- Compared against Transaction aggregates by report functions.
|
- 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
|
## Consistency Rules
|
||||||
- Actual cashflow reports include only categories whose `cashflow_kind` represents actual income or expense according to report semantics.
|
- 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.
|
- Account movement summaries should be separate from actual household income/expense summaries.
|
||||||
@@ -128,6 +144,8 @@
|
|||||||
|
|
||||||
## Persistence Notes
|
## Persistence Notes
|
||||||
- Schema changes should be additive/idempotent unless a destructive migration is explicitly approved and backed up.
|
- 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.
|
- 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.
|
- Raw payloads may contain sensitive financial evidence and should not be committed to git.
|
||||||
|
|
||||||
@@ -138,3 +156,4 @@
|
|||||||
|
|
||||||
## Change Log
|
## Change Log
|
||||||
- 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation.
|
- 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 22:38
|
||||||
- 마지막 갱신 시각(KST): 2026-06-26 23:31
|
- 마지막 갱신 시각(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))
|
- 생성된 REQ 참조: REQ-001 ([docs/srs/Interface/req-001_category-rule-ops.md](../srs/Interface/req-001_category-rule-ops.md))
|
||||||
|
|
||||||
# 1. 계획 상태 요약
|
# 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
|
||||||
@@ -7,3 +7,4 @@
|
|||||||
| ID | 날짜 | 이슈명 | 문서 | 상태 |
|
| 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-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 |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
||||||
- Status: draft
|
- Status: draft
|
||||||
- Owner: Jarca / project maintainer
|
- 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
|
- Source Discovery / Batch: bootstrap-baseline
|
||||||
|
|
||||||
## Project Summary
|
## Project Summary
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
- Stability: internal but should preserve existing success response shapes for compatibility.
|
- Stability: internal but should preserve existing success response shapes for compatibility.
|
||||||
|
|
||||||
### Representative Tool Groups
|
### 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`
|
- Accounts: `create_ledger_account`, `list_ledger_accounts`
|
||||||
- Transactions: `create_ledger_transaction`, `list_ledger_transactions`, `update_ledger_transaction`, `delete_ledger_transaction`
|
- 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
|
- Budgets/reports: `create_ledger_budget`, `list_ledger_budgets`, monthly/category/fixed/budget/investable reports
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
|
|
||||||
### Outputs
|
### Outputs
|
||||||
- Python dictionary payloads containing success data, counts, records, previews, reports, or error details.
|
- 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.
|
- Existing success shape compatibility is preferred; new fields should be additive where possible.
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
@@ -46,7 +48,8 @@
|
|||||||
|
|
||||||
### Compatibility Rules
|
### Compatibility Rules
|
||||||
- Preserve existing success response shapes unless a migration/REQ explicitly approves a breaking change.
|
- 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.
|
- Source evidence should remain auditable through raw fields and import batch records.
|
||||||
|
|
||||||
## Interface: Streamlit Dashboard
|
## Interface: Streamlit Dashboard
|
||||||
@@ -90,6 +93,8 @@
|
|||||||
- All import flows should support preview before mutation where implemented.
|
- 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.
|
- Dedupe should use deterministic source/external IDs or same-account/date/type/amount/merchant evidence.
|
||||||
- Import batch metadata should support rollback and audit.
|
- 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.
|
- Financial data and passwords are sensitive runtime inputs and should not be committed.
|
||||||
|
|
||||||
## Current Gaps / Planned Changes
|
## Current Gaps / Planned Changes
|
||||||
@@ -99,3 +104,4 @@
|
|||||||
|
|
||||||
## Change Log
|
## Change Log
|
||||||
- 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation.
|
- 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 비노출 정책 추가.
|
||||||
|
|||||||
@@ -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
@@ -4,7 +4,7 @@
|
|||||||
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
- Baseline Seed: `.stage-pilot/bootstrap/baseline.yaml`
|
||||||
- Status: draft
|
- Status: draft
|
||||||
- Owner: Jarca / project maintainer
|
- 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
|
- Source Discovery / Batch: bootstrap-baseline
|
||||||
- Primary Runtime: mixed
|
- Primary Runtime: mixed
|
||||||
|
|
||||||
@@ -63,11 +63,21 @@
|
|||||||
3. Matched self-transfers become `이체 / 본인계좌이동`; investment movements become `이체 / 투자이동`; ambiguous rows remain `확인필요`.
|
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.
|
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
|
## Flow Constraints
|
||||||
- Preview before broad import or categorization mutation where supported.
|
- 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.
|
- 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.
|
- Preserve raw import evidence for audit and future reclassification.
|
||||||
- Avoid destructive migrations without a backup/approval plan.
|
- 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.
|
- Sensitive passwords are runtime inputs and must not be committed.
|
||||||
|
|
||||||
## Current Gaps / Planned Changes
|
## Current Gaps / Planned Changes
|
||||||
@@ -82,3 +92,4 @@
|
|||||||
|
|
||||||
## Change Log
|
## Change Log
|
||||||
- 2026-06-26 22:24 KST: Created by `bootstrap-baseline` from repository observation.
|
- 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
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
## Next Requirement ID
|
## Next Requirement ID
|
||||||
|
|
||||||
- Current: `REQ-002`
|
- Current: `REQ-003`
|
||||||
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
|
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
|
||||||
|
|
||||||
## Requirement Register
|
## Requirement Register
|
||||||
@@ -41,8 +41,11 @@
|
|||||||
| ID | Title | Type | Status | Priority | Owner | Link |
|
| 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-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
|
## Recent Change Log Summary
|
||||||
|
|
||||||
- 2026-06-26 23:31 KST: dcy-001 후속 첫 구현 범위로 REQ-001을 Approved 상태로 등록.
|
- 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-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 상태로 전환.
|
||||||
|
|||||||
@@ -14,6 +14,26 @@ def ledger_health_check() -> dict:
|
|||||||
"""Verify the Jarca Ledger MCP server and SQLite database are reachable."""
|
"""Verify the Jarca Ledger MCP server and SQLite database are reachable."""
|
||||||
return service.health_check()
|
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()
|
@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:
|
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."""
|
"""Create an account/card/cash/investment/loan ledger account."""
|
||||||
|
|||||||
+161
-1
@@ -4,7 +4,8 @@ import csv
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import date, datetime
|
import sqlite3
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -31,6 +32,12 @@ END)"""
|
|||||||
|
|
||||||
CROSS_TRANSFER_MATCH_TAG = "cross_transfer_match:"
|
CROSS_TRANSFER_MATCH_TAG = "cross_transfer_match:"
|
||||||
CROSS_TRANSFER_REVIEW_TAG = "cross_transfer_review:"
|
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:
|
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]
|
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)
|
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]:
|
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():
|
if not name.strip():
|
||||||
return fail("validation_error", "account name is required")
|
return fail("validation_error", "account name is required")
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user