diff --git a/docs/proposals/repeated-word-check/checks-ui-integration-suggestion.md b/docs/proposals/repeated-word-check/checks-ui-integration-suggestion.md index 43756f3a..dac70a6d 100644 --- a/docs/proposals/repeated-word-check/checks-ui-integration-suggestion.md +++ b/docs/proposals/repeated-word-check/checks-ui-integration-suggestion.md @@ -24,14 +24,14 @@ This document's own decisions are numbered **W1–W12** (W = web) to avoid colli Revisions made in response to PR [#305](https://github.com/eten-tech-foundation/fluent-web/pull/305) review (reviewers: kaseywright, chadw-eten; product input via joelthe1/Ulf). The cards themselves were edited mid-review; this revision tracks the **current** card text and mocks (re-pulled 2026-06-16). -| Change | Driver | Where | -| --- | --- | --- | -| Settings endpoint moves from `GET/PUT /users/settings` to a new **`self` domain**: `GET/PUT /self/settings`, guarded by `authenticateUser` alone. | kaseywright (CHANGES_REQUESTED): clean semantic separation, no redundant `{id}` param, no over-exposure risk, simpler middleware (no `requireSelf`/`requireUserAccess`/`UserPolicy`). | W7, §7.2, §8 | -| Button labels become **"Ignore Here" / "Ignore Everywhere"**. | joelthe1/Ulf (card #278 comment); chadw updated the #278 mocks to match. | W6, §5, §6.5 | -| **"Ignore Everywhere" now shows a confirmation dialog**, while remaining reversible via `[Undo ▾]`. | card #278 ("Ignore Always should have a standard confirm dialog"). Kept the three-layer cascade & undo (no explicit directive against it; ambiguity of "undo" with surface/suppress entries at multiple layers is better resolved by the cascade — see §6.4/§6.5); added the dialog. | W6, §5.3, §6.5 | -| **"Show Ignored" toggle**: now default **OFF** and **not persisted** (resets per session). Ignored items show the ignore-type label (e.g. _"Ignore Here"_, _"Ignore Everywhere"_, _"Default Ignore"_) + an **"Undo Ignore"** control. | card #278 was revised to specify exactly this; we drop our earlier default-ON/persisted proposal and the `showResolvedChecks` editor-state key. | W5, W6, W11, §5.3, §6.4, §6.6, §7.1 | -| §5.2 mock/text inconsistencies **S1 (zero-state dot)** and **S2 (language dropdown)** are **resolved**: chadw fixed the zero-state mock and removed the language dropdown from the #278 Checks mocks. | chadw-eten ("Fixed." / "good idea… I have adjusted the mocks."). | §5.2, §12 | -| New requirement adopted: **highlight repeated words in the verse text** (underline/color). | joelthe1/Ulf (card #278 comment). We will implement it; because it touches the verse-rendering pane (outside the left panel this proposal builds) and warrants its own design pass, it ships as a **separate, follow-on PR** so the tab/panel work can land independently. §5.3 item 4 records the intent; the visual/interaction details are specced just before that PR. | §5.3, §11 | +| Change | Driver | Where | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| Settings endpoint moves from `GET/PUT /users/settings` to a new **`self` domain**: `GET/PUT /self/settings`, guarded by `authenticateUser` alone. | kaseywright (CHANGES_REQUESTED): clean semantic separation, no redundant `{id}` param, no over-exposure risk, simpler middleware (no `requireSelf`/`requireUserAccess`/`UserPolicy`). | W7, §7.2, §8 | +| Button labels become **"Ignore Here" / "Ignore Everywhere"**. | joelthe1/Ulf (card #278 comment); chadw updated the #278 mocks to match. | W6, §5, §6.5 | +| **"Ignore Everywhere" now shows a confirmation dialog**, while remaining reversible via `[Undo ▾]`. | card #278 ("Ignore Always should have a standard confirm dialog"). Kept the three-layer cascade & undo (no explicit directive against it; ambiguity of "undo" with surface/suppress entries at multiple layers is better resolved by the cascade — see §6.4/§6.5); added the dialog. | W6, §5.3, §6.5 | +| **"Show Ignored" toggle**: now default **OFF** and **not persisted** (resets per session). Ignored items show the ignore-type label (e.g. _"Ignore Here"_, _"Ignore Everywhere"_, _"Default Ignore"_) + an **"Undo Ignore"** control. | card #278 was revised to specify exactly this; we drop our earlier default-ON/persisted proposal and the `showResolvedChecks` editor-state key. | W5, W6, W11, §5.3, §6.4, §6.6, §7.1 | +| §5.2 mock/text inconsistencies **S1 (zero-state dot)** and **S2 (language dropdown)** are **resolved**: chadw fixed the zero-state mock and removed the language dropdown from the #278 Checks mocks. | chadw-eten ("Fixed." / "good idea… I have adjusted the mocks."). | §5.2, §12 | +| New requirement adopted: **highlight repeated words in the verse text** (underline/color). | joelthe1/Ulf (card #278 comment). We will implement it; because it touches the verse-rendering pane (outside the left panel this proposal builds) and warrants its own design pass, it ships as a **separate, follow-on PR** so the tab/panel work can land independently. §5.3 item 4 records the intent; the visual/interaction details are specced just before that PR. | §5.3, §11 | --- @@ -54,12 +54,12 @@ The ignore actions require server-side persistence that does not exist yet. #172 ### 1.2 Repos touched -| Repo | Touched | What changes | -| --- | --- | --- | -| **fluent-web** | yes (bulk) | Left-panel tab container, Checks panel feature, check-trigger hook, suppression cascade, editor-state keys. | -| **fluent-api** | yes (small) | New `user_settings` table + a new `self` domain exposing `GET/PUT /self/settings`; Zod-schema extension of the editor-state `resources` blob (no migration). | -| **fluent-ai** | no | The check itself is unchanged. | -| **fluent-platform** | no | No new services or env vars. | +| Repo | Touched | What changes | +| ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **fluent-web** | yes (bulk) | Left-panel tab container, Checks panel feature, check-trigger hook, suppression cascade, editor-state keys. | +| **fluent-api** | yes (small) | New `user_settings` table + a new `self` domain exposing `GET/PUT /self/settings`; Zod-schema extension of the editor-state `resources` blob (no migration). | +| **fluent-ai** | no | The check itself is unchanged. | +| **fluent-platform** | no | No new services or env vars. | --- @@ -92,20 +92,20 @@ The ignore actions require server-side persistence that does not exist yet. #172 Restated conclusions; supporting analysis in the cited sections. -| # | Decision | Short rationale | -| --- | --- | --- | -| **W1** | Suppression persistence ships as an extension of #172's scope; no new product card. | #172's wording covers backend dependencies of the UI work; D1's deferral was about caching tool results, not user preferences. See §1.1. | -| **W2** | Hybrid storage: "Ignore This Time" lives in the existing per-chapter editor-state JSONB (schema-key addition, no migration); "Ignore Always" lives in a new `user_settings` table (one row per user, single Zod-typed JSONB column). Findings are filtered **client-side**. | Each rule is stored at exactly the scope it governs. Client-side filtering keeps the AI proxy a pure pass-through (D8/D9). `user_settings` deliberately establishes Fluent's user-global preference store: future settings become schema extensions, not migrations. See §7. | -| **W3** | Chapter-wide check on every successful verse save: a TanStack `useQuery` keyed on `(chapterAssignmentId, saveCounter)`, where `saveCounter` increments in the verse-save mutation's `onSuccess`. The request sends **all currently drafted verses** of the chapter. No extra coalescing debounce. | The dot needs chapter-wide awareness; per-verse merging is more code for no user-visible gain. The existing 2 s per-verse save debounce already rate-limits; the check is <1 s. KISS — optimize if heavier checks arrive. See §6.2. | -| **W4** | `snt_id` = `"{bookCode} {chapter}:{verse}"` (USFM book code, e.g. `JDG 4:3`), matching the convention already used by the repo smoke tests. Occurrence identity for suppressions = `(snt_id, repeated_word, ordinal)` where ordinal is the index of the finding among same-`repeated_word` findings in the verse, ordered by `start_position`. | Ordinals survive unrelated edits (positions don't); adding/removing an earlier same-pair occurrence conservatively re-flags, the safe failure direction. We count Greek Room's findings, never tokenize text ourselves, so Greek Room's equivalence policy is inherited consistently. See §6.3. | -| **W5** | Every finding resolves to **active/inactive** via a three-layer cascade — Greek Room verdict (`legitimate`), user-global word-pair rule, occurrence rule — where the **most specific non-silent verdict wins**. Rules are tri-state maps (`absent / 'suppress' / 'surface'`). The dot counts active findings only. Inactive findings are hidden by default and revealed by the **"Show Ignored" toggle** (default OFF, not persisted, per revised #278); when shown they render greyed with the ignore-type label. | One mechanism explains machine-legitimate and user-ignored alike ("pre-ignored by Greek Room"). Specificity, not temporal order, keeps resolution deterministic. Greyed-not-removed (under the toggle) keeps undo discoverable. See §6.4. | -| **W6** | Active findings show `[Ignore Here] [Ignore Everywhere]` (per revised card). `[Ignore Everywhere]` opens a **standard confirm dialog** before writing the global rule. Inactive findings show one `[Undo ▾]` split button: default click acts at the occurrence layer (delete own rule, or write an occurrence `surface` override); the chevron menu offers explicit global actions with consequence-naming labels. Any global write first purges the user's occurrence rules for that pair **in the current chapter's editor state only**. | Default click never silently edits global state. The card asks for a confirm on the global action because it reads as irreversible; we add the dialog **and** keep the `[Undo ▾]` reversibility — the two are complementary (confirm guards the click; undo guards the mistake). Purge-local prevents the just-clicked panel from appearing to ignore the action; other chapters' specific pronouncements deliberately stand. See §6.5. | -| **W7** | Settings endpoint lives in a **new `self` domain**: **`GET /self/settings` + `PUT /self/settings`**, session-implicit user (no user in URL), guarded by `authenticateUser` alone, full-replace upsert of one Zod-typed JSONB blob. File quartet `domains/self/settings/self-settings.{route,service,repository,types}.ts`. | Per kaseywright: a `self` domain gives clean semantic separation, avoids the redundant `{userId}` param (and the `requireSelf`/`UserPolicy` middleware that `/users/{id}/…` routes carry), and prevents accidental over-exposure (no `{id}` to misuse). Last-writer-wins on concurrent tabs is inherited from editor-state and accepted. See §8. | -| **W8** | Graceful degradation by feature detection: if `GET /self/settings` 404s, the session records `globalIgnoresAvailable = false` and the `[Ignore Everywhere]` button plus global menu entries are **not rendered** (capability hidden, never a dead control). Unknown/absent JSONB keys parse as empty on both sides. Either repo's PR can land first. | The UI must not assume the backend half exists. An invisible capability is honest; a dead button is a bug report. See §9. | -| **W9** | Check-call failure surfaces as a single inline `text-sm text-red-500` line at the top of the Checks panel ("Checks failed to refresh"); the panel keeps rendering the last successful findings (TanStack keeps `query.data` on refetch failure) and the dot reflects that last-known state. Failures log via the existing `Logger`. No toast/banner/popup. | Matches the drafting page's own inline-status precedent ("Auto-save failed"). Failure mode degrades to "results are one save behind." See §9.2. | -| **W10** | The check runs in drafting mode only — not in the read-only `/view` route — and is skipped (`enabled: false`) when no verse has content and while the settings feature-detection probe (§9.1) is unresolved. | Card scope is the drafting view; empty chapters have nothing to check; the probe is one fast `GET`, and waiting for it means the first findings render is already cascade-correct. Review-stage checks are future work. See §6.2. | -| **W11** | Left-panel architecture follows the mocks: a text-tab header row ("Resources \| Checks", blue underline active state, blue dot after "Checks"), dot visible from either tab; Checks content = per-check accordion sections with verse-grouped snippets, the two buttons, and a "Show Ignored" toggle at the bottom; zero state inside the accordion. Only `activeLeftTab` is persisted in editor state (the "Show Ignored" state resets per session, per revised #278). Proposed deviation for sign-off: when the whole panel is closed, the dot is mirrored on the panel-toggle button. | Mock-faithful where the mocks speak; the toggle-button dot preserves #277's intent (translator is notified) when the panel is hidden. See §5. | -| **W12** | One proposal document (this file) covering both repos; two implementation PRs (fluent-web, fluent-api), cross-referencing each other and the cards. | Splitting the proposal doubles reviewer overhead for a design whose halves only make sense together. See §9.4. | +| # | Decision | Short rationale | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **W1** | Suppression persistence ships as an extension of #172's scope; no new product card. | #172's wording covers backend dependencies of the UI work; D1's deferral was about caching tool results, not user preferences. See §1.1. | +| **W2** | Hybrid storage: "Ignore This Time" lives in the existing per-chapter editor-state JSONB (schema-key addition, no migration); "Ignore Always" lives in a new `user_settings` table (one row per user, single Zod-typed JSONB column). Findings are filtered **client-side**. | Each rule is stored at exactly the scope it governs. Client-side filtering keeps the AI proxy a pure pass-through (D8/D9). `user_settings` deliberately establishes Fluent's user-global preference store: future settings become schema extensions, not migrations. See §7. | +| **W3** | Chapter-wide check on every successful verse save: a TanStack `useQuery` keyed on `(chapterAssignmentId, saveCounter)`, where `saveCounter` increments in the verse-save mutation's `onSuccess`. The request sends **all currently drafted verses** of the chapter. No extra coalescing debounce. | The dot needs chapter-wide awareness; per-verse merging is more code for no user-visible gain. The existing 2 s per-verse save debounce already rate-limits; the check is <1 s. KISS — optimize if heavier checks arrive. See §6.2. | +| **W4** | `snt_id` = `"{bookCode} {chapter}:{verse}"` (USFM book code, e.g. `JDG 4:3`), matching the convention already used by the repo smoke tests. Occurrence identity for suppressions = `(snt_id, repeated_word, ordinal)` where ordinal is the index of the finding among same-`repeated_word` findings in the verse, ordered by `start_position`. | Ordinals survive unrelated edits (positions don't); adding/removing an earlier same-pair occurrence conservatively re-flags, the safe failure direction. We count Greek Room's findings, never tokenize text ourselves, so Greek Room's equivalence policy is inherited consistently. See §6.3. | +| **W5** | Every finding resolves to **active/inactive** via a three-layer cascade — Greek Room verdict (`legitimate`), user-global word-pair rule, occurrence rule — where the **most specific non-silent verdict wins**. Rules are tri-state maps (`absent / 'suppress' / 'surface'`). The dot counts active findings only. Inactive findings are hidden by default and revealed by the **"Show Ignored" toggle** (default OFF, not persisted, per revised #278); when shown they render greyed with the ignore-type label. | One mechanism explains machine-legitimate and user-ignored alike ("pre-ignored by Greek Room"). Specificity, not temporal order, keeps resolution deterministic. Greyed-not-removed (under the toggle) keeps undo discoverable. See §6.4. | +| **W6** | Active findings show `[Ignore Here] [Ignore Everywhere]` (per revised card). `[Ignore Everywhere]` opens a **standard confirm dialog** before writing the global rule. Inactive findings show one `[Undo ▾]` split button: default click acts at the occurrence layer (delete own rule, or write an occurrence `surface` override); the chevron menu offers explicit global actions with consequence-naming labels. Any global write first purges the user's occurrence rules for that pair **in the current chapter's editor state only**. | Default click never silently edits global state. The card asks for a confirm on the global action because it reads as irreversible; we add the dialog **and** keep the `[Undo ▾]` reversibility — the two are complementary (confirm guards the click; undo guards the mistake). Purge-local prevents the just-clicked panel from appearing to ignore the action; other chapters' specific pronouncements deliberately stand. See §6.5. | +| **W7** | Settings endpoint lives in a **new `self` domain**: **`GET /self/settings` + `PUT /self/settings`**, session-implicit user (no user in URL), guarded by `authenticateUser` alone, full-replace upsert of one Zod-typed JSONB blob. File quartet `domains/self/settings/self-settings.{route,service,repository,types}.ts`. | Per kaseywright: a `self` domain gives clean semantic separation, avoids the redundant `{userId}` param (and the `requireSelf`/`UserPolicy` middleware that `/users/{id}/…` routes carry), and prevents accidental over-exposure (no `{id}` to misuse). Last-writer-wins on concurrent tabs is inherited from editor-state and accepted. See §8. | +| **W8** | Graceful degradation by feature detection: if `GET /self/settings` 404s, the session records `globalIgnoresAvailable = false` and the `[Ignore Everywhere]` button plus global menu entries are **not rendered** (capability hidden, never a dead control). Unknown/absent JSONB keys parse as empty on both sides. Either repo's PR can land first. | The UI must not assume the backend half exists. An invisible capability is honest; a dead button is a bug report. See §9. | +| **W9** | Check-call failure surfaces as a single inline `text-sm text-red-500` line at the top of the Checks panel ("Checks failed to refresh"); the panel keeps rendering the last successful findings (TanStack keeps `query.data` on refetch failure) and the dot reflects that last-known state. Failures log via the existing `Logger`. No toast/banner/popup. | Matches the drafting page's own inline-status precedent ("Auto-save failed"). Failure mode degrades to "results are one save behind." See §9.2. | +| **W10** | The check runs in drafting mode only — not in the read-only `/view` route — and is skipped (`enabled: false`) when no verse has content and while the settings feature-detection probe (§9.1) is unresolved. | Card scope is the drafting view; empty chapters have nothing to check; the probe is one fast `GET`, and waiting for it means the first findings render is already cascade-correct. Review-stage checks are future work. See §6.2. | +| **W11** | Left-panel architecture follows the mocks: a text-tab header row ("Resources \| Checks", blue underline active state, blue dot after "Checks"), dot visible from either tab; Checks content = per-check accordion sections with verse-grouped snippets, the two buttons, and a "Show Ignored" toggle at the bottom; zero state inside the accordion. Only `activeLeftTab` is persisted in editor state (the "Show Ignored" state resets per session, per revised #278). Proposed deviation for sign-off: when the whole panel is closed, the dot is mirrored on the panel-toggle button. | Mock-faithful where the mocks speak; the toggle-button dot preserves #277's intent (translator is notified) when the panel is hidden. See §5. | +| **W12** | One proposal document (this file) covering both repos; two implementation PRs (fluent-web, fluent-api), cross-referencing each other and the cards. | Splitting the proposal doubles reviewer overhead for a design whose halves only make sense together. See §9.4. | --- @@ -141,7 +141,7 @@ The response envelope is consumed whole (D9): the hook inspects `status` and `re The cards' mockups (drafting page, Judges 4, Gujarati IRV project) define the visual target. The #278 mocks were updated during the first review round (§0); the description below tracks the **current** mocks: -- **Tab header (#277 mock):** the left panel's current `Resources` heading is replaced by a text-tab row — **"Resources | Checks"** — active tab in blue with a blue underline, inactive tab plain. The **notification dot is a solid blue filled circle immediately right of the "Checks" label**, and the mock shows it while the *Resources* tab is active: the dot is visible from either tab whenever the panel is open. +- **Tab header (#277 mock):** the left panel's current `Resources` heading is replaced by a text-tab row — **"Resources | Checks"** — active tab in blue with a blue underline, inactive tab plain. The **notification dot is a solid blue filled circle immediately right of the "Checks" label**, and the mock shows it while the _Resources_ tab is active: the dot is visible from either tab whenever the panel is open. - **Checks content (#278 mocks):** below the tab header, a card containing a **collapsible "Repeated Words" accordion section** (expanded by default). The accordion-per-check structure anticipates future sibling checks (the board already holds draft cards for Greek Room Wildebeest and Spell checks) without UI rework. Inside the section, findings are grouped by verse: - bold **"Verse N"** heading per verse that has findings (verses without findings get no group); - one row per occurrence: a one-line context snippet showing the repeated pair in context; @@ -221,14 +221,14 @@ useQuery({ - **Request body** is the full `RepeatedWordsRequest` (D8 shape): `lang_code`/`lang_name` from the project's target language, `project_id`/`project_name` from `projectItem`, and `verses[]` covering **all currently drafted verses** of the chapter (content from the drafting state, not a refetch — what the translator sees is what gets checked). - **`snt_id` = `"{bookCode} {chapter}:{verse}"`** with the USFM book code (e.g. `JDG 4:3`), the convention the repo smoke tests already use. Implementation note: verify the field carrying the USFM code on the drafting page's `projectItem` (vs. display name) and thread it into the builder. - **Where it runs (W10):** drafting route only; `enabled` is false in the read-only `/view` route (reviewers see no Checks activity in v1), when no verse has content, and while the settings feature-detection probe (§9.1) is unresolved — the probe is a single fast `GET`, and waiting for it means the first findings render is already cascade-correct (the user's global rules are known, present or absent). -- **Emptied chapters resolve naturally:** any save of emptied text triggers a fresh check whose empty findings clear the panel and dot. (When *every* verse is emptied the query disables instead; the panel and dot then treat findings as empty rather than rendering stale data.) +- **Emptied chapters resolve naturally:** any save of emptied text triggers a fresh check whose empty findings clear the panel and dot. (When _every_ verse is emptied the query disables instead; the panel and dot then treat findings as empty rather than rendering stale data.) ### 6.3 Occurrence identity (W4) A suppression must survive verse edits without our re-parsing text. The key is **`(snt_id, repeated_word, ordinal)`**: - `ordinal` = index of this finding among findings in the same verse with the same `repeated_word`, ordered by `start_position` ("x of n"). Computed from **Greek Room's findings only** — we never tokenize verse text ourselves, so Greek Room's case/diacritic equivalence policy is inherited and consistent between runs. -- Ordinals survive unrelated edits (`start_position` does not). Adding or removing an *earlier* same-pair occurrence shifts later ordinals and conservatively re-flags — the safe failure direction. +- Ordinals survive unrelated edits (`start_position` does not). Adding or removing an _earlier_ same-pair occurrence shifts later ordinals and conservatively re-flags — the safe failure direction. - String comparison between a stored rule's `repeated_word` and a fresh finding's: **NFC-normalize both, compare exactly, no case folding of our own.** Unicode case folding is locale-sensitive (the Turkish dotless-ı problem) and Fluent targets minority languages; NFC handles composed/decomposed accent variation. Note that Greek Room already delivers `repeated_word` lowercased ("word word" form, per the fluent-ai schema) — original casing lives only in `surf`, which we display but never compare — so case equivalence is wholly Greek Room's policy, inherited rather than re-implemented. - Documented caveat: a triple repetition ("the the the") yields two overlapping findings (ordinals 1 and 2). Mechanically fine; slightly odd UX; accepted for v1. @@ -236,11 +236,11 @@ A suppression must survive verse edits without our re-parsing text. The key is * Every finding resolves through three layers; the **most specific non-silent verdict wins** (specificity, not temporal order — deterministic and reproducible from stored state): -| Layer | Scope | Verdicts | -| --- | --- | --- | -| 0 — Greek Room | this finding | `active` (suspicious) / `inactive` (`legitimate: true`) — always present | -| 1 — User global | word pair, all the user's projects | _silent_ / `suppress` / `surface` | -| 2 — Occurrence | `(snt_id, repeated_word, ordinal)`, this chapter assignment | _silent_ / `suppress` / `surface` | +| Layer | Scope | Verdicts | +| --------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | +| 0 — Greek Room | this finding | `active` (suspicious) / `inactive` (`legitimate: true`) — always present | +| 1 — User global | word pair, all the user's projects | _silent_ / `suppress` / `surface` | +| 2 — Occurrence | `(snt_id, repeated_word, ordinal)`, this chapter assignment | _silent_ / `suppress` / `surface` | - Stored rules are **tri-state maps** (`key → 'suppress' | 'surface'`, absent = silent), not bare suppression sets — `surface` is what makes per-occurrence undo of a global rule (or of a Greek Room `legitimate` verdict) possible. - A machine-legitimate finding is conceptually "pre-ignored by Greek Room": same inactive state as user-ignored, different actor, same undo affordance. @@ -253,17 +253,18 @@ Every finding resolves through three layers; the **most specific non-silent verd - **Confirm dialog on `[Ignore Everywhere]`** (revised #278): the card asks for it because the action reads as global and irreversible. We honor the request **and** keep the action reversible via `[Undo ▾]` below — the dialog guards the deliberate click; the undo guards a mistake that slips past it. The two are not redundant: confirm is pre-action friction, undo is post-action recourse. Why reversibility falls out of the design rather than being an extra feature we chose to bolt on — the implication chain (see also §6.4, which this depends on): - 1. Greek Room reports **both** polarities: repetitions it flags (suspicious) and repetitions it judges `legitimate` (a *negative* / "don't flag" verdict). + 1. Greek Room reports **both** polarities: repetitions it flags (suspicious) and repetitions it judges `legitimate` (a _negative_ / "don't flag" verdict). 2. Once we decide to **show** the legitimate ones (§5.3 item 2 — so the translator can see what the checker waved through), it's natural to let the translator disagree and mark one **illegitimate** (re-surface it). 3. Expressing "re-surface this" requires a **positive** record (`surface`), not only the negative (`suppress`) records an ignore-list would hold. So the stores must be tri-state, not suppression-only (§6.4). - 4. Given positive *and* negative records at more than one scope, a **layered** model is the natural fit: a local (occurrence) record can override a global (word-pair) one, and the override works in **both polarities** (locally surface something suppressed globally, or locally suppress something surfaced). - 5. With that layering in place, a single bare "undo" is **ambiguous** — at which layer, in which polarity? Resolving that ambiguity (the `[Undo ▾]` split button: default = act at the occurrence layer; chevron = act globally) is what the design *has* to do anyway — and the same mechanism incidentally gives the translator the "oops, I hit the wrong button" recourse for `[Ignore Everywhere]`. Reversibility is therefore inherited, not added. + 4. Given positive _and_ negative records at more than one scope, a **layered** model is the natural fit: a local (occurrence) record can override a global (word-pair) one, and the override works in **both polarities** (locally surface something suppressed globally, or locally suppress something surfaced). + 5. With that layering in place, a single bare "undo" is **ambiguous** — at which layer, in which polarity? Resolving that ambiguity (the `[Undo ▾]` split button: default = act at the occurrence layer; chevron = act globally) is what the design _has_ to do anyway — and the same mechanism incidentally gives the translator the "oops, I hit the wrong button" recourse for `[Ignore Everywhere]`. Reversibility is therefore inherited, not added. We are not requesting that this override the card's "cannot be undone" framing; it is documented here for anyone who wants to follow the reasoning, and the implemented PR is reviewed again before merge. + - **Inactive finding:** one `[Undo ▾]` split button (shadcn `dropdown-menu`, already in the codebase — no hidden gestures): - - **Default click** acts at the occurrence layer: if inactive via its *own* occurrence rule → delete that entry; if inactive via a global rule or Greek Room's verdict → write an occurrence-level `surface` override (the global rule / machine verdict survives; only this occurrence resurfaces). + - **Default click** acts at the occurrence layer: if inactive via its _own_ occurrence rule → delete that entry; if inactive via a global rule or Greek Room's verdict → write an occurrence-level `surface` override (the global rule / machine verdict survives; only this occurrence resurfaces). - **Chevron menu** offers the deliberate global actions with consequence-naming labels, e.g. _"Stop ignoring 'the the' everywhere"_ (deletes the global entry). -- **Global writes purge local rules — current chapter only.** Executing any global (everywhere) suppress or surface first deletes the user's occurrence-level rules for that word pair in the *current chapter assignment's* editor state, then writes the global rule. This is purely UI coherence: without it, the just-clicked panel would appear to ignore the action (occurrence beats global in the cascade). Occurrence rules in **other** chapters/projects deliberately stand — they were specific pronouncements, and we don't revert decisions from afar. Client-side operation; no special endpoint semantics. +- **Global writes purge local rules — current chapter only.** Executing any global (everywhere) suppress or surface first deletes the user's occurrence-level rules for that word pair in the _current chapter assignment's_ editor state, then writes the global rule. This is purely UI coherence: without it, the just-clicked panel would appear to ignore the action (occurrence beats global in the cascade). Occurrence rules in **other** chapters/projects deliberately stand — they were specific pronouncements, and we don't revert decisions from afar. Client-side operation; no special endpoint semantics. - **Direct edits resolve silently** (card #278): if the translator fixes the text, the next auto-save's fresh findings simply no longer contain the occurrence — no action needed, panel and dot update. ### 6.6 Left-panel container and persisted UI state (W11) @@ -273,7 +274,7 @@ Every finding resolves through three layers; the **most specific non-silent verd - **`LeftPanel`** owns the tab header row ("Resources | Checks", blue underline on the active tab, blue dot right of "Checks" when active findings exist) and renders either the existing `ResourcePanel` or the new `ChecksPanel` below it. `ResourcePanel` loses only its `