Skip to content

Phase 4: Notes Domain Chunker #45

Description

@tinkermonkey

Implement domains/notes.py with the NotesDomain class that splits markdown content into semantically coherent chunks with context headers using mistune for AST-based parsing.

Requirements

  • Implement NotesDomain class in domains/notes.py
  • Constructor: __init__(self, soft_limit: int = 512, hard_limit: int = 1024, min_floor: int = 64)
  • Implement chunk(content: NormalizedContent) -> list[Chunk] method:
    1. Parse markdown to AST using mistune with an AST renderer
    2. Walk the AST to identify block types: headings (h1–h6), code blocks (fenced), tables, paragraphs
    3. Split on headings — each heading and its subsequent sibling content until the next same-or-higher-level heading forms a candidate chunk
    4. Code blocks must be kept atomic (never split mid-block)
    5. Tables must be kept atomic (never split mid-table)
    6. Generate context headers as heading breadcrumb paths (e.g., "# Architecture > ## Storage > ### SQLite")
    7. Respect soft and hard token limits: join short adjacent sections below min_floor; split oversized sections at paragraph boundaries without exceeding hard_limit
    8. Compute chunk_hash = SHA-256(normalized_content) for each chunk — content excludes context header; apply whitespace normalization before hashing
    9. Prepend context header to content field before constructing Chunk (context header is part of the embedded text but excluded from the hash)
    10. Assign sequential chunk_index values
    11. Return list of Chunk instances
  • Token counting: use whitespace-split word count as proxy (1 word ≈ 1 token) — no tokenizer dependency for Phase 1
  • Implement domains/base.py with BaseDomain(ABC) abstract base class defining the chunk() method contract
  • Create tests/domains/test_notes.py with unit tests covering: heading splits, code block atomicity, table atomicity, context header format, soft/hard limit enforcement, chunk_hash determinism

Design Guidance

  • domains/notes.py already exists as a stub — implement NotesDomain there
  • domains/base.py already exists as a stub — implement BaseDomain(ABC) there
  • mistune import pattern: import mistune; md = mistune.create_markdown(renderer=mistune.AstRenderer())
  • The mistune AST renderer produces a list of block dicts. Each dict has a type key. Known types: heading (with attrs.level 1–6 and children), block_code (with attrs.info for language), table, paragraph, list, block_quote, thematic_break
  • Pin mistune to >=3.0,<4.0 (already in pyproject.toml) — add a regression test that verifies expected AST structure for a known input to catch upstream AST format changes
  • Heading breadcrumb algorithm: maintain a stack of heading texts indexed by level. When a heading at level N is encountered, pop all stack entries at levels >= N, push the new heading. The breadcrumb is >.join of stack entries formatted as #{level} {text}.
  • Whitespace normalization before hashing: re.sub(r'[ \t]+', ' ', text) then '\n'.join(line.rstrip() for line in text.splitlines()) then text.strip()
  • Context header format: "# Root > ## Section > ### Subsection" — the # count reflects the heading level
  • chunk_type field: use "standard" for normal text chunks, "code" for code block chunks, "table" for table chunks
  • domain_metadata field: for Phase 1, set to None or include {"heading_level": N} for heading-derived chunks
  • Oversized block splitting: when a paragraph block exceeds hard_limit, split at sentence boundaries (. , ? , ! ) as a fallback — never split a sentence mid-way
  • The chunk() method is the only required method on BaseDomain; its signature: def chunk(self, content: NormalizedContent) -> list[Chunk]

Acceptance Criteria

  • BaseDomain(ABC) defines abstract chunk(content: NormalizedContent) -> list[Chunk]
  • NotesDomain implements BaseDomain
  • Given a markdown document with H1, H2, H3 headings, each heading and its content form a separate chunk candidate
  • Context header for an H3 section under H2 under H1 is formatted as "# H1 text > ## H2 text > ### H3 text"
  • A fenced code block is never split across chunk boundaries
  • A markdown table is never split across chunk boundaries
  • Two adjacent heading sections both below min_floor tokens may be joined if combined size is within hard_limit
  • A chunk candidate exceeding hard_limit is split at paragraph boundaries
  • chunk_hash is identical for two chunks with the same content and different context headers
  • chunk_index values are sequential starting from 0
  • tests/domains/test_notes.py passes all cases
  • Mistune AST regression test passes for a known markdown input

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