Extraction writes doubled carriage returns (data munging on every save)
Problem
Every tab file the extractor saves has doubled carriage returns: lines end in \r\r\n instead of \r\n. The tool is munging user data on its primary output path, and has been since v0.1.x.
Root cause (notepad_cleanup/saver.py:94):
filepath.write_text(text, encoding="utf-8")
write_text opens the file in text mode with default newline translation. On Windows that converts every \n to \r\n at write time. The text arriving from extraction (WM_GETTEXT on RichEditD2DPT, discovery.py:read_richedit_text) already uses \r\n line endings, so each one becomes \r + (\n -> \r\n) = \r\r\n.
Evidence -- a controlled specimen. The same Notepad tab, captured two ways: once by our extractor, once by manually saving the tab from Notepad itself. The files should be byte-identical. Instead:
|
extractor output |
Notepad's own save |
| bytes |
5,180 |
5,054 (delta = exactly 1 CR per line) |
| line endings |
\r\r\n x126 |
\r\n x126 |
after collapsing \r\r\n -> \r\n |
character-identical |
(ground truth) |
Zero other differences -- no encoding damage, em-dashes and all content intact. The corruption is purely the CR doubling.
Spread. Sampling raw window*/tab*.txt files across three sessions spanning v0.1.x through v0.2.x: 20/24 contain \r\r\n (the clean minority are files with no line breaks to corrupt). This has shipped in every release.
User impact:
- Files render double-spaced in any editor that treats a lone
\r as a line break (modern Notepad, VS Code, most tools) -- users must clean up every extracted note.
- Round-trip identity is broken. Re-saving the same content from Notepad produces a "different" file, as the specimen shows.
- Dedup is silently defeated for mixed pairs.
normalize_text() turns x\r\r\n into x\n\n -- the phantom blank lines survive into the hash. Two extracted copies still match each other (both corrupted identically), which is why compare has appeared to work -- but an extracted copy never exact-matches a clean copy of the same text, and with one phantom newline per line the char diff (126 in the specimen) far exceeds the near-match threshold (~54 at that size), so fuzzy misses it too. The tool cannot recognize its own output as a duplicate of the real file.
- The AI organize step reads doubled text -- inflated size hints in the manifest (
chars counts include the phantom CRs) and double-spaced content in every file Claude reads.
Proposed solution
Normalize line endings at the extraction boundary, and make the disk format explicit at the write boundary.
Option A (recommended): internal \n, explicit CRLF on disk.
- In extraction (both Phase 1
read_richedit_text and the Phase 2 UIA path), normalize immediately: text.replace("\r\n", "\n").replace("\r", "\n"). One internal representation everywhere -- the same one normalize_text() already assumes.
- In
saver.py, write with explicit newline serialization: filepath.write_text(text, encoding="utf-8", newline="\r\n") -- output files then match what Notepad itself would save.
- Manifest
chars counts become CR-free and stable across platforms.
Option B: pass-through. Keep the control's \r\n and write with newline="" (no translation). Fewer lines changed, but the internal text keeps \r\n, which leaks into labels, char counts, and any future platform (the Sublime adaptation extracts \n-only text -- Option A gives both platforms the same internal form for free).
Option A is preferred: platform-specific line endings stay at the platform boundary, which is exactly the seam the multi-editor architecture wants.
Remediation for existing archives
Every historical session on disk is corrupted, and after the writer fix, new extractions will be clean -- so cross-era dedup (clean new file vs corrupted archive) breaks everywhere until archives are repaired. Proposal:
notepad-cleanup repair --line-endings <folder> with --dry-run: collapse \r\r\n -> \r\n in window*/tab*.txt and organized/ text files. The transform is unambiguous (a legitimate \r\r\n sequence cannot be produced by Notepad).
- Rewriting changes mtime, which correctly invalidates the hash cache (it revalidates by mtime+size); hashes then change, so stale hashes in old
_compare_results.json are re-derived on the next compare. Link targets are path-based and unaffected.
- Alternative considered: hash-time tolerance (collapse
\r\r\n inside normalize_text). Rejected as the primary fix -- it hides the corruption instead of removing it, and the files stay double-spaced for users. Possibly worth adding temporarily so unrepaired archives still dedup, but the repair command is the real remedy.
Acceptance criteria
Related issues
Analysis
Specimen pair (project-private, for maintainers): get-the-beep-outta-here-blues-shuffle-lyrics.txt (extractor output) vs get-the-beep-outta-here-blues-shuffle-lyrics v2.txt (Notepad's own save of the same tab).
Extraction writes doubled carriage returns (data munging on every save)
Problem
Every tab file the extractor saves has doubled carriage returns: lines end in
\r\r\ninstead of\r\n. The tool is munging user data on its primary output path, and has been since v0.1.x.Root cause (
notepad_cleanup/saver.py:94):write_textopens the file in text mode with default newline translation. On Windows that converts every\nto\r\nat write time. The text arriving from extraction (WM_GETTEXTonRichEditD2DPT,discovery.py:read_richedit_text) already uses\r\nline endings, so each one becomes\r+ (\n->\r\n) =\r\r\n.Evidence -- a controlled specimen. The same Notepad tab, captured two ways: once by our extractor, once by manually saving the tab from Notepad itself. The files should be byte-identical. Instead:
\r\r\nx126\r\nx126\r\r\n->\r\nZero other differences -- no encoding damage, em-dashes and all content intact. The corruption is purely the CR doubling.
Spread. Sampling raw
window*/tab*.txtfiles across three sessions spanning v0.1.x through v0.2.x: 20/24 contain\r\r\n(the clean minority are files with no line breaks to corrupt). This has shipped in every release.User impact:
\ras a line break (modern Notepad, VS Code, most tools) -- users must clean up every extracted note.normalize_text()turnsx\r\r\nintox\n\n-- the phantom blank lines survive into the hash. Two extracted copies still match each other (both corrupted identically), which is why compare has appeared to work -- but an extracted copy never exact-matches a clean copy of the same text, and with one phantom newline per line the char diff (126 in the specimen) far exceeds the near-match threshold (~54 at that size), so fuzzy misses it too. The tool cannot recognize its own output as a duplicate of the real file.charscounts include the phantom CRs) and double-spaced content in every file Claude reads.Proposed solution
Normalize line endings at the extraction boundary, and make the disk format explicit at the write boundary.
Option A (recommended): internal
\n, explicit CRLF on disk.read_richedit_textand the Phase 2 UIA path), normalize immediately:text.replace("\r\n", "\n").replace("\r", "\n"). One internal representation everywhere -- the same onenormalize_text()already assumes.saver.py, write with explicit newline serialization:filepath.write_text(text, encoding="utf-8", newline="\r\n")-- output files then match what Notepad itself would save.charscounts become CR-free and stable across platforms.Option B: pass-through. Keep the control's
\r\nand write withnewline=""(no translation). Fewer lines changed, but the internal text keeps\r\n, which leaks into labels, char counts, and any future platform (the Sublime adaptation extracts\n-only text -- Option A gives both platforms the same internal form for free).Option A is preferred: platform-specific line endings stay at the platform boundary, which is exactly the seam the multi-editor architecture wants.
Remediation for existing archives
Every historical session on disk is corrupted, and after the writer fix, new extractions will be clean -- so cross-era dedup (clean new file vs corrupted archive) breaks everywhere until archives are repaired. Proposal:
notepad-cleanup repair --line-endings <folder>with--dry-run: collapse\r\r\n->\r\ninwindow*/tab*.txtandorganized/text files. The transform is unambiguous (a legitimate\r\r\nsequence cannot be produced by Notepad)._compare_results.jsonare re-derived on the next compare. Link targets are path-based and unaffected.\r\r\ninsidenormalize_text). Rejected as the primary fix -- it hides the corruption instead of removing it, and the files stay double-spaced for users. Possibly worth adding temporarily so unrepaired archives still dedup, but the repair command is the real remedy.Acceptance criteria
\r\nline endings, saved by the saver, produces a file with\r\nonly (no\r\r\nanywhere)\nbefore returning textcharscounts exclude CR charactersrepair --line-endingscommand with--dry-run, reporting per-file changes; no file without\r\r\nis touchedRelated issues
\nconvention (Option A) is the same boundary the cross-platform work needsAnalysis
Specimen pair (project-private, for maintainers):
get-the-beep-outta-here-blues-shuffle-lyrics.txt(extractor output) vsget-the-beep-outta-here-blues-shuffle-lyrics v2.txt(Notepad's own save of the same tab).