sillsdev / sillsdev/interlinearizer-extension
Analysis Catalog v1: searchable list of token analyses
@alex-rawlings-yyc is already working on this.
Since Jul 30, 2026.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 0
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 55
Description
A panel listing every token analysis the user has made, so glossing work already done is reachable, reviewable, and correctable.
Scope: token analyses only. Phrase and morpheme rows are follow-ups. Only token analyses are deduped and shared on write, so only they are true records — phrase and morpheme rows would be read-time groupings over non-deduped data, with fan-out writes and different edit semantics per type.
Not a concordance: the catalog indexes analyses, so an unanalyzed token does not appear in it. Coverage questions ("what haven't I glossed?") are out of scope and belong to #50 / #117.
Row model
One row per distinct token analysis — the deduped shared payload, the unit analysesAreIdentical already defines. Each row carries:
- surface form, gloss in the active analysis language, morpheme breakdown as row detail
- usage count (total) and usage count in the current book
- usage locations, parsed from
tokenAnalysisLinks[].token.tokenRef("GEN 1:1:0"embeds the verse SID and char offset). Inline list capped, with an expander.
Zero-usage rows are listed, not hidden — that is how unused records become reachable.
Data source
Derived entirely from the draft's TextAnalysis in Redux. Whole draft, all books, no text-layer reads. The catalog reports what was recorded and never verifies that a usage's token still exists or still says the same thing.
This is deliberate. The catalog spans every analyzed book while the editor loads one book at a time, so verification would mean either fetching every book's USJ up front or flagging staleness in the open book and silently not elsewhere — and an unflagged row would read as verified. Zero data dependency is what makes whole-draft scope affordable. Orphan/drift detection stays with #143 (which needs #136 first).
Search
- Matches surface form + gloss across all
analysisLanguages, plus the row's morpheme forms and glosses. - Uses a search fold distinct from identity normalization:
NFD-> strip\p{Mn}->NFC-> lowercase, soαρχηfindsἀρχῇandsalomfindsšālôm. (Note it issalom, notshalom: the fold strips the caron fromšto gives— it does not transliterate to a digraph.) Source texts are pointed Hebrew and accented Greek; nobody types those into a search box. - The search fold must never be used for identity, dedupe, or pool bucketing.
normalizeSurfaceForm(NFC -> lowercase -> NFC) stays exactly as-is. Folding a diacritic there would merge genuinely different words onto one shared payload and silently drop a distinction. Two near-identical helpers, deliberately not unified — do not DRY them together. - Fold each row's searchable text once at build time, not per keystroke. Plain substring over the whole query, no term splitting, no debounce, no index.
Sort
Default usage count descending. Also: count in current book, surface form, gloss, first usage in text order.
Collate with Intl.Collator — the source project's platform.languageTag for surface text, the active analysis language for gloss. Code-point ordering is wrong for Greek and Hebrew.
Recency sort is out of scope: no timestamp exists on an analysis, and a payload-level one would record when a meaning first appeared rather than when the user worked, since payloads are shared by reference. It needs link-level timestamps plus a schema version (#137).
Filters
Four that have real values today:
- Book (multi-select, plus a "current book only" shortcut)
- Zero usages
- Missing gloss in the active analysis language
- Has / lacks a morpheme breakdown
Plus facet derivation for closed-vocabulary fields: collect distinct values, hide any facet with fewer than two. status, confidence, pos, features, and every lexicon ref are never written by any current code path — they appear only in emptiness checks and dedupe comparisons. Those filters therefore render nothing today and appear on their own once PT9 import (#150) populates them. No dead dropdowns now, no rework later.
Mutations: analysis-keyed, therefore global
Existing reducers (writeGloss, writeMorphemes, writeMorphemeGloss) take a tokenRef and fork any shared payload, so they are inherently per-token. The catalog has no token in context — a row is the shared analysis — so it needs new reducers keyed by analysisId that never fork. The key alone determines blast radius, so no scope flag and no intent-recovery confirmation modal is needed. Do not collapse the two families; that would collapse the local/global distinction itself.
- Edit gloss and morpheme breakdown. Editing a row's breakdown fixes every usage at once — 100 occurrences of a mis-split word are one row, one edit.
- Merge into another row, first-class and prominent when the row has pool peers. Most deletions are really re-assignments: merge moves the links instead of stranding them.
mergeIntoIdenticalPayloadalready performs this when an edit makes two payloads identical. - Delete, with a
ModalShellconfirmation stating the concrete outcome rather than a generic "are you sure". Deleting an analysis also removes it from the suggestion pool, so unless a competing homograph survives, all N tokens go genuinely blank rather than falling back to a suggestion. The confirmation must say which case applies. - Merge-on-edit must be visible. Editing row A into equality with row B makes A vanish and B's count jump; unexplained that reads as data loss. Surface it ("merged into beginning, now 59 usages") and scroll to the combined row.
Single-row only. Delete ships before undo (#184) — accepted, but it is the sharpest edge in this design.
Placement and UI
- A panel inside the existing
interlinearizer.mainWebView, beside the interlinear view. Not a modal (jump-to-usage must keep it open), and not a separate WebView tab (a separate iframe means a separate Redux store and no live sync — that path needs the query API in #128). - Open/closed state and panel width in
useWebViewState(tab-scoped, restores with the tab), not project settings, which are shared by every tab on that project. - Opened by a new
interlinearizer.openAnalysisCatalogcommand registered inmain.tsandmenus.json, under a new top-menu column — the existing column holds project/file/draft actions. - Search, sort, and filter state stays ephemeral; persisting a filter across reload is the classic "where did my data go" bug.
platform-bible-reactprovidesTable,Badge,Checkbox,Select,Command,Separator,ToggleGroup. Extend__mocks__/platform-bible-react.tsxfor each newly-used component.
Navigation
Clicking a usage navigates the interlinear view to that exact token (the ref carries charStart) and highlights it. The panel stays open with the row still selected — reviewing an analysis in context means clicking usage after usage. Cross-book clicks trigger a book load and retarget the shared scroll group, which propagates to other tabs in that group.
Architecture
Derive in a pure analysis-query.ts — TextAnalysis + query -> rows, no React, no store, no PAPI — memoized via createSelector in analysisSlice.ts, same shape as selectPoolIndex. state.analysis changes on every gloss blur, so a naively derived catalog would rebuild thousands of rows while the user types. Window the row list following the existing useSegmentWindow / IntersectionObserver precedent; no new dependency.
Keeping the core pure is the point: the same module should later back the analysis query API (#128) so external extensions and the internal catalog share one implementation.
Lexicon compatibility
Reserve the seam, do not integrate. No lexicon calls in v1.
Lexicon references (glossSenseRef, entryRef, senseRef, allomorphRef, grammarRef) are outbound only: the lexicon owns its data and never depends on ours, so editing or deleting an analysis drops our reference and leaves the lexicon untouched. Nothing here may cascade into the lexicon.
Sense resolution waits on the four documented Lexicon gaps — no by-id entry lookup, no sense-level service method, and neither IMoForm nor IMoMorphSynAnalysis exported.
Related: #46, #128, #143, #50, #117, #150.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.