Implement the BaseAdapter abstract base class in adapters/base.py and create the FilesystemAdapter in adapters/filesystem.py that yields NormalizedContent for all .md files found recursively in a directory.
Requirements
- Implement
BaseAdapter(ABC) in adapters/base.py with:
- Abstract method:
fetch(source_ref: str) -> Iterator[NormalizedContent]
- Abstract property:
adapter_id -> str
- Abstract property:
domain -> Domain
- Abstract property:
normalizer_version -> str
- Concrete method:
register(document_store: DocumentStore) -> str — builds AdapterConfig, calls document_store.register_adapter(), returns adapter_id
- Create
adapters/filesystem.py with FilesystemAdapter(BaseAdapter):
- Constructor:
__init__(self, directory: str | Path)
adapter_id property: deterministic string derived from adapter type + directory path (e.g., f"filesystem:{Path(directory).resolve()}")
domain property: returns Domain.NOTES
normalizer_version property: returns "1.0.0"
fetch(): walks directory recursively using Path.rglob("*.md"), yields NormalizedContent for each .md file
- Each
NormalizedContent must have:
markdown: file contents read as UTF-8
source_id: relative path from base directory as string (e.g., "notes/meeting.md")
structural_hints: populated with file_path (absolute path string), modified_at (ISO 8601 from os.stat().st_mtime), file_size_bytes (from os.stat().st_size), plus boolean flags (has_headings, has_lists, has_tables) derived from simple content inspection
normalizer_version: "1.0.0"
- Files that are not
.md must not be yielded
- Subdirectories must be traversed recursively
- Create tests in
tests/adapters/ covering: recursive discovery, NormalizedContent field population, non-.md file exclusion, StructuralHints population
Design Guidance
adapters/base.py already exists as a stub — implement the ABC there, do not create a new file
adapters/filesystem.py does not exist — create it
- Import path for models:
from context_library.storage.models import NormalizedContent, StructuralHints, AdapterConfig, Domain
- Import path for document store:
from context_library.storage.document_store import DocumentStore
adapter_id determinism: the same directory path must always produce the same adapter_id. Use str(Path(directory).resolve()) as input to ensure absolute paths.
modified_at ISO 8601 format: datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
- Boolean flags in
StructuralHints from simple string inspection: has_headings = "#" in markdown, has_lists = bool(re.search(r'^[\-\*\+]\s', markdown, re.MULTILINE)), has_tables = "|" in markdown — these are heuristic approximations acceptable for Phase 1
natural_boundaries in StructuralHints: set to empty list [] for the filesystem adapter (domain chunker determines boundaries)
- The
register() concrete method in BaseAdapter should construct AdapterConfig(adapter_id=self.adapter_id, adapter_type=type(self).__name__, domain=self.domain, normalizer_version=self.normalizer_version, config=None) and call document_store.register_adapter(config)
- Handle file read errors gracefully: skip files that cannot be decoded as UTF-8 (log a warning, do not raise)
Acceptance Criteria
Dependencies
Phase 1: Data Models and Schema Foundation, Phase 2: Document Store (SQLite)
Parent Issue
Part of #40
Discussion
This work is detailed in discussion 41
Implement the
BaseAdapterabstract base class inadapters/base.pyand create theFilesystemAdapterinadapters/filesystem.pythat yieldsNormalizedContentfor all.mdfiles found recursively in a directory.Requirements
BaseAdapter(ABC)inadapters/base.pywith:fetch(source_ref: str) -> Iterator[NormalizedContent]adapter_id -> strdomain -> Domainnormalizer_version -> strregister(document_store: DocumentStore) -> str— buildsAdapterConfig, callsdocument_store.register_adapter(), returnsadapter_idadapters/filesystem.pywithFilesystemAdapter(BaseAdapter):__init__(self, directory: str | Path)adapter_idproperty: deterministic string derived from adapter type + directory path (e.g.,f"filesystem:{Path(directory).resolve()}")domainproperty: returnsDomain.NOTESnormalizer_versionproperty: returns"1.0.0"fetch(): walks directory recursively usingPath.rglob("*.md"), yieldsNormalizedContentfor each.mdfileNormalizedContentmust have:markdown: file contents read as UTF-8source_id: relative path from base directory as string (e.g.,"notes/meeting.md")structural_hints: populated withfile_path(absolute path string),modified_at(ISO 8601 fromos.stat().st_mtime),file_size_bytes(fromos.stat().st_size), plus boolean flags (has_headings,has_lists,has_tables) derived from simple content inspectionnormalizer_version:"1.0.0".mdmust not be yieldedtests/adapters/covering: recursive discovery,NormalizedContentfield population, non-.mdfile exclusion,StructuralHintspopulationDesign Guidance
adapters/base.pyalready exists as a stub — implement the ABC there, do not create a new fileadapters/filesystem.pydoes not exist — create itfrom context_library.storage.models import NormalizedContent, StructuralHints, AdapterConfig, Domainfrom context_library.storage.document_store import DocumentStoreadapter_iddeterminism: the same directory path must always produce the sameadapter_id. Usestr(Path(directory).resolve())as input to ensure absolute paths.modified_atISO 8601 format:datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()StructuralHintsfrom simple string inspection:has_headings = "#" in markdown,has_lists = bool(re.search(r'^[\-\*\+]\s', markdown, re.MULTILINE)),has_tables = "|" in markdown— these are heuristic approximations acceptable for Phase 1natural_boundariesinStructuralHints: set to empty list[]for the filesystem adapter (domain chunker determines boundaries)register()concrete method inBaseAdaptershould constructAdapterConfig(adapter_id=self.adapter_id, adapter_type=type(self).__name__, domain=self.domain, normalizer_version=self.normalizer_version, config=None)and calldocument_store.register_adapter(config)Acceptance Criteria
BaseAdapteris an ABC with the specified abstract methods and propertiesBaseAdapter.register()callsdocument_store.register_adapter()and returns theadapter_idFilesystemAdapterimplements all abstract members ofBaseAdapter.mdfiles in nested subdirectories,fetch()yieldsNormalizedContentfor every.mdfile.mdfiles, those files are not yieldedNormalizedContent.source_idis the relative path from the base directoryNormalizedContent.structural_hintscontains a non-Nonefile_path,modified_at(ISO 8601 format), andfile_size_bytesadapter_idis deterministic: same directory → same id on repeated callsFilesystemAdapter.domainreturnsDomain.NOTEStests/adapters/passDependencies
Phase 1: Data Models and Schema Foundation, Phase 2: Document Store (SQLite)
Parent Issue
Part of #40
Discussion
This work is detailed in discussion 41