Splits the original single-file exporter into a package and adds a query
layer, so notes can be searched and read live instead of only exported.
Reading the Core Data store rather than driving Notes.app through AppleScript
is what makes full-text search practical. Measured on an 818-note store,
osascript needs ~0.325s per note body (`first note whose id is` is an
unindexed linear scan), so searching everything costs minutes; decoding every
gzipped protobuf body here takes 0.11s total. No cache, so results are always
current.
- store.py read-only SQLite access, folder tree, attachments
- parser.py gzip + protobuf decoding and Markdown rendering (moved verbatim)
- search.py search, recency listing, single-note reads
- export.py Markdown output and the incremental manifest
Three correctness details found while building this:
- Open with mode=ro, never immutable=1. The immutable flag makes SQLite skip
the -wal file and silently serve a stale snapshot; the live store routinely
carries megabytes of un-checkpointed WAL.
- Recently Deleted is ZFOLDERTYPE = 1, and notes in it keep
ZMARKEDFORDELETION = 0, so the existing filter still surfaced them. Matching
the folder type is also language-independent. Now excluded by default.
- Creation timestamps live in whichever of ZCREATIONDATE..3 is populated, so
every candidate column is read.
Export no longer writes a CLAUDE.md into the output directory unless asked
with --write-claude-md.
tests/test_parity.py keeps the original suite byte-for-byte as a regression
check on the split; test_search.py covers the new query layer against a
synthetic store. 48 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>