ADR-002: AST-Pure Chunking
Replaces a broken chunker with AST-pure logic so every line belongs to exactly one semantically typed chunk and no orphans remain.
What this file does
Replaces a broken chunker with AST-pure logic so every line belongs to exactly one semantically typed chunk and no orphans remain.
When to use it
- Your code chunker leaves orphan lines or splits methods mid-statement
- You need chunks that preserve AST node types like function, class, or module
- You want JSDoc comments attached to their method chunk for better embeddings
- You are building a code indexing or retrieval system that consumes chunk metadata
Assumes this stack
ADR-002: AST-Pure Chunking
Status: Accepted
Date: 2026-03-02
Supersedes: Previous extractChunks() + splitLargeChunk() approach
Related: ADR-001 (chunk size limits)
Problem
The previous chunker had two critical flaws:
1. ~16% of code was orphaned (not in any chunk)
Tested on indexer.ts (1562 lines): 227 non-empty lines were not covered by any chunk.
| Orphan type | Example | Why missed |
|---|---|---|
| File imports | import { foo } from "./bar" | import_statement not in getChunkType() |
| Module constants | const MAX = 100 | lexical_declaration not in getChunkType() |
| Class fields | private field: string | public_field_definition not recognized |
| Static properties | static readonly ITEMS = [...] | Same — field definition |
| JSDoc comments | /** Does something */ | comment nodes separate from method nodes |
| Small methods | clear(): void { ... } | Below MIN_CHUNK_TOKENS threshold |
2. Oversized nodes got blind line-split
splitLargeChunk() cut by line count with zero AST awareness. A 640-line method
became 22 arbitrary 29-line blocks that started/ended mid-statement.
Decision
Replace extractChunks() + splitLargeChunk() with AST-pure chunking:
Every line of source code belongs to exactly one chunk. Every chunk maps to an AST node (or connected group of sibling nodes). No orphans. No blind splits.
New Architecture
Step 1: Top-down AST walk with size-aware decisions (processChildren)
For each child node:
if recognized type && fits MAX_CHUNK_TOKENS → emit as one chunk
if recognized type && too large && container (class/module) → descend into body
if recognized type && too large && leaf (function/method) → split into connected parts
if unrecognized (import, field, comment) → accumulate as gap
Step 2: Connected parts for oversized leaves (splitIntoConnectedParts)
Oversized functions/methods split by line count at MAX_CHUNK_TOKENS boundaries, but each part carries:
- Original
chunkType(not "block") - Name with
(part K/N)suffix partIndex/totalPartsfor downstream consumers
Step 3: Gap absorption (flushGap)
Non-chunk siblings (imports, fields, comments) are handled:
- JSDoc comments immediately before a method → included in that method's chunk
- Field declarations in class body → merged into a "fields" module chunk
- File header (imports + constants before first function) → "preamble" module chunk
- Trivial gaps (< 10 chars) → dropped
Data Model Changes
Added to ParsedChunk and CodeChunk:
partIndex?: number; // 1-based index within the group
totalParts?: number; // total parts in the group
Consequences
Positive
- ~100% code coverage (no orphan lines)
- Every chunk has semantic type (function, method, class, module)
- JSDoc attached to its method (better embeddings)
- Class fields grouped as meaningful chunks
- File preamble (imports/constants) preserved
- Oversized splits preserve original type + are linked via partIndex/totalParts
splitLargeChunk()eliminated (source of "block" type + cross-boundary splits)
Negative
- Chunk count increases ~10-15% (gap chunks for fields/imports)
- Gap chunks ("fields", "preamble") have lower search value but prevent orphans
- Comment attachment is heuristic (JSDoc goes to next sibling — correct 99% of time)
Migration
- Requires
claudemem index --forceto re-index after this change splitLargeChunk()removed, replaced bysplitIntoConnectedParts()extractChunks()+walkTree()removed, replaced byextractChunksAST()+processChildren()fallbackChunk()kept for truly unparseable files only
What's inside
Problem analysis, decision statement, three-step architecture, data model changes, and consequences with migration notes
Change this for your project
- Replace
indexer.tswith your own test file path if you run the orphan analysis - Replace
claudemem index --forcewith your own re-index command - Replace
MAX_CHUNK_TOKENSandMIN_CHUNK_TOKENSwith your own thresholds
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Gap absorption that attaches JSDoc to the next sibling and groups field declarations into a single chunk
- Oversized leaf splitting that preserves the original chunk type and links parts via partIndex/totalParts
Related Documents
基于命题分块以增强RAG
Implements proposition chunking for RAG: decomposes documents into atomic facts, evaluates quality, and compares retrieval against standard chunking.
TileMap Chunk Manager
Divides a large tilemap into fixed-size chunks, loading and rendering only those visible to reduce memory and draw calls by over 99%.
Message Chunking for MCP stdio Transport
Explains macOS 64KB pipe write limit and provides a chunking layer for MCP stdio transport that splits and reassembles large messages transparently.
🤖 n8n AI Agent Mastery Course 2025
Serves as the main README for an n8n course repository, listing 10+ AI agent workflow projects with links to YouTube tutorials and Docker setup instructions.