diff --git a/README.md b/README.md
index e89731e..41deafe 100644
--- a/README.md
+++ b/README.md
@@ -1,328 +1,244 @@
# Visual Notes
-> A living concept map of your day, generated from your Obsidian daily notes.
+> Turn an Obsidian daily note into a living concept map.
-You write in your daily note. Visual Notes reads it, extracts the day's
-concepts and how they connect, and renders an interactive graph at the
-top of the note — refreshed automatically as you write.
+Visual Notes watches the markdown files you already use for daily notes,
+asks Claude to extract the main concepts and relationships, and renders an
+interactive Cytoscape.js graph directly inside Obsidian.
-It captures **everything in the file** — AI session summaries, manually
-typed notes, mobile edits, content from any source. The markdown is the
-universal interface; whatever lands there ends up in the visual.
+The markdown note stays the source of truth. Manual notes, AI session
+summaries, mobile edits, Templater output, and synced changes all flow
+through the same pipeline: if it lands in a watched note, it can appear in
+the visual.
-```
- ┌─────────────────────────────────┐
- │ 20260501-overview.html │
- │ │
- │ ┌─[hook fix]──┐ │
- │ │ ▼ │
- │ │ ┌─────────────┐ │
- │ │ │ matcher/if │ │
- │ │ │ split │ │
- │ │ └──────┬──────┘ │
- │ │ │ powers │
- │ │ ▼ │
- │ │ ┌─────────────┐ │
- │ │ │ PostToolUse │ │
- │ │ │ hook │ ◄──┐ │
- │ │ └─────────────┘ │ │
- │ │ │ │
- │ │ ┌──[Brian's CDP]──┐ │ │
- │ │ ▼ │ │ │
- │ │ ◇ context ─ ─ ┘ │ │
- │ │ │ │
- │ └──── (cross-domain)──────┘ │
- │ dashed │
- └─────────────────────────────────┘
- inline at top of daily note
-```
-
-*(Above: stylized representation of an actual rendered overview. Real output
-uses Catppuccin colors; rectangles for systems, ellipses for tasks, diamonds
-for decisions; thick edges for strong relationships, dashed for cross-domain.)*
+## Why it is useful
----
+- **A visual daily recap:** see the day's work, decisions, blockers, and
+ cross-domain connections at a glance.
+- **Works with any note source:** Claude Code, OpenCode, Copilot, manual
+ typing, and mobile edits all become ordinary markdown input.
+- **No separate graph editor:** edit the note; Visual Notes regenerates the
+ graph sidecar.
+- **Useful for memory and navigation:** nodes summarize important concepts;
+ labeled edges explain why they matter.
-## What this is
-
-The repo houses **two plugins** that share a JSON sidecar schema:
-
-| Plugin | What it does | Required? |
-|---|---|---|
-| **Obsidian plugin** (TypeScript) | Watches markdown, calls Claude API, renders Cytoscape inline | Yes (primary artifact) |
-| **Claude Code plugin** (markdown + bash) | Lets AI agents pre-populate the sidecar before LLM extraction runs | Optional |
-
-Either plugin works alone. They compose if you have both.
-
----
-
-## How it works (concept)
+## What it does
```mermaid
flowchart LR
- subgraph Sources["Anything that edits the note"]
- A1[Claude Code]
- A2[claude.ai mobile/web]
- A3[Manual typing]
- A4[Obsidian Sync from another device]
+ classDef input fill:#eef2ff,stroke:#4f46e5,stroke-width:2px,color:#111827
+ classDef plugin fill:#ecfeff,stroke:#0891b2,stroke-width:2px,color:#0f172a
+ classDef ai fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#111827
+ classDef data fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#052e16
+ classDef render fill:#fae8ff,stroke:#c026d3,stroke-width:2px,color:#111827
+ classDef future fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,stroke-dasharray: 5,5,color:#334155
+
+ subgraph Sources["Everyday note inputs"]
+ Manual["Manual notes"]
+ Sessions["AI session summaries"]
+ Mobile["Mobile edits"]
+ Templates["Templates and automations"]
end
- Note[("Daily note YYYYMMDD.md")]
- Plugin[Obsidian plugin]
- API((Claude API))
- Side[("YYYYMMDD-overview.json sidecar")]
- Viz["Concept map rendered inline"]
-
- Sources --> Note
- Note -->|on save| Plugin
- Plugin -->|markdown content| API
- API -->|graph JSON| Plugin
- Plugin --> Side
- Side --> Viz
- Viz -. embedded in .-> Note
-```
-The thesis: **the markdown file is the universal interface**. Anything that
-edits it triggers the plugin; the visual reflects whatever lands in the
-file, regardless of who wrote it.
+ Note[("Watched daily note YYYYMMDD.md")]
----
+ subgraph Plugin["Visual Notes Obsidian plugin"]
+ Watch["Watch + debounce"]
+ Hash["Hash unchanged notes"]
+ Extract["Extract graph"]
+ Validate["Validate schema"]
+ Render["Render inline"]
+ end
-## Install
+ Claude(("Anthropic Claude"))
+ Sidecar[("Graph sidecar YYYYMMDD-overview.json")]
+ Pane["Interactive Cytoscape map inside the note"]
+ Future["Future: section-aware idempotent updates"]
-### For Obsidian users (recommended path)
+ Manual --> Note
+ Sessions --> Note
+ Mobile --> Note
+ Templates --> Note
+ Note --> Watch --> Hash --> Extract --> Claude
+ Claude --> Validate --> Sidecar --> Render --> Pane
+ Pane -. displayed above note content .-> Note
+ Future -. planned evolution .-> Hash
-```
-1. Install BRAT in Obsidian
-2. In BRAT settings, add: bobthearsonist/visual-notes
-3. Open Visual Notes settings, paste your Anthropic API key
-4. Add at least one folder to "Watched folders" (your daily-notes folder; add multiple if you keep separate work/personal/project journals)
-5. Edit a daily note → visual appears at the top
+ class Manual,Sessions,Mobile,Templates,Note input
+ class Watch,Hash,Extract,Validate,Render plugin
+ class Claude ai
+ class Sidecar data
+ class Pane render
+ class Future future
```
-Full instructions: [`plugins/obsidian-plugin/README.md`](plugins/obsidian-plugin/README.md).
+## What it looks like in Obsidian
-### For Claude Code users (optional companion)
+Visual Notes renders as a card at the top of the note in both reading and edit
+views. The graph is interactive; the note remains normal markdown underneath.
-```
-/plugin install bobthearsonist/visual-notes/plugins/claude-code-plugin
-```
+
-Full instructions: [`plugins/claude-code-plugin/README.md`](plugins/claude-code-plugin/README.md).
+Feature overview:
----
+- Watches one or more configured daily-note folders.
+- Debounces saves and skips unchanged content using a markdown hash.
+- Sends the full note markdown to the Anthropic Messages API.
+- Validates the returned graph against the shared sidecar schema.
+- Writes `{date}-overview.json` next to the note.
+- Renders the sidecar inline in reading and source views.
+- Supports pin/unpin/delete/regenerate commands for manual control.
+- Shows extraction count and status in the Obsidian status bar.
+- Tracks token usage and estimated cost metadata in the sidecar when the API
+ response includes usage.
-## Settings
+## Project pieces
-```
-┌─ Visual Notes ─────────────────────────────────────┐
-│ │
-│ Anthropic API key [••••••••••••••••] [Show] │
-│ │
-│ Watched folders [Captains Log ] [×] │
-│ [0 Daily ADHD Brain ] [×] │
-│ [+ Add folder] │
-│ ⚠ Required. Empty = inactive │
-│ │
-│ Debounce (ms) [1500 ] │
-│ │
-│ Model [Haiku 4.5 ▼] │
-│ │
-│ ▶ Advanced (custom prompt, debug logging…) │
-│ │
-└────────────────────────────────────────────────────┘
-```
+This repository contains two plugins and one shared schema:
-Watch as many folders as you want — work daily notes, personal daily
-notes, per-project journals — all share the same model and prompt.
-The list is intentionally empty by default so the plugin stays inert
-until you configure it.
-
-Commands available in the command palette:
-
-- **Visual Notes: Extract from current note** — manual extraction
-- **Visual Notes: Regenerate (force)** — discard the cached hash + ignore `_pinned`, with a 30s per-file cooldown
-- **Visual Notes: Pin this overview** — sets `_pinned: true`; the plugin will skip auto-extraction for this note
-- **Visual Notes: Unpin this overview** — clears `_pinned`; auto-extraction resumes
-- **Visual Notes: Delete sidecar** — removes the sidecar JSON; next save auto-re-extracts
+| Piece | Purpose | Status |
+|---|---|---|
+| [`plugins/obsidian-plugin`](plugins/obsidian-plugin/README.md) | Primary Obsidian plugin. Watches notes, calls Claude, writes sidecars, and renders Cytoscape inline. | MVP implementation in progress |
+| [`plugins/claude-code-plugin`](plugins/claude-code-plugin/README.md) | Optional companion for agent-curated sidecars after AI session summaries. | Scaffolded; migration pending |
+| [`shared/schema.json`](shared/schema.json) | JSON Schema contract for sidecar graph files. | Defined |
+| [`docs/design.md`](docs/design.md) | Living design document for open/future work. | Maintained as decisions evolve |
----
+The Obsidian plugin is the main product. The Claude Code plugin is optional:
+it can pre-populate or pin curated sidecars, but Visual Notes does not depend
+on Claude Code.
-## Detailed architecture
+## Architecture at a glance
-The high-level flow above hides the lifecycle. Here's the full pipeline:
+```text
+Daily note folder
+├── 20260501.md # source markdown
+└── 20260501-overview.json # generated graph sidecar
-```mermaid
-sequenceDiagram
- autonumber
- participant User
- participant Vault as Obsidian Vault
- participant Plugin as Visual Notes plugin
- participant API as Anthropic API
- participant View as Markdown view
-
- User->>Vault: save daily note (Cmd-S, idle save, etc.)
- Vault->>Plugin: vault.on('modify')
- Plugin->>Plugin: debounce 1.5s (configurable)
- Plugin->>Plugin: SHA-256 the markdown body
- Plugin->>Vault: read existing sidecar (if any)
- alt hash matches sidecar._lastProcessedHash
- Plugin->>Plugin: skip — already extracted
- else hash differs OR no sidecar
- Plugin->>View: status-bar: "Visual Notes: extracting…"
- Plugin->>API: messages.parse({system, user, output_config})
- API-->>Plugin: structured JSON {nodes, edges, ...}
- Plugin->>Vault: write {date}-overview.json
- Vault->>View: trigger MarkdownPostProcessor refresh
- View->>View: load sidecar → mount/update Cytoscape
- Plugin->>View: status-bar: clear
- View-->>User: rendered concept map
- end
+Obsidian plugin
+├── settings tab # API key, watched folders, debounce, model
+├── file watcher # only watched markdown files
+├── extractor # requestUrl -> Anthropic Messages API
+├── schema validation # Zod + shared/schema.json
+└── renderer # Cytoscape in MarkdownRenderChild
```
-### Component boundaries
+Important invariants:
-```
-┌────────────────────────────────────────────────────────────────┐
-│ visual-notes/ │
-│ │
-│ ┌─────────────────────────────┐ │
-│ │ shared/ │ │
-│ │ schema.json │ │
-│ │ (the contract: nodes, │ │
-│ │ edges, status, kind…) │ │
-│ └──────┬──────────────┬───────┘ │
-│ │ consumed by │ │
-│ ┌───────────┘ └─────────────┐ │
-│ ▼ ▼ │
-│ ┌─────────────────────┐ ┌──────────────────────┐ │
-│ │ Obsidian plugin │ │ Claude Code plugin │ │
-│ │ (TypeScript) │ │ (markdown + bash) │ │
-│ │ │ │ │ │
-│ │ PRIMARY │ │ OPTIONAL │ │
-│ │ │ │ │ │
-│ │ Distribution: │ │ Distribution: │ │
-│ │ community store / │ │ /plugin install … │ │
-│ │ BRAT │ │ │ │
-│ └─────────────────────┘ └──────────────────────┘ │
-│ │
-└────────────────────────────────────────────────────────────────┘
-```
+1. The `.md` note is read-only input for the plugin.
+2. The sidecar JSON is the source of truth for rendered graph data.
+3. `_pinned: true` on a sidecar suppresses automatic re-extraction unless the
+ user runs force regenerate.
+4. The renderer tolerates unsupported future sidecar kinds by showing a
+ placeholder instead of crashing.
-The two plugins do NOT depend on each other. Either alone provides full
-functionality; both together compose with **last-writer-wins** sidecar
-semantics, escape-hatched via `_pinned: true` (see [§5.2 of the design
-doc](docs/design.md#52-coexistence-with-obsidian-plugin)).
+## Install and setup
-### Repository layout
+### Obsidian plugin
-```
-visual-notes/
-├── README.md # this file
-├── LICENSE # MIT
-├── docs/
-│ └── design.md # full design — start here for contributing
-├── shared/
-│ └── schema.json # the JSON Schema contract
-└── plugins/
- ├── claude-code-plugin/
- │ ├── README.md
- │ ├── .claude-plugin/plugin.json
- │ ├── hooks/
- │ └── skills/visual-notes/
- └── obsidian-plugin/
- ├── README.md
- ├── manifest.json
- ├── package.json
- ├── prompts/
- │ └── extract-graph.md
- └── src/
+Until a release is published, use the development workflow:
+
+```bash
+pnpm install
+pnpm --filter @visual-notes/obsidian-plugin build
```
----
+Then copy or symlink `plugins/obsidian-plugin` into your vault's
+`.obsidian/plugins/visual-notes` directory and enable **Visual Notes** in
+Obsidian's Community plugins settings.
-## Coexistence: when both plugins are installed
+After enabling:
-If you install both, here's what happens:
+1. Open **Settings → Visual Notes**.
+2. Paste an Anthropic API key.
+3. Add at least one watched folder, such as `Daily Notes` or `Captains Log`.
+4. Choose a debounce and model, or keep the defaults.
+5. Save or manually extract a note with
+ **Visual Notes: Extract from current note**.
-```
-agent appends session summary to daily note
- │
- │ Bash hook fires
- ▼
- agent writes curated sidecar ── _pinned: true (sticky) ──┐
- │ │
- │ 1.5s debounce │
- ▼ │
- Obsidian plugin's file watcher │
- │ │
- ├─ check sidecar: is _pinned: true? ───────────────┤
- │ │
- │ yes (from agent) no │
- │ │ │ │
- │ ▼ ▼ │
- │ skip extraction extract via Claude API ──> overwrite sidecar
-```
+See the plugin README for detailed Obsidian-specific instructions:
+[`plugins/obsidian-plugin/README.md`](plugins/obsidian-plugin/README.md).
-The agent path is **deliberate, curated** content. The plugin path is
-**LLM extraction** of whatever's in the file. `_pinned` lets the agent
-say "I'm authoritative; don't overwrite me." Without it, the plugin's
-extraction always wins eventually (last-writer-wins).
+### Claude Code companion plugin
----
+The Claude Code companion is scaffolded but not yet migrated from the
+original private workflow. Its intended install path is:
-## Status
+```text
+/plugin install bobthearsonist/visual-notes/plugins/claude-code-plugin
+```
-| Component | Status |
-|---|---|
-| Design doc | ✅ Complete (see `docs/design.md`) |
-| Repo scaffold | ✅ Complete |
-| Sidecar schema | ✅ Defined (`shared/schema.json`) |
-| Obsidian plugin | 🚧 Phase 1 — scaffold only |
-| Claude Code plugin | 🚧 Migration pending from private dotfiles repo |
-| Distribution | ⏳ Awaiting first release |
+See [`plugins/claude-code-plugin/README.md`](plugins/claude-code-plugin/README.md).
----
+## Commands
-## Cost expectations
+The Obsidian command palette exposes:
-The Obsidian plugin sends the full markdown content of a daily note to
-Anthropic's API on every (debounced, deduped) save. At default settings
-(Claude Haiku 4.5, 1.5s debounce):
+- **Visual Notes: Extract from current note** — manually extract the active
+ markdown file unless its sidecar is pinned.
+- **Visual Notes: Regenerate (force)** — bypasses cached hash and pin state,
+ with a per-file cooldown.
+- **Visual Notes: Pin this overview** — preserves the current sidecar from
+ automatic replacement.
+- **Visual Notes: Unpin this overview** — resumes automatic extraction.
+- **Visual Notes: Delete sidecar** — removes the generated graph sidecar.
-- **~$0.006 per extraction**
-- Typical day: 5–15 extractions
-- Monthly cost: **~$2–5**
+## Privacy and cost
-Bring your own API key. The plugin does not proxy or aggregate usage.
+Visual Notes sends the **full markdown content** of watched notes to the
+Anthropic API when extraction runs. Do not add folders containing notes you do
+not want sent to a third-party API.
-A status-bar indicator shows today's extraction count so you can spot
-runaway behavior without a full dashboard.
+Bring your own API key; this project does not proxy requests or aggregate
+usage. At the default Haiku model, a typical extraction is designed to cost
+only a few fractions of a cent, but actual spend depends on note length,
+model, save frequency, and retries. The plugin shows today's extraction count
+and stores usage metadata when available.
----
+## Development
-## Privacy
+Requirements:
-The plugin sends the **full content** of your daily note to the Claude
-API for extraction. By default Anthropic does not retain content beyond
-the request lifetime, but read [their privacy
-policy](https://www.anthropic.com/legal/privacy). Don't put sensitive
-content in any watched folder.
+- Node.js 20+
+- pnpm
----
+Common commands:
-## Contributing
+```bash
+pnpm install
+pnpm build
+pnpm typecheck
+pnpm lint
+```
-This repo is in design phase. Read [`docs/design.md`](docs/design.md)
-before opening issues or PRs.
+Repository layout:
-- **Architecture decisions:** see `docs/design.md` §10 (Open Questions)
-- **Patterns to follow:** see `docs/design.md` §4–7
-- **Implementation phases:** see `docs/design.md` §9
+```text
+visual-notes/
+├── README.md
+├── docs/
+│ └── design.md
+├── shared/
+│ ├── package.json
+│ └── schema.json
+└── plugins/
+ ├── obsidian-plugin/
+ │ ├── README.md
+ │ ├── manifest.json
+ │ ├── prompts/extract-graph.md
+ │ └── src/
+ └── claude-code-plugin/
+ ├── README.md
+ ├── .claude-plugin/plugin.json
+ ├── hooks/
+ └── skills/visual-notes/
+```
-Major design changes go via PR to `docs/design.md`. Decisions get an
-ADR-style record under `docs/decisions/`.
+## Deeper docs and planning
----
+- [Obsidian plugin README](plugins/obsidian-plugin/README.md)
+- [Claude Code plugin README](plugins/claude-code-plugin/README.md)
+- [Living design document](docs/design.md)
+- [Project issues](https://github.com/bobthearsonist/visual-notes/issues)
## License
diff --git a/docs/assets/obsidian-note-preview.svg b/docs/assets/obsidian-note-preview.svg
new file mode 100644
index 0000000..1cd89f5
--- /dev/null
+++ b/docs/assets/obsidian-note-preview.svg
@@ -0,0 +1,121 @@
+
diff --git a/docs/design.md b/docs/design.md
index 5d81d8c..e69e156 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -1,1185 +1,239 @@
-# Visual Notes — Design Document
+# Visual Notes — Living Design Document
-**Version:** 0.1 (design phase)
-**Date:** 2026-05-02
-**Status:** Approved for implementation
+**Status:** living design notes for remaining and future work
+**Last updated:** 2026-05-02
-> A system that watches daily-note markdown in Obsidian, extracts a concept-map
-> graph via an LLM, and renders an interactive visual at the top of the note.
-> Replaces an earlier Claude-Code-hook-driven approach that could only see
-> content the agent passed through tool calls — leaving manual edits invisible
-> to the visual.
+This document is intentionally future-facing. Product overview, current setup,
+and the stable architecture summary live in the top-level
+[`README.md`](../README.md). This file records design work that is still open,
+planned, or likely to change.
----
+## Design principles to preserve
-## 1. Vision & Goals
+These are the constraints future changes should not casually break:
-### Why this exists
+1. **Markdown remains the universal input.** The Obsidian plugin reacts to
+ watched `.md` files and does not require a specific AI client.
+2. **The plugin does not modify note markdown.** It reads notes and writes a
+ sibling sidecar (`{date}-overview.json`) only.
+3. **The sidecar is the render contract.** The renderer consumes
+ `shared/schema.json`; optional producers may write the same schema.
+4. **Every edge label carries meaning.** A graph with unlabeled or generic
+ edges is less useful than a smaller, accurate graph.
+5. **Pinning is authoritative.** `_pinned: true` tells the Obsidian plugin not
+ to overwrite a curated sidecar unless the user explicitly force-regenerates.
-Today's daily-note workflow blends:
+## Current implementation snapshot
-- AI-written session summaries appended via `obsidian append` (from Claude Code, OpenCode, and other AI clients)
-- Manually-typed notes (meeting summaries, asides, status, ideas)
-- Auto-generated content (Dataview queries, Templater output)
+The Obsidian plugin has an MVP implementation:
-A previous iteration generated an iframe-embedded Cytoscape concept map by
-having the AI agent write a JSON sidecar after each session. That worked but
-**structurally cannot capture content the agent didn't write itself**. Manual
-edits, content from other AI clients, and offline-typed mobile notes are all
-invisible to the visual.
+- settings tab for API key, watched folders, debounce, and model
+- watched-folder save handling with debounce
+- content hash dedup via `_lastProcessedHash`
+- Anthropic Messages API extraction through Obsidian `requestUrl`
+- tool-based structured graph output validated with Zod
+- sidecar writes stamped with producer/schema/hash/pin/usage metadata
+- inline Cytoscape rendering in Obsidian
+- status bar extraction count
+- command palette controls for extract, force-regenerate, pin, unpin, and
+ delete sidecar
-The plugin path makes the **markdown file the universal interface**: whoever
-modifies it (Claude Code, claude.ai web/mobile, OpenCode, Cline, manual
-typing on any device) → the plugin reacts and updates the visual.
+The Claude Code plugin remains scaffolded. Its full hook/skill migration is
+future work.
-### Goals
+## Remaining design work
-1. **Categorical completeness.** The visual reflects the entire day's content,
- not curated subsets.
-2. **Cross-platform.** Runs wherever Obsidian runs (desktop + mobile).
-3. **Cross-client decoupled.** Independent of which AI agent (if any) wrote
- the source content.
-4. **Self-contained installable artifact.** A user with no Claude Code
- subscription can install just the Obsidian plugin and benefit.
-5. **Deterministic rendering.** Same markdown → same visual structure (modulo
- layout). Visual quality is the LLM's responsibility but consistency comes
- from a fixed schema and prompt.
+### Section-aware updates
-### Before / after comparison
+Current extraction reads the whole note and rewrites the whole sidecar. That is
+simple and correct, but it can cost more than necessary and makes every save a
+full-graph regeneration.
-For users currently on the Claude-Code-hook-driven workflow:
+Future design target:
-| Concern | Before (hook-driven) | After (plugin-driven) |
-|---|---|---|
-| Trigger | Agent runs `obsidian append` | Anything that writes to the .md file |
-| Sidecar author | Agent designs graph + writes JSON | LLM extraction reads the .md, produces JSON |
-| Manual edits | Invisible to the visual | Captured on next save |
-| Mobile edits | Invisible | Captured after sync |
-| Multi-client (Copilot, OpenCode, etc.) | Each needs its own integration | Each writes markdown; plugin handles the rest |
-| Cost basis | Agent token spend on graph design | Anthropic API spend per save (~$0.006) |
-| Dependency on Claude Code | Required | Optional (works with no AI client at all) |
-
-### Non-goals
-
-- A general-purpose graph editor.
-- Cross-note relationship visualization (that's ExcaliBrain's territory; we're
- scoped to per-note concept extraction).
-- Real-time collaborative editing.
-- A fancy settings dashboard (cost UI, model picker, prompt-template editor).
- Minimum viable settings: API key, watched folders (list), debounce ms.
-
----
-
-## 2. System Architecture
-
-### Three-component system
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ Daily note folder (in vault) │
-│ │
-│ ┌─────────────────────┐ ┌─────────────────────┐ │
-│ │ 20260501.md │ │ 20260501- │ │
-│ │ (markdown, │ │ overview.json │ │
-│ │ the source of │ │ (sidecar, written │ │
-│ │ truth for input) │ │ by either plugin) │ │
-│ │ │ └─────────────────────┘ │
-│ │ Read by Obsidian │ ▲ │
-│ │ plugin's file │ │ read at render │
-│ │ watcher │ │ time + on change │
-│ └──────────┬──────────┘ │ │
-│ │ │ │
-│ │ vault.on('modify') │ │
-│ ▼ │ │
-│ ┌──────────────────────────────┐ │ │
-│ │ Obsidian plugin │──────┘ │
-│ │ - reads markdown │ writes sidecar │
-│ │ - calls Claude API │ │
-│ │ - mounts Cytoscape inline │ │
-│ │ via MarkdownPostProcessor │ │
-│ └──────────────────────────────┘ │
-│ │
-│ Note: the plugin never modifies the daily note (.md) itself. │
-│ Reading-only on .md, read+write on the .json sidecar. │
-└─────────────────────────────────────────────────────────────────┘
- ▲ ▲
- │ │
- ┌────────────┴───────────┐ ┌───────────────┴──────────────┐
- │ Obsidian plugin │ │ Claude Code plugin │
- │ (primary) │ │ (optional companion) │
- │ │ │ │
- │ - Watches markdown │ │ - Hook on `obsidian append` │
- │ - Calls Anthropic API │ │ - May write sidecar with │
- │ - Renders inline │ │ `_pinned: true` to claim │
- │ Cytoscape via │ │ authoritative ownership │
- │ MarkdownPostProc. │ │ - Skill: design heuristics │
- │ - Honors `_pinned` │ │ for agent pre-population │
- │ on the sidecar │ │ │
- └────────────────────────┘ └───────────────────────────────┘
-```
-
-The two plugins **share a JSON schema** (defined in `shared/schema.json`) for
-the sidecar file. They do NOT need to be installed together — either alone
-suffices for the user's workflow.
-
-**Invariants** (any future contributor must preserve):
-
-1. The Obsidian plugin **only reads** the `.md` file. It writes the `.json`
- sidecar. This avoids feedback loops with any future hook that watches
- `.md` files.
-2. The sidecar is the **single source of truth** for visual content. Anyone
- wanting to influence the visual writes the sidecar.
-3. `_pinned: true` on a sidecar **suppresses LLM extraction**. Treat as a
- contract, not a hint.
-
-### Data flow: Obsidian plugin path (primary)
-
-```
-User saves daily note
- ↓
-vault.on('modify') fires
- ↓
-Debounce 1.5s (Obsidian's built-in debounce())
- ↓
-Hash check: SHA-256 of markdown body vs. sidecar's _lastProcessedHash
- ↓
-If unchanged → skip (handles Obsidian Sync's per-device modify storm)
-If changed ↓
- ↓
-Anthropic Messages API call (Haiku 4.5 default)
- - System: extraction prompt + schema + few-shot examples
- - User: full markdown content
- - output_config: structured JSON via zodOutputFormat
- ↓
-Parse response into typed object (zod-validated)
- ↓
-Write {date}-overview.json sidecar (with new _lastProcessedHash)
- ↓
-Trigger MarkdownPostProcessor refresh of the daily note's view
- ↓
-Cytoscape mounts in MarkdownRenderChild container, reading the sidecar
-```
-
-### Data flow: Claude Code plugin path (legacy / complementary)
-
-The existing Claude-Code-hook-driven system continues to work. When the agent
-runs `obsidian append` to a Captain's Log file, a PostToolUse hook injects a
-prompt asking the agent to update the visual. The agent reads the daily note
-and writes the sidecar JSON directly. Last-writer-wins on the sidecar; either
-system can populate it.
-
-This is intentionally kept as a coexistence pattern, not a replacement: users
-who don't have Claude Code installed still get full functionality from the
-Obsidian plugin alone.
-
----
-
-## 3. Shared Sidecar Schema
-
-The contract between the two plugins. Lives at `shared/schema.json` as a
-JSON Schema document; both plugins import it (the Obsidian plugin generates
-TypeScript types via `json-schema-to-typescript`; the Claude Code plugin
-references it in skill documentation).
-
-### Format
-
-```json
-{
- "title": "Daily Overview - 2026-05-01",
- "header": "Daily Overview",
- "subtitle": "2026-05-01 — Hook fix · sidecar architecture · if-syntax decoded",
- "_lastProcessedHash": "sha256:abc123...",
- "_extractedBy": "obsidian-plugin@0.1.0",
- "nodes": [
- {
- "data": { "id": "kebab-id", "label": "Display\nLabel" },
- "classes": "system completed",
- "position": { "x": 250, "y": 200 }
- }
- ],
- "edges": [
- {
- "data": { "source": "id-a", "target": "id-b", "label": "verb phrase" },
- "classes": "strong-edge"
- }
- ]
-}
-```
-
-### Field semantics
-
-- `title` / `header` / `subtitle`: optional. Auto-derived from filename + date
- when omitted.
-- `_lastProcessedHash`: SHA-256 of the markdown content the sidecar was
- generated from. Used to skip redundant API calls.
-- `_extractedBy`: identifier for the producer (so debugging can attribute
- whether the Obsidian plugin or the Claude Code skill wrote a given sidecar).
-- `nodes[].classes`: one type class + one status class.
- - **Type:** `system` (rectangle), `task` (ellipse), `decision` (diamond)
- - **Status:** `completed` (green), `active` (yellow), `context` (blue), `blocked` (red)
-- `edges[].classes`: optional `strong-edge` (thick line) or `weak-edge`
- (dashed). Default is regular weight.
-- `position`: pixel coordinates. **Currently provided by the LLM**; future
- versions may switch to Cytoscape's native layout (decision deferred — see
- §10 Open Questions).
-
-### Versioning
-
-Schema is at `v0.1.0` (matches initial repo version). Breaking changes bump
-minor. Both plugins must agree on the schema version they support; the
-sidecar gets an optional `_schemaVersion` field once we hit v1.0.
-
----
-
-## 4. Obsidian Plugin Design
-
-### 4.1 Components
-
-```
-plugins/obsidian-plugin/
-├── manifest.json # Plugin manifest (id, version, minAppVersion)
-├── package.json # npm dependencies
-├── esbuild.config.mjs # Build config (from obsidian-sample-plugin)
-├── tsconfig.json
-├── styles.css # Plugin styles + Cytoscape container CSS
-├── prompts/
-│ └── extract-graph.md # The structured-output prompt template
-└── src/
- ├── main.ts # Plugin entry, lifecycle, event registration
- ├── extractor.ts # Anthropic API client, structured output
- ├── renderer.ts # MarkdownPostProcessor, Cytoscape mount
- ├── settings.ts # PluginSettingTab + persisted SettingsSchema
- ├── theme.ts # Map Obsidian CSS vars → Cytoscape style
- ├── storage.ts # API key storage (SecretStorage on desktop, data.json on mobile)
- ├── debounce.ts # Wraps Obsidian's debounce() with content-hash dedup
- └── schema.ts # Generated TypeScript types from shared/schema.json
-```
-
-### 4.2 Lifecycle
-
-**Plugin load (`onload()`):**
-1. Load settings from `data.json`
-2. Initialize secure storage helper (probe `app.vault.getAdapter().getSecretStorage()`)
-3. Register `PluginSettingTab`
-4. Register `vault.on('modify')` event with debounced + hash-checked handler
-5. Register `MarkdownPostProcessor` for files matching any watched-folder pattern
-6. Register `app.workspace.on('css-change')` for theme refresh
-
-**File save:**
-1. `vault.on('modify')` fires
-2. Path filter: only files inside any of the watched folders (recursive),
- only `.md` files (not the sidecar `.json` itself, which would create
- a feedback loop)
-3. Pass to debounced handler (1.5s wait, configurable)
-4. Hash check the markdown body; skip if unchanged from sidecar's
- `_lastProcessedHash`
-5. Call `extractor.extract(markdownContent)`
-6. Write sidecar JSON via `vault.modify()` (atomic write, triggers another
- modify event but the hash check prevents loop)
-7. Notify any open MarkdownPostProcessor instances of the file to re-render
-
-**Markdown view render (`MarkdownPostProcessor`):**
-1. For each rendered daily note, check for sibling `{date}-overview.json`
-2. If sibling exists, mount a `MarkdownRenderChild` at the top of the rendered
- markdown
-3. The child loads the sidecar, applies theme variables, mounts Cytoscape
-4. Cache the Cytoscape instance by file path (don't re-mount on every view)
-
-### 4.3 LLM Integration
-
-**Transport: hand-rolled `requestUrl` against the REST endpoint.** Do NOT
-wrap the official `@anthropic-ai/sdk`. The SDK uses `fetch` internally; on
-mobile (Capacitor WebView) `fetch` is restricted by CORS. `requestUrl`
-from Obsidian is the only path that works cross-platform. Hand-rolling
-~30 lines of REST + Zod validation is simpler than monkey-patching the
-SDK's transport.
-
-**Validation: `zod ^3.25.0`.** Define a `GraphSchema` (Zod) mirroring
-`shared/schema.json`; parse the response body through it. If validation
-fails, treat as a soft error (log + retry with a "your previous response
-was malformed JSON, please retry following the schema" follow-up message).
-
-**API request shape (concrete):**
-
-```typescript
-const body = {
- model: 'claude-haiku-4-5', // user-configurable
- max_tokens: 2048,
- system: systemPrompt, // bundled extraction prompt
- messages: [{ role: 'user', content: markdownContent }],
- output_config: { format: { type: 'json_schema', schema: graphJsonSchema } }
-};
-
-const response = await requestUrl({
- url: 'https://api.anthropic.com/v1/messages',
- method: 'POST',
- headers: {
- 'x-api-key': apiKey,
- 'anthropic-version': '2023-06-01',
- 'content-type': 'application/json',
- },
- body: JSON.stringify(body),
- throw: false, // we handle non-2xx ourselves
-});
-```
-
-The response's `content[0].text` is the JSON string; parse + validate
-against `GraphSchema`.
-
-**Type-safety pipeline.** Without the SDK, type safety lands client-side:
-
-```
-requestUrl(...) // returns RequestUrlResponse
- ↓ .text // string
- ↓ JSON.parse(...) // any (UNSAFE)
- ↓ GraphSchema.parse(...) // typed Graph (Zod) ← belt
- ↓ rendered // Cytoscape consumes
-```
-
-The Anthropic API's `output_config.format.schema` is the server-side
-schema enforcer (suspenders). The Zod parse is the client-side
-enforcer (belt). Both layers are kept because:
-- Server-side enforcement reduces malformed responses (cheaper retry)
-- Client-side validation produces typed objects (no `any` leaks)
-- If they ever disagree, our code crashes loudly rather than silently
- consuming bad data.
-
-**Default model:** Claude Haiku 4.5. Concept extraction is a
-pattern-matching task, not deep reasoning. ~$0.006/call, ~$2-5/month at
-10 extractions/day. Sonnet 4.6 available as a settings opt-in (3× cost).
-Opus is overkill — not exposed as a default option.
-
-**Token budget pre-flight:**
-- Estimate input tokens client-side (rough char/4 heuristic) before
- sending; reject notes over 100k tokens with a Notice.
-- Typical daily note: 1,250–3,750 input tokens.
-- System prompt: ~2,000 tokens.
-- Output: 500–1,000 tokens.
-
-**Error handling (bounded retry):**
-
-| HTTP status | Action |
-|---|---|
-| 200 | Parse + validate against Zod schema. On Zod fail, retry once with schema-correction prompt. |
-| 400 (bad request) | Log + Notice. Don't retry. |
-| 401 (auth) | Notice with "open settings" affordance. Don't retry. |
-| 429 (rate limit) | Honor `retry-after` header, exponential backoff, **max 3 retries**, then queue for next manual trigger (status-bar widget shows "queued"). |
-| 5xx | Exponential backoff, **max 3 retries**, then queue. |
-| Network failure | Treat as 5xx. |
-
-All retries respect a **single `AbortController`** held on the Plugin
-instance for the plugin's lifetime. Created in `onload()`; `abort()`
-called in `onunload()`. Every `requestUrl` call passes the
-controller's `.signal`, so plugin-disable cancels both in-flight
-requests and any backoff-waiting queue entries. One controller, not
-one-per-call — keeps lifecycle simple.
-
-**Sidecar reload event-emitter** lives on the Plugin instance as
-`this.sidecarEvents = new Events()` (Obsidian's built-in `Events`
-class). Each `MarkdownRenderChild` constructor receives a reference to
-the plugin and subscribes to `sidecarEvents.on('changed', filePath, …)`
-in `onload()`, unsubscribes in `onunload()`. The extractor fires the
-event after a successful write. No module-level singletons; teardown
-is clean when the plugin is disabled.
-
-**Logging strategy:** use `console.debug` for routine operations,
-`console.warn` for recoverable problems (sidecar kind unknown,
-malformed sidecar repaired), `console.error` for terminal errors that
-also fire a Notice. No telemetry / analytics in v0.1. The Plugin class
-exposes a `log(level, msg, data?)` helper that namespaces every
-message with `[visual-notes]` so users filtering DevTools can find
-plugin output quickly.
-
-**No streaming.** Fire-and-forget extraction; the JSON response is small
-enough (~750 output tokens) to render instantly when complete.
-
-### 4.4 Rendering: Cytoscape inline
-
-**Pattern:** `registerMarkdownPostProcessor()` → `MarkdownRenderChild` →
-mount Cytoscape into a `
` injected at the top of the rendered note.
-
-**Theme integration:** Read Obsidian CSS variables at mount time:
-```typescript
-const cs = getComputedStyle(document.documentElement);
-const theme = {
- bg: cs.getPropertyValue('--background-primary').trim(),
- text: cs.getPropertyValue('--text-normal').trim(),
- accent: cs.getPropertyValue('--accent-color').trim(),
- // ... map to Cytoscape's style format
-};
-```
-
-Subscribe to `app.workspace.on('css-change')` → rebuild Cytoscape style on
-theme toggle (no re-mount needed, just `cy.style().fromJson(...).update()`).
-
-**Caching strategy.** Don't use a static `Map` —
-Obsidian creates multiple `MarkdownRenderChild` instances for the same
-file across split panes, hover previews, embedded references, and tab
-switches. A path-keyed singleton causes one view's `onunload()` to
-dispose a Cytoscape instance another view is using.
-
-Instead: **the Cytoscape instance lives on the `MarkdownRenderChild`**.
-Each child gets its own instance, mounted on `onload()` and disposed in
-`onunload()`. Multiple views of the same file = multiple Cytoscape
-instances; this is fine because graph data is small and instance
-construction is fast (~tens of ms).
-
-If profiling later shows this is too expensive, fall back to a
-`WeakMap` keyed by the container element — but only if measurement
-demands it.
-
-**Sidecar reload:** When the sidecar JSON is rewritten by extraction,
-notify all live `MarkdownRenderChild` instances watching that file via
-a shared event-emitter. Each instance loads the new graph data into its
-own Cytoscape via `cy.json({ elements: { ... } })`. No remount, no
-flicker — just a data update.
-
-**Why not iframe?** The legacy approach used a `file://` iframe with a
-self-contained HTML file. Inside a plugin, we own the renderer — direct
-Cytoscape mount is simpler, faster, theme-integrated, no sandbox
-concerns. The iframe path is dropped in v0.1.
-
-### 4.5 Settings UI
-
-Minimum viable. `PluginSettingTab` with these fields:
-
-| Setting | Type | Default | Storage |
-|---|---|---|---|
-| Anthropic API key | text (password style) | empty | SecretStorage on desktop, data.json on mobile (with warning) |
-| Watched folders | list of text inputs (add/remove buttons) | empty list (forces explicit config) | data.json |
-| Debounce (ms) | number | 1500 | data.json |
-| Model | dropdown (Haiku 4.5 / Sonnet 4.6) | Haiku 4.5 | data.json |
-
-**Watched folders is a list, not a single value.** Many users have
-multiple daily-note folders (work + personal, or per-project). The
-plugin watches all of them; each folder produces its own per-day
-sidecars in-place. There is no per-folder configuration — same prompt,
-same model, same schema across folders. If a user needs different
-behavior per folder (e.g., different model for personal vs work),
-that's a future feature; v0.1 keeps the dial uniform.
-
-The "Watched folders" default is **deliberately empty**. The plugin
-stays inert until the user adds at least one folder. On plugin load, if
-the list is empty AND the API key is set, surface a one-time Notice:
-"Visual Notes: add a watched folder in Settings to enable extraction."
-
-Watcher logic: `vault.on('modify')` fires for any file change. The
-plugin checks whether the file's parent (or any ancestor) is in the
-watched-folders list. If yes, queue for extraction. If no, ignore.
-Subfolders inherit watch by default (e.g., adding `Captains Log`
-also watches `Captains Log/2026/`).
-
-**Command palette entries** (always available):
-
-| Command | Behavior |
-|---|---|
-| `Visual Notes: Extract from current note` | Manual extraction. Bypasses debounce, runs immediately. Useful for "the visual is stale, force it." Honors `_pinned: true` and silently no-ops on pinned sidecars (with a Notice "sidecar is pinned — unpin first"). |
-| `Visual Notes: Regenerate (force)` | Discards the cached `_lastProcessedHash` AND ignores `_pinned: true`. Useful when the LLM produced a bad graph and you want a fresh attempt regardless of pin state. **Rate-limit guard: 30-second cooldown per file.** Repeated invocations within the cooldown silent-no-op with a Notice "regenerate cooldown — wait Ns"; protects against rage-click rate-limit blowouts. |
-| `Visual Notes: Pin this overview` | Sets `_pinned: true` on the current note's sidecar. Suppresses future LLM extractions until unpinned. Use this when the current visual is exactly what you want kept (e.g., agent-curated graph you don't want overwritten). |
-| `Visual Notes: Unpin this overview` | Sets `_pinned: false`. Resumes auto-extraction on next save. |
-| `Visual Notes: Delete sidecar` | Removes the sidecar JSON (and the rendered visual). Escape hatch. Fires Notice "sidecar deleted — next save will re-extract" so the user knows the recovery path. |
-
-**Status bar:** A small indicator showing today's extraction count and a
-spinner during in-flight calls ("Visual Notes: extracting…"). Replaces a
-full cost-tracking dashboard for v0.1; gives users enough visibility to
-catch runaway behavior without complexity.
-
-- **"Today" boundary**: local midnight in the OS timezone (vault has no
- timezone concept; OS is the closest stable proxy). Persisted to
- `data.json` as `{date: "YYYY-MM-DD", count: N}`. Resets on first
- extraction after midnight.
-- **Color/state**: gray when configured + idle; yellow with spinner
- during in-flight; red when configuration is incomplete (no API key
- OR empty watched-folders list). Red state is the "high-discoverability cue"
- for first-run users who haven't finished setup.
-- **First-run Notice**: after the first successful extraction in a
- fresh install, fire a one-time Notice (gated by `firstRunComplete: false`
- in `data.json`): _"Visual Notes: first extraction succeeded. Cost ~$0.006
- per save at default model. See settings to change."_ Sets cost
- expectations at the moment they matter.
-- **401 affordance**: on 401, fire a `Notice` with text _"Visual Notes:
- API key invalid. Open Settings → Visual Notes."_ at 8s duration on
- desktop, 15s on mobile. Obsidian Notices don't natively support
- clickable links; the text-instruction pattern is the standard
- Obsidian-plugin idiom.
-
-**Cut from MVP:**
-- Custom prompt override (textarea). Premature on day 1; users haven't
- yet hit cases where the bundled prompt fails them.
-- Cost dashboard. Status-bar count is sufficient.
-- Per-folder configs.
-
-**Settings migration.** `data.json` carries an internal field
-`_settingsVersion` (semver string, separate from the plugin's manifest
-version). Plugin `onload()` reads `_settingsVersion`; if the value is
-older than the current code expects, runs an idempotent migration
-function before settings are bound to the UI. The migration list is a
-chain of `(from → to)` transformations checked in declared order; each
-should be safe to re-run. v0.1 ships with `_settingsVersion: "0.1.0"`
-and an empty migration list — the convention is established before it
-becomes painful to add.
-
-### 4.6 Sync collision handling
-
-Multi-device problem: every device running Obsidian Sync sees its own
-`vault.on('modify')` for the same change. Without dedup, N devices each
-hit the API for the same content.
-
-Solution: **content-hash dedup**.
-
-```typescript
-// Schema requires the "sha256:" prefix; prepend it once at compute time.
-const hash = 'sha256:' + sha256(markdownContent); // hex-digest, 64 chars
-const sidecar = await readSidecarIfExists(notePath);
-if (sidecar?._lastProcessedHash === hash) {
- return; // Already extracted this exact content
-}
-const result = await extract(markdownContent);
-result._lastProcessedHash = hash;
-await writeSidecar(notePath, result);
-```
-
-This solves the most common sync race. Two narrower windows remain:
-
-**Mid-flight race:** Device A starts extraction at T=0; device B
-receives the same markdown via Sync at T=0.5; B's hash check sees no
-sidecar yet (A hasn't written) → B starts a duplicate extraction. Both
-write sidecars; last-writer-wins, but the user paid the API call twice.
-
-Mitigation (deferred to Phase 6, documented as known limitation here):
-write a `.lock` placeholder sidecar before the API call (atomic rename
-on completion). Or use a content-addressed-temp-file + rename pattern.
-
-**Concurrent edits across devices:** Device A and B both edit a daily
-note simultaneously while offline; Obsidian Sync resolves the markdown
-conflict; both devices then trigger extraction on the merged result. In
-practice this manifests as one extra API call (the second device sees
-its own merged hash mismatch the first device's sidecar). Acceptable
-for v0.1.
-
-**Document as known limitation in the README + plugin settings help
-text.** No data corruption, just occasional duplicate API spend.
-
----
-
-## 5. Claude Code Plugin Design
-
-### 5.1 Components
-
-```
-plugins/claude-code-plugin/
-├── .claude-plugin/
-│ └── plugin.json # Manifest (name, version, description)
-├── hooks/
-│ ├── hooks.json # Wrapper format (PostToolUse → Bash matcher)
-│ ├── post-obsidian-append.md # Frontmatter + prompt body
-│ └── run-hook.sh # Generic hook runner; honors match_content frontmatter
-└── skills/
- └── visual-notes/
- ├── SKILL.md # Stripped-down: points users at the Obsidian plugin
- └── references/ # (optional, may stay empty post-migration)
-```
-
-### 5.2 Coexistence with Obsidian plugin
-
-Both plugins write the same sidecar schema. The Claude Code path stays
-useful for:
-
-- Users who write detailed session summaries via AI clients and want the
- agent to control visualization narrative (e.g., highlight specific nodes)
-- Workflows where the agent needs to inject curated graph state that the
- LLM extraction wouldn't produce automatically
-
-The two systems compose:
-1. Agent appends session summary to daily note
-2. Obsidian plugin's file-watcher kicks in (debounced 1.5s)
-3. Plugin extracts a fresh graph from the FULL note content
-4. Sidecar gets overwritten with the LLM's view
+- Track hashes per markdown section, not only per full note.
+- Re-extract only changed sections when the graph can be patched safely.
+- Preserve stable node IDs across partial updates so existing layout and pins
+ remain useful.
+- Fall back to full-note extraction when:
+ - headings are heavily reorganized
+ - too many sections changed
+ - a changed section participates in many cross-section edges
+ - the sidecar schema version is older than the section-aware format
-OR (alternative ordering):
-1. Agent appends session summary
-2. Claude Code hook fires, agent writes sidecar with curated graph
-3. Obsidian's debounced extraction triggers next, overwrites with LLM view
+Open questions:
-**Last writer wins WITH a `_pinned` escape hatch (v0.1, not deferred).**
-Both producers target the same schema. The Obsidian plugin honors
-`_pinned: true` on read: if the existing sidecar has `_pinned: true`,
-the plugin skips its own extraction and respects the existing data.
+- Should the sidecar store a `sections` map with heading slug, source range,
+ hash, and associated node IDs?
+- Should section-level extraction return graph patches, or should the plugin
+ ask Claude to merge old graph + changed markdown into a new full graph?
+- How should manual edits to headings affect historical node IDs?
-This protects deliberate, curated agent-authored graphs from being
-silently overwritten by probabilistic LLM extraction. Without `_pinned`,
-the design ships data loss as a feature — unacceptable.
+### Layout strategy
-Implementation cost: ~5 lines in the Obsidian plugin's
-`shouldSkipExtraction()` check. Deferring it to a "future feature" was
-called out by the architecture review as ship-blocking; landing it on
-day 1.
+v0.1 uses LLM-provided `position: {x, y}` coordinates and Cytoscape's
+`preset` layout. This keeps the prompt in control of visual grouping, but it
+can produce overlaps or off-canvas nodes.
-**Default behavior:** the Claude Code plugin's hook does NOT
-automatically set `_pinned`. The agent has to choose to pin a sidecar
-explicitly when it wants its content to be authoritative. This avoids
-surprising the user when they install both plugins and lose the
-LLM-extraction behavior they signed up for.
+Future options:
-### 5.3 Migration path
+1. **Keep LLM positions.**
+ - Pro: preserves explicit semantic clustering.
+ - Con: prompt quality directly affects readability.
+2. **Switch to Cytoscape layout such as `cose-bilkent`.**
+ - Pro: less prompt burden, likely fewer overlaps.
+ - Con: may lose deliberate "daily narrative" placement.
+3. **Hybrid approach.**
+ - LLM returns clusters and ranks; Cytoscape computes positions inside
+ cluster constraints.
-- **Phase 0 (today):** Claude Code plugin handles everything. Obsidian
- plugin doesn't exist yet.
-- **Phase 1:** Obsidian plugin shipped, sidecar schema unchanged. Both
- systems coexist; user installs both.
-- **Phase 2 (optional):** Strip the visual-notes skill down to a stub
- pointing at the Obsidian plugin. Claude Code plugin keeps the
- obsidian-append hook for note-writing assistance only.
+Decision for now: keep preset LLM positions, but A/B against a force-directed
+layout before a stable release.
----
+Design tasks:
-## 6. The Extraction Prompt
+- Add a repeatable layout comparison fixture with the same sidecar rendered
+ under preset and force-directed strategies.
+- Decide whether `position` remains required in schema v1.
+- Define behavior for nodes outside schema coordinate bounds.
-Lives at `plugins/obsidian-plugin/prompts/extract-graph.md`. Bundled with
-the plugin; user can override via settings (advanced).
+### Marketplace and release planning
-### Structure
+Obsidian plugin release path:
-```markdown
-You are extracting a concept map from an Obsidian daily note. Read the
-markdown below and return a JSON object describing the day's main concepts
-and their relationships.
+1. Finish MVP hardening.
+2. Create a beta release consumable by BRAT.
+3. Verify install/update behavior in a clean test vault.
+4. Tag releases with an Obsidian-specific prefix, e.g. `obsidian-v0.1.0`.
+5. Submit to the Obsidian community plugin store after beta feedback.
-# Heuristics (apply all)
+Claude Code plugin release path:
-1. **Every edge has a label.** The label IS the insight ("caused by",
- "blocks", "is part of", "led to"). No bare connections.
-2. **Hierarchy encodes importance.** Central concepts have multiple edges;
- peripheral ones have one or two.
-3. **Max 30 nodes total.** If the note covers more, group related items
- into cluster nodes labeled with a short summary like "build issues (4)".
-4. **Semantic status colors:**
- - `completed` (green) — finished outcomes, decisions made
- - `active` (yellow) — in-progress work, open questions
- - `context` (blue) — background facts, references, dependencies
- - `blocked` (red) — explicitly stuck items
-5. **Shape encodes type:**
- - `system` (rectangle) — tools, services, codebases, files
- - `task` (ellipse) — actions, work items
- - `decision` (diamond) — choices made, discoveries, design points
-6. **Cross-domain links are gold.** When the note connects unrelated areas,
- surface those as weak edges (dashed style) — they're often the most
- interesting findings.
+1. Migrate hook and skill content into `plugins/claude-code-plugin`.
+2. Keep the skill focused on agent-curated sidecar pre-population, not on
+ duplicating the Obsidian plugin's automatic extraction behavior.
+3. Add marketplace metadata when the plugin is functional.
+4. Tag releases with a Claude-specific prefix, e.g. `claude-v0.1.0`.
+
+CI/CD still needs to be designed and added:
+
+- PR checks for lint/typecheck/build.
+- JSON schema validation.
+- Obsidian release packaging for `manifest.json`, `main.js`, and `styles.css`.
+- Claude Code plugin metadata validation.
-# Schema
+### Future schema changes
-Return JSON only, matching this structure:
+Current schema supports:
-{
- "title": "Daily Overview - YYYY-MM-DD",
- "subtitle": "",
- "nodes": [
- {
- "data": { "id": "kebab-id", "label": "Display\nLabel" },
- "classes": "",
- "position": { "x": , "y": }
- }
- ],
- "edges": [
- {
- "data": { "source": "id1", "target": "id2", "label": "verb phrase" },
- "classes": ""
- }
- ]
-}
+- `kind`: `daily-overview`, `session-whiteboard`, `rollup`
+- graph nodes with type/status classes and required positions
+- labeled edges with optional strength classes
+- producer metadata (`_extractedBy`, `_schemaVersion`)
+- extraction metadata (`_lastProcessedHash`, `_usage`)
+- pinning (`_pinned`)
-# Layout
+Potential schema evolution:
-Place clusters in a horizontal sweep across the canvas. Each major theme
-gets its own cluster (~250px apart vertically, 250px between nodes within
-a cluster). Major clusters are separated by ~450px horizontally.
+- Make `_schemaVersion` required at v1.
+- Add section provenance for click-to-source and section-aware updates.
+- Add stable cluster/group metadata separate from visual node classes.
+- Add layout metadata so `position` can become optional or layout-specific.
+- Add confidence/grounding fields for nodes and edges.
+- Define producer ownership semantics if multiple producers cooperate on one
+ sidecar.
-# Examples
+Compatibility rule: renderers should warn and skip unsupported `kind` values
+without deleting or rewriting data they do not understand.
-[Two or three short markdown→JSON examples, ~100 words of markdown each]
+### Claude Code companion migration
-# Markdown
+The companion plugin should not compete with automatic Obsidian extraction. It
+should exist for workflows where an agent intentionally curates graph content.
-{full_markdown_content}
-```
+Migration checklist:
-### Few-shot examples
+- Port the existing hook runner and visual-notes skill from the private
+ workflow.
+- Update the skill to reference this repository's schema and extraction
+ prompt as canonical heuristics.
+- Ensure agent-authored sidecars use `_pinned: true` only when the agent is
+ deliberately claiming ownership.
+- Document how users recover from a stale curated sidecar: unpin or force
+ regenerate in Obsidian.
-Two examples bundled:
-1. A short engineering session (3-5 nodes, demonstrates type/status mapping)
-2. A multi-cluster day (15+ nodes, demonstrates clustering + cross-domain
- weak edges)
+### Privacy and storage
-These are the highest-leverage prompt-engineering investment. Iterate on
-the examples first when extraction quality is off.
+Current first pass stores the Anthropic API key in plugin `data.json`.
-### Prompt-engineering anti-patterns to avoid
+Future design work:
-- ❌ "Best represent this as a concept map" → vague
-- ❌ Generic node names like "Concept", "Idea", "Thing"
-- ❌ Asking the LLM to determine "max nodes" itself
-- ✅ Concrete constraints, schema with example values, explicit do/don't lists
+- Investigate Obsidian desktop secret storage support and mobile limitations.
+- Decide whether mobile should keep plaintext storage, warn more strongly, or
+ require a separate low-risk key.
+- Consider per-folder warnings for sensitive folders.
+- Add documentation for what is sent to Anthropic and when.
----
+### Cost controls
-## 7. Look & Feel
+Existing controls:
-### Visual style
+- watched folders default to empty
+- debounce between saves
+- content hash dedup
+- force-regenerate cooldown
+- status bar count
+- usage metadata when available
-Cytoscape rendered with **Catppuccin** color palette (Latte for light,
-Mocha for dark). Theme variables read from Obsidian's CSS at mount time.
+Potential additions:
-| Status | Latte (light) bg | Latte border | Mocha (dark) bg | Mocha border |
-|---|---|---|---|---|
-| `completed` | `#a6e3a1` | `#40a02b` | `#a6e3a1` | `#94e2d5` |
-| `active` | `#f9e2af` | `#df8e1d` | `#f9e2af` | `#fab387` |
-| `context` | `#89b4fa` | `#1e66f5` | `#89b4fa` | `#74c7ec` |
-| `blocked` | `#f38ba8` | `#d20f39` | `#f38ba8` | `#eba0ac` |
+- Daily or monthly soft budget warnings.
+- Per-folder extraction enable/disable.
+- "Manual only" watched-folder mode.
+- Token-count preflight using Anthropic's token counting endpoint instead of a
+ character heuristic.
-Status fill colors stay the same across themes (Catppuccin's accent
-palette is consistent); the page background, text, and borders shift
-between Latte and Mocha. Read all values from Obsidian's CSS variables
-at mount time — don't hardcode in the plugin.
+### Rendering and navigation polish
-### Shapes
+Open areas:
-- `system` — `round-rectangle`, padding 12px, label inside
-- `task` — `ellipse`, padding 12px
-- `decision` — `diamond`, padding 16px (more padding because diamonds visually
- shrink vs. rectangles at the same node-size setting)
+- Click node to jump to the best matching markdown heading or text span.
+- Keyboard shortcuts for fit/reset.
+- Better mobile canvas height and touch behavior.
+- More accessible legend and ARIA labels.
+- Placeholder states for missing API key, missing sidecar, malformed sidecar,
+ unsupported sidecar kind, and stale pinned graphs.
+- Multiple split panes showing the same file without duplicate containers or
+ lifecycle leaks.
-### Edges
+## Known limitations
-- Default — 2px solid, bezier curve, small triangle arrowhead
-- `.strong-edge` — 3px, darker color, same shape
-- `.weak-edge` — 1px dashed, lighter color, indicates cross-domain links
+- Full-note extraction can duplicate API spend during tight multi-device sync
+ races.
+- LLM-generated positions can overlap or produce less readable layouts.
+- The plugin cannot guarantee graph quality; bad extraction requires manual
+ regenerate or prompt/schema improvement.
+- API key storage is plaintext in the current implementation.
+- Claude Code companion hooks are scaffolded but not functional yet.
+- The renderer currently supports daily overview sidecars; other `kind` values
+ are reserved for future use.
-### Layout (current)
+## Open decisions
-LLM produces explicit `{x, y}` positions following the prompt's layout
-guidance. Cytoscape uses `layout: { name: 'preset' }` to honor them.
-
-**Future:** A/B against `cose-bilkent` force-directed layout (let Cytoscape
-position nodes; LLM only produces structure). Decision deferred — see §10.
-
-### Interactivity
-
-- **Click a node → scroll the markdown to the related section.** This is
- the killer interaction for journal use — turns the visual into a
- navigation aid, not just decoration. **Match precedence:**
- 1. **Heading-slug match.** Compare the node's `data.id` (already
- kebab-case) to each markdown heading slugified the same way. First
- match wins. Most reliable signal because the LLM derives ids from
- headings.
- 2. **Label substring match (case-insensitive).** Search the markdown
- body for the first occurrence of the node's `data.label` (with `\n`
- replaced by space).
- 3. **No match.** Brief Notice "no match in markdown for '$label'"; no
- scroll. Indicates LLM hallucinated a node not grounded in text;
- useful debug signal.
- Document this precedence in the implementation notes for renderer.ts
- so behavior stays consistent across versions.
-- **Hover** on a node → bold border, highlight all connected edges
-- **Mouse out** → restore default
-- **Pan** with click-drag, **zoom** with wheel/pinch (touch on mobile)
-- **No node-drag.** Preset layout is authoritative; users who don't like
- positioning fix the markdown, not the visual.
-- Min zoom 0.3, max zoom 3.0
-- Keyboard: `f` fits viewport to graph, `r` resets zoom
-
-### Mobile-specific
-
-- Cytoscape canvas height: 350px on phones (vs. 450px on desktop) to
- preserve note-text real estate on narrow viewports.
-- Touch gestures: pinch-zoom + drag-pan. No hover; tap a node for the
- jump-to-section behavior.
-- Settings page: avoid horizontal layouts; vertical stack of text inputs.
- The "Advanced" disclosure stays collapsed by default on mobile.
-
-### Header
-
-Top-left corner of the canvas:
-- `
` with the `header` field (e.g., "Daily Overview")
-- Subtitle below in muted text (e.g., "2026-05-01 — Hook fix · sidecar
- architecture")
-
-### Legend
-
-Top-right corner: small floating box with status dots (completed/active/
-context/blocked) and shape labels (system/task/decision). Helps the user
-parse the visual the first time they see it.
-
----
-
-## 8. Repo Structure
-
-```
-visual-notes/
-├── README.md # Top-level: project overview, install paths
-├── LICENSE # MIT
-├── .gitignore
-├── pnpm-workspace.yaml # Declares plugins/* and shared/ as workspaces
-├── package.json # Root: dev tooling (typescript, eslint, prettier)
-│
-├── docs/
-│ ├── design.md # THIS document
-│ ├── architecture.md # (Optional) deeper technical spec when impl starts
-│ └── decisions/ # ADR-style records as decisions accumulate
-│ └── 0001-shared-schema.md
-│
-├── shared/
-│ ├── schema.json # JSON Schema for sidecar
-│ └── package.json # Workspace package; generates TS types
-│
-└── plugins/
- ├── claude-code-plugin/
- │ ├── README.md # Install: /plugin install ...
- │ ├── .claude-plugin/
- │ │ └── plugin.json
- │ ├── hooks/
- │ │ ├── hooks.json
- │ │ ├── post-obsidian-append.md
- │ │ └── run-hook.sh
- │ └── skills/
- │ └── visual-notes/
- │ └── SKILL.md
- │
- └── obsidian-plugin/
- ├── README.md # Install: BRAT or community store
- ├── package.json
- ├── manifest.json
- ├── tsconfig.json
- ├── esbuild.config.mjs
- ├── styles.css
- ├── prompts/
- │ └── extract-graph.md # System prompt + few-shot examples
- └── src/
- ├── main.ts
- ├── extractor.ts
- ├── renderer.ts
- ├── settings.ts
- ├── theme.ts
- ├── storage.ts
- ├── debounce.ts
- └── schema.ts
-```
-
-### Why pnpm workspaces
-
-- Shared schema package is consumed by both plugins; workspace symlinks
- avoid copying.
-- Single root `node_modules` keeps disk usage and install time manageable.
-- Per-plugin `package.json` keeps dependencies scoped (Obsidian plugin
- has Anthropic SDK + cytoscape; Claude Code plugin has nothing).
-
-### CI/CD
-
-`.github/workflows/`:
-- `obsidian-release.yml` — on tag `obsidian-v*`: build, package, create
- GitHub release with `manifest.json` + `main.js` + `styles.css` as
- assets (BRAT/community-store consumable)
-- `claude-code-release.yml` — on tag `claude-v*`: validate plugin.json
- schema, no build artifacts (Claude Code plugins distribute as git refs)
-- `ci.yml` — on PR: typecheck, lint, validate JSON schemas
-
-Path filters limit each workflow to its component's files.
-
-### Versioning
-
-**Independent.** Plugins evolve at different cadences. Tag prefix
-distinguishes:
-- `obsidian-v0.1.0` — Obsidian plugin release
-- `claude-v0.1.0` — Claude Code plugin release
-- `schema-v0.1.0` — Sidecar schema bump (forces both plugins to declare
- compatibility)
-
----
-
-## 9. Implementation Phases
-
-### Phase 1 — Scaffold + Obsidian plugin MVP (1.5–2 weeks)
-
-Budget honestly: this is a fresh TypeScript Obsidian plugin with esbuild,
-zod, an external API integration, and inline-rendered Cytoscape. A week
-is doable for someone who's shipped Obsidian plugins before; budget 1.5–2
-weeks otherwise.
-
-- Repo scaffolded ✅ (initial commit landed)
-- Extraction prompt authored ✅ (`prompts/extract-graph.md`, with two
- few-shot examples). The plugin has nothing to send without this.
-- Copy `esbuild.config.mjs`, `tsconfig.json`, `version-bump.mjs`,
- `versions.json`, `styles.css` from
- [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin).
- Don't reinvent.
-- Obsidian plugin skeleton: manifest, plugin entry that registers a
- no-op `MarkdownPostProcessor` and `PluginSettingTab`
-- Settings tab with API key field (plaintext, no SecretStorage yet)
-- Generate `src/schema.ts` from `shared/schema.json` via
- `json-schema-to-typescript`
-- Manual command: `Visual Notes: Extract from current note`. No
- file-watching, no debouncing yet.
-- `extractor.ts`: hand-rolled `requestUrl` to Anthropic Messages API,
- Zod-validate response
-- Write sidecar JSON next to the note
-- Verify happy path on one daily note end-to-end
-
-### Phase 2 — Auto-extraction + rendering (week 2)
-
-- File-watcher with debounce + content-hash dedup
-- MarkdownPostProcessor mounts Cytoscape from sidecar JSON
-- Theme integration (CSS vars → Cytoscape style)
-- Caching: Map with onload/onunload lifecycle
-
-### Phase 3 — Settings polish + storage (week 3)
-
-- SecretStorage on desktop, plaintext warning on mobile
-- Per-folder configurability (different model/prompt per watched folder — v0.1 keeps the dial uniform)
-- Model dropdown (Haiku / Sonnet)
-- Custom prompt override (textarea)
-
-### Phase 4 — Distribution (week 4)
-
-- BRAT release (tag `obsidian-v0.1.0-beta.0`)
-- Submit to Obsidian community plugin store
-- Document install paths in README
-
-### Phase 5 — Claude Code plugin migration (week 5)
-
-- Pull existing `~/ai/skills/visual-notes/` and `~/ai/hooks/` content
- into `plugins/claude-code-plugin/`
-- Strip skill down to "the Obsidian plugin handles this; write good notes"
-- Create `.claude-plugin/marketplace.json` for distribution
-- Both plugins coexist; users can install either or both
-
-### Phase 6 — Polish (ongoing)
-
-- A/B test LLM-positioned vs `cose-bilkent` layout
-- Cost-tracking widget (optional, originally cut from MVP)
-- Multi-vault config
-- More few-shot prompt examples
-
----
-
-## 9b. Failure modes & lifecycle states
-
-Every external dependency can fail; designing for it up front is cheaper
-than debugging in the wild.
-
-### Lifecycle state machine
-
-```mermaid
-stateDiagram-v2
- [*] --> Idle
- Idle --> Debouncing: vault.modify (md in scope)
- Debouncing --> Debouncing: another modify resets timer
- Debouncing --> Hashing: 1.5s elapsed
- Hashing --> Idle: hash matches sidecar._lastProcessedHash
- Hashing --> CheckPin: hash differs OR no sidecar
- CheckPin --> Idle: sidecar._pinned == true
- CheckPin --> Extracting: not pinned
-
- Idle --> Extracting: cmd: Extract from current note (skips Hashing+CheckPin if pinned-aware)
- Idle --> Extracting: cmd: Regenerate (force) — bypasses _pinned + hash, 30s cooldown
- Idle --> Idle: cmd: Pin this overview (writes _pinned=true)
- Idle --> Idle: cmd: Unpin this overview (writes _pinned=false)
- Idle --> Idle: cmd: Delete sidecar
-
- Extracting --> Writing: 200 OK + Zod-valid
- Extracting --> Extracting: Zod fail (1 retry with correction)
- Extracting --> Queued: 429 / 5xx (with backoff, max 3)
- Extracting --> Failed: 401 / 400 (terminal)
- Extracting --> Idle: AbortController.abort() (plugin unload)
- Queued --> Extracting: backoff timer
- Queued --> Idle: AbortController.abort() (plugin unload)
- Queued --> Failed: 3 retries exhausted
- Writing --> Idle: sidecar written, render triggered
- Failed --> Idle: user dismisses Notice or fixes config
-```
-
-The `Idle → Extracting (cmd: Regenerate force)` transition is the
-unpin-and-extract escape hatch. The user is never stuck with a pinned
-sidecar; force-regen overrides.
-
-### Failure scenarios + handling
-
-| Scenario | Handling |
-|---|---|
-| **API key missing** on plugin load | Status-bar shows "Visual Notes: configure API key". File watcher stays inert. |
-| **Watched folders list empty** on plugin load | Status-bar shows "Visual Notes: add a watched folder". One-time Notice on first load. |
-| **A configured folder doesn't exist in the vault** | Notice on plugin load naming the missing folder. Other configured folders continue to be watched normally; the missing one is rechecked next load. |
-| **Anthropic API down** (5xx) | Bounded retry (3×, exponential backoff). On exhaustion, queue for next manual trigger; status-bar shows "queued". |
-| **Network failure** (no internet) | Treated as 5xx. Same retry/queue behavior. |
-| **Rate limit** (429) | Honor `retry-after`, exponential backoff, max 3 retries. |
-| **Auth failure** (401) | Notice with "open settings" affordance. Don't retry. |
-| **Bad request** (400) | Log to console, Notice. Suggests model/prompt issue; don't retry. |
-| **Malformed response from API** (Zod-invalid JSON) | One retry with schema-correction prompt. Then fail. |
-| **Sidecar JSON malformed** (someone wrote bad JSON) | Renderer logs the error, displays a "⚠ malformed sidecar" placeholder in the note. Does NOT crash the post-processor. |
-| **Daily note deleted mid-flight** | Catch the `vault.modify()` error on sidecar write; discard result silently. |
-| **Sidecar exists but markdown doesn't** (orphaned) | Renderer shows the visual as-is (data is still valid). User can manually delete via the "Delete sidecar" command. |
-| **Plugin disabled mid-flight** | `AbortController.abort()` in `onunload()` cancels the in-flight `requestUrl`. No write happens. |
-| **Two views of same file open** | Each `MarkdownRenderChild` owns its own Cytoscape instance. No cache collision. |
-| **Obsidian Sync delivers sidecar mid-extraction** | Hash check at extraction completion; if the just-arrived sidecar's hash matches what we just extracted, no-op. |
-| **Sidecar `kind` is non-default** (`session-whiteboard`, `rollup`) | v0.1 renders only `kind: "daily-overview"` (or sidecars with `kind` omitted, which default to that). For unsupported kinds, the renderer logs `console.warn("Visual Notes: unsupported sidecar kind '${k}', skipping render")` and skips mounting. The sidecar is preserved unchanged. |
-| **Sidecar `kind` field schema-valid but unknown to plugin** (future kind we don't yet support) | Same as above — log + skip, don't crash. |
-| **API key valid but user hits org quota** (529 overloaded) | Treat as 5xx: bounded retry with backoff, then queue. |
-| **User pastes API key with leading/trailing whitespace** | Trim on save in settings handler. |
-
-### What we explicitly DON'T handle in v0.1
-
-- Multi-vault settings divergence (Obsidian vault model is per-vault by default)
-- Recovery from a corrupted plugin `data.json` (Obsidian re-creates from defaults)
-- API key rotation mid-session (user restarts plugin or Obsidian)
-- LLM hallucination of nonsense graphs (manual `Regenerate (force)` is the escape hatch)
-
----
-
-## 10. Open Questions / Decisions for the Implementation Team
-
-### Layout algorithm
-
-**Question:** Stick with LLM-produced positions (current schema), or strip
-positions from the schema and use Cytoscape's `cose-bilkent` force-directed
-layout?
-
-**Decision:** **Stick with LLM positions for v0.1.** A/B against `cose-bilkent`
-as a Phase 6 polish item. The LLM-positions approach preserves the
-hand-crafted clustered layout the user has been using; switching costs
-prompt re-engineering and a visual style change.
-
-### Mobile API key UX
-
-**Question:** Mobile can't use SecretStorage. Three options:
-- (a) Plaintext in `data.json` with a stark warning
-- (b) Disable extraction on mobile entirely
-- (c) Require user to set up a separate mobile-scoped API key with
- reduced permissions
-
-**Decision:** **(a)** for v0.1. Mobile users opt-in by entering the key,
-warned in the settings description. Future: explore (c) when Anthropic
-ships fine-grained API key scoping.
-
-### Multi-vault config
-
-**Question:** What if a user has multiple Obsidian vaults and wants
-different settings per vault?
-
-**Decision:** Out of scope for v0.1. Settings are per-vault by Obsidian
-default (each vault has its own `.obsidian/plugins//data.json`).
-That's good enough.
-
-### Obsidian Sync race conditions
-
-**Question:** Two devices simultaneously extract the same note. Each
-writes a sidecar. Which wins?
-
-**Decision:** Last-writer-wins via Obsidian Sync's natural file-merge
-semantics. Content-hash dedup prevents the extraction from happening on
-both devices in the common case (one device extracts first, sidecar syncs,
-second device sees matching hash and skips).
-
-### Custom prompts
-
-**Question:** Should we support per-folder or per-tag prompt customization?
-
-**Decision:** Out of scope. v0.1 is one global prompt. Power users can
-override the whole prompt via settings.
-
----
-
-## 11. References
-
-### Obsidian Plugin Development
-
-- [Obsidian Plugin Docs](https://docs.obsidian.md/Home)
-- [Sample Plugin Repository](https://github.com/obsidianmd/obsidian-sample-plugin)
-- [MarkdownPostProcessor reference](https://docs.obsidian.md/Reference/TypeScript+API/MarkdownPostProcessor)
-- [PluginSettingTab reference](https://docs.obsidian.md/Reference/TypeScript+API/PluginSettingTab)
-- [Vault API docs](https://docs.obsidian.md/Plugins/Vault)
-- [Obsidian community plugin store submission](https://github.com/obsidianmd/obsidian-releases)
-- [BRAT (Beta Reviewers Auto-update Tool)](https://github.com/TfTHacker/obsidian42-brat)
-- [Juggl plugin (Cytoscape reference impl)](https://github.com/HEmile/juggl)
-
-### Anthropic API
-
-- [Claude API quickstart](https://platform.claude.com/docs/en/docs/quickstart)
-- [Structured outputs guide](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
-- [Token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting)
-- [Error handling reference](https://platform.claude.com/docs/en/api/errors)
-- [Prompt engineering best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)
-- [Pricing](https://platform.claude.com/pricing)
-- [`@anthropic-ai/sdk` npm](https://www.npmjs.com/package/@anthropic-ai/sdk)
-
-### Claude Code Plugins
-
-- [Claude Code plugin docs](https://code.claude.com/docs/en/plugins)
-- [Plugin reference](https://code.claude.com/docs/en/plugins-reference)
-- [Plugin marketplaces](https://code.claude.com/docs/en/plugin-marketplaces)
-- [Hooks docs](https://code.claude.com/docs/en/hooks)
-- [Anthropic's official marketplace](https://github.com/anthropics/claude-plugins-official)
-
-### Cytoscape.js
-
-- [Cytoscape.js docs](https://js.cytoscape.org/)
-- [cose-bilkent layout](https://github.com/cytoscape/cytoscape.js-cose-bilkent)
-- [cytoscape-css-variables extension](https://github.com/lukethacoder/cytoscape-css-variables)
-
-### Tooling
-
-- [pnpm workspaces](https://pnpm.io/workspaces)
-- [json-schema-to-typescript](https://github.com/bcherny/json-schema-to-typescript)
-- [Catppuccin palette](https://github.com/catppuccin/catppuccin)
-
----
-
-## Appendix A — Glossary
-
-| Term | Meaning |
-|---|---|
-| **Sidecar** | The `{date}-overview.json` file that lives next to the daily note, holding the extracted graph data. |
-| **Watched folders** | The list of Obsidian folders the plugin monitors for daily-note changes. Empty by default; user must add at least one. Subfolders are watched recursively. |
-| **Daily note** | A markdown file named `YYYYMMDD.md` in any of the watched folders. |
-| **Overview** | The visual concept map rendered for a daily note. |
-| **Session-whiteboard** (legacy) | Per-conversation `{date}-session-{n}.json` sidecars from the original visual-notes skill. **Note:** "session" elsewhere in this doc refers to a conversational unit (an "AI session summary" in a daily note). When ambiguous, use "session-whiteboard" for the file format and "session summary" or "conversation" for the content unit. |
-| **Producer** | Any code that writes a sidecar. The Obsidian plugin and the Claude Code plugin are the two producers. |
-| **`_pinned`** | A boolean field in the sidecar that, when `true`, suppresses LLM extraction by the Obsidian plugin. Used by the Claude Code plugin to claim authoritative ownership of a sidecar. User toggles via the Pin/Unpin commands; force-regenerate command bypasses. |
-| **MarkdownPostProcessor** | Obsidian API hook for post-render content injection. The plugin uses it to inject the Cytoscape canvas into the rendered markdown view of any daily note that has a sidecar. |
-| **MarkdownRenderChild** | Obsidian lifecycle wrapper for rendered content. Each instance owns one Cytoscape canvas and is torn down when the view closes. |
-| **BRAT** | Beta Reviewers Auto-update Tool. The standard Obsidian-plugin distributor for pre-release builds. Users add a repo URL and BRAT pulls the latest tagged release. |
-
-## Appendix B — Debugging Visual Notes
-
-For maintainers and the user when something doesn't work as expected.
-
-| Symptom | Where to look |
-|---|---|
-| Visual doesn't appear | Open DevTools (Ctrl+Shift+I in Obsidian). Filter console for `[visual-notes]`. Check for "no watched folders configured", "API key invalid", or extraction errors. Also: confirm the note's folder is in the watched-folders list. |
-| Visual is stale | Run `Visual Notes: Regenerate (force)` from command palette. Bypasses pin and cached hash. |
-| Visual shows wrong content | Check the sidecar: open `{date}-overview.json` next to the daily note. Is `_pinned: true`? An agent may have locked it; run `Visual Notes: Unpin this overview` then regenerate. |
-| Repeated API calls visible in status-bar count | Check that the `_lastProcessedHash` field is being written to the sidecar (look for `sha256:` prefix). If absent, the dedup is broken. |
-| Plugin loads then errors immediately | Verify `manifest.json` `minAppVersion ≤` your Obsidian version. Otherwise `getSecretStorage` may be unavailable. |
-| Extraction succeeds but render is blank | Validate the sidecar against `shared/schema.json` — possibly an unknown `kind`, malformed JSON, or out-of-bounds positions. |
-| Settings UI fields are gone after upgrade | Check `data.json` `_settingsVersion`. The migration step may have failed; restore from a backup of `data.json` (Obsidian writes `data.json.bak` on save). |
-
-The Plugin instance exposes `(window as any).__visualNotes` in dev
-builds — gives DevTools console access to the extractor, sidecar
-events, and current settings. Useful for live debugging without
-reaching for the source.
-
----
-
-## Appendix C — Things explicitly cut from MVP
-
-These are intentional non-goals for the first release. Documenting them so
-future contributors don't relitigate.
-
-- Cost transparency UI (running token spend display in settings)
-- Streaming API responses
-- Per-folder configs
-- Per-tag configs
-- Custom theme palettes
-- Layout algorithm picker (deferred to Phase 6)
-- Obsidian Canvas (.canvas) output format
-- Native Obsidian Graph View integration
-- Mind-map export
-- PNG/SVG export of the rendered graph
-- Bidirectional sync (visual edits writing back to markdown)
+| Topic | Current leaning | Decision needed before |
+|---|---|---|
+| Section-aware sidecar metadata | Add after MVP; do not block v0.1 | schema v1 |
+| Layout algorithm | Keep preset positions; A/B force-directed | stable release |
+| API key storage | Improve desktop storage; document mobile caveat | public beta |
+| Cost dashboard | Keep status count for MVP | after beta feedback |
+| Claude Code pin defaults | Do not pin automatically | companion migration |
+| Rollup/session sidecar rendering | Preserve schema values, skip in v0.1 renderer | adding those modes |
+
+## Reference links
+
+- [Top-level README](../README.md)
+- [Obsidian plugin README](../plugins/obsidian-plugin/README.md)
+- [Claude Code plugin README](../plugins/claude-code-plugin/README.md)
+- [Shared schema](../shared/schema.json)
+- [Extraction prompt](../plugins/obsidian-plugin/prompts/extract-graph.md)
+- [Issue #4](https://github.com/bobthearsonist/visual-notes/issues/4)
diff --git a/plugins/claude-code-plugin/README.md b/plugins/claude-code-plugin/README.md
index c36f9d8..23fc151 100644
--- a/plugins/claude-code-plugin/README.md
+++ b/plugins/claude-code-plugin/README.md
@@ -20,8 +20,8 @@ common case without it.
repo and is being ported here. Until then, the manifest and directory
structure are scaffolded but the hooks/skills are placeholders.
-See [`../../docs/design.md`](../../docs/design.md) §5 for the design and §9
-Phase 5 for the migration plan.
+See [`../../docs/design.md`](../../docs/design.md) for the remaining migration
+plan and open decisions.
## Install (after migration)
diff --git a/plugins/claude-code-plugin/skills/visual-notes/SKILL.md b/plugins/claude-code-plugin/skills/visual-notes/SKILL.md
index e36ee21..75fc4d4 100644
--- a/plugins/claude-code-plugin/skills/visual-notes/SKILL.md
+++ b/plugins/claude-code-plugin/skills/visual-notes/SKILL.md
@@ -6,8 +6,8 @@ description: Concept-map visualization for daily notes. Documents the sidecar JS
# Visual Notes (Claude Code skill)
> **Scaffold placeholder.** Migration of the full skill from the private
-> dotfiles repo is pending. See [`../../../docs/design.md`](../../../docs/design.md)
-> §5 for the migration plan.
+> dotfiles repo is pending. See the living design document for the migration
+> plan.
## What this skill is for
@@ -46,5 +46,5 @@ agent pre-populating a sidecar should follow them. Quick summary:
If the Obsidian plugin is also installed, both producers can write the
sidecar. **Last writer wins.** For workflows where you want the agent's
-curated graph to stick, set `_pinned: true` in the sidecar (future feature)
-to suppress auto-extraction.
+curated graph to stick, set `_pinned: true` in the sidecar to suppress
+auto-extraction until the user unpins or force-regenerates.
diff --git a/plugins/obsidian-plugin/README.md b/plugins/obsidian-plugin/README.md
index 1415814..83d0eb2 100644
--- a/plugins/obsidian-plugin/README.md
+++ b/plugins/obsidian-plugin/README.md
@@ -7,11 +7,14 @@ the top of the rendered note view.
## Status
-🚧 **Phase 1 MVP.** The plugin now includes the Obsidian build scaffold,
-settings tab, manual and watched-folder extraction, sidecar writing,
-pin/unpin/delete commands, and inline Cytoscape rendering from the sidecar.
-See [`../../docs/design.md`](../../docs/design.md) §4 for the full design
-and §9 for implementation phases.
+🚧 **MVP implementation in progress.** The plugin includes the Obsidian build
+scaffold, settings tab, manual and watched-folder extraction, sidecar writing,
+pin/unpin/delete commands, status-bar extraction count, token-usage metadata,
+and inline Cytoscape rendering from the sidecar.
+
+The top-level [`../../README.md`](../../README.md) has the product and
+architecture overview. The living [`../../docs/design.md`](../../docs/design.md)
+tracks remaining design work and open decisions.
## Install (after first release)
@@ -73,7 +76,8 @@ sequenceDiagram
Plugin->>User: render Cytoscape inline
```
-See [`../../docs/design.md`](../../docs/design.md) §4.2 for the full lifecycle.
+See the architecture overview in [`../../README.md`](../../README.md) and the
+future-facing notes in [`../../docs/design.md`](../../docs/design.md).
## Cost