{kernelViews &&
panels.map((panel, index) => (
diff --git a/docs/src/components/SnapshotEvolution.astro b/docs/src/components/SnapshotEvolution.astro
index 02869e335..42f18f5f4 100644
--- a/docs/src/components/SnapshotEvolution.astro
+++ b/docs/src/components/SnapshotEvolution.astro
@@ -8,12 +8,27 @@
// kernels only, `core` tiers only, one series per analyzer, no combined score.
import {
snapshots,
+ analyzerCohort,
coreKernelPopulations,
vendorColorClass,
vendorName,
vendorOrder,
+ type AnalyzerCohort,
} from '../data/snapshots';
+interface Props {
+ cohort: AnalyzerCohort;
+ heading?: boolean;
+}
+
+const { cohort, heading = false } = Astro.props;
+const cohortLabel = cohort === 'generalist' ? 'Generalists' : 'Specialists';
+const cohortDescription =
+ cohort === 'generalist'
+ ? 'analyzers spanning several language ecosystems'
+ : 'analyzers deliberately focused on one ecosystem or a small related family';
+const scope = cohort === 'generalist' ? 'generalists' : 'specialists';
+
interface SeriesPoint {
version: string;
slug: string;
@@ -123,6 +138,7 @@ const series: Series[] = [...byTool.entries()]
vendorOrder(left.tool) - vendorOrder(right.tool) ||
left.name.localeCompare(right.name),
);
+const cohortSeries = series.filter((entry) => analyzerCohort(entry.tool) === cohort);
// ---- Geometry -------------------------------------------------------------
const height = 440;
@@ -149,7 +165,7 @@ const bandMargin = 24;
const preferredSpacing = 13;
const band = Math.max(
104,
- preferredSpacing * Math.max(series.length - 1, 1) + bandMargin,
+ preferredSpacing * Math.max(cohortSeries.length - 1, 1) + bandMargin,
);
const plotWidth = band * columns.length;
const width = padLeft + plotWidth + padRight;
@@ -185,10 +201,10 @@ const y = (value: number) => padTop + plotHeight * (1 - value / axisMax);
// tightens them, so no previously published layout moves.
const fannedOffset = (index: number, preferred: number) => {
const spacing =
- series.length <= 1
+ cohortSeries.length <= 1
? 0
- : Math.min(preferred, (band - bandMargin) / (series.length - 1));
- return (index - (series.length - 1) / 2) * spacing;
+ : Math.min(preferred, (band - bandMargin) / (cohortSeries.length - 1));
+ return (index - (cohortSeries.length - 1) / 2) * spacing;
};
const seriesOffset = (index: number) => fannedOffset(index, preferredSpacing);
@@ -225,7 +241,7 @@ const percentOffset = (index: number) => fannedOffset(index, 30);
// When the fan is tighter than a `k/n` label is wide, those labels are dealt
// out over three rows so neighbours cannot overprint each other.
const percentSpacing =
- series.length <= 1 ? 0 : Math.abs(percentOffset(1) - percentOffset(0));
+ cohortSeries.length <= 1 ? 0 : Math.abs(percentOffset(1) - percentOffset(0));
const labelRows = percentSpacing >= 26 ? 1 : 3;
const labelRow = (index: number) => index % labelRows;
/** Marker area grows with the share of the snapshot's kernels covered. */
@@ -288,7 +304,7 @@ const evidenceHref = (slug: string) =>
`/snapshots/${slug}/evidence/`;
const rows = columns.flatMap((column) =>
- series
+ cohortSeries
.map((entry) => ({
column,
entry,
@@ -313,12 +329,12 @@ const sentenceList = (items: string[]) =>
? items.join(' and ')
: `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`;
const debutList = sentenceList(
- series
+ cohortSeries
.filter((entry) => entry.points[0]!.version !== first.version)
.map((entry) => `${entry.name} first appears in ${entry.points[0]!.version}`),
);
/** Analyzers whose line is a single point: present in exactly one freeze. */
-const singlePoint = series.filter((entry) => entry.points.length === 1);
+const singlePoint = cohortSeries.filter((entry) => entry.points.length === 1);
const singlePointList = sentenceList(singlePoint.map((entry) => entry.name));
// The snapshot that expanded the population the most: the freeze where a
@@ -333,10 +349,15 @@ const biggestJump = columns
.sort((left, right) => right.growth - left.growth)[0]!;
---
-
How the benchmark and its analyzers evolved
+{heading &&
How the benchmark and its analyzers evolved
}
+
+
{cohortLabel}
- One series per analyzer, across every published freeze. The vertical axis
+ One series per {cohort === 'generalist' ? 'generalist' : 'specialist'} analyzer,
+ across every published freeze. This panel contains {cohortDescription}; the
+ other cohort is drawn separately because breadth and specialization are not
+ directly comparable. The vertical axis
counts decisive-correct assertions — true positives plus
true negatives — on the benchmark-controlled kernel population,
the same no-pooling population the cards at the top of this page read. The
@@ -352,28 +373,28 @@ const biggestJump = columns
- One series per {cohort === 'generalist' ? 'generalist' : 'specialist'} analyzer,
- across every published freeze. This panel contains {cohortDescription}; the
- other cohort is drawn separately because breadth and specialization are not
- directly comparable. The vertical axis
- counts decisive-correct assertions — true positives plus
- true negatives — on the benchmark-controlled kernel population,
- the same no-pooling population the cards at the top of this page read. The
- grey step is the benchmark itself: the total core population of that
- snapshot, which grew from {first.population} assertions in
- {first.version} to {last.population} in {last.version}. Nothing here is
- pooled with the modeling matrix or the tool-native probes, and there is no
- combined cross-analyzer score: read each line on its own. The second view
- divides each analyzer's decisive-correct count by the population it actually
- covers, so a narrow language footprint no longer reads as a weak result. The
- third view drops the drawing entirely and lists every figure as a table.
+ {heading ? (
+ <>
+ One line per analyzer across every freeze. The vertical axis counts
+ decisive-correct assertions; the grey step is the full benchmark core,
+ which grew from {first.population} assertions in {first.version} to{' '}
+ {last.population} in {last.version}. Nothing is pooled with other result
+ profiles or turned into a combined score.
+ >
+ ) : (
+ <>
+ {cohortDescription}, separated because breadth and specialization are not
+ directly comparable. Uses the same axes as the generalist panel.
+ >
+ )}
@@ -692,68 +663,21 @@ const biggestJump = columns
-{/* The view-note caption and whichever view caption is showing are adjacent
- and were each carrying their own two columns, so the right column held
- the tail of one and then the tail of the other. One wrapper flows both
- down the left column first; the hidden view captions cost it nothing. */}
-
-
- Absolute counts plot progress against the whole growing
- benchmark: the marker is the analyzer's decisive-correct count and the grey
- step is the entire core population of that snapshot, so an analyzer that
- supports only part of the benchmark sits far below the step by construction.
- Accuracy on covered kernels instead divides each analyzer's
- decisive-correct count by its own covered population — the assertions
- in the kernels it reported on at all — so it answers how well a tool does on
- the slice it supports. Coverage is stated beside every number in that view:
- the marker grows with the share of the snapshot's kernels covered, each point
- carries its k/n kernel count, and the Data table
- view spells out the denominator, so 78% over 6 kernels is never mistaken for
- 78% over 13. Inconclusive and unsupported outcomes inside covered
- kernels stay in the denominator — they are non-answers on cases the analyzer
- took on. The narrower correct ÷ decided ratio is reported only as a secondary
- figure, in the tooltips and the last column of the data table.
- Data table is the third setting of the same toggle: it
- carries the exact figures behind both charts — every count, every denominator
- and every percentage — for anyone who would rather read the numbers than the
- shape.
-
-
-
- Each marker sits at the analyzer's decisive-correct count. The faint stub
- above it reaches that analyzer's own covered population — the
- assertions it reported on — so the part of the stub above the marker is
- wrong answers plus coverage outcomes, and the remaining gap up to the grey
- step is kernels it does not cover at all. A snapshot without a marker is a
- snapshot the analyzer was not run in: {debutList}. Absence is never drawn as
- a zero and never interpolated across, so an analyzer that appears in exactly
- one freeze is one marker and no line —
- {singlePoint.length > 0
- ? `${singlePointList} ${
- singlePoint.length === 1 ? 'is' : 'are'
- } drawn that way here, and no earlier value is implied for ${
- singlePoint.length === 1 ? 'it' : 'them'
- }`
- : 'no analyzer is drawn that way here'}.
- inconclusive, unsupported and
- runner-error are capability coverage: excluded from
- correctness, never converted into wrong answers. Every snapshot label links
- to that freeze's per-case evidence.
-
-
-
- Each marker sits at the analyzer's decisive-correct share of its own covered
- population; the faint stub above it runs to 100%, which is that same covered
- population, so the stub is wrong answers plus coverage outcomes inside the
- kernels it did report on. A snapshot without a marker is a snapshot the
- analyzer was not run in: {debutList}.
- Absence is never drawn as a zero and never interpolated across. Percentages
- from different snapshots are not the same exam, and percentages from
- different analyzers in the same snapshot are only the same exam when their
- k/n kernel counts match. Every snapshot label links to that
- freeze's per-case evidence.
-
-
+
+ {heading ? (
+ <>
+ Marker: decisive-correct. Cap or stub: that analyzer's covered population.
+ Grey step: the full core population. The covered-kernel view keeps
+ non-answers in its denominator and shows coverage as k/n;
+ missing runs are absent, never zero. Exact figures are in the table.
+ >
+ ) : (
+ <>
+ Same scales and encoding as the generalists. The covered-kernel view keeps
+ specialist scope visible as k/n; exact figures are in the table.
+ >
+ )}
+
@@ -821,27 +745,10 @@ const biggestJump = columns
- Every value the two chart views draw, one row per analyzer per snapshot, with
- each denominator spelled out beside the figure it belongs to. This is the
- same data the charts plot — nothing here is computed differently, and nothing
- is rounded away except the percentages, which are given to one decimal place.
- Switch back to Absolute counts or Accuracy on
- covered kernels for the shape of it.
+ The exact values behind both chart views, one row per analyzer and snapshot.
-
- Read the shape, not a ranking. Two different things move a line: an analyzer
- getting better, and the benchmark getting harder. The largest expansion so
- far is {biggestJump.column.version}, which took the core
- population from {biggestJump.previous!.population} to
- {biggestJump.column.population} assertions — every analyzer's share of the
- population fell there without any of them changing. Because the denominator
- is drawn alongside the counts, saturation and expansion stay
- distinguishable, and no two snapshots' fractions are ever compared as if
- they were the same exam.
-
-
diff --git a/docs/src/components/LandingResults.astro b/docs/src/components/LandingResults.astro
index 7804d0090..b1cec20ef 100644
--- a/docs/src/components/LandingResults.astro
+++ b/docs/src/components/LandingResults.astro
@@ -16,6 +16,7 @@ import {
type ScoreTier,
} from '../data/snapshots';
import SnapshotEvolution from './SnapshotEvolution.astro';
+import AnalyzerLandscape from './AnalyzerLandscape.astro';
import LatencyRanking from './LatencyRanking.astro';
const snapshot = currentSnapshot;
@@ -501,7 +502,7 @@ const snapshotPage = `/snapshots/${snapshot.slug}`;