diff --git a/CLAUDE.md b/CLAUDE.md index 98f88a6d..51279053 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,20 @@ Red → Green → Refactor → Commit. One cycle per commit. For bugs: write a f After adding a Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit. Use Mermaid for diagrams (not ASCII). Exception: spatial wireframe layouts. +## Architecture Decision Records (ADRs) + +ADRs live in `docs/adr/`. Read `docs/adr/README.md` for the format. + +**Before making any architectural choice** (storage strategy, new dependency, platform approach, data model change): check `docs/adr/` for an existing decision that covers it. If one exists and your work aligns with it — proceed. If your work **supersedes** an existing ADR, **do not modify the existing file** — instead: +1. Update the existing ADR: set `status: superseded` and `superseded_by: "NNNN"` +2. Create a new ADR with the updated decision + +**After completing a task**: if you made a significant architectural decision that isn't already documented in `docs/adr/`, create a new ADR in the same commit. A decision is "significant" if it affects: data storage, platform support, major dependencies, core abstractions, or cross-cutting concerns. + +ADRs use Laputa note format — YAML frontmatter with `type: ADR`, `status: active|superseded|proposed`, `date: YYYY-MM-DD`. + +**Do not create ADRs for**: implementation details, UI styling choices, refactoring decisions, or anything that doesn't affect how future code should be written. + ## Design File (UI tasks) 1. Open `ui-design.pen` first — study existing frames for visual language. diff --git a/docs/adr/0001-tauri-react-stack.md b/docs/adr/0001-tauri-react-stack.md new file mode 100644 index 00000000..c5377261 --- /dev/null +++ b/docs/adr/0001-tauri-react-stack.md @@ -0,0 +1,31 @@ +--- +type: ADR +id: "0001" +title: "Tauri v2 + React as application stack" +status: active +date: 2026-02-14 +--- + +## Context + +Laputa is a desktop app for macOS (with iPad as a future target) that reads and writes a vault of markdown files. The app needs a native feel, filesystem access, git integration, and a rich text editor. A single developer (with AI assistance) is building it. + +## Decision + +Use **Tauri v2** (Rust backend + WebView frontend) with **React + TypeScript** for the UI, **BlockNote** for the editor, and **Vitest + Playwright** for testing. + +## Alternatives considered + +- **Electron**: heavier runtime (~150MB), slower, but more mature ecosystem. Rejected — Tauri is lighter and has better native integration. +- **SwiftUI**: best native macOS/iOS experience, but locks to Apple platforms only, no code sharing with a potential web version, and requires rewriting the entire UI. Rejected for the initial version — revisited in ADR-0005. +- **Flutter**: cross-platform but WebView-based editor would have been poor; Dart ecosystem is thin for markdown tooling. +- **Pure web app**: no filesystem access, no git, would require a backend server. Rejected — offline-first is a core principle. + +## Consequences + +- React frontend can be shared with a future web version +- Rust backend provides safe, fast filesystem/git operations +- Tauri v2 supports iOS (beta) — see ADR-0005 for iPad strategy +- CodeScene code health monitoring applies to both Rust and TypeScript code +- Claude Code can work on both layers without context switching +- Triggers re-evaluation if: Tauri iOS proves unstable for production, or if SwiftUI becomes the primary target platform diff --git a/docs/adr/0002-filesystem-source-of-truth.md b/docs/adr/0002-filesystem-source-of-truth.md new file mode 100644 index 00000000..15f5db6c --- /dev/null +++ b/docs/adr/0002-filesystem-source-of-truth.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0002" +title: "Filesystem as the single source of truth" +status: active +date: 2026-02-14 +--- + +## Context + +Laputa needs a persistence model. The core question: does the app own the data, or does the filesystem? This affects sync, conflict resolution, offline support, portability, and long-term trust with users. + +## Decision + +**The vault is the source of truth.** The app never owns the data — it only reads and writes `.md` files. All cache, React state, and in-memory representations are derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins. + +## Alternatives considered + +- **Database-first (SQLite)**: faster queries, easier relationships. Rejected — creates lock-in, makes files unreadable outside the app, complicates sync. +- **Cloud-first (proprietary sync)**: easier multi-device. Rejected — zero lock-in is a core principle; git handles sync. +- **Hybrid (DB + files)**: DB as primary, files as export. Rejected — two sources of truth always diverge. + +## Consequences + +- Notes are plain markdown files, readable and editable by any text editor +- Git provides history, sync, and collaboration for free +- Vault can be opened/edited externally without app corruption +- App rebuilds cache on startup — acceptable cost for integrity guarantees +- No "save" button needed — autosave writes to disk immediately +- Triggers re-evaluation if: vault size grows to millions of files and filesystem scanning becomes a bottleneck diff --git a/docs/adr/0003-single-note-model.md b/docs/adr/0003-single-note-model.md new file mode 100644 index 00000000..d0b368af --- /dev/null +++ b/docs/adr/0003-single-note-model.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0003" +title: "Single note open at a time (no tabs)" +status: active +date: 2026-03-24 +--- + +## Context + +The app originally had a tab bar allowing multiple notes to be open simultaneously (similar to a browser or code editor). After building and shipping it, the tab model was found to add significant UI complexity, state management overhead, and confusion — without a proportional benefit for a notes app. + +## Decision + +**Remove the tab bar. Only one note is open at a time.** Navigation history (Back/Forward with Cmd+[/]) replaces tabs for moving between recently visited notes. Closed tab history and `useTabManagement` are removed. + +## Alternatives considered + +- **Keep tabs**: familiar UX, allows comparing notes side by side. Rejected — adds ~2000 lines of complexity, confusing state (which tab is "active"?), and breaks the "editor is sacred" principle. +- **Tabs + single-note toggle**: configurable per user. Rejected — doubles the state surface and testing burden. +- **Split pane (two notes at once)**: useful for reference. Deferred — can be added later without tabs, via a dedicated split layout. + +## Consequences + +- Removes ~2000 lines of code (`TabBar`, `useClosedTabHistory`, `useEditorTabSwap`, `tabLayout`) +- `handleSelectNote` replaces the current note instead of adding a tab +- Cmd+W (close tab) and Cmd+Shift+T (reopen closed tab) removed from shortcuts +- Back/Forward navigation (Cmd+[/Cmd+]) preserves history without tab state +- Significant simplification of `App.tsx` and editor state +- Triggers re-evaluation if: multi-note workflows become a top user request diff --git a/docs/adr/0004-vault-vs-app-settings-storage.md b/docs/adr/0004-vault-vs-app-settings-storage.md new file mode 100644 index 00000000..40a1ea50 --- /dev/null +++ b/docs/adr/0004-vault-vs-app-settings-storage.md @@ -0,0 +1,42 @@ +--- +type: ADR +id: "0004" +title: "Vault vs app settings for state storage" +status: active +date: 2026-03-24 +--- + +## Context + +As features were added, there was recurring ambiguity about where to persist configuration and state: in the vault (as frontmatter in `.md` files) or in app settings (`~/.config/com.laputa.app/settings.json` / localStorage). Without a clear rule, some decisions were inconsistent. + +## Decision + +**Ask: "Would the user want this to follow the vault across all their installations?"** + +- If **yes** → store in the vault (as frontmatter in the relevant `.md` file, using the `_` convention for system properties) +- If **no** → store in app settings + +Examples: +| Data | Storage | Reason | +|------|---------|--------| +| Note type icon (`_icon`) | Vault frontmatter | Follows the vault everywhere | +| Note type color (`_color`) | Vault frontmatter | Follows the vault everywhere | +| Note sort preference | Vault frontmatter (type file) | Per-vault, consistent across devices | +| API keys (Anthropic, OpenAI) | App settings | Installation-specific | +| GitHub token | App settings | Installation-specific | +| Window size / zoom | App settings | Device-specific | +| Editor zoom level | App settings | Device-specific | +| Telemetry consent | App settings | Installation-specific | + +## Alternatives considered + +- **Everything in localStorage**: simple, but breaks cross-device sync for vault-level config. +- **Everything in vault**: pure, but makes device-specific settings (zoom, window size) propagate to all devices — confusing. + +## Consequences + +- Config that "belongs to a note or type" lives in frontmatter — readable/diffable in git +- The `_` prefix convention (see ABSTRACTIONS.md) distinguishes system properties from user properties +- App rebuilds from vault state on open — no stale config files to manage +- Triggers re-evaluation if: vault files become too polluted with system frontmatter properties diff --git a/docs/adr/0005-tauri-ios-for-ipad.md b/docs/adr/0005-tauri-ios-for-ipad.md new file mode 100644 index 00000000..24a47543 --- /dev/null +++ b/docs/adr/0005-tauri-ios-for-ipad.md @@ -0,0 +1,38 @@ +--- +type: ADR +id: "0005" +title: "Tauri v2 iOS for iPad support (vs SwiftUI rewrite)" +status: active +date: 2026-03-27 +--- + +## Context + +Laputa runs on macOS via Tauri v2. The goal is to also support iPad without changing the stack or redesigning the app from scratch. The core question: extend the existing stack to iOS, or rewrite in SwiftUI for a fully native experience? + +## Decision + +**Use Tauri v2 iOS (beta) for the iPad prototype.** The React frontend stays identical. The Rust backend compiles for iOS with `#[cfg(desktop)]` / `#[cfg(mobile)]` guards for platform-specific features. Desktop-only features (git CLI, macOS menu bar, MCP server, Claude CLI) are stubbed or skipped on mobile. + +The prototype (`feat: add iPad/iOS prototype via Tauri v2 mobile target`, build `b492`) successfully builds and runs on iPad Pro 13" simulator (iOS 18.3.1). + +## Alternatives considered + +- **SwiftUI rewrite**: best native macOS/iPad experience, full App Store integration, native TextKit 2 editor. Rejected for now — would discard all existing React code, Rust backend, 2200+ tests, and Claude Code's accumulated context. Worth revisiting if Laputa becomes iOS-first. +- **Capacitor**: replaces Tauri layer, keeps React, but the Rust backend is lost entirely — git and file operations would need reimplementation in JS or Swift. +- **React Native + WebView**: wraps the React app in a WebView. Too hacky, performance concerns, App Store review risks. + +## Git on iPad + +`git` CLI is unavailable on iOS. Options for production: +- **Option A (recommended)**: `isomorphic-git` — pure JS git implementation, no native dependencies, runs in WebView. Replaces Rust git commands on mobile. +- **Option B (prototype)**: Working Copy as iOS Files provider — user manages git separately. +- **Option C**: iCloud Drive sync — no git history. Not recommended. + +## Consequences + +- Zero frontend changes needed for basic iPad support +- Desktop features (git, MCP, Claude CLI) unavailable on iPad until isomorphic-git is integrated +- Tauri v2 iOS is still beta — production stability unknown +- App Store distribution requires Apple Developer account and TestFlight +- Triggers re-evaluation if: Tauri iOS remains unstable after 6 months, or iPad becomes the primary target (in which case SwiftUI rewrite becomes rational) diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..4e398550 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,51 @@ +# Architecture Decision Records + +This folder contains Architecture Decision Records (ADRs) for the Laputa app. + +## Format + +Each ADR is a Laputa-compatible markdown note with YAML frontmatter: + +```markdown +--- +type: ADR +id: "0001" +title: "Short decision title" +status: active # active | superseded | proposed +date: YYYY-MM-DD +superseded_by: "0007" # only if status: superseded +--- + +## Context +What situation led to this decision? What forces were at play? + +## Decision +What was decided? State it clearly in one or two sentences. + +## Alternatives considered +- **Option A** (chosen): pros / cons +- **Option B**: pros / cons +- **Option C**: pros / cons + +## Consequences +What becomes easier or harder as a result of this decision? +What triggers re-evaluation of this decision? +``` + +## Rules + +- One decision per file +- Files named `NNNN-short-title.md` (monotonic numbering) +- Once `active`, never edit — supersede instead +- When superseded: update `status: superseded` and add `superseded_by: "NNNN"` +- ARCHITECTURE.md reflects the current state (active decisions only) + +## Index + +| ID | Title | Status | +|----|-------|--------| +| [0001](0001-tauri-react-stack.md) | Tauri v2 + React as application stack | active | +| [0002](0002-filesystem-source-of-truth.md) | Filesystem as the single source of truth | active | +| [0003](0003-single-note-model.md) | Single note open at a time (no tabs) | active | +| [0004](0004-vault-vs-app-settings-storage.md) | Vault vs app settings for state storage | active | +| [0005](0005-tauri-ios-for-ipad.md) | Tauri v2 iOS for iPad support (vs SwiftUI rewrite) | active |