Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,6 @@ jobs:
- name: Find dead code and dependencies
run: pnpm -w knip

# Informational until the pending dead-code removals land; then these
# categories move into the blocking `pnpm -w knip` step above. Findings
# exit 0, so they do not add a failure annotation to every run.
- name: Report unused exports
continue-on-error: true
run: pnpm -w knip --include exports,types,duplicates --no-exit-code

- name: Build
run: pnpm -w build

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"docs:check": "node scripts/audit-profile-docs.mjs --check && node scripts/prompt-catalog-docs.mjs --check && node scripts/docs-check.mjs",
"format": "prettier --write .",
"format:check": "prettier --check .",
"knip": "pnpm dlx knip@6.33.0 --include files,dependencies,unlisted,unresolved --treat-config-hints-as-errors",
"knip": "pnpm dlx knip@6.33.0 --include files,dependencies,unlisted,unresolved,exports,types,duplicates --treat-config-hints-as-errors",
"lint": "eslint \"packages/**/*.{ts,tsx,js,mjs,cjs}\" \"scripts/**/*.{ts,tsx,js,mjs,cjs}\" \"*.{js,mjs,cjs}\"",
"lint:fix": "eslint \"packages/**/*.{ts,tsx,js,mjs,cjs}\" \"scripts/**/*.{ts,tsx,js,mjs,cjs}\" \"*.{js,mjs,cjs}\" --fix",
"lint:strict": "ULTRAFUZZ_STRICT_LINT=1 eslint \"packages/**/*.{ts,tsx,js,mjs,cjs}\" \"scripts/**/*.{ts,tsx,js,mjs,cjs}\" \"*.{js,mjs,cjs}\" --max-warnings 0",
Expand Down
3 changes: 1 addition & 2 deletions packages/artifacts/src/findings-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,7 @@ export const findingReportBoundTextSchema = nonEmptyString
if (issue !== undefined) context.addIssue({ code: "custom", message: issue.message });
})
.meta({ id: "findingReportBoundText", allOf: findingNoteJsonSchemaConstraints });
export const findingNoteSchema = findingReportBoundTextSchema;
const findingNotesSchema = z.array(findingNoteSchema).max(MAX_FINDING_NESTED_ITEMS);
const findingNotesSchema = z.array(findingReportBoundTextSchema).max(MAX_FINDING_NESTED_ITEMS);
const uniqueNonEmptyStrings = z
.array(nonEmptyString)
.max(MAX_FINDING_NESTED_ITEMS)
Expand Down
9 changes: 4 additions & 5 deletions packages/artifacts/src/workflow-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
MAX_FINDING_NESTED_ITEMS,
findingLifecycleSchema,
findingLifecycleStageSchema,
findingNoteSchema,
findingReportBoundTextSchema,
findingSchema,
findingStrategyHitSchema,
Expand Down Expand Up @@ -170,7 +169,7 @@ export const strategyDetectionsSchema = withDocumentMetadata(
const triagedFindingSchema = findingSchema
.safeExtend({
triage_classification: z.enum(TRIAGE_CLASSIFICATIONS),
notes: z.array(findingNoteSchema).min(1).max(MAX_FINDING_NESTED_ITEMS)
notes: z.array(findingReportBoundTextSchema).min(1).max(MAX_FINDING_NESTED_ITEMS)
})
.meta({
allOf: [
Expand Down Expand Up @@ -243,7 +242,7 @@ const severityClassifiedFindingSchema = findingSchema
// Severity review must preserve the already-validated triage notes byte for
// byte, but the copied values remain inside the report-vocabulary trust
// boundary and are therefore validated again at this contract boundary.
notes: z.array(findingNoteSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
notes: z.array(findingReportBoundTextSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
severity: z.enum(FINDING_SEVERITIES).optional(),
impact: z.enum(FINDING_SEVERITIES).optional(),
likelihood: z.enum(FINDING_SEVERITIES).optional(),
Expand Down Expand Up @@ -1613,7 +1612,7 @@ const reportCoverageUnavailableSchema = z.strictObject({
});

const reportIssueSchema = findingSchema.safeExtend({
notes: z.array(findingNoteSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
notes: z.array(findingReportBoundTextSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
description: nonEmptyString,
severity: z.enum(FINDING_SEVERITIES),
likelihood: z.enum(FINDING_SEVERITIES),
Expand All @@ -1630,7 +1629,7 @@ const reportIssueSchema = findingSchema.safeExtend({
});

const reportNonProductionOutcomeSchema = findingSchema.safeExtend({
notes: z.array(findingNoteSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
notes: z.array(findingReportBoundTextSchema).max(MAX_FINDING_NESTED_ITEMS).optional(),
triage_classification: z.enum(TRIAGE_CLASSIFICATIONS),
recommended_next_action: nonEmptyString,
lifecycle: findingLifecycleSchema
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/benchmark-analysis/lib/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function validateZipEntryNames(entries: readonly AdmZip.IZipEntry[]): ReadonlyMa
return byName;
}

export function assertCanonicalRelativePath(value: string): void {
function assertCanonicalRelativePath(value: string): void {
if (
!/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/u.test(value) ||
value.split("/").some((segment) => segment === "." || segment === "..")
Expand Down
129 changes: 76 additions & 53 deletions packages/cli/src/benchmark-analysis/lib/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,7 @@ export async function buildScoreChart(result: AnalysisResult, outputDir: string)
return writeSvgAndPng(svgDocument(width, height, parts.join("\n")), svgPath);
}

export async function buildPairwiseChart(result: AnalysisResult, outputDir: string): Promise<string[]> {
const pairs = result.pairComparison;
if (pairs.length === 0) throw new Error("No Ultrafuzz/no-fuzz row pairs are available for pairwise analysis");

function pairwisePanels(pairs: PairComparison[]): PairwisePanel[] {
const creditMaximum = niceAxisMaximum(
Math.max(
1,
Expand All @@ -359,7 +356,7 @@ export async function buildPairwiseChart(result: AnalysisResult, outputDir: stri
Math.max(1, ...pairs.flatMap((pair) => [pair.ultrafuzz.totalTokensMillions, pair.noFuzz.totalTokensMillions])),
4
);
const panels: PairwisePanel[] = [
return [
{
title: "Ground-truth TP credits",
note: "Higher is better",
Expand Down Expand Up @@ -390,7 +387,79 @@ export async function buildPairwiseChart(result: AnalysisResult, outputDir: stri
formatTick: (value) => value.toFixed(0)
}
];
}

interface PairwiseLayout {
panelWidth: number;
rowTop: number;
rowStep: number;
axisTop: number;
axisBottom: number;
}

function pairwisePanelParts(
panel: PairwisePanel,
left: number,
pairs: PairComparison[],
layout: PairwiseLayout
): string[] {
const { panelWidth, rowTop, rowStep, axisTop, axisBottom } = layout;
const parts: string[] = [];
const right = left + panelWidth;
const xAt = (value: number) => left + (value / panel.maximum) * panelWidth;
parts.push(text(left, 172, panel.title, { "font-size": 17, "font-weight": 650 }));
parts.push(text(right, 195, panel.note, { "font-size": 12, fill: "#666", "text-anchor": "end" }));

for (let tick = 0; tick <= panel.divisions; tick += 1) {
const value = (panel.maximum * tick) / panel.divisions;
const x = xAt(value);
parts.push(line(x, axisTop, x, axisBottom, { stroke: tick === 0 ? "#999" : "#e1e1e1" }));
parts.push(
text(x, axisBottom + 28, panel.formatTick(value), {
"font-size": 11,
"text-anchor": "middle",
fill: "#666"
})
);
}

pairs.forEach((pair, pairIndex) => {
const y = rowTop + pairIndex * rowStep;
const ultrafuzz = panel.value(pair, "ultrafuzz");
const noFuzz = panel.value(pair, "no-fuzz");
if (ultrafuzz === null || noFuzz === null) {
parts.push(text((left + right) / 2, y + 5, "NA", { "font-size": 12, "text-anchor": "middle", fill: "#777" }));
return;
}
const ultrafuzzX = xAt(ultrafuzz);
const noFuzzX = xAt(noFuzz);
parts.push(line(ultrafuzzX, y, noFuzzX, y, { stroke: "#777", "stroke-width": 2 }));
parts.push(circle(ultrafuzzX, y, 8, CONDITION_STYLES[0]));
parts.push(markerSquare(noFuzzX, y, 8, CONDITION_STYLES[1]));
parts.push(
text(ultrafuzzX, y - 15, panel.format(ultrafuzz), {
"font-size": 11,
"font-weight": 600,
"text-anchor": "middle"
})
);
parts.push(
text(noFuzzX, y + 25, panel.format(noFuzz), {
"font-size": 11,
"font-weight": 600,
"text-anchor": "middle",
fill: "#555"
})
);
});
return parts;
}

async function buildPairwiseChart(result: AnalysisResult, outputDir: string): Promise<string[]> {
const pairs = result.pairComparison;
if (pairs.length === 0) throw new Error("No Ultrafuzz/no-fuzz row pairs are available for pairwise analysis");

const panels = pairwisePanels(pairs);
const width = 1600;
const panelLeft = [145, 655, 1165];
const panelWidth = 375;
Expand Down Expand Up @@ -426,54 +495,8 @@ export async function buildPairwiseChart(result: AnalysisResult, outputDir: stri
});

panels.forEach((panel, panelIndex) => {
const left = panelLeft[panelIndex] ?? 145;
const right = left + panelWidth;
const xAt = (value: number) => left + (value / panel.maximum) * panelWidth;
parts.push(text(left, 172, panel.title, { "font-size": 17, "font-weight": 650 }));
parts.push(text(right, 195, panel.note, { "font-size": 12, fill: "#666", "text-anchor": "end" }));

for (let tick = 0; tick <= panel.divisions; tick += 1) {
const value = (panel.maximum * tick) / panel.divisions;
const x = xAt(value);
parts.push(line(x, axisTop, x, axisBottom, { stroke: tick === 0 ? "#999" : "#e1e1e1" }));
parts.push(
text(x, axisBottom + 28, panel.formatTick(value), {
"font-size": 11,
"text-anchor": "middle",
fill: "#666"
})
);
}

pairs.forEach((pair, pairIndex) => {
const y = rowTop + pairIndex * rowStep;
const ultrafuzz = panel.value(pair, "ultrafuzz");
const noFuzz = panel.value(pair, "no-fuzz");
if (ultrafuzz === null || noFuzz === null) {
parts.push(text((left + right) / 2, y + 5, "NA", { "font-size": 12, "text-anchor": "middle", fill: "#777" }));
return;
}
const ultrafuzzX = xAt(ultrafuzz);
const noFuzzX = xAt(noFuzz);
parts.push(line(ultrafuzzX, y, noFuzzX, y, { stroke: "#777", "stroke-width": 2 }));
parts.push(circle(ultrafuzzX, y, 8, CONDITION_STYLES[0]));
parts.push(markerSquare(noFuzzX, y, 8, CONDITION_STYLES[1]));
parts.push(
text(ultrafuzzX, y - 15, panel.format(ultrafuzz), {
"font-size": 11,
"font-weight": 600,
"text-anchor": "middle"
})
);
parts.push(
text(noFuzzX, y + 25, panel.format(noFuzz), {
"font-size": 11,
"font-weight": 600,
"text-anchor": "middle",
fill: "#555"
})
);
});
const layout = { panelWidth, rowTop, rowStep, axisTop, axisBottom };
parts.push(...pairwisePanelParts(panel, panelLeft[panelIndex] ?? 145, pairs, layout));
});

parts.push(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/benchmark-analysis/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function csvEscape(value: unknown): string {
function csvEscape(value: unknown): string {
if (value === null || value === undefined || (typeof value === "number" && !Number.isFinite(value))) return "";
const raw = Array.isArray(value)
? value.join("; ")
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/benchmark-analysis/lib/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function mean(values: number[]): number | null {
return values.length ? sum(values) / values.length : null;
}

export function median(values: number[]): number | null {
function median(values: number[]): number | null {
if (!values.length) return null;
const ordered = [...values].sort((a, b) => a - b);
const middle = Math.floor(ordered.length / 2);
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/benchmark-analysis/lib/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Resvg } from "@resvg/resvg-js";

import { escapeXml } from "./format.js";

export const FONT = "Inter, Arial, Helvetica, sans-serif";
const FONT = "Inter, Arial, Helvetica, sans-serif";

export function attrs(values: Record<string, string | number | boolean | null | undefined>): string {
function attrs(values: Record<string, string | number | boolean | null | undefined>): string {
return Object.entries(values)
.filter(([, value]) => value !== null && value !== undefined && value !== false)
.map(([key, value]) => `${key.replaceAll("_", "-")}="${escapeXml(value === true ? key : value)}"`)
Expand Down Expand Up @@ -66,7 +66,7 @@ export function svgDocument(width: number, height: number, content: string): str
return `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img">\n<rect width="100%" height="100%" fill="#fff"/>\n${content}\n</svg>\n`;
}

export async function writeSvg(svg: string, svgPath: string): Promise<void> {
async function writeSvg(svg: string, svgPath: string): Promise<void> {
await writeFile(svgPath, svg, "utf8");
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/benchmark-analysis/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const SEVERITY_ORDER = ["H", "M", "L"] as const;

export type Severity = (typeof SEVERITY_ORDER)[number];
export type Classification = "true-positive" | "false-positive" | "needs-human-review";
type Classification = "true-positive" | "false-positive" | "needs-human-review";
export type AnalysisCommandName = "upset" | "scores" | "provenance" | "table" | "cost" | "pairwise" | "all";

export interface BenchmarkRow {
Expand Down
Loading
Loading