# Phase 4: Item and Recipient Detail Editing - Research

**Researched:** 2026-03-06
**Domain:** Rust in-memory mutation flows, Slint UI hover/popover patterns, optimistic-concurrency persistence
**Confidence:** HIGH

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Recipient summary popover behavior**
- Opening: recipient name interaction should include a dedicated summary affordance (`name + icon`) instead of name-only text target.
- Placement: summary is a floating panel anchored to the selected card, not a centered modal or permanent side panel.
- Structure: keep concise sections for required CARD-10 fields (all items in possession, last shipment date, products received in last shipment, last status update).
- Dismissal: close via outside click and `Esc`.

**Item add/edit/remove workflow**
- Entry point: show inline item action controls when hovering the card's products/items list.
- Add flow: input is name-first, but behaves as fuzzy search over existing reusable item catalog entries and matching Shopify products so pre-defined items (including image metadata) can be reused quickly.
- Shopify-linked items: allow local rename/display-name adjustments; do not allow full structural edits that can drift from upstream linkage.
- Removal: require confirmation, then remove item association from the card/package context without deleting the reusable local item catalog record.

**Latest note editing workflow**
- Entry point: notes are edited inline on the card (package context), not on recipient profile surfaces.
- Save semantics: explicit save action (not blur auto-save or per-keystroke persistence).
- Format: multi-line plain text is supported for note content.
- Data target: each card represents a package, so edits commit to that card/package's latest note.

### Claude's Discretion
- Exact iconography, hover reveal animation, and spacing for inline item controls.
- Popover width/height constraints and long-content overflow treatment.
- Exact wording/style for validation and save-success/error feedback.

### Deferred Ideas (OUT OF SCOPE)
- Full note history browsing/timeline per card (beyond editing the current latest note) is a separate capability and should be planned as a future phase.
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| ITEM-01 | User can add, edit, and remove WITwhat-owned "items in possession." | Covered by: service mutation API pattern, `EditCommand` enum extension on `DashboardDataClient`, `upsert_item`/version-fenced writes in `Repository`, UI hover-reveal inline controls in Slint |
| ITEM-03 | User can add/edit latest note for recipient/package context. | Covered by: `Package.latest_note` field already modeled and stored; `upsert_package` with optimistic concurrency exists; note edit UI pattern (inline multi-line TextInput in Slint with explicit save button) |
| ITEM-04 | System stores item metadata needed to render first-item thumbnail. | Covered by: `Item` domain model needs `image_hint` field added; `items` table needs `image_hint` column in migration 0002; `first_item_image_hint` projection already exists in `RecipientSnapshot` |
| CARD-10 | Clicking recipient name opens a floating summary with all items in possession, last shipment date, products received in last shipment, and last status update. | Covered by: Slint `PopupWindow` element with `close-on-click: false` for interactive content; `RecipientSnapshot` already carries `item_count`, `item_summary`, `shipment_status`, `shipment_status_date`; needs `last_shipment_date` and `last_received_items` fields added to snapshot |
</phase_requirements>

---

## Summary

Phase 4 adds mutation capabilities to a codebase that currently has only read/refresh flows. The project is a Rust workspace with a `crates/app` frontend layer (Slint UI, view-model, projection pipeline) and a `crates/service` backend layer (in-memory `Repository`, domain models, service API). All three of the phase's functional areas -- item CRUD, note editing, and recipient summary panel -- require coordinated changes across these layers rather than isolated additions.

The mutation path follows a clear existing pattern: commands flow through an extended `DashboardDataClient` trait -> service `Repository` upsert methods (already version-fenced with `UpdateOutcome`) -> snapshot refresh -> projection re-run -> card view model update. The key task is wiring this round-trip for writes (currently the trait only exposes reads and refresh). The `upsert_item` and `upsert_package` methods in `Repository` already handle optimistic-concurrency conflict detection, so persistence does not need to be designed from scratch -- it needs to be surfaced through the client trait and new service API endpoints.

