# Phase 1: Foundation + Local Plant Database **Goal:** Core infrastructure with camera capture and local plant database integration using `houseplants_list.json` (2,278 plants, 11 categories, 50 families) --- ## Data Source Overview **File:** `data/houseplants_list.json` - **Total Plants:** 2,278 - **Categories:** Air Plant, Bromeliad, Cactus, Fern, Flowering Houseplant, Herb, Orchid, Palm, Succulent, Trailing/Climbing, Tropical Foliage - **Families:** 50 unique botanical families - **Structure per plant:** ```json { "scientific_name": "Philodendron hederaceum", "common_names": ["Heartleaf Philodendron", "Sweetheart Plant"], "family": "Araceae", "category": "Tropical Foliage" } ``` --- ## Tasks ### 1.1 Create Local Plant Database Model **File:** `PlantGuide/Data/DataSources/Local/PlantDatabase/LocalPlantEntry.swift` - [ ] Create `LocalPlantEntry` Codable struct matching JSON structure: ```swift struct LocalPlantEntry: Codable, Identifiable, Sendable { let scientificName: String let commonNames: [String] let family: String let category: PlantCategory var id: String { scientificName } enum CodingKeys: String, CodingKey { case scientificName = "scientific_name" case commonNames = "common_names" case family case category } } ``` - [ ] Create `PlantCategory` enum with 11 cases matching JSON categories - [ ] Create `LocalPlantDatabase` Codable wrapper: ```swift struct LocalPlantDatabase: Codable, Sendable { let sourceDate: String let totalPlants: Int let sources: [String] let plants: [LocalPlantEntry] enum CodingKeys: String, CodingKey { case sourceDate = "source_date" case totalPlants = "total_plants" case sources, plants } } ``` **Acceptance Criteria:** Models compile and can decode `houseplants_list.json` without errors --- ### 1.2 Implement Plant Database Service **File:** `PlantGuide/Data/DataSources/Local/PlantDatabase/PlantDatabaseService.swift` - [ ] Create `PlantDatabaseServiceProtocol`: ```swift protocol PlantDatabaseServiceProtocol: Sendable { func loadDatabase() async throws func searchByScientificName(_ query: String) async -> [LocalPlantEntry] func searchByCommonName(_ query: String) async -> [LocalPlantEntry] func searchAll(_ query: String) async -> [LocalPlantEntry] func getByFamily(_ family: String) async -> [LocalPlantEntry] func getByCategory(_ category: PlantCategory) async -> [LocalPlantEntry] func getPlant(scientificName: String) async -> LocalPlantEntry? var allCategories: [PlantCategory] { get } var allFamilies: [String] { get } var plantCount: Int { get } } ``` - [ ] Implement `PlantDatabaseService` actor for thread safety: - Load JSON from bundle on first access - Build search indices for fast lookups - Implement fuzzy matching for search (handle typos) - Cache loaded database in memory - [ ] Create `PlantDatabaseError` enum: - `fileNotFound` - `decodingFailed(Error)` - `notLoaded` **Acceptance Criteria:** - Service loads all 2,278 plants without memory issues - Search returns results in < 50ms for any query - Case-insensitive search works for scientific and common names --- ### 1.3 Add JSON to Xcode Bundle - [ ] Copy `data/houseplants_list.json` to `PlantGuide/Resources/` folder - [ ] Add file to Xcode project target (ensure "Copy Bundle Resources" includes it) - [ ] Verify file accessible via `Bundle.main.url(forResource:withExtension:)` **Acceptance Criteria:** `Bundle.main.url(forResource: "houseplants_list", withExtension: "json")` returns valid URL --- ### 1.4 Create Plant Lookup Use Case **File:** `PlantGuide/Domain/UseCases/PlantLookup/LookupPlantUseCase.swift` - [ ] Create `LookupPlantUseCase`: ```swift protocol LookupPlantUseCaseProtocol: Sendable { func execute(scientificName: String) async -> LocalPlantEntry? func search(query: String) async -> [LocalPlantEntry] func suggestMatches(for identifiedName: String, confidence: Double) async -> [LocalPlantEntry] } ``` - [ ] Implement suggestion logic: - If confidence < 0.7, return top 5 fuzzy matches from local database - If confidence >= 0.7, return exact match + similar species from same genus - [ ] Handle cultivar names (e.g., `'Brasil'`, `'Pink Princess'`) by matching base species **Acceptance Criteria:** - `suggestMatches(for: "Philodendron hederaceum", confidence: 0.9)` returns the plant + related cultivars - Fuzzy search for "Philo brasil" finds "Philodendron hederaceum 'Brasil'" --- ### 1.5 Integrate with Identification Flow **File:** `PlantGuide/Presentation/Scenes/Identification/IdentificationViewModel.swift` - [ ] Inject `LookupPlantUseCaseProtocol` via DI container - [ ] After ML identification, look up plant in local database: - Enrich results with category and family data - Show "Found in local database" badge for verified matches - Display related species suggestions for low-confidence identifications - [ ] Add `localDatabaseMatch: LocalPlantEntry?` property to view model state **Acceptance Criteria:** - Identification results show category (e.g., "Tropical Foliage") from local database - Low-confidence results display "Did you mean..." suggestions from local database --- ### 1.6 Create Plant Browse View **File:** `PlantGuide/Presentation/Scenes/Browse/BrowsePlantsView.swift` - [ ] Create `BrowsePlantsView` with: - Category filter chips (11 categories) - Search bar for name lookup - Alphabetical section list grouped by first letter - Plant count badge showing total matches - [ ] Create `BrowsePlantsViewModel`: ```swift @MainActor final class BrowsePlantsViewModel: ObservableObject { @Published var searchQuery = "" @Published var selectedCategory: PlantCategory? @Published var plants: [LocalPlantEntry] = [] @Published var isLoading = false func loadPlants() async func search() async func filterByCategory(_ category: PlantCategory?) async } ``` - [ ] Create `LocalPlantRow` component showing: - Scientific name (primary) - Common names (secondary, comma-separated) - Family badge - Category icon **Acceptance Criteria:** - Browse view displays all 2,278 plants with smooth scrolling - Category filter correctly shows only plants in selected category - Search finds plants by any name (scientific or common) --- ### 1.7 Add Browse Tab to Navigation **File:** `PlantGuide/Presentation/Navigation/MainTabView.swift` - [ ] Add "Browse" tab between Camera and Collection: - Icon: `book.fill` or `leaf.fill` - Label: "Browse" - [ ] Update tab order: Camera → Browse → Collection → Care → Settings - [ ] Wire up `BrowsePlantsView` with DI container dependencies **Acceptance Criteria:** Browse tab displays and switches correctly, shows plant database --- ### 1.8 Update DI Container **File:** `PlantGuide/Core/DI/DIContainer.swift` - [ ] Register `PlantDatabaseService` as singleton (load once, reuse) - [ ] Register `LookupPlantUseCase` with database service dependency - [ ] Register `BrowsePlantsViewModel` factory - [ ] Add lazy initialization for database service (load on first access, not app launch) **Acceptance Criteria:** All new dependencies resolve correctly without circular references --- ### 1.9 Create Local Database Tests **File:** `PlantGuideTests/Data/PlantDatabaseServiceTests.swift` - [ ] Test JSON loading success - [ ] Test plant count equals 2,278 - [ ] Test category count equals 11 - [ ] Test family count equals 50 - [ ] Test search by scientific name (exact match) - [ ] Test search by common name (partial match) - [ ] Test case-insensitive search - [ ] Test category filter returns only plants in category - [ ] Test empty search returns empty array - [ ] Test cultivar name matching (e.g., searching "Pink Princess" finds `Philodendron erubescens 'Pink Princess'`) **Acceptance Criteria:** All tests pass, code coverage > 80% for `PlantDatabaseService` --- ## End-of-Phase Validation ### Functional Verification | Test | Steps | Expected Result | Status | |------|-------|-----------------|--------| | Database Load | Launch app, go to Browse tab | Plants display without crash, count shows 2,278 | [ ] | | Category Filter | Select "Cactus" category | Only cactus plants shown, count updates | [ ] | | Search Scientific | Search "Monstera deliciosa" | Exact match appears at top | [ ] | | Search Common | Search "Snake Plant" | Sansevieria varieties appear | [ ] | | Search Partial | Search "philo" | All Philodendron species appear | [ ] | | Identification Enrichment | Identify a plant via camera | Category and family from local DB shown in results | [ ] | | Low Confidence Suggestions | Get low-confidence identification | "Did you mean..." suggestions appear from local DB | [ ] | | Scroll Performance | Scroll through all plants quickly | No dropped frames, smooth 60fps | [ ] | | Memory Usage | Load database, navigate away, return | Memory stable, no leaks | [ ] | ### Code Quality Verification | Check | Criteria | Status | |-------|----------|--------| | Build | Project builds with zero warnings | [ ] | | Tests | All PlantDatabaseService tests pass | [ ] | | Coverage | Code coverage > 80% for new code | [ ] | | Sendable | All new types conform to Sendable | [ ] | | Actor Isolation | PlantDatabaseService is thread-safe actor | [ ] | | Error Handling | All async functions have proper try/catch | [ ] | | Accessibility | Browse view has accessibility labels | [ ] | ### Performance Verification | Metric | Target | Status | |--------|--------|--------| | Database Load | < 500ms first load | [ ] | | Search Response | < 50ms per query | [ ] | | Memory (Browse) | < 30 MB additional | [ ] | | Scroll FPS | 60 fps constant | [ ] | | App Launch Impact | < 100ms added to launch | [ ] | --- ## Phase 1 Completion Checklist - [ ] All 9 tasks completed - [ ] All functional tests pass - [ ] All code quality checks pass - [ ] All performance targets met - [ ] Unit tests written and passing - [ ] Code committed with descriptive message - [ ] Ready for Phase 2 (On-Device ML Integration) --- ## File Manifest New files to create: ``` PlantGuide/ ├── Data/ │ └── DataSources/ │ └── Local/ │ └── PlantDatabase/ │ ├── LocalPlantEntry.swift │ ├── LocalPlantDatabase.swift │ ├── PlantCategory.swift │ ├── PlantDatabaseService.swift │ └── PlantDatabaseError.swift ├── Domain/ │ └── UseCases/ │ └── PlantLookup/ │ └── LookupPlantUseCase.swift ├── Presentation/ │ └── Scenes/ │ └── Browse/ │ ├── BrowsePlantsView.swift │ ├── BrowsePlantsViewModel.swift │ └── Components/ │ └── LocalPlantRow.swift └── Resources/ └── houseplants_list.json (copied from data/) PlantGuideTests/ └── Data/ └── PlantDatabaseServiceTests.swift ``` Files to modify: ``` PlantGuide/ ├── Core/DI/DIContainer.swift (add new registrations) ├── Presentation/Navigation/MainTabView.swift (add Browse tab) └── Presentation/Scenes/Identification/IdentificationViewModel.swift (add local lookup) ``` --- ## Notes - Use `actor` for `PlantDatabaseService` to ensure thread safety for concurrent searches - Consider implementing Trie data structure for fast prefix-based search if needed - The JSON should be loaded lazily on first browse access, not at app launch - For cultivar matching, strip quotes and match base species name - Category icons suggestion: - Air Plant: `leaf.arrow.triangle.circlepath` - Bromeliad: `sparkles` - Cactus: `sun.max.fill` - Fern: `leaf.fill` - Flowering Houseplant: `camera.macro` - Herb: `leaf.circle` - Orchid: `camera.macro.circle` - Palm: `tree.fill` - Succulent: `drop.fill` - Trailing/Climbing: `arrow.up.right` - Tropical Foliage: `leaf.fill`