diff --git a/.gitignore b/.gitignore index 1461fed..aeb294f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ coverage/ # superpowers skill state .superpowers/ + +# playwright/chrome-devtools MCP scratch output +.playwright-mcp/ +*-screenshot.png diff --git a/README.md b/README.md index fd76479..ce31730 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ # 🔭 RepoLens -### One click turns any repo into a plain-English briefing. +### One click opens the case file on any repo. -**What it is · whether it's a good fit · how it's actually built · what it connects to.** +**The verdict · the evidence · the red flags · how it's actually built — in plain English, before the README's pitch.**    - - + +  @@ -32,6 +32,9 @@ A scan opens to a **verdict landing** and fans out into focused tabs: | ⚖️ | **Verdict** | Fit call (strong / solid / care / risky), a one-line bottom line, measured facts, and the top things worth noting — first thing you see. | | 🧠 | **Deep Dive** | The core concepts → how they build on each other → a plain-English ("explain it like I'm five") walkthrough. Optionally grounded by **measured facts** from the local runner. | | 📚 | **Library** | Every repo you've analyzed, as a sortable / filterable triage grid with fit chips, a stats bar, **bulk multi-select delete**, and one-click **Export / Import / Backup**. | +| 🗂️ | **Triage & decide** | Keyboard-first **Adopt / Trial / Hold / Reject**, a Tech Radar, Boards, fit-delta tracking, notes, and daily **drift alerts** when repos go stale. | +| ★ | **Evaluate & compare** | Score repos **1–5** against your own rubric, grade docs **A–F**, and put any **2–10** side-by-side in a decision matrix (CSV / Markdown export). | +| 🔍 | **Discover** | Search GitHub from inside the extension, or get **recommendations** from the repos you've already adopted. | | 🕸️ | **Connections** | A walkable map centred on the current repo, showing how it relates to the others you've scanned. | | 🤝 | **Synergies** · **Versus** · **Combinator** | Complements, head-to-heads, and fused project ideas — grounded in *your* library. | diff --git a/docs/audits/audit-v3.0.0.html b/docs/audits/audit-v3.0.0.html new file mode 100644 index 0000000..3787ff6 --- /dev/null +++ b/docs/audits/audit-v3.0.0.html @@ -0,0 +1,412 @@ + + +
+ + +A full audit of the v3.0.0 extension and the redesigned website across security, code & architecture, tests, and accessibility. The short version: zero critical issues, the security fundamentals that matter most are verified clean, and the work to do is concentrated and mostly known — starting with a one-line fix that turns CI green.
+ +0 CRITICAL across all four dimensions. Security is genuinely sound for a key-handling extension — keys are never logged, the settings export is provably allowlist-driven, PKCE + OAuth state are correct, there's no eval, and the content script is minimal. Test coverage is high on pure logic (57 of ~75 modules), accessibility intent is above average, and the website's motion/perf hygiene is exemplary. The issues cluster in five places: a red CI from a known flaky test (trivial fix), maintainability debt in three oversized files, accessibility polish (focus lifecycle + a few low-contrast tokens), attack-surface hardening (endpoint validation), and basic SEO (sitemap / OG image).
+A single thread connects three of the four passes — and it's a one-line fix.
+main — caused by a time-flaky test, not a missing scripttests/maintenance.test.js · maintenance.js:15,25CI run #19 failed while #18 (24 min earlier) passed — exactly when daysSincePush(daysAgo(30)) tipped from 31→32 as the UTC date rolled to the 15th. The website pass flagged this as "no npm test script," but that was verified wrong: ci.yml runs at the repo root where npm test = vitest run exists. The real cause: bandFromSignals and daysSincePush call real Date.now() with no injection point, while the test pins NOW=2026-06-13 to build inputs — so measured age drifts +1 day/day. A latent sibling exists in diff-analysis.test.js (loose assertions hide it for now), and the band-boundary tests will break later (≈Aug 12 and Jan 13).
+Fix: give bandFromSignals/daysSincePush an injectable today = Date.now() param (mirroring buildMaintenancePrompt, which already does this) and pass NOW in the test — or vi.useFakeTimers() + vi.setSystemTime('2026-06-13'). vi.useFakeTimers is currently used in zero test files; adopt it for any time-based test. This turns CI green.
+An extension that holds 20+ provider keys + OAuth tokens and feeds untrusted README content to an LLM. The core handling is sound; the gaps are attack-surface and log hygiene.
+ +<all_urls> to any originmanifest.json:41 · options-providers.js:51Saving a custom endpoint calls chrome.permissions.request({ origins: [origin + '/*'] }) against the optional https://*/* grant. A social-engineered URL (e.g. https://company-intranet.corp/) could hand the extension persistent cross-origin fetch to an internal network.
+Fix: validate the endpoint is a public host before requesting — block RFC-1918 ranges, localhost, and .internal/.local TLDs (Ollama already has static localhost perms).
+manifest.json:38 · background.js (callCompat)Static http://localhost/* + 127.0.0.1/* permissions plus a user/import-writable endpoint string mean a tampered endpoint (http://localhost:8080/internal-admin) is fetched with no further consent.
+Fix: validate stored endpoints against the provider's registered host pattern before fetch; pin Ollama to port 11434.
+prompt.js:7 ASCII keyword filter is bypassable by small-caps/Cyrillic lookalikes; the structural delimiters are the real (probabilistic) guard. The changelog's "hardened" claim overstates it — soften the wording.background.js:447,498,502,1557 log callback URLs + JSON.stringify(err) provider bodies; truncate/strip token-ish fields, gate behind a DEBUG flag. (Also flagged by the code pass.)whats-new.html is web-accessible to <all_urls> — enables extension fingerprinting from any page; scope to chrome-extension://*/* or drop it.oauth-xai.js:61; a partial clear leaves a stale xaiKey. Consolidate to structured creds after a migration window.backup.js enforces MAX_ROWS but a 1 MB repoId passes; cap field lengths (~256).esc() doesn't escape single quotes (format.js:4) — latent for single-quoted attributes; align with safe-html.js's escapeHtml.background.js:1435) — Google's REST design, not a bug, but more exposed than header auth.tabs permission broader than needed — the tabs.onUpdated OAuth listener duplicates the webNavigation one; dropping it could remove the tabs perm.sendMessage + the aiChain serializer.Three files hold most of the logic — library.js (2,593), output-tab.js (2,576), background.js (1,567), all ~3× the size guideline. The HIGH findings are duplication that has already silently diverged.
render() and getVisibleRows() duplicate the filter/sort pipeline — and have divergedlibrary.js:177 vs 1834The export path re-implements the filter→sort pipeline and has already dropped the eval sort, the NL-filter dedup, and parts of the collection/decision filters — so exports can return the wrong rows today.
+Fix: extract one pure applyFilters(rows, state, maps) that both call. Highest-leverage refactor in the codebase.
+background.js:771–1169Every runXxx redefines a 3-line read-merge-write against a session sub-key. Two concurrent lens writers to the same key can clobber each other.
+Fix: a makeSessionPatcher(key, subKey) factory — removes 33 lines and gives one place to serialize writes.
+LANG_COLORS and FIT_ORDER — both already driftinglibrary.js:25,113… · output-tab.js:617LANG_COLORS is copied in both big files (the copies diverge in palette); FIT_ORDER is re-declared 6× inside functions, one of which adds 'unrated' so ordering is inconsistent.
+Fix: a shared lang-colors.js; hoist FIT_ORDER to module scope once.
+callAI queue serializes unrelated flowsbackground.js:1201One module-level promise chain throttles all AI calls — so a multi-minute deep-dive blocks a quick ASK_CACHED for its full duration; the keep-alive .catch(()=>{}) also swallows rejections.
+Fix: exempt low-latency parts (ask) from the deep-dive queue; document the throttling intent at the call site.
+catch {} at output-tab.js:2573 swallows a whole render block with no signal — add a console.warn + visible fallback.RERUN set has no .catch (background.js:198) — a storage reject leaves the output tab polling to its 90s timeout; add error propagation to sendResponse.openNote/openQuickAsk (library.js:353,465) re-add keydown handlers on every open (the comment says it's fixed; it isn't). Use { once: true }.runXxx re-reads PROVIDER_KEYS (~10 IPC round-trips on "Run All"); pass keys down from one read.library.js — the root cause of the render/getVisibleRows drift; consolidate into one state object incrementally.window.prompt fallback (library.js:1318) — blocked in some extension contexts; remove the dead path.msg.type string literals with no shared enum; a typo is a silent no-op. Export a MSG_TYPES constant.Strong by design — ~60 vitest files cover 57 of ~75 modules, targeting pure logic (the model: extract library-data.js out of library.js and test it). The three big files are untested deliberately (zero exports, explicitly excluded) — not fake coverage. The gaps are real pure logic still trapped inside, plus two security-relevant modules.
evaluations.js → computeScore has zero testsevaluations.jsA pure weighted-average with null-guards, range-filtering and zero-weight fallback — used by both the eval panel and the N-way compare matrix, yet untested. The single highest-value, lowest-effort add.
+Fix: unit-test computeScore; mock the chrome.storage CRUD exactly as oauth-openai.test.js does.
+oauth-pkce.js & oauth-xai.js are untestedoauth-pkce.js · oauth-xai.jsSecurity-sensitive: base64url/createPkcePair are pure/deterministic, and the full xAI device-code flow (request/poll/store/refresh) has no coverage — while the sibling oauth-openai.js is tested.
+Fix: add deterministic tests for the PKCE primitives and the xAI token lifecycle.
+diff-analysis.test.js sibling; adopt vi.useFakeTimers (used in 0 files today).library.js — radarToMarkdown, exportCompareMatrix, exportDecisionMatrix, exportDigest are pure builders that could move to a library-export.js and be tested like library-data.js..verify/drive.mjs is a real Playwright harness (~20 assertions, loads the unpacked MV3 extension, 12 screenshot stages) but it's gitignored, playwright isn't a dep, and CI never runs it. Promote it: add the dep + an npm script + non-zero exit + CI wiring.store.test.js doesn't uniformly clear collections; vitest.config.js sets no clearMocks/restoreMocks.Above-average intent — global :focus-visible, single-key handlers that guard typing fields, an exemplary aria-hidden mascot, real live regions on the library, and strong command-palette ARIA. Two consistent gaps: focus lifecycle on modals and announcement on the main output surface. Plus a cross-product contrast theme (the website pass found the same class of issue in its tokens).
output-tab.html:763 · 27 tab panelsThe primary analysis surface cycles loading copy and injects into 27 panels with no aria-live/role="status" — a screen-reader user gets no signal a scan is running or done. (WCAG 4.1.3)
+Fix: role="status" aria-live="polite" on #loading-msg + one polite sr-only region announcing start/complete/error.
+j/k nav is visual-onlyoutput-tab.html:832 · library.js:2066Tabs are plain buttons with an .active class (no role="tablist/tab/tabpanel", no aria-selected). And library setJkFocus toggles a class + scrolls but never calls .focus() on the <div> cards — SRs announce nothing, and Tab jumps away. (WCAG 4.1.2, 2.4.3)
+Fix: add tablist roles + aria-selected; give cards tabindex="-1"/role="article" and move real focus on j/k.
+palette.js:95 · library.js (compare/boards)Modals have good role="dialog"+aria-modal+Escape, but none trap Tab or restore focus to the trigger on close, and the palette listbox lacks aria-activedescendant (arrowing is silent to SRs). (WCAG 2.1.2, 4.1.3)
+Fix: a shared open/close focus-lifecycle helper (move focus in, trap Tab, restore on close) applied to every popover/modal; add aria-activedescendant to the palette.
+themes.css (--text-muted) · website global.css (--faint/--accent/--muted)Extension: --text-muted (descriptions/hints, 65 uses) fails AA on 5 of 13 themes — including the default midnight (4.15:1) and worst rosepine (3.41:1); --accent-as-text fails on solarized/claude/bmw. Website: --faint body text fails AA in both themes (2.20 latte / 2.69 dark — .hero-foot, footer), and small --accent/--muted labels are AA-large-only.
+Fix: bump --text-muted/--faint to ≥4.5:1 per theme (darken light, lighten dark); route --accent-as-text through the darker --accent-deep/--accent-2.
+batch.html & stack-tab.html — the only two pages missing it; both run infinite pulse loaders. Add the same @media (prefers-reduced-motion: reduce) reset (better: one shared a11y.css).:focus-visible suppressed on focusable widgets — decision-popover buttons, eval badge, palette input get programmatic focus but show no ring (library.html:481, palette.css:46). (WCAG 2.4.11)title, not aria-label — card actions (pin/compare/note/copy/remove) + header buttons; inconsistent with #settings which has a label.<div> (its rows are real buttons, so still operable).♥/★/? symbols without text alternatives in a few spots.width over 25s — perf nit vs the team's own rules; disabled under reduced-motion, so no a11y issue.The fresh redesign's motion/perf hygiene is genuinely exemplary (GSAP fully lazy + 100% behind prefers-reduced-motion, correct reduced-motion video fallback, clean heading order, CLS well-defended with explicit dimensions, the case-sensitive /RepoLens basePath handled). The gaps are SEO basics and a couple of perf nits.
sitemap.xml or robots.txtadd app/sitemap.ts · app/robots.tsNothing tells crawlers the canonical URL set for a freshly launched marketing site. Next 15 emits both under output: export.
+Fix: add app/sitemap.ts + app/robots.ts with absolute /RepoLens URLs enumerating /, /changelog, and the 10 docs routes.
+layout.tsx sets summary_large_image with no images; link unfurls render bare. Add public/og.png (1200×630) or app/opengraph-image.tsx.preload="metadata" to the video + a poster <link rel=preload>.backdrop-filter: blur; trace on a throttled device.size-adjust; next/font mitigates); focus-ring contrast weak in latte; warm tab underline 1.88:1 (decorative card); fumadocs lazy-files coupling in lib/source.ts is version-fragile; the RSC .txt 404 stays cosmetic; stray untracked whats-new.html at repo root (doesn't ship).An audit is only honest if it says what's done right. A lot is.
+pickSafe() excludes keys/tokens on export and import.eval / dynamic code anywhere.aria-hidden, meaning carried by text/color.aria-selected.aspect-ratio on media./RepoLens, manual asset prefixing.In order of leverage-per-effort.
+Inject today into bandFromSignals/daysSincePush (or vi.setSystemTime in the test); fix the latent diff-analysis sibling. One change, main goes green.
applyFilters() HIGHCollapse the render()/getVisibleRows() divergence — it's an active wrong-export bug source — then lift FIT_ORDER/LANG_COLORS to shared modules.
computeScore + the OAuth helpers HIGHHigh-value, low-effort, security-relevant — the biggest coverage holes that are trivially testable.
+Add the output-tab live region + tab semantics, make j/k move real focus, and add a shared modal focus-trap/restore helper.
Bump --text-muted/--faint to AA on the failing themes (incl. default midnight) across both extension and site; stop suppressing focus rings on focusable widgets; add the reduced-motion reset to batch.html/stack-tab.html.
Validate custom/Ollama endpoints (block private ranges), scope whats-new.html, truncate OAuth logs, cap backup string lengths.
Add sitemap.ts + robots.ts + an OG image; preload the hero poster.
Split library.js/output-tab.js/background.js along their seams (export builders, session patcher, render vs. state) as growth demands. Promote .verify/drive.mjs to enforced E2E.
- One token-aware lens, thirteen themes, one accent swap away. Try a palette — the whole - page and the mascot re-skin live. Every expression maps to a real scan moment, and all of - it folds to a static glyph under reduced-motion. + The lens mascot reacts to what it finds — wide-open on a clean repo, narrowed and + skeptical on a risky one. Every expression maps to a real scan moment, and all of it + folds to a static glyph under reduced-motion. Works the day shift or the night stakeout.