Compare commits
13
Commits
c7a9cfa50d
...
v0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68212e773b | ||
|
|
f5de4118d7 | ||
|
|
2ab68f9278 | ||
|
|
4b8b74ba2a | ||
|
|
432e6f851a | ||
|
|
63fba3670c | ||
|
|
31d1c11dcf | ||
|
|
3bf9b77c6c | ||
|
|
f49568273e | ||
|
|
8caf1f8379 | ||
|
|
fc2d2a6fae | ||
|
|
d258cfdeb4 | ||
|
|
e41db959e4 |
@@ -34,7 +34,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
initContainers:
|
||||
- name: init-db
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.4
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- python
|
||||
@@ -62,7 +62,7 @@ spec:
|
||||
mountPath: /tmp
|
||||
containers:
|
||||
- name: mcp
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.4
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: JARCA_LEDGER_DB_PATH
|
||||
@@ -104,7 +104,7 @@ spec:
|
||||
- name: mcp-tmp
|
||||
mountPath: /tmp
|
||||
- name: dashboard
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.4
|
||||
image: gitea.jsfamily.quest/jsfamily/jarca-ledger-mcp:v0.1.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- uvicorn
|
||||
|
||||
@@ -60,6 +60,13 @@
|
||||
| `return_rate` | REAL | 선택 | NULL | finite number | ratio | 아니오 |
|
||||
| `representative_liability_definition` | TEXT | loan 필수 | NULL | principal_only/principal_interest_fees | enum | 아니오 |
|
||||
|
||||
#### `select_account_valuation` 선택 규칙과 기존 인덱스 매핑
|
||||
|
||||
- `select_account_valuation`은 `account_id is mandatory`인 단일 평가 선택 query다. `currency and valuation_kind/purpose are optional filters`이며, `basis_date exact match or <= upper bound` 조건을 지원한다.
|
||||
- 선택 결과는 `verified leaf valuations only`다. 즉 superseded parent가 아닌 leaf만 권위로 사용하며, `future-dated and unverified valuations are excluded`한다.
|
||||
- `idx_account_valuations_account_date supports account/date selection`; `idx_account_valuations_status_date supports status/date selection`. `neither index covers account + currency + purpose`이므로 해당 조합의 전체 선택 조건을 기존 인덱스가 대신 보장한다고 해석하지 않는다.
|
||||
- `idempotency and single_correction indexes enforce write integrity, not selection`이다. 이 인덱스들은 재기록 방지와 correction 단일 child 불변조건을 위한 것이며 `select_account_valuation`의 후보 검색 인덱스가 아니다.
|
||||
|
||||
### manual-assets — 수동 자산/부채
|
||||
|
||||
- 책임: transaction이 아닌 평가 source
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
{
|
||||
"baseline_revision": "5813dd484196239ab1fac4ba066c5b4922fb33e6",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"items": [
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.01",
|
||||
"semantic_key": "additive_schema_migration_and_legacy_preservation",
|
||||
"meaning": "Schema 11 tables, fields, rows, and transactions remain readable; schema 12 adds valuation and snapshot structures and migration is idempotent.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.01",
|
||||
"REQ-003.01-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N01"
|
||||
],
|
||||
"planned_status": "strengthen_verification_only; semantics_unchanged",
|
||||
"authorization": "Preserve additive migration, old-row retention, and repeatability; rework may make evidence stricter but may not delete, infer-migrate, or rename legacy data.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/migrations.py",
|
||||
"docs/data-model.md",
|
||||
"tests/test_account_valuations.py::test_account_valuation_schema_is_v12_and_idempotent",
|
||||
"tests/test_issue_12_regressions.py::test_schema12_migration_preserves_legacy_rows_indexes_and_is_idempotent"
|
||||
],
|
||||
"verification": "Reviewed head contains schema-12 documentation and named tests; REQ-001/REQ-002 requirement files are unchanged from baseline."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.02",
|
||||
"semantic_key": "verified_leaf_exact_as_of_selection",
|
||||
"meaning": "Selection is scoped by account, currency, and valuation kind; exact selects same-date verified leaf, as_of selects latest prior verified leaf, and future/unverified/rejected/unknown evidence is excluded or warned.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.02",
|
||||
"REQ-003.02-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"C05"
|
||||
],
|
||||
"planned_status": "strengthen_authority_and_warning_contract; semantics_unchanged",
|
||||
"authorization": "Only verified leaves remain authoritative; rework may expose more explicit warnings, exclusions, age, and scope but cannot broaden authority.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"docs/interface-contract.md",
|
||||
"tests/test_account_valuations.py::test_select_exact_and_as_of_excludes_unverified_and_reports_carry_forward",
|
||||
"tests/test_issue_12_regressions.py::test_preview_warns_about_unverified_candidate_when_verified_value_is_selected"
|
||||
],
|
||||
"verification": "Selection terminology, scope, and exclusion rules are documented and backed by focused tests."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.03",
|
||||
"semantic_key": "idempotency_and_append_only_correction",
|
||||
"meaning": "Equal payloads reuse existing rows or runs; changed payloads conflict; corrections append one reasoned child without mutating the parent; invalid parent scope, cycles, self-parent, duplicate child, and source/key conflicts are no-write failures.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.03",
|
||||
"REQ-003.03-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"C06",
|
||||
"D03"
|
||||
],
|
||||
"planned_status": "strengthen_conflict_and_replay_evidence; semantics_unchanged",
|
||||
"authorization": "Preserve immutable history, one-child cardinality, request-fingerprint equality, and no-write conflict behavior.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"docs/data-model.md",
|
||||
"tests/test_account_valuations.py::test_register_same_idempotency_payload_reuses_row_and_changed_payload_conflicts",
|
||||
"tests/test_account_valuations.py::test_correction_is_append_only_reasoned_and_single_child",
|
||||
"tests/test_issue_12_review59_account.py::test_direct_correction_requires_matching_existing_parent_and_reason",
|
||||
"tests/test_issue_12_review59_account.py::test_correction_rejects_self_parent_and_cycles_without_write",
|
||||
"tests/test_issue_12_review59_reports.py::test_idempotent_snapshot_close_replay_returns_original_immutable_response"
|
||||
],
|
||||
"verification": "Current source and tests cover valuation correction, evidence identity, close replay, and no-write conflicts."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.04",
|
||||
"semantic_key": "integer_amounts_nullable_optional_metrics_and_representative_liability",
|
||||
"meaning": "Amounts and optional monetary fields use exact integers; NULL means not provided and zero means explicit zero; investment metrics are not double-counted; only the defined representative loan amount enters liabilities.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.04",
|
||||
"REQ-003.04-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N02"
|
||||
],
|
||||
"planned_status": "strengthen_field_mapping_and_formula_assertions; semantics_unchanged",
|
||||
"authorization": "Preserve physical principal fields, NULL/zero distinction, loan definition requirement, and no double sum.",
|
||||
"evidence_paths": [
|
||||
"docs/data-model.md",
|
||||
"docs/interface-contract.md",
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"tests/test_issue_12_review59_account.py::test_valuation_round_trip_preserves_null_zero_and_physical_amount_mapping",
|
||||
"tests/test_issue_12_regressions.py::test_loan_representative_is_always_aggregated_as_liability",
|
||||
"tests/test_issue_12_regressions.py::test_loan_balance_fallback_without_representative_definition_is_excluded"
|
||||
],
|
||||
"verification": "The reviewed contract explicitly distinguishes NULL, zero, principal meanings, and representative liability."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.05",
|
||||
"semantic_key": "current_display_and_historical_authority_separation",
|
||||
"meaning": "Historical dated evidence does not roll back newer current balance display; account/import writes create only dated unverified candidates when real evidence exists; name-only updates create none.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.05",
|
||||
"REQ-003.05-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N01",
|
||||
"C05"
|
||||
],
|
||||
"planned_status": "strengthen_current_cache_and_candidate_provenance; semantics_unchanged",
|
||||
"authorization": "Preserve separate current and historical decisions, unverified candidate status, and no inferred evidence.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"jarca_ledger_mcp/application/imports_service.py",
|
||||
"docs/runtime-flow.md",
|
||||
"tests/test_account_valuations.py::test_account_balance_write_records_unverified_candidate_only_with_real_basis_date",
|
||||
"tests/test_account_valuations.py::test_account_name_only_update_does_not_create_candidate",
|
||||
"tests/test_issue_12_regressions.py::test_historical_account_update_preserves_current_and_adds_one_candidate",
|
||||
"tests/test_issue_12_review59_account.py::test_import_historical_kb_does_not_replace_newer_current_balance"
|
||||
],
|
||||
"verification": "The reviewed head has explicit late-history/current-cache regression tests and runtime decision guards."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.06",
|
||||
"semantic_key": "read_only_preview_and_explicit_conditional_close",
|
||||
"meaning": "Preview performs no database write and reports complete, incomplete, or conditional state; unsafe evidence requires explicit approval metadata; close persists immutable run evidence atomically.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.06",
|
||||
"REQ-003.06-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"D03",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_preview_fingerprint_and_approval_boundary; semantics_unchanged",
|
||||
"authorization": "Preserve no-write preview, explicit approval, rejection of unsafe evidence, and atomic close.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"jarca_ledger_mcp/adapters/streamlit/assets_gateway.py",
|
||||
"docs/interface-contract.md",
|
||||
"tests/test_issue_12_regressions.py::test_preview_marks_approval_eligible_missing_valuation_as_incomplete_draft",
|
||||
"tests/test_issue_12_review59_reports.py::test_dated_manual_asset_is_verified_and_preview_needs_approval",
|
||||
"tests/test_issue_12_review59_reports.py::test_approved_carried_close_is_conditional_and_persists_provenance",
|
||||
"tests/test_issue_12_review59_reports.py::test_close_rejects_authority_drift_since_preview"
|
||||
],
|
||||
"verification": "Preview/close boundaries, approval fields, drift rejection, and no-partial-write behavior are documented and tested."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.07",
|
||||
"semantic_key": "immutable_close_correction_and_net_worth_formula",
|
||||
"meaning": "Same-basis re-close requires a reason and creates a new run and rows; old runs remain; latest run is report authority; net worth is asset buckets minus representative loan and excludes missing valuations.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.07",
|
||||
"REQ-003.07-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"C06",
|
||||
"D03"
|
||||
],
|
||||
"planned_status": "strengthen_latest-run_and_formula_provenance; semantics_unchanged",
|
||||
"authorization": "Preserve append-only runs, correction linkage, latest-run authority, and exact formula.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"docs/data-model.md",
|
||||
"tests/test_issue_12_regressions.py::test_empty_conditional_close_is_closed_and_second_close_requires_correction",
|
||||
"tests/test_issue_12_regressions.py::test_legacy_snapshot_requires_explicit_correction_before_new_close",
|
||||
"tests/test_issue_12_review59_reports.py::test_asset_close_and_report_expose_provenance_aliases"
|
||||
],
|
||||
"verification": "Run correction fields, latest authority, and formula semantics are present in canonical docs and tests."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.08",
|
||||
"semantic_key": "reconciliation_and_monthly_read_model_propagation",
|
||||
"meaning": "Verified investment uses VALUATION_MANAGED without fake transactions; incomplete evidence reports unavailable/incomplete calculation; monthly and downstream reports propagate actual valuation date, age, carry, verification, run, approval, correction, warnings, and exclusions.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.08",
|
||||
"REQ-003.08-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"C05",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_cross_read_model_alias_and_completeness_contract; semantics_unchanged",
|
||||
"authorization": "Preserve read-only propagation, explicit incompleteness, no inferred balance, and monthly_source_snapshot_id as a read-model alias only.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/application/reconciliation_service.py",
|
||||
"jarca_ledger_mcp/application/monthly_settlement_service.py",
|
||||
"jarca_ledger_mcp/adapters/streamlit/overview_gateway.py",
|
||||
"docs/data-model.md",
|
||||
"tests/test_issue_12_regressions.py::test_reconciliation_uses_corrected_investment_leaf_without_transaction_count_change",
|
||||
"tests/test_issue_12_review59_reports.py::test_investment_without_verified_valuation_and_without_opening_balance_is_incomplete",
|
||||
"tests/test_issue_12_review59_reports.py::test_timeseries_and_monthly_expose_canonical_asset_provenance",
|
||||
"tests/test_issue_12_review59_reports.py::test_monthly_settlement_propagates_snapshot_provenance_id_and_actual_valuation_date"
|
||||
],
|
||||
"verification": "Named source owners and report propagation tests cover reconciliation and monthly semantics."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.09",
|
||||
"semantic_key": "shared_application_adapters_and_auth_boundary",
|
||||
"meaning": "MCP and Dashboard delegate to shared services, retain success/error envelopes, require Dashboard authentication before DB access, and keep render/read paths non-mutating.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.09",
|
||||
"REQ-003.09-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N01",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_adapter_and_auth-order assertions; semantics_unchanged",
|
||||
"authorization": "Preserve shared policy ownership, thin adapters, auth-before-DB, and existing MCP authentication scope.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/adapters/mcp/server.py",
|
||||
"jarca_ledger_mcp/adapters/streamlit/accounts_gateway.py",
|
||||
"jarca_ledger_mcp/adapters/streamlit/overview_gateway.py",
|
||||
"dashboard/app.py",
|
||||
"tests/test_contract_regression.py",
|
||||
"tests/test_dashboard_app.py",
|
||||
"tests/test_dashboard_menu_transition.py"
|
||||
],
|
||||
"verification": "The reviewed head retains named adapter paths and existing REQ-001/REQ-002 gates; requirement files are unchanged from baseline."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003.10",
|
||||
"semantic_key": "synthetic_privacy_and_excluded_scope",
|
||||
"meaning": "Docs, tests, fixtures, and evidence contain only synthetic/redacted data; credentials, tokens, cookies, real financial data, external prices, FX, automatic approval, inferred transactions, and redesign remain out of scope.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003.10",
|
||||
"REQ-003.10-AC01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N02",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_recursive_redaction_and_static_inventory; semantics_unchanged",
|
||||
"authorization": "Preserve privacy boundary and excluded scope; rework may add redaction checks but may not introduce real data or new financial inference.",
|
||||
"evidence_paths": [
|
||||
"docs/project-structure.md",
|
||||
"docs/interface-contract.md",
|
||||
"tests/test_requirement_docs.py",
|
||||
"tests/test_issue_12_review59_reports.py::test_close_metadata_is_allowlisted_recursively",
|
||||
"tests/test_issue_12_review59_reports.py::test_public_included_items_redact_source_note_and_memo_recursively"
|
||||
],
|
||||
"verification": "Canonical docs state synthetic-only and recursive redaction; focused tests validate metadata and evidence redaction."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003-INV01",
|
||||
"semantic_key": "authority_and_safety_invariants",
|
||||
"meaning": "Only verified leaves are authority; future/unverified/rejected/unknown evidence is not summed; preview/render do not write; close is transactional; current and historical authority remain separate.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003-INV01"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"C05",
|
||||
"D03",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen explicit invariant checks; semantics_unchanged",
|
||||
"authorization": "All listed invariants are preservation constraints, not permission to alter business meaning.",
|
||||
"evidence_paths": [
|
||||
"docs/requirements/req-003_account-valuation-history.md",
|
||||
"docs/data-model.md",
|
||||
"docs/runtime-flow.md",
|
||||
"tests/test_issue_12_regressions.py",
|
||||
"tests/test_issue_12_review59_reports.py"
|
||||
],
|
||||
"verification": "The requirement, data model, and runtime flow repeat the same authority and no-write invariants."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"baseline_id": "REQ-003-INV02",
|
||||
"semantic_key": "database_read_lifecycle_boundary",
|
||||
"meaning": "Writer/bootstrap explicitly calls init_db; read_db uses SQLite mode=ro; missing DB returns db_not_initialized without mkdir/migrate/commit; initialized read failure returns db_error.",
|
||||
"reviewed_ids": [
|
||||
"REQ-003-INV02"
|
||||
],
|
||||
"reviewed_status": "present_and_approved",
|
||||
"planned_ids": [
|
||||
"N01"
|
||||
],
|
||||
"planned_status": "strengthen_no_create_and_error_classification; semantics_unchanged",
|
||||
"authorization": "Preserve explicit initialization and stable read-only failure envelopes.",
|
||||
"evidence_paths": [
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/connection.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/migrations.py",
|
||||
"docs/interface-contract.md",
|
||||
"docs/runtime-flow.md",
|
||||
"tests/test_issue_12_review59_readonly.py::test_read_queries_missing_nested_db_return_stable_error_without_creating_path",
|
||||
"tests/test_issue_12_review59_readonly.py::test_read_schema_error_is_not_misreported_as_uninitialized"
|
||||
],
|
||||
"verification": "C13 database-boundary documentation and focused read-only tests exist at reviewed head."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-001_dashboard-administrator-authentication.md",
|
||||
"baseline_id": "REQ-001.01-REQ-001.11",
|
||||
"semantic_key": "dashboard_auth_gate_cookie_cleanup_logout_and_rotation",
|
||||
"meaning": "REQ-001 authentication remains fail-closed before DB/page dispatch, with server-only credential binding, hardened persistent cookie, absolute TTL, invalid-cookie cleanup, explicit logout, rotation invalidation, and no DB-backed session.",
|
||||
"reviewed_ids": [
|
||||
"REQ-001.01",
|
||||
"REQ-001.05",
|
||||
"REQ-001.06",
|
||||
"REQ-001.07",
|
||||
"REQ-001.08",
|
||||
"REQ-001.09",
|
||||
"REQ-001.10"
|
||||
],
|
||||
"reviewed_status": "unchanged_from_baseline",
|
||||
"planned_ids": [
|
||||
"N01"
|
||||
],
|
||||
"planned_status": "must_remain_unchanged",
|
||||
"authorization": "Issue #12 rework is additive and cannot modify authentication semantics or claim MCP authentication.",
|
||||
"evidence_paths": [
|
||||
"docs/requirements/req-001_dashboard-administrator-authentication.md",
|
||||
"docs/interface-contract.md",
|
||||
"dashboard/authentication.py",
|
||||
"dashboard/auth_server.py",
|
||||
"tests/test_dashboard_authentication.py",
|
||||
"tests/test_dashboard_persistent_authentication.py",
|
||||
"tests/test_dashboard_auth_server.py",
|
||||
"tests/test_dashboard_authentication_app.py"
|
||||
],
|
||||
"verification": "Git diff confirms REQ-001 file has no changes between baseline and reviewed head."
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-002_dashboard-menu-transition.md",
|
||||
"baseline_id": "REQ-002.01-REQ-002.09",
|
||||
"semantic_key": "dashboard_menu_identity_query_and_stable_shell",
|
||||
"meaning": "The exact ten menu labels/order, selected state, page-only query update with sibling preservation, stable shell/background, local loading/error/retry, latest serialized selection, auth gate, finance pages, and clear_cache contract remain unchanged.",
|
||||
"reviewed_ids": [
|
||||
"REQ-002.01",
|
||||
"REQ-002.02",
|
||||
"REQ-002.03",
|
||||
"REQ-002.04",
|
||||
"REQ-002.05",
|
||||
"REQ-002.06",
|
||||
"REQ-002.07",
|
||||
"REQ-002.08",
|
||||
"REQ-002.09"
|
||||
],
|
||||
"reviewed_status": "unchanged_from_baseline",
|
||||
"planned_ids": [
|
||||
"N01"
|
||||
],
|
||||
"planned_status": "must_remain_unchanged",
|
||||
"authorization": "Issue #12 rework may add valuation content behind existing pages but may not alter menu identity, query semantics, shell continuity, or browser evidence requirements.",
|
||||
"evidence_paths": [
|
||||
"docs/requirements/req-002_dashboard-menu-transition.md",
|
||||
"docs/interface-contract.md",
|
||||
"dashboard/app.py",
|
||||
"dashboard/common.py",
|
||||
"tests/test_dashboard_menu_transition.py",
|
||||
"tests/test_dashboard_app.py",
|
||||
"tests/test_dashboard_authentication_app.py",
|
||||
"docs/evidence/issue-9-dashboard-menu-transition/"
|
||||
],
|
||||
"verification": "Git diff confirms REQ-002 file has no changes between baseline and reviewed head."
|
||||
},
|
||||
{
|
||||
"path": "docs/interface-contract.md",
|
||||
"baseline_id": "COMMON-IFACE-01",
|
||||
"semantic_key": "success_error_envelope_and_public_service_signatures",
|
||||
"meaning": "Existing success/error envelopes remain; valuation, snapshot, MCP, Dashboard gateway, and service signatures are additive and re-exported through the established service boundary.",
|
||||
"reviewed_ids": [
|
||||
"COMMON-IFACE-01"
|
||||
],
|
||||
"reviewed_status": "preserved_with_additive_REQ003_contract",
|
||||
"planned_ids": [
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_exact_output_and_replay_contract; semantics_unchanged",
|
||||
"authorization": "No existing response key may be renamed or removed.",
|
||||
"evidence_paths": [
|
||||
"docs/interface-contract.md",
|
||||
"jarca_ledger_mcp/service.py",
|
||||
"jarca_ledger_mcp/adapters/mcp/server.py",
|
||||
"tests/test_contract_regression.py"
|
||||
],
|
||||
"verification": "Reviewed contract explicitly lists signatures, envelopes, top-level aliases, nested provenance, and replay rules."
|
||||
},
|
||||
{
|
||||
"path": "docs/data-model.md",
|
||||
"baseline_id": "COMMON-DATA-01",
|
||||
"semantic_key": "legacy_entities_and_storage_ownership",
|
||||
"meaning": "Existing ledger entities and relationships remain; valuation and snapshot history are append-only; monthly alias is not a persistence field; application services own policy and SQLite owns storage.",
|
||||
"reviewed_ids": [
|
||||
"COMMON-DATA-01"
|
||||
],
|
||||
"reviewed_status": "preserved_with_additive_REQ003_model",
|
||||
"planned_ids": [
|
||||
"N02",
|
||||
"D03"
|
||||
],
|
||||
"planned_status": "strengthen_physical_field_and_ownership_checks; semantics_unchanged",
|
||||
"authorization": "No guessed migration, cascade deletion, fake transaction, or alternate policy boundary is authorized.",
|
||||
"evidence_paths": [
|
||||
"docs/data-model.md",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/schema.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"jarca_ledger_mcp/application/reconciliation_service.py"
|
||||
],
|
||||
"verification": "Data model names legacy preservation, append-only relationships, physical fields, and canonical owners."
|
||||
},
|
||||
{
|
||||
"path": "docs/runtime-flow.md",
|
||||
"baseline_id": "COMMON-RUNTIME-01",
|
||||
"semantic_key": "auth_before_db_and_explicit_write_boundaries",
|
||||
"meaning": "Dashboard auth precedes DB access; read/preview paths are read-only; first-use writer initialization is explicit; approval precedes atomic close; downstream reports are reads.",
|
||||
"reviewed_ids": [
|
||||
"COMMON-RUNTIME-01"
|
||||
],
|
||||
"reviewed_status": "preserved_with_additive_REQ003_flow",
|
||||
"planned_ids": [
|
||||
"N01",
|
||||
"D03",
|
||||
"D04"
|
||||
],
|
||||
"planned_status": "strengthen_transition_guards_and_drift_revalidation; semantics_unchanged",
|
||||
"authorization": "Preserve ordering, transaction boundaries, stable missing-DB behavior, and no implicit writes.",
|
||||
"evidence_paths": [
|
||||
"docs/runtime-flow.md",
|
||||
"dashboard/app.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/connection.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"tests/test_issue_12_review59_readonly.py"
|
||||
],
|
||||
"verification": "Runtime flow explicitly records auth-before-DB, read-only database lifecycle, preview drift revalidation, and atomic close."
|
||||
},
|
||||
{
|
||||
"path": "docs/project-structure.md",
|
||||
"baseline_id": "COMMON-STRUCTURE-01",
|
||||
"semantic_key": "canonical_module_ownership_and_regression_map",
|
||||
"meaning": "Valuation/snapshot policy remains in application services; adapters remain thin; Dashboard pages do not own SQL or policy; named tests and documentation validators are required evidence.",
|
||||
"reviewed_ids": [
|
||||
"COMMON-STRUCTURE-01"
|
||||
],
|
||||
"reviewed_status": "preserved_with_REQ003_ownership_map",
|
||||
"planned_ids": [
|
||||
"N01",
|
||||
"C06"
|
||||
],
|
||||
"planned_status": "strengthen ownership and named-test traceability; semantics_unchanged",
|
||||
"authorization": "Rework must preserve canonical owners and may not introduce duplicate business logic in adapters.",
|
||||
"evidence_paths": [
|
||||
"docs/project-structure.md",
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"jarca_ledger_mcp/application/monthly_settlement_service.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/connection.py",
|
||||
"tests/test_requirement_docs.py"
|
||||
],
|
||||
"verification": "Reviewed structure document contains explicit ownership, dependency, preservation, and named regression-test sections."
|
||||
}
|
||||
],
|
||||
"missing_count": 0,
|
||||
"ambiguous_count": 0,
|
||||
"notes": [
|
||||
"REQ-001 and REQ-002 requirement files are byte-for-byte unchanged from baseline; common docs were extended additively for REQ-003.",
|
||||
"Source contract digest supplied by the caller is 6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818.",
|
||||
"The checked-out repository does not define a canonical glossary mapping finding labels N01, C05, C06, N02, D03, and D04 to independent issue descriptions; planned mappings above are therefore preservation-oriented semantic associations inferred from the reviewed contract text.",
|
||||
"No files were created or modified; no commit, push, fetch, or Gitea call was performed.",
|
||||
"Main-agent resolution: Review #60 body is the authoritative glossary for N01/C05/C06/N02/D03/D04; each label and prose was read from admission-snapshot.json, so the delegate-reported label-mapping ambiguity is resolved without changing contract meaning."
|
||||
],
|
||||
"schema": "gitea-agent.contract-preservation-inventory/v1"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,181 +0,0 @@
|
||||
{
|
||||
"environment": [
|
||||
"os",
|
||||
"kernel",
|
||||
"architecture",
|
||||
"python",
|
||||
"uv"
|
||||
],
|
||||
"command_env_allowlist": [
|
||||
"PATH",
|
||||
"HOME",
|
||||
"TMPDIR",
|
||||
"LANG",
|
||||
"VIRTUAL_ENV",
|
||||
"UV_CACHE_DIR"
|
||||
],
|
||||
"secret_env_names": [],
|
||||
"forbidden_regexes": [],
|
||||
"hash_paths": [
|
||||
"dashboard/page_views/accounts.py",
|
||||
"docs/data-model.md",
|
||||
"docs/interface-contract.md",
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"jarca_ledger_mcp/application/imports_service.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/schema.py",
|
||||
"scripts/ci_fixture_db.py",
|
||||
"tests/test_account_valuations.py",
|
||||
"tests/test_issue_12_regressions.py",
|
||||
"tests/test_issue_12_review59_account.py",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_asset_snapshot_idempotency.py",
|
||||
"tests/test_import_current_balance_authority.py",
|
||||
"tests/test_valuation_contract_docs.py",
|
||||
"jarca_ledger_mcp/adapters/mcp/server.py",
|
||||
"tests/test_account_basis_public_consumers.py"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"id": "review60-regressions",
|
||||
"purpose": "Run Review 60 and all independent-review named RED-to-GREEN regressions",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_import_current_balance_authority.py",
|
||||
"tests/test_asset_snapshot_idempotency.py",
|
||||
"tests/test_valuation_contract_docs.py",
|
||||
"tests/test_account_basis_public_consumers.py",
|
||||
"tests/test_issue_12_regressions.py"
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"review60-and-independent-regressions-green"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "adversarial-compatibility",
|
||||
"purpose": "Run correction, replay, current authority, read-only, provenance, contract and documentation compatibility coverage",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_import_current_balance_authority.py",
|
||||
"tests/test_asset_snapshot_idempotency.py",
|
||||
"tests/test_valuation_contract_docs.py",
|
||||
"tests/test_issue_12_regressions.py",
|
||||
"tests/test_account_valuations.py",
|
||||
"tests/test_asset_snapshots.py",
|
||||
"tests/test_issue_12_review59_account.py",
|
||||
"tests/test_issue_12_review59_reports.py",
|
||||
"tests/test_issue_12_review59_readonly.py",
|
||||
"tests/test_contract_regression.py",
|
||||
"tests/test_requirement_docs.py"
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"adversarial-compatibility-green"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "full-suite",
|
||||
"purpose": "Run the repository full test suite with declared optional dependencies",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-rs"
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"full-suite-green"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ruff",
|
||||
"purpose": "Run repository lint gate",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"ruff",
|
||||
"check",
|
||||
"."
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"ruff-green"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "compileall",
|
||||
"purpose": "Compile Python sources, dashboard, tests and scripts",
|
||||
"argv": [
|
||||
"python3",
|
||||
"-m",
|
||||
"compileall",
|
||||
"-q",
|
||||
"jarca_ledger_mcp",
|
||||
"dashboard",
|
||||
"tests",
|
||||
"scripts"
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"compileall-green"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "commit-diff-check",
|
||||
"purpose": "Validate committed rework diff whitespace",
|
||||
"argv": [
|
||||
"git",
|
||||
"diff",
|
||||
"--check",
|
||||
"HEAD^",
|
||||
"HEAD"
|
||||
],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": [
|
||||
"diff-check-green"
|
||||
]
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Four external PDF fixture tests are expected to skip when private sample files/passwords are unavailable.",
|
||||
"No actual financial data or production credential is used; tests use synthetic fixtures.",
|
||||
"Review 60 does not require a new real-browser observation; existing synthetic Streamlit/AppTest regressions remain in the full suite."
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"specification_gate": {
|
||||
"passed": true,
|
||||
"findings": [],
|
||||
"summary": "Exact-current-diff specification gate passed. Review60 semantics N01, C05, C06, N02, D03, D04; prior defects; documentation, adapters, dashboard consumers, fixtures, and named regressions were rechecked. Full pytest suite, Ruff, compileall, and git diff --check passed."
|
||||
},
|
||||
"code_security_gate": {
|
||||
"passed": true,
|
||||
"security_concerns": [],
|
||||
"logic_errors": [],
|
||||
"suggestions": [],
|
||||
"summary": "Final current-diff code/security gate passed. Full pytest suite passed with four fixture-dependent skips and one existing Starlette deprecation warning; Ruff, compileall, diff-check, migration/idempotency/consumer tests, and added-line security scan passed."
|
||||
},
|
||||
"reviewed_parent_sha": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"delegation_id": "deleg_49abf596"
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.finding-matrix/v1",
|
||||
"review_id": 60,
|
||||
"review_author": "gitea-spec-agent",
|
||||
"review_state": "REQUEST_CHANGES",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||
"findings": [
|
||||
{
|
||||
"finding": "60/N01-loan-balance-definition",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 확정 결정 8; REQ-003.04/05",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_account_valuation_edge_contracts.py"
|
||||
],
|
||||
"verification": "named RED→GREEN test plus consolidated adversarial suite",
|
||||
"result": "pending",
|
||||
"reason": "Loan account balance alias bypasses mandatory representative liability definition."
|
||||
},
|
||||
{
|
||||
"finding": "60/C05-current-import",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 현재값과 과거값; REQ-003.05",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/imports_service.py",
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/schema.py",
|
||||
"jarca_ledger_mcp/infrastructure/sqlite/migrations.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_import_current_balance_authority.py"
|
||||
],
|
||||
"verification": "named RED→GREEN sequential late-import test plus migration/full suite",
|
||||
"result": "pending",
|
||||
"reason": "Import compares actual evidence date against synchronization timestamp and loses current-cache basis authority."
|
||||
},
|
||||
{
|
||||
"finding": "60/C06-idempotency",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 중복 및 정정; REQ-003.03",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_account_valuation_edge_contracts.py"
|
||||
],
|
||||
"verification": "named RED→GREEN same-evidence correction replay and changed-payload conflict tests",
|
||||
"result": "pending",
|
||||
"reason": "A same source_ref/parent/effective payload correction retry without explicit key conflicts instead of reusing the child."
|
||||
},
|
||||
{
|
||||
"finding": "60/N02-close-replay-run-binding",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 마감 불변성; REQ-003.03/07",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/assets_service.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_asset_snapshot_idempotency.py"
|
||||
],
|
||||
"verification": "named RED→GREEN old-key replay-after-correction exact response binding test",
|
||||
"result": "pending",
|
||||
"reason": "Replay identifies the old run but loads latest-run rows and metadata."
|
||||
},
|
||||
{
|
||||
"finding": "60/D03-data-model",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 데이터 모델 산출물; REQ-003.05",
|
||||
"classification": "evidence_gap",
|
||||
"implementation_paths": [
|
||||
"docs/data-model.md"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_valuation_contract_docs.py"
|
||||
],
|
||||
"verification": "named RED→GREEN canonical data-model contract assertion",
|
||||
"result": "pending",
|
||||
"reason": "Data model does not fix actual cache basis versus sync timestamp authority and no-rollback owner."
|
||||
},
|
||||
{
|
||||
"finding": "60/D04-interface-contract",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 인터페이스 계약 산출물; REQ-003.04/09",
|
||||
"classification": "evidence_gap",
|
||||
"implementation_paths": [
|
||||
"docs/interface-contract.md"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_valuation_contract_docs.py"
|
||||
],
|
||||
"verification": "named RED→GREEN exact enum/conditional/output mapping assertion",
|
||||
"result": "pending",
|
||||
"reason": "Public interface delegates allowed values and loan definition/output mapping to source."
|
||||
},
|
||||
{
|
||||
"finding": "independent-spec/C05-account-write-no-inference",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 current/history separation and no inferred basis; REQ-003.05",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_import_current_balance_authority.py"
|
||||
],
|
||||
"verification": "named RED→GREEN explicit-basis and no-candidate account-write regression",
|
||||
"result": "pending",
|
||||
"reason": "Account writes inferred evidence basis and historical candidate from sync timestamp."
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/legacy-unknown-basis-reporting",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 unknown-basis non-retroactivity; REQ-003.05",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/assets_service.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_import_current_balance_authority.py"
|
||||
],
|
||||
"verification": "named RED→GREEN legacy unknown-basis report regression",
|
||||
"result": "pending",
|
||||
"reason": "Live report exposed last_synced_at as valuation_date despite unknown evidence basis."
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/zero-row-run-replay",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"contract": "Issue #12 immutable close evidence; REQ-003.03/07",
|
||||
"classification": "valid_fix",
|
||||
"implementation_paths": [
|
||||
"jarca_ledger_mcp/application/assets_service.py"
|
||||
],
|
||||
"test_paths": [
|
||||
"tests/test_asset_snapshot_idempotency.py"
|
||||
],
|
||||
"verification": "named RED→GREEN empty-run replay after nonempty correction exact equality",
|
||||
"result": "pending",
|
||||
"reason": "Empty persisted applied_evidence triggered fallback to latest correction rows."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.red-regression-evidence/v1",
|
||||
"reviewed_head": "6a8a27a74ece9a9cce6f9772259677813d91ddf8",
|
||||
"environment": {
|
||||
"runner": "uv run --frozen --extra dev --extra dashboard pytest",
|
||||
"fixture_policy": "synthetic only"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"finding": "60/N01-loan-balance-definition",
|
||||
"test": "tests/test_account_valuation_edge_contracts.py::test_loan_balance_requires_representative_liability_definition",
|
||||
"exit_code": 1,
|
||||
"failure": "missing_definition success was True; expected validation_error and no persisted valuation"
|
||||
},
|
||||
{
|
||||
"finding": "60/C06-idempotency",
|
||||
"test": "tests/test_account_valuation_edge_contracts.py::test_correction_replays_by_evidence_and_conflicts_on_changed_payload",
|
||||
"exit_code": 1,
|
||||
"failure": "identical correction replay success was False; expected reused=True with the original child"
|
||||
},
|
||||
{
|
||||
"finding": "60/C05-current-import",
|
||||
"test": "tests/test_import_current_balance_authority.py::test_late_import_current_balance_is_authoritative_by_evidence_basis_date",
|
||||
"exit_code": 1,
|
||||
"failure": "current last_balance was 700 after newer-basis late import; expected 900"
|
||||
},
|
||||
{
|
||||
"finding": "60/N02-close-replay-run-binding",
|
||||
"test": "tests/test_asset_snapshot_idempotency.py::test_exact_run_replay_after_correction_reconstructs_original_persisted_run",
|
||||
"exit_code": 1,
|
||||
"failure": "old-key replay returned correction-run row IDs, approval and provenance instead of exact original response"
|
||||
},
|
||||
{
|
||||
"finding": "60/D03-data-model",
|
||||
"test": "tests/test_valuation_contract_docs.py::test_data_model_documents_cache_authority_and_legacy_basis_migration",
|
||||
"exit_code": 1,
|
||||
"failure": "canonical last_balance current-cache contract marker was absent"
|
||||
},
|
||||
{
|
||||
"finding": "60/D04-interface-contract",
|
||||
"test": "tests/test_valuation_contract_docs.py::test_interface_contract_documents_public_valuation_enums_validation_and_mapping",
|
||||
"exit_code": 1,
|
||||
"failure": "interface contract still delegated allowed values to 'actual enums in source'"
|
||||
},
|
||||
{
|
||||
"finding": "independent-spec/C05-account-write-no-inference",
|
||||
"test": "tests/test_import_current_balance_authority.py::test_account_write_sync_timestamp_does_not_infer_balance_basis_or_candidate",
|
||||
"exit_code": 1,
|
||||
"failure": "account creation inferred last_balance_basis_date=2026-07-01 from last_synced_at; expected unknown basis and no candidate"
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/legacy-unknown-basis-reporting",
|
||||
"test": "tests/test_import_current_balance_authority.py::test_unknown_legacy_balance_basis_is_not_reported_as_valuation_date",
|
||||
"exit_code": 1,
|
||||
"failure": "excluded valuation_date exposed last_synced_at timestamp; expected empty unknown basis"
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/zero-row-run-replay",
|
||||
"test": "tests/test_asset_snapshot_idempotency.py::test_empty_run_replay_after_nonempty_correction_preserves_original_empty_payload",
|
||||
"exit_code": 1,
|
||||
"failure": "old empty run replay fell back to later run items and returned total_assets/net_worth=100 instead of exact original empty response"
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/historical-explicit-basis-no-rollback",
|
||||
"test": "tests/test_issue_12_regressions.py::test_historical_account_update_preserves_current_and_adds_one_candidate",
|
||||
"exit_code": 1,
|
||||
"failure": "historical explicit update replaced the newer current cache instead of retaining current state while adding one historical candidate"
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/dashboard-basis-consumer",
|
||||
"test": "tests/test_account_basis_public_consumers.py::test_dashboard_valuation_basis_never_uses_sync_timestamp",
|
||||
"exit_code": 1,
|
||||
"failure": "Dashboard page_accounts used last_synced_at for valuation selection and registration defaults"
|
||||
},
|
||||
{
|
||||
"finding": "independent-code/mcp-basis-forwarding",
|
||||
"test": "tests/test_account_basis_public_consumers.py::test_mcp_account_write_tools_forward_explicit_balance_basis",
|
||||
"exit_code": 1,
|
||||
"failure": "MCP account-write tools rejected/dropped last_balance_basis_date"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.contract-preservation-inventory/v1",
|
||||
"owner": "jsfamily",
|
||||
"repository": "jarca-ledger-mcp",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||
"review_id": 61,
|
||||
"reviewed_parent_sha": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
|
||||
"reviewed_head": "1b9eda683c66d3c7129716b9899eddf61df3362d",
|
||||
"preserved_semantics": [
|
||||
{
|
||||
"criterion": "N01-loan-balance-definition",
|
||||
"required": "Every valuation attached to a loan account explicitly records principal_only or principal_interest_fees; no writer or account-type transition may infer or bypass it.",
|
||||
"evidence": [
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_issue_12_review59_account.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"criterion": "C05-current-import",
|
||||
"required": "Loan cache and import operations remain successful without creating definition-less history, and late evidence never rolls back newer current cache.",
|
||||
"evidence": [
|
||||
"test_dated_loan_create_and_historical_update_preserve_current_cache_without_valuations",
|
||||
"test_loan_import_preserves_transactions_and_current_cache_without_valuation_candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"criterion": "C13-read-only-and-no-partial-write",
|
||||
"required": "Validation failure changes neither account state nor append-only valuation history.",
|
||||
"evidence": [
|
||||
"test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"criterion": "compatibility",
|
||||
"required": "A non-loan account with no valuation history may still be converted to loan; non-loan candidate insertion and replay are unchanged.",
|
||||
"evidence": [
|
||||
"test_bank_to_loan_conversion_without_valuations_remains_allowed",
|
||||
"test_non_loan_candidate_still_succeeds_and_reuses_idempotently"
|
||||
]
|
||||
}
|
||||
],
|
||||
"excluded_scope": [
|
||||
"No inferred or automatic liability-definition migration.",
|
||||
"No schema change.",
|
||||
"No changes to snapshot, reconciliation, dashboard, authentication, or menu behavior.",
|
||||
"No merge, Issue close, or release operation."
|
||||
]
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.evidence-manifest/v1",
|
||||
"owner": "jsfamily",
|
||||
"repository": "jarca-ledger-mcp",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review": 61,
|
||||
"reviewed_parent_sha": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
|
||||
"source_head_sha": "1b9eda683c66d3c7129716b9899eddf61df3362d",
|
||||
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||
"environment": {
|
||||
"platform": "Linux-6.18.34+rpt-rpi-v8-aarch64-with-glibc2.41",
|
||||
"python": "3.13.5",
|
||||
"uv": "uv 0.11.6 (aarch64-unknown-linux-musl)",
|
||||
"fixture_policy": "synthetic repository fixtures only; no credentials or real financial data"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"id": "conversion-regression",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-rs",
|
||||
"tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation",
|
||||
"tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_without_valuations_remains_allowed"
|
||||
],
|
||||
"purpose": "Verify conversion rejection is atomic and history-free conversion remains compatible",
|
||||
"started_at_utc": "2026-10-07T03:59:49.130624Z",
|
||||
"finished_at_utc": "2026-10-07T03:59:51.888407Z",
|
||||
"duration_seconds": 2.757882494479418,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": ".. [100%]\n"
|
||||
},
|
||||
{
|
||||
"id": "review61-regressions",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-rs",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_issue_12_review59_account.py",
|
||||
"tests/test_valuation_contract_docs.py"
|
||||
],
|
||||
"purpose": "Run Review 61 and independent-review named regressions",
|
||||
"started_at_utc": "2026-10-07T03:59:51.888570Z",
|
||||
"finished_at_utc": "2026-10-07T03:59:58.351942Z",
|
||||
"duration_seconds": 6.463426006026566,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": "........................ [100%]\n"
|
||||
},
|
||||
{
|
||||
"id": "full-suite",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-rs"
|
||||
],
|
||||
"purpose": "Run the complete repository test suite at the exact source head",
|
||||
"started_at_utc": "2026-10-07T03:59:58.352023Z",
|
||||
"finished_at_utc": "2026-10-07T04:01:03.222283Z",
|
||||
"duration_seconds": 64.87031899858266,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": "........................................................................ [ 25%]\n........................................................................ [ 51%]\n...............................................................ss....... [ 77%]\n.....................................................ss...... [100%]\n=============================== warnings summary ===============================\ntests/test_dashboard_auth_server.py:11\n /opt/data/gitea-agent/repositories/jsfamily/jarca-ledger-mcp/worktrees/issue-12/checkout/tests/test_dashboard_auth_server.py:11: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.\n from starlette.testclient import TestClient\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html\n=========================== short test summary info ============================\nSKIPPED [1] tests/test_kb_bank_pdf.py:26: sample KB Bank PDF/password is not available\nSKIPPED [1] tests/test_kb_bank_pdf.py:42: sample KB Bank PDF/password is not available\nSKIPPED [1] tests/test_toss_securities_pdf.py:20: sample Toss Securities PDF is not available\nSKIPPED [1] tests/test_toss_securities_pdf.py:32: sample Toss Securities PDF is not available\n"
|
||||
},
|
||||
{
|
||||
"id": "ruff",
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"ruff",
|
||||
"check",
|
||||
"."
|
||||
],
|
||||
"purpose": "Run Python lint checks",
|
||||
"started_at_utc": "2026-10-07T04:01:03.222360Z",
|
||||
"finished_at_utc": "2026-10-07T04:01:03.426930Z",
|
||||
"duration_seconds": 0.20462407637387514,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": "All checks passed!\n"
|
||||
},
|
||||
{
|
||||
"id": "compileall",
|
||||
"argv": [
|
||||
"python3",
|
||||
"-m",
|
||||
"compileall",
|
||||
"-q",
|
||||
"jarca_ledger_mcp",
|
||||
"dashboard",
|
||||
"tests",
|
||||
"scripts"
|
||||
],
|
||||
"purpose": "Compile production, dashboard, test, and script Python surfaces",
|
||||
"started_at_utc": "2026-10-07T04:01:03.427003Z",
|
||||
"finished_at_utc": "2026-10-07T04:01:03.684488Z",
|
||||
"duration_seconds": 0.25754371751099825,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": ""
|
||||
},
|
||||
{
|
||||
"id": "diff-check",
|
||||
"argv": [
|
||||
"git",
|
||||
"diff",
|
||||
"--check",
|
||||
"5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8..1b9eda683c66d3c7129716b9899eddf61df3362d"
|
||||
],
|
||||
"purpose": "Check the complete rework diff for whitespace errors",
|
||||
"started_at_utc": "2026-10-07T04:01:03.684567Z",
|
||||
"finished_at_utc": "2026-10-07T04:01:03.730543Z",
|
||||
"duration_seconds": 0.04603164829313755,
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"output": ""
|
||||
}
|
||||
],
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "docs/data-model.md",
|
||||
"sha256": "f9932a04f3c74907da1c21d754a16e1401f64fd948945017d410de7d91494dca",
|
||||
"git_blob": "70f26ce1522b8609a93471655c9506040a4078f1"
|
||||
},
|
||||
{
|
||||
"path": "docs/interface-contract.md",
|
||||
"sha256": "01a3f37e23ee17b2b928b44e3e07d4d6982faead1ab6f0298889225b0c5ce916",
|
||||
"git_blob": "4304427a604949b8295b4f93cb6054357805fe92"
|
||||
},
|
||||
{
|
||||
"path": "docs/requirements/req-003_account-valuation-history.md",
|
||||
"sha256": "f5a3bf9f69d622451a1a08854062218382f03355a3464538a35b1e270ca5b70f",
|
||||
"git_blob": "2a7be4d780999fce47402f15f0b05d0b92990715"
|
||||
},
|
||||
{
|
||||
"path": "docs/verification/issue-12-review-61/red-evidence.json",
|
||||
"sha256": "567c03d28e6de74b6adfc0f6141dd96e9e129bab9d4455761615a1655870b4bf",
|
||||
"git_blob": "b5f4bc9286b4ea443671ab0744935a11c295b124"
|
||||
},
|
||||
{
|
||||
"path": "jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"sha256": "f7f5bac1e224401dfd02efb321b7e7bd87e02875b727a5366bcc223bf633fb57",
|
||||
"git_blob": "0dfd7b7ce94a699b855e40b336afa7bf5cdc2595"
|
||||
},
|
||||
{
|
||||
"path": "jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"sha256": "41dd6579dc422549316da05347c6d5e33d89da1e1502d6065b5b78a0160776d3",
|
||||
"git_blob": "48a1dd5acfe782c9cc35e8bd615dc71671dadf6f"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_account_valuation_edge_contracts.py",
|
||||
"sha256": "fed17aec3518e2380d00b46252402ad69a71e7d265961a6264b03d790d579f42",
|
||||
"git_blob": "0b08cb32af63084225e01642d333ca291c7445c4"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_issue_12_review59_account.py",
|
||||
"sha256": "a8a7491c7b78841c2be0dbc66913c1e076bdd231281fd64818493a0f3a1de550",
|
||||
"git_blob": "f5cee3e993f189e688f8e8ab3829b5c4ab34cf5a"
|
||||
},
|
||||
{
|
||||
"path": "tests/test_valuation_contract_docs.py",
|
||||
"sha256": "7443b8425d6b733e0e95d21b3c460a42dceeb2f99f84e1fb8fadcd88f6b77514",
|
||||
"git_blob": "c473ee14a530496e11c28e33008b0d8a50d774cb"
|
||||
}
|
||||
],
|
||||
"added_line_security_scan": {
|
||||
"hardcoded_secret": [],
|
||||
"shell_injection": [],
|
||||
"dynamic_eval": [],
|
||||
"pickle": [],
|
||||
"interpolated_sql": []
|
||||
},
|
||||
"worktree_clean_after": true,
|
||||
"limitations": [
|
||||
"Four external private PDF fixture tests may be skipped when fixtures are unavailable.",
|
||||
"Gitea status/readiness is not functional acceptance evidence."
|
||||
],
|
||||
"changed_files_scope": "Behavior-bearing source, tests, normative docs, and immutable RED evidence at source_head_sha; mutable verification metadata and generated GREEN/manifest artifacts are intentionally excluded from hash binding."
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.evidence-plan/v1",
|
||||
"owner": "jsfamily",
|
||||
"repository": "jarca-ledger-mcp",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review_id": 61,
|
||||
"reviewed_parent_sha": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
|
||||
"reviewed_head": "1b9eda683c66d3c7129716b9899eddf61df3362d",
|
||||
"implementation_parent": "b8b8811a2a50371795a02e4fa65f097c9d9508ac",
|
||||
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||
"manifest_generator": {
|
||||
"path": "/opt/data/gitea-agent/.run/i12-r61/run_final_evidence.py",
|
||||
"execution": "python3 /opt/data/gitea-agent/.run/i12-r61/run_final_evidence.py"
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"id": "conversion-regression",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-q", "-rs", "tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation", "tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_without_valuations_remains_allowed"]
|
||||
},
|
||||
{
|
||||
"id": "review61-regressions",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-q", "-rs", "tests/test_account_valuation_edge_contracts.py", "tests/test_issue_12_review59_account.py", "tests/test_valuation_contract_docs.py"]
|
||||
},
|
||||
{
|
||||
"id": "full-suite",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-q", "-rs"]
|
||||
},
|
||||
{
|
||||
"id": "ruff",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "ruff", "check", "."]
|
||||
},
|
||||
{
|
||||
"id": "compileall",
|
||||
"argv": ["python3", "-m", "compileall", "-q", "jarca_ledger_mcp", "dashboard", "tests", "scripts"]
|
||||
},
|
||||
{
|
||||
"id": "diff-check",
|
||||
"argv": ["git", "diff", "--check", "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8..1b9eda683c66d3c7129716b9899eddf61df3362d"]
|
||||
}
|
||||
],
|
||||
"bound_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_issue_12_review59_account.py",
|
||||
"tests/test_valuation_contract_docs.py",
|
||||
"docs/data-model.md",
|
||||
"docs/interface-contract.md",
|
||||
"docs/requirements/req-003_account-valuation-history.md",
|
||||
"docs/verification/issue-12-review-61/red-evidence.json",
|
||||
"docs/verification/issue-12-review-61/finding-matrix.json",
|
||||
"docs/verification/issue-12-review-61/contract-preservation-inventory.json",
|
||||
"docs/verification/issue-12-review-61/evidence-plan.json"
|
||||
],
|
||||
"redaction": "Synthetic fixtures only; no credentials, real account identifiers, financial documents, or environment secrets.",
|
||||
"limitations": [
|
||||
"External private PDF fixtures may be skipped when unavailable.",
|
||||
"Gitea commit status is readiness metadata, not functional acceptance evidence."
|
||||
]
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.finding-matrix/v1",
|
||||
"owner": "jsfamily",
|
||||
"repository": "jarca-ledger-mcp",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review_id": 61,
|
||||
"reviewed_parent_sha": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
|
||||
"reviewed_head": "1b9eda683c66d3c7129716b9899eddf61df3362d",
|
||||
"implementation_parent": "b8b8811a2a50371795a02e4fa65f097c9d9508ac",
|
||||
"findings": [
|
||||
{
|
||||
"id": "N01-loan-balance-definition",
|
||||
"classification": "implementation_defect",
|
||||
"status": "resolved_and_verified_at_source_head",
|
||||
"root_cause": "Internal candidate writers could persist loan balance evidence without an explicit representative liability definition, and unsupported loan purposes could bypass the definition rule.",
|
||||
"tests": [
|
||||
"test_direct_loan_candidate_requires_liability_definition_and_writes_nothing",
|
||||
"test_import_loan_candidate_requires_liability_definition_and_writes_nothing",
|
||||
"test_dated_loan_create_and_historical_update_preserve_current_cache_without_valuations",
|
||||
"test_loan_cash_balance_and_total_value_are_rejected_without_writes",
|
||||
"test_loan_import_preserves_transactions_and_current_cache_without_valuation_candidate"
|
||||
],
|
||||
"changed_paths": [
|
||||
"jarca_ledger_mcp/application/account_valuation_service.py",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"tests/test_issue_12_review59_account.py",
|
||||
"docs/data-model.md",
|
||||
"docs/interface-contract.md",
|
||||
"docs/requirements/req-003_account-valuation-history.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "N01-account-type-conversion-bypass",
|
||||
"classification": "implementation_defect",
|
||||
"status": "resolved",
|
||||
"root_cause": "update_ledger_account allowed a non-loan account with definition-less historical valuations to become a loan account without validating the persisted history.",
|
||||
"named_regression": "test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation",
|
||||
"positive_regression": "test_bank_to_loan_conversion_without_valuations_remains_allowed",
|
||||
"red_evidence": "docs/verification/issue-12-review-61/red-evidence.json",
|
||||
"minimal_fix": "Reject non-loan to loan conversion before mutation when any attached valuation lacks one of the two explicit valid representative liability definitions.",
|
||||
"changed_paths": [
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"tests/test_account_valuation_edge_contracts.py",
|
||||
"docs/data-model.md",
|
||||
"docs/interface-contract.md",
|
||||
"docs/requirements/req-003_account-valuation-history.md"
|
||||
],
|
||||
"risks": [
|
||||
"Do not infer or backfill a liability definition.",
|
||||
"Do not reject conversion when no valuation history exists.",
|
||||
"Do not partially mutate account or valuation history on rejection."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"schema": "gitea-agent.green-evidence/v1",
|
||||
"finding": "N01-account-type-conversion-bypass",
|
||||
"source_commit": "1b9eda683c66d3c7129716b9899eddf61df3362d",
|
||||
"tests": [
|
||||
"test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation",
|
||||
"test_bank_to_loan_conversion_without_valuations_remains_allowed"
|
||||
],
|
||||
"argv": [
|
||||
"uv",
|
||||
"run",
|
||||
"--frozen",
|
||||
"--extra",
|
||||
"dev",
|
||||
"--extra",
|
||||
"dashboard",
|
||||
"pytest",
|
||||
"-q",
|
||||
"-rs",
|
||||
"tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation",
|
||||
"tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_without_valuations_remains_allowed"
|
||||
],
|
||||
"started_at_utc": "2026-10-07T03:59:49.130624Z",
|
||||
"finished_at_utc": "2026-10-07T03:59:51.888407Z",
|
||||
"exit_code": 0,
|
||||
"expected_exit_code": 0,
|
||||
"result": "GREEN",
|
||||
"output": ".. [100%]\n",
|
||||
"fixture_policy": "synthetic only; no credentials or real financial data"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"gate": "independent-bank-to-loan-conversion",
|
||||
"source_sha": "b8b8811a2a50371795a02e4fa65f097c9d9508ac",
|
||||
"test": "tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation",
|
||||
"command": "uv run --frozen --extra dev --extra dashboard pytest tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation -q",
|
||||
"exit_code": 1,
|
||||
"result": "RED",
|
||||
"output": "F [100%]\n=================================== FAILURES ===================================\n_ test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation _\n\n assert rejected[\"success\"] is False\nE assert True is False\n=========================== short test summary info ============================\nFAILED tests/test_account_valuation_edge_contracts.py::test_bank_to_loan_conversion_rejects_existing_undefined_valuation_without_mutation"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"schema": "contract-preservation-inventory/v1",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review_id": 63,
|
||||
"source_revision": "3db086a7fd15e9152a57fb95ab7942c6dc3be2b1",
|
||||
"contracts": [
|
||||
{
|
||||
"contract": "append-only immutable snapshot runs and corrections",
|
||||
"preserved_by": ["test_f002_correction_replay_preserves_full_initial_response", "test_f006_initial_replay_survives_multiple_later_corrections", "tests/test_asset_snapshot_idempotency.py"]
|
||||
},
|
||||
{
|
||||
"contract": "preview-to-close authority drift protection",
|
||||
"preserved_by": ["test_f001_preview_fingerprint_uses_same_unredacted_authority_and_rejects_drift"]
|
||||
},
|
||||
{
|
||||
"contract": "conditional zero-row closes remain visible historical authority",
|
||||
"preserved_by": ["test_f003_zero_row_conditional_runs_are_timeseries_authority_with_aliases"]
|
||||
},
|
||||
{
|
||||
"contract": "closed consumers use frozen valuation identity rather than live valuation state",
|
||||
"preserved_by": ["test_f004_closed_report_restores_frozen_valuation_identity_without_legacy_invention"]
|
||||
},
|
||||
{
|
||||
"contract": "NULL and zero remain distinct and balance basis dates require an actual balance",
|
||||
"preserved_by": ["test_create_rejects_basis_only_without_mutation", "test_basis_only_create_cannot_block_later_dated_import"]
|
||||
},
|
||||
{
|
||||
"contract": "existing Issue 12 behavior outside Review 63 remains intact",
|
||||
"preserved_by": ["review63-full-suite", "review63-ruff", "review63-compileall", "review63-diff-check"]
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Private KB Bank and Toss Securities PDF fixtures were unavailable; four fixture-dependent tests are reported as skips."
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"environment": ["os", "kernel", "architecture", "python", "uv"],
|
||||
"command_env_allowlist": ["PATH", "HOME", "TMPDIR", "LANG", "VIRTUAL_ENV", "UV_CACHE_DIR"],
|
||||
"secret_env_names": [],
|
||||
"forbidden_regexes": [],
|
||||
"hash_paths": [
|
||||
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"jarca_ledger_mcp/application/assets_service.py",
|
||||
"tests/test_issue_12_review63_accounts.py",
|
||||
"tests/test_issue_12_review63_assets.py",
|
||||
"tests/test_asset_snapshot_idempotency.py",
|
||||
"tests/test_issue_12_regressions.py",
|
||||
"docs/interface-contract.md",
|
||||
"docs/data-model.md",
|
||||
"docs/requirements/req-003_account-valuation-history.md"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"id": "review63-regressions",
|
||||
"purpose": "Verify all named Review 63 RED to GREEN regressions",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-q", "tests/test_issue_12_review63_accounts.py", "tests/test_issue_12_review63_assets.py"],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": ["review63-findings-green"]
|
||||
},
|
||||
{
|
||||
"id": "full-suite",
|
||||
"purpose": "Run the canonical full repository suite at the functional source revision",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-ra"],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": ["review63-full-suite"]
|
||||
},
|
||||
{
|
||||
"id": "ruff",
|
||||
"purpose": "Run repository lint checks",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "ruff", "check", "."],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": ["review63-ruff"]
|
||||
},
|
||||
{
|
||||
"id": "compileall",
|
||||
"purpose": "Compile package, dashboard, tests, and scripts",
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "python", "-m", "compileall", "-q", "jarca_ledger_mcp", "dashboard", "tests", "scripts"],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": ["review63-compileall"]
|
||||
},
|
||||
{
|
||||
"id": "diff-check",
|
||||
"purpose": "Check the complete pull-request diff for whitespace errors",
|
||||
"argv": ["git", "diff", "--check", "main...HEAD"],
|
||||
"expected_exit_code": 0,
|
||||
"max_output_bytes": 65536,
|
||||
"claims": ["review63-diff-check"]
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Four PDF fixture tests are skipped because private KB Bank and Toss Securities PDF fixtures are unavailable."
|
||||
]
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"schema": "review-finding-matrix/v1",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review_id": 63,
|
||||
"reviewed_head": "3b5101128d5a2cf038b8fb84facfd9d176928c26",
|
||||
"source_revision": "3db086a7fd15e9152a57fb95ab7942c6dc3be2b1",
|
||||
"findings": [
|
||||
{
|
||||
"id": "F-001",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/assets_service.py",
|
||||
"regression": "test_f001_preview_fingerprint_uses_same_unredacted_authority_and_rejects_drift",
|
||||
"claim": "preview and close hash the same private authority representation; real drift is rejected without a run write"
|
||||
},
|
||||
{
|
||||
"id": "F-002",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/assets_service.py",
|
||||
"regressions": [
|
||||
"test_f002_close_replay_is_byte_for_byte_equal_for_mixed_account_manual_as_of",
|
||||
"test_f002_correction_replay_preserves_full_initial_response"
|
||||
],
|
||||
"claim": "initial and correction replays reconstruct the original immutable response, warning set, provenance, item ordering, and IDs"
|
||||
},
|
||||
{
|
||||
"id": "F-003",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/assets_service.py",
|
||||
"regression": "test_f003_zero_row_conditional_runs_are_timeseries_authority_with_aliases",
|
||||
"claim": "timeseries enumerates both legacy snapshot and run-only basis dates and exposes the latest zero-row conditional run"
|
||||
},
|
||||
{
|
||||
"id": "F-004",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/assets_service.py",
|
||||
"regression": "test_f004_closed_report_restores_frozen_valuation_identity_without_legacy_invention",
|
||||
"claim": "closed reports restore persisted valuation identity and currency without consulting live authority or inventing legacy IDs"
|
||||
},
|
||||
{
|
||||
"id": "F-005",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/accounts_write_services.py",
|
||||
"regressions": [
|
||||
"test_create_rejects_basis_only_without_mutation",
|
||||
"test_basis_only_create_cannot_block_later_dated_import"
|
||||
],
|
||||
"claim": "basis-only creates fail before mutation while zero-with-basis and unknown pairs remain valid"
|
||||
},
|
||||
{
|
||||
"id": "F-006",
|
||||
"status": "resolved",
|
||||
"production": "jarca_ledger_mcp/application/assets_service.py",
|
||||
"regression": "test_f006_initial_replay_survives_multiple_later_corrections",
|
||||
"claim": "an immutable run can be replayed through the complete later correction chain while unrelated live-authority drift still conflicts"
|
||||
}
|
||||
],
|
||||
"green_evidence": "evidence-manifest.json"
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"schema": "review-red-evidence/v1",
|
||||
"issue": 12,
|
||||
"pull_request": 13,
|
||||
"review_id": 63,
|
||||
"reviewed_head": "3b5101128d5a2cf038b8fb84facfd9d176928c26",
|
||||
"review_state": "REQUEST_CHANGES",
|
||||
"findings": [
|
||||
{
|
||||
"id": "F-001",
|
||||
"test": "test_f001_preview_fingerprint_uses_same_unredacted_authority_and_rejects_drift",
|
||||
"red_observation": "unchanged preview fingerprint was rejected with authority_drift"
|
||||
},
|
||||
{
|
||||
"id": "F-002",
|
||||
"tests": [
|
||||
"test_f002_close_replay_is_byte_for_byte_equal_for_mixed_account_manual_as_of",
|
||||
"test_f002_correction_replay_preserves_full_initial_response"
|
||||
],
|
||||
"red_observation": "replay responses differed in warnings/provenance/items or conflicted"
|
||||
},
|
||||
{
|
||||
"id": "F-003",
|
||||
"test": "test_f003_zero_row_conditional_runs_are_timeseries_authority_with_aliases",
|
||||
"red_observation": "zero-row closed run produced no timeseries point"
|
||||
},
|
||||
{
|
||||
"id": "F-004",
|
||||
"test": "test_f004_closed_report_restores_frozen_valuation_identity_without_legacy_invention",
|
||||
"red_observation": "closed report omitted persisted valuation_id and currency"
|
||||
},
|
||||
{
|
||||
"id": "F-005",
|
||||
"tests": [
|
||||
"test_create_rejects_basis_only_without_mutation",
|
||||
"test_basis_only_create_cannot_block_later_dated_import"
|
||||
],
|
||||
"red_observation": "basis-only account creation succeeded and could suppress a real dated import balance"
|
||||
},
|
||||
{
|
||||
"id": "F-006",
|
||||
"test": "test_f006_initial_replay_survives_multiple_later_corrections",
|
||||
"red_observation": "replaying the initial key first conflicted after two corrections; after that path was enabled, unrelated live-authority drift after the correction chain was incorrectly accepted"
|
||||
}
|
||||
],
|
||||
"red_runs": [
|
||||
{
|
||||
"argv": ["pytest", "-q", "tests/test_issue_12_review63_assets.py"],
|
||||
"exit_code": 1,
|
||||
"result": "5 failed before production changes",
|
||||
"limitation": "isolated development environment used by the delegated TDD worker"
|
||||
},
|
||||
{
|
||||
"argv": ["uv", "run", "pytest", "-q", "tests/test_issue_12_review63_accounts.py"],
|
||||
"exit_code": 1,
|
||||
"result": "2 failed before production changes"
|
||||
},
|
||||
{
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-q", "tests/test_issue_12_review63_assets.py::test_f002_correction_replay_preserves_full_initial_response"],
|
||||
"exit_code": 1,
|
||||
"result": "correction replay omitted existing_snapshot_rows and differed from the original response"
|
||||
},
|
||||
{
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-q", "tests/test_issue_12_review63_assets.py::test_f006_initial_replay_survives_multiple_later_corrections"],
|
||||
"exit_code": 1,
|
||||
"result": "initial replay after two later corrections returned idempotency_conflict"
|
||||
},
|
||||
{
|
||||
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-q", "tests/test_issue_12_review63_assets.py::test_f006_initial_replay_survives_multiple_later_corrections"],
|
||||
"exit_code": 1,
|
||||
"result": "unrelated live-authority drift after the correction chain returned success instead of idempotency_conflict"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Jarca Ledger MCP server."""
|
||||
|
||||
__version__ = "0.1.4"
|
||||
__version__ = "0.1.5"
|
||||
|
||||
@@ -27,6 +27,12 @@ def create_ledger_account(name: str, type: str = "bank", institution: str = "",
|
||||
except Exception as e: return fail("validation_error", str(e))
|
||||
if basis_date and (last_balance is None or last_balance == ""):
|
||||
return fail("validation_error", "last_balance_basis_date requires last_balance")
|
||||
if last_balance is not None:
|
||||
try:
|
||||
from .account_valuation_service import _strict_int
|
||||
last_balance = _strict_int(last_balance, "last_balance", allow_none=False)
|
||||
except (ValueError, TypeError) as e:
|
||||
return fail("validation_error", str(e))
|
||||
with db(db_path) as conn:
|
||||
conn.execute("INSERT INTO accounts(id,name,type,institution,currency,active,memo,account_number_masked,holder_name,last_balance,last_balance_basis_date,last_synced_at,statement_status,liquidity_bucket,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (account_id,name.strip(),type,institution,currency.upper(),int(active),memo,account_number_masked,holder_name,last_balance,basis_date,last_synced_at,statement_status_norm,liquidity_bucket_norm,ts,ts))
|
||||
row = conn.execute("SELECT * FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
@@ -44,6 +50,13 @@ def update_ledger_account(account_id: str, db_path: str | None = None, **fields:
|
||||
basis_update = "last_balance_basis_date" in fields and fields.get("last_balance_basis_date") is not None
|
||||
if basis_update and not balance_update:
|
||||
return fail("validation_error", "last_balance_basis_date requires last_balance")
|
||||
normalized_balance = None
|
||||
if balance_update:
|
||||
try:
|
||||
from .account_valuation_service import _strict_int
|
||||
normalized_balance = _strict_int(fields["last_balance"], "last_balance", allow_none=False)
|
||||
except (ValueError, TypeError) as e:
|
||||
return fail("validation_error", str(e))
|
||||
normalized_basis = None
|
||||
if basis_update:
|
||||
try:
|
||||
@@ -58,7 +71,7 @@ def update_ledger_account(account_id: str, db_path: str | None = None, **fields:
|
||||
if k == "statement_status": v = normalize_statement_status(str(v))
|
||||
if k == "liquidity_bucket": v = normalize_liquidity_bucket(str(v))
|
||||
if k == "active": v = int(bool(v))
|
||||
if k == "last_balance" and v != "": v = int(v)
|
||||
if k == "last_balance": v = normalized_balance
|
||||
if k == "currency": v = str(v).upper()
|
||||
if k == "last_balance_basis_date": v = normalized_basis
|
||||
except Exception as e: return fail("validation_error", str(e))
|
||||
@@ -77,7 +90,7 @@ def update_ledger_account(account_id: str, db_path: str | None = None, **fields:
|
||||
stored_date = str(current["last_balance_basis_date"] or "")[:10]
|
||||
incoming_date = normalized_basis if basis_update else ""
|
||||
historical = bool(balance_update and incoming_date and stored_date and incoming_date < stored_date)
|
||||
candidate_balance = fields.get("last_balance", current["last_balance"])
|
||||
candidate_balance = normalized_balance if balance_update else current["last_balance"]
|
||||
if historical:
|
||||
kept=[(u,p) for u,p in zip(updates,params) if not u.startswith(("last_balance=","last_balance_basis_date=","last_synced_at="))]
|
||||
updates=[u for u,_ in kept]; params=[p for _,p in kept]
|
||||
|
||||
@@ -259,6 +259,16 @@ def _manual_asset_valuation_items(conn: sqlite3.Connection, active_only: bool =
|
||||
return included, excluded
|
||||
|
||||
|
||||
def _persisted_evidence_array(value: Any) -> list[dict[str, Any]] | None:
|
||||
try:
|
||||
decoded = json.loads(value or "[]")
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return None
|
||||
if not isinstance(decoded, list) or any(not isinstance(item, dict) for item in decoded):
|
||||
return None
|
||||
return decoded
|
||||
|
||||
|
||||
def _snapshot_rows_for_run(conn: sqlite3.Connection, run_id: str) -> tuple[list[sqlite3.Row], dict[str, Any]]:
|
||||
"""Load one immutable close run and only the rows persisted by that run."""
|
||||
run = conn.execute(
|
||||
@@ -287,6 +297,11 @@ def _snapshot_rows_for_run(conn: sqlite3.Connection, run_id: str) -> tuple[list[
|
||||
)""",
|
||||
(basis_date, run["_run_rowid"]),
|
||||
).fetchone()[0]
|
||||
evidence = {
|
||||
field: _persisted_evidence_array(run[field])
|
||||
for field in ("applied_evidence", "excluded_evidence", "carried_evidence")
|
||||
}
|
||||
integrity_error = any(value is None for value in evidence.values())
|
||||
metadata = {
|
||||
"snapshot_run_id": run["id"],
|
||||
"is_correction": bool(run["correction_of_run_id"]),
|
||||
@@ -298,9 +313,10 @@ def _snapshot_rows_for_run(conn: sqlite3.Connection, run_id: str) -> tuple[list[
|
||||
"approved_at": run["approved_at"],
|
||||
"approval_reason": run["approval_reason"],
|
||||
"approval_reference": run["approval_reference"],
|
||||
"applied_evidence": json.loads(run["applied_evidence"] or "[]"),
|
||||
"excluded_evidence": json.loads(run["excluded_evidence"] or "[]"),
|
||||
"carried_evidence": json.loads(run["carried_evidence"] or "[]"),
|
||||
"applied_evidence": evidence["applied_evidence"] or [],
|
||||
"excluded_evidence": evidence["excluded_evidence"] or [],
|
||||
"carried_evidence": evidence["carried_evidence"] or [],
|
||||
"evidence_integrity_error": integrity_error,
|
||||
"closed_at": run["created_at"] or "",
|
||||
"previous_snapshot_row_count": previous_rows,
|
||||
"total_snapshot_rows_for_basis_date": total_rows,
|
||||
@@ -312,6 +328,32 @@ def _snapshot_rows_for_latest_run(conn: sqlite3.Connection, basis_date: str) ->
|
||||
rows = conn.execute("SELECT rowid AS _rowid, * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC", (basis_date,)).fetchall()
|
||||
run = conn.execute("SELECT * FROM asset_snapshot_runs WHERE basis_date=? ORDER BY rowid DESC LIMIT 1", (basis_date,)).fetchone()
|
||||
if not run:
|
||||
legacy_run_rows = [row for row in rows if row["closed_at"] and row["snapshot_run_id"]]
|
||||
if legacy_run_rows:
|
||||
# Schema 11 persisted run identity only on the snapshot rows. Keep
|
||||
# its rowid-based latest-run rule rather than falling back to live
|
||||
# authority merely because schema 12 has no matching run record.
|
||||
latest = max(
|
||||
legacy_run_rows,
|
||||
key=lambda row: (
|
||||
int(row["_rowid"]),
|
||||
row["closed_at"] or row["created_at"] or "",
|
||||
row["snapshot_run_id"],
|
||||
),
|
||||
)
|
||||
latest_run_id = latest["snapshot_run_id"]
|
||||
latest_rows = [row for row in legacy_run_rows if row["snapshot_run_id"] == latest_run_id]
|
||||
return latest_rows, {
|
||||
"snapshot_run_id": latest_run_id,
|
||||
"legacy_closed": True,
|
||||
"is_correction": bool(latest["is_correction"]),
|
||||
"correction_of_run_id": latest["correction_of_run_id"] or "",
|
||||
"correction_reason": latest["correction_reason"] or "",
|
||||
"close_status": "legacy_read_only",
|
||||
"selection_policy": latest["selection_policy"] or "exact",
|
||||
"closed_at": latest["closed_at"] or latest["created_at"] or "",
|
||||
"total_snapshot_rows_for_basis_date": len(rows),
|
||||
}
|
||||
return rows, {"snapshot_run_id": "", "is_correction": False, "correction_of_run_id": "", "correction_reason": "", "total_snapshot_rows_for_basis_date": len(rows)} if rows else {}
|
||||
return _snapshot_rows_for_run(conn, run["id"])
|
||||
|
||||
@@ -372,7 +414,10 @@ def _closed_run_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
for item in exclusions:
|
||||
if item.get("candidate"):
|
||||
key = (item.get("source_type", ""), item.get("source_id", ""))
|
||||
warning = candidate_warnings.setdefault(key, {"code": "unverified_candidate", "source_type": key[0], "source_id": key[1], "candidates": []})
|
||||
warning_shape = {"code": "unverified_candidate", "source_type": key[0], "source_id": key[1], "candidates": []}
|
||||
if item.get("blocking") is False:
|
||||
warning_shape["blocking"] = False
|
||||
warning = candidate_warnings.setdefault(key, warning_shape)
|
||||
warning["candidates"].append(item["candidate"])
|
||||
else:
|
||||
warnings.append({"code": item.get("reason", "excluded_item"), "source_type": item.get("source_type", ""), "source_id": item.get("source_id", ""), "currency": item.get("currency", "KRW")})
|
||||
@@ -384,7 +429,7 @@ def _closed_run_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
"snapshot_run_id": metadata.get("snapshot_run_id", ""),
|
||||
"close_status": metadata.get("close_status", ""),
|
||||
"selection_policy": metadata.get("selection_policy", ""),
|
||||
"approval": {k: metadata.get(k, "") for k in ("approved_by", "approved_at", "approval_reason", "approval_reference")},
|
||||
"approval": {} if metadata.get("legacy_closed") else {k: metadata.get(k, "") for k in ("approved_by", "approved_at", "approval_reason", "approval_reference")},
|
||||
"correction": {k: metadata.get(k, "") for k in ("is_correction", "correction_of_run_id", "correction_reason")},
|
||||
}
|
||||
|
||||
@@ -667,6 +712,8 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
if preview_fingerprint and preview_fingerprint != _preview_fingerprint(preview, basis, selection_policy):
|
||||
return fail("authority_drift", "asset authority changed since preview", basis_date=basis)
|
||||
latest_rows, latest_meta = _snapshot_rows_for_latest_run(conn, basis)
|
||||
if latest_meta.get("evidence_integrity_error"):
|
||||
return fail("data_integrity_error", "snapshot evidence is malformed", snapshot_run_id=latest_meta.get("snapshot_run_id", ""))
|
||||
legacy_snapshot_rows = bool(latest_rows and not latest_meta.get("snapshot_run_id"))
|
||||
if legacy_snapshot_rows and not correction_reason.strip():
|
||||
return fail(
|
||||
@@ -734,6 +781,8 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
or immutable_replay_run
|
||||
):
|
||||
replay_rows, replay_meta = _snapshot_rows_for_run(conn, existing_run["id"])
|
||||
if replay_meta.get("evidence_integrity_error"):
|
||||
return fail("data_integrity_error", "snapshot evidence is malformed", snapshot_run_id=existing_run["id"])
|
||||
replay_items = [_public_evidence(item) for item in replay_meta.get("applied_evidence", [])]
|
||||
replay_warnings = _closed_warnings(replay_meta, basis, replay_rows)
|
||||
replay_public = _closed_run_metadata(replay_meta)
|
||||
@@ -816,7 +865,7 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
),
|
||||
)
|
||||
snapshot_ids.append(snapshot_id)
|
||||
applied_evidence = [{k: item.get(k) for k in ("id", "source_type", "source_id", "authority_source", "name", "type", "liquidity_bucket", "active", "amount", "valuation_amount", "last_balance", "last_synced_at", "currency", "valuation_id", "valuation_kind", "valuation_date", "age_days", "carried_forward", "verification_status", "evidence_kind") if item.get(k) is not None} for item in included]
|
||||
applied_evidence = [{k: item.get(k) for k in ("id", "source_type", "source_id", "authority_source", "name", "type", "liquidity_bucket", "active", "amount", "valuation_amount", "last_balance", "last_synced_at", "currency", "valuation_id", "valuation_kind", "valuation_date", "age_days", "carried_forward", "verification_status", "evidence_kind", "unverified_candidates") if item.get(k) is not None} for item in included]
|
||||
excluded_evidence = [{k: item.get(k) for k in ("source_type", "source_id", "reason", "currency")} for item in preview.get("excluded_items", [])]
|
||||
excluded_evidence.extend(
|
||||
{
|
||||
@@ -824,6 +873,7 @@ def close_asset_snapshot(basis_date: str = "", source_note: str = "", memo: str
|
||||
"source_id": warning.get("source_id", ""),
|
||||
"reason": "unverified_candidate",
|
||||
"candidate": candidate,
|
||||
"blocking": warning.get("blocking", True),
|
||||
"state_hash": next(
|
||||
state["state_hash"]
|
||||
for state in preview["_candidate_state_hashes"]
|
||||
@@ -922,17 +972,22 @@ def get_asset_snapshot_report(basis_date: str = "", db_path: str | None = None,
|
||||
return fail("validation_error", f"invalid date: {basis_date}")
|
||||
with read_db(db_path) as conn:
|
||||
rows, close_metadata = _snapshot_rows_for_latest_run(conn, basis)
|
||||
if close_metadata.get("evidence_integrity_error"):
|
||||
return fail("data_integrity_error", "snapshot evidence is malformed", snapshot_run_id=close_metadata.get("snapshot_run_id", ""))
|
||||
has_close_run = bool(close_metadata.get("snapshot_run_id"))
|
||||
legacy_closed = bool(close_metadata.get("legacy_closed"))
|
||||
snapshots = _snapshot_items(conn, basis) if rows else []
|
||||
legacy_snapshots = snapshots if rows and not has_close_run else []
|
||||
if has_close_run:
|
||||
if has_close_run or legacy_closed:
|
||||
run_public = _closed_run_metadata(close_metadata)
|
||||
run_public["warnings"] = _closed_warnings(close_metadata, basis, rows)
|
||||
if legacy_closed:
|
||||
run_public["approval"] = {}
|
||||
summary = _summarize_valuation_items(snapshots, run_public["excluded_items"])
|
||||
evidence_provenance = _evidence_provenance(close_metadata.get("applied_evidence", []), basis)
|
||||
return _ok_with_provenance_aliases(
|
||||
basis_date=basis,
|
||||
source="asset_snapshots",
|
||||
source="legacy_asset_snapshots" if legacy_closed else "asset_snapshots",
|
||||
is_closed_snapshot=True,
|
||||
snapshot_run_id=close_metadata.get("snapshot_run_id", ""),
|
||||
snapshot_count=len(snapshots),
|
||||
@@ -1032,8 +1087,10 @@ def get_net_worth_timeseries(start_date: str = "", end_date: str = "", months: i
|
||||
points = []
|
||||
for row in basis_rows:
|
||||
basis = row["basis_date"]
|
||||
items = _snapshot_items(conn, basis)
|
||||
_, metadata = _snapshot_rows_for_latest_run(conn, basis)
|
||||
if metadata.get("evidence_integrity_error"):
|
||||
return fail("data_integrity_error", "snapshot evidence is malformed", snapshot_run_id=metadata.get("snapshot_run_id", ""))
|
||||
items = _snapshot_items(conn, basis)
|
||||
run_public = _closed_run_metadata(metadata)
|
||||
run_public["warnings"] = _closed_warnings(metadata, basis, conn.execute("SELECT rowid AS _rowid, * FROM asset_snapshots WHERE basis_date=? ORDER BY created_at ASC, name ASC", (basis,)).fetchall())
|
||||
run_provenance = _evidence_provenance(metadata.get("applied_evidence", []), basis)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jarca-ledger-mcp"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
description = "Jarca household ledger MCP server for transactions, accounts, budgets, cashflow, and investable cashflow analysis"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,10 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
|
||||
|
||||
def test_replay_preserves_redacted_unverified_candidates_after_correction(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("투자 계좌", type="investment", db_path=db_path)
|
||||
account_id = account["account"]["id"]
|
||||
verified = service.register_account_valuation(
|
||||
account_id, "2026-06-30", 100, verification_status="verified",
|
||||
idempotency_key="valuation-verified", db_path=db_path,
|
||||
)
|
||||
candidate = service.register_account_valuation(
|
||||
account_id, "2026-06-30", 110, verification_status="unverified",
|
||||
idempotency_key="valuation-candidate", db_path=db_path,
|
||||
)
|
||||
|
||||
first = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="candidate-close-a", db_path=db_path,
|
||||
)
|
||||
assert first["success"] is True
|
||||
assert first["included_items"][0]["unverified_candidates"] == [{
|
||||
"id": candidate["valuation"]["id"],
|
||||
"basis_date": "2026-06-30",
|
||||
"currency": "KRW",
|
||||
"valuation_kind": "balance",
|
||||
"verification_status": "unverified",
|
||||
}]
|
||||
assert {warning["blocking"] for warning in first["warnings"] if warning["code"] == "unverified_candidate"} == {False}
|
||||
assert first["included_items"][0]["valuation_id"] == verified["valuation"]["id"]
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
persisted = conn.execute("SELECT applied_evidence, excluded_evidence FROM asset_snapshot_runs WHERE id=?", (first["snapshot_run_id"],)).fetchone()
|
||||
applied_evidence, excluded_evidence = map(json.loads, persisted)
|
||||
assert applied_evidence[0]["unverified_candidates"] == first["included_items"][0]["unverified_candidates"]
|
||||
candidate_evidence = [item for item in excluded_evidence if item.get("reason") == "unverified_candidate"]
|
||||
assert candidate_evidence[0]["candidate"] == first["included_items"][0]["unverified_candidates"][0]
|
||||
assert candidate_evidence[0]["blocking"] is False
|
||||
assert candidate_evidence[0]["state_hash"]
|
||||
|
||||
corrected = service.close_asset_snapshot(
|
||||
"2026-06-30", correction_reason="preserve evidence", idempotency_key="candidate-close-b",
|
||||
db_path=db_path,
|
||||
)
|
||||
assert corrected["success"] is True
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
before_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
|
||||
before_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
|
||||
replay = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="candidate-close-a", db_path=db_path,
|
||||
)
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
after_replay = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
|
||||
after_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
|
||||
|
||||
assert replay == first
|
||||
assert replay["included_items"] == first["included_items"]
|
||||
assert replay["warnings"] == first["warnings"]
|
||||
assert after_replay == before_replay
|
||||
assert after_runs == before_runs
|
||||
|
||||
|
||||
def test_replay_preserves_blocking_candidate_warning_shape(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
account_id = service.create_ledger_account(
|
||||
"미확인 계좌", type="investment", db_path=db_path,
|
||||
)["account"]["id"]
|
||||
service.register_account_valuation(
|
||||
account_id, "2026-06-30", 110, verification_status="unverified",
|
||||
idempotency_key="only-candidate", db_path=db_path,
|
||||
)
|
||||
|
||||
first = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="blocking-candidate-close",
|
||||
allow_missing_exclusions=True, approved_by="reviewer",
|
||||
approval_reason="exclude unverified evidence",
|
||||
approval_reference="synthetic-approval", db_path=db_path,
|
||||
)
|
||||
assert first["success"] is True
|
||||
warning = next(item for item in first["warnings"] if item["code"] == "unverified_candidate")
|
||||
assert "blocking" not in warning
|
||||
|
||||
replay = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="blocking-candidate-close",
|
||||
allow_missing_exclusions=True, approved_by="reviewer",
|
||||
approval_reason="exclude unverified evidence",
|
||||
approval_reference="synthetic-approval", db_path=db_path,
|
||||
)
|
||||
|
||||
assert replay == first
|
||||
replay_warning = next(item for item in replay["warnings"] if item["code"] == "unverified_candidate")
|
||||
assert "blocking" not in replay_warning
|
||||
|
||||
|
||||
def test_malformed_persisted_evidence_fails_closed_without_snapshot_writes(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_manual_asset("현금", 100, "2026-06-30", "cash", db_path=db_path)
|
||||
first = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="malformed-evidence-close", db_path=db_path,
|
||||
)
|
||||
assert first["success"] is True
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
"UPDATE asset_snapshot_runs SET excluded_evidence=? WHERE id=?",
|
||||
("{malformed", first["snapshot_run_id"]),
|
||||
)
|
||||
before_rows = conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0]
|
||||
before_runs = conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0]
|
||||
|
||||
report = service.get_asset_snapshot_report("2026-06-30", db_path=db_path)
|
||||
replay = service.close_asset_snapshot(
|
||||
"2026-06-30", idempotency_key="malformed-evidence-close", db_path=db_path,
|
||||
)
|
||||
timeseries = service.get_net_worth_timeseries(
|
||||
"2026-06-01", "2026-06-30", db_path=db_path,
|
||||
)
|
||||
|
||||
for result in (report, replay, timeseries):
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "data_integrity_error"
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
assert conn.execute("SELECT COUNT(*) FROM asset_snapshots").fetchone()[0] == before_rows
|
||||
assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == before_runs
|
||||
|
||||
|
||||
def test_exact_run_replay_after_correction_reconstructs_original_persisted_run(tmp_path):
|
||||
db_path = str(tmp_path / "ledger.sqlite3")
|
||||
service.create_manual_asset("현금", 100, "2026-06-30", "cash", db_path=db_path)
|
||||
|
||||
@@ -65,6 +65,163 @@ def test_late_import_current_balance_is_authoritative_by_evidence_basis_date(mon
|
||||
}
|
||||
|
||||
|
||||
def test_empty_balance_with_basis_date_is_rejected_without_mutation_before_import(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("KB", institution="KB국민은행", db_path=path)["account"]["id"]
|
||||
before = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
before_valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
|
||||
rejected = service.update_ledger_account(
|
||||
account,
|
||||
last_balance="",
|
||||
last_balance_basis_date="2026-07-01",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
after = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
after_valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert after == before
|
||||
assert after_valuations == before_valuations
|
||||
|
||||
monkeypatch.setattr(
|
||||
imports_service,
|
||||
"parse_kb_bank_pdf",
|
||||
lambda *_: _parsed(_transaction("2026-06-30", 900, 1), "hash-valid"),
|
||||
)
|
||||
monkeypatch.setattr(imports_service, "now_iso", lambda: "2026-07-02T09:00:00")
|
||||
imported = imports_service.import_kb_bank_pdf("synthetic", "pw", account_id=account, db_path=path)
|
||||
|
||||
current = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
assert imported["success"] is True
|
||||
assert current["last_balance"] == 900
|
||||
assert current["last_balance_basis_date"] == "2026-06-30"
|
||||
assert {(item["basis_date"], item["amount"]) for item in valuations} == {("2026-06-30", 900)}
|
||||
|
||||
|
||||
def test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
baseline = service.create_ledger_account("baseline", db_path=path)
|
||||
before = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"]
|
||||
before_valuations = service.list_account_valuations(baseline["account"]["id"], db_path=path)["valuations"]
|
||||
|
||||
for invalid_balance in (" ", "not-an-integer", True, False, 1.2, -1.2):
|
||||
rejected = service.create_ledger_account(
|
||||
"invalid",
|
||||
last_balance=invalid_balance,
|
||||
last_balance_basis_date="2026-07-01",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert service.list_ledger_accounts(active_only=False, db_path=path)["accounts"] == before
|
||||
assert service.list_account_valuations(baseline["account"]["id"], db_path=path)["valuations"] == before_valuations
|
||||
|
||||
|
||||
def test_update_rejects_bool_or_float_dated_balance_without_mutation(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account(
|
||||
"baseline",
|
||||
last_balance=900,
|
||||
last_balance_basis_date="2026-07-01",
|
||||
db_path=path,
|
||||
)["account"]["id"]
|
||||
before = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
before_valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
|
||||
for invalid_balance in (True, False, 1.2, -1.2):
|
||||
rejected = service.update_ledger_account(
|
||||
account,
|
||||
last_balance=invalid_balance,
|
||||
last_balance_basis_date="2026-07-02",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0] == before
|
||||
assert service.list_account_valuations(account, db_path=path)["valuations"] == before_valuations
|
||||
|
||||
|
||||
def test_update_rejects_empty_undated_balance_without_mutation(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account(
|
||||
"baseline",
|
||||
last_balance=900,
|
||||
last_balance_basis_date="2026-07-01",
|
||||
db_path=path,
|
||||
)["account"]["id"]
|
||||
before = service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0]
|
||||
before_valuations = service.list_account_valuations(account, db_path=path)["valuations"]
|
||||
|
||||
rejected = service.update_ledger_account(account, last_balance="", db_path=path)
|
||||
|
||||
assert rejected["success"] is False
|
||||
assert rejected["error_code"] == "validation_error"
|
||||
assert service.list_ledger_accounts(active_only=False, db_path=path)["accounts"][0] == before
|
||||
assert service.list_account_valuations(account, db_path=path)["valuations"] == before_valuations
|
||||
|
||||
|
||||
def test_update_preserves_zero_and_signed_integer_balance_semantics(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
account = service.create_ledger_account("baseline", db_path=path)["account"]["id"]
|
||||
|
||||
zero = service.update_ledger_account(
|
||||
account,
|
||||
last_balance=" +0 ",
|
||||
last_balance_basis_date="2026-07-01",
|
||||
db_path=path,
|
||||
)
|
||||
signed = service.update_ledger_account(
|
||||
account,
|
||||
last_balance="-12",
|
||||
last_balance_basis_date="2026-07-02",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert zero["success"] is True
|
||||
assert zero["account"]["last_balance"] == 0
|
||||
assert signed["success"] is True
|
||||
assert signed["account"]["last_balance"] == -12
|
||||
assert signed["account"]["last_balance_basis_date"] == "2026-07-02"
|
||||
assert [(item["basis_date"], item["amount"]) for item in service.list_account_valuations(account, db_path=path)["valuations"]] == [
|
||||
("2026-07-01", 0),
|
||||
("2026-07-02", -12),
|
||||
]
|
||||
|
||||
|
||||
def test_zero_dated_balance_and_valid_import_recovery(tmp_path, monkeypatch):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
zero = service.create_ledger_account(
|
||||
"zero",
|
||||
last_balance=0,
|
||||
last_balance_basis_date="2026-06-01",
|
||||
db_path=path,
|
||||
)
|
||||
|
||||
assert zero["success"] is True
|
||||
assert zero["account"]["last_balance"] == 0
|
||||
assert [(item["basis_date"], item["amount"]) for item in service.list_account_valuations(zero["account"]["id"], db_path=path)["valuations"]] == [("2026-06-01", 0)]
|
||||
|
||||
account = service.create_ledger_account("recoverable", db_path=path)["account"]["id"]
|
||||
monkeypatch.setattr(
|
||||
imports_service,
|
||||
"parse_kb_bank_pdf",
|
||||
lambda *_: _parsed(_transaction("2026-06-30", 900, 1), "hash-valid"),
|
||||
)
|
||||
monkeypatch.setattr(imports_service, "now_iso", lambda: "2026-07-02T09:00:00")
|
||||
|
||||
imported = imports_service.import_kb_bank_pdf("synthetic", "pw", account_id=account, db_path=path)
|
||||
|
||||
current = next(item for item in service.list_ledger_accounts(active_only=False, db_path=path)["accounts"] if item["id"] == account)
|
||||
assert imported["success"] is True
|
||||
assert current["last_balance"] == 900
|
||||
assert current["last_balance_basis_date"] == "2026-06-30"
|
||||
|
||||
|
||||
def test_account_write_sync_timestamp_does_not_infer_balance_basis_or_candidate(tmp_path):
|
||||
path = str(tmp_path / "ledger.sqlite3")
|
||||
unknown = service.create_ledger_account(
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from jarca_ledger_mcp import service
|
||||
from jarca_ledger_mcp.db import init_db
|
||||
|
||||
|
||||
BASIS = "2026-06-30"
|
||||
|
||||
|
||||
def _legacy_closed_db(tmp_path):
|
||||
path = str(tmp_path / "legacy.sqlite3")
|
||||
init_db(path)
|
||||
asset = service.create_manual_asset("legacy cash", 100, "2026-06-01", "cash", db_path=path)["manual_asset"]
|
||||
with sqlite3.connect(path) as conn:
|
||||
# These are schema-11 close rows after the additive schema-12 migration:
|
||||
# run identity is absent, but row close/correction fields remain frozen.
|
||||
conn.execute(
|
||||
"""INSERT INTO asset_snapshots
|
||||
(id, basis_date, source_type, source_id, name, amount, liquidity_bucket,
|
||||
valuation_source, snapshot_run_id, correction_of_run_id, correction_reason,
|
||||
is_correction, closed_at, created_at)
|
||||
VALUES (?, ?, 'manual_asset', ?, ?, 100, 'cash', 'manual_assets', 'legacy-run-correction',
|
||||
'legacy-initial', 'legacy correction', 1, '2026-06-30T12:00:00+09:00',
|
||||
'2026-06-30T12:00:00+09:00')""",
|
||||
("legacy-row", BASIS, asset["id"], asset["name"]),
|
||||
)
|
||||
conn.execute("DROP TABLE asset_snapshot_runs")
|
||||
conn.execute("UPDATE schema_meta SET value='11' WHERE key='schema_version'")
|
||||
init_db(path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0
|
||||
return path, asset["id"]
|
||||
|
||||
|
||||
def test_schema11_closed_rows_remain_frozen_after_live_value_and_date_change(tmp_path):
|
||||
path, asset_id = _legacy_closed_db(tmp_path)
|
||||
service.update_manual_asset(asset_id, valuation_amount=900, valuation_date="2026-06-30", db_path=path)
|
||||
before = hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
report = service.get_asset_snapshot_report(BASIS, db_path=path)
|
||||
|
||||
after = hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
assert report["success"] is True
|
||||
assert report["source"] == "legacy_asset_snapshots"
|
||||
assert report["is_closed_snapshot"] is True
|
||||
assert report["close_status"] == "legacy_read_only"
|
||||
assert report["net_worth"] == 100
|
||||
assert report["included_items"][0]["amount"] == 100
|
||||
assert report["correction"] == {
|
||||
"is_correction": True,
|
||||
"correction_of_run_id": "legacy-initial",
|
||||
"correction_reason": "legacy correction",
|
||||
}
|
||||
assert report["snapshot_run_id"] == "legacy-run-correction"
|
||||
assert report["approval"] == {}
|
||||
assert "valuation_id" not in report["included_items"][0]
|
||||
assert after == before
|
||||
|
||||
|
||||
def test_schema11_closed_rows_are_frozen_in_timeseries(tmp_path):
|
||||
path, asset_id = _legacy_closed_db(tmp_path)
|
||||
service.update_manual_asset(asset_id, valuation_amount=900, valuation_date="2026-07-01", db_path=path)
|
||||
|
||||
point = service.get_net_worth_timeseries(BASIS, BASIS, db_path=path)["points"][0]
|
||||
|
||||
assert point["net_worth"] == 100
|
||||
assert point["item_count"] == 1
|
||||
assert point["close_status"] == "legacy_read_only"
|
||||
assert point["snapshot_run_id"] == "legacy-run-correction"
|
||||
assert point["approval"] == {}
|
||||
assert point["correction"] == {
|
||||
"is_correction": True,
|
||||
"correction_of_run_id": "legacy-initial",
|
||||
"correction_reason": "legacy correction",
|
||||
}
|
||||
|
||||
|
||||
def test_schema11_closed_rows_are_frozen_in_monthly_settlement(tmp_path):
|
||||
path, asset_id = _legacy_closed_db(tmp_path)
|
||||
service.update_manual_asset(asset_id, valuation_amount=900, valuation_date="2026-07-01", db_path=path)
|
||||
|
||||
monthly = service.get_monthly_settlement_summary("2026-06", db_path=path)
|
||||
assets = monthly["reports"]["assets"]
|
||||
coverage = monthly["source_coverage"]["assets"]
|
||||
|
||||
assert assets["net_worth"] == 100
|
||||
assert assets["is_closed_snapshot"] is True
|
||||
assert coverage["net_worth"] == 100
|
||||
assert coverage["is_closed_snapshot"] is True
|
||||
assert coverage["monthly_source_snapshot_id"] == "legacy-run-correction"
|
||||
assert assets["approval"] == {}
|
||||
assert coverage["approval"] == {}
|
||||
assert coverage["correction"] == assets["correction"]
|
||||
|
||||
|
||||
def test_schema11_latest_legacy_correction_run_is_selected_as_a_complete_group(tmp_path):
|
||||
path = str(tmp_path / "legacy-corrections.sqlite3")
|
||||
init_db(path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
rows = [
|
||||
("old-a", "legacy-run-1", "", "", 0, "2026-06-30T10:00:00+09:00", 40),
|
||||
("old-b", "legacy-run-1", "", "", 0, "2026-06-30T10:00:00+09:00", 60),
|
||||
("new-a", "legacy-run-2", "legacy-run-1", "correct frozen close", 1, "2026-07-01T10:00:00+09:00", 70),
|
||||
("new-b", "legacy-run-2", "legacy-run-1", "correct frozen close", 1, "2026-07-01T10:00:00+09:00", 80),
|
||||
]
|
||||
for row_id, run_id, prior, reason, correction, closed_at, amount in rows:
|
||||
conn.execute(
|
||||
"""INSERT INTO asset_snapshots
|
||||
(id, basis_date, source_type, source_id, name, amount, currency,
|
||||
liquidity_bucket, valuation_source, snapshot_run_id,
|
||||
correction_of_run_id, correction_reason, is_correction,
|
||||
closed_at, created_at)
|
||||
VALUES (?, ?, 'manual_asset', ?, ?, ?, 'KRW', 'cash',
|
||||
'manual_assets', ?, ?, ?, ?, ?, ?)""",
|
||||
(row_id, BASIS, row_id, row_id, amount, run_id, prior, reason,
|
||||
correction, closed_at, closed_at),
|
||||
)
|
||||
conn.execute("DROP TABLE asset_snapshot_runs")
|
||||
conn.execute("UPDATE schema_meta SET value='11' WHERE key='schema_version'")
|
||||
init_db(path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
assert conn.execute("SELECT COUNT(*) FROM asset_snapshot_runs").fetchone()[0] == 0
|
||||
|
||||
before = hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
report = service.get_asset_snapshot_report(BASIS, db_path=path)
|
||||
after = hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
assert report["success"] is True
|
||||
assert report["source"] == "legacy_asset_snapshots"
|
||||
assert report["snapshot_run_id"] == "legacy-run-2"
|
||||
assert report["snapshot_count"] == 2
|
||||
assert [item["id"] for item in report["included_items"]] == ["new-a", "new-b"]
|
||||
assert report["net_worth"] == 150
|
||||
assert report["correction"] == {
|
||||
"is_correction": True,
|
||||
"correction_of_run_id": "legacy-run-1",
|
||||
"correction_reason": "correct frozen close",
|
||||
}
|
||||
assert report["approval"] == {}
|
||||
assert all("valuation_id" not in item for item in report["included_items"])
|
||||
assert after == before
|
||||
@@ -43,6 +43,25 @@ def test_interface_contract_documents_public_valuation_enums_validation_and_mapp
|
||||
assert marker in interface, marker
|
||||
|
||||
|
||||
def test_data_model_documents_valuation_selection_contract_and_index_mapping() -> None:
|
||||
data_model = _text("docs/data-model.md")
|
||||
|
||||
required_contract = (
|
||||
"select_account_valuation",
|
||||
"account_id is mandatory",
|
||||
"currency and valuation_kind/purpose are optional filters",
|
||||
"basis_date exact match or <= upper bound",
|
||||
"verified leaf valuations only",
|
||||
"future-dated and unverified valuations are excluded",
|
||||
"idx_account_valuations_account_date supports account/date selection",
|
||||
"idx_account_valuations_status_date supports status/date selection",
|
||||
"neither index covers account + currency + purpose",
|
||||
"idempotency and single_correction indexes enforce write integrity, not selection",
|
||||
)
|
||||
for marker in required_contract:
|
||||
assert marker in data_model, marker
|
||||
|
||||
|
||||
def test_loan_cache_writers_document_no_inferred_valuation_history() -> None:
|
||||
data_model = _text("docs/data-model.md")
|
||||
interface = _text("docs/interface-contract.md")
|
||||
|
||||
Reference in New Issue
Block a user