Fix loan account conversion invariant

This commit is contained in:
2026-10-07 03:58:52 +00:00
parent b8b8811a2a
commit 1b9eda683c
9 changed files with 219 additions and 2 deletions
@@ -0,0 +1,49 @@
{
"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_head": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
"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."
]
}
@@ -0,0 +1,60 @@
{
"schema": "gitea-agent.evidence-plan/v1",
"owner": "jsfamily",
"repository": "jarca-ledger-mcp",
"issue": 12,
"pull_request": 13,
"review_id": 61,
"reviewed_head": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
"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..<SOURCE_SHA>"]
}
],
"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."
]
}
@@ -0,0 +1,55 @@
{
"schema": "gitea-agent.finding-matrix/v1",
"owner": "jsfamily",
"repository": "jarca-ledger-mcp",
"issue": 12,
"pull_request": 13,
"review_id": 61,
"reviewed_head": "5e721b0ddd648f3e80f341a3c304d1cc3ca47ee8",
"implementation_parent": "b8b8811a2a50371795a02e4fa65f097c9d9508ac",
"findings": [
{
"id": "N01-loan-balance-definition",
"classification": "implementation_defect",
"status": "resolved_at_implementation_parent",
"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."
]
}
]
}
@@ -0,0 +1,9 @@
{
"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"
}