Skip to content

Phase 3: Adapter Contract and Filesystem Adapter #44

Description

@tinkermonkey

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

  • BaseAdapter is an ABC with the specified abstract methods and properties
  • BaseAdapter.register() calls document_store.register_adapter() and returns the adapter_id
  • FilesystemAdapter implements all abstract members of BaseAdapter
  • Given a directory with .md files in nested subdirectories, fetch() yields NormalizedContent for every .md file
  • Given a directory with non-.md files, those files are not yielded
  • Each yielded NormalizedContent.source_id is the relative path from the base directory
  • Each yielded NormalizedContent.structural_hints contains a non-None file_path, modified_at (ISO 8601 format), and file_size_bytes
  • adapter_id is deterministic: same directory → same id on repeated calls
  • FilesystemAdapter.domain returns Domain.NOTES
  • Tests in tests/adapters/ pass

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions