Inspiration

What it does

How we built it

Challenges we ran into

Accomplishments that we're proud of

What we learned

What's next for CODELENS

CodeLens™ — The Story Inspiration Every developer has shipped a bug they should have caught. Not because they were careless — but because code review is cognitively expensive. You're scanning hundreds of lines for subtle patterns: a missing conn.close(), an f-string wired directly into a SQL query, a nested loop that looks innocent at $n = 10$ but detonates at $n = 10{,}000$.

The idea behind CodeLens was simple: what if a second pair of eyes never got tired, never missed a pattern, and could explain exactly what would go wrong in production — before you pushed?

Static linters catch syntax. CodeLens catches intent. There's a difference between a tool that tells you "this line has an issue" and one that tells you "this line will hand an attacker your entire database."

I wanted to build the second kind.

How I Built It The Stack The architecture is deliberately lean — two services, one AI model, zero overhead:

React 19 (Vercel) → FastAPI (Render) → Groq LLM (llama-3.3-70b) The frontend is a pure React app with no component library — every card, gauge, and animation is hand-crafted in inline styles. The backend is a FastAPI service with three core endpoints: /analyze, /fix, and /github/analyze.

The Prompt Engineering Problem The hardest engineering decision wasn't the stack — it was the prompt. Getting a language model to return structured, accurate JSON consistently is non-trivial. The analysis response needs to be machine-parseable every time, across any language, any code quality, any edge case.

The system prompt enforces a strict JSON contract:

{ "health_score": 28, "issues": [ { "severity": "critical", "line_start": 7, "suggestion": "Use parameterized queries..." } ] } Any deviation — a markdown fence, a trailing explanation, a missing field — breaks the frontend. The solution was to strip markdown artifacts with regex post-processing and wrap every parse in a structured error handler.

The Health Score The health score is a 0–100 integer computed by the LLM, guided by this rubric baked into the system prompt:

$$H = \begin{cases} 90\text{–}100 & \text{Excellent — production ready} \ 70\text{–}89 & \text{Good — minor issues} \ 50\text{–}69 & \text{Needs work} \ 30\text{–}49 & \text{Poor — significant risks} \ 0\text{–}29 & \text{Critical — do not ship} \end{cases}$$

The gauge animates from 0 to the score using a cubic-bezier SVG stroke-dashoffset transition:

$$\text{offset} = C - \frac{H}{100} \cdot C \quad \text{where } C = 2\pi r \cdot 0.75$$

The $0.75$ factor clips the arc to 270° (a three-quarter circle), giving the classic speedometer aesthetic.

The Rework Pipeline The "Rework Code" feature chains two LLM calls conceptually into one UI action:

/analyze returns a structured list of issues with suggestions The user clicks ✦ Rework Code with AI Fixes /fix receives the original code + full issue list, and returns a rewritten file with inline # FIX: comments The fix prompt encodes every issue as a line-referenced instruction:

  • [Line 7] [CRITICAL] SQL Injection: Use parameterized queries
  • [Line 27] [CRITICAL] Hardcoded Credentials: Use os.environ.get(...) The LLM treats this as a diff spec — it knows exactly what to change and where.

What I Learned

  1. LLMs hallucinate line numbers badly In early versions, the model would confidently report issues on lines that didn't exist. A 50-line file would get issues reported at lines 73, 91, 108. The model was pattern-matching against its training data rather than the actual input.

The fix: inject line numbers directly into the prompt.

1 | import sqlite3 2 | import os 3 | 4 | def get_user(username): 5 | conn = sqlite3.connect("users.db") With explicit line markers and an instruction "you MUST only reference lines 1 to N", hallucinated line numbers dropped to near zero. The model now has a concrete anchor rather than a floating reference.

  1. CORS is unforgiving with wildcard origins allow_origins=["*"] and allow_credentials=True is invalid per the CORS specification. Browsers reject it at the preflight stage. The fix is either:

Use allow_credentials=False with wildcard origins, or Enumerate exact origins with credentials enabled This cost more debugging time than it should have.

  1. Free hosting has cold start physics Render's free tier sleeps services after 15 minutes of inactivity. The first request after sleep takes ~30–50 seconds as the container spins up. This is invisible in the backend logs but catastrophic for UX — users see a timeout and assume the app is broken.

The workaround is a loading state with a message explaining the cold start, so users wait instead of leave.

  1. Environment variables at build time vs runtime Vite bakes VITE_* variables into the JavaScript bundle at build time, not at runtime. This means:

Changing VITE_API_BASE in Vercel's dashboard requires a full redeploy Old deployment URLs (preview builds) serve old bundles permanently The production domain always reflects the latest build This distinction matters more than it seems when you're debugging why a live app is still hitting a dead backend.

Challenges Railway → Render Migration The original backend ran on Railway. After deployment, Railway was routing external traffic to port 8000 while the $PORT environment variable was set to 8080 — a silent mismatch that produced persistent 502s. Internal healthchecks passed (Railway probed the container directly), but external traffic failed at the edge.

After exhausting Railway's debugging options, I migrated to Render. The first Render build failed because Python 3.14 (Render's default) had no pre-built wheel for pydantic-core==2.23.4, and Render's filesystem is read-only so Rust compilation failed mid-build. The fix was relaxing the pydantic version constraint to >=2.10.0, letting pip resolve a wheel with 3.14 support.

Scroll-Snap Vulnerability Slides The right panel uses CSS scroll-snap-type: y mandatory with each vulnerability card occupying exactly 100% of the container height. The challenge: the container height is dynamic (depends on the header and panels). Getting the snap to feel native — no jank, correct dot navigation sync, SLIDE button jumping to the right card — required careful coordination between the onScroll handler, scrollTo(), and React state.

$$\text{slide index} = \left\lfloor \frac{\text{scrollTop}}{\text{containerHeight}} + 0.5 \right\rfloor$$

Rounding to the nearest integer (rather than flooring) prevents the dot nav from flickering during the snap animation.

What's Next VS Code extension — inline annotations directly in the editor gutter GitHub Actions integration — block PRs that drop the health score below a threshold Multi-file analysis — scan an entire repository, not just a single file Trend dashboard — track health score over time per repository Built by Steven K. · Head of AXON LATTICE LABS™ CodeLens™ — See your code's future before it ships.

Built With

  • all
  • analysis
  • auto-deploy-from-github)
  • eslint
  • free-tier)-fonts-&-design-google-fonts-?-space-grotesk
  • github
  • httpx
  • is
  • javascript-(jsx)-frameworks-&-libraries-fastapi
  • jetbrains-mono
  • languages-python
  • node.js
  • on-demand
  • outfit-dev-tools-git
  • per
  • pip-no-database-?-stateless-api
  • pydantic
  • react-19
  • render-(backend
  • uvicorn-ai-/-apis-groq-api-?-llama-3.3-70b-versatile-model-cloud-&-hosting-vercel-(frontend
  • vite
Share this project:

Updates