# Project export: VibeRight

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Vibe mastering your vibe coding.
- Devpost: https://devpost.com/software/viberight
- GitHub: https://github.com/dennisliang01/VibeCheck.git
- Demo: http://vibe-right.netlify.app/
- Video: https://www.youtube.com/embed/s2G2bzgBH0M?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Dennis Liang (20 commits), salmazainana (1 commits), Cursor (1 commits)

## Devpost submission (written by the team)

### Inspiration

We hear a lot about AI and vibe coding. People are building startups with it. But there’s a gap. SENIOR ENGINEERS use VIBE CODING BETTER than JUNIORS. Why? Because they’ve accumulated years of reading code. They can spot issues instantly, debug faster, and anticipate edge cases before they explode in production. Seniors are compounding. Juniors are overwhelmed. They stress over debugging vibe coding with no concrete conception of what their code does. We are NOT just talking about debugging. We are talking about sustainability and scalability of YOUR code so you can vibe code better.

### What it does

We are here to fill the Vibe GAP. VibeRight allows learners to become owners of that code without forcing them to manually inspect every line of code. It helps them: review with structure, take ownership, test with intention, catch scalability issues early, ship with confidence. The goal isn’t to slow creativity. It’s to make creativity deliverable.

### How we built it

VibeRight has two core components: validating and understanding. 1. Validating — A Multi-Agent Expert System Instead of one general critic, we built specialized agents, each obsessed with one dimension of code quality: Functional agent → Your code runs, tests pass, no hidden debt. API contract agent → Robust APIs. Validation, errors, security boundaries. And bulletproof dependency management. Architecture agent → Clean separation of concerns. No unmaintainable spaghetti. Concurrency agent → No race conditions. No intermittent production failures. Performance agent → Scales efficiently. Algorithmic bottlenecks caught early. Security agent → Injection attacks, unsafe execution, auth vulnerabilities : blocked before prod. Documentation agent → Code others can actually understands. Resilience agent → Graceful failures. Recovers, doesn't crash silently. Quality agent → Readable. Maintainable. Low cognitive load. Dependency agent → Supply chain protected. Vulnerabilities, outdated packages, licensing risks are flagged. Each agent produces structured feedback. Then we cluster, merge, and score the outputs into actionable quality metrics. 2. Understanding — Active Ownership Training Validation isn’t enough. So we built a chat-based learning loop powered by two LLMs: An Interviewer that quizzes you on your own code. An Evaluator that checks whether you actually understand it. The questions go deeper over time . You refine your answers. You correct misconceptions. Step by step, you build durable knowledge in an accessible environment. We’re not just reviewing code. We’re training long-term engineering intuition.

### Challenges we ran into

Defining “Good Vibe Code”. How do we define those agents and decide what makes vibe code good without impairing creativity? Unlike LeetCode, there’s no fixed input-output pattern. We wanted to preserve creativity, not constrain it. So how do you enforce quality without standardizing thinking? We had to define expert mental models, not rigid rules. Another challenge is SCALE — how can these agents, even when parallelized, handle huge files and interconnected folders/repos effectively?

### Accomplishments we're proud of

We turned an abstract frustration into a working system we actually use. We are proud to see the concept come to life. It’s something we can actually use to become better builders. We focused on real pain points shared by all coders. We debated hard. We disagreed productively. We made structured decisions. And we built something non-obvious.

### What we learned

We started trying to "validate code", we ended up learning to capture how experts actually think. Our main takeaway: don't build one smart critic, build 10 specialized ones: each focused on a single thing like security or performance. This catches what a general tool misses, and keeps feedback fast enough to not break your flow. We also learned that how you ask is as important as what you build. The right prompts are basically your system design. We mostly learned a lot about ourselves, and how to divide and conquer when we're tackling building abstract concepts. We found our own pattern for tough problems : find someone who does it well, break down how they think, then automate that. We learned to spend compute where it matters.

### What's next

VibeRight expands to all users, feedback and comments on each other's performance and code. Challenges on new concepts. Online vibe hackathons where you can be as creative as you want, without intimidation from bulk programs. We can see this having an impact on education. We believe this changes how we learn to code: less theory, more building, faster feedback loops. New platform to assess student skills. A LeetCode for the AI era. VibeRight becomes a platform for technical interviews preparation + mock interviews that test how you think, and use AI. Students and career-changers get immediate, quality feedback that used to take years of senior mentorship. Catch up fast. Ship faster.

## README (from the GitHub repository)

# VibeRight

![Banner](Banner.png)

