docs: clarify draft ownership and cancel recovery (PR16 review71)
This commit is contained in:
@@ -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. |
|
| `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.
|
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.
|
||||||
|
|||||||
@@ -257,3 +257,18 @@ Exact requires same-date verified evidence; carried_before uses verified prior w
|
|||||||
### failure/recovery and read-only guards
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -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"required top-level metadata fields", legacy, re.I)
|
||||||
assert re.search(r"not (?:an )?exhaustive", legacy, re.I)
|
assert re.search(r"not (?:an )?exhaustive", legacy, re.I)
|
||||||
assert "additive settlement-draft finalize response" in legacy
|
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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user