mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Add verification control detail page with clickable catalog rows
Adds /verifications/:slug route with detail page showing control description, stat cards, evaluation history, recent runs, checks/examples, and sibling navigation. Makes verification catalog rows fully clickable. Fixes dark mode hydration mismatch with suppressHydrationWarning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bda63295ed
commit
ae72cd059d
5 changed files with 660 additions and 11 deletions
|
|
@ -214,3 +214,269 @@ export function getCriteriaSummary(criteria: readonly Criterion[]) {
|
|||
export function getAllCriteria(categories: readonly VerificationCategory[]) {
|
||||
return categories.flatMap((c) => c.criteria);
|
||||
}
|
||||
|
||||
export function slugify(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
}
|
||||
|
||||
export function findCriterionBySlug(slug: string): {
|
||||
criterion: Criterion;
|
||||
category: VerificationCategory;
|
||||
performance: CriterionPerformance;
|
||||
} | null {
|
||||
for (const category of verificationCategories) {
|
||||
for (const criterion of category.criteria) {
|
||||
if (slugify(criterion.name) === slug) {
|
||||
const performance = criterionPerformance[criterion.name];
|
||||
if (performance) {
|
||||
return { criterion, category, performance };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface ControlDetail {
|
||||
description: string;
|
||||
checks: string[];
|
||||
passExample: string;
|
||||
failExample: string;
|
||||
}
|
||||
|
||||
export const controlDetails: Record<string, ControlDetail> = {
|
||||
"Motivation": {
|
||||
description: "Verifies that every change traces back to a clear origin — whether a ticket, RFC, customer request, or incident. Without documented motivation, reviewers lack context for evaluating whether the change is appropriate.",
|
||||
checks: ["PR body or linked issue explains why the change is needed", "Commit messages reference a ticket or context", "No orphaned changes without traceable origin"],
|
||||
passExample: "PR links to JIRA-1234 and explains the user-facing pain point being resolved.",
|
||||
failExample: "PR description is empty or says only 'fix stuff'.",
|
||||
},
|
||||
"Specifications": {
|
||||
description: "Checks that functional and non-functional requirements are written down before implementation begins. Specifications prevent scope creep and ensure everyone agrees on what done looks like.",
|
||||
checks: ["Acceptance criteria listed in the issue or PR", "Edge cases documented", "Non-functional requirements (performance, security) stated when relevant"],
|
||||
passExample: "Issue includes acceptance criteria with three testable scenarios.",
|
||||
failExample: "Issue body says 'implement the feature' with no acceptance criteria.",
|
||||
},
|
||||
"Documentation": {
|
||||
description: "Ensures developer-facing and user-facing documentation is added or updated alongside code changes. Stale docs degrade team velocity and increase onboarding cost.",
|
||||
checks: ["README or docs updated for new features", "API documentation reflects endpoint changes", "Inline comments for non-obvious logic"],
|
||||
passExample: "New API endpoint has corresponding OpenAPI spec update and usage example in docs.",
|
||||
failExample: "New CLI flag added with no mention in README or --help text.",
|
||||
},
|
||||
"Minimization": {
|
||||
description: "Flags extraneous changes that inflate the diff — formatting-only edits, unrelated refactors, or drive-by fixes. Keeping PRs focused improves review quality and reduces revert risk.",
|
||||
checks: ["No unrelated formatting or whitespace changes", "Refactors separated from feature work", "Each commit addresses a single concern"],
|
||||
passExample: "PR touches only files directly related to the new caching layer.",
|
||||
failExample: "PR adds a feature but also reformats 12 unrelated files.",
|
||||
},
|
||||
"Formatting": {
|
||||
description: "Validates that code layout conforms to the project's formatting standard (e.g., Prettier, rustfmt). Automated formatting removes subjective style debates from code review.",
|
||||
checks: ["All files pass the project formatter", "No manual formatting overrides without justification"],
|
||||
passExample: "All changed files pass `prettier --check` and `rustfmt --check`.",
|
||||
failExample: "Several files have inconsistent indentation that the formatter would fix.",
|
||||
},
|
||||
"Linting": {
|
||||
description: "Confirms that static analysis findings are resolved. Linter warnings left unaddressed accumulate into tech debt and mask real issues.",
|
||||
checks: ["No new linter warnings introduced", "Existing warnings not suppressed without explanation", "Lint config not weakened"],
|
||||
passExample: "ESLint and Clippy pass with zero warnings on changed files.",
|
||||
failExample: "New `// eslint-disable-next-line` added to suppress a legitimate warning.",
|
||||
},
|
||||
"Style": {
|
||||
description: "Evaluates whether the code follows the team's house style conventions beyond what automated formatters catch — naming, file organization, import ordering, and idiomatic patterns.",
|
||||
checks: ["Naming conventions followed (camelCase, snake_case as appropriate)", "Import ordering matches project convention", "Idiomatic patterns used for the language"],
|
||||
passExample: "New TypeScript module uses camelCase variables, groups imports by source, and uses `Map` instead of plain objects for lookups.",
|
||||
failExample: "Mix of camelCase and snake_case in the same module with random import ordering.",
|
||||
},
|
||||
"Completeness": {
|
||||
description: "Checks that the implementation fully covers the specified requirements. Partial implementations ship broken experiences and create follow-up tickets that could have been avoided.",
|
||||
checks: ["All acceptance criteria addressed", "Edge cases handled", "Error states implemented"],
|
||||
passExample: "Feature handles all three specified user roles with appropriate permissions.",
|
||||
failExample: "Only the happy path is implemented; error and empty states are missing.",
|
||||
},
|
||||
"Defects": {
|
||||
description: "Identifies potential or likely bugs through static analysis and AI review. Catching defects before merge is orders of magnitude cheaper than finding them in production.",
|
||||
checks: ["No off-by-one errors in loops or slices", "Null/undefined handled at boundaries", "Race conditions considered in async code"],
|
||||
passExample: "API handler validates input, handles missing fields gracefully, and returns appropriate HTTP status codes.",
|
||||
failExample: "Array index accessed without bounds check; crashes on empty input.",
|
||||
},
|
||||
"Performance": {
|
||||
description: "Assesses whether the change impacts hot paths or introduces algorithmic regressions. Performance problems that ship to production are expensive to diagnose and fix.",
|
||||
checks: ["No N+1 queries introduced", "Large collections not processed synchronously", "Caching considered for repeated expensive operations"],
|
||||
passExample: "Database query uses a JOIN instead of N separate queries for related records.",
|
||||
failExample: "Loop makes a separate HTTP call for each item in a 1000-element list.",
|
||||
},
|
||||
"Test Coverage": {
|
||||
description: "Measures whether production code is exercised by automated tests. Coverage gaps mean regressions can ship undetected.",
|
||||
checks: ["New code has corresponding unit tests", "Coverage does not decrease", "Critical paths have integration tests"],
|
||||
passExample: "New service method has 6 unit tests covering happy path, error cases, and edge cases.",
|
||||
failExample: "New 200-line module has zero test files.",
|
||||
},
|
||||
"Test Quality": {
|
||||
description: "Evaluates whether tests are robust, readable, and actually verify behavior rather than implementation details. Low-quality tests give false confidence.",
|
||||
checks: ["Tests verify behavior, not implementation", "Assertions are specific and meaningful", "Tests are independent and deterministic"],
|
||||
passExample: "Tests assert on API response shape and status codes, not on internal method call counts.",
|
||||
failExample: "Tests mock every dependency and only verify that mocks were called.",
|
||||
},
|
||||
"E2E Coverage": {
|
||||
description: "Checks that user-facing workflows are exercised by end-to-end browser automation. E2E tests catch integration issues that unit tests miss.",
|
||||
checks: ["Critical user flows have Playwright/Cypress tests", "E2E tests run in CI", "No flaky E2E tests introduced"],
|
||||
passExample: "New checkout flow has a Playwright test that completes a purchase end-to-end.",
|
||||
failExample: "New multi-step wizard has no browser automation tests.",
|
||||
},
|
||||
"Architecture": {
|
||||
description: "Validates that layering and dependency directions conform to the project's architectural design. Architectural violations compound over time and make systems harder to evolve.",
|
||||
checks: ["Dependencies point inward (domain doesn't depend on infra)", "No circular dependencies introduced", "Module boundaries respected"],
|
||||
passExample: "New repository implementation depends on domain interfaces, not the other way around.",
|
||||
failExample: "Domain model imports directly from the HTTP framework package.",
|
||||
},
|
||||
"Interfaces": {
|
||||
description: "Reviews public API surfaces for clarity, consistency, and backward compatibility. Interfaces are contracts — once published, they're expensive to change.",
|
||||
checks: ["Public API types are well-defined", "Breaking changes documented", "Consistent naming across endpoints"],
|
||||
passExample: "New endpoint follows existing naming and error format conventions.",
|
||||
failExample: "New endpoint uses different error format than all other endpoints.",
|
||||
},
|
||||
"Duplication": {
|
||||
description: "Detects similar or identical code blocks that could be consolidated. Duplication increases maintenance burden and creates inconsistency risk.",
|
||||
checks: ["No copy-pasted logic across files", "Shared utilities used for common patterns", "Similar test setup consolidated"],
|
||||
passExample: "Date formatting logic extracted into a shared utility used by 4 components.",
|
||||
failExample: "Same 15-line validation function copy-pasted into three different handlers.",
|
||||
},
|
||||
"Simplicity": {
|
||||
description: "Flags unnecessarily complex code that could be simplified without changing behavior. Simpler code is easier to review, debug, and extend.",
|
||||
checks: ["No premature abstractions", "Control flow is straightforward", "Functions are focused and short"],
|
||||
passExample: "Conditional logic uses early returns instead of deeply nested if-else chains.",
|
||||
failExample: "Three-level generic abstraction for a function called in one place.",
|
||||
},
|
||||
"Dead Code": {
|
||||
description: "Identifies unexecuted code paths and unused dependencies. Dead code misleads readers and bloats bundles.",
|
||||
checks: ["No unreachable code paths", "Unused imports and variables removed", "Deprecated functions removed if no longer called"],
|
||||
passExample: "Old feature flag and its associated code paths removed after rollout completed.",
|
||||
failExample: "Commented-out function left in file 'in case we need it later'.",
|
||||
},
|
||||
"Vulnerabilities": {
|
||||
description: "Scans for known security vulnerabilities using both AI analysis and static scanning tools. Shipping known vulnerabilities exposes users and the organization to risk.",
|
||||
checks: ["No SQL injection or XSS vectors", "User input sanitized at boundaries", "Authentication/authorization checks present"],
|
||||
passExample: "User input passed through parameterized queries; HTML output escaped.",
|
||||
failExample: "Raw SQL string concatenation with user-supplied values.",
|
||||
},
|
||||
"IaC Scanning": {
|
||||
description: "Validates infrastructure-as-code definitions against security best practices. Misconfigured infrastructure is a leading cause of data breaches.",
|
||||
checks: ["No publicly accessible storage buckets", "Encryption at rest enabled", "Least-privilege IAM policies"],
|
||||
passExample: "Terraform module creates S3 bucket with encryption, versioning, and private ACL.",
|
||||
failExample: "CloudFormation template creates an RDS instance with no encryption and public accessibility.",
|
||||
},
|
||||
"Dependency Alerts": {
|
||||
description: "Checks that third-party dependencies are free from known CVEs. Vulnerable dependencies are an easy attack vector that automated tools can detect.",
|
||||
checks: ["No dependencies with known critical CVEs", "Lock file updated to patched versions", "Unused dependencies removed"],
|
||||
passExample: "Dependabot alert resolved by updating lodash from 4.17.20 to 4.17.21.",
|
||||
failExample: "Package.json pins a version of axios with a known SSRF vulnerability.",
|
||||
},
|
||||
"Security Controls": {
|
||||
description: "Verifies that organization-specific security standards are applied — rate limiting, audit logging, CORS policies, and secret management.",
|
||||
checks: ["Secrets not hardcoded in source", "Rate limiting on public endpoints", "Audit logging for sensitive operations"],
|
||||
passExample: "API key loaded from environment variable; rate limiter configured on login endpoint.",
|
||||
failExample: "AWS credentials committed in a config file.",
|
||||
},
|
||||
"Compatibility": {
|
||||
description: "Detects breaking changes in APIs, database schemas, or wire formats that could disrupt consumers. Breaking changes require coordination that surprises prevent.",
|
||||
checks: ["No removed or renamed public API fields", "Database migrations are backward-compatible", "Wire format changes are additive"],
|
||||
passExample: "New field added to API response; no existing fields removed or renamed.",
|
||||
failExample: "Column renamed in migration while old code is still deployed.",
|
||||
},
|
||||
"Rollout / Rollback": {
|
||||
description: "Confirms that the change has a clear deployment plan and can be safely rolled back if issues arise. Every production deploy should be reversible.",
|
||||
checks: ["Feature flag available for gradual rollout", "Database migration is reversible", "Rollback procedure documented"],
|
||||
passExample: "Feature behind a LaunchDarkly flag with 10% initial rollout and documented rollback steps.",
|
||||
failExample: "Irreversible database migration with no rollback plan.",
|
||||
},
|
||||
"Observability": {
|
||||
description: "Ensures that logging, metrics, and tracing are instrumented for new code paths. Without observability, production issues are invisible until users report them.",
|
||||
checks: ["Structured logging for new operations", "Metrics emitted for key business events", "Distributed tracing propagated"],
|
||||
passExample: "New payment endpoint logs transaction IDs, emits latency metrics, and propagates trace context.",
|
||||
failExample: "New background job has no logging or metrics; failures are silent.",
|
||||
},
|
||||
"Cost": {
|
||||
description: "Estimates the infrastructure and operational cost impact of the change. Unchecked cost growth erodes margins and can cause budget surprises.",
|
||||
checks: ["New infrastructure resources sized appropriately", "No unbounded resource consumption", "Cost estimate provided for significant changes"],
|
||||
passExample: "New Lambda function has memory limit set and estimated monthly cost noted in PR.",
|
||||
failExample: "New service provisions a db.r5.4xlarge for a table with 100 rows.",
|
||||
},
|
||||
"Change Control": {
|
||||
description: "Validates that separation-of-duties policies are met — the author is not the sole reviewer, approvals are obtained, and the change went through the proper process.",
|
||||
checks: ["PR has at least one approval from non-author", "Required reviewers have signed off", "No self-merging without policy exception"],
|
||||
passExample: "PR approved by two team members before merge; CI checks all green.",
|
||||
failExample: "Author approved and merged their own PR with no other reviewers.",
|
||||
},
|
||||
"AI Governance": {
|
||||
description: "Checks that AI-generated or AI-assisted code meets the organization's governance requirements — attribution, review depth, and acceptable use.",
|
||||
checks: ["AI-generated code clearly attributed", "Human review of AI suggestions documented", "AI usage within acceptable-use policy"],
|
||||
passExample: "PR notes that implementation was AI-assisted; human reviewer verified logic and tests.",
|
||||
failExample: "Entire module generated by AI with no human review or attribution.",
|
||||
},
|
||||
"Privacy": {
|
||||
description: "Ensures that personally identifiable information (PII) is identified, classified, and handled according to privacy standards (GDPR, CCPA).",
|
||||
checks: ["PII fields identified and documented", "Data retention policies applied", "Consent mechanisms in place for data collection"],
|
||||
passExample: "New user profile endpoint masks email in logs and respects data deletion requests.",
|
||||
failExample: "User email addresses logged in plaintext to application logs.",
|
||||
},
|
||||
"Accessibility": {
|
||||
description: "Verifies that UI changes meet accessibility requirements (WCAG 2.1 AA). Inaccessible software excludes users and creates legal risk.",
|
||||
checks: ["Semantic HTML elements used", "ARIA labels present on interactive elements", "Color contrast meets WCAG AA standards"],
|
||||
passExample: "New modal uses <dialog>, has aria-labelledby, and focus is trapped within.",
|
||||
failExample: "Custom dropdown built with <div> elements, no keyboard navigation, no ARIA roles.",
|
||||
},
|
||||
"Licensing": {
|
||||
description: "Ensures that all third-party dependencies comply with the organization's intellectual property policy. License violations can have severe legal consequences.",
|
||||
checks: ["No GPL-licensed dependencies in proprietary code", "License file present for new dependencies", "Supply chain attestation where required"],
|
||||
passExample: "New dependency uses MIT license; added to approved dependency list.",
|
||||
failExample: "AGPL-licensed library added to a closed-source commercial product.",
|
||||
},
|
||||
};
|
||||
|
||||
export interface RecentControlResult {
|
||||
runId: string;
|
||||
runTitle: string;
|
||||
workflow: string;
|
||||
result: VerificationStatus;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export const recentControlResults: Record<string, RecentControlResult[]> = {
|
||||
"Motivation": [
|
||||
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
|
||||
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
|
||||
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "fail", timestamp: "1d ago" },
|
||||
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
|
||||
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
|
||||
],
|
||||
"Documentation": [
|
||||
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
|
||||
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "fail", timestamp: "5h ago" },
|
||||
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "pass", timestamp: "1d ago" },
|
||||
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
|
||||
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "fail", timestamp: "3d ago" },
|
||||
],
|
||||
"Rollout / Rollback": [
|
||||
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "fail", timestamp: "2h ago" },
|
||||
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
|
||||
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "fail", timestamp: "1d ago" },
|
||||
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "fail", timestamp: "2d ago" },
|
||||
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
|
||||
],
|
||||
};
|
||||
|
||||
// Default recent results for controls without specific data
|
||||
const defaultRecentResults: RecentControlResult[] = [
|
||||
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
|
||||
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
|
||||
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "pass", timestamp: "1d ago" },
|
||||
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
|
||||
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
|
||||
];
|
||||
|
||||
export function getRecentResults(criterionName: string): RecentControlResult[] {
|
||||
return recentControlResults[criterionName] ?? defaultRecentResults;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const links: Route.LinksFunction = () => [
|
|||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="h-full bg-atmosphere">
|
||||
<html lang="en" className="h-full bg-atmosphere" suppressHydrationWarning>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
<meta charSet="utf-8" />
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export default [
|
|||
route("retro", "routes/run-retro.tsx"),
|
||||
]),
|
||||
route("verifications", "routes/verifications.tsx"),
|
||||
route("verifications/:slug", "routes/verification-detail.tsx"),
|
||||
route("retros", "routes/retros.tsx"),
|
||||
route("insights", "routes/insights.tsx", [
|
||||
index("routes/insights-editor.tsx"),
|
||||
|
|
|
|||
374
apps/arc-web/app/routes/verification-detail.tsx
Normal file
374
apps/arc-web/app/routes/verification-detail.tsx
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
import { Link, useParams } from "react-router";
|
||||
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,
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
MinusCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
findCriterionBySlug,
|
||||
slugify,
|
||||
typeConfig,
|
||||
modeConfig,
|
||||
statusConfig,
|
||||
criterionPerformance,
|
||||
controlDetails,
|
||||
getRecentResults,
|
||||
} from "../data/verifications";
|
||||
import type {
|
||||
VerificationType,
|
||||
VerificationMode,
|
||||
EvaluationResult,
|
||||
VerificationStatus,
|
||||
} from "../data/verifications";
|
||||
import type { Route } from "./+types/verification-detail";
|
||||
|
||||
export const handle = { hideHeader: true };
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
const match = findCriterionBySlug(params.slug ?? "");
|
||||
const name = match?.criterion.name ?? "Verification";
|
||||
return [{ title: `${name} — Verifications — Arc` }];
|
||||
}
|
||||
|
||||
type IconComponent = React.ComponentType<{ className?: string }>;
|
||||
|
||||
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": BeakerIcon,
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
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 StatCard({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel/60 px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-fg-muted">{label}</p>
|
||||
<p className={`mt-1 font-mono text-lg font-semibold tabular-nums ${warn ? "text-amber" : "text-fg"}`}>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EvaluationBar({ evaluations }: { evaluations: readonly EvaluationResult[] }) {
|
||||
if (evaluations.length === 0) {
|
||||
return <p className="text-sm italic text-fg-muted">No evaluations yet</p>;
|
||||
}
|
||||
|
||||
const passCount = evaluations.filter((e) => e === "pass").length;
|
||||
const failCount = evaluations.filter((e) => e === "fail").length;
|
||||
const skipCount = evaluations.filter((e) => e === "skip").length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
{evaluations.map((result, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-6 flex-1 rounded-sm ${
|
||||
result === "pass"
|
||||
? "bg-mint/70"
|
||||
: result === "fail"
|
||||
? "bg-coral/70"
|
||||
: "bg-navy-600/50"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-4 text-xs text-fg-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="inline-block size-2 rounded-sm bg-mint/70" />
|
||||
{passCount} pass
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="inline-block size-2 rounded-sm bg-coral/70" />
|
||||
{failCount} fail
|
||||
</span>
|
||||
{skipCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="inline-block size-2 rounded-sm bg-navy-600/50" />
|
||||
{skipCount} skip
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultIcon({ result }: { result: VerificationStatus }) {
|
||||
const config = statusConfig[result];
|
||||
if (result === "pass") return <CheckCircleIcon className={`size-4 ${config.color}`} />;
|
||||
if (result === "fail") return <XCircleIcon className={`size-4 ${config.color}`} />;
|
||||
return <MinusCircleIcon className={`size-4 ${config.color}`} />;
|
||||
}
|
||||
|
||||
export default function VerificationDetail() {
|
||||
const { slug } = useParams();
|
||||
const match = findCriterionBySlug(slug ?? "");
|
||||
|
||||
if (!match) {
|
||||
return <p className="py-8 text-center text-sm text-fg-muted">Verification not found.</p>;
|
||||
}
|
||||
|
||||
const { criterion, category, performance } = match;
|
||||
const Icon = criterionIcons[criterion.name];
|
||||
const CatIcon = categoryIcons[category.name];
|
||||
const detail = controlDetails[criterion.name];
|
||||
const recentResults = getRecentResults(criterion.name);
|
||||
const siblings = category.criteria.filter((c) => c.name !== criterion.name);
|
||||
|
||||
const passRate = performance.evaluations.length > 0
|
||||
? (performance.evaluations.filter((e) => e === "pass").length / performance.evaluations.length * 100).toFixed(0)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-1 text-sm text-fg-muted">
|
||||
<Link to="/verifications" className="text-fg-3 hover:text-fg">Verifications</Link>
|
||||
<ChevronRightIcon className="size-3" />
|
||||
<span className="text-fg-3">{category.name}</span>
|
||||
<ChevronRightIcon className="size-3" />
|
||||
<span>{criterion.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-3">
|
||||
{Icon && <Icon className="mt-0.5 size-6 text-fg-3" />}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-xl font-semibold text-fg">{criterion.name}</h2>
|
||||
<p className="mt-1 text-sm text-fg-muted">{criterion.description}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<TypeBadge type={criterion.type} />
|
||||
<ModeBadge mode={performance.mode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description block */}
|
||||
{detail && (
|
||||
<div className="rounded-md border border-line bg-panel/60 px-5 py-4">
|
||||
<p className="text-sm leading-relaxed text-fg-2">{detail.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stat cards */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<StatCard label="Accuracy (F1)" value={performance.f1 != null ? performance.f1.toFixed(2) : "—"} />
|
||||
<StatCard label="pass@1" value={performance.passAt1 != null ? performance.passAt1.toFixed(2) : "—"} />
|
||||
<StatCard label="Pass Rate" value={passRate != null ? `${passRate}%` : "—"} />
|
||||
<StatCard label="Total Evals" value={String(performance.evaluations.length)} />
|
||||
</div>
|
||||
|
||||
{/* Evaluation history */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-fg">Evaluation History</h3>
|
||||
<EvaluationBar evaluations={performance.evaluations} />
|
||||
</div>
|
||||
|
||||
{/* Recent runs table */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-fg">Recent Runs</h3>
|
||||
<div className="rounded-md border border-line overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
|
||||
<th className="py-2.5 pl-4 pr-3 font-medium">Run</th>
|
||||
<th className="py-2.5 px-3 font-medium w-8">Result</th>
|
||||
<th className="py-2.5 px-3 font-medium">Workflow</th>
|
||||
<th className="py-2.5 pl-3 pr-4 font-medium text-right">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentResults.map((run) => (
|
||||
<tr key={run.runId} className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay">
|
||||
<td className="py-2.5 pl-4 pr-3">
|
||||
<Link to={`/runs/${run.runId}`} className="font-medium text-fg-2 hover:text-fg">
|
||||
{run.runTitle}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-2.5 px-3">
|
||||
<ResultIcon result={run.result} />
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-fg-muted">{run.workflow}</td>
|
||||
<td className="py-2.5 pl-3 pr-4 text-right text-fg-muted">{run.timestamp}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* What this checks / Examples */}
|
||||
{detail && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-md border border-line bg-panel/60 px-5 py-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-fg">What This Checks</h3>
|
||||
<ul className="space-y-2 text-sm text-fg-2">
|
||||
{detail.checks.map((check) => (
|
||||
<li key={check} className="flex items-start gap-2">
|
||||
<span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-fg-muted" />
|
||||
{check}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-line bg-panel/60 px-5 py-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-mint">Pass Example</h3>
|
||||
<p className="text-sm text-fg-2">{detail.passExample}</p>
|
||||
</div>
|
||||
<div className="rounded-md border border-line bg-panel/60 px-5 py-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-coral">Fail Example</h3>
|
||||
<p className="text-sm text-fg-2">{detail.failExample}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sibling controls */}
|
||||
{siblings.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold text-fg">
|
||||
{CatIcon && <CatIcon className="mr-1.5 inline size-4 text-fg-3" />}
|
||||
Other {category.name} Controls
|
||||
</h3>
|
||||
<div className="rounded-md border border-line overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{siblings.map((sibling) => {
|
||||
const SibIcon = criterionIcons[sibling.name];
|
||||
const sibPerf = criterionPerformance[sibling.name];
|
||||
return (
|
||||
<tr key={sibling.name} className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay">
|
||||
<td className="w-8 py-2.5 pl-4 pr-0">
|
||||
{SibIcon && <SibIcon className="size-4 text-fg-3" />}
|
||||
</td>
|
||||
<td className="py-2.5 pl-2 pr-3">
|
||||
<Link
|
||||
to={`/verifications/${slugify(sibling.name)}`}
|
||||
className="font-medium text-fg-2 hover:text-fg"
|
||||
>
|
||||
{sibling.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-fg-muted">
|
||||
{sibling.description || <span className="italic">Not configured</span>}
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 px-3 text-right">
|
||||
<TypeBadge type={sibling.type} />
|
||||
</td>
|
||||
<td className="whitespace-nowrap py-2.5 pl-3 pr-4">
|
||||
{sibPerf && <ModeBadge mode={sibPerf.mode} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
|
|
@ -54,6 +55,7 @@ import {
|
|||
typeConfig,
|
||||
modeConfig,
|
||||
criterionPerformance,
|
||||
slugify,
|
||||
} from "../data/verifications";
|
||||
import type {
|
||||
VerificationType,
|
||||
|
|
@ -142,6 +144,18 @@ function TypeBadge({ type }: { type: VerificationType | null }) {
|
|||
|
||||
type ViewMode = "grouped" | "ungrouped";
|
||||
|
||||
function CriterionRow({ slug, children }: { slug: string; children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<tr
|
||||
className="border-b border-line last:border-b-0 cursor-pointer transition-colors hover:bg-overlay"
|
||||
onClick={() => navigate(`/verifications/${slug}`)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryCard({ category }: { category: VerificationCategory }) {
|
||||
return (
|
||||
<Disclosure
|
||||
|
|
@ -180,10 +194,7 @@ function CategoryCard({ category }: { category: VerificationCategory }) {
|
|||
const Icon = criterionIcons[criterion.name];
|
||||
const perf = criterionPerformance[criterion.name];
|
||||
return (
|
||||
<tr
|
||||
key={criterion.name}
|
||||
className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay"
|
||||
>
|
||||
<CriterionRow key={criterion.name} slug={slugify(criterion.name)}>
|
||||
<td className="w-8 py-2.5 pl-5 pr-0">
|
||||
{Icon && <Icon className="size-4 text-fg-3" />}
|
||||
</td>
|
||||
|
|
@ -204,7 +215,7 @@ function CategoryCard({ category }: { category: VerificationCategory }) {
|
|||
<td className="whitespace-nowrap py-2.5 pl-1 pr-4">
|
||||
{perf && <EvaluationDots evaluations={perf.evaluations} />}
|
||||
</td>
|
||||
</tr>
|
||||
</CriterionRow>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
|
|
@ -281,10 +292,7 @@ function UngroupedView({ categories }: { categories: readonly VerificationCatego
|
|||
const Icon = criterionIcons[criterion.name];
|
||||
const perf = criterionPerformance[criterion.name];
|
||||
return (
|
||||
<tr
|
||||
key={`${category.name}-${criterion.name}`}
|
||||
className="border-b border-line last:border-b-0 transition-colors hover:bg-overlay"
|
||||
>
|
||||
<CriterionRow key={`${category.name}-${criterion.name}`} slug={slugify(criterion.name)}>
|
||||
<td className="w-8 py-2.5 pl-4 pr-0">
|
||||
{Icon && <Icon className="size-4 text-fg-3" />}
|
||||
</td>
|
||||
|
|
@ -314,7 +322,7 @@ function UngroupedView({ categories }: { categories: readonly VerificationCatego
|
|||
<td className="whitespace-nowrap py-2.5 pl-3 pr-4">
|
||||
{perf && <EvaluationDots evaluations={perf.evaluations} />}
|
||||
</td>
|
||||
</tr>
|
||||
</CriterionRow>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue