docs: research control center recovery patterns

This commit is contained in:
LLLin000 2026-07-14 23:08:26 +08:00
parent 68f342ddf9
commit 29a0e27077
4 changed files with 1434 additions and 10 deletions

View file

@ -1,11 +1,11 @@
> **Branch:** `feat/ocr-rebuild-ux` | **Last Updated:** 2026-07-14
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65) is charted. The current-contract audit is resolved; capability vocabulary and the two reference-product research tickets form the live frontier.
> **Active work:** [Wayfinder: Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65). The current-contract audit and both reference-product research tickets are resolved; capability vocabulary (#69) and managed-runtime architecture (#70) are the live frontier.
>
> ---
>
> **Current state:** Retrieval recovery is merged and live. OCR multi-key rebuild/redo now streams progress, supports cooperative stop between papers, and refreshes canonical maintenance state. The control-plane audit found four critical contract gaps: headless setup can skip runtime installation yet return success, Python and TypeScript disagree on legacy path precedence, global `setup_complete` can mask unavailable modules, and stale runtime snapshots can render as ready.
>
> Next: define the capability envelope in [#69](https://github.com/LLLin000/PaperForge/issues/69) while completing [Obsidian-native setup/settings research](https://github.com/LLLin000/PaperForge/issues/67) and [desktop installation/health/recovery research](https://github.com/LLLin000/PaperForge/issues/68); do not choose the managed-runtime architecture or start production implementation until its blockers close.
> Next: define the canonical capability/action vocabulary in [#69](https://github.com/LLLin000/PaperForge/issues/69) and choose the managed-runtime architecture in [#70](https://github.com/LLLin000/PaperForge/issues/70). Their outputs unblock the control-center and maintenance prototypes; do not start production implementation before #73 closes.
## 1. Architecture
### 1.1 The problem (pre-v2)
@ -76,7 +76,7 @@ raw observations → structural signatures → stable anchors/families → zone
| **M metadata search** | ✅ sql.js and Python CLI paths restored |
| **@ deep search** | ✅ deep CLI invocation and result-envelope consumption restored |
| **Vector build control** | ✅ canonical SQLite lifecycle and health state restored |
| **Control-plane contracts** | 🟡 Current-state audit complete; capability vocabulary and managed-runtime decisions pending |
| **Control-plane contracts** | 🟡 Audit + Obsidian/desktop reference research complete; capability vocabulary and managed-runtime decisions pending |
### 2.3 Fix Status
@ -147,10 +147,10 @@ The OCR maintenance slice now has a canonical All/Recommended state model, selec
Remaining legacy OCR issues (carried forward):
## 4. Active Queue
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted; current-contract audit resolved.
2. 🔴 **[Capability-state vocabulary](https://github.com/LLLin000/PaperForge/issues/69)** — unblocked by the audit; define the shared module-state/action envelope.
3. 🟡 **Reference-product research** — complete [Obsidian-native](https://github.com/LLLin000/PaperForge/issues/67) and [desktop runtime/recovery](https://github.com/LLLin000/PaperForge/issues/68) evidence.
4. ⏳ **Managed runtime and prototypes** — runtime choice remains blocked on #68; prototypes remain blocked on state/runtime decisions.
1. 🔴 **[Control-center Wayfinder](https://github.com/LLLin000/PaperForge/issues/65)** — map charted; audit and external research gates resolved.
2. 🔴 **[Capability-state vocabulary](https://github.com/LLLin000/PaperForge/issues/69)** — define the shared module-state, reason-code, severity, transition, and action envelope.
3. 🔴 **[Managed runtime](https://github.com/LLLin000/PaperForge/issues/70)** — choose ownership, install, update, rollback, health, and cross-platform contracts using the completed desktop evidence.
4. ⏳ **Control-center prototypes** — #71 and #72 remain blocked on #69/#70; #73 integrates the accepted model.
5. 🟡 **Downstream tooling** — section-aware chunking and separate figure/table handling.
6. ⏳ **Compatibility naming cleanup** — deferred post-release.
### 4.1 Immediate Next Steps
@ -162,6 +162,8 @@ Remaining legacy OCR issues (carried forward):
- [x] Grill and domain-model the control-center destination
- [x] Create the Wayfinder map, eight child tickets, and native dependency graph
- [x] Audit current setup, readiness, recovery, cache, and migration contracts ([#66](https://github.com/LLLin000/PaperForge/issues/66))
- [x] Study Obsidian-native setup/settings patterns ([#67](https://github.com/LLLin000/PaperForge/issues/67))
- [x] Study desktop installation/health/recovery patterns ([#68](https://github.com/LLLin000/PaperForge/issues/68))
- [ ] Close or follow up [OCR rebuild: streaming progress + maintenance UI redesign](https://github.com/LLLin000/PaperForge/issues/64)
- [ ] Reconcile the remaining slices of [Unified rebuild UX](https://github.com/LLLin000/PaperForge/issues/63)
- [ ] #21 Display hash staleness in status output
@ -309,6 +311,8 @@ Remaining legacy OCR issues (carried forward):
| 2026-07-14 | Successful updates leave maintenance; quality anomalies are opt-in reports | Routine quality scores create permanent, non-actionable noise. Maintenance shows only stale/failed/corrupt work with a concrete action; unacceptable OCR is reported through a user-reviewed GitHub Issue draft. |
| 2026-07-14 | Control-plane facts come from independent backend capability probes | A global setup boolean and frontend inference cannot represent partial readiness or stale state; every module needs a reason code, one primary action, and revision/freshness evidence. |
| 2026-07-14 | Preserve durable domain truth; migrate JSON runtime snapshots to versioned caches | SQLite build state and per-paper OCR metadata already support recovery, while independently written JSON snapshots have no coherency or staleness protocol. |
| 2026-07-14 | Keep normal readiness in the control center, not permanent status-bar chrome | Obsidian status bars work for active operations and exceptional attention; duplicating six-module health there creates noise and conflicts with the actionable-only maintenance model. |
| 2026-07-14 | Diagnostics stay local until the user reviews an issue draft | Docker-style opaque uploads require a support service and hide bundle contents; PaperForge instead exports redacted data locally and opens a prefilled GitHub draft without token storage or automatic submission. |
---
@ -407,6 +411,7 @@ python -m ruff check paperforge/worker/ocr_*.py
| 2026-07-14 | OCR rebuild streaming + maintenance UX | Added flushed rebuild/redo progress contracts, full keyed redo, cross-platform cooperative stop, canonical All/Recommended filters, selected batch actions, and cache migration. Verification: 99 focused Python tests, 93 plugin tests, typecheck/build, and live 734/700-row Obsidian state with no captured errors. | §2-4 |
| 2026-07-14 | Control-center Wayfinder charted | Defined the six-module destination (安装 / 文献库 / OCR / 记忆 / 维护 / 帮助), module-independent readiness, actionable-only maintenance, opt-in OCR issue drafts, and a three-ticket research frontier. | [Wayfinder map](https://github.com/LLLin000/PaperForge/issues/65) |
| 2026-07-14 | Control-center current-contract audit | Inventoried setup, configuration, runtime, readiness, persistence, cache, failure, and recovery contracts across plugin and Python. Closed #66 with a ranked contradiction matrix and preserve/migrate/remove plan; unblocked #69 while #70 remains blocked on desktop/runtime evidence. | [Contract audit](https://github.com/LLLin000/PaperForge/issues/66#issuecomment-4968837257) |
| 2026-07-14 | Obsidian and desktop recovery research | Closed #67 and #68 with primary-source pattern reports. Preserved the six-module IA, independent capability probes, Obsidian-managed plugin updates, module-scoped recovery, local diagnostics, and user-reviewed issue drafts; unblocked #70. | [#67](https://github.com/LLLin000/PaperForge/issues/67#issuecomment-4970653461), [#68](https://github.com/LLLin000/PaperForge/issues/68#issuecomment-4970660288) |
## 9. Historical Detail Archive

View file

@ -0,0 +1,764 @@
# Desktop Runtime / Recovery Patterns for Managed-Runtime Architecture
**Date:** 2026-07-14
**Issue:** [#68 — Research desktop installation/health/recovery patterns](https://github.com/LLLin000/PaperForge/issues/68)
**Parent map:** [#65 — Make the PaperForge control center self-explanatory](https://github.com/LLLin000/PaperForge/issues/65)
**Input to:** [#70 — Managed-runtime architecture](https://github.com/LLLin000/PaperForge/issues/70)
**Products studied:** VS Code, Docker Desktop, Raycast, Ollama, LM Studio, Homebrew, Obsidian (plugin system), Electron autoUpdater
---
## Executive summary
PaperForge requires a managed runtime architecture that coordinates an Obsidian plugin (TypeScript/Electron) and a Python backend across **安装 / 文献库 / OCR / 记忆 / 维护 / 帮助** modules — each with independent health, recovery, and update semantics. This report studies mature desktop tools to extract patterns that apply to PaperForge's constraints:
1. **Constrained host**: Obsidian plugins run inside an Electron renderer but do not own the host app lifecycle, autoUpdater, or native addon lifecycle. Python backend runs via subprocess or HTTP.
2. **Cross-platform**: Windows, macOS, and Linux all primary.
3. **Two-layer runtime**: TypeScript plugin + Python backend, each with its own lifecycle.
4. **Local-first, privacy-sensitive**: Medical research data; diagnostic export and issue reporting must never leak identifiable content.
5. **Derived-artifact-heavy**: OCR outputs, embeddings, vector indices — disposable but expensive to rebuild.
### Key findings
| Pattern | Observed in | PaperForge fit |
|---|---|---|
| **Independent module health probes** with stable reason codes | VS Code (Runtime Status), Docker Desktop (diagnose), Raycast (Extension Diagnostics) | **Adopt**: Backend capability probes own readiness; plugin renders without reclassification |
| **Extension isolation principle** | VS Code (Extension Bisect) | **Note**: Binary-search isolation is VS Code-specific. PaperForge retains the principle of systematic diagnostic isolation but does not implement a bisect command. |
| **Per-module actionable maintenance, never global OK** | VS Code (per-extension Runtime Status), Raycast (Issue Dashboard per-extension) | **Adopt**: Already aligned with #66 audit finding |
| **Reset with graduated destructiveness** | Docker Desktop (restart → clean/purge data → factory reset) | **Adopt**: Clear three-tier recovery model |
| **Local diagnostic export with user review before issue creation** | VS Code (prefilled Report Issue), Docker Desktop (local diagnose collection, user uploads ID separately) | **Adopt**: User reviews local diagnostic bundle before creating a GitHub Issue draft. No upload service; no opaque ID. |
| **Version pinning / rollback** | Ollama (model tags), VS Code (Install Another Version), Homebrew (pin) | **Adapt**: Tag all model/runtime versions; rollback mechanism to be chosen in #70. Acceptance criteria: restore known-good state. |
| **Separate CLI and GUI diagnostics** | LM Studio (lms CLI + Desktop), Docker Desktop (GUI + CLI diagnose) | **Adopt**: Python CLI diagnostics + plugin diagnostics view |
| **Auto-update with compatibility gate** | Raycast (API version check), VS Code (compatibility check), Electron (Squirrel/MSIX) | **Adopt**: Never force-update the Python runtime or plugin without compatibility verification |
| **JIT model loading with TTL eviction** | LM Studio (JIT Loader + Idle TTL + Auto-Evict), Ollama (single-load scheduler) | **Reject** for \#68 scope (model loading is already handled; revisit if memory pressure becomes a problem) |
| **Setup verification / repair commands** | Homebrew (doctor, update-reset), Docker Desktop (factory reset) | **Adopt**: \`paperforge doctor\` as a unified diagnostic entry point |
| **Plugin update via Obsidian** | Obsidian (manual community plugin update) | **Accept constraint**: PaperForge plugin uses Obsidian's community plugin update flow. Python backend update mechanism chosen in #70. |
| **Rollback via package manager pinning** | Homebrew (pin), Ollama (brew pin / apt-mark hold) | **Note**: Pin pattern prevents accidental upgrades. PaperForge defers rollback mechanism to #70. |
---
## 1. Product deep dives
### 1.1 VS Code (Electron, managed extensions)
**Runtime ownership model:**
- VS Code runs extensions in isolated **Extension Host** processes (Node.js, web, or hybrid). \`extensionKind\` determines placement: \`workspace\` runs on the workspace host, \`ui\` runs near the UI. The host selection prefers Node.js, falls back to web-only hosts. Extensions cannot crash the main editor.
- Source: [Extension Host | VS Code Extension API](https://code.visualstudio.com/api/advanced-topics/extension-host)
**Module health model:**
- The **Runtime Status tab** (Extensions view → extension → Runtime Status) shows per-extension: activation time, activation event, and any warnings/errors. Activation time breakdown (code load / activate call / resolved) has been requested but not yet implemented (\#174255).
- Extensions are lazily activated via **Activation Events** (\`onCommand\`, \`onView\`, \`onStartupFinished\`, etc.) to avoid startup bloat.
- Source: [July 2021 (1.59) — Runtime Status tab](https://code.visualstudio.com/updates/v1_59)
**Update/rollback behavior:**
- **Auto-update:** Extensions update automatically by default (configurable via \`extensions.autoUpdate\`; default delay 2 hours via \`extensions.autoUpdateDelay\`). Auto-update only applies compatible extensions — incompatible ones are held until VS Code itself is updated.
- **Rollback:** Right-click → **Install Another Version** → pick any previous version from the marketplace. CLI equivalent: \`code --install-extension publisher.extension@1.2.3\`.
- **Pin version:** No explicit pin UI, but installing a specific version and disabling auto-update achieves the same effect.
- Source: [Extension Marketplace](https://code.visualstudio.com/docs/configure/extensions/extension-marketplace)
**Repair/reset actions:**
- **Extension Bisect** (Help → Start Extension Bisect): Binary search over enabled extensions to isolate a faulty one in O(log N) steps. Works like \`git bisect\`: answer "Good Now" or "This Is Bad" at each step. First step disables all extensions.
- **Disable all extensions**, **Restart Extension Host** (Developer: Reload Window With Extensions Disabled).
- **Reset VS Code** (via \`--user-data-dir\` switch or resetting settings to defaults).
- Source: [Resolving extension issues with bisect](https://code.visualstudio.com/blogs/2021/02/16/extension-bisect)
**Diagnostic export:**
- **Developer: Show Running Extensions** → copies detailed activation/performance data to clipboard.
- Log level setting, developer console (Chromium DevTools), and \`--verbose\` CLI flag.
- Extension bisect session state can be shared.
**Issue-report flow:**
- **Report Issue** from Help menu → opens browser with pre-filled template (VS Code version, extensions, logs).
- Extensions bisect result can be attached to extension-specific GitHub issues.
- GitHub issue templates in each extension repository.
**Security/privacy boundary:**
- Extensions run in the Extension Host process, isolated from the main VS Code process. Multiple extensions typically share one host.
- Extensions declare capabilities in their manifest but are not individually permission-gated; users approve the extension as a whole, not per-API-call.
- Web extensions run in a tighter sandbox (no Node.js APIs).
**Cross-platform behavior:**
- Extensions host architecture identical across Windows, macOS, Linux.
- Remote extensions (WSL, SSH, Dev Containers) require a window reload after update instead of the local in-process restart.
---
### 1.2 Docker Desktop (VM-based runtime, hybrid native+VM)
**Runtime ownership model:**
- Docker Desktop runs a **Linux VM** (via Hyper-V on Windows, Apple Hypervisor on macOS, a qemu VM on Linux) that hosts the Docker engine and containers. The Desktop app is a native Electron/GUI wrapper that communicates with the VM.
- The VM is owned and managed by Docker Desktop — users do not directly manage it.
**Module health model:**
- **Dashboard** shows: Engine running, containers status, resource usage (CPU/memory/disk), Kubernetes status, extensions status.
- **Settings > Troubleshoot** section groups all health and recovery actions.
- Automatic health checks via \`docker desktop status\` (CLI, v4.60+).
- Source: [Docker Desktop CLI reference](https://docs.docker.com/desktop/features/desktop-cli/)
**Update/rollback behavior:**
- **Auto-update:** Docker Desktop checks for updates automatically; user is prompted to download and install.
- **Rollback:** Manual — download and install a previous version from the release notes page. No built-in rollback command.
- **Channel selection:** Stable, Beta, or Dev release channels.
- Source: [Docker Desktop release notes](https://docs.docker.com/desktop/release-notes)
**Repair/reset actions (three-tier):**
| Tier | Action | What it does | Data loss |
|---|---|---|---|
| 1 | Restart | Restarts Docker Desktop process | None |
| 2 | Clean / Purge data | Removes all containers/images/volumes (reported to also purge volumes — [docker/for-mac#6758](https://github.com/docker/for-mac/issues/6758)) | All container data, images, volumes |
| 3 | Reset to factory defaults | Destroys VM, resets all settings, credentials, Kubernetes data; restores to first-install state | Everything |
- Source: [Troubleshoot Docker Desktop](https://docs.docker.com/desktop/troubleshoot-and-support/troubleshoot/)
**Diagnostic export:**
- **\`docker desktop diagnose\`** (CLI, v4.60+) collects system info, logs, and configuration into a diagnostic bundle.
- **\`docker desktop diagnose --upload\`** uploads the bundle and returns a **Diagnostic ID** (UUID/timestamp format).
- **In-app:** Troubleshoot > Get support → generates and optionally uploads diagnostics.
- Manual fallback: \`com.docker.diagnose\` binary in the install directory.
- **Log location:** \`$HOME/.docker/desktop/log/\`
- Source: [docker desktop diagnose](https://docs.docker.com/reference/cli/docker/desktop/diagnose/)
**Issue-report flow:**
- Paid users: use Diagnostic ID with Docker Support ticket.
- Free users: **report a GitHub bug** with the Diagnostic ID.
- The diagnostic ID is a reference to an already-uploaded bundle; it reduces what the user pastes into a ticket but does not itself create a privacy boundary.
- Source: [Troubleshoot Docker Desktop](https://docs.docker.com/desktop/troubleshoot-and-support/troubleshoot/)
**Security/privacy boundary:**
- Docker Desktop runs as a user-level process (not system-wide on modern setups).
- Diagnostics bundle may contain container names, image names, and environment variable _names_ (not values, per the privacy policy).
- **Restricted Mode** (Docker Desktop settings) can disable usage statistics collection.
**Cross-platform behavior:**
- macOS, Windows, and Linux all run Linux containers inside a managed VM; the hypervisor and storage paths differ by platform.
- The **backup/restore** procedure differs per platform: \`docker_data.vhdx\` + WSL distros on Windows; \`Docker.raw\` on macOS/Linux.
- \`docker desktop diagnose\` binary path differs per OS.
- Source: [Backup and restore Docker Desktop data](https://docs.docker.com/desktop/settings-and-maintenance/backup-and-restore/)
---
### 1.3 Raycast (Native macOS desktop, managed extensions)
**Runtime ownership model:**
- Raycast extensions run as **Node.js processes** managed by the Raycast app (AppKit native app). A custom React reconciler translates React updates into native AppKit updates via JSON render tree + JSON Patch diff — extensions run in separate processes, communication happens over the render tree protocol.
- Extensions are distributed through the **Raycast Store**. Version and API compatibility are checked before installation.
- Source: [How the Raycast API and extensions work](https://www.raycast.com/blog/how-raycast-api-extensions-work)
**Module health model:**
- **Extension Diagnostics** command: Shows:
- Loaded Commands (all currently loaded extensions)
- Commands with Background Refresh
- Latest Events (activation, errors)
- **Extension Issues Dashboard** (\`raycast.com/extension-issues\`): Real-time visibility into user-reported errors, with per-issue details (stack trace, breadcrumbs, release dates, environment).
- Source: [Changelog — Extension Diagnostics](https://developers.raycast.com/misc/changelog.md)
**Update/rollback behavior:**
- **Auto-update:** Extensions are auto-updated by Raycast only if compatible with the current Raycast version and API version. Users cannot disable auto-update per extension.
- **Versioning:** Single implicit "latest" version. No version pinning in the store model.
- **Rollback:** Not supported through the official mechanism. Developers must push a fix as a new version.
- **Migration:** Raycast provides automated API migrations and deprecation plans for breaking changes.
- Source: [Versioning — Raycast Developers](https://developers.raycast.com/information/versioning)
**Repair/reset actions:**
- **Restart Raycast** (clear all extension state).
- **Reinstall extension** (uninstall → install).
- **Deactivate menu bar commands** without removing them.
- **Force production mode** for dev extensions (Preferences > Advanced > "Use Node production environment").
**Diagnostic export:**
- **Extension Diagnostics** command (built-in).
- **Developer tools:** DevTools-accessible via React DevTools.
- **Console logs** (development only; disabled for store extensions).
- **Error overlay** in-app for unhandled exceptions/Promise rejections (production shows a generic message).
**Issue-report flow:**
- **Extension Issues Dashboard** aggregates all user-reported errors by extension.
- For developers: per-issue details include stack trace, breadcrumbs, release dates, environment.
- No user-initiated issue creation flow — errors are auto-captured.
**Security/privacy boundary:**
- Extensions are sandboxed (no filesystem access beyond what the Raycast API provides).
- Environment modes: store extensions run in Node production mode; development extensions run in development mode.
- Console logging disabled for store extensions (privacy-conscious).
**Cross-platform behavior:**
- Currently **macOS only**. No Windows/Linux version.
---
### 1.4 Ollama (Local AI runtime, Go daemon + CLI)
**Runtime ownership model:**
- Ollama is a **Go daemon** (\`ollama serve\`) that manages a **Llama-based model runner** as a child process. The server is a single-binary with embedded model runner.
- **Scheduler** (\`server/sched.go\`): Only one model actively loaded at a time. Load management is single-threaded; already-loaded models handle concurrent requests. GPU-aware placement with eviction/relocation for memory pressure.
- Source: [Ollama server routes.go](https://github.com/ollama/ollama/blob/c42e9d24/server/routes.go), [server/sched.go](https://github.com/ollama/ollama/blob/e09b3f9f/server/sched.go)
**Module health model:**
- **\`ollama ps\`** — list running models with their PID, VRAM usage, and expiration.
- **\`ollama list\`** — list all downloaded models with tags.
- **\`ollama show <model>\`** — detailed model manifest (modelfile, parameters, quantization, digest).
- **Graceful shutdown** on SIGINT/SIGTERM — scheduler cleanup, model unload, server shutdown.
- Source: [Ollama API docs](https://github.com/ollama/ollama/blob/9db4bdbad6a4981ad761aa2b603e69e8fb83212c/docs/api.md)
**Update/rollback behavior:**
- **Binary updates**: Download new binary from [ollama.com](https://ollama.com) — binary replaces previous.
- **Docker**: \`ollama/ollama:0.x.y\` version tags; rollback = restart container with older tag.
- **Package manager**: \`brew pin ollama\` (macOS) or \`apt-mark hold ollama\` (Linux) to pin version.
- **Model pinning**: Always pin quantization tags (\`llama3.2:8b-instruct-q4_K_M\`); floating tags (\`latest\`) can cause silent regressions.
- **Model digest**: Since v0.4.0, models have reliable digest hashes for true rollback verification.
- Source: [Ollama GitHub README — model tags and versioning](https://github.com/ollama/ollama?tab=readme-ov-file#model-library); [Ollama API docs — /api/tags and model management](https://github.com/ollama/ollama/blob/c42e9d24/docs/api.md)
**Repair/reset actions:**
- **Restart daemon** (\`ollama serve\`).
- **Delete and re-pull model**.
- **\`~/.ollama/models\`** — model cache directory; clear individual model blobs to force re-download.
- **Graceful shutdown** handles cleanup of running model sessions.
**Diagnostic export:**
- **\`ollama serve\`** runs with visible logs; log level via environment variable (\`OLLAMA_DEBUG=1\`).
- GPU discovery and VRAM status logged at startup.
- **API endpoints**: \`GET /api/tags\`, \`GET /api/show\`, \`GET /api/ps\` for current state.
- Source: [Ollama server routes.go](https://github.com/ollama/ollama/blob/c42e9d24/server/routes.go)
**Issue-report flow:**
- **GitHub Issues** on [ollama/ollama](https://github.com/ollama/ollama).
- Debug bundle: \`ollama serve\` logs + model manifest + system info.
- Discord community for troubleshooting.
**Security/privacy boundary:**
- Local-only by default; \`OLLAMA_HOST=0.0.0.0\` for network access.
- Models run locally; no telemetry by default.
- No user authentication mechanism.
**Cross-platform behavior:**
- macOS, Windows, Linux — single binary per platform.
- Linux: systemd service file included.
- Windows: installer manages PATH and auto-start.
- Platform differences in GPU detection (CUDA vs Metal vs ROCm).
---
### 1.5 LM Studio (Local AI runtime, Desktop App + daemon + CLI)
**Runtime ownership model:**
- Three-component architecture:
- **Desktop App**: GUI for interactive use, model discovery, multi-turn chat, RAG, visual config
- **llmster** (headless daemon): Server-native core, suitable for remote/automated environments
- **lms** (CLI): Unified control plane for both Desktop and llmster
- Model management: **JIT Loader** (load model on first request), **Idle TTL** (~60 minutes default), **Auto-Evict** (unloads older JIT models to free memory).
- Source: [LM Studio Developer Docs — CLI, daemon, architecture](https://lmstudio.ai/docs/developer)
**Module health model:**
- **\`lms ps --json\`**: List loaded models with state (Loaded / Idle / Auto-Evict).
- **\`lms daemon status\`**: Check daemon health.
- **\`lms server start/stop/status\`**: API server lifecycle management.
- Server modes: GUI mode (inside desktop app) and Headless mode (standalone \`llmster\` process).
- Source: [LM Studio CLI reference — lms ps, lms daemon, model management](https://lmstudio.ai/docs/cli)
**Update/rollback behavior:**
- **Auto-update** within Desktop App (prompts user to download).
- **CLI** (\`lms\`) can update independently.
- **Rollback**: Manual — download previous version from release archives.
- **Model versioning**: No built-in version pinning for downloaded models; rely on Hugging Face commit hashes.
**Repair/reset actions:**
- **Restart daemon** (\`lms daemon restart\`).
- **Reinstall model** (delete and re-download).
- **Clear cache** (\`~/.lmstudio/models\` — manual cleanup).
**Diagnostic export:**
- **\`lms log stream\`**: Real-time log streaming with options:
- \`--source model\` (default): model IO logs
- \`--source server\`: HTTP API server logs
- \`--source runtime\`: runtime logs
- \`--filter input\`, \`--filter output\`: directional filtering
- \`--json\`: structured output
- \`--stats\`: tok/sec and prediction statistics
- **Server logs** stored at \`~/.lmstudio/server-logs\`.
- Source: [lms log stream](https://lmstudio.ai/docs/cli/serve/log-stream), [src/subcommands/log.ts](https://github.com/lmstudio-ai/lms/blob/bf809b57/src/subcommands/log.ts)
**Issue-report flow:**
- **GitHub Issues** on [lmstudio-ai/lms](https://github.com/lmstudio-ai/lms) or lmstudio-ai/lmstudio (private).
- **Discord community**.
- Diagnostic data shared via log stream exports or server-logs directory.
**Security/privacy boundary:**
- API token authentication (optional; \`x-api-key\` header).
- Local-only by default.
- Desktop App and llmster share the same core; data never leaves the machine.
- \`~/.lmstudio/\` config directory contains user settings but no telemetry.
**Cross-platform behavior:**
- macOS, Windows, Linux.
- Desktop App and llmster available on all platforms.
- GPU backend varies by platform (CUDA, Metal, Vulkan).
---
### 1.6 Homebrew (Package manager — included for update/rollback/repair patterns)
**Runtime ownership model:**
- Homebrew is a **Ruby-based package manager**. Formulae (packages) and Casks (GUI apps) are installed into \`/usr/local\` (Intel) or \`/opt/homebrew\` (Apple Silicon) — the **Cellar** for formulae, \`/Applications\` for casks.
- **No daemon**; \`brew\` is invoked as a CLI command.
**Update/rollback behavior:**
- **\`brew update\`**: Updates Homebrew itself and formulae indexes from GitHub.
- **\`brew upgrade <formula>\`**: Upgrades specific formula to latest.
- **\`brew pin <formula>\`**: Prevents a formula from being upgraded.
- **\`brew unpin <formula>\`**: Removes the pin.
- **\`brew switch <formula> <version>\`**: Switch to an installed older version (if multiple versions are keg-only).
- **\`brew update-reset\`**: Destructive — resets Homebrew and taps to origin/master, discarding local changes.
- **Auto-cleanup**: Old versions removed ~30 days after upgrade.
- Source: [Homebrew Manpage](https://github.com/Homebrew/brew/blob/9b56b133a546d0d40cd6b020290f6a59fc14729f/docs/Manpage.md)
**Diagnostic/repair:**
- **\`brew doctor\`**: Checks system for potential problems. Warns with non-zero exit if issues found. Key checks: untracked files in brew repo, stray config files (e.g., \`.curlrc\`), outdated Homebrew, missing dependencies.
- **\`brew missing\`**: Checks for missing formula dependencies.
- **\`brew cleanup -n\`**: Dry-run cleanup of old versions.
- Source: [Homebrew Common Issues](https://docs.brew.sh/Common-Issues)
---
### 1.7 Obsidian plugin system (host environment for PaperForge)
**Plugin runtime:**
- Plugins run as **Electron renderer process JavaScript** (TypeScript compiled to \`main.js\`).
- Plugins have broad access: "they can read files, access the internet, and install other programs" — Obsidian cannot restrict permissions by plugin.
- **Restricted Mode** (default on) disables all community plugins.
- Source: [Plugin security](https://github.com/obsidianmd/obsidian-help/blob/master/en/Extending%20Obsidian/Plugin%20security.md)
**Plugin update mechanism:**
- **No auto-update**: Users must manually go to Settings → Community Plugins → Check for updates → Update individual or all.
- **Manual install**: Download ZIP, extract to \`.obsidian/plugins/<plugin-id>/\`.
- **Beta plugins**: BRAT plugin manages beta updates (can set frozen version).
- Source: [Community plugins](https://obsidian.md/help/community-plugins)
**Plugin review/diagnostics:**
- **Automated scanning** on every plugin version (security, code quality, malware detection).
- **Scorecards** for plugin safety (declared access: network, filesystem, clipboard).
- **Developer tools**: DevTools (Ctrl+Shift+I) for debugging; Hot Reload plugin for auto-reload during development.
- **No built-in diagnostic export** for plugins — rely on console.log and DevTools.
- Source: [The future of Obsidian plugins](https://obsidian.md/blog/future-of-plugins/)
**Key constraint for PaperForge:**
- Obsidian plugin API does not expose Electron's \`autoUpdater\`.
- Plugins can use native Node addons but these carry ABI/packaging risk across Obsidian versions. Python backend runs via subprocess or HTTP.
- No per-plugin permission model — plugins are all-or-nothing.
---
### 1.8 Electron autoUpdater (reference for update architecture)
**Capabilities:**
- **macOS**: Uses Squirrel.Mac (ZIP-based updates via JSON feed).
- **Windows**: Uses Squirrel.Windows (NuGet packages + RELEASES file) or MSIX (direct MSIX/JSON feed with optional \`allowAnyVersion\` for downgrades).
- **Linux**: No built-in updater; use OS package manager.
- Source: [Electron autoUpdater docs](https://github.com/electron/electron/blob/main/docs/api/auto-updater.md)
**Key API:**
\`\`\`javascript
autoUpdater.setFeedURL({ url: 'https://update.example.com/update' });
autoUpdater.checkForUpdates(); // Don't call twice
// Events: 'update-available', 'update-downloaded', 'error', 'checking-for-update'
autoUpdater.quitAndInstall();
\`\`\`
**Rollback:**
- **Squirrel.Windows**: No built-in rollback; uninstall and install old version.
- **MSIX**: Downgrades supported when \`allowAnyVersion: true\` is passed to \`setFeedURL()\`.
- Source: [Electron autoUpdater](https://electronjs.org/docs/latest/api/auto-updater)
---
## 2. Cross-cutting pattern matrix
### 2.1 Runtime ownership models
| Product | Runtime type | Process model | Plugin isolation |
|---|---|---|---|
| VS Code | Electron app | Shared Extension Host process(es) | Process-level isolation (main process separate from host); multiple extensions share a host |
| Docker Desktop | VM + native app | VM managed by GUI; CLI communicates with engine | Container-level; VM is the isolation boundary |
| Raycast | Native macOS app | Separate Node.js processes per extension | Process-level; React reconciler bridges UI |
| Ollama | Go daemon | Single daemon spawns Llama runner subprocess | Subprocess-level; GPU memory is the shared resource |
| LM Studio | Desktop + daemon + CLI | Three-component; shared core library | JIT-loaded model processes with TTL eviction |
| **PaperForge model** | Obsidian plugin + Python backend | Plugin in Electron renderer, Python as subprocess or REST API (decision in #70) | Process-level between plugin and Python |
### 2.2 Module health model comparison
| Product | Health interface | Granularity | Staleness protection |
|---|---|---|---|
| VS Code | Runtime Status tab | Per-extension | Activation time, event, warnings — refreshed on extension reload |
| Docker Desktop | Dashboard + CLI status | Engine, containers, K8s, extensions | Real-time by default |
| Raycast | Extension Diagnostics command | Per-extension commands + Background Refresh | Refreshed on command invocation |
| Ollama | \`ollama ps\` / API | Per-model process | Real-time by polling |
| LM Studio | \`lms ps --json\` | Per-model state (Loaded/Idle/Auto-Evict) | Real-time by polling |
| **PaperForge target** | Backend capability probes → plugin renders | Per-module (安装/文献库/OCR/记忆/维护/帮助) | TTL gate with revision/computed_at per response |
### 2.3 Update / rollback matrix
| Product | Update mechanism | Rollback mechanism | Version pinning |
|---|---|---|---|
| VS Code | Auto-update with 2h delay | Install Another Version (UI/CLI) | Disable auto-update per extension |
| Docker Desktop | Auto-update prompt | Manual download of old version | Release channel selection (stable/beta/dev) |
| Raycast | Auto (implicit, latest only) | Not supported (dev must push fix) | Not supported |
| Ollama | Binary download / Docker tag | Docker tag rollback / brew pin | Model tag pinning + digest verification |
| LM Studio | App auto-update prompt | Manual download of old version | None for models |
| Homebrew | \`brew upgrade\` | None (but \`brew pin\` prevents upgrades) | \`brew pin\` / \`brew switch\` |
| Obsidian plugins | Manual (Check for Updates) | Manual (download old version) | BRAT can freeze version |
| **PaperForge target** | Auto-check compatibility → prompt | Restore previous version; mechanism chosen in #70 | Version metadata tracked; pinning mechanism chosen in #70 |
### 2.4 Repair/reset actions (three-tier model)
| Tier | VS Code | Docker Desktop | Homebrew | PaperForge analog |
|---|---|---|---|---|
| 1 | Reload Window | Restart Docker Desktop | \`brew update\` | Restart plugin / restart Python backend |
| 2 | Extension Bisect | Clean / Purge data | \`brew doctor\` → fix warnings | Module-specific rebuild (OCR redo, re-embed, re-index) |
| 3 | Reset settings / \`--user-data-dir\` | Reset to factory defaults | \`brew update-reset\` / reinstall | Module-scoped destructive reset → recreate module's derived artifacts only; source materials preserved |
### 2.5 Diagnostic export maturity
| Product | Export method | Content | Privacy boundary |
|---|---|---|---|
| VS Code | Show Running Extensions (clipboard) | Extension names, versions, activation times, errors | No PII by default; logs may contain file paths |
| Docker Desktop | \`docker desktop diagnose --upload\` | System info, logs, config; returns opaque Diagnostic ID | ID is shared with support; bundle uploaded first |
| Raycast | Extension Issues Dashboard (server-side) | Stack traces, breadcrumbs, release dates, environment | User reports are auto-captured; developer sees anonymized data |
| Ollama | \`OLLAMA_DEBUG=1\` logs | Model manifest, GPU info, server logs | Local-only |
| LM Studio | \`lms log stream --json\` | Model IO, server logs, runtime logs with tok/sec stats | Local-only; token-level content in logs (user-review before sharing) |
| **PaperForge target** | \`paperforge doctor --export\` | Module capability states, log tail, config (redacted), OCR health | User reviews local diagnostic bundle and redacts before creating a GitHub Issue draft; no upload service |
---
## 3. PaperForge fit analysis
### 3.1 Architectural constraints
PaperForge operates in a unique architectural position:
1. **Constrained host**: Obsidian plugin does not own the host app lifecycle or autoUpdater. Native Node addons carry ABI/packaging risk. Python backend runs via subprocess or HTTP.
2. **Two-layer system** — the TypeScript plugin renders UI and accepts user input; the Python backend does all computational work (OCR, embedding, memory, sync).
3. **Cross-platform required** — Windows, macOS, and Linux all primary.
4. **Privacy-sensitive** — medical research literature, may contain patient data in PDFs.
5. **Derived artifacts are expensive** — full OCR redo on a 30-page paper takes minutes; full re-embed of 500+ papers takes hours.
### 3.2 What works for constrained Obsidian plugin + Python backend
**Patterns that transfer directly:**
| Pattern | Source | How it applies |
|---|---|---|
| Independent module health probes | VS Code, Docker Desktop, Raycast | Each PaperForge module (安装/文献库/OCR/记忆/维护/帮助) has its own capability probe returning state + reason_code + action. Plugin renders without reclassification. |
| Three-tier repair (restart → rebuild → reset) | Docker Desktop | PaperForge: (1) restart Python backend, (2) rebuild single module's derived artifacts, (3) module-scoped destructive reset that rebuilds all derived artifacts for that module while preserving source materials. |
| Local diagnostic export with privacy review | Docker Desktop (diagnostic collection), VS Code (prefilled issue report) | `paperforge doctor --export` generates a local diagnostic bundle; user reviews and redacts before optional GitHub Issue creation. No upload service. |
| Version pinning for runtime | Homebrew (pin), Ollama (brew pin / apt-mark hold) | Version metadata tracked; exact pinning and upgrade mechanism chosen in #70. Upgrade should be explicit, never automatic. |
| Compatibility gate on update | VS Code (compatible extensions only), Raycast (API version check) | Python backend version vs. plugin API version checked before any update is applied. |
| Per-module actionable maintenance | VS Code (per-extension Runtime Status) | Maintenance view shows only modules needing action, with one concrete action per state. No global "everything is fine" indicator. |
| User reviews diagnostic before issue creation | VS Code (prefilled Report Issue), Docker (local diagnose, user reviews before sharing ID) | PaperForge creates a GitHub Issue draft; user reviews and redacts before submitting. |
**Patterns that need adaptation:**
| Pattern | Source | Adaptation needed |
|---|---|---|
| Extension isolation principle | VS Code | PaperForge adopts the principle of systematic fault isolation — ability to toggle between known-good and current state — without implementing a literal binary-search command. |
| JIT model loading with TTL | LM Studio, Ollama | Reject for this scope: it addresses model memory pressure, not runtime installation or recovery. Revisit only if measurements show a problem. |
| Rollback via package manager pin | Homebrew, Ollama | Pin pattern is noted; PaperForge defers runtime management mechanism to #70. |
| Host-app update mechanism | Electron, Obsidian | PaperForge cannot own the Obsidian host updater. Plugin updates follow Obsidian's community-plugin flow; backend update ownership is decided in #70. |
**Patterns to reject:**
| Pattern | Reason for rejection |
|---|---|
| Docker-style VM isolation | Overkill for PaperForge; process-level subprocess isolation is sufficient |
| Single-implicit-latest-version (Raycast store model) | PaperForge needs explicit version pinning for reproducibility |
| Auto-cleanup of old versions (Homebrew 30-day) | Derived artifact rebuild is too expensive to auto-cleanup; must be explicit user action |
| Cross-process AppKit rendering (Raycast) | Not applicable; PaperForge renders in Obsidian's existing UI framework |
### 3.3 Security/privacy boundary implications
PaperForge holds medical research literature that **may contain identifiable patient information** in PDFs. The diagnostic export pattern must be stricter than general desktop tools:
1. **All diagnostic exports are opt-in** — never auto-uploaded.
2. **Diagnostic bundle structure**:
- **Safe fields** (always included): module capability states, plugin version, Python version, config keys (not values), error codes, log severity counts.
- **Review-required fields** (user must redact before sharing): log tail lines, paper keys, file paths, environment variable _names_.
- **Never-included** (stripped by default): environment variable values, paper titles, PDF text, file contents, network request payloads.
3. **GitHub Issue draft** is user-reviewed before submission.
---
## 4. Adopt / Adapt / Reject decisions
### 4.1 Adopt (use as-is with configuration only)
| # | Decision | Evidence | Rationale |
|---|---|---|---|
| A1 | **Independent module capability probes** returning states | VS Code Runtime Status, Docker Desktop dashboard, Raycast Extension Diagnostics | Directly resolves the #66 audit finding. Each module probe owns its health. |
| A2 | **Three-tier repair model** (restart → module rebuild → module reset) | Docker Desktop Troubleshoot tiers | Graduated escalation. Each tier scoped to a single module. T1 is fast and safe; T3 rebuilds all derived artifacts for that module while preserving source materials. |
| A3 | **User reviews diagnostic before GitHub issue creation** | VS Code (prefilled Report Issue), Docker (local diagnose, user shares ID) | Privacy boundary: user sees what will be shared before sharing. PaperForge uses local export + user-reviewed GitHub Issue draft; no upload token service. VS Code's prefilled Report Issue is the closer precedent for the draft pattern. |
| A4 | **Compatibility gate before any update** | VS Code, Raycast | Prevents update-induced breakage. |
| A5 | **Plugin renders backend facts without reclassification** | VS Code Runtime Status, Raycast Dashboard | Backend emits canonical state; plugin renders them. No frontend heuristic. |
| A6 | **Per-module actionable maintenance view** | VS Code, Raycast | Only modules needing action are shown, with exactly one action per state. |
| A7 | **Python CLI diagnostic command** (\`paperforge doctor\`) | Homebrew \`doctor\`, Docker Desktop \`diagnose\`, LM Studio \`lms log stream\` | Unified entry point for health check, log export, and repair. |
### 4.2 Adapt (modify for PaperForge's context)
| # | Decision | Source pattern | Adaptation |
|---|---|---|---|
| D1 | **Version pinning** | Homebrew \`pin\`, Ollama model tags | Version metadata should be tracked for runtime and models. Exact pinning mechanism chosen in #70. |
| D2 | **Diagnostic isolation principle** | VS Code Extension Bisect | Adopt the principle of systematic isolation (known-good vs. current state) without implementing a bisect command. |
| D3 | **Rollback via versioned installs** | VS Code "Install Another Version", Ollama Docker tags | Releases carry version metadata; system tracks previous version identity for rollback. Exact mechanism chosen in #70. |
| D4 | **Diagnostic bundle with privacy tiers** | Docker Desktop \`diagnose\` | Three tiers: safe, review-required, never-included. Schema part of diagnostic contract. |
| D5 | **Two-channel update** (plugin via Obsidian, backend via \`paperforge update\`) | Electron autoUpdater (two-channel), Obsidian manual updates | Independent channels. Plugin: Obsidian flow. Backend update source chosen in #70. |
| D6 | **Idempotent and resumable repair** | Docker Desktop multi-tier, VS Code bisect resumable | Checkpoint state per repair step. Cooperative stop reused from #64. |
### 4.3 Reject (not applicable or counterproductive)
| # | Decision | Reason |
|---|---|---|
| R1 | JIT model loading with TTL eviction (LM Studio/Ollama) | Adds complexity without demonstrated need. Revisit if memory pressure arises. |
| R2 | VM-level isolation (Docker Desktop) | Process-level subprocess isolation is sufficient. |
| R3 | Single-implicit-latest-version store model (Raycast) | PaperForge needs explicit version pinning for reproducibility. |
| R4 | Auto-cleanup of old versions (Homebrew 30-day) | Derived artifact rebuild is too expensive. User explicitly deletes or rebuilds. |
| R5 | Cross-process AppKit rendering (Raycast) | PaperForge renders in Obsidian's existing UI. |
| R6 | Electron autoUpdater integration | Not accessible from Obsidian plugin context. |
| R7 | Per-extension permission model (none exists in Obsidian) | Accept Obsidian's all-or-nothing model; design internal controls to compensate. |
---
## 5. Managed-runtime architecture implications for #70
### 5.1 Runtime identity and command resolver
**Observation across studied products**: VS Code, Docker Desktop, Raycast, Ollama, LM Studio, and Homebrew each expose a single, well-known runtime identity (code binary, docker CLI + app, Raycast app, ollama binary, lms CLI, brew command). The Obsidian plugin host is the exception — it provides no single runtime identity for its plugins.
- VS Code: \`code\` binary with well-defined extension host
- Docker Desktop: \`docker\` CLI + Desktop app
- Raycast: Raycast app owns all extension processes
- Ollama: \`ollama\` serves as both CLI and daemon
- LM Studio: \`lms\` CLI for all runtime control
- Homebrew: \`brew\` command
- Obsidian plugins: each loads in the Electron renderer
**Implication for PaperForge**: The audit finding (#66) that Python and TypeScript resolve conflicting path values must be resolved by selecting **one command runtime**. Two viable models:
1. **Plugin-invokes-Python via subprocess** (current model but with one canonical resolver). Requires: managed Python runtime installation, version pinning, diagnostic endpoint via CLI.
2. **Python-backend-as-service** (REST API from plugin to a local Python HTTP server). Requires: daemon lifecycle management, health check endpoint, port conflict resolution.
**Recommendation**: Let the evidence inform but do not choose for #70. Both models have precedent (CLI: Homebrew, Ollama; Service: Docker Desktop, LM Studio). The evidence supports either.
### 5.2 Module health probe contract
VS Code (Runtime Status), Docker Desktop (Dashboard), and Raycast (Extension Diagnostics) all describe module-health state with a consistent shape:
\`\`\`
module: { state, reason_code, summary, action, revision, computed_at }
\`\`\`
**For PaperForge** (reusing the #66 audit proposal verbatim):
\`\`\`json
{
"module": "ocr",
"state": "ready | needs_input | needs_action | running | limited | unavailable | unknown",
"reason_code": "stable-machine-code",
"summary": "localized-human-summary-key",
"action": {
"id": "configure_ocr",
"kind": "open_setting | run_command | open_url | create_issue_draft",
"destructive": false
},
"source_revision": "sha256-of-manifest",
"computed_at": "2026-07-14T12:00:00Z"
}
\`\`\`
Rules (confirmed by cross-product patterns):
- State is **factual**; plugin never infers severity from counts.
- \`unknown\` and stale are **never** success.
- Every \`needs_action\` has **exactly one** primary next action.
- Destructive actions declare data preserved, replaced, and rollback availability.
### 5.3 Update and rollback contract
From VS Code, Ollama, Homebrew, and Electron patterns:
| Operation | Requirement |
|---|---|
| **Discovery** | Expose an explicit update check. Whether checks also run automatically, and at what cadence, is a product decision for #70. |
| **Compatibility gate** | Before any download, verify compatibility: plugin API ↔ backend version, backend ↔ embedding model, schema version ↔ data. |
| **Download** | Atomic download with checksum verification. Resume support for large downloads (models). |
| **Apply** | Plugin follows Obsidian's update flow. Backend application mechanism is chosen in #70 and must be atomic. |
| **Rollback** | Restore previous known-good state. Version metadata tracked to identify candidates. Exact rollback mechanism chosen in #70. |
| **Failure handling** | If update applies but probe returns bad state, auto-suggest rollback. Never leave system in partially-updated state. |
### 5.4 Diagnostic and issue-report contract
From VS Code (prefilled Report Issue) and Docker Desktop (local diagnose → user-reviewed ID sharing):
\`\`\`
paperforge doctor # Quick health overview (plugin view)
paperforge doctor --export <path> # Full diagnostic bundle to local path
paperforge doctor --check <module> # Single module deep probe
\`\`\`
The report flow:
1. User experiences an issue.
2. User runs \`paperforge doctor --export diagnostics.zip\`.
3. User reviews the diagnostic bundle (auto-redacted for PII) and may redact further.
4. User selects "Create Issue Draft" → opens a pre-filled GitHub Issue draft with a redacted text summary. The user may manually attach the reviewed export file.
5. User reviews the draft and submits manually. No automatic upload; no opaque Diagnostic ID.
### 5.5 Cross-platform compatibility
From Docker Desktop and Ollama (both true cross-platform):
- Python runtime must be managed identically across platforms (mechanism chosen in #70).
- File paths: \`paperforge.json\` uses platform-independent path references resolved by the config resolver.
- Subprocess signals: \`SIGTERM\` for cooperative stop on Linux/macOS; \`stdin\` control line for Windows (confirmed working in #64).
- Diagnostic export: bundle format must be platform-agnostic (JSON + zipped logs).
- Update sources must work across platforms; exact mechanism chosen in #70.
---
## 6. Acceptance criteria PaperForge can reuse
### 6.1 Health probe acceptance
\`\`\`
GIVEN a module (e.g., OCR) that is fully set up
WHEN the capability probe runs
THEN state = "ready", reason_code is stable, action is empty
AND computed_at is within TTL of current time
GIVEN a module with stale derived artifacts
WHEN the capability probe runs
THEN state = "needs_action", reason_code identifies the artifact type
AND action.id is the concrete rebuild command
GIVEN a module that has never been set up
WHEN the capability probe runs
THEN state = "needs_input" or "unavailable"
AND action.id points to the setup command
\`\`\`
### 6.2 Update acceptance
\`\`\`
GIVEN the current backend version is N
WHEN \`paperforge update\` checks for updates
THEN it discovers version N+1 with a compatible API contract
AND the download produces a checksum-verified artifact
WHEN the user rolls back to a previous version
THEN version N is restored
AND all module probes return state that matches version N's known-good state
\`\`\`
### 6.3 Diagnostic export acceptance
\`\`\`
GIVEN a diagnostic export to path X
WHEN the user inspects X
THEN the bundle contains:
- Module capability states for all 6 modules
- Plugin version and build metadata
- Python backend version
- Configuration keys (NOT values for sensitive fields)
- Last N lines of log (tagged as "review-required")
- Reason codes for any non-ready module
GIVEN an export marked "review-required" fields
WHEN the bundle would be uploaded
THEN the user has reviewed and approved the review-required fields
\`\`\`
### 6.4 Repair acceptance
\`\`\`
GIVEN a module in "needs_action" state with a destructive action
WHEN the user triggers the action
THEN the system warns about data that will be replaced and whether rollback is available
AND the action proceeds only after explicit user confirmation
GIVEN a rebuild operation that can be interrupted
WHEN the user sends a stop signal
THEN the current operation completes, and the system records a resumable checkpoint
AND the module state returns to "needs_action" (not "unknown")
\`\`\`
### 6.5 Cross-platform acceptance
\`\`\`
GIVEN operations on Windows, macOS, and Linux
WHEN any health probe, update, repair, or diagnostic operation runs
THEN it produces the same capability envelope structure
AND it succeeds or fails with the same reason codes
AND file paths are platform-independent in diagnostic output
\`\`\`
---
## 7. Unresolved questions
1. **Python calling pattern for #70**: Subprocess vs. REST API? Both precedented. The choice affects health probe design (CLI exit codes vs. HTTP status codes), diagnostic flow, and update mechanism.
2. **Embedding model update frequency**: If PaperForge's embedding model updates monthly, does updating the model count as a "module action" in the 6-module model, or a sub-action of the 记忆 module?
3. **Obsidian plugin submission timing**: The automated scanning pipeline (announced by Obsidian) is rolling out. PaperForge must ensure the diagnostic and issue-report flow does not trigger false positives in the scanner (e.g., plugin making network requests must be declared).
4. **Offline mode**: Not all products handle offline the same way. Docker Desktop, VS Code auto-update, and Raycast need periodic online access for updates. Ollama and LM Studio serve cached models fully offline. PaperForge must document which operations degrade gracefully offline (health probes: yes; updates: no; local diagnostic export: yes).
5. **Rollback data integrity**: Does rollback restore previously downloaded model weights, or does the user re-download from Hugging Face?
---
## Evidence index
| Finding | Source | URL |
|---|---|---|
| VS Code Extension Host isolation | VS Code Extension API docs | https://code.visualstudio.com/api/advanced-topics/extension-host |
| VS Code Runtime Status tab | VS Code 1.59 release notes | https://code.visualstudio.com/updates/v1_59 |
| VS Code Extension Bisect | VS Code blog | https://code.visualstudio.com/blogs/2021/02/16/extension-bisect |
| VS Code Install Another Version / rollback | Extension Marketplace docs | https://code.visualstudio.com/docs/configure/extensions/extension-marketplace |
| Docker Desktop CLI diagnose | Docker Desktop CLI reference | https://docs.docker.com/desktop/features/desktop-cli/ |
| Docker Desktop troubleshoot and reset | Docker Desktop troubleshoot | https://docs.docker.com/desktop/troubleshoot-and-support/troubleshoot/ |
| Docker Desktop diagnose command | Docker CLI reference | https://docs.docker.com/reference/cli/docker/desktop/diagnose/ |
| Docker Desktop backup/restore | Docker docs | https://docs.docker.com/desktop/settings-and-maintenance/backup-and-restore/ |
| Docker Desktop purge data issue | docker/for-mac#6758 | https://github.com/docker/for-mac/issues/6758 |
| Raycast extension architecture | Raycast blog | https://www.raycast.com/blog/how-raycast-api-extensions-work |
| Raycast extension diagnostics | Raycast changelog | https://developers.raycast.com/misc/changelog.md |
| Raycast debug an extension | Raycast dev docs | https://developers.raycast.com/basics/debug-an-extension |
| Raycast versioning | Raycast dev docs | https://developers.raycast.com/information/versioning |
| Ollama server routes.go | GitHub (source) | https://github.com/ollama/ollama/blob/c42e9d24/server/routes.go |
| Ollama server/sched.go | GitHub (source) | https://github.com/ollama/ollama/blob/e09b3f9f/server/sched.go |
| Ollama API docs | GitHub | https://github.com/ollama/ollama/blob/9db4bdbad6a4981ad761aa2b603e69e8fb83212c/docs/api.md |
| Ollama model tags and versioning | GitHub README | https://github.com/ollama/ollama?tab=readme-ov-file#model-library |
| LM Studio developer docs | lmstudio.ai | https://lmstudio.ai/docs/developer |
| LM Studio API server | lmstudio.ai | https://lmstudio.ai/docs/api |
| LM Studio CLI reference | lmstudio.ai | https://lmstudio.ai/docs/cli |
| LM Studio lms log stream | lmstudio.ai | https://lmstudio.ai/docs/cli/serve/log-stream |
| Homebrew manpage | GitHub | https://github.com/Homebrew/brew/blob/9b56b133a546d0d40cd6b020290f6a59fc14729f/docs/Manpage.md |
| Homebrew common issues | Homebrew docs | https://docs.brew.sh/Common-Issues |
| Obsidian community plugins | Obsidian help | https://obsidian.md/help/community-plugins |
| Obsidian plugin security | GitHub | https://github.com/obsidianmd/obsidian-help/blob/master/en/Extending%20Obsidian/Plugin%20security.md |
| Obsidian future of plugins | Obsidian blog | https://obsidian.md/blog/future-of-plugins/ |
| Electron autoUpdater | Electron docs | https://github.com/electron/electron/blob/main/docs/api/auto-updater.md |
| Electron updates tutorial | Electron docs | https://github.com/electron/electron/blob/main/docs/tutorial/updates.md |
| PaperForge control-center audit | Working tree | \`docs/research/2026-07-14-control-center-contract-audit.md\` |
---
## Appendix: Comparative decision table
| Capability | VS Code | Docker Desktop | Raycast | Ollama | LM Studio | Homebrew | PaperForge target |
|---|---|---|---|---|---|---|---|
| Multi-module runtime | Yes: Ext. Host | Yes: VM+GUI | Yes: processes | No: single daemon | Yes: 3 components | No: single tool | Yes: plugin+Python |
| Per-module health | Yes: Runtime Status | Yes: Dashboard | Yes: diagnostics cmd | Yes: `ollama ps` | Yes: `lms ps` | No: `brew doctor` is global | Yes: capability probes |
| Actionable maintenance | Yes | Yes | Yes | No: manual model management | No: manual model management | Yes | Yes: module-scoped repair |
| Update compatibility gate | Yes | No: manual rollback | Yes | No: binary swap | No: manual | Partial: pin holds version | Required: plugin/backend/model |
| Rollback mechanism | Install Another Version | Manual download | Not supported | Docker/brew pin | Manual download | `brew switch` | Deferred to #70 |
| Diagnostic export | Clipboard | `diagnose --upload` | Issues Dashboard | Debug logs only | `lms log stream` | `brew doctor` text | `paperforge doctor --export` |
| Privacy review before share | Prefilled draft | Partial: uploads before ID sharing | No: auto-captured | Local only | Local only | Local only | User reviews local export and draft |
| Systematic fault isolation | Extension Bisect | None | None | None | None | None | Principle only; no bisect command |
| Cross-platform | Windows/macOS/Linux | Windows/macOS/Linux | macOS | Windows/macOS/Linux | Windows/macOS/Linux | macOS/Linux | Windows/macOS/Linux |
| Offline capable | Most features | Engine is local; updates/support need network | Store checks need network | Cached models | Cached models | Updates need network | Health probes and local diagnosis |

View file

@ -0,0 +1,653 @@
# Obsidian Native Control Patterns — PaperForge Fit Analysis
- **Date:** 2026-07-14
- **Wayfinder ticket:** [Issue #67](https://github.com/LLLin000/PaperForge/issues/67)
---
## Research Method
Six plugins plus official API docs were studied from source code and official documentation:
| # | Plugin | Version Studied | Repository | Key Areas |
|---|--------|----------------|------------|-----------|
| 1 | **Dataview** | 0.5.70 | [blacksmithgu/obsidian-dataview](https://github.com/blacksmithgu/obsidian-dataview) | Settings tab, imperative `display()`, feature toggles, refresh debouncing |
| 2 | **Templater** | 2.18.1 | [SilentVoid13/Templater](https://github.com/SilentVoid13/Templater) | Per-vault dangerous settings, `ConfirmDangerousSettingModal`, modal-based config, decl settings adoption |
| 3 | **Obsidian Git** | 2.38.6 | [Vinzent03/obsidian-git](https://github.com/Vinzent03/obsidian-git) | External runtime detection (`git` binary), `checkRequirements()` pattern, status bar, `displayError()` |
| 4 | **Tasks** | 8.0.0 | [obsidian-tasks-group/obsidian-tasks](https://github.com/obsidian-tasks-group/obsidian-tasks) | Dynamic settings from `generalSettings`, debug/log options, heading state tracking, i18n |
| 5 | **Obsidian Sample Plugin** | — | [obsidianmd/obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin) | Canonical `PluginSettingTab` implementation |
| 6 | **Obsidian Core API** | 1.13.0+ | [docs.obsidian.md](https://docs.obsidian.md/) | Declarative Settings API, SecretStorage, Platform, Notices, Modals, Commands |
**PaperForge manifest context:** `isDesktopOnly: true`, `minAppVersion: 1.9.0` — confirmed from `manifest.json` in the repo.
---
## Pattern Catalogue
### 1. Settings Tab Architecture
#### Obsidian Convention
**Imperative (v0.9.7+):** Extend `PluginSettingTab`, implement `display()`, build DOM with `new Setting(containerEl)`.
```ts
import { PluginSettingTab, Setting } from 'obsidian';
class MySettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Toggle')
.addToggle(t => t.setValue(val).onChange(async v => { /* save */ }));
}
}
```
Source: [Settings.md — obsidian-developer-docs](https://github.com/obsidianmd/obsidian-developer-docs/blob/31946e5a/en/Plugins/User%20interface/Settings.md)
**Declarative (1.13.0+):** Override `getSettingDefinitions()`, return typed definition objects. Obsidian handles rendering, search indexing, persistence, and validation.
```ts
class MySettingTab extends PluginSettingTab {
getSettingDefinitions() {
return [{
name: 'Enable feature',
control: { type: 'toggle', key: 'enabled' },
}];
}
}
```
Source: [Migrate to declarative settings — obsidian-developer-docs](https://github.com/obsidianmd/obsidian-developer-docs/blob/main/en/Plugins/Guides/Migrate%20to%20declarative%20settings.md)
#### Evidence from Plugins
| Plugin | Approach | minAppVersion | Notes |
|--------|----------|--------------|-------|
| Dataview | Imperative `display()` | 0.12+ | Manual tab state, `Setting` per row |
| Obsidian Git | Imperative `display()` | 0.12+ | Manual tab, `icon` on settings tab |
| Templater | Declarative (recent) + imperative fallback | 1.13.0 (recent) | Dual support via Path B in older versions |
| Tasks | Imperative `display()` | 0.12+ | Uses `customFunctions` map for extensibility |
**Strength of imperative:** Full control over layout, conditional rendering, async loading, custom component composition.
**Limitation of imperative:** No built-in search indexing, manual persistence wiring, no keyboard nav.
**Strength of declarative:** Free search, auto-save, validation, sub-pages, groups with visibility predicates, lists, buttons.
**Limitation of declarative:** Requires 1.13.0+, limited to supported control types, `render` callbacks lose auto-save.
#### PaperForge Fit
PaperForge's current `display()` is 2394 lines of imperative code with custom tab bar. This is **unsustainable** — it duplicates the tab infrastructure Obsidian provides natively. Recommend **Path B (dual support)**: adopt declarative `getSettingDefinitions()` for simple controls (toggles, dropdowns, text inputs, secret components) while keeping imperative `render` callbacks for the custom sections (skills list, status health, embed build controls).
---
### 2. Tab and Section Navigation
#### Obsidian Convention
Use `.setHeading()` on `Setting` objects to demarcate sections. Official guidance: "General settings should be at the top… should not have a heading."
```ts
new Setting(containerEl).setName("Appearance").setHeading();
```
Source: [Settings.md](https://github.com/obsidianmd/obsidian-developer-docs/blob/31946e5a/en/Plugins/User%20interface/Settings.md)
For multi-tab layouts, plugins implement **custom CSS-tab toggling** (like PaperForge already does). The declarative API supports **sub-pages** natively via the `page` property in definitions.
**Obsidian Git** uses a single tab with icons in the settings sidebar (`icon: "git-pull-request"` on the tab class).
**Tasks** uses heading tracking (`headingOpened: HeadingState`) to remember which sections the user has collapsed, persisted in `data.json`.
#### PaperForge Fit
PaperForge's custom tab bar (`.paperforge-settings-tabs`) is well-implemented. Replace the inline `<style>` injection with entries in `styles.css`. The tab labels should map to PaperForge's six target modules: 安装 / 文献库 / OCR / 记忆 / 维护 / 帮助. There is no canonical six-tab layout among studied plugins, but PaperForge's current 4-tab separation (setup / features / maintenance / release-notes) can be expanded to six by splitting the current 功能 tab into per-module tabs.
---
### 3. Onboarding / First-Launch Setup
#### Obsidian Convention
Most plugins **do not have a setup wizard**. They present all settings in the tab and let users configure at will. Exceptions exist for plugins requiring external dependencies:
**Obsidian Git** uses `init()``checkRequirements()` which returns `"missing-git" | "missing-repo" | "valid"`. On `"missing-git"`, it calls `displayError()` with a descriptive Notice. On `"missing-repo"`, it shows a Notice with instructions and offers **commands** (`Create new repo`, `Clone existing repo`) that users can run from the command palette.
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) lines 600-650
```ts
const result = await this.gitManager.checkRequirements();
switch (result) {
case "missing-git":
this.displayError(`Cannot run git command. Trying to run: '${gitPath}' .`);
break;
case "missing-repo":
new Notice("Can't find a valid git repository. Please create one via the given command or clone an existing repo.", 10000);
break;
case "valid":
this.gitReady = true;
// ...
}
```
**Templater** uses per-vault dangerous settings with a **confirmation modal** (`ConfirmDangerousSettingModal`) before enabling risky operations like system command execution. On mobile (where `Platform.isDesktop` is false), the checkbox is replaced with updated button text.
Source: [Templater commit 70a24ce](https://github.com/SilentVoid13/Templater/commit/70a24cefece47be55018b989b255b224a9b3b718)
#### PaperForge Fit
PaperForge's dedicated 安装 tab and `PaperForgeSetupModal` fit Obsidian's modal pattern, but initialization is overly coupled to settings rendering. **Adapt the Obsidian Git pattern**: separate startup capability checks from the wizard UI. The control center should expose setup as the primary action for modules that need input; an onboarding-dismissed marker may control first-run presentation, but `setup_complete` must not represent runtime readiness.
---
### 4. Dependency Detection & External Runtime Boundaries
#### Obsidian Convention
**Obsidian Git** is the canonical reference. It:
1. Defines `checkRequirements(): Promise<'missing-git' | 'missing-repo' | 'valid'>`
2. In `init()`, creates the appropriate `GitManager` implementation (`SimpleGit` on desktop, `IsomorphicGit` on mobile)
3. Uses `Platform.isDesktopApp` to select implementation:
```ts
get useSimpleGit(): boolean { return Platform.isDesktopApp; }
```
4. Stores the Git binary path in `localStorage` (via `LocalStorageSettings`), not `data.json`, so it persists across plugin updates
5. Reports clear errors via `displayError()` — which calls `new Notice()` with the error message
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) lines 600-650, `init()` method
**Templater** on mobile uses a reduced UI (`ConfirmDangerousSettingModal` without checkbox), acknowledging that mobile has different interaction constraints.
#### PaperForge Fit
PaperForge's `resolvePythonExecutable()` already implements a similar strategy — searching for Python in standard locations, checking custom paths, marking stale paths. However, the **error reporting** is mixed: some errors go to `Notice`, others to `console.log`, and the settings tab redraws on every failure. **Adopt the Git pattern exactly**: a dedicated `checkRequirements()` Promise that returns a typed result union, called from `onload()` and re-checkable via command. All external runtime errors should route through a single `displayError()` method that shows a Notice with duration === 8000ms for errors.
PaperForge's `manifest.json` already has `isDesktopOnly: true`, which means Obsidian blocks loading the plugin on mobile entirely. No Platform guards are needed for the current deployment surface. The `Platform` patterns documented here are a prerequisite reference **if mobile support is ever added**.
---
### 5. Error Display Patterns (Notices, Modals, Status Bar)
#### Obsidian Convention
**Notice:** Transient message at bottom of viewport. Duration in ms (default 4000). Use for:
- Operation completion (`sync done`, `OCR started`)
- Transient errors (`command failed: ...`)
- Informational messages (`No results found`)
```ts
new Notice("Message", 5000); // 5 second duration
```
Source: [Notices — docs.obsidian.md](https://docs.obsidian.md/Plugins/User+interface/Notices)
**Modal:** Persistent dialog for decisions or multi-step flows. Extend `Modal`, implement `onOpen()` / `onClose()`. Use for:
- Privacy/consent dialogs
- Multi-field setup steps
- Confirmation before destructive operations
Source: [Modals — docs.obsidian.md](https://docs.obsidian.md/Plugins/User+interface/Modals)
**Status Bar:** `addStatusBarItem()` returns an `HTMLElement`. Use for persistent status (e.g., git branch, sync time).
**Obsidian Git** demonstrates all three:
- `displayError()``new Notice()` with error text
- `GeneralModal` for user input (clone URL, directory selection)
- `BranchModal` for branch switching
- `StatusBar` and `BranchStatusBar` for persistent git state
- `setPluginState()` drives status bar content
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts)
**Tasks** uses `createFragmentWithHTML` for rich descriptions in settings, and `loggingOptions` with per-module log levels for diagnostics.
Source: [Tasks Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts)
#### PaperForge Fit
PaperForge already uses all three patterns but inconsistently:
- Notice duration varies (0, 4000, 5000, 6000, 8000 ms) — **standardize**: 4000 for success, 6000 for warnings, 8000 for errors
- `console.log` for debug info — **migrate** to `loggingOptions` with per-module levels (adopt Tasks pattern)
- Status bar unused — **reserve** it for active operations or one actionable failure; normal readiness remains in the control center
- Error messages mix Chinese and English — **keep** i18n through the existing `t()` function
---
### 6. Command Availability & Conditional Commands
#### Obsidian Convention
```ts
this.addCommand({
id: 'command-id',
name: 'Command name',
callback: () => { /* ... */ }
});
```
For conditional availability, use `checkCallback`:
```ts
this.addCommand({
id: 'conditional-command',
name: 'Do something only when ready',
checkCallback: (checking: boolean) => {
if (!this.gitReady) return false;
if (checking) return true;
// perform action
}
});
```
Source: [Commands — docs.obsidian.md](https://docs.obsidian.md/Plugins/User+interface/Commands)
**Obsidian Git** uses `isAllInitialized()` as its capability probe — it checks the actual `gitManager` state:
```ts
async isAllInitialized(): Promise<boolean> {
if (!this.gitReady) {
await this.init({ fromReload: true });
}
return this.gitReady;
}
```
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) lines 770-775
**Dataview** registers commands with simple callbacks (force refresh, drop cache, rebuild view) — no guards because the commands work regardless of external state.
#### PaperForge Fit
PaperForge registers commands from `ACTIONS` definition array but uses **no `checkCallback` guards** — every command unconditionally runs `resolvePythonExecutable()`. This means commands appear enabled even when Python is missing.
**Adopt `checkCallback` with principle-only guidance** — each command's availability must be gated by an independent capability probe, not by `setup_complete`. Probes must be:
- **Lightweight**: cached results, not full `execFile` calls on every palette open
- **Specific**: each command probes the exact runtime capability it needs
- **Re-evaluated**: on settings change or explicit refresh
**Important:** Path resolution (`resolvePythonExecutable`) is not capability readiness — a file existing at a path does not guarantee the binary works or has the required packages installed. Separate these concerns in the capability model.
The `setup_complete` flag is for **onboarding flow gating** (whether to show the wizard), not for runtime capability checks. Exact probe design is deferred to Issue #69 (Settings Redesign).
---
### 7. Secret/Credential Handling
#### Obsidian Convention
**Current PaperForge pattern:** Stores API keys directly in `data.json` as plaintext:
```ts
paddleocr_api_key: string;
vector_db_api_key: string;
```
Source: [PaperForge constants.ts](https://github.com/LLLin000/PaperForge/blob/master/paperforge/plugin/src/constants.ts)
**Obsidian 1.11.4+ native pattern:** Use `SecretStorage` + `SecretComponent`:
```ts
import { SecretComponent } from 'obsidian';
new Setting(containerEl)
.setName('API key')
.addComponent(el => new SecretComponent(this.app, el)
.setValue(this.plugin.settings.mySetting)
.onChange(value => {
this.plugin.settings.mySetting = value; // stores NAME, not value
this.plugin.saveSettings();
}));
// At runtime:
const secret = app.secretStorage.getSecret(this.settings.mySetting);
```
Source: [Store secrets — obsidian-developer-docs](https://github.com/obsidianmd/obsidian-developer-docs/blob/main/en/Plugins/Guides/Store%20secrets.md)
**Obsidian Git** stores Git credentials through `LocalStorageSettings` (Obsidian's `app.saveLocalStorage()` API), not in `data.json`. Obsidian Git settings that formerly held credentials are migrated to localStorage.
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) `migrateSettings()` method at lines 480-500
#### PaperForge Fit
**Adopt SecretStorage** for `paddleocr_api_key` and `vector_db_api_key`. This requires bumping `minAppVersion` from `1.9.0` to at least `1.11.4`.
**Min-version tradeoff:** PaperForge's current `minAppVersion: 1.9.0` supports Obsidian releases from mid-2024. SecretStorage (1.11.4) was released approximately late 2025. By mid-2026, a 1.11.4 floor is likely sustainable — but the project must explicitly decide:
- **Option A:** Bump to 1.11.4, adopt SecretStorage, remove plaintext keys from settings
- **Option B:** Stay at 1.9.0, keep plaintext keys, migrate later
This decision should be informed by PaperForge's actual user version distribution (not available during this research).
The migration function must: (1) detect existing keys in `data.json`, (2) import them into SecretStorage, (3) remove them from settings on next load. A migration crash must not lose credentials — keep originals in `data.json` until SecretStorage confirms the write.
---
### 8. Progressive Disclosure & Collapsible Sections
#### Obsidian Convention
Plugins implement collapsible sections with CSS + click handlers:
**Tasks:** Uses `headingOpened` in settings to persist which sections are expanded:
```ts
headingOpened: HeadingState = { 'section-id': true };
```
Source: [Tasks Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts)
**PaperForge:** Already implements this pattern with `_advCollapsed` + CSS toggle. The `getDisclosureState`/`toggleDisclosureState` utility at `src/utils/disclosure.ts` wraps the pattern cleanly.
**Templater:** Uses modals for complex settings (folder templates, regex templates) instead of inline collapsible sections, keeping the main settings tab clean.
Source: [Templater commit 8d41bf9](https://github.com/SilentVoid13/Templater/commit/8d41bf9a35187d221e26fba284ba3a94ba27af8e)
**Declarative API equivalent:** Groups with `visible` predicates:
```ts
{
type: 'group',
name: 'Advanced Settings',
visible: () => settings.showAdvanced,
children: [ /* ... */ ],
}
```
#### PaperForge Fit
PaperForge's collapsible "Advanced" section and skills collapsible groups are well-implemented. Suggested improvements:
- Persist collapse state in `data.json` (currently `_advCollapsed` lives in the tab instance, so it resets on re-render)
- **Consider Templater's pattern**: move complex multi-field configuration (embed build controls, vector DB deps install) into dedicated modals, keeping the settings tab focused
---
### 9. Platform Constraints
#### Obsidian Convention
```ts
import { Platform } from 'obsidian';
// Platform.isDesktop, Platform.isMobile
// Platform.isIosApp, Platform.isAndroidApp
// Platform.isPhone, Platform.isTablet
```
Source: [Platform API — docs.obsidian.md](https://docs.obsidian.md/Reference/TypeScript+API/Platform)
**Obsidian Git** uses `Platform.isDesktopApp` to choose between `SimpleGit` (desktop, native binary) and `IsomorphicGit` (mobile, JS implementation).
**Templater** removes the checkbox in `ConfirmDangerousSettingModal` on mobile and uses updated button text instead.
Source: [Templater commit 70a24ce](https://github.com/SilentVoid13/Templater/commit/70a24cefece47be55018b989b255b224a9b3b718)
#### PaperForge Fit
PaperForge's `manifest.json` declares `isDesktopOnly: true`, which means Obsidian blocks the plugin from loading on mobile entirely. No runtime Platform guards are needed for the current deployment surface.
**If mobile support is ever added**, the following patterns become prerequisites:
1. All `execFile`/`spawn`/`exec` calls must be guarded by `Platform.isDesktopApp` checks
2. Mobile-relevant operations must fall back to a Node.js/WASM implementation (analogous to Obsidian Git's `IsomorphicGit`)
3. Python runtime detection must return `"not-available"` on mobile
4. The settings tab must handle the absence of Python gracefully
This report documents the Platform patterns from mature plugins as a **reference for that future decision**, not as a current gap.
---
### 10. Diagnostics, Support & Logging
#### Obsidian Convention
**Tasks** provides a structured logging pattern:
```ts
interface LogOptions {
minLevels: {
'': string; // root logger
'tasks': string; // module-level
'tasks.Cache': string;
'tasks.Events': string;
'tasks.File': string;
'tasks.Query': string;
'tasks.Task': string;
};
}
```
Source: [Tasks Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts) lines 90-107
Each module logs at its configured minimum level (`debug` | `info` | `warn` | `error`). The settings UI exposes an About section with a "Show debug log" button.
**Dataview** logs version info on load:
```ts
console.log(`Dataview: version ${this.manifest.version} (requires obsidian ${this.manifest.minAppVersion})`);
```
Source: [Dataview main.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/main.ts) line 120
#### PaperForge Fit
PaperForge logs only via `console.log`/`console.warn` with no structured logging. **Adopt the Tasks pattern**: define `LogOptions` with per-module levels (`sync`, `ocr`, `embed`, `memory`, `doctor`) and create a debug section in the 维护 tab that shows recent log entries and provides a "Copy diagnostics" button for support.
---
### 11. Settings Persistence & Migration
#### Obsidian Convention
**Tasks** has a mature migration pattern:
```ts
function migrateSettings(loadedSettings: any): Partial<Settings> {
const migratedSettings = { ...loadedSettings };
if ('includes' in migratedSettings && !('presets' in migratedSettings)) {
migratedSettings.presets = migratedSettings.includes;
delete migratedSettings.includes;
}
// Add future migrations here as needed
return migratedSettings;
}
```
Source: [Tasks Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts)
**Obsidian Git** also migrates settings in `migrateSettings()`:
```ts
async migrateSettings(): Promise<void> {
if (this.settings.mergeOnPull != undefined) {
this.settings.syncMethod = this.settings.mergeOnPull ? "merge" : "rebase";
this.settings.mergeOnPull = undefined;
await this.saveSettings();
}
// ...
}
```
Source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts)
#### PaperForge Fit
PaperForge has no centralized schema-versioned settings migration — defaults are applied with `Object.assign()` which only works for new installations. **Adopt the Tasks migration pattern**: add a `migrateSettings()` function called from `loadSettings()` that handles:
- SecretStorage key import (if minAppVersion is bumped)
- Adding new default fields to existing configs
- Handling `_python_path_stale` cleanup
---
### 12. Internationalization (i18n)
#### Obsidian Convention
**Tasks** uses a custom `i18n` module with JSON locale files:
```ts
import { i18n } from '../i18n/i18n';
new Setting(containerEl)
.setName(i18n.t('settings.format.displayName.tasksEmojiFormat'))
```
Source: [Tasks SettingsTab.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/SettingsTab.ts)
**PaperForge** already has an i18n system (`i18n.ts` + `t()` function) covering Chinese and English.
#### PaperForge Fit
PaperForge's i18n is ahead of the majority of Obsidian plugins. Keep it. Minor suggestion: call `setLanguage(this.app)` earlier in `onload()` (before any UI registration) to prevent initial language flash.
---
### 13. Release Notes & Update Notification
#### Obsidian Convention
Most plugins use `CHANGELOG.md` only. **Obsidian Git** references its `CHANGELOG.md` for release notes.
**PaperForge** implements a dedicated release-notes tab rendered from a structured `release-notes.json` file, with version comparison against `last_seen_version`. This goes beyond what the studied plugins do.
#### PaperForge Fit
PaperForge's release-notes tab is a well-designed feature that should be kept. Minor improvement: visually indicate when a new version has been released since last open (like VS Code's changelog badge), using `last_seen_version` comparison in `_checkReleaseNotes()`.
---
## Evidence Matrix
| Pattern | Source Plugin | Source URL | Observable Code Seam | Strength | Limitation | PaperForge Fit |
|---------|--------------|-----------|---------------------|----------|------------|---------------|
| Settings Tab (imperative) | Dataview | [src/main.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/main.ts) | `addSettingTab(new GeneralSettingsTab(...))` | Full DOM control | No search indexing | Keep for complex tabs; adopt declarative for simple controls |
| Declarative Settings | Templater | [commit 957a175](https://github.com/SilentVoid13/Templater/commit/957a17541a3bc87eac0fe6e4729ccbeaef42f199) | `getSettingDefinitions()` override | Free search, auto-save, validation | Requires 1.13.0+ | Path B (dual support) |
| Dependency Detection | Obsidian Git | [src/main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) L600-660 | `checkRequirements()` → union result | Clear error states, retry flow | N/A | Adopt directly |
| External Runtime Selection | Obsidian Git | [src/main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) | `Platform.isDesktopApp` for git impl | Platform-adaptive | N/A | Reference for future mobile support |
| Secret/Key Storage | Obsidian Core | [Store secrets guide](https://github.com/obsidianmd/obsidian-developer-docs/blob/main/en/Plugins/Guides/Store%20secrets.md) | `SecretComponent` + `app.secretStorage.getSecret()` | Vault-keyed, cross-plugin, no plaintext | Requires 1.11.4+ | Adopt for API keys; requires minAppVersion decision |
| Per-Vault Dangerous Settings | Templater | [commit 957a175](https://github.com/SilentVoid13/Templater/commit/957a175) | `ConfirmDangerousSettingModal`, vault-scoped flags | Social engineering mitigation | More UI clicks | Consider for auto-update/exec features |
| Settings Migration | Tasks | [src/Config/Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts) | `migrateSettings()` function | Backward compat, explicit | Must be maintained | Adopt directly |
| Structured Logging | Tasks | [src/Config/Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts) L90-107 | `LogOptions.minLevels` per module | Debuggable, user-controllable | Configuration overhead | Adopt for sync/ocr/embed |
| Conditional Commands | Obsidian Git | [src/main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) | `isAllInitialized()` guard via `checkCallback` | Prevents no-op execution | N/A | Adopt with independent capability probes |
| Status Bar | Obsidian Git | [src/main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) | `addStatusBarItem()` + `StatusBar` class | Persistent, glanceable | Limited real estate | Adopt for sync status |
| Error Display | Obsidian Git | [src/main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) | `displayError()``new Notice()` | Consistent routing | Duration not standardized | Standardize durations |
| Privacy/Consent Modal | PaperForge (self) | [src/views/modals.ts](https://github.com/LLLin000/PaperForge) | `PaperForgeOcrPrivacyModal` | Clear consent flow | N/A | Keep |
| Collapsible Sections (persisted) | Tasks | [src/Config/Settings.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/Settings.ts) | `headingOpened: HeadingState` | Persisted state | Minor complexity | Adopt persistence |
| Setup Wizard Modal | PaperForge (self) | [src/views/modals.ts](https://github.com/LLLin000/PaperForge) | `PaperForgeSetupModal` | Step-by-step | Coupled to settings tab | Decouple from settings |
| Plugin Version Logging | Dataview | [src/main.ts](https://github.com/blacksmithgu/obsidian-dataview/blob/master/src/main.ts) L120 | `console.log` on load | Simple, informative | Not user-visible | Adopt with logging module |
| i18n | Tasks | [src/Config/SettingsTab.ts](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/b157cadb/src/Config/SettingsTab.ts) | `i18n.t('key')` throughout | Full localization | Maintenance cost | Keep existing |
| Release Notes Tab | PaperForge (self) | [src/settings.ts](https://github.com/LLLin000/PaperForge) | `release-notes.json` + render tab | Structured, version-tracked | N/A | Keep |
| Modal-Based Complex Config | Templater | [commit 8d41bf9](https://github.com/SilentVoid13/Templater/commit/8d41bf9) | `FileRegexTemplateModal`, `FolderTemplateModal` | Clean settings tab | Extra modal navigation | Consider for embed build controls |
---
## Adopt / Adapt / Reject Decisions
### Adopt (directly integrate without significant change)
| Pattern | Rationale |
|---------|-----------|
| `checkRequirements()` result union | Replaces ad-hoc `resolvePythonExecutable` + status string checks; enables typed error states |
| `migrateSettings()` function | PaperForge has no centralized schema-versioned settings migration; needed for schema evolution |
| `LogOptions` with per-module levels | Structured debugging essential for external runtime issues |
| `checkCallback` with principle guidance | Prevents users from clicking commands that cannot work; probes must be independent capability checks, not setup_complete. Path resolution ≠ capability readiness |
| Minimal status bar for active work or one actionable failure | Keeps transient operation state glanceable without duplicating the normal control-center dashboard |
| `displayError()` / `displayMessage()` | Consolidates scattered Notice creation patterns |
| Persist collapse state in `data.json` | Currently resets on every re-render |
### Adapt (modify for PaperForge context)
| Pattern | Adaptation |
|---------|------------|
| Declarative Settings (Path B) | Adopt simple controls (toggles, dropdowns, text) while keeping imperative `render` for skills list, embed build, health display |
| SecretStorage | API keys → SecretStorage; config URL/model stays in settings. Migration function required. Requires minAppVersion decision (1.9.0 → ≥1.11.4) |
| Per-vault dangerous settings | Not applicable to `auto_update_on_startup` — plugin `data.json` is already vault-local per Obsidian's plugin isolation model |
| Templater's modal-based complex config | Move embed build controls + vector DB dependency install into dedicated modals instead of inline collapse |
| Notice duration standardization | 4000ms success, 6000ms warnings, 8000ms errors — currently inconsistent |
### Reject (not suitable for PaperForge)
| Pattern | Rationale |
|---------|-----------|
| Pure declarative settings (Path A) | PaperForge has too many custom UI components (skills list, live status, embed progress) that can't be expressed declaratively |
| Heading tracking for all sections | Overkill for PaperForge's multi-tab layout; current tab-based navigation is sufficient |
| Full Svelte/React settings (some plugins use frameworks) | Would require build system changes; PaperForge's vanilla TS approach matches the current `esbuild` setup |
---
## Pattern Guidance for the Six PaperForge Modules
The approved information architecture remains **安装 / 文献库 / OCR / 记忆 / 维护 / 帮助**. This research does not define the final controls; Issues #69 and #71 own that design.
| Module | Verified current seams | Pattern guidance |
|---|---|---|
| 安装 | `paperforge/plugin/src/settings.ts`, `views/modals.ts`, `services/python-bridge.ts` | Keep progressive setup and validation in a focused modal. Separate capability checks from settings rendering and from onboarding history. |
| 文献库 | Path settings and sync actions in `settings.ts` and `main.ts` | Simple stored values may use declarative settings after a minimum-version decision; live validation and sync actions remain imperative. |
| OCR | Privacy modal in `views/modals.ts`; maintenance rendering in `settings.ts` and `services/ocr-maintenance-ui.ts` | Preserve explicit consent. Gate actions on OCR capability facts, not on a global setup flag. |
| 记忆 | Runtime snapshots in `services/memory-state.ts`; controls in `settings.ts` | Render backend-owned memory and vector states independently. Keep progress and rebuild actions imperative. |
| 维护 | OCR maintenance service, doctor/repair command paths, and maintenance tab rendering | Show only actionable or currently running work. Put advanced diagnostics behind disclosure and keep destructive actions explicit. |
| 帮助 | `release-notes.json`, release-notes rendering, and `last_seen_version` | Keep dynamic release notes and support guidance together. Issue reporting must open a user-reviewed draft rather than submit automatically. |
Across all six modules, reuse one capability vocabulary and one action vocabulary once #69 defines them. Avoid duplicating backend classification rules in the plugin.
### Implementation Notes (cross-module)
- **Tab architecture:** Keep the current custom CSS-tab pattern through the redesign; consider declarative sub-pages only if `minAppVersion` is deliberately raised to 1.13.0+.
- **Declarative mix:** Simple settings can use declarative definitions; complex live sections remain imperative. Dual support avoids forcing a version decision in this research ticket.
- **Settings persistence:** Add a centralized schema-versioned migration path before changing stored settings, especially before any SecretStorage adoption.
- **i18n:** Route all new user-facing settings text through the existing `t()` function.
### Cross-Module: Status Bar
Use the status bar exclusively for active background operations or a single failure needing user attention. Normal readiness state belongs in the control center (dashboard view), not in the status bar.
Pattern source: [Obsidian Git main.ts](https://github.com/Vinzent03/obsidian-git/blob/master/src/main.ts) uses status bar only for git action state and branch display, driven by `setPluginState()` — it shows the current operation (pulling, committing) when active, and is minimal otherwise.
### Cross-Module: Conditional Command Availability
Commands that depend on external runtime availability **must** use `checkCallback` for conditional enablement. Probes must be independent capability checks, not `setup_complete`:
```ts
this.addCommand({
id: 'paperforge-sync',
name: 'PaperForge: Sync Library',
checkCallback: (checking: boolean) => {
if (!capabilityProbe.pythonAvailable()) return false;
if (checking) return true;
// perform sync
}
});
```
Capability probes must be lightweight (cached results, not subprocess calls) and re-evaluated on settings change or explicit refresh.
**Important:** Path resolution (`resolvePythonExecutable`) checks that a file exists at the configured path — that is **path resolution, not capability readiness**. A file existing at a path does not guarantee the binary works or has the required packages installed. Separate these concerns:
- **Path resolution** → lights-on check (during `checkRequirements()`)
- **Capability readiness** → per-command probe (Python can actually run the target module)
Exact probe design is deferred to Issue #69 (Settings Redesign).
## Unresolved Questions
1. **Minimum Obsidian version target:** PaperForge's current `minAppVersion: 1.9.0` predates SecretStorage (1.11.4+) and declarative settings (1.13.0+). Adopting either pattern requires a version bump. This research cannot determine the user version distribution — that data lives in the plugin's Obsidian dashboard stats. The team should check dashboard analytics and decide whether the 1.9.0 floor is still necessary.
2. **Settings tab DOM weight on lower-end desktops:** The 2394-line `display()` method constructs the full DOM for all tabs on every call. Lazy rendering (build tab DOM only when selected) would improve initial render time.
3. **SecretStorage migration safety:** Existing users have API keys in `data.json`. The migration must import existing keys into SecretStorage on first launch after upgrade, then remove them from `data.json`. If the migration crashes, users lose credentials. Strategy: keep originals in `data.json` until SecretStorage confirms the write.
4. **Status bar on mobile (if ever supported):** Obsidian's status bar exists on mobile but has limited space. Follow the same principle: only active operations or a single failure; normal readiness in the control center.
5. **i18n maintenance burden:** PaperForge's i18n is comprehensive, but adding a new locale requires translating ~200+ strings. Consider whether community contributions for additional locales are expected.
---
## Appendix: Source URLs
| Resource | URL |
|----------|-----|
| Obsidian Settings (imperative) | https://github.com/obsidianmd/obsidian-developer-docs/blob/31946e5a/en/Plugins/User%20interface/Settings.md |
| Declarative Settings Migration | https://github.com/obsidianmd/obsidian-developer-docs/blob/main/en/Plugins/Guides/Migrate%20to%20declarative%20settings.md |
| Secret Storage Guide | https://github.com/obsidianmd/obsidian-developer-docs/blob/main/en/Plugins/Guides/Store%20secrets.md |
| Platform API | https://docs.obsidian.md/Reference/TypeScript+API/Platform |
| Notices API | https://docs.obsidian.md/Reference/TypeScript+API/Notice |
| Modals API | https://docs.obsidian.md/Reference/TypeScript+API/Modal |
| Commands API | https://docs.obsidian.md/Plugins/User+interface/Commands |
| Dataview | https://github.com/blacksmithgu/obsidian-dataview |
| Templater | https://github.com/SilentVoid13/Templater |
| Obsidian Git | https://github.com/Vinzent03/obsidian-git |
| Tasks | https://github.com/obsidian-tasks-group/obsidian-tasks |
| Sample Plugin | https://github.com/obsidianmd/obsidian-sample-plugin |
| PaperForge Manifest | manifest.json in repo (isDesktopOnly: true, minAppVersion: 1.9.0) |

View file

@ -1,5 +1,5 @@
# OCR-v2 Active Queue
> Status: OCR-v2 is stable; OCR maintenance streaming is implemented, the control-center UX is charted, and the current setup/readiness/recovery contract audit is resolved.
> Status: OCR-v2 is stable; OCR maintenance streaming is implemented, the control-center UX is charted, and the audit plus both reference-product research gates are resolved.
> Last updated: 2026-07-14
## Current checkpoint
@ -10,6 +10,8 @@
- The maintenance tab now exposes all papers plus the canonical `_needs_derived_rebuild()` recommendation set, selected batch actions, an above-table progress state, and full refresh on completion.
- Source Corpus data remains authoritative and was not modified during verification. Only the deployed plugin bundle and disposable maintenance cache were refreshed.
- [Current-contract audit](https://github.com/LLLin000/PaperForge/issues/66#issuecomment-4968837257) identified the migration boundary: preserve durable OCR/SQLite truth and recovery actions; replace global setup state, duplicate runtime/config resolution, and freshness-free snapshots.
- [Obsidian-native research](https://github.com/LLLin000/PaperForge/issues/67#issuecomment-4970653461) preserves the six-module IA, progressive setup, capability-gated commands, and minimal transient status chrome.
- [Desktop runtime/recovery research](https://github.com/LLLin000/PaperForge/issues/68#issuecomment-4970660288) establishes module-scoped repair, compatibility-gated updates, local redacted diagnostics, and user-reviewed issue drafts.
## Verification status
@ -21,8 +23,8 @@
## Frontier
- [ ] Define the shared module capability-state and action vocabulary in [#69](https://github.com/LLLin000/PaperForge/issues/69), now unblocked by the audit.
- [ ] Complete [Obsidian-native patterns](https://github.com/LLLin000/PaperForge/issues/67) and [desktop installation/health/recovery patterns](https://github.com/LLLin000/PaperForge/issues/68); keep the managed-runtime decision blocked until #68 closes.
- [ ] Define the shared module capability-state and action vocabulary in [#69](https://github.com/LLLin000/PaperForge/issues/69).
- [ ] Choose the managed-runtime ownership, installation, update, rollback, and health contracts in [#70](https://github.com/LLLin000/PaperForge/issues/70); its audit and research blockers are closed.
- [ ] Keep routine OCR quality outside maintenance; successful updates leave the queue, while unacceptable results use a user-reviewed GitHub Issue draft.
- [ ] Close or follow up [Unified rebuild UX](https://github.com/LLLin000/PaperForge/issues/63) after the OCR slice lands.