From e3695bfa5bdf25b73a79c0516b22467fc2353640 Mon Sep 17 00:00:00 2001 From: AbdaullahAG Date: Sat, 25 Jul 2026 21:40:36 +0300 Subject: [PATCH 1/3] feat(taxonomy): show full breadcrumb path for technique leaves (#1972) Signed-off-by: AbdaullahAG --- .../TechniqueIntent/TaxonomyAxisList.tsx | 23 +++++++- .../TechniqueIntent/TaxonomyPathBadges.tsx | 55 +++++++++++++++++++ .../__tests__/TaxonomyAxisList.test.tsx | 34 +++++++++++- .../__tests__/TaxonomyPathBadges.test.tsx | 47 ++++++++++++++++ .../src/components/TechniqueIntent/index.ts | 1 + garak-report/src/components/index.ts | 2 +- .../utils/__tests__/taxonomyLabels.test.ts | 47 ++++++++++++++++ garak-report/src/utils/taxonomyLabels.ts | 24 ++++++++ 8 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 garak-report/src/components/TechniqueIntent/TaxonomyPathBadges.tsx create mode 100644 garak-report/src/components/TechniqueIntent/__tests__/TaxonomyPathBadges.test.tsx create mode 100644 garak-report/src/utils/__tests__/taxonomyLabels.test.ts diff --git a/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx b/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx index a3a3625aa..cb42c6c1d 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,9 @@ const CellDetail = ({ cell, title }: { cell: MatrixCell; title?: string }) => { {getSeverityLabelByLevel(defcon)} + {taxonomyKey && isTechniqueKey(taxonomyKey) && ( + + )} {selectedEntry && (
- +
)} @@ -317,6 +335,7 @@ const TaxonomyAxisList = ({ {group.label} + {isTechniqueKey(group.key) && } {group.description && ( {group.description} 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..3a4637384 100644 --- a/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx +++ b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx @@ -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,31 @@ describe("TaxonomyAxisList", () => { ).toBeInTheDocument(); }); + it("shows the taxonomy path breadcrumb under a technique group's label", () => { + renderList(); + // techD = "demon:Fictionalizing:Roleplaying:User_persona" — the breadcrumb + // should surface every branch segment as its own tag, not just the label. + expect(screen.getByText("Fictionalizing"), "broadest branch segment").toBeInTheDocument(); + expect(screen.getByText("Roleplaying"), "middle branch segment").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 From 79a78ff7f380f0172107626e629eb74e663f154a Mon Sep 17 00:00:00 2001 From: AbdaullahAG Date: Mon, 27 Jul 2026 18:05:23 +0300 Subject: [PATCH 2/3] chore: rebuild report HTML index for technique breadcrumb (#1972) Signed-off-by: AbdaullahAG --- garak/analyze/ui/index.html | 111 ------------------------------------ 1 file changed, 111 deletions(-) delete mode 100644 garak/analyze/ui/index.html diff --git a/garak/analyze/ui/index.html b/garak/analyze/ui/index.html deleted file mode 100644 index 77f74b51e..000000000 --- a/garak/analyze/ui/index.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - NVIDIA Garak - - - - -
- - From b202983e438ec728ffa82cfedfdada24dc25d46a Mon Sep 17 00:00:00 2001 From: AbdaullahAG Date: Sat, 1 Aug 2026 01:16:32 +0300 Subject: [PATCH 3/3] fix(taxonomy): move breadcrumb to expanded content with a label, per review Signed-off-by: AbdaullahAG --- .../TechniqueIntent/TaxonomyAxisList.tsx | 16 ++- .../__tests__/TaxonomyAxisList.test.tsx | 28 +++-- garak/analyze/ui/index.html | 111 ++++++++++++++++++ 3 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 garak/analyze/ui/index.html diff --git a/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx b/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx index cb42c6c1d..dacc8a3d2 100644 --- a/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx +++ b/garak-report/src/components/TechniqueIntent/TaxonomyAxisList.tsx @@ -151,7 +151,12 @@ const CellDetail = ({
{taxonomyKey && isTechniqueKey(taxonomyKey) && ( - + + + Taxonomy path + + + )} @@ -234,6 +239,14 @@ const GroupChildrenChart = ({ }, [initialSelected, selected, focusNonce]); return ( + {isTechniqueKey(group.key) && ( + + + Taxonomy path + + + + )} Pass rate by {childNoun}. Click a bar for the pass/fail breakdown. @@ -335,7 +348,6 @@ const TaxonomyAxisList = ({ {group.label} - {isTechniqueKey(group.key) && } {group.description && ( {group.description} diff --git a/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx b/garak-report/src/components/TechniqueIntent/__tests__/TaxonomyAxisList.test.tsx index 3a4637384..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"; @@ -162,13 +162,25 @@ describe("TaxonomyAxisList", () => { ).toBeInTheDocument(); }); - it("shows the taxonomy path breadcrumb under a technique group's label", () => { - renderList(); - // techD = "demon:Fictionalizing:Roleplaying:User_persona" — the breadcrumb - // should surface every branch segment as its own tag, not just the label. - expect(screen.getByText("Fictionalizing"), "broadest branch segment").toBeInTheDocument(); - expect(screen.getByText("Roleplaying"), "middle branch segment").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(); diff --git a/garak/analyze/ui/index.html b/garak/analyze/ui/index.html new file mode 100644 index 000000000..f55e7126e --- /dev/null +++ b/garak/analyze/ui/index.html @@ -0,0 +1,111 @@ + + + + + + NVIDIA Garak + + + + +
+ +