The Slint UI currently has a static card grid baseline. Phase 4 requires hover-conditional control rendering (inline item controls), an inline TextInput for note editing, and a floating summary panel anchored to a card. Slint provides `PopupWindow` as a built-in element with `close-on-click` property (defaults to `true`; set to `false` for interactive popovers that stay open when clicked inside). The `PopupWindow` is positioned via `x`/`y` properties relative to its parent, supports `show()` and `close()` functions, and automatically closes on Escape key press with the default close policy. No new third-party crates are anticipated beyond potentially `uuid` for ID generation; the existing near-zero-dependency setup should be maintained.

**Primary recommendation:** Extend `DashboardDataClient` with mutation methods, add service-side mutation API functions mirroring `get_recipient_snapshot`, wire inline edit UI in Slint with explicit save callbacks, and use Slint `PopupWindow` with `close-on-click: false` for the CARD-10 floating summary. Round-trip always goes through snapshot refresh -- never ad hoc UI-only mutation.

---

## Standard Stack

### Core

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Rust (workspace) | edition 2021 | All application logic | Already the project stack; no alternatives |
| Slint | (workspace dep) | Declarative UI for the desktop app | Already adopted in `dashboard.slint`; project UI framework |
| wit_core domain types | local crate | `Item`, `Package`, `Recipient` models | Shared domain -- all edits must go through these types |
| service `Repository` | local crate | In-memory optimistic-concurrency store | `upsert_item`, `upsert_package` already exist with version fencing |

### Supporting

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `std::collections::HashMap` | stdlib | Per-card runtime edit state tracking | Track edit-mode, saving, error per `recipient_id` in `DashboardRuntime` |
| `RefCell` (tests) | stdlib | Interior mutability in fake clients | Keep existing test pattern for `FakeClient` implementations |

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Extending `DashboardDataClient` trait | New separate mutation trait | Separate trait adds indirection without benefit at this scale; one trait per consumer is simpler |
| Snapshot round-trip after edit | Ad hoc ViewModel field mutation | Ad hoc mutation breaks projection consistency and creates divergence risk; round-trip is established pattern |
| Slint `PopupWindow` for floating panel | Centered modal or z-ordered Rectangle | PopupWindow is the native Slint floating element; anchored to card matches locked decision; modal violates constraint |

**Installation:**

No new dependencies are required. All work uses the existing Rust workspace and Slint toolkit. If UUID generation is needed for new item IDs, add `uuid = { version = "1", features = ["v4"] }` to `service/Cargo.toml`.

---

## Architecture Patterns

### Recommended Project Structure

```
crates/
  app/
    src/dashboard/
      actions.rs        # Add EditCommand enum alongside RefreshCommand
      state.rs          # Add CardEditState, CardSummaryState to CardUiState
      view_model.rs     # No structural change; populated via projection
      mod.rs            # Export new state/command types
    ui/
      dashboard.slint   # Add hover controls, note TextInput, PopupWindow summary
  core/
    src/domain/
      item.rs           # Add image_hint and is_active fields to Item
  service/
    src/api/
      recipients.rs     # Existing read snapshot; add mutation functions
      items.rs          # New: add_item, remove_item, rename_item functions
    src/db/
      repository.rs     # Add deactivate_item, load_all_items methods
    migrations/
      0002_item_metadata.sql  # Add image_hint and is_active columns to items
```

### Pattern 1: Command Dispatch for Mutations (mirrors RefreshCommand)

**What:** Define `EditCommand` enum parallel to `RefreshCommand`. Extend `DashboardDataClient` trait with mutation methods. Build an `EditDispatcher` that dispatches commands and returns an `EditReceipt`.
**When to use:** Any user-initiated mutation (add item, remove item, rename item, save note).

```rust
// crates/app/src/dashboard/actions.rs -- extend alongside existing RefreshCommand
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditCommand {
    AddItem { recipient_id: String, package_id: String, display_name: String, image_hint: Option<String>, shopify_product_id: Option<String> },
    RemoveItem { recipient_id: String, package_id: String, item_id: String },
    RenameItem { item_id: String, new_display_name: String },
    SaveNote { package_id: String, note: String },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditReceipt {
    pub succeeded: bool,
    pub error: Option<String>,
    pub conflict: bool,
}
```

