diff --git a/fusion_accounting/docs/superpowers/specs/2026-04-19-phase-1-bank-rec-design.md b/fusion_accounting/docs/superpowers/specs/2026-04-19-phase-1-bank-rec-design.md
new file mode 100644
index 00000000..425d30d6
--- /dev/null
+++ b/fusion_accounting/docs/superpowers/specs/2026-04-19-phase-1-bank-rec-design.md
@@ -0,0 +1,1153 @@
+# Phase 1 — Bank Reconciliation Design
+
+**Status:** Design (drafted 2026-04-19, pending user approval before plan)
+**Owner:** Nexa Systems Inc.
+**Scope:** Build `fusion_accounting_bank_rec` — a native bank-reconciliation widget that replaces Odoo Enterprise's `account_accountant` bank rec, in V19 OWL architecture, with a clean-room reconcile engine reading/writing Community's `account.partial.reconcile` tables.
+**Estimate:** ~5.5-6 weeks single-engineer.
+
+---
+
+## 1. Context and Goals
+
+### 1.1 Where Phase 1 Fits
+
+Phase 0 (complete, merged at `c450bb2`, range `fusion_accounting/pre-phase-0..fusion_accounting/phase-0-complete`) shipped:
+
+- 4 sub-modules (`fusion_accounting`, `_core`, `_ai`, `_migration`)
+- Data-adapter pattern with `BankRecAdapter` already stubbed at `fusion_accounting_ai/services/data_adapters/bank_rec.py` — `_via_fusion()` returns the community fallback
+- Empirical verification that bank reconciliation data lives in Community `account.partial.reconcile` and survives Enterprise uninstall (16,500 rows on Westin's DB confirmed safe)
+- Migration safety guard blocks Enterprise uninstall until per-module flag set
+- Migration wizard skeleton with `_inherit`-extension point at `action_run_migration`
+
+Phase 1 fills in the `_via_fusion()` paths of the bank-rec adapter with a real implementation.
+
+### 1.2 What Phase 1 Delivers
+
+A `fusion_accounting_bank_rec` sub-module providing:
+
+- A V19-architecture OWL bank-rec widget (frontend-only, no `_auto=False` Python widget model — Enterprise removed that pattern in V19)
+- A clean-room reconcile engine (Python, AbstractModel) that produces `account.partial.reconcile` rows byte-identical to what Enterprise produces
+- Per-line AI confidence badges with one-click Accept and an expandable alternatives panel
+- Behavioural learning: pattern aggregation per partner + nearest-neighbour precedent lookup, bootstrapped from existing 16,500 historical reconciliations
+- Architecture ready for local LLM (Ollama, LM Studio, vLLM, llamafile) via the existing OpenAI-compatible adapter with configurable `base_url`
+- Coexistence with Enterprise: when `account_accountant` is installed, fusion's menu hides; widget surfaces only after Enterprise uninstall
+- Migration wizard step that bootstraps pattern memory and produces a downloadable audit report proving every reconciliation preserved
+
+### 1.3 Non-Goals (deferred to later phases)
+
+- Mexican CFDI electronic accounting (account_accountant Mexican branch)
+- Cash-basis tax move generation (CABA flow)
+- Batch payment matching (`account_batch_payment`)
+- ISO 20022 statement attachment parsing
+- Native Ollama adapter UI (model picker, registry pull)
+- Local embedding model integration with pgvector (keyword tokenization is v1)
+- Mobile-responsive widget below 768px viewport
+- True auto-apply mode (cron applies high-confidence reconciles silently) — explicitly chose AI-assistive over AI-native in scoping
+
+### 1.4 Locked Decisions from Brainstorming Session
+
+| Dimension | Decision |
+|---|---|
+| Scope | CORE — manual + auto reconcile, write-offs, partial, multi-currency, chatter, model picker. Defers Mexican / cash-basis / batch-payment edge cases |
+| UI mirror depth | Strict mirror — all 18 Enterprise OWL units replicated 1:1 (revised from initial "moderate" after user push-back) |
+| AI integration depth | Assistive — per-line confidence badges, background batch-suggest cron, statistical-mode-without-API-key as first-class |
+| AI badge layout | Hybrid: Layout A inline strip with one-click Accept/Reject + Layout B's ranked-alternatives panel as inline expansion |
+| Coexistence | Enterprise wins by default (augment mode); fusion menu/widget hide when `account_accountant` detected |
+| Performance target | Beat Enterprise: P95 widget open <1.5s @ 500 lines, <3s @ 5000 lines via materialized view + batched ORM |
+| Test posture | TDD on engine algorithms + Hypothesis property-based tests for amount invariants + tour tests for UI smoke + migration round-trip integration test |
+| LLM provider | Architecture-ready for local LLM via OpenAI-compatible adapter with configurable `base_url`; per-feature provider routing |
+| Behavioural learning | Two-layer: `fusion.reconcile.pattern` (per-partner aggregate) + `fusion.reconcile.precedent` (per-historical-decision memory). Bootstrapped from existing reconciliations on first install |
+| History preservation | Chatter, mail tracking, attachments, prior-reconciler audit fields all surface in fusion widget; `cron_last_check` field added to `fusion_accounting_core` shared-field-ownership |
+
+---
+
+## 2. Sub-Module Topology
+
+### 2.1 Where `fusion_accounting_bank_rec` Slots In
+
+```mermaid
+graph TD
+ community["account
Odoo Community base"]
+ core["fusion_accounting_core
(security, shared schema, helpers)"]
+ bankrec["fusion_accounting_bank_rec
NEW (Phase 1)
reconcile engine + OWL widget"]
+ ai["fusion_accounting_ai
(BankRecAdapter._via_fusion fills in here)"]
+ migration["fusion_accounting_migration
(adds bank-rec migration step)"]
+ meta["fusion_accounting
(meta — depends on _bank_rec now too)"]
+
+ core --> community
+ bankrec --> core
+ ai --> core
+ migration --> core
+
+ ai -.adapter routes to.-> bankrec
+ migration -.registers migration step from.-> bankrec
+ meta --> bankrec
+```
+
+### 2.2 File Layout (canonical, per roadmap Section 5.5)
+
+```
+fusion_accounting_bank_rec/
+├── __manifest__.py # depends: fusion_accounting_core; runtime-detect account_accountant
+├── __init__.py
+├── CLAUDE.md, UPGRADE_NOTES.md, README.md
+├── docs/odoo_diff/v19/ # snapshots of Enterprise reference for V20 diffing
+│ ├── account_accountant__bank_reconciliation_service.js
+│ ├── account_accountant__account_reconcile_model.py
+│ └── account_accountant__account_auto_reconcile_wizard.py
+├── data/
+│ ├── ir_cron.xml # AI batch-suggest cron (every 15 min); pattern refresh cron (nightly)
+│ └── materialized_view.xml # SQL for fusion_unreconciled_per_partner_mv
+├── migrations/
+│ └── 19.0.1.0.0/
+│ └── post-migration.py # Bootstrap pattern memory from existing reconciliations
+├── models/
+│ ├── __init__.py
+│ ├── fusion_reconcile_engine.py # ABSTRACT — FIFO + write-off + exchange-diff orchestration
+│ ├── fusion_reconcile_suggestion.py # MODEL — persisted AI suggestions
+│ ├── fusion_reconcile_pattern.py # MODEL — per-partner aggregate profile
+│ ├── fusion_reconcile_precedent.py # MODEL — per-historical-decision memory
+│ ├── account_bank_statement_line.py # MIRROR/inherit — adds compute fields for badge state
+│ ├── account_reconcile_model.py # MIRROR/inherit — AI integration hooks
+│ └── fusion_bank_rec_widget.py # MIRROR — TransientModel for widget round-trip data
+├── controllers/
+│ ├── __init__.py
+│ └── bank_rec_controller.py # JSON-RPC endpoints for OWL components
+├── security/
+│ ├── ir.model.access.csv
+│ └── bank_rec_security.xml # ACLs + record rules
+├── services/ # ABSTRACT — pure-Python helpers
+│ ├── matching_strategies.py # AmountExact, FIFO, MultiInvoice strategies
+│ ├── exchange_diff.py # FX gain/loss helpers
+│ ├── confidence_scoring.py # 4-pass scoring pipeline
+│ ├── pattern_extractor.py # Aggregates patterns from precedents
+│ ├── precedent_lookup.py # K-nearest precedent search
+│ └── memo_tokenizer.py # Canadian bank memo keyword extraction
+├── static/
+│ ├── description/
+│ │ ├── icon.png
+│ │ └── index.html
+│ └── src/
+│ └── components/bank_reconciliation/
+│ ├── _bank_rec_tokens.scss # SCSS token partial (loaded first)
+│ ├── bank_rec_widget.scss
+│ ├── bank_reconciliation_service.js
+│ ├── kanban_controller.js + .xml
+│ ├── kanban_renderer.js + .xml
+│ ├── apply_amount/ # mirror
+│ ├── bankrec_form_dialog/ # mirror
+│ ├── button/ # mirror
+│ ├── button_list/ # mirror
+│ ├── chatter/ # mirror
+│ ├── file_uploader/ # mirror
+│ ├── line_info_pop_over/ # mirror
+│ ├── line_to_reconcile/ # mirror
+│ ├── list_view/ # mirror (3 files)
+│ ├── quick_create/ # mirror
+│ ├── reconciled_line_name/ # mirror
+│ ├── search_dialog/ # mirror (4 files)
+│ ├── statement_line/ # mirror
+│ ├── statement_summary/ # mirror
+│ ├── ai_suggestion/ # NEW (fusion-only)
+│ │ ├── ai_confidence_badge.js + .xml
+│ │ ├── ai_suggestion_strip.js + .xml
+│ │ └── ai_alternatives_panel.js + .xml
+│ ├── batch_action_bar/ # NEW (fusion-only)
+│ ├── attachment_strip/ # NEW (fusion-only)
+│ ├── partner_history_panel/ # NEW (fusion-only)
+│ └── reconcile_model_picker/ # NEW (fusion-only)
+├── tests/
+│ ├── __init__.py
+│ ├── _fixtures/
+│ ├── _factories.py
+│ ├── test_reconcile_engine_unit.py
+│ ├── test_reconcile_engine_property.py
+│ ├── test_reconcile_engine_integration.py
+│ ├── test_pattern_extraction.py
+│ ├── test_precedent_lookup.py
+│ ├── test_memo_tokenizer.py
+│ ├── test_confidence_scoring.py
+│ ├── test_ai_suggestion_lifecycle.py
+│ ├── test_widget_endpoints.py
+│ ├── test_coexistence.py
+│ ├── test_migration_round_trip.py
+│ ├── test_performance.py
+│ ├── test_local_llm_compat.py
+│ └── tours/
+│ ├── manual_reconcile_tour.js
+│ ├── ai_assist_accept_tour.js
+│ ├── batch_reconcile_tour.js
+│ ├── partial_reconcile_tour.js
+│ └── write_off_tour.js
+├── views/
+│ ├── bank_rec_widget_views.xml
+│ ├── account_journal_views.xml # adds "Reconcile (Fusion)" button
+│ ├── account_reconcile_model_views.xml
+│ └── menus.xml
+└── wizards/
+ ├── __init__.py
+ ├── auto_reconcile_wizard.py # clean-room rewrite of Enterprise auto-reconcile
+ ├── auto_reconcile_wizard.xml
+ ├── reconcile_wizard.py # bulk action: reconcile selected lines
+ ├── reconcile_wizard.xml
+ └── migration_wizard_inherit.py # extends fusion.migration.wizard.action_run_migration
+```
+
+### 2.3 Additions to Existing Sub-Modules
+
+| Sub-module | Phase 1 addition |
+|---|---|
+| `fusion_accounting_core/models/account_bank_statement_line.py` | Add `cron_last_check = fields.Datetime()` to existing inherit (shared-field-ownership for Enterprise's auto-reconcile timestamp) |
+| `fusion_accounting_core/models/res_users.py` | NEW — computed group `group_fusion_show_when_enterprise_absent` for menu coexistence |
+| `fusion_accounting_ai/services/data_adapters/bank_rec.py` | Fill `list_unreconciled_via_fusion()`; add `get_ai_suggestions`, `accept_suggestion`, `compute_partner_history_score` |
+| `fusion_accounting_ai/services/tools/bank_reconciliation.py` | Add tools: `get_pending_suggestions`, `explain_suggestion`, `accept_suggestion_via_chat`, `reject_suggestion_via_chat`, `batch_accept_high_confidence` |
+| `fusion_accounting_ai/services/prompts/` | NEW file: `bank_rec_prompt.py` with the suggestion-ranking prompt |
+| `fusion_accounting_ai/services/adapters/_base.py` | NEW — `LLMProvider` contract base class |
+| `fusion_accounting_ai/services/adapters/openai_adapter.py` | Generalise to accept configurable `base_url` from `ir.config_parameter` |
+| `fusion_accounting/__manifest__.py` | Add `'fusion_accounting_bank_rec'` to `depends` |
+
+---
+
+## 3. Reconcile Engine Architecture
+
+### 3.1 Engine Module Layout
+
+```
+fusion_accounting_bank_rec/models/
+├── fusion_reconcile_engine.py # AbstractModel — orchestrator
+├── fusion_reconcile_suggestion.py # Model — persisted AI suggestions
+├── fusion_reconcile_pattern.py # Model — per-partner aggregate
+├── fusion_reconcile_precedent.py # Model — per-decision memory
+├── fusion_bank_rec_widget.py # TransientModel — per-request widget state
+└── account_bank_statement_line.py # Inherit — adds compute fields for badge state
+
+fusion_accounting_bank_rec/services/ # Pure-Python helpers, no ORM dependency
+├── matching_strategies.py
+├── exchange_diff.py
+├── confidence_scoring.py
+├── pattern_extractor.py
+├── precedent_lookup.py
+└── memo_tokenizer.py
+```
+
+`models/fusion_reconcile_engine.py` is `models.AbstractModel` so it inherits Odoo conventions (env, security, transactions). Helpers in `services/` are pure Python — directly unit-testable without an Odoo DB, supporting the TDD-on-engine discipline.
+
+### 3.2 Public API Surface (six methods)
+
+```python
+class FusionReconcileEngine(models.AbstractModel):
+ _name = "fusion.reconcile.engine"
+
+ def reconcile_one(self, statement_line, *, against_lines, write_off_vals=None):
+ """Reconcile a single bank line against given journal items.
+ Returns: {partial_ids, exchange_diff_move_id, write_off_move_id}"""
+
+ def reconcile_batch(self, statement_lines, *, strategy='auto'):
+ """Bulk-reconcile a recordset using the chosen strategy.
+ Returns: {reconciled_count, skipped, errors}"""
+
+ def suggest_matches(self, statement_lines, *, limit_per_line=3):
+ """Compute AI-ranked candidate matches per line.
+ Returns: {statement_line_id: [{move_line_ids, confidence, reasoning}, ...]}"""
+
+ def accept_suggestion(self, suggestion):
+ """User clicked Accept on a suggestion → invoke reconcile_one with its proposal."""
+
+ def write_off(self, statement_line, *, account, amount, tax_id=None, label):
+ """Create write-off line + post → invoke reconcile_one with the new line."""
+
+ def unreconcile(self, partial_reconciles):
+ """Reverse a reconciliation. Handles full vs. partial chains."""
+```
+
+Every OWL component, AI tool, and wizard goes through one of these six methods. No direct ORM writes from the frontend or the wizards.
+
+### 3.3 Internal Pipeline
+
+Every reconcile request flows through five phases:
+
+```mermaid
+graph LR
+ in["1. Request
(statement_line + targets)"]
+ val["2. Validate
amounts balance, currency consistent,
partner allowed, period not locked"]
+ diff["3. Compute deltas
residual, exchange diff,
tax recomputation if write-off"]
+ write["4. Write
account.partial.reconcile rows
+ optional exchange-diff move
+ optional write-off move"]
+ audit["5. Audit
log to mail.message
+ trigger AI scoring update
+ refresh materialized view"]
+
+ in --> val --> diff --> write --> audit
+```
+
+Steps 2-3 are pure functions in `services/`. Step 4 is the only ORM-write step. Step 5 is fire-and-forget.
+
+### 3.4 Algorithm Choices
+
+**Matching strategy `auto`-mode order:**
+
+1. Pass 1: amount-exact-first (single open item where `amount_residual == bank_line.amount` to within currency rounding)
+2. Pass 2: smallest set of open items (≤3) summing to bank amount
+3. Pass 3: FIFO oldest-due-first, partial-reconcile until exhausted
+4. Pass 4: leave for AI suggestion or manual
+
+This order reflects what Canadian bookkeepers do mentally (~70% exact-match dominance per Westin's ledger). FIFO is the CRA-expected fallback.
+
+**Exchange difference:** delegate to Odoo's existing `account.move._create_exchange_difference_move(partial_ids)`. Don't reinvent FX math; respect company-currency vs. journal-currency rates and posted gain/loss accounts on `res.currency`.
+
+**Write-off taxes:** set `tax_ids` on the new line; let `account.move.line._compute_tax_amount` do the math. Don't compute tax independently.
+
+**Partial reconcile:** first-class case, not a special case. `reconcile_one` always produces `account.partial.reconcile` rows; full-reconcile coalescence checked via Odoo's existing `_create_or_update_full_reconcile`.
+
+### 3.5 AI Integration Hooks
+
+The engine exposes exactly two AI hooks; AI never writes `account.partial.reconcile` directly.
+
+| Hook | When it fires | What it does |
+|---|---|---|
+| `suggest_matches()` | (a) cron every 15 min; (b) widget-open lazy fill; (c) user "Show alternatives" | Calls `services/confidence_scoring.py`, writes `fusion.reconcile.suggestion` rows |
+| `accept_suggestion()` | User clicks Accept | Records feedback, then calls `reconcile_one` with the suggestion's proposal |
+
+If AI hallucinates a wrong match, worst case is a wrong suggestion in the UI — never a wrong reconciliation in the books.
+
+### 3.6 Behavioural Pattern Learning
+
+Two layers:
+
+**Layer 1 — `fusion.reconcile.pattern`** (one row per `(company_id, partner_id)`):
+
+```python
+class FusionReconcilePattern(models.Model):
+ _name = "fusion.reconcile.pattern"
+
+ company_id = fields.Many2one('res.company', required=True)
+ partner_id = fields.Many2one('res.partner', required=True)
+ reconcile_count = fields.Integer()
+ typical_amount_range = fields.Char()
+ typical_cadence_days = fields.Float()
+ typical_day_of_month = fields.Char()
+ pref_strategy = fields.Selection([
+ ('exact_amount', 'Exact-amount-first'),
+ ('fifo', 'FIFO oldest-due-first'),
+ ('multi_invoice', 'Multi-invoice consolidation'),
+ ('cherry_pick', 'Cherry-pick specific invoices'),
+ ])
+ pref_account_id = fields.Many2one('account.account')
+ common_memo_tokens = fields.Char()
+ common_writeoff_account_id = fields.Many2one('account.account')
+ common_writeoff_tax_id = fields.Many2one('account.tax')
+ typical_writeoff_amount = fields.Float()
+```
+
+Refreshed nightly via cron from new precedents.
+
+**Layer 2 — `fusion.reconcile.precedent`** (one row per historical reconcile):
+
+```python
+class FusionReconcilePrecedent(models.Model):
+ _name = "fusion.reconcile.precedent"
+
+ company_id = fields.Many2one('res.company', required=True)
+ partner_id = fields.Many2one('res.partner', index=True)
+ amount = fields.Monetary(currency_field='currency_id')
+ currency_id = fields.Many2one('res.currency')
+ date = fields.Date()
+ memo_tokens = fields.Char()
+ journal_id = fields.Many2one('account.journal')
+ matched_move_line_count = fields.Integer()
+ matched_account_ids = fields.Char()
+ matched_invoice_ages_days = fields.Char()
+ write_off_amount = fields.Float()
+ write_off_account_id = fields.Many2one('account.account')
+ exchange_diff = fields.Boolean()
+ reconciler_user_id = fields.Many2one('res.users')
+ reconciled_at = fields.Datetime()
+ source = fields.Selection([
+ ('historical_bootstrap', 'Imported from history'),
+ ('manual', 'Manual reconcile via fusion'),
+ ('ai_accepted', 'AI suggestion accepted'),
+ ('auto_rule', 'account.reconcile.model auto-fired'),
+ ])
+```
+
+Used for K-nearest neighbour lookup when scoring new lines.
+
+**Bootstrap on install:** scan `account.partial.reconcile` rows in batches of 1000, write precedents, aggregate patterns. ~30-60s for Westin's 16,500 reconciliations. Idempotent via `source='historical_bootstrap'` dedupe.
+
+### 3.7 Performance
+
+- **Suggestion generation** runs in cron, not in request context. OWL widget reads pre-computed `fusion.reconcile.suggestion` rows. Sub-300ms render even for 500 unreconciled lines.
+- **Partner-unreconciled counts** come from a materialized view `fusion_unreconciled_per_partner_mv`, refreshed on `account.move` post via overridden `_post`. Constant-time lookup vs. Enterprise's per-record compute (the main culprit in Westin's slow widget loads).
+- **`reconcile_batch`** uses chunked ORM (50 lines per chunk, commit between) to avoid lock contention.
+
+### 3.8 Local LLM Readiness
+
+**Provider contract** (`fusion_accounting_ai/services/adapters/_base.py`):
+
+```python
+class LLMProvider:
+ supports_tool_calling: bool
+ supports_streaming: bool
+ max_context_tokens: int
+ supports_embeddings: bool
+
+ def complete(self, *, system, messages, max_tokens, temperature) -> dict:
+ """Plain text completion. Required for ALL providers."""
+
+ def complete_with_tools(self, *, system, messages, tools, max_tokens) -> dict:
+ """Optional. Engine checks supports_tool_calling first."""
+
+ def embed(self, texts: list[str]) -> list[list[float]]:
+ """Optional. Engine falls back to keyword tokenization if absent."""
+```
+
+**OpenAI-compatible adapter generalisation** — single change:
+
+```python
+# fusion_accounting_ai/services/adapters/openai_adapter.py
+class OpenAIAdapter(LLMProvider):
+ def __init__(self, env):
+ param = env['ir.config_parameter'].sudo()
+ api_key = param.get_param('fusion_accounting.openai_api_key', 'unused')
+ base_url = param.get_param('fusion_accounting.openai_base_url',
+ 'https://api.openai.com/v1')
+ self.client = openai.OpenAI(api_key=api_key, base_url=base_url)
+```
+
+Set `base_url` to `http://host.docker.internal:1234/v1` (LM Studio) or `http://host.docker.internal:11434/v1` (Ollama) — local LLM works through the existing adapter.
+
+**Per-feature provider routing** via `ir.config_parameter`:
+
+```
+fusion_accounting.provider.chat = claude_sonnet_4_5
+fusion_accounting.provider.bank_rec_suggest = openai_compatible
+fusion_accounting.provider.tier3_review = claude_sonnet_4_5
+fusion_accounting.provider.embeddings = openai_compatible
+```
+
+**Engine guards:** treats AI as an optional re-ranker on top of a deterministic statistical baseline. No tool-calling support? Engine uses `complete()` only and parses JSON. Context too small? Truncates precedent batch to fit budget. Provider unreachable? Falls back to statistical-only, logs warning.
+
+### 3.9 Architectural Invariants
+
+These commitments must not be broken by Phase 1 implementation or future phases:
+
+1. **No model-specific code in engines.** Engine talks to `LLMProvider` interface, never `import anthropic` directly.
+2. **Deterministic statistical baseline.** Every AI feature has a non-AI fallback that produces useful output.
+3. **Capability negotiation, not assumption.** Code never assumes tool-calling, never assumes 200k context, never assumes streaming.
+4. **Per-feature, per-company provider routing.** Multi-company installs can route Company A through Claude, Company B through local Llama.
+5. **Privacy-by-default feature vectors.** AI calls send extracted features (amount, partner ID, memo tokens, candidate IDs), never raw transaction descriptions.
+
+---
+
+## 4. UI Components & Layout
+
+### 4.1 Component Inventory (23 total — 18 mirrored + 5 fusion-new)
+
+**Top-level units (Enterprise-mirrored):**
+
+| Unit | Type | Responsibility |
+|---|---|---|
+| `bank_reconciliation_service.js` | service | Frontend state hub; ORM helpers; chatter state; reconcile-model availability |
+| `bank_rec_widget.scss` | styles | All widget styling, light + dark mode via SCSS branch |
+| `kanban_controller.js + .xml` | component | Widget root; orchestrates summary, grid, dialog, batch bar |
+| `kanban_renderer.js + .xml` | component | Renders the grid of statement lines |
+
+**Sub-components (Enterprise-mirrored, 14 folders):**
+
+`statement_summary`, `statement_line`, `line_to_reconcile`, `list_view`, `apply_amount`, `bankrec_form_dialog`, `button`, `button_list`, `chatter`, `file_uploader`, `line_info_pop_over`, `quick_create`, `reconciled_line_name`, `search_dialog`.
+
+**Fusion-only additions (5 folders):**
+
+| Folder | Responsibility |
+|---|---|
+| `ai_suggestion/` | Confidence badge + inline strip + alternatives panel (3 files) |
+| `batch_action_bar/` | Bottom bar: "Accept all 47 high-confidence (≥95%)" |
+| `attachment_strip/` | Per-line attachment thumbnails (renders preserved `bank_statement_attachment_ids`) |
+| `partner_history_panel/` | Slide-out: "Sarah has 47 prior reconciles for this partner" |
+| `reconcile_model_picker/` | Per-line dropdown to apply an `account.reconcile.model` rule |
+
+### 4.2 Component Nesting
+
+```mermaid
+graph TD
+ KC["kanban_controller
(widget root)"]
+
+ KC --> SS["statement_summary
(top totals)"]
+ KC --> KR["kanban_renderer
(grid)"]
+ KC --> BAB["batch_action_bar
(bottom)"]
+ KC --> BFD["bankrec_form_dialog
(modal — manual fallback)"]
+ KC --> CHT["chatter
(per-line history panel)"]
+
+ KR --> SL["statement_line
(per-line card)"]
+ KR --> QC["quick_create
(per-line action)"]
+
+ SL --> ACB["ai_confidence_badge"]
+ SL --> ASS["ai_suggestion_strip"]
+ SL --> AAP["ai_alternatives_panel"]
+ SL --> LIP["line_info_pop_over"]
+ SL --> RLN["reconciled_line_name"]
+ SL --> RMP["reconcile_model_picker"]
+ SL --> BL["button_list"]
+ SL --> AS["attachment_strip"]
+
+ BFD --> LTR["line_to_reconcile"]
+ BFD --> LV["list_view"]
+ BFD --> SD["search_dialog"]
+ BFD --> FU["file_uploader"]
+ BFD --> BTN["button"]
+ LV --> AA["apply_amount"]
+```
+
+### 4.3 Data Contract — Controllers and Endpoints
+
+Every UI action goes through `controllers/bank_rec_controller.py`, which calls one of the engine's six public methods.
+
+| Route | Verb | Engine method | Used by component |
+|---|---|---|---|
+| `/fusion_bank_rec/load_kanban` | GET | (read-only ORM) | kanban_controller on mount |
+| `/fusion_bank_rec/reconcile` | POST | `reconcile_one()` | bankrec_form_dialog on confirm |
+| `/fusion_bank_rec/reconcile_batch` | POST | `reconcile_batch()` | batch_action_bar on click |
+| `/fusion_bank_rec/accept_suggestion` | POST | `accept_suggestion()` | ai_suggestion_strip Accept |
+| `/fusion_bank_rec/reject_suggestion` | POST | (writes to suggestion model) | ai_suggestion_strip Reject |
+| `/fusion_bank_rec/refresh_suggestions` | POST | `suggest_matches()` | ai_alternatives_panel on expand |
+| `/fusion_bank_rec/write_off` | POST | `write_off()` | bankrec_form_dialog write-off branch |
+| `/fusion_bank_rec/unreconcile` | POST | `unreconcile()` | reconciled_line_name on click |
+| `/fusion_bank_rec/partner_history` | GET | (read-only) | partner_history_panel on partner select |
+| `/fusion_bank_rec/upload_attachment` | POST | (writes ir.attachment) | file_uploader on drop |
+
+All routes use `type='jsonrpc'` (`type='json'` deprecated in V19). All write-routes have `auth='user'` + imperative `has_group()` check inside the handler.
+
+### 4.4 SCSS Discipline
+
+```scss
+// _bank_rec_tokens.scss — loaded first per workspace SCSS rule (no @import)
+$o-webclient-color-scheme: bright !default;
+
+$_page-hex: #f3f4f6;
+$_card-hex: #ffffff;
+$_border-hex: #d8dadd;
+$_match-hex: #22c55e;
+$_partial-hex: #f59e0b;
+$_nomatch-hex: #94a3b8;
+
+@if $o-webclient-color-scheme == dark {
+ $_page-hex: #1a1d21 !global;
+ $_card-hex: #22262d !global;
+ $_border-hex: #3a3f47 !global;
+}
+
+$br-page: var(--br-page-bg, $_page-hex);
+$br-card: var(--br-card-bg, $_card-hex);
+$br-border: var(--br-border, $_border-hex);
+$br-match: var(--br-match, $_match-hex);
+$br-partial: var(--br-partial, $_partial-hex);
+$br-nomatch: var(--br-nomatch, $_nomatch-hex);
+```
+
+All component SCSS references only `$br-*` tokens. No hardcoded hex outside `_bank_rec_tokens.scss`. Asset bundle declares tokens file FIRST (per workspace rule that `@import` is forbidden in Odoo 19).
+
+### 4.5 OWL Conventions
+
+- `static props = ["*"]` — never `static props = []`
+- Rich-HTML rendering (markdown reasoning, partner-history HTML): `onMounted` + `onPatched` + direct `innerHTML`, NOT `markup()`/`t-out`
+- Component class names: `Fusion` prefix (`FusionStatementLine`, `FusionAISuggestionStrip`)
+- File pairs: `.js` + `.xml`
+
+### 4.6 Coexistence Mode
+
+Menu and views wrapped in `groups` filter referencing dynamically-computed group:
+
+```xml
+
+```
+
+`group_fusion_show_when_enterprise_absent` is a computed group whose membership is "all users when `ir.module.module._fusion_is_enterprise_accounting_installed()` returns False." Implemented in `fusion_accounting_core/models/res_users.py` Phase 1 addition.
+
+---
+
+## 5. AI Integration
+
+### 5.1 Suggestion Record Schema
+
+```python
+class FusionReconcileSuggestion(models.Model):
+ _name = "fusion.reconcile.suggestion"
+ _order = "statement_line_id, confidence desc"
+
+ company_id = fields.Many2one('res.company', required=True, index=True)
+ statement_line_id = fields.Many2one('account.bank.statement.line',
+ required=True, index=True, ondelete='cascade')
+
+ proposed_move_line_ids = fields.Many2many('account.move.line',
+ string="Proposed matches")
+ proposed_write_off_amount = fields.Monetary()
+ proposed_write_off_account_id = fields.Many2one('account.account')
+
+ confidence = fields.Float(required=True)
+ confidence_band = fields.Selection([
+ ('high', 'High (≥95%)'),
+ ('medium', 'Medium (70-94%)'),
+ ('low', 'Low (50-69%)'),
+ ('none', 'No confidence (<50%)'),
+ ], compute='_compute_band', store=True)
+ rank = fields.Integer()
+ reasoning = fields.Text()
+
+ score_amount_match = fields.Float()
+ score_partner_pattern = fields.Float()
+ score_precedent_similarity = fields.Float()
+ score_ai_rerank = fields.Float()
+
+ generated_at = fields.Datetime(default=fields.Datetime.now)
+ generated_by = fields.Selection([
+ ('cron_batch', 'Batch cron'),
+ ('on_demand', 'User refreshed alternatives'),
+ ('on_open', 'Widget opened (lazy)'),
+ ])
+ provider_used = fields.Char()
+ tokens_used = fields.Integer()
+ generation_ms = fields.Integer()
+
+ state = fields.Selection([
+ ('pending', 'Pending review'),
+ ('accepted', 'Accepted'),
+ ('rejected', 'Rejected'),
+ ('superseded', 'Superseded by newer suggestion'),
+ ('stale', 'Stale (line changed since)'),
+ ], default='pending', index=True)
+ accepted_at = fields.Datetime()
+ accepted_by = fields.Many2one('res.users')
+ rejected_at = fields.Datetime()
+ rejected_reason = fields.Selection([
+ ('wrong_invoice', 'Wrong invoice'),
+ ('wrong_partner', 'Wrong partner'),
+ ('wrong_amount', 'Amount off'),
+ ('not_a_match', 'No good match exists'),
+ ('other', 'Other'),
+ ])
+```
+
+### 5.2 Suggestion Generation Flow
+
+```mermaid
+graph TD
+ trigger[Trigger]
+ trigger --> cron["Cron batch (every 15 min)"]
+ trigger --> open["Widget open (lazy fill)"]
+ trigger --> demand["User clicks Show alternatives"]
+
+ cron --> work["Pick unreconciled lines
without fresh suggestions
(LIMIT 200 per run)"]
+ open --> work
+ demand --> work
+
+ work --> p1["Pass 1: SQL filter
partner match + reconcilable account"]
+ p1 --> p2["Pass 2: Statistical scoring
(amount + pattern + precedent features)"]
+ p2 --> top5["Take top 5 candidates per line"]
+
+ top5 --> aikey{AI key configured?}
+ aikey -->|yes| p3["Pass 3: AI re-rank
send features + candidates
get ranking + reasoning"]
+ aikey -->|no| stat["Use statistical reasoning"]
+
+ p3 --> write
+ stat --> write
+ write["Write fusion.reconcile.suggestion rows
(supersede prior pending)"]
+
+ write --> badge["Frontend reads on next render
→ confidence badge appears"]
+```
+
+### 5.3 Domain Prompt
+
+`fusion_accounting_ai/services/prompts/bank_rec_prompt.py` builds the suggestion-ranking prompt. Same pattern as existing domain prompts.
+
+**System prompt (sent once per request):**
+
+```
+You are a bank reconciliation assistant for a Canadian accounting team.
+Your task: rank candidate matches for a bank statement line and explain
+each ranking in one sentence.
+
+Output format: JSON only, no prose. Schema:
+{
+ "ranked": [
+ {"candidate_id": , "confidence": <0.0-1.0>, "reason": ""},
+ ...
+ ]
+}
+
+Rules:
+- Confidence >0.95 only when amount + partner + memo all align perfectly
+- Confidence 0.70-0.94 when 2 of 3 align
+- Confidence <0.70 when only one aligns or evidence is conflicting
+- If a candidate is clearly wrong, omit it
+- Reason must cite specific evidence (amount, prior reconciles, memo tokens)
+- Currency: Canadian English. Amounts in $X,XXX.XX format.
+```
+
+**User prompt** assembles bank line features + partner pattern + 3 nearest precedents + top-5 statistical candidates. Token budget bounded by `fusion_accounting.bank_rec.suggest_token_budget_per_line` (default 1500).
+
+**Key prompt design choices:**
+
+- JSON output, not tool-calling — works on local models that lack reliable tool-calling
+- Feature vector only, no raw transaction descriptions — privacy + token efficiency
+- Pre-filtered candidates — statistical pass narrows to top 5 before LLM sees anything
+- Precedents inline as concrete evidence, not abstract rules
+
+### 5.4 Per-Feature Provider Defaults
+
+| Setting | Default | Rationale |
+|---|---|---|
+| `fusion_accounting.provider.bank_rec_suggest` | (global default) | Statistical baseline always works; AI re-rank is bonus |
+| `fusion_accounting.provider.bank_rec_explain` | (global default) | Used for chat-panel "why?" queries |
+| `fusion_accounting.bank_rec.suggest_temperature` | `0.0` | Deterministic ranking |
+| `fusion_accounting.bank_rec.suggest_max_tokens` | `800` | Bounded output for 5 candidates × ~150 tokens |
+| `fusion_accounting.bank_rec.suggest_token_budget_per_line` | `1500` | Truncates precedent context if would exceed |
+
+### 5.5 Auto-Accept Threshold (the safety question)
+
+Per the assistive-not-native scope decision: **AI never auto-applies a reconciliation.** What looks like "auto" is:
+
+1. Cron generates suggestions overnight (writes pending suggestions)
+2. User opens widget, sees N high-confidence (≥95%) suggestions
+3. User clicks "Accept all N high-confidence" in `batch_action_bar`
+4. Batch processed via `engine.reconcile_batch()` — same audit trail, just bulk-clicked
+
+Threshold (`0.95`) is configurable via `fusion_accounting.bank_rec.high_confidence_threshold`. User owns the decision; no silent reconciles in the books.
+
+### 5.6 Feedback Loop
+
+| User action | Suggestion record | Precedent record | Pattern impact |
+|---|---|---|---|
+| Click Accept on AI suggestion | `state='accepted'`, `accepted_by`, `accepted_at` | New, `source='ai_accepted'` | Reinforces partner's pref_strategy |
+| Click Reject with reason | `state='rejected'`, `rejected_reason` | (none) | Pattern weighting lowered if multiple rejects show same wrong-strategy |
+| Manually reconcile against different items | Existing `state='superseded'` | New, `source='manual'` | Updated normally |
+| Manually reconcile after refusing AI | Suggestion `state='rejected'` (auto), `rejected_reason='other'` | New, `source='manual'` | Pattern shifts toward what user picked |
+
+Pattern refresh cron (nightly) reads accepted/rejected/superseded suggestions and adjusts `fusion.reconcile.pattern` weights via exponential moving average (90-day half-life, configurable).
+
+### 5.7 Integration with Existing Phase 0 AI Tools
+
+**New tools** added to `fusion_accounting_ai/services/tools/bank_reconciliation.py`:
+
+- `get_pending_suggestions(journal_id, limit=100)` — returns current pending suggestions
+- `explain_suggestion(suggestion_id)` — expands reasoning with precedent details
+- `accept_suggestion_via_chat(suggestion_id)` — Tier 3 action (requires approval card)
+- `reject_suggestion_via_chat(suggestion_id, reason)` — Tier 2 action; logged
+- `batch_accept_high_confidence(journal_id, threshold=0.95)` — Tier 3 action
+
+**Existing tools refactored** to use the engine:
+
+- `get_unreconciled_bank_lines` — reads from materialized view, includes confidence badges
+- `find_similar_bank_lines` — uses precedent lookup (much better matches than original SQL)
+- `find_missing_itc_bills` — pulls AI reasoning for HST suggestions
+
+The chat AI can now have conversations like: *"Look at the RBC unreconciled lines. Anything obvious?" → "There are 47 lines with ≥95% confidence ready, including 12 from Westin Plating Co matching their typical $1,847.50 cadence. Want to walk through any, or batch-accept all 47?"*
+
+### 5.8 Cost / Token Budget Controls
+
+| Knob | Default | Purpose |
+|---|---|---|
+| `fusion_accounting.bank_rec.cron_max_lines_per_run` | `200` | Per-cron-tick cap |
+| `fusion_accounting.bank_rec.suggest_token_budget_per_line` | `1500` | Per-prompt cap including precedent context |
+| `fusion_accounting.bank_rec.daily_token_budget_per_company` | `100000` | Hard daily cap; falls back to statistical when exceeded |
+| `fusion_accounting.bank_rec.suggest_min_confidence_to_persist` | `0.50` | Skip writing suggestion if statistical baseline < 50% |
+
+Token usage logged per-suggestion (`tokens_used` field) for cost auditing via SQL.
+
+---
+
+## 6. Migration & Switchover Story
+
+### 6.1 Phase 1 Contribution to Migration Wizard
+
+Phase 1 fills the wizard's `action_run_migration` extension point with a five-step bank-rec migration:
+
+```mermaid
+graph TD
+ s1["Step 1: Snapshot pre-migration counts
account.partial.reconcile, account.full.reconcile,
mail.message linked to reconciles, ir.attachment"]
+ s2["Step 2: Bootstrap fusion.reconcile.precedent
(scan 16,500 historical reconciles, batched 1000/commit)"]
+ s3["Step 3: Aggregate fusion.reconcile.pattern per partner"]
+ s4["Step 4: Verify counts unchanged
(re-snapshot, assert equality)"]
+ s5["Step 5: Set fusion_accounting.migration.account_accountant.completed = True
(safety guard now permits Enterprise uninstall)"]
+ s6["Step 6: Generate audit report PDF
(saved as ir.attachment, downloadable from wizard)"]
+
+ s1 --> s2 --> s3 --> s4 --> s5 --> s6
+```
+
+Bootstrap timing on Westin's 16,500 reconciles: ~30-60 seconds. Steps run inside `cr.savepoint()` per batch — failure mid-batch doesn't corrupt state; next click resumes (idempotent via `source='historical_bootstrap'` check).
+
+### 6.2 Audit Report Contents
+
+Generated as PDF + JSON `ir.attachment` linked to wizard. Sections:
+
+**Summary:**
+- Migration timestamp, operator user, company
+- Pre/post counts side-by-side: `account.partial.reconcile`, `account.full.reconcile`, `mail.message`-on-reconciles, `ir.attachment`-on-bank-statements
+- Verdict: ✓ identical / ✗ mismatch (with delta)
+
+**Pattern bootstrap:**
+- Total precedents created (e.g., 16,500)
+- Patterns aggregated (e.g., 412 unique partners)
+- Per-partner snapshot table (top 20 by reconcile volume)
+- Anomalies flagged: partners with conflicting historical patterns, broken xml-id links
+
+**Sample reconciliation deep-dive:**
+- 10 randomly-sampled historical reconciliations
+- For each: bank line summary, matched journal items, who reconciled, when, full chatter dump, attachments listed
+- Same 10 lines re-fetched after Enterprise uninstall → assert byte-identical
+
+The report is the operator's *proof of preservation* — handoff artifact for auditor or client.
+
+### 6.3 Flag Mechanics
+
+Safety guard (Phase 0 Task 17) blocks Enterprise uninstall until `fusion_accounting.migration.account_accountant.completed = 'True'`. Phase 1 sets that flag in Step 5 only after Step 4 verification passes.
+
+If verification fails (count mismatch — defensively impossible since data is Community-owned, but checked anyway):
+- Flag NOT set
+- Audit report generated marked `verdict: FAIL`
+- Wizard surfaces UserError linking to support
+- Safety guard remains active
+
+### 6.4 Full Switchover Sequence
+
+```mermaid
+graph TD
+ pre["Pre-switchover
Client running Odoo 19 Enterprise"]
+
+ step1["1. Take pg_dump backup"]
+ step2["2. Install fusion_accounting meta-module"]
+ step3["3. Verify install
HTTP /web/login → 200"]
+ step4["4. Coexistence detected
fusion_bank_rec menu HIDDEN
Enterprise widget keeps working"]
+ step5["5. Open Settings → Fusion Accounting → Migrate from Enterprise
Click 'Run Migration'
Wait ~30-60s for bootstrap"]
+ step6["6. Review audit report PDF"]
+ step7["7. Uninstall account_accountant
(safety guard checks flag, allows uninstall)"]
+ step8["8. Coexistence re-detects
fusion_bank_rec menu APPEARS"]
+ step9["9. Spot-check: open 5 historical reconciles
Verify chatter visible, attachments, all marked reconciled"]
+ step10["10. Hand audit report to client/auditor"]
+
+ pre --> step1 --> step2 --> step3 --> step4 --> step5 --> step6 --> step7 --> step8 --> step9 --> step10
+```
+
+Total switchover wall-clock: ~30-60 minutes (excluding pg_dump). Most of that is human verification.
+
+### 6.5 Rollback Scenarios
+
+**Scenario A: Operator regrets after Enterprise uninstall**
+- Restore pg_dump from Step 1
+- Restore previous extra-addons checkout
+- Restart Odoo
+- Wall-clock: ~10-30 min
+
+**Scenario B: Step 5 fails mid-bootstrap**
+- Bootstrap is batched + idempotent
+- Re-clicking "Run Migration" resumes
+- No partial-state corruption possible
+
+**Scenario C: Step 7 fails (safety guard blocks)**
+- Migration not run yet → wizard error tells operator to run it
+- Migration ran but Step 4 verification failed → audit report explains; do not proceed
+- Operator wants to force-uninstall → set flag manually via Developer Mode → Settings → Technical → System Parameters
+
+**Scenario D: Step 8 fails (fusion menu doesn't appear)**
+- Coexistence detection re-evaluates user groups on session refresh, not instantly
+- Operator logs out + back in → menu appears
+
+### 6.6 Edge Cases
+
+**Multi-company:**
+- Migration is per-company; per-company completion flag (`fusion_accounting.migration.account_accountant.completed.`)
+- Safety guard checks all-companies-completed before allowing Enterprise uninstall
+- Audit report shows per-company sections
+
+**Live reconciles during migration:**
+- Wizard takes brief advisory lock on `account.partial.reconcile` during snapshot steps
+- Bootstrap step doesn't lock — runs against snapshot view
+- New Enterprise reconciles during bootstrap are outside scope; next pattern refresh cron picks them up
+
+**Re-running migration:**
+- Safe; bootstrap dedupe-checks via existing precedent rows (`source='historical_bootstrap'`)
+- Useful for re-bootstrap after manual data fixes
+
+### 6.7 Inputs from Other Sub-Modules
+
+| Source | Needed | Status |
+|---|---|---|
+| `fusion_accounting_core/models/account_bank_statement_line.py` | Shared-field-ownership for `cron_last_check` | Phase 1 adds (1-line addition) |
+| `fusion_accounting_core/models/res_users.py` | Computed group `group_fusion_show_when_enterprise_absent` | Phase 1 NEW file |
+| `fusion_accounting_migration` wizard | `action_run_migration` extension point | Already exists (Phase 0 Task 17) |
+| `fusion_accounting_ai` | `BankRecAdapter._via_fusion` paths | Already stubbed (Phase 0 Task 9), Phase 1 fills |
+
+No new sub-modules needed; Phase 1's migration is entirely additive.
+
+---
+
+## 7. Testing Strategy
+
+### 7.1 Test File Layout
+
+```
+tests/
+├── _fixtures/ # SQL-captured Enterprise reconcile outcomes
+│ ├── westin_simple_match.sql
+│ ├── westin_partial_chain.sql
+│ ├── westin_exchange_diff_usd_cad.sql
+│ ├── westin_writeoff_with_hst.sql
+│ └── westin_unreconcile.sql
+├── _factories.py # Test data builders
+├── test_reconcile_engine_unit.py # Pure algorithm tests
+├── test_reconcile_engine_property.py # Hypothesis property-based
+├── test_reconcile_engine_integration.py # Full-stack ORM tests
+├── test_pattern_extraction.py
+├── test_precedent_lookup.py
+├── test_memo_tokenizer.py
+├── test_confidence_scoring.py
+├── test_ai_suggestion_lifecycle.py
+├── test_widget_endpoints.py
+├── test_coexistence.py
+├── test_migration_round_trip.py
+├── test_performance.py
+├── test_local_llm_compat.py
+└── tours/
+ ├── manual_reconcile_tour.js
+ ├── ai_assist_accept_tour.js
+ ├── batch_reconcile_tour.js
+ ├── partial_reconcile_tour.js
+ └── write_off_tour.js
+```
+
+~150 tests total (~115 Python + ~35 user flows in 5 tours).
+
+### 7.2 TDD Discipline
+
+Every engine algorithm gets a failing test first, then implementation, then green. Order matters: tests written by dependency layer.
+
+**Layer 1: Pure validators (no ORM).** Tests written first, fail initially.
+**Layer 2: Matching strategy classes.** Built on Layer 1.
+**Layer 3: Engine orchestration.** Uses Layers 1-2.
+
+### 7.3 Property-Based Tests for Amount Invariants
+
+Hypothesis library generates thousands of input combinations. Specific invariants:
+
+```python
+@given(bank_amount=st.decimals(min_value='0.01', max_value='100000.00', places=2),
+ invoice_amounts=st.lists(st.decimals(min_value='0.01', max_value='100000.00', places=2),
+ min_size=1, max_size=10))
+def test_invariant_total_debits_equal_total_credits(bank_amount, invoice_amounts):
+ """Every reconciliation MUST produce balanced debit/credit totals."""
+
+@given(amounts=st.lists(st.decimals(places=2), min_size=2, max_size=20))
+def test_invariant_partial_reconciles_sum_to_full(amounts):
+ """Sum of all account.partial.reconcile rows must equal the line residual."""
+
+@given(rate_invoice=st.decimals(min_value='0.5', max_value='2.0'),
+ rate_reconcile=st.decimals(min_value='0.5', max_value='2.0'))
+def test_invariant_exchange_diff_balances_books(rate_invoice, rate_reconcile):
+ """Cross-currency reconciles produce balanced exchange-diff move."""
+```
+
+Hypothesis runs each test 100 times with shrinking — finds failures by minimising input.
+
+### 7.4 Integration Tests — Real Odoo, Real Data
+
+5 SQL-captured fixtures from real Westin reconciliations. Each tests `fusion.reconcile.engine.reconcile_one()` produces byte-identical `account.partial.reconcile` rows to what Enterprise produced.
+
+```python
+@classmethod
+def setUpClass(cls):
+ super().setUpClass()
+ cls._load_fixture('westin_simple_match.sql')
+
+def test_replay_simple_match(self):
+ line = self.env.ref('test_fixture.westin_bank_line_4827')
+ candidates = self.env.ref('test_fixture.westin_invoice_00118').line_ids
+ expected_partial = self.env.ref('test_fixture.westin_partial_42891')
+
+ self.env['account.partial.reconcile'].browse(expected_partial.id).unlink()
+ result = self.env['fusion.reconcile.engine'].reconcile_one(line, against_lines=candidates)
+
+ new_partial = self.env['account.partial.reconcile'].browse(result['partial_ids'])
+ for fname in ('amount', 'debit_move_id', 'credit_move_id', 'company_id', 'currency_id'):
+ assert new_partial[fname] == expected_partial[fname]
+```
+
+Coverage: simple, partial chain, exchange diff USD/CAD, write-off with HST, unreconcile reversal.
+
+### 7.5 Tour Tests
+
+5 OWL tours, one per major user flow. Slow (~30s each) but catches JS regressions no unit test would.
+
+| Tour | Flow validated |
+|---|---|
+| `manual_reconcile_tour.js` | Open widget → search invoice → click Reconcile |
+| `ai_assist_accept_tour.js` | See badge → click Accept on strip |
+| `batch_reconcile_tour.js` | Click "Accept all 47 high-confidence" |
+| `partial_reconcile_tour.js` | Pick 2 invoices summing to less than line → assert partial residual |
+| `write_off_tour.js` | $0.05 write-off with HST → assert move with tax split posted |
+
+### 7.6 Performance Benchmarks
+
+Per Q5 target: P95 <1.5s @ 500 lines, <3s @ 5000 lines.
+
+```python
+def test_kanban_load_500_lines_under_1500ms(env, benchmark):
+ seed_unreconciled_lines(env, count=500, journal=env.ref('test.bank_journal_rbc'))
+ result = benchmark(env['fusion.bank.rec.widget'].action_open_kanban,
+ journal_id=env.ref('test.bank_journal_rbc').id)
+ assert benchmark.stats.stats.percentiles[95] < 1.5
+
+def test_suggest_matches_uses_materialized_view(env):
+ """Regression guard: must hit materialized view, not per-record compute."""
+ seed_lines(env, count=100)
+ with env.cr.queries_recorded() as queries:
+ env['fusion.reconcile.engine'].suggest_matches(env['account.bank.statement.line'].search([], limit=100))
+ mv_hits = [q for q in queries if 'fusion_unreconciled_per_partner_mv' in q.sql]
+ assert len(mv_hits) >= 1
+```
+
+Performance regressions become test failures.
+
+### 7.7 Migration Round-Trip Test
+
+The single most important integration test — codifies the seamlessness guarantee.
+
+```python
+@tagged('post_install', '-at_install', 'migration_round_trip')
+class TestMigrationRoundTrip(TransactionCase):
+ def test_full_round_trip_preserves_every_reconciliation(self):
+ # Step 1: with Enterprise installed, create representative dataset
+ self._install_enterprise_modules()
+ bank_lines = self._create_bank_lines(count=50)
+ invoices = self._create_invoices(count=30)
+ self._enterprise_reconcile(bank_lines, invoices)
+
+ # Step 2: snapshot
+ before = self._snapshot_reconcile_state()
+ assert before['partial_count'] > 0
+
+ # Step 3: install fusion + run migration
+ self._install_module('fusion_accounting_bank_rec')
+ wizard = self.env['fusion.migration.wizard'].create({})
+ wizard.action_run_migration()
+
+ # Step 4: assert flag set
+ flag = self.env['ir.config_parameter'].get_param(
+ 'fusion_accounting.migration.account_accountant.completed')
+ assert flag == 'True'
+
+ # Step 5: uninstall Enterprise (safety guard now permits)
+ self._uninstall_module('account_accountant')
+
+ # Step 6: re-snapshot and compare
+ after = self._snapshot_reconcile_state()
+ assert after == before, f"Reconcile state changed: {self._diff(before, after)}"
+
+ # Step 7: open historical reconcile via fusion widget, assert chatter visible
+ line = bank_lines[0]
+ widget_state = self.env['fusion.bank.rec.widget']._render_for_line(line)
+ assert widget_state['is_reconciled'] is True
+ assert widget_state['chatter_messages']
+```
+
+Run on every CI push; failure blocks merge.
+
+### 7.8 Local LLM Compatibility Tests
+
+Skipped automatically if no local LLM configured. When LM Studio reachable:
+
+```python
+@pytest.mark.skipif(not _is_lmstudio_reachable(), reason='LM Studio not running')
+def test_openai_compat_adapter_against_lmstudio(env):
+ env['ir.config_parameter'].set_param(
+ 'fusion_accounting.openai_base_url', 'http://host.docker.internal:1234/v1')
+ env['ir.config_parameter'].set_param(
+ 'fusion_accounting.openai_api_key', 'lm-studio')
+ env['ir.config_parameter'].set_param(
+ 'fusion_accounting.provider.bank_rec_suggest', 'openai_compatible')
+
+ result = env['fusion.reconcile.engine'].suggest_matches(
+ env['account.bank.statement.line'].search([], limit=1))
+ suggestions = env['fusion.reconcile.suggestion'].search([])
+ assert suggestions
+ assert suggestions[0].provider_used.startswith('openai_compatible')
+ assert 0.0 <= suggestions[0].confidence <= 1.0
+```
+
+These tests document the local LLM contract executably.
+
+### 7.9 Coexistence Tests
+
+```python
+def test_menu_hidden_when_enterprise_installed(env):
+ self._install_module('account_accountant')
+ self._install_module('fusion_accounting_bank_rec')
+ user = self._create_user_with_group('fusion_accounting_core.group_fusion_accounting_user')
+ menu = env.ref('fusion_accounting_bank_rec.menu_fusion_bank_rec').sudo(user)
+ assert not _menu_visible_to(menu, user)
+
+def test_menu_visible_when_enterprise_absent(env):
+ self._uninstall_module('account_accountant')
+ user = self._create_user_with_group('fusion_accounting_core.group_fusion_accounting_user')
+ menu = env.ref('fusion_accounting_bank_rec.menu_fusion_bank_rec').sudo(user)
+ assert _menu_visible_to(menu, user)
+```
+
+### 7.10 CI Integration
+
+Phase 1 adds `fusion_accounting_bank_rec` to existing CI matrix at `.gitea/workflows/fusion_accounting_ci.yml`:
+
+```yaml
+matrix:
+ sub_module:
+ - fusion_accounting_core
+ - fusion_accounting_ai
+ - fusion_accounting_migration
+ - fusion_accounting_bank_rec # NEW
+```
+
+Plus separate slow-tests job runs migration_round_trip + performance benchmarks weekly (~10 min).
+
+---
+
+## 8. Acceptance Criteria for This Roadmap
+
+Phase 1 is "done" and ready for Phase 2 brainstorming when:
+
+- All 6 sections of this design implemented and committed to a feature branch
+- 23 OWL components rendered correctly in browser smoke test (manual visual check)
+- Test suite passes: ~150 Python tests + 5 tour tests + migration round-trip + performance benchmarks meet P95 targets
+- Migration wizard runs successfully against the local OrbStack VM (which has 13k bank lines)
+- Audit report PDF generates correctly with sample 10 reconciliations
+- Coexistence verified: menu hides when `account_accountant` installed, appears when uninstalled
+- Local LLM smoke test passes against LM Studio (`http://host.docker.internal:1234/v1`)
+- AI tools in chat panel can answer "what's queued for reconcile?" and "why this suggestion?"
+- BankRecAdapter `_via_fusion` paths return real data (no longer stubs)
+- `fusion_accounting/__manifest__.py` (meta-module) depends on `fusion_accounting_bank_rec`
+- All commits on `main`, tagged `fusion_accounting/phase-1-complete`
+- Empirical Enterprise-to-fusion switchover tested on a clone of Westin's local DB (not production)
+
+---
+
+## 9. Open Items Deferred to Phase 1.x or Later
+
+- Native Ollama adapter UI (model picker, pull from registry) — separate sub-module if needed
+- Local embedding model + pgvector pattern matching — keyword tokenization is v1 baseline
+- "Privacy mode" UI badge when local provider active
+- Mobile-responsive widget below 768px viewport
+- True auto-apply mode (cron applies high-confidence reconciles silently) — separate AI-native sub-module
+- Mexican CFDI, cash-basis CABA, batch payment matching — out-of-scope features, deferred to dedicated phases
+- Native llama.cpp Python binding (no HTTP) — niche
+- Pre-migration "dry-run" mode for the wizard
+- Multi-language prompts beyond English
+
+---
+
+## 10. References
+
+- Roadmap design: [`fusion_accounting/docs/superpowers/specs/2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md`](2026-04-18-fusion-accounting-enterprise-takeover-roadmap-design.md) Section 4.3
+- Phase 0 plan: [`fusion_accounting/docs/superpowers/plans/2026-04-18-phase-0-foundation-plan.md`](../plans/2026-04-18-phase-0-foundation-plan.md)
+- Phase 0 empirical verification: [`fusion_accounting/docs/superpowers/specs/2026-04-18-empirical-uninstall-test-results.md`](2026-04-18-empirical-uninstall-test-results.md)
+- Workspace conventions: [`/Users/gurpreet/Github/Odoo-Modules/CLAUDE.md`](../../../../CLAUDE.md)
+- Environment safety rule: [`/Users/gurpreet/Github/Odoo-Modules/.cursor/rules/environment-safety.mdc`](../../../../.cursor/rules/environment-safety.mdc)
+- Reference Enterprise source (V19 snapshot): `/Users/gurpreet/Github/RePackaged-Odoo/accounting/account_accountant/`
+- Visual design mockups (this session): `/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/`