diff --git a/api/routers/debug.py b/api/routers/debug.py index 6932b6174a..4d8821afff 100644 --- a/api/routers/debug.py +++ b/api/routers/debug.py @@ -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 @@ -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 @@ -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(), @@ -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"), @@ -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", @@ -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, diff --git a/api/routers/libraries.py b/api/routers/libraries.py index 3c2978a741..0eabc1cdd5 100644 --- a/api/routers/libraries.py +++ b/api/routers/libraries.py @@ -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), diff --git a/api/routers/specs.py b/api/routers/specs.py index 7574033410..679dcfdb50 100644 --- a/api/routers/specs.py +++ b/api/routers/specs.py @@ -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 ] diff --git a/app/src/constants/index.ts b/app/src/constants/index.ts index 8e72ef139d..fa53399c8b 100644 --- a/app/src/constants/index.ts +++ b/app/src/constants/index.ts @@ -25,3 +25,22 @@ export const LIB_ABBREV: Record = { 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 = { + altair: 'python', + bokeh: 'python', + ggplot2: 'r', + highcharts: 'python', + letsplot: 'python', + matplotlib: 'python', + plotly: 'python', + plotnine: 'python', + pygal: 'python', + seaborn: 'python', +}; diff --git a/app/src/pages/DebugPage.test.tsx b/app/src/pages/DebugPage.test.tsx index 740d0553ae..a862003a61 100644 --- a/app/src/pages/DebugPage.test.tsx +++ b/app/src/pages/DebugPage.test.tsx @@ -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, }, ], diff --git a/app/src/pages/DebugPage.tsx b/app/src/pages/DebugPage.tsx index 83020a0eab..45c76a7421 100644 --- a/app/src/pages/DebugPage.tsx +++ b/app/src/pages/DebugPage.tsx @@ -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'; @@ -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; @@ -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; @@ -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); @@ -624,7 +626,7 @@ export function DebugPage() { - incomplete {'<'}9 + incomplete {'<'}{LIBRARIES.length} 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} {abbrev} diff --git a/docs/reference/api.md b/docs/reference/api.md index f928fccde9..d89c6203ba 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -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 } @@ -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..." diff --git a/tests/unit/api/test_debug.py b/tests/unit/api/test_debug.py index c097e256ae..829293d6fc 100644 --- a/tests/unit/api/test_debug.py +++ b/tests/unit/api/test_debug.py @@ -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, @@ -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 @@ -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"]} @@ -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]) @@ -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."""