diff --git a/.cursor/hooks.json b/.cursor/hooks.json deleted file mode 100644 index 2f21a45..0000000 --- a/.cursor/hooks.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "hooks": { - "stop": [ - { - "command": "bash .cursor/hooks/run-strict-gates.sh", - "timeout": 3600, - "failClosed": true - } - ] - } -} diff --git a/.cursor/hooks/run-strict-gates.sh b/.cursor/hooks/run-strict-gates.sh deleted file mode 100644 index 424639b..0000000 --- a/.cursor/hooks/run-strict-gates.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Read hook payload (unused for now, but consumed to avoid broken pipes) -HOOK_INPUT="$(cat || true)" -export HOOK_INPUT - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$REPO_ROOT" -LOG_FILE="context/quality-gate-feedback.log" -mkdir -p "$(dirname "$LOG_FILE")" - -CHANGED_FILES="$(git status --porcelain | awk '{print $2}')" - -if [[ -z "$CHANGED_FILES" ]]; then - echo "[strict-gates] No changed files detected; skipping checks." - printf "%s | skip | no-changes\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$LOG_FILE" - exit 0 -fi - -RUN_BACKEND=0 -RUN_FRONTEND=0 - -while IFS= read -r file; do - [[ -z "$file" ]] && continue - [[ "$file" == backend/* ]] && RUN_BACKEND=1 - [[ "$file" == frontend/* ]] && RUN_FRONTEND=1 -done <<< "$CHANGED_FILES" - -EXIT_CODE=0 - -if [[ $RUN_BACKEND -eq 1 ]]; then - echo "[strict-gates] Running backend tests: pytest -q" - if ! (cd backend && pytest -q); then - echo "[strict-gates] Backend quality gate failed." - EXIT_CODE=1 - fi -fi - -if [[ $RUN_FRONTEND -eq 1 ]]; then - echo "[strict-gates] Running frontend lint: npm run lint" - if ! (cd frontend && npm run lint); then - echo "[strict-gates] Frontend lint gate failed." - EXIT_CODE=1 - fi - - echo "[strict-gates] Running frontend build: npm run build" - if ! (cd frontend && npm run build); then - echo "[strict-gates] Frontend build gate failed." - EXIT_CODE=1 - fi -fi - -if [[ $RUN_BACKEND -eq 0 && $RUN_FRONTEND -eq 0 ]]; then - echo "[strict-gates] No backend/frontend source changes detected; skipping checks." - printf "%s | skip | non-app-changes\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$LOG_FILE" -fi - -if [[ $EXIT_CODE -eq 0 ]]; then - printf "%s | pass | backend=%s frontend=%s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$RUN_BACKEND" "$RUN_FRONTEND" >> "$LOG_FILE" -else - printf "%s | fail | backend=%s frontend=%s\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$RUN_BACKEND" "$RUN_FRONTEND" >> "$LOG_FILE" -fi - -exit "$EXIT_CODE" diff --git a/.cursor/rules/backend-api-routers.mdc b/.cursor/rules/backend-api-routers.mdc deleted file mode 100644 index 81d7856..0000000 --- a/.cursor/rules/backend-api-routers.mdc +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: FastAPI router conventions — dependency injection, auth, and response patterns -globs: backend/app/api/**/*.py -alwaysApply: false ---- - -# API Router Conventions - -## Handler Signature - -All route handlers must be `async def` with explicit `response_model`: - -```python -@router.post("/analyze", response_model=AnalysisEnqueueResponse) -async def analyze( - file: UploadFile = File(...), - user: UserModel = Depends(get_current_user), - session: AsyncSession = Depends(get_db_session), -): -``` - -## Dependency Injection - -- **Auth**: `Depends(get_current_user)` — Bearer JWT only. -- **Database**: `Depends(get_db_session)` — yields an `AsyncSession`. -- **Services**: Instantiated in the handler body, not injected: - ```python - svc = AnalysisService(session) - result = await svc.enqueue_job(...) - ``` - -## Rules - -- Routers **may** import lightweight SQLAlchemy model types or enums (e.g. `UserModel` for `Depends` typing, `JobType` / `JobStatus` for comparisons). -- Routers **must not** run raw SQLAlchemy queries, `session.execute`, or ad-hoc ORM loads—**DB access goes through repositories/services**. -- Return Pydantic schema instances directly (FastAPI serializes them). -- File uploads use `File(...)` / `UploadFile` with manual size checks before handing off to services. -- For project uploads, keep behavior consistent with current product flow: safe scan + queue analysis of detected files, and preserve explicit compose deploy gating in deploy endpoints. -- Each router file: `router = APIRouter(prefix=..., tags=[...])` at module level. -- All routers aggregated in `app/api/router.py` via `api_router.include_router(...)`. diff --git a/.cursor/rules/backend-infrastructure.mdc b/.cursor/rules/backend-infrastructure.mdc deleted file mode 100644 index 1efe24a..0000000 --- a/.cursor/rules/backend-infrastructure.mdc +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Infrastructure layer — repositories, DB access, Docker gateway, Redis event bus -globs: backend/app/infrastructure/**/*.py -alwaysApply: false ---- - -# Infrastructure Layer - -## Repositories - -- Constructor takes `AsyncSession` — instantiated inside services, not injected by FastAPI. -- All queries enforce **user-scoped access**: `WHERE user_id = :user_id` on every read/write. -- Pattern: `UserRepository`, `JobRepository` in `db/repositories.py`. - -## Database Access - -- Fully async: `sqlalchemy[asyncio]` + `asyncpg`. -- Engine: `create_async_engine` with `pool_pre_ping=True`. -- Session: `async_sessionmaker` with `expire_on_commit=False`. -- `get_db_session()` is an async generator yielding from `SessionLocal()` — used as `Depends()`. -- UUID primary keys, JSONB columns for flexible data (`input_metadata`, `result`). - -## Docker Gateway (`docker/client.py`) - -- Wraps the synchronous Docker SDK behind **`asyncio.to_thread()`** for **all** SDK calls—never block the event loop inside async code. -- **`build_image`** — image builds from context + Dockerfile path. -- **`inspect_image`** — metadata after build/pull. -- **`inspect_container_metrics`** — CPU, memory, network, I/O, PIDs, and container info while running. -- **`inspect_container_final_state`** — post-exit / final inspection (exit code, OOM, restart counts, logs summary as implemented). - -## Redis Event Bus (`events/bus.py`) - -- `publish_event(DomainEvent)` publishes to multiple channels based on event context. -- `subscribe(channel)` returns an `AsyncIterator[dict]` with `try/finally` cleanup. -- Channel patterns: `user:{id}:events`, `job:{id}:events`, `container:{id}:metrics`, `user:{id}:container:{id}:metrics`. - -## Deploy State in Redis - -- Key: `deploy:{user_id}:{job_id}` — JSON payload with 6-hour TTL. -- Payload should include **`containers`**, aggregates **`running_count`**, **`exited_count`**, **`unhealthy_count`**, plus **`runtime_state`** when tracked (overall deploy/monitor rollup). -- Runtime state values can include: `none`, `running`, `partial`, `unhealthy`, `exited`, `failed`, `stopping`, `stopped_by_user`, `cleanup_completed`. -- Stop signal: `deploy-stop:{user_id}:{job_id}` — flag key. - -## Subprocess Tools (`tools/process.py`) - -- `run_command(command, timeout=45)` — wraps `asyncio.create_subprocess_exec`. -- Returns stdout on success or non-zero exit with stdout (for linters that exit non-zero on findings). -- Raises `HTTPException(504)` on timeout, `HTTPException(500)` on missing binary. diff --git a/.cursor/rules/backend-plugins.mdc b/.cursor/rules/backend-plugins.mdc deleted file mode 100644 index c1eee48..0000000 --- a/.cursor/rules/backend-plugins.mdc +++ /dev/null @@ -1,56 +0,0 @@ ---- -description: Plugin system — BasePlugin interface, registry, and implementation patterns -globs: backend/app/plugins/**/*.py -alwaysApply: false ---- - -# Plugin System - -## Creating a New Plugin - -1. Subclass `BasePlugin` from `app/plugins/base.py`: - -```python -from app.plugins.base import BasePlugin - -class MyPlugin(BasePlugin): - name = "my_plugin" - - async def run(self, context: dict[str, Any]) -> dict[str, Any]: - # Return a dict — keys with list values named "findings" are treated as issues - # All other keys go into the job's meta dict - return {"findings": [...]} -``` - -2. Register in `PLUGIN_MAP` in `app/plugins/registry.py`: - -```python -PLUGIN_MAP["my_plugin"] = MyPlugin -``` - -## Context Dict Keys - -The `context` dict passed to `run()` may contain: -- `dockerfile_content` — raw Dockerfile text -- `compose_content` — raw docker-compose YAML text -- `filename` — original uploaded filename -- `project_path` — path to extracted project archive - -## Two Plugin Flavors - -- **Subprocess plugins** (hadolint): Use `run_command()` from `infrastructure/tools/process.py`. Write content to a tempfile, invoke the binary, parse JSON stdout. 45-second timeout. -- **Pure Python plugins** (`compose_validator`, `compose_runnability`, `security_scanner`, `resource_estimation`): Analyze content in-process with no external dependencies. - -## Issue Normalization - -`AnalysisService._normalize_issue()` handles multiple output formats (hadolint JSON and generic dict outputs). Plugin findings just need to be a list of dicts — normalization happens in the service layer. - -## Current Registry - -`PLUGIN_MAP` currently includes: - -- `hadolint` -- `compose_validator` -- `compose_runnability` -- `security_scanner` -- `resource_estimation` diff --git a/.cursor/rules/backend-python-conventions.mdc b/.cursor/rules/backend-python-conventions.mdc deleted file mode 100644 index d10fc83..0000000 --- a/.cursor/rules/backend-python-conventions.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -description: Python coding conventions — type hints, Pydantic, config, error handling -globs: backend/**/*.py -alwaysApply: false ---- - -# Python Conventions - -## Type Hints (Python 3.12+) - -Use modern built-in generics and union syntax: - -```python -# Good -def get_user(user_id: uuid.UUID) -> UserModel | None: ... -def process(items: list[str], meta: dict[str, Any]) -> None: ... - -# Bad — do not use -from typing import Optional, Dict, List -def get_user(user_id: uuid.UUID) -> Optional[UserModel]: ... -``` - -## Pydantic - -- All request/response schemas live in `app/application/schemas.py`. -- Use Pydantic v2 `BaseModel` with `Field()` constraints. -- Use `Literal["error", "warning", "info"]` for enum-like fields — no Python enums in schemas. -- Serialize with `model_dump()`. - -## Configuration - -- `pydantic-settings` `BaseSettings` in `app/core/config.py`. -- `@lru_cache` on `get_settings()` for singleton access. -- Three environments: `dev`, `test`, `prod` (via `Literal`). -- All secrets/URLs from env vars or `backend/.env`. - -## Error Handling - -- Raise `HTTPException` directly — no custom exception hierarchy. -- Services catch exceptions, persist failure state to DB, publish failure events, then re-raise. -- `run_command()` raises `HTTPException(504)` on timeout, `HTTPException(500)` on binary not found. - -## IDs and Data - -- UUID primary keys on all entities, generated server-side. -- JSONB columns (`input_metadata`, `result`) for flexible/evolving data structures. - -## Async - -- All I/O-bound operations are `async`. Synchronous Docker SDK calls use `asyncio.to_thread()`. -- Worker tasks manage their own `SessionLocal()` sessions (separate from request lifecycle). diff --git a/.cursor/rules/creative-frontend-animation.mdc b/.cursor/rules/creative-frontend-animation.mdc deleted file mode 100644 index 7c059f4..0000000 --- a/.cursor/rules/creative-frontend-animation.mdc +++ /dev/null @@ -1,41 +0,0 @@ ---- -description: Motion & animation — Motion (default), CSS/Tailwind, accessibility -globs: frontend/src/**/*.{tsx,ts,css} -alwaysApply: false ---- - -# Frontend Motion & Animation - -## Default stack - -**Motion** (`motion`, formerly Framer Motion) is the default library for UI motion—use it for layout transitions, gestures, and enter/exit unless a lighter CSS-only transition is enough. - -**Also use:** - -- **Tailwind**: `transition-[colors,opacity,…]`, `duration-200`, `ease-out`. -- **`motion-safe:`** / **`motion-reduce:`** (and `motion-reduce:animate-none`). -- **CSS** `@keyframes` when Motion is unnecessary. - -Do **not** add **GSAP**, **React Three Fiber**, **Rive**, or **Lottie/dotLottie** unless the user explicitly asks for that stack. - -## Heavy libraries (only when requested) - -| Need | Library | -|------|---------| -| Timelines, ScrollTrigger, SVG morph | GSAP | -| WebGL / 3D | React Three Fiber | -| Designer-export loops | Lottie / dotLottie | -| Stateful vector mascots | Rive | - -## Performance - -- Prefer **transform** and **opacity** over layout properties (`width`, `height`, `top`, `margin`). -- Avoid **`transition-all`**—list properties explicitly. -- Use **`will-change` sparingly** and only during active animation. -- Respect **`prefers-reduced-motion`**. -- Lazy-load heavy animation modules/routes. - -## Design - -- Motion supports hierarchy and feedback; it should not obstruct readability or contrast (see `frontend-styling.mdc`). -- Glassmorphism / heavy gradients only when they aid comprehension—not as default chrome. diff --git a/.cursor/rules/docker-and-devops.mdc b/.cursor/rules/docker-and-devops.mdc deleted file mode 100644 index ad08622..0000000 --- a/.cursor/rules/docker-and-devops.mdc +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Docker Compose stack, service topology, and DevOps conventions -alwaysApply: true ---- - -# Docker & DevOps - -## 6-Service Compose Stack - -Compose stack is split across `compose.yaml` (base) with overlays `compose.dev.yaml` and `compose.prod.yaml` at the repo root. - -| Service | Role | Port | -|---|---|---| -| frontend | Nginx-served React SPA + reverse proxy | 3000:80 | -| api | FastAPI HTTP server | 8000:8000 | -| worker | arq task worker (same image as api, different CMD) | — | -| dind | Docker-in-Docker daemon (privileged) | — | -| redis | Job queue (arq) + pub/sub event bus | 6379 | -| postgres | Primary relational DB | 5432 | - -## Dependency Chain (health-check gated) - -`frontend → api → redis, postgres` and `worker → dind, redis, postgres` - -## Key Patterns - -- **Single-image, dual-role backend**: Same Dockerfile produces `api` (uvicorn) and `worker` (arq CMD override). -- **Shared volume**: `uploads_data` mounted in both `api` and `worker` for uploaded file access. -- **DinD isolation**: Worker talks to DinD over TLS (`DOCKER_HOST=tcp://docker:2376`, certs via `DOCKER_CERT_PATH`, `DOCKER_TLS_VERIFY=1`). Never exposes host Docker socket. - -## Essential Commands - -```bash -cp .env.example .env # one-time setup -mkdir -p secrets && cp secrets/postgres_password.txt.example secrets/postgres_password.txt && cp secrets/jwt_secret.txt.example secrets/jwt_secret.txt -docker compose --env-file .env -f compose.yaml -f compose.dev.yaml up --build -docker compose exec api alembic upgrade head # run DB migrations -docker compose logs -f frontend api worker # tail logs -docker compose down -v # stop + wipe volumes -``` - -## Configuration - -Backend settings loaded by `pydantic-settings` from env vars or `backend/.env`. Key toggles: -Backend settings loaded by `pydantic-settings` from env vars/secrets. Key toggles: -- `APP_ENV=dev` enables permissive CORS (`allow_origins=["*"]`) -- `APP_ENV=prod` restricts CORS -- `JWT_SECRET_KEY` is loaded from secrets in Compose deployments and must be strong in production diff --git a/.cursor/rules/frontend-react-patterns.mdc b/.cursor/rules/frontend-react-patterns.mdc deleted file mode 100644 index 7c374dc..0000000 --- a/.cursor/rules/frontend-react-patterns.mdc +++ /dev/null @@ -1,74 +0,0 @@ ---- -description: React patterns — exports, auth, api.ts clients, state, WS, forms, a11y -globs: frontend/src/**/*.tsx -alwaysApply: false ---- - -# React Patterns - -## Component exports - -- **Named exports only**: `export function ComponentName() { … }` -- **Default export** only for `App.tsx`. -- Prefer function components; handlers as inline arrows or named functions inside the component. - -## Auth - -- `useAuth()` from `src/auth/AuthProvider.tsx`. -- Routes wrapped with `` in `routes.tsx`. -- While `isBooting`, show `DockerLoader` (same as token validation). - -## API layer (`src/utils/api.ts`) - -All HTTP goes through exported clients—**never** raw `fetch()` in pages/components. - -```typescript -import { jobs, dockerfile, compose, project, research } from "../utils/api"; - -const list = await jobs.list(); -const job = await jobs.get(jobId); -await dockerfile.analyze(file); -await compose.deploy({ job_id: id, run_stack: true }); -await research.summary(90); -await research.jobs({ limit: 20, offset: 0 }); -``` - -Also exported: `auth`, `project`, `ws`, `checkHealth`, shared types (`Job`, `ApiError`, etc.). - -## Project upload UX assumptions - -- Current project upload flow queues analysis for all detected Dockerfiles/Compose files. -- Current project upload flow communicates automatic image builds in UX copy. -- Compose runtime start/deploy remains an explicit follow-up action from results/deploy controls. - -## WebSocket - -- Hold socket in `useRef`. -- Open in `useEffect`, close in cleanup. -- Pass JWT as `?token=` when connecting (see existing monitoring/progress pages). - -## State - -- **No Redux/Zustand** — auth via Context; local state via `useState`. -- **sessionStorage**: uploads, job IDs, execution flow. -- **localStorage**: auth (`dpa_*`), notifications (`dqa:` prefix). -- **URL**: `jobId` query; `containerId` route params where applicable. - -## Pages & layout - -- Authenticated flows wrap content in `` (standalone: Login/Register). - -## Forms & native controls - -- **Labels**: visible, `htmlFor` matched to control `id`; spacing **`gap-3`** between label group and input/select. -- **Selects**: prefer **`NativeSelectField`** (`components/ui/native-select.tsx`). -- **Keyboard**: modals/dialogs—Escape closes, focus returns to trigger; trap Tab within dialog content when practical. - -## Overlays (modals) - -- `role="dialog"`, `aria-modal="true"`, `aria-labelledby` pointing at title `id`. -- Provide visible **Close** + backdrop click policy consistent with existing pages. - -## Charts - -- Use **Recharts** where needed; follow styling rule: **`currentColor`** ticks/grid inside a tinted wrapper—do not rely on SVG `var(--muted-foreground)` alone for axis labels. diff --git a/.cursor/rules/frontend-styling.mdc b/.cursor/rules/frontend-styling.mdc deleted file mode 100644 index 829374d..0000000 --- a/.cursor/rules/frontend-styling.mdc +++ /dev/null @@ -1,66 +0,0 @@ ---- -description: Styling — Tailwind v4, semantic design tokens, contrast, shadcn/ui, forms & charts -globs: frontend/src/**/*.{css,tsx} -alwaysApply: false ---- - -# Frontend Styling Standards - -## Tailwind CSS v4 - -- Utilities + `@import "./tailwind.css"` (see `src/styles/index.css`). -- Theme colors map to CSS variables (`--background`, `--foreground`, `--card`, `--border`, `--primary`, `--destructive`, etc.) via `@theme inline`. -- Prefer **semantic utilities** (`bg-card`, `text-foreground`, `border-border`) over raw `slate-*` on new UI. Legacy pages may still mix slate on layout chrome; migrate toward tokens when touching a file. - -## Theme Mode - -- `App.tsx` adds `dark` to `document.documentElement`. Tokens in `.dark { … }` define the live palette. -- `html.dark { color-scheme: dark; }` (in `index.css`) keeps native controls (select menus) readable—do not strip this. -- UI code should remain **theme-token-driven** so removing forced dark or adding a toggle does not require rewriting components. - -## Color & Contrast (required) - -- **Never** hardcode `text-black`, `border-black`, or `hover:bg-black/*` on shared primitives—it breaks dark backgrounds (see `Button` in `components/ui/button.tsx`: uses `text-primary-foreground`, `text-foreground`, `border-input`, etc.). -- **`text-muted-foreground`** on near-black panels can fail WCAG at small sizes. For critical secondary copy (table headers, chart axes, captions, pagination), use explicit readable pairs, e.g. `text-slate-600 dark:text-slate-400` or `text-slate-700 dark:text-slate-300`, or lighten `--muted-foreground` in `index.css` with intent—not random one-offs. -- **Charts (Recharts):** avoid `fill: "var(--muted-foreground)"` on SVG ticks—many browsers resolve this poorly. Wrap the chart in a container with a readable `text-*` color and use `tick={{ fill: "currentColor" }}` plus `CartesianGrid` with `stroke="currentColor"` and controlled opacity. - -## Semantic Roles (reference) - -| Role | Tailwind / token | -|------|-------------------| -| Page background | `bg-background` | -| Surfaces / cards | `bg-card`, `text-card-foreground` | -| Borders | `border-border` | -| Primary CTA | `bg-primary`, `text-primary-foreground` | -| Muted secondary | `bg-muted`, `text-muted-foreground` (with contrast checks) | -| Destructive | `bg-destructive`, `text-destructive-foreground` | -| Focus ring | `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background` | - -Accent hues (blue for primary nav, emerald/sky/destructive for status) stay **semantic**, not rainbow decoration. - -## Layout & rhythm - -- **8px grid**: spacing via `gap-4`, `p-4`, `py-6`, etc. -- **Touch targets**: interactive controls ≥ **44px** height where practical (`min-h-[44px]`, `h-11`). -- **Forms**: label stack uses clear separation (`gap-3` between label block and control); labels ≥ `text-sm font-medium text-foreground`; hints use readable muted slate utilities—not cramped `text-xs` glued to controls without spacing. - -## Class merging - -Use `cn()` from `src/components/ui/utils.ts` (relative imports from pages, e.g. `../components/ui/utils`): - -```tsx -import { cn } from "../components/ui/utils"; - -
-``` - -## UI primitives (`components/ui/`) - -- **Button**, **Badge**, **Card**, **Tabs**: CVA variants; extend with tokens, not raw black/white. -- **Native selects**: use **`NativeSelectField`** from `components/ui/native-select.tsx` (chevron, spacing, popover-aligned trigger). Avoid bare `