From d597c010c3e86aed829609040f405eead6f23531 Mon Sep 17 00:00:00 2001 From: user198743 Date: Mon, 27 Jul 2026 07:03:35 -0700 Subject: [PATCH] fix(web): order requirements by severity --- .../components/blueprint/harness-slide.tsx | 29 +++++++++----- apps/web/src/lib/requirement-severity.test.ts | 39 +++++++++++++++++++ apps/web/src/lib/requirement-severity.ts | 24 ++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/lib/requirement-severity.test.ts create mode 100644 apps/web/src/lib/requirement-severity.ts diff --git a/apps/web/src/components/blueprint/harness-slide.tsx b/apps/web/src/components/blueprint/harness-slide.tsx index 434c019..be98e75 100644 --- a/apps/web/src/components/blueprint/harness-slide.tsx +++ b/apps/web/src/components/blueprint/harness-slide.tsx @@ -18,6 +18,11 @@ import { XCircle, } from '@phosphor-icons/react'; import { useMemo, useState } from 'react'; +import { + REQUIREMENT_SEVERITIES, + sortRequirementsBySeverity, + type RequirementSeverity, +} from '@/lib/requirement-severity'; import type { HarnessSnapshot } from './use-project-stream'; type Requirement = { @@ -25,7 +30,7 @@ type Requirement = { excerpt: string | null; description: string; type: 'explicit' | 'hidden'; - severity: 'critical' | 'high' | 'medium' | 'low'; + severity: RequirementSeverity; // True once a user has created or edited this row through the UI — it is then // manually owned and the worker's auto-derivation leaves it alone. "Reset to // auto" clears it. @@ -49,8 +54,6 @@ type StagedOp = type Verdict = 'pass' | 'fail' | 'inconclusive' | 'unverified'; -const SEVERITIES = ['critical', 'high', 'medium', 'low'] as const; - const SEVERITY_BADGE: Record = { critical: 'bg-red-100 text-red-700', high: 'bg-orange-100 text-orange-700', @@ -189,7 +192,7 @@ function RequirementForm({ onChange={(e) => setSeverity(e.target.value as Requirement['severity'])} className="rounded-md border border-slate-200 px-2 py-1.5 text-sm" > - {SEVERITIES.map((s) => ( + {REQUIREMENT_SEVERITIES.map((s) => ( @@ -281,13 +284,19 @@ export function HarnessSlide({ }, [effective, verdicts]); const grouped = useMemo( - () => - SEVERITIES.map((sev) => ({ + () => { + const visible = sortRequirementsBySeverity( + effective.filter( + (requirement) => + filter === 'all' || verdicts.get(requirement.id) === filter + ) + ); + + return REQUIREMENT_SEVERITIES.map((sev) => ({ sev, - items: effective.filter( - (r) => r.severity === sev && (filter === 'all' || verdicts.get(r.id) === filter) - ), - })).filter((g) => g.items.length > 0), + items: visible.filter((requirement) => requirement.severity === sev), + })).filter((group) => group.items.length > 0); + }, [effective, filter, verdicts] ); diff --git a/apps/web/src/lib/requirement-severity.test.ts b/apps/web/src/lib/requirement-severity.test.ts new file mode 100644 index 0000000..b7cc2db --- /dev/null +++ b/apps/web/src/lib/requirement-severity.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { sortRequirementsBySeverity } from './requirement-severity'; + +describe('sortRequirementsBySeverity', () => { + it('orders requirements from critical to low', () => { + const requirements = [ + { id: 'low', severity: 'low' as const }, + { id: 'medium', severity: 'medium' as const }, + { id: 'critical', severity: 'critical' as const }, + { id: 'high', severity: 'high' as const }, + ]; + + expect(sortRequirementsBySeverity(requirements).map(({ id }) => id)).toEqual([ + 'critical', + 'high', + 'medium', + 'low', + ]); + }); + + it('preserves source data, input order within a severity, and the input array', () => { + const requirements = [ + { id: 'high-first', description: 'first', severity: 'high' as const }, + { id: 'low', description: 'last', severity: 'low' as const }, + { id: 'high-second', description: 'second', severity: 'high' as const }, + ]; + const original = [...requirements]; + const sorted = sortRequirementsBySeverity(requirements); + + expect(sorted).toEqual([ + requirements[0], + requirements[2], + requirements[1], + ]); + expect(requirements).toEqual(original); + expect(sorted[0]).toBe(requirements[0]); + expect(sorted[1]).toBe(requirements[2]); + }); +}); diff --git a/apps/web/src/lib/requirement-severity.ts b/apps/web/src/lib/requirement-severity.ts new file mode 100644 index 0000000..a956afc --- /dev/null +++ b/apps/web/src/lib/requirement-severity.ts @@ -0,0 +1,24 @@ +export const REQUIREMENT_SEVERITIES = [ + 'critical', + 'high', + 'medium', + 'low', +] as const; + +export type RequirementSeverity = (typeof REQUIREMENT_SEVERITIES)[number]; + +const SEVERITY_RANK: Record = { + critical: 0, + high: 1, + medium: 2, + low: 3, +}; + +export function sortRequirementsBySeverity< + T extends { severity: RequirementSeverity }, +>(requirements: readonly T[]): T[] { + return [...requirements].sort( + (left, right) => + SEVERITY_RANK[left.severity] - SEVERITY_RANK[right.severity], + ); +}