diff --git a/.claude/skills/package-security-check/SKILL.md b/.claude/skills/package-security-check/SKILL.md new file mode 100644 index 0000000..d197015 --- /dev/null +++ b/.claude/skills/package-security-check/SKILL.md @@ -0,0 +1,62 @@ +--- +name: package-security-check +description: Check whether a package is secure before installing or adding it as a dependency. Use this skill EVERY time you are about to install a package or add a dependency — e.g. npm install, npm i, yarn add, pnpm add, pip install, adding entries to package.json or requirements.txt — or whenever the user asks whether a package is safe, secure, trustworthy, vulnerable, or asks for a security check/score of a package. Trigger even if the user doesn't mention security, because installing any third-party package should be preceded by this check. +--- + +# Package Security Check + +Before installing any third-party package, verify it has no known vulnerabilities and a reasonable security health score. If it's clean, proceed with the installation. If not, STOP and show the user a warning, then let them decide. + +## Workflow + +1. **Run the check** before executing any install command: + + ```bash + python3 scripts/check_package.py [version] [--ecosystem npm] + ``` + + - If the user specified a version (e.g. `lodash@4.17.20`), pass it. Otherwise the script checks the latest version. + - Ecosystem defaults to `npm`. Also supports `PyPI`, `Go`, `Maven`, `NuGet`, `RubyGems`, `crates.io`. + - When installing multiple packages, check each one. + +2. **Act on the exit code:** + + - **Exit 0 (SECURE)** — proceed with the installation without asking. Briefly mention the check passed, e.g. "✓ express@4.19.2 — no known vulnerabilities, Scorecard 7.6/10. Installing…" + - **Exit 1 (WARNING)** — do NOT install yet. Show the user a clear warning and ask whether to proceed, pick a different version, or choose an alternative package. See the warning format below. + - **Exit 2 (ERROR)** — the check itself failed (network, package not found). Tell the user the check could not be completed and ask whether to proceed unverified. Never silently install after a failed check. + +3. **If the user explicitly says to skip the check or install anyway**, respect that — this skill informs, it doesn't block. + +## What the script checks + +- **Known vulnerabilities** via OSV.dev — authoritative CVE/GHSA data for the exact version. Any hit triggers a warning. +- **OpenSSF Scorecard** via deps.dev — a 0–10 health heuristic (code review, maintenance, branch protection, etc.). Scores below 4.0 trigger a warning. This is a signal, not proof of insecurity — say so when warning about score alone. +- A missing Scorecard (no public GitHub repo, or repo not scanned) is common and NOT by itself a reason to warn. + +## Warning format + +When the verdict is WARNING, present it like this before asking how to proceed: + +``` +⚠️ Security warning: @ + +Known vulnerabilities: +- GHSA-xxxx [HIGH] Prototype pollution in ... + (fixed in , if the script output mentions one) + +OpenSSF Scorecard: 2.9/10 (low — weak maintenance/review signals) + +Options: +1. Install a patched version (recommended if one exists) +2. Pick an alternative package +3. Install anyway +``` + +When a vulnerability is fixed in a newer version, recommend installing that version as the default suggestion. + +## Notes & edge cases + +- Scoped npm packages work as-is (`@nestjs/core`) — the script handles URL-encoding. +- For version *ranges* in package.json, check the version that would actually resolve (run `npm view version` to find it if unsure). +- This check covers known vulnerabilities and project health. It does NOT detect zero-day malware or typosquatting — if a package name looks like a misspelling of a popular package (e.g. `expresss`, `lodahs`), flag that to the user separately. +- The APIs used (api.osv.dev, api.deps.dev) are free and require no authentication. If the environment blocks these domains, report exit code 2 behavior. \ No newline at end of file diff --git a/.claude/skills/package-security-check/check_package.py b/.claude/skills/package-security-check/check_package.py new file mode 100644 index 0000000..b860106 --- /dev/null +++ b/.claude/skills/package-security-check/check_package.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +check_package.py - Check whether a package is safe to install. + +Data sources (all free, no API key): + 1. OSV.dev -> known vulnerabilities (ground truth: CVEs/GHSAs) + 2. deps.dev -> package metadata + OpenSSF Scorecard (health score 0-10) + +Usage: + python3 check_package.py [version] [--ecosystem npm] + +Examples: + python3 check_package.py lodash 4.17.20 + python3 check_package.py express + python3 check_package.py requests 2.19.0 --ecosystem PyPI + +Exit codes: + 0 = SECURE (no known vulnerabilities, acceptable health score) + 1 = WARNING (known vulnerabilities and/or low health score) + 2 = ERROR (network failure, package not found, etc.) +""" + +import argparse +import json +import sys +import urllib.parse +import urllib.request + +OSV_API = "https://api.osv.dev/v1/query" +DEPSDEV_API = "https://api.deps.dev/v3" +SCORECARD_WARN_THRESHOLD = 4.0 # warn if OpenSSF score is below this +TIMEOUT = 15 + + +def http_json(url, payload=None): + req = urllib.request.Request( + url, + data=json.dumps(payload).encode() if payload is not None else None, + headers={"Content-Type": "application/json", + "User-Agent": "package-security-check"}, + method="POST" if payload is not None else "GET", + ) + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: + return json.loads(resp.read().decode()) + + +def get_latest_version(name, ecosystem): + """Resolve the default/latest version from deps.dev.""" + url = f"{DEPSDEV_API}/systems/{ecosystem}/packages/{urllib.parse.quote(name, safe='')}" + data = http_json(url) + for v in data.get("versions", []): + if v.get("isDefault"): + return v["versionKey"]["version"] + versions = data.get("versions", []) + return versions[-1]["versionKey"]["version"] if versions else None + + +def query_osv(name, version, ecosystem): + """Return list of known vulnerabilities for this package version.""" + payload = {"package": {"name": name, "ecosystem": ecosystem}} + if version: + payload["version"] = version + data = http_json(OSV_API, payload) + return data.get("vulns", []) + + +def query_scorecard(name, version, ecosystem): + """Return (score, repo) from deps.dev's OpenSSF Scorecard data, or (None, None).""" + url = ( + f"{DEPSDEV_API}/systems/{ecosystem}/packages/" + f"{urllib.parse.quote(name, safe='')}/versions/{urllib.parse.quote(version, safe='')}" + ) + data = http_json(url) + for project in data.get("relatedProjects", []): + project_key = project.get("projectKey", {}).get("id") + if not project_key: + continue + try: + pdata = http_json( + f"{DEPSDEV_API}/projects/{urllib.parse.quote(project_key, safe='')}") + except Exception: + continue + scorecard = pdata.get("scorecard") + if scorecard and "overallScore" in scorecard: + return scorecard["overallScore"], project_key + return None, None + + +def severity_label(vuln): + """Best-effort severity extraction from an OSV record.""" + sev = vuln.get("database_specific", {}).get("severity") + if sev: + return sev.upper() + for s in vuln.get("severity", []): + if s.get("type", "").startswith("CVSS"): + return f"CVSS {s.get('score', '?')}" + return "UNKNOWN" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("package") + parser.add_argument("version", nargs="?", default=None) + parser.add_argument("--ecosystem", default="npm", + help="npm (default), PyPI, Go, Maven, NuGet, RubyGems, crates.io") + parser.add_argument("--json", action="store_true", + help="machine-readable output") + args = parser.parse_args() + + name, version, eco = args.package, args.version, args.ecosystem + result = {"package": name, "ecosystem": eco, "version": version, + "vulnerabilities": [], "scorecard": None, "verdict": None, "notes": []} + + # Resolve version if not given (needed for precise OSV + scorecard lookup) + if not version: + try: + version = get_latest_version(name, eco) + result["version"] = version + result["notes"].append( + f"No version specified; checked latest ({version}).") + except Exception as e: + result["notes"].append(f"Could not resolve latest version: {e}") + + # 1. Known vulnerabilities (OSV) — this is the authoritative check + try: + vulns = query_osv(name, version, eco) + for v in vulns: + result["vulnerabilities"].append({ + "id": v.get("id"), + "summary": (v.get("summary") or v.get("details", ""))[:140], + "severity": severity_label(v), + }) + except Exception as e: + result["notes"].append(f"OSV query failed: {e}") + result["verdict"] = "ERROR" + + # 2. Health score (OpenSSF Scorecard via deps.dev) — heuristic signal + if version: + try: + score, repo = query_scorecard(name, version, eco) + if score is not None: + result["scorecard"] = {"score": score, "repo": repo} + else: + result["notes"].append( + "No OpenSSF Scorecard available (no scored GitHub repo).") + except Exception as e: + result["notes"].append(f"Scorecard lookup failed: {e}") + + # Verdict + if result["verdict"] != "ERROR": + vuln_count = len(result["vulnerabilities"]) + low_score = (result["scorecard"] is not None + and result["scorecard"]["score"] < SCORECARD_WARN_THRESHOLD) + result["verdict"] = "WARNING" if ( + vuln_count or low_score) else "SECURE" + + # Output + if args.json: + print(json.dumps(result, indent=2)) + else: + v = result["verdict"] + icon = {"SECURE": "[OK]", "WARNING": "[!!]", "ERROR": "[??]"}[v] + print(f"{icon} {v}: {name}@{result['version'] or '?'} ({eco})") + if result["vulnerabilities"]: + print( + f"\nKnown vulnerabilities ({len(result['vulnerabilities'])}):") + for vu in result["vulnerabilities"]: + print(f" - {vu['id']} [{vu['severity']}] {vu['summary']}") + if result["scorecard"]: + sc = result["scorecard"] + flag = " (below threshold!)" if sc["score"] < SCORECARD_WARN_THRESHOLD else "" + print( + f"\nOpenSSF Scorecard: {sc['score']}/10{flag} repo: {sc['repo']}") + for note in result["notes"]: + print(f"note: {note}") + + sys.exit({"SECURE": 0, "WARNING": 1, "ERROR": 2}[result["verdict"]]) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/react-doctor/SKILL.md b/.claude/skills/react-doctor/SKILL.md new file mode 100644 index 0000000..c917efe --- /dev/null +++ b/.claude/skills/react-doctor/SKILL.md @@ -0,0 +1,50 @@ +--- +name: react-doctor +description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook. +version: "1.2.0" +--- + +# React Doctor + +Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. + +## After making React code changes: + +Run `npx react-doctor@latest --verbose --diff` and check the score did not regress. + +If the score dropped, fix the regressions before committing. + +## For general cleanup or code improvement: + +Run `npx react-doctor@latest --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings. + +## /doctor — full local triage workflow + +When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it: + +```bash +curl --fail --silent --show-error \ + --header 'Cache-Control: no-cache' \ + https://www.react.doctor/prompts/react-doctor-agent.md +``` + +The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed. + +Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules//.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe. + +## Configuring or explaining rules + +When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), read [references/explain.md](references/explain.md) and follow it. Start with `npx react-doctor@latest rules explain `, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`). + +## Command + +```bash +npx react-doctor@latest --verbose --diff +``` + +| Flag | Purpose | +| ----------- | --------------------------------------------- | +| `.` | Scan current directory | +| `--verbose` | Show affected files and line numbers per rule | +| `--diff` | Only scan changed files vs base branch | +| `--score` | Output only the numeric score | diff --git a/.claude/skills/react-doctor/references/explain.md b/.claude/skills/react-doctor/references/explain.md new file mode 100644 index 0000000..8e4defe --- /dev/null +++ b/.claude/skills/react-doctor/references/explain.md @@ -0,0 +1,72 @@ +# Explaining and configuring rules + +Explain React Doctor rules and edit `doctor.config.*` safely. Use this when a user +wants to understand a rule or change which rules run — not for fixing diagnostics +(that is the main `react-doctor` skill / `/doctor`). + +Triggers: "why did this rule fire", "I disagree with this rule", "turn this rule off", +"stop flagging X", "too noisy", "disable design rules". + +## Workflow + +1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`). +2. Explain it before changing anything: + +```bash +npx react-doctor@latest rules explain react-doctor/no-array-index-as-key +``` + +3. Pick the narrowest control that matches the user's intent (see decision guide). +4. Apply it with a `rules` subcommand (edits your `doctor.config.*` or `package.json#reactDoctor` in place, preserving other fields and formatting). +5. Validate the change did what they wanted: + +```bash +npx react-doctor@latest --verbose --diff +``` + +## Commands + +```bash +npx react-doctor@latest rules list # every rule + its effective severity +npx react-doctor@latest rules list --configured # only what your config changed +npx react-doctor@latest rules list --category Performance # filter by category +npx react-doctor@latest rules explain # why it matters + how to configure +npx react-doctor@latest rules disable # rule never runs +npx react-doctor@latest rules enable # turn back on at its recommended severity +npx react-doctor@latest rules set warn # off | warn | error +npx react-doctor@latest rules category "React Native" off # whole category +npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …) +npx react-doctor@latest rules unignore-tag design +``` + +Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`). + +## Decision guide + +Match the control to the intent — prefer the narrowest one: + +- **User disagrees with one rule / it's a false positive for them** → `rules disable ` (sets `rules. = "off"`; the rule stops running everywhere). This is the default for "I don't want this rule". +- **Rule is fine but wrong severity** → `rules set warn` or `rules set error`. +- **A disabled-by-default rule they want on** → `rules enable `. +- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "" off`. +- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag `. +- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output. + +How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs. + +## Config shape + +Config lives in `doctor.config.ts` (or `.js`/`.mjs`/`.cjs`/`.json`/`.jsonc`), or the `reactDoctor` key in `package.json`. The `rules` commands edit whichever exists — TS/JS edits preserve formatting (via magicast) — and create `doctor.config.json` when none does, stamping `$schema`: + +```ts +// doctor.config.ts +export default { + rules: { "react-doctor/no-array-index-as-key": "off" }, + categories: { "React Native": "warn" }, + ignore: { tags: ["design"] }, +}; +``` + +## Educating the user + +When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules//.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues. diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml new file mode 100644 index 0000000..c71e083 --- /dev/null +++ b/.github/workflows/react-doctor.yml @@ -0,0 +1,29 @@ +name: React Doctor + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [master] + +permissions: + contents: read + pull-requests: write + issues: write + statuses: write + +concurrency: + group: react-doctor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + react-doctor: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/client + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: millionco/react-doctor@v2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..468e32d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**buildml** is a full-stack ML education platform where users solve AI/ML coding challenges. Built on the T3 Stack: Next.js 15 (App Router), React 19, TypeScript, Prisma + PostgreSQL, tRPC, NextAuth.js v5, Tailwind CSS v4, and shadcn/ui. + +## Commands + +```bash +# Development +bun run dev # Start dev server (next dev --turbo) +bun run typecheck # TypeScript type checking (tsc --noEmit) + +# Code quality (Biome - replaces ESLint + Prettier) +bun run check # Lint and format check +bun run check:write # Auto-fix linting and formatting +bun run check:unsafe # Auto-fix with unsafe transformations + +# Build +bun run build # Production build +bun run start # Start production server + +# Database (Prisma + PostgreSQL) +bun run db:push # Push schema to database (prototyping) +bun run db:migrate # Create migration (production changes) +bun run db:generate # Generate Prisma client (prisma migrate dev) +bun run db:studio # Open Prisma Studio GUI +``` + +No test framework is currently configured. + +## Architecture + +### Code Execution Flow + +This is the core domain logic: + +1. User submits code via tRPC (`submission.run` or `submission.submit`) +2. Rate limiting is enforced (run: 5/10s, submit: 2/30s) via Upstash Redis +3. Job is published to Upstash QStash async queue +4. QStash calls the webhook at `api/webhooks/process-submission/` +5. Webhook calls a FastAPI executor service at `EXECUTOR_URL/execute` +6. Results stored in Redis (run) or PostgreSQL (submit) +7. Client polls tRPC for status updates + +### tRPC API Layer + +Routers live in `src/server/api/routers/`. The root router (`src/server/api/root.ts`) combines: `feedback`, `problem`, `problemSet`, `submission`, `user`. + +- `publicProcedure` - open endpoints with timing middleware +- `protectedProcedure` - requires authenticated session +- Input validation with Zod schemas +- Client consumed via `api.[router].[procedure].useQuery/useMutation` + +### Authentication + +NextAuth.js v5 with Google OAuth and JWT strategy. Config at `src/server/auth/config.ts`. Middleware (`src/middleware.ts`) protects `/dashboard/*`, `/practice/*`, `/leaderboard`, `/profile/*`. + +### Database + +Prisma schema at `prisma/schema.prisma`. Core models: `User`, `ProblemSet`, `Problem`, `Submission` (status: PENDING|PASS|FAIL|ERROR). Client singleton at `src/db/client.ts` uses `@prisma/adapter-pg`. + +Seed scripts: `prisma/seed.ts`, `seed-numpy.ts`, `seed-nn.ts`. + +### Environment Variables + +Validated via `@t3-oss/env-nextjs` in `src/env.js`. Always use `env.VAR_NAME` (imported from `~/env.js`), never `process.env` directly. Set `SKIP_ENV_VALIDATION=true` to skip validation during Docker builds. + +Required server vars: `DATABASE_URL`, `GOOGLE_CLIENT_ID/SECRET`, `UPSTASH_REDIS_REST_URL/TOKEN`, `QSTASH_*` keys, `DEPLOYMENT_URL`, `EXECUTOR_URL`, `EXECUTOR_SECRET`. + +## Code Conventions + +- **Path alias**: `~/` maps to `./src/` (e.g., `~/lib/utils`) +- **Formatting/Linting**: Biome (`biome.jsonc`) - enforces sorted Tailwind classes (`useSortedClasses`), organized imports +- **Styling**: `cn()` utility (clsx + tailwind-merge) for conditional classes. CSS variables for theming in `src/styles/globals.css` +- **Components**: shadcn/ui in `src/components/ui/` with `class-variance-authority` (CVA) for variants +- **TypeScript**: Strict mode with `noUncheckedIndexedAccess` +- **Imports**: Group by: external -> third-party -> local -> types. Use `~/` alias, prefer named imports +- **Naming**: PascalCase for components/types, camelCase for functions/variables, kebab-case for filenames diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..82d0530 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to buildml + +Thanks for your interest in contributing! Here's how to get started. + +## Prerequisites + +- [Bun](https://bun.sh) installed +- PostgreSQL database running +- Google OAuth credentials (for auth) +- Copy `.env.example` to `.env` and fill in the values + +## Setup + +```bash +bun install +bun run db:push +bun run dev +``` + +## Development Workflow + +1. Fork the repo and create a branch from `master` +2. Make your changes +3. Run checks before committing: + +```bash +bun run check # Lint & format check +bun run typecheck # TypeScript validation +``` + +4. If you changed the database schema, run `bun run db:migrate` +5. Open a pull request against `master` + +## Code Style + +- Formatting and linting are handled by **Biome** — run `bun run check:write` to auto-fix +- Use the `~/` path alias for imports from `src/` +- Use `cn()` for conditional Tailwind classes +- Follow existing patterns in the codebase + +## Commit Messages + +Use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: add new problem set page +fix: resolve submission polling timeout +refactor: simplify rate limiter logic +docs: update README badges +``` + +## Pull Requests + +- Keep PRs focused — one feature or fix per PR +- Provide a clear description of what changed and why +- Ensure `bun run check` and `bun run typecheck` pass +- Link related issues if applicable + +## Reporting Bugs + +Open an issue with: + +- Steps to reproduce +- Expected vs actual behavior +- Browser/OS info if relevant + +## License + +By contributing, you agree that your contributions will be subject to the project's [All Rights Reserved license](./LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..621a35f --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2026 buildml. All Rights Reserved. + +This source code and all associated documentation, assets, and materials +(collectively, the "Software") are the exclusive property of the copyright +holder. + +RESTRICTIONS: + +1. You may NOT copy, reproduce, distribute, publish, display, modify, create + derivative works from, or exploit any part of this Software without prior + written permission from the copyright holder. + +2. You may NOT use this Software, in whole or in part, for commercial or + non-commercial purposes without explicit authorization. + +3. You may NOT sublicense, sell, resell, transfer, assign, or otherwise + dispose of the Software or any rights therein. + +4. Viewing this source code on a public repository does NOT grant any rights + to use, copy, or distribute the code. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 67943c7..8701e2c 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,49 @@ -# Create T3 App +
-This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. +# buildml -## What's next? How do I make an app with this? +**Master AI/ML through hands-on coding challenges.** -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. +[![License](https://img.shields.io/badge/license-All%20Rights%20Reserved-red.svg)](./LICENSE) +[![Next.js](https://img.shields.io/badge/Next.js-15-black?logo=next.js)](https://nextjs.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.8-3178C6?logo=typescript&logoColor=white)](https://typescriptlang.org) -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. +Buy Me A Coffee -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Drizzle](https://orm.drizzle.team) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) +
-## Learn More +--- -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: +## Tech Stack -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials + + + + + + + + + + + + + + + +
Next.js
Next.js 15
React
React 19
TypeScript
TypeScript
Tailwind
Tailwind v4
PostgreSQL
PostgreSQL
Prisma
Prisma
tRPC
tRPC
Auth.js
NextAuth.js
Redis
Upstash Redis
Vercel
Vercel
-You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! +## Getting Started -## How do I deploy this? +```bash +bun install +bun run dev +``` -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. +## License + +Copyright (c) 2026 buildml. All Rights Reserved. + +This source code is proprietary and confidential. Unauthorized copying, modification, distribution, or use of this software, via any medium, is strictly prohibited without explicit written permission from the author. + +See [LICENSE](./LICENSE) for details. diff --git a/public/favicon.ico b/public/favicon.ico index f721e77..a530445 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..8184e1c --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,38 @@ +# buildml + +> Master AI/ML by implementing research papers from scratch. Practice coding challenges, understand algorithms, and build real skills through hands-on implementation. + +buildml is an interactive ML education platform where users solve AI/ML coding challenges by writing real implementations. The learning model is: read a paper, write the code, pass the tests. + +## Pages + +- [Home](https://buildml.website): Landing page introducing the platform and its mission. +- [Practice](https://buildml.website/practice): Browse all problem sets (NumPy, Neural Networks, etc.) and start solving challenges. +- [Leaderboard](https://buildml.website/leaderboard): Ranked community leaderboard based on points earned from solved challenges. +- [About](https://buildml.website/about): Mission, philosophy, FAQ, and team info. +- [Sponsor](https://buildml.website/sponsor): Support the project. +- [Sign In](https://buildml.website/signin): Google OAuth authentication to track progress and save solutions. + +## How It Works + +1. Users pick a problem set (e.g. NumPy Fundamentals, Neural Networks). +2. Each problem set contains ordered challenges with a description, starter code template, and hidden test suite. +3. Users write Python code in a browser-based Monaco editor. +4. Code is executed server-side against the test suite. Results are PASS, FAIL, or ERROR. +5. Solved problems earn points that appear on the leaderboard. + +## Problem Sets + +Problem sets cover topics from basic NumPy operations to neural network implementations. Each problem has a difficulty rating (Easy, Medium, Hard) and belongs to a set that teaches a progression of concepts. + +## API + +The platform uses tRPC for its API layer. Key endpoints handle: +- Problem and problem set retrieval +- Code submission (run for testing, submit for grading) +- User profiles and leaderboard data +- Submission status polling + +## Optional + +- [Sitemap](https://buildml.website/sitemap.xml) diff --git a/public/og.png b/public/og.png index 26c053d..d6f3478 100644 Binary files a/public/og.png and b/public/og.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 843a39c..2c69f73 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,6 +9,7 @@ import { createMetadata, generateOrganizationSchema, generateWebsiteSchema, + safeJsonLd, } from "~/lib/seo"; import { TRPCReactProvider } from "~/trpc/react"; @@ -37,8 +38,8 @@ const instrumentSerif = Instrument_Serif({ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { - const organizationSchema = generateOrganizationSchema(); - const websiteSchema = generateWebsiteSchema(); + const organizationJsonLd = safeJsonLd(generateOrganizationSchema()); + const websiteJsonLd = safeJsonLd(generateWebsiteSchema()); return ( - + - + diff --git a/src/app/practice/[slug]/[problemSlug]/page.tsx b/src/app/practice/[slug]/[problemSlug]/page.tsx index 9d6a2f1..e62934f 100644 --- a/src/app/practice/[slug]/[problemSlug]/page.tsx +++ b/src/app/practice/[slug]/[problemSlug]/page.tsx @@ -26,6 +26,207 @@ import { ResizablePanelGroup, } from "~/components/ui/resizable"; +function ProblemHeaderBar({ + setSlug, + problemSetTitle, + problemTitle, + difficulty, + prevProblemSlug, + nextProblemSlug, +}: { + setSlug: string; + problemSetTitle: string; + problemTitle: string; + difficulty: string; + prevProblemSlug: string | null; + nextProblemSlug: string | null; +}) { + return ( +
+
+
+ + Practice + + + + {problemSetTitle} + + +
+