**Why:** Matches the established `RefreshCommand`/`RefreshReceipt`/`RefreshDispatcher` pattern already in `actions.rs`. Keeps all command routing symmetric.

### Pattern 2: DashboardDataClient Trait Extension for Mutations

**What:** Add mutation methods to `DashboardDataClient` with default no-op implementations (matching existing `refresh_recipient`/`refresh_all` defaults).
**When to use:** Ensures FakeClient test doubles work without boilerplate; live client overrides.

```rust
// crates/app/src/service_client.rs -- extend existing trait
pub trait DashboardDataClient {
    fn fetch_card_snapshots(&self) -> Vec<RecipientCardSnapshot>;
    fn refresh_recipient(&self, _recipient_id: &str) -> Result<(), String> { Ok(()) }
    fn refresh_all(&self) -> Result<(), String> { Ok(()) }

    // Phase 4 additions:
    fn add_item(&self, _package_id: &str, _display_name: &str, _image_hint: Option<&str>, _shopify_product_id: Option<&str>) -> Result<String, String> { Ok(String::new()) }
    fn remove_item(&self, _item_id: &str) -> Result<(), String> { Ok(()) }
    fn rename_item(&self, _item_id: &str, _new_display_name: &str) -> Result<(), String> { Ok(()) }
    fn save_note(&self, _package_id: &str, _note: &str) -> Result<(), String> { Ok(()) }
    fn search_item_catalog(&self, _query: &str) -> Vec<ItemCatalogEntry> { vec![] }
    fn fetch_recipient_summary(&self, _recipient_id: &str) -> Option<RecipientSummary> { None }
}

pub struct ItemCatalogEntry {
    pub item_id: String,
    pub display_name: String,
    pub image_hint: Option<String>,
    pub shopify_product_id: Option<String>,
}

pub struct RecipientSummary {
    pub recipient_id: String,
    pub all_items: Vec<String>,           // display_name list
    pub last_shipment_date: Option<String>,
    pub last_received_items: Vec<String>, // items from last shipment
    pub last_status_update: Option<String>,
}
```

### Pattern 3: Per-Card Edit State in DashboardRuntime

**What:** Extend `CardUiState` with an `edit_state` field (enum: `Idle`, `EditingNote`, `EditingItems`, `Saving`, `SaveError`) and `summary_open: bool`. Mirror the existing `CardRefreshState` pattern.
**When to use:** Drives inline control visibility and save feedback in UI.

```rust
// crates/app/src/dashboard/state.rs -- extend CardUiState
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CardEditState {
    Idle,
    EditingNote,
    EditingItems,
    Saving,
    SaveError,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardUiState {
    pub selection: DashboardSelectionState,
    pub refresh_state: CardRefreshState,
    pub error_chip: Option<String>,
    // Phase 4 additions:
    pub edit_state: CardEditState,
    pub summary_open: bool,
    pub edit_error: Option<String>,
}
```

### Pattern 4: Service API Mutation Functions

**What:** Add mutation functions to `crates/service/src/api/` following the `get_recipient_snapshot` pattern. Functions accept `&mut Repository` and return `Result<(), ApiError>`.
**When to use:** Backing the client trait implementations.

```rust
// crates/service/src/api/items.rs -- new file
pub fn add_item_to_package(
    repository: &mut Repository,
    package_id: &str,
    display_name: &str,
    image_hint: Option<String>,
    shopify_product_id: Option<String>,
) -> Result<String, ApiError> {
    // Generate item_id (uuid or deterministic key)
    // Build Item with OwnershipMode based on shopify_product_id
    // Call repository.upsert_item(item, 0) -- version 0 for new
    // Return Ok(item_id)
}

pub fn remove_item_from_package(
    repository: &mut Repository,
    item_id: &str,
) -> Result<(), ApiError> {
    // Deactivate item: set is_active = false
    // Catalog record remains for future reuse
}

pub fn save_package_note(
    repository: &mut Repository,
    package_id: &str,
    note: String,
) -> Result<(), ApiError> {
    // Fetch-then-save internally: load package, get current version,
    // update latest_note, upsert with current version as expected_version
    // Return Conflict as ApiError::Conflict only on concurrent writes
}
```

