# Magnum Opus v0.4 — GTD Tasks & Unified Triage

**Goal:** Add local task management (VTODO files) and GTD triage (defer, file, discard) to the email client. Emails and tasks are the same thing from the user's perspective — a unified list of items to process. No CalDAV sync yet (v0.6); tasks are local VTODO files with SQLite cache.

**Builds on:** v0.3 functional email client (IMAP sync, SMTP compose, triage actions, ActionQueue).

---

## Core Concept

Every item in the system — whether it arrived as an email or was created as a standalone task — is something the user must process. The user sees one unified stream, not "emails" and "tasks" as separate concepts. Triage actions work identically on both.

**Two backing stores, one UX:**

| Item type | Storage | Source |
|-----------|---------|--------|
| Email | IMAP message (existing) | Arrives via IMAP sync |
| Task | VTODO `.ics` file + SQLite cache | Created by user |

An email can spawn a task that belongs to it (linked via `ATTACH:mid:<message-id>` in the VTODO). The thread detail view shows both emails and linked tasks in one timeline.

---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────┐
│                      SwiftUI Apps                            │
│                                                              │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐             │
│  │DeferPicker │  │TaskEditView│  │ SidebarView │             │
│  └─────┬──────┘  └─────┬──────┘  └─────┬──────┘             │
│        └────────────────┼───────────────┘                    │
│                   ┌─────┴──────┐                             │
│                   │ ViewModels │                             │
│                   └─────┬──────┘                             │
├─────────────────────────┼────────────────────────────────────┤
│              MagnumOpusCore (Swift Package)                   │
│                   ┌─────┴──────┐                             │
│                   │ SyncEngine │                             │
│                   │            │                             │
│         ┌─────────┼─────────┐  │                             │
│         │   ActionQueue     │  │                             │
│         │  + DeferralCheck  │  │                             │
│         └──┬──────────┬─────┘  │                             │
│   ┌────────┘          └──────┐ │                             │
│   │ SMTPClient │  │IMAPClient│ │  ┌──────────┐  ┌─────────┐ │
│   └────────────┘  └─────────┘ │  │ MailStore │  │TaskStore│ │
│                               │  │(GRDB+FTS5)│  │(VTODO  )│ │
│                               │  └───────────┘  └─────────┘ │
└───────────────────────────────┼─────────────────────────────┘
                                │
              ┌─────────────────┼──────────────┐
              │                 │              │
              ▼                 ▼              ▼
        ┌──────────┐    ┌──────────┐   ┌─────────────┐
        │   IMAP   │    │   SMTP   │   │  ~/.../tasks │
        │  Server  │    │  Server  │   │  (VTODO .ics)│
        └──────────┘    └──────────┘   └─────────────┘
```

---

## Data Model

### Unified Item

The UI operates on a union type that wraps both emails and tasks:

```swift
public enum ItemSummary: Sendable, Identifiable {
    case email(MessageSummary)
    case task(TaskSummary)

    var id: String { ... }
    var title: String { ... }       // subject or summary
    var date: Date? { ... }         // message date or created date
    var isRead: Bool { ... }
    var deferral: Deferral? { ... }
    var labels: [LabelInfo] { ... }
}
```

### TaskSummary (new)

```swift
public struct TaskSummary: Sendable, Identifiable {
    public var id: String
    public var accountId: String
    public var summary: String
    public var description: String?
    public var status: TaskStatus
    public var priority: Int          // 0 = undefined, 1 = highest, 9 = lowest
    public var dueDate: Date?
    public var deferUntil: Date?      // DTSTART in VTODO
    public var createdAt: Date
    public var linkedMessageId: String?  // mid: reference to email
    public var categories: [String]
}

