diff --git a/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx b/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx index a3a3625aa..dacc8a3d2 100644 --- a/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx +++ b/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx @@ -29,6 +29,7 @@ import { formatRate } from "../../utils/formatPercentage"; import useSeverityColor from "../../hooks/useSeverityColor"; import DefconBadge from "../DefconBadge"; import TaxonomyCellChart from "./TaxonomyCellChart"; +import TaxonomyPathBadges from "./TaxonomyPathBadges"; import { buildAxisGroups, type AxisGroup, @@ -36,6 +37,7 @@ import { type MatrixView, type TaxonomyAxis, } from "../../utils/techniqueIntentRollup"; +import { isTechniqueKey } from "../../utils/taxonomyLabels"; import type { SortOption } from "../../hooks/useModuleFilters"; /** @@ -122,7 +124,16 @@ const Stat = ({ label, value }: { label: string; value: string }) => ( * digest provides a detector count here, so we report how many judges scored * the pairing rather than inventing a per-detector breakdown. */ -const CellDetail = ({ cell, title }: { cell: MatrixCell; title?: string }) => { +const CellDetail = ({ + cell, + title, + taxonomyKey, +}: { + cell: MatrixCell; + title?: string; + /** Underlying key for this pairing's leaf, when it's a technique (drives the breadcrumb). */ + taxonomyKey?: string; +}) => { const { getSeverityLabelByLevel, getDefconBadgeColor } = useSeverityColor(); const defcon = scoreToDefcon(cell.score); const hasFailures = cell.score < 1; @@ -139,6 +150,14 @@ const CellDetail = ({ cell, title }: { cell: MatrixCell; title?: string }) => { {getSeverityLabelByLevel(defcon)} + {taxonomyKey && isTechniqueKey(taxonomyKey) && ( + + + Taxonomy path + + + + )} + {isTechniqueKey(group.key) && ( + + + Taxonomy path + + + + )} Pass rate by {childNoun}. Click a bar for the pass/fail breakdown. @@ -232,7 +259,11 @@ const GroupChildrenChart = ({ /> {selectedEntry && (
- +
)} diff --git a/garak-report/src/components/TechniqueIntent/TaxonomyPathBadges.tsx b/garak-report/src/components/TechniqueIntent/TaxonomyPathBadges.tsx new file mode 100644 index 000000000..b7032c98d --- /dev/null +++ b/garak-report/src/components/TechniqueIntent/TaxonomyPathBadges.tsx @@ -0,0 +1,55 @@ +/** + * @file TaxonomyPathBadges.tsx + * @description Breadcrumb-style rendering of a technique's full taxonomy path + * (e.g. Fictionalizing › Roleplaying › User_persona), reusing the + * gray outline tag styling from {@link ProbeTagsList} so the + * taxonomy context reads consistently with the Probes view. + * @module components/TechniqueIntent + * + * @copyright NVIDIA Corporation 2023-2026 + * @license Apache-2.0 + */ + +import { Badge, Flex, Text } from "@kui/react"; +import { ChevronRight } from "lucide-react"; +import { techniquePathSegments } from "../../utils/taxonomyLabels"; + +/** Props for TaxonomyPathBadges component */ +interface TaxonomyPathBadgesProps { + /** Full `demon:`-prefixed technique key to render as a taxonomy path. */ + techniqueKey: string; +} + +/** + * Renders a technique's full taxonomy path as a chain of gray outline badges + * (matching {@link ProbeTagsList}'s tag style) separated by chevrons, e.g. + * `Fictionalizing › Roleplaying › User_persona`. Gives leaf-level entries the + * branch context that shortened labels alone drop. Renders nothing for a + * single-segment path, since there is no hierarchy to show. + * + * @param props - Component props + * @param props.techniqueKey - Full `demon:` technique key + * @returns Breadcrumb badge chain, or null when the key has no hierarchy + */ +const TaxonomyPathBadges = ({ techniqueKey }: TaxonomyPathBadgesProps) => { + const segments = techniquePathSegments(techniqueKey); + if (segments.length <= 1) return null; + + return ( + + {segments.map((segment, index) => ( + // eslint-disable-next-line react/no-array-index-key -- segments are positional and stable per key + + + {segment} + + {index < segments.length - 1 && ( + + ))} + + ); +}; + +export default TaxonomyPathBadges; diff --git a/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx index 68242c679..142fbcd19 100644 --- a/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx +++ b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx @@ -8,7 +8,7 @@ * @license Apache-2.0 */ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import type { ComponentProps } from "react"; import TaxonomyAxisList from "../TaxonomyAxisList"; @@ -75,7 +75,9 @@ const cell = (over: Partial): MatrixCell => ({ }); // techA: 3 intents (chart path, worst-first). techB: 1 failing intent -// (single-child detail). techC: 1 clean intent. +// (single-child detail). techC: 1 clean intent. techD is a real hierarchical +// demon: key, used to exercise the taxonomy-path breadcrumb (Issue #1972). +const techD = "demon:Fictionalizing:Roleplaying:User_persona"; const cellMap: Record = { "techA|i1": cell({ col: "i1", @@ -86,10 +88,11 @@ const cellMap: Record = { "techA|i3": cell({ col: "i3", score: 1, passed: 100 }), "techB|i1": cell({ row: "techB", col: "i1", score: 0.4, passed: 40, nAttempts: 20 }), "techC|i1": cell({ row: "techC", col: "i1", score: 1, passed: 100 }), + [`${techD}|i1`]: cell({ row: techD, col: "i1", score: 0.2, passed: 20 }), }; const view: MatrixView = { - rows: ["techA", "techB", "techC"], + rows: ["techA", "techB", "techC", techD], cols: ["i1", "i2", "i3"], rowLabel: key => key, colLabel: key => key, @@ -116,7 +119,7 @@ const renderList = (props: Partial> = {} describe("TaxonomyAxisList", () => { it("renders one accordion entry per visible primary group", () => { renderList(); - expect(screen.getAllByTestId("accordion-item"), "a row per technique").toHaveLength(3); + expect(screen.getAllByTestId("accordion-item"), "a row per technique").toHaveLength(4); }); it("renders a bar chart for every group and auto-shows detail for single-intent groups", () => { @@ -159,6 +162,43 @@ describe("TaxonomyAxisList", () => { ).toBeInTheDocument(); }); +it("shows a labelled taxonomy path breadcrumb in a technique group's expanded content", () => { + renderList(); + expect(screen.getAllByText("Taxonomy path").length, "labelled, not a bare tag chain").toBeGreaterThan( + 0 + ); + expect(screen.getByText("Fictionalizing"), "broadest branch segment").toBeInTheDocument(); + expect(screen.getByText("Roleplaying"), "middle branch segment").toBeInTheDocument(); +}); + +it("keeps the breadcrumb out of the always-visible trigger — it only shows once expanded", () => { + renderList(); + for (const trigger of screen.getAllByTestId("accordion-trigger")) { + expect( + within(trigger).queryByText("Fictionalizing"), + "breadcrumb segments must not render in the collapsed trigger" + ).toBeNull(); + } + expect(screen.getByText("Fictionalizing")).toBeInTheDocument(); +}); + + it("does not show a breadcrumb for flat (non-technique) group keys", () => { + renderList(); + // techA/techB/techC are flat mock keys with no demon: hierarchy, so their + // groups should contribute no breadcrumb segments — only techD's do. + expect(screen.getAllByText("Fictionalizing"), "only techD's group has a breadcrumb").toHaveLength( + 1 + ); + }); + + it("does not show a breadcrumb on the intent axis, since intent codes are flat", () => { + renderList({ axis: "intent" }); + expect( + screen.queryByText("Fictionalizing"), + "intent-axis groups (flat codes) never render technique breadcrumbs" + ).toBeNull(); + }); + it("supports the intent axis and alphabetical sort", () => { renderList({ axis: "intent", sortBy: "alphabetical" }); expect( diff --git a/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyPathBadges.test.tsx b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyPathBadges.test.tsx new file mode 100644 index 000000000..cea127c0c --- /dev/null +++ b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyPathBadges.test.tsx @@ -0,0 +1,47 @@ +/** + * @file TaxonomyPathBadges.test.tsx + * @description Verifies the breadcrumb rendering for a technique's full + * taxonomy path (Issue #1972): every branch segment renders as a + * tag, in order, and single-segment keys render nothing. + * + * @copyright NVIDIA Corporation 2023-2026 + * @license Apache-2.0 + */ + +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import TaxonomyPathBadges from "../TaxonomyPathBadges"; +import type { MockBadgeProps, MockFlexProps, MockTextProps } from "../../../test-utils/mockTypes"; + +// Reuse the same gray-outline Badge the Probes tag list renders with, so this +// test only asserts on content/order, not on KUI's internals. +vi.mock("@kui/react", () => ({ + Badge: ({ children, color, kind }: MockBadgeProps) => ( + + {children} + + ), + Flex: ({ children }: MockFlexProps) =>
{children}
, + Text: ({ children }: MockTextProps) => {children}, +})); + +describe("TaxonomyPathBadges", () => { + it("renders every branch segment, broadest first, down to the leaf", () => { + render(); + const badges = screen.getAllByTestId("badge"); + expect(badges.map(b => b.textContent)).toEqual(["Fictionalizing", "Roleplaying", "User_persona"]); + }); + + it("uses the same gray outline tag styling as the Probes tag list", () => { + render(); + for (const badge of screen.getAllByTestId("badge")) { + expect(badge.dataset.color).toBe("gray"); + expect(badge.dataset.kind).toBe("outline"); + } + }); + + it("renders nothing for a single-segment key (no hierarchy to show)", () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/garak-report/src/components/TechniqueIntent/index.ts b/garak-report/src/components/TechniqueIntent/index.ts index e1ae119ee..e08cedaaa 100644 --- a/garak-report/src/components/TechniqueIntent/index.ts +++ b/garak-report/src/components/TechniqueIntent/index.ts @@ -10,3 +10,4 @@ export { default as TechniqueIntentPanel } from "./TechniqueIntentPanel"; export type { TechniqueIntentPanelProps } from "./TechniqueIntentPanel"; export { default as TaxonomyAxisList } from "./TaxonomyAxisList"; +export { default as TaxonomyPathBadges } from "./TaxonomyPathBadges"; diff --git a/garak-report/src/components/index.ts b/garak-report/src/components/index.ts index 581be8c44..a27ea048c 100644 --- a/garak-report/src/components/index.ts +++ b/garak-report/src/components/index.ts @@ -28,7 +28,7 @@ export { default as ProbesChart } from "./ProbesChart"; export { default as DetectorsView } from "./DetectorsView"; // Technique/Intent taxonomy components -export { TechniqueIntentPanel, TaxonomyAxisList } from "./TechniqueIntent"; +export { TechniqueIntentPanel, TaxonomyAxisList, TaxonomyPathBadges } from "./TechniqueIntent"; export type { TechniqueIntentPanelProps } from "./TechniqueIntent"; // Subcomponent exports diff --git a/garak-report/src/utils/__tests__/taxonomyLabels.test.ts b/garak-report/src/utils/__tests__/taxonomyLabels.test.ts new file mode 100644 index 000000000..fa9b38759 --- /dev/null +++ b/garak-report/src/utils/__tests__/taxonomyLabels.test.ts @@ -0,0 +1,47 @@ +/** + * @file taxonomyLabels.test.ts + * @description Verifies the taxonomy label/path helpers, including the + * breadcrumb segments used to show a technique's full taxonomy + * path (Issue #1972). + * + * @copyright NVIDIA Corporation 2023-2026 + * @license Apache-2.0 + */ + +import { describe, expect, it } from "vitest"; +import { isTechniqueKey, shortenTechnique, techniquePathSegments } from "../taxonomyLabels"; + +describe("isTechniqueKey", () => { + it("recognizes a demon:-prefixed technique key", () => { + expect(isTechniqueKey("demon:Fictionalizing:Roleplaying:User_persona")).toBe(true); + }); + + it("rejects a flat intent code", () => { + expect(isTechniqueKey("T009ignore")).toBe(false); + }); +}); + +describe("techniquePathSegments", () => { + it("returns every branch from broadest to leaf, stripping the demon: prefix", () => { + expect( + techniquePathSegments("demon:Fictionalizing:Roleplaying:User_persona"), + "full path is preserved, unlike shortenTechnique's last-two truncation" + ).toEqual(["Fictionalizing", "Roleplaying", "User_persona"]); + }); + + it("handles a single-segment key", () => { + expect(techniquePathSegments("demon:Base64")).toEqual(["Base64"]); + }); + + it("handles a key with no demon: prefix by treating it as a bare path", () => { + expect(techniquePathSegments("Encoding:Base64")).toEqual(["Encoding", "Base64"]); + }); +}); + +describe("shortenTechnique (existing behavior, unchanged)", () => { + it("keeps only the two most specific segments", () => { + expect(shortenTechnique("demon:Fictionalizing:Roleplaying:User_persona")).toBe( + "Roleplaying:User_persona" + ); + }); +}); \ No newline at end of file diff --git a/garak-report/src/utils/taxonomyLabels.ts b/garak-report/src/utils/taxonomyLabels.ts index 07154d499..7b5a35fb5 100644 --- a/garak-report/src/utils/taxonomyLabels.ts +++ b/garak-report/src/utils/taxonomyLabels.ts @@ -46,3 +46,27 @@ export function shortenTechnique(key: string): string { const segments = stripped.split(":"); return segments.slice(-2).join(":"); } +/** + * Whether a taxonomy key is a hierarchical `demon:` technique key (as opposed + * to a flat intent code). Technique keys are the only axis that currently + * carries an explicit taxonomy path, so this gates the breadcrumb display. + */ +export function isTechniqueKey(key: string): boolean { + return key.startsWith("demon:"); +} + +/** + * Full taxonomy path for a hierarchical `demon:` technique key, from broadest + * branch to the specific leaf — the same segments {@link shortenTechnique} + * truncates to the last two, kept in full for breadcrumb display. + * + * @example + * techniquePathSegments("demon:Fictionalizing:Roleplaying:User_persona") + * // ["Fictionalizing", "Roleplaying", "User_persona"] + */ +export function techniquePathSegments(key: string): string[] { + return key + .replace(/^demon:/, "") + .split(":") + .filter(Boolean); +} \ No newline at end of file diff --git a/garak/analyze/ui/index.html b/garak/analyze/ui/index.html index 77f74b51e..f55e7126e 100644 --- a/garak/analyze/ui/index.html +++ b/garak/analyze/ui/index.html @@ -4,16 +4,16 @@ NVIDIA Garak - - + `},[r])}const dB=["#0074df","#3f8500","#a846db","#0d8473","#d73d00","#e52020"];function L1t(r,t,e,n){const{getDefconColor:a}=Mi(),i=e?ma.text.dark:ma.text.light,o=D1t(r),s=z.useMemo(()=>{const l=n??(()=>{const c=new Set;return r.forEach(h=>{const d=h.label.split(".")[0];d&&c.add(d)}),Array.from(c).sort()})(),u=new Map;return l.forEach((c,h)=>{u.set(c,dB[h%dB.length])}),u},[n,r]);return z.useMemo(()=>({grid:{containLabel:Pe.grid.containLabel,bottom:Pe.grid.bottom,left:Pe.grid.left,right:Pe.grid.right},tooltip:{trigger:"item",formatter:o,confine:!0},xAxis:{type:"category",data:r.map(l=>{const[,...u]=l.label.split(".");return u.join(".")}),triggerEvent:!0,axisLabel:{rotate:Pe.axis.labelRotation,interval:0,fontSize:Pe.axis.fontSize,color:i,rich:{selected1:{fontWeight:"bold",fontSize:Pe.axis.fontSize,color:a(1)},selected2:{fontWeight:"bold",fontSize:Pe.axis.fontSize,color:a(2)},selected3:{fontWeight:"bold",fontSize:Pe.axis.fontSize,color:a(3)},selected4:{fontWeight:"bold",fontSize:Pe.axis.fontSize,color:a(4)},selected5:{fontWeight:"bold",fontSize:Pe.axis.fontSize,color:a(5)},dimmed:{fontSize:Pe.axis.fontSize,color:i,opacity:tf.dimmed},...Object.fromEntries(Array.from(s.entries()).map(([l,u])=>[`dot_${l}`,{backgroundColor:u,width:8,height:8,borderRadius:4}]))},formatter:(l,u)=>{const c=r[u],h=t?.summary?.probe_name===c.summary?.probe_name,d=c.severity??0,p=s.size>1,g=c.label.split(".")[0],m=p?`{dot_${g}| } `:"";return t&&!h?`${m}{dimmed|${l}}`:h?`${m}{selected${d}|${l}}`:`${m}${l}`}},axisLine:{lineStyle:{color:i}}},yAxis:{type:"value",min:0,max:100,axisLabel:{color:i},axisLine:{lineStyle:{color:i}},splitLine:{lineStyle:{color:e?ma.chart.splitLine.dark:ma.chart.splitLine.light}}},series:[{type:"bar",barMinHeight:Pe.bar.minHeight,barMaxWidth:Pe.bar.maxWidth,data:r.map(l=>{const u=t?.summary?.probe_name===l.summary?.probe_name;return{name:l.label,value:l.value,label:{show:pS.show,position:pS.position,formatter:({value:c})=>Zd(c),fontSize:pS.fontSize,fontWeight:u?"bold":"normal",color:u?a(l.severity??0):i},itemStyle:{color:l.color,opacity:t?u?tf.full:tf.dimmed:tf.full}}}),barCategoryGap:Pe.bar.categoryGap}]}),[r,t,o,a,e,i,s])}const E1t=({probesData:r,selectedProbe:t,onProbeClick:e,allProbes:n,isDark:a,allModuleNames:i})=>{const o=L1t(r,t,a,i),s=l=>{let u=l.name;if(l.componentType==="xAxis"){const h=typeof l.value=="string"?l.value:String(l.value),d=n.find(p=>{const g=p.summary?.probe_name||"",[,...m]=g.split(".");return m.join(".")===h});if(d)u=d.summary?.probe_name;else{const p=n.find(g=>g.summary?.probe_name?.includes(h));p&&(u=p.summary?.probe_name)}}const c=n.find(h=>h.summary?.probe_name===u);c&&e(t?.summary?.probe_name===c.summary?.probe_name?null:c)};return E.jsx(i2,{option:o,onEvents:{click:s}})},pB=["blue","green","purple","teal","yellow","red"],R1t={blue:"var(--color-blue-500)",green:"var(--color-green-500)",purple:"var(--color-purple-500)",teal:"var(--color-teal-500)",yellow:"var(--color-yellow-500)",red:"var(--color-red-500)",gray:"var(--color-gray-500)"};function I1t(r){return pB[r%pB.length]}const O1t=({moduleNames:r,selectedModules:t,onSelectModule:e})=>{const n=r.length>1,a=t.length>0;return E.jsxs(Qt,{gap:"density-xs",paddingTop:"density-sm",paddingBottom:"density-md",children:[E.jsxs(Bt,{align:"center",gap:"density-xxs",children:[E.jsx(ht,{kind:"label/bold/sm",children:"Modules"}),E.jsx(js,{slotContent:E.jsxs(Qt,{gap:"density-xxs",children:[E.jsx(ht,{kind:"body/regular/sm",children:"Modules are probe families that group related security tests."}),n&&E.jsx(ht,{kind:"body/regular/sm",children:"Click to filter the chart by module. Multiple selections supported."})]}),children:E.jsx(Yn,{kind:"tertiary",children:E.jsx(j0,{size:14})})})]}),E.jsxs(Bt,{gap:"density-xs",wrap:"wrap",children:[r.map((i,o)=>{const s=t.includes(i),l=n?I1t(o):"gray",u=R1t[l];return E.jsx(Dr,{color:l,kind:s?"solid":"outline",onClick:n?()=>e(i):void 0,className:n?"cursor-pointer":"",children:n?E.jsxs(Bt,{align:"center",gap:"density-xxs",children:[E.jsx("span",{style:{width:8,height:8,borderRadius:"50%",backgroundColor:u,flexShrink:0}}),E.jsx(ht,{kind:"label/bold/sm",children:i})]}):E.jsx(ht,{kind:"label/bold/sm",children:i})},i)}),n&&a&&E.jsx(Dr,{color:"gray",kind:"outline",onClick:()=>t.forEach(i=>e(i)),className:"cursor-pointer",children:E.jsx(ht,{kind:"label/regular/sm",children:"Clear all"})})]})]})},P1t=({module:r,selectedProbe:t,setSelectedProbe:e,isDark:n})=>{const{getSeverityColorByLevel:a,getSeverityLabelByLevel:i}=Mi(),[o,s]=z.useState([]),l=z.useMemo(()=>[...r.probes].sort((g,m)=>{const _=(g.summary?.probe_name??g.probe_name).split(".").slice(1).join("."),x=(m.summary?.probe_name??m.probe_name).split(".").slice(1).join(".");return _.localeCompare(x)}).map(g=>{const m=g.summary?.probe_score??0,_=g.summary?.probe_name??g.probe_name,x=g.summary?.probe_severity;return{...g,label:_,value:m*100,color:a(x),severity:x,severityLabel:i(x)}}),[r,a,i]),u=z.useMemo(()=>{const p=new Set;return l.forEach(g=>{const m=g.label.split(".")[0];m&&p.add(m)}),Array.from(p).sort()},[l]),c=z.useMemo(()=>o.length===0?l:l.filter(p=>{const g=p.label.split(".")[0];return o.includes(g)}),[l,o]),h=p=>{s(g=>g.includes(p)?g.filter(m=>m!==p):[...g,p]),e(null)},d=z.useMemo(()=>{const p=new Set;return r.probes.forEach(g=>{g.summary?.probe_tags?.forEach(m=>p.add(m))}),Array.from(p).sort()},[r.probes]);return E.jsx(E.Fragment,{children:c.length===0?E.jsx("p",{className:"text-sm italic text-gray-500 py-8",children:"No probes meet the current filter."}):E.jsxs(Jd,{cols:t?2:1,children:[E.jsxs(Qt,{gap:"density-md",children:[E.jsx(A1t,{}),E.jsx(M1t,{tags:d}),E.jsx(O1t,{moduleNames:u,selectedModules:o,onSelectModule:h}),E.jsx(E1t,{probesData:c,selectedProbe:t,onProbeClick:e,allProbes:r.probes,isDark:n,allModuleNames:u})]}),t&&E.jsx(w1t,{probe:t,isDark:n,"data-testid":"detectors-view"})]})})},N1t=r=>{if(!r)return"Score";const t=r.replace(/_/g," ");return t.charAt(0).toUpperCase()+t.slice(1)},k1t=({modules:r,accordionKey:t,isDark:e})=>{const[n,a]=z.useState(null),[i,o]=z.useState(""),{getDefconBadgeColor:s}=Mi();return E.jsx(B0,{value:i,items:r.map(l=>({slotTrigger:E.jsxs(Bt,{direction:"row",gap:"density-lg",children:[E.jsxs(Bt,{direction:"col",gap:"density-sm",children:[E.jsx(js,{slotContent:E.jsxs(Qt,{gap:"density-xxs",children:[E.jsx(ht,{kind:"body/bold/sm",children:N1t(l.summary.group_aggregation_function)}),E.jsxs(ht,{kind:"body/regular/sm",children:["This score is the ",l.summary.group_aggregation_function?.replace(/_/g," ")||"aggregate"," of all probe scores in this module."]})]}),children:E.jsx(Dr,{color:s(l.summary.group_defcon),kind:"solid",className:"w-[70px]",children:E.jsxs(ht,{kind:"label/bold/xl",children:[(l.summary.score*100).toFixed(0),"%"]})})}),E.jsx(Dr,{color:s(l.summary.group_defcon),kind:"outline",className:"w-[70px]",children:E.jsxs(ht,{kind:"label/bold/md",children:["DC-",l.summary.group_defcon]})})]}),E.jsxs(Qt,{align:"start",gap:"density-md",children:[E.jsx(ht,{kind:"label/bold/2xl",children:l.summary.group||l.group_name}),l.summary.group_link?E.jsx(OA,{href:l.summary.group_link,target:"_blank",rel:"noopener noreferrer",children:E.jsx(ht,{dangerouslySetInnerHTML:{__html:l.summary.doc}})}):E.jsx(ht,{dangerouslySetInnerHTML:{__html:l.summary.doc}})]})]}),slotContent:E.jsx(Zl,{fallbackMessage:"Failed to load chart for this module.",children:E.jsx(P1t,{module:{...l,probes:l.probes??[]},setSelectedProbe:a,selectedProbe:n,isDark:e},`${t}-${l.group_name}`)}),value:l.group_name})),onValueChange:l=>{o(l),a(null)}},t)};function z1t(r,t){return t?.[r]?.name||void 0}function B1t(r,t){return t?.[r]?.descr?.trim()||void 0}function V1t(r){return r.replace(/^demon:/,"").split(":").slice(-2).join(":")}function o7(r){return r.startsWith("demon:")}function G1t(r){return r.replace(/^demon:/,"").split(":").filter(Boolean)}const H1t=r=>{const t=[];for(const e of Object.keys(r)){const n=r[e],a=n._summary;for(const i of Object.keys(n)){if(i==="_summary")continue;const o=n[i];!o||o.score==null||o.total_evaluated===0||t.push({technique:e,intent:i,techniqueName:a?.name??void 0,techniqueDescription:a?.description??void 0,intentName:o.name??void 0,score:o.score,nEvaluations:o.total_evaluated,nAttempts:o.n_attempts??0,passed:o.passed,nones:o.nones,nDetectors:o.n_detectors})}}return t};function j1t(r,t){const e=t==="technique"?r.rows:r.cols,n=t==="technique"?r.cols:r.rows,a=t==="technique"?r.rowLabel:r.colLabel,i=t==="technique"?r.colLabel:r.rowLabel,o=t==="technique"?r.rowDescription:r.colDescription,s=(u,c)=>t==="technique"?r.cell(u,c):r.cell(c,u),l=[];for(const u of e){const c=[];for(const h of n){const d=s(u,h);d&&c.push({otherKey:h,otherLabel:i(h),cell:d})}c.length&&(c.sort((h,d)=>h.cell.score-d.cell.score),l.push({key:u,label:a(u),description:o?.(u),score:Math.min(...c.map(h=>h.cell.score)),nEvaluations:c.reduce((h,d)=>h+d.cell.nEvaluations,0),nAttempts:c.reduce((h,d)=>h+d.cell.nAttempts,0),cells:c}))}return l}function F1t(r,t){const a=[];for(const l of r.rows)for(const u of r.cols){const c=r.cell(l,u);c&&a.push(c)}const i=new Map,o=new Map;for(const l of a)i.set(l.row,Math.max(i.get(l.row)??0,l.score)),o.set(l.col,Math.max(o.get(l.col)??0,l.score));const s=[];for(const l of a){const u=i.get(l.row)??0,c=o.get(l.col)??0,h=Math.min(u,c)-l.score;h>=.5&&s.push({rowKey:l.row,colKey:l.col,rowLabel:r.rowLabel(l.row),colLabel:r.colLabel(l.col),score:l.score,nEvaluations:l.nEvaluations,rowBest:u,colBest:c,gap:h})}return s.sort((l,u)=>u.gap-l.gap),s.slice(0,5)}function U1t(r,t){const e=H1t(r),n=new Map,a=new Map,i=new Map;for(const x of e)x.techniqueName&&n.set(x.technique,x.techniqueName),x.techniqueDescription&&a.set(x.technique,x.techniqueDescription),x.intentName&&i.set(x.intent,x.intentName);const o=x=>n.get(x)??V1t(x),s=x=>{const b=z1t(x,t)??i.get(x);return b?`${x} - ${b}`:x},l=x=>a.get(x),u=x=>B1t(x,t),c=new Map,h=(x,b)=>`${x}\0${b}`;for(const x of e)c.set(h(x.technique,x.intent),{row:x.technique,col:x.intent,score:x.score,nEvaluations:x.nEvaluations,nAttempts:x.nAttempts,passed:x.passed,nones:x.nones,nDetectors:x.nDetectors});const d=new Map,p=new Map;for(const x of c.values())d.set(x.row,Math.min(d.get(x.row)??1,x.score)),p.set(x.col,Math.min(p.get(x.col)??1,x.score));const g=x=>(b,w)=>{const T=(x.get(b)??1)-(x.get(w)??1);return T!==0?T:b.localeCompare(w)},m=[...d.keys()].sort(g(d)),_=[...p.keys()].sort(g(p));return{rows:m,cols:_,rowLabel:o,colLabel:s,rowDescription:l,colDescription:u,cell:(x,b)=>c.get(h(x,b))}}const Y1t=30;function W1t(r,t,e){const{getSeverityColorByLevel:n}=Mi(),a=e?ma.text.dark:ma.text.light,i=e?ma.chart.splitLine.dark:ma.chart.splitLine.light,o=z.useMemo(()=>({grid:{left:8,right:56,top:8,bottom:8,containLabel:!0},tooltip:{trigger:"item",confine:!0,formatter:l=>{const u=Array.isArray(l)?l[0]:l,c=r[u.dataIndex];return c?`${c.otherLabel}
${O_(c.cell.score)} · ${c.cell.nEvaluations.toLocaleString()} evals`:""}},xAxis:{type:"value",min:0,max:100,axisLabel:{formatter:"{value}%",color:a},splitLine:{lineStyle:{color:i}}},yAxis:{type:"category",inverse:!0,data:r.map(l=>l.otherLabel),axisTick:{show:!1},triggerEvent:!0,axisLabel:{color:a}},series:[{type:"bar",barMaxWidth:22,data:r.map(l=>{const u=!!t&&t!==l.otherKey;return{value:Math.round(l.cell.score*100),itemStyle:{color:n(yd(l.cell.score)),opacity:u?tf.dimmed:tf.full,borderRadius:2}}}),label:{show:!0,position:"right",formatter:"{c}%",color:a,fontWeight:600}}]}),[r,t,n,a,i]),s=Math.max(120,r.length*Y1t+32);return{option:o,height:s}}const X1t=({cells:r,isDark:t,selectedKey:e,onSelect:n})=>{const{option:a,height:i}=W1t(r,e,t),o=s=>{const l=s.componentType==="yAxis"?r.find(u=>u.otherLabel===s.value):r[s.dataIndex];l&&n(e===l.otherKey?null:l.otherKey)};return E.jsx(i2,{option:a,style:{height:i,width:"100%"},onEvents:{click:o}})},s7=({techniqueKey:r})=>{const t=G1t(r);return t.length<=1?null:E.jsx(Bt,{align:"center",gap:"density-xxs",wrap:"wrap",children:t.map((e,n)=>E.jsxs(Bt,{align:"center",gap:"density-xxs",children:[E.jsx(Dr,{color:"gray",kind:"outline",children:E.jsx(ht,{kind:"label/regular/xs",children:e})}),n{if(typeof r.scrollIntoView!="function")return;const t=()=>r.scrollIntoView({behavior:"smooth",block:"start"}),n=r.closest(".nv-accordion-content")?.getAnimations?.().find(a=>a.playState==="running");if(!n){t();return}n.finished.then(t).catch(t)},$1t={technique:"intent",intent:"technique"},Fw=(r,t)=>`${r} ${t}${r===1?"":"s"}`,q1t=({score:r,defcon:t})=>{const{getDefconBadgeColor:e}=Mi(),n=e(t);return E.jsxs(Bt,{direction:"col",gap:"density-sm",style:{flexShrink:0},children:[E.jsx(Dr,{color:n,kind:"solid",className:"w-[70px]",children:E.jsx(ht,{kind:"label/bold/xl",children:O_(r,0)})}),E.jsx(Dr,{color:n,kind:"outline",className:"w-[70px]",children:E.jsxs(ht,{kind:"label/bold/md",children:["DC-",t]})})]})},kv=({label:r,value:t})=>E.jsxs(Qt,{gap:"density-xxs",children:[E.jsx(ht,{kind:"label/regular/md",className:"opacity-60",children:r}),E.jsx(ht,{kind:"label/bold/xl",children:t})]}),K1t=({cell:r,title:t,taxonomyKey:e})=>{const{getSeverityLabelByLevel:n,getDefconBadgeColor:a}=Mi(),i=yd(r.score),o=r.score<1,s=Math.max(r.nEvaluations-r.passed-r.nones,0),l=r.nAttempts>0;return E.jsx(H0,{children:E.jsxs(Qt,{gap:"density-2xl",paddingY:"density-sm",children:[E.jsxs(Qt,{gap:"density-lg",children:[E.jsxs(Bt,{gap:"density-md",align:"center",wrap:"wrap",children:[E.jsx(vu,{defcon:i}),t&&E.jsx(ht,{kind:"title/sm",children:t}),E.jsx(Dr,{color:a(i),kind:"outline",children:E.jsx(ht,{kind:"label/bold/md",children:n(i)})})]}),e&&o7(e)&&E.jsxs(Qt,{gap:"density-xxs",children:[E.jsx(ht,{kind:"label/regular/md",className:"opacity-60",children:"Taxonomy path"}),E.jsx(s7,{techniqueKey:e})]}),E.jsxs(Bt,{gap:"density-2xl",wrap:"wrap",children:[E.jsx(kv,{label:"Pass rate",value:O_(r.score)}),E.jsx(kv,{label:"Passed",value:`${r.passed.toLocaleString()} of ${r.nEvaluations.toLocaleString()}`}),E.jsx(kv,{label:"Failed",value:s.toLocaleString()}),r.nones>0&&E.jsx(kv,{label:"Undetermined",value:r.nones.toLocaleString()}),l&&E.jsx(kv,{label:"Prompts",value:r.nAttempts.toLocaleString()})]}),E.jsx(ht,{kind:"label/regular/sm",className:"opacity-60",children:l?`${Fw(r.nAttempts,"prompt")} scored by ${Fw(r.nDetectors,"detector")} = ${r.nEvaluations.toLocaleString()} evaluations.`:`Counts are evaluations — one per prompt scored by each of ${Fw(r.nDetectors,"detector")}.`})]}),E.jsx(V0,{}),o?E.jsx(ht,{kind:"body/regular/md",className:"opacity-60",children:"A response counts as a failure when any detector flags it."}):E.jsx(Au,{size:"small",slotIcon:E.jsx(LQ,{size:20}),slotHeading:"No failures recorded",slotSubheading:"The target passed every evaluation for this pairing. There are no attempts to inspect."})]})})},Q1t=({group:r,childNoun:t,isDark:e,initialSelected:n,focusNonce:a})=>{const i=r.cells.length===1?r.cells[0].otherKey:null,[o,s]=z.useState(n??i),l=z.useRef(null),u=z.useRef(void 0);z.useEffect(()=>{n&&s(n)},[n]);const c=r.cells.find(h=>h.otherKey===o);return z.useEffect(()=>{!n||o!==n||a===void 0||u.current===a||!l.current||(u.current=a,Z1t(l.current))},[n,o,a]),E.jsxs(Qt,{gap:"density-md",paddingY:"density-sm",children:[o7(r.key)&&E.jsxs(Qt,{gap:"density-xxs",children:[E.jsx(ht,{kind:"label/regular/md",className:"opacity-60",children:"Taxonomy path"}),E.jsx(s7,{techniqueKey:r.key})]}),E.jsxs(ht,{kind:"label/regular/md",className:"opacity-60",children:["Pass rate by ",t,". Click a bar for the pass/fail breakdown."]}),E.jsxs(Jd,{cols:c?2:1,gap:"density-lg",className:"items-start",children:[E.jsx(X1t,{cells:r.cells,isDark:e,selectedKey:o,onSelect:s}),c&&E.jsx("div",{ref:l,style:{scrollMarginTop:"1rem"},children:E.jsx(K1t,{cell:c.cell,title:c.otherLabel,taxonomyKey:c.otherKey})})]})]})},J1t=({group:r,childNoun:t,isDark:e,initialSelected:n,focusNonce:a})=>E.jsx(Q1t,{group:r,childNoun:t,isDark:e,initialSelected:n,focusNonce:a}),gB=({view:r,axis:t,selectedDefcons:e,sortBy:n,openValue:a,onOpenChange:i,focusSecondaryKey:o,focusNonce:s,isDark:l})=>{const u=z.useMemo(()=>j1t(r,t),[r,t]),c=$1t[t],h=z.useMemo(()=>{const d=u.filter(p=>e.includes(yd(p.score)));return n==="alphabetical"?[...d].sort((p,g)=>p.label.localeCompare(g.label)):d},[u,e,n]);return h.length?E.jsx(B0,{value:a,onValueChange:d=>i(d),items:h.map(d=>{const p=yd(d.score);return{value:d.key,slotTrigger:E.jsxs(Bt,{gap:"density-lg",align:"center",style:{width:"100%"},children:[E.jsx(q1t,{score:d.score,defcon:p}),E.jsxs(Qt,{gap:"density-xs",align:"start",children:[E.jsx(ht,{kind:"label/bold/2xl",children:d.label}),d.description&&E.jsx(ht,{kind:"body/regular/md",className:"opacity-70",children:d.description})]})]}),slotContent:E.jsx(J1t,{group:d,childNoun:c,isDark:l,initialSelected:d.key===a?o:void 0,focusNonce:d.key===a?s:void 0})}})}):E.jsx(Au,{size:"small",slotHeading:`No ${t}s match the current filters`,slotSubheading:"Try enabling more DEFCON levels above."})},txt=r=>!!r&&Object.keys(r).length>0,ext=({items:r,onSelect:t})=>E.jsx(PA,{status:"warning",density:"spacious",slotHeading:"Notable pairings",slotSubheading:E.jsxs(Qt,{gap:"density-lg",children:[E.jsx(ht,{kind:"body/regular/sm",className:"opacity-70",children:"These combinations fail far worse than the technique or the intent does on its own — the kind of interaction worth a closer look. Select one to open it in the list below."}),E.jsx(Jd,{cols:{base:1,lg:2},gap:"density-md",children:r.map(e=>E.jsx("button",{type:"button",onClick:()=>t(e),className:"w-full cursor-pointer rounded text-left transition-opacity hover:opacity-70",style:{background:"none",border:0,padding:0},children:E.jsxs(Bt,{align:"center",gap:"density-sm",children:[E.jsx(vu,{defcon:yd(e.score)}),E.jsx(ht,{kind:"label/bold/md",children:O_(e.score)}),E.jsxs(ht,{kind:"body/regular/md",children:[e.rowLabel," × ",e.colLabel]})]})},`${e.rowKey}\0${e.colKey}`))})]})}),rxt=({techniqueIntent:r,intentTypology:t,isDark:e})=>{const[n,a]=z.useState([...kA]),[i,o]=z.useState("defcon"),[s,l]=z.useState("technique"),[u,c]=z.useState(""),[h,d]=z.useState(""),[p,g]=z.useState(""),[m,_]=z.useState(0),x=z.useMemo(()=>U1t(r??{},t),[r,t]),b=txt(r),w=z.useMemo(()=>F1t(x),[x]),T=z.useCallback(R=>{a(I=>I.includes(R)?I.filter(O=>O!==R):[...I,R])},[]),A=z.useCallback(R=>{c(R),g("")},[]),M=z.useCallback(R=>{l("technique"),c(R.rowKey),g(R.colKey),_(I=>I+1)},[]);if(!b)return E.jsx(Au,{size:"medium",slotHeading:"No technique/intent data in this report",slotSubheading:"This report was generated without technique and intent taxonomy tags."});const L=[{value:"technique",children:"By technique",slotContent:E.jsx(Bt,{direction:"col",style:{width:"100%"},children:E.jsx(Zl,{fallbackMessage:"Failed to load the technique list.",children:E.jsx(gB,{view:x,axis:"technique",selectedDefcons:n,sortBy:i,openValue:u,onOpenChange:A,focusSecondaryKey:p,focusNonce:m,isDark:e})})})},{value:"intent",children:"By intent",slotContent:E.jsx(Bt,{direction:"col",style:{width:"100%"},children:E.jsx(Zl,{fallbackMessage:"Failed to load the intent list.",children:E.jsx(gB,{view:x,axis:"intent",selectedDefcons:n,sortBy:i,openValue:h,onOpenChange:d,isDark:e})})})}];return E.jsxs(Bt,{direction:"col",gap:"density-2xl",style:{width:"100%"},children:[w.length>0&&E.jsx(ext,{items:w,onSelect:M}),E.jsxs(Bt,{direction:"col",gap:"density-sm",children:[E.jsx(pG,{selectedDefcons:n,onToggleDefcon:T,sortBy:i,onSortChange:o}),E.jsx(Bf,{value:s,onValueChange:R=>l(R),items:L})]})]})};function nxt(r){return typeof r=="object"&&r!==null&&"_summary"in r&&typeof r._summary=="object"}function axt(r){if(typeof r!="object"||r===null||!("_summary"in r))return!1;const t=r._summary;return typeof t=="object"&&t!==null&&"probe_name"in t}function ixt(r){return typeof r=="object"&&r!==null&&!("_summary"in r)&&("absolute_score"in r||"detector_name"in r)}function yB(r){return typeof r=="object"&&r!==null}function oxt(r,t){return t.absolute_score==null?null:{detector_name:r,detector_descr:t.detector_descr??"",absolute_score:t.absolute_score,absolute_defcon:t.absolute_defcon??5,absolute_comment:t.absolute_comment??"",relative_score:t.relative_score??0,relative_defcon:t.relative_defcon??5,relative_comment:t.relative_comment??"",detector_defcon:t.detector_defcon??5,calibration_used:t.calibration_used??!1,total_evaluated:t.total_evaluated??t.attempt_count,hit_count:t.hit_count,passed:t.passed,attempt_count:t.attempt_count}}function sxt(r){return z.useMemo(()=>{if(!r)return[];const t=r.meta?.setup,e=yB(t)?!!(t["reporting.show_100_pass_modules"]??!1):!1,n=yB(t)?!!(t["reporting.show_top_group_score"]??!0):!0,a=!!r.meta?.aggregation_unknown,i=[];return Object.entries(r.eval??{}).forEach(([o,s])=>{if(!nxt(s))return;const l=s,u={...l._summary,unrecognised_aggregation_function:a,show_top_group_score:n};if(u.score<1||e){const c={group_name:o,summary:u,probes:[]};Object.entries(l).forEach(([h,d])=>{if(h==="_summary"||!axt(d))return;const p=d,g=p._summary,m={probe_name:h,summary:g,detectors:[]};Object.entries(p).forEach(([_,x])=>{if(_==="_summary"||!ixt(x))return;const b=oxt(_,x);b&&(b.absolute_score<1||e)&&m.detectors.push(b)}),c.probes.push(m)}),i.push(c)}}),i},[r])}const Uw=typeof __GARAK_INSERT_HERE__<"u"?__GARAK_INSERT_HERE__:[];function lxt(){const[r,t]=z.useState(null),[e,n]=z.useState(null),[a,i]=z.useState(null);return z.useEffect(()=>{Array.isArray(Uw)&&Uw.length>0?t(Uw[0]):window.reportsData&&Array.isArray(window.reportsData)&&t(window.reportsData[0])},[]),z.useEffect(()=>{n(r?.meta.calibration||null),i(r?.meta.setup||null)},[r]),{selectedReport:r,calibrationData:e,setupData:a}}function uxt(r){const[t,e]=z.useState([...kA]),[n,a]=z.useState("defcon"),i=z.useCallback(s=>{e(l=>l.includes(s)?l.filter(u=>u!==s):[...l,s].sort())},[]);return{modules:z.useMemo(()=>{let s=r.filter(l=>t.includes(l.summary.group_defcon));return n==="defcon"?s=s.sort((l,u)=>l.summary.group_defcon-u.summary.group_defcon):s=s.sort((l,u)=>l.group_name.localeCompare(u.group_name)),s},[r,t,n]),selectedDefcons:t,sortBy:n,toggleDefcon:i,setSortBy:a}}function cxt(r,t){const e=z.useMemo(()=>r==="dark"?!0:r==="light"?!1:typeof window<"u"?window.matchMedia("(prefers-color-scheme: dark)").matches:!1,[r]),n=z.useCallback(()=>{t&&t(e?"light":"dark")},[e,t]);return{isDark:e,toggleTheme:n}}function fxt({onThemeChange:r,currentTheme:t="system"}){const{selectedReport:e,calibrationData:n,setupData:a}=lxt(),i=sxt(e),{modules:o,selectedDefcons:s,sortBy:l,toggleDefcon:u,setSortBy:c}=uxt(i),{isDark:h,toggleTheme:d}=cxt(t,r),p=!!(e?.technique_intent_matrix&&Object.keys(e.technique_intent_matrix).length>0);if(z.useEffect(()=>{const _=e?.meta?.target_name||e?.meta?.model_name||a?.["plugins.model_name"]||null;return document.title=_?`NVIDIA Garak - ${_}`:"NVIDIA Garak",()=>{document.title="NVIDIA Garak"}},[e?.meta?.target_name,e?.meta?.model_name,a]),!e)return E.jsx(Bt,{style:{height:"100vh",width:"100vw"},align:"center",justify:"center",children:E.jsx(iG,{size:"medium",description:"Loading reports..."})});const g=E.jsxs(Bt,{direction:"col",style:{width:"100%"},children:[E.jsx(pG,{selectedDefcons:s,onToggleDefcon:u,sortBy:l,onSortChange:c}),o.length>0?E.jsx(Zl,{fallbackMessage:"Failed to load modules. Please refresh the page.",children:E.jsx(k1t,{modules:o,accordionKey:e?.meta.run_uuid??"default",isDark:h})}):E.jsx(Au,{slotMedia:E.jsx("i",{className:"nv-icons-line-warning"}),slotHeading:"No modules found in this report",slotSubheading:"Try changing the filters or sorting options"})]}),m=E.jsx(Zl,{fallbackMessage:"Failed to load technique/intent analysis.",children:E.jsx(Bt,{direction:"col",style:{width:"100%"},children:E.jsx(rxt,{techniqueIntent:e.technique_intent_matrix,intentTypology:e.intent_typology,isDark:h})})});return E.jsxs(Bt,{direction:"col",style:{minHeight:"100vh"},children:[E.jsx(PQ,{onThemeToggle:d,isDark:h}),E.jsxs(Bt,{direction:"col",style:{flex:1},children:[E.jsxs(Jd,{cols:{base:1,md:2},gap:"density-lg",padding:"density-lg",children:[E.jsx(Zl,{fallbackMessage:"Failed to load report details. Please refresh the page.",children:E.jsx(jQ,{setupData:a,calibrationData:n,meta:e.meta})}),E.jsx(Zl,{fallbackMessage:"Failed to load summary statistics. Please refresh the page.",children:E.jsx(FQ,{modules:i})})]}),p?E.jsx(Bf,{defaultValue:"modules",items:[{value:"modules",children:"Modules",slotContent:g},{value:"techniques",children:"Techniques & Intents",slotContent:m}]}):g]}),E.jsx(yQ,{})]})}function hxt(){const[r,t]=z.useState("system");z.useEffect(()=>{const n=localStorage.getItem("kui-theme");n&&t(n)},[]);const e=n=>{t(n),localStorage.setItem("kui-theme",n)};return z.useEffect(()=>{const n=document.documentElement;if(n.classList.remove("nv-dark","nv-light"),r==="dark")n.classList.add("nv-dark");else if(r==="light")n.classList.add("nv-light");else if(r==="system"){const a=window.matchMedia("(prefers-color-scheme: dark)").matches;n.classList.add(a?"nv-dark":"nv-light")}},[r]),E.jsx(vG,{theme:r,children:E.jsx(fxt,{onThemeChange:e,currentTheme:r})})}zY.createRoot(document.getElementById("root")).render(E.jsx(z.StrictMode,{children:E.jsx(hxt,{})})); +