A wayfinder inspired map plugin for obisidian

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, 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#

* 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 <key>: <value> 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.

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.

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#

chainlink tree              # Full issue hierarchy
chainlink list              # All open issues
chainlink ready             # Issues with no open blockers (what to work on next)
chainlink show <id>         # Full issue details with spec
chainlink milestone show <id>  # 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 cachegeo: coordinates stored in the note itself (portable, no external DB)
  6. Structured field parsing<key>: <value> sub-bullets parsed into fields: Record<string, string> 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 <id> 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 <id> when complete
  7. Use chainlink start <id> 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