Skip to content

Phase 5: Differ #46

Description

@tinkermonkey

Implement core/differ.py with the Differ class that performs hash-based change detection between document versions, enabling the pipeline to skip unchanged content during re-ingestion.

Requirements

  • Implement Differ class in core/differ.py
  • Implement diff(prev_markdown: str | None, curr_markdown: str, prev_chunk_hashes: set[str] | None, curr_chunk_hashes: set[str]) -> DiffResult method:
    1. If prev_markdown is None (first ingest): return DiffResult(changed=True, added_hashes=curr_chunk_hashes, removed_hashes=set(), unchanged_hashes=set(), prev_hash=None, curr_hash=SHA256(normalized(curr_markdown)))
    2. Apply whitespace normalization to both markdowns
    3. Compute SHA-256 of both normalized markdowns
    4. If hashes match: return DiffResult(changed=False, added_hashes=set(), removed_hashes=set(), unchanged_hashes=curr_chunk_hashes, prev_hash=prev_hash, curr_hash=curr_hash)
    5. If hashes differ: compute set operations on chunk hashes:
      • added = curr_chunk_hashes - prev_chunk_hashes
      • removed = prev_chunk_hashes - curr_chunk_hashes
      • unchanged = curr_chunk_hashes & prev_chunk_hashes
    6. Return DiffResult(changed=True, added_hashes=added, removed_hashes=removed, unchanged_hashes=unchanged, prev_hash=prev_hash, curr_hash=curr_hash)
  • Whitespace normalization must treat trailing-whitespace-only edits as unchanged
  • Create tests/core/test_differ.py covering: first ingest (all added), unchanged document (full-document hash match), modified chunk (partial change), added chunk, removed chunk, whitespace-only change (classified as unchanged)

Design Guidance

  • core/differ.py already exists as a stub — implement Differ there
  • Import: from context_library.storage.models import DiffResult
  • Whitespace normalization function (reuse same normalization as Chunk hash computation):
    import re
    def _normalize(text: str) -> str:
        text = re.sub(r'[ \t]+', ' ', text)
        text = '\n'.join(line.rstrip() for line in text.splitlines())
        return text.strip()
  • SHA-256 computation: hashlib.sha256(normalized.encode('utf-8')).hexdigest()
  • The Differ class has no constructor parameters — it is stateless
  • The differ does NOT chunk the content itself; it receives pre-computed curr_chunk_hashes from the domain chunker. This is important: the pipeline calls the chunker first, then the differ.
  • DiffResult is a frozen Pydantic model — construct it with keyword arguments
  • The differ's set operations work on chunk_hash strings (SHA-256 hex digests of chunk content)

Acceptance Criteria

  • Differ.diff() with prev_markdown=None returns changed=True with all curr_chunk_hashes in added_hashes
  • Differ.diff() with identical content (after normalization) returns changed=False regardless of whitespace differences
  • Differ.diff() with one modified chunk correctly places it in added_hashes (new hash) and removed_hashes (old hash)
  • Differ.diff() with a new chunk added correctly places it in added_hashes only
  • Differ.diff() with a chunk removed correctly places it in removed_hashes only
  • Content differing only in trailing whitespace or blank lines returns changed=False
  • tests/core/test_differ.py passes all cases

Dependencies

Phase 1: Data Models and Schema Foundation

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