Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions services/grader/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Python cache and build artifacts
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
dist/
*.egg-info/
*.egg

# Virtual environments (we build our own in the container)
.venv/
venv/
ENV/
env/

# Testing artifacts
.coverage
.pytest_cache/
htmlcov/
.tox/
.nox/
tests/
*.test.py

# Type checking and linting cache
.mypy_cache/
.ruff_cache/

# IDE and editor files
.idea/
.vscode/
.devcontainer/
*.swp
*.swo
*~
.DS_Store
Thumbs.db

# Git
.git/
.gitignore
.gitattributes
.github/

# Environment files (CRITICAL: Never include secrets)
.env
.env.*
!.env.example

# Documentation
docs/
CLAUDE.md

# CI/CD
.gitlab-ci.yml
.travis.yml
Jenkinsfile

# Docker files (avoid recursive copying)
Dockerfile
docker-compose.yml
.dockerignore

# Plan files
.claude/
9 changes: 9 additions & 0 deletions services/grader/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# grader configuration. Copy to .env and adjust. All variables use the
# GRADER_ prefix (pydantic-settings).

# --- Web server ---
# Setting this to true switches structlog to the human-readable console
# renderer for local debugging. Keep it false anywhere you want JSON logs
# (the default, matching config.py's Settings.debug).
GRADER_DEBUG=false
GRADER_ALLOWED_ORIGINS=["http://localhost:5173"]
1 change: 1 addition & 0 deletions services/grader/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
105 changes: 105 additions & 0 deletions services/grader/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project: Profile Grader (grader)