### Pattern 5: Slint PopupWindow for Floating Summary

**What:** Use Slint's built-in `PopupWindow` element for the recipient summary floating panel. Set `close-on-click: false` to keep the panel open for reading. Trigger via `popup.show()` from a TouchArea on the recipient name + icon. Close via explicit `popup.close()` or Escape key.
**When to use:** CARD-10 floating summary implementation.

```slint
// Recipient summary floating panel
popup := PopupWindow {
    close-on-click: false;
    x: 0px;
    y: card-height + 4px;
    width: 280px;

    Rectangle {
        background: #ffffff;
        border-radius: 8px;
        // Shadow/border styling

        // Section: All items in possession
        // Section: Last shipment date
        // Section: Products received in last shipment
        // Section: Last status update

        // Close button
        TouchArea {
            clicked => { popup.close(); }
        }
    }
}

// Trigger from recipient name area
TouchArea {
    clicked => { popup.show(); }
}
```

**Important Slint PopupWindow constraints (verified from official docs):**
- PopupWindow content properties cannot be accessed from outside the PopupWindow element.
- Data for the popup must be passed via `in property` bindings set before calling `show()`.
- PopupWindow is positioned relative to its parent element via `x`/`y` properties.
- Default close behavior closes on any click or Escape; set `close-on-click: false` to disable auto-close and use `close()` function manually.
- Escape key closes PopupWindow with default policy (`close-on-click: true`) but does NOT auto-close when `close-on-click: false`. Escape dismissal must be handled manually via `FocusScope` key handler inside the popup when using `close-on-click: false`.

### Pattern 6: Slint Hover Controls for Item Actions

**What:** Use `TouchArea` with `has-hover` property to show/hide inline item action controls.
**When to use:** Item add/edit/remove controls that appear on hover.

```slint
// Hover-reveal item controls
item-area := TouchArea {
    // item list area content here
}
if item-area.has-hover : Rectangle {
    // inline add/remove/edit icon buttons
    // positioned over or beside the item list
}
```

### Pattern 7: Inline Note Edit with Explicit Save

```slint
// Note area - switches between preview and edit mode
if !root.editing-note : Text {
    text: root.note-preview;
    font-size: 12px;
    color: #515d70;
    TouchArea { clicked => { root.editing-note = true; } }
}

if root.editing-note : Rectangle {
    TextInput {
        text <=> root.note-draft;
        wrap: word-wrap;
        font-size: 12px;
    }
}

if root.editing-note : Rectangle {
    // Save button
    TouchArea { clicked => { root.save-note(root.note-draft); } }
}
```

### Anti-Patterns to Avoid

- **Ad hoc ViewModel mutation:** Never mutate `DashboardCardViewModel` fields directly from UI callbacks. Always go through the snapshot round-trip (edit -> service call -> refresh snapshot -> re-project).
- **Blur auto-save:** The locked decision is explicit save only. Do not add `edited()` or focus-loss hooks that trigger persistence.
- **Deleting catalog records on remove:** Item removal must only deactivate the item (set `is_active = false`). The reusable item catalog record must remain intact for future assignment.
- **Full structural edits on Shopify-linked items:** `OwnershipMode::ShopifyLinked` items may only have `display_name` updated -- never `shopify_product_id` or `ownership_mode` changed from the edit UI.
- **Per-keystroke persistence for search:** The fuzzy search in the item add flow is UI-only filtering against an already-fetched catalog; do not call the service on every keystroke.
- **Accessing PopupWindow properties from outside:** Slint does not allow reading or writing properties of elements inside a `PopupWindow` from outside it. All data must be bound via `in property` declarations before `show()` is called.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Optimistic concurrency for note/item edits | Custom version-check logic | `repository.upsert_package` / `repository.upsert_item` with `expected_version` | Already implemented with `UpdateOutcome::Conflict` detection |
| Fuzzy search over item catalog | Custom string distance algorithm | Simple `contains`/`to_lowercase` filter over a fetched `Vec<ItemCatalogEntry>` | Catalog is small (< 100 items); full-text search is overkill |
| Floating panel positioning | Custom overlay/z-order manager | Slint `PopupWindow` with `x`/`y` positioning | Native Slint element handles paint ordering and dismiss behavior |
| Version ID generation for new items | Custom sequential counter | `uuid` crate v4 or timestamp-based deterministic key | Must be unique across restarts; in-memory counters do not survive restart |
| Escape key dismissal for popup | Manual global key listener | Slint `FocusScope` inside PopupWindow | Standard Slint keyboard handling pattern |

