# obs-map-viewer An 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. ## Project Context This 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. ### Expected Note Format ```markdown * Sagrada Familia * Amazing architecture, book tickets in advance * category: Architecture * geo: 41.403600,2.174400 * [The Louvre](https://en.wikipedia.org/wiki/Louvre) * Must see the Mona Lisa * category: Art * geo: 48.860600,2.337600 * Blue Bottle Coffee, Tokyo ``` - Top-level bullets (`*` or `-`) define places - Sub-bullets matching `: ` are parsed as structured fields - The `geo:` field is special: valid coordinates are extracted and used for map markers - Sub-bullets not matching key-value format are stored as freeform notes - Place names can be plain text, markdown links `[name](url)`, or wiki-links `[[Page Name]]` ## Development Methodology: VSDD This 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). This includes having red gated tests. We must start with the tests, show that they all fail, and only then proceed to implementation. ### Roles | Role | Entity | Function | |------|--------|----------| | **Architect** | Human developer | Strategic vision, domain expertise, acceptance authority | | **Builder** | Claude (OpenCode) | Spec authorship, test generation, code implementation, refactoring | | **Adversary** | @adversary agent | Hyper-critical reviewer, fresh context on every pass, zero tolerance | | **Tracker** | Chainlink CLI | Hierarchical issue tracking with milestones, blocking relationships, and sub-issues | ### VSDD Pipeline (Adapted) The full VSDD ceremony is adapted for this project's scope (Obsidian plugin, TypeScript, no formal verification toolchain): #### Phase 2: TDD Implementation Test-first development for each module in dependency order: 1. `parser.ts` (pure, no deps) 2. `geocoder.ts` (needs Place type) 3. `mapRenderer.ts` (needs Place type) 4. `mapView.ts` (needs all above) 5. `main.ts` (needs mapView) For each: write failing tests -> flag that the red gate exists (all tests fail) -> implement minimum to pass -> adversarial review -> refactor #### Adversarial Review Each module reviewed by the Adversary in a fresh context after implementation. Plus a final full-codebase review looking at cross-module interactions. We will only move on to the next module once adversarial review is passed. #### Phase 4: Feedback Integration Adversary findings feed back: spec fixes -> test fixes -> implementation fixes. #### Phase 5: Hardening - Property-based tests for the parser via `fast-check` - Edge case stress tests for the geocoder - Final adversarial pass #### Phase 6: Convergence Done when the adversary is nitpicking style, not finding real bugs. Four dimensions must converge: specs, tests, implementation, and hardening. ## Chainlink Issue Tracking All 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. ### Milestones | ID | Phase | Issues | |----|-------|--------| | M1 | Phase 2: TDD Implementation | #1-#22 (scaffolding + 5 modules x (parent + 3 sub-issues) + styles) | | M2 | Phase 3: Adversarial Review | #23-#28 (per-module reviews + full codebase) | | M3 | Phase 4: Feedback Integration | #29-#31 (spec/test/impl fixes) | | M4 | Phase 5: Hardening | #32-#34 (property tests, stress tests, final adversary) | | M5 | Phase 6: Convergence | #35-#36 (convergence check, smoke test) | ### Commands ```bash chainlink tree # Full issue hierarchy chainlink list # All open issues chainlink ready # Issues with no open blockers (what to work on next) chainlink show # Full issue details with spec chainlink milestone show # Milestone progress chainlink blocked # What's waiting on what ``` ### Labels - `spec` — specification work - `test` — test writing - `impl` — implementation - `review` — adversarial review - `fix` — feedback integration - `infra` — scaffolding/build config ## Module Architecture ``` obs-map-viewer/ ├── main.ts # Plugin entry — view registration, commands, events ├── mapView.ts # ItemView subclass — sidebar, refresh, geo write-back, cursor sync ├── parser.ts # Pure parser — markdown bullet lists → Place[] ├── mapRenderer.ts # Leaflet map — markers, popups, selection, highlight ├── geocoder.ts # Nominatim geocoding — rate limiting, dedup, cancellation ├── styles.css # CSS using Obsidian variables for theme compat ├── manifest.json # Obsidian plugin manifest ├── package.json # Dependencies and build scripts ├── tsconfig.json # TypeScript config ├── esbuild.config.mjs # esbuild bundler config (CJS output, Leaflet bundled) ├── vitest.config.ts # Vitest test config └── tests/ ├── parser.test.ts ├── geocoder.test.ts ├── mapRenderer.test.ts ├── mapView.test.ts └── main.test.ts ``` ### Dependency Graph ``` main.ts → mapView.ts → parser.ts (pure) → geocoder.ts (effectful, fetch) → mapRenderer.ts (effectful, DOM/Leaflet) ``` `parser.ts` is the only pure module. All types (`Place`, etc.) are exported from it. ### Key Design Decisions 1. **Leaflet bundled via esbuild** — no CDN dependency, ~10K lines but tree-shaken 2. **Leaflet CSS inlined as string literal** — avoids needing a CSS loader 3. **CJS output, ES2018 target** — Obsidian compatibility 4. **No API keys** — Nominatim + Stadia Watercolor + CartoDB labels are all free 5. **Document as cache** — `geo:` coordinates stored in the note itself (portable, no external DB) 6. **Structured field parsing** — `: ` sub-bullets parsed into `fields: Record` for extensibility 7. **Write-back safety** — re-parse inside `vault.process()`, match by name, never use stale line numbers 8. **Geocoding mutex** — single in-flight operation via AbortController, prevents concurrent writes 9. **Theme integration** — all colors from Obsidian CSS variables ## For AI Agents When working on this project: 1. **Check `chainlink ready`** to see what's unblocked and ready to work on 2. **Check `chainlink show `** before starting any issue — the description IS the spec 3. **Follow strict TDD**: write tests first, verify they fail, then implement minimum to pass 4. **Never write implementation without a failing test demanding it** 5. **Run the adversary review** after completing each module (Phase 3 issues) 6. **Mark issues done** via `chainlink close ` when complete 7. **Use `chainlink start `** to track time on issues 8. **The spec in the issue is the source of truth** — if the code contradicts the issue description, the code is wrong