Makes the Analysis Catalog (#186) editable. Depends on the read-only panel.
Analysis-keyed reducers — the key is the blast radius
The 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 a second family keyed by analysisId that never forks:
writeAnalysisGloss({ analysisId, value })
writeAnalysisMorphemes({ analysisId, forms })
writeAnalysisMorphemeGloss({ analysisId, morphemeId, value })
deleteAnalysis({ analysisId })
mergeAnalysisInto({ sourceAnalysisId, targetAnalysisId })
Because the key alone determines scope — tokenRef → per-token (forks), analysisId → global (never forks) — no scope flag and no intent-recovery confirmation modal is needed.
Do not collapse the two families into one reducer parameterized by a boolean. That would put the local/global distinction behind a flag, which is exactly the distinction worth keeping structural.
Each new reducer calls mergeIntoIdenticalPayload after its edit, as the per-token reducers do, and returns enough for the UI to report what happened.
Operations
- 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.
mergeIntoIdenticalPayload (src/store/analysisSlice.ts) already performs this when an edit makes two payloads identical.
- Delete, with a
ModalShell confirmation 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 — no multi-select, no bulk edit.
Behaviors to test
Reducers:
writeAnalysisGloss changes the gloss for every token linked to the payload — no fork.
writeAnalysisMorphemes likewise.
deleteAnalysis removes the payload and every link to it; the affected tokens read as blank.
deleteAnalysis on a payload with a surviving homograph peer leaves the tokens falling back to that peer as a suggestion — the two delete outcomes differ, and the confirmation must be able to tell them apart.
mergeAnalysisInto moves every link to the target and drops the source; the target's usage count becomes the sum.
- Editing row A into content-equality with row B collapses A into B via
mergeIntoIdenticalPayload, and the surviving row is B.
Panel:
- Editing a row's gloss updates the interlinear view for all its tokens in one edit.
- Delete opens a
ModalShell confirmation naming the concrete outcome — "N tokens will become blank" vs "N tokens will fall back to " — driven by behavior 4.
- Canceling the confirmation leaves the analysis untouched.
- Merge is offered prominently when the row has pool peers, and not at all when it has none.
- After a merge-on-edit the panel announces it and scrolls to the combined row — the row must not simply vanish.
The sharp edge
Delete ships before undo (#184). #186 accepts this explicitly. The confirmation copy is the only guard, so behaviors 4 and 8 are the highest-value tests here — an inaccurate confirmation is worse than none.
Lexicon
No lexicon calls. Lexicon references (glossSenseRef, entryRef, senseRef, allomorphRef, grammarRef) are outbound only: editing or deleting an analysis drops our reference and leaves the lexicon untouched. Nothing here may cascade into the lexicon.
Makes the Analysis Catalog (#186) editable. Depends on the read-only panel.
Analysis-keyed reducers — the key is the blast radius
The existing reducers (
writeGloss,writeMorphemes,writeMorphemeGloss) take atokenRefand 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 a second family keyed byanalysisIdthat never forks:Because the key alone determines scope —
tokenRef→ per-token (forks),analysisId→ global (never forks) — no scope flag and no intent-recovery confirmation modal is needed.Do not collapse the two families into one reducer parameterized by a boolean. That would put the local/global distinction behind a flag, which is exactly the distinction worth keeping structural.
Each new reducer calls
mergeIntoIdenticalPayloadafter its edit, as the per-token reducers do, and returns enough for the UI to report what happened.Operations
mergeIntoIdenticalPayload(src/store/analysisSlice.ts) already performs this when an edit makes two payloads identical.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.Single-row only — no multi-select, no bulk edit.
Behaviors to test
Reducers:
writeAnalysisGlosschanges the gloss for every token linked to the payload — no fork.writeAnalysisMorphemeslikewise.deleteAnalysisremoves the payload and every link to it; the affected tokens read as blank.deleteAnalysison a payload with a surviving homograph peer leaves the tokens falling back to that peer as a suggestion — the two delete outcomes differ, and the confirmation must be able to tell them apart.mergeAnalysisIntomoves every link to the target and drops the source; the target's usage count becomes the sum.mergeIntoIdenticalPayload, and the surviving row is B.Panel:
ModalShellconfirmation naming the concrete outcome — "N tokens will become blank" vs "N tokens will fall back to " — driven by behavior 4.The sharp edge
Delete ships before undo (#184). #186 accepts this explicitly. The confirmation copy is the only guard, so behaviors 4 and 8 are the highest-value tests here — an inaccurate confirmation is worse than none.
Lexicon
No lexicon calls. Lexicon references (
glossSenseRef,entryRef,senseRef,allomorphRef,grammarRef) are outbound only: editing or deleting an analysis drops our reference and leaves the lexicon untouched. Nothing here may cascade into the lexicon.