**Key insight:** The repository and domain models already do the hard work. Phase 4 is primarily about surfacing existing capabilities through new API functions and new UI affordances -- not building new infrastructure.

---

## Common Pitfalls

### Pitfall 1: Item Soft-Removal Strategy is Undefined in the Current Schema

**What goes wrong:** The current `items` table and `Item` domain model have no `deleted` or `active` flag. "Removing" an item with the locked decision ("remove item association from the card/package context without deleting the reusable local item catalog record") requires a clear strategy that the current model does not provide.

**Why it happens:** The initial schema was built for reading/importing, not editing. No soft-delete mechanism exists yet.

**How to avoid:** Plan 04-01 must add `is_active: bool` to the `Item` domain struct (`crates/core/src/domain/item.rs`) and `is_active INTEGER NOT NULL DEFAULT 1` to `items` table in migration 0002. Filter inactive items in `load_recipient_aggregate` and `build_item_summary`. Keep inactive items queryable via `search_item_catalog` for reuse in add flows.

**Warning signs:** If the planner does not address soft-removal in 04-01, the "remove item" feature cannot be correctly implemented without breaking the reuse invariant.

### Pitfall 2: RecipientSummary Fields Missing from Current Snapshot

**What goes wrong:** `CARD-10` requires "last shipment date" and "products received in last shipment" -- these are not currently fields in `RecipientSnapshot` (it has `shipment_status_date` but not a distinct "last shipment date" vs "last status update date", and no "items received in last shipment" concept).

**Why it happens:** The current snapshot was built for card display, not summary panel detail.

**How to avoid:** Plan 04-03 (floating summary panel) must add fields to `RecipientSnapshot`:
  - `last_shipment_date: Option<String>` -- date of most recent shipment event.
  - `last_received_items: Vec<String>` -- display names of items from last shipment package.
  Also extend `RecipientCardSnapshot` in `service_client.rs` if these fields need to flow to the app layer.

**Warning signs:** If the planner assumes existing snapshot fields are sufficient for CARD-10, the summary panel will be incomplete.

### Pitfall 3: Slint Static Prototype Must Become Data-Driven

**What goes wrong:** The current `dashboard.slint` is a static visual prototype (`for card in [0:6]`). Phase 4 needs dynamic hover states, edit modes, and callback wiring. If the planner treats Slint changes as minor additions, the scope will be underestimated.

**Why it happens:** The baseline was intentionally minimal for Phase 3 visual contracts.

**How to avoid:** Plan 04-01 should include a task to refactor the Slint component to accept proper data model arrays and callback bindings before adding interactive elements. Layering hover controls on the current static prototype without first making cards data-driven will produce brittle Slint code.

**Warning signs:** Any Slint task that references `for card in [0:6]` (the static iteration) as the production structure is building on a placeholder.

### Pitfall 4: Version Fencing for Note Saves Requires Package Version in UI State

**What goes wrong:** `upsert_package` requires `expected_version: i64`. The app layer currently does not store the version of any card/package data it has received. Calling `save_note` without the correct current version will always result in either version=0 (treating every save as a new-record insert) or an incorrect conflict detection.

**Why it happens:** `RecipientCardSnapshot` and `DashboardCardViewModel` do not carry version numbers -- they are display-only.

**How to avoid:** Hide versioning inside the service mutation function. The service API `save_package_note` fetches current version internally, applies update, and returns conflict only if a concurrent write happened at the service layer. The client trait only passes note content.

