Implement the ObsidianAdapter that ingests an Obsidian vault, extracting per-note YAML frontmatter metadata and vault-level wikilink graph data using obsidiantools.
Requirements
- Ingest an Obsidian vault directory, treating each
.md note as a source ref (FR-5.1)
- Extract per-note metadata: YAML frontmatter, tags, aliases using a frontmatter parsing library (FR-5.2)
- Extract vault-level graph metadata (backlinks, forward wikilinks, connectivity) using
obsidiantools (FR-5.3)
- Store tags, aliases, frontmatter properties, wikilink graph edges, creation/modification dates in
domain_metadata on each Chunk (FR-5.4)
- Use
_watching.py FileSystemWatcher when configured for push-based ingestion (FR-5.5)
- No format conversion — vault notes are already markdown (FR-5.6)
- Set
domain = Domain.NOTES (FR-5.7)
adapter_id must be f"obsidian:{resolved_vault_path}"
- Add
obsidiantools to optional dependencies under obsidian group in pyproject.toml
Design Guidance
File to create: src/context_library/adapters/obsidian.py
Note: The exploration found no existing stub for this file — create it from scratch.
Class signature:
class ObsidianAdapter(BaseAdapter):
def __init__(
self,
vault_path: Path,
poll_strategy: PollStrategy = PollStrategy.PULL,
) -> None:
self._vault_path = vault_path.resolve()
self._poll_strategy = poll_strategy
self._vault = None # lazy-loaded obsidiantools.Vault
@property
def adapter_id(self) -> str:
return f"obsidian:{self._vault_path}"
@property
def domain(self) -> Domain:
return Domain.NOTES
@property
def normalizer_version(self) -> str:
return "1.0.0"
def fetch(self, source_ref: str) -> Iterator[NormalizedContent]: ...
obsidiantools integration:
import obsidiantools.api as otools
# Initialize and connect vault (builds the wikilink graph)
vault = otools.Vault(self._vault_path).connect()
# Per-note graph data:
backlinks = vault.get_backlinks(note_name) # notes linking TO this note
wikilinks = vault.get_wikilinks(note_name) # notes this note links TO
YAML frontmatter parsing: Use python-frontmatter or PyYAML directly:
import frontmatter # python-frontmatter package
post = frontmatter.load(str(note_path))
fm_data = post.metadata # dict of YAML frontmatter
body = post.content # markdown without frontmatter
tags = fm_data.get('tags', [])
aliases = fm_data.get('aliases', [])
Metadata to store in domain_metadata (passed through extra_metadata on StructuralHints):
{
"tags": ["tag1", "tag2"],
"aliases": ["alias1"],
"frontmatter": {"key": "value", ...}, # all frontmatter properties
"wikilinks": ["Note Title 1", "Note Title 2"], # forward links
"backlinks": ["Note Title 3"], # notes that link to this note
"created_at": "2024-01-01T00:00:00",
"modified_at": "2024-01-02T00:00:00",
}
NotesDomain reuse: The ObsidianAdapter sets domain = Domain.NOTES, so the existing NotesDomain chunker handles chunking. The adapter passes vault metadata through StructuralHints.extra_metadata so the pipeline can store it in Chunk.domain_metadata.
pyproject.toml change:
[project.optional-dependencies]
obsidian = ["obsidiantools", "python-frontmatter", "watchdog"]
Push mode: When poll_strategy == PollStrategy.PUSH, instantiate FileSystemWatcher scoped to vault path with extensions={'.md'}.
Acceptance Criteria
Dependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata), Phase 3: Shared Filesystem Watcher (_watching.py)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84
Implement the ObsidianAdapter that ingests an Obsidian vault, extracting per-note YAML frontmatter metadata and vault-level wikilink graph data using obsidiantools.
Requirements
.mdnote as a source ref (FR-5.1)obsidiantools(FR-5.3)domain_metadataon eachChunk(FR-5.4)_watching.pyFileSystemWatcherwhen configured for push-based ingestion (FR-5.5)domain = Domain.NOTES(FR-5.7)adapter_idmust bef"obsidian:{resolved_vault_path}"obsidiantoolsto optional dependencies underobsidiangroup inpyproject.tomlDesign Guidance
File to create:
src/context_library/adapters/obsidian.pyNote: The exploration found no existing stub for this file — create it from scratch.
Class signature:
obsidiantools integration:
YAML frontmatter parsing: Use
python-frontmatteror PyYAML directly:Metadata to store in domain_metadata (passed through extra_metadata on StructuralHints):
{ "tags": ["tag1", "tag2"], "aliases": ["alias1"], "frontmatter": {"key": "value", ...}, # all frontmatter properties "wikilinks": ["Note Title 1", "Note Title 2"], # forward links "backlinks": ["Note Title 3"], # notes that link to this note "created_at": "2024-01-01T00:00:00", "modified_at": "2024-01-02T00:00:00", }NotesDomain reuse: The
ObsidianAdaptersetsdomain = Domain.NOTES, so the existingNotesDomainchunker handles chunking. The adapter passes vault metadata throughStructuralHints.extra_metadataso the pipeline can store it inChunk.domain_metadata.pyproject.toml change:
Push mode: When
poll_strategy == PollStrategy.PUSH, instantiateFileSystemWatcherscoped to vault path withextensions={'.md'}.Acceptance Criteria
ObsidianAdapter(vault_path=path).adapter_idreturnsf"obsidian:{path.resolve()}"fetch("")on a vault directory yields oneNormalizedContentper.mdnoteNormalizedContent.markdowncontains the note body without YAML frontmatter blockNormalizedContent.structural_hintsincludestags,aliases, andfrontmatterfrom YAML frontmatterNormalizedContent.structural_hintsincludeswikilinks(forward links) andbacklinksfor the noteNormalizedContentwithout errordomainproperty returnsDomain.NOTESpoll_strategy=PollStrategy.PUSH, aFileSystemWatcherinstance scoped to the vault path is createdObsidianAdapteris importable fromcontext_library.adapters.obsidianDependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata), Phase 3: Shared Filesystem Watcher (_watching.py)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84