## Review result: **NOT PASS** Actual HEAD verified: `bd18300878c3ce13a7e819c640b50d38519f135e`. ### P1 — Confirm API does not enforce business readiness - `src/jarvis_finance/services/household_import.py:1079-1090` computes `business_ready_for_confirm`. - `src/jarvis_finance/services/household_import.py:1120-1121` keeps `confirmable` equivalent to technical readiness only. - `src/jarvis_finance/services/household_import.py:1408-1414` checks only `reconstructed["confirmable"]` before writing. Thus the UI gate at `frontend/src/pages/HouseholdImportsPage.vue:65,171-177` is bypassable by calling the confirm API directly. Verified with a synthetic unresolved import: ```text technically_confirmable= True business_ready_for_confirm= False coverage_ratio= 0.0000 confirm_status= confirmed batch_count= 1 ``` **Fix:** Reject new confirmations unless `reconstructed["business_ready_for_confirm"] is True`, and add service/API tests proving a technically valid but business-unready preview creates no batch, candidates, transactions, or audit event. ### P1 — “Unmatched transfer” can neutralize any mapped bank transaction - `src/jarvis_finance/services/household_classification.py:380-403` accepts the decision for any mapped CHF AKB/Raiffeisen row outside a short disposition denylist, before merchant/category classification. - `src/jarvis_finance/services/household_import.py:1198-1204` advertises the action for ordinary income/expense rows, including already category-ready rows. - `src/jarvis_finance/services/household_import.py:933-936` removes review and converts the row to a candidate. Synthetic proof with a normally categorized COOP expense: ```text normal= expense Essen & Haushalt proposal_ready with_decision= user_confirmed_unmatched_transfer None 0.00 special_case ``` This permits an ordinary expense or salary to be posted as a zero-budget-effect transfer. **Fix:** Define a server-derived, narrow `unmatched_transfer_candidate` state based on transfer evidence. Expose the action only for unresolved rows in that state, and reject submitted decisions for every other row server-side. ### P1 — Cluster confirmation applies to transactions the user cannot inspect or exclude - `src/jarvis_finance/services/household_import.py:416-440` includes all members in `row_tokens` and `count`, but returns details for only `members[:3]`. - `src/jarvis_finance/services/household_import.py:471-492` applies the category to every member not explicitly excluded. - `frontend/src/pages/HouseholdImportsPage.vue:42-49` renders exclusion controls only for `cluster.examples`. For clusters larger than three rows, unseen members are categorized automatically and cannot be individually excluded through the UI, despite the backend message claiming an “exakt angezeigte Händlergruppe” decision. **Fix:** Before accepting the decision, expose and render the complete exact member list—possibly paginated—with exclusion controls for every member. Representative examples may remain in the collapsed summary, but confirmation must show the exact included count/list. ### P1 — Preview can become bound to stale file/mapping state - File and mapping controls remain enabled during requests: `frontend/src/pages/HouseholdImportsPage.vue:15,19`. - The watcher at `frontend/src/pages/HouseholdImportsPage.vue:116-118` clears state but does not invalidate in-flight requests. - `createPreview` at `frontend/src/pages/HouseholdImportsPage.vue:163-169` unconditionally installs the response and captured old files. If the file or mapping changes while preview creation is pending, the old response can repopulate `previewResult`/`previewFiles` while the header reflects the new selected mapping. Confirmation then submits the old bound payload. **Fix:** Increment a preview-generation token on every input change/discard, apply responses only when the generation and captured inputs still match, and disable file/mapping controls while a request is active. Add an out-of-order deferred-promise test. ### P2 — New small transfer action lacks a guaranteed 44px target `frontend/src/pages/HouseholdImportsPage.vue:60` uses PrimeVue `size="small"`, while no household rule guarantees a 44px minimum target for this button. The new selects, summaries, and checkbox labels do meet 2.75rem/44px sizing. **Fix:** Remove `size="small"` or apply `min-height: 2.75rem` to the action button and add a rendered target-size assertion. ## Verification - Focused backend tests: **72 passed** - Frontend household/API tests: **13 passed** - Vue TypeScript check: **passed** - `git diff --check`: **passed** - Worktree remained clean; **no files created or modified** - One initial Vitest invocation used repo-prefixed paths from inside `frontend/` and found no tests; rerun with correct relative paths passed.