A wayfinder inspired map plugin for obisidian
1# obs-map-viewer
2
3An Obsidian sidebar plugin that reads places from bullet lists in the active note and displays them as markers on an interactive map. Geocodes place names via OpenStreetMap Nominatim, caches coordinates as `geo:` sub-bullets in the note, and provides bidirectional cursor sync between the editor and map.
4
5## Project Context
6
7This plugin is modeled on [obs-calendar-viewer](../obs-calendar-viewer), which has a calendar + map split view. `obs-map-viewer` strips out the calendar and focuses entirely on the map. Key architectural decisions are carried over: Leaflet for mapping, Nominatim for geocoding (no API keys), document-as-cache for geo data, Obsidian CSS variables for theming.
8
9### Expected Note Format
10
11```markdown
12* Sagrada Familia
13 * Amazing architecture, book tickets in advance
14 * category: Architecture
15 * geo: 41.403600,2.174400
16* [The Louvre](https://en.wikipedia.org/wiki/Louvre)
17 * Must see the Mona Lisa
18 * category: Art
19 * geo: 48.860600,2.337600
20* Blue Bottle Coffee, Tokyo
21```
22
23- Top-level bullets (`*` or `-`) define places
24- Sub-bullets matching `<key>: <value>` are parsed as structured fields
25- The `geo:` field is special: valid coordinates are extracted and used for map markers
26- Sub-bullets not matching key-value format are stored as freeform notes
27- Place names can be plain text, markdown links `[name](url)`, or wiki-links `[[Page Name]]`
28
29## Development Methodology: VSDD
30
31This project follows **Verified Spec-Driven Development (VSDD)**, a methodology that fuses Spec-Driven Development, Test-Driven Development, and Verification-Driven Development into a single pipeline. See the [full VSDD spec](https://gist.github.com/dollspace-gay/d8d3bc3ecf4188df049d7a4726bb2a00).
32
33This includes having red gated tests. We must start with the tests, show that they all fail, and only then proceed to implementation.
34
35### Roles
36
37| Role | Entity | Function |
38|------|--------|----------|
39| **Architect** | Human developer | Strategic vision, domain expertise, acceptance authority |
40| **Builder** | Claude (OpenCode) | Spec authorship, test generation, code implementation, refactoring |
41| **Adversary** | @adversary agent | Hyper-critical reviewer, fresh context on every pass, zero tolerance |
42| **Tracker** | Chainlink CLI | Hierarchical issue tracking with milestones, blocking relationships, and sub-issues |
43
44### VSDD Pipeline (Adapted)
45
46The full VSDD ceremony is adapted for this project's scope (Obsidian plugin, TypeScript, no formal verification toolchain):
47
48#### Phase 2: TDD Implementation
49Test-first development for each module in dependency order:
501. `parser.ts` (pure, no deps)
512. `geocoder.ts` (needs Place type)
523. `mapRenderer.ts` (needs Place type)
534. `mapView.ts` (needs all above)
545. `main.ts` (needs mapView)
55
56For each: write failing tests -> flag that the red gate exists (all tests fail) -> implement minimum to pass -> adversarial review -> refactor
57
58#### Adversarial Review
59Each module reviewed by the Adversary in a fresh context after implementation. Plus a final full-codebase review looking at cross-module interactions.
60
61We will only move on to the next module once adversarial review is passed.
62
63#### Phase 4: Feedback Integration
64Adversary findings feed back: spec fixes -> test fixes -> implementation fixes.
65
66#### Phase 5: Hardening
67- Property-based tests for the parser via `fast-check`
68- Edge case stress tests for the geocoder
69- Final adversarial pass
70
71#### Phase 6: Convergence
72Done when the adversary is nitpicking style, not finding real bugs. Four dimensions must converge: specs, tests, implementation, and hardening.
73
74## Chainlink Issue Tracking
75
76All work is tracked via `chainlink`, a local CLI issue tracker. Issues are organized into milestones (one per VSDD phase) with blocking relationships enforcing dependency order.
77
78### Milestones
79
80| ID | Phase | Issues |
81|----|-------|--------|
82| M1 | Phase 2: TDD Implementation | #1-#22 (scaffolding + 5 modules x (parent + 3 sub-issues) + styles) |
83| M2 | Phase 3: Adversarial Review | #23-#28 (per-module reviews + full codebase) |
84| M3 | Phase 4: Feedback Integration | #29-#31 (spec/test/impl fixes) |
85| M4 | Phase 5: Hardening | #32-#34 (property tests, stress tests, final adversary) |
86| M5 | Phase 6: Convergence | #35-#36 (convergence check, smoke test) |
87
88### Commands
89
90```bash
91chainlink tree # Full issue hierarchy
92chainlink list # All open issues
93chainlink ready # Issues with no open blockers (what to work on next)
94chainlink show <id> # Full issue details with spec
95chainlink milestone show <id> # Milestone progress
96chainlink blocked # What's waiting on what
97```
98
99### Labels
100
101- `spec` — specification work
102- `test` — test writing
103- `impl` — implementation
104- `review` — adversarial review
105- `fix` — feedback integration
106- `infra` — scaffolding/build config
107
108## Module Architecture
109
110```
111obs-map-viewer/
112├── main.ts # Plugin entry — view registration, commands, events
113├── mapView.ts # ItemView subclass — sidebar, refresh, geo write-back, cursor sync
114├── parser.ts # Pure parser — markdown bullet lists → Place[]
115├── mapRenderer.ts # Leaflet map — markers, popups, selection, highlight
116├── geocoder.ts # Nominatim geocoding — rate limiting, dedup, cancellation
117├── styles.css # CSS using Obsidian variables for theme compat
118├── manifest.json # Obsidian plugin manifest
119├── package.json # Dependencies and build scripts
120├── tsconfig.json # TypeScript config
121├── esbuild.config.mjs # esbuild bundler config (CJS output, Leaflet bundled)
122├── vitest.config.ts # Vitest test config
123└── tests/
124 ├── parser.test.ts
125 ├── geocoder.test.ts
126 ├── mapRenderer.test.ts
127 ├── mapView.test.ts
128 └── main.test.ts
129```
130
131### Dependency Graph
132
133```
134main.ts → mapView.ts → parser.ts (pure)
135 → geocoder.ts (effectful, fetch)
136 → mapRenderer.ts (effectful, DOM/Leaflet)
137```
138
139`parser.ts` is the only pure module. All types (`Place`, etc.) are exported from it.
140
141### Key Design Decisions
142
1431. **Leaflet bundled via esbuild** — no CDN dependency, ~10K lines but tree-shaken
1442. **Leaflet CSS inlined as string literal** — avoids needing a CSS loader
1453. **CJS output, ES2018 target** — Obsidian compatibility
1464. **No API keys** — Nominatim + Stadia Watercolor + CartoDB labels are all free
1475. **Document as cache** — `geo:` coordinates stored in the note itself (portable, no external DB)
1486. **Structured field parsing** — `<key>: <value>` sub-bullets parsed into `fields: Record<string, string>` for extensibility
1497. **Write-back safety** — re-parse inside `vault.process()`, match by name, never use stale line numbers
1508. **Geocoding mutex** — single in-flight operation via AbortController, prevents concurrent writes
1519. **Theme integration** — all colors from Obsidian CSS variables
152
153## For AI Agents
154
155When working on this project:
156
1571. **Check `chainlink ready`** to see what's unblocked and ready to work on
1582. **Check `chainlink show <id>`** before starting any issue — the description IS the spec
1593. **Follow strict TDD**: write tests first, verify they fail, then implement minimum to pass
1604. **Never write implementation without a failing test demanding it**
1615. **Run the adversary review** after completing each module (Phase 3 issues)
1626. **Mark issues done** via `chainlink close <id>` when complete
1637. **Use `chainlink start <id>`** to track time on issues
1648. **The spec in the issue is the source of truth** — if the code contradicts the issue description, the code is wrong