Indexing: scan-as-you-type pipeline (EI.3) - #9
Open
TheWhatDeep wants to merge 1 commit into
Open
Conversation
Closes the editor->engine loop: typing a scene now schedules a scan, the scan
runs off the typing thread, and the records are written through the mentions
service. This is the SOLE editor->engine path and it carries data, never
decisions — no continuity logic lives anywhere in it.
- editor/indexScheduler.ts: debounce + forward. Coalesces keystrokes, skips
redundant work when the revision is unchanged, re-runs when the scene changed
mid-scan, cancels cleanly on scene delete, and reports failures rather than
throwing (typing must never break). Fully injectable, so it is tested with
fake timers and no editor/worker/disk.
- editor/indexWorker.ts + indexRunner.ts: the scan runs in a Web Worker whose
only output is posting records back — it holds no handle to the editor and
cannot trigger a render (S3.2 thread wall). Degrades to in-process indexing if
workers are unavailable, so typing works either way.
- indexing.ts: the pipeline coordinator (dependency-injected, tested), including
catchUp() — on project open it adopts cached records whose revision still
matches and re-scans only what is stale (S1.6/D38), and forgets cached scenes
that no longer exist.
- Wiring: Editor.svelte onUpdate does extract+forward only; App.svelte catches
up after every open and stops the pipeline on sign-out; ManuscriptTree forgets
a deleted scene's (and a deleted chapter's scenes') records immediately.
Bug found and fixed while testing: the mid-scan re-run chained on any revision
mismatch, so a persistently failing scan recursed forever (nothing committed =>
still looks un-indexed) and hung the test worker. Failures now stop and wait for
the next change; only a successful commit chains.
Verified in the browser preview beyond unit tests: the worker really does start
under Vite and returned correctly-anchored records for a real document
("Aria" 1-5, "Corin" 10-15, "Castle Raven" 19-31), with no worker or indexing
errors in the console.
Note: no UI creates entities yet, so the live lexicon is empty and the pipeline
will find nothing in the app until highlight-to-declare lands. The plumbing is
complete and correct; the payoff needs that next slice.
Tests: vitest 133 (+22). No Rust touched.
Co-Authored-By: Claude Opus 5 <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.
Closes the editor→engine loop: typing a scene schedules a scan, the scan runs off the typing thread, and the records are written through the mentions service. This is the sole editor→engine path, and it carries data (text → mentions), never decisions — no continuity logic lives anywhere in it.
What's here
editor/indexScheduler.ts— debounce + forward. Coalesces keystrokes; skips redundant work when the revision is unchanged; re-runs when the scene changed mid-scan (latest content wins); cancels cleanly on scene delete; reports failures rather than throwing (typing must never break). Fully dependency-injected, so it's tested with fake timers and no editor, worker, or disk.editor/indexWorker.ts+indexRunner.ts— off-thread scanning. The worker's only output is posting records back: it holds no handle to the editor and cannot trigger a render (S3.2 thread wall). It imports the sameindexScenethe tests exercise, so worker and test paths can't diverge. Degrades to in-process indexing if workers are unavailable — typing works either way (EI.3's constraint is that typing never blocks).indexing.ts— the coordinator (injected, tested), includingcatchUp(): on project open it adopts cached records whose revision still matches, re-scans only what is stale (S1.6/D38 — a stale record is never trusted), and forgets cached scenes that no longer exist.Wiring.
Editor.svelte'sonUpdatedoes extract + forward only (EI.3 audit item 5).App.sveltecatches up after every open path and stops the pipeline on sign-out.ManuscriptTreeforgets a deleted scene's — and a deleted chapter's scenes' — records immediately (the chapter case collects scene ids before deletion).Bug found while testing (worth a look)
The mid-scan re-run originally chained on any revision mismatch. On a persistently failing scan nothing is committed, so the revision still looks un-indexed → infinite recursion. It hung the Vitest worker for 57s and crashed it. Fixed: a failure reports and stops, waiting for the next real change; only a successful commit chains. Both behaviours are now tested (
retries on the next change after a failure, and the mid-flight re-run).Also worth noting: one test initially failed because my stub read the revision when it resolved rather than when it was called — unrealistic. The stub now snapshots at call time, as the real indexer does. The scheduler was correct; the harness was lying.
Verified beyond unit tests
In the browser preview I dynamically imported
IndexRunnerand ran a real document through it. The worker genuinely starts under Vite and returned correctly-anchored records —Aria1–5,Corin10–15,Castle Raven19–31 — which match the true ProseMirror positions. No worker or indexing errors in the console (only the pre-existing "Tauri unavailable in a browser" ones).Reviewer notes
catchUpassumes a scene's cached records share one revision, which holds because a scene's set is always written as one unit (per the D18 storage shape).src-tauri/Cargo.tomledit is deliberately left out, as in Indexer (Layer 2): name matching, mention store, and position-mapped anchoring #6–Mentions: persist found names in the bundle (D18) #8.Checks
vitest 133 (+22) · svelte-check 0 errors/0 warnings · eslint 0 · prettier clean · web build OK (worker emitted as its own chunk).