# Project export: Glovebox

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: The roadside assistant that works precisely when Google Maps doesn't - an on-device AI that you can chat with to fix your car issues step by step. Also get nearby resources. No signal required.
- Devpost: https://devpost.com/software/glovebox
- GitHub: https://github.com/hiratinspace/Glovebox
- Video: https://www.youtube.com/embed/9Q2jNf-NdiI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Hirat Rahi (9 commits)

## Devpost submission (written by the team)

### Inspiration

Five of us packed into a car and drove from Chicago to San Francisco to attend the hackathon. Somewhere in the Nevada desert- no towns, no cell signal, just flat highway stretching to the horizon- our tire blew out. We had a spare. We had tools. What we didn't have was any idea how to use them properly. None of us remembered the lug nut torque sequence. Nobody knew if our car had a full-size spare or a donut with a speed limit. We couldn't look any of it up. We sat stranded on the side of the road for hours. Thirty seconds on Google would have answered every question. But we had no Google. That gap — between having a smartphone and having actual help — is exactly what Glovebox is built to close.

### What it does

Glovebox is an offline AI copilot for stranded drivers. Open the app, describe your problem in plain English, and it walks you through it — step by step, in a conversational chat — with zero internet required. It's not just a generic how-to guide. Glovebox uses RAG (retrieval-augmented generation) over your vehicle's owner's manual, so when you ask "where's the jack storage on my car," it finds the right answer for your specific model and year — not a generic YouTube video that might not match your setup.

### How we built it

Glovebox is a React Native app for iOS built in TypeScript. The core is on-device LLM inference via llama.rn, running a quantized GGUF model (Llama 3.2-1B-Instruct or Gemma-2-2B at Q4_K_M — under 1.5GB). All inference happens locally on the iPhone; no API calls, no backend. On top of the LLM, I built a lightweight keyword-based RAG pipeline that chunks and indexes the vehicle owner's manual and retrieves the most relevant sections to include in the model's context window for each query. Offline state is detected via React Native's NetInfo API and surfaced clearly in the UI — the "Offline mode: ON" indicator reflects real network state, not a hardcoded flag.

### Challenges we ran into

Getting a quantized LLM to run acceptably fast on-device was the biggest challenge — model selection and quantization level directly affect both response latency and accuracy. Bundling the GGUF weights as a native resource in Xcode and wiring it into the React Native bridge via llama.rn took significant trial and error. Building this as a solo developer under hackathon time pressure meant making deliberate tradeoffs: iOS only for now, keyword-based RAG rather than vector embeddings, a curated set of emergency procedures rather than an exhaustive manual library.

### Accomplishments we're proud of

Apart from the technical accomplishments, I'm proud that this solves a real problem I actually lived. The five of us stranded in Nevada weren't edge cases. There are 46 million roadside breakdowns in the US every year, and a huge chunk of them happen where there's no signal. Glovebox would have gotten us back on the road in 20 minutes instead of 3 hours.

### What we learned

I learned that on-device AI is genuinely viable in 2025 — but model selection is everything. The difference between a 1B and a 3B parameter model isn't just size; it's whether the responses are actually useful under the constraints of a glove compartment emergency. Quantization level (Q4_K_M vs Q8) has a real effect on both latency and coherence, and finding that sweet spot took most of my first night. And building this solo taught me to be brutal about scope. No Android, no vector DB, no fancy UI, just one thing that works completely offline, answers car questions accurately, and launches in under three seconds. Constraints forced clarity.

### What's next

Android support via the same llama.rn bridge Vector-based semantic RAG for more accurate retrieval creating an offline network mesh for users to connect

## README (from the GitHub repository)

<div align="center">

<img src="GloveboxApp/Resources/Assets.xcassets/BrandIcon.imageset/glove_icon.png" width="96" alt="Glovebox icon">

# Glovebox

**Offline-first roadside assistance for iOS.**
Diagnose car trouble with an on-device LLM and reach cached emergency help,
built to keep working with zero or unreliable signal, at the exact moment you need it.

