Skip to content
Merged
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
78 changes: 78 additions & 0 deletions .claude/skills/new-bill-feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions .claude/skills/test-webhook/SKILL.md
Original file line number Diff line number Diff line change
@@ -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 <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
78 changes: 78 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <args> # 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 <cmd> # 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)