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: 6 additions & 1 deletion api/routers/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class SpecStatusItem(BaseModel):
# Library scores - None means no implementation
altair: float | None = None
bokeh: float | None = None
ggplot2: float | None = None
highcharts: float | None = None
letsplot: float | None = None
matplotlib: float | None = None
Expand Down Expand Up @@ -165,6 +166,7 @@ class RecentActivity(BaseModel):
spec_id: str
spec_title: str
library_id: str
language_id: str
quality_score: float | None
generated_by: str | None
updated: str # ISO datetime
Expand Down Expand Up @@ -276,6 +278,7 @@ async def get_debug_status(request: Request, db: AsyncSession = Depends(require_
spec_id=spec.id,
spec_title=spec.title,
library_id=lib_id,
language_id=impl.language_id,
quality_score=score,
generated_by=impl.generated_by,
updated=impl.updated.isoformat(),
Expand Down Expand Up @@ -308,6 +311,7 @@ async def get_debug_status(request: Request, db: AsyncSession = Depends(require_
avg_score=round(avg_score, 1) if avg_score else None,
altair=spec_scores.get("altair"),
bokeh=spec_scores.get("bokeh"),
ggplot2=spec_scores.get("ggplot2"),
highcharts=spec_scores.get("highcharts"),
letsplot=spec_scores.get("letsplot"),
matplotlib=spec_scores.get("matplotlib"),
Expand All @@ -328,6 +332,7 @@ async def get_debug_status(request: Request, db: AsyncSession = Depends(require_
library_names = {
"altair": "Altair",
"bokeh": "Bokeh",
"ggplot2": "ggplot2",
"highcharts": "Highcharts",
"letsplot": "lets-plot",
"matplotlib": "Matplotlib",
Expand Down Expand Up @@ -414,7 +419,7 @@ async def get_debug_status(request: Request, db: AsyncSession = Depends(require_
# ========================================================================

response_time_ms = (time.time() - start_time) * 1000
coverage = (total_implementations / (len(all_specs) * 9) * 100) if all_specs else 0
coverage = (total_implementations / (len(all_specs) * len(SUPPORTED_LIBRARIES)) * 100) if all_specs else 0

system_health = SystemHealth(
database_connected=True,
Expand Down
1 change: 1 addition & 0 deletions api/routers/libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ async def get_library_images(library_id: str, db: AsyncSession = Depends(require
{
"spec_id": spec.id,
"library": impl.library_id,
"language": impl.language_id,
"url": impl.preview_url,
"html": impl.preview_html,
"code": strip_noqa_comments(impl.code),
Expand Down
2 changes: 1 addition & 1 deletion api/routers/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async def _build_spec_images(db: AsyncSession, spec_id: str) -> dict:
raise_not_found("Spec with implementations", spec_id)

images = [
{"library": impl.library_id, "url": impl.preview_url, "html": impl.preview_html}
{"library": impl.library_id, "language": impl.language_id, "url": impl.preview_url, "html": impl.preview_html}
for impl in spec.impls
if impl.preview_url
]
Expand Down
19 changes: 19 additions & 0 deletions app/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,22 @@ export const LIB_ABBREV: Record<string, string> = {
letsplot: 'lp',
ggplot2: 'gg',
};

// Static library → language map, mirroring core/constants.py LIBRARIES_METADATA.
// Used to build correct /{spec}/{language}/{library} links from contexts that
// only know a library id — e.g. the debug-page spec matrix, where columns are
// keyed by library and the per-cell payload doesn't carry a language. The
// recent-activity list does NOT need this map; it gets `language_id` straight
// from /debug/status.
export const LIB_TO_LANG: Record<string, string> = {
altair: 'python',
bokeh: 'python',
ggplot2: 'r',
highcharts: 'python',
letsplot: 'python',
matplotlib: 'python',
plotly: 'python',
plotnine: 'python',
pygal: 'python',
seaborn: 'python',
};
2 changes: 1 addition & 1 deletion app/src/pages/DebugPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const mockDebugData = {
title: 'Basic Scatter',
updated: '2025-01-01',
avg_score: 92,
altair: 90, bokeh: 91, highcharts: null, letsplot: null,
altair: 90, bokeh: 91, ggplot2: null, highcharts: null, letsplot: null,
matplotlib: 95, plotly: 88, plotnine: null, pygal: null, seaborn: 94,
},
],
Expand Down
22 changes: 12 additions & 10 deletions app/src/pages/DebugPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import Tooltip from '@mui/material/Tooltip';

import { DEBUG_API_URL, LIBRARIES, LIB_ABBREV } from '../constants';
import { DEBUG_API_URL, LIBRARIES, LIB_ABBREV, LIB_TO_LANG } from '../constants';
import { specPath } from '../utils/paths';
import { SectionHeader } from '../components/SectionHeader';
import { typography, colors, semanticColors, fontSize } from '../theme';
Expand All @@ -21,6 +21,7 @@ interface SpecStatus {
avg_score: number | null;
altair: number | null;
bokeh: number | null;
ggplot2: number | null;
highcharts: number | null;
letsplot: number | null;
matplotlib: number | null;
Expand Down Expand Up @@ -63,6 +64,7 @@ interface RecentActivity {
spec_id: string;
spec_title: string;
library_id: string;
language_id: string;
quality_score: number | null;
generated_by: string | null;
updated: string;
Expand Down Expand Up @@ -318,7 +320,7 @@ export function DebugPage() {
const q = searchText.toLowerCase();
filtered = filtered.filter(s => s.id.toLowerCase().includes(q) || s.title.toLowerCase().includes(q));
}
if (showIncomplete) filtered = filtered.filter(s => countImpls(s) < 9);
if (showIncomplete) filtered = filtered.filter(s => countImpls(s) < LIBRARIES.length);
if (showLowScores) filtered = filtered.filter(hasLowScore);
if (missingLibrary) filtered = filtered.filter(s => s[missingLibrary as keyof SpecStatus] === null);

Expand Down Expand Up @@ -624,7 +626,7 @@ export function DebugPage() {
<Link
key={`${act.spec_id}-${act.library_id}-${idx}`}
component={RouterLink}
to={specPath(act.spec_id, 'python', act.library_id)}
to={specPath(act.spec_id, act.language_id, act.library_id)}
sx={{
display: 'grid',
gridTemplateColumns: { xs: '55px 1fr 30px', sm: '65px 90px minmax(0, 1fr) 30px 130px' },
Expand Down Expand Up @@ -802,7 +804,7 @@ export function DebugPage() {
borderColor: showIncomplete ? colors.primary : 'var(--rule)',
}}
>
incomplete {'<'}9
incomplete {'<'}{LIBRARIES.length}
</Box>
<Box
component="button"
Expand Down Expand Up @@ -844,7 +846,7 @@ export function DebugPage() {
{/* Header row */}
<Box sx={{
display: 'grid',
gridTemplateColumns: '180px minmax(180px, 1fr) 50px 50px repeat(9, 40px) 80px',
gridTemplateColumns: `180px minmax(180px, 1fr) 50px 50px repeat(${LIBRARIES.length}, 40px) 80px`,
gap: 0, alignItems: 'center',
position: 'sticky', top: 0, zIndex: 1,
bgcolor: 'var(--bg-page)',
Expand All @@ -869,7 +871,7 @@ export function DebugPage() {
key={spec.id}
sx={{
display: 'grid',
gridTemplateColumns: '180px minmax(180px, 1fr) 50px 50px repeat(9, 40px) 80px',
gridTemplateColumns: `180px minmax(180px, 1fr) 50px 50px repeat(${LIBRARIES.length}, 40px) 80px`,
gap: 0, alignItems: 'center', py: 0.5,
borderBottom: '1px solid var(--rule)',
'&:hover': { bgcolor: 'var(--bg-surface)' },
Expand All @@ -896,10 +898,10 @@ export function DebugPage() {
</Typography>
<Typography sx={{
fontFamily: typography.fontFamily, fontSize: fontSize.xs, fontWeight: 600,
color: implCount === 9 ? colors.success : implCount > 0 ? semanticColors.mutedText : 'var(--ink-muted)',
color: implCount === LIBRARIES.length ? colors.success : implCount > 0 ? semanticColors.mutedText : 'var(--ink-muted)',
textAlign: 'center',
}}>
{implCount}/9
{implCount}/{LIBRARIES.length}
</Typography>
<Typography sx={{
fontFamily: typography.fontFamily, fontSize: fontSize.xs, fontWeight: 600,
Expand All @@ -920,7 +922,7 @@ export function DebugPage() {
<Link
key={lib}
component={RouterLink}
to={specPath(spec.id, 'python', lib)}
to={specPath(spec.id, LIB_TO_LANG[lib] ?? 'python', lib)}
sx={{
textAlign: 'center', textDecoration: 'none',
fontFamily: typography.fontFamily, fontSize: fontSize.xs, fontWeight: 600,
Expand Down Expand Up @@ -996,7 +998,7 @@ export function DebugPage() {
<Link
key={lib}
component={RouterLink}
to={specPath(spec.id, 'python', lib)}
to={specPath(spec.id, LIB_TO_LANG[lib] ?? 'python', lib)}
sx={{ ...commonSx, '&:hover': { opacity: 0.75 } }}
>
<Box component="span">{abbrev}</Box>
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ The anyplot API is a **FastAPI-based REST API** serving plot data to the fronten
"images": [
{
"library": "matplotlib",
"language": "python",
"url": "https://storage.googleapis.com/.../plot.png",
"html": null
}
Expand Down Expand Up @@ -140,6 +141,7 @@ The anyplot API is a **FastAPI-based REST API** serving plot data to the fronten
{
"spec_id": "scatter-basic",
"library": "matplotlib",
"language": "python",
"url": "https://storage.googleapis.com/.../plot.png",
"html": null,
"code": "import matplotlib.pyplot as plt..."
Expand Down
22 changes: 15 additions & 7 deletions tests/unit/api/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async def mock_get_db():

def _make_impl(
library_id="matplotlib",
language_id="python",
quality_score=92.5,
preview_url="https://example.com/plot.png",
updated=None,
Expand All @@ -86,6 +87,7 @@ def _make_impl(
"""Helper to create a mock implementation."""
impl = MagicMock()
impl.library_id = library_id
impl.language_id = language_id
impl.quality_score = quality_score
impl.preview_url = preview_url
impl.updated = updated
Expand Down Expand Up @@ -148,8 +150,8 @@ def test_debug_status_with_specs_and_impls(self, db_client) -> None:
data = response.json()
assert data["total_specs"] == 1
assert data["total_implementations"] == 2
# coverage = 2 / (1 * 9) * 100 = 22.2%
assert data["coverage_percent"] == 22.2
# coverage = 2 / (1 * len(SUPPORTED_LIBRARIES)) * 100
assert data["coverage_percent"] == round(2 / len(SUPPORTED_LIBRARIES) * 100, 1)

# Check library stats
lib_stats_by_id = {ls["id"]: ls for ls in data["library_stats"]}
Expand Down Expand Up @@ -277,14 +279,18 @@ def test_debug_status_daily_impls_shape(self, db_client) -> None:
assert today_point["impls_updated"] == 1

def test_debug_status_recent_activity(self, db_client) -> None:
"""recent_activity should return impls sorted by updated DESC, capped at 15."""
"""recent_activity should return impls sorted by updated DESC, capped at 15,
and surface each impl's language so the frontend can build correct deep links
(regression: Python + R impls were both linked as /{spec}/python/{library}
before, which sent R/ggplot2 clicks to the Python language overview).
"""
client, _ = db_client

older = datetime(2026, 3, 1, tzinfo=timezone.utc)
newer = datetime(2026, 4, 20, tzinfo=timezone.utc)
impl_old = _make_impl(library_id="matplotlib", updated=older, generated_by="claude-opus-4-6")
impl_new = _make_impl(library_id="seaborn", updated=newer, generated_by="claude-opus-4-7")
spec = _make_spec(impls=[impl_old, impl_new])
impl_py = _make_impl(library_id="matplotlib", updated=older, generated_by="claude-opus-4-6")
impl_r = _make_impl(library_id="ggplot2", language_id="r", updated=newer, generated_by="claude-opus-4-7")
spec = _make_spec(impls=[impl_py, impl_r])

mock_repo = MagicMock()
mock_repo.get_all = AsyncMock(return_value=[spec])
Expand All @@ -295,9 +301,11 @@ def test_debug_status_recent_activity(self, db_client) -> None:
data = response.json()
activity = data["recent_activity"]
assert len(activity) == 2
assert activity[0]["library_id"] == "seaborn"
assert activity[0]["library_id"] == "ggplot2"
assert activity[0]["language_id"] == "r"
assert activity[0]["generated_by"] == "claude-opus-4-7"
assert activity[1]["library_id"] == "matplotlib"
assert activity[1]["language_id"] == "python"

def test_debug_status_common_weaknesses(self, db_client) -> None:
"""common_weaknesses should aggregate review_weaknesses case-insensitively, top 10."""
Expand Down