## Outcome Read-only analysis completed against exact `origin/main` commit `785e18b`. The worktree was clean and remained unchanged. ## Current architecture and reusable patterns - **API:** One large FastAPI router in `src/jarvis_finance/api/routers/budget.py`. Household routes use untyped `dict` payloads. - **Household domain:** Most import, review, pagination, fingerprinting, and confirmed-transaction read logic is concentrated in the 3,127-line `src/jarvis_finance/services/household_import.py`. - **Storage:** SQLite compatibility migrations live in `src/jarvis_finance/storage/migrations.py`; current schema is version **47**. - **Write security:** Global write middleware in `api/main.py` and `api/security.py` already blocks writes unless explicitly enabled. - **Existing reusable safeguards:** - HMAC-backed opaque tokens via `_sha()`. - Deterministic canonical JSON via `_canonical()`. - Global baseline fingerprint via `_baseline()`. - Review/transaction `data_version` digests and cursor-bound stale detection. - Preview → confirm recomputation and `409` on drift. - `BEGIN IMMEDIATE`/savepoint atomicity. - Deterministic IDs and audit logging. - Exact transfer confirmation in `services/transfer_pairing.py`. - **Important existing limitations:** - Group action endpoints operate on an entire classification, not an `item_token`. - Group idempotency checks only the derived audit entity and do not bind retries to the full request. - Confirmed transaction list strips `budget_transaction_id` and exposes no replacement token, so drawer/detail navigation is impossible. - `update_budget_transaction()` mutates category directly without preview, stale binding, idempotency, or specialty guards. - Duplicate candidates have `duplicate_of_transaction_id`, but no self-reference to another candidate and no persistent keep/later state. - Credit-card pairing supports only unique, exact, opposite-sign matches. Partial amounts are not safely representable by the current candidate/transfer model. ## Recommended PR1 contract ### Shared optimistic-concurrency envelope All write previews should accept: ```json { "item_token": "item_…", "data_version": "…", "action": "…" } ``` Preview returns: ```json { "item_token": "item_…", "action": "…", "data_version": "…", "baseline_version": "…", "current_version": "…", "preview_fingerprint": "…", "summary": "…", "impact": {}, "warnings": [], "can_confirm": true } ``` Confirm additionally requires `confirm: true` and the preview fingerprint. Any mismatch returns structured `409`: ```json { "code": "stale_household_item", "baseline_version": "client version", "current_version": "server version" } ``` The fingerprint must bind action, item token, action parameters, item version, global relevant baseline, and classification contract version. ### Proposed endpoints 1. **Item actions** - `GET /api/budget/household/review/items/{item_token}/details?data_version=…` - `POST /api/budget/household/review/items/action/preview` - `POST /api/budget/household/review/items/action/confirm` 2. **Supported duplicate actions** - `duplicate_keep` - `duplicate_exclude` - `duplicate_later` - `duplicate_reopen` - Details remain a side-effect-free GET. - Detail response exposes an opaque `duplicate_of` descriptor, never raw candidate/transaction IDs. 3. **Credit-card settlement** - Action: `credit_card_settlement` - Optional `counterpart_item_token`. - Preview deterministically reports `exact`, `partial`, `ambiguous`, or `missing`. - **Bounded PR1 recommendation:** only a unique exact amount/currency/opposite-sign match is confirmable. Partial/ambiguous/missing previews are read-only and `can_confirm=false`; confirm returns `409` with no writes. Do not silently truncate, synthesize residual candidates, or post unequal transfer legs. - If true partial allocation is mandatory, it is a separate data-model slice requiring allocation/residual lineage; it should not be improvised by weakening `confirm_transfer_pair()`. 4. **Confirmed transaction drawer/category correction** - Add `transaction_token` to every `/household/transactions` item. - `GET /api/budget/household/transactions/{transaction_token}?data_version=…` - `POST /api/budget/household/transactions/{transaction_token}/category/preview` - `POST /api/budget/household/transactions/{transaction_token}/category/confirm` - Specialty guards should reject category mutation for: - `transfer` - `reversal` - linked refunds, whose effective category belongs to the original expense - reversed/archived transactions - malformed transfer membership - inactive or wrong-type categories - Ordinary confirmed income/expense/fee rows remain editable through preview/confirm. ## Migration map Update `src/jarvis_finance/storage/migrations.py` to schema **48**, e.g. `048_household_review_corrections_v1`. Add to `budget_transaction_candidates`: - `duplicate_of_candidate_id TEXT REFERENCES budget_transaction_candidates(transaction_candidate_id)` - `special_action_state TEXT` - allowed values conceptually: `keep`, `exclude`, `later`, or `NULL` - `special_action_updated_at TEXT` Add a durable command receipt table rather than relying solely on non-unique audit queries: ```text household_action_receipts - action_receipt_id PK - entity_kind review_item | transaction - entity_token - action - request_fingerprint UNIQUE - preview_fingerprint - baseline_version - result_json - audit_id FK audit_log - created_at ``` This provides concurrency-safe replay identity and guarantees one audit event per confirmed command. Add indexes for candidate duplicate linkage and action-state review lookup. For duplicate detection/import: - Preserve the predecessor candidate before salting a colliding logical fingerprint. - Populate `duplicate_of_candidate_id` for candidate-to-candidate matches. - Continue using existing `duplicate_of_transaction_id` for candidate-to-confirmed-transaction matches. - Never expose either raw FK through the household API. ## Exact implementation map ### Backend 1. **`src/jarvis_finance/storage/migrations.py`** - Bump 47 → 48. - Add candidate columns, receipt table, indexes, and compatibility migration call. - Preserve existing production data; nullable additive migration only. 2. **`src/jarvis_finance/services/household_import.py`** - Add stable transaction token generation/resolution. - Include `transaction_token` in `list_household_transactions()`. - Make special-case classification account for persistent duplicate resolution state. - Populate and render opaque `duplicate_of`. - Preserve candidate duplicate lineage during import. - Reuse `_household_review_data_version()`, `_household_transaction_data_version()`, `_baseline()`, `_canonical()`, `_sha()`, and savepoint patterns. - Deprecate group special-action usage for these cases; leave legacy endpoints temporarily for compatibility. 3. **New `src/jarvis_finance/services/household_corrections.py`** - Keep PR1 action code out of the already oversized import service. - Implement: - item resolver and item-version digest - item details - duplicate preview/confirm/reopen - deterministic settlement candidate selection and exact confirm - transaction details - category-change preview/confirm - stale `409` helper - action-receipt/idempotent replay helper - Import only narrow token/version helpers from `household_import.py`; avoid a circular import. 4. **`src/jarvis_finance/api/routers/budget.py`** - Register item details/action and transaction detail/category routes. - Keep all confirms behind existing global write middleware. - Require explicit `confirm=true`. 5. **New `src/jarvis_finance/api/schemas/household.py`** - Add Pydantic models for the new request/response envelopes and structured stale detail. - Do not attempt a broad conversion of all existing budget endpoints in PR1. 6. **`src/jarvis_finance/services/transfer_pairing.py`** - Reuse validation and deterministic ID concepts. - Extract a narrow exact-pair writer if needed. - Do **not** relax equal-amount checks or mutate the existing generic matcher for partial settlements. 7. **`src/jarvis_finance/services/budget_transactions.py`** - Leave legacy `update_budget_transaction()` for compatibility, but the household UI must use the new preview/confirm category route. - Optionally share category-validation helpers; do not route household corrections through the direct commit-based updater. ### Frontend contract file needed by the eventual UI PR - **`frontend/src/api/household.ts`** - Extend types for `transaction_token`, item detail, `duplicate_of`, action envelope, stale response, settlement status, drawer detail, and category preview/confirm. - Remove reliance on group action APIs for special cases. ## Test map Create **`tests/unit/test_sprint17b_household_corrections.py`** with synthetic-only fixtures covering: - Schema 48 and migration from a schema-47 database. - Opaque deterministic tokens; no raw candidate/transaction IDs or source references in responses/audits. - Preview is byte/state read-only. - Preview fingerprint changes on action parameters, item mutation, category mutation, mapping mutation, or counterpart mutation. - Stale preview/confirm returns `409` with baseline/current versions and zero writes. - Immediate identical replay returns `idempotent=true` and creates exactly one receipt and one audit row. - Same preview with changed payload returns `409`, not an idempotent success. - Duplicate: - candidate and confirmed-transaction `duplicate_of` - keep/exclude/later - reopen restores open special state - unrelated item remains untouched - Settlement: - unique exact pair confirms two neutral transfer legs and one transfer membership - missing, ambiguous, same-sign, same-account, currency mismatch, stale counterpart, already-consumed counterpart - partial amount is reported deterministically and never writes - Drawer: - detail token resolution - stale transaction version - linked refund/original relationship - transfer membership and masked source/account metadata - Category correction: - preview/confirm success - wrong category type/inactive category - transfer/reversal/linked-refund guards - concurrent category/status mutation - idempotent replay and exactly one audit - HTTP write-mode disabled returns `403` for all new confirms. Also update: - **`tests/unit/test_household_import_v1_golden.py`** - Change exact schema assertion 47 → 48. - Add regression that import duplicate lineage is preserved. - Keep current data-version/pagination and financial-semantics tests green. - **`tests/unit/test_api_write_security.py`** - Assert new confirm routes are blocked when writes are disabled. - Add OpenAPI path assertions for every new route. ## Key pitfalls - Existing `item_token` is HMAC-derived but the top-level `data_version` is not item-specific; confirm must bind both. - Do not use `updated_at` alone as a version—category, mappings, counterpart rows, and classification lookups also affect safety. - Current audit table has no uniqueness constraint; audit lookup alone is not concurrency-safe. - “Keep duplicate” cannot merely leave the candidate unchanged or it remains permanently classified as a special case. - “Later” must be persisted; otherwise it is indistinguishable from no decision. - Never expose `linked_candidate_id`, `duplicate_of_*`, `source_candidate_id`, or ledger IDs directly. - Do not call helpers that commit internally while inside a larger correction transaction. - Current generic transfer confirmation only supports equal full amounts. Partial settlement allocation requires a separate allocation model and residual accounting, not a relaxed equality check. - Category changes on linked refunds are semantically misleading because current read models inherit the original expense category. ## Files changed None. This was read-only analysis; no production or private data was accessed and no tests were executed.