docs: Synthesis Document Generation — implementation plan (#027)#3
Merged
Conversation
…skayyali#28) - Redesign mskayyali#27 from "Study Guide Export" to "Synthesis Document Generation" with a three-phase AI pipeline (decontextualize, cluster, synthesize) plus human-owned phases 0 and 4 - Add docs/synthesis-document-plan.md with full implementation spec: data types, prompt structures, file breakdown, open questions - Update mskayyali#28 to reflect that Phase 4 (human review) requires no plugin code — handled externally via Claude Code / Gemini CLI - Close mskayyali#22–mskayyali#26 in Completed section (all shipped in plugin PR #2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three-phase AI pipeline that consolidates sparse nodespace notes into
a structured, contextualized markdown document:
- lib/synthesis.ts
- buildEdgeMap(): Phase 1 — serialises block graph from
influencedBy edges; separates reference nodes as source anchors
- callDecontextualize(): Call A — rewrites each node into a
self-contained statement using only its graph neighbours and
source anchor as context (no external knowledge fill)
- callCluster(): Call B — groups nodes into named sections by
semantic meaning; post-processes to ensure every ID is assigned
to exactly one section, missing nodes go to "General"
- callSynthesize(): Call C — writes section headings, intros,
expounding prompts (Socratic, not recall), and gap markers;
nodeIds merged from clustering step by index
- generateSynthesisDocument(): orchestrates Phases 1–3 with
calls A+B in parallel then call C sequential
- lib/synthesis-export.ts
- renderSynthesisDocument(): Phase 3 renderer — YAML frontmatter,
self-contained statements with source attribution, expounding
prompts and gaps as Obsidian callout blocks
- components/vim-input.tsx
- Add "Synthesis document" action to command palette
- pluginOnly: false (web-app only for now; plugin port later)
- app/page.tsx
- Handle "synthesis-doc" command: captures project snapshot
synchronously then runs async pipeline, downloads result
- synthesisStatus state drives a spinner toast during generation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous setProjects(prev => ...) trick for reading state is not reliable in React 18 concurrent mode — the updater is not guaranteed to run before the subsequent if (blocks) check. projectsRef.current is already the established pattern in this file for reading the latest project state synchronously inside callbacks (see enrichBlock, generateGhost, moveBlock, copyBlock). Use it here for consistency and correctness. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Log first 1000 chars of raw response before parsing so parse failures show what the AI actually returned - Wrap parse error to include the raw response start in the message - Normalise common alternative top-level keys (data, synthesis, overview, description) so models that wrap the output don't fail - Error message now includes the actual keys the AI returned, making the next debugging round self-diagnosing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On any JSON parse or shape error in callSynthesize, download the raw response as synthesis-call-c-raw.txt instead of logging to console. Makes the actual AI output inspectable without DevTools. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini returned the sections array directly at the top level instead
of wrapping it in { summary, sections }. Keys 0,1,2,3,4 in the error
confirm this — Object.keys on an array returns its indices.
Now detects Array.isArray(parsed) and uses the array as sections
directly with an empty summary, rather than failing the shape check.
Also widens the object-path to check data/synthesis as section keys
and overview/description as summary keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pipeline changes: - Replace single callSynthesize with callSynthesizeCluster (Call C×N): parallel per-cluster synthesis, each receiving full nodespace context (all statements + annotations + document structure) steered to its target section. Parallel processes instructed not to explain or prompt about other sections. - Add callPolish (Call D): optional sequential editorial pass over the rendered draft markdown, forbidden from touching source attributions or callout blocks - generateSynthesisDocument now emits structured ProgressEvents (phase_start, phase_done, clusters_known, error) and returns CallTiming[] for the footer; Call D handled by page.tsx - SynthesisSection gains sectionSynthesis (flowing prose paragraphs) - SynthesisOutline drops top-level summary (no Call D for that) Renderer changes (synthesis-export.ts): - sectionSynthesis prose leads each section - Source notes moved to collapsible [!note]- callout - Timing table in collapsible [!info]- footer on both files - renderPolishedDocument() appends timing footer to Call D output - isPolished flag tags frontmatter and title New components: - SynthesisConfirmDialog: pre-generation modal showing source anchors (or warning if none), node count, polish toggle, and warnings about node snapshot, token costs, and time estimates - SynthesisProgressPanel: clickable bottom pill that opens a progress dialog with live per-call status, elapsed timers for running calls, parallel/sequential grouping, and completion summary page.tsx wiring: - synthesis-doc command opens confirm dialog (no immediate generation) - startSynthesis(enablePolish) captures project snapshot, runs pipeline, downloads raw .md, optionally runs Call D and downloads polished .md - Synthesis state replaces old synthesisStatus string with structured CallTiming[] and isActive/isDialogOpen booleans Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LLMs generating LaTeX inside JSON strings sometimes emit single backslashes (\mathbb, \frac, \cong) which are invalid JSON escape sequences. JSON.parse throws on \m, \c etc. — only \", \, \/, \n, \r, \t, \b, \f, \uXXXX are legal. Fix: on first parse failure, apply a regex that escapes any backslash not already part of a valid JSON escape sequence, then retry. The outer catch still saves the raw dump if the fixed parse also fails. Regex: /\(?!["\/bfnrtu])/g — matches \ not followed by a JSON escape character, replaces with \. Verified against all four raw dump files from the test run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation checked for '[' first. Synthesis responses
are JSON objects that contain arrays (expandingPrompts, gaps). When
Gemini returned without a fenced code block, extractJson found the '['
inside expandingPrompts and sliced from there to the last ']', producing
a broken substring like '["p1","p2"],\n "gaps":["g1"]'. Both JSON.parse
attempts then failed. The dump showed the original raw string (which was
always valid) because dumpRawResponse is called with raw, not jsonStr.
Fix: find which delimiter appears first in the text and use that.
- Object responses (synthesis): '{' appears before any '[' in the JSON body
- Array responses (decontextualize, cluster): '[' is the first char
This correctly handles both without affecting the other callers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pill previously stayed visible indefinitely after synthesis completed. Now shows a small X on the right side of the pill when isActive=false, allowing the user to dismiss it. The pill is also reset (undismissed) when a new synthesis starts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw file: total of all Call A/B/C timings. Polished file: total including Call D, injected into the polished markdown's existing frontmatter (replaces if already present from a previous run, inserts before closing --- if absent). The two files now have different generated_in values reflecting when each actually completed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summing all timing durations gives inflated numbers because parallel calls (A+B together, all Cs together) run simultaneously. The sum counted each worker's time independently, e.g. 5m 6s vs the actual ~2m 29s the user waited. Fix: capture generationStart = Date.now() at the top of startSynthesis and compute wall-clock elapsed at each download point: - raw file: Date.now() - generationStart (after all Cs complete) - polished file: Date.now() - generationStart (after Call D completes) Both renderers now take wallClockMs? as an explicit parameter rather than deriving from the timing array. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- renderTimingTable now takes wallClockMs? and uses it for the Total
row label ("Total (wall-clock)"), consistent with the generated_in
frontmatter field. Both renderSynthesisDocument and
renderPolishedDocument pass their wallClockMs through.
- Remove clusters_known from ProgressEvent type and its emission in
callCluster — the event was never consumed by the UI handler since
Call C phase_start events fire immediately after B completes,
making the signal redundant.
- Update BACKLOG mskayyali#27 to reflect web-app implementation is complete;
plugin port remains open as future work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Transport layer:
- lib/synthesis.ts: export SynthesisCallFn type; rename internal callAI
to defaultCallAI; all call functions (callDecontextualize, callCluster,
callSynthesizeCluster, callPolish, generateSynthesisDocument) accept
optional callFn/injectedConfig so the plugin can swap fetch/api/ai
for requestUrl/spawnCLI without duplicating any prompt or parsing logic
- plugin/src/ai-adapter.ts: add makeSynthesisCallFn(plugin) which returns
a SynthesisCallFn — routes Gemini CLI through spawnCLI("gemini",
["--policy","simple"], combinedPrompt), all other providers via
requestUrl() directly to the provider (no /api/ai proxy needed)
UI components:
- SynthesisConfirmDialog: add container? prop; render via createPortal
when provided so it scopes to the Obsidian leaf instead of document.body
- SynthesisProgressPanel: same container?/portal treatment
Plugin wiring (plugin/src/view.tsx):
- NodepadView gains synthesisTriggerRef and triggerSynthesis() method
(called from the command, shows Notice if no canvas is open)
- NodepadApp gains full synthesis state (same as page.tsx) wired to
synthesisTriggerRef so the command opens the confirm dialog
- startSynthesis() runs the pipeline with makeSynthesisCallFn and
writes output via app.vault.create() + workspace.getLeaf().openFile()
— files appear in the same folder as the canvas, opened immediately
in a new tab; no browser download
- Error surfaced via Obsidian Notice instead of console-only
Command (plugin/src/main.ts):
- Register "Generate Synthesis Document" with checkCallback that
requires an active NodepadView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
docs/synthesis-document-plan.mdas the full implementation specPipeline design
Calls A and B run in parallel. Call C is sequential after both complete.
Key design decisions
.mdfile any AI tool with vault access can annotate and correctFiles changed
BACKLOG.mddocs/synthesis-document-plan.mdNo implementation code in this PR — spec only. Implementation follows in a separate PR.
🤖 Generated with Claude Code