+ {problemTitle} +

+ + {difficulty} + +
+ + {/* Problem Navigation */} +
+ {prevProblemSlug && ( + + + + )} + {nextProblemSlug && ( + + + + )} +
+
+ ); +} + +function ConsolePanel({ + isExecuting, + isRunPending, + isSubmitPending, + result, + onRun, + onSubmit, +}: { + isExecuting: boolean; + isRunPending: boolean; + isSubmitPending: boolean; + result: any; + onRun: () => void; + onSubmit: () => void; +}) { + return ( +
+
+
+
+ + + Console + +
+ +
+ + +
+
+ + {result && ( +
+ {result.status === "PASS" ? ( + +
+ ALL TESTS PASSED + + ) : ( + +
+ EXECUTION {result.status} + + )} +
+ )} +
+ +
+ {isExecuting && ( +
+ + Executing test suite... +
+ )} + + {result ? ( +
+
+
+ Output Logs + + PID: {Math.floor(Math.random() * 9000) + 1000} + +
+
+
+									{result.output || "(no output returned)"}
+								
+
+
+
+ ) : ( + !isExecuting && ( +
+ + {"◆"} + + Ready for execution. Click 'Run' to test your logic. +
+ ) + )} +
+
+ ); +} + export default function PracticeProblemPage({ params, }: { @@ -125,70 +326,14 @@ export default function PracticeProblemPage({
- {/* Compact Header Bar */} -
-
-
- - Practice - - - - {problemSet?.title || setSlug} - - -
-

- {problem.title} -

- - {problem.difficulty} - -
- - {/* Problem Navigation */} -
- {prevProblem && ( - - - - )} - {nextProblem && ( - - - - )} -
-
+ -
-
-
-
- - - Console - -
- -
- - -
-
- - {result && ( -
- {result.status === "PASS" ? ( - -
- ALL TESTS PASSED - - ) : ( - -
- EXECUTION {result.status} - - )} -
- )} -
- -
- {isExecuting && ( -
- - Executing test suite... -
- )} - - {result ? ( -
-
-
- Output Logs - - PID: {Math.floor(Math.random() * 9000) + 1000} - -
-
-
-															{result.output || "(no output returned)"}
-														
-
-
-
- ) : ( - !isExecuting && ( -
- - {"◆"} - - Ready for execution. Click 'Run' to test your logic. -
- ) - )} -
-
+ diff --git a/src/app/robots.ts b/src/app/robots.ts index 129f175..06fc33a 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -9,6 +9,6 @@ export default function robots(): MetadataRoute.Robots { disallow: ["/api/", "/practice/*/edit"], }, ], - sitemap: "https://buildml.com/sitemap.xml", + sitemap: "https://buildml.website/sitemap.xml", }; } diff --git a/src/app/signin/_components/signin-client.tsx b/src/app/signin/_components/signin-client.tsx index 93caed9..dfcfce4 100644 --- a/src/app/signin/_components/signin-client.tsx +++ b/src/app/signin/_components/signin-client.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { MotionConfig, motion } from "framer-motion"; import { ChevronLeft, Loader2 } from "lucide-react"; import Link from "next/link"; import { signIn } from "next-auth/react"; @@ -39,6 +39,7 @@ export function SignInClient() { const [isLoading, setIsLoading] = useState(false); return ( +
{/* Decorative grid overlay matching homepage/leaderboard styling */}
@@ -137,5 +138,6 @@ export function SignInClient() {
+
); } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 51117df..817f69a 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -7,7 +7,7 @@ type ProblemSet = { }; export default async function sitemap(): Promise { - const baseUrl = "https://buildml.com"; + const baseUrl = "https://buildml.website"; // Static pages const staticPages: MetadataRoute.Sitemap = [ diff --git a/src/app/sponsor/_components/buy-me-coffee.tsx b/src/app/sponsor/_components/buy-me-coffee.tsx index c32ab9c..3f3e433 100644 --- a/src/app/sponsor/_components/buy-me-coffee.tsx +++ b/src/app/sponsor/_components/buy-me-coffee.tsx @@ -1,35 +1,9 @@ import Link from "next/link"; import { cn } from "~/lib/utils"; -function BuyMeCoffee({ - classname, - iconClassName, - textSvgClassName, -}: { - classname?: string; - iconClassName?: string; - textSvgClassName?: string; -}) { +function BackgroundPaths1() { return ( - - {/* biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee animation decoration */} - + <> + + ); +} + +function BackgroundPaths2() { + return ( + <> - + + ); +} -
- {/* biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee mug animation */} +function BackgroundTextSvg() { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee animation decoration + + + + + ); +} + +function CoffeeMugIcon({ iconClassName }: { iconClassName?: string }) { + return ( +
+ {/* biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee mug animation */} -
- {/* biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee text banner */} - + ); +} + +function HoverTextBanner({ + textSvgClassName, +}: { textSvgClassName?: string }) { + return ( + // biome-ignore lint/a11y/noSvgWithoutTitle: buy me coffee text banner + - + + ); +} + +function BuyMeCoffee({ + classname, + iconClassName, + textSvgClassName, +}: { + classname?: string; + iconClassName?: string; + textSvgClassName?: string; +}) { + return ( + + + + ); } diff --git a/src/lib/seo.ts b/src/lib/seo.ts index e399e09..ccfbd0b 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -4,7 +4,7 @@ export const siteConfig = { name: "buildml", description: "Master AI/ML by implementing research papers from scratch. Practice coding challenges, understand algorithms, and build real skills through hands-on implementation.", - url: "https://buildml.com", + url: "https://buildml.website", ogImage: "/og.png", creator: "praash", keywords: [ @@ -93,6 +93,14 @@ export function createMetadata({ }; } +/** HTML-safe JSON-LD serializer — escapes characters that could break out of a script tag */ +export function safeJsonLd(data: unknown): string { + return JSON.stringify(data) + .replaceAll("&", "\\u0026") + .replaceAll("<", "\\u003c") + .replaceAll(">", "\\u003e"); +} + // JSON-LD Structured Data helpers export function generateOrganizationSchema() { return { diff --git a/src/styles/globals.css b/src/styles/globals.css index f4ca766..1906ea3 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -269,3 +269,15 @@ color 0.18s ease, border-color 0.18s ease; } + +/* ─ REDUCED MOTION ─ */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +}