feat(auditor): waive_finding verb + findings queue panel

Wire the long-unwired mark_waived repo method to a new auditor-only
flow verb waive_finding, severity-scoped to minor/nit (blocker/major
must be fixed, never waived), requiring a note, with a task.finding_waived
audit event and no task status change. Add the verb to the IntentSpec
table (auto-derived into the auditor manifest), the flow_auditor route,
and the flow_server MCP tool.

Surface open review findings (cross-task, blocking-first) on the
auditor dashboard via ReviewFindingsRepository.list_open_findings and a
new findings field on AuditorDashboard. Restore the panel's 4-card
auditor layout with a new read-only FindingsQueuePanel as the 4th card.
This commit is contained in:
Renn F
2026-07-14 08:56:55 +02:00
committed by Renzo F
parent 62e19ea729
commit 05a83f45cb
18 changed files with 700 additions and 8 deletions
@@ -10,6 +10,7 @@ import {
import { QualityMetricsPanel } from "./quality-metrics-panel";
import { FlaggedItemsPanel } from "./flagged-items-panel";
import { ReportsPanel } from "./reports-panel";
import { FindingsQueuePanel } from "./findings-queue-panel";
import { Button } from "@/components/ui/button";
import { FileText } from "lucide-react";
import { toast } from "sonner";
@@ -73,11 +74,17 @@ export function AuditorDashboard() {
</div>
</div>
{/* Quality Metrics */}
<QualityMetricsPanel
metrics={dashboard?.metrics}
isLoading={loadingDashboard}
/>
{/* Top Row: Open Findings + Quality Metrics */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 gap-6">
<FindingsQueuePanel
findings={dashboard?.findings}
isLoading={loadingDashboard}
/>
<QualityMetricsPanel
metrics={dashboard?.metrics}
isLoading={loadingDashboard}
/>
</div>
{/* Bottom Row: Flagged Items + Reports */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 gap-6">
@@ -0,0 +1,120 @@
"use client";
import { AuditorFinding } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ListChecks } from "lucide-react";
import Link from "next/link";
interface FindingsQueuePanelProps {
findings: AuditorFinding[] | undefined;
isLoading: boolean;
}
// Severity stored as the finding's lowercase value (blocker/major/minor/nit).
const severityColors: Record<string, string> = {
blocker: "bg-red-100 text-red-700",
major: "bg-orange-100 text-orange-700",
minor: "bg-yellow-100 text-yellow-700",
nit: "bg-blue-100 text-blue-700",
};
const severityOrder: Record<string, number> = {
blocker: 0,
major: 1,
minor: 2,
nit: 3,
};
export function FindingsQueuePanel({
findings,
isLoading,
}: FindingsQueuePanelProps) {
// The API already returns blocking-first, but keep the sort stable client-side
// in case a later re-fetch reorders.
const sorted = [...(findings ?? [])].sort((a, b) => {
const diff = (severityOrder[a.severity] ?? 9) - (severityOrder[b.severity] ?? 9);
if (diff !== 0) return diff;
return (b.created_at ?? "").localeCompare(a.created_at ?? "");
});
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CardTitle className="text-lg flex items-center gap-2">
<ListChecks className="h-5 w-5" />
Open Findings
</CardTitle>
{sorted.length > 0 && (
<Badge variant="destructive">{sorted.length}</Badge>
)}
</div>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-24" />
))}
</div>
) : sorted.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm">
<ListChecks className="h-8 w-8 mx-auto mb-2 opacity-50" />
No open review findings
</div>
) : (
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-3">
{sorted.map((finding) => (
<div
key={finding.id}
className="p-4 rounded-lg border bg-muted/50"
>
<div className="flex items-center gap-2 mb-1 flex-wrap">
<Badge
className={
(severityColors[finding.severity] ?? "") + " text-xs"
}
>
{finding.severity}
</Badge>
<Badge variant="outline" className="text-xs">
{finding.origin}
</Badge>
<span className="text-xs text-muted-foreground">
round {finding.round}
</span>
</div>
<p className="text-sm text-muted-foreground mb-2">
{finding.actual ?? finding.expected ?? finding.criterion ?? "—"}
</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{finding.file && (
<span className="font-mono">
{finding.file}
{finding.line ? `:${finding.line}` : ""}
</span>
)}
<Link
href={"/tasks/" + finding.task_id}
prefetch={false}
>
<span className="text-primary hover:underline">
Task #{finding.task_id.slice(0, 8)}
</span>
</Link>
</div>
</div>
))}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
);
}
+1
View File
@@ -1187,6 +1187,7 @@ export const mockAuditorDashboard = {
},
],
recent_reports: mockAuditorReports,
findings: [],
};
// Mock mode: dev = mock, production = real backend
+15
View File
@@ -611,6 +611,20 @@ export interface AuditorReport {
sent_at: string | null;
}
export interface AuditorFinding {
id: string;
task_id: string;
origin: string;
severity: string;
file: string | null;
line: number | null;
criterion: string | null;
expected: string | null;
actual: string | null;
round: number;
created_at: string | null;
}
export interface AuditorDashboard {
flagged_items: AuditorFlag[];
metrics: Record<string, number>;
@@ -621,6 +635,7 @@ export interface AuditorDashboard {
team: string | null;
}>;
recent_reports: AuditorReport[];
findings: AuditorFinding[];
}
export interface TeamHealth {