Problem
After building a .docx with vibepaper and importing it into Google Docs for collaborative review, every rebuild requires re-uploading the entire document — destroying all comments and track changes history.
Proposed solution
Paragraph-level diffing of rendered markdown, with changes pushed to Google Docs as suggestions (tracked changes). Existing comments on unchanged text survive. The lab head sees exactly what changed and can accept/reject.
This fits vibepaper especially well because most rebuilds change only numbers (updated CSVs), so diffs are small and surgical.
Two phases
Phase 1: vibepaper diff (no external dependencies)
- Cache fully-rendered markdown (
build/.last_render.md) after each build
- New
vibepaper diff command compares previous cache vs current render at paragraph level
- Shows which paragraphs changed with old → new context
- Immediately useful for manual updates too
Phase 2: vibepaper sync (Google Docs API)
- OAuth2 authentication (
.vibepaper/credentials.json from Google Cloud Console)
- First sync: creates a Google Doc from rendered content, stores doc ID
- Subsequent syncs: diffs against last-synced version, pushes only changed paragraphs as suggestions via Docs API
batchUpdate with suggestedInsertionIds/suggestedDeletionIds
--dry-run flag for safety
Key design decisions
- Paragraph as the diff unit — blocks separated by blank lines (headings, prose, tables each treated atomically)
- Sync state in
.vibepaper/ not paper.toml — sync state is user-specific and mutable; paper.toml is version-controlled project config
- Google API deps are optional —
pip install vibepaper[sync]; Phase 1 needs no new deps
- Fuzzy paragraph matching — uses
difflib.SequenceMatcher to match local paragraphs to Google Doc positions, tolerating minor edits in the Doc
Files
| Action |
File |
| Create |
src/vibepaper/diff.py — paragraph parsing, diffing, caching |
| Create |
src/vibepaper/gdocs.py — Google Docs API: auth, create, sync suggestions |
| Create |
tests/test_diff.py, tests/test_gdocs.py |
| Modify |
src/vibepaper/build.py — hook save_cache() after table rendering |
| Modify |
src/vibepaper/cli.py — add diff and sync subcommands |
| Modify |
pyproject.toml — add [project.optional-dependencies] sync |
Known limitations
- Tables inserted as plain text (not native Docs tables)
- Markdown bold/italic not converted to Docs formatting
- Images/figures not synced
- Heavy collaborative editing in the Doc may degrade paragraph matching —
--dry-run helps
Detailed implementation plan with code snippets
See the full plan at: .claude/plans/sorted-brewing-gosling.md (local)
Phase 1: diff module
Core abstraction — a Paragraph dataclass (kind, text, line_start) with a fingerprint() method for normalized comparison. parse_paragraphs() splits rendered markdown on blank lines, classifying blocks as heading/prose/table. diff_paragraphs() uses difflib.SequenceMatcher on fingerprints to produce ParagraphChange objects (added/removed/changed with heading context).
Cache hook in build.py:run_build() at line 319 (after table-rendering loop, before pandoc):
rendered_text = concatenate_sections(build_paper, all_sections)
save_cache(rendered_text, build_dir)
vibepaper diff re-runs Jinja2 + tables (no pandoc) and diffs against the cached previous build.
Phase 2: Google Docs sync
Auth: OAuth2 installed-app flow via google-auth-oauthlib. Client secret at .vibepaper/credentials.json, refresh token cached at .vibepaper/token.json.
Initial sync: create_doc() inserts full text via insertText, then applies heading styles via updateParagraphStyle.
Subsequent syncs:
get_doc_paragraphs() reads the Doc's structural paragraph elements with start/end indices
match_paragraphs() fuzzy-matches local changed paragraphs to Doc positions (>0.4 similarity threshold)
build_suggestion_requests() builds batchUpdate requests in reverse index order:
- Changed:
deleteContentRange + insertText with shared suggestedInsertionIds/suggestedDeletionIds
- Added:
insertText with suggestedInsertionIds
- Removed:
deleteContentRange with suggestedDeletionIds
sync_to_doc() executes the batch
Sync state stored in .vibepaper/sync_state.json (doc_id, doc_url) + .vibepaper/last_synced.md (full text for diffing).
🤖 Generated with Claude Code
Problem
After building a
.docxwith vibepaper and importing it into Google Docs for collaborative review, every rebuild requires re-uploading the entire document — destroying all comments and track changes history.Proposed solution
Paragraph-level diffing of rendered markdown, with changes pushed to Google Docs as suggestions (tracked changes). Existing comments on unchanged text survive. The lab head sees exactly what changed and can accept/reject.
This fits vibepaper especially well because most rebuilds change only numbers (updated CSVs), so diffs are small and surgical.
Two phases
Phase 1:
vibepaper diff(no external dependencies)build/.last_render.md) after each buildvibepaper diffcommand compares previous cache vs current render at paragraph levelPhase 2:
vibepaper sync(Google Docs API).vibepaper/credentials.jsonfrom Google Cloud Console)batchUpdatewithsuggestedInsertionIds/suggestedDeletionIds--dry-runflag for safetyKey design decisions
.vibepaper/notpaper.toml— sync state is user-specific and mutable;paper.tomlis version-controlled project configpip install vibepaper[sync]; Phase 1 needs no new depsdifflib.SequenceMatcherto match local paragraphs to Google Doc positions, tolerating minor edits in the DocFiles
src/vibepaper/diff.py— paragraph parsing, diffing, cachingsrc/vibepaper/gdocs.py— Google Docs API: auth, create, sync suggestionstests/test_diff.py,tests/test_gdocs.pysrc/vibepaper/build.py— hooksave_cache()after table renderingsrc/vibepaper/cli.py— adddiffandsyncsubcommandspyproject.toml— add[project.optional-dependencies] syncKnown limitations
--dry-runhelpsDetailed implementation plan with code snippets
See the full plan at:
.claude/plans/sorted-brewing-gosling.md(local)Phase 1: diff module
Core abstraction — a
Paragraphdataclass (kind,text,line_start) with afingerprint()method for normalized comparison.parse_paragraphs()splits rendered markdown on blank lines, classifying blocks as heading/prose/table.diff_paragraphs()usesdifflib.SequenceMatcheron fingerprints to produceParagraphChangeobjects (added/removed/changed with heading context).Cache hook in
build.py:run_build()at line 319 (after table-rendering loop, before pandoc):vibepaper diffre-runs Jinja2 + tables (no pandoc) and diffs against the cached previous build.Phase 2: Google Docs sync
Auth: OAuth2 installed-app flow via
google-auth-oauthlib. Client secret at.vibepaper/credentials.json, refresh token cached at.vibepaper/token.json.Initial sync:
create_doc()inserts full text viainsertText, then applies heading styles viaupdateParagraphStyle.Subsequent syncs:
get_doc_paragraphs()reads the Doc's structural paragraph elements with start/end indicesmatch_paragraphs()fuzzy-matches local changed paragraphs to Doc positions (>0.4 similarity threshold)build_suggestion_requests()builds batchUpdate requests in reverse index order:deleteContentRange+insertTextwith sharedsuggestedInsertionIds/suggestedDeletionIdsinsertTextwithsuggestedInsertionIdsdeleteContentRangewithsuggestedDeletionIdssync_to_doc()executes the batchSync state stored in
.vibepaper/sync_state.json(doc_id, doc_url) +.vibepaper/last_synced.md(full text for diffing).🤖 Generated with Claude Code