### Pitfall 5: Item Image Metadata (ITEM-04) Requires Schema and Domain Change

**What goes wrong:** `Item` currently has no `image_hint` field. `first_item_image_hint` in the snapshot is computed as a placeholder string (`format!("placeholder://item/{}", item.item_id)`). ITEM-04 requires the system to actually store item image metadata.

**Why it happens:** Phase 3 deferred real image storage to Phase 4 (ITEM-04 was marked pending).

**How to avoid:** Plan 04-01 must add `image_hint: Option<String>` to the `Item` struct in `crates/core/src/domain/item.rs`, add `image_hint TEXT NULL` to the `items` table in migration 0002, and update `get_recipient_snapshot` to use `item.image_hint.clone()` instead of the placeholder format string.

### Pitfall 6: PopupWindow Cannot Access External State Directly

**What goes wrong:** Slint `PopupWindow` elements cannot have their internal properties read or written from outside the popup. If the summary panel tries to bind to card data dynamically after show(), values may not update.

**Why it happens:** Slint architectural constraint on PopupWindow element scoping.

**How to avoid:** Set all `in property` values on the PopupWindow before calling `show()`. Use property bindings (not imperative assignment) for data that the popup needs to display. If the popup needs to trigger actions, use `callback` declarations inside the PopupWindow and connect them from outside.

---

## Code Examples

Verified patterns from existing codebase:

### Extending CardUiState with Edit Fields

```rust
// Source: crates/app/src/dashboard/state.rs -- extend existing struct
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardUiState {
    pub selection: DashboardSelectionState,
    pub refresh_state: CardRefreshState,
    pub error_chip: Option<String>,
    // New for Phase 4:
    pub edit_state: CardEditState,
    pub summary_open: bool,
    pub edit_error: Option<String>,
}

impl Default for CardUiState {
    fn default() -> Self {
        Self {
            selection: DashboardSelectionState::None,
            refresh_state: CardRefreshState::Idle,
            error_chip: None,
            edit_state: CardEditState::Idle,
            summary_open: false,
            edit_error: None,
        }
    }
}
```

### DashboardRuntime Edit State Methods (mirrors mark_refreshing pattern)

```rust
// Source: crates/app/src/dashboard/mod.rs -- extend DashboardRuntime
impl DashboardRuntime {
    pub fn begin_note_edit(&mut self, recipient_id: &str) {
        let state = self.card_ui.entry(recipient_id.to_string()).or_default();
        state.edit_state = CardEditState::EditingNote;
        state.edit_error = None;
    }

    pub fn mark_saving(&mut self, recipient_id: &str) {
        let state = self.card_ui.entry(recipient_id.to_string()).or_default();
        state.edit_state = CardEditState::Saving;
    }

    pub fn mark_save_complete(&mut self, recipient_id: &str) {
        let state = self.card_ui.entry(recipient_id.to_string()).or_default();
        state.edit_state = CardEditState::Idle;
        state.edit_error = None;
    }

    pub fn mark_save_error(&mut self, recipient_id: &str, error: String) {
        let state = self.card_ui.entry(recipient_id.to_string()).or_default();
        state.edit_state = CardEditState::SaveError;
        state.edit_error = Some(error);
    }

    pub fn toggle_summary(&mut self, recipient_id: &str, open: bool) {
        let state = self.card_ui.entry(recipient_id.to_string()).or_default();
        state.summary_open = open;
    }
}
```

### Repository Item Soft-Removal (recommended approach)

```rust
// Source: crates/service/src/db/repository.rs -- new method
pub fn deactivate_item(&mut self, item_id: &str) -> bool {
    if let Some(item) = self.items.get_mut(item_id) {
        item.is_active = false;
        true
    } else {
        false
    }
}

// Updated load_recipient_aggregate filtering:
let items: Vec<Item> = self
    .items
    .values()
    .filter(|i| package_ids.iter().any(|id| *id == i.package_id) && i.is_active)
    .cloned()
    .collect();
```

### Test Pattern for Edit Command Dispatch (mirrors existing FakeClient)

