7.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Development
# Install dependencies (generates composer.lock if missing — commit it)
composer install
# Run dev server
APP_ENV=dev php -S localhost:8080 -t public
# Test the graph endpoint
curl "http://localhost:8080/graph.svg?theme=dark" -o graph.svg
curl "http://localhost:8080/health"
Testing conventions (apply to every test file)
declare(strict_types=1)at the top of every test file- Test classes are
finaland extendTestCase - Methods:
#[Test]attribute +it_prefix +snake_case— e.g.it_returns_empty_when_no_contributions - Structure: Arrange → Act → Assert, separated by blank lines; one assertion per test when possible
- Use
$this->assert*()notself::assert*()
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Service\SvgRenderer;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class SvgRendererTest extends TestCase
{
#[Test]
public function it_renders_an_svg_with_no_contributions(): void
{
$renderer = new SvgRenderer();
$svg = $renderer->render([], 'light');
$this->assertStringContainsString('<svg', $svg);
}
}
TDD: Red → Green → Refactor
Always drive new features and bug fixes with this cycle. Claude writes implementation first by default — override that with explicit prompts.
Phase 1 — Red (write a failing test first)
Write a failing test for [feature description].
Do NOT write the implementation yet.
The test should fail because the class/method does not exist.
Phase 2 — Green (minimal implementation)
The tests are written and failing. Now implement the minimum code to make them pass. Nothing more.
Phase 3 — Refactor
Tests are green. Refactor the implementation for [readability / removing duplication / naming].
Run vendor/bin/phpunit after each change to confirm tests stay green.
Common anti-patterns
| Wrong prompt | Why it breaks TDD | Correct prompt |
|---|---|---|
| "Write tests for this feature" | Claude implements first, then fits tests to it | "Write failing tests for [feature]. Stop before any implementation." |
| "Add tests and implementation" | Loses the design feedback of failing tests | Two separate prompts: Red, then Green |
| "Make the tests pass" | Encourages skipping to a green state | "Implement the minimum to make the failing tests pass." |
| Combining Red + Green in one request | No failing baseline | Always separate the two phases |
Running tests
Prever to use the tests in docker compose exec graph
# Run full suite
vendor/bin/phpunit
# Run with human-readable output
vendor/bin/phpunit --testdox
# Run a single test file
vendor/bin/phpunit tests/Unit/Service/SvgRendererTest.php
# Run tests matching a filter
vendor/bin/phpunit --filter it_renders
Auto-run hook
Add to .claude/settings.json to run PHPUnit automatically after every file edit:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "vendor/bin/phpunit 2>&1 | tail -20"
}
]
}
}
Gitea Workflows
Workflow files live in .gitea/workflows/. This project uses Gitea Actions (GitHub Actions-compatible syntax).
When editing workflow files:
- Use
gitea.*context variables, notgithub.*— e.g.${{ gitea.server_url }},${{ gitea.actor }},${{ gitea.repository }} GITHUB_ENVandGITHUB_OUTPUTare still the correct env file names (Gitea Actions re-uses these)- Secrets are set under Repository → Settings → Secrets → Actions
- The registry hostname must be derived from
gitea.server_url(strip the protocol prefix) - Triggers use standard
on:syntax;tags: '*.*.*'matches semver pushes without avprefix
Current workflows:
| File | Trigger | Purpose |
|---|---|---|
docker-publish.yml |
Push tag *.*.* |
Build & push multi-arch image to Gitea container registry |
Docker
Development
docker-compose.override.yml is picked up automatically and targets the dev stage (Xdebug + all deps, source mounted).
# Start dev container (override applied automatically)
docker compose up -d --build
# Shell into the container to run commands
docker compose exec graph sh
# Run tests inside the container
docker compose exec graph vendor/bin/phpunit
# Disable Xdebug for faster test runs
XDEBUG_MODE=off docker compose up -d
# Force cache clear
docker compose exec graph rm -rf var/cache/*
# View logs
docker compose logs -f graph
Xdebug: listens on port 9003. Configure your IDE to accept connections from Docker. On Linux host.docker.internal is resolved via extra_hosts: host-gateway in the override file.
Production
Use only the base compose file to skip the dev override:
docker compose -f docker-compose.yml up -d --build
The final stage runs as a non-root app user and contains no Composer binary. The build pipeline is:
base → deps (composer install --no-dev)
→ build (copy source + dump-autoload)
→ final (copy vendor + source, chown app, USER app)
There is no composer.lock in the repo. If you add or change dependencies, run composer install locally and commit the resulting lock file — without it, Docker builds resolve versions fresh each time and may produce inconsistent results.
Architecture
Single-controller Symfony app with no database. The request flow:
GET /graph.svg?theme=dark|light
└─ GraphController
├─ host check (ALLOWED_HOSTS env, optional)
├─ cache lookup (filesystem, 1h TTL, key = "graph_{theme}")
│ └─ on miss:
│ ├─ GitHubProvider → GitHub GraphQL API (contributionCalendar query)
│ ├─ GitLabProvider → GitLab REST /users/:id/events (paginated, 100/page)
│ └─ GiteaProvider → Gitea REST /api/v1/users/:user/heatmap
│ each returns array<string, int> (Y-m-d => count)
│ failures are caught and logged; remaining providers still render
│ └─ merge by date (sum counts across providers)
│ └─ SvgRenderer::render()
└─ Response: image/svg+xml, Cache-Control: public max-age=3600
Provider activation: a provider only runs when its env vars are non-empty. GitHub and GitLab require _USER + _TOKEN; Gitea additionally requires _URL. GitLab resolves a numeric user ID from the username via a /api/v4/users?username= lookup before fetching events.
SvgRenderer: builds a 53-column × 7-row grid aligned so the last column always ends on the Saturday of the current week. Five intensity levels (0 contributions → level 0, 1–3 → 1, 4–6 → 2, 7–9 → 3, 10+ → 4) mapped to GitHub's exact colour tokens. No external assets — the SVG is fully self-contained.
Cache: filesystem adapter (var/cache/), mounted as a Docker volume to survive container restarts. Theme is part of the cache key so dark and light are cached independently.
Environment variables
| Variable | Required | Notes |
|---|---|---|
APP_SECRET |
Yes | 32+ char random string |
GITHUB_USER / GITHUB_TOKEN |
For GitHub | Token scope: read:user |
GITLAB_USER / GITLAB_TOKEN |
For GitLab | Token scopes: read_user, read_api |
GITLAB_URL |
No | Defaults to https://gitlab.com |
GITEA_USER / GITEA_TOKEN / GITEA_URL |
For Gitea | Token scope: read:user |
ALLOWED_HOSTS |
No | Comma-separated hostnames; empty = allow all |
Copy .env to .env.local for local development — .env.local is gitignored.