Skip to content

feat(ui): duplicate review with locally stored pending decisions - #1983

Merged
BYK merged 2 commits into
mainfrom
devin/1791146816-mem-01-dedup-review
Oct 5, 2026
Merged

BYK merged 2 commits into
mainfrom
devin/1791146816-mem-01-dedup-review

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

MEM-01: a read-only, project-scoped duplicate review screen at /ui/projects/:projectId/duplicates. Review decisions are stored only in this browser. Closes #1803. Part of the P2 gate of #1824.

The typed dry-run preview (POST /api/v1/projects/:id/dedup, which returns groups[] plus the legacy project/global DedupResults) and the explicit apply route already landed with #1826/#1828. This PR adds the UI on top of them, plus additive candidate fields. Nothing in this PR writes to the server.

API (additive only): every groups[].candidates[] entry gains these fields; existing fields and the legacy payload are unchanged:

scope: "project" | "shared"   // shared ⇔ project_id IS NULL || cross_project
project_id: string | null
category: string
confidence: number
source_session: string | null
updated_at: number | null

UI:

  • Group list: titles, best match score, reasons, scope, and a pending/accepted/skipped/stale status.
  • Focused group: the candidates side by side, each showing:
    • full current content, loaded lazily from GET /knowledge/:id/versions
    • v{revision}, match score and reasons, confidence, category, scope
    • source-session link, last-updated time, short id, and a document link
    • a "Keep this one" choice
  • Each column warns "Changed since this scan" or "Removed since this scan" when the entry has moved on. Accepting is disabled until you rescan.
  • Keyboard: j/k move between groups, a accept, s skip, u clear, 1–9 choose the keeper. Shortcuts only fire inside the review screen and never while typing. Every action is also a button.
  • Entry points: the "Review duplicates" link on the project page, and the sidebar "Find duplicates" item (previously a UI-08 placeholder).
  • Honest states: loading ("Scanning for duplicates"), error with retry, empty, and an "IndexedDB unavailable" notice.

Local decisions (IndexedDB lore-ui v3 → v4): a new reviewDecisions store holds DedupReviewMark { projectId, groupId, decision, keepId, mergeIds, expectedRevisions, markedAt }, keyed by logical id. This is local working state; it is never merged into the entity stores.

  • markStatus() treats a mark as stale unless the fresh group has the same members and every revision matches the one recorded at marking time. Stale marks never count as accepted.
  • Marks for groups that are no longer in the scan are listed as orphaned, with an explicit "Discard" button. They are never deleted automatically, because a scan can shrink temporarily (for example, when embeddings are unavailable).

Scope vocabulary: the UI never shows "global". A group.scope: "global" group (the existing global run over entries with project_id IS NULL) is labelled Shared (no project).

Open questions

Tests

Run on e5eb4512:

  • pnpm install, pnpm run typecheck, pnpm run lint (warnings only, exit 0), pnpm run format:check: pass
  • pnpm test: the only failures are the known sandbox cases (.lore.md mtime, git URL rewrite). Both fail the same way on clean origin/main (fde28620).
  • pnpm run build, pnpm --filter @loreai/core build, pnpm --filter @loreai/ui build, pnpm --filter @loreai/gateway run bundle: pass
  • pnpm --filter @loreai/ui test: 47 files, 760 tests passed
  • node scripts/ui-deep-link-smoke.mjs: pass
  • pnpm --filter @loreai/ui test:e2e: 189 passed, 11 skipped (the skips are the existing viewport-specific ones)

New or extended tests:

  • gateway: test/api.test.ts (additive candidate fields on the real project and shared paths); test/ui-contracts.test.ts, which writes the dedup-preview.json fixture
  • ui: test/duplicate-review.test.tsx, test/dedup-review.test.ts, test/db.test.ts (v3 → v4 upgrade), test/api-client.test.ts, test/contracts.test.ts, test/href.test.ts, test/shell.test.tsx
  • e2e: e2e/dedup-review.spec.ts. e2e/db-migration.spec.ts, all-knowledge.spec.ts and nav-projects.spec.ts were updated only because the seed data changed: one new store, two shared rows, one new project.