public enum TaskStatus: String, Sendable, Codable {
    case needsAction = "NEEDS-ACTION"
    case inProcess   = "IN-PROCESS"
    case completed   = "COMPLETED"
    case cancelled   = "CANCELLED"
}
```

### Labels

Labels are user-created tags attachable to any item. A label with `isProject = true` appears in the Projects sidebar perspective.

```swift
public struct LabelInfo: Sendable, Identifiable {
    public var id: String
    public var name: String
    public var isProject: Bool
    public var color: String?   // hex color, optional
}
```

---

## Schema Changes

### Migration v3_task: Task cache table

```sql
CREATE TABLE task (
    id          TEXT PRIMARY KEY,
    accountId   TEXT NOT NULL REFERENCES account(id),
    summary     TEXT NOT NULL,
    description TEXT,
    status      TEXT NOT NULL DEFAULT 'NEEDS-ACTION',
    priority    INTEGER NOT NULL DEFAULT 0,
    dueDate     TEXT,
    deferUntil  TEXT,
    createdAt   TEXT NOT NULL,
    updatedAt   TEXT NOT NULL,
    filePath    TEXT NOT NULL,
    linkedMessageId TEXT,
    isSomeday   INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_task_status ON task(status);
CREATE INDEX idx_task_deferUntil ON task(deferUntil);
CREATE INDEX idx_task_dueDate ON task(dueDate);
CREATE INDEX idx_task_linkedMessageId ON task(linkedMessageId);
```

`updatedAt` is populated from VTODO `LAST-MODIFIED` during parsing and updated alongside it during writes.

### Migration v3_label: Labels and item-label junction

```sql
CREATE TABLE label (
    id          TEXT PRIMARY KEY,
    accountId   TEXT NOT NULL REFERENCES account(id),
    name        TEXT NOT NULL,
    isProject   INTEGER NOT NULL DEFAULT 0,
    color       TEXT
);
CREATE UNIQUE INDEX idx_label_name ON label(accountId, name);

CREATE TABLE itemLabel (
    labelId     TEXT NOT NULL REFERENCES label(id) ON DELETE CASCADE,
    itemType    TEXT NOT NULL,   -- "email" or "task"
    itemId      TEXT NOT NULL,
    PRIMARY KEY (labelId, itemType, itemId)
);
```

### Migration v3_deferral: Email deferral tracking

The `deferral` table is for **emails only**. Tasks use `task.deferUntil` and `task.isSomeday` (derived from the VTODO file, survives cache rebuilds).

```sql
CREATE TABLE deferral (
    id              TEXT PRIMARY KEY,
    messageId       TEXT NOT NULL REFERENCES message(id) ON DELETE CASCADE,
    deferUntil      TEXT,           -- ISO8601 date, NULL = someday
    originalMailbox TEXT,           -- restore to this mailbox on resurface
    createdAt       TEXT NOT NULL,
    UNIQUE(messageId)
);
CREATE INDEX idx_deferral_until ON deferral(deferUntil);
```

Add `isSomeday` to the task table (in v3_task migration):

```sql
-- Added to the task CREATE TABLE above:
    isSomeday   INTEGER NOT NULL DEFAULT 0,
```

---

## TaskStore Module

New module in MagnumOpusCore. Manages VTODO files and their SQLite cache.

### VTODO File Format

Standard iCalendar, minimal properties for v0.4:

```
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//MagnumOpus//v0.4//EN
BEGIN:VTODO
UID:<uuid>
DTSTAMP:<iso8601>
CREATED:<iso8601>
LAST-MODIFIED:<iso8601>
SUMMARY:Task title
DESCRIPTION:Optional body text
STATUS:NEEDS-ACTION
PRIORITY:0
DTSTART:<defer-until-date>
DUE:<due-date>
CATEGORIES:ProjectName,AnotherLabel
ATTACH:mid:<message-id>
END:VTODO
END:VCALENDAR
```

### File Storage

Tasks stored at: `~/Library/Application Support/MagnumOpus/<accountId>/tasks/<taskId>.ics`

The directory is created on first task creation. When CalDAV arrives (v0.6), vdirsyncer syncs this directory with the server.

### Public API

```swift
public struct TaskStore: Sendable {
    /// Directory where .ics files live
    private let taskDirectory: URL

    // File operations
    public func writeTask(_ task: TaskRecord) throws    // write .ics + update cache
    public func deleteTask(id: String) throws           // delete .ics + remove from cache
    public func rebuildCache() throws                   // scan directory, rebuild SQLite

    // VTODO parsing
    public static func parseVTODO(_ icsContent: String) -> TaskRecord?
    public static func formatVTODO(_ task: TaskRecord) -> String
}
```

### VTODO Parser

Simple line-based parser (no external dependency). Handles:
- Unfolding (lines starting with space/tab are continuations)
- Property extraction: `SUMMARY`, `DESCRIPTION`, `STATUS`, `PRIORITY`, `DTSTART`, `DUE`, `CATEGORIES`, `ATTACH`, `UID`, `DTSTAMP`, `CREATED`, `LAST-MODIFIED`
- Date parsing: `DTSTART;VALUE=DATE:20260315` and `DTSTART:20260315T090000Z` formats

### VTODO Formatter

Writes standard iCalendar from a TaskRecord. Line folding at 75 octets per RFC 5545.

---

## Deferral System

### How Deferral Works

**Deferring an email:**
1. Create `deferral` record (itemType="email", deferUntil=date, originalMailbox=current)
2. Email disappears from Inbox perspective (filtered by query)
3. Email stays in IMAP mailbox — no server-side change
4. When deferUntil ≤ now: delete deferral record → email reappears in Today

**Deferring a task:**
1. Update VTODO `DTSTART` property, rewrite `.ics` file
2. Update `task.deferUntil` in SQLite cache
3. Task disappears from Inbox (query checks `task.deferUntil`), appears in Today when date arrives

Note: Tasks do NOT use the `deferral` table. The `task.deferUntil` column is authoritative — it's derived from the VTODO file (source of truth) and survives cache rebuilds. The `deferral` table exists only for emails, which have no native defer field.

**Deferring to Someday:**
- For emails: create deferral record with `deferUntil = NULL`
- For tasks: set a `someday` flag (use `DTSTART` with a sentinel value, or a dedicated `X-MAGNUM-SOMEDAY:TRUE` property in the VTODO, plus `task.isSomeday` in cache)
- Item only appears in Someday perspective, never resurfaces automatically
- User manually pulls it back by clearing the deferral/someday flag

### Resurfacing

On each sync cycle (and on app launch), the SyncEngine checks both sources:

```sql
-- Emails
SELECT * FROM deferral WHERE deferUntil IS NOT NULL AND deferUntil <= datetime('now')

-- Tasks
SELECT * FROM task WHERE deferUntil IS NOT NULL AND deferUntil <= datetime('now') AND isSomeday = 0
```

For each matched email deferral:
- Delete the deferral record → email reappears in Inbox

For each matched task:
- Clear `deferUntil` in the task record and VTODO file → task reappears in Inbox

**Resurfaced items go to Inbox, not Today.** The Today perspective shows items with `dueDate = today` (a deadline, not a deferral). This is the GTD distinction: a deferred item was "hidden until ready to process" — when it resurfaces, it goes back to the inbox for triage, just like a new email would.

This is a simple poll, not a timer. Items may resurface slightly late (by up to the sync interval, default 5 minutes). Acceptable for v0.4.

---

## Sidebar Perspectives

### Perspective Definitions

| Perspective | Content | Query Logic |
|-------------|---------|-------------|
| Inbox | Unprocessed items | Emails in INBOX (no deferral) + tasks with status=NEEDS-ACTION, deferUntil is NULL, isSomeday=0 |
| Today | Due today | Emails/tasks with dueDate = today |
| Upcoming | Future dated items | Items with dueDate > today, ordered by date |
| Projects | Grouped by project | Items with a label where isProject=true, grouped by label name |
| Someday | Parked indefinitely | Emails with deferral where deferUntil IS NULL + tasks with isSomeday=1 |
| Archive | Done/filed | Emails in Archive folder + tasks with status=COMPLETED or CANCELLED |

### Unified Query

Each perspective runs a query that unions emails and tasks:

```sql
-- Example: Inbox perspective
SELECT 'email' as itemType, id, subject as title, date, ...
FROM message WHERE mailboxId = :inboxId
  AND id NOT IN (SELECT messageId FROM deferral)
UNION ALL
SELECT 'task' as itemType, id, summary as title, createdAt as date, ...
FROM task WHERE status = 'NEEDS-ACTION'
  AND deferUntil IS NULL AND isSomeday = 0
ORDER BY date DESC
```

The unified query returns `ItemSummary` values. GRDB ValueObservation on both tables drives reactive UI updates.

---

## Triage Actions

### Unified Triage (works on any item)

| Action | Key | Email Effect | Task Effect |
|--------|-----|-------------|-------------|
| Defer to date | `d` | Create deferral record, hide from inbox | Set DTSTART in VTODO + deferral record |
| Defer to Someday | `⇧D` | Create deferral (deferUntil=NULL) | Same |
| File to project | `p` | Attach project label | Set CATEGORIES in VTODO + label |
| Discard | `⌫` | Archive email (move to Archive folder) | Set STATUS=CANCELLED in VTODO |
| Complete | `⌘⏎` | Archive email | Set STATUS=COMPLETED in VTODO |

### Existing v0.3 Email Triage (unchanged)

| Action | Key | Effect |
|--------|-----|--------|
| Archive | `e` | IMAP MOVE to Archive |
| Delete | `⌫` | IMAP MOVE to Trash |
| Flag/unflag | `s` | IMAP STORE ±\Flagged |
| Read/unread | `⇧⌘U` | IMAP STORE ±\Seen |
| Move to folder | `⇧⌘M` | IMAP MOVE |

**v0.3 → v0.4 behavioral change for `⌫`:** In v0.3, `⌫` always meant "delete" (move to Trash). In v0.4, the behavior depends on the active perspective:

- **GTD perspectives** (Inbox, Today, Upcoming, Projects, Someday): `⌫` means "discard" — archive the email or cancel the task. This matches GTD: discarding isn't destroying, it's removing from the action queue.
- **Mailbox folder views** (when browsing a specific IMAP folder): `⌫` retains v0.3 behavior — "delete" (move to Trash).

This is a deliberate change. The `e` key (archive) remains available in all views as an alternative.

### Defer Picker

Pressing `d` opens a compact popover/sheet with:
- **Tomorrow** — quick button
- **Next Week** (next Monday) — quick button
- **Pick Date** — date picker for custom date
- **Someday** — equivalent to `⇧D`

Selecting any option immediately defers and dismisses the picker.

### Create Task

`⌘⇧N` opens a minimal form:
- Title (required)
- Body (optional, TextEditor)
- Due date (optional, date picker)

Creates a VTODO file, updates cache. Task appears in Inbox.

### Link Task to Email

When viewing an email thread, `t` creates a new task pre-linked to the thread:
- Title pre-filled with email subject
- VTODO gets `ATTACH:mid:<message-id>`
- Task appears in Inbox AND in the thread's detail view

---

## Thread Detail Integration

The thread detail view currently shows messages in chronological order. v0.4 extends this to also show linked tasks:

```
Thread: "Q1 Planning"
├── Email from Alice (Jan 5)
├── Your reply (Jan 6)
├── [Task] Draft Q1 budget — NEEDS-ACTION
├── Email from Bob (Jan 8)
└── [Task] Review Bob's proposal — COMPLETED ✓
```

Linked tasks are queried via:
```sql
SELECT * FROM task WHERE linkedMessageId IN (
    SELECT messageId FROM message
    JOIN threadMessage ON threadMessage.messageId = message.id
    WHERE threadMessage.threadId = :threadId
)
```

Tasks render with a distinct visual style (checkbox icon, status badge) but are inline in the timeline, sorted by `createdAt`.

**Orphaned links:** If a linked email is deleted from the server during IMAP sync, the task's `linkedMessageId` becomes orphaned. The task remains visible as a standalone item — it simply no longer appears in any thread detail view. The `ATTACH:mid:` property stays in the VTODO file (harmless). No user notification needed.

---

## Label Management

### Creating Labels

Labels are created inline — when filing to a project (`p`), the user sees existing labels and can type a new name. No separate label management UI in v0.4.

### Label Picker

Pressing `p` opens a compact popover:
- Searchable list of existing project labels
- Type-to-create: entering a name that doesn't exist offers "Create project: <name>"
- Selecting a label attaches it and dismisses the picker

### Storage

Labels are local-only in v0.4.

- **Task labels** are written to the VTODO `CATEGORIES` property, so they survive a cache rebuild. During `rebuildCache()`, the TaskStore parses `CATEGORIES` from each VTODO file and populates the `itemLabel` junction table.
- **Email labels** are stored only in the `itemLabel` junction table in SQLite. They do NOT survive a cache rebuild. This is a known v0.4 limitation — emails have no standard mechanism for custom labels outside of IMAP keywords (which not all servers support). Acceptable for v0.4; can be addressed later with IMAP keywords or a local manifest file.

---

## Testing Strategy

### New Test Targets

- **TaskStoreTests** — VTODO parsing/formatting, file I/O, cache rebuild
- Extended **MailStoreTests** — label CRUD, deferral CRUD, unified queries
- Extended **SyncEngineTests** — deferral resurfacing on sync

### Key Test Scenarios

1. Parse standard VTODO → correct TaskRecord fields
2. Format TaskRecord → valid iCalendar output, round-trip fidelity
3. Create task → .ics file written, cache updated, appears in Inbox query
4. Defer email → deferral record created, email hidden from Inbox, appears in Today when date arrives
5. Defer task → VTODO DTSTART updated, deferral record created, same visibility behavior
6. Defer to Someday → appears only in Someday perspective
7. Resurface check → items with deferUntil ≤ now have deferral removed
8. File to project → label created/attached, item appears in Projects perspective
9. Link task to email → task appears in thread detail view
10. Complete task → STATUS=COMPLETED in file, appears in Archive
11. Cache rebuild → delete cache, rebuild from .ics files, all tasks restored

---

## Dependencies

No new external dependencies. v0.4 uses the same packages as v0.3:

| Package | Purpose |
|---------|---------|
| swift-nio-imap | IMAP protocol (existing) |
| swift-nio | Networking (existing) |
| swift-nio-ssl | TLS (existing) |
| GRDB.swift | SQLite, FTS5, ValueObservation (existing) |

VTODO parsing is built in-house (simple line-based format, ~200 lines).

---

## v0.4 Scope

### In

- TaskStore module (VTODO parser/formatter, file I/O, SQLite cache)
- Standalone task creation (⌘⇧N)
- Task linked to email thread (t key from thread view)
- Unified item model (ItemSummary wrapping email or task)
- GTD triage: defer to date, defer to someday, file to project, discard, complete
- Defer picker (tomorrow, next week, pick date, someday)
- Deferral resurfacing on sync cycle
- Labels with project flag
- Label picker for filing
- Sidebar perspectives: Inbox, Today, Upcoming, Projects, Someday, Archive
- Thread detail shows linked tasks inline
- Schema migrations for task, label, itemLabel, deferral tables

### Out (Deferred)

- CalDAV sync (v0.6)
- Task dependencies / blocking (v0.6)
- Delegation / waiting loop (v0.5)
- Forecast / calendar view (v0.6, needs VEVENT)
- Tags / GTD contexts sidebar (v0.5+)
- Defer to time-of-day / notifications (v0.5+)
- Task search in FTS5 (v0.5)
- Rich text task descriptions (future)
- Multiple accounts (future)
