Files
waggle-os/docs/ux-refactor/p3-review-record.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

4.7 KiB

P3 — D2 Two-Mind Memory Center: adversarial review record (2026-06-11)

Workflow: 6 dimension finders (races / contract / security / parity / tests / ux) → per-finding adversarial refuter. 36 agents. 19 confirmed / 11 refuted. First run was VOID (all finders hit the session limit and returned nothing — recorded here because an empty "confirmed" list from a dead run is indistinguishable from a clean one unless you check the failures block).

The HIGH (confirmed independently by 5 of 6 dimensions)

Cross-mind fall-through on mutationscandidateStores pushed the personal store unconditionally after the workspace store, and PATCH/archive/DELETE/merge acted on the first store resolving the id. Frame ids collide across the per-mind SQLite DBs (observed on real data: id 36 in both minds), so a stale workspace row whose id had vanished from the workspace store would resolve to — and hard-delete/patch/merge-archive — an unrelated PERSONAL memory, returning 200. The path became reachable for the first time in this phase (pre-P3, the UI never sent workspace on these mutations). Verifier note: the "two concurrent surfaces" trigger is weak in the single-window app, but background deletion lanes (W4.3 replace-on-update, compaction, cleanup_frames MCP) + open stale lists make it ordinary anyway.

Fix: mind-strict resolution. candidateStores(workspace, mind?) — when the caller declares mind, exactly that store is consulted, no fallback; parseMind 400s on invalid values. All five single-memory routes + merge accept mind; the adapter threads it; MemoryCenterTab (the only caller) always declares it. Legacy no-mind fall-through preserved and PINNED as back-compat. Server pins: workspace-mind PATCH/archive/DELETE succeed; personal-only id + mind=workspace → 404 with the personal frame intact; merge never resolves its id set in personal; 400 contracts.

Other confirmed findings → dispositions

# Sev Finding Disposition
2 MED mutate()'s closured await load() beats the loadSeq guard — old-mind/old-filter rows can win after a mid-mutation mind/filter switch FIXED: post-mutation refetch rides a reloadTick state through the load effect (always current props); pinned with a deferred-archive + mid-flight switch test
3,15 LOW/MED In-flight old-mind response can commit into the reset→next-load window FIXED: mind-switch reset effect bumps loadSeq (invalidates in-flight commits)
5 LOW Reset/load keyed on raw workspaceId — personal-mind instances wiped on workspace churn FIXED: keyed on effective wsParam
18 LOW Misleading "No memories match these filters." during the 250ms debounce window after a switch FIXED: reset effect sets loading=true
7 MED Audit events stamped the REQUEST's workspaceId, not the resolved mind FIXED: all five audit sites attribute c.mind === 'workspace' ? workspace : 'personal' (incl. POST-create unknown-workspace fallback)
4,19 LOW ?filter= stash strands when landing on a legacy tab; replays later FIXED: route stashes only when the landing view is Memories
10 LOW URL keeps advertising ?filter= after the one-shot stash is consumed FIXED: route strips the param post-consumption (replace); refresh/share can't re-seed a stale filter
12 MED J08 cold-load route stash unpinned FIXED: route-level pins (stash + strip; legacy-tab drop)
13 MED No server test exercised ANY workspace-mind mutation FIXED: mutation suite added (see HIGH pins)
14 MED Cross-mind fall-through unpinned FIXED: HIGH pin above
11,17 LOW Workspace pill could announce pressed+disabled simultaneously; pills lacked group semantics FIXED: aria-pressed only when enabled; role="group" labeled
16 LOW Tab bar lacked tablist semantics FIXED: role="tablist"/role="tab"/aria-selected

Refuted (11) — examples

XSS via the moved dangerouslySetInnerHTML (renderSimpleMarkdown's two defenses hold); path traversal via the workspace param (getWorkspaceMindDb resolves from an allowlisted map); mind+filter interaction double-counting (per-store getRecent precedes the merge slice exactly as before); WorkspaceDesktopApp uncontrolled-mode regression (import is side-effect-free); StrictMode double-stash (ref-latched); several parity claims (banner gating, context-rail wiring, KG scope selector all carried over).

Behavior change vs the pre-review build (review happened before anything was committed)

?filter= no longer survives in the URL after consumption, and a mind switch no longer carries it (one-shot intent semantics) — the original "filter survives the switch" route pin was replaced accordingly.