sillsdev / sillsdev/interlinearizer-extension

Bulk approval: a segment's suggestions, and one analysis everywhere

Open
#254 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
2
Forks
0
Avg merge
1d 17h
Merged PRs (30d)
55

Description

Approval is strictly one token at a time today: useApproveAnalysisDispatch (src/components/AnalysisStore.tsx:454) dispatches one approveAnalysisForToken (src/store/analysisSlice.ts:706-750) and calls save() per click. user-questions.md parks the stakeholder question ("Suggestion engine: bulk acceptance (post-v1)"), and #190 parks the catalog-side bulk actions while noting the two "probably deserve one interaction model rather than two". This issue is the suggestion-side half.

There are two independent affordances here, not one action at two scopes. They answer different questions, are reached from different places, and have different failure modes:

  1. Approve every suggestion in this segment — "this verse is done."
  2. Approve this analysis on every occurrence of the same form — "this gloss is right everywhere."

Either can ship without the other. Both are cheap mechanically and expensive to get wrong.

Why bulk approve is safer than bulk delete

#190 is blocked on #184 (Undo/Redo) because bulk delete without undo is forty mistakes at once. Approval is additive: approveAnalysisForToken only adds an approved link to an existing payload, raising its pool frequency; it never rewrites the shared payload's content, so no other token's gloss changes. That is the argument for not inheriting #190's blocker wholesale — with one exception, below, which is the sharpest edge in this issue.

Option 1 — approve every suggestion in this segment

The unit is the segment, not the verse: segments default to one per verse but the user moves boundaries (SegmentationDelta, src/utils/segmentation.ts), so the button must mean the thing on screen.

Eligible set — and this is a question, not a given. Two populations of unapproved work exist, and they don't overlap:

engine-derived suggestions persisted non-approved links
origin computed on read, never stored written by PT9 import
covers tokens only tokens and phrases
in the pool? is the pool never — the pool requires an approved link
visible today yes, as a suggestion no, nothing renders one (#310)

Taking only the first — word tokens (TokenType) in the segment with no approved link and a deriveTokenSuggestion hit (src/utils/suggestion-engine.ts:129-158) — makes the button a no-op on a freshly imported PT9 project. An analysis enters the pool only once it already holds an approved link (selectApprovedIdByTokenRef keeps status === 'approved', src/store/analysisSlice.ts:986, feeding buildPoolIndex), and an imported project's verses mostly carry no Hash, so nearly every link is suggested: the pool is near-empty, the derive returns nothing, and "this verse is done" approves nothing. Nothing on screen explains the no-op either — per the table's last row, a persisted non-approved link is not rendered at all.

Option 1 promises "this verse is done", which holds only if it covers both populations. The second population is invisible and unapprovable today — split out as #310, which renders it and makes single approval work. Until #310 lands, Option 1 can only mean engine suggestions and should say so in its label; once it lands, the eligible set is a union over two visible populations and the only question left here is whether bulk covers both.

Question to settle: does approval apply to things without a gloss?

glossedSuggestionEntries (src/utils/suggestion-engine.ts:202) drops any pool payload whose gloss is blank in the active analysis language. So a payload carrying only a morpheme breakdown, or a gloss in another of the project's analysisLanguages but not the active one, is invisible in the dropdown while still being exactly what deriveTokenSuggestion returns. A loop over the engine would approve what the UI never offered, and the user would have no way to see what they just accepted.

Options:

  • (a) Approve only what the dropdown would have offered — filter through glossedSuggestionEntries in the active language.
  • (b) Approve any pool match, gloss or not.
  • (c) Approve the glossed ones and report how many were skipped and why.

Recommend (a) + (c): the button's promise is "approve what you can see", and a silent skip on a blank-in-language payload reads as the button not working. (b) also quietly approves morpheme-only analyses whose gloss will later be filled in by someone else — approval of content that does not exist yet.

Also decide
  • Token-level only? The pool is built from tokenAnalyses alone (buildPoolIndex, src/utils/suggestion-engine.ts:95-113), and segment free translations are typed (writeSegmentFreeTranslation), so neither is ever engine-suggested. Phrases are a different case — they are not only ever hand-made via createPhrase: PT9 import writes a phrase as suggested when its verse carries no Hash (src/converters/pt9/languageAnalysisBuilder.ts:152), and mergeStatus returns suggested whenever the gloss languages disagree (src/converters/pt9/analysisMerger.ts:376). Still recommend token-only for v1, but on the real reason: the phrase views read approved links only (selectPhraseLinks, src/store/analysisSlice.ts:1154-1156), so a suggested phrase has nothing on screen to bulk-extend — an imported suggested phrase is approvable by no means at all today, bulk or single (#310). Say token-only in the label rather than leaving the user to infer it.
  • Where the control lives. A per-segment affordance in the segment header vs. a command over the active segment. A per-segment button appears hundreds of times in a chapter; a command needs no chrome but is undiscoverable without a menu entry (contributions/menus.json).

Option 2 — approve this analysis on every occurrence of the same form

Reached from a token's dropdown, and from a catalog row. Mechanically: for every unapproved word token whose normalizeSurfaceForm(surfaceText) matches the chosen payload's, add an approved link to that analysisId.

Question to settle: book, or whole project?

The constraint is not the analysis layer, it is the text. The store already holds the whole draft's TextAnalysisCatalogRow reports usageCount across the draft alongside usageCountInBook (src/utils/analysis-query.ts:36-42) — but only one book's text is in memory: useInterlinearizerBookData fetches and tokenizes exactly one BookUSJ for scrRef.book (src/hooks/useInterlinearizerBookData.ts:55-58). Project-wide means 66 fetch-and-tokenize passes in the WebView, or a new backend pass, purely to enumerate candidate tokens.

And the payoff is smaller than it looks. Suggestions are derived on read and never persisted, so an unapproved matching token in an unopened book already shows this gloss the moment that book is opened. Project-wide bulk approval buys only the flip from suggested-blue to approved — on text the user has never looked at. That is precisely the "does that still count as human review?" question user-questions.md raises about book-level acceptance, and it lands harder at project scope.

Recommend current book for v1 (it matches the loaded data and the existing usageCountInBook count), with the affected count stated before the action, and revisit project scope only if users ask for it with a reason.

The sharp edge: this can silently delete hand-made analyses

approveAnalysisForToken does not only add links. When the token already has an approved link, it repoints that link to the chosen payload, and if the old payload loses its last approved reference the payload is dropped from the draft entirely (src/store/analysisSlice.ts:726-741). Applied in bulk over "every occurrence of the form", that would overwrite deliberate per-token decisions — exactly the homograph disambiguation the candidate mechanism exists to support — and destroy the analyses behind them.

So the eligible set must be tokens with no approved link, full stop. If re-pointing already-approved tokens is wanted, it is a separate and much louder action, and it is the one that genuinely blocks on #184.

Homographs

A form whose pool bucket holds more than one entry is where blanket approval is most likely wrong. Decide whether the action is offered at all on a homograph form, or offered with the count of tokens whose current top-ranked suggestion is a different payload — the number of decisions being overridden is the number that matters.

Shared design questions

  • Confidence. AnalysisLink.confidence is per-assignment (src/types/interlinearizer.d.ts:842). A bulk-approved link could be stamped lower than a hand-approved one so the two stay distinguishable — otherwise "approved" means both "a human read this" and "a human swept a hundred of these", and the distinction is unrecoverable afterward. But the field is not free: PT9 import already spends it on anchor ambiguity, so this needs #309's decision before it can be stamped for provenance.
  • One action, not a loop. 200 approvals through the current dispatcher are 200 dispatches, 200 re-renders, and 200 draft writes through draftQueues (src/services/projectStorage.ts). Take ids[] in a single action so it is one transaction, one re-render, one save — the same argument #190 makes, and relevant to #87.
  • Blast radius before the click. State the token count, and for option 2 the books affected, not just "approve all".
  • Selection versus what is visible. Option 1's set is what is on screen, which is the safe case. Option 2's set is mostly off screen, which is not — do not let the two share one confirmation pattern by default.

Behaviors to test

  1. Segment approve adds an approved link for every eligible token in the segment and none outside it.
  2. Segment approve skips tokens whose pool match has no gloss in the active analysis language, and reports the count (per the decision above).
  3. Segment approve leaves already-approved tokens untouched — no repoint, no payload reclaim.
  4. Segment approve is a no-op, and the affordance is disabled, when the segment has no eligible token.
  5. Form-wide approve links every unapproved matching token in scope to the chosen payload and raises its pool frequency by that count.
  6. Form-wide approve does not touch a token approved to a different payload, and no payload is dropped as a result.
  7. Form-wide approve does not cross the scope boundary decided above (no links outside the current book).
  8. Both paths persist once, not once per token.

Related

#190 (catalog-side bulk actions — design the interaction model together), #184 (gates the re-point variant), #310 (renders and single-approves the imported population Option 1 depends on), #255 (a review sweep is jump-then-approve-the-segment), #309 (what confidence means, which the bulk-approval stamp depends on).

Per AGENTS.md's UX-decisions guidance, user-questions.md's bulk-acceptance entry should be updated with this two-option split before either is built — the questions it asks assume a single "bulk-accept" with one scope.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with AGENTS.md, user-questions.md, useApproveAnalysisDispatch in src/components/AnalysisStore.tsx, and the approval and suggestion paths in src/store/analysisSlice.ts and src/utils/suggestion-engine.ts. Resolve the two scopes and their eligibility, boundaries, and confirmation behavior before implementation; done includes updating the bulk-acceptance decision and covering the listed segment, form-wide, safety, and persistence behaviors with tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.