Update documentation to reflect audit fixes

- CLAUDE.md: Document sync lock, temp cleanup on failure, metadata-first
  deletes, YAML 64KB limit, mtime dedup tiebreaker, saturating version,
  watcher-error event, and accessibility features
- docs/API.md: Add YAML frontmatter row to size limits table, document
  sync lock and delete ordering in sync strategy, update path traversal
  and atomic write descriptions
- PLAN.md: Update version field comment to note saturating behavior

https://claude.ai/code/session_01AJoK28N4vqLqzskq6ybGri
This commit is contained in:
Claude 2026-04-06 11:07:29 +00:00
parent 6174836b7f
commit 7e9d35d6d6
No known key found for this signature in database
3 changed files with 14 additions and 10 deletions

View file

@ -35,15 +35,15 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
### Key patterns ### Key patterns
- **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. Atomic writes (temp file + rename) for all metadata files. Input validation: task titles max 500 chars, descriptions max 1MB, list names max 255 chars. - **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. Atomic writes (temp file + rename, with temp cleanup on failure) for all metadata files. Input validation: task titles max 500 chars, descriptions max 1MB, list names max 255 chars, YAML frontmatter max 64KB. Delete operations update metadata before removing files to prevent orphaned metadata on crash.
- **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here. - **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here.
- **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate. Atomic writes (temp file + rename) prevent corruption on crash. - **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate. Atomic writes (temp file + rename) prevent corruption on crash.
- **Sync** (`sync.rs`): Three-way diff sync with offline queue. Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix. Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows. Path traversal validation on all sync paths. Atomic writes for sync state and queue files. - **Sync** (`sync.rs`): Three-way diff sync with offline queue. File-based `.sync.lock` prevents concurrent sync operations (auto-cleaned after 5 minutes if stale). Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix (duplicate file cleaned up if metadata update fails). Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows. Path traversal validation rejects `..` components and backslashes anywhere in sync paths. Atomic writes for sync state and queue files (temp cleanup on failure).
- **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Rejects non-HTTPS URLs. `Zeroizing<String>` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB cap on both PROPFIND responses and file downloads. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain). - **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Rejects non-HTTPS URLs. `Zeroizing<String>` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB cap on both PROPFIND responses and file downloads. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain).
### On-disk format ### On-disk format
Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories. Task frontmatter contains `id`, `status`, `version` (u64, increments on every write, defaults to 1 for legacy files), and optionally `due`, `has_time`, `parent` (omitted when false/null). `list_tasks` auto-deduplicates by UUID, keeping the highest-version file and deleting stale copies. Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories. Task frontmatter contains `id`, `status`, `version` (u64, increments via `saturating_add` on every write, defaults to 1 for legacy files), and optionally `due`, `has_time`, `parent` (omitted when false/null). `list_tasks` auto-deduplicates by UUID, keeping the highest-version file and deleting stale copies; when versions are equal, filesystem modification time is used as a tiebreaker.
### Tauri GUI structure ### Tauri GUI structure
@ -95,7 +95,7 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
- Per-workspace sync interval (configurable in settings) - Per-workspace sync interval (configurable in settings)
- Upload/download counts in drawer footer (hidden when zero) - Upload/download counts in drawer footer (hidden when zero)
- Transient sync error suppression (connectivity issues update status dot only, no error banner) - Transient sync error suppression (connectivity issues update status dot only, no error banner)
- File watcher (notify crate, 500ms debounce, auto-reloads on external changes) - File watcher (notify crate, 500ms debounce, auto-reloads on external changes, emits `watcher-error` event to frontend on failures)
- WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces - WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces
- Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog) - Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog)
- Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI)
@ -106,6 +106,7 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to
- Custom confirmation dialogs (ConfirmDialog component replaces native confirm()) - Custom confirmation dialogs (ConfirmDialog component replaces native confirm())
- Workspace path validation (rejects system directories) - Workspace path validation (rejects system directories)
- Task detail auto-cleanup (taskStack clears when viewed task is deleted or list switches) - Task detail auto-cleanup (taskStack clears when viewed task is deleted or list switches)
- Accessibility: ARIA labels/roles on interactive components, keyboard handlers, `prefers-reduced-motion` CSS support
### GUI features NOT yet done ### GUI features NOT yet done

View file

@ -86,7 +86,7 @@ Task {
status: TaskStatus, // Backlog or Completed status: TaskStatus, // Backlog or Completed
due_date: Option<DateTime>, due_date: Option<DateTime>,
has_time: bool, // Whether due_date includes a specific time has_time: bool, // Whether due_date includes a specific time
version: u64, // Increments on every write; used for sync dedup version: u64, // Increments (saturating) on every write; used for sync dedup
parent_id: Option<Uuid>, // For subtasks parent_id: Option<Uuid>, // For subtasks
} }

