GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

17. Bound the chunk count without dropping bytes

Context and problem

ADR-0012 makes one promise about every source file groove indexes: it contributes every byte it has, whether or not it parses. Nothing may be dropped for being outside a definition, and nothing may be dropped for being in a region the grammar could not read. ADR-0014 added a bound on how deeply a definition may sit and kept that promise on purpose: a file past the bound is chunked by lines rather than refused, and tagged parse:too-deep so the choice is visible.

A third bound predates both and kept no such promise. MAX_CHUNKS_PER_FILE says one file may contribute at most 512 chunks, and it was applied by keeping the first 512 pieces and dropping the rest. The pieces are sorted by position, so what it dropped was always the end of the file. A file of six hundred definitions was indexed from its first line to somewhere in its middle; the rest was in documents (the code parser keeps the source verbatim, so get_document returned the whole file) and in no chunk, so no search could reach it. One tracing::warn! line said so at index time and nothing else did — no tag on the document, nothing for groove doctor to read back, nothing in the documentation.

It was also applied outside the branch that chose the fallback, so it truncated the scope fallback’s output as well: a file degraded specifically to keep it whole then had its tail cut for the other reason.

The question this answers: what does a bound on chunk count mean, when the file has to survive it?

Decision drivers

Options considered

  1. Keep truncating, and say so. Add the tag and the doctor finding the audit asked for, leave the tail dropped.
  2. Chunk by lines instead, and accept that the count can exceed the bound when the configured budget is narrow.
  3. Chunk by lines instead, widening the line budget so the result fits the bound.
  4. Drop the bound, on the grounds that the 1 MiB byte cap already bounds the file.

Decision

Option 3. A file whose definitions want more chunks than the bound allows is chunked by lines — the same fallback the scope bound already used — and tagged parse:too-many-chunks. The line budget is widened where it has to be so that what comes back fits the bound.

Why not option 1. The tail of a file being unsearchable while get_document hands it back is not a limitation to write down; it is the failure ADR-0012 exists to prevent, and writing it down would not give anyone a way out. MAX_CHUNKS_PER_FILE is a constant rather than a setting, so the only remedy a finding could name would be “make the file smaller”.

Why not option 2. The line fallback is bounded by the budget, and the budget is a configuration key with no floor. A knowledge base that set max_chunk_chars to a small number would make one 1 MiB file produce hundreds of thousands of chunks, every one of them embedded while rebuild_index holds the embedder and the database — the availability shape ADR-0014 exists to prevent, reached from the other direction. The bound would have become advice.

Why not option 4. The byte cap bounds bytes, not chunks, and the two are not the same question. split_by_lines starts a new piece only when the next line would overrun the budget, so any two neighbouring pieces weigh more than the budget together, and a text weighing W non-whitespace characters yields fewer than 2W / budget + 1 pieces. Putting the shipped budget of 3500 and the 1 MiB cap into that gives roughly six hundred for a file with no whitespace in it — over the bound. (An arithmetic bound, not a measurement; the the_widened_budget_keeps_the_split_under_the_bound test checks the arithmetic against split_by_lines itself rather than restating it.) Dropping the bound would leave no answer at all to “how many chunks may one file contribute”.

That same arithmetic is what option 3 uses. Asking for fewer than limit pieces gives a budget of 2W / (limit - 1), and the fallback takes the wider of that and the configured one. The widening is derived from the file, so the same file still chunks the same way on any machine, and it is inert on any file that did not need it.

The bound itself stays at 512. Counting against this repository’s own sources (62 .rs files) in a scratch knowledge base, the widest file produced 297 chunks , so ordinary code has room to grow by about three-quarters before it meets the bound. What changed is what happens when it does.

Consequences

References