diff --git a/.claude/skills/new-bill-feature/SKILL.md b/.claude/skills/new-bill-feature/SKILL.md new file mode 100644 index 0000000..ac0b6bb --- /dev/null +++ b/.claude/skills/new-bill-feature/SKILL.md @@ -0,0 +1,78 @@ +--- +name: new-bill-feature +description: Guide for adding a new feature to the Bill/voting system in webiscite +disable-model-invocation: true +argument-hint: "[description of the feature]" +--- + +## Task +Implement a new feature for the Bill/voting system: $ARGUMENTS + +## Checklist of files to consider + +The Bill system spans these files. Review each to determine if it needs changes: + +**Models & Logic:** +- `democrasite/webiscite/models.py` — Bill, Vote, PullRequest models (fields, `vote()`, `submit()`, `_check_approval()`, Status choices) +- `democrasite/webiscite/managers.py` — `BillManager.create_from_github()`, queryset annotations (yes_percent, no_percent, user_vote) +- `democrasite/webiscite/constitution.py` — `is_constitutional()`, `update_constitution()` for protected file ranges +- `democrasite/webiscite/tasks.py` — `submit_bill()` Celery task (approval check → GitHub merge → constitution update) +- `democrasite/webiscite/webhooks.py` — `PullRequestHandler` (opened/reopened/closed) and HMAC-validated `GithubWebhookView` + +**Views & API:** +- `democrasite/webiscite/views.py` — BillListView, BillDetailView, BillUpdateView, vote_view (AJAX POST) +- `democrasite/webiscite/api/views.py` — BillViewSet (list/retrieve/update + vote action), IsAuthorOrReadOnly permission +- `democrasite/webiscite/api/serializers.py` — BillSerializer, PullRequestSerializer +- `democrasite/webiscite/urls.py` — Template view URL patterns +- `config/api_router.py` — DRF router registration + +**Templates & Frontend:** +- `democrasite/templates/webiscite/bill_list.html` — Card grid of bills +- `democrasite/templates/webiscite/bill_detail.html` — Single bill page +- `democrasite/templates/webiscite/snippets/vote.html` — Vote progress bar + yes/no buttons +- `democrasite/templates/webiscite/bill_form.html` — Edit form (name, description) +- `democrasite/static/js/vote.js` — AJAX vote handler, DOM updates for counts and progress bar + +**Tests (mirror each area above):** +- `democrasite/webiscite/tests/test_models.py` +- `democrasite/webiscite/tests/test_views.py` +- `democrasite/webiscite/tests/test_tasks.py` +- `democrasite/webiscite/tests/test_webhooks.py` +- `democrasite/webiscite/tests/test_constitution.py` +- `democrasite/webiscite/tests/test_templates.py` +- `democrasite/webiscite/tests/factories.py` — PullRequestFactory, BillFactory, TaskFactory + +**Config & Admin:** +- `democrasite/webiscite/admin.py` — SimpleHistoryAdmin for Bill, PullRequest +- `democrasite/webiscite/context_processors.py` — Exposes `github_repo` to templates +- `config/settings/base.py` — WEBISCITE_* settings (quorum, majority thresholds, voting period, GitHub token/repo) + +## Key patterns to follow +- Bill status choices: DRAFT, OPEN, APPROVED, REJECTED, FAILED, CLOSED +- Votes are M2M through Vote model with unique constraint on (bill, user) +- Bill.vote() toggles existing votes; raises ClosedBillVoteError if bill not OPEN +- Constitutional bills need WEBISCITE_SUPERMAJORITY (66.67%), normal need WEBISCITE_NORMAL_MAJORITY (50%) +- Each Bill has a OneToOne PeriodicTask for scheduled submission +- Managers annotate querysets with vote percentages and user vote status +- API vote endpoint expects {"support": true/false}, template vote_view expects POST with "vote" field +- django-simple-history tracks Bill and Vote changes automatically +- PullRequest has a `draft` boolean field tracking GitHub's draft state +- Draft bills (from draft PRs) cannot be voted on or submitted; they transition to OPEN via Bill.publish() when the PR is marked ready for review +- The `unique_active_pull_request` constraint prevents duplicate bills for the same PR in both `open` and `draft` statuses +- PullRequest.close() closes both open and draft bills +- The submit PeriodicTask is created disabled for draft bills; Bill.publish() enables it and resets last_run_at so the voting period starts from publication +- GitHub's `ready_for_review` webhook action triggers PullRequestHandler.ready_for_review(), which updates the PR and publishes the draft bill + +## Steps +1. Read the relevant files from the checklist above +2. Plan the changes needed across all layers (model → serializer → view → template → test) +3. If adding a model field, create a migration with `just manage makemigrations` +4. Implement changes +5. Update or add factories in factories.py for any new model fields +6. Write tests covering the new functionality +7. Run `just run pytest democrasite/webiscite/tests/` to verify +8. Run `just lint` to check style +9. Update documentation: + - Update the "Key patterns to follow" section in this skill file (`.claude/skills/new-bill-feature/SKILL.md`) with the new feature's patterns + - Update `docs/webiscite.rst` if the feature changes the pull request processing pipeline or bill lifecycle + - If files were created or deleted, run `just run make -C docs apidocs` to regenerate API docs diff --git a/.claude/skills/test-webhook/SKILL.md b/.claude/skills/test-webhook/SKILL.md new file mode 100644 index 0000000..702635c --- /dev/null +++ b/.claude/skills/test-webhook/SKILL.md @@ -0,0 +1,40 @@ +--- +name: test-webhook +description: Set up smee to forward GitHub webhooks to local Django for manual testing +disable-model-invocation: true +argument-hint: https://smee.io/2ckdUNpB3Qt0UvE7 +--- + +## Task +Start a smee webhook proxy to forward GitHub webhook events to the local Django server for manual testing. + +## Prerequisites +- `smee` CLI installed (`npm install -g smee-client`) +- Docker containers running (`just up`) +- A smee channel URL from https://smee.io or a personal one +- A GitHub webhook configured on your fork pointing to the smee URL, with the `GITHUB_WEBHOOK_SECRET` matching your `.envs/.local/.django` file + +## Steps + +1. Parse the smee URL from `$ARGUMENTS` +2. Verify containers are running with `just up` +3. Start smee in the background: + ``` + smee --url --path /hooks/github/ --port 8000 + ``` +4. Confirm smee connects successfully +5. Inform the user they are ready to test by creating or updating pull requests on their GitHub fork + +## Configuration reference +- Webhook path: `/hooks/github/` +- Local port: `8000` (Docker maps this to the Django container) +- Webhook secret env var: `GITHUB_WEBHOOK_SECRET` in `.envs/.local/.django` +- Django setting: `WEBISCITE_GITHUB_WEBHOOK_SECRET` in `config/settings/base.py` +- Webhook handler: `democrasite/webiscite/webhooks.py` — `GithubWebhookView` +- Supported events: `pull_request` (opened, reopened, closed, ready_for_review), `push`, `ping` + +## Troubleshooting +- If webhook returns 403: check that `GITHUB_WEBHOOK_SECRET` matches the secret configured on the GitHub webhook +- If webhook returns 400: check the `x-github-event` and `x-hub-signature-256` headers are present +- If smee doesn't connect: verify the smee URL is correct and the channel exists +- Check Django logs with `docker compose -f docker-compose.local.yml logs django -f` for webhook processing errors diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..326c362 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Democrasite is a Django web application that implements democratic voting on GitHub pull requests. Users vote on "bills" (proposals to merge PRs), and approved bills are automatically merged. Core functionality is protected by a "constitution" system that requires supermajority votes (66.67%) to modify protected files/line ranges, while normal bills need simple majority (>50%). + +## Tech Stack + +- **Python 3.12 / Django 5.x** with PostgreSQL, Redis, Celery +- **Docker Compose** for local development (all commands run inside containers) +- **Django REST Framework** with JWT auth and drf-spectacular for API docs +- **django-allauth** for GitHub/Google OAuth +- **Ruff** (linter/formatter), **mypy** (type checker), **djLint** (template linter), **pre-commit** hooks + +## Common Commands + +All development uses Docker. The `justfile` sets `COMPOSE_FILE=docker-compose.local.yml` automatically. + +```bash +just build # Build Docker images +just up # Start all containers +just down # Stop containers +just test # Run pytest suite +just lint # Run pre-commit hooks +just typecheck # Run mypy +just manage # Run manage.py (e.g., just manage makemigrations) +just migrate # makemigrations + migrate +just coverage # Run tests with coverage + open HTML report +just shell # Bash shell in django container +just pyshell # Django shell_plus (IPython) +just run # Execute arbitrary command in django container +just loaddata # Load fixtures (democrasite + activitypub) +``` + +To run a single test file or test: +```bash +just run pytest democrasite/webiscite/tests/test_models.py +just run pytest democrasite/webiscite/tests/test_models.py::TestBill::test_method_name -v +``` + +Pytest is configured with `--ds=config.settings.test --reuse-db` in `pyproject.toml`. + +## Architecture + +### Django Apps + +- **`democrasite/webiscite/`** — Core app. Models: `PullRequest`, `Bill`, `Vote`. Handles GitHub webhooks, voting logic, constitution enforcement, and Celery tasks for auto-merging. +- **`democrasite/users/`** — Custom `User` model (extends `AbstractUser` with single `name` field instead of first/last). OAuth integration. +- **`democrasite/activitypub/`** — ActivityPub federation. Models: `Person` (linked to User with keypair), `Follow`. + +### Configuration + +- **`config/settings/`** — Split settings: `base.py`, `local.py`, `production.py`, `test.py` +- **`config/urls.py`** — URL routing. Admin is only enabled in DEBUG mode. +- **`config/api_router.py`** — DRF router registering `UserViewSet` and `BillViewSet` +- **`config/celery_app.py`** — Celery configuration with Redis backend + +### Key Patterns + +- **Constitution system** (`webiscite/constitution.py`, `constitution.json`): Maps files to protected line ranges. PRs touching protected code become "constitutional" bills requiring supermajority. Line numbers auto-update after merges. +- **Bill lifecycle**: OPEN → APPROVED/REJECTED/FAILED/CLOSED. Each Bill has a OneToOne to a Celery `PeriodicTask` that runs `submit_bill()` at voting period end. +- **Webhook flow** (`webiscite/webhooks.py`): GitHub push events → HMAC validation → create/update `PullRequest` → create `Bill`. +- **Vote constraints**: One vote per user per bill (DB unique constraint). Votes can be changed. +- **Audit trail**: `django-simple-history` tracks changes on key models. + +### API + +- REST API at `/api/` with Swagger docs at `/api/docs/` +- Auth: session, token, JWT (`/api/token/refresh/`), GitHub OAuth (`/api/auth/github/`) + +## Code Style + +- **Ruff**: 88-char lines, force single-line imports, double quotes, spaces for indentation +- **djLint**: Django template profile, 119-char max line length, 2-space indent +- **mypy**: Strict-ish config with Django/DRF stubs, migrations ignored +- Env files live in `.envs/` (copied from `.envs.template/` on setup)