# Issue 22794 — proof audit, pass 1

Read-only source audit. Base `70e5e384da`, head `def9e75af2`, branch `fix/issue-22794`, worktree `/home/ubuntu/wt/issue-22794`. No server/browser was started, no captures made, no PR exists. All runtime expectations below remain unverified.

## Scope and lead proof

Six-line diff in `apps/web/src/components/settings/ProjectIconPickerDialog.tsx`: add a grapheme segmenter and require normalized monograms to have at most two graphemes, in addition to the existing contract predicate. Count is after NFKC, trim, uppercase. Existing contract permits up to 32 code units and letters/numbers with subsequent marks/joiners; server already rejects new monograms over two graphemes.

Lead with the real settings flow, not an isolated component: Settings → Projects → choose an editable project/environment → Project icon → Choose icon → Monogram. BEFORE: enter ABC, show Save icon enabled (UI accepts the value), click Save icon, show dialog closing and generic failure toast headed “Failed to update project icon”; reopen and show previous icon unchanged. ABC is not successfully persisted: the server rejects it. Record the actual toast description, rather than assuming its exact text. AFTER: enter ABC, show invalid input, automatic-project-monogram preview fallback and disabled Save icon; attempt activation, demonstrate no close/no request/no failure toast, then replace with KC, save, reopen and show persistence. Narrate every action and the observed outcome.

## Fixture matrix

A = behavior affected by diff; R = focused unchanged regression; U = unavailable/unverified surface. Every row involving typing, clicking, persistence, reopening, or waiting requires a recording with narrated steps; stills cannot substitute. A single recording may cover multiple rows.

| ID | Mark | Path / caller / data / condition | Smallest distinct fixture and required observation | Evidence |
|---|---|---|---|---|
| A1 | A | Only caller: ProjectSettingsPanel → lazy ProjectIconPickerDialog; monogram; >2 graphemes | Existing saved KC; type ABC. Before enabled/ABC preview → save closes → generic error → KC remains. After invalid/fallback preview/disabled → cannot save; correct to KC → success. | Before + after narrated recordings in all four width/theme cells |
| A2 | A/R | Same mode; 1 and 2 graphemes; normalized Unicode | Enter B (one-grapheme preview); save/reopen. Enter literal e + combining acute + B (`e\u0301B`); save/reopen; normalized ÉB accepted. Verify no false rejection by code-unit count. | Include in one 1440/light after recording |
| A3 | R | Existing persisted data, initial state, close/reopen effect | Begin with existing KC; open without editing, enabled; save/reopen KC. Make invalid draft ABC, Cancel, reopen → KC restored. | Include in after recordings; narrate cancel/reopen |
| A4 | A | Legacy persisted >2-grapheme text allowed by snapshot contract | Optional synthetic legacy ABC fixture: project display still decodes; opening picker becomes invalid with fallback; correction to KC succeeds. Not necessary for main fix proof, but needed to claim legacy behavior verified. | Supplemental narrated recording; otherwise explicitly unverified |
| R1 | R | Existing schema failures; empty/whitespace/punctuation/over-32-code-unit | Empty and A! stay invalid/disabled; normalize ` ab ` → AB enabled. Length >32 remains invalid even if combining marks form ≤2 graphemes. These share invalid visual state with A1; no separate theme/width captures. | Fold brief transitions into 1440/light recording; code inspection for 32-unit boundary if not exercised |
| R2 | R | Mode conditional: lucide / emoji bypass monogram guard | With ABC draft, switch Icons → Save enabled; switch Emoji → Save enabled; switch back Monogram → disabled. Save one non-monogram mode and reopen to confirm unchanged path. | Include in 1440/light recording; icon search, empty search, emoji grids/colors unchanged, no exhaustive permutation |
| R3 | R | Color and automatic identity; input preview true/false branch | Use project name “Proof Audit” so fallback PA visibly differs from ABC/KC; keep one color throughout. Invalid preview PA vs valid KC/ÉB. | Covered by A1/A2; no color sweep |
| R4 | R | Width/theme | 1440 and 390 CSS-pixel viewport widths, light and dark. Show input, hint, preview, all footer controls and no clipping; 390 is responsive web, not native mobile. Record viewport height and browser zoom. | Four before + four after recordings; no extra stills required |
| R5 | R | Group/member count, permissions, environment connection | One editable connected local project is minimum. Caller iterates grouped members, permission/connection failures and remote destination routing unchanged. No added count-based render branch; multi-member/remote/relay/tunnel not required for this six-line predicate proof. | Source audit only; do not claim runtime verified |
| U1 | U | Desktop wraps changed web picker | Shares picker; native icon-file bridge is unrelated. Desktop unavailable on VM. | Explicitly UNVERIFIED; responsive web is not desktop evidence |
| R6/U | R/U | Native mobile ProjectFavicon, project overview / thread lists / task and archive displays | Display-only consumer of saved projectIcon; no changed native editor. Existing KC and saved ÉB should display after sync. | Source audit only, native runtime UNVERIFIED; no mobile capture required to prove web guard |
| R7 | R | Other entry points / providers / contracts | Search finds one picker caller. No separate command-palette/keybinding picker or provider-specific branch; provider adapters, contracts/server unchanged. No new reverse-state feature. | Source audit; Cancel/reopen covered by A3 |

## Minimum capture set

Eight narrated screen recordings total: `before-1440-light`, `after-1440-light`, `before-1440-dark`, `after-1440-dark`, `before-390-light`, `after-390-light`, `before-390-dark`, `after-390-dark` (MP4 or WebM). Each pair follows A1 using equivalent isolated state. The 1440/light after clip also carries A2, A3 and R1/R2; other after clips need only ABC invalid → KC correction/save/reopen. Narration may be recorded voice or clearly timestamped on-screen step captions plus accompanying transcript; label BEFORE/AFTER, commit, viewport and theme. Keep the failure toast visible long enough to read. Do not use screenshots to claim disabled activation, async error timing, save persistence, reset, or mode transitions. No arbitrary timeout claim: observe request completion/toast/reopened persisted state. Optional A4 needs one additional clip only if legacy coverage is claimed.

## Seed real app settings route

Do not seed a fake dialog/harness. With the isolated dev server stopped, run `vp run migrate-dev-db` in the worktree to create `.t3/userdata/statev2.sqlite` from a read-only snapshot of real state. Never run against or modify `~/.t3/userdata`. Use a stopped project with real workspace metadata; avoid exposing real titles/paths/history in public evidence. If a neutral fixture is needed, add/select a disposable workspace/project through the app and name it “Proof Audit”. Use the real app icon picker to save KC before recording; this is valid on both base and head and exercises the normal mutation path. The unchanged current data KC should be retained if already available; renaming is optional if privacy requires it.

Navigate `/settings/projects` and select the project using the real settings scope selector. The bare route without project scope only shows “Choose a project…”; retain the UI-generated `project`, `machine`, and optional `checkout` search parameters, rather than guessing a project ID/key. Select one connected environment with project-edit permission. Open “Choose a project icon” (not the icon-file chooser). Set theme through Appearance and viewport through the authorized browser. Save/reopen after each accepted input. Use the same seed/reset state for base and head; keep the running T3 session untouched. Future browser work needs the parent's authorized integrated workflow; none is performed in this pass.

Required state fixture path: `/home/ubuntu/wt/issue-22794/.t3/userdata/statev2.sqlite` (generated later; not created in this audit). Required input fixtures: `KC`, `ABC`, `B`, literal `e\u0301B`; no additional committed fixture files needed. Optional legacy fixture must be introduced only into an isolated snapshot/event-consistent test state; current server cannot create ABC via normal mutation. Do not directly edit live projections to manufacture legacy history. No legacy seed script is supplied or required for the minimum capture set.

## Source anchors

- `apps/web/src/components/settings/ProjectIconPickerDialog.tsx`: normalization/validity/save guard lines 94–108; preview and aria-invalid in monogram branch; disabled footer button.
- `apps/web/src/components/settings/ProjectSettingsPanel.tsx`: reportFailure/updateAllMembers, setProjectIcon and single picker caller.
- `apps/web/src/routes/settings.projects.tsx`, `apps/web/src/components/settings/ProjectsSettings.tsx`: scoped real route.
- `packages/contracts/src/project.ts`: permissive legacy monogram decoding.
- `apps/server/src/orchestration-v2/ProjectCommands.ts`: authoritative two-grapheme mutation guard.
- `apps/web/src/components/ProjectMonogram.tsx`: one-vs-multiple-grapheme glyph sizing, unchanged.
- `apps/mobile/src/lib/projectIcon.ts`, `apps/mobile/src/components/ProjectFavicon.tsx`, `apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx`: unchanged display consumption.

Audit result: source supports the proposed fix and minimal evidence plan. No runtime finding asserted. Desktop/native mobile, optional legacy seed, remote/grouped environments remain unverified.

FRICTION: none