```rust
// In tests -- extend FakeClient with mutation tracking
#[derive(Default)]
struct FakeClient {
    calls: RefCell<Vec<String>>,
    saved_notes: RefCell<Vec<(String, String)>>,  // (package_id, note)
    added_items: RefCell<Vec<(String, String)>>,   // (package_id, display_name)
    removed_items: RefCell<Vec<String>>,            // item_ids
}

impl DashboardDataClient for FakeClient {
    fn fetch_card_snapshots(&self) -> Vec<RecipientCardSnapshot> { vec![] }
    fn save_note(&self, package_id: &str, note: &str) -> Result<(), String> {
        self.saved_notes.borrow_mut().push((package_id.to_string(), note.to_string()));
        Ok(())
    }
    // ... add_item, remove_item, etc.
}
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Static placeholder `for card in [0:6]` in Slint | Data-driven card component with model array | Phase 4 (this phase) | Cards become interactive; requires Slint component refactor |
| Placeholder image hint (`format!("placeholder://item/{}")`) | Real `image_hint` from `Item.image_hint` field | Phase 4 (this phase) | ITEM-04 finally satisfied; requires domain model + schema change |
| `DashboardDataClient` read-only | `DashboardDataClient` with mutation methods | Phase 4 (this phase) | Enables all edit flows; maintained trait default pattern |

**Deprecated/outdated:**
- `first_item_image_hint: Some(format!("placeholder://item/{}", item.item_id))` in `crates/service/src/api/recipients.rs`: This placeholder must be replaced in Phase 4 with `item.image_hint.clone()` once the domain field is added.

---

## Open Questions

1. **UUID/ID generation strategy for new items**
   - What we know: `item_id` is a `String` primary key; existing samples use `"uuid-item-1"` style strings.
   - What's unclear: The current service crate has no UUID dependency. Is the plan to add `uuid` crate, use a timestamp-based key, or generate IDs elsewhere?
   - Recommendation: Add `uuid` crate to `service/Cargo.toml` with the `v4` feature for random UUID generation in new `add_item` functions. This is a minimal, standard dependency for this purpose.

2. **Fuzzy item search scope: local catalog only, or also in-memory Shopify product list?**
   - What we know: The locked decision says search covers "existing reusable item catalog entries and matching Shopify products." The current codebase has no in-memory Shopify product list separate from items already imported.
   - What's unclear: Does Phase 4 need to implement a Shopify product cache, or does the search only cover items already in the local `items` table?
   - Recommendation: For Phase 4, scope the search to items already in the in-memory `items` catalog (i.e., items that have been previously added, including those with `OwnershipMode::ShopifyLinked`). Full Shopify product discovery is a Phase 2 data sync concern.

3. **Soft-removal: is a second migration needed, or can the domain model carry an `is_active` flag in memory only?**
   - What we know: The service currently uses an in-memory `Repository` (HashMap). The SQL schema in `0001_initial_schema.sql` exists and is loaded via `include_str!` in `schema.rs`.
   - What's unclear: Whether SQLite is actively used in production or the in-memory store is the operative store for v1.
   - Recommendation: Add `is_active: bool` to the `Item` domain struct regardless. Also add migration 0002 with `ALTER TABLE items ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1` and `ALTER TABLE items ADD COLUMN image_hint TEXT NULL` for forward compatibility.

---

## Validation Architecture

### Test Framework

| Property | Value |
|----------|-------|
| Framework | Rust built-in `#[test]` + `cargo test` |
| Config file | `Cargo.toml` workspace (resolver = "2") |
| Quick run command | `cargo test -p app --lib` |
| Full suite command | `cargo test --workspace` |

