From 9f6a2443a985040f32b900d9cbd4938da96bdd61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 11:18:51 +0000 Subject: [PATCH 1/2] fix(debug): preserve language in plot links so R/ggplot2 deep-links work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DebugPage hardcoded 'python' when building plot-detail URLs (recent activity + spec matrix, desktop and mobile). For ggplot2 (R) this produced /spec/python/ggplot2; SpecPage then validated that an impl exists with language=python AND library=ggplot2, found none, and redirected to the Python-filtered hub overview — exactly the symptom reported (lands on overview, plot missing). Fixes alongside, all triggered by the same R-language rollout: - /debug/status RecentActivity now carries language_id so the frontend doesn't have to guess - SpecStatusItem gains a ggplot2 column (was silently missing — the matrix column was always empty) - coverage = total / (specs * len(SUPPORTED_LIBRARIES)) instead of *9 - library_names map includes ggplot2 - Hardcoded 9s in DebugPage replaced with LIBRARIES.length (filter, count display, grid template) - /specs/{id}/images and /libraries/{id}/images now expose language too, so external API consumers can build the same deep links Frontend constants gain LIB_TO_LANG (mirror of LIB_ABBREV) so the matrix grid can resolve a column's language without waiting for /libraries to load. --- api/routers/debug.py | 7 ++++++- api/routers/libraries.py | 1 + api/routers/specs.py | 7 ++++++- app/src/constants/index.ts | 17 +++++++++++++++++ app/src/pages/DebugPage.test.tsx | 2 +- app/src/pages/DebugPage.tsx | 22 ++++++++++++---------- docs/reference/api.md | 2 ++ tests/unit/api/test_debug.py | 23 ++++++++++++++++------- 8 files changed, 61 insertions(+), 20 deletions(-) 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..a5cd4184c2 100644 --- a/api/routers/specs.py +++ b/api/routers/specs.py @@ -137,7 +137,12 @@ 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..6db4b19367 100644 --- a/app/src/constants/index.ts +++ b/app/src/constants/index.ts @@ -25,3 +25,20 @@ 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 and recent-activity +// list, which would otherwise have to wait for /libraries to load). +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..882314ae72 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,19 @@ 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/... before). + """ 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 +302,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.""" From 71616a3345597d3abcfdf74b417f0adbd422c544 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 11:49:38 +0000 Subject: [PATCH 2/2] fix(debug): apply ruff format and Copilot review nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ruff format collapsed two over-wrapped literals (api/routers/specs.py spec-images dict and the recent-activity test impl_r construction). - Narrow the LIB_TO_LANG comment to the spec-matrix use case — once recent activity started carrying language_id from /debug/status it no longer relies on the static map. - Update the recent-activity regression docstring so the URL shape matches the real specPath output ({specId}/{language}/{library}, not /spec/python/...). --- api/routers/specs.py | 7 +------ app/src/constants/index.ts | 6 ++++-- tests/unit/api/test_debug.py | 7 +++---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/api/routers/specs.py b/api/routers/specs.py index a5cd4184c2..679dcfdb50 100644 --- a/api/routers/specs.py +++ b/api/routers/specs.py @@ -137,12 +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, - "language": impl.language_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 6db4b19367..fa53399c8b 100644 --- a/app/src/constants/index.ts +++ b/app/src/constants/index.ts @@ -28,8 +28,10 @@ export const LIB_ABBREV: Record = { // 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 and recent-activity -// list, which would otherwise have to wait for /libraries to load). +// 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', diff --git a/tests/unit/api/test_debug.py b/tests/unit/api/test_debug.py index 882314ae72..829293d6fc 100644 --- a/tests/unit/api/test_debug.py +++ b/tests/unit/api/test_debug.py @@ -281,16 +281,15 @@ def test_debug_status_daily_impls_shape(self, db_client) -> None: def test_debug_status_recent_activity(self, db_client) -> None: """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/... before). + (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_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" - ) + 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()