Skip to content

feat(dismiss): remember dismissed findings across review runs (#59) - #509

Open
nitishagar wants to merge 2 commits into
alibaba:mainfrom
nitishagar:feat/issue-59-dismissed-suggestion-memory
Open

feat(dismiss): remember dismissed findings across review runs (#59)#509
nitishagar wants to merge 2 commits into
alibaba:mainfrom
nitishagar:feat/issue-59-dismissed-suggestion-memory

Conversation

@nitishagar

Copy link
Copy Markdown
Contributor

Summary

Closes #59.

Adds a per-repo, filesystem-based dismissal store that remembers findings a user dismissed, so identical recurring false positives stop resurfacing on every review run. Aligned with the maintainer's stated "filesystem-based, still in design" direction.

  • New internal/session/dismissal.go: DismissalStore (load/save/record/remove/contains), DismissalFingerprint (SHA256 of path + line range + normalized content), DismissalFilter (read-side filter on the returned comment slice), and LoadComments (walks a session JSONL for the flattened comment list).
  • internal/agent/agent.go: one new optional Args.Dismissals field + one conditional Suppress call on the success path of Agent.Run. Collectors, session JSONL, and resume index are untouched.
  • New ocr dismiss subcommand (cmd/opencodereview/dismiss_cmd.go): add <session-id> <index|path:line>, list, remove <fingerprint|index>.

Design (key invariants)

  • Identity = exact-content fingerprint, not semantic. Rephrased findings will resurface — this is a documented limitation; semantic identity is deferred as future work. The exact-content match covers the dominant case in the issue (identical false positives recurring each run).
  • Opt-in = file existence. No ocr review flag is added; if ~/.opencodereview/dismissals/<encoded-repo>.json exists and parses, the review suppresses matches; otherwise behavior is byte-identical to today (one stat, no read).
  • Read-side filter, non-destructive. Suppression filters the returned []LlmComment after collection; the collector's raw output and the session JSONL are never mutated.
  • Fail-safe on corrupt input. A corrupt store yields an empty store + a stderr warning and is left untouched; the review proceeds stateless. Saving is atomic (temp + rename) at mode 0600.

Schema is version: 1 so future semantic-identity changes are additive, not breaking.

How to test

# 1. Run a review; note a finding you want to dismiss and its session id.
ocr review --from main --to feature
ocr session list

# 2. Dismiss the finding by 0-based index (or path:line).
ocr dismiss add <session-id> 0

# 3. Re-run the review — the dismissed finding no longer appears.
ocr review --from main --to feature

# 4. Manage dismissals.
ocr dismiss list
ocr dismiss remove <fingerprint|index>

Checklist

  • make build succeeds
  • make test green (all packages, -race)
  • make coverage ≥ 80% (81.9% total; new dismissal.go core functions at 87–100%)
  • go vet ./... and go fmt ./... clean
  • Tests cover the spec invariants (D1–D6) and acceptance scenarios (AS1–AS8), including corrupt-store fail-safe, idempotent record, input-slice immutability, nil-filter no-op, and an end-to-end Agent.Run suppression test over a real git repo

Out of scope (deliberately)

  • Semantic/embedding-based identity (rephrased findings resurface) — future work requiring an LLM/service.
  • No new ocr review flag (the file's existence is the opt-in).
  • No mutation of the comment collector or session JSONL (suppression is strictly a read-side filter).
  • No GitHub-Action-side changes (CLI-side cross-run memory only).

A validated plan bundle (PLAN / IMPLICIT_SPEC / PLAN_VALIDATION / IMPLEMENTATION_VALIDATION) for this change lives under `thoughts/shared/plans/2026-07-25-issue-59-dismissed-suggestion-memory/`.

…a#59)

Add a per-repo, filesystem-based dismissal store
(~/.opencodereview/dismissals/<encoded-repo>.json) that records
SHA-256 fingerprints of findings a user dismissed. A review whose
store exists automatically suppresses dismissed findings from its
output via a read-side filter applied to the returned comment slice;
a review with no store behaves byte-identically to today.

The fingerprint is SHA256(path + start_line + end_line +
normalizedContent), where normalization is TrimSpace + ToLower.
Identity is exact-content by design (documented limitation: rephrased
findings resurface); semantic identity is deferred as future work.

New ocr dismiss subcommand manages the store:
- add <session-id> <index|path:line>  record a dismissal
- list                                 show recorded dismissals
- remove <fingerprint|index>          remove a dismissal

Loading is fail-safe: a missing file is the no-opt-in case (one stat,
no read); a corrupt/unreadable file yields an empty store plus a
warning and is left untouched. Saving is atomic via temp+rename at
mode 0600.

Storage lives in internal/session/dismissal.go (DismissalStore,
DismissalFingerprint, DismissalFilter, LoadComments). The filter is
wired into Agent.Run on the success path only (err == nil), so a
failed review's partial output is never masked. No existing session
JSONL, resume index, or collector output is mutated.

The new session.LoadComments helper walks a session JSONL and returns
the flattened comment list (existing LoadDetail exposes only counts),
which dismiss add uses to resolve findings by index or path:line.
@CLAassistant

CLAassistant commented Jul 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 4 issue(s) in this PR.

  • ✅ Successfully posted inline: 4 comment(s)

Comment thread cmd/opencodereview/review_cmd.go Outdated
Comment thread cmd/opencodereview/dismiss_cmd.go
Comment thread cmd/opencodereview/dismiss_cmd.go Outdated
Comment thread cmd/opencodereview/dismiss_cmd.go Outdated
Four fixes from the automated review on alibaba#509:

1. runDismissList: guard against a nil store. LoadDismissals returns
   (nil, err) when the store path cannot be resolved (HOME unset);
   the previous code printed a warning then dereferenced store.List(),
   panicking. Return an error on a nil store instead.

2. loadDismissalFilter: drop the explicit os.Stat before LoadDismissals.
   ReadFile inside LoadDismissals already performs the existence check,
   so the stat was redundant and opened a stat->read TOCTOU window in
   which a deleted file returned an empty filter instead of nil. Rely
   on LoadDismissals and return nil for an empty/missing store to keep
   the "nil filter == stateless" invariant (D2).

3. resolveRemoveRef: when a short-fingerprint prefix matches more than
   one entry, report the ambiguity explicitly rather than falling
   through to a generic "no match" error (consistent with
   resolveFindingRef).

4. printDismissTable: stop double-truncating ContentPreview, which is
   already capped at store time.

Adds regression tests for the nil-store panic guard and the ambiguous
prefix report.
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.

PR-aware reviews

2 participants