mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Add verifications catalog page and per-run verifications tab
Verifications catalog (/verifications) shows all verification criteria organized by category with search, mode filter, and grouped/list toggle. Each criterion has a unique icon, type badge, mode badge, and evaluation sparkline. List view adds F1 accuracy, pass@1, and category columns. Per-run verifications tab (/runs/:id/verifications) shows the same criteria with pass/fail status for a specific run. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3d46c3885
commit
706413ec65
5 changed files with 757 additions and 0 deletions
216
apps/arc-web/app/data/verifications.ts
Normal file
216
apps/arc-web/app/data/verifications.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
export type VerificationStatus = "pass" | "fail" | "na";
|
||||
|
||||
export type VerificationType = "ai" | "automated" | "analysis" | "ai-analysis";
|
||||
|
||||
export interface Criterion {
|
||||
name: string;
|
||||
description: string;
|
||||
type: VerificationType | null;
|
||||
status: VerificationStatus;
|
||||
}
|
||||
|
||||
export interface VerificationCategory {
|
||||
name: string;
|
||||
question: string;
|
||||
status: VerificationStatus;
|
||||
criteria: Criterion[];
|
||||
}
|
||||
|
||||
export const statusConfig = {
|
||||
pass: {
|
||||
label: "Pass",
|
||||
color: "text-mint",
|
||||
bg: "bg-mint/15",
|
||||
dot: "bg-mint",
|
||||
border: "border-l-mint/50",
|
||||
},
|
||||
fail: {
|
||||
label: "Fail",
|
||||
color: "text-coral",
|
||||
bg: "bg-coral/15",
|
||||
dot: "bg-coral",
|
||||
border: "border-l-coral/50",
|
||||
},
|
||||
na: {
|
||||
label: "N/A",
|
||||
color: "text-navy-600",
|
||||
bg: "bg-white/[0.04]",
|
||||
dot: "bg-navy-600",
|
||||
border: "border-l-navy-600/50",
|
||||
},
|
||||
} as const satisfies Record<
|
||||
VerificationStatus,
|
||||
{ label: string; color: string; bg: string; dot: string; border: string }
|
||||
>;
|
||||
|
||||
export const typeConfig = {
|
||||
ai: { label: "AI", color: "text-teal-300", bg: "bg-teal-500/10" },
|
||||
automated: { label: "Automated", color: "text-mint", bg: "bg-mint/10" },
|
||||
analysis: { label: "Analysis", color: "text-amber", bg: "bg-amber/10" },
|
||||
"ai-analysis": { label: "AI + Analysis", color: "text-teal-300", bg: "bg-teal-500/10" },
|
||||
} as const satisfies Record<
|
||||
VerificationType,
|
||||
{ label: string; color: string; bg: string }
|
||||
>;
|
||||
|
||||
export const verificationCategories: VerificationCategory[] = [
|
||||
{
|
||||
name: "Traceability",
|
||||
question: "Do we understand what this change is and why we're making it?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Motivation", description: "Origin of proposal identified", type: "ai", status: "pass" },
|
||||
{ name: "Specifications", description: "Requirements written down", type: "ai", status: "pass" },
|
||||
{ name: "Documentation", description: "Developer and user docs added", type: "ai", status: "pass" },
|
||||
{ name: "Minimization", description: "No extraneous changes", type: "ai", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Readability",
|
||||
question: "Can a human or agent quickly read this and understand what it does?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Formatting", description: "Code layout matches standard", type: "automated", status: "pass" },
|
||||
{ name: "Linting", description: "Linter issues resolved", type: "automated", status: "pass" },
|
||||
{ name: "Style", description: "House style applied", type: "ai", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Reliability",
|
||||
question: "Will this behave correctly and safely under real-world conditions and failures?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Completeness", description: "Implementation covers requirements", type: "ai", status: "pass" },
|
||||
{ name: "Defects", description: "Potential or likely bugs remediated", type: "ai-analysis", status: "pass" },
|
||||
{ name: "Performance", description: "Hot path impact identified", type: "ai", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Code Coverage",
|
||||
question: "Do we have trustworthy, automated evidence that it works and won't regress?",
|
||||
status: "fail",
|
||||
criteria: [
|
||||
{ name: "Test Coverage", description: "Production code exercised by unit tests", type: "analysis", status: "pass" },
|
||||
{ name: "Test Quality", description: "Tests are robust and clear", type: "ai", status: "fail" },
|
||||
{ name: "E2E Coverage", description: "Browser automation exercises UX", type: "analysis", status: "na" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Maintainability",
|
||||
question: "Will this be easy to modify or extend later without creating new risk?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Architecture", description: "Layering and dependency graph meets design", type: "analysis", status: "pass" },
|
||||
{ name: "Interfaces", description: "", type: null, status: "pass" },
|
||||
{ name: "Duplication", description: "Similar and identical code blocks identified", type: "analysis", status: "pass" },
|
||||
{ name: "Simplicity", description: "Extra review for reducing complexity", type: "ai", status: "pass" },
|
||||
{ name: "Dead Code", description: "Unexecuted code and dependencies removed", type: "analysis", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
question: "Does this preserve or improve our security posture and avoid vulnerabilities?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Vulnerabilities", description: "Security issues are remediated", type: "ai-analysis", status: "pass" },
|
||||
{ name: "IaC Scanning", description: "", type: null, status: "pass" },
|
||||
{ name: "Dependency Alerts", description: "Known CVEs are patched", type: "analysis", status: "pass" },
|
||||
{ name: "Security Controls", description: "Organization standards applied", type: "ai", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Deployability",
|
||||
question: "Is this changeset safe to ship to production immediately?",
|
||||
status: "fail",
|
||||
criteria: [
|
||||
{ name: "Compatibility", description: "Breaking changes are avoided", type: "analysis", status: "pass" },
|
||||
{ name: "Rollout / Rollback", description: "Known rollback plan if deploy fails", type: "ai", status: "fail" },
|
||||
{ name: "Observability", description: "Logging, metrics, tracing instrumented", type: "ai", status: "fail" },
|
||||
{ name: "Cost", description: "Tech ops costs estimated", type: "analysis", status: "pass" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Compliance",
|
||||
question: "Does this meet our regulatory, contractual, and policy obligations?",
|
||||
status: "pass",
|
||||
criteria: [
|
||||
{ name: "Change Control", description: "Separation of Duties policy met", type: "analysis", status: "pass" },
|
||||
{ name: "AI Governance", description: "AI involvement was acceptable", type: "analysis", status: "pass" },
|
||||
{ name: "Privacy", description: "PII is identified and handled to standards", type: "ai", status: "pass" },
|
||||
{ name: "Accessibility", description: "Software meets accessibility requirements", type: "analysis", status: "pass" },
|
||||
{ name: "Licensing", description: "Supply chain meets IP policy", type: "analysis", status: "pass" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export type EvaluationResult = "pass" | "fail" | "skip";
|
||||
|
||||
export type VerificationMode = "active" | "evaluate" | "disabled";
|
||||
|
||||
export interface CriterionPerformance {
|
||||
f1: number | null;
|
||||
passAt1: number | null;
|
||||
mode: VerificationMode;
|
||||
evaluations: EvaluationResult[];
|
||||
}
|
||||
|
||||
export const modeConfig = {
|
||||
active: { label: "Active", color: "text-mint", bg: "bg-mint/10" },
|
||||
evaluate: { label: "Evaluate", color: "text-amber", bg: "bg-amber/10" },
|
||||
disabled: { label: "Disabled", color: "text-navy-600", bg: "bg-white/[0.04]" },
|
||||
} as const satisfies Record<
|
||||
VerificationMode,
|
||||
{ label: string; color: string; bg: string }
|
||||
>;
|
||||
|
||||
export const criterionPerformance: Record<string, CriterionPerformance> = {
|
||||
"Motivation": { f1: 0.87, passAt1: 0.82, mode: "active", evaluations: ["pass","pass","fail","pass","pass","pass","pass","fail","pass","pass"] },
|
||||
"Specifications": { f1: 0.83, passAt1: 0.78, mode: "active", evaluations: ["pass","fail","pass","pass","pass","fail","pass","pass","pass","pass"] },
|
||||
"Documentation": { f1: 0.79, passAt1: 0.74, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","fail","pass","pass","fail"] },
|
||||
"Minimization": { f1: 0.72, passAt1: 0.68, mode: "evaluate", evaluations: ["pass","fail","pass","fail","pass","pass","fail","pass","pass","pass"] },
|
||||
"Formatting": { f1: 0.99, passAt1: 0.98, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","pass"] },
|
||||
"Linting": { f1: 0.98, passAt1: 0.97, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","fail","pass"] },
|
||||
"Style": { f1: 0.81, passAt1: 0.76, mode: "active", evaluations: ["pass","fail","pass","pass","pass","pass","fail","pass","pass","pass"] },
|
||||
"Completeness": { f1: 0.76, passAt1: 0.71, mode: "active", evaluations: ["pass","pass","fail","pass","fail","pass","pass","pass","fail","pass"] },
|
||||
"Defects": { f1: 0.84, passAt1: 0.79, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","fail"] },
|
||||
"Performance": { f1: 0.69, passAt1: 0.63, mode: "evaluate", evaluations: ["fail","pass","pass","fail","pass","fail","pass","pass","fail","pass"] },
|
||||
"Test Coverage": { f1: 0.95, passAt1: 0.93, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","fail","pass","pass","pass"] },
|
||||
"Test Quality": { f1: 0.71, passAt1: 0.65, mode: "evaluate", evaluations: ["pass","fail","fail","pass","pass","fail","pass","fail","pass","pass"] },
|
||||
"E2E Coverage": { f1: 0.91, passAt1: 0.88, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","pass"] },
|
||||
"Architecture": { f1: 0.88, passAt1: 0.84, mode: "active", evaluations: ["pass","pass","pass","pass","fail","pass","pass","pass","pass","pass"] },
|
||||
"Interfaces": { f1: null, passAt1: null, mode: "disabled", evaluations: [] },
|
||||
"Duplication": { f1: 0.96, passAt1: 0.94, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","fail","pass","pass"] },
|
||||
"Simplicity": { f1: 0.74, passAt1: 0.69, mode: "active", evaluations: ["pass","fail","pass","pass","fail","pass","pass","fail","pass","pass"] },
|
||||
"Dead Code": { f1: 0.93, passAt1: 0.90, mode: "active", evaluations: ["pass","pass","pass","pass","pass","fail","pass","pass","pass","pass"] },
|
||||
"Vulnerabilities": { f1: 0.86, passAt1: 0.81, mode: "active", evaluations: ["pass","pass","fail","pass","pass","pass","pass","pass","fail","pass"] },
|
||||
"IaC Scanning": { f1: null, passAt1: null, mode: "disabled", evaluations: [] },
|
||||
"Dependency Alerts": { f1: 0.97, passAt1: 0.95, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","fail"] },
|
||||
"Security Controls": { f1: 0.80, passAt1: 0.75, mode: "active", evaluations: ["pass","pass","fail","pass","pass","fail","pass","pass","pass","pass"] },
|
||||
"Compatibility": { f1: 0.89, passAt1: 0.85, mode: "active", evaluations: ["pass","pass","pass","pass","fail","pass","pass","pass","pass","pass"] },
|
||||
"Rollout / Rollback": { f1: 0.66, passAt1: 0.60, mode: "evaluate", evaluations: ["fail","pass","fail","pass","fail","pass","pass","fail","pass","fail"] },
|
||||
"Observability": { f1: 0.73, passAt1: 0.67, mode: "evaluate", evaluations: ["pass","fail","pass","fail","pass","pass","fail","pass","fail","pass"] },
|
||||
"Cost": { f1: 0.78, passAt1: 0.72, mode: "evaluate", evaluations: ["pass","pass","fail","pass","fail","pass","pass","fail","pass","pass"] },
|
||||
"Change Control": { f1: 0.94, passAt1: 0.91, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","fail","pass"] },
|
||||
"AI Governance": { f1: 0.85, passAt1: 0.80, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","pass"] },
|
||||
"Privacy": { f1: 0.77, passAt1: 0.72, mode: "active", evaluations: ["pass","fail","pass","pass","pass","fail","pass","pass","pass","fail"] },
|
||||
"Accessibility": { f1: 0.90, passAt1: 0.87, mode: "active", evaluations: ["pass","pass","pass","pass","pass","fail","pass","pass","pass","pass"] },
|
||||
"Licensing": { f1: 0.96, passAt1: 0.93, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","pass"] },
|
||||
};
|
||||
|
||||
export function getCategorySummary(categories: readonly VerificationCategory[]) {
|
||||
const passing = categories.filter((c) => c.status === "pass").length;
|
||||
return { passing, total: categories.length };
|
||||
}
|
||||
|
||||
export function getCriteriaSummary(criteria: readonly Criterion[]) {
|
||||
return {
|
||||
passing: criteria.filter((c) => c.status === "pass").length,
|
||||
failing: criteria.filter((c) => c.status === "fail").length,
|
||||
na: criteria.filter((c) => c.status === "na").length,
|
||||
total: criteria.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAllCriteria(categories: readonly VerificationCategory[]) {
|
||||
return categories.flatMap((c) => c.criteria);
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ export default [
|
|||
route("configuration", "routes/run-configuration.tsx"),
|
||||
route("graph", "routes/run-graph.tsx"),
|
||||
route("files", "routes/run-files-changed.tsx"),
|
||||
route("verifications", "routes/run-verifications.tsx"),
|
||||
route("usage", "routes/run-usage.tsx"),
|
||||
route("retro", "routes/run-retro.tsx"),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const tabs = [
|
|||
{ name: "Overview", path: "", count: null },
|
||||
{ name: "Stages", path: "/stages/detect-drift", count: 4 },
|
||||
{ name: "Files Changed", path: "/files", count: 3 },
|
||||
{ name: "Verifications", path: "/verifications", count: null },
|
||||
{ name: "Retro", path: "/retro", count: null },
|
||||
{ name: "Usage", path: "/usage", count: null },
|
||||
];
|
||||
|
|
|
|||
128
apps/arc-web/app/routes/run-verifications.tsx
Normal file
128
apps/arc-web/app/routes/run-verifications.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
MinusCircleIcon,
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
verificationCategories,
|
||||
statusConfig,
|
||||
typeConfig,
|
||||
getCriteriaSummary,
|
||||
} from "../data/verifications";
|
||||
import type {
|
||||
VerificationStatus,
|
||||
VerificationType,
|
||||
VerificationCategory,
|
||||
} from "../data/verifications";
|
||||
|
||||
function StatusIcon({
|
||||
status,
|
||||
className = "size-5",
|
||||
}: {
|
||||
status: VerificationStatus;
|
||||
className?: string;
|
||||
}) {
|
||||
switch (status) {
|
||||
case "pass":
|
||||
return <CheckCircleIcon className={`${className} text-mint`} />;
|
||||
case "fail":
|
||||
return <XCircleIcon className={`${className} text-coral`} />;
|
||||
case "na":
|
||||
return <MinusCircleIcon className={`${className} text-navy-600`} />;
|
||||
}
|
||||
}
|
||||
|
||||
function TypeBadge({ type }: { type: VerificationType | null }) {
|
||||
if (type === null) return null;
|
||||
const config = typeConfig[type];
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.color} ${config.bg}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryCard({ category }: { category: VerificationCategory }) {
|
||||
const criteriaStats = getCriteriaSummary(category.criteria);
|
||||
const applicable = criteriaStats.total - criteriaStats.na;
|
||||
const config = statusConfig[category.status];
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={category.status === "fail"}
|
||||
className={`rounded-md border border-white/[0.06] overflow-hidden border-l-2 ${config.border}`}
|
||||
>
|
||||
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-white/[0.02]">
|
||||
<StatusIcon status={category.status} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="shrink-0 text-sm font-semibold text-white">
|
||||
{category.name}
|
||||
</span>
|
||||
<span className="truncate text-xs text-navy-600">
|
||||
{category.question}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-xs tabular-nums text-navy-600">
|
||||
{criteriaStats.passing}/{applicable}
|
||||
</span>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-navy-600 transition-transform duration-200 group-data-open:rotate-90" />
|
||||
</DisclosureButton>
|
||||
|
||||
<DisclosurePanel
|
||||
transition
|
||||
className="origin-top transition duration-200 ease-out data-closed:-translate-y-1 data-closed:opacity-0"
|
||||
>
|
||||
<div className="border-t border-white/[0.06]">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{category.criteria.map((criterion) => (
|
||||
<tr
|
||||
key={criterion.name}
|
||||
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
|
||||
>
|
||||
<td className="w-8 py-2.5 pl-5 pr-0">
|
||||
<span
|
||||
className={`inline-block size-2 rounded-full ${statusConfig[criterion.status].dot}`}
|
||||
/>
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
|
||||
{criterion.name}
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-navy-600">
|
||||
{criterion.description || (
|
||||
<span className="italic">Not configured</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-3 pr-4 text-right">
|
||||
<TypeBadge type={criterion.type} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RunVerifications() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{verificationCategories.map((category) => (
|
||||
<CategoryCard key={category.name} category={category} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
411
apps/arc-web/app/routes/verifications.tsx
Normal file
411
apps/arc-web/app/routes/verifications.tsx
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
import { useState } from "react";
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/react";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
LightBulbIcon,
|
||||
ClipboardDocumentListIcon,
|
||||
BookOpenIcon,
|
||||
FunnelIcon,
|
||||
Bars3BottomLeftIcon,
|
||||
WrenchIcon,
|
||||
PaintBrushIcon,
|
||||
CheckBadgeIcon,
|
||||
BugAntIcon,
|
||||
BoltIcon,
|
||||
BeakerIcon,
|
||||
StarIcon,
|
||||
ComputerDesktopIcon,
|
||||
CubeTransparentIcon,
|
||||
ArrowsRightLeftIcon,
|
||||
DocumentDuplicateIcon,
|
||||
SparklesIcon,
|
||||
ArchiveBoxXMarkIcon,
|
||||
ShieldExclamationIcon,
|
||||
ServerStackIcon,
|
||||
ExclamationTriangleIcon,
|
||||
LockClosedIcon,
|
||||
PuzzlePieceIcon,
|
||||
ArrowUturnLeftIcon,
|
||||
EyeIcon,
|
||||
CurrencyDollarIcon,
|
||||
ClipboardDocumentCheckIcon,
|
||||
CpuChipIcon,
|
||||
FingerPrintIcon,
|
||||
HandRaisedIcon,
|
||||
ScaleIcon,
|
||||
MapPinIcon,
|
||||
DocumentTextIcon,
|
||||
ShieldCheckIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
KeyIcon,
|
||||
RocketLaunchIcon,
|
||||
BuildingLibraryIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
ChevronDownIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
verificationCategories,
|
||||
typeConfig,
|
||||
modeConfig,
|
||||
criterionPerformance,
|
||||
} from "../data/verifications";
|
||||
import type {
|
||||
VerificationType,
|
||||
VerificationMode,
|
||||
EvaluationResult,
|
||||
VerificationCategory,
|
||||
} from "../data/verifications";
|
||||
import type { Route } from "./+types/verifications";
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Verifications — Arc" }];
|
||||
}
|
||||
|
||||
type IconComponent = React.ComponentType<{ className?: string }>;
|
||||
|
||||
function TrafficLightIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" className={className}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7 3a3 3 0 0 1 6 0v14a3 3 0 0 1-6 0V3Zm3 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm0 5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm0 5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const criterionIcons: Record<string, IconComponent> = {
|
||||
"Motivation": LightBulbIcon,
|
||||
"Specifications": ClipboardDocumentListIcon,
|
||||
"Documentation": BookOpenIcon,
|
||||
"Minimization": FunnelIcon,
|
||||
"Formatting": Bars3BottomLeftIcon,
|
||||
"Linting": WrenchIcon,
|
||||
"Style": PaintBrushIcon,
|
||||
"Completeness": CheckBadgeIcon,
|
||||
"Defects": BugAntIcon,
|
||||
"Performance": BoltIcon,
|
||||
"Test Coverage": BeakerIcon,
|
||||
"Test Quality": StarIcon,
|
||||
"E2E Coverage": ComputerDesktopIcon,
|
||||
"Architecture": CubeTransparentIcon,
|
||||
"Interfaces": ArrowsRightLeftIcon,
|
||||
"Duplication": DocumentDuplicateIcon,
|
||||
"Simplicity": SparklesIcon,
|
||||
"Dead Code": ArchiveBoxXMarkIcon,
|
||||
"Vulnerabilities": ShieldExclamationIcon,
|
||||
"IaC Scanning": ServerStackIcon,
|
||||
"Dependency Alerts": ExclamationTriangleIcon,
|
||||
"Security Controls": LockClosedIcon,
|
||||
"Compatibility": PuzzlePieceIcon,
|
||||
"Rollout / Rollback": ArrowUturnLeftIcon,
|
||||
"Observability": EyeIcon,
|
||||
"Cost": CurrencyDollarIcon,
|
||||
"Change Control": ClipboardDocumentCheckIcon,
|
||||
"AI Governance": CpuChipIcon,
|
||||
"Privacy": FingerPrintIcon,
|
||||
"Accessibility": HandRaisedIcon,
|
||||
"Licensing": ScaleIcon,
|
||||
};
|
||||
|
||||
const categoryIcons: Record<string, IconComponent> = {
|
||||
"Traceability": MapPinIcon,
|
||||
"Readability": DocumentTextIcon,
|
||||
"Reliability": ShieldCheckIcon,
|
||||
"Code Coverage": TrafficLightIcon,
|
||||
"Maintainability": WrenchScrewdriverIcon,
|
||||
"Security": KeyIcon,
|
||||
"Deployability": RocketLaunchIcon,
|
||||
"Compliance": BuildingLibraryIcon,
|
||||
};
|
||||
|
||||
function TypeBadge({ type }: { type: VerificationType | null }) {
|
||||
if (type === null) return null;
|
||||
const config = typeConfig[type];
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.color} ${config.bg}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type ViewMode = "grouped" | "ungrouped";
|
||||
|
||||
function CategoryCard({ category }: { category: VerificationCategory }) {
|
||||
return (
|
||||
<Disclosure
|
||||
as="div"
|
||||
className="rounded-md border border-white/[0.06] overflow-hidden"
|
||||
>
|
||||
<DisclosureButton className="group flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-white/[0.02]">
|
||||
{(() => {
|
||||
const CatIcon = categoryIcons[category.name];
|
||||
return CatIcon ? <CatIcon className="size-5 shrink-0 text-ice-300" /> : null;
|
||||
})()}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="shrink-0 text-sm font-semibold text-white">
|
||||
{category.name}
|
||||
</span>
|
||||
<span className="truncate text-xs text-navy-600">
|
||||
{category.question}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="shrink-0 text-xs text-navy-600">
|
||||
<span className="font-mono tabular-nums">{category.criteria.length}</span> controls
|
||||
</span>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-navy-600 transition-transform duration-200 group-data-open:rotate-90" />
|
||||
</DisclosureButton>
|
||||
|
||||
<DisclosurePanel
|
||||
transition
|
||||
className="origin-top transition duration-200 ease-out data-closed:-translate-y-1 data-closed:opacity-0"
|
||||
>
|
||||
<div className="border-t border-white/[0.06]">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{category.criteria.map((criterion) => {
|
||||
const Icon = criterionIcons[criterion.name];
|
||||
const perf = criterionPerformance[criterion.name];
|
||||
return (
|
||||
<tr
|
||||
key={criterion.name}
|
||||
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
|
||||
>
|
||||
<td className="w-8 py-2.5 pl-5 pr-0">
|
||||
{Icon && <Icon className="size-4 text-ice-300" />}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
|
||||
{criterion.name}
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-navy-600">
|
||||
{criterion.description || (
|
||||
<span className="italic">Not configured</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-3 pr-1 text-right">
|
||||
<TypeBadge type={criterion.type} />
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-1">
|
||||
{perf && <ModeBadge mode={perf.mode} />}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-1 pr-4">
|
||||
{perf && <EvaluationDots evaluations={perf.evaluations} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupedView({ categories }: { categories: readonly VerificationCategory[] }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{categories.map((category) => (
|
||||
<CategoryCard key={category.name} category={category} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModeBadge({ mode }: { mode: VerificationMode }) {
|
||||
const config = modeConfig[mode];
|
||||
return (
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.color} ${config.bg}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function EvaluationDots({ evaluations }: { evaluations: readonly EvaluationResult[] }) {
|
||||
if (evaluations.length === 0) {
|
||||
return <span className="text-xs italic text-navy-600">—</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{evaluations.map((result, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`inline-block size-2.5 rounded-sm ${
|
||||
result === "pass"
|
||||
? "bg-mint/70"
|
||||
: result === "fail"
|
||||
? "bg-coral/70"
|
||||
: "bg-navy-600/50"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UngroupedView({ categories }: { categories: readonly VerificationCategory[] }) {
|
||||
return (
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="w-8 py-2.5 pl-4 pr-0 font-medium" />
|
||||
<th className="py-2.5 pl-2 pr-3 font-medium">Verification</th>
|
||||
<th className="py-2.5 px-3 font-medium">Description</th>
|
||||
<th className="py-2.5 px-3 font-medium">Category</th>
|
||||
<th className="py-2.5 px-3 font-medium text-right">Type</th>
|
||||
<th className="py-2.5 px-3 font-medium text-right">Accuracy (F1)</th>
|
||||
<th className="py-2.5 px-3 font-medium text-right">pass@1</th>
|
||||
<th className="py-2.5 px-3 font-medium">Mode</th>
|
||||
<th className="py-2.5 pl-3 pr-4 font-medium">Evaluations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{categories.flatMap((category) =>
|
||||
category.criteria.map((criterion) => {
|
||||
const Icon = criterionIcons[criterion.name];
|
||||
const perf = criterionPerformance[criterion.name];
|
||||
return (
|
||||
<tr
|
||||
key={`${category.name}-${criterion.name}`}
|
||||
className="border-b border-white/[0.04] last:border-b-0 transition-colors hover:bg-white/[0.015]"
|
||||
>
|
||||
<td className="w-8 py-2.5 pl-4 pr-0">
|
||||
{Icon && <Icon className="size-4 text-ice-300" />}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-2 pr-3 font-medium text-ice-100">
|
||||
{criterion.name}
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-navy-600">
|
||||
{criterion.description || (
|
||||
<span className="italic">Not configured</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3 text-xs text-navy-600">
|
||||
{category.name}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3 text-right">
|
||||
<TypeBadge type={criterion.type} />
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-ice-100">
|
||||
{perf?.f1 != null ? perf.f1.toFixed(2) : <span className="text-navy-600">—</span>}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3 text-right font-mono text-xs tabular-nums text-ice-100">
|
||||
{perf?.passAt1 != null ? perf.passAt1.toFixed(2) : <span className="text-navy-600">—</span>}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3">
|
||||
{perf && <ModeBadge mode={perf.mode} />}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-3 pr-4">
|
||||
{perf && <EvaluationDots evaluations={perf.evaluations} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function filterCategories(
|
||||
categories: readonly VerificationCategory[],
|
||||
query: string,
|
||||
modeFilter: VerificationMode | "all",
|
||||
): VerificationCategory[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return categories
|
||||
.map((category) => {
|
||||
const filtered = category.criteria.filter((c) => {
|
||||
const perf = criterionPerformance[c.name];
|
||||
const matchesMode = modeFilter === "all" || perf?.mode === modeFilter;
|
||||
const matchesQuery =
|
||||
lowerQuery === "" ||
|
||||
c.name.toLowerCase().includes(lowerQuery) ||
|
||||
c.description.toLowerCase().includes(lowerQuery) ||
|
||||
category.name.toLowerCase().includes(lowerQuery);
|
||||
return matchesMode && matchesQuery;
|
||||
});
|
||||
return { ...category, criteria: filtered };
|
||||
})
|
||||
.filter((category) => category.criteria.length > 0);
|
||||
}
|
||||
|
||||
export default function Verifications() {
|
||||
const [view, setView] = useState<ViewMode>("grouped");
|
||||
const [query, setQuery] = useState("");
|
||||
const [modeFilter, setModeFilter] = useState<VerificationMode | "all">("all");
|
||||
|
||||
const filtered = filterCategories(verificationCategories, query, modeFilter);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Toolbar */}
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search verifications…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={modeFilter}
|
||||
onChange={(e) => setModeFilter(e.target.value as VerificationMode | "all")}
|
||||
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
|
||||
>
|
||||
<option value="all">All modes</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="evaluate">Evaluate</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-md border border-white/[0.06] bg-navy-800/80 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("grouped")}
|
||||
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "grouped" ? "bg-white/[0.06] text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="size-3.5" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-1.5Z" />
|
||||
</svg>
|
||||
Grouped
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("ungrouped")}
|
||||
className={`inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors ${view === "ungrouped" ? "bg-white/[0.06] text-teal-500" : "text-navy-600 hover:text-ice-300"}`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="size-3.5" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />
|
||||
</svg>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === "grouped" ? <GroupedView categories={filtered} /> : <UngroupedView categories={filtered} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue