Skip to content

Commit e19ad80

Browse files
committed
fix(cpp): stop CRLF checkout from breaking the chunking parity test
The Windows job checked the fixtures out with translated line endings, so the documents no longer matched the chunks committed alongside them and the parity test failed. Both runtimes still agree on CRLF input — verified by rerunning the Python splitter against a CRLF copy — so the contract was intact and only the fixture bytes moved. Marks the fixture directory -text, next to the existing rule that keeps the image fixtures from being mangled the same way, and records the line endings each case was generated from so a future translation fails with that reason rather than an unreadable chunk diff. parity_unicode.md carries CRLF on purpose, so the flag is per-case rather than a blanket "no CR" assertion.
1 parent eb6f1b0 commit e19ad80

4 files changed

Lines changed: 36 additions & 3 deletions

File tree

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
cpp/tests/fixtures/*.png binary
33
cpp/tests/fixtures/*.jpg binary
44

5+
# Chunk-boundary fixtures are a byte-exact cross-runtime contract; a CRLF
6+
# checkout rewrites the documents the committed chunks were generated from.
7+
cpp/tests/fixtures/chunking/* -text
8+
59
# Every email-agent PR appends an [Unreleased] bullet to this changelog;
610
# without a union merge, sibling PRs merged close together conflict on the
711
# same insertion point.

cpp/tests/fixtures/chunking/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ The documents cover the branches that decide chunk boundaries:
1616
| `parity_unicode.md` | Non-ASCII capitals, non-breaking space, CRLF |
1717
| `parity_three_sections.txt` / `parity_four_sections.txt` | The `sections <= 3` branch, from either side |
1818

19+
Each case records `file_has_crlf`, the line endings its chunks were generated
20+
from. The C++ test compares it against the bytes on disk so a checkout that
21+
translated them fails with that reason instead of an unreadable chunk diff.
22+
`.gitattributes` marks this directory `-text` to stop the translation happening
23+
in the first place — chunk boundaries are a byte-exact contract, and
24+
`parity_unicode.md` carries CRLF on purpose.
25+
1926
## Regenerating
2027

2128
Only needed when a fixture changes or the Python splitter changes. Run against
@@ -40,8 +47,10 @@ def chunk(text, size, overlap):
4047
# For each {"file", "chunk_size", "chunk_overlap"} case in parity_expected.json.
4148
# read_bytes().decode() — NOT read_text(), whose newline translation would hide
4249
# a CR that the C++ extractor (binary read) keeps.
43-
# text = Path(case["file"]).read_bytes().decode("utf-8").strip()
44-
# case["chunks"] = chunk(text, case["chunk_size"], case["chunk_overlap"])
50+
# raw = Path(case["file"]).read_bytes()
51+
# case["file_has_crlf"] = b"\r\n" in raw
52+
# case["chunks"] = chunk(raw.decode("utf-8").strip(),
53+
# case["chunk_size"], case["chunk_overlap"])
4554
```
4655

4756
A regenerated file that changes existing chunks means the Python algorithm moved;

cpp/tests/fixtures/chunking/parity_expected.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
2-
"_note": "Expected chunk boundaries produced by RAGSDK._split_text_into_chunks (src/gaia/rag/sdk.py). Regenerate by calling that method on the fixture text with the chunk_size/chunk_overlap of each case.",
2+
"_note": "Expected chunk boundaries produced by RAGSDK._split_text_into_chunks (src/gaia/rag/sdk.py) for the documents in this directory. file_has_crlf records the line endings each case was generated from; see README.md.",
33
"cases": [
44
{
55
"file": "parity_sections.md",
66
"chunk_size": 60,
77
"chunk_overlap": 15,
8+
"file_has_crlf": false,
89
"chunks": [
910
"# GAIA Retrieval Notes The retrieval stack indexes documents locally so a question can be answered\nwithout sending the source text to a hosted model. Everything below is a\ndescription of that pipeline as it exists in the Python runtime today.",
1011
"of that pipeline as it exists in the Python runtime today. ## Extraction\n\nText files are read as UTF-8, with a latin-1 fallback for legacy encodings.\nMarkdown is treated as text; the headers are what the splitter later uses to\nfind section boundaries. Source files go down the same path.",
@@ -22,6 +23,7 @@
2223
"file": "parity_sections.md",
2324
"chunk_size": 500,
2425
"chunk_overlap": 100,
26+
"file_has_crlf": false,
2527
"chunks": [
2628
"# GAIA Retrieval Notes The retrieval stack indexes documents locally so a question can be answered\nwithout sending the source text to a hosted model. Everything below is a\ndescription of that pipeline as it exists in the Python runtime today. ## Extraction\n\nText files are read as UTF-8, with a latin-1 fallback for legacy encodings.\nMarkdown is treated as text; the headers are what the splitter later uses to\nfind section boundaries. Source files go down the same path. ## Chunking Chunks are sized in estimated tokens, where one token is approximated as four\ncharacters. A paragraph that fits inside the budget is kept whole so a single\nidea does not straddle two chunks. A paragraph that does not fit is split on\nsentence boundaries instead of on words. --- ## Overlap Each chunk carries the tail of its predecessor. Without that overlap a question\nwhose answer spans a boundary retrieves half an answer, which reads as a\nconfident but incomplete response. The overlap is trimmed to a word boundary. ## Encoding Notes A café menu, a naïve reader, and a résumé with accents all round-trip through\nthe extractor unchanged. Counting is done in code points, not bytes, so\nmulti-byte text does not shift chunk boundaries between the two runtimes. ## Limits The index is bounded by a maximum file count and a maximum chunk count. When\neither limit is reached the SDK refuses to index more rather than silently\nevicting content a user still expects to be searchable."
2729
]
@@ -30,6 +32,7 @@
3032
"file": "parity_prose.txt",
3133
"chunk_size": 40,
3234
"chunk_overlap": 10,
35+
"file_has_crlf": false,
3336
"chunks": [
3437
"Retrieval quality is decided long before the embedding model runs. It is decided by where the text was cut.",
3538
"is decided by where the text was cut. A chunk that ends mid-argument gives the reranker two halves that each look irrelevant, and the answer that comes back is confidently wrong.",
@@ -45,6 +48,7 @@
4548
"file": "parity_prose.txt",
4649
"chunk_size": 120,
4750
"chunk_overlap": 30,
51+
"file_has_crlf": false,
4852
"chunks": [
4953
"Retrieval quality is decided long before the embedding model runs. It is decided by where the text was cut. A chunk that ends mid-argument gives the reranker two halves that each look irrelevant, and the answer that comes back is confidently wrong. Dr. Ada Lovelace would have called that a machine doing exactly what it was told. The splitter therefore prefers sentence boundaries over word boundaries, e.g. it will overshoot the token budget slightly rather than cut a clause in half.",
5054
"boundaries over word boundaries, e.g. it will overshoot the token budget slightly rather than cut a clause in half. Mr. Babbage, vs. the alternative, would probably agree. That preference costs a little index size and buys a lot of answer quality, i.e. it is the trade every retrieval system eventually makes. Overlap is the second half of the same idea, etc. Without it, a fact that straddles two chunks is retrievable from neither. The second paragraph is short.",
@@ -55,6 +59,7 @@
5559
"file": "parity_mixed.txt",
5660
"chunk_size": 45,
5761
"chunk_overlap": 12,
62+
"file_has_crlf": false,
5863
"chunks": [
5964
"Release Checklist",
6065
"Release Checklist This file exercises the title-line heuristic: a short capitalized line with no\ntrailing punctuation, surrounded by blank lines, is treated as a section break.\n\nBuild The Wheel",
@@ -73,6 +78,7 @@
7378
"file": "parity_mixed.txt",
7479
"chunk_size": 200,
7580
"chunk_overlap": 40,
81+
"file_has_crlf": false,
7682
"chunks": [
7783
"Release Checklist This file exercises the title-line heuristic: a short capitalized line with no\ntrailing punctuation, surrounded by blank lines, is treated as a section break.\n\nBuild The Wheel Run the packaging job and confirm the artifact hash matches the one recorded in\nthe release notes. If it does not, stop and investigate before tagging. ===\n\nlowercase heading is not a boundary A line that starts lowercase is left inside the previous section, and so is a\nline that ends with punctuation, because the heuristic requires a capital start\nand no trailing period. This line is far too long to be treated as a section title because it runs well\npast the one hundred character limit the heuristic enforces on candidate lines. # Hash Marks Start A Section",
7884
"be treated as a section title because it runs well past the one hundred character limit the heuristic enforces on candidate lines. # Hash Marks Start A Section Any line beginning with a hash is a boundary regardless of length or case. That\ncovers Markdown headers and shell comments alike, which is why a source file\nfull of comments fragments into many sections.\n\nFinal Notes Dr. Knuth wrote about literate programming. He argued that a program should be\nwritten for a human reader first, e.g. as an essay, and only incidentally for\nthe machine. That argument applies to chunking too: the cut points should fall\nwhere a human would pause, not where a byte counter happens to land."
@@ -82,6 +88,7 @@
8288
"file": "parity_unicode.md",
8389
"chunk_size": 35,
8490
"chunk_overlap": 8,
91+
"file_has_crlf": true,
8592
"chunks": [
8693
"# Überblick Über die Chunking-Strategie",
8794
"Über die Chunking-Strategie Die Zeile darüber beginnt mit einem Großbuchstaben und endet ohne Satzzeichen, also behandelt die Heuristik sie als Abschnittstitel.",
@@ -97,6 +104,7 @@
97104
"file": "parity_unicode.md",
98105
"chunk_size": 150,
99106
"chunk_overlap": 25,
107+
"file_has_crlf": true,
100108
"chunks": [
101109
"# Überblick Über die Chunking-Strategie Die Zeile darüber beginnt mit einem Großbuchstaben und endet ohne Satzzeichen, also behandelt die Heuristik sie als Abschnittstitel. Ελληνικά κείμενα Greek and Cyrillic headings must start a section too, exactly as they do in the Python runtime. Привет is a heading below. Привет мир A paragraph separated by a non-breaking space line follows this one. That line held only a non-breaking space, which Python treats as whitespace, so the two paragraphs are separated rather than joined. Đường dẫn tài liệu",
102110
"treats as whitespace, so the two paragraphs are separated rather than joined. Đường dẫn tài liệu Vietnamese capitals are also cased, so this heading opens its own section and the chunk boundaries stay identical between the two runtimes."
@@ -106,6 +114,7 @@
106114
"file": "parity_sections.md",
107115
"chunk_size": 40,
108116
"chunk_overlap": 0,
117+
"file_has_crlf": false,
109118
"chunks": [
110119
"# GAIA Retrieval Notes The retrieval stack indexes documents locally so a question can be answered\nwithout sending the source text to a hosted model.",
111120
"GAIA Retrieval Notes The retrieval stack indexes documents locally so a question can be answered without sending the source text to a hosted model. Everything below is a\ndescription of that pipeline as it exists in the Python runtime today.",
@@ -125,6 +134,7 @@
125134
"file": "parity_three_sections.txt",
126135
"chunk_size": 200,
127136
"chunk_overlap": 40,
137+
"file_has_crlf": false,
128138
"chunks": [
129139
"# Alpha The first section body is one line that ends with a period. # Beta The second section body is also a single line ending in a period. # Gamma The third section body closes this fixture out with one more line."
130140
]
@@ -133,6 +143,7 @@
133143
"file": "parity_four_sections.txt",
134144
"chunk_size": 200,
135145
"chunk_overlap": 40,
146+
"file_has_crlf": false,
136147
"chunks": [
137148
"# Alpha\n\nThe first section body is one line that ends with a period. # Beta\n\nThe second section body is also a single line ending in a period. # Gamma\n\nThe third section body closes this fixture out with one more line. # Delta\n\nThe fourth section body only exists in the four-section fixture."
138149
]

cpp/tests/test_chunking.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,15 @@ TEST_F(ChunkingTest, ChunkBoundariesMatchPythonRagSdk) {
435435
const auto expectedChunks = testCase["chunks"].get<std::vector<std::string>>();
436436

437437
const std::string text = extractFile((fixtures / file).string()).text;
438+
439+
// Fail with the real reason rather than a chunk diff when the checkout
440+
// rewrote line endings (see the -text rule in .gitattributes).
441+
const std::string raw = readFile(fixtures / file);
442+
const bool onDiskHasCrlf = raw.find("\r\n") != std::string::npos;
443+
ASSERT_EQ(onDiskHasCrlf, testCase["file_has_crlf"].get<bool>())
444+
<< file << " on disk no longer has the line endings its expected chunks were "
445+
"generated from -- the checkout translated them";
446+
438447
const auto actual = splitTextIntoChunks(text, config);
439448

440449
const std::string label = file + " (size=" + std::to_string(config.chunkSize) +

0 commit comments

Comments
 (0)