diff --git a/backend/apps/benchmarks/api.py b/backend/apps/benchmarks/api.py
index 34ef928..5969939 100644
--- a/backend/apps/benchmarks/api.py
+++ b/backend/apps/benchmarks/api.py
@@ -257,7 +257,12 @@ def get_benchmark_case(request: HttpRequest, suite: str, case_id: str, revision:
raise HttpError(404, f"Revision {target.revision} has no public case {case_id!r}.")
runs = leaderboard.latest_per_tool_and_case(leaderboard.approved_runs(target), case_id=case.pk)
with_public_screenshots(runs)
- return {**_case_out(case), "revision": target.revision, "runs": [_run_out(r) for r in runs]}
+ return {
+ **_case_out(case),
+ "revision": target.revision,
+ "revision_is_current": target.is_current,
+ "runs": [_run_out(r) for r in runs],
+ }
@router.get(
@@ -298,6 +303,7 @@ def get_benchmark_tool_report(
"tool": leaderboard.tool_ref(tool),
"suite": suite,
"revision": target.revision,
+ "revision_is_current": target.is_current,
"scope": scope,
"case_count": target.cases.count(),
"surfaces": surfaces,
@@ -331,6 +337,7 @@ def get_benchmark_run(request: HttpRequest, run_id: str):
"tier": submission.tier,
"notes": submission.notes,
"revision": run.case.revision.revision,
+ "revision_is_current": run.case.revision.is_current,
}
diff --git a/backend/apps/benchmarks/schemas.py b/backend/apps/benchmarks/schemas.py
index 1ba2e71..f05db6a 100644
--- a/backend/apps/benchmarks/schemas.py
+++ b/backend/apps/benchmarks/schemas.py
@@ -168,10 +168,14 @@ class RunDetailOut(RunOut):
tier: str
notes: str
revision: str
+ #: False for a superseded revision - the site links only those with `?revision=`.
+ revision_is_current: bool
class CaseDetailOut(CaseOut):
revision: str
+ #: False for a superseded revision - the site links only those with `?revision=`.
+ revision_is_current: bool
runs: list[RunOut]
@@ -187,6 +191,8 @@ class ToolReportOut(Schema):
tool: ToolRefOut
suite: str
revision: str
+ #: False for a superseded revision - the site links only those with `?revision=`.
+ revision_is_current: bool
scope: Literal["all", "verified"]
case_count: int
surfaces: list[ToolSurfaceOut]
diff --git a/backend/openapi.json b/backend/openapi.json
index 81a1ef3..5128843 100644
--- a/backend/openapi.json
+++ b/backend/openapi.json
@@ -7198,6 +7198,10 @@
"title": "Revision",
"type": "string"
},
+ "revision_is_current": {
+ "title": "Revision Is Current",
+ "type": "boolean"
+ },
"runs": {
"items": {
"$ref": "#/components/schemas/RunOut"
@@ -7215,6 +7219,7 @@
"pdf_url",
"preview",
"revision",
+ "revision_is_current",
"runs"
],
"title": "CaseDetailOut",
@@ -7482,6 +7487,10 @@
"title": "Revision",
"type": "string"
},
+ "revision_is_current": {
+ "title": "Revision Is Current",
+ "type": "boolean"
+ },
"scope": {
"enum": [
"all",
@@ -7506,6 +7515,7 @@
"tool",
"suite",
"revision",
+ "revision_is_current",
"scope",
"case_count",
"surfaces"
@@ -7712,6 +7722,10 @@
"revision": {
"title": "Revision",
"type": "string"
+ },
+ "revision_is_current": {
+ "title": "Revision Is Current",
+ "type": "boolean"
}
},
"required": [
@@ -7741,7 +7755,8 @@
"tool_version",
"tier",
"notes",
- "revision"
+ "revision",
+ "revision_is_current"
],
"title": "RunDetailOut",
"type": "object"
diff --git a/backend/tests/test_benchmarks_api.py b/backend/tests/test_benchmarks_api.py
index 2316525..22298c0 100644
--- a/backend/tests/test_benchmarks_api.py
+++ b/backend/tests/test_benchmarks_api.py
@@ -121,6 +121,29 @@ def test_a_run_page_carries_the_whole_report(client, published):
assert body["scored_by"] == "submitter"
+def _detail_urls(run):
+ return [
+ f"{BASE}/suites/pdf/cases/extraction-conditions-1?revision=v0.1.1",
+ f"{BASE}/suites/pdf/tools/pdf-redaction?revision=v0.1.1",
+ f"{BASE}/runs/{run.run_id}",
+ ]
+
+
+def test_a_detail_page_says_its_revision_is_the_current_one(client, published):
+ """So the site can link the clean URL rather than a `?revision=` copy of it."""
+ for url in _detail_urls(published):
+ assert client.get(url).json()["revision_is_current"] is True, url
+
+
+def test_a_detail_page_says_when_its_revision_has_been_superseded(client, published):
+ revision = published.case.revision
+ revision.is_current = False
+ revision.save()
+
+ for url in _detail_urls(published):
+ assert client.get(url).json()["revision_is_current"] is False, url
+
+
def test_a_run_not_yet_approved_is_not_public(client, published):
published.submission.status = SubmissionStatus.PENDING_REVIEW
published.submission.save()
diff --git a/frontend/src/app/(site)/benchmarks/[suite]/page.tsx b/frontend/src/app/(site)/benchmarks/[suite]/page.tsx
index e135839..9a360e6 100644
--- a/frontend/src/app/(site)/benchmarks/[suite]/page.tsx
+++ b/frontend/src/app/(site)/benchmarks/[suite]/page.tsx
@@ -4,7 +4,7 @@ import { notFound } from 'next/navigation'
import { BenchmarkSuiteView } from '@/features/benchmarks/benchmark-suite-view'
import { getGetBenchmarkSuiteQueryKey } from '@/lib/api/generated/benchmarks/benchmarks'
-import { parseBenchmarkParams } from '@/lib/benchmarks/params'
+import { isAlternateView, parseBenchmarkParams } from '@/lib/benchmarks/params'
import { fetchBenchmarkSuite } from '@/lib/benchmarks/server'
import { clientEnv } from '@/lib/env'
import { getQueryClient } from '@/lib/query/client'
@@ -25,8 +25,10 @@ export async function generateMetadata({
title: `${data.name} benchmark ${data.revision.revision} — leak rates by tool`,
description: `${data.leaderboard.length} redaction tools scored on ${data.cases.length} synthetic cases: how many sensitive values each leaves recoverable, with 95% intervals.`,
...canonicalMetadata(`/benchmarks/${suite}`),
- // Another revision or the verified-only view is the same page sliced differently.
- robots: query.revision || query.scope !== 'all' ? { index: false, follow: true } : undefined,
+ // An older revision or the verified-only view is the same page sliced differently.
+ robots: isAlternateView(query, data.revision.is_current)
+ ? { index: false, follow: true }
+ : undefined,
}
}
diff --git a/frontend/src/app/(site)/benchmarks/[suite]/tools/[slug]/page.tsx b/frontend/src/app/(site)/benchmarks/[suite]/tools/[slug]/page.tsx
index b296cb3..9888cc7 100644
--- a/frontend/src/app/(site)/benchmarks/[suite]/tools/[slug]/page.tsx
+++ b/frontend/src/app/(site)/benchmarks/[suite]/tools/[slug]/page.tsx
@@ -5,7 +5,7 @@ import { notFound } from 'next/navigation'
import { ToolReportView } from '@/features/benchmarks/tool-report-view'
import { getGetBenchmarkToolReportQueryKey } from '@/lib/api/generated/benchmarks/benchmarks'
import { formatRate } from '@/lib/benchmarks/format'
-import { parseBenchmarkParams } from '@/lib/benchmarks/params'
+import { isAlternateView, parseBenchmarkParams } from '@/lib/benchmarks/params'
import { fetchBenchmarkToolReport } from '@/lib/benchmarks/server'
import { getQueryClient } from '@/lib/query/client'
import { canonicalMetadata } from '@/lib/seo/canonical'
@@ -25,7 +25,9 @@ export async function generateMetadata({
title: `${report.tool.name} redaction benchmark — ${headline ? formatRate(headline.leak_rate) : ''} leak rate`,
description: `How many sensitive values ${report.tool.name} leaves recoverable in our ${suite.toUpperCase()} benchmark ${report.revision}, by layer, category and case, with overlays.`,
...canonicalMetadata(`/benchmarks/${suite}/tools/${slug}`),
- robots: query.revision || query.scope !== 'all' ? { index: false, follow: true } : undefined,
+ robots: isAlternateView(query, report.revision_is_current)
+ ? { index: false, follow: true }
+ : undefined,
}
}
diff --git a/frontend/src/features/benchmarks/benchmark-suite-view.test.tsx b/frontend/src/features/benchmarks/benchmark-suite-view.test.tsx
index f8693ab..29e4b32 100644
--- a/frontend/src/features/benchmarks/benchmark-suite-view.test.tsx
+++ b/frontend/src/features/benchmarks/benchmark-suite-view.test.tsx
@@ -34,6 +34,15 @@ describe('BenchmarkSuiteView', () => {
name: /first page of extraction-conditions-1/i,
})
expect(thumbnail.closest('a')).toHaveAttribute(
+ 'href',
+ '/benchmarks/pdf/cases/extraction-conditions-1',
+ )
+ })
+
+ it('names a superseded revision in its case links', () => {
+ render(makeSuite({ revision: makeRevision({ is_current: false }) }))
+
+ expect(screen.getByRole('link', { name: 'extraction-conditions-1' })).toHaveAttribute(
'href',
'/benchmarks/pdf/cases/extraction-conditions-1?revision=v0.1.1',
)
@@ -65,12 +74,24 @@ describe('BenchmarkSuiteView', () => {
expect(screen.getByText('v0.1.1')).toHaveAttribute('aria-current', 'page')
})
+ // Viewing an old revision, the way back is the plain page, not a named copy of it.
+ it('links the current revision by its plain URL', () => {
+ render(
+ makeSuite({
+ revision: makeRevision({ revision: 'v0.1.0', is_current: false }),
+ revisions: [makeRevision(), makeRevision({ revision: 'v0.1.0', is_current: false })],
+ }),
+ )
+
+ expect(screen.getByRole('link', { name: 'v0.1.1' })).toHaveAttribute('href', '/benchmarks/pdf')
+ })
+
it('switches between every result and the ones we scored or verified', () => {
render()
expect(screen.getByRole('link', { name: /verified only/i })).toHaveAttribute(
'href',
- '/benchmarks/pdf?revision=v0.1.1&scope=verified',
+ '/benchmarks/pdf?scope=verified',
)
})
diff --git a/frontend/src/features/benchmarks/benchmark-suite-view.tsx b/frontend/src/features/benchmarks/benchmark-suite-view.tsx
index 1d3e7aa..ab15a4e 100644
--- a/frontend/src/features/benchmarks/benchmark-suite-view.tsx
+++ b/frontend/src/features/benchmarks/benchmark-suite-view.tsx
@@ -8,6 +8,7 @@ import { Card, CardDescription, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { useGetBenchmarkSuite } from '@/lib/api/generated/benchmarks/benchmarks'
import type { GetBenchmarkSuiteParams } from '@/lib/api/generated/model'
+import { benchmarkHref } from '@/lib/benchmarks/params'
import { cn } from '@/lib/utils'
import { previewAlt } from './case-preview'
@@ -30,7 +31,8 @@ export function BenchmarkSuiteView({
if (!data) return
- + {suite.toUpperCase()} benchmark {data.revision}
diff --git a/frontend/src/features/benchmarks/fixtures.ts b/frontend/src/features/benchmarks/fixtures.ts index 9170314..b952e13 100644 --- a/frontend/src/features/benchmarks/fixtures.ts +++ b/frontend/src/features/benchmarks/fixtures.ts @@ -152,6 +152,7 @@ export function makeToolReport(overrides: Partial- + {suite.toUpperCase()} benchmark {run.revision} {run.tool.listable ? ( <> {' / '} {run.tool.name} @@ -132,7 +134,7 @@ export function RunReportView({ suite, runId }: { suite: string; runId: string } ) : null} The case, and every tool on it diff --git a/frontend/src/features/benchmarks/tool-report-view.test.tsx b/frontend/src/features/benchmarks/tool-report-view.test.tsx index dbba154..6ddd025 100644 --- a/frontend/src/features/benchmarks/tool-report-view.test.tsx +++ b/frontend/src/features/benchmarks/tool-report-view.test.tsx @@ -23,6 +23,24 @@ function render(report: ToolReportOut = makeToolReport()) { } describe('ToolReportView', () => { + it('links back to the leaderboard by its plain URL when the revision is current', () => { + render() + + expect(screen.getByRole('link', { name: /pdf benchmark v0\.1\.1/i })).toHaveAttribute( + 'href', + '/benchmarks/pdf', + ) + }) + + it('names a superseded revision in the way back', () => { + render(makeToolReport({ revision_is_current: false })) + + expect(screen.getByRole('link', { name: /pdf benchmark v0\.1\.1/i })).toHaveAttribute( + 'href', + '/benchmarks/pdf?revision=v0.1.1', + ) + }) + it('leads with the pooled verdict, its interval and its sample size', () => { render() diff --git a/frontend/src/features/benchmarks/tool-report-view.tsx b/frontend/src/features/benchmarks/tool-report-view.tsx index 12fe33c..6ef9c23 100644 --- a/frontend/src/features/benchmarks/tool-report-view.tsx +++ b/frontend/src/features/benchmarks/tool-report-view.tsx @@ -7,6 +7,7 @@ import { ToolLogo } from '@/features/catalog/tool-logo' import { useGetBenchmarkToolReport } from '@/lib/api/generated/benchmarks/benchmarks' import type { GetBenchmarkToolReportParams } from '@/lib/api/generated/model' import { surfaceLabel } from '@/lib/benchmarks/format' +import { benchmarkHref } from '@/lib/benchmarks/params' import { type Breakdowns, ReportBreakdowns, ReportHero } from './report-sections' import { RunCards } from './run-cards' @@ -28,7 +29,13 @@ export function ToolReportView({
- + {suite.toUpperCase()} benchmark {data.revision}
diff --git a/frontend/src/lib/api/generated/model/caseDetailOut.ts b/frontend/src/lib/api/generated/model/caseDetailOut.ts index e9ed7b8..97048c5 100644 --- a/frontend/src/lib/api/generated/model/caseDetailOut.ts +++ b/frontend/src/lib/api/generated/model/caseDetailOut.ts @@ -18,5 +18,6 @@ export interface CaseDetailOut { pdf_url: string; preview: PreviewOut | null; revision: string; + revision_is_current: boolean; runs: RunOut[]; } diff --git a/frontend/src/lib/api/generated/model/runDetailOut.ts b/frontend/src/lib/api/generated/model/runDetailOut.ts index d01b891..4e44a81 100644 --- a/frontend/src/lib/api/generated/model/runDetailOut.ts +++ b/frontend/src/lib/api/generated/model/runDetailOut.ts @@ -45,4 +45,5 @@ export interface RunDetailOut { tier: string; notes: string; revision: string; + revision_is_current: boolean; } diff --git a/frontend/src/lib/api/generated/model/toolReportOut.ts b/frontend/src/lib/api/generated/model/toolReportOut.ts index 440e2bc..38ded62 100644 --- a/frontend/src/lib/api/generated/model/toolReportOut.ts +++ b/frontend/src/lib/api/generated/model/toolReportOut.ts @@ -13,6 +13,7 @@ export interface ToolReportOut { tool: ToolRefOut; suite: string; revision: string; + revision_is_current: boolean; scope: ToolReportOutScope; case_count: number; surfaces: ToolSurfaceOut[]; diff --git a/frontend/src/lib/benchmarks/params.test.ts b/frontend/src/lib/benchmarks/params.test.ts index 90e2c17..9a9530e 100644 --- a/frontend/src/lib/benchmarks/params.test.ts +++ b/frontend/src/lib/benchmarks/params.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { parseBenchmarkParams } from './params' +import { benchmarkHref, isAlternateView, parseBenchmarkParams } from './params' describe('parseBenchmarkParams', () => { it('defaults to the current revision and every result', () => { @@ -21,3 +21,44 @@ describe('parseBenchmarkParams', () => { }) }) }) + +describe('benchmarkHref', () => { + // `?revision=