![iOS 17+](https://img.shields.io/badge/iOS-17%2B-black?logo=apple&logoColor=white)
![SwiftUI](https://img.shields.io/badge/UI-SwiftUI-orange?logo=swift&logoColor=white)
![On-device LLM](https://img.shields.io/badge/inference-llama.cpp%20(on--device)-4CAF6A)
![Offline-first](https://img.shields.io/badge/network-offline--first-A4D65E)
![MIT License](https://img.shields.io/badge/license-MIT-lightgrey)

Built at the **UC Berkeley AI Hackathon 2026**.

</div>

<br>

<div align="center">
<img src="docs/screens-preview.png" width="100%" alt="Glovebox screens: Welcome, Home, Diagnose, Travel Mode, Emergency">
<sub>Welcome · Home · Diagnose (RAG chat with safety caution) · Travel Mode · Emergency</sub>
</div>

> **A note on the images above:** this environment has no Xcode/Simulator installed, so
> these aren't device screen captures. They're an HTML/CSS reconstruction of the five
> screens built directly from the app's real design tokens (`GBColor`, `GBGradient`,
> `GBFont`, and the actual `BrandIcon` asset) so the layout, copy, and states shown are
> accurate to the SwiftUI source. Swap in real device screenshots when you have Xcode
> available; see [Building](#building).

<br>

## Table of contents
- [Why Glovebox](#why-glovebox)
- [Development process](#development-process)
- [Features](#features)
- [How diagnosis works (RAG + on-device LLM)](#how-diagnosis-works-rag--on-device-llm)
- [Safety design](#safety-design)
- [Travel Mode & the offline cache](#travel-mode--the-offline-cache)
- [Architecture](#architecture)
- [Project structure](#project-structure)
- [Building](#building)
- [Debugging & screenshot-driving env vars](#debugging--screenshot-driving-env-vars)
- [Current limitations](#current-limitations)
- [License](#license)

<br>

## Why Glovebox

Roadside trouble tends to happen exactly where connectivity doesn't: a canyon road, a
rural highway, a parking garage. Most "car help" apps assume you have a live connection
to reach a chatbot, a map, or a tow dispatcher. Glovebox assumes the opposite:
**every screen defines what it does with zero signal**, and does the expensive work
(model inference, manual retrieval, POI search) *before* you need it, not during.

<br>

## Development process

Built end-to-end during the **UC Berkeley AI Hackathon 2026**. I laid out the product
spec and architecture myself (the screen-by-screen flow, the safety philosophy behind
`SafetyFilter`, the requirement that every feature define its own offline/degraded
state), then deployed AI coding agents to orchestrate and execute that plan:
scaffolding the SwiftUI screens, wiring the RAG + on-device LLM pipeline, and
implementing the Travel Mode/background-caching logic against the spec, with review
and correction at each step rather than one unsupervised pass. The commit history
reflects that human-directed, agent-executed workflow.

<br>

## Features

### 🔧 Vehicle-aware offline diagnosis
A chat interface backed by **retrieval-augmented generation (RAG) running entirely
on-device**. Each vehicle has its own cached manual/issue-reference index; a question
is matched against that vehicle's cached chunks, grounded into a prompt, and answered
by a local **Llama 3.2 1B Instruct** model via `llama.cpp`: no network round-trip, no
server, no data leaving the phone. Answers are direct and actionable (no "I'm not a
mechanic" hedging), cite their source ("From your cached common-issue guide"), and are
tagged **SAFE TO DIY** when the grounding chunk supports it.

### 🛡️ Code-enforced safety filter
Every request *and* every generated response is scanned for five safety-critical
systems (brakes, airbags/SRS, high-voltage EV/hybrid battery, fuel system, and
structural/frame work) using a deterministic regex classifier (`SafetyFilter.swift`),
not a model-side prompt that could be rephrased around. See [Safety design](#safety-design)
for the reasoning and exact behavior.

### 🧭 Travel Mode: predictive offline caching
Turn it on before a trip and Glovebox quietly pre-caches roadside help (mechanics,
towing, hospitals/urgent care, fuel & EV charging, non-emergency police) along your
route using `CoreLocation` + `MKLocalSearch`. It's battery-aware (refreshes every
~3 miles of movement, not continuous GPS polling), keeps only a trailing window of
data near you (auto-evicts anything >~40 miles behind or older than an hour), and
schedules background refreshes via `BGTaskScheduler` so the cache stays warm even
when the app isn't open.

### 🚨 Always-reachable Emergency screen
Reads straight from the on-device POI cache: it **never silently requires a
network call**. Every cached entry shows a visible staleness label ("cached 6 min
ago") and is flagged amber once it's past 15 minutes old, so stale data never
masquerades as fresh. One tap to call or send a pre-filled SMS with your last known
location, plus a sticky "I need help now" action and a direct 911 dial from anywhere
in the app.

### 🚗 Garage: multi-vehicle profiles
Add multiple vehicles, switch which one is active, and re-sync a vehicle's cached
manual/issue data independently. `SwiftData` keeps everything local by default.

<br>

## How diagnosis works (RAG + on-device LLM)

```mermaid
flowchart TD
    A[User types a question] --> B[SafetyFilter.classifyInput]
    B --> C[Retriever: keyword-overlap search\nover this vehicle's cached ManualChunks]
    C --> D{Strong match?\nscore ≥ 0.18}
    D -- yes --> E[PromptBuilder grounds the\nprompt with the matched chunk]
    D -- no --> F[PromptBuilder falls back to\ngeneral automotive knowledge]
    E --> G[LlamaInference streams tokens\nvia llama.cpp, off the main thread]
    F --> G
    G --> H[SafetyFilter.classifyOutput\non the generated answer]
    H --> I[Answer rendered with source badge,\nSAFE TO DIY tag, and/or safety caution]
    G -. model missing / times out .-> J[Low-confidence fallback bubble\n→ Find a mechanic]
```

The pipeline lives across four small, single-purpose files:

| Step | File |
|---|---|
| Orchestration (safety → retrieve → prompt → generate → safety → persist) | `Chat/DiagnoseViewModel.swift` |
| Retrieval: keyword-overlap scoring over cached manual chunks (title-weighted) | `Retrieval/Retriever.swift` |
| Prompt assembly in Llama 3.x instruct chat format | `Retrieval/PromptBuilder.swift` |
| Inference: actor-isolated `llama.cpp` context, streamed off the main thread | `LLM/InferenceEngine.swift`, `LLM/LlamaContext.swift` |

A **120-second watchdog** cancels generation on the simulator's slow CPU-only path
without hanging the UI; a failed/empty/too-short answer degrades gracefully to a
"find a mechanic" fallback bubble instead of showing nothing.

<br>

## Safety design

> *"Glovebox is for drivers who may be stranded with no mechanic and no signal, so
> it does not withhold guidance. Instead, a hit surfaces a prominent 'safety-critical
> — proceed at your own risk' caution attached to the answer."* (`SafetyFilter.swift`)

This is a deliberate product decision, not an oversight: refusing to answer is only
the *safe* choice if the driver has another option. Glovebox instead:

1. **Classifies both directions.** Input is checked before generation; the generated
   *output* is checked again, so a caution still gets attached even if the risky
   topic only surfaces in the model's own steps (not the user's original phrasing).
2. **Can't be talked around.** Detection is regex-based pattern matching in Swift
   code (`SafetyFilter.swift`), evaluated the same way regardless of how the question
   is worded; there's no prompt for a model to be argued out of.
3. **Warns instead of blocking**, for exactly five system c

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 48 recognized source files, 160 KB.
- Swift (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Ruby (language) — claimed on Devpost, not found in the code
- TypeScript (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (56 of 56)

```
.gitignore
GloveboxApp/App/AppDelegate.swift
GloveboxApp/App/AppRootView.swift
GloveboxApp/App/GloveboxApp.swift
GloveboxApp/App/MainTabView.swift
GloveboxApp/Chat/DiagnoseView.swift
GloveboxApp/Chat/DiagnoseViewModel.swift
GloveboxApp/Chat/MessageBubbles.swift
GloveboxApp/Core/AppRouter.swift
GloveboxApp/Core/NetworkMonitor.swift
GloveboxApp/Core/RelativeTime.swift
GloveboxApp/Data/CachedPOI.swift
GloveboxApp/Data/ChatMessage.swift
GloveboxApp/Data/ManualChunk.swift
GloveboxApp/Data/PlaceholderManualData.swift
GloveboxApp/Data/PlaceholderPOIData.swift
GloveboxApp/Data/Vehicle.swift
GloveboxApp/Data/VehicleStore.swift
GloveboxApp/DesignSystem/GBButton.swift
GloveboxApp/DesignSystem/GBColor.swift
GloveboxApp/DesignSystem/GBEffects.swift
GloveboxApp/DesignSystem/GBFont.swift
GloveboxApp/DesignSystem/GBGradient.swift
GloveboxApp/DesignSystem/GBLayout.swift
GloveboxApp/DesignSystem/GBTextField.swift
GloveboxApp/Features/Emergency/EmergencyView.swift
GloveboxApp/Features/Garage/GarageView.swift
GloveboxApp/Features/Help/HelpPill.swift
GloveboxApp/Features/Help/HelpSheetView.swift
GloveboxApp/Features/Home/HomeView.swift
GloveboxApp/Features/Onboarding/AddVehicleFlow.swift
GloveboxApp/Features/Onboarding/AddVehicleView.swift
GloveboxApp/Features/Onboarding/SyncCoordinator.swift
GloveboxApp/Features/Onboarding/SyncView.swift
GloveboxApp/Features/Travel/TravelActivateView.swift
GloveboxApp/Features/Travel/TravelActiveView.swift
GloveboxApp/Features/Welcome/WelcomeView.swift
GloveboxApp/LLM/InferenceEngine.swift
GloveboxApp/LLM/LlamaContext.swift
GloveboxApp/LLM/ModelLocator.swift
GloveboxApp/LLM/SafetyFilter.swift
GloveboxApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
GloveboxApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
GloveboxApp/Resources/Assets.xcassets/BrandIcon.imageset/Contents.json
GloveboxApp/Resources/Assets.xcassets/Contents.json
GloveboxApp/Resources/Assets.xcassets/LaunchBackground.colorset/Contents.json
GloveboxApp/Resources/Assets.xcassets/LaunchLogo.imageset/Contents.json
GloveboxApp/Resources/Info.plist
GloveboxApp/Retrieval/PromptBuilder.swift
GloveboxApp/Retrieval/Retriever.swift
GloveboxApp/Travel/BGTaskManager.swift
GloveboxApp/Travel/LocationManager.swift
GloveboxApp/Travel/POISearch.swift
GloveboxApp/Travel/TravelService.swift
project.yml
README.md
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Safety: help with an at-your-own-risk caution instead of blocking
- Make diagnosis answers direct and grounded, not evasive
- Add 'New conversation' control to clear a diagnosis thread
- Remove design handoff folder from repo (keep locally)
- Replace React Native prototype with native SwiftUI app
- Remove Android support, ship iOS-only
- Add model bundling setup script
- Build chat UI with citations and offline indicator
- Integrate llama.rn for on-device inference
- Add keyword-based retrieval and prompt building
- Add vehicle/manual/waypoint stub data and cached-content seam
- Add iOS native project
- Add Android native project
- Scaffold React Native bare TypeScript project

## Key source files (fetched from GitHub, selected and truncated for size)

### project.yml

```yaml
name: Glovebox
options:
  bundleIdPrefix: com.glovebox
  deploymentTarget:
    iOS: "17.0"
  createIntermediateGroups: true
  groupSortPosition: top

settings:
  base:
    MARKETING_VERSION: "1.0"
    CURRENT_PROJECT_VERSION: "1"
    SWIFT_VERSION: "5.0"
    TARGETED_DEVICE_FAMILY: "1"
    DEVELOPMENT_TEAM: ""
    CODE_SIGN_STYLE: Automatic
    ENABLE_USER_SCRIPT_SANDBOXING: "NO"

targets:
  Glovebox:
    type: application
    platform: iOS
    sources:
      - path: GloveboxApp
        excludes:
          - "Resources/Info.plist"
      # On-device LLM weights bundled as a resource (used as-is). Path is resolved
      # at runtime via ModelLocator, never hardcoded in inference code.
      - path: Models/Llama-3.2-1B-Instruct-Q4_K_M.gguf
        buildPhase: resources
    dependencies:
      # Official prebuilt llama.cpp xcframework (b9748). Dynamic framework → embed.
      - framework: Vendor/llama.xcframework
        embed: true
        codeSign: true
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.glovebox.app
        PRODUCT_NAME: Glovebox
        INFOPLIST_FILE: GloveboxApp/Resources/Info.plist
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
        ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
        ENABLE_PREVIEWS: "YES"
        SWIFT_EMIT_LOC_STRINGS: "YES"

```

### GloveboxApp/App/AppDelegate.swift

```swift
import UIKit

/// BGTaskScheduler identifiers must be registered before launch completes.
final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        BGTaskManager.register()
        return true
    }
}

```

### GloveboxApp/Core/RelativeTime.swift

```swift
import Foundation

enum RelativeTime {
    /// Compact "synced" phrasing: "just now", "2 min ago", "3 hr ago", "2 days ago".
    static func short(_ date: Date, now: Date = Date()) -> String {
        let seconds = max(0, now.timeIntervalSince(date))
        switch seconds {
        case ..<90:            return "just now"
        case ..<3600:          return "\(Int(seconds / 60)) min ago"
        case ..<86_400:        return "\(Int(seconds / 3600)) hr ago"
        default:               return "\(Int(seconds / 86_400)) days ago"
        }
    }
}

```

### GloveboxApp/Core/AppRouter.swift

```swift
import SwiftUI
import Observation

/// Cross-cutting presentation state so the Help sheet and the always-reachable
/// Emergency screen can be opened from anywhere (Help pill, Home, the Phase 3
/// safety-block branch, Travel Mode), regardless of the selected tab.
@Observable
final class AppRouter {
    var helpPresented = false
    var emergencyPresented = false

    func openHelp() { helpPresented = true }
    func closeHelp() { helpPresented = false }

    func openEmergency() {
        helpPresented = false
        emergencyPresented = true
    }
    func closeEmergency() { emergencyPresented = false }
}

```

### GloveboxApp/DesignSystem/GBLayout.swift

```swift
import SwiftUI

/// Spacing scale: 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64
enum GBSpace {
    static let xxs: CGFloat = 4
    static let xs:  CGFloat = 8
    static let sm:  CGFloat = 12
    static let md:  CGFloat = 16
    static let lg:  CGFloat = 24
    static let xl:  CGFloat = 32
    static let xxl: CGFloat = 48
    static let xxxl: CGFloat = 64
}

/// Corner radii: small 12–14, medium 16–18, large 20–22, app icon 28.
enum GBRadius {
    static let input:  CGFloat = 14   // form fields
    static let small:  CGFloat = 13
    static let button: CGFloat = 16
    static let card:   CGFloat = 18
    static let large:  CGFloat = 20
    static let xLarge: CGFloat = 22
    static let icon:   CGFloat = 28   // app icon / hero glyph
    static let pill:   CGFloat = 24
}

enum GBMetrics {
    static let minTouch: CGFloat = 44
}

```

### GloveboxApp/Core/NetworkMonitor.swift

```swift
import Foundation
import Network
import Observation

/// Real connectivity signal from `NWPathMonitor`. Drives offline banners, sync
/// availability, and emergency-from-cache. Replaces the prototype's demo toggle.
@Observable
final class NetworkMonitor {
    private(set) var isOnline: Bool = true

    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "com.glovebox.networkmonitor")

    init() {
        #if DEBUG
        // Dev-only seam for verifying the offline UI on a simulator that is
        // always online. Not a shipped affordance.
        if ProcessInfo.processInfo.environment["GB_FORCE_OFFLINE"] == "1" {
            isOnline = false
            return
        }
        #endif
        monitor.pathUpdateHandler = { [weak self] path in
            let online = path.status == .satisfied
            DispatchQueue.main.async { self?.isOnline = online }
        }
        monitor.start(queue: queue)
    }

    deinit { monitor.cancel() }
}

```

### GloveboxApp/Data/ChatMessage.swift

```swift
import Foundation
import SwiftData

/// A diagnosis conversation message, persisted **per vehicle** (Phase 3 fleshes
/// out generation; the model is defined here so the SwiftData schema is stable).
@Model
final class ChatMessage {
    enum Role: String, Codable {
        case user, bot, block, fallback
    }

    @Attribute(.unique) var id: UUID
    var roleRaw: String
    var text: String
    var source: String?      // e.g. "From your cached owner's manual"
    var safeForDIY: Bool
    var blockedTopic: String? // for safety-block messages, e.g. "Brake"
    var createdAt: Date
    var vehicle: Vehicle?

    var role: Role { Role(rawValue: roleRaw) ?? .bot }

    init(role: Role,
         text: String,
         source: String? = nil,
         safeForDIY: Bool = false,
         blockedTopic: String? = nil,
         vehicle: Vehicle? = nil) {
        self.id = UUID()
        self.roleRaw = role.rawValue
        self.text = text
        self.source = source
        self.safeForDIY = safeForDIY
        self.blockedTopic = blockedTopic
        self.createdAt = Date()
        self.vehicle = vehicle
    }
}

```

### GloveboxApp/Data/ManualChunk.swift

```swift
import Foundation
import SwiftData

/// A unit of cached manual / issue-reference text for a vehicle. These are what
/// the local vector index retrieves over (RAG) in Phase 3. Stored on-device.
@Model
final class ManualChunk {
    @Attribute(.unique) var id: UUID

    /// One of the cached sections shown on the Sync screen, e.g.
    /// "Owner's manual", "Common issues & fixes", "Warning-light meanings",
    /// "Fluids & capacities", "Torque specs".
    var section: String
    var title: String
    var text: String

    /// Each issue is tagged safe-for-DIY or not, per the spec.
    var safeForDIY: Bool

    /// TRUE while we have no real manual data source wired up. Surfaced so we
    /// never silently present placeholder data as real manufacturer content.
    var isPlaceholder: Bool

    var vehicle: Vehicle?

    init(section: String,
         title: String,
         text: String,
         safeForDIY: Bool,
         isPlaceholder: Bool = true,
         vehicle: Vehicle? = nil) {
        self.id = UUID()
        self.section = section
        self.title = title
        self.text = text
        self.safeForDIY = safeForDIY
        self.isPlaceholder = isPlaceholder
        self.vehicle = vehicle
    }
}

```

### GloveboxApp/App/GloveboxApp.swift

```swift
import SwiftUI
import SwiftData

@main
struct GloveboxApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    @Environment(\.scenePhase) private var scenePhase

    @State private var network = NetworkMonitor()
    @State private var router = AppRouter()
    @State private var travel: TravelService
    private let container: ModelContainer

    init() {
        let container = try! ModelContainer(
            for: Vehicle.self, ManualChunk.self, ChatMessage.self, CachedPOI.self)
        self.container = container
        _travel = State(initialValue: TravelService(context: container.mainContext))
    }

    var body: some Scene {
        WindowGroup {
            AppRootView()
                .preferredColorScheme(.dark)
                .tint(GBColor.statusLime)
                .environment(network)
                .environment(router)
                .environment(travel)
                .onAppear {
                    BGTaskManager.onRefresh = { [travel] in await travel.backgroundRefresh() }
                }
        }
        .modelContainer(container)
        .onChange(of: scenePhase) { _, phase in
            if phase == .background { BGTaskManager.schedule() }
        }
    }
}

```

### GloveboxApp/Data/VehicleStore.swift

```swift
import Foundation
import SwiftData

/// Helpers for vehicle persistence + the single-active-vehicle invariant.
enum VehicleStore {

    static func activeVehicle(in context: ModelContext) -> Vehicle? {
        let descriptor = FetchDescriptor<Vehicle>(predicate: #Predicate { $0.isActive })
        return (try? context.fetch(descriptor))?.first
    }

    static func allVehicles(in context: ModelContext) -> [Vehicle] {
        let descriptor = FetchDescriptor<Vehicle>(sortBy: [SortDescriptor(\.createdAt)])
        return (try? context.fetch(descriptor)) ?? []
    }

    /// Create a vehicle, insert it, and make it the active one.
    @discardableResult
    static func add(year: String, make: String, model: String, trim: String,
                    in context: ModelContext) -> Vehicle {
        let vehicle = Vehicle(year: year, make: make, model: model, trim: trim)
        context.insert(vehicle)
        setActive(vehicle, in: context)
        return vehicle
    }

    /// Make `vehicle` the sole active vehicle.
    static func setActive(_ vehicle: Vehicle, in context: ModelContext) {
        for other in allVehicles(in: context) where other.isActive && other.id != vehicle.id {
            other.isActive = false
        }
        vehicle.isActive = true
        try? context.save()
    }
}

```

[37 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]