**Live demo:** [vibe-right.netlify.app](https://vibe-right.netlify.app/)

A lightweight hackathon MVP: upload a project (zip), build a **Project Map** once, then run a **Q/A loop**—answer code-understanding questions, get graded with feedback, and see your learner model update. Single-user demo, no auth.

---

## Contents

- [Understanding the project](#understanding-the-project)
- [Quick start](#quick-start)
- [API keys and environment](#api-keys-and-environment)
- [Running the project in the future](#running-the-project-in-the-future)
- [Flow](#flow)
- [Persistence](#persistence)
- [API reference](#api-for-reference)
- [Tech & constraints](#tech--constraints)

---

## Understanding the project

**What it does:** You upload a `.zip` of a codebase. VibeRight builds a one-time **project map** (key files and structure), then runs a **learn** flow: it asks you questions about the code, you answer, and it grades you and updates a simple learner model so later questions adapt to what you’ve mastered.

**Architecture:**

- **Next.js 14** (App Router) + **TypeScript**. No auth; single-user.
- **LLM:** By default a **mock client** (no API key). With your own **Anthropic API key**, the app uses **Claude** for building the project map, generating questions, and grading answers.
- **Data:** Locally, everything is file-based (`workspaces/`, `data/`). On deploy (e.g. Netlify), you can add blob storage so projects persist; see `lib/blobStorage.ts` and `lib/storage.ts`.

**User flow:**

1. **Home** – Upload a zip (max 50MB, ~200 files).
2. **Project** – Build project map once; it’s saved in the project workspace.
3. **Learn** – Q/A loop: question → answer → grade + feedback → next topic; session and learner model are persisted.

**Key directories:**

| Path | Purpose |
|------|--------|
| `app/` | Pages and API routes (home, project, learn, upload, map, question, grade, etc.). |
| `lib/` | Core logic: `schemas.ts` (Zod), `llm/` (mock + Claude), `workspace.ts`, `storage.ts`, `buildProjectMapSkill.ts`, zip/file handling. |
| `components/` | Shared UI (e.g. toasts). |
| `examples/` | `sample-src/` and script to build `sample.zip` for demos. |

---

## Quick start

```bash
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000). You can use the app immediately with the **mock LLM** (no API key). To try with a sample codebase:

```bash
npm run create-sample-zip
```

Then upload `examples/sample.zip` from the home page.

---

## API keys and environment

The app works **without any API keys** using a built-in mock LLM. To use **your own Claude API** for real project maps, questions, and grading:

### 1. Get an API key

- Go to [Anthropic Console](https://console.anthropic.com/).
- Create or copy an API key (starts with `sk-ant-`).

### 2. Create local env file

In the **project root** (same folder as `package.json`):

```bash
cp .env.example .env.local
```

(On Windows: `copy .env.example .env.local`.)

### 3. Add your key

Edit `.env.local` and set:

```bash
USE_CLAUDE_LLM=true
ANTHROPIC_API_KEY=sk-ant-your-actual-key-here
```

- **Do not** commit `.env.local` or share your key (it’s in `.gitignore`).
- **Optional:** `ANTHROPIC_MODEL=claude-3-5-sonnet-20241022` (or another model). If unset, the app uses a default.

### 4. Restart the dev server

```bash
npm run dev
```

The app reads these at runtime: if `USE_CLAUDE_LLM` is `true` and `ANTHROPIC_API_KEY` is set, it uses Claude for map building, question generation, and grading. No code changes needed.

### Summary of env vars

| Variable | Required | Description |
|----------|----------|-------------|
| `USE_CLAUDE_LLM` | No | Set to `true` to enable Claude. Omit or set to anything else to use mock LLM. |
| `ANTHROPIC_API_KEY` | Only if using Claude | Your Anthropic API key. |
| `ANTHROPIC_MODEL` | No | Claude model name; optional, has a default. |

---

## Running the project in the future

### Commands

| Command | Description |
|---------|-------------|
| `npm install` | Install dependencies (run after clone or when `package.json` changes). |
| `npm run dev` | Start dev server at [http://localhost:3000](http://localhost:3000). |
| `npm run build` | Production build. |
| `npm run start` | Run production server (after `npm run build`). |
| `npm run create-sample-zip` | Build `examples/sample.zip` from `examples/sample-src` for testing. |
| `npm run dev:netlify` | Run with Netlify CLI (`npx netlify dev`) for local Netlify-style dev. |

### Environment

- **Local:** Copy `.env.example` to `.env.local` and add your API key if you want Claude. See [API keys and environment](#api-keys-and-environment).
- **Deploy (e.g. Netlify):** Set the same variables in the host’s environment (e.g. Netlify → Site settings → Environment variables). Never commit real keys.

### Where data lives

- **Project files:** `workspaces/<project_id>/`
- **Project map:** `workspaces/<project_id>/project_map.json`
- **Learner model:** `data/learner_model.json`
- **Sessions:** `data/sessions_<project_id>.json`

`workspaces` and `data` are in `.gitignore`. They are created when you upload a project and build a map.

### Coming back to the repo later

1. `git pull` (if applicable).
2. `npm install`.
3. If you use Claude: ensure `.env.local` exists with `USE_CLAUDE_LLM=true` and `ANTHROPIC_API_KEY` set.
4. `npm run dev` and open http://localhost:3000.

For more detail on architecture and conventions, see `AGENTS.md`.

---

## Flow

1. **Home (`/`)** – Upload a `.zip` of your project (max 50MB, ~200 files). Optional: use `examples/sample.zip` after running `npm run create-sample-zip`.
2. **Project (`/project/[id]`)** – Overview and **Build Project Map** (one-time). Map is stored as `project_map.json` in the project workspace.
3. **Learn (`/project/[id]/learn`)** – Q/A loop: see a question → type answer → submit → get score + feedback + next recommended topic. Session history and learner model are persisted.

---

## Persistence

- **Project files**: extracted under `workspaces/<project_id>/`.
- **Project map**: `workspaces/<project_id>/project_map.json`.
- **Learner model**: `data/learner_model.json` (single local user).
- **Session history**: `data/sessions_<project_id>.json`.

`workspaces` and `data` are in `.gitignore`; create them by uploading a project and building a map.

---

## API (for reference)

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/upload` | Upload zip (form field `file`) |
| GET | `/api/projects` | List project IDs |
| GET/POST | `/api/project/[id]/map` | Get or build project map |
| GET | `/api/project/[id]/question` | Get next question |
| POST | `/api/project/[id]/grade` | Submit answer, get grade |
| GET | `/api/project/[id]/tree` | File tree |
| GET | `/api/project/[id]/file?path=...` | Read file |
| GET | `/api/project/[id]/search?q=...` | Text search |
| GET | `/api/project/[id]/session` | Session history |

---

## Tech & constraints

**Tech:** Next.js 14 (App Router), TypeScript, Tailwind. Zod for schemas (`project_map`, question, grade, learner model, session). No auth, no payments, no vector DB; simple text search over files.

**Constraints (MVP):** Repos up to ~200 files; zip-only upload (no GitHub in this MVP); single-user; data stored on disk (or blob on deploy).


## Detected evidence (automated analysis)

Indexed codebase: 106 recognized source files, 357 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository
- AI coding agent: Cursor — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 123)

```
.cursor/debug.log
.env.example
.eslintrc.json
.gitignore
AGENTS.md
app/api/load-sample/route.ts
app/api/project/[id]/file/route.ts
app/api/project/[id]/grade/route.ts
app/api/project/[id]/map/route.ts
app/api/project/[id]/question/categories/route.ts
app/api/project/[id]/question/route.ts
app/api/project/[id]/search/route.ts
app/api/project/[id]/session/route.ts
app/api/project/[id]/tree/route.ts
app/api/project/[id]/validation/route.ts
app/api/project/[id]/validation/run/route.ts
app/api/project/[id]/validation/status/route.ts
app/api/projects/route.ts
app/api/sample-validation/route.ts
app/api/upload/route.ts
app/globals.css
app/layout.tsx
app/page.test.tsx
app/page.tsx
app/project/[id]/learn/page.test.tsx
app/project/[id]/learn/page.tsx
app/project/[id]/page.tsx
app/project/[id]/validate/page.tsx
Backend/.gitignore
Backend/codeval/__init__.py
Backend/codeval/agents/__init__.py
Backend/codeval/agents/api_contract.py
Backend/codeval/agents/architecture.py
Backend/codeval/agents/base.py
Backend/codeval/agents/concurrency.py
Backend/codeval/agents/consolidation.py
Backend/codeval/agents/dependency.py
Backend/codeval/agents/documentation.py
Backend/codeval/agents/functional.py
Backend/codeval/agents/performance.py
Backend/codeval/agents/quality.py
Backend/codeval/agents/resilience.py
Backend/codeval/agents/security.py
Backend/codeval/cli.py
Backend/codeval/fingerprint.py
Backend/codeval/heuristics.py
Backend/codeval/html_report.py
Backend/codeval/llm.py
Backend/codeval/orchestrator.py
Backend/codeval/osv.py
Backend/codeval/schemas.py
Backend/codeval/slicer.py
Backend/config.example.yaml
Backend/pyproject.toml
Backend/README.md
Backend/report.json
Backend/tests/conftest.py
Backend/tests/test_failure_tracking.py
Backend/tests/test_fingerprint.py
Backend/tests/test_heuristics.py
Backend/tests/test_html.py
Backend/tests/test_llm.py
Backend/tests/test_orchestrator.py
Backend/tests/test_slicer.py
Backend/weather-report.json
components/code/CodePanel.tsx
components/HomeNavLink.tsx
components/ThemeContext.tsx
components/ThemeToggle.tsx
components/ToastContext.tsx
components/understanding/QAPanel.tsx
components/understanding/UnderstandingPanel.tsx
components/validation/FeedbackCard.tsx
components/validation/FeedbackList.tsx
components/validation/ScoreCard.tsx
components/validation/ScoreGrid.tsx
components/validation/ValidationPanel.tsx
components/validation/ValidationSectionCard.tsx
components/workspace/Tabs.tsx
components/workspace/WorkspaceContext.tsx
components/workspace/WorkspaceShell.tsx
components/workspace/WorkspaceTabs.tsx
docs/accessibility.md
docs/validation-api-contract.md
examples/sample-src/README.md
examples/sample-src/src/app.ts
examples/sample-src/src/components/Button.tsx
examples/sample-src/src/index.ts
examples/sample-src/src/utils/format.ts
examples/test_sample/test_sample/main.py
examples/test_sample/test_sample/requirements.txt
examples/test_sample/test_sample/src/user_service.py
examples/test_sample/validation_report_demo.json
examples/validation_report_demo.json
jest.config.js
jest.setup.ts
lib/blobStorage.ts
lib/buildProjectMapSkill.ts
lib/codevalReport.ts
lib/filesSummary.ts
lib/highlight.ts
lib/llm/claude.ts
lib/llm/index.ts
lib/llm/mock.ts
lib/llm/types.ts
lib/mockData.ts
lib/questionCategories.ts
lib/schemas.ts
lib/storage.ts
lib/workspace.ts
lib/zipExtract.ts
netlify.toml
next.config.js
package.json
postcss.config.js
public/validation_report_sample.json
README.md
scripts/create-sample-zip.js
scripts/create-test-sample-zip.js
tailwind.config.ts
[3 more files omitted for size]
```

### Dependencies

- Backend/pyproject.toml: anthropic@>=0.45.0, httpx@>=0.25.0, pydantic@>=2.0, pytest@>=7.0, pytest-asyncio@>=0.21.0, python-dotenv@>=1.0.0, pyyaml@>=6.0, rich@>=13.0, typer@>=0.9.0
- examples/test_sample/test_sample/requirements.txt: flask@>=1.0.0, requests@>=2.0.0
- package.json: @netlify/plugin-nextjs@^5.0.0, @testing-library/dom@^10.4.1, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @testing-library/user-event@^14.6.1, @types/adm-zip@^0.5.5, @types/jest@^30.0.0, @types/node@^22.9.0, @types/react@^18.3.12, @types/react-dom@^18.3.1, @vercel/blob@^2.2.0, adm-zip@^0.5.16, autoprefixer@^10.4.20, eslint@^8.57.1, eslint-config-next@^14.2.18, eslint-plugin-jsx-a11y@^6.10.2, jest@^30.2.0, jest-environment-jsdom@^30.2.0, netlify-cli@^17.0.0, next@14.2.18, postcss@^8.4.47, react@^18.3.1, react-dom@^18.3.1, shiki@^3.22.0, tailwindcss@^3.4.14, typescript@^5.6.3, zod@^3.23.8

### Recent commits (newest first)

- Wrap up project and documentation
- Netlify deployment fix
- Update for Netlify deployment
- Final demo update
- Fix workspace build issue
- Fixed npm run build failures
- Resolve ES5 strict mode build errors
- Updated code for Vercel deployment
- Tweaked small UI changes
- Integrated multi-agent validation workflow
- Merged New Backend
- Change branding to VibeRight
- Fixed code editor color inconsistencies
- Updated code browser
- Update understanding and validation panel
- Merge branch 'main' of https://github.com/dennisliang01/VibeCheck
- Added new understanding and validation interface
- Add .env.example and backend code_validator
- Update branding and added WCAG accessibility support
- Added light mode

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
# Agent guide

Short reference for the AI assistant working in this repo.

## Architecture

- **Next.js 14** (App Router) + **TypeScript**. Single-user demo; no auth.
- **Flow:** User uploads a .zip → files go to `workspaces/<project_id>/` → one-time **project map** is built (discovery + LLM or mock) → **Q/A learn page** (questions, answer, grade, learner model).
- **Persistence:** File-based locally. On **Vercel**, when a Blob store is connected (`BLOB_READ_WRITE_TOKEN`), workspaces and data (project map, learner model, session) are stored in **Vercel Blob** so projects persist across serverless invocations; see `lib/blobStorage.ts`.
- **LLM:** Interface in `lib/llm/`; **MockLLMClient** by default, **ClaudeLLMClient** when `USE_CLAUDE_LLM=true` and `ANTHROPIC_API_KEY` set.

## Key directories

| Path | Purpose |
|------|--------|
| `app/` | Pages and API routes. `app/page.tsx` (home), `app/project/[id]/` (redirect to learn), `app/project/[id]/learn/page.tsx` (Q/A + code viewer), `app/api/` (upload, load-sample, project/[id]/map, question, grade, tree, file, search, session). |
| `lib/` | Core logic. `schemas.ts` (Zod), `llm/` (types, mock, claude), `workspace.ts` (repo_tree, get_file, search_repo; async blob APIs when on Vercel), `blobStorage.ts` (Vercel Blob for workspaces/data), `storage.ts` (project map, learner model, session), `buildProjectMapSkill.ts` (discovery + key files), `zipExtract.ts`, `filesSummary.ts`. |
| `components/` | Shared UI (e.g. `ToastContext.tsx`). |
| `examples/` | `sample-src/` and `sample.zip` for demo. |

## Commands

```bash
npm install
npm run dev          # http://localhost:3000
npm run build
npm run create-sample-zip   # create examples/sample.zip
```

## Conventions

- **TypeScript:** Strict. Use types/interfaces; avoid `any`.
- **Schemas:** All shared shapes (project map, question, grade, learner model, session) live in `lib/schemas.ts` and use **Zod**. Validate at API boundaries (e.g. `ProjectMapSchema.parse(...)`).
- **Edits:** Prefer **minimal, targeted changes**. Use search-and-replace or small edits; **do not rewrite whole files** unless the task clearly requires it (e.g. new page or major refactor). Preserve existing style and structure when editing.
- **APIs:** Route handlers in `app/api/` return JSON; use `NextResponse.json()`. Parse request body with the same Zod schemas where applicable.
- **Styling:** Tailwind. CSS variables in `app/globals.css` (`--bg`, `--card`, `--border`, `--text`, `--muted`, `--accent`, etc.).

## Vercel deployment

- **Persistent projects:** Connect a **Vercel Blob** store to the project (Storage → Create Blob). This sets `BLOB_READ_WRITE_TOKEN`; uploads and project/session data then persist across requests. Without Blob, `/tmp` is used and data is ephemeral (~30s).
- **Validation run:** `POST /api/project/[id]/validation/run` returns 501 on Vercel (codeval subprocess not supported in serverless). Status and reading an existing report work via Blob when available.

```

### docs/validation-api-contract.md

```markdown
# Validation API Contract

This document describes the JSON schema expected from the Python validation backend. When the Python scripts are integrated, they should produce output matching this contract.

## Output Location (Phase 2)

The Next.js API will check for validation data in one of these locations:

- `workspaces/[projectId]/validation_report.json` – single file with both scores and feedback
- Or split files: `validation_scores.json` and `validation_feedback.json`

## Schema

### ValidationReport (single file)

```json
{
  "scores": {
    "performance": 0-100,
    "security": 0-100,
    "codeQuality": 0-100,
    "architecture": 0-100
  },
  "feedback": [
    {
      "id": "string",
      "title": "string",
      "severity": "high" | "medium" | "low",
      "filePath": "string (optional – enables Jump to file)",
      "recommendation": "string"
    }
  ]
}
```

### Scores

| Field        | Type   | Range   |
| ------------ | ------ | ------- |
| performance  | number | 0–100   |
| security     | number | 0–100   |
| codeQuality  | number | 0–100   |
| architecture | number | 0–100   |

### Feedback Item

| Field         | Type     | Required | Notes                                    |
| ------------- | -------- | -------- | ---------------------------------------- |
| id            | string   | yes      | Unique identifier                        |
| title         | string   | yes      | Short title for the finding              |
| severity      | string   | yes      | `"high"` \| `"medium"` \| `"low"`        |
| filePath      | string   | no       | Relative path within project; enables "Jump to file" |
| recommendation| string   | yes      | Human-readable recommendation text       |

## API Endpoint

`GET /api/project/[id]/validation` returns the same shape. The frontend fetches this endpoint; the API will read from workspace JSON (when present) or fall back to mock data.

```

### package.json

```
{
  "name": "viberight",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --port 3000",
    "dev:alt": "next dev --port 3001",
    "dev:turbo": "next dev --port 3000 --turbo",
    "dev:fresh": "node -e \"try{require('fs').rmSync('.next',{recursive:true,force:true})}catch(e){}\" && next dev --port 3000",
    "dev:fresh:turbo": "node -e \"try{require('fs').rmSync('.next',{recursive:true,force:true})}catch(e){}\" && next dev --port 3000 --turbo",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "create-sample-zip": "node scripts/create-sample-zip.js",
    "create-test-sample-zip": "node scripts/create-test-sample-zip.js",
    "dev:netlify": "npx netlify dev"
  },
  "dependencies": {
    "@vercel/blob": "^2.2.0",
    "adm-zip": "^0.5.16",
    "next": "14.2.18",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "shiki": "^3.22.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@netlify/plugin-nextjs": "^5.0.0",
    "netlify-cli": "^17.0.0",
    "@testing-library/dom": "^10.4.1",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/adm-zip": "^0.5.5",
    "@types/jest": "^30.0.0",
    "@types/node": "^22.9.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "autoprefixer": "^10.4.20",
    "eslint": "^8.57.1",
    "eslint-config-next": "^14.2.18",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "jest": "^30.2.0",
    "jest-environment-jsdom": "^30.2.0",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.14",
    "typescript": "^5.6.3"
  }
}

```

### Backend/pyproject.toml

```
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "codeval"
version = "0.1.0"
description = "Multi-agent code validator MVP"
requires-python = ">=3.11"
dependencies = [
    "pydantic>=2.0",
    "typer>=0.9.0",
    "httpx>=0.25.0",
    "pyyaml>=6.0",
    "python-dotenv>=1.0.0",
    "anthropic>=0.45.0",
    "rich>=13.0",
]

[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-asyncio>=0.21.0"]

[project.scripts]
codeval = "codeval.cli:main"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

```

### examples/test_sample/test_sample/requirements.txt

```
flask>=1.0.0
requests>=2.0.0

```

### app/layout.tsx

```typescript
import type { Metadata } from 'next';
import { Suspense } from 'react';
import './globals.css';
import { ToastProvider } from '@/components/ToastContext';
import { ThemeProvider } from '@/components/ThemeContext';
import { HomeNavLink } from '@/components/HomeNavLink';
import { ThemeToggle } from '@/components/ThemeToggle';

export const metadata: Metadata = {
  title: 'VibeRight',
  description: 'Learn by answering code-understanding questions on your project',
};

const themeScript = `
(function() {
  var s = localStorage.getItem('viberight-theme');
  var p = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: light)').matches;
  document.documentElement.setAttribute('data-theme', s === 'light' || (!s && p) ? 'light' : 'dark');
})();
`;

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="min-h-screen bg-[var(--bg)] text-[var(--text)] antialiased">
        <script dangerouslySetInnerHTML={{ __html: themeScript }} />
        <ThemeProvider>
          <ToastProvider>
            <a
              href="#main-content"
              className="fixed left-4 top-4 z-[100] -translate-y-20 rounded-md bg-[var(--accent)] px-4 py-2 text-sm font-medium text-white shadow-lg transition-transform focus:translate-y-0 focus:outline-none focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2 focus:ring-offset-[var(--bg)]"
            >
              Skip to content
            </a>
            <header className="border-b border-[var(--border)] border-opacity-50 px-6 py-4 flex items-center justify-between gap-4">
              <a
                href="/"
                className="text-xl font-semibold text-[var(--text)] hover:text-[var(--accent)] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)] rounded"
              >
                VibeRight
              </a>
              <div className="flex items-center gap-2">
                <Suspense fallback={null}>
                  <HomeNavLink />
                </Suspense>
                <ThemeToggle />
              </div>
            </header>
            <main id="main-content" className="min-h-[calc(100vh-4rem)] flex flex-col" tabIndex={-1}>{children}</main>
          </ToastProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
'use client';

import { useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useToast } from '@/components/ToastContext';

export default function HomePage() {
  const router = useRouter();
  const { showToast } = useToast();
  const [file, setFile] = useState<File | null>(null);
  const [uploading, setUploading] = useState(false);
  const [loadingSample, setLoadingSample] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const uploadErrorRef = useRef<HTMLParagraphElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const doUpload = async (fileToUpload: File) => {
    setError(null);
    setUploading(true);
    try {
      const formData = new FormData();
      formData.append('file', fileToUpload);
      const res = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Upload failed');
      setFile(null);
      showToast('Project uploaded successfully', 'success');
      router.push(`/project/${data.projectId}`);
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Upload failed';
      setError(msg);
      showToast(msg, 'error');
    } finally {
      setUploading(false);
    }
  };

  const handleUpload = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!file) {
      setError('Please select a .zip file');
      showToast('Please select a .zip file', 'error');
      requestAnimationFrame(() => uploadErrorRef.current?.focus() ?? fileInputRef.current?.focus());
      return;
    }
    await doUpload(file);
  };

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selected = e.target.files?.[0] ?? null;
    setFile(selected);
    if (selected) {
      doUpload(selected);
    }
    e.target.value = '';
  };

  const handleLoadSample = async () => {
    setError(null);
    setLoadingSample(true);
    try {
      const res = await fetch('/api/load-sample', { method: 'POST' });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed to load sample');
      showToast('Sample project loaded', 'success');
      router.push(`/project/${data.projectId}?sample=1`);
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Failed to load sample';
      setError(msg);
      showToast(msg, 'error');
    } finally {
      setLoadingSample(false);
    }
  };

  return (
    <div className="mx-auto max-w-xl w-full px-6 flex flex-col min-h-[calc(100vh-4rem)]">
      <section className="flex flex-col justify-center py-16 min-h-[calc(100vh-4rem)]">
        <div className="text-center">
          <h1 className="hero-title-start text-4xl font-semibold text-[var(--text)] tracking-tight">
            VibeRight
          </h1>
          <p className="hero-subtitle-start mt-2 text-[var(--muted)]">
            Know your system. Ship with confidence.
          </p>
        </div>

        <div className="hero-block-start relative flex flex-col items-center mt-10">
          <form id="upload-form" onSubmit={handleUpload} className="flex w-full flex-col gap-4">
          <label className="flex cursor-pointer flex-col items-center gap-3 rounded-xl border border-dashed border-[var(--border)] bg-[var(--card)] py-8 px-6 transition-colors hover:border-[var(--muted)]">
            <input
              ref={fileInputRef}
              id="upload-file"
              type="file"
              accept=".zip"
              onChange={handleFileChange}
              className="hidden"
            />
            <svg
              width="40"
              height="40"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
              className="text-[var(--accent)]"
            >
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
              <polyline points="17 8 12 3 7 8" />
              <line x1="12" y1="3" x2="12" y2="15" />
            </svg>
            <span className="text-sm font-medium text-[var(--muted)]">
              {uploading ? 'Uploading…' : file ? file.name : 'Upload code'}
            </span>
            <span className="text-xs text-[var(--muted)] opacity-80">.zip · Max 50MB</span>
          </label>

          {error && (
            <p id="upload-error" ref={uploadErrorRef} className="text-center text-sm text-[var(--error)]" tabIndex={-1} role="alert">
              {error}
            </p>
          )}

          <div className="flex gap-3 justify-center">
            <button
              type="submit"
              disabled={uploading || !file}
              aria-describedby={error ? 'upload-error' : undefined}
              className="rounded-lg bg-[var(--accent)] px-5 py-2.5 text-sm font-medium text-white hover:bg-[var(--accent-hover)] disabled:opacity-40 disabled:pointer-events-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)]"
            >
              {uploading ? 'Uploading…' : 'Upload'}
            </button>
            <button
              type="button"
              onClick={handleLoadSample}
              disabled={loadingSample}
              className="rounded-lg border border-[var(--border)] px-5 py-2.5 text-sm font-medium text-[var(--muted)] hover:bg-[var(--card)] hover:text-[var(--text)] disabled:opacity-40 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)]"
            >
              {loadingSample ? 'Loading…' : 'Sample'}
            </button>
          </div>
        </form>
        </div>
      </section>
    </div>
  );
}

```

### lib/llm/index.ts

```typescript
import type { LLMClient } from './types';
import { mockLLMClient } from './mock';
import { claudeLLMClient } from './claude';

export type { LLMClient, FilesSummary, ProjectContext } from './types';
export { MockLLMClient, mockLLMClient } from './mock';
export { ClaudeLLMClient, claudeLLMClient } from './claude';

export function getLLMClient(): LLMClient {
  if (process.env.USE_CLAUDE_LLM === 'true' && process.env.ANTHROPIC_API_KEY) {
    return claudeLLMClient;
  }
  return mockLLMClient;
}

```

### Backend/codeval/cli.py

```python
"""CLI for codeval."""

from __future__ import annotations

import asyncio
import json
import os
import sys
from pathlib import Path

import typer
from dotenv import load_dotenv

# Load .env BEFORE other codeval imports so env vars are available
def _load_env() -> None:
    cwd = Path.cwd()
    pkg_root = Path(__file__).resolve().parent.parent
    for env_path in [cwd / ".env", pkg_root / ".env"]:
        if env_path.exists():
            load_dotenv(env_path, override=True)
            # Fallback: manual parse if dotenv didn't load (encoding/BOM issues)
            if not os.environ.get("ANTHROPIC_API_KEY", "").strip():
                try:
                    raw = env_path.read_text(encoding="utf-8-sig").strip()
                    for line in raw.splitlines():
                        line = line.strip()
                        if line and not line.startswith("#") and "=" in line:
                            k, _, v = line.partition("=")
                            k, v = k.strip(), v.strip().strip('"\'')
                            if k and v and v != "your-api-key-here":
                                os.environ[k] = v
                except Exception:
                    pass
            break

_load_env()

from codeval.html_report import render_html
from codeval.llm import is_llm_available, set_concurrency
from codeval.orchestrator import run_validation
from codeval.schemas import FinalReport

app = typer.Typer(help="Multi-agent code validator")


# ── Markdown rendering ───────────────────────────────────────────────

def _render_markdown(report: FinalReport) -> str:
    """Render FinalReport to Markdown."""
    lines = [
        "# Code Validation Report",
        "",
        report.summary,
        "",
        "## Scores",
        "",
        f"- **Overall**: {report.scores.overall:.2f}/100",
        "",
    ]

    failed = set(report.failed_categories)

    # Warning banner for failed agents
    if failed:
        names = ", ".join(c.replace("_", " ").title() for c in failed)
        lines.append(f"> **Warning:** {len(failed)} agent(s) failed LLM analysis ({names}). Their scores show N/A.")
        lines.append("")

    # Group scores by tier for readability
    TIER_LABELS = {
        "Critical": ["functional", "security", "resilience"],
        "Important": ["performance", "quality", "dependency", "architecture"],
        "Supplemental": ["documentation", "concurrency", "api_contract"],
    }
    for tier_name, cats in TIER_LABELS.items():
        lines.append(f"**{tier_name}:**")
        for cat in cats:
            score = report.scores.categories.get(cat, 100.0)
            label = cat.replace("_", " ").title()
            if cat in failed:
                lines.append(f"- {label}: **N/A** _(analysis failed)_")
            else:
                lines.append(f"- {label}: {score:.2f}/100")
        lines.append("")

    # Show clusters if available, otherwise fall back to raw findings
    if report.clusters:
        lines.extend(["## Issues (Clustered)", ""])
        # Sort: critical first, then high, medium, low
        sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
        sorted_clusters = sorted(
            report.clusters, key=lambda c: sev_order.get(c.consolidated_severity, 4)
        )
        for i, c in enumerate(sorted_clusters[:15], 1):
            merged = len(c.related_finding_ids)
            merge_note = f" ({merged + 1} findings merged)" if merged else ""
            lines.append(
                f"### {i}. [{c.consolidated_severity.upper()}] {c.consolidated_title}{merge_note}"
            )
            lines.append(f"- **Category**: {c.category}")
            lines.append(f"- **Confidence**: {c.match_confidence:.0%}")
            lines.append(f"- **Impact**: {c.consolidated_impact}")
            lines.append(f"- **Recommendation**: {c.consolidated_recommendation}")

            # Show primary finding evidence
            primary = next((f for f in report.findings if f.id == c.primary_finding_id), None)
            if primary:
                lines.append(f"- **File**: {primary.evidence.file}:{primary.evidence.lines}")
                if primary.evidence.snippet:
                    lines.append(f"- **Snippet**: `{primary.evidence.snippet[:120]}`")
            lines.append("")

        total_raw = len(report.all_findings) if report.all_findings else len(report.findings)
        lines.append(
            f"*{len(report.clusters)} unique issues identified from {total_raw} raw findings*"
        )
        lines.append("")
    else:
        lines.extend(["## Top Findings", ""])
        for i, f in enumerate(report.findings[:10], 1):
            lines.append(f"### {i}. [{f.severity.upper()}] {f.title}")
            lines.append(f"- **File**: {f.evidence.file}:{f.evidence.lines}")
            lines.append(f"- **Impact**: {f.impact}")
            lines.append(f"- **Recommendation**: {f.recommendation}")
            lines.append("")

    lines.extend(["## Recommended Next Steps", ""])
    for step in report.recommended_next_steps:
        lines.append(f"- {step}")
    lines.append("")
    return "\n".join(lines)


# ── Output helpers ───────────────────────────────────────────────────

def _write_report(report: FinalReport, out: Path, fmt: str, project_name: str = "") -> list[str]:
    """Write report in one or more formats. Returns list of written file paths."""
    out.parent.mkdir(parents=True, exist_ok=True)
    written: list[str] = []

    formats = ["json", "md", "html"] if fmt == "all" else [fmt]

    for f in formats:
        if f == "json":
            target = out.with_suffix(".json")
            target.write_text(report.model_dump_json(indent=2), encoding="utf-8")
            written.append(str(target))
        elif f == "md":
            target = out.with_suffix(".md")
            target.writ
[truncated — 10077 more characters]
```

### examples/sample-src/src/app.ts

```typescript
export function initApp(): void {
  console.log('App started');
}

```

[96 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]