-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Context
The new practice_finding entity is @Immutable (append-only, Hibernate enforced). Contributors cannot mark findings as applied, dismissed, or disputed. The legacy system had userState (FIXED/WONT_FIX/WRONG) on PullRequestBadPractice plus a separate BadPracticeFeedback table — neither is connected to the new practice model.
Without feedback, we cannot answer RQ2 ("did users act on guidance?") or RQ4 ("do users find the guidance helpful?"). The dashboard (#897 v2) needs this for engagement metrics.
Audit Findings (applied)
- Must be @immutable — For research data integrity, feedback must be append-only. A contributor submitting a second reaction creates a new row, not an upsert. This preserves the temporal record of when a contributor first saw and then changed their mind about a finding. The original issue proposed
UNIQUE (finding_id, user_id)with upsert — this is wrong for research. - 3 actions, not 4 — DISMISSED vs WONT_FIX is ambiguous. User testing will show confusion. Start with: APPLIED ("I fixed this"), DISPUTED ("The AI is wrong"), NOT_APPLICABLE ("Not relevant to my case" — aligns with existing
Verdict.NOT_APPLICABLEenum). Add WONT_FIX only if user testing reveals a gap. - Explicitly excluded from agent context — Injecting "contributor disputed this finding" into the prompt (feat(application-server): inject contributor practice history into agent context #895) creates a feedback loop that contaminates AI accuracy measurement. Feedback is for research analysis and dashboard display only, not for the agent.
- Industry validation — CodeRabbit found that emoji-based feedback (thumbs up/down) was too coarse but structured taxonomy works well for aggregate analysis (source). Keep the taxonomy small (3 actions), make it append-only.
- Denormalize for research queries — Store
practiceSlugandverdicton the feedback entity to enable efficient research queries without joining back topractice_finding.
Schema
CREATE TABLE finding_feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id UUID NOT NULL REFERENCES practice_finding(id),
contributor_id BIGINT NOT NULL REFERENCES "user"(id),
action VARCHAR(16) NOT NULL CHECK (action IN ('APPLIED', 'DISPUTED', 'NOT_APPLICABLE')),
explanation TEXT,
-- Denormalized for research queries
practice_slug VARCHAR(64) NOT NULL,
finding_verdict VARCHAR(16) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_finding_feedback_finding ON finding_feedback(finding_id);
CREATE INDEX idx_finding_feedback_contributor ON finding_feedback(contributor_id, created_at DESC);No UNIQUE constraint on (finding_id, contributor_id) — append-only. Multiple feedback events for the same finding are valid (contributor changes mind over time). The latest feedback per finding is the "current" state for dashboard display (query: ORDER BY created_at DESC LIMIT 1).
Action semantics
| Action | Meaning | Maps to legacy | Research signal |
|---|---|---|---|
| APPLIED | "I fixed/will fix this" | FIXED | User acted on guidance (RQ2) |
| DISPUTED | "The AI is wrong" | WRONG | Precision feedback, detection quality (RQ1, RQ4) |
| NOT_APPLICABLE | "Valid observation but not relevant to my context" | — | Scope tuning signal (RQ4) |
No DISMISSED — "I saw it but don't care" is the absence of feedback, not a feedback type. It adds noise without research signal.
No ACKNOWLEDGED — same reasoning. Non-action is captured by the lack of a feedback row.
No WONT_FIX — overlaps too much with NOT_APPLICABLE. Revisit after user testing.
explanation field
- Optional for APPLIED, required for DISPUTED (to feed detection quality improvement)
- Free text, max 2000 chars
- DISPUTED explanations are valuable for tuning detection prompts
Scope
Entity
@Entity
@Immutable // Append-only for research integrity
@Table(name = "finding_feedback")
public class FindingFeedback {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "finding_id", nullable = false)
private UUID findingId; // Raw UUID, no @ManyToOne (same pattern as PracticeFinding.agentJobId)
@Column(name = "contributor_id", nullable = false)
private Long contributorId;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 16)
private FindingFeedbackAction action;
@Column(columnDefinition = "TEXT")
private String explanation;
// Denormalized
@Column(name = "practice_slug", nullable = false, length = 64)
private String practiceSlug;
@Column(name = "finding_verdict", nullable = false, length = 16)
private String findingVerdict;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
}Enum
public enum FindingFeedbackAction {
APPLIED,
DISPUTED,
NOT_APPLICABLE
}Repository
public interface FindingFeedbackRepository extends JpaRepository<FindingFeedback, UUID> {
// Latest feedback per finding for dashboard display
@Query(nativeQuery = true, value = """
SELECT DISTINCT ON (finding_id) *
FROM finding_feedback
WHERE finding_id IN :findingIds
ORDER BY finding_id, created_at DESC
""")
List<FindingFeedback> findLatestByFindingIds(@Param("findingIds") Collection<UUID> findingIds);
// Engagement stats for contributor
@Query("""
SELECT ff.action AS action, COUNT(*) AS count
FROM FindingFeedback ff
WHERE ff.contributorId = :contributorId
GROUP BY ff.action
""")
List<Object[]> countByContributorGroupByAction(@Param("contributorId") Long contributorId);
}Service + Controller
-
FindingFeedbackService.java:- Validate finding exists
- Validate
contributor == finding.contributorId(contributor-only access) - Populate denormalized fields from the finding
- Persist (insert, not upsert)
- For DISPUTED: optionally route to Langfuse (reuse pattern from
BadPracticeFeedbackService)
-
FindingFeedbackController.java:POST /api/practices/findings/{findingId}/feedback— submit feedback{action, explanation?}GET /api/practices/findings/{findingId}/feedback— latest feedback for a findingGET /api/practices/findings/me/engagement— engagement stats (action counts)
Integration with #896
Include latest FindingFeedbackDTO in PracticeFindingListDTO and PracticeFindingDetailDTO responses (nullable). This enables the #897 v2 follow-up to show feedback state on practice cards without additional API calls.
Files
| File | Change |
|---|---|
practices/feedback/FindingFeedback.java |
NEW — @immutable entity |
practices/feedback/FindingFeedbackAction.java |
NEW — enum (3 values) |
practices/feedback/FindingFeedbackRepository.java |
NEW — JPA repo with DISTINCT ON query |
practices/feedback/FindingFeedbackService.java |
NEW — service with contributor auth |
practices/feedback/FindingFeedbackController.java |
NEW — REST controller |
practices/feedback/dto/FindingFeedbackDTO.java |
NEW — response DTO |
practices/feedback/dto/CreateFindingFeedbackDTO.java |
NEW — request DTO |
practices/finding/dto/PracticeFindingListDTO.java |
MODIFY — add nullable feedback field |
| Liquibase migration | NEW — finding_feedback table + indexes |
Research Design Decision
Finding feedback is explicitly excluded from agent context (#895). The agent must NOT know whether a contributor previously disputed a finding. Reasons:
- Contaminates AI accuracy measurement (the agent may soften future assessments to avoid disputes)
- Creates a perverse incentive (dispute everything → agent stops flagging)
- First establish baseline detection quality, then decide whether to close the feedback loop
Verification
- POST feedback: APPLIED, DISPUTED, NOT_APPLICABLE all work
- Append-only: second feedback for same finding creates new row (not upsert)
- Only the finding's contributor can submit feedback (403 for others)
- DISPUTED requires
explanationfield (400 without it) -
GET engagementreturns correct action counts -
PracticeFindingListDTOincludes latest feedback when present - DISPUTED feedback with explanation routes to Langfuse (if configured)
- Denormalized
practiceSlugandfindingVerdictpopulated correctly
Dependencies
- No blockers — can start immediately
- Consumed by: feat(application-server,webapp): My Practices contributor dashboard #897 v2 (feedback buttons on dashboard), research analysis