ReviewOS

stacks/bench-review

legal: anonymous reviews (clerk/attorney retaliation protection)

#36
Closed glennmichael123 opened this 9 days ago ·
9 days ago

For bench-review's audience — clerks, junior associates, court staff — reviewing your own bench under your real name carries real retaliation risk. Yelp's "identity attached" model doesn't fit this domain. Without anonymous reviews, the people best positioned to write the most insightful reviews are exactly the ones who can't safely write them.

Scope

  • New column: judge_reviews.anonymized: boolean default false.
  • New column: users.role_label: enum — "attorney" / "clerk" / "court_staff" / "litigant" / "observer" (self-declared on signup; eventually tied to credential verification — see sibling issue).
  • Submit-review UI: toggle "Post anonymously" + dropdown "I am writing this as a(n) ..." (defaults to whatever's on users.role_label).
  • Render rules:
    • Public read endpoints (/api/reviews, /api/judges/{id}/reviews, /api/court-houses/{id}/reviews): replace author name with "Anonymous {role_label}" when anonymized=true.
    • /article/{id}: same.
    • /user/{author_id}: hide anonymized reviews from the public profile.
    • /me/reviews: author always sees their own reviews with full identity.
    • /admin/reviews: admin always sees the real author for moderation (audit trail integrity).

Acceptance

  • Author can mark a review anonymous at submit time and at edit time.
  • Public surfaces show "Anonymous attorney" / "Anonymous clerk" instead of the author's name.
  • The reviewer's identity is never exposed in any public API response (check the JSON payloads, not just the rendered HTML).
  • Admins can still attribute the review for moderation purposes.
  • A user's profile page doesn't list their anonymous reviews.

Why this matters specifically here

Multiple state and federal bar opinions warn attorneys against publicly criticizing the bench by name. Anonymous reviews aren't a polish feature — they're the only way the trustworthy reviewers will participate. Without this, the platform is functionally read-only for the credibility-bearing audience.

9 days ago

Shipped — the security boundary + the submit flow. Edit-page toggle is a small follow-up.

Schema — database/migrations/0000000110-add-anonymized-and-role-label.sql:

  • judge_reviews.anonymized INTEGER NOT NULL DEFAULT 0 — flag the public render path keys off.
  • users.role_label TEXT NULL — self-declared classification (attorney / clerk / court_staff / litigant / observer). NULL until set in settings; anonymous reviewers without a role surface as plain "Anonymous reviewer".
  • Index on anonymized for the admin queries that segment by it.

Server:

  • app/Helpers/reviewerLabel.ts (new) — publicReviewerFor(review, user) is the single source of truth for "what name does the public see for this review's reviewer". Returns id: null for anonymous reviews so anonymized reviewer cards CAN'T link back to /user/{id} by construction. Belt-and-suspenders: id null + name override.
  • SubmitReviewAction + UpdateMyReviewAction accept anonymized from the request (boolean OR string "true"/"1" for serializer tolerance). Persists to the row; the author's true user_id link stays intact for moderation.
  • ShowReviewAction returns the gated author payload via publicReviewerFor — viewer-is-author short-circuit so the author still sees their own identity on their own published article.
  • UserShowAction aggregate counts EXCLUDE anonymized reviews — your public profile's "Reviews / Judges Reviewed / Avg Rating / Total Likes" reflect only non-anonymous activity.
  • UserReviewsAction listing filters anonymized = 0 — third parties can't enumerate the author's anonymous reviews via the public profile.
  • Admin/ReviewIndexAction is unchanged — admins see the real user: { id, name, email } regardless of the flag; moderation requires identity.

UI:

  • ReviewForm.stx has the anonymity toggle above the action bar with explanatory copy ("Your name won't appear publicly — readers see 'Anonymous reviewer' or your role, if set in settings. A moderator still sees your identity for review.").
  • Default is OFF — anonymity is an explicit opt-in, not the default privacy posture.
  • ArticleView.stx renders the reviewer line in three modes: linked (real user → "by Name" with /user/:id click-through), plain (anonymous → "by Anonymous attorney" with no link), hidden (factory/seeded with no author).

Deferred (small):

  • ArticleEdit.stx toggle — authors can change the flag at submit time, but the inline edit page doesn't expose the toggle yet. ~10 min follow-up; meanwhile authors can edit the field via re-submission or by hitting PATCH /api/me/reviews/{id} directly.
  • LatestReviewsAction / ReviewsByJudgeAction — neither returns real author info today (the frontend uses a PLACEHOLDER_AUTHOR constant for feed cards). When those feeds DO start returning real authors, run them through publicReviewerFor like ShowReviewAction does.
  • users.role_label is in the schema but the settings UI for picking it isn't wired. Anonymous reviewers without a role surface as plain "Anonymous reviewer" until the settings UI lands.
  • Email notifications for moderation events (#approved/#rejected) — current copy uses the real reviewer name in the salutation. That's fine because the email goes to the reviewer themselves. No leak.

Why the schema flag is "anonymize the display" rather than "remove the link": deleting the user_id would break ownership (the author couldn't edit/delete their own review), break the moderation audit trail, and make abuse harder to spot when an account starts spamming under anonymity. The link stays in DB; only render paths gate it.

Sign in to comment on this issue.