HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1/**2 * D3 QA sweep (temporal · graph · research · changes · search · explore · claims · admin · SEO).3 * Widths 320 · 360 · 375 · 390 · 430 · 768 · 1366 · 1440 · 1920, dark + light — HTTP status, zero console errors, no horizontal4 * overflow, screenshot — plus assertions: graph draws ≥ 2 nodes, compiled chips on /search, backfill honesty line on the digest,5 * admin login → /admin/quality → /admin/entity-resolution (token never in HTML), OG image routes, sitemap shards.6 * Run: node qa/d3.mjs [BASE_URL] [API_URL] [ADMIN_TOKEN] (defaults http://localhost:8343, http://127.0.0.1:8332, AIA_ADMIN_TOKEN from ../../.env)7 * QUICK=1 limits widths to 390 and 1440.8 */9import { chromium } from '/Users/simon-pierreboucher/Desktop/uqo-eval/node_modules/playwright/index.mjs';10import { existsSync, mkdirSync, readFileSync } from 'node:fs';1112const BASE = process.argv[2] ?? 'http://localhost:8343';13const API = process.argv[3] ?? 'http://127.0.0.1:8332';14function envToken() {15 for (const p of [new URL('../../../.env', import.meta.url).pathname, new URL('../.env', import.meta.url).pathname]) {16 if (!existsSync(p)) continue;17 const m = /^AIA_ADMIN_TOKEN=(.+)$/m.exec(readFileSync(p, 'utf8'));18 if (m) return m[1].trim().replace(/^["']|["']$/g, '');19 }20 return null;21}22const TOKEN = process.argv[4] ?? process.env.AIA_ADMIN_TOKEN ?? envToken();23const OUT = new URL('./screens/d3/', import.meta.url).pathname;24mkdirSync(OUT, { recursive: true });25const WIDTHS = process.env.QUICK === '1' ? [390, 1440] : [320, 360, 375, 390, 430, 768, 1366, 1440, 1920];26const THEMES = ['dark', 'light'];27const today = new Date().toISOString().slice(0, 10);28const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);2930const j = async (path, headers = {}) => {31 const r = await fetch(`${API}/api/v1${path}`, { headers: { accept: 'application/json', ...headers } });32 if (!r.ok) throw new Error(`API ${r.status} ${path}`);33 return r.json();34};3536// ---- discover slugs live (nothing hardcoded)37const trending = await j('/trending?kind=views&type=model&limit=1&days=7').catch(() => null);38const models = await j('/models?limit=1&sort=quality');39const modelSlug = trending?.items?.[0]?.slug ?? models.items[0].slug;40const papers = await j('/papers?limit=60&sort=updated');41const paperSlug = (papers.items.find((p) => (p.counts?.relations ?? 0) > 2) ?? papers.items[0]).slug;42const orgs = await j('/companies?limit=1&sort=models');43const orgSlug = orgs.items.find((o) => o.slug === 'anthropic')?.slug ?? orgs.items[0].slug;44const claims = await j(`/entities/${encodeURIComponent(modelSlug)}/claims?limit=1`);45const claimId = claims.items[0]?.id;46const researchers = await j('/explore/researcher?limit=1');47const researcherSlug = researchers.items[0]?.slug;48let snapshotId = null;49if (TOKEN) {50 try {51 const docs = await j('/admin/documents?limit=1', { 'x-aia-admin-token': TOKEN });52 const doc = await j(`/admin/documents/${docs.items[0].id}`, { 'x-aia-admin-token': TOKEN });53 snapshotId = Array.isArray(doc.snapshots) && doc.snapshots[0] ? doc.snapshots[0].id : null;54 } catch {55 snapshotId = null;56 }57}58console.log(`root model ${modelSlug} · paper ${paperSlug} · org ${orgSlug} · claim ${claimId} · researcher ${researcherSlug} · snapshot ${snapshotId ?? '—'} · token ${TOKEN ? 'yes' : 'NO'}`);5960const PAGES = [61 { path: '/graph', check: 'graph' },62 { path: `/graph?node=${orgSlug}&mode=company`, check: 'graph' },63 { path: `/graph/${modelSlug}?mode=lineage`, check: 'graph' },64 { path: '/timeline', check: 'timeline' },65 { path: '/timeline?include_backfill=1&year=2025', check: 'timeline' },66 { path: '/time-machine?date=2025-06-01', check: 'tm' },67 { path: `/diff?a=${weekAgo}&b=${today}`, check: 'diff' },68 { path: '/changes' },69 { path: `/changes/${today}`, check: 'digest' },70 { path: '/search?q=open+reasoning+models+over+30B+released+in+2026', check: 'search' },71 { path: '/explore', check: 'builder' },72 { path: '/papers' },73 { path: `/papers/${paperSlug}` },74 { path: '/companies' },75 { path: `/companies/${orgSlug}` },76 ...(researcherSlug ? [{ path: `/explore/researcher/${researcherSlug}` }] : []),77 ...(claimId ? [{ path: `/claims/${claimId}` }] : []),78 { path: '/sources' },79 { path: '/methodology' },80 { path: '/developers', check: 'builder-dev' },81 { path: '/graph/does-not-exist-xyz', expected: 404 },82 { path: '/claims/claim_does_not_exist', expected: 404 },83];84const LIGHT_SUBSET = new Set(['/graph', `/graph/${modelSlug}?mode=lineage`, '/timeline', '/time-machine?date=2025-06-01', `/diff?a=${weekAgo}&b=${today}`, `/changes/${today}`, '/search?q=open+reasoning+models+over+30B+released+in+2026', '/explore', `/papers/${paperSlug}`, `/companies/${orgSlug}`, `/claims/${claimId}`, '/sources', '/methodology', '/developers']);8586const filterErrors = (errors, expected) => errors.filter((e) => !/favicon|Failed to load resource: the server responded with a status of 404|the server responded with a status of 429/.test(e)).filter((e) => !(expected === 404 && /Encountered a script tag while rendering React component/.test(e)));8788const browser = await chromium.launch();89let failures = 0;90const slug = (p) => p.replace(/^\//, '').replace(/[^a-z0-9]+/gi, '_').slice(0, 60) || 'home';9192async function checks(page, kind) {93 const problems = [];94 if (kind === 'graph') {95 await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 }).catch(() => undefined);96 const n = await page.locator('[data-graph-canvas] [data-node]').count();97 if (n < 2) problems.push(`graph nodes=${n}`);98 if (!(await page.locator('[data-graph-count]').count())) problems.push('graph count line missing');99 }100 if (kind === 'search') {101 const n = await page.locator('[data-compiled-chip]').count();102 if (n < 1) problems.push(`compiled chips=${n}`);103 if (!(await page.locator('[data-open-builder]').count())) problems.push('builder link missing');104 }105 if (kind === 'digest') {106 const text = await page.evaluate(() => document.body.innerText);107 if (!/occurred/i.test(text)) problems.push('digest lacks occurred caption');108 }109 if (kind === 'tm') {110 if (!(await page.locator('[data-tm-banner]').count())) problems.push('time-machine banner missing');111 const text = await page.evaluate(() => document.body.innerText);112 if (!/reconstructed/i.test(text)) problems.push('time-machine lacks reconstructed note');113 }114 if (kind === 'diff') {115 const text = await page.evaluate(() => document.body.innerText);116 if (!/new models/i.test(text) || !/price changes/i.test(text)) problems.push('diff sections missing');117 }118 if (kind === 'timeline') {119 if (!(await page.locator('[data-timeline-workbench]').count()) && !(await page.evaluate(() => /No events/i.test(document.body.innerText)))) problems.push('timeline workbench missing');120 }121 if (kind === 'builder' && !(await page.locator('[data-query-builder]').count())) problems.push('query builder missing');122 if (kind === 'builder-dev' && !(await page.locator('[data-request-builder]').count())) problems.push('request builder missing');123 return problems;124}125126for (const theme of THEMES) {127 for (const width of WIDTHS) {128 const mobile = width < 768;129 const ctx = await browser.newContext({ viewport: { width, height: mobile ? 844 : 900 }, deviceScaleFactor: 1, isMobile: mobile, hasTouch: mobile, colorScheme: theme });130 await ctx.addInitScript((t) => localStorage.setItem('aia-theme', t), theme);131 const page = await ctx.newPage();132 for (const { path, check, expected: exp } of PAGES) {133 if (theme === 'light' && !LIGHT_SUBSET.has(path)) continue;134 if (theme === 'light' && ![390, 1440, 320, 1920].includes(width)) continue;135 const errors = [];136 page.on('pageerror', (e) => errors.push(String(e)));137 page.on('console', (m) => {138 if (m.type() === 'error') errors.push(m.text());139 });140 const t0 = Date.now();141 const res = await page.goto(BASE + path, { waitUntil: 'networkidle', timeout: 90000 }).catch((e) => ({ status: () => `ERR ${e.message.slice(0, 40)}` }));142 await page.waitForTimeout(700);143 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth).catch(() => -1);144 const expected = exp ?? 200;145 const status = res.status();146 const filtered = filterErrors(errors, expected);147 const problems = status === 200 && check ? await checks(page, check) : [];148 const ok = status === expected && overflow <= 0 && filtered.length === 0 && problems.length === 0;149 if (!ok) failures++;150 console.log(`${ok ? 'OK ' : 'FAIL'} ${theme.padEnd(5)} ${String(width).padStart(4)} ${status} ${String(Date.now() - t0).padStart(5)}ms overflow=${overflow} errors=${filtered.length} ${path}${problems.length ? ' :: ' + problems.join('; ') : ''}${filtered.length ? ' :: ' + filtered[0].slice(0, 160) : ''}`);151 if ([320, 390, 1440, 1920].includes(width)) await page.screenshot({ path: `${OUT}${theme}-${width}-${slug(path)}.png`, fullPage: width >= 768 }).catch(() => undefined);152 page.removeAllListeners('pageerror');153 page.removeAllListeners('console');154 }155 await ctx.close();156 }157}158159// ---- flows160async function flow(name, width, fn) {161 const ctx = await browser.newContext({ viewport: { width, height: width < 768 ? 844 : 900 }, colorScheme: 'dark', isMobile: width < 768, hasTouch: width < 768 });162 await ctx.addInitScript(() => localStorage.setItem('aia-theme', 'dark'));163 const page = await ctx.newPage();164 const errors = [];165 page.on('pageerror', (e) => errors.push(String(e)));166 page.on('console', (m) => {167 if (m.type() === 'error') errors.push(m.text());168 });169 try {170 await fn(page);171 const filtered = filterErrors(errors, 200);172 if (filtered.length) throw new Error(`console: ${filtered[0].slice(0, 160)}`);173 console.log(`OK flow ${name} @${width}`);174 } catch (e) {175 failures++;176 console.log(`FAIL flow ${name} @${width} :: ${e.message.slice(0, 200)}`);177 await page.screenshot({ path: `${OUT}flow-${name}-${width}-fail.png`, fullPage: true }).catch(() => undefined);178 }179 await ctx.close();180}181182for (const width of [390, 1440]) {183 // Graph: select a node → inspector shows it; expand loads more nodes (or reports already expanded).184 await flow('graph-inspect-expand', width, async (page) => {185 await page.goto(`${BASE}/graph/${modelSlug}?mode=lineage`, { waitUntil: 'networkidle' });186 await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 });187 const before = await page.locator('[data-graph-canvas] [data-node]').count();188 const nodes = page.locator('[data-graph-canvas] [data-node]');189 const target = nodes.nth(Math.min(1, before - 1));190 await target.dispatchEvent('pointerdown', { pointerId: 1, clientX: 0, clientY: 0, isPrimary: true, button: 0 });191 await target.dispatchEvent('pointerup', { pointerId: 1, clientX: 0, clientY: 0, isPrimary: true, button: 0 });192 // Fallback: keyboard selection is always available.193 await target.focus();194 await page.keyboard.press('Enter');195 await page.waitForTimeout(400);196 const inspectors = await page.locator('[data-graph-inspector]').count();197 if (inspectors < 1) throw new Error('inspector not rendered after selection');198 const expand = page.locator('[data-graph-expand]:not([disabled]):visible').first();199 if (await expand.count()) {200 await expand.click();201 await page.waitForTimeout(2500);202 const after = await page.locator('[data-graph-canvas] [data-node]').count();203 if (after < before) throw new Error(`nodes shrank ${before} → ${after}`);204 }205 await page.screenshot({ path: `${OUT}flow-graph-${width}.png` });206 });207208 // Graph: switch mode via the rail (desktop) → count line updates without navigation error.209 if (width >= 1024)210 await flow('graph-mode-switch', width, async (page) => {211 await page.goto(`${BASE}/graph?node=${orgSlug}&mode=company`, { waitUntil: 'networkidle' });212 await page.waitForSelector('[data-graph-canvas] [data-node]', { timeout: 20000 });213 await page.locator('[data-graph-mode="provider"]').click();214 await page.waitForTimeout(2500);215 if (!/mode=provider/.test(page.url())) throw new Error(`url not updated: ${page.url()}`);216 const text = await page.locator('[data-graph-count]').innerText();217 if (!/nodes/.test(text)) throw new Error('count line missing after mode switch');218 });219220 // Search: remove a compiled chip → re-query without its words.221 await flow('search-chip-remove', width, async (page) => {222 await page.goto(`${BASE}/search?q=open+reasoning+models+over+30B+released+in+2026`, { waitUntil: 'networkidle' });223 const chips = await page.locator('[data-compiled-chip]').count();224 if (chips < 2) throw new Error(`chips=${chips}`);225 const remove = page.locator('[data-compiled-chip] a[aria-label^="Remove"]').first();226 const label = await remove.getAttribute('aria-label');227 const before = page.url();228 await Promise.all([page.waitForURL((u) => u.toString() !== before), remove.click()]);229 await page.waitForLoadState('networkidle');230 const after = await page.locator('[data-compiled-chip]').count();231 if (after >= chips) throw new Error(`chips not reduced after "${label}": ${chips} → ${after}`);232 });233234 // Explore builder: set a filter, run → /models URL with the param; save a query → appears in the list.235 await flow('explore-builder', width, async (page) => {236 await page.goto(`${BASE}/explore`, { waitUntil: 'networkidle' });237 await page.locator('[data-query-builder] select').nth(2).selectOption({ index: 3 }); // Parameters ≥ 7B238 await page.waitForTimeout(300);239 const target = await page.locator('[data-target-url]').innerText();240 if (!/\/models\?.*min_params=/.test(target)) throw new Error(`target url ${target}`);241 if (!/type=model/.test(page.url())) throw new Error(`share url not mirrored: ${page.url()}`);242 await page.locator('[data-save-query]').click();243 await page.waitForTimeout(300);244 if ((await page.locator('[data-saved-queries] li').count()) < 1) throw new Error('saved query not listed');245 await Promise.all([page.waitForURL(/\/models\?/), page.locator('[data-run-query]').click()]);246 });247248 // Time machine: presets navigate and the banner says reconstructed / observed.249 await flow('time-machine-presets', width, async (page) => {250 await page.goto(`${BASE}/time-machine`, { waitUntil: 'networkidle' });251 await Promise.all([page.waitForURL(/date=2024-01-01/), page.locator('a[href*="date=2024-01-01"]').first().click()]);252 await page.waitForLoadState('networkidle');253 const text = await page.locator('[data-tm-banner]').innerText();254 if (!/reconstructed|observed/i.test(text)) throw new Error('banner missing basis');255 await Promise.all([page.waitForURL(/scope=benchmarks/), page.locator('a[href*="scope=benchmarks"]').first().click()]);256 });257258 // Changes feed: load more via cursor.259 await flow('changes-load-more', width, async (page) => {260 await page.goto(`${BASE}/changes`, { waitUntil: 'networkidle' });261 const before = await page.locator('[data-event-id]').count();262 const btn = page.locator('[data-load-more]');263 if (!(await btn.count())) return; // fewer than 50 events: nothing to page264 await btn.click();265 await page.waitForTimeout(2500);266 const after = await page.locator('[data-event-id]').count();267 if (after <= before) throw new Error(`load more did not append (${before} → ${after})`);268 });269270 // Developers: request builder → Try → status line.271 await flow('developers-try', width, async (page) => {272 await page.goto(`${BASE}/developers`, { waitUntil: 'networkidle' });273 await page.locator('[data-route-picker]').selectOption('stats');274 await page.locator('[data-try]').click();275 await page.waitForSelector('[data-try-result] pre', { timeout: 20000 });276 const text = await page.locator('[data-try-result]').innerText();277 if (!/HTTP 200/.test(text)) throw new Error(`try result: ${text.slice(0, 80)}`);278 });279280 // Admin: login → quality → entity-resolution → anomalies → audit → extractions; token never in HTML.281 if (TOKEN)282 await flow('admin-workbenches', width, async (page) => {283 await page.goto(`${BASE}/admin`, { waitUntil: 'networkidle' });284 if ((await page.content()).includes(TOKEN)) throw new Error('token leaked before login');285 await page.locator('input[type="password"]').first().fill(TOKEN);286 await Promise.all([page.waitForURL(/\/admin\/(overview|connectors)/, { timeout: 30000 }), page.locator('form button[type="submit"]').first().click()]);287 const pages = ['/admin/quality', '/admin/entity-resolution', '/admin/anomalies', '/admin/quarantine', '/admin/audit', '/admin/runs', ...(snapshotId ? [`/admin/extractions/${snapshotId}`] : [])];288 for (const p of pages) {289 const r = await page.goto(`${BASE}${p}`, { waitUntil: 'networkidle', timeout: 60000 });290 if (r.status() !== 200) throw new Error(`${p} → ${r.status()}`);291 const html = await page.content();292 if (html.includes(TOKEN)) throw new Error(`token leaked in ${p}`);293 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);294 if (overflow > 0) throw new Error(`${p} overflow ${overflow}`);295 if (p === '/admin/quality' && !(await page.locator('[data-quality-tiles] li').count())) throw new Error('quality tiles missing');296 if (p === '/admin/entity-resolution' && !(await page.locator('[data-resolution-pair]').count()) && !/No candidate pairs/.test(await page.evaluate(() => document.body.innerText))) throw new Error('resolution pairs missing');297 if (p === '/admin/runs' && !(await page.locator('[data-rollback]').count())) throw new Error('rollback buttons missing');298 if (p.startsWith('/admin/extractions') && !(await page.locator('[data-extraction-text]').count())) throw new Error('extraction text missing');299 await page.screenshot({ path: `${OUT}admin-${width}-${slug(p)}.png`, fullPage: width >= 768 });300 }301 // Sources shows operator columns when the cookie exists (cookie path is /admin, so /sources must not see it → public view).302 await page.goto(`${BASE}/sources`, { waitUntil: 'networkidle' });303 if ((await page.content()).includes(TOKEN)) throw new Error('token leaked in /sources');304 // Rollback button opens a confirmation field, disabled until the id is typed.305 await page.goto(`${BASE}/admin/runs`, { waitUntil: 'networkidle' });306 const rb = page.locator('[data-rollback]:not([disabled])').first();307 if (await rb.count()) {308 await rb.click();309 const confirm = page.locator('[data-rollback-form] button[type="submit"]');310 if (!(await confirm.isDisabled())) throw new Error('rollback confirm enabled before typing the id');311 }312 });313}314315// ---- OG images and sitemap (plain HTTP)316for (const p of ['/graph/og', `/graph/og?node=${modelSlug}&mode=lineage`, '/time-machine/og?date=2025-06-01', `/diff/og?a=${weekAgo}&b=${today}`, `/changes/${today}/opengraph-image`, `/companies/${orgSlug}/opengraph-image`, `/papers/${paperSlug}/opengraph-image`]) {317 const r = await fetch(BASE + p);318 const ok = r.status === 200 && /image\/png/.test(r.headers.get('content-type') ?? '');319 if (!ok) failures++;320 console.log(`${ok ? 'OK ' : 'FAIL'} og ${r.status} ${r.headers.get('content-type')} ${p}`);321}322{323 const idx = await (await fetch(`${BASE}/sitemap.xml`)).text();324 const st = await (await fetch(`${BASE}/sitemap/static.xml`)).text();325 const okIdx = /model_family-0\.xml/.test(idx) && /license-0\.xml/.test(idx) && /artifact-0\.xml/.test(idx) && /changes\.xml/.test(idx);326 const okSt = ['/time-machine', '/graph', '/frontier', '/pulse', '/families', '/licenses', '/agents', '/calculator', '/run-locally', '/find-a-model', '/open'].every((r) => st.includes(`<loc>`) && st.includes(r));327 if (!okIdx || !okSt) failures++;328 console.log(`${okIdx && okSt ? 'OK ' : 'FAIL'} sitemap index(model_family/license/artifact/changes)=${okIdx} static(new routes)=${okSt}`);329 const fam = await fetch(`${BASE}/sitemap/model_family-0.xml`);330 const famBody = await fam.text();331 const okFam = fam.status === 200 && /\/families\//.test(famBody);332 if (!okFam) failures++;333 console.log(`${okFam ? 'OK ' : 'FAIL'} sitemap model_family-0 ${fam.status}`);334}335336await browser.close();337console.log(failures ? `\n${failures} failure(s)` : '\nAll D3 checks passed');338process.exit(failures ? 1 : 0);339