### Phase Requirements -> Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| ITEM-01 | Add item dispatches through client, upserts in repository, returns receipt | unit | `cargo test -p app -- edit_command` | No - Wave 0 |
| ITEM-01 | Remove item deactivates (is_active=false), catalog record survives | unit | `cargo test -p service -- deactivate_item` | No - Wave 0 |
| ITEM-01 | Rename item updates display_name, blocks structural edit on ShopifyLinked | unit | `cargo test -p service -- rename_item` | No - Wave 0 |
| ITEM-03 | Save note persists to package.latest_note via upsert_package | unit | `cargo test -p service -- save_package_note` | No - Wave 0 |
| ITEM-03 | Note edit round-trip: save -> refresh snapshot -> projection shows updated note | integration | `cargo test -p app -- note_edit_roundtrip` | No - Wave 0 |
| ITEM-04 | Item with image_hint stores and projects to first_item_image_hint | unit | `cargo test -p service -- item_image_hint` | No - Wave 0 |
| ITEM-04 | Placeholder image_hint replaced with real value in snapshot | unit | `cargo test -p service -- image_hint_projection` | No - Wave 0 |
| CARD-10 | fetch_recipient_summary returns all CARD-10 fields | unit | `cargo test -p app -- recipient_summary` | No - Wave 0 |
| CARD-10 | Summary includes all_items, last_shipment_date, last_received_items, last_status_update | unit | `cargo test -p service -- recipient_summary_fields` | No - Wave 0 |

### Sampling Rate

- **Per task commit:** `cargo test -p app --lib && cargo test -p service --lib`
- **Per wave merge:** `cargo test --workspace`
- **Phase gate:** Full suite green before `/gsd:verify-work`

### Wave 0 Gaps

- [ ] `crates/app/tests/dashboard_edit_tests.rs` -- covers ITEM-01 (edit command dispatch), ITEM-03 (note edit round-trip)
- [ ] `crates/service/tests/item_mutation_tests.rs` -- covers ITEM-01 (add/remove/rename), ITEM-04 (image hint storage)
- [ ] `crates/service/tests/note_mutation_tests.rs` -- covers ITEM-03 (save_package_note with version fencing)
- [ ] `crates/service/tests/recipient_summary_tests.rs` -- covers CARD-10 (summary field projection)
- [ ] No framework install needed -- `cargo test` is already functional

---

## Sources

### Primary (HIGH confidence)

- Direct codebase inspection (crates/app, crates/service, crates/core) -- all patterns derived from reading actual source files
  - `crates/app/src/dashboard/actions.rs` -- RefreshCommand/RefreshDispatcher pattern
  - `crates/app/src/dashboard/state.rs` -- CardUiState, CardRefreshState pattern
  - `crates/app/src/service_client.rs` -- DashboardDataClient trait with default impls
  - `crates/service/src/db/repository.rs` -- upsert_item, upsert_package, UpdateOutcome
  - `crates/service/src/api/recipients.rs` -- RecipientSnapshot, get_recipient_snapshot
  - `crates/core/src/domain/item.rs` -- Item, OwnershipMode
  - `crates/core/src/domain/package.rs` -- Package with latest_note
  - `crates/service/migrations/0001_initial_schema.sql` -- current schema
  - `crates/app/ui/dashboard.slint` -- current static UI baseline
  - All existing test files for pattern verification

### Secondary (MEDIUM confidence)

- [Slint PopupWindow official docs](https://docs.slint.dev/latest/docs/slint/reference/window/popupwindow/) -- `close-on-click` property, `show()`/`close()` functions, positioning behavior, property access restrictions
- [Slint PopupWindow keep-open discussion](https://github.com/slint-ui/slint/discussions/5301) -- verified `close-on-click: false` pattern for interactive popovers

### Tertiary (LOW confidence)

- None -- all findings are grounded in codebase inspection or verified documentation.

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- derived from existing Cargo.toml and source imports; no speculative dependencies
- Architecture patterns: HIGH -- all patterns directly extend established codebase conventions
- Pitfalls: HIGH -- all pitfalls identified from direct gap analysis between current code and phase requirements
- Slint PopupWindow: MEDIUM -- verified from official docs and community discussion; exact positioning behavior for card-anchored popups should be validated during implementation
- Validation architecture: HIGH -- test framework and patterns verified from existing test files

**Research date:** 2026-03-06
**Valid until:** 2026-04-06 (stable domain; 30-day window appropriate; re-verify if Slint version changes)