View file

@ -20,7 +20,7 @@ pub struct Task {
pub status: TaskStatus, pub status: TaskStatus,
pub due_date: Option<DateTime<Utc>>, pub due_date: Option<DateTime<Utc>>,
pub has_time: bool, // Whether due_date includes a specific time pub has_time: bool, // Whether due_date includes a specific time
pub version: u64, // Increments on every write; used for sync dedup pub version: u64, // Increments (saturating) on every write; used for sync dedup
pub parent_id: Option<Uuid>, pub parent_id: Option<Uuid>,
} }
@ -377,8 +377,10 @@ client.delete_file("old-task.md").await?;
- **Sync state**: Stored in `.syncstate.json` within the workspace directory - **Sync state**: Stored in `.syncstate.json` within the workspace directory
- **Auto-sync**: Periodic polling (configurable `sync_interval_secs`), debounced file-change trigger (5s), window-focus trigger (30s stale threshold) - **Auto-sync**: Periodic polling (configurable `sync_interval_secs`), debounced file-change trigger (5s), window-focus trigger (30s stale threshold)
- **Response size cap**: PROPFIND responses and file downloads are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers - **Response size cap**: PROPFIND responses and file downloads are limited to 10 MB (checked via `Content-Length` header and actual body size) to prevent memory exhaustion from malicious servers
- **Path traversal protection**: Sync paths are validated to reject `..` and `\` components before any file system operation - **Path traversal protection**: Sync paths are validated to reject `..` components and backslashes anywhere in the path before any file system operation
- **Atomic writes**: Sync state (`.syncstate.json`) and offline queue (`.syncqueue.json`) use atomic write pattern (temp file + rename) to prevent corruption on crash - **Concurrent sync lock**: File-based `.sync.lock` prevents overlapping sync operations on the same workspace. Stale locks older than 5 minutes are automatically cleaned up
- **Atomic writes**: Sync state (`.syncstate.json`) and offline queue (`.syncqueue.json`) use atomic write pattern (temp file + rename, with cleanup on failure) to prevent corruption on crash
- **Delete ordering**: Delete operations update metadata before removing files, so a crash between steps leaves an orphaned file (recoverable) rather than an orphaned metadata entry
- **Syncable files**: Only processes files at expected depths — `.onyx-workspace.json` at root (depth 1), `.listdata.json` and `*.md` inside list directories (depth 2) - **Syncable files**: Only processes files at expected depths — `.onyx-workspace.json` at root (depth 1), `.listdata.json` and `*.md` inside list directories (depth 2)
## Error Handling ## Error Handling
@ -413,10 +415,11 @@ The storage layer enforces the following limits:
| List name | 255 characters | `InvalidData` | | List name | 255 characters | `InvalidData` |
| WebDAV file download | 10 MB | `WebDav` | | WebDAV file download | 10 MB | `WebDav` |
| PROPFIND response | 10 MB | `WebDav` | | PROPFIND response | 10 MB | `WebDav` |
| YAML frontmatter | 65,536 bytes (64 KB) | `InvalidData` |
### Atomic Writes ### Atomic Writes
All metadata and state files use an atomic write pattern (write to `.tmp` then rename) to prevent data corruption if the process crashes mid-write: All metadata and state files use an atomic write pattern (write to `.tmp` then rename) to prevent data corruption if the process crashes mid-write. If the rename step fails, the `.tmp` file is cleaned up to prevent accumulation. Affected files:
- `.onyx-workspace.json` (root metadata) - `.onyx-workspace.json` (root metadata)
- `.listdata.json` (list metadata) - `.listdata.json` (list metadata)
@ -427,7 +430,7 @@ All metadata and state files use an atomic write pattern (write to `.tmp` then r
### Path Safety ### Path Safety
- **List names**: Rejected if they contain `/`, `\`, or `..` components. Canonicalized and verified to stay within workspace root. - **List names**: Rejected if they contain `/`, `\`, or `..` components. Canonicalized and verified to stay within workspace root.
- **Sync paths**: Validated to reject `..` and `\` before any file system operation. - **Sync paths**: Validated to reject `..` components and backslashes anywhere in the path before any file system operation.
- **Workspace paths** (Tauri): Rejected if they point to system directories (`/etc`, `/usr`, `/bin`, etc.). - **Workspace paths** (Tauri): Rejected if they point to system directories (`/etc`, `/usr`, `/bin`, etc.).
- **Filenames**: Sanitized to replace `/ \ : * ? " < > |` and control characters with `_`. - **Filenames**: Sanitized to replace `/ \ : * ? " < > |` and control characters with `_`.