feat(core,chunker): structure-aware chunker (Step 1.5)#62
Merged
officialCodeWork merged 1 commit intoMay 25, 2026
Merged
Conversation
Step 1.5 introduces the chunking stage of the ingest pipeline. The new Chunker SPI in rag-core sits between Parser (Step 1.3) / OCR (Step 1.4) and the downstream embedder + vector store. The default HeadingAwareChunker emits heading sections with parent-linked body chunks, token-budgeted with sentence-boundary fallback. ### New SPI — rag-core 0.9.0 → 0.10.0 - `rag_core.spi.chunker.Chunker` ABC: `async chunk(ctx, parsed) -> list[Chunk]`. - `rag_core.spi.noop.NoopChunker` — one chunk per non-empty block, no parent links; used by tests and as fallback when no real chunker is configured. - Contract invariants enforced by `tests/contract/test_chunker.py`: monotonic 0-based `position`; `parent_id` references earlier chunks only (no forward refs); tenant_id / document_id propagated from ctx and parsed; whitespace-only blocks dropped. - SPI signature linter extended (`tests/contract/spi_signature.py`) so any future Chunker method must take `ctx: RequestContext` first. ### New rag-chunker package — packages/chunker/, v0.1.0 - `HeadingAwareChunker` — walks `ParsedDocument.blocks`, maintains a heading-level stack for parent-link semantics, accumulates body blocks under each heading up to a token budget, splits oversized blocks on sentence boundaries (pure-Python regex, no NLP deps), hard-splits sentences that still exceed the budget. Overlap is within-section only; never bleeds across heading boundaries. - `TokenCounter` Protocol + `TiktokenCounter` (default cl100k_base); swap to o200k_base or any custom tokenizer via the constructor. - `sentence.split_sentences()` — stdlib-only sentence splitter using a forward scan (no variable-width lookbehind so it works on stdlib `re`). - `ocr_adapter.ocr_result_to_parsed_document()` — turns an `OCRResult` (Step 1.4) into a synthetic `ParsedDocument` so OCR'd image-only pages flow through the same chunker as native parser output; preserves engine, per-region confidence, and bbox in `Block.attributes`. ### ragctl chunk New `ragctl chunk <path>` smoke command (`--max-tokens`, `--overlap-tokens`, `--mime`, `--chunks N`). Pipes parser output into the chunker and prints per-kind tally + first-N chunks with parent-link arrows. Mirrors the `ragctl parse` / `ragctl ocr` shape. ### Tests - `tests/chunker/test_sentence.py` (5) — splitter on punctuation, no-op, newlines, quote-aware boundary. - `tests/chunker/test_tokens.py` (4) — TiktokenCounter contract + special-token safety. - `tests/chunker/test_heading_aware.py` (16) — heading hierarchy + parent linking, body flush on budget, overlap within section, overlap suppression across headings, sentence-boundary split for oversized blocks, ACL/tenant propagation, error wrapping, helper functions. - `tests/chunker/test_ocr_adapter.py` (3) — empty input, region → paragraph-block with bbox attributes, whitespace skip. - `tests/contract/test_chunker.py` (7) — SPI conformance. - `packages/ragctl/tests/test_chunk.py` (5) — CLI surface. Full sweep clean: `uv run pytest tests/ packages/ -x -q`, `uv run ruff check .`, `uv run ruff format --check .`, `uv run mypy packages/ apps/gateway/` (131 source files), `uv run python scripts/check_logging.py`. ### Policy boundary The chunker is upstream of PolicyEngine in the ingest direction (no Chunks exist for the PDP to ACL-check yet). The Step 1.7 PII detector and Step 1.10 ingest pipeline are the enforcement points. rag-chunker depends only on rag-core, not on rag-policy. PolicyEngine coverage linter passes — no governed calls in the new code. ### Cross-platform Default install is pure-Python except for the `tiktoken` extension, which ships wheels for ubuntu / macos / windows-latest. Unit tests use a deterministic `_WordCounter` stand-in so assertions about token boundaries don't depend on tiktoken's BPE specifics. ## Documentation - `docs/reference/chunker.md` — `Chunker` SPI, `HeadingAwareChunker` semantics, `TokenCounter` protocol, OCR adapter, CLI, errors, extension points. - `docs/architecture/chunker.md` — why structure-aware, boundary detection strategy, parent-link semantics, overlap policy, token counting rationale, OCR-region handling, pipeline placement, policy boundary, hot-path discipline, cross-platform notes, ADR backlinks. - `docs/README.md` — both new entries added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 1.5 introduces the chunking stage of the ingest pipeline. New `Chunker` SPI in rag-core, default `HeadingAwareChunker` in the new `rag-chunker` package, OCR adapter so Step 1.4 output flows through the same chunker, and a `ragctl chunk` smoke command.
Chunker SPI — rag-core 0.9.0 → 0.10.0
HeadingAwareChunker — packages/chunker/ v0.1.0
Walks `ParsedDocument.blocks` and:
Pluggable tokenizer
`TokenCounter` Protocol + `TiktokenCounter` (default `cl100k_base`). Swap encodings (`o200k_base` for GPT-4o-style budgets) via constructor, or plug in a HuggingFace / Cohere tokenizer by implementing the two-method Protocol. `tiktoken` is a default dependency — pure-extension wheels for every platform in the CI matrix, no extras needed.
OCR adapter
`ocr_result_to_parsed_document(ctx, result, …)` turns Step 1.4's `OCRResult` into a synthetic `ParsedDocument` so OCR'd image-only pages flow through `HeadingAwareChunker` identically to native parser output. Each `OCRRegion` becomes one paragraph-typed `Block` whose `attributes` preserve `engine`, `ocr_confidence`, `bbox` coordinates, and optional `page` for downstream consumers (GUI overlay, faithfulness scoring).
ragctl chunk
```bash
ragctl chunk doc.md # default budget (512/64)
ragctl chunk doc.txt --max-tokens 256 --overlap-tokens 32
ragctl chunk doc.md -n 10 # show first 10 chunks
```
Pipes the Step 1.3 parser into the Step 1.5 chunker and prints block/chunk counts, per-kind tally, and first-N chunks with parent-link arrows. 5 CLI tests.
Policy boundary
The chunker is upstream of PolicyEngine in the ingest direction (no `Chunk`s exist for the PDP to ACL-check yet). The Step 1.7 PII detector and Step 1.10 ingest pipeline are the enforcement points. `rag-chunker` depends only on `rag-core`, not on `rag-policy`. PolicyEngine coverage linter passes.
Cross-platform
Pure-Python except the `tiktoken` extension, which ships wheels for ubuntu / macos / windows-latest. Unit tests use a deterministic `_WordCounter` stand-in so token-boundary assertions don't depend on tiktoken's BPE specifics.
Tests
Full local sweep clean:
```
uv run pytest tests/ packages/ -x -q # 40 new tests, no regressions
uv run ruff check . # clean
uv run ruff format --check . # clean
uv run mypy packages/ apps/gateway/ # 131 source files, no issues
uv run python scripts/check_logging.py # RAG001 clean
```
Documentation
Test plan
🤖 Generated with Claude Code