Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf9b77c6c | ||
|
|
f49568273e | ||
|
|
8caf1f8379 |
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"schema": "contract-preservation-inventory/v1",
|
||||||
|
"issue": 12,
|
||||||
|
"pull_request": 13,
|
||||||
|
"review_id": 65,
|
||||||
|
"reviewed_head": "fc2d2a6faeac3c64c0ed604f2aa1ad7c37c65570",
|
||||||
|
"source_revision": "8caf1f8379171a128a2c12971a2eadc9a324309a",
|
||||||
|
"contracts": [
|
||||||
|
{
|
||||||
|
"contract": "current cache balance is an exact integer or unknown NULL; zero remains a valid known balance",
|
||||||
|
"status": "preserved",
|
||||||
|
"preserved_by": [
|
||||||
|
"test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation",
|
||||||
|
"test_zero_dated_balance_and_valid_import_recovery"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "invalid dated account creation fails with validation_error before account or valuation mutation",
|
||||||
|
"status": "preserved",
|
||||||
|
"preserved_by": [
|
||||||
|
"test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "a valid dated import can establish current cache and append a valuation after invalid create attempts are rejected",
|
||||||
|
"status": "preserved",
|
||||||
|
"preserved_by": [
|
||||||
|
"test_zero_dated_balance_and_valid_import_recovery",
|
||||||
|
"test_late_import_current_balance_is_authoritative_by_evidence_basis_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "unknown basis remains distinct from last_synced_at and historical imports cannot roll back a newer usable cache",
|
||||||
|
"status": "preserved",
|
||||||
|
"preserved_by": [
|
||||||
|
"test_account_write_sync_timestamp_does_not_infer_balance_basis_or_candidate",
|
||||||
|
"test_unknown_legacy_balance_basis_is_not_reported_as_valuation_date",
|
||||||
|
"review65-focused-regressions"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "all prior Issue 12 and Review 64 public envelopes, valuation selection, snapshot replay, read-only, migration, privacy, authentication, and menu semantics remain intact",
|
||||||
|
"status": "preserved",
|
||||||
|
"preserved_by": [
|
||||||
|
"review65-focused-regressions",
|
||||||
|
"review65-full-suite",
|
||||||
|
"review65-ruff",
|
||||||
|
"review65-compileall",
|
||||||
|
"review65-diff-check"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missing": 0,
|
||||||
|
"ambiguous": 0,
|
||||||
|
"limitations": [
|
||||||
|
"Four fixture-dependent PDF tests remain skipped because private KB Bank and Toss Securities fixtures are unavailable."
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"environment": ["os", "kernel", "architecture", "python", "uv"],
|
||||||
|
"command_env_allowlist": ["PATH", "HOME", "TMPDIR", "LANG", "VIRTUAL_ENV", "UV_CACHE_DIR"],
|
||||||
|
"secret_env_names": ["GITEA_TOKEN"],
|
||||||
|
"forbidden_regexes": [],
|
||||||
|
"hash_paths": [
|
||||||
|
"jarca_ledger_mcp/application/accounts_write_services.py",
|
||||||
|
"tests/test_import_current_balance_authority.py",
|
||||||
|
"tests/test_account_basis_public_consumers.py",
|
||||||
|
"tests/test_account_valuation_edge_contracts.py",
|
||||||
|
"tests/test_account_valuations.py",
|
||||||
|
"tests/test_issue_12_regressions.py",
|
||||||
|
"tests/test_issue_12_review63_accounts.py",
|
||||||
|
"docs/data-model.md",
|
||||||
|
"docs/requirements/req-003_account-valuation-history.md"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"id": "review65-focused-regressions",
|
||||||
|
"purpose": "Run Review 65 and preserved account authority regression coverage",
|
||||||
|
"argv": ["uv", "run", "--frozen", "--extra", "dev", "--extra", "dashboard", "pytest", "-o", "addopts=", "-q", "tests/test_import_current_balance_authority.py", "tests/test_account_basis_public_consumers.py", "tests/test_account_valuation_edge_contracts.py", "tests/test_account_valuations.py", "tests/test_issue_12_regressions.py", "tests/test_issue_12_review63_accounts.py"],
|
||||||
|
"expected_exit_code": 0,
|
||||||
|
"max_output_bytes": 65536,
|
||||||
|
"claims": ["review65-findings-green"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "review65-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=", "-q"],
|
||||||
|
"expected_exit_code": 0,
|
||||||
|
"max_output_bytes": 65536,
|
||||||
|
"claims": ["review65-full-suite"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "review65-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": ["review65-ruff"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "review65-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": ["review65-compileall"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "review65-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": ["review65-diff-check"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limitations": [
|
||||||
|
"Four PDF fixture tests are skipped because private KB Bank and Toss Securities PDF fixtures are unavailable."
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"schema": "review-finding-matrix/v1",
|
||||||
|
"issue": 12,
|
||||||
|
"pull_request": 13,
|
||||||
|
"review_id": 65,
|
||||||
|
"reviewed_head": "fc2d2a6faeac3c64c0ed604f2aa1ad7c37c65570",
|
||||||
|
"source_revision": "8caf1f8379171a128a2c12971a2eadc9a324309a",
|
||||||
|
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"id": "C05-current-import-create",
|
||||||
|
"classification": "valid_fix",
|
||||||
|
"status": "resolved",
|
||||||
|
"production": "jarca_ledger_mcp/application/accounts_write_services.py",
|
||||||
|
"regressions": [
|
||||||
|
"test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation",
|
||||||
|
"test_zero_dated_balance_and_valid_import_recovery"
|
||||||
|
],
|
||||||
|
"claim": "account creation rejects unusable dated balances before mutation while preserving zero and later valid dated import authority"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "IR-001-strict-integer-create",
|
||||||
|
"classification": "valid_fix",
|
||||||
|
"status": "resolved",
|
||||||
|
"production": "jarca_ledger_mcp/application/accounts_write_services.py",
|
||||||
|
"regression": "test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation",
|
||||||
|
"claim": "boolean and floating-point balances are rejected instead of coercing or truncating; canonical signed integer text remains compatible"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"green_evidence": "evidence-manifest.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"schema": "review-red-evidence/v1",
|
||||||
|
"issue": 12,
|
||||||
|
"pull_request": 13,
|
||||||
|
"review_id": 65,
|
||||||
|
"reviewed_head": "fc2d2a6faeac3c64c0ed604f2aa1ad7c37c65570",
|
||||||
|
"contract_sha256": "6d86b591b022fcc722611bfec6d24e54a64216ab6f7c4098f357e25420218818",
|
||||||
|
"reproductions": [
|
||||||
|
{
|
||||||
|
"id": "C05-current-import-create",
|
||||||
|
"command": "uv run pytest -q tests/test_import_current_balance_authority.py::test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation tests/test_import_current_balance_authority.py::test_zero_dated_balance_and_valid_import_recovery",
|
||||||
|
"exit_code": 1,
|
||||||
|
"observed": "invalid whitespace or non-integer dated balance creation returned success, persisted unusable cache authority, omitted its valuation candidate, and exposed ValueError to a public liquidity consumer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "IR-001-strict-integer-create",
|
||||||
|
"command": "uv run pytest -o addopts= -q tests/test_import_current_balance_authority.py::test_create_rejects_whitespace_or_non_integer_dated_balance_without_mutation",
|
||||||
|
"exit_code": 1,
|
||||||
|
"observed": "boolean and floating-point balances were accepted through int coercion and persisted values different from caller input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sensitive_data": "synthetic fixtures only; no credentials or real financial data"
|
||||||
|
}
|
||||||
@@ -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))
|
except Exception as e: return fail("validation_error", str(e))
|
||||||
if basis_date and (last_balance is None or last_balance == ""):
|
if basis_date and (last_balance is None or last_balance == ""):
|
||||||
return fail("validation_error", "last_balance_basis_date requires 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:
|
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))
|
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()
|
row = conn.execute("SELECT * FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||||
|
|||||||
@@ -101,6 +101,55 @@ def test_empty_balance_with_basis_date_is_rejected_without_mutation_before_impor
|
|||||||
assert {(item["basis_date"], item["amount"]) for item in valuations} == {("2026-06-30", 900)}
|
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_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):
|
def test_account_write_sync_timestamp_does_not_infer_balance_basis_or_candidate(tmp_path):
|
||||||
path = str(tmp_path / "ledger.sqlite3")
|
path = str(tmp_path / "ledger.sqlite3")
|
||||||
unknown = service.create_ledger_account(
|
unknown = service.create_ledger_account(
|
||||||
|
|||||||
Reference in New Issue
Block a user