Definition of done

  • A user can review duplicate candidates for one project, side by side, with ids, scores, reasons, scope, versions and sources.
  • Accept/skip/keeper marks, stored locally in IndexedDB, with staleness and orphan handling.
  • Nothing is applied: e2e records every request (only GETs plus the preview POST) and checks the knowledge list is unchanged.
  • Contract tests for the typed dry-run shape (dedup-preview.json fixture plus the ArkType contract).
  • Playwright coverage, desktop and mobile (e2e/dedup-review.spec.ts).
  • Hostile titles and content render as inert text.

Screenshots

Light Dark
Desktop desktop light desktop dark
Mobile mobile light mobile dark

Link to Devin session: https://app.devin.ai/sessions/36dfe06b1fff4f36932bc410ca42a2cc
Open in Devin Desktop: https://app.devin.ai/desktop/session/36dfe06b1fff4f36932bc410ca42a2cc?variant=devin
Requested by: @BYK

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Results 📊

✅ Patch coverage is 100.00% (4 of 4 changed executable lines covered; target 80%).
Project statement coverage is 85.76% (up 0.01 percentage points from base (1b91457) to head (986fd25)).

Changed files with executable lines (1)
File Patch coverage Changed executable lines
packages/ui/src/contracts/dedup.ts 100.00% 4/4 covered
Coverage diff
@@            Coverage Diff             @@
##          main     #1983       +/-##
==========================================
+ Coverage    85.75%    85.76%    +0.01%
==========================================
  Files          331       332        +1
  Tracked lines     51485     51489        +4
  Branches     42477     42481        +4
==========================================
+ Hits         44151     44159        +8
+ Misses        7334      7330        -4
  Partials      4447      4447         —

Generated by Coverage Action

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Manual browser verification on e5eb4512, run against the real bundled gateway with the e2e seed (node packages/ui/e2e/gateway.mjs) in Chromium at desktop width and 390px mobile, light and dark themes set via the theme toggle.

  • Entry points: the project page's "Review duplicates" link and the sidebar's "Find duplicates" both open /ui/projects/:id/duplicates under /ui. The sidebar item is highlighted as active; with no project selected it reads "choose a project".
  • Evidence: both seeded groups render.
    • Group labels are Project and Shared (no project), with "NN% match", reasons, and status.
    • Each candidate column shows full content, version, Match %, Confidence %, category, scope, source session, updated time, short id, a document link, and "Keep this one".
    • "global" never appears on the page.
  • Shared candidates: document links go to /ui/knowledge/:id. A source session with no project is shown as plain text, not a link.
  • Hostile content: <img src=x onerror=…> in a title and in content shows as literal text. No img element is injected and window.__pwned stays unset.
  • Marks:
    • a, s and u update the counts and survive a reload.
    • IndexedDB lore-ui is at v4, and its reviewDecisions store holds the decision, keeper and expected revisions.
    • Changing the keeper with 2 on an accepted group persists.
    • Choosing a keeper on an unmarked group is not saved until you Accept or Skip. That is intended: a mark is only recorded once there is a decision.
  • Shortcut guards: typing "a" in the header search box, or pressing keys while a sidebar link has focus, does not trigger review shortcuts.
  • No mutation: passive capture of every request showed 59 GETs and 12 preview POST /dedup calls, and no /dedup/apply. The project's knowledge count stays at 2 after marking and after a fresh reload.
  • States: Rescan shows "scanning again". A reload under added latency shows "Scanning for duplicates" first. On mobile the columns stack, actions stay reachable, and there is no horizontal overflow.
  • Not exercised manually: stale and orphaned marks, IndexedDB write failure, and backend errors. Jsdom tests in test/duplicate-review.test.tsx cover these.
Light Dark
Desktop desktop light desktop dark
Mobile mobile light mobile dark

Shared-group evidence:
shared candidate

Written by Devin

BYK and others added 2 commits October 5, 2026 08:21
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1791146816-mem-01-dedup-review branch from e5eb451 to 986fd25 Compare October 5, 2026 08:24
Comment thread packages/ui/src/components/lore/DuplicateReview.tsx
@BYK
BYK merged commit 34a4d85 into main Oct 5, 2026
34 of 36 checks passed
@BYK
BYK deleted the devin/1791146816-mem-01-dedup-review branch October 5, 2026 08:38
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-05 08:39 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MEM-01: Project-scoped duplicate preview and pair/group review (read-only)

1 participant