From bae137dce77245f1f506c2a0086101b6a067c9ab Mon Sep 17 00:00:00 2001 From: gitea-impl-agent Date: Sat, 10 Oct 2026 09:49:47 +0000 Subject: [PATCH] docs: clarify draft ownership and cancel recovery (PR16 review71) --- docs/project-structure.md | 26 ++++++++++++++++++ docs/runtime-flow.md | 15 +++++++++++ tests/test_settlement_draft_docs.py | 41 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/docs/project-structure.md b/docs/project-structure.md index 8c19cf4..6917124 100644 --- a/docs/project-structure.md +++ b/docs/project-structure.md @@ -224,3 +224,29 @@ Reconciliation ownership remains in `reconciliation_service.py`: opening evidenc | `tests/test_monthly_settlement_drafts.py` | Synthetic DB lifecycle, failure injection, replay and no-write regressions. | Never document invented `jarca_ledger_mcp.mcp` or `monthly.py` modules. `transaction readiness` is separate from asset completeness and monthly reads remain read-only. + + +### Shared settlement-draft entrypoints + +| Path | Responsibility / entrypoint and dependency boundary | +|---|---| +| `jarca_ledger_mcp/service.py` | Existing public re-export facade exports the ten lifecycle operations from `application/settlement_draft_service.py`; adapters share these functions, not separate business implementations. | +| `jarca_ledger_mcp/adapters/mcp/server.py` | Same-name MCP lifecycle tools delegate to the shared facade, retaining existing success/error envelopes. | +| `jarca_ledger_mcp/adapters/mcp/tool_contracts.py` | Registers the same facade callables and their inputSchema contracts; internal `db_path` remains outside public MCP input. | +| `jarca_ledger_mcp/adapters/streamlit/settlement_gateway.py` | Wraps shared lifecycle functions plus valuation list/register/correct delegates, preserving auth-before-DB and internal database-path resolution. | +| `dashboard/page_views/settlement_drafts.py` | `render_settlement_drafts` owns reusable presentation and explicit user-action controls through the gateway; no direct SQL or duplicated approval policy. | +| `dashboard/page_views/monthly_settlement.py` | `page_monthly_settlement` invokes the shared panel beside the existing read-only monthly summary. | +| `dashboard/page_views/assets.py` | `page_assets` invokes the same panel beside existing valuation and immutable asset-history views. | +| `jarca_ledger_mcp/application/account_valuation_service.py` | `register_account_valuation`, `list_account_valuations`, and `correct_account_valuation` retain registration, validation and append-only correction ownership; the draft service reuses their evidence. | +| `jarca_ledger_mcp/application/assets_service.py` | Owns established valuation/amount authority and asset close/report semantics; draft finalization does not bypass review by calling legacy live-select close. | +| `jarca_ledger_mcp/application/monthly_settlement_service.py` | `get_monthly_settlement_summary` owns read-only propagation of finalized evidence, not draft mutation or transaction-readiness writes. | +| `jarca_ledger_mcp/infrastructure/sqlite/schema.py` | schema 13 initialization uses the additive migration path; no adapter owns persistence constraints. | +| `jarca_ledger_mcp/infrastructure/sqlite/migrations.py` | Creates persistent draft/item tables and indexes, preserving existing schema and rows on repeated initialization. | + +### Shared settlement-draft policies + +- `jarca_ledger_mcp/application/settlement_draft_service.py` checks one active draft per month for ordinary and correction drafts; SQLite independently enforces it with `idx_settlement_drafts_active_month` on month `WHERE status='active'`. Completed and cancelled history is retained, not deleted to release that slot. +- The same application service validates strict month syntax and month-end basis-date equality. Dashboard and MCP cannot supply an alternate date policy. +- The shared review service exposes unverified `candidate_amount` with authority amount NULL; no adapter may sum it as verified evidence. A confirmed child requires explicit reselection, fresh review and approval; correction children are not silently followed. +- Dependencies remain page → gateway → public facade → application → SQLite, with MCP server/tool contracts → the same facade. No UI/MCP direct SQL, application-to-Dashboard reverse dependency or duplicated business validation is introduced. +- Paired synthetic coverage: `tests/test_monthly_settlement_drafts.py` (month-end, active-month, unverified/reselection, cancellation and atomicity), `tests/test_settlement_draft_adapters.py` (facade/gateway/MCP signatures and no-create reads), `tests/test_settlement_draft_dashboard.py` and `tests/test_settlement_draft_panel_workflow.py` (shared panel/actions), `tests/test_settlement_draft_mcp_runtime.py` (real stdio responses), and `tests/test_settlement_draft_docs.py` (this mapping). Existing authentication/menu/legacy regression tests remain unchanged. diff --git a/docs/runtime-flow.md b/docs/runtime-flow.md index 5c9e352..88c3526 100644 --- a/docs/runtime-flow.md +++ b/docs/runtime-flow.md @@ -257,3 +257,18 @@ Exact requires same-date verified evidence; carried_before uses verified prior w ### failure/recovery and read-only guards Draft create/update/finalize use transaction-scoped writes; any DB failure rolls back run, rows and completion. Same finalize request returns persisted response, not a replay-only `reused` field; different request conflicts. Monthly summary and review do not mutate; transaction readiness remains independent. `read_db()` missing path is `db_not_initialized`, with no-create/no mkdir/no migrate/no commit. Dashboard gate precedes every protected DB access; existing auth cookie lifecycle and exact menu/query/shell/browser semantics remain unchanged. + + +### Cancellation and reopening + +- Entry: authenticated Dashboard explicit action or the existing MCP tool delegates through the shared service facade to `cancel_settlement_draft(draft_id, expected_version, cancelled_by, reason, db_path=None)`. This adds no MCP authentication claim and does not alter Dashboard authentication. + +| Stage ID | Owner / input and guard | State, success and failure transition | +|---|---|---| +| `settlement-draft-cancel` | Shared `_mutate` requires an existing active draft, matching positive-integer `expected_version`, nonempty `cancelled_by` actor and nonempty reason. | In `BEGIN IMMEDIATE`, persist status cancelled plus `cancelled_by`, `cancellation_reason`, `cancelled_at`; increment draft version and `updated_at`. Clear `approval_fingerprint`, `approval_version`, approver/time/reason/reference and flags. Success terminates in cancelled; state/version conflicts return conflict with no-write to the existing draft. Invalid input returns validation_error. | +| `settlement-draft-cancel-recovery` | Cancellation is a transaction-scoped metadata mutation, not evidence deletion or an asset close. | Existing draft item/evidence rows, `account_valuations`, `asset_snapshot_runs`, and `asset_snapshots` are retained. DB failures rollback both cancellation and approval invalidation and return db_error; correct input or reread current state/version before retry. Completed/already cancelled drafts cannot be cancelled again; stale expected versions never overwrite current state. | +| `settlement-draft-reopen` | Only completed or cancelled drafts release the active-month slot. Start a new explicit create request with a fresh create key; ordinary and correction drafts still share the one-active-month restriction. | The same create key with its original equal payload returns the previous draft, even after completion/cancellation; changed payload conflicts. A fresh key is not permission to overwrite history. No close exists: create without a correction parent. A close exists: explicitly link the latest prior run using `correction_of_run_id` and nonempty `correction_reason`, still at month-end. Continue at settlement-draft-create; missing/wrong parent or reason is rejected. | + +Cancellation reason is stored in `cancellation_reason`, not memo; the cancellation actor and timestamp are persisted, not inferred from a later caller. Approval invalidation prevents reuse of the cancelled draft's approval. Reopening creates new draft/item identity and requires its own review/approval; finalization rechecks the correction parent and preserves prior immutable run/rows. Cancellation itself is not an idempotent finalization replay: retrying cancellation after its successful terminal transition returns conflict. + +Coverage: `tests/test_monthly_settlement_drafts.py` checks cancellation/reopen, version conflicts and lifecycle persistence; `tests/test_settlement_draft_authority.py` checks cancellation approval invalidation; `tests/test_settlement_draft_correction_contract.py` checks latest-run linkage, reason, immutable history and persisted replay; `tests/test_settlement_draft_docs.py` checks these documented transitions. Existing auth/menu and read-only recovery flows above remain unchanged. diff --git a/tests/test_settlement_draft_docs.py b/tests/test_settlement_draft_docs.py index 1eed6e1..5de5e90 100644 --- a/tests/test_settlement_draft_docs.py +++ b/tests/test_settlement_draft_docs.py @@ -135,3 +135,44 @@ def test_legacy_metadata_field_lists_are_required_retained_fields_not_exhaustive assert re.search(r"required top-level metadata fields", legacy, re.I) assert re.search(r"not (?:an )?exhaustive", legacy, re.I) assert "additive settlement-draft finalize response" in legacy + + +def test_settlement_draft_project_ownership_and_shared_policy_wiring() -> None: + doc = text("docs/project-structure.md") + required_rows = { + "jarca_ledger_mcp/service.py": "public re-export", + "jarca_ledger_mcp/adapters/mcp/tool_contracts.py": "inputSchema", + "jarca_ledger_mcp/adapters/streamlit/settlement_gateway.py": "auth-before-DB", + "dashboard/page_views/settlement_drafts.py": "render_settlement_drafts", + "dashboard/page_views/monthly_settlement.py": "page_monthly_settlement", + "dashboard/page_views/assets.py": "page_assets", + "jarca_ledger_mcp/application/account_valuation_service.py": "register_account_valuation", + "jarca_ledger_mcp/infrastructure/sqlite/schema.py": "schema 13", + } + for path, responsibility in required_rows.items(): + rows = [row for row in doc.splitlines() if row.startswith("|") and f"`{path}`" in row] + assert rows and any(responsibility in row for row in rows), (path, responsibility) + assert (ROOT / path).is_file(), path + section = doc.split("### Shared settlement-draft policies", 1)[1] + for clause in ("one active draft per month", "idx_settlement_drafts_active_month", "WHERE status='active'", + "settlement_draft_service.py", "month-end", "unverified", "authority amount NULL", + "confirmed child", "explicit reselection", "review", "approval", + "tests/test_settlement_draft_adapters.py", "tests/test_monthly_settlement_drafts.py"): + assert clause in section, clause + + +def test_settlement_draft_runtime_cancel_and_recovery_contract() -> None: + doc = text("docs/runtime-flow.md") + assert "### Cancellation and reopening" in doc, "cancel/reopen lifecycle must be documented" + section = doc.split("### Cancellation and reopening", 1)[1] + for clause in ("cancel_settlement_draft(draft_id, expected_version, cancelled_by, reason, db_path=None)", + "active", "nonempty", "cancelled_by", "cancellation_reason", "cancelled_at", + "approval_fingerprint", "approval_version", "version", "updated_at", "BEGIN IMMEDIATE", + "retained", "account_valuations", "asset_snapshot_runs", "asset_snapshots", + "rollback", "no-write", "completed", "cancelled", "fresh create key", + "same create key", "latest prior run", "correction_of_run_id", "correction_reason", + "tests/test_settlement_draft_correction_contract.py"): + assert clause in section, clause + source = ast.parse(text("jarca_ledger_mcp/application/settlement_draft_service.py")) + cancel = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "cancel_settlement_draft") + assert [arg.arg for arg in cancel.args.args] == ["draft_id", "expected_version", "cancelled_by", "reason", "db_path"]