Mechanical and, later, LLM-assisted grading for Evermore kennel card
Compositions. This is the tech-stack scaffold (issue #293): a health
endpoint, `/llms.txt` discovery, and structured logging/tracing. No
domain grading logic, data layer, or auth are wired up yet: those land in
#294 (domain), #296 (data layer), #298 (auth).

**Architecture:** Standalone FastAPI service, no database (yet).

**Stack:** Python 3.14, FastAPI, Pydantic 2.x, pydantic-settings, structlog
(JSON logging), OpenTelemetry API (tracing scaffolding only, no SDK/exporter).

## Commands

Run from this directory (`services/grader/`):

```bash
# Install dependencies (including dev tooling)
uv sync --extra dev

# Run development server
uv run uvicorn grader.main:app --reload --port 8003

# Tests
uv run pytest

# Quality gates (must all pass before commit)
uv run ruff format src/ tests/
uv run ruff check src/ tests/
uvx bandit -r src/ -q
uv run mypy src/
```

## Environment

Environment variables use the `GRADER_` prefix (pydantic-settings). Copy
`.env.example` to `.env` and adjust. See `src/grader/config.py` for the
full `Settings` model.

## Local development (Docker)

```bash
cp .env.example .env
docker compose up -d # builds and runs grader on host port 8003
docker compose down
```

Port 8003 is reserved for grader in the local stack (8001 retriever, 8002
petdata are taken).

## Project structure

```
src/grader/
β”œβ”€β”€ config.py # Settings via pydantic-settings (GRADER_ prefix)
β”œβ”€β”€ main.py # FastAPI application factory (create_app)
└── observability/
β”œβ”€β”€ logging.py # structlog JSON configuration + OTel trace correlation
└── tracing.py # OpenTelemetry API-only tracer + traced_span helper

tests/
β”œβ”€β”€ conftest.py # TestClient(create_app()) fixture
β”œβ”€β”€ test_health.py # GET /health, create_app() smoke test
β”œβ”€β”€ test_llms_txt.py # GET /llms.txt
β”œβ”€β”€ test_logging.py # structlog JSON rendering
└── test_compose.py # docker-compose.yml port/healthcheck assertions
```

## Coding conventions

### Configuration management

```python
from grader.config import get_settings

settings = get_settings()
```

### Observability

- `configure_logging(debug=...)` (called from `create_app()`) sets up
structlog to emit one line of JSON per event in production, and a
human-readable console renderer in debug mode.
- `tracing.py` exposes a module-level `tracer` and a `traced_span(name)`
context manager. No `TracerProvider`/exporter is configured here (API
only, per the tech-stack standard); spans are no-ops until a host
process installs a real SDK provider.

### Type hints

Required everywhere (mypy --strict enforced): typed signatures and return
types (including `-> None`). Use `from __future__ import annotations` for
forward references.

## Out of scope (tracked separately)

- Domain grading logic: #294
- Data layer (persistence for grades/history): #296
- Auth: #298
- CI wiring (`.github/workflows/`): follow-up, not part of this scaffold
61 changes: 61 additions & 0 deletions services/grader/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1

# This Dockerfile builds from the SERVICE-DIRECTORY context (services/grader/),
# unlike services/petdata's Dockerfile, which builds from the repo root because
# petdata depends on a sibling `packages/*` path source. grader has no such
# dependency, so all COPY paths below are service-relative and the build
# context is just this directory.

# --- Build stage ---
FROM python:3.14-slim AS builder

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/

WORKDIR /app

# Build the venv at its final runtime path so uv's console-script shebangs
# (absolute, non-relocatable) stay valid after COPY into the runtime stage.
ENV UV_PROJECT_ENVIRONMENT=/app/.venv

# Dependency manifests first for layer caching.
COPY pyproject.toml uv.lock ./

# --no-editable installs grader (once copied below) as a built wheel, so the
# runtime stage needs only the venv, not the source tree.
RUN uv sync --frozen --no-install-project --no-dev --no-editable

# Copy source and README (needed by the project-wheel build) and install the
# project itself.
COPY src/ ./src/
COPY README.md ./
RUN uv sync --frozen --no-dev --no-editable

# --- Runtime stage ---
FROM python:3.14-slim AS runtime

# curl for the healthcheck (no libpq/asyncpg: grader has no data layer)
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*

# Non-root user
RUN useradd --system --create-home --shell /bin/bash grader
WORKDIR /app

# Copy the virtualenv from builder. The venv is built directly at
# /app/.venv (UV_PROJECT_ENVIRONMENT), so no relocation happens here.
# grader is installed into the venv as a non-editable wheel, so no
# separate copy of src/ is needed at runtime.
COPY --from=builder /app/.venv /app/.venv

USER grader

ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f "http://localhost:8000/health" || exit 1

CMD ["uvicorn", "grader.main:app", "--host", "0.0.0.0", "--port", "8000"]
45 changes: 45 additions & 0 deletions services/grader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# grader

Profile Grader for Evermore kennel card compositions.

## Overview

grader mechanically and, later, model-assisted grades a generated
Composition against its source Package, so staff and pipelines get a score
before a card goes out. This is the tech-stack scaffold (issue #293): a
health endpoint, `/llms.txt` discovery, and structured logging/tracing. No
domain grading logic, data layer, or auth are wired up yet (tracked
separately: #294, #296, #298).

## Quick start

```bash
cd services/grader
uv sync --extra dev
cp .env.example .env
uv run uvicorn grader.main:app --reload --port 8003
```

## Configuration

Environment variables use the `GRADER_` prefix (pydantic-settings). See
`.env.example` for the full set.

## Development

```bash
uv run pytest # tests
uv run ruff format src/ tests/ # format
uv run ruff check src/ tests/ # lint
uvx bandit -r src/ -q # security scan
uv run mypy src/ # type check (strict)
```

See `CLAUDE.md` for the full gate sequence and project layout.

## License

Apache License 2.0 (Apache-2.0). See the root
[LICENSE](../../LICENSE) for the full text.

Copyright (C) 2026 Backchain LLC
28 changes: 28 additions & 0 deletions services/grader/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Local development: Profile Grader service.
#
# grader has no data layer, so this compose file runs only the app container
# itself (unlike petdata's/retriever's docker-compose.yml, which run a
# standalone Postgres for those services).
#
# Port 8003 (not 8001 retriever, 8002 petdata) keeps grader clear of the
# other local services. This compose file runs grader standalone; wiring it
# into the repo-wide one-command local stack (root Makefile targets and
# ../../docs/local-development.md) is a follow-up, not yet done.
#
# Usage:
# cp .env.example .env # first time only
# docker compose up -d # start grader
# docker compose down # stop it
services:
grader:
build: .
ports:
- "8003:8000"
env_file:
- path: .env
required: false
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 5s
timeout: 3s
